Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(UChild): add UChildERC20Permit #165

Open
wants to merge 1 commit into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
128 changes: 128 additions & 0 deletions contracts/child/ChildToken/UpgradeableChildERC20/UChildERC20Permit.sol
Original file line number Diff line number Diff line change
@@ -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);
}
}
2 changes: 1 addition & 1 deletion contracts/common/NativeMetaTransaction.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
186 changes: 186 additions & 0 deletions test/forge/child/UChildERC20Permit.t.sol
Original file line number Diff line number Diff line change
@@ -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
);
}
}