Overview
Let’s start with an existing contract, and walk through the steps to migrate the contract to CoFHE. The contract is a very simple voting contract.Original Contract
- Constant time computation
- Handling the
if/elsecase - Handling the
requirecase - Revealing the result with the decrypt-with-proof pattern
Migrating the Contract
Step 1: Import FHE.sol
The first thing that we need to do is import FHE.sol from the cofhe-contracts repo. This package allows smart contracts to start using FHE operations and encrypted variables.
Step 2: Encrypt the vote counters with euint64
The amount of votes cast for each option needs to be encrypted, so we can switch the variable type from a uint64 to an euint64 (euint64 is included in the FHE.sol import). The vote counts will be represented by a ctHash which acts as a handle and pointer to the encrypted number of votes.
Step 3: Initialize the euint64 vote counts
Now that the votes type has changed, it must be initialized by performing a trivialEncrypt of the starting value:
Step 4: Handle user votes with InEuint8
We now need to handle the user’s vote casting. The first thing that we need to do is hide which option the user is voting for. We can do this by replacing the vote function parameter uint256 _optionIndex with InEuint8 memory _optionIndex. InEuint8 is an encrypted input type. We then need to convert the InEuint8 to an euint8 for use in computation.
Encrypting inputs requires the use of the Client SDK (
@cofhe/sdk).Read more about encrypted inputs.Step 5: Constant time computation
In order to preserve the confidentiality of the user’s vote, we must make sure that we aren’t leaking any information about the user’s choice. If we only updated the voting option that the user has selected, then a user’s vote could be deduced by simply watching which vote counter changes. Therefore, we must update all the vote counters to hide the user’s true vote:- We iterate through each of the proposal options
- We always perform an
FHE.addon every options’ vote count, this means that every options’ vote count will change any time a user votes - We only want to increment the user’s selected choice, so we use
FHE.select - The API for select is
FHE.select(conditional, ifTrue, ifFalse) - Without the FHE syntax, the logic is as follows:
Step 6: Handling if/else
Branching based on encrypted variables is not allowed, so there is one more change to make to the vote function, which is to remove the if/else branch that relies on _optionIndex.
It is important to never use an encrypted variable as part of an if/else branch, since the encrypted variable is always truthy. Instead, use
FHE.select to replace the value with 0.Step 7: Update VoteCast event
Currently the VoteCast event emits a uint256 optionIndex. Now that the user’s vote is an encrypted variable (euint8), we need to update the event to emit the user’s encrypted input:
euint8 optionIndex. This is important as in the future the FHE block explorer will be able to decrypt these variables and show the true event log, but only if we make one more change.
Step 8: Granting access with FHE.allow
By default, access and computation on an encrypted variable is blocked, and trying to use a variable before access has been granted will cause AccessControlList.sol (ACL) to revert with an ACLNotAllowed error. Access is granted using FHE.allow (and its variants FHE.allowSender, FHE.allowThis, and FHE.allowPublic). After access is granted, the encrypted variable may be used in a computation, or decrypted by any of the authorized users or contracts.
FHE.allow variants:
FHE.allow(ctHash, address)- grants access toaddress.ctHashis any encrypted variable likeeuint64oreboolFHE.allowSender(ctHash)- grants access tomsg.senderFHE.allowThis(ctHash)- grants access to the executing contract (address(this))FHE.allowPublic(ctHash)- grants access to everyone, useful for things like an encrypted totalSupply variable, which everyone should have access to
Step 9: Finalize the voting with FHE.allowPublic
Because the votes are encrypted for the lifetime of the proposal, after the proposal has ended, we need to decrypt the results and reveal the winner. Let’s start by adding a finalizeVote function that marks the vote counts as publicly decryptable:
FHE.allowPublic marks each vote count as eligible for public decryption. Anyone can now request the plaintext values off-chain.
Step 10: Reveal the results with FHE.publishDecryptResult
After finalizeVote is called, the vote counts need to be decrypted off-chain and published on-chain with proof. We add a revealResults function that accepts the decrypted values and their Threshold Network signatures:
Step 11: Checking the results with FHE.getDecryptResultSafe
Finally, we can update our getProposal function to check the final state of the proposal. Once the results have been published, FHE.getDecryptResultSafe will return the plaintext values:
options and votes from the getProposal return type, which allows us to better handle the decryption results.
We use FHE.getDecryptResultSafe to fetch the decryption result of each of the vote counts, which returns the result as well as a flag indicating whether the decryption has posted.
Once all the decryptions have posted, the finalized flag will update to be true, and the winner determined based on the vote counts.
Conclusions
In this tutorial, we walked through migrating a simple voting contract to use CoFHE. The key changes were:- Changing the vote counts from plain
uint64to encrypted values usingeuint64 - Modifying the voting function to use encrypted addition instead of plain addition
- Using
FHE.allowPublicto mark values as decryptable after the voting deadline - Adding a
revealResultsfunction that publishes decrypted values on-chain withFHE.publishDecryptResult - Updating the getter function to handle decryption of results safely
Final FHEVotingExample.sol
Next Steps
- Learn about Migrating from FHE.decrypt to the new decryption flow
- Explore ACL Usage Examples for complex access control patterns
- Review Best Practices for security considerations