Confidential ERC-20 Tokens Using Homomorphic Encryption and the fhEVM

June 28, 2023
Clément Danjou

This is the first post in a series dedicated to our fhEVM protocol, which enables private smart contracts using homomorphic encryption.

Fully Homomorphic Encryption (FHE) is a technology that allows computations to be performed on encrypted data without requiring its decryption. FHE can be used in blockchain to enable confidential smart contracts, where transaction inputs and states remain encrypted at all time, even during computation. You can read more about the fhEVM protocol here.

Building smart contracts for the fhEVM

Before we look at the features of the fhEVM, it is important to keep in mind a few things:

  • The fhEVM is a regular EVM that can compute on both encrypted and non-encrypted values. Developers can use all the existing EVM tools without change to build their applications.
  • All the computation is happening on-chain, whether they are FHE operations or regular EVM operations. This ensures the data remains available at all times.
  • The transaction inputs and on-chain states are encrypted under a global network key. This is necessary for composability and multi-user interactions. The network key is split amongst validators to ensure no one can decrypt the blockchain state without first reaching a consensus.
  • Smart contracts contain all the logic for the computation of states, and for the visibility of the encrypted states. The smart contract developer is the one who decides how to reveal states from his contract to the user requesting it, effectively making the contract itself perform the access control logic. Validators then simply reach consensus on what to decrypt by evaluating the contracts, and run a threshold decryption protocol to reveal the value to the user. 
  • To avoid malicious actors from using on-chain encrypted states as transaction inputs, and writing contracts that can decrypt it, users need to provide a ZK proof that they know the plaintext value of the encrypted inputs they sent in their transactions.

FHE data types

To make it simple for developers to use encrypted values in their contracts, the fhEVM exposes several encrypted integer data types, such as euint8, euint16, euint32. They act as the encrypted equivalent to the regular uint data types, with one major difference: smaller types are much faster to compute in FHE than bigger types, so if you only need an 8 bit encrypted value, you should not use a 256 container for it! Right now, the fhEVM only supports 8, 16 and 32 bit unsigned encrypted integers, but larger precision, signed integers and other data types are coming soon.

// A mapping from address to an encrypted balance.
mapping(address => euint32) public balances;

Computing on encrypted values

The fhEVM supports many of the traditional integer operations, such as add, sub, mul, shift, min, max, etc. We will be adding more over time, as well as provide some cryptographically optimized methods for the most common workflows.

// balance[msg.sender] and amount are euint32. We subtract amount from balance.
balance[msg.sender] = balance[msg.sender] - amount

Reverting transactions based on an encrypted condition

The require operator is an error-handling, global function in Solidity: if the condition within require comes out to be true then the compiler will execute the method, otherwise it will throw an error. In homomorphic encryption, a comparison between two ciphertexts will return an encrypted Boolean, so to evaluate it at runtime we need a specific method, similar to require, but for encrypted Booleans. Behind the scenes, this will send a request to the validators to run a threshold decryption on the control bit, and return a plaintext value for the condition.

// Verify that the balance as enough fund before a transfer
require(TFHE.decrypt(TFHE.lte(amount, balances[from])));

Parsing and validating encrypted transaction inputs

When a user sends an encrypted value in a transaction, it arrives in the contract as a byte array. The contract then needs to explicitly cast it to the correct data type, which will both parse the bytes and verify the ZKPoK attached to it. It then returns an euint that can be used in the rest of the contract’s logic.

// Verify bytes sent by the user and returns euint32
euint32 verifiedAmount = TFHE.asEuint32(amount);

An example fhEVM contract: confidential ERC20 tokens

Now that we understand the basic building blocks of the fhEVM, we can go into a concrete example and build an ERC20 token contract where the user balances and amounts being transferred are kept hidden at all times.

This confidential ERC20 contract is a classic ERC20 contract with a few differences:

  • Balances are encrypted unsigned integers;
  • Total supply is also encrypted, but decryptable by any user. To avoid leaking information about balances and ensure differential privacy, we add a random number of encrypted tokens on each mint (we’ll detail this later).

