fhEVM best practices [from Discord]

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. :arrow_down:

(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():

Avoid 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)
  • A loop, where you expect a decrypt to be false to break, will never end in gas estimate method (and fails), since the decrypt will always return 1 (true)
  • On the other hand, if your loop should last 2 or 3 cycles, until the value is 1, the estimation will be below.
  • If you have branches (if/else) based on a decryption, the estimation will use the branch running when the decryption is 1

While it’s challenging to accurately estimate gas consumption when using TFHE.decrypt, we strongly encourage you to take this into consideration.