Skip to main content

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

In this contract, the owner can create a proposal with anywhere from 2-4 possible voting options. Users can then vote on this proposal by the deadline, at which point the proposal can be closed, and the result revealed. Unfortunately, the contract is completely public, and the vote tallies can be observed during the voting period. We will update this contract to interact with CoFHE, and use FHE encrypted variables to store the votes until the result is revealed. We will pay special attention to the following updates that need to be made:
  1. Constant time computation
  2. Handling the if/else case
  3. Handling the require case
  4. 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:
This will work, but it may make sense to prepare the trivially encrypted values in the constructor rather than in each transaction to save on gas and the number of FHE operations being executed. Let’s see how that would look:

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:
Let’s break down how this works:
  • We iterate through each of the proposal options
  • We always perform an FHE.add on 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.
require statements are also not allowed when working with encrypted variables for the same reason as above.
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:
And its invocation:
This event will be emitted with the encrypted 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 to address. ctHash is any encrypted variable like euint64 or ebool
  • FHE.allowSender(ctHash) - grants access to msg.sender
  • FHE.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
It is critical to ensure that FHE.allowThis is used on encrypted variables that need to be used later in the contract’s lifecycle. Contracts must have access to variables in order to perform FHE operations on those variables.

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:
The client-side flow looks like this:

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:
In this block you can see a few changes. The first is that we have split 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:
  1. Changing the vote counts from plain uint64 to encrypted values using euint64
  2. Modifying the voting function to use encrypted addition instead of plain addition
  3. Using FHE.allowPublic to mark values as decryptable after the voting deadline
  4. Adding a revealResults function that publishes decrypted values on-chain with FHE.publishDecryptResult
  5. Updating the getter function to handle decryption of results safely
The resulting contract provides the same functionality as the original, but with the added privacy benefit that individual votes are not visible on-chain until the final tally is decrypted. This demonstrates how CoFHE can be used to add privacy to existing contracts with minimal changes to the core logic.

Final FHEVotingExample.sol

Next Steps