Let’s start by writing our contract with its properties:

// SPDX-License-Identifier: BSD-3-Clause-Clear
pragma solidity >=0.8.13 <0.9.0;

// Loading TFHE library
import "fhevm/lib/TFHE.sol";

contract EncryptedERC20 {
  euint32 public totalSupply;
  euint32 private burnable;

  uint8 internal mintedCounter;

  string public name = "Naraggara"; // City of Zama's battle
  string public symbol = "NARA";
  uint8 public decimals = 18;

  // A mapping from address to an encrypted balance.
  mapping(address => euint32) private balances;

  // A mapping of the form mapping(owner => mapping(spender => allowance)).
  mapping(address => mapping(address => euint32)) private allowances;

  // The owner of the contract.
  address internal contractOwner;

  constructor() {
    contractOwner = msg.sender;
    mintedCounter = 0;
  }
}

Transferring tokens

Transferring tokens involves a series of steps to ensure security and confidentiality. First, the ciphertext of the amount to be transferred is verified and the method returns an euint32. Then, the contract checks that the amount to be transferred is within the sender's balance, ensuring that there is no overspending. Finally, the transfer is executed by deducting the amount from the sender's balance and adding it to the recipient's balance, homomorphically.

// Transfers an encrypted amount from the message sender address to the `to` address.
function transfer(address to, bytes calldata encryptedAmount) public {
  euint32 amount = TFHE.asEuint32(encryptedAmount);
  address from = msg.sender;
  // Make sure the sender has enough tokens.
  require(TFHE.decrypt(TFHE.lte(amount, balances[from])));

  // Add to the balance of `to` and subtract from the balance of `from`.
  balances[to] = balances[to] + amount
  balances[from] = balances[from] - amount
}

Displaying a balance

In order for a user to view their balance, the contract needs to tell the network to re-encrypt the value from the network's key into the user's key. This is done directly in Solidity via a simple view function. This view function however needs to be authenticated by having the user provide a signature in the contract method call so that the network knows the user is actually the one owning the balance. The signed token respects the EIP-712 standard.

// Returns the balance of the caller under their public FHE key.
function balanceOf(bytes32 publicKey, bytes calldata signature) public view onlySignedPublicKey(publicKey, signature) returns (bytes memory) {
  return TFHE.reencrypt(balances[msg.sender], publicKey);
}

Minting tokens without leaking information about balances

When a user mints an encrypted amount of tokens, the increase in the contract's total supply can reveal the number of tokens that have been minted. To prevent this, a random number of burnable tokens are minted such that someone decrypting the total supply doesn’t know how many real tokens were minted. The tokens in the burnable pool are then regularly burned to maintain equivalence between the total supply and the actual supply. To simplify, we will reset burnable tokens every 10 mints.

// Sets the balance of the owner to the given encrypted balance.
function mint(bytes calldata encryptedAmount, bytes calldata encryptedRandom) public onlyContractOwner {
  euint32 amount = TFHE.asEuint32(encryptedAmount);
  euint32 rand = TFHE.asEuint32(encryptedRandom);
  balances[contractOwner] = amount + balances[contractOwner];

  // Add it to the pool of burnable token
  burnable = burnable + rand;

  euint32 toAdd = rand + amount;
  totalSupply = totalSupply + toAdd;
  mintedCounter += 1;
  if (mintedCounter >= 10) {
    totalSupply = totalSupply - burnable;
    // Reset burnable
    burnable = 0;
    // Reset counter
    mintedCounter = 0;
  }
}

In the mint method of our ERC20 contract, a user-generated random number has been used. This isn’t ideal however, which is why we are planning to add an FHE random number generator to the fhEVM itself, which would enable contracts to generate randomness on-demand.

Transaction Approved

Tokens are a simple use case, but hopefully this tutorial showed how easy it is to build with FHE. Now that we have a confidential token, we can start thinking about all kinds of applications where this would be useful, such as blind auctions, confidential DeFi and more, which we will explore in future posts.


Join the
Zama x FHENIX hackathon during EthCC and take the Zama's fhEVM for a spin!

Additional links

Read more related posts