Building an Onchain Confidential Single-Price Auction for Token Sales with Sealed Bids using Zama's fhEVM

March 18, 2025
The Zama Team

The programmable privacy features of Zama’s fhEVM is especially useful for financial and token applications, like token sale auctions where participants’ decisions are influenced by others. A confidential auction was the focus of Season 7 of Zama’s bounty program.

This season's bounty challenged developers to build a smart contract system in Solidity for launching a Single-Price Auction (SPA) with sealed bids for a set quantity of fungible assets.

We received many high-quality submissions, and selecting a winner wasn’t easy. But after careful evaluation, three projects stood out:

This post highlights the winning submission by community member Palra, who found a balance between confidentiality and scalability.

A focus on the bounty challenge

The bounty challenged developers to build a Single-Price Auction using sealed bids for a set quantity of fungible assets. Participants placed confidential bids, specifying both the number of tokens they wanted to buy and the price they were willing to pay.

The settlement price was determined by the last token sold—it was the lowest price among the winning bids. Winners were those who placed the highest bids within the available token limit, with all winning bids settling at the same price.

For example, suppose there are 10 assets for sale and 4 bidders:

  • Bidder A: 4 tokens at 100 USDC
  • Bidder B: 7 tokens at 10 USDC
  • Bidder C: 8 tokens at 42 USDC
  • Bidder D: 9 tokens at 7 USDC

The settlement price is 42 USDC. Bidder A receives 4 tokens, while Bidder C gets 6 tokens (out of the 8 requested). Both pay 42 USDC per token. The auction organizer collects 420 USDC (10 * 42).

If the auction didn’t sell out, developers had to define a resolution mechanism, such as refunds or execution at the lowest price. Beyond handling edge cases, the main technical challenge was computing the settlement price. Ordering and sorting confidential bids with TFHE in Solidity is computationally expensive and constrained by gas limits (i.e., block size limits).

Most submissions prioritized confidentiality, but this often came at the cost of scalability in large auctions. The winning submission by Palra struck an interesting balance between the two.

A focus on the winning submission

The winning submission introduced two key design choices: (1) using a dedicated data structure to store bids, and (2) opting for partial hiding—encrypting quantities while keeping prices visible.

The submission uses a Fenwick tree, a data structure well-suited for the EVM. It supports three key operations—interrogation, update, and search—each with O(log n) complexity, where n is the number of bidders. This makes it efficient for auctions while avoiding costly initialization. The implementation leverages two of these operations:

  1. The update operation inserts new bids.
  2. The search operation computes the settlement price.
    function update(Storage storage _this, uint16 atKey, euint128 quantity) internal {
        if (atKey == 0) revert FT_InvalidPriceRange();

        _this.largestIndex = TFHE.select(
            TFHE.eq(quantity, 0),
            _this.largestIndex,
            TFHE.select(TFHE.gt(atKey, _this.largestIndex), TFHE.asEuint16(atKey), _this.largestIndex)
        );
        TFHE.allowThis(_this.largestIndex);

        uint16 index = atKey;

        while (index >= atKey) {
            _this.tree[index] = TFHE.add(_this.tree[index], quantity);
            TFHE.allowThis(_this.tree[index]);

            unchecked {
                index += lsb(index);
            }
        }

        _this.tree[0] = TFHE.add(_this.tree[0], quantity);
        TFHE.allowThis(_this.tree[0]);
    }

For a deeper dive into how this feature works, check out our documentation.

When computing the settlement price, if the total auctioned tokens aren’t found in the tree, the system defaults to the lowest registered price. Each bid addition also tracks the lowest bid price separately from the Fenwick tree.

The winning submission introduced a custom Solidity library, HalfEncryptedFenwickTree. This data structure improves scalability by avoiding full sorting when settling the auction. Notably, the library is versatile and can be repurposed for other use cases.

The submission also incorporates key building blocks from Zama’s fhevm-contracts repository, including:

A focus on the auction process

Auction creation and initialization

First, the auction owner deploys a contract with the following parameters:

  • [.c-inline-code]auctionToken[.c-inline-code] – The token being sold (must follow the IConfidentialERC20 interface).
  • [.c-inline-code]auctionTokenSupply[.c-inline-code] – The total supply available for sale.
  • [.c-inline-code]baseToken[.c-inline-code] – The token used for bidding (also follows the IConfidentialERC20 interface).
  • [.c-inline-code]auctionEnd[.c-inline-code] – The timestamp when the auction ends.
  • [.c-inline-code]minPrice[.c-inline-code] – The minimum bid price.
  • [.c-inline-code]maxPrice[.c-inline-code] – The maximum bid price.
    constructor(
        address _auctioneer,
        IConfidentialERC20 _auctionToken,
        uint64 _auctionTokenSupply,
        IConfidentialERC20 _baseToken,
        uint256 _auctionEnd,
        uint64 _minPrice,
        uint64 _maxPrice
    ) Ownable(_auctioneer) EncryptedErrors(uint8(type(ErrorCodes).max)) 

After creating the auction, the owner (organizer) must deposit the auctioned tokens. This triggers a request to the gateway, which verifies that the deposited amount is sufficient for the auction to begin.

Active bidding phase.

Users submit bids by specifying the price (in clear) and the quantity (encrypted). Since prices remain public, ensuring auction confidentiality requires submitting multiple bids, including 0-value bids—bids where the encrypted quantity is 0.

    function bid(
        uint64 _price,
        einput _encryptedQuantity,
        bytes calldata _inputProof
    ) external onlyState(AuctionState.Active) nonReentrant {

For example, Bidder A might submit bids at 10 USDC, 50 USDC, 100 USDC, and 1000 USDC, all with encrypted quantities. Only the 100 USDC bid would have a nonzero quantity. Observers would see the possible prices but wouldn’t know which bid is real.

The contract also includes [.c-inline-code]error-handling[.c-inline-code] and verifies that bid quantities are properly transferred before recording the encrypted amount.

Settlement of the auction.

The auction settlement phase determines the settlement price.

While inserting values into the [.c-inline-code]HalfEncryptedFenwickTree[.c-inline-code] doesn’t require decryption, settlement is an iterative process that involves indexing encrypted quantities. A binary search is performed over the tree, branching left or right based on encrypted values.

Settlement relies on an asynchronous decryption scheme that works as follows:

  1. The contract requests decryption of an encrypted value.
  2. The Gateway triggers a callback, returning the cleartext value.
    function stepWithdrawalDecryption()
        external
        onlyState(AuctionState.WithdrawalPending)
        checkTimeLockTag(TL_TAG_COMPUTE_SETTLEMENT_STEP)
    {
        uint256[] memory cts = new uint256[](1);
        bytes4 selector;
        if (TFHE.isInitialized(_searchIterator.foundIdx)) {
            cts[0] = Gateway.toUint256(_searchIterator.foundIdx);
            selector = this.callbackWithdrawalDecryptionFinal.selector;
        } else {
            cts[0] = Gateway.toUint256(_searchIterator.idx);
            selector = this.callbackWithdrawalDecryptionStep.selector;
        }

        Gateway.requestDecryption(cts, selector, 0, block.timestamp + CALLBACK_MAX_DURATION, false);
        _startTimeLockForDuration(TL_TAG_COMPUTE_SETTLEMENT_STEP, CALLBACK_MAX_DURATION);
    }

Claiming phase.

The claiming process is the final phase of the auction.

  • Users with bids at or above the settlement price can claim auction assets based on their encrypted bid quantity.
  • The auction owner can withdraw the collected funds.
  • Non-winning participants—those with bids below the settlement price—can reclaim their transferred tokens.

Conclusion

While unaudited, this implementation presents a promising approach for sealed auctions within EVM constraints, leveraging Zama’s fhEVM for confidentiality. You can check out the full implementation of this season’s winning bounty at: GitHub – palra/zama-bounty-confidential-auction. If you wish you understand the architecture design of the code, check out the user guide documentation. And remember that smart contracts carry risks, and this submission is a proof-of-concept by an external contributor.

Additional links 

Read more related posts

No items found.