Skip to content

Commit

Permalink
Merge pull request #417 from zama-ai/test/decrypt
Browse files Browse the repository at this point in the history
feat: updated async decrypt and ex contracts
  • Loading branch information
jatZama authored Jun 27, 2024
2 parents f7d1f47 + a2c4049 commit 6f97c98
Show file tree
Hide file tree
Showing 8 changed files with 251 additions and 96 deletions.
165 changes: 99 additions & 66 deletions examples/BlindAuction.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,29 +3,37 @@
pragma solidity ^0.8.25;

import "../lib/TFHE.sol";

import "../abstracts/Reencrypt.sol";

import "./EncryptedERC20.sol";
import "@openzeppelin/contracts/access/Ownable2Step.sol";
import "../gateway/GatewayCaller.sol";

contract BlindAuction is Reencrypt {
uint public endTime;
contract BlindAuction is Ownable2Step, GatewayCaller {
uint256 public endTime;

address public beneficiary;

// Current highest bid.
euint64 internal highestBid;
euint64 private highestBid;

// ticket corresponding to the highest bid, used during reencryption to know if a user has won the bid
euint64 private winningTicket;
uint64 private decryptedWinningTicket; // decryption of winningTicket, can be requested by anyone after auction ends

// ticket randomly sampled for each user
// WARNING : we assume probability of duplicated tickets is null (an improved implementation could simply sample 4 random euint64 tickets per user for negligible collision probability)
mapping(address account => euint64 ticket) private userTickets;

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

// Number of bid
uint public bidCounter;
uint256 public bidCounter;

// The token contract used for encrypted bids.
EncryptedERC20 public tokenContract;

// Whether the auction object has been claimed.
// WARNING : if there is a draw, only first highest bidder will get the prize (an improved implementation could handle this case differently)
ebool private objectClaimed;

// If the token has been transferred to the beneficiary
Expand All @@ -35,115 +43,145 @@ contract BlindAuction is Reencrypt {

bool public manuallyStopped = false;

// The owner of the contract.
address public contractOwner;

// The function has been called too early.
// Try again at `time`.
error TooEarly(uint time);
error TooEarly(uint256 time);
// The function has been called too late.
// It cannot be called after `time`.
error TooLate(uint time);

constructor(address _beneficiary, EncryptedERC20 _tokenContract, uint biddingTime, bool isStoppable) {
error TooLate(uint256 time);

constructor(
address _beneficiary,
EncryptedERC20 _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;
contractOwner = msg.sender;
}

// Bid an `encryptedValue`.
function bid(einput encryptedValue, bytes calldata inputProof) public onlyBeforeEnd {
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.balanceOfMe();
euint64 balanceBefore = tokenContract.balanceOf(address(this));
ebool isHigher = TFHE.lt(existingBid, value);
// Update bid with value
bids[msg.sender] = TFHE.select(isHigher, value, existingBid);
// Transfer only the difference between existing and value
euint64 toTransfer = TFHE.sub(value, existingBid);
// Transfer only if bid is higher

// 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.balanceOfMe();
euint64 sentBalance = TFHE.sub(balanceAfter, balanceBefore);
euint64 balanceAfter = tokenContract.balanceOf(address(this));
sentBalance = TFHE.sub(balanceAfter, balanceBefore);
euint64 newBid = TFHE.add(existingBid, sentBalance);
// Update bid with value
bids[msg.sender] = newBid;
} else {
bidCounter++;
euint64 balanceBefore = tokenContract.balanceOfMe();
euint64 balanceBefore = tokenContract.balanceOf(address(this));
TFHE.allowTransient(value, address(tokenContract));
tokenContract.transferFrom(msg.sender, address(this), value);
euint64 balanceAfter = tokenContract.balanceOfMe();
euint64 sentBalance = TFHE.sub(balanceAfter, balanceBefore);
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 {
highestBid = TFHE.select(TFHE.lt(highestBid, currentBid), currentBid, highestBid);
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);
}

// function getBid(
// bytes32 publicKey,
// bytes calldata signature
// ) public view onlySignedPublicKey(publicKey, signature) returns (bytes memory) {
// return TFHE.reencrypt(bids[msg.sender], publicKey);
// }
// Returns the `account`'s encrypted bid, can be used in a reencryption request
function getBid(address account) external view returns (euint64) {
return bids[account];
}

// Returns the user bid
function stop() public onlyContractOwner {
function stop() external onlyOwner {
require(stoppable);
manuallyStopped = true;
}

// Returns an encrypted boolean under the caller's public key, indicating
// if the caller has the highest bid.
function doIHaveHighestBid(
bytes32 publicKey,
bytes calldata signature
) public view onlyAfterEnd onlySignedPublicKey(publicKey, signature) returns (bytes memory) {
// TODO
revert();
// if (TFHE.isInitialized(highestBid) && TFHE.isInitialized(bids[msg.sender])) {
// return TFHE.reencrypt(TFHE.le(highestBid, bids[msg.sender]), publicKey);
// } else {
// return TFHE.reencrypt(TFHE.asEuint64(0), publicKey);
// }
// Returns the `account`'s encrypted ticket, can be used in a reencryption request, then compared to
// `decryptedWinningTicket` when auction ends, so the user could learn if he won the auction
function ticketUser(address account) external view returns (euint64) {
return userTickets[account];
}

// Claim the object. Succeeds only if the caller has the highest bid.
// WARNING : if there is a draw, only first highest bidder to claim will get the prize (an improved implementation could handle this case differently)
function claim() public onlyAfterEnd {
ebool canClaim = TFHE.and(TFHE.le(highestBid, bids[msg.sender]), TFHE.not(objectClaimed));
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);
}

function setDecryptedWinningTicket(uint256, uint64 resultDecryption) public onlyGateway {
decryptedWinningTicket = resultDecryption;
}

// if `userTickets[account]` is an encryption of decryptedWinningTicket, then `account` won and can call `claim` succesfully
function getDecryptedWinningTicket() external view returns (uint64) {
require(decryptedWinningTicket != 0, "Winning ticket has not been decrypted yet");
return decryptedWinningTicket;
}

// Claim the object. 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);
bids[msg.sender] = TFHE.select(canClaim, TFHE.asEuint64(0), bids[msg.sender]);
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);
}

// Transfer token to beneficiary
function auctionEnd() public onlyAfterEnd {
require(!tokenTransferred);

tokenTransferred = true;
TFHE.allowTransient(highestBid, address(tokenContract));
tokenContract.transfer(beneficiary, highestBid);
}

// Withdraw a bid from the auction to the caller once the auction has stopped.
function withdraw() public onlyAfterEnd {
euint64 bidValue = bids[msg.sender];
ebool isHighestBid = TFHE.eq(bidValue, highestBid);
ebool canWithdraw = TFHE.not(TFHE.and(isHighestBid, TFHE.not(objectClaimed)));
tokenContract.transfer(msg.sender, TFHE.select(canWithdraw, bidValue, TFHE.asEuint64(0)));
bids[msg.sender] = TFHE.select(canWithdraw, TFHE.asEuint64(0), 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);
}

modifier onlyBeforeEnd() {
Expand All @@ -152,12 +190,7 @@ contract BlindAuction is Reencrypt {
}

modifier onlyAfterEnd() {
if (block.timestamp <= endTime && manuallyStopped == false) revert TooEarly(endTime);
_;
}

modifier onlyContractOwner() {
require(msg.sender == contractOwner);
if (block.timestamp < endTime && manuallyStopped == false) revert TooEarly(endTime);
_;
}
}
33 changes: 19 additions & 14 deletions examples/EncryptedERC20.sol
Original file line number Diff line number Diff line change
Expand Up @@ -51,28 +51,25 @@ contract EncryptedERC20 is Ownable2Step {
}

// Transfers an encrypted amount from the message sender address to the `to` address.
function transfer(address to, einput amount, bytes calldata inputProof) public virtual {
transfer(to, TFHE.asEuint64(amount, inputProof));
function transfer(address to, einput encryptedAmount, bytes calldata inputProof) public virtual returns (bool) {
transfer(to, TFHE.asEuint64(encryptedAmount, inputProof));
return true;
}

// Transfers an amount from the message sender address to the `to` address.
function transfer(address to, euint64 amount) public virtual {
function transfer(address to, euint64 amount) public virtual returns (bool) {
require(TFHE.isSenderAllowed(amount));
// makes sure the owner has enough tokens
ebool canTransfer = TFHE.le(amount, balances[msg.sender]);
_transfer(msg.sender, to, amount, canTransfer);
return true;
}

// Returns the balance of the given address.
// Returns the balance handle of the caller.
function balanceOf(address wallet) public view virtual returns (euint64) {
return balances[wallet];
}

// Returns the balance of the caller.
function balanceOfMe() public view virtual returns (euint64) {
return balances[msg.sender];
}

// Sets the `encryptedAmount` as the allowance of `spender` over the caller's tokens.
function approve(address spender, einput encryptedAmount, bytes calldata inputProof) public virtual returns (bool) {
approve(spender, TFHE.asEuint64(encryptedAmount, inputProof));
Expand All @@ -89,22 +86,29 @@ contract EncryptedERC20 is Ownable2Step {
}

// Returns the remaining number of tokens that `spender` is allowed to spend
// on behalf of the caller. The returned ciphertext is under the caller public FHE key.
// on behalf of the caller.
function allowance(address owner, address spender) public view virtual returns (euint64) {
return _allowance(owner, spender);
}

// Transfers `encryptedAmount` tokens using the caller's allowance.
function transferFrom(address from, address to, einput encryptedAmount, bytes calldata inputProof) public virtual {
function transferFrom(
address from,
address to,
einput encryptedAmount,
bytes calldata inputProof
) public virtual returns (bool) {
transferFrom(from, to, TFHE.asEuint64(encryptedAmount, inputProof));
return true;
}

// Transfers `amount` tokens using the caller's allowance.
function transferFrom(address from, address to, euint64 amount) public virtual {
function transferFrom(address from, address to, euint64 amount) public virtual returns (bool) {
require(TFHE.isSenderAllowed(amount));
address spender = msg.sender;
ebool isTransferable = _updateAllowance(from, spender, amount);
_transfer(from, to, amount, isTransferable);
return true;
}

function _approve(address owner, address spender, euint64 amount) internal virtual {
Expand Down Expand Up @@ -132,11 +136,12 @@ contract EncryptedERC20 is Ownable2Step {
// Transfers an encrypted amount.
function _transfer(address from, address to, euint64 amount, ebool isTransferable) internal virtual {
// Add to the balance of `to` and subract from the balance of `from`.
euint64 newBalanceTo = TFHE.add(balances[to], TFHE.select(isTransferable, amount, TFHE.asEuint64(0)));
euint64 transferValue = TFHE.select(isTransferable, amount, TFHE.asEuint64(0));
euint64 newBalanceTo = TFHE.add(balances[to], transferValue);
balances[to] = newBalanceTo;
TFHE.allow(newBalanceTo, address(this));
TFHE.allow(newBalanceTo, to);
euint64 newBalanceFrom = TFHE.sub(balances[from], TFHE.select(isTransferable, amount, TFHE.asEuint64(0)));
euint64 newBalanceFrom = TFHE.sub(balances[from], transferValue);
balances[from] = newBalanceFrom;
TFHE.allow(newBalanceFrom, address(this));
TFHE.allow(newBalanceFrom, from);
Expand Down
Loading

0 comments on commit 6f97c98

Please sign in to comment.