Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding Blind Auctions contract to the dApps #16

Merged
merged 3 commits into from
Dec 22, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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