Blind Auction
// SPDX-License-Identifier: BSD-3-Clause-Clear
pragma solidity ^0.8.24;
import "fhevm/lib/TFHE.sol";
import "fhevm/gateway/GatewayCaller.sol";
import "@openzeppelin/contracts/access/Ownable2Step.sol";
import "./ConfidentialERC20.sol";
/// @notice Main contract for the blind auction
contract BlindAuction is Ownable2Step, GatewayCaller {
/// @notice Auction end time
uint256 public endTime;
/// @notice Address of the beneficiary
address public beneficiary;
/// @notice Current highest bid
euint64 private highestBid;
/// @notice Ticket corresponding to the highest bid
/// @dev Used during reencryption to know if a user has won the bid
euint64 private winningTicket;
/// @notice Decryption of winningTicket
/// @dev Can be requested by anyone after auction ends
uint64 private decryptedWinningTicket;
/// @notice Ticket randomly sampled for each user
/// @dev WARNING: We assume probability of duplicated tickets is null
/// @dev An improved implementation could sample 4 random euint64 tickets per user for negligible collision probability
mapping(address account => euint64 ticket) private userTickets;
/// @notice Mapping from bidder to their bid value
mapping(address account => euint64 bidAmount) private bids;
/// @notice Number of bids
uint256 public bidCounter;
/// @notice The token contract used for encrypted bids
ConfidentialERC20 public tokenContract;
/// @notice Flag indicating whether the auction object has been claimed
/// @dev WARNING : If there is a draw, only the first highest bidder will get the prize
/// An improved implementation could handle this case differently
ebool private objectClaimed;
/// @notice Flag to check if the token has been transferred to the beneficiary
bool public tokenTransferred;
/// @notice Flag to determine if the auction can be stopped manually
bool public stoppable;
/// @notice Flag to check if the auction has been manually stopped
bool public manuallyStopped = false;
/// @notice Error thrown when a function is called too early
/// @dev Includes the time when the function can be called
error TooEarly(uint256 time);
/// @notice Error thrown when a function is called too late
/// @dev Includes the time after which the function cannot be called
error TooLate(uint256 time);
/// @notice Constructor to initialize the auction
/// @param _beneficiary Address of the beneficiary who will receive the highest bid
/// @param _tokenContract Address of the EncryptedERC20 token contract used for bidding
/// @param biddingTime Duration of the auction in seconds
/// @param isStoppable Flag to determine if the auction can be stopped manually
constructor(
address _beneficiary,
ConfidentialERC20 _tokenContract,
uint256 biddingTime,
bool isStoppable
) Ownable(msg.sender) {
beneficiary = _beneficiary;
tokenContract = _tokenContract;
endTime = block.timestamp + biddingTime;
objectClaimed = TFHE.asEbool(false);
TFHE.allow(objectClaimed,address(this));
tokenTransferred = false;
bidCounter = 0;
stoppable = isStoppable;
}
/// @notice Submit a bid with an encrypted value
/// @dev Transfers tokens from the bidder to the contract
/// @param encryptedValue The encrypted bid amount
/// @param inputProof Proof for the encrypted input
function bid(einput encryptedValue, bytes calldata inputProof) external onlyBeforeEnd {
euint64 value = TFHE.asEuint64(encryptedValue, inputProof);
euint64 sentBalance;
if (TFHE.isInitialized(bids[msg.sender])) {
euint64 existingBid = bids[msg.sender];
euint64 balanceBefore = tokenContract.balanceOf(address(this));
ebool isHigher = TFHE.lt(existingBid, value);
euint64 toTransfer = TFHE.sub(value, existingBid);
// Transfer only if bid is higher, also to avoid overflow from previous line
euint64 amount = TFHE.select(isHigher, toTransfer, TFHE.asEuint64(0));
TFHE.allowTransient(amount, address(tokenContract));
tokenContract.transferFrom(msg.sender, address(this), amount);
euint64 balanceAfter = tokenContract.balanceOf(address(this));
sentBalance = TFHE.sub(balanceAfter, balanceBefore);
euint64 newBid = TFHE.add(existingBid, sentBalance);
bids[msg.sender] = newBid;
} else {
bidCounter++;
euint64 balanceBefore = tokenContract.balanceOf(address(this));
TFHE.allowTransient(value, address(tokenContract));
tokenContract.transferFrom(msg.sender, address(this), value);
euint64 balanceAfter = tokenContract.balanceOf(address(this));
sentBalance = TFHE.sub(balanceAfter, balanceBefore);
bids[msg.sender] = sentBalance;
}
euint64 currentBid = bids[msg.sender];
TFHE.allow(currentBid,address(this));
TFHE.allow(currentBid, msg.sender);
euint64 randTicket = TFHE.randEuint64();
euint64 userTicket;
if (TFHE.isInitialized(highestBid)) {
userTicket = TFHE.select(TFHE.ne(sentBalance, 0), randTicket, userTickets[msg.sender]); // don't update ticket if sentBalance is null (or else winner sending an additional zero bid would lose the prize)
} else {
userTicket = randTicket;
}
userTickets[msg.sender] = userTicket;
if (!TFHE.isInitialized(highestBid)) {
highestBid = currentBid;
winningTicket = userTicket;
} else {
ebool isNewWinner = TFHE.lt(highestBid, currentBid);
highestBid = TFHE.select(isNewWinner, currentBid, highestBid);
winningTicket = TFHE.select(isNewWinner, userTicket, winningTicket);
}
TFHE.allow(highestBid,address(this));
TFHE.allow(winningTicket,address(this));
TFHE.allow(userTicket, msg.sender);
}
/// @notice Get the encrypted bid of a specific account
/// @dev Can be used in a reencryption request
/// @param account The address of the bidder
/// @return The encrypted bid amount
function getBid(address account) external view returns (euint64) {
return bids[account];
}
/// @notice Manually stop the auction
/// @dev Can only be called by the owner and if the auction is stoppable
function stop() external onlyOwner {
require(stoppable);
manuallyStopped = true;
}
/// @notice Get the encrypted ticket of a specific account
/// @dev Can be used in a reencryption request
/// @param account The address of the bidder
/// @return The encrypted ticket
function ticketUser(address account) external view returns (euint64) {
return userTickets[account];
}
/// @notice Initiate the decryption of the winning ticket
/// @dev Can only be called after the auction ends
function decryptWinningTicket() public onlyAfterEnd {
uint256[] memory cts = new uint256[](1);
cts[0] = Gateway.toUint256(winningTicket);
Gateway.requestDecryption(cts, this.setDecryptedWinningTicket.selector, 0, block.timestamp + 100, false);
}
/// @notice Callback function to set the decrypted winning ticket
/// @dev Can only be called by the Gateway
/// @param resultDecryption The decrypted winning ticket
function setDecryptedWinningTicket(uint256, uint64 resultDecryption) public onlyGateway {
decryptedWinningTicket = resultDecryption;
}
/// @notice Get the decrypted winning ticket
/// @dev Can only be called after the winning ticket has been decrypted - if `userTickets[account]` is an encryption of decryptedWinningTicket, then `account` won and can call `claim` succesfully
/// @return The decrypted winning ticket
function getDecryptedWinningTicket() external view returns (uint64) {
require(decryptedWinningTicket != 0, "Winning ticket has not been decrypted yet");
return decryptedWinningTicket;
}
/// @notice Claim the auction object
/// @dev Succeeds only if the caller was the first to get the highest bid
function claim() public onlyAfterEnd {
ebool canClaim = TFHE.and(TFHE.eq(winningTicket, userTickets[msg.sender]), TFHE.not(objectClaimed));
objectClaimed = TFHE.or(canClaim, objectClaimed);
TFHE.allow(objectClaimed,address(this));
euint64 newBid = TFHE.select(canClaim, TFHE.asEuint64(0), bids[msg.sender]);
bids[msg.sender] = newBid;
TFHE.allow(bids[msg.sender],address(this));
TFHE.allow(bids[msg.sender], msg.sender);
}
/// @notice Transfer the highest bid to the beneficiary
/// @dev Can only be called once after the auction ends
function auctionEnd() public onlyAfterEnd {
require(!tokenTransferred);
tokenTransferred = true;
TFHE.allowTransient(highestBid, address(tokenContract));
tokenContract.transfer(beneficiary, highestBid);
}
/// @notice Withdraw a bid from the auction
/// @dev Can only be called after the auction ends and by non-winning bidders
function withdraw() public onlyAfterEnd {
euint64 bidValue = bids[msg.sender];
ebool canWithdraw = TFHE.ne(winningTicket, userTickets[msg.sender]);
euint64 amount = TFHE.select(canWithdraw, bidValue, TFHE.asEuint64(0));
TFHE.allowTransient(amount, address(tokenContract));
tokenContract.transfer(msg.sender, amount);
euint64 newBid = TFHE.select(canWithdraw, TFHE.asEuint64(0), bids[msg.sender]);
bids[msg.sender] = newBid;
TFHE.allow(newBid,address(this));
TFHE.allow(newBid, msg.sender);
}
/// @notice Modifier to ensure function is called before auction ends
/// @dev Reverts if called after the auction end time or if manually stopped
modifier onlyBeforeEnd() {
if (block.timestamp >= endTime || manuallyStopped == true) revert TooLate(endTime);
_;
}
/// @notice Modifier to ensure function is called after auction ends
/// @dev Reverts if called before the auction end time and not manually stopped
modifier onlyAfterEnd() {
if (block.timestamp < endTime && manuallyStopped == false) revert TooEarly(endTime);
_;
}
}
You can explore the Hardhat contract for BlindAuction at the following link: BlindAuction Contract.
Last updated