Behind the Scenes of a Blind Auction

Blockchains are transparent by design—but sometimes, that’s exactly the problem.
There are moments when transparency becomes an issue: bidding wars, sealed offers, private sales. In all these situations, keeping values hidden until the end isn’t a feature—it’s a necessity. That’s where blind auctions powered by Fully Homomorphic Encryption (FHE) come in.
In this post, we’ll go under the hood of a confidential blind auction, covering how encrypted bids are submitted, how a winner is determined without revealing any bids, and how the final results are revealed only when the time is right. As always, it all comes down to a few key homomorphic operations—TFHE.add(), TFHE.sub(), TFHE.select()—and the ability to compute over encrypted values without ever decrypting them onchain.
What Is a Blind Auction?
In a blind auction, bidders submit their offers without knowing anyone else’s bid. This keeps the process fair and discourages gaming the system. But doing this on a transparent blockchain? That’s tricky.
With confidential blind auctions, every bid is encrypted using TFHE. The blockchain stores encrypted values only. Not even the contract itself can read who bid what. The winner? Revealed after the auction ends—never before.
Homomorphic Arithmetic in Action
The auction logic leans heavily on three core operations:
- TFHE.add() – to increase a bidder’s total offer.
- TFHE.sub() – to calculate the additional amount a user is sending when they rebid.
- TFHE.select() – to determine who’s winning and when to update internal state—all while everything stays encrypted.
Let’s walk through the steps.
Submitting a an Encrypted Bid When someone bids, they submit an encrypted value using:
function bid(einput encryptedValue, bytes calldata inputProof) external onlyBeforeEnd
Under the hood:
Convert the encrypted input into a usable encrypted integer:
euint64 value = TFHE.asEuint64(encryptedValue, inputProof);
If the bidder has already bid, we compare their previous encrypted amount with the new one:
ebool isHigher = TFHE.lt(existingBid, value);
euint64 toTransfer = TFHE.sub(value, existingBid);
euint64 amount = TFHE.select(isHigher, toTransfer, TFHE.asEuint64(0));
Only if the new bid is higher do we allow the encrypted difference to be transferred to the contract. This prevents users from rebidding the same amount or less.
Track encrypted bids and assign each bidder a random encrypted ticket. This ticket becomes their identity in the auction:
euint64 randTicket = TFHE.randEuint64();
userTickets[msg.sender] = TFHE.select(TFHE.ne(sentBalance, 0), randTicket, userTickets[msg.sender]);
Picking a Winner (Without Knowing Who Won Yet)
As bids come in, the contract silently keeps track of the current highest encrypted bid and the ticket of the person who placed it:
ebool isNewWinner = TFHE.lt(highestBid, currentBid);
highestBid = TFHE.select(isNewWinner, currentBid, highestBid);
winningTicket = TFHE.select(isNewWinner, userTicket, winningTicket);
At no point are the actual values or bidders exposed. The system simply runs comparisons on ciphertexts—picking a winner without ever seeing who it is.
Ending the Auction
When the auction ends (either naturally or manually), the owner can call:
function auctionEnd()
This triggers the transfer of the encrypted highest bid to the beneficiary. Even this transfer uses TFHE under the hood:
TFHE.allowTransient(highestBid, address(tokenContract));
tokenContract.transfer(beneficiary, highestBid);
Again, the blockchain never sees the actual number—only that something encrypted was transferred.
Who Won? Time to Reveal
Once the auction ends, anyone can initiate the decryption of the winning ticket:
function decryptWinningTicket()
After a short delay, the decrypted winning ticket becomes available onchain.
Then, any bidder can check if they won by calling:
function claim()
This compares the bidder’s encrypted ticket with the decrypted winner:
ebool canClaim = TFHE.and(
TFHE.eq(winningTicket, userTickets[msg.sender]),
TFHE.not(objectClaimed)
);
If it matches, the contract recognizes them as the winner—without ever having known their identity during the auction.
Non-Winners Can Still Withdraw
If a bidder didn’t win, they can take their encrypted bid back:
function withdraw()
The contract checks that their ticket doesn’t match the winning one, and returns their encrypted bid:
ebool canWithdraw = TFHE.ne(winningTicket, userTickets[msg.sender]);
euint64 amount = TFHE.select(canWithdraw, bidValue, TFHE.asEuint64(0));
tokenContract.transfer(msg.sender, amount);
Again—private values stay private.
Why Blind Auctions Need Encryption
Blind auctions work because no one knows what anyone else bid. Doing this on a transparent blockchain sounds like a contradiction—but with FHE, it's possible.
- Privacy: No one can peek at bids—not even the contract.
- Security: The highest bid is transferred only after the auction ends.
- Trustlessness: The winner is picked by encrypted comparisons, not offchain votes.
Conclusion
Blind auctions on the fhEVM use the same building blocks we’ve seen in confidential votes and private token transfers: homomorphic operations that let us calculate with encrypted values. But here, those tools come together to enable a fair, sealed-bid auction—onchain, trustless, and fully private.
TFHE.select() helps choose a winner. TFHE.add() and TFHE.sub() update balances securely. And all of this runs without revealing a single bid until the moment of truth.
Privacy and trust don’t have to be opposites. With encrypted smart contracts, we get both—and we’re just getting started.
Subscribe to our newsletter
Top industry insights on FHE.