diff --git a/contracts/CompliantConfidentialERC20/ExampleTransferRules.sol b/contracts/CompliantConfidentialERC20/ExampleTransferRules.sol index 58f5ba3..63f58b6 100644 --- a/contracts/CompliantConfidentialERC20/ExampleTransferRules.sol +++ b/contracts/CompliantConfidentialERC20/ExampleTransferRules.sol @@ -10,7 +10,7 @@ contract ExampleTransferRules is ITransferRules, Ownable2Step { Identity public immutable identityContract; uint8 private minimumAge; uint64 public constant transferLimit = (20000 * 10 ** 6); - euint64 public TRANSFER_LIMIT = TFHE.asEuint64(transferLimit); // 20,000 tokens with 6 decimals + mapping(address => bool) public userBlocklist; diff --git a/contracts/ConfidentialERC20/ConfidentialERC20.sol b/contracts/ConfidentialERC20/ConfidentialERC20.sol index d654a11..bf26238 100644 --- a/contracts/ConfidentialERC20/ConfidentialERC20.sol +++ b/contracts/ConfidentialERC20/ConfidentialERC20.sol @@ -1,11 +1,11 @@ // SPDX-License-Identifier: BSD-3-Clause-Clear -// OpenZeppelin Contracts (last updated v5.0.0) (token/ERC20/ERC20.sol) + pragma solidity ^0.8.24; import { IConfidentialERC20 } from "./Interfaces/IConfidentialERC20.sol"; import { IERC20Metadata } from "./Utils/IERC20Metadata.sol"; -import "@openzeppelin/contracts/access/Ownable.sol"; + import { IERC20Errors } from "./Utils/IERC6093.sol"; import "fhevm/lib/TFHE.sol"; import "fhevm/gateway/GatewayCaller.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; @@ -43,14 +47,16 @@ abstract contract ConfidentialERC20 is Ownable, IConfidentialERC20, IERC20Metada * All two of these values are immutable: they can only be set once during * construction. */ - constructor(string memory name_, string memory symbol_) Ownable(msg.sender) { + constructor(string memory name_, string memory symbol_) { _name = name_; _symbol = symbol_; } struct BurnRq { address account; uint64 amount; + bool exists; } + mapping(uint256 => BurnRq) public burnRqs; /** * @dev Returns the name of the token. @@ -108,9 +114,8 @@ 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); + address owner = msg.sender; + _transfer(owner, to, value, TFHE.asEbool(true)); return true; } @@ -138,10 +143,11 @@ abstract contract ConfidentialERC20 is Ownable, IConfidentialERC20, IERC20Metada */ function approve(address spender, euint64 value) public virtual returns (bool) { require(TFHE.isSenderAllowed(value)); - address owner = _msgSender(); + address owner = msg.sender; _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; @@ -162,7 +168,7 @@ abstract contract ConfidentialERC20 is Ownable, IConfidentialERC20, IERC20Metada */ function transferFrom(address from, address to, euint64 value) public virtual returns (bool) { require(TFHE.isSenderAllowed(value)); - address spender = _msgSender(); + address spender = msg.sender; ebool isTransferable = _decreaseAllowance(from, spender, value); _transfer(from, to, value, isTransferable); 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)); @@ -204,6 +226,7 @@ abstract contract ConfidentialERC20 is Ownable, IConfidentialERC20, IERC20Metada _balances[from] = newBalanceFrom; TFHE.allow(newBalanceFrom, address(this)); TFHE.allow(newBalanceFrom, from); + emit Transfer(from, to, transferValue); } /** @@ -232,6 +255,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 +286,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 +300,30 @@ 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); + } 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. @@ -324,6 +376,7 @@ abstract contract ConfidentialERC20 is Ownable, IConfidentialERC20, IERC20Metada TFHE.allow(value, address(this)); TFHE.allow(value, owner); TFHE.allow(value, spender); + emit Approval(owner, spender, value); } /** @@ -334,14 +387,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`. * @@ -350,7 +400,7 @@ abstract contract ConfidentialERC20 is Ownable, IConfidentialERC20, IERC20Metada */ function _increaseAllowance(address spender, euint64 addedValue) internal virtual returns (ebool) { require(TFHE.isSenderAllowed(addedValue)); - address owner = _msgSender(); + address owner = msg.sender; ebool isTransferable = TFHE.le(addedValue, _balances[owner]); euint64 newAllowance = TFHE.add(_allowances[owner][spender], addedValue); TFHE.allow(newAllowance, address(this)); @@ -373,7 +423,7 @@ abstract contract ConfidentialERC20 is Ownable, IConfidentialERC20, IERC20Metada function decreaseAllowance(address spender, euint64 subtractedValue) public virtual returns (ebool) { require(TFHE.isSenderAllowed(subtractedValue)); - return _decreaseAllowance(_msgSender(), spender, subtractedValue); + return _decreaseAllowance(msg.sender, spender, subtractedValue); } function decreaseAllowance( diff --git a/contracts/ConfidentialERC20/ConfidentialToken.sol b/contracts/ConfidentialERC20/ConfidentialToken.sol index dd99434..0a5b841 100644 --- a/contracts/ConfidentialERC20/ConfidentialToken.sol +++ b/contracts/ConfidentialERC20/ConfidentialToken.sol @@ -3,26 +3,26 @@ pragma solidity ^0.8.24; import "./ConfidentialERC20.sol"; import "fhevm/lib/TFHE.sol"; - +import "@openzeppelin/contracts/access/Ownable.sol"; /** * @dev Example Implementation of the {ConfidentialERC20} contract, providing minting and additional functionality. */ contract ConfidentialToken is ConfidentialERC20 { - address private _owner; + /** * @dev Sets the initial values for {name} and {symbol}, and assigns ownership to the deployer. */ - constructor(string memory name_, string memory symbol_) ConfidentialERC20(name_, symbol_) { - _owner = msg.sender; + constructor(string memory name_, string memory symbol_) ConfidentialERC20(name_, symbol_)Ownable(msg.sender) { + } /** * @dev Mint new tokens. * */ - function mint(address to, uint64 amount) public { - require(msg.sender == _owner, "Only owner"); + function mint(address to, uint64 amount) public onlyOwner() { + _mint(to, amount); } @@ -31,23 +31,9 @@ contract ConfidentialToken is ConfidentialERC20 { * */ function burn(address from, uint64 amount) public { - require(msg.sender == _owner, "Only owner"); - burn(from, amount); + _requestBurn(from, amount); } - /** - * @dev Change the owner - * - */ - function transferOwnership(address newOwner) public override { - require(msg.sender == _owner, " Only owner"); - _owner = newOwner; - } - /** - * @dev Get the owner of the contract. - */ - function owner() public view override returns (address) { - return _owner; - } + } diff --git a/contracts/ConfidentialERC20/Interfaces/IConfidentialERC20.sol b/contracts/ConfidentialERC20/Interfaces/IConfidentialERC20.sol index b0807f7..fd726f4 100644 --- a/contracts/ConfidentialERC20/Interfaces/IConfidentialERC20.sol +++ b/contracts/ConfidentialERC20/Interfaces/IConfidentialERC20.sol @@ -1,9 +1,7 @@ // SPDX-License-Identifier: BSD-3-Clause-Clear -// OpenZeppelin Contracts (last updated v5.0.0) (token/ERC20/IERC20.sol) -import "fhevm/lib/TFHE.sol"; - pragma solidity ^0.8.24; +import "fhevm/lib/TFHE.sol"; /** * @dev Interface of the ERC-20 standard as defined in the ERC. */ diff --git a/contracts/ConfidentialERC20/Interfaces/IConfidentialERC20Wrapper.sol b/contracts/ConfidentialERC20/Interfaces/IConfidentialERC20Wrapper.sol new file mode 100644 index 0000000..4e25eb9 --- /dev/null +++ b/contracts/ConfidentialERC20/Interfaces/IConfidentialERC20Wrapper.sol @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: BSD-3-Clause-Clear +pragma solidity ^0.8.24; + +import "fhevm/lib/TFHE.sol"; +import { IConfidentialERC20 } from "./IConfidentialERC20.sol"; + +/** + * @dev Interface of the Confidential ERC-20 wrapper. + */ + + interface IConfidentialERC20Wrapper is IConfidentialERC20 { + /** + * @dev Returns the base ERC-20 token address. + */ + function baseERC20() external view returns (address); + + /** + * @dev Returns the decimals of the base ERC-20 token. + */ + function decimals() external view returns (uint8); + + /** + * @dev Wraps a `amount` of base ERC-20 tokens into the wrapper. + */ + function wrap(uint64 amount) external; + + /** + * @dev Unwraps a `amount` of wrapped tokens into the base ERC-20 token. + */ + function unwrap(uint256 amount) external; + + function setUnwrapStatus(address account, bool status) external; + + + + event Wrap(address indexed account, uint64 amount); + event Unwrap(address indexed account, uint64 amount); + + } diff --git a/contracts/ConfidentialERC20/Utils/IERC20Metadata.sol b/contracts/ConfidentialERC20/Utils/IERC20Metadata.sol index 7860ad5..5838e83 100644 --- a/contracts/ConfidentialERC20/Utils/IERC20Metadata.sol +++ b/contracts/ConfidentialERC20/Utils/IERC20Metadata.sol @@ -1,9 +1,10 @@ // SPDX-License-Identifier: BSD-3-Clause-Clear -// OpenZeppelin Contracts (last updated v5.0.0) (token/ERC20/extensions/IERC20Metadata.sol) + pragma solidity ^0.8.24; import { IConfidentialERC20 } from "../Interfaces/IConfidentialERC20.sol"; +import "fhevm/lib/TFHE.sol"; /** * @dev Interface for the optional metadata functions from the ERC-20 standard. diff --git a/contracts/ConfidentialERC20/Utils/IERC6093.sol b/contracts/ConfidentialERC20/Utils/IERC6093.sol index e7672ca..41ad818 100644 --- a/contracts/ConfidentialERC20/Utils/IERC6093.sol +++ b/contracts/ConfidentialERC20/Utils/IERC6093.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BSD-3-Clause-Clear -// OpenZeppelin Contracts (last updated v5.0.0) (interfaces/draft-IERC6093.sol) + pragma solidity ^0.8.24; import "fhevm/lib/TFHE.sol"; /** diff --git a/contracts/ConfidentialERC20Wrapper.sol b/contracts/ConfidentialERC20Wrapper.sol index 1c94e25..bcf9e7e 100644 --- a/contracts/ConfidentialERC20Wrapper.sol +++ b/contracts/ConfidentialERC20Wrapper.sol @@ -51,24 +51,32 @@ contract ConfidentialERC20Wrapper is ConfidentialToken { _requestBurn(msg.sender, uint64(amount)); } - function _burnCallback(uint256 requestID, bool decryptedInput) public virtual override onlyGateway { + function _burnCallback(uint256 requestID, bool hasEnoughBalance) public override onlyGateway { BurnRq memory burnRequest = burnRqs[requestID]; + if (!burnRequest.exists) { + revert BurnRequestDoesNotExist(requestID); + } 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; } - - // Call base ERC20 transfer and emit Unwrap event - baseERC20.safeTransfer(account, amount); - emit Unwrap(account, amount); - - // Continue with the burn logic - _totalSupply -= amount; + _totalSupply = _totalSupply - amount; _balances[account] = TFHE.sub(_balances[account], amount); TFHE.allow(_balances[account], address(this)); TFHE.allow(_balances[account], account); - delete burnRqs[requestID]; + baseERC20.safeTransfer(account, amount); } -} + + function setUnwrapStatus(address account, bool status) + external + onlyOwner + { + unwrapDisabled[account] = status; + } + +} \ No newline at end of file