-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: added Blind Auctions contract to the dApps with tests (#16)
* 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
1 parent
dda4a16
commit 1458d81
Showing
6 changed files
with
490 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
_; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
Oops, something went wrong.