Onchain Blind Auctions Using Homomorphic Encryption and the fhEVM
This example uses an encrypted ERC20 token, which was introduced in a previous article.
Blind auctions are notoriously hard to do in smart contracts, since all the transaction and states are public. While some solutions have been published, they either require using ZK and thus keeping the data off-chain, or using a 2 step process that includes a reveal function. Thanks to homomorphic encryption and the fhEVM however, it is now possible to run blind auctions fully on-chain, without the need for a reveal function.
The blind auction protocol we will be implementing here has two phases: a bidding and a claim phase. The bidding phase consists of users bidding an encrypted amount of tokens (using the encrypted ERC20 contract).
When the auction ends, the contract homomorphically determines who the highest bidder is. If the user is the highest bidder, they can claim their prize. If the user is not the highest bidder, they can withdraw their bidding tokens.
In the end, the contract only discloses the winning bidder, while keeping the winning bid value and non-winning bid values private.
Writing our blind auction contract
The contract we will implement is almost identical to this non-encrypted one. We simply use the FHE data types where necessary to keep things hidden.
The first step in writing the blind auction smart contract is to define the auction parameters. This includes the address of the beneficiary, the encrypted ERC20 address, and the end time of the auction. These parameters are defined as properties in the smart contract code. Note that in the code below, the [.c-inline-code]euint[.c-inline-code] data type is used to represent encrypted unsigned integers.
Writing the bid function
The bid function is split into two parts:
- When a user bids for the first time, we set the user’s bid to provided encrypted value
- If the user had placed a bid before, we check that the current bid is higher than the previous one, then transfer the difference between the new bid amount and the previous bid amount.
Once the bid is set, we compare the bid with the current highest bid for the auction. If the bid is higher, we update [.c-inline-code]highestBid[.c-inline-code].
But wait, how can we do an [.c-inline-code]if[.c-inline-code] statement on an encrypted value? Well, we can’t, which is why we need to modify the code to have the same effect, without using conditionals.
Ifs without ifs
To illustrate how homomorphic conditionals can be implemented, let’s take a simple example, where user A wants to send tokens to user B. We need to check that user A has enough tokens without revealing any other data. This can be done with the following FHE code:
In this code, we first evaluate a homomorphic comparison checking that the user has more balance than the amount to be transferred. This homomorphic comparison will return an encryption of 0 if false, or an encryption of 1 if true. We then simply homomorphically multiply the amount by this encrypted comparison value, which will simply set the amount to 0 if the user doesn’t have enough balance. The transaction will still go through, and a new balance ciphertext will be generated, but it’s actual value won’t change (since we transferred 0 tokens).
Now applying this to the bid function, first when there is no existing bid:
We can then check if the bid is the highest bid. If this is the first bid, it’s easy; otherwise, we need to perform a homomorphic comparison. The comparison is done with a method which acts as a ternary operator ([.c-inline-code]cmux[.c-inline-code] stands for encrypted mux, aka [.c-inline-code]condition ? value_if_true : value_if_false[.c-inline-code]) .
It is important to keep in mind that each time we assign a value using [.c-inline-code]TFHE.cmux[.c-inline-code], the value changes, even if the plaintext value remains the same.
Next, let’s implement the version where the user already has a bid, and wants to update it:
Going Once, Going Twice, Sold
Once the auction is over, participants will be able to claim their item, or get their tokens back:
- [.c-inline-code]auctionEnd()[.c-inline-code] transfers the correct amount of tokens to the beneficiary
- [.c-inline-code]claim()[.c-inline-code] allows the winner to claim their prize
- [.c-inline-code]withdraw()[.c-inline-code] allows other participants to withdraw their bid
To allow users to determine if they have the highest bid, we need to decrypt the encrypted boolean value expressing whether or not they have the highest bid. This is done via the [.c-inline-code]TFHE.reencrypt[.c-inline-code] method, which re-encrypts a ciphertext from the network’s public key under the user’s public key. This is done in Solidity with 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 caller is actually the owner of the bid. The signed token respects the EIP-712 standard.
With this method, the user can determine if they have the highest bid or not: if they do, the winner can use the [.c-inline-code]claim()[.c-inline-code] method, otherwise, they can withdraw their tokens.
The beneficiary can call [.c-inline-code]auctionEnd()[.c-inline-code] to start the transfer of tokens.
We have a winner
Using homomorphic encryption in a blind auction smart contract can provide an additional layer of security and privacy for participants. By following the steps outlined in this article, you can create a secure and transparent blind auction smart contract where bids are encrypted and computations are performed on the encrypted data. The claim function replaces the reveal function, allowing the winner of the auction to claim their prize without revealing the highest bid.
Join the Zama x FHENIX hackathon during EthCC and take Zama's fhEVM for a spin!
Additional links
- Follow Zama on Twitter
- Check out Zama on Github
- Deep dive into Zama fhEVM smart contract protocol with this blog post