diff --git a/contracts/ConfidentialERC20/ConfidentialERC20.sol b/contracts/ConfidentialERC20/ConfidentialERC20.sol index d654a11..400345d 100644 --- a/contracts/ConfidentialERC20/ConfidentialERC20.sol +++ b/contracts/ConfidentialERC20/ConfidentialERC20.sol @@ -30,6 +30,10 @@ import "fhevm/gateway/GatewayCaller.sol"; abstract contract ConfidentialERC20 is Ownable, IConfidentialERC20, IERC20Metadata, IERC20Errors, GatewayCaller { mapping(address account => euint64) public _balances; + // To safely handle burn requests we must ensure we lock up an amount of token that is in-flight so that the + // we can guarantee there will be sufficient balance to be burnt at the point that _burnCallback runs. + mapping(address account => uint64) public _lockedBalances; + mapping(address account => mapping(address spender => euint64)) internal _allowances; uint64 public _totalSupply; @@ -50,7 +54,9 @@ abstract contract ConfidentialERC20 is Ownable, IConfidentialERC20, IERC20Metada struct BurnRq { address account; uint64 amount; + bool exists; } + mapping(uint256 => BurnRq) public burnRqs; /** * @dev Returns the name of the token. @@ -109,8 +115,7 @@ abstract contract ConfidentialERC20 is Ownable, IConfidentialERC20, IERC20Metada function transfer(address to, euint64 value) public virtual returns (bool) { require(TFHE.isSenderAllowed(value)); address owner = _msgSender(); - ebool isTransferable = TFHE.le(value, _balances[msg.sender]); - _transfer(owner, to, value, isTransferable); + _transfer(owner, to, value, TFHE.asEbool(true)); return true; } @@ -142,6 +147,7 @@ abstract contract ConfidentialERC20 is Ownable, IConfidentialERC20, IERC20Metada _approve(owner, spender, value); return true; } + function approve(address spender, einput encryptedAmount, bytes calldata inputProof) public virtual returns (bool) { approve(spender, TFHE.asEuint64(encryptedAmount, inputProof)); return true; @@ -178,6 +184,17 @@ abstract contract ConfidentialERC20 is Ownable, IConfidentialERC20, IERC20Metada return true; } + /** + * @dev Checks the available balance of an account exceeds amount + */ + function _hasSufficientBalance(address owner, euint64 amount) internal virtual returns (ebool) { + return TFHE.le(TFHE.add(amount, _lockedBalances[owner]), _balances[owner]); + } + + function _hasSufficientBalance(address owner, uint64 amount) internal virtual returns (ebool) { + return _hasSufficientBalance(owner, TFHE.asEuint64(amount)); + } + /** * @dev Moves a `value` amount of tokens from `from` to `to`. * @@ -195,7 +212,12 @@ abstract contract ConfidentialERC20 is Ownable, IConfidentialERC20, IERC20Metada if (to == address(0)) { revert ERC20InvalidReceiver(address(0)); } - euint64 transferValue = TFHE.select(isTransferable, value, TFHE.asEuint64(0)); + // Enforce sufficient balance constraint universally + euint64 transferValue = TFHE.select( + TFHE.and(_hasSufficientBalance(from, value), isTransferable), + value, + TFHE.asEuint64(0) + ); euint64 newBalanceTo = TFHE.add(_balances[to], transferValue); _balances[to] = newBalanceTo; TFHE.allow(newBalanceTo, address(this)); @@ -232,6 +254,25 @@ abstract contract ConfidentialERC20 is Ownable, IConfidentialERC20, IERC20Metada _totalSupply += value; } + event BurnRequested(uint256 requestId, address account, uint64 amount); + + event BurnRequestCancelled(uint256 requestID); + + error BurnRequestDoesNotExist(uint256 requestId); + + function _cancelBurn(uint256 requestId) internal virtual { + BurnRq memory burnRequest = burnRqs[requestId]; + if (!burnRequest.exists) { + revert BurnRequestDoesNotExist(requestId); + } + address account = burnRequest.account; + uint64 amount = burnRequest.amount; + // Unlock the burn amount unconditionally + _lockedBalances[account] = _lockedBalances[account] - amount; + delete burnRqs[requestId]; + emit BurnRequestCancelled(requestId); + } + /** * @dev Destroys a `value` amount of tokens from `account`, lowering the total supply. * Relies on the `_update` mechanism. @@ -244,7 +285,9 @@ abstract contract ConfidentialERC20 is Ownable, IConfidentialERC20, IERC20Metada if (account == address(0)) { revert ERC20InvalidReceiver(address(0)); } - ebool enoughBalance = TFHE.le(amount, _balances[account]); + ebool enoughBalance = _hasSufficientBalance(account, amount); + // Unconditionally lock the burn amount after calculating sufficient balance + _lockedBalances[account] = _lockedBalances[account] + amount; TFHE.allow(enoughBalance, address(this)); uint256[] memory cts = new uint256[](1); cts[0] = Gateway.toUint256(enoughBalance); @@ -256,22 +299,31 @@ abstract contract ConfidentialERC20 is Ownable, IConfidentialERC20, IERC20Metada block.timestamp + 100, false ); - - burnRqs[requestID] = BurnRq(account, amount); + burnRqs[requestID] = BurnRq(account, amount, true); + emit BurnRequested(requestID, account, amount); } - function _burnCallback(uint256 requestID, bool decryptedInput) public virtual onlyGateway { + event InsufficientBalanceToBurn(address account, uint64 burnAmount); + + function _burnCallback(uint256 requestID, bool hasEnoughBalance) public virtual onlyGateway { BurnRq memory burnRequest = burnRqs[requestID]; + if (!burnRequest.exists) { + revert BurnRequestDoesNotExist(requestID); + return; + } address account = burnRequest.account; uint64 amount = burnRequest.amount; - if (!decryptedInput) { - revert("Decryption failed"); + // Unlock the burn amount unconditionally + _lockedBalances[account] = _lockedBalances[account] - amount; + delete burnRqs[requestID]; + if (!hasEnoughBalance) { + emit InsufficientBalanceToBurn(account, amount); + return; } _totalSupply = _totalSupply - amount; _balances[account] = TFHE.sub(_balances[account], amount); TFHE.allow(_balances[account], address(this)); TFHE.allow(_balances[account], account); - delete burnRqs[requestID]; } /** * @dev Sets `value` as the allowance of `spender` over the `owner` s tokens. @@ -334,14 +386,11 @@ abstract contract ConfidentialERC20 is Ownable, IConfidentialERC20, IERC20Metada */ function _decreaseAllowance(address owner, address spender, euint64 amount) internal virtual returns (ebool) { euint64 currentAllowance = _allowances[owner][spender]; - - ebool allowedTransfer = TFHE.le(amount, currentAllowance); - - ebool canTransfer = TFHE.le(amount, _balances[owner]); - ebool isTransferable = TFHE.and(canTransfer, allowedTransfer); + ebool isTransferable = TFHE.le(amount, currentAllowance); _approve(owner, spender, TFHE.select(isTransferable, TFHE.sub(currentAllowance, amount), currentAllowance)); return isTransferable; } + /** * @dev Increases `owner` s allowance for `spender` based on spent `value`. *