diff --git a/src/token/ERC20/ERC20/ERC20BurnFacet.sol b/src/token/ERC20/ERC20/ERC20BurnFacet.sol new file mode 100644 index 00000000..7cc5eca0 --- /dev/null +++ b/src/token/ERC20/ERC20/ERC20BurnFacet.sol @@ -0,0 +1,91 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +contract ERC20BurnFacet { + /// @notice Thrown when an account has insufficient balance for a transfer or burn. + /// @param _sender Address attempting the transfer. + /// @param _balance Current balance of the sender. + /// @param _needed Amount required to complete the operation. + error ERC20InsufficientBalance(address _sender, uint256 _balance, uint256 _needed); + + /// @notice Thrown when a spender tries to use more than the approved allowance. + /// @param _spender Address attempting to spend. + /// @param _allowance Current allowance for the spender. + /// @param _needed Amount required to complete the operation. + error ERC20InsufficientAllowance(address _spender, uint256 _allowance, uint256 _needed); + + /// @notice Emitted when tokens are transferred between two addresses. + /// @param _from Address sending the tokens. + /// @param _to Address receiving the tokens. + /// @param _value Amount of tokens transferred. + event Transfer(address indexed _from, address indexed _to, uint256 _value); + + // @dev Storage position determined by the keccak256 hash of the diamond storage identifier. + bytes32 constant STORAGE_POSITION = keccak256("compose.erc20"); + + /** + * @dev ERC-8042 compliant storage struct for ERC20 token data. + * @custom:storage-location erc8042:compose.erc20 + */ + struct ERC20Storage { + mapping(address owner => uint256 balance) balanceOf; + uint256 totalSupply; + mapping(address owner => mapping(address spender => uint256 allowance)) allowances; + } + + /** + * @notice Returns the ERC20 storage struct from the predefined diamond storage slot. + * @dev Uses inline assembly to set the storage slot reference. + * @return s The ERC20 storage struct reference. + */ + function getStorage() internal pure returns (ERC20Storage storage s) { + bytes32 position = STORAGE_POSITION; + assembly { + s.slot := position + } + } + + /** + * @notice Burns (destroys) a specific amount of tokens from the caller's balance. + * @dev Emits a {Transfer} event to the zero address. + * @param _value The amount of tokens to burn. + */ + function burn(uint256 _value) external { + ERC20Storage storage s = getStorage(); + uint256 balance = s.balanceOf[msg.sender]; + if (balance < _value) { + revert ERC20InsufficientBalance(msg.sender, balance, _value); + } + unchecked { + s.balanceOf[msg.sender] = balance - _value; + s.totalSupply -= _value; + } + emit Transfer(msg.sender, address(0), _value); + } + + /** + * @notice Burns tokens from another account, deducting from the caller's allowance. + * @dev Emits a {Transfer} event to the zero address. + * @param _account The address whose tokens will be burned. + * @param _value The amount of tokens to burn. + */ + function burnFrom(address _account, uint256 _value) external { + ERC20Storage storage s = getStorage(); + uint256 currentAllowance = s.allowances[_account][msg.sender]; + if (currentAllowance < _value) { + revert ERC20InsufficientAllowance(msg.sender, currentAllowance, _value); + } + uint256 balance = s.balanceOf[_account]; + if (balance < _value) { + revert ERC20InsufficientBalance(_account, balance, _value); + } + unchecked { + if (currentAllowance != type(uint256).max) { + s.allowances[_account][msg.sender] = currentAllowance - _value; + } + s.balanceOf[_account] = balance - _value; + s.totalSupply -= _value; + } + emit Transfer(_account, address(0), _value); + } +} diff --git a/src/token/ERC20/ERC20/ERC20Facet.sol b/src/token/ERC20/ERC20/ERC20Facet.sol index c1ca976d..e4a8280f 100644 --- a/src/token/ERC20/ERC20/ERC20Facet.sol +++ b/src/token/ERC20/ERC20/ERC20Facet.sol @@ -26,18 +26,6 @@ contract ERC20Facet { /// @param _spender Invalid spender address. error ERC20InvalidSpender(address _spender); - /// @notice Thrown when a permit signature is invalid or expired. - /// @param _owner The address that signed the permit. - /// @param _spender The address that was approved. - /// @param _value The amount that was approved. - /// @param _deadline The deadline for the permit. - /// @param _v The recovery byte of the signature. - /// @param _r The r value of the signature. - /// @param _s The s value of the signature. - error ERC2612InvalidSignature( - address _owner, address _spender, uint256 _value, uint256 _deadline, uint8 _v, bytes32 _r, bytes32 _s - ); - /// @notice Emitted when an approval is made for a spender by an owner. /// @param _owner The address granting the allowance. /// @param _spender The address receiving the allowance. @@ -58,13 +46,12 @@ contract ERC20Facet { * @custom:storage-location erc8042:compose.erc20 */ struct ERC20Storage { - string name; - string symbol; - uint8 decimals; - uint256 totalSupply; mapping(address owner => uint256 balance) balanceOf; + uint256 totalSupply; mapping(address owner => mapping(address spender => uint256 allowance)) allowances; - mapping(address owner => uint256) nonces; + uint8 decimals; + string name; + string symbol; } /** @@ -205,143 +192,4 @@ contract ERC20Facet { emit Transfer(_from, _to, _value); return true; } - - /** - * @notice Burns (destroys) a specific amount of tokens from the caller's balance. - * @dev Emits a {Transfer} event to the zero address. - * @param _value The amount of tokens to burn. - */ - function burn(uint256 _value) external { - ERC20Storage storage s = getStorage(); - uint256 balance = s.balanceOf[msg.sender]; - if (balance < _value) { - revert ERC20InsufficientBalance(msg.sender, balance, _value); - } - unchecked { - s.balanceOf[msg.sender] = balance - _value; - s.totalSupply -= _value; - } - emit Transfer(msg.sender, address(0), _value); - } - - /** - * @notice Burns tokens from another account, deducting from the caller's allowance. - * @dev Emits a {Transfer} event to the zero address. - * @param _account The address whose tokens will be burned. - * @param _value The amount of tokens to burn. - */ - function burnFrom(address _account, uint256 _value) external { - ERC20Storage storage s = getStorage(); - uint256 currentAllowance = s.allowances[_account][msg.sender]; - if (currentAllowance < _value) { - revert ERC20InsufficientAllowance(msg.sender, currentAllowance, _value); - } - uint256 balance = s.balanceOf[_account]; - if (balance < _value) { - revert ERC20InsufficientBalance(_account, balance, _value); - } - unchecked { - if (currentAllowance != type(uint256).max) { - s.allowances[_account][msg.sender] = currentAllowance - _value; - } - s.balanceOf[_account] = balance - _value; - s.totalSupply -= _value; - } - emit Transfer(_account, address(0), _value); - } - - // EIP-2612 Permit Extension - - /** - * @notice Returns the current nonce for an owner. - * @dev This value changes each time a permit is used. - * @param _owner The address of the owner. - * @return The current nonce. - */ - function nonces(address _owner) external view returns (uint256) { - return getStorage().nonces[_owner]; - } - - /** - * @notice Returns the domain separator used in the encoding of the signature for {permit}. - * @dev This value is unique to a contract and chain ID combination to prevent replay attacks. - * @return The domain separator. - */ - function DOMAIN_SEPARATOR() external view returns (bytes32) { - return keccak256( - abi.encode( - keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"), - keccak256(bytes(getStorage().name)), - keccak256("1"), - block.chainid, - address(this) - ) - ); - } - - /** - * @notice Sets the allowance for a spender via a signature. - * @dev This function implements EIP-2612 permit functionality. - * @param _owner The address of the token owner. - * @param _spender The address of the spender. - * @param _value The amount of tokens to approve. - * @param _deadline The deadline for the permit (timestamp). - * @param _v The recovery byte of the signature. - * @param _r The r value of the signature. - * @param _s The s value of the signature. - */ - function permit( - address _owner, - address _spender, - uint256 _value, - uint256 _deadline, - uint8 _v, - bytes32 _r, - bytes32 _s - ) external { - if (_spender == address(0)) { - revert ERC20InvalidSpender(address(0)); - } - if (block.timestamp > _deadline) { - revert ERC2612InvalidSignature(_owner, _spender, _value, _deadline, _v, _r, _s); - } - - ERC20Storage storage s = getStorage(); - uint256 currentNonce = s.nonces[_owner]; - bytes32 structHash = keccak256( - abi.encode( - keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"), - _owner, - _spender, - _value, - currentNonce, - _deadline - ) - ); - - bytes32 hash = keccak256( - abi.encodePacked( - "\x19\x01", - keccak256( - abi.encode( - keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"), - keccak256(bytes(s.name)), - keccak256("1"), - block.chainid, - address(this) - ) - ), - structHash - ) - ); - - address signer = ecrecover(hash, _v, _r, _s); - if (signer != _owner || signer == address(0)) { - revert ERC2612InvalidSignature(_owner, _spender, _value, _deadline, _v, _r, _s); - } - - s.allowances[_owner][_spender] = _value; - s.nonces[_owner] = currentNonce + 1; - emit Approval(_owner, _spender, _value); - } } diff --git a/src/token/ERC20/ERC20/ERC20PermitFacet.sol b/src/token/ERC20/ERC20/ERC20PermitFacet.sol new file mode 100644 index 00000000..462283ff --- /dev/null +++ b/src/token/ERC20/ERC20/ERC20PermitFacet.sol @@ -0,0 +1,159 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +contract ERC20PermitFacet { + /// @notice Thrown when a permit signature is invalid or expired. + /// @param _owner The address that signed the permit. + /// @param _spender The address that was approved. + /// @param _value The amount that was approved. + /// @param _deadline The deadline for the permit. + /// @param _v The recovery byte of the signature. + /// @param _r The r value of the signature. + /// @param _s The s value of the signature. + error ERC2612InvalidSignature( + address _owner, address _spender, uint256 _value, uint256 _deadline, uint8 _v, bytes32 _r, bytes32 _s + ); + + /// @notice Thrown when the spender address is invalid (e.g., zero address). + /// @param _spender Invalid spender address. + error ERC20InvalidSpender(address _spender); + + /// @notice Emitted when an approval is made for a spender by an owner. + /// @param _owner The address granting the allowance. + /// @param _spender The address receiving the allowance. + /// @param _value The amount approved. + event Approval(address indexed _owner, address indexed _spender, uint256 _value); + + bytes32 constant ERC20_STORAGE_POSITION = keccak256("compose.erc20"); + + /// The `ERC20PermitFacet` only uses the `allowances` and `name` variables inside + /// the `ERC20Storage` struct from `ERC20Facet`. + /// We cannot remove the `balanceOf`, `totalSupply`, and `decimals` variables from + /// the struct even though they aren't used. This is because we must maintain the + /// order of variables defined in structs. Only variables at the end of structs can be + /// removed. In this case there is only one variable at the end that isn't used and that + /// is the `symbol` variable so that is removed from the struct below. + /// @custom:storage-location erc8042:compose.erc20 + struct ERC20Storage { + mapping(address owner => uint256 balance) balanceOf; + uint256 totalSupply; + mapping(address owner => mapping(address spender => uint256 allowance)) allowances; + uint8 decimals; + string name; + } + + function getERC20Storage() internal pure returns (ERC20Storage storage s) { + bytes32 position = ERC20_STORAGE_POSITION; + assembly { + s.slot := position + } + } + + bytes32 constant STORAGE_POSITION = keccak256("compose.erc20.permit"); + + /// @custom:storage-location erc8042:compose.erc20.permit + struct ERC20PermitStorage { + mapping(address owner => uint256) nonces; + } + + function getStorage() internal pure returns (ERC20PermitStorage storage s) { + bytes32 position = STORAGE_POSITION; + assembly { + s.slot := position + } + } + + /** + * @notice Returns the current nonce for an owner. + * @dev This value changes each time a permit is used. + * @param _owner The address of the owner. + * @return The current nonce. + */ + function nonces(address _owner) external view returns (uint256) { + return getStorage().nonces[_owner]; + } + + /** + * @notice Returns the domain separator used in the encoding of the signature for {permit}. + * @dev This value is unique to a contract and chain ID combination to prevent replay attacks. + * @return The domain separator. + */ + function DOMAIN_SEPARATOR() external view returns (bytes32) { + return keccak256( + abi.encode( + keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"), + keccak256(bytes(getERC20Storage().name)), + keccak256("1"), + block.chainid, + address(this) + ) + ); + } + + /** + * @notice Sets the allowance for a spender via a signature. + * @dev This function implements EIP-2612 permit functionality. + * @param _owner The address of the token owner. + * @param _spender The address of the spender. + * @param _value The amount of tokens to approve. + * @param _deadline The deadline for the permit (timestamp). + * @param _v The recovery byte of the signature. + * @param _r The r value of the signature. + * @param _s The s value of the signature. + */ + function permit( + address _owner, + address _spender, + uint256 _value, + uint256 _deadline, + uint8 _v, + bytes32 _r, + bytes32 _s + ) external { + if (_spender == address(0)) { + revert ERC20InvalidSpender(address(0)); + } + if (block.timestamp > _deadline) { + revert ERC2612InvalidSignature(_owner, _spender, _value, _deadline, _v, _r, _s); + } + + ERC20PermitStorage storage s = getStorage(); + ERC20Storage storage sERC20 = getERC20Storage(); + uint256 currentNonce = s.nonces[_owner]; + bytes32 structHash = keccak256( + abi.encode( + keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"), + _owner, + _spender, + _value, + currentNonce, + _deadline + ) + ); + + bytes32 hash = keccak256( + abi.encodePacked( + "\x19\x01", + keccak256( + abi.encode( + keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"), + keccak256(bytes(sERC20.name)), + keccak256("1"), + block.chainid, + address(this) + ) + ), + structHash + ) + ); + + address signer = ecrecover(hash, _v, _r, _s); + if (signer != _owner || signer == address(0)) { + revert ERC2612InvalidSignature(_owner, _spender, _value, _deadline, _v, _r, _s); + } + + sERC20.allowances[_owner][_spender] = _value; + s.nonces[_owner] = currentNonce + 1; + emit Approval(_owner, _spender, _value); + } +} diff --git a/test/token/ERC20/ERC20/ERC20BurnFacet.t.sol b/test/token/ERC20/ERC20/ERC20BurnFacet.t.sol new file mode 100644 index 00000000..5201a8a0 --- /dev/null +++ b/test/token/ERC20/ERC20/ERC20BurnFacet.t.sol @@ -0,0 +1,203 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +import {Test} from "forge-std/Test.sol"; +import {ERC20BurnFacet} from "../../../../src/token/ERC20/ERC20/ERC20BurnFacet.sol"; +import {ERC20BurnFacetHarness} from "./harnesses/ERC20BurnFacetHarness.sol"; + +contract ERC20BurnFacetTest is Test { + ERC20BurnFacetHarness public token; + + address public alice; + address public bob; + address public charlie; + + uint256 constant INITIAL_SUPPLY = 1000000e18; + + event Transfer(address indexed _from, address indexed _to, uint256 _value); + event Approval(address indexed _owner, address indexed _spender, uint256 _value); + + function setUp() public { + alice = makeAddr("alice"); + bob = makeAddr("bob"); + charlie = makeAddr("charlie"); + + token = new ERC20BurnFacetHarness(); + token.mint(alice, INITIAL_SUPPLY); + } + + function test_Burn() public { + uint256 amount = 100e18; + + vm.prank(alice); + vm.expectEmit(true, true, true, true); + emit Transfer(alice, address(0), amount); + token.burn(amount); + + assertEq(token.balanceOf(alice), INITIAL_SUPPLY - amount); + assertEq(token.totalSupply(), INITIAL_SUPPLY - amount); + } + + function test_Burn_EntireBalance() public { + vm.prank(alice); + token.burn(INITIAL_SUPPLY); + + assertEq(token.balanceOf(alice), 0); + assertEq(token.totalSupply(), 0); + } + + function testFuzz_Burn(uint256 amount) public { + vm.assume(amount <= INITIAL_SUPPLY); + + vm.prank(alice); + token.burn(amount); + + assertEq(token.balanceOf(alice), INITIAL_SUPPLY - amount); + assertEq(token.totalSupply(), INITIAL_SUPPLY - amount); + } + + function test_RevertWhen_BurnInsufficientBalance() public { + uint256 amount = INITIAL_SUPPLY + 1; + + vm.prank(alice); + vm.expectRevert( + abi.encodeWithSelector(ERC20BurnFacet.ERC20InsufficientBalance.selector, alice, INITIAL_SUPPLY, amount) + ); + token.burn(amount); + } + + function test_RevertWhen_BurnFromZeroBalance() public { + vm.prank(bob); + vm.expectRevert(abi.encodeWithSelector(ERC20BurnFacet.ERC20InsufficientBalance.selector, bob, 0, 1)); + token.burn(1); + } + + function test_BurnFrom() public { + uint256 amount = 100e18; + + vm.prank(alice); + token.approve(bob, amount); + + vm.prank(bob); + vm.expectEmit(true, true, true, true); + emit Transfer(alice, address(0), amount); + token.burnFrom(alice, amount); + + assertEq(token.balanceOf(alice), INITIAL_SUPPLY - amount); + assertEq(token.allowance(alice, bob), 0); + assertEq(token.totalSupply(), INITIAL_SUPPLY - amount); + } + + function test_BurnFrom_PartialAllowance() public { + uint256 allowanceAmount = 200e18; + uint256 burnAmount = 100e18; + + vm.prank(alice); + token.approve(bob, allowanceAmount); + + vm.prank(bob); + token.burnFrom(alice, burnAmount); + + assertEq(token.balanceOf(alice), INITIAL_SUPPLY - burnAmount); + assertEq(token.allowance(alice, bob), allowanceAmount - burnAmount); + assertEq(token.totalSupply(), INITIAL_SUPPLY - burnAmount); + } + + function testFuzz_BurnFrom(uint256 approval, uint256 amount) public { + vm.assume(approval <= INITIAL_SUPPLY); + vm.assume(amount <= approval); + + vm.prank(alice); + token.approve(bob, approval); + + vm.prank(bob); + token.burnFrom(alice, amount); + + assertEq(token.balanceOf(alice), INITIAL_SUPPLY - amount); + assertEq(token.allowance(alice, bob), approval - amount); + assertEq(token.totalSupply(), INITIAL_SUPPLY - amount); + } + + function test_BurnFrom_UnlimitedAllowance() public { + uint256 amount = 100e18; + uint256 maxAllowance = type(uint256).max; + + // Set unlimited allowance + vm.prank(alice); + token.approve(bob, maxAllowance); + + // Perform first burn + vm.prank(bob); + token.burnFrom(alice, amount); + + // Check that allowance remains unchanged (unlimited) + assertEq(token.allowance(alice, bob), maxAllowance); + assertEq(token.balanceOf(alice), INITIAL_SUPPLY - amount); + assertEq(token.totalSupply(), INITIAL_SUPPLY - amount); + + // Perform second burn to verify allowance is still unlimited + vm.prank(bob); + token.burnFrom(alice, amount); + + // Check that allowance is still unchanged (unlimited) + assertEq(token.allowance(alice, bob), maxAllowance); + assertEq(token.balanceOf(alice), INITIAL_SUPPLY - 2 * amount); + assertEq(token.totalSupply(), INITIAL_SUPPLY - 2 * amount); + } + + function test_BurnFrom_UnlimitedAllowance_MultipleBurns() public { + uint256 maxAllowance = type(uint256).max; + uint256 burnAmount = 50e18; + uint256 numBurns = 10; + + // Set unlimited allowance + vm.prank(alice); + token.approve(bob, maxAllowance); + + // Perform multiple burns + for (uint256 i = 0; i < numBurns; i++) { + vm.prank(bob); + token.burnFrom(alice, burnAmount); + + // Verify allowance remains unlimited after each burn + assertEq(token.allowance(alice, bob), maxAllowance); + } + + // Verify final balances and total supply + assertEq(token.balanceOf(alice), INITIAL_SUPPLY - (burnAmount * numBurns)); + assertEq(token.totalSupply(), INITIAL_SUPPLY - (burnAmount * numBurns)); + } + + function test_RevertWhen_BurnFromInsufficientAllowance() public { + uint256 allowanceAmount = 50e18; + uint256 burnAmount = 100e18; + + vm.prank(alice); + token.approve(bob, allowanceAmount); + + vm.prank(bob); + vm.expectRevert( + abi.encodeWithSelector(ERC20BurnFacet.ERC20InsufficientAllowance.selector, bob, allowanceAmount, burnAmount) + ); + token.burnFrom(alice, burnAmount); + } + + function test_RevertWhen_BurnFromInsufficientBalance() public { + uint256 amount = INITIAL_SUPPLY + 1; + + vm.prank(alice); + token.approve(bob, amount); + + vm.prank(bob); + vm.expectRevert( + abi.encodeWithSelector(ERC20BurnFacet.ERC20InsufficientBalance.selector, alice, INITIAL_SUPPLY, amount) + ); + token.burnFrom(alice, amount); + } + + function test_RevertWhen_BurnFromNoAllowance() public { + vm.prank(bob); + vm.expectRevert(abi.encodeWithSelector(ERC20BurnFacet.ERC20InsufficientAllowance.selector, bob, 0, 100e18)); + token.burnFrom(alice, 100e18); + } +} diff --git a/test/token/ERC20/ERC20/ERC20Facet.t.sol b/test/token/ERC20/ERC20/ERC20Facet.t.sol index 88a2308f..818d36cd 100644 --- a/test/token/ERC20/ERC20/ERC20Facet.t.sol +++ b/test/token/ERC20/ERC20/ERC20Facet.t.sol @@ -372,659 +372,4 @@ contract ERC20FacetTest is Test { vm.expectRevert(abi.encodeWithSelector(ERC20Facet.ERC20InsufficientAllowance.selector, bob, 0, 100e18)); token.transferFrom(alice, charlie, 100e18); } - - // ============================================ - // Burn Tests - // ============================================ - - function test_Burn() public { - uint256 amount = 100e18; - - vm.prank(alice); - vm.expectEmit(true, true, true, true); - emit Transfer(alice, address(0), amount); - token.burn(amount); - - assertEq(token.balanceOf(alice), INITIAL_SUPPLY - amount); - assertEq(token.totalSupply(), INITIAL_SUPPLY - amount); - } - - function test_Burn_EntireBalance() public { - vm.prank(alice); - token.burn(INITIAL_SUPPLY); - - assertEq(token.balanceOf(alice), 0); - assertEq(token.totalSupply(), 0); - } - - function testFuzz_Burn(uint256 amount) public { - vm.assume(amount <= INITIAL_SUPPLY); - - vm.prank(alice); - token.burn(amount); - - assertEq(token.balanceOf(alice), INITIAL_SUPPLY - amount); - assertEq(token.totalSupply(), INITIAL_SUPPLY - amount); - } - - function test_RevertWhen_BurnInsufficientBalance() public { - uint256 amount = INITIAL_SUPPLY + 1; - - vm.prank(alice); - vm.expectRevert( - abi.encodeWithSelector(ERC20Facet.ERC20InsufficientBalance.selector, alice, INITIAL_SUPPLY, amount) - ); - token.burn(amount); - } - - function test_RevertWhen_BurnFromZeroBalance() public { - vm.prank(bob); - vm.expectRevert(abi.encodeWithSelector(ERC20Facet.ERC20InsufficientBalance.selector, bob, 0, 1)); - token.burn(1); - } - - // ============================================ - // BurnFrom Tests - // ============================================ - - function test_BurnFrom() public { - uint256 amount = 100e18; - - vm.prank(alice); - token.approve(bob, amount); - - vm.prank(bob); - vm.expectEmit(true, true, true, true); - emit Transfer(alice, address(0), amount); - token.burnFrom(alice, amount); - - assertEq(token.balanceOf(alice), INITIAL_SUPPLY - amount); - assertEq(token.allowance(alice, bob), 0); - assertEq(token.totalSupply(), INITIAL_SUPPLY - amount); - } - - function test_BurnFrom_PartialAllowance() public { - uint256 allowanceAmount = 200e18; - uint256 burnAmount = 100e18; - - vm.prank(alice); - token.approve(bob, allowanceAmount); - - vm.prank(bob); - token.burnFrom(alice, burnAmount); - - assertEq(token.balanceOf(alice), INITIAL_SUPPLY - burnAmount); - assertEq(token.allowance(alice, bob), allowanceAmount - burnAmount); - assertEq(token.totalSupply(), INITIAL_SUPPLY - burnAmount); - } - - function testFuzz_BurnFrom(uint256 approval, uint256 amount) public { - vm.assume(approval <= INITIAL_SUPPLY); - vm.assume(amount <= approval); - - vm.prank(alice); - token.approve(bob, approval); - - vm.prank(bob); - token.burnFrom(alice, amount); - - assertEq(token.balanceOf(alice), INITIAL_SUPPLY - amount); - assertEq(token.allowance(alice, bob), approval - amount); - assertEq(token.totalSupply(), INITIAL_SUPPLY - amount); - } - - function test_BurnFrom_UnlimitedAllowance() public { - uint256 amount = 100e18; - uint256 maxAllowance = type(uint256).max; - - // Set unlimited allowance - vm.prank(alice); - token.approve(bob, maxAllowance); - - // Perform first burn - vm.prank(bob); - token.burnFrom(alice, amount); - - // Check that allowance remains unchanged (unlimited) - assertEq(token.allowance(alice, bob), maxAllowance); - assertEq(token.balanceOf(alice), INITIAL_SUPPLY - amount); - assertEq(token.totalSupply(), INITIAL_SUPPLY - amount); - - // Perform second burn to verify allowance is still unlimited - vm.prank(bob); - token.burnFrom(alice, amount); - - // Check that allowance is still unchanged (unlimited) - assertEq(token.allowance(alice, bob), maxAllowance); - assertEq(token.balanceOf(alice), INITIAL_SUPPLY - 2 * amount); - assertEq(token.totalSupply(), INITIAL_SUPPLY - 2 * amount); - } - - function test_BurnFrom_UnlimitedAllowance_MultipleBurns() public { - uint256 maxAllowance = type(uint256).max; - uint256 burnAmount = 50e18; - uint256 numBurns = 10; - - // Set unlimited allowance - vm.prank(alice); - token.approve(bob, maxAllowance); - - // Perform multiple burns - for (uint256 i = 0; i < numBurns; i++) { - vm.prank(bob); - token.burnFrom(alice, burnAmount); - - // Verify allowance remains unlimited after each burn - assertEq(token.allowance(alice, bob), maxAllowance); - } - - // Verify final balances and total supply - assertEq(token.balanceOf(alice), INITIAL_SUPPLY - (burnAmount * numBurns)); - assertEq(token.totalSupply(), INITIAL_SUPPLY - (burnAmount * numBurns)); - } - - function test_RevertWhen_BurnFromInsufficientAllowance() public { - uint256 allowanceAmount = 50e18; - uint256 burnAmount = 100e18; - - vm.prank(alice); - token.approve(bob, allowanceAmount); - - vm.prank(bob); - vm.expectRevert( - abi.encodeWithSelector(ERC20Facet.ERC20InsufficientAllowance.selector, bob, allowanceAmount, burnAmount) - ); - token.burnFrom(alice, burnAmount); - } - - function test_RevertWhen_BurnFromInsufficientBalance() public { - uint256 amount = INITIAL_SUPPLY + 1; - - vm.prank(alice); - token.approve(bob, amount); - - vm.prank(bob); - vm.expectRevert( - abi.encodeWithSelector(ERC20Facet.ERC20InsufficientBalance.selector, alice, INITIAL_SUPPLY, amount) - ); - token.burnFrom(alice, amount); - } - - function test_RevertWhen_BurnFromNoAllowance() public { - vm.prank(bob); - vm.expectRevert(abi.encodeWithSelector(ERC20Facet.ERC20InsufficientAllowance.selector, bob, 0, 100e18)); - token.burnFrom(alice, 100e18); - } - - // ============================================ - // EIP-2612 Permit Tests - // ============================================ - - function test_Nonces() public view { - assertEq(token.nonces(alice), 0); - assertEq(token.nonces(bob), 0); - } - - function test_DOMAIN_SEPARATOR() public view { - bytes32 expectedDomainSeparator = keccak256( - abi.encode( - keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"), - keccak256(bytes(TOKEN_NAME)), - keccak256("1"), - block.chainid, - address(token) - ) - ); - assertEq(token.DOMAIN_SEPARATOR(), expectedDomainSeparator); - } - - function test_DOMAIN_SEPARATOR_ConsistentWithinSameChain() public view { - // First call - computes domain separator - bytes32 separator1 = token.DOMAIN_SEPARATOR(); - - // Second call - recomputes and should return same value for same chain ID - bytes32 separator2 = token.DOMAIN_SEPARATOR(); - - assertEq(separator1, separator2); - } - - function test_DOMAIN_SEPARATOR_RecalculatesAfterFork() public { - // Get initial domain separator on chain 1 - uint256 originalChainId = block.chainid; - bytes32 separator1 = token.DOMAIN_SEPARATOR(); - - // Simulate chain fork (chain ID changes) - vm.chainId(originalChainId + 1); - - // Domain separator should recalculate with new chain ID - bytes32 separator2 = token.DOMAIN_SEPARATOR(); - - // Separators should be different - assertTrue(separator1 != separator2); - - // New separator should match expected value for new chain ID - bytes32 expectedSeparator = keccak256( - abi.encode( - keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"), - keccak256(bytes(TOKEN_NAME)), - keccak256("1"), - originalChainId + 1, - address(token) - ) - ); - assertEq(separator2, expectedSeparator); - } - - function test_Permit() public { - uint256 ownerPrivateKey = 0xA11CE; - address owner = vm.addr(ownerPrivateKey); - address spender = bob; - uint256 value = 100e18; - uint256 nonce = 0; - uint256 deadline = block.timestamp + 1 hours; - - bytes32 structHash = keccak256( - abi.encode( - keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"), - owner, - spender, - value, - nonce, - deadline - ) - ); - - bytes32 hash = keccak256(abi.encodePacked("\x19\x01", token.DOMAIN_SEPARATOR(), structHash)); - - (uint8 v, bytes32 r, bytes32 s) = vm.sign(ownerPrivateKey, hash); - - vm.expectEmit(true, true, true, true); - emit Approval(owner, spender, value); - token.permit(owner, spender, value, deadline, v, r, s); - - assertEq(token.allowance(owner, spender), value); - assertEq(token.nonces(owner), 1); - } - - function test_Permit_IncreasesNonce() public { - uint256 ownerPrivateKey = 0xB0B; - address owner = vm.addr(ownerPrivateKey); - uint256 deadline = block.timestamp + 1 hours; - - for (uint256 i = 0; i < 3; i++) { - bytes32 structHash = keccak256( - abi.encode( - keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"), - owner, - bob, - 100e18, - i, - deadline - ) - ); - - bytes32 hash = keccak256(abi.encodePacked("\x19\x01", token.DOMAIN_SEPARATOR(), structHash)); - (uint8 v, bytes32 r, bytes32 s) = vm.sign(ownerPrivateKey, hash); - - token.permit(owner, bob, 100e18, deadline, v, r, s); - assertEq(token.nonces(owner), i + 1); - } - } - - function test_RevertWhen_PermitExpired() public { - uint256 ownerPrivateKey = 0xA11CE; - address owner = vm.addr(ownerPrivateKey); - uint256 value = 100e18; - uint256 deadline = block.timestamp - 1; - - bytes32 structHash = keccak256( - abi.encode( - keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"), - owner, - bob, - value, - 0, - deadline - ) - ); - - bytes32 hash = keccak256(abi.encodePacked("\x19\x01", token.DOMAIN_SEPARATOR(), structHash)); - (uint8 v, bytes32 r, bytes32 s) = vm.sign(ownerPrivateKey, hash); - - vm.expectRevert( - abi.encodeWithSelector(ERC20Facet.ERC2612InvalidSignature.selector, owner, bob, value, deadline, v, r, s) - ); - token.permit(owner, bob, value, deadline, v, r, s); - } - - function test_RevertWhen_PermitInvalidSignature() public { - uint256 ownerPrivateKey = 0xA11CE; - address owner = vm.addr(ownerPrivateKey); - uint256 wrongPrivateKey = 0xBAD; - uint256 value = 100e18; - uint256 deadline = block.timestamp + 1 hours; - - bytes32 structHash = keccak256( - abi.encode( - keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"), - owner, - bob, - value, - 0, - deadline - ) - ); - - bytes32 hash = keccak256(abi.encodePacked("\x19\x01", token.DOMAIN_SEPARATOR(), structHash)); - (uint8 v, bytes32 r, bytes32 s) = vm.sign(wrongPrivateKey, hash); - - vm.expectRevert( - abi.encodeWithSelector(ERC20Facet.ERC2612InvalidSignature.selector, owner, bob, value, deadline, v, r, s) - ); - token.permit(owner, bob, value, deadline, v, r, s); - } - - function test_RevertWhen_PermitReplay() public { - uint256 ownerPrivateKey = 0xA11CE; - address owner = vm.addr(ownerPrivateKey); - uint256 value = 100e18; - uint256 deadline = block.timestamp + 1 hours; - - bytes32 structHash = keccak256( - abi.encode( - keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"), - owner, - bob, - value, - 0, - deadline - ) - ); - - bytes32 hash = keccak256(abi.encodePacked("\x19\x01", token.DOMAIN_SEPARATOR(), structHash)); - (uint8 v, bytes32 r, bytes32 s) = vm.sign(ownerPrivateKey, hash); - - token.permit(owner, bob, value, deadline, v, r, s); - - vm.expectRevert( - abi.encodeWithSelector(ERC20Facet.ERC2612InvalidSignature.selector, owner, bob, value, deadline, v, r, s) - ); - token.permit(owner, bob, value, deadline, v, r, s); - } - - function test_RevertWhen_PermitZeroAddressSpender() public { - uint256 ownerPrivateKey = 0xA11CE; - address owner = vm.addr(ownerPrivateKey); - uint256 value = 100e18; - uint256 deadline = block.timestamp + 1 hours; - - bytes32 structHash = keccak256( - abi.encode( - keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"), - owner, - address(0), - value, - 0, - deadline - ) - ); - - bytes32 hash = keccak256(abi.encodePacked("\x19\x01", token.DOMAIN_SEPARATOR(), structHash)); - (uint8 v, bytes32 r, bytes32 s) = vm.sign(ownerPrivateKey, hash); - - vm.expectRevert(abi.encodeWithSelector(ERC20Facet.ERC20InvalidSpender.selector, address(0))); - token.permit(owner, address(0), value, deadline, v, r, s); - } - - function test_Permit_MaxValue() public { - uint256 ownerPrivateKey = 0xA11CE; - address owner = vm.addr(ownerPrivateKey); - uint256 value = type(uint256).max; - uint256 deadline = block.timestamp + 1 hours; - - bytes32 structHash = keccak256( - abi.encode( - keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"), - owner, - bob, - value, - 0, - deadline - ) - ); - - bytes32 hash = keccak256(abi.encodePacked("\x19\x01", token.DOMAIN_SEPARATOR(), structHash)); - (uint8 v, bytes32 r, bytes32 s) = vm.sign(ownerPrivateKey, hash); - - token.permit(owner, bob, value, deadline, v, r, s); - - assertEq(token.allowance(owner, bob), type(uint256).max); - assertEq(token.nonces(owner), 1); - } - - function test_Permit_ThenTransferFrom() public { - uint256 ownerPrivateKey = 0xA11CE; - address owner = vm.addr(ownerPrivateKey); - uint256 permitValue = 500e18; - uint256 transferAmount = 300e18; - uint256 deadline = block.timestamp + 1 hours; - - token.mint(owner, 1000e18); - - bytes32 structHash = keccak256( - abi.encode( - keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"), - owner, - bob, - permitValue, - 0, - deadline - ) - ); - - bytes32 hash = keccak256(abi.encodePacked("\x19\x01", token.DOMAIN_SEPARATOR(), structHash)); - (uint8 v, bytes32 r, bytes32 s) = vm.sign(ownerPrivateKey, hash); - - token.permit(owner, bob, permitValue, deadline, v, r, s); - - uint256 ownerBalanceBefore = token.balanceOf(owner); - - vm.prank(bob); - token.transferFrom(owner, charlie, transferAmount); - - assertEq(token.balanceOf(owner), ownerBalanceBefore - transferAmount); - assertEq(token.balanceOf(charlie), transferAmount); - assertEq(token.allowance(owner, bob), permitValue - transferAmount); - } - - function test_RevertWhen_PermitWrongNonce() public { - uint256 ownerPrivateKey = 0xA11CE; - address owner = vm.addr(ownerPrivateKey); - uint256 value = 100e18; - uint256 wrongNonce = 99; - uint256 deadline = block.timestamp + 1 hours; - - bytes32 structHash = keccak256( - abi.encode( - keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"), - owner, - bob, - value, - wrongNonce, - deadline - ) - ); - - bytes32 hash = keccak256(abi.encodePacked("\x19\x01", token.DOMAIN_SEPARATOR(), structHash)); - (uint8 v, bytes32 r, bytes32 s) = vm.sign(ownerPrivateKey, hash); - - vm.expectRevert( - abi.encodeWithSelector(ERC20Facet.ERC2612InvalidSignature.selector, owner, bob, value, deadline, v, r, s) - ); - token.permit(owner, bob, value, deadline, v, r, s); - } - - function test_Permit_ZeroValue() public { - uint256 ownerPrivateKey = 0xA11CE; - address owner = vm.addr(ownerPrivateKey); - uint256 value = 0; - uint256 deadline = block.timestamp + 1 hours; - - bytes32 structHash = keccak256( - abi.encode( - keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"), - owner, - bob, - value, - 0, - deadline - ) - ); - - bytes32 hash = keccak256(abi.encodePacked("\x19\x01", token.DOMAIN_SEPARATOR(), structHash)); - (uint8 v, bytes32 r, bytes32 s) = vm.sign(ownerPrivateKey, hash); - - token.permit(owner, bob, value, deadline, v, r, s); - - assertEq(token.allowance(owner, bob), 0); - assertEq(token.nonces(owner), 1); - } - - function test_Permit_MultipleDifferentSpenders() public { - uint256 ownerPrivateKey = 0xA11CE; - address owner = vm.addr(ownerPrivateKey); - uint256 deadline = block.timestamp + 1 hours; - - address[] memory spenders = new address[](3); - spenders[0] = bob; - spenders[1] = charlie; - spenders[2] = makeAddr("dave"); - - uint256[] memory values = new uint256[](3); - values[0] = 100e18; - values[1] = 200e18; - values[2] = 300e18; - - for (uint256 i = 0; i < spenders.length; i++) { - bytes32 structHash = keccak256( - abi.encode( - keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"), - owner, - spenders[i], - values[i], - i, - deadline - ) - ); - - bytes32 hash = keccak256(abi.encodePacked("\x19\x01", token.DOMAIN_SEPARATOR(), structHash)); - (uint8 v, bytes32 r, bytes32 s) = vm.sign(ownerPrivateKey, hash); - - token.permit(owner, spenders[i], values[i], deadline, v, r, s); - assertEq(token.allowance(owner, spenders[i]), values[i]); - } - - assertEq(token.nonces(owner), 3); - } - - function test_Permit_OverwritesExistingAllowance() public { - uint256 ownerPrivateKey = 0xA11CE; - address owner = vm.addr(ownerPrivateKey); - uint256 deadline = block.timestamp + 1 hours; - - token.mint(owner, 1000e18); - - vm.prank(owner); - token.approve(bob, 100e18); - assertEq(token.allowance(owner, bob), 100e18); - - uint256 newValue = 500e18; - bytes32 structHash = keccak256( - abi.encode( - keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"), - owner, - bob, - newValue, - 0, - deadline - ) - ); - - bytes32 hash = keccak256(abi.encodePacked("\x19\x01", token.DOMAIN_SEPARATOR(), structHash)); - (uint8 v, bytes32 r, bytes32 s) = vm.sign(ownerPrivateKey, hash); - - token.permit(owner, bob, newValue, deadline, v, r, s); - - assertEq(token.allowance(owner, bob), newValue); - } - - function test_RevertWhen_PermitMalformedSignature() public { - uint256 ownerPrivateKey = 0xA11CE; - address owner = vm.addr(ownerPrivateKey); - uint256 value = 100e18; - uint256 deadline = block.timestamp + 1 hours; - - bytes32 structHash = keccak256( - abi.encode( - keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"), - owner, - bob, - value, - 0, - deadline - ) - ); - - bytes32 hash = keccak256(abi.encodePacked("\x19\x01", token.DOMAIN_SEPARATOR(), structHash)); - (uint8 v, bytes32 r, bytes32 s) = vm.sign(ownerPrivateKey, hash); - - // Test with invalid v value (should be 27 or 28) - vm.expectRevert( - abi.encodeWithSelector(ERC20Facet.ERC2612InvalidSignature.selector, owner, bob, value, deadline, 99, r, s) - ); - token.permit(owner, bob, value, deadline, 99, r, s); - - // Test with zero r value - vm.expectRevert( - abi.encodeWithSelector( - ERC20Facet.ERC2612InvalidSignature.selector, owner, bob, value, deadline, v, bytes32(0), s - ) - ); - token.permit(owner, bob, value, deadline, v, bytes32(0), s); - - // Test with zero s value - vm.expectRevert( - abi.encodeWithSelector( - ERC20Facet.ERC2612InvalidSignature.selector, owner, bob, value, deadline, v, r, bytes32(0) - ) - ); - token.permit(owner, bob, value, deadline, v, r, bytes32(0)); - } - - function testFuzz_Permit(uint256 ownerKey, address spender, uint256 value, uint256 deadline) public { - vm.assume(ownerKey != 0 && ownerKey < type(uint256).max / 2); - vm.assume(spender != address(0)); - vm.assume(deadline > block.timestamp); - - address owner = vm.addr(ownerKey); - - bytes32 structHash = keccak256( - abi.encode( - keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"), - owner, - spender, - value, - 0, - deadline - ) - ); - - bytes32 hash = keccak256(abi.encodePacked("\x19\x01", token.DOMAIN_SEPARATOR(), structHash)); - (uint8 v, bytes32 r, bytes32 s) = vm.sign(ownerKey, hash); - - token.permit(owner, spender, value, deadline, v, r, s); - - assertEq(token.allowance(owner, spender), value); - assertEq(token.nonces(owner), 1); - } } diff --git a/test/token/ERC20/ERC20/ERC20PermitFacet.t.sol b/test/token/ERC20/ERC20/ERC20PermitFacet.t.sol new file mode 100644 index 00000000..42f0a24f --- /dev/null +++ b/test/token/ERC20/ERC20/ERC20PermitFacet.t.sol @@ -0,0 +1,508 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +import {Test} from "forge-std/Test.sol"; +import {ERC20PermitFacet} from "../../../../src/token/ERC20/ERC20/ERC20PermitFacet.sol"; +import {ERC20PermitFacetHarness} from "./harnesses/ERC20PermitFacetHarness.sol"; + +contract ERC20BurnFacetTest is Test { + ERC20PermitFacetHarness public token; + + address public alice; + address public bob; + address public charlie; + + string constant TOKEN_NAME = "Test Token"; + uint256 constant INITIAL_SUPPLY = 1000000e18; + + event Transfer(address indexed _from, address indexed _to, uint256 _value); + event Approval(address indexed _owner, address indexed _spender, uint256 _value); + + function setUp() public { + alice = makeAddr("alice"); + bob = makeAddr("bob"); + charlie = makeAddr("charlie"); + + token = new ERC20PermitFacetHarness(); + token.initialize(TOKEN_NAME); + token.mint(alice, INITIAL_SUPPLY); + } + + function test_Nonces() public view { + assertEq(token.nonces(alice), 0); + assertEq(token.nonces(bob), 0); + } + + function test_DOMAIN_SEPARATOR() public view { + bytes32 expectedDomainSeparator = keccak256( + abi.encode( + keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"), + keccak256(bytes(TOKEN_NAME)), + keccak256("1"), + block.chainid, + address(token) + ) + ); + assertEq(token.DOMAIN_SEPARATOR(), expectedDomainSeparator); + } + + function test_DOMAIN_SEPARATOR_ConsistentWithinSameChain() public view { + // First call - computes domain separator + bytes32 separator1 = token.DOMAIN_SEPARATOR(); + + // Second call - recomputes and should return same value for same chain ID + bytes32 separator2 = token.DOMAIN_SEPARATOR(); + + assertEq(separator1, separator2); + } + + function test_DOMAIN_SEPARATOR_RecalculatesAfterFork() public { + // Get initial domain separator on chain 1 + uint256 originalChainId = block.chainid; + bytes32 separator1 = token.DOMAIN_SEPARATOR(); + + // Simulate chain fork (chain ID changes) + vm.chainId(originalChainId + 1); + + // Domain separator should recalculate with new chain ID + bytes32 separator2 = token.DOMAIN_SEPARATOR(); + + // Separators should be different + assertTrue(separator1 != separator2); + + // New separator should match expected value for new chain ID + bytes32 expectedSeparator = keccak256( + abi.encode( + keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"), + keccak256(bytes(TOKEN_NAME)), + keccak256("1"), + originalChainId + 1, + address(token) + ) + ); + assertEq(separator2, expectedSeparator); + } + + function test_Permit() public { + uint256 ownerPrivateKey = 0xA11CE; + address owner = vm.addr(ownerPrivateKey); + address spender = bob; + uint256 value = 100e18; + uint256 nonce = 0; + uint256 deadline = block.timestamp + 1 hours; + + bytes32 structHash = keccak256( + abi.encode( + keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"), + owner, + spender, + value, + nonce, + deadline + ) + ); + + bytes32 hash = keccak256(abi.encodePacked("\x19\x01", token.DOMAIN_SEPARATOR(), structHash)); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(ownerPrivateKey, hash); + + vm.expectEmit(true, true, true, true); + emit Approval(owner, spender, value); + token.permit(owner, spender, value, deadline, v, r, s); + + assertEq(token.allowance(owner, spender), value); + assertEq(token.nonces(owner), 1); + } + + function test_Permit_IncreasesNonce() public { + uint256 ownerPrivateKey = 0xB0B; + address owner = vm.addr(ownerPrivateKey); + uint256 deadline = block.timestamp + 1 hours; + + for (uint256 i = 0; i < 3; i++) { + bytes32 structHash = keccak256( + abi.encode( + keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"), + owner, + bob, + 100e18, + i, + deadline + ) + ); + + bytes32 hash = keccak256(abi.encodePacked("\x19\x01", token.DOMAIN_SEPARATOR(), structHash)); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(ownerPrivateKey, hash); + + token.permit(owner, bob, 100e18, deadline, v, r, s); + assertEq(token.nonces(owner), i + 1); + } + } + + function test_RevertWhen_PermitExpired() public { + uint256 ownerPrivateKey = 0xA11CE; + address owner = vm.addr(ownerPrivateKey); + uint256 value = 100e18; + uint256 deadline = block.timestamp - 1; + + bytes32 structHash = keccak256( + abi.encode( + keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"), + owner, + bob, + value, + 0, + deadline + ) + ); + + bytes32 hash = keccak256(abi.encodePacked("\x19\x01", token.DOMAIN_SEPARATOR(), structHash)); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(ownerPrivateKey, hash); + + vm.expectRevert( + abi.encodeWithSelector( + ERC20PermitFacet.ERC2612InvalidSignature.selector, owner, bob, value, deadline, v, r, s + ) + ); + token.permit(owner, bob, value, deadline, v, r, s); + } + + function test_RevertWhen_PermitInvalidSignature() public { + uint256 ownerPrivateKey = 0xA11CE; + address owner = vm.addr(ownerPrivateKey); + uint256 wrongPrivateKey = 0xBAD; + uint256 value = 100e18; + uint256 deadline = block.timestamp + 1 hours; + + bytes32 structHash = keccak256( + abi.encode( + keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"), + owner, + bob, + value, + 0, + deadline + ) + ); + + bytes32 hash = keccak256(abi.encodePacked("\x19\x01", token.DOMAIN_SEPARATOR(), structHash)); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(wrongPrivateKey, hash); + + vm.expectRevert( + abi.encodeWithSelector( + ERC20PermitFacet.ERC2612InvalidSignature.selector, owner, bob, value, deadline, v, r, s + ) + ); + token.permit(owner, bob, value, deadline, v, r, s); + } + + function test_RevertWhen_PermitReplay() public { + uint256 ownerPrivateKey = 0xA11CE; + address owner = vm.addr(ownerPrivateKey); + uint256 value = 100e18; + uint256 deadline = block.timestamp + 1 hours; + + bytes32 structHash = keccak256( + abi.encode( + keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"), + owner, + bob, + value, + 0, + deadline + ) + ); + + bytes32 hash = keccak256(abi.encodePacked("\x19\x01", token.DOMAIN_SEPARATOR(), structHash)); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(ownerPrivateKey, hash); + + token.permit(owner, bob, value, deadline, v, r, s); + + vm.expectRevert( + abi.encodeWithSelector( + ERC20PermitFacet.ERC2612InvalidSignature.selector, owner, bob, value, deadline, v, r, s + ) + ); + token.permit(owner, bob, value, deadline, v, r, s); + } + + function test_RevertWhen_PermitZeroAddressSpender() public { + uint256 ownerPrivateKey = 0xA11CE; + address owner = vm.addr(ownerPrivateKey); + uint256 value = 100e18; + uint256 deadline = block.timestamp + 1 hours; + + bytes32 structHash = keccak256( + abi.encode( + keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"), + owner, + address(0), + value, + 0, + deadline + ) + ); + + bytes32 hash = keccak256(abi.encodePacked("\x19\x01", token.DOMAIN_SEPARATOR(), structHash)); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(ownerPrivateKey, hash); + + vm.expectRevert(abi.encodeWithSelector(ERC20PermitFacet.ERC20InvalidSpender.selector, address(0))); + token.permit(owner, address(0), value, deadline, v, r, s); + } + + function test_Permit_MaxValue() public { + uint256 ownerPrivateKey = 0xA11CE; + address owner = vm.addr(ownerPrivateKey); + uint256 value = type(uint256).max; + uint256 deadline = block.timestamp + 1 hours; + + bytes32 structHash = keccak256( + abi.encode( + keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"), + owner, + bob, + value, + 0, + deadline + ) + ); + + bytes32 hash = keccak256(abi.encodePacked("\x19\x01", token.DOMAIN_SEPARATOR(), structHash)); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(ownerPrivateKey, hash); + + token.permit(owner, bob, value, deadline, v, r, s); + + assertEq(token.allowance(owner, bob), type(uint256).max); + assertEq(token.nonces(owner), 1); + } + + function test_Permit_ThenTransferFrom() public { + uint256 ownerPrivateKey = 0xA11CE; + address owner = vm.addr(ownerPrivateKey); + uint256 permitValue = 500e18; + uint256 transferAmount = 300e18; + uint256 deadline = block.timestamp + 1 hours; + + token.mint(owner, 1000e18); + + bytes32 structHash = keccak256( + abi.encode( + keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"), + owner, + bob, + permitValue, + 0, + deadline + ) + ); + + bytes32 hash = keccak256(abi.encodePacked("\x19\x01", token.DOMAIN_SEPARATOR(), structHash)); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(ownerPrivateKey, hash); + + token.permit(owner, bob, permitValue, deadline, v, r, s); + + uint256 ownerBalanceBefore = token.balanceOf(owner); + + vm.prank(bob); + token.transferFrom(owner, charlie, transferAmount); + + assertEq(token.balanceOf(owner), ownerBalanceBefore - transferAmount); + assertEq(token.balanceOf(charlie), transferAmount); + assertEq(token.allowance(owner, bob), permitValue - transferAmount); + } + + function test_RevertWhen_PermitWrongNonce() public { + uint256 ownerPrivateKey = 0xA11CE; + address owner = vm.addr(ownerPrivateKey); + uint256 value = 100e18; + uint256 wrongNonce = 99; + uint256 deadline = block.timestamp + 1 hours; + + bytes32 structHash = keccak256( + abi.encode( + keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"), + owner, + bob, + value, + wrongNonce, + deadline + ) + ); + + bytes32 hash = keccak256(abi.encodePacked("\x19\x01", token.DOMAIN_SEPARATOR(), structHash)); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(ownerPrivateKey, hash); + + vm.expectRevert( + abi.encodeWithSelector( + ERC20PermitFacet.ERC2612InvalidSignature.selector, owner, bob, value, deadline, v, r, s + ) + ); + token.permit(owner, bob, value, deadline, v, r, s); + } + + function test_Permit_ZeroValue() public { + uint256 ownerPrivateKey = 0xA11CE; + address owner = vm.addr(ownerPrivateKey); + uint256 value = 0; + uint256 deadline = block.timestamp + 1 hours; + + bytes32 structHash = keccak256( + abi.encode( + keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"), + owner, + bob, + value, + 0, + deadline + ) + ); + + bytes32 hash = keccak256(abi.encodePacked("\x19\x01", token.DOMAIN_SEPARATOR(), structHash)); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(ownerPrivateKey, hash); + + token.permit(owner, bob, value, deadline, v, r, s); + + assertEq(token.allowance(owner, bob), 0); + assertEq(token.nonces(owner), 1); + } + + function test_Permit_MultipleDifferentSpenders() public { + uint256 ownerPrivateKey = 0xA11CE; + address owner = vm.addr(ownerPrivateKey); + uint256 deadline = block.timestamp + 1 hours; + + address[] memory spenders = new address[](3); + spenders[0] = bob; + spenders[1] = charlie; + spenders[2] = makeAddr("dave"); + + uint256[] memory values = new uint256[](3); + values[0] = 100e18; + values[1] = 200e18; + values[2] = 300e18; + + for (uint256 i = 0; i < spenders.length; i++) { + bytes32 structHash = keccak256( + abi.encode( + keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"), + owner, + spenders[i], + values[i], + i, + deadline + ) + ); + + bytes32 hash = keccak256(abi.encodePacked("\x19\x01", token.DOMAIN_SEPARATOR(), structHash)); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(ownerPrivateKey, hash); + + token.permit(owner, spenders[i], values[i], deadline, v, r, s); + assertEq(token.allowance(owner, spenders[i]), values[i]); + } + + assertEq(token.nonces(owner), 3); + } + + function test_Permit_OverwritesExistingAllowance() public { + uint256 ownerPrivateKey = 0xA11CE; + address owner = vm.addr(ownerPrivateKey); + uint256 deadline = block.timestamp + 1 hours; + + token.mint(owner, 1000e18); + + vm.prank(owner); + token.approve(bob, 100e18); + assertEq(token.allowance(owner, bob), 100e18); + + uint256 newValue = 500e18; + bytes32 structHash = keccak256( + abi.encode( + keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"), + owner, + bob, + newValue, + 0, + deadline + ) + ); + + bytes32 hash = keccak256(abi.encodePacked("\x19\x01", token.DOMAIN_SEPARATOR(), structHash)); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(ownerPrivateKey, hash); + + token.permit(owner, bob, newValue, deadline, v, r, s); + + assertEq(token.allowance(owner, bob), newValue); + } + + function test_RevertWhen_PermitMalformedSignature() public { + uint256 ownerPrivateKey = 0xA11CE; + address owner = vm.addr(ownerPrivateKey); + uint256 value = 100e18; + uint256 deadline = block.timestamp + 1 hours; + + bytes32 structHash = keccak256( + abi.encode( + keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"), + owner, + bob, + value, + 0, + deadline + ) + ); + + bytes32 hash = keccak256(abi.encodePacked("\x19\x01", token.DOMAIN_SEPARATOR(), structHash)); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(ownerPrivateKey, hash); + + // Test with invalid v value (should be 27 or 28) + vm.expectRevert( + abi.encodeWithSelector( + ERC20PermitFacet.ERC2612InvalidSignature.selector, owner, bob, value, deadline, 99, r, s + ) + ); + token.permit(owner, bob, value, deadline, 99, r, s); + + // Test with zero r value + vm.expectRevert( + abi.encodeWithSelector( + ERC20PermitFacet.ERC2612InvalidSignature.selector, owner, bob, value, deadline, v, bytes32(0), s + ) + ); + token.permit(owner, bob, value, deadline, v, bytes32(0), s); + + // Test with zero s value + vm.expectRevert( + abi.encodeWithSelector( + ERC20PermitFacet.ERC2612InvalidSignature.selector, owner, bob, value, deadline, v, r, bytes32(0) + ) + ); + token.permit(owner, bob, value, deadline, v, r, bytes32(0)); + } + + function testFuzz_Permit(uint256 ownerKey, address spender, uint256 value, uint256 deadline) public { + vm.assume(ownerKey != 0 && ownerKey < type(uint256).max / 2); + vm.assume(spender != address(0)); + vm.assume(deadline > block.timestamp); + + address owner = vm.addr(ownerKey); + + bytes32 structHash = keccak256( + abi.encode( + keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"), + owner, + spender, + value, + 0, + deadline + ) + ); + + bytes32 hash = keccak256(abi.encodePacked("\x19\x01", token.DOMAIN_SEPARATOR(), structHash)); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(ownerKey, hash); + + token.permit(owner, spender, value, deadline, v, r, s); + + assertEq(token.allowance(owner, spender), value); + assertEq(token.nonces(owner), 1); + } +} diff --git a/test/token/ERC20/ERC20/harnesses/ERC20BurnFacetHarness.sol b/test/token/ERC20/ERC20/harnesses/ERC20BurnFacetHarness.sol new file mode 100644 index 00000000..3ac302a9 --- /dev/null +++ b/test/token/ERC20/ERC20/harnesses/ERC20BurnFacetHarness.sol @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +import {ERC20BurnFacet} from "../../../../../src/token/ERC20/ERC20/ERC20BurnFacet.sol"; + +/// @title ERC20BurnFacetHarness +/// @notice Test harness for ERC20BurnFacet that adds initialization and minting for testing +contract ERC20BurnFacetHarness is ERC20BurnFacet { + event Approval(address indexed _owner, address indexed _spender, uint256 _value); + + /// @notice ERC20 view helpers so tests can call the standard API + function balanceOf(address _account) external view returns (uint256) { + return getStorage().balanceOf[_account]; + } + + function totalSupply() external view returns (uint256) { + return getStorage().totalSupply; + } + + function allowance(address _owner, address _spender) external view returns (uint256) { + return getStorage().allowances[_owner][_spender]; + } + + /// @notice Minimal approve implementation for tests (writes into the same storage used by burnFrom) + function approve(address _spender, uint256 _value) external returns (bool) { + require(_spender != address(0), "ERC20: approve to zero address"); + ERC20Storage storage s = getStorage(); + s.allowances[msg.sender][_spender] = _value; + emit Approval(msg.sender, _spender, _value); + return true; + } + + /// @notice Mint tokens to an address + /// @dev Only used for testing - exposes internal mint functionality + function mint(address _to, uint256 _value) external { + ERC20Storage storage s = getStorage(); + require(_to != address(0), "ERC20: mint to zero address"); + unchecked { + s.totalSupply += _value; + s.balanceOf[_to] += _value; + } + emit Transfer(address(0), _to, _value); + } +} diff --git a/test/token/ERC20/ERC20/harnesses/ERC20PermitFacetHarness.sol b/test/token/ERC20/ERC20/harnesses/ERC20PermitFacetHarness.sol new file mode 100644 index 00000000..21df8aff --- /dev/null +++ b/test/token/ERC20/ERC20/harnesses/ERC20PermitFacetHarness.sol @@ -0,0 +1,69 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +import {ERC20PermitFacet} from "../../../../../src/token/ERC20/ERC20/ERC20PermitFacet.sol"; + +/// @title ERC20PermitFacetHarness +/// @notice Test harness for ERC20PermitFacet that adds initialization and minting for testing +contract ERC20PermitFacetHarness is ERC20PermitFacet { + event Transfer(address indexed _from, address indexed _to, uint256 _value); + + /// @notice Initialize the ERC20 token storage + /// @dev Only used for testing - production diamonds should initialize in constructor + function initialize(string memory _name) external { + ERC20Storage storage s = getERC20Storage(); + s.name = _name; + } + + /// @notice Mint tokens to an address + /// @dev Only used for testing - exposes internal mint functionality + function mint(address _to, uint256 _value) external { + ERC20Storage storage s = getERC20Storage(); + require(_to != address(0), "ERC20: mint to zero address"); + unchecked { + s.totalSupply += _value; + s.balanceOf[_to] += _value; + } + emit Transfer(address(0), _to, _value); + } + + /// @notice ERC20 view helpers so tests can call the standard API + function balanceOf(address _account) external view returns (uint256) { + return getERC20Storage().balanceOf[_account]; + } + + function totalSupply() external view returns (uint256) { + return getERC20Storage().totalSupply; + } + + function allowance(address _owner, address _spender) external view returns (uint256) { + return getERC20Storage().allowances[_owner][_spender]; + } + + /// @notice Minimal approve implementation for tests + function approve(address _spender, uint256 _value) external returns (bool) { + require(_spender != address(0), "ERC20: approve to zero address"); + ERC20Storage storage s = getERC20Storage(); + s.allowances[msg.sender][_spender] = _value; + emit Approval(msg.sender, _spender, _value); + return true; + } + + /// @notice TransferFrom implementation for tests (needed by test_Permit_ThenTransferFrom) + function transferFrom(address _from, address _to, uint256 _value) external returns (bool) { + ERC20Storage storage s = getERC20Storage(); + require(_to != address(0), "ERC20: transfer to zero address"); + require(s.balanceOf[_from] >= _value, "ERC20: insufficient balance"); + + uint256 currentAllowance = s.allowances[_from][msg.sender]; + require(currentAllowance >= _value, "ERC20: insufficient allowance"); + + unchecked { + s.allowances[_from][msg.sender] = currentAllowance - _value; + s.balanceOf[_from] -= _value; + } + s.balanceOf[_to] += _value; + emit Transfer(_from, _to, _value); + return true; + } +}