diff --git a/README.md b/README.md index a7550ae..4b3e6fd 100644 --- a/README.md +++ b/README.md @@ -4,16 +4,14 @@ **DISCLAIMER: This code has NOT been externally audited and is actively being developed. Please do not use in production without taking the appropriate steps to ensure maximum security.** - Basic ERC-20 contract designed to be inherited and extended. Leveraging native overflow checks in solc 0.8 to simplify ERC-20 implementation. It should be noted that this ERC-20 implementation does not include some functionality that is commonly used in other tokens, such as: - `address(0)` checks on `_transfer` -- `permit()` - `increaseAllowance` - `decreaseAllowance` -- `push` -- `pull` -This was intentional, as this ERC-20 was intended to have the minimum functionality necessary, allowing for maximum extendability and customizability. +This was intentional, as this ERC-20 was intended to have the minimum functionality necessary, allowing for maximum extendability and customizability. + +This token implementation includes ERC-2612 [permit](https://eips.ethereum.org/EIPS/eip-2612) capability as well as `increaseAllowance` and `decreaseAllowance` functions. To clone, set up and run tests: ``` diff --git a/contracts/ERC20.sol b/contracts/ERC20.sol index 7c2d5bc..2a58a9f 100644 --- a/contracts/ERC20.sol +++ b/contracts/ERC20.sol @@ -9,6 +9,10 @@ import { IERC20 } from "./interfaces/IERC20.sol"; */ contract ERC20 is IERC20 { + /**************/ + /*** ERC-20 ***/ + /**************/ + string public override name; string public override symbol; @@ -20,6 +24,15 @@ contract ERC20 is IERC20 { mapping(address => mapping(address => uint256)) public override allowance; + /****************/ + /*** ERC-2612 ***/ + /****************/ + + // PERMIT_TYPEHASH = keccak256("Permit(address owner,address spender,uint256 amount,uint256 nonce,uint256 deadline)"); + bytes32 public constant override PERMIT_TYPEHASH = 0xfc77c2b9d30fe91687fd39abb7d16fcdfe1472d065740051ab8b13e4bf4a617f; + + mapping (address => uint256) public override nonces; + /** * @param name_ The name of the token. * @param symbol_ The symbol of the token. @@ -50,6 +63,20 @@ contract ERC20 is IERC20 { return true; } + function permit(address owner, address spender, uint256 amount, uint256 deadline, uint8 v, bytes32 r, bytes32 s) external override { + require(deadline >= block.timestamp, "ERC20:P:EXPIRED"); + bytes32 digest = keccak256( + abi.encodePacked( + "\x19\x01", + DOMAIN_SEPARATOR(), + keccak256(abi.encode(PERMIT_TYPEHASH, owner, spender, amount, nonces[owner]++, deadline)) + ) + ); + address recoveredAddress = ecrecover(digest, v, r, s); + require(recoveredAddress == owner && owner != address(0), "ERC20:P:INVALID_SIGNATURE"); + _approve(owner, spender, amount); + } + function transfer(address recipient_, uint256 amount_) external override returns (bool success_) { _transfer(msg.sender, recipient_, amount_); return true; @@ -61,6 +88,22 @@ contract ERC20 is IERC20 { return true; } + /**********************/ + /*** View Functions ***/ + /**********************/ + + function DOMAIN_SEPARATOR() public view override returns (bytes32 domainSeparator_) { + return keccak256( + abi.encode( + keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"), + keccak256(bytes(name)), + keccak256(bytes("1")), + block.chainid, + address(this) + ) + ); + } + /**************************/ /*** Internal Functions ***/ /**************************/ diff --git a/contracts/ERC20Permit.sol b/contracts/ERC20Permit.sol deleted file mode 100644 index 1688eb3..0000000 --- a/contracts/ERC20Permit.sol +++ /dev/null @@ -1,132 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-only -pragma solidity ^0.8.7; - -import { IERC20Permit } from "./interfaces/IERC20Permit.sol"; - -/** - * @title Modern and gas efficient ERC-20 implementation. - * @dev Acknowledgements to Solmate, OpenZeppelin, and DSS for inspiring this code. - */ -contract ERC20Permit is IERC20Permit { - - /**************/ - /*** ERC-20 ***/ - /**************/ - - string public override name; - string public override symbol; - - uint8 public immutable override decimals; - - uint256 public override totalSupply; - - mapping(address => uint256) public override balanceOf; - - mapping(address => mapping(address => uint256)) public override allowance; - - /****************/ - /*** ERC-2612 ***/ - /****************/ - - // PERMIT_TYPEHASH = keccak256("Permit(address owner,address spender,uint256 amount,uint256 nonce,uint256 deadline)"); - bytes32 public constant override PERMIT_TYPEHASH = 0xfc77c2b9d30fe91687fd39abb7d16fcdfe1472d065740051ab8b13e4bf4a617f; - - mapping (address => uint256) public override nonces; - - /** - * @param name_ The name of the token. - * @param symbol_ The symbol of the token. - * @param decimals_ The decimal precision used by the token. - */ - constructor(string memory name_, string memory symbol_, uint8 decimals_) { - name = name_; - symbol = symbol_; - decimals = decimals_; - } - - /**************************/ - /*** External Functions ***/ - /**************************/ - - function DOMAIN_SEPARATOR() public view override returns (bytes32 domainSeparator_) { - return keccak256( - abi.encode( - keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"), - keccak256(bytes(name)), - keccak256(bytes("1")), - block.chainid, - address(this) - ) - ); - } - - function approve(address spender_, uint256 amount_) external override returns (bool success_) { - _approve(msg.sender, spender_, amount_); - return true; - } - - function decreaseAllowance(address spender_, uint256 subtractedAmount_) external override returns (bool success_) { - _approve(msg.sender, spender_, allowance[msg.sender][spender_] - subtractedAmount_); - return true; - } - - function increaseAllowance(address spender_, uint256 addedAmount_) external override returns (bool success_) { - _approve(msg.sender, spender_, allowance[msg.sender][spender_] + addedAmount_); - return true; - } - - function permit(address owner, address spender, uint256 amount, uint256 deadline, uint8 v, bytes32 r, bytes32 s) external override { - require(deadline >= block.timestamp, "ERC20Permit:EXPIRED"); - bytes32 digest = keccak256( - abi.encodePacked( - "\x19\x01", - DOMAIN_SEPARATOR(), - keccak256(abi.encode(PERMIT_TYPEHASH, owner, spender, amount, nonces[owner]++, deadline)) - ) - ); - address recoveredAddress = ecrecover(digest, v, r, s); - require(recoveredAddress == owner && owner != address(0), "ERC20Permit:INVALID_SIGNATURE"); - _approve(owner, spender, amount); - } - - function transfer(address recipient_, uint256 amount_) external override returns (bool success_) { - _transfer(msg.sender, recipient_, amount_); - return true; - } - - function transferFrom(address owner_, address recipient_, uint256 amount_) external override returns (bool success_) { - _approve(owner_, msg.sender, allowance[owner_][msg.sender] - amount_); - _transfer(owner_, recipient_, amount_); - return true; - } - - /**************************/ - /*** Internal Functions ***/ - /**************************/ - - function _approve(address owner_, address spender_, uint256 amount_) internal { - emit Approval(owner_, spender_, allowance[owner_][spender_] = amount_); - } - - function _burn(address owner_, uint256 amount_) internal { - balanceOf[owner_] -= amount_; - totalSupply -= amount_; - - emit Transfer(owner_, address(0), amount_); - } - - function _mint(address recipient_, uint256 amount_) internal { - totalSupply += amount_; - balanceOf[recipient_] += amount_; - - emit Transfer(address(0), recipient_, amount_); - } - - function _transfer(address owner_, address recipient_, uint256 amount_) internal { - balanceOf[owner_] -= amount_; - balanceOf[recipient_] += amount_; - - emit Transfer(owner_, recipient_, amount_); - } - -} diff --git a/contracts/interfaces/IERC20.sol b/contracts/interfaces/IERC20.sol index 27980a2..2e257c3 100644 --- a/contracts/interfaces/IERC20.sol +++ b/contracts/interfaces/IERC20.sol @@ -1,9 +1,13 @@ // SPDX-License-Identifier: AGPL-3.0-only pragma solidity ^0.8.7; -/// @title Interface of the ERC20 standard as defined in the EIP. +/// @title Interface of the ERC20 standard as defined in the EIP, including ERC-2612 permit functionality. interface IERC20 { + /**************/ + /*** Events ***/ + /**************/ + /** * @dev Emits an event indicating that tokens have moved from one account to another. * @param owner_ Account that tokens have moved from. @@ -20,38 +24,9 @@ interface IERC20 { */ event Approval(address indexed owner_, address indexed spender_, uint256 amount_); - /** - * @dev Returns the name of the token. - */ - function name() external view returns (string memory name_); - - /** - * @dev Returns the symbol of the token. - */ - function symbol() external view returns (string memory symbol_); - - /** - * @dev Returns the decimal precision used by the token. - */ - function decimals() external view returns (uint8 decimals_); - - /** - * @dev Returns the total amount of tokens in existence. - */ - function totalSupply() external view returns (uint256 totalSupply_); - - /** - * @dev Returns the amount of tokens owned by a given account. - * @param account_ Account that owns the tokens. - */ - function balanceOf(address account_) external view returns (uint256 balance_); - - /** - * @dev Function that returns the allowance that one account has given another over their tokens. - * @param owner_ Account that tokens are approved from. - * @param spender_ Account that tokens are approved for. - */ - function allowance(address owner_, address spender_) external view returns (uint256 allowance_); + /**************************/ + /*** External Functions ***/ + /**************************/ /** * @dev Function that allows one account to set the allowance of another account over their tokens. @@ -80,6 +55,18 @@ interface IERC20 { */ function increaseAllowance(address spender_, uint256 addedAmount_) external returns (bool success_); + /** + * @dev Approve by signature. + * @param owner_ Owner address that signed the permit. + * @param spender_ Spender of the permit. + * @param amount_ Permit approval spend limit. + * @param deadline_ Deadline after which the permit is invalid. + * @param v_ ECDSA signature v component. + * @param r_ ECDSA signature r component. + * @param s_ ECDSA signature s component. + */ + function permit(address owner_, address spender_, uint amount_, uint deadline_, uint8 v_, bytes32 r_, bytes32 s_) external; + /** * @dev Moves an amount of tokens from `msg.sender` to a specified account. * Emits a {Transfer} event. @@ -100,4 +87,60 @@ interface IERC20 { */ function transferFrom(address owner_, address recipient_, uint256 amount_) external returns (bool success_); + /**********************/ + /*** View Functions ***/ + /**********************/ + + /** + * @dev Function that returns the allowance that one account has given another over their tokens. + * @param owner_ Account that tokens are approved from. + * @param spender_ Account that tokens are approved for. + */ + function allowance(address owner_, address spender_) external view returns (uint256 allowance_); + + /** + * @dev Returns the amount of tokens owned by a given account. + * @param account_ Account that owns the tokens. + */ + function balanceOf(address account_) external view returns (uint256 balance_); + + /** + * @dev Returns the decimal precision used by the token. + */ + function decimals() external view returns (uint8 decimals_); + + /** + * @dev Returns the signature domain separator. + * @return domainSeparator_ The domain for the contract. + */ + function DOMAIN_SEPARATOR() external view returns (bytes32 domainSeparator_); + + /** + * @dev Returns the name of the token. + */ + function name() external view returns (string memory name_); + + /** + * @dev Returns the nonce for the given owner. + * @param owner The address of the owner account. + * @return nonce_ The current nonce. + */ + function nonces(address owner) external view returns (uint256 nonce_); + + /** + * @dev Returns the permit type hash. + * @return hash_ The typehash for the commit. + */ + function PERMIT_TYPEHASH() external view returns (bytes32 hash_); + + /** + * @dev Returns the symbol of the token. + */ + function symbol() external view returns (string memory symbol_); + + /** + * @dev Returns the total amount of tokens in existence. + */ + function totalSupply() external view returns (uint256 totalSupply_); + } diff --git a/contracts/interfaces/IERC20Permit.sol b/contracts/interfaces/IERC20Permit.sol deleted file mode 100644 index 608686b..0000000 --- a/contracts/interfaces/IERC20Permit.sol +++ /dev/null @@ -1,40 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-only -pragma solidity ^0.8.7; - -import { IERC20 } from "./IERC20.sol"; - -/// @title Interface of the ERC20 standard with the addition of permit functionality outlined in EIP 2612 -interface IERC20Permit is IERC20 { - - /** - * @dev Approve by signature. - * @param owner_ Owner address that signed the permit. - * @param spender_ Spender of the permit. - * @param amount_ Permit approval spend limit. - * @param deadline_ Deadline after which the permit is invalid. - * @param v_ ECDSA signature v component. - * @param r_ ECDSA signature r component. - * @param s_ ECDSA signature s component. - */ - function permit(address owner_, address spender_, uint amount_, uint deadline_, uint8 v_, bytes32 r_, bytes32 s_) external; - - /** - * @dev Returns the permit type hash. - * @return hash_ The typehash for the commit. - */ - function PERMIT_TYPEHASH() external pure returns (bytes32 hash_); - - /** - * @dev Returns the nonce for the given owner. - * @param owner The address of the owner account. - * @return nonce_ The current nonce. - */ - function nonces(address owner) external view returns (uint256 nonce_); - - /** - * @dev Returns the signature domain separator. - * @return domainSeparator_ The domain for the contract. - */ - function DOMAIN_SEPARATOR() external view returns (bytes32 domainSeparator_); - -} diff --git a/contracts/test/ERC20.t.sol b/contracts/test/ERC20.t.sol index 4c70a22..11b0983 100644 --- a/contracts/test/ERC20.t.sol +++ b/contracts/test/ERC20.t.sol @@ -3,10 +3,12 @@ pragma solidity ^0.8.7; import { InvariantTest, TestUtils } from "../../modules/contract-test-utils/contracts/test.sol"; +import { ERC20 } from "../ERC20.sol"; + import { ERC20User } from "./accounts/ERC20User.sol"; import { MockERC20 } from "./mocks/MockERC20.sol"; -contract ERC20Test is TestUtils { +contract ERC20BaseTest is TestUtils { bytes constant ARITHMETIC_ERROR = abi.encodeWithSignature("Panic(uint256)", 0x11); @@ -173,6 +175,137 @@ contract ERC20Test is TestUtils { } +contract ERC20PermitTest is TestUtils { + + bytes constant ARITHMETIC_ERROR = abi.encodeWithSignature("Panic(uint256)", 0x11); + + ERC20 token; + ERC20User user; + + uint256 skOwner = 1; + uint256 skSpender = 2; + uint256 nonce = 0; + uint256 deadline = 5_000_000_000; // Timestamp far in the future + + address owner; + address spender; + + uint256 constant WAD = 10 ** 18; + + function setUp() public virtual { + owner = vm.addr(skOwner); + spender = vm.addr(skSpender); + + vm.warp(deadline - 52 weeks); + token = new ERC20("Maple Token", "MPL", 18); + user = new ERC20User(); + } + + function test_typehash() external { + assertEq(token.PERMIT_TYPEHASH(), keccak256("Permit(address owner,address spender,uint256 amount,uint256 nonce,uint256 deadline)")); + } + + // NOTE: Virtual so inheriting tests can override with different DOMAIN_SEPARATORs because of different addresses + function test_domainSeparator() external virtual { + assertEq(token.DOMAIN_SEPARATOR(), 0x06c0ee43424d25534e5af6b6af862333b542f6583ff9948b8299442926099eec); + } + + function test_permit() external { + uint256 amount = 10 * WAD; + assertEq(token.nonces(owner), 0); + assertEq(token.allowance(owner, spender), 0); + + ( uint8 v, bytes32 r, bytes32 s ) = _getValidPermitSignature(amount, owner, skOwner, deadline); + user.erc20_permit(address(token), owner, spender, amount, deadline, v, r, s); + + assertEq(token.allowance(owner, spender), amount); + assertEq(token.nonces(owner), 1); + } + + function test_permitZeroAddress() external { + uint256 amount = 10 * WAD; + ( uint8 v, bytes32 r, bytes32 s ) = _getValidPermitSignature(amount, owner, skOwner, deadline); + + vm.expectRevert(bytes("ERC20:P:INVALID_SIGNATURE")); + user.erc20_permit(address(token), address(0), spender, amount, deadline, 17, r, s); // https://ethereum.stackexchange.com/questions/69328/how-to-get-the-zero-address-from-ecrecover + + vm.expectRevert(bytes("ERC20:P:INVALID_SIGNATURE")); + user.erc20_permit(address(token), address(0), spender, amount, deadline, v, r, s); + } + + function test_permitNonOwnerAddress() external { + uint256 amount = 10 * WAD; + + ( uint8 v, bytes32 r, bytes32 s ) = _getValidPermitSignature(amount, owner, skOwner, deadline); + + vm.expectRevert(bytes("ERC20:P:INVALID_SIGNATURE")); + user.erc20_permit(address(token), spender, owner, amount, deadline, v, r, s); + + ( v, r, s ) = _getValidPermitSignature(amount, spender, skSpender, deadline); + + vm.expectRevert(bytes("ERC20:P:INVALID_SIGNATURE")); + user.erc20_permit(address(token), owner, spender, amount, deadline, v, r, s); + } + + function test_permitWithExpiry() external { + uint256 amount = 10 * WAD; + uint256 expiry = 482112000 + 1 hours; + + // Expired permit should fail + vm.warp(482112000 + 1 hours + 1); + assertEq(block.timestamp, 482112000 + 1 hours + 1); + + ( uint8 v, bytes32 r, bytes32 s ) = _getValidPermitSignature(amount, owner, skOwner, expiry); + + vm.expectRevert(bytes("ERC20:P:EXPIRED")); + user.erc20_permit(address(token), owner, spender, amount, expiry, v, r, s); + + assertEq(token.allowance(owner, spender), 0); + assertEq(token.nonces(owner), 0); + + // Valid permit should succeed + vm.warp(482112000 + 1 hours); + assertEq(block.timestamp, 482112000 + 1 hours); + + ( v, r, s ) = _getValidPermitSignature(amount, owner, skOwner, expiry); + user.erc20_permit(address(token), owner, spender, amount, expiry, v, r, s); + + assertEq(token.allowance(owner, spender), amount); + assertEq(token.nonces(owner), 1); + } + + function test_permitReplay() external { + uint256 amount = 10 * WAD; + ( uint8 v, bytes32 r, bytes32 s ) = _getValidPermitSignature(amount, owner, skOwner, deadline); + + // First time should succeed + user.erc20_permit(address(token), owner, spender, amount, deadline, v, r, s); + + // Second time nonce has been consumed and should fail + vm.expectRevert(bytes("ERC20:P:INVALID_SIGNATURE")); + user.erc20_permit(address(token), owner, spender, amount, deadline, v, r, s); + } + + // Returns an ERC-2612 `permit` digest for the `owner` to sign + function _getDigest(address owner_, address spender_, uint256 value_, uint256 nonce_, uint256 deadline_) internal view returns (bytes32) { + return keccak256( + abi.encodePacked( + '\x19\x01', + token.DOMAIN_SEPARATOR(), + keccak256(abi.encode(token.PERMIT_TYPEHASH(), owner_, spender_, value_, nonce_, deadline_)) + ) + ); + } + + // Returns a valid `permit` signature signed by this contract's `owner` address + function _getValidPermitSignature(uint256 value_, address owner_, uint256 ownerSk_, uint256 deadline_) internal returns (uint8 v_, bytes32 r_, bytes32 s_) { + bytes32 digest = _getDigest(owner_, spender, value_, nonce, deadline_); + ( uint8 v, bytes32 r, bytes32 s ) = vm.sign(ownerSk_, digest); + return (v, r, s); + } + +} + contract ERC20Invariants is TestUtils, InvariantTest { BalanceSum balanceSum; diff --git a/contracts/test/ERC20Permit.t.sol b/contracts/test/ERC20Permit.t.sol deleted file mode 100644 index 5651df4..0000000 --- a/contracts/test/ERC20Permit.t.sol +++ /dev/null @@ -1,194 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-only -pragma solidity ^0.8.7; - -import { InvariantTest, TestUtils } from "../../modules/contract-test-utils/contracts/test.sol"; - -import { ERC20Permit } from "../ERC20Permit.sol"; - -import { ERC20PermitUser } from "./accounts/ERC20User.sol"; -import { MockERC20Permit } from "./mocks/MockERC20.sol"; -import { ERC20Test, MockERC20 } from "./ERC20.t.sol"; - -contract ERC20PermitBaseTest is ERC20Test { - - function setUp() override public { - token = MockERC20(address(new MockERC20Permit("Token", "TKN", 18))); - } - -} - -contract ERC20PermitTest is TestUtils { - - bytes constant ARITHMETIC_ERROR = abi.encodeWithSignature("Panic(uint256)", 0x11); - - ERC20Permit token; - ERC20PermitUser user; - - uint256 skOwner = 1; - uint256 skSpender = 2; - uint256 nonce = 0; - uint256 deadline = 5_000_000_000; // Timestamp far in the future - - address owner; - address spender; - - uint256 constant WAD = 10 ** 18; - - function setUp() public virtual { - owner = vm.addr(skOwner); - spender = vm.addr(skSpender); - - vm.warp(deadline - 52 weeks); - token = new ERC20Permit("Maple Token", "MPL", 18); - user = new ERC20PermitUser(); - } - - function test_typehash() external { - assertEq(token.PERMIT_TYPEHASH(), keccak256("Permit(address owner,address spender,uint256 amount,uint256 nonce,uint256 deadline)")); - } - - // NOTE: Virtual so inheriting tests can override with different DOMAIN_SEPARATORs because of different addresses - function test_domainSeparator() external virtual { - assertEq(token.DOMAIN_SEPARATOR(), 0x06c0ee43424d25534e5af6b6af862333b542f6583ff9948b8299442926099eec); - } - - function test_permit() external { - uint256 amount = 10 * WAD; - assertEq(token.nonces(owner), 0); - assertEq(token.allowance(owner, spender), 0); - - ( uint8 v, bytes32 r, bytes32 s ) = _getValidPermitSignature(amount, owner, skOwner, deadline); - user.erc20_permit(address(token), owner, spender, amount, deadline, v, r, s); - - assertEq(token.allowance(owner, spender), amount); - assertEq(token.nonces(owner), 1); - } - - function test_permitZeroAddress() external { - uint256 amount = 10 * WAD; - ( uint8 v, bytes32 r, bytes32 s ) = _getValidPermitSignature(amount, owner, skOwner, deadline); - - vm.expectRevert(bytes("ERC20Permit:INVALID_SIGNATURE")); - user.erc20_permit(address(token), address(0), spender, amount, deadline, 17, r, s); // https://ethereum.stackexchange.com/questions/69328/how-to-get-the-zero-address-from-ecrecover - - vm.expectRevert(bytes("ERC20Permit:INVALID_SIGNATURE")); - user.erc20_permit(address(token), address(0), spender, amount, deadline, v, r, s); - } - - function test_permitNonOwnerAddress() external { - uint256 amount = 10 * WAD; - - ( uint8 v, bytes32 r, bytes32 s ) = _getValidPermitSignature(amount, owner, skOwner, deadline); - - vm.expectRevert(bytes("ERC20Permit:INVALID_SIGNATURE")); - user.erc20_permit(address(token), spender, owner, amount, deadline, v, r, s); - - ( v, r, s ) = _getValidPermitSignature(amount, spender, skSpender, deadline); - - vm.expectRevert(bytes("ERC20Permit:INVALID_SIGNATURE")); - user.erc20_permit(address(token), owner, spender, amount, deadline, v, r, s); - } - - function test_permitWithExpiry() external { - uint256 amount = 10 * WAD; - uint256 expiry = 482112000 + 1 hours; - - // Expired permit should fail - vm.warp(482112000 + 1 hours + 1); - assertEq(block.timestamp, 482112000 + 1 hours + 1); - - ( uint8 v, bytes32 r, bytes32 s ) = _getValidPermitSignature(amount, owner, skOwner, expiry); - - vm.expectRevert(bytes("ERC20Permit:EXPIRED")); - user.erc20_permit(address(token), owner, spender, amount, expiry, v, r, s); - - assertEq(token.allowance(owner, spender), 0); - assertEq(token.nonces(owner), 0); - - // Valid permit should succeed - vm.warp(482112000 + 1 hours); - assertEq(block.timestamp, 482112000 + 1 hours); - - ( v, r, s ) = _getValidPermitSignature(amount, owner, skOwner, expiry); - user.erc20_permit(address(token), owner, spender, amount, expiry, v, r, s); - - assertEq(token.allowance(owner, spender), amount); - assertEq(token.nonces(owner), 1); - } - - function test_permitReplay() external { - uint256 amount = 10 * WAD; - ( uint8 v, bytes32 r, bytes32 s ) = _getValidPermitSignature(amount, owner, skOwner, deadline); - - // First time should succeed - user.erc20_permit(address(token), owner, spender, amount, deadline, v, r, s); - - // Second time nonce has been consumed and should fail - vm.expectRevert(bytes("ERC20Permit:INVALID_SIGNATURE")); - user.erc20_permit(address(token), owner, spender, amount, deadline, v, r, s); - } - - // Returns an ERC-2612 `permit` digest for the `owner` to sign - function _getDigest(address owner_, address spender_, uint256 value_, uint256 nonce_, uint256 deadline_) internal view returns (bytes32) { - return keccak256( - abi.encodePacked( - '\x19\x01', - token.DOMAIN_SEPARATOR(), - keccak256(abi.encode(token.PERMIT_TYPEHASH(), owner_, spender_, value_, nonce_, deadline_)) - ) - ); - } - - // Returns a valid `permit` signature signed by this contract's `owner` address - function _getValidPermitSignature(uint256 value_, address owner_, uint256 ownerSk_, uint256 deadline_) internal returns (uint8 v_, bytes32 r_, bytes32 s_) { - bytes32 digest = _getDigest(owner_, spender, value_, nonce, deadline_); - ( uint8 v, bytes32 r, bytes32 s ) = vm.sign(ownerSk_, digest); - return (v, r, s); - } - -} - -contract ERC20Invariants is TestUtils, InvariantTest { - - BalanceSum balanceSum; - - function setUp() public { - balanceSum = new BalanceSum(); - addTargetContract(address(balanceSum)); - } - - function invariant_balanceSum() public { - assertEq(balanceSum.token().totalSupply(), balanceSum.sum()); - } - -} - -contract BalanceSum { - - MockERC20Permit public token = new MockERC20Permit("Token", "TKN", 18); - - uint256 public sum; - - function mint(address account, uint256 amount) external { - token.mint(account, amount); - sum += amount; - } - - function burn(address account, uint256 amount) external { - token.burn(account, amount); - sum -= amount; - } - - function approve(address dst, uint256 amount) external { - token.approve(dst, amount); - } - - function transferFrom(address src, address dst, uint256 amount) external { - token.transferFrom(src, dst, amount); - } - - function transfer(address dst, uint256 amount) external { - token.transfer(dst, amount); - } - -} diff --git a/contracts/test/accounts/ERC20User.sol b/contracts/test/accounts/ERC20User.sol index 9f9eae5..150a9be 100644 --- a/contracts/test/accounts/ERC20User.sol +++ b/contracts/test/accounts/ERC20User.sol @@ -1,8 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-only pragma solidity ^0.8.7; -import { IERC20 } from "../../interfaces/IERC20.sol"; -import { IERC20Permit } from "../../interfaces/IERC20Permit.sol"; +import { IERC20 } from "../../interfaces/IERC20.sol"; contract ERC20User { @@ -10,20 +9,8 @@ contract ERC20User { IERC20(token_).approve(spender_, amount_); } - function erc20_transfer(address token_, address recipient_, uint256 amount_) external { - IERC20(token_).transfer(recipient_, amount_); - } - - function erc20_transferFrom(address token_, address owner_, address recipient_, uint256 amount_) external { - IERC20(token_).transferFrom(owner_, recipient_, amount_); - } - -} - -contract ERC20PermitUser is ERC20User { - function erc20_permit( - address mplToken_, + address token_, address owner_, address spender_, uint256 amount_, @@ -34,7 +21,15 @@ contract ERC20PermitUser is ERC20User { ) external { - IERC20Permit(mplToken_).permit(owner_, spender_, amount_, deadline_, v_, r_, s_); + IERC20(token_).permit(owner_, spender_, amount_, deadline_, v_, r_, s_); + } + + function erc20_transfer(address token_, address recipient_, uint256 amount_) external { + IERC20(token_).transfer(recipient_, amount_); + } + + function erc20_transferFrom(address token_, address owner_, address recipient_, uint256 amount_) external { + IERC20(token_).transferFrom(owner_, recipient_, amount_); } } diff --git a/contracts/test/mocks/MockERC20.sol b/contracts/test/mocks/MockERC20.sol index 9d603da..861006f 100644 --- a/contracts/test/mocks/MockERC20.sol +++ b/contracts/test/mocks/MockERC20.sol @@ -3,8 +3,6 @@ pragma solidity ^0.8.7; import { ERC20 } from "../../ERC20.sol"; -import { ERC20Permit } from "../../ERC20Permit.sol"; - contract MockERC20 is ERC20 { constructor(string memory name_, string memory symbol_, uint8 decimals_) ERC20(name_, symbol_, decimals_) {} @@ -18,17 +16,3 @@ contract MockERC20 is ERC20 { } } - -contract MockERC20Permit is ERC20Permit { - - constructor(string memory name_, string memory symbol_, uint8 decimals_) ERC20Permit(name_, symbol_, decimals_) {} - - function mint(address to_, uint256 value_) external { - _mint(to_, value_); - } - - function burn(address from_, uint256 value_) external { - _burn(from_, value_); - } - -} diff --git a/contracts/test/utils/InvariantTest.sol b/contracts/test/utils/InvariantTest.sol deleted file mode 100644 index 2c45ecc..0000000 --- a/contracts/test/utils/InvariantTest.sol +++ /dev/null @@ -1,17 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-only -pragma solidity ^0.8.7; - -contract InvariantTest { - - address[] private _targetContracts; - - function targetContracts() public view returns (address[] memory targetContracts_) { - require(_targetContracts.length != uint256(0), "NO_TARGET_CONTRACTS"); - return _targetContracts; - } - - function addTargetContract(address newTargetContract_) internal { - _targetContracts.push(newTargetContract_); - } - -} diff --git a/contracts/test/utils/Vm.sol b/contracts/test/utils/Vm.sol deleted file mode 100644 index 611629b..0000000 --- a/contracts/test/utils/Vm.sol +++ /dev/null @@ -1,84 +0,0 @@ - -// SPDX-License-Identifier: AGPL-3.0-or-later -pragma solidity ^0.8.7; - -interface Vm { - - // Set block.timestamp (newTimestamp) - function warp(uint256) external; - - // Set block.height (newHeight) - function roll(uint256) external; - - // Set block.basefee (newBasefee) - function fee(uint256) external; - - // Loads a storage slot from an address (who, slot) - function load(address,bytes32) external returns (bytes32); - - // Stores a value to an address' storage slot, (who, slot, value) - function store(address,bytes32,bytes32) external; - - // Signs data, (privateKey, digest) => (v, r, s) - function sign(uint256,bytes32) external returns (uint8,bytes32,bytes32); - - // Gets address for a given private key, (privateKey) => (address) - function addr(uint256) external returns (address); - - // Performs a foreign function call via terminal, (stringInputs) => (result) - function ffi(string[] calldata) external returns (bytes memory); - - // Sets the *next* call's msg.sender to be the input address - function prank(address) external; - - // Sets all subsequent calls' msg.sender to be the input address until `stopPrank` is called - function startPrank(address) external; - - // Sets the *next* call's msg.sender to be the input address, and the tx.origin to be the second input - function prank(address,address) external; - - // Sets all subsequent calls' msg.sender to be the input address until `stopPrank` is called, and the tx.origin to be the second input - function startPrank(address,address) external; - - // Resets subsequent calls' msg.sender to be `address(this)` - function stopPrank() external; - - // Sets an address' balance, (who, newBalance) - function deal(address, uint256) external; - - // Sets an address' code, (who, newCode) - function etch(address, bytes calldata) external; - - // Expects an error on next call - function expectRevert(bytes calldata) external; - - function expectRevert(bytes4) external; - - // Record all storage reads and writes - function record() external; - - // Gets all accessed reads and write slot from a recording session, for a given address - function accesses(address) external returns (bytes32[] memory reads, bytes32[] memory writes); - - // Prepare an expected log with (bool checkTopic1, bool checkTopic2, bool checkTopic3, bool checkData). - // Call this function, then emit an event, then call a function. Internally after the call, we check if - // logs were emitted in the expected order with the expected topics and data (as specified by the booleans) - function expectEmit(bool,bool,bool,bool) external; - - // Mocks a call to an address, returning specified data. - // Calldata can either be strict or a partial match, e.g. if you only - // pass a Solidity selector to the expected calldata, then the entire Solidity - // function will be mocked. - function mockCall(address,bytes calldata,bytes calldata) external; - - // Clears all mocked calls - function clearMockedCalls() external; - - // Expect a call to an address with the specified calldata. - // Calldata can either be strict or a partial match - function expectCall(address,bytes calldata) external; - - function getCode(string calldata) external returns (bytes memory); - -} -