Skip to main content

Overview

This example demonstrates how to build a fully confidential auction system where bids remain encrypted throughout the bidding process. Only when the auction closes can the winner be revealed, ensuring that bidders cannot see or react to each other’s bids.

What You’ll Learn

In this example, you’ll see practical implementations of:
  • Encrypted input handling - Processing encrypted bid amounts
  • Encrypted comparisons - Finding the highest bid without revealing values
  • Conditional logic with select - Updating the highest bidder based on encrypted conditions
  • Access control management - Properly managing permissions for encrypted data
  • Asynchronous decryption - The two-step process of requesting and retrieving decrypted results
  • Safe vs unsafe decryption - Both approaches to handling decryption results

How It Works

The auction follows this flow:
1

Initialization

The auctioneer deploys the contract, which initializes the auction with zero bid and address values, both encrypted.
2

Bidding Phase

Participants submit bids by sending plaintext amounts that are immediately encrypted. Each bid is compared against the current highest bid using encrypted comparison (FHE.gt), and the highest bid and bidder are updated accordingly.
3

Close Auction

The auctioneer closes the auction and requests decryption of the highest bid and bidder address. This triggers the asynchronous decryption process.
4

Reveal Winner

After decryption completes, the auctioneer can reveal the winner. The contract provides both safe and unsafe methods for retrieving the decrypted results.

Key Concepts Demonstrated

1. Encrypted State Variables

The contract stores the highest bid and bidder as encrypted values:
euint64 private highestBid;      // Encrypted bid amount
eaddress private highestBidder;  // Encrypted bidder address
These values remain encrypted throughout the entire auction, preventing anyone from seeing the current highest bid.

2. Encrypted Comparisons and Updates

When a new bid comes in, the contract uses encrypted operations to update the highest bid:
euint64 emount = FHE.asEuint64(amount);           // Encrypt the bid
ebool isHigher = FHE.gt(emount, highestBid);      // Compare encrypted values
highestBid = FHE.max(emount, highestBid);         // Take the maximum
highestBidder = FHE.select(isHigher, newBidder, currentBidder);  // Update bidder

3. Two-Step Decryption Process

The contract demonstrates the asynchronous nature of FHE decryption: Step 1: Request decryption (in a transaction)
FHE.decrypt(highestBid);
FHE.decrypt(highestBidder);
Step 2: Retrieve results (in a later transaction)
(uint64 bidValue, bool bidReady) = FHE.getDecryptResultSafe(highestBid);
(address bidderValue, bool bidderReady) = FHE.getDecryptResultSafe(highestBidder);

Complete Contract Code

Here’s the full implementation of the encrypted auction contract:
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.19 <0.9.0;

import "@fhenixprotocol/cofhe-contracts/FHE.sol";

contract AuctionExample {
    address private auctioneer;
    euint64 private highestBid;
    eaddress private highestBidder;
    uint64 public winningBid;
    address public winningBidder;
    bool public auctionClosed;

    event BidPlaced(address indexed bidder);
    event AuctionClosed();
    event RevealedWinningBid(address winner, uint64 amount);

    modifier onlyAuctioneer() {
        require(
            msg.sender == auctioneer,
            "Only the auctioneer can call this function"
        );
        _;
    }

    constructor() {
        auctioneer = msg.sender; // Set deployer as auctioneer
        auctionClosed = false;
        highestBid = FHE.asEuint64(0);
        highestBidder = FHE.asEaddress(address(0));

        // Preserve ownership for further access
        FHE.allowThis(highestBid);
        FHE.allowThis(highestBidder);
    }

    function bid(uint256 amount) external {
        require(!auctionClosed, "Auction is closed");

        euint64 emount = FHE.asEuint64(amount);
        ebool isHigher = FHE.gt(emount, highestBid);
        highestBid = FHE.max(emount, highestBid);
        highestBidder = FHE.select(
            isHigher,
            FHE.asEaddress(msg.sender), // Encrypt the sender's address
            highestBidder
        );

        // Preserve ownership for further access
        FHE.allowThis(highestBid);
        FHE.allowThis(highestBidder);

        emit BidPlaced(msg.sender);
    }

    // ------------------------------------------------------
    // Step 1. Request on-chain decryption (in transaction)
    // ------------------------------------------------------
    function closeBidding() external onlyAuctioneer {
        require(!auctionClosed, "Auction is already closed");
        FHE.decrypt(highestBid);
        FHE.decrypt(highestBidder);
        auctionClosed = true;

        emit AuctionClosed();
    }

    // ------------------------------------------------------
    // Step 2. Process the decrypted result (Safe Method)
    // ------------------------------------------------------
    function safelyRevealWinner() external onlyAuctioneer {
        require(auctionClosed, "Auction isn't closed");

        (uint64 bidValue, bool bidReady) = FHE.getDecryptResultSafe(highestBid);
        require(bidReady, "Bid not yet decrypted");

        (address bidderValue, bool bidderReady) = FHE.getDecryptResultSafe(highestBidder);
        require(bidderReady, "Bidder not yet decrypted");

        winningBid = bidValue;
        winningBidder = bidderValue;
        emit RevealedWinningBid(bidderValue, bidValue);
    }

    // ------------------------------------------------------
    // Step 2. Process the decrypted result (Unsafe Method)
    // ------------------------------------------------------
    function unsafeRevealWinner() external onlyAuctioneer {
        require(auctionClosed, "Auction isn't closed");

        uint64 bidValue = FHE.getDecryptResult(highestBid);
        address bidderValue = FHE.getDecryptResult(highestBidder);

        winningBid = bidValue;
        winningBidder = bidderValue;
        emit RevealedWinningBid(bidderValue, bidValue);
    }
}

