diff --git a/contracts/token/ERC20/EncryptedERC20Wrapped.sol b/contracts/token/ERC20/EncryptedERC20Wrapped.sol new file mode 100644 index 0000000..095b624 --- /dev/null +++ b/contracts/token/ERC20/EncryptedERC20Wrapped.sol @@ -0,0 +1,163 @@ +// SPDX-License-Identifier: BSD-3-Clause-Clear +pragma solidity ^0.8.24; + +import { IERC20Metadata } from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; +import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; + +import { EncryptedERC20 } from "./EncryptedERC20.sol"; + +import "fhevm/lib/TFHE.sol"; +import "fhevm/gateway/GatewayCaller.sol"; + +/** + * @title EncryptedERC20Wrapped + * @notice This contract allows users to wrap/unwrap trustlessly + * ERC20 tokens to EncryptedERC20 tokens. + * @dev This implementation does not support tokens with rebase functions or + * tokens with a fee on transfer. All ERC20 tokens must have decimals + * inferior or equal to 18 decimals but superior or equal to 6 decimals. + */ +abstract contract EncryptedERC20Wrapped is EncryptedERC20, GatewayCaller { + using SafeERC20 for IERC20Metadata; + + /// @notice Returns if user cannot transfer or mint. + error CannotTransferOrMint(); + + /// @notice Returned if the amount is greater than 2**64. + error AmountTooHigh(); + + /// @notice Emitted when token is unwrapped. + event Unwrap(address indexed to, uint64 amount); + + /// @notice Emitted if unwrap fails. + event UnwrapFail(address account, uint64 amount); + + /// @notice Emitted when token is wrapped. + event Wrap(address indexed to, uint64 amount); + + /** + * @notice Keeps track of unwrap information. + * @param account Account that initiates the unwrap request. + * @param amount Amount to be unwrapped. + */ + struct UnwrapRequest { + address account; + uint64 amount; + } + + /// @notice ERC20 token that is wrapped. + IERC20Metadata public immutable ERC20_TOKEN; + + /// @notice Tracks whether the account can move funds. + mapping(address account => bool canMoveFunds) public isAccountRestricted; + + /// @notice Tracks the unwrap request to a unique request id. + mapping(uint256 requestId => UnwrapRequest unwrapRequest) public unwrapRequests; + + /** + * @notice Deposit/withdraw ERC20 tokens using encrypted ERC20 tokens. + * @param erc20_ Address of the ERC20 token to wrap/unwrap. + * @dev The name/symbol are autogenerated. + * For instance, + * "Wrapped Ether" --> "Encrypted Wrapped Ether" + * "WETH" --> "eWETH". + */ + constructor( + address erc20_ + ) + EncryptedERC20( + string(abi.encodePacked("Encrypted ", IERC20Metadata(erc20_).name())), + string(abi.encodePacked("e", IERC20Metadata(erc20_).symbol())) + ) + { + ERC20_TOKEN = IERC20Metadata(erc20_); + } + + /** + * @notice Unwrap EncryptedERC20 tokens to standard ERC20 tokens. + * @param amount Amount to unwrap. + */ + function unwrap(uint64 amount) public virtual { + _canTransferOrMint(msg.sender); + + /// @dev Once this function is called, it becomes impossible for the sender to move any token. + isAccountRestricted[msg.sender] = true; + ebool canUnwrap = TFHE.le(amount, _balances[msg.sender]); + + uint256[] memory cts = new uint256[](1); + cts[0] = Gateway.toUint256(canUnwrap); + + uint256 requestId = Gateway.requestDecryption( + cts, + this.callbackUnwrap.selector, + 0, + block.timestamp + 100, + false + ); + + unwrapRequests[requestId] = UnwrapRequest({ account: msg.sender, amount: amount }); + } + + /** + * @notice Wrap ERC20 tokens to an encrypted format. + * @param amount Amount to wrap. + */ + function wrap(uint256 amount) public virtual { + ERC20_TOKEN.safeTransferFrom(msg.sender, address(this), amount); + + uint256 amountAdjusted = amount / (10 ** (ERC20_TOKEN.decimals() - decimals())); + + if (amountAdjusted > type(uint64).max) { + revert AmountTooHigh(); + } + + uint64 amountUint64 = uint64(amountAdjusted); + + _unsafeMint(msg.sender, TFHE.asEuint64(amountUint64)); + _totalSupply += amountUint64; + + emit Wrap(msg.sender, amountUint64); + } + + /** + * @notice Callback function for the gateway. + * @param requestId Request id. + * @param canUnwrap Whether it can be unwrapped. + */ + function callbackUnwrap(uint256 requestId, bool canUnwrap) public virtual onlyGateway { + UnwrapRequest memory unwrapRequest = unwrapRequests[requestId]; + delete unwrapRequests[requestId]; + + if (canUnwrap) { + _unsafeBurn(unwrapRequest.account, TFHE.asEuint64(unwrapRequest.amount)); + _totalSupply -= unwrapRequest.amount; + + /// @dev It does a supply adjustment. + uint256 amountUint256 = unwrapRequest.amount * (10 ** (ERC20_TOKEN.decimals() - decimals())); + + ERC20_TOKEN.safeTransfer(unwrapRequest.account, amountUint256); + + emit Unwrap(unwrapRequest.account, unwrapRequest.amount); + } else { + emit UnwrapFail(unwrapRequest.account, unwrapRequest.amount); + } + + delete isAccountRestricted[unwrapRequest.account]; + } + + function _canTransferOrMint(address account) internal virtual { + if (isAccountRestricted[account]) { + revert CannotTransferOrMint(); + } + } + + function _transferNoEvent( + address from, + address to, + euint64 amount, + ebool isTransferable + ) internal virtual override { + _canTransferOrMint(from); + super._transferNoEvent(from, to, amount, isTransferable); + } +} diff --git a/contracts/token/ERC20/EncryptedERC20Wrapper.sol b/contracts/token/ERC20/EncryptedERC20Wrapper.sol deleted file mode 100644 index 736a6db..0000000 --- a/contracts/token/ERC20/EncryptedERC20Wrapper.sol +++ /dev/null @@ -1,89 +0,0 @@ -// SPDX-License-Identifier: BSD-3-Clause-Clear -pragma solidity ^0.8.24; - -import { IERC20Metadata } from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; -import { EncryptedERC20 } from "./EncryptedERC20.sol"; - -import "fhevm/lib/TFHE.sol"; - -/** - * @title EncryptedERC20Wrapped - * @notice This contract allows users to wrap/unwrap trustlessly - * ERC20 tokens to EncryptedERC20 tokens. - * @dev This implementation does not support tokens with rebase functions or - * tokens with a fee on transfer. All ERC20 tokens must have decimals - * inferior or equal to 18 decimals. - */ -abstract contract EncryptedERC20Wrapped is EncryptedERC20 { - /// @notice Returned if the amount is greater than 2**64. - error AmountTooHigh(); - - /// @notice Emitted when token is unwrapped. - event Unwrap(address indexed to, uint64 amount); - - /// @notice Emitted when token is wrapped. - event Wrap(address indexed to, uint64 amount); - - /// @notice ERC20 token that is wrapped. - IERC20Metadata public immutable ERC20_TOKEN; - - /** - * @notice Deposit/withdraw ERC20 tokens using encrypted ERC20 tokens. - * @param erc20_ Address of the ERC20 token to wrap/unwrap. - * @dev The name/symbol are autogenerated. - * For instance, - * "Wrapped Ether" --> "Encrypted Wrapped Ether" - * "WETH" --> "eWETH". - */ - constructor( - address erc20_ - ) - EncryptedERC20( - string(abi.encodePacked("Encrypted ", IERC20Metadata(erc20_).name())), - string(abi.encodePacked("e", IERC20Metadata(erc20_).symbol())) - ) - { - ERC20_TOKEN = IERC20Metadata(erc20_); - } - - /** - * @notice Unwrap EncryptedERC20 tokens to standard ERC20 tokens. - * @param amount Amount to unwrap. - */ - function unwrap(uint64 amount) public virtual { - _balances[msg.sender] = TFHE.sub(_balances[msg.sender], amount); - TFHE.allowThis(_balances[msg.sender]); - TFHE.allow(_balances[msg.sender], msg.sender); - - _totalSupply -= amount; - - /// @dev It does a supply adjustment. - uint256 amountUint256 = amount * (10 ** (ERC20_TOKEN.decimals() - decimals())); - - ERC20_TOKEN.transfer(msg.sender, amountUint256); - - emit Unwrap(msg.sender, amount); - } - - /** - * @notice Wrap ERC20 tokens to an encrypted format. - * @param amount Amount to wrap. - */ - function wrap(uint256 amount) public virtual { - ERC20_TOKEN.transferFrom(msg.sender, address(this), amount); - - if (amount > type(uint64).max) { - revert AmountTooHigh(); - } - - uint64 amountUint64 = uint64(amount / (10 ** (ERC20_TOKEN.decimals() - decimals()))); - _balances[msg.sender] = TFHE.add(_balances[msg.sender], amountUint64); - - TFHE.allowThis(_balances[msg.sender]); - TFHE.allow(_balances[msg.sender], msg.sender); - - _totalSupply += amountUint64; - - emit Wrap(msg.sender, amountUint64); - } -} diff --git a/contracts/token/ERC20/EncryptedWETH.sol b/contracts/token/ERC20/EncryptedWETH.sol index a91f1e3..025a229 100644 --- a/contracts/token/ERC20/EncryptedWETH.sol +++ b/contracts/token/ERC20/EncryptedWETH.sol @@ -60,11 +60,14 @@ abstract contract EncryptedWETH is EncryptedERC20 { * @notice Wrap ether to an encrypted format. */ function wrap() public payable virtual { - if (msg.value > type(uint64).max) { + uint256 amountAdjusted = msg.value / (10 ** (18 - decimals())); + + if (amountAdjusted > type(uint64).max) { revert AmountTooHigh(); } - uint64 amountUint64 = uint64(msg.value / (10 ** (18 - decimals()))); + uint64 amountUint64 = uint64(amountAdjusted); + _balances[msg.sender] = TFHE.add(_balances[msg.sender], amountUint64); TFHE.allowThis(_balances[msg.sender]);