Sealed-bid auction

This contract is an example of a confidential sealed-bid auction built with FHEVM. Refer to the Tutorial to learn how it is implemented step by step.

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.

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

import { FHE, externalEuint64, euint64, eaddress, ebool } from "@fhevm/solidity/lib/FHE.sol";
import { SepoliaConfig } from "@fhevm/solidity/config/ZamaConfig.sol";
import { Ownable2Step, Ownable } from "@openzeppelin/contracts/access/Ownable2Step.sol";
import { IERC20Errors } from "@openzeppelin/contracts/interfaces/draft-IERC6093.sol";
import { IERC721 } from "@openzeppelin/contracts/token/ERC721/IERC721.sol";
import { ReentrancyGuard } from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";

import { ConfidentialERC20 } from "./ConfidentialERC20.sol";

contract BlindAuction is SepoliaConfig, ReentrancyGuard {
  /// @notice The recipient of the highest bid once the auction ends
  address public beneficiary;

  /// @notice Confidenctial Payment Token
  ConfidentialERC20 public confidentialERC20;

  /// @notice Token for the auction
  IERC721 public nftContract;
  uint256 public tokenId;

  /// @notice Auction duration
  uint256 public auctionStartTime;
  uint256 public auctionEndTime;

  /// @notice Encrypted auction info
  euint64 private highestBid;
  eaddress private winningAddress;

  /// @notice Winner address defined at the end of the auction
  address public winnerAddress;

  /// @notice Indicate if the NFT of the auction has been claimed
  bool public isNftClaimed;

  /// @notice Request ID used for decryption
  uint256 internal _decryptionRequestId;

  /// @notice Mapping from bidder to their bid value
  mapping(address account => euint64 bidAmount) private bids;

  // ========== Errors ==========

  /// @notice Error thrown when a function is called too early
  /// @dev Includes the time when the function can be called
  error TooEarlyError(uint256 time);

  /// @notice Error thrown when a function is called too late
  /// @dev Includes the time after which the function cannot be called
  error TooLateError(uint256 time);

  /// @notice Thrown when attempting an action that requires the winner to be resolved
  /// @dev Indicates the winner has not yet been decrypted
  error WinnerNotYetRevealed();

  // ========== Modifiers ==========

  /// @notice Modifier to ensure function is called before auction ends.
  /// @dev Reverts if called after the auction end time.
  modifier onlyDuringAuction() {
    if (block.timestamp < auctionStartTime) revert TooEarlyError(auctionStartTime);
    if (block.timestamp >= auctionEndTime) revert TooLateError(auctionEndTime);
    _;
  }

  /// @notice Modifier to ensure function is called after auction ends.
  /// @dev Reverts if called before the auction end time.
  modifier onlyAfterEnd() {
    if (block.timestamp < auctionEndTime) revert TooEarlyError(auctionEndTime);
    _;
  }

  /// @notice Modifier to ensure function is called when the winner is revealed.
  /// @dev Reverts if called before the winner is revealed.
  modifier onlyAfterWinnerRevealed() {
    if (winnerAddress == address(0)) revert WinnerNotYetRevealed();
    _;
  }

  // ========== Views ==========

  function getEncryptedBid(address account) external view returns (euint64) {
    return bids[account];
  }

  /// @notice Get the winning address when the auction is ended
  /// @dev Can only be called after the winning address has been decrypted
  /// @return winnerAddress The decrypted winning address
  function getWinnerAddress() external view returns (address) {
    require(winnerAddress != address(0), "Winning address has not been decided yet");
    return winnerAddress;
  }

  constructor(
    address _nftContractAddress,
    address _confidentialERC20Address,
    uint256 _tokenId,
    uint256 _auctionStartTime,
    uint256 _auctionEndTime
  ) {
    beneficiary = msg.sender;
    confidentialERC20 = ConfidentialERC20(_confidentialERC20Address);
    nftContract = IERC721(_nftContractAddress);

    // Transfer the NFT to the contract for the auction
    nftContract.safeTransferFrom(msg.sender, address(this), _tokenId);

    require(_auctionStartTime < _auctionEndTime, "INVALID_TIME");
    auctionStartTime = _auctionStartTime;
    auctionEndTime = _auctionEndTime;
  }

  function bid(externalEuint64 encryptedAmount, bytes calldata inputProof) public onlyDuringAuction nonReentrant {
    // Get and verify the amount from the user
    euint64 amount = FHE.fromExternal(encryptedAmount, inputProof);

    // Transfer the confidential token as payment
    euint64 balanceBefore = confidentialERC20.balanceOf(address(this));
    FHE.allowTransient(amount, address(confidentialERC20));
    confidentialERC20.transferFrom(msg.sender, address(this), amount);
    euint64 balanceAfter = confidentialERC20.balanceOf(address(this));
    euint64 sentBalance = FHE.sub(balanceAfter, balanceBefore);

    // Need to update the bid balance
    euint64 previousBid = bids[msg.sender];
    if (FHE.isInitialized(previousBid)) {
      // The user increase his bid
      euint64 newBid = FHE.add(previousBid, sentBalance);
      bids[msg.sender] = newBid;
    } else {
      // First bid for the user
      bids[msg.sender] = sentBalance;
    }

    // Compare the total value of the user from the highest bid
    euint64 currentBid = bids[msg.sender];
    FHE.allowThis(currentBid);
    FHE.allow(currentBid, msg.sender);

    if (FHE.isInitialized(highestBid)) {
      ebool isNewWinner = FHE.lt(highestBid, currentBid);
      highestBid = FHE.select(isNewWinner, currentBid, highestBid);
      winningAddress = FHE.select(isNewWinner, FHE.asEaddress(msg.sender), winningAddress);
    } else {
      highestBid = currentBid;
      winningAddress = FHE.asEaddress(msg.sender);
    }
    FHE.allowThis(highestBid);
    FHE.allowThis(winningAddress);
  }

  /// @notice Initiate the decryption of the winning address
  /// @dev Can only be called after the auction ends
  function decryptWinningAddress() public onlyAfterEnd {
    bytes32[] memory cts = new bytes32[](1);
    cts[0] = FHE.toBytes32(winningAddress);
    _decryptionRequestId = FHE.requestDecryption(cts, this.resolveAuctionCallback.selector);
  }

  /// @notice Claim the NFT prize.
  /// @dev Only the winner can call this function when the auction is ended.
  function winnerClaimPrize() public onlyAfterWinnerRevealed {
    require(winnerAddress == msg.sender, "Only winner can claim item");
    require(!isNftClaimed, "NFT has already been claimed");
    isNftClaimed = true;

    // Reset bid value
    bids[msg.sender] = FHE.asEuint64(0);
    FHE.allowThis(bids[msg.sender]);
    FHE.allow(bids[msg.sender], msg.sender);

    // Transfer the highest bid to the beneficiary
    FHE.allowTransient(highestBid, address(confidentialERC20));
    confidentialERC20.transfer(beneficiary, highestBid);

    // Send the NFT to the winner
    nftContract.safeTransferFrom(address(this), msg.sender, tokenId);
  }

  /// @notice Withdraw a bid from the auction
  /// @dev Can only be called after the auction ends and by non-winning bidders
  function withdraw(address bidder) public onlyAfterWinnerRevealed {
    if (bidder == winnerAddress) revert TooLateError(auctionEndTime);

    // Get the user bid value
    euint64 amount = bids[bidder];
    FHE.allowTransient(amount, address(confidentialERC20));

    // Reset user bid value
    euint64 newBid = FHE.asEuint64(0);
    bids[bidder] = newBid;
    FHE.allowThis(newBid);
    FHE.allow(newBid, bidder);

    // Refund the user with his bid amount
    confidentialERC20.transfer(bidder, amount);
  }

  // ========== Oracle Callback ==========

  /// @notice Callback function to set the decrypted winning address
  /// @dev Can only be called by the Gateway
  /// @param requestId Request Id created by the Oracle.
  /// @param resultWinnerAddress The decrypted winning address.
  /// @param signatures Signature to verify the decryption data.
  function resolveAuctionCallback(uint256 requestId, address resultWinnerAddress, bytes[] memory signatures) public {
    require(requestId == _decryptionRequestId, "Invalid requestId");
    FHE.checkSignatures(requestId, signatures);

    winnerAddress = resultWinnerAddress;
  }
}

Last updated