pragma solidity 0.8.20; import {Test} from "forge-std/Test.sol"; import "../../src/mocks/SigUtils.sol"; import "@openzeppelin-contracts/token/ERC20/extensions/ERC20Permit.sol"; abstract contract ERC20PermitTest is Test { SigUtils sigUtils; ERC20Permit permitToken; address owner; address spender; uint256 permitAmount; uint256 maxPermitAmount; uint256 constant ownerPrivateKey = 0xA11CE; uint256 constant spenderPrivateKey = 0xB0B; function initializePermit( address token, uint256 amount, uint256 maxAmount ) public { permitAmount = amount; maxPermitAmount = maxAmount; permitToken = ERC20Permit(token); sigUtils = new SigUtils(permitToken.DOMAIN_SEPARATOR()); owner = vm.addr(ownerPrivateKey); spender = vm.addr(spenderPrivateKey); } function test_permit_initialNonceIsZero() public view { assertEq(permitToken.nonces(owner), 0); assertEq(permitToken.nonces(spender), 0); } function test_permit_acceptsOwnerSignature() public { _mintPermitTokens(owner, permitAmount); SigUtils.Permit memory permit = SigUtils.Permit({ owner: owner, spender: spender, value: permitAmount, nonce: 0, deadline: 1 days }); bytes32 digest = sigUtils.getTypedDataHash(permit); (uint8 v, bytes32 r, bytes32 s) = vm.sign(ownerPrivateKey, digest); permitToken.permit( permit.owner, permit.spender, permit.value, permit.deadline, v, r, s ); assertEq(permitToken.allowance(owner, spender), permitAmount); assertEq(permitToken.nonces(owner), 1); } function test_permit_expiredPermit() public { _mintPermitTokens(owner, permitAmount); SigUtils.Permit memory permit = SigUtils.Permit({ owner: owner, spender: spender, value: permitAmount, nonce: permitToken.nonces(owner), deadline: 1 days }); bytes32 digest = sigUtils.getTypedDataHash(permit); (uint8 v, bytes32 r, bytes32 s) = vm.sign(ownerPrivateKey, digest); vm.warp(1 days + 1 seconds); // fast forward one second past the deadline vm.expectRevert(); permitToken.permit( permit.owner, permit.spender, permit.value, permit.deadline, v, r, s ); } function test_permit_invalidSigner() public { _mintPermitTokens(owner, permitAmount); SigUtils.Permit memory permit = SigUtils.Permit({ owner: owner, spender: spender, value: permitAmount, nonce: permitToken.nonces(owner), deadline: 1 days }); bytes32 digest = sigUtils.getTypedDataHash(permit); (uint8 v, bytes32 r, bytes32 s) = vm.sign(spenderPrivateKey, digest); // spender signs owner's approval vm.expectRevert(); permitToken.permit( permit.owner, permit.spender, permit.value, permit.deadline, v, r, s ); } function test_permit_invalidNonce() public { _mintPermitTokens(owner, permitAmount); SigUtils.Permit memory permit = SigUtils.Permit({ owner: owner, spender: spender, value: permitAmount, nonce: 1, // owner nonce stored on-chain is 0 deadline: 1 days }); bytes32 digest = sigUtils.getTypedDataHash(permit); (uint8 v, bytes32 r, bytes32 s) = vm.sign(ownerPrivateKey, digest); vm.expectRevert(); permitToken.permit( permit.owner, permit.spender, permit.value, permit.deadline, v, r, s ); } function test_permit_signatureReplay() public { _mintPermitTokens(owner, permitAmount); SigUtils.Permit memory permit = SigUtils.Permit({ owner: owner, spender: spender, value: permitAmount, nonce: 0, deadline: 1 days }); bytes32 digest = sigUtils.getTypedDataHash(permit); (uint8 v, bytes32 r, bytes32 s) = vm.sign(ownerPrivateKey, digest); permitToken.permit( permit.owner, permit.spender, permit.value, permit.deadline, v, r, s ); vm.expectRevert(); permitToken.permit( permit.owner, permit.spender, permit.value, permit.deadline, v, r, s ); } function test_permit_transferFromLimitedPermit() public { _mintPermitTokens(owner, permitAmount); SigUtils.Permit memory permit = SigUtils.Permit({ owner: owner, spender: spender, value: permitAmount, nonce: 0, deadline: 1 days }); bytes32 digest = sigUtils.getTypedDataHash(permit); (uint8 v, bytes32 r, bytes32 s) = vm.sign(ownerPrivateKey, digest); permitToken.permit( permit.owner, permit.spender, permit.value, permit.deadline, v, r, s ); vm.prank(spender); permitToken.transferFrom(owner, spender, permitAmount); assertEq(permitToken.balanceOf(owner), 0); assertEq(permitToken.balanceOf(spender), permitAmount); assertEq(permitToken.allowance(owner, spender), 0); } function test_permit_transferFromMaxPermit() public { _mintPermitTokens(owner, permitAmount); SigUtils.Permit memory permit = SigUtils.Permit({ owner: owner, spender: spender, value: maxPermitAmount, nonce: 0, deadline: 1 days }); bytes32 digest = sigUtils.getTypedDataHash(permit); (uint8 v, bytes32 r, bytes32 s) = vm.sign(ownerPrivateKey, digest); permitToken.permit( permit.owner, permit.spender, permit.value, permit.deadline, v, r, s ); vm.prank(spender); permitToken.transferFrom(owner, spender, permitAmount); assertEq(permitToken.balanceOf(owner), 0); assertEq(permitToken.balanceOf(spender), permitAmount); assertEq(permitToken.allowance(owner, spender), maxPermitAmount); } function test_permit_invalidAllowance() public { _mintPermitTokens(owner, permitAmount); SigUtils.Permit memory permit = SigUtils.Permit({ owner: owner, spender: spender, value: 1, nonce: 0, deadline: 1 days }); bytes32 digest = sigUtils.getTypedDataHash(permit); (uint8 v, bytes32 r, bytes32 s) = vm.sign(ownerPrivateKey, digest); permitToken.permit( permit.owner, permit.spender, permit.value, permit.deadline, v, r, s ); vm.expectRevert(); vm.prank(spender); permitToken.transferFrom(owner, spender, permitAmount); } function test_permit_ivnalidBalance() public { _mintPermitTokens(owner, permitAmount); SigUtils.Permit memory permit = SigUtils.Permit({ owner: owner, spender: spender, value: 2, nonce: 0, deadline: 1 days }); bytes32 digest = sigUtils.getTypedDataHash(permit); (uint8 v, bytes32 r, bytes32 s) = vm.sign(ownerPrivateKey, digest); permitToken.permit( permit.owner, permit.spender, permit.value, permit.deadline, v, r, s ); vm.expectRevert(); vm.prank(spender); permitToken.transferFrom(owner, spender, permitAmount); } function _mintPermitTokens(address who, uint256 permitAmount) internal virtual; }