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.
Before we look at the features of the fhEVM, it is important to keep in mind a few things:
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;
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] - amountThe 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])));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);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:
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;="" private="" burnable;="" uint8="" internal="" mintedcounter;="" string="" name="Naraggara" ;="" city="" of="" zama's="" battle="" symbol="NARA" 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 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
}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);
}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.
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!
News, research and product releases