Blind Auction

Blind auction where bids remain fully encrypted until the end. Uses FHE.gt/select to find the winner without decrypting losing bids. Only the winning amount is revealed after the auction closes.

circle-info

To run this example correctly, make sure the files are placed in the following directories:

  • .sol file → <your-project-root-dir>/contracts/

  • .ts file → <your-project-root-dir>/test/

This ensures Hardhat can compile and test your contracts as expected.

chevron-right🔐 FHE API Reference (12 items)hashtag

Types: ebool · euint64 · externalEuint64

Functions:

  • FHE.allowThis() - Grants contract permission to operate on ciphertext

  • FHE.asEuint64() - Encrypts a plaintext uint64 value into euint64

  • FHE.checkSignatures() - Verifies KMS decryption proof (reverts if invalid)

  • FHE.fromExternal() - Validates and converts external encrypted input using inputProof

  • FHE.ge() - Encrypted greater-or-equal: returns ebool(a >= b)

  • FHE.gt() - Encrypted greater-than: returns ebool(a > b)

  • FHE.makePubliclyDecryptable() - Marks ciphertext for public decryption via relayer

  • FHE.select() - Encrypted if-then-else: select(cond, a, b) → returns a if true, b if false

  • FHE.toBytes32() - Converts encrypted handle to bytes32 for proof arrays

// SPDX-License-Identifier: BSD-3-Clause-Clear
pragma solidity ^0.8.24;

import {
    FHE,
    euint64,
    ebool,
    externalEuint64
} from "@fhevm/solidity/lib/FHE.sol";
import {ZamaEthereumConfig} from "@fhevm/solidity/config/ZamaConfig.sol";

/**
 * @notice Blind auction where bids remain fully encrypted until the end.
 *         Uses FHE.gt/select to find the winner without decrypting losing bids.
 *         Only the winning amount is revealed after the auction closes.

 * @dev Flow: bid() → endAuction() → revealWinner()
 *      Uses FHE.gt/select to find winner without revealing losing bids.
 */
contract BlindAuction is ZamaEthereumConfig {
    enum AuctionState {
        Open, // Accepting bids
        Closed, // Bidding ended, pending reveal
        Revealed // Winner revealed on-chain
    }

    /// Auction owner (deployer)
    address public owner;

    /// Current auction state
    AuctionState public auctionState;

    /// Minimum bid in plaintext (for gas efficiency)
    uint64 public minimumBid;

    /// Auction end timestamp
    uint256 public endTime;

    /// All bidder addresses (for iteration)
    address[] public bidders;

    /// Mapping from bidder address to their encrypted bid
    mapping(address => euint64) private _bids;

    /// Whether an address has bid
    mapping(address => bool) public hasBid;

    /// Encrypted winning bid amount (set after auction ends)
    euint64 private _winningBid;

    /// Encrypted winner index in bidders array
    euint64 private _winnerIndex;

    /// Address of the winner (set after reveal)
    address public winner;

    /// Revealed winning amount (set after reveal)
    uint64 public winningAmount;

    /// @notice Emitted when a new bid is placed
    /// @param bidder Address of the bidder
    event BidPlaced(address indexed bidder);

    /// @notice Emitted when auction is closed
    /// @param encryptedWinningBid Handle for encrypted winning bid
    /// @param encryptedWinnerIndex Handle for encrypted winner index
    event AuctionEnded(
        euint64 encryptedWinningBid,
        euint64 encryptedWinnerIndex
    );

    /// @notice Emitted when winner is revealed
    /// @param winner Address of the winner
    /// @param amount Winning bid amount
    event WinnerRevealed(address indexed winner, uint64 amount);

    /// @dev Restricts function to owner only
    modifier onlyOwner() {
        require(msg.sender == owner, "Only owner can call");
        _;
    }

    /// @notice Creates a new blind auction
    /// @param _endTime Unix timestamp when bidding ends
    /// @param _minimumBid Minimum bid amount (plaintext)
    constructor(uint256 _endTime, uint64 _minimumBid) {
        require(_endTime > block.timestamp, "End time must be in future");
        owner = msg.sender;
        endTime = _endTime;
        minimumBid = _minimumBid;
        auctionState = AuctionState.Open;
    }

    /// @notice Submit an encrypted bid to the auction
    /// @dev Each address can only bid once
    /// @param inputProof Proof validating the encrypted input
    function bid(
        externalEuint64 encryptedBid,
        bytes calldata inputProof
    ) external {
        require(auctionState == AuctionState.Open, "Auction not open");
        require(block.timestamp < endTime, "Auction has ended");
        require(!hasBid[msg.sender], "Already placed a bid");

        // Convert external encrypted input to internal handle (proof verified)
        euint64 bidAmount = FHE.fromExternal(encryptedBid, inputProof);
        FHE.allowThis(bidAmount);

        // Store bid - amount stays encrypted
        _bids[msg.sender] = bidAmount;
        hasBid[msg.sender] = true;
        bidders.push(msg.sender);

        emit BidPlaced(msg.sender);
    }

    /// @notice End the auction and compute the winner
    /// @dev ⚡ Gas: O(n) loop with FHE.gt/select. ~200k gas per bidder!
    /// @dev Only owner can call after end time
    function endAuction() external onlyOwner {
        require(auctionState == AuctionState.Open, "Auction not open");
        require(block.timestamp >= endTime, "Auction not yet ended");
        require(bidders.length > 0, "No bids placed");

        // Find winner using encrypted comparisons (no bids revealed!)
        euint64 currentMax = _bids[bidders[0]];
        euint64 currentWinnerIdx = FHE.asEuint64(0);

        for (uint256 i = 1; i < bidders.length; i++) {
            euint64 candidateBid = _bids[bidders[i]];

            // 🔀 Why select? if/else would leak which bid is higher!
            // Losing bids remain encrypted forever
            ebool isGreater = FHE.gt(candidateBid, currentMax);
            currentMax = FHE.select(isGreater, candidateBid, currentMax);
            currentWinnerIdx = FHE.select(
                isGreater,
                FHE.asEuint64(uint64(i)),
                currentWinnerIdx
            );
        }

        // Check minimum bid (comparison stays encrypted)
        ebool meetsMinimum = FHE.ge(currentMax, FHE.asEuint64(minimumBid));
        _winningBid = FHE.select(meetsMinimum, currentMax, FHE.asEuint64(0));
        _winnerIndex = currentWinnerIdx;

        // Mark for public decryption via KMS relayer
        FHE.allowThis(_winningBid);
        FHE.allowThis(_winnerIndex);
        FHE.makePubliclyDecryptable(_winningBid);
        FHE.makePubliclyDecryptable(_winnerIndex);

        auctionState = AuctionState.Closed;

        emit AuctionEnded(_winningBid, _winnerIndex);
    }

    /// @notice Reveal the winner with KMS decryption proof
    /// @dev Anyone can call once relayer provides proof.
    ///      ⚠️ Order matters! cts[] must match abi.decode() order.
    function revealWinner(
        bytes memory abiEncodedResults,
        bytes memory decryptionProof
    ) external {
        require(auctionState == AuctionState.Closed, "Auction not closed");

        // Build handle array - order must match abi.decode() below!
        bytes32[] memory cts = new bytes32[](2);
        cts[0] = FHE.toBytes32(_winningBid);
        cts[1] = FHE.toBytes32(_winnerIndex);

        // Verify KMS signatures (reverts if proof invalid)
        FHE.checkSignatures(cts, abiEncodedResults, decryptionProof);

        // Decode verified plaintext results
        (uint64 revealedAmount, uint64 winnerIdx) = abi.decode(
            abiEncodedResults,
            (uint64, uint64)
        );

        if (revealedAmount >= minimumBid && winnerIdx < bidders.length) {
            winner = bidders[winnerIdx];
            winningAmount = revealedAmount;
        } else {
            winner = address(0);
            winningAmount = 0;
        }

        auctionState = AuctionState.Revealed;

        emit WinnerRevealed(winner, winningAmount);
    }

    /// @notice Get the number of bidders
    function getBidderCount() external view returns (uint256) {
        return bidders.length;
    }

    /// @notice Get bidder address by index
    function getBidder(uint256 index) external view returns (address) {
        require(index < bidders.length, "Index out of bounds");
        return bidders[index];
    }

    /// @notice Get encrypted winning bid handle (after auction ends)
    function getEncryptedWinningBid() external view returns (euint64) {
        require(auctionState != AuctionState.Open, "Auction still open");
        return _winningBid;
    }

    /// @notice Get encrypted winner index handle (after auction ends)
    function getEncryptedWinnerIndex() external view returns (euint64) {
        require(auctionState != AuctionState.Open, "Auction still open");
        return _winnerIndex;
    }
}

Last updated