Hello everyone!
I wanted to share good practice regarding your contract using fhevm library. Developing on fhEVM is new, and it’s hard to judge what is “best practice”, so here some clues.
(This Topic relates to the Discord conversation here)
Hello everyone!
I wanted to share good practice regarding your contract using fhevm library. Developing on fhEVM is new, and it’s hard to judge what is “best practice”, so here some clues.
(This Topic relates to the Discord conversation here)
Avoid TFHE.decrypt
For now, a TFHE.decrypt
is pretty cheap, making it tempting to use constructs like if(TFHE.decrypt(encryptedBool))
. However, it is advisable to avoid this approach. Instead, use cmux
operator to handle conditions. You can prevent the use of TFHE.decrypt
in many cases. There are two main reasons to use TFHE.decrypt()
:
TFHE.decrypt
will be an external call to an oracle. You can take a look at our ERC20Rules we’ve implemented for our article regarding identity: a lot of rules, only using cmux https://github.com/zama-ai/fhevm/blob/17d43894e925db08bbf1f8b570903ead8445a993/examples/Identity/ERC20Rules.sol#L42Avoid TFHE.decrypt (2)
I insist on this? Yes maybe. I saw recently a TFHE.decrypt(encryptedBool) && TFHE.decrypt(encryptedBool2) && TFHE.decrypt(encryptedBool3)
and it violates the above rule. Prefer a cmux
or a TFHE.decrypt(TFHE.and(encryptedBool, TFHE.and(encryptedBool2, encryptedBool3))
Error handling (because you avoid TFHE.decrypt)
Since you won’t use TFHE.decrypt
, how can you handle error? I’m glad you ask. We’re working on a small util to help you manage that, but in the meantime, you can take a look at we did in the ERC20 contract: https://github.com/zama-ai/fhevm/blob/main/examples/EncryptedERC20.sol#L134
The idea is to maintain a mapping containing the last error, encrypted: the transfer is not reverted, but you can check with an encrypted integer if everything went well or not. You can elaborate on this idea by using a struct with more metadata.
Initial properties
You can’t init encrypted properties directly, such as euint32 private value = TFHE.asEuint(0);
. This won’t work because the Solidity Compiler will try to retrieve a value from this statement and will get a 0
. To init your encrypted variable, you need to do it in the constructor.
Also, you can check that a value “exists” with TFHE.isInitialized(encryptedValue)
. For example, this will return false
euint32 value;
return TFHE.isInitialized(value);
Prefer cmux over mul
Technically, this two lines would give the same result:
myValue = myValue + amount * condition;
myValue = TFHE.cmux(condition, myValue, myValue + amount);
In terms of performance, cmux will be faster as it employs the best method to evaluate the condition.
Avoid optimistic require
We are discussing optimistic requires, and there’s a possibility that it won’t be supported at some point. Therefore, it’s advisable to avoid using this function. (However, this is already the case since you are avoiding TFHE.decrypt, correct?)
Avoid surprises with gas limit
When you call estimate gas method, we can’t determine accurately the gas usage if your function uses TFHE.decrypt
. In the gas estimation, all TFHE.decrypt()
will return 1
.
What does it mean?
require(TFHE.decrypt(ebool));
will be ok but require(!TFHE.decrypt(ebool));
will fail during estimation (revert transaction)1
(true)1
While it’s challenging to accurately estimate gas consumption when using TFHE.decrypt
, we strongly encourage you to take this into consideration.