Code Walkthrough

Constructor

The constructor initializes the auction with encrypted zero values:
constructor() {
    auctioneer = msg.sender;
    auctionClosed = false;
    highestBid = FHE.asEuint64(0);
    highestBidder = FHE.asEaddress(address(0));

    // Grant contract access to these encrypted values
    FHE.allowThis(highestBid);
    FHE.allowThis(highestBidder);
}
The FHE.allowThis() calls are crucial - they grant the contract permission to access these encrypted values in future transactions.

Bidding Function

The bid() function handles incoming bids:
function bid(uint256 amount) external {
    require(!auctionClosed, "Auction is closed");

    // 1. Encrypt the bid amount
    euint64 emount = FHE.asEuint64(amount);

    // 2. Check if this bid is higher (encrypted comparison)
    ebool isHigher = FHE.gt(emount, highestBid);

    // 3. Update highest bid using max
    highestBid = FHE.max(emount, highestBid);

    // 4. Update highest bidder using select
    highestBidder = FHE.select(
        isHigher,
        FHE.asEaddress(msg.sender),
        highestBidder
    );

    // 5. Grant contract access to new encrypted values
    FHE.allowThis(highestBid);
    FHE.allowThis(highestBidder);

    emit BidPlaced(msg.sender);
}
Notice how the contract never reveals the current highest bid to bidders. All comparisons and updates happen on encrypted data, maintaining complete confidentiality throughout the bidding process.

Closing the Auction

The auctioneer closes the auction and requests decryption:
function closeBidding() external onlyAuctioneer {
    require(!auctionClosed, "Auction is already closed");

    // Request asynchronous decryption
    FHE.decrypt(highestBid);
    FHE.decrypt(highestBidder);

    auctionClosed = true;
    emit AuctionClosed();
}
This starts the decryption process, but results aren’t immediately available.

Revealing the Winner

The contract provides two methods to reveal the winner:
function safelyRevealWinner() external onlyAuctioneer {
    require(auctionClosed, "Auction isn't closed");

    // Check if bid decryption is ready
    (uint64 bidValue, bool bidReady) = FHE.getDecryptResultSafe(highestBid);
    require(bidReady, "Bid not yet decrypted");

    // Check if bidder decryption is ready
    (address bidderValue, bool bidderReady) = FHE.getDecryptResultSafe(highestBidder);
    require(bidderReady, "Bidder not yet decrypted");

    winningBid = bidValue;
    winningBidder = bidderValue;
    emit RevealedWinningBid(bidderValue, bidValue);
}
The unsafe method will revert the entire transaction if decryption results aren’t ready. The safe method allows you to handle this gracefully with a custom error message.

Usage Flow

1. Deploy the Contract

const auction = await AuctionExample.deploy();
await auction.waitForDeployment();

2. Place Bids

await auction.connect(bidder1).bid(1000);
await auction.connect(bidder2).bid(1500);
await auction.connect(bidder3).bid(1200);

3. Close the Auction

await auction.connect(auctioneer).closeBidding();

4. Wait for Decryption

Wait a few blocks for the decryption to complete (timing varies by network).

5. Reveal the Winner

// Using the safe method
await auction.connect(auctioneer).safelyRevealWinner();

// Check the results
const winner = await auction.winningBidder();
const amount = await auction.winningBid();
console.log(`Winner: ${winner}, Bid: ${amount}`);

Key Takeaways

Privacy Throughout

Bids remain completely encrypted during the auction. No one can see the current highest bid or react to other bids.

Encrypted Comparisons

The contract uses FHE.gt(), FHE.max(), and FHE.select() to update the highest bid without decrypting values.

Access Control

Every encrypted value created must have permissions granted via FHE.allowThis() for the contract to access it later.

Asynchronous Decryption

Decryption is a two-step process: request it in one transaction, retrieve results in another after processing completes.