diff --git a/contracts/child/ChildToken/UpgradeableChildERC20/UChildERC20Permit.sol b/contracts/child/ChildToken/UpgradeableChildERC20/UChildERC20Permit.sol new file mode 100644 index 00000000..9ef5fae7 --- /dev/null +++ b/contracts/child/ChildToken/UpgradeableChildERC20/UChildERC20Permit.sol @@ -0,0 +1,128 @@ +pragma solidity 0.6.6; + +import {ERC20} from "./ERC20.sol"; +import {AccessControlMixin} from "../../../common/AccessControlMixin.sol"; +import {IChildToken} from "../IChildToken.sol"; +import {NativeMetaTransaction} from "../../../common/NativeMetaTransaction.sol"; +import {ContextMixin} from "../../../common/ContextMixin.sol"; +import {UChildERC20} from "./UChildERC20.sol"; + +/** + * @title UChildERC20Permit EIP2612 + * @author KaizenDeveloperA + * @notice UChildERC20 template with EIP-3009 (https://eips.ethereum.org/EIPS/eip-3009) + */ +contract UChildERC20Permit is UChildERC20 { + /// @dev Access related state variables + bytes32 public constant PERMIT2_REVOKER_ROLE = + 0xbd4c1461ef59750b24719a44d7e2a7948c57fd12c98e333541b7ea7b61f07cb7; + address public constant PERMIT2 = 0x000000000022D473030F116dDEE9F6B43aC78BA3; + bool public permit2Enabled; + + /// @dev Permit related state variables + bytes32 public DOMAIN_SEPARATOR; + bytes32 public constant PERMIT_TYPEHASH = + 0x6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c9; + event Permit2AllowanceUpdated(bool enabled); + string internal constant PERMIT_EXPRIED = "UChildERC20Permit: permit expired"; + string internal constant INVALID_SIGNATURE = + "UChildERC20Permit: invalid signature"; + + constructor(address permit2revoker) public { + /// Initialize DOMAIN_SEPARATOR for EIP-712 permit + uint256 chainId; + assembly { + chainId := chainid() + } + DOMAIN_SEPARATOR = keccak256( + abi.encode( + keccak256( + "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)" + ), + keccak256("UChildERC20WithPermit"), + keccak256("1"), + chainId, + address(this) + ) + ); + _setupRole(PERMIT2_REVOKER_ROLE, permit2revoker); + _updatePermit2Allowance(true); + } + + /// @notice Manages the default max approval to the permit2 contract + /// @param enabled If true, the permit2 contract has full approval by default, if false, it has no approval by default + function updatePermit2Allowance( + bool enabled + ) external only(PERMIT2_REVOKER_ROLE) { + _updatePermit2Allowance(enabled); + } + + /// @notice The permit2 contract has full approval by default. If the approval is revoked, it can still be manually approved. + function allowance( + address owner, + address spender + ) public view override returns (uint256) { + if (spender == PERMIT2 && permit2Enabled) return uint256(-1); + return super.allowance(owner, spender); + } + + /** + * @dev Sets `value` as the allowance of `spender` over ``owner``'s tokens, + * given ``owner``'s signed approval. + * + * + * Emits an {Approval} event. + * + * Requirements: + * + * - `spender` cannot be the zero address. + * - `deadline` must be a timestamp in the future. + * - `v`, `r` and `s` must be a valid `secp256k1` signature from `owner` + * over the EIP712-formatted function arguments. + * - the signature must use ``owner``'s current nonce (see {nonces}). + * + * For more information on the signature format, see the + * https://eips.ethereum.org/EIPS/eip-2612#specification[relevant EIP + * section]. + * + * CAUTION: See Security Considerations above. + */ + function permit( + address owner, + address spender, + uint256 value, + uint256 deadline, + uint8 v, + bytes32 r, + bytes32 s + ) public virtual { + require(block.timestamp < deadline, PERMIT_EXPRIED); + bytes32 digest = keccak256( + abi.encodePacked( + "\x19\x01", + DOMAIN_SEPARATOR, + keccak256( + abi.encode( + PERMIT_TYPEHASH, + owner, + spender, + value, + ++nonces[owner], + deadline + ) + ) + ) + ); + address recoveredAddress = ecrecover(digest, v, r, s); + require( + recoveredAddress != address(0) && recoveredAddress == owner, + INVALID_SIGNATURE + ); + _approve(owner, spender, value); + } + + function _updatePermit2Allowance(bool enabled) private { + permit2Enabled = enabled; + emit Permit2AllowanceUpdated(enabled); + } +} diff --git a/contracts/common/NativeMetaTransaction.sol b/contracts/common/NativeMetaTransaction.sol index ccde1fb5..0a1fbe63 100644 --- a/contracts/common/NativeMetaTransaction.sol +++ b/contracts/common/NativeMetaTransaction.sol @@ -15,7 +15,7 @@ contract NativeMetaTransaction is EIP712Base { address payable indexed relayerAddress, bytes functionSignature ); - mapping(address => uint256) nonces; + mapping(address => uint256) public nonces; /* * Meta transaction structure. diff --git a/test/forge/child/UChildERC20Permit.t.sol b/test/forge/child/UChildERC20Permit.t.sol new file mode 100644 index 00000000..cf9d6497 --- /dev/null +++ b/test/forge/child/UChildERC20Permit.t.sol @@ -0,0 +1,186 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.6.2; + +import "test/forge/utils/Test.sol"; +import {UChildERC20Permit} from "contracts/child/ChildToken/UpgradeableChildERC20/UChildERC20Permit.sol"; + +/** + * @title UChildERC20Permit EIP2612 Test + * @author KaizenDeveloperA + * @notice UChildERC20 template with EIP-3009 (https://eips.ethereum.org/EIPS/eip-3009) + */ +contract UChildPermitTest is Test { + event Permit2AllowanceUpdated(bool enabled); + UChildERC20Permit internal uChildERC20Permit; + Account holder; + address spender; + address permit2revoker; + address childChainManager; + bytes32 public constant PERMIT_TYPEHASH = + keccak256( + "Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)" + ); + + function setUp() public virtual { + vm.warp(1641070800); + permit2revoker = makeAddr("permit2revoker"); + holder = makeAccount("holder"); + spender = makeAddr("spender"); + childChainManager = makeAddr("childChainManager"); + uChildERC20Permit = new UChildERC20Permit(permit2revoker); + uChildERC20Permit.initialize( + "Name", + "Symbol", + 18, + childChainManager + ); + } + + function test_TypeHash() public { + assertEq(uChildERC20Permit.PERMIT_TYPEHASH(), PERMIT_TYPEHASH); + } + + function test_PermitWithValidSignature() public { + deal(address(uChildERC20Permit), holder.addr, 2 ether); + bytes32 digest = keccak256( + abi.encodePacked( + "\x19\x01", + uChildERC20Permit.DOMAIN_SEPARATOR(), + keccak256( + abi.encode( + uChildERC20Permit.PERMIT_TYPEHASH(), + holder.addr, + spender, + 2 ether, + 1, + block.timestamp + 1 days + ) + ) + ) + ); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(holder.key, digest); + uChildERC20Permit.permit( + holder.addr, + spender, + 2 ether, + block.timestamp + 1 days, + v, + r, + s + ); + assertEq(uChildERC20Permit.allowance(holder.addr, spender), 2 ether); + vm.startPrank(spender); + uChildERC20Permit.transferFrom(holder.addr, spender, 2 ether); + assertEq(uChildERC20Permit.balanceOf( spender), 2 ether); + // a nonce can not be used twice + digest = keccak256( + abi.encodePacked( + "\x19\x01", + uChildERC20Permit.DOMAIN_SEPARATOR(), + keccak256( + abi.encode( + uChildERC20Permit.PERMIT_TYPEHASH(), + holder.addr, + spender, + 2 ether, + 1, + block.timestamp + 1 days + ) + ) + ) + ); + (v, r, s) = vm.sign(holder.key, digest); + vm.expectRevert("UChildERC20Permit: invalid signature"); + uChildERC20Permit.permit( + holder.addr, + spender, + 2 ether, + block.timestamp + 1 days, + v, + r, + s + ); + } + + function testFail_PermitWithInvalidSignature() public { + // Invalid deadline + deal(address(uChildERC20Permit), holder.addr, 20 ether); + bytes32 digest = keccak256( + abi.encodePacked( + "\x19\x01", + uChildERC20Permit.DOMAIN_SEPARATOR(), + keccak256( + abi.encode( + uChildERC20Permit.PERMIT_TYPEHASH(), + holder.addr, + spender, + 10 ether, + 1, + block.timestamp - 1 days + ) + ) + ) + ); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(holder.key, digest); + vm.expectRevert("UChildERC20Permit: permit expired"); + uChildERC20Permit.permit( + holder.addr, + spender, + 10 ether, + block.timestamp + 1 days, + v, + r, + s + ); + + // // Invalid nonce + digest = keccak256( + abi.encodePacked( + uChildERC20Permit.DOMAIN_SEPARATOR(), + keccak256( + abi.encode( + "\x19\x01", + uChildERC20Permit.PERMIT_TYPEHASH(), + holder.addr, + spender, + 10 ether, + 0, + block.timestamp + 1 days + ) + ) + ) + ); + (v, r, s) = vm.sign(holder.key, digest); + vm.expectRevert("UChildERC20Permit: invalid signature"); + uChildERC20Permit.permit( + holder.addr, + spender, + 10 ether, + block.timestamp + 1 days, + v, + r, + s + ); + } + + function testFail_Permit2Revoke(address user) external { + + vm.assume(user != permit2revoker); + vm.startPrank(user); + vm.expectRevert("Assertion failed."); + uChildERC20Permit.updatePermit2Allowance(false); + } + + function test_RevokePermit2Allowance(address owner) external { + assertEq(uChildERC20Permit.allowance(owner, uChildERC20Permit.PERMIT2()), uint256(-1)); + vm.prank(permit2revoker); + vm.expectEmit(true, true, true, true); + emit Permit2AllowanceUpdated(false); + uChildERC20Permit.updatePermit2Allowance(false); + assertFalse(uChildERC20Permit.permit2Enabled()); + assertEq( + uChildERC20Permit.allowance(owner, uChildERC20Permit.PERMIT2()), + 0 + ); + } +}