From b9192083fdbd8502313022e937229e507ba60e16 Mon Sep 17 00:00:00 2001 From: MyTH-zyxeon Date: Wed, 20 May 2026 15:23:26 +0900 Subject: [PATCH] test: add permit fuzz coverage --- forge-test/Stablecoin.t.sol | 242 +++++++++++++++++++++--------------- 1 file changed, 141 insertions(+), 101 deletions(-) diff --git a/forge-test/Stablecoin.t.sol b/forge-test/Stablecoin.t.sol index 8ec5809..ebb2d32 100644 --- a/forge-test/Stablecoin.t.sol +++ b/forge-test/Stablecoin.t.sol @@ -5,6 +5,13 @@ import "forge-std/Test.sol"; import "../contracts/Stablecoin.sol"; contract StablecoinTest is Test { + bytes32 private constant PERMIT_TYPEHASH = keccak256( + "Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)" + ); + bytes32 private constant EIP712_DOMAIN_TYPEHASH = keccak256( + "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)" + ); + Stablecoin public coin; address public admin = address(1); address public user1 = address(2); @@ -146,6 +153,115 @@ contract StablecoinTest is Test { assertEq(coin.balanceOf(user2), 500_000); } + // ==================== Permit ==================== + + function testFuzz_permitSetsAllowanceAndIncrementsNonce( + uint256 ownerKey, + address spender, + uint256 value, + uint256 deadline + ) public { + ownerKey = bound(ownerKey, 1, type(uint128).max); + value = bound(value, 0, type(uint128).max); + deadline = bound(deadline, block.timestamp, type(uint64).max); + address owner = vm.addr(ownerKey); + + (uint8 v, bytes32 r, bytes32 s) = _signPermit( + ownerKey, owner, spender, value, coin.nonces(owner), deadline, coin.DOMAIN_SEPARATOR() + ); + + coin.permit(owner, spender, value, deadline, v, r, s); + + assertEq(coin.allowance(owner, spender), value); + assertEq(coin.nonces(owner), 1); + } + + function testFuzz_expiredPermitReverts(uint256 ownerKey, address spender, uint256 value) + public + { + ownerKey = bound(ownerKey, 1, type(uint128).max); + value = bound(value, 0, type(uint128).max); + vm.warp(10); + uint256 expiredDeadline = block.timestamp - 1; + address owner = vm.addr(ownerKey); + + (uint8 v, bytes32 r, bytes32 s) = _signPermit( + ownerKey, + owner, + spender, + value, + coin.nonces(owner), + expiredDeadline, + coin.DOMAIN_SEPARATOR() + ); + + vm.expectRevert(); + coin.permit(owner, spender, value, expiredDeadline, v, r, s); + } + + function testFuzz_wrongChainPermitReverts( + uint256 ownerKey, + address spender, + uint256 value, + uint256 deadline + ) public { + ownerKey = bound(ownerKey, 1, type(uint128).max); + value = bound(value, 0, type(uint128).max); + deadline = bound(deadline, block.timestamp, type(uint64).max); + address owner = vm.addr(ownerKey); + bytes32 wrongChainDomain = _domainSeparator(block.chainid + 1, address(coin)); + + (uint8 v, bytes32 r, bytes32 s) = _signPermit( + ownerKey, owner, spender, value, coin.nonces(owner), deadline, wrongChainDomain + ); + + vm.expectRevert(); + coin.permit(owner, spender, value, deadline, v, r, s); + } + + function testFuzz_wrongVerifyingContractPermitReverts( + uint256 ownerKey, + address spender, + uint256 value, + uint256 deadline, + address wrongContract + ) public { + ownerKey = bound(ownerKey, 1, type(uint128).max); + value = bound(value, 0, type(uint128).max); + deadline = bound(deadline, block.timestamp, type(uint64).max); + vm.assume(wrongContract != address(coin)); + address owner = vm.addr(ownerKey); + bytes32 wrongContractDomain = _domainSeparator(block.chainid, wrongContract); + + (uint8 v, bytes32 r, bytes32 s) = _signPermit( + ownerKey, owner, spender, value, coin.nonces(owner), deadline, wrongContractDomain + ); + + vm.expectRevert(); + coin.permit(owner, spender, value, deadline, v, r, s); + } + + function testFuzz_permitReplayReverts( + uint256 ownerKey, + address spender, + uint256 value, + uint256 deadline + ) public { + ownerKey = bound(ownerKey, 1, type(uint128).max); + value = bound(value, 0, type(uint128).max); + deadline = bound(deadline, block.timestamp, type(uint64).max); + address owner = vm.addr(ownerKey); + + (uint8 v, bytes32 r, bytes32 s) = _signPermit( + ownerKey, owner, spender, value, coin.nonces(owner), deadline, coin.DOMAIN_SEPARATOR() + ); + + coin.permit(owner, spender, value, deadline, v, r, s); + + vm.expectRevert(); + coin.permit(owner, spender, value, deadline, v, r, s); + } + // ==================== Fuzz Tests ==================== function testFuzz_mintAnyAmount(uint256 amount) public { @@ -197,111 +313,35 @@ contract StablecoinTest is Test { assertEq(coin.balanceOf(user1) + coin.balanceOf(user2), amount); } - // ==================== EIP-2612 Permit Tests ==================== - - function _deployCoinWithOwner() internal returns (Stablecoin c, address owner_, uint256 ownerKey_) { - ownerKey_ = 0x1234567890123456789012345678901234567890123456789012345678901234; - owner_ = vm.addr(ownerKey_); - vm.prank(owner_); - c = new Stablecoin("Test Stablecoin", "TSTBL", owner_); - vm.prank(owner_); - c.mint(owner_, 1_000_000 ether); - } - - function testFuzz_permitValid(uint256 value, uint256 deadline) public { - (Stablecoin c, address owner_, uint256 ownerKey_) = _deployCoinWithOwner(); - address spender_ = address(0x5678); - vm.assume(value > 0 && value <= 1_000_000 ether); - vm.assume(deadline > block.timestamp); - - uint256 nonce = c.nonces(owner_); - bytes32 digest = keccak256( - abi.encodePacked( - "\x19\x01", - c.DOMAIN_SEPARATOR(), - keccak256( - abi.encode( - keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"), - owner_, - spender_, - value, - nonce, - deadline - ) - ) - ) + function _signPermit( + uint256 ownerKey, + address owner, + address spender, + uint256 value, + uint256 nonce, + uint256 deadline, + bytes32 domainSeparator + ) internal pure returns (uint8 v, bytes32 r, bytes32 s) { + bytes32 structHash = keccak256( + abi.encode(PERMIT_TYPEHASH, owner, spender, value, nonce, deadline) ); - - (uint8 v, bytes32 r, bytes32 s) = vm.sign(ownerKey_, digest); - - vm.prank(spender_); - c.permit(owner_, spender_, value, deadline, v, r, s); - - assertEq(c.allowance(owner_, spender_), value); - assertEq(c.nonces(owner_), nonce + 1); + bytes32 digest = keccak256(abi.encodePacked("\x19\x01", domainSeparator, structHash)); + return vm.sign(ownerKey, digest); } - function testFuzz_permitExpiredDeadlineReverts(uint256 value) public { - (Stablecoin c, address owner_, uint256 ownerKey_) = _deployCoinWithOwner(); - address spender_ = address(0x5678); - vm.assume(value > 0 && value <= 1_000_000 ether); - uint256 deadline = block.timestamp - 1; - - bytes32 digest = keccak256( - abi.encodePacked( - "\x19\x01", - c.DOMAIN_SEPARATOR(), - keccak256( - abi.encode( - keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"), - owner_, - spender_, - value, - c.nonces(owner_), - deadline - ) - ) + function _domainSeparator(uint256 chainId, address verifyingContract) + internal + pure + returns (bytes32) + { + return keccak256( + abi.encode( + EIP712_DOMAIN_TYPEHASH, + keccak256(bytes("Test Stablecoin")), + keccak256(bytes("1")), + chainId, + verifyingContract ) ); - - (uint8 v, bytes32 r, bytes32 s) = vm.sign(ownerKey_, digest); - - vm.prank(spender_); - vm.expectRevert(); - c.permit(owner_, spender_, value, deadline, v, r, s); - } - - - function testFuzz_permitReplayReverts(uint256 value, uint256 deadline) public { - (Stablecoin c, address owner_, uint256 ownerKey_) = _deployCoinWithOwner(); - address spender_ = address(0x5678); - vm.assume(value > 0 && value <= 1_000_000 ether); - vm.assume(deadline > block.timestamp); - - bytes32 digest = keccak256( - abi.encodePacked( - "\x19\x01", - c.DOMAIN_SEPARATOR(), - keccak256( - abi.encode( - keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"), - owner_, - spender_, - value, - c.nonces(owner_), - deadline - ) - ) - ) - ); - - (uint8 v, bytes32 r, bytes32 s) = vm.sign(ownerKey_, digest); - - vm.prank(spender_); - c.permit(owner_, spender_, value, deadline, v, r, s); - - vm.prank(spender_); - vm.expectRevert(); - c.permit(owner_, spender_, value, deadline, v, r, s); } }