Skip to content

Commit

Permalink
feat: added Blind Auctions contract to the dApps with tests (#16)
Browse files Browse the repository at this point in the history
* blind auction tests work with one error

* feat: fix missing edge case in select and use euint256 for tickets (#17)

* remove one comment

---------

Co-authored-by: jat <[email protected]>
  • Loading branch information
poppyseedDev and jatZama authored Dec 22, 2024
1 parent dda4a16 commit 1458d81
Show file tree
Hide file tree
Showing 6 changed files with 490 additions and 2 deletions.
244 changes: 244 additions & 0 deletions hardhat/contracts/auctions/BlindAuction.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,244 @@
// SPDX-License-Identifier: BSD-3-Clause-Clear

pragma solidity ^0.8.24;

import "fhevm/lib/TFHE.sol";
import { ConfidentialERC20 } from "fhevm-contracts/contracts/token/ERC20/ConfidentialERC20.sol";
import "@openzeppelin/contracts/access/Ownable2Step.sol";
import "fhevm/config/ZamaFHEVMConfig.sol";
import "fhevm/config/ZamaGatewayConfig.sol";
import "fhevm/gateway/GatewayCaller.sol";

/// @notice Main contract for the blind auction
contract BlindAuction is SepoliaZamaFHEVMConfig, SepoliaZamaGatewayConfig, GatewayCaller, Ownable2Step {
/// @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
euint256 private winningTicket;

/// @notice Decryption of winningTicket
/// @dev Can be requested by anyone after auction ends
uint256 private decryptedWinningTicket;

/// @notice Ticket randomly sampled for each user
mapping(address account => euint256 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 ConfidentialERC20 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) {
// TFHE.setFHEVM(FHEVMConfig.defaultConfig());
// Gateway.setGateway(GatewayConfig.defaultGatewayContract());
beneficiary = _beneficiary;
tokenContract = _tokenContract;
endTime = block.timestamp + biddingTime;
objectClaimed = TFHE.asEbool(false);
TFHE.allowThis(objectClaimed);
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 existingBid = bids[msg.sender];
euint64 sentBalance;
if (TFHE.isInitialized(existingBid)) {
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.allowThis(currentBid);
TFHE.allow(currentBid, msg.sender);

euint256 randTicket = TFHE.randEuint256();
euint256 userTicket;
if (TFHE.isInitialized(highestBid)) {
if (TFHE.isInitialized(userTickets[msg.sender])) {
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 = TFHE.select(TFHE.ne(sentBalance, 0), randTicket, TFHE.asEuint256(0));
}
} 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.allowThis(highestBid);
TFHE.allowThis(winningTicket);
TFHE.allowThis(userTicket);
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 (euint256) {
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, uint256 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 (uint256) {
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.allowThis(objectClaimed);
euint64 newBid = TFHE.select(canClaim, TFHE.asEuint64(0), bids[msg.sender]);
bids[msg.sender] = newBid;
TFHE.allowThis(bids[msg.sender]);
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.allowThis(newBid);
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);
_;
}
}
46 changes: 46 additions & 0 deletions hardhat/contracts/auctions/MyConfidentialERC20.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// SPDX-License-Identifier: BSD-3-Clause-Clear

pragma solidity ^0.8.24;

import "fhevm/lib/TFHE.sol";
import "fhevm/config/ZamaFHEVMConfig.sol";
import "fhevm/config/ZamaGatewayConfig.sol";
import "fhevm/gateway/GatewayCaller.sol";
import "fhevm-contracts/contracts/token/ERC20/extensions/ConfidentialERC20Mintable.sol";

/// @notice This contract implements an encrypted ERC20-like token with confidential balances using Zama's FHE library.
/// @dev It supports typical ERC20 functionality such as transferring tokens, minting, and setting allowances,
/// @dev but uses encrypted data types.
contract BlindAuctionConfidentialERC20 is
SepoliaZamaFHEVMConfig,
SepoliaZamaGatewayConfig,
GatewayCaller,
ConfidentialERC20Mintable
{
// @note `SECRET` is not so secret, since it is trivially encrypted and just to have a decryption test
euint64 internal immutable SECRET;

// @note `revealedSecret` will hold the decrypted result once the Gateway will relay the decryption of `SECRET`
uint64 public revealedSecret;

/// @notice Constructor to initialize the token's name and symbol, and set up the owner
/// @param name_ The name of the token
/// @param symbol_ The symbol of the token
constructor(string memory name_, string memory symbol_) ConfidentialERC20Mintable(name_, symbol_, msg.sender) {
SECRET = TFHE.asEuint64(42);
TFHE.allowThis(SECRET);
}

/// @notice Request decryption of `SECRET`
function requestSecret() public {
uint256[] memory cts = new uint256[](1);
cts[0] = Gateway.toUint256(SECRET);
Gateway.requestDecryption(cts, this.callbackSecret.selector, 0, block.timestamp + 100, false);
}

/// @notice Callback function for `SECRET` decryption
/// @param `decryptedValue` The decrypted 64-bit unsigned integer
function callbackSecret(uint256, uint64 decryptedValue) public onlyGateway {
revealedSecret = decryptedValue;
}
}
18 changes: 18 additions & 0 deletions hardhat/test/blindAuction/BlindAuction.fixture.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { AddressLike, BigNumberish, Signer } from "ethers";
import { ethers } from "hardhat";

import type { BlindAuction } from "../../types";

export async function deployBlindAuctionFixture(
account: Signer,
tokenContract: AddressLike,
biddingTime: BigNumberish,
isStoppable: boolean,
): Promise<BlindAuction> {
const contractFactory = await ethers.getContractFactory("BlindAuction");
const contract = await contractFactory
.connect(account)
.deploy(account.getAddress(), tokenContract, biddingTime, isStoppable);
await contract.waitForDeployment();
return contract;
}
Loading

0 comments on commit 1458d81

Please sign in to comment.