Never use encrypted types for constant or immutable state variables, even if they should actually stay constants, or else any transaction involving those will fail. This is because ciphertexts should always be stored in the privileged storage of the contract (see parapgraph 4.4 of whitepaper) while constant and immutable variables are just appended to the bytecode of the deployed contract at construction time.
❌ So, even if a
and b
should never change after construction, this code :
✅ Should be replaced by this snippet:
Declaring an encrypted state variable as public exposes the variable to any external untrusted smart contract to access and potentially decrypt them, compromising their confidentiality.
❌ In summary, never write in production:
✅ Instead, you should declare the state variable as follow:
In this last snippet, the internal
keyword could have been omitted (state variables are internal by default) or alternatively have been replaced by private
.
If a view function is using TFHE.reencrypt
it is mandatory to protect its access to not leak confidentiality, for instance this is doable easily via the onlySignedPublicKey
modifier imported from "fhevm/abstracts/Reencrypt.sol"
. See the example from the Decrypt page. Failing to address this allows anyone to reencrypt another person's ciphertext. This vulnerability comes from the ability to impersonate any msg.sender
address during a static call to a view function, as it does not require a signature, unlike transactions.
Any use of decryption should be avoided as much as possible. Current version of TFHE.decrypt
will soon be deprecated and get replaced by an asynchronous version, so please consider this operator as a very expensive one which should be used only if absolutely necessary.
Whenever your code contains a branch depending on the result of a decryption, we recommend to replace it by a TFHE.cmux
.
❌ For instance, instead of:
✅ We recommend instead to use the following pattern:
The previous paragraph emphasized that branch logic should rely as much as possible on TFHE.cmux
instead of decryptions. It hides effectively which branch has been executed.
However, this is sometimes not enough. Enhancing the privacy of smart contracts often requires revisiting your application's logic.
For example, if implementing a simple AMM for two encrypted ERC20 tokens based on a linear constant function, it is recommended to not only hide the amounts being swapped, but also the token which is swapped in a pair.
✅ Here is a very simplified example implementations, we suppose here that the the rate between tokenA and tokenB is constant and equals to 1:
Notice that to preserve confidentiality, we had to make two inputs transfers on both tokens from the user to the AMM contract, and similarly two output transfers from the AMM to the user, even if technically most of the times it will make sense that one of the user inputs encryptedAmountAIn
or encryptedAmountBIn
is actually an encrypted zero.
This is different from a classical non-confidential AMM with regular ERC20 tokens: in this case, the user would need to just do one input transfer to the AMM on the token being sold, and receive only one output transfer from the AMM on the token being bought.
❌ Avoid using this type of loop because it might require many decryption operations, and in the example given, we're also leaking how much was added to x
, which may not be intended behavior.:
If your code logic requires looping on an encrypted boolean condition, we highly suggest to try to replace it by a finite loop with an appropriate constant maximum number of steps and use TFHE.cmux
inside the loop.
✅ For example, the previous code could maybe be replaced by the following snippet:
Using encrypted indexes to pick an element from an array without revealing it is not very efficient, because you would still need to loop on all the indexes to preserve confidentiality.
However, there are plans to make this kind of operation much more efficient in the future, by adding specialized operators for arrays.
For instance, imagine you have an encrypted array called encArray
and you want to update an encrypted value x
to match an item from this list, encArray[i]
, without disclosing which item you're choosing.
❌ You must loop over all the indexes and check equality homomorphically, however this pattern is very expensive in gas and should be avoided whenever possible.
Some TFHE operators exist in two versions : one where all operands are ciphertexts handles, and another where one of the operands is an unencrypted scalar. Whenever possible, use the scalar operand version, as this will save a lot of gas. See the page on Gas to discover which operators support scalar operands and compare the gas saved between both versions: all-encrypted operands vs scalar.
❌ For example, this snippet cost way more in gas:
✅ Than this one:
Despite both leading to the same encrypted result!
TFHE arithmetic operators can overflow. Do not forget to take into account such a possibility when implementing fhEVM smart contracts.
❌ For example, if you wanted to create a mint function for an encrypted ERC20 tokens with an encrypted totalSupply
state variable, this code is vulnerable to overflows:
✅ But you can fix this issue by using TFHE.cmux
to cancel the mint in case of an overflow:
Notice that we did not check separately the overflow on balances[msg.sender]
but only on totalSupply
variable, because totalSupply
is the sum of the balances of all the users, so balances[msg.sender]
could never overflow if totalSupply
did not.