From ce9b9a2fdd97812422293c6ea7cab291e9982552 Mon Sep 17 00:00:00 2001 From: Abhivansh <31abhivanshj@gmail.com> Date: Fri, 21 Nov 2025 03:09:11 +0530 Subject: [PATCH 1/9] WIP: refactor ERC20Facet --- lib/forge-std | 2 +- src/token/ERC20/ERC20/ERC20BurnFacet.sol | 92 +++ src/token/ERC20/ERC20/ERC20Facet.sol | 162 +---- src/token/ERC20/ERC20/ERC20PermitFacet.sol | 149 ++++ test/token/ERC20/ERC20/ERC20BurnFacet.t.sol | 204 ++++++ test/token/ERC20/ERC20/ERC20Facet.t.sol | 655 ------------------ test/token/ERC20/ERC20/ERC20PermitFacet.t.sol | 500 +++++++++++++ .../harnesses/LibERC20BurnFacetHarness.t.sol | 29 + .../LibERC20PermitFacetHarness.t.sol | 30 + 9 files changed, 1010 insertions(+), 813 deletions(-) create mode 100644 src/token/ERC20/ERC20/ERC20BurnFacet.sol create mode 100644 src/token/ERC20/ERC20/ERC20PermitFacet.sol create mode 100644 test/token/ERC20/ERC20/ERC20BurnFacet.t.sol create mode 100644 test/token/ERC20/ERC20/ERC20PermitFacet.t.sol create mode 100644 test/token/ERC20/ERC20/harnesses/LibERC20BurnFacetHarness.t.sol create mode 100644 test/token/ERC20/ERC20/harnesses/LibERC20PermitFacetHarness.t.sol diff --git a/lib/forge-std b/lib/forge-std index 8e40513d..100b0d75 160000 --- a/lib/forge-std +++ b/lib/forge-std @@ -1 +1 @@ -Subproject commit 8e40513d678f392f398620b3ef2b418648b33e89 +Subproject commit 100b0d756adda67bc70aab816fa5a1a95dcf78b6 diff --git a/src/token/ERC20/ERC20/ERC20BurnFacet.sol b/src/token/ERC20/ERC20/ERC20BurnFacet.sol new file mode 100644 index 00000000..42ae4bbe --- /dev/null +++ b/src/token/ERC20/ERC20/ERC20BurnFacet.sol @@ -0,0 +1,92 @@ +// 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.erc20burn"); + + /** + * @dev ERC-8042 compliant storage struct for ERC20 token data. + * @custom:storage-location erc8042:compose.erc20burn + */ + struct ERC20BurnStorage { + mapping(address owner => uint256 balance) balanceOf; + mapping(address owner => mapping(address spender => uint256 allowance)) allowances; + uint256 totalSupply; + } + + /** + * @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 (ERC20BurnStorage 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 { + ERC20BurnStorage 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 { + ERC20BurnStorage 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); + } +} \ No newline at end of file diff --git a/src/token/ERC20/ERC20/ERC20Facet.sol b/src/token/ERC20/ERC20/ERC20Facet.sol index c1ca976d..3603e029 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. @@ -57,14 +45,13 @@ contract ERC20Facet { * @dev ERC-8042 compliant storage struct for ERC20 token data. * @custom:storage-location erc8042:compose.erc20 */ - struct ERC20Storage { + struct ERC20Storage { + mapping(address owner => uint256 balance) balanceOf; + mapping(address owner => mapping(address spender => uint256 allowance)) allowances; + uint256 totalSupply; string name; string symbol; - uint8 decimals; - uint256 totalSupply; - mapping(address owner => uint256 balance) balanceOf; - mapping(address owner => mapping(address spender => uint256 allowance)) allowances; - mapping(address owner => uint256) nonces; + uint8 decimals; } /** @@ -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..4978f35d --- /dev/null +++ b/src/token/ERC20/ERC20/ERC20PermitFacet.sol @@ -0,0 +1,149 @@ +// 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); + + /// @dev Storage position determined by the keccak256 hash of the diamond storage identifier. + bytes32 constant STORAGE_POSITION = keccak256("compose.erc20permit"); + + /** + * @dev ERC-8042 compliant storage struct for ERC20 token data. + * @custom:storage-location erc8042:compose.erc20permit + */ + struct ERC20PermitStorage { + string name; + uint256 totalSupply; + mapping(address owner => mapping(address spender => uint256 allowance)) allowances; + mapping(address owner => uint256) nonces; + mapping(address owner => uint256 balance) balanceOf; + } + + /** + * @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 (ERC20PermitStorage storage s) { + bytes32 position = STORAGE_POSITION; + assembly { + s.slot := position + } + } + + // 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); + } + + ERC20PermitStorage 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); + } +} \ No newline at end of file diff --git a/test/token/ERC20/ERC20/ERC20BurnFacet.t.sol b/test/token/ERC20/ERC20/ERC20BurnFacet.t.sol new file mode 100644 index 00000000..2bf1c1e2 --- /dev/null +++ b/test/token/ERC20/ERC20/ERC20BurnFacet.t.sol @@ -0,0 +1,204 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +import {Test} from "forge-std/Test.sol"; +import {ERC20Facet} from "../../../../src/token/ERC20/ERC20/ERC20Facet.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 ERC20FacetHarness(); + token.initialize(); + 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(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); + } + + 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); + } +} \ No newline at end of file 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..56818f92 --- /dev/null +++ b/test/token/ERC20/ERC20/ERC20PermitFacet.t.sol @@ -0,0 +1,500 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +import {Test} from "forge-std/Test.sol"; +import {ERC20Facet} from "../../../../src/token/ERC20/ERC20/ERC20Facet.sol"; +import {ERC20FacetHarness} from "./harnesses/ERC20FacetHarness.sol"; + +contract ERC20BurnFacetTest is Test { + ERC20FacetHarness public token; + + address public alice; + address public bob; + address public charlie; + + string constant TOKEN_NAME = "Test Token"; + string constant TOKEN_SYMBOL = "TEST"; + uint8 constant TOKEN_DECIMALS = 18; + 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 ERC20FacetHarness(); + token.initialize(TOKEN_NAME, TOKEN_SYMBOL, TOKEN_DECIMALS); + 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(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); + } +} \ No newline at end of file diff --git a/test/token/ERC20/ERC20/harnesses/LibERC20BurnFacetHarness.t.sol b/test/token/ERC20/ERC20/harnesses/LibERC20BurnFacetHarness.t.sol new file mode 100644 index 00000000..213448d5 --- /dev/null +++ b/test/token/ERC20/ERC20/harnesses/LibERC20BurnFacetHarness.t.sol @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +import {ERC20BurnFacet} from "../../../../../src/token/ERC20/ERC20/ERC20BurnFacet.sol"; + +/// @title ERC20PermitFacetHarness +/// @notice Test harness for ERC20PermitFacet that adds initialization and minting for testing +contract ERC20PermitFacetHarness is ERC20PermitFacet { + /// @notice Initialize the ERC20 token storage + /// @dev Only used for testing - production diamonds should initialize in constructor + function initialize(uint256 _totalSupply) external { + ERC20BurnStorage storage s = getStorage(); + s.totalSupply = _totalSupply; + } + + /// @notice Mint tokens to an address + /// @dev Only used for testing - exposes internal mint functionality + function mint(address _to, uint256 _value) external { + ERC20BurnStorage storage s = getStorage(); + if (_to == address(0)) { + revert ERC20InvalidReceiver(address(0)); + } + unchecked { + s.totalSupply += _value; + s.balanceOf[_to] += _value; + } + emit Transfer(address(0), _to, _value); + } +} diff --git a/test/token/ERC20/ERC20/harnesses/LibERC20PermitFacetHarness.t.sol b/test/token/ERC20/ERC20/harnesses/LibERC20PermitFacetHarness.t.sol new file mode 100644 index 00000000..bb349dda --- /dev/null +++ b/test/token/ERC20/ERC20/harnesses/LibERC20PermitFacetHarness.t.sol @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +import {ERC20PermitFacet} from "../../../../../src/token/ERC20/ERC20/ERC20PermitFacet.sol"; + +/// @title ERC20BurnFacetHarness +/// @notice Test harness for ERC20BurnFacet that adds initialization and minting for testing +contract ERC20BurnFacetHarness is ERC20BurnFacet { + /// @notice Initialize the ERC20 token storage + /// @dev Only used for testing - production diamonds should initialize in constructor + function initialize(string _name, uint256 _totalSupply) external { + ERC20PermitStorage storage s = getStorage(); + s.name = _name; + s.totalSupply = _totalSupply; + } + + /// @notice Mint tokens to an address + /// @dev Only used for testing - exposes internal mint functionality + function mint(address _to, uint256 _value) external { + ERC20PermitStorage storage s = getStorage(); + if (_to == address(0)) { + revert ERC20InvalidReceiver(address(0)); + } + unchecked { + s.totalSupply += _value; + s.balanceOf[_to] += _value; + } + emit Transfer(address(0), _to, _value); + } +} From 91e989ec0099e1341ac913653911106b98f73d80 Mon Sep 17 00:00:00 2001 From: Abhivansh <31abhivanshj@gmail.com> Date: Fri, 21 Nov 2025 04:25:22 +0530 Subject: [PATCH 2/9] WIP: testing is left --- src/token/ERC20/ERC20/ERC20BurnFacet.sol | 97 +++++++++++++++ src/token/ERC20/ERC20/ERC20PermitFacet.sol | 113 +++++++++++++++++- test/token/ERC20/ERC20/ERC20BurnFacet.t.sol | 17 +-- test/token/ERC20/ERC20/ERC20PermitFacet.t.sol | 29 +++-- ...arness.t.sol => ERC20BurnFacetHarness.sol} | 6 +- ...ness.t.sol => ERC20PermitFacetHarness.sol} | 8 +- 6 files changed, 239 insertions(+), 31 deletions(-) rename test/token/ERC20/ERC20/harnesses/{LibERC20BurnFacetHarness.t.sol => ERC20BurnFacetHarness.sol} (83%) rename test/token/ERC20/ERC20/harnesses/{LibERC20PermitFacetHarness.t.sol => ERC20PermitFacetHarness.sol} (77%) diff --git a/src/token/ERC20/ERC20/ERC20BurnFacet.sol b/src/token/ERC20/ERC20/ERC20BurnFacet.sol index 42ae4bbe..83cfbcc7 100644 --- a/src/token/ERC20/ERC20/ERC20BurnFacet.sol +++ b/src/token/ERC20/ERC20/ERC20BurnFacet.sol @@ -15,12 +15,30 @@ contract ERC20BurnFacet { /// @param _needed Amount required to complete the operation. error ERC20InsufficientAllowance(address _spender, uint256 _allowance, uint256 _needed); + /// @notice Thrown when the sender address is invalid (e.g., zero address). + /// @param _sender Invalid sender address. + error ERC20InvalidSender(address _sender); + + /// @notice Thrown when the receiver address is invalid (e.g., zero address). + /// @param _receiver Invalid receiver address. + error ERC20InvalidReceiver(address _receiver); + + /// @notice Thrown when the spender address is invalid (e.g., zero address). + /// @param _spender Invalid spender address. + error ERC20InvalidSpender(address _spender); + /// @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); + /// @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); + // @dev Storage position determined by the keccak256 hash of the diamond storage identifier. bytes32 constant STORAGE_POSITION = keccak256("compose.erc20burn"); @@ -46,6 +64,85 @@ contract ERC20BurnFacet { } } + /** + * @notice Returns the total supply of tokens. + * @return The total token supply. + */ + function totalSupply() external view returns (uint256) { + return getStorage().totalSupply; + } + + /** + * @notice Returns the balance of a specific account. + * @param _account The address of the account. + * @return The account balance. + */ + function balanceOf(address _account) external view returns (uint256) { + return getStorage().balanceOf[_account]; + } + + /** + * @notice Transfers tokens on behalf of another account, provided sufficient allowance exists. + * @dev Emits a {Transfer} event and decreases the spender's allowance. + * @param _from The address to transfer tokens from. + * @param _to The address to transfer tokens to. + * @param _value The amount of tokens to transfer. + * @return True if the transfer was successful. + */ + function transferFrom(address _from, address _to, uint256 _value) external returns (bool) { + ERC20BurnStorage storage s = getStorage(); + if (_from == address(0)) { + revert ERC20InvalidSender(address(0)); + } + if (_to == address(0)) { + revert ERC20InvalidReceiver(address(0)); + } + uint256 currentAllowance = s.allowances[_from][msg.sender]; + if (currentAllowance < _value) { + revert ERC20InsufficientAllowance(msg.sender, currentAllowance, _value); + } + uint256 fromBalance = s.balanceOf[_from]; + if (fromBalance < _value) { + revert ERC20InsufficientBalance(_from, fromBalance, _value); + } + unchecked { + if (currentAllowance != type(uint256).max) { + s.allowances[_from][msg.sender] = currentAllowance - _value; + } + s.balanceOf[_from] = fromBalance - _value; + } + s.balanceOf[_to] += _value; + emit Transfer(_from, _to, _value); + return true; + } + + /** + * @notice Returns the remaining number of tokens that a spender is allowed to spend on behalf of an owner. + * @param _owner The address of the token owner. + * @param _spender The address of the spender. + * @return The remaining allowance. + */ + function allowance(address _owner, address _spender) external view returns (uint256) { + return getStorage().allowances[_owner][_spender]; + } + + /** + * @notice Approves a spender to transfer up to a certain amount of tokens on behalf of the caller. + * @dev Emits an {Approval} event. + * @param _spender The address approved to spend tokens. + * @param _value The number of tokens to approve. + * @return True if the approval was successful. + */ + function approve(address _spender, uint256 _value) external returns (bool) { + ERC20BurnStorage storage s = getStorage(); + if (_spender == address(0)) { + revert ERC20InvalidSpender(address(0)); + } + s.allowances[msg.sender][_spender] = _value; + emit Approval(msg.sender, _spender, _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. diff --git a/src/token/ERC20/ERC20/ERC20PermitFacet.sol b/src/token/ERC20/ERC20/ERC20PermitFacet.sol index 4978f35d..a3b944da 100644 --- a/src/token/ERC20/ERC20/ERC20PermitFacet.sol +++ b/src/token/ERC20/ERC20/ERC20PermitFacet.sol @@ -15,10 +15,36 @@ contract ERC20PermitFacet { address _owner, address _spender, uint256 _value, uint256 _deadline, uint8 _v, bytes32 _r, bytes32 _s ); + /// @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 Thrown when the sender address is invalid (e.g., zero address). + /// @param _sender Invalid sender address. + error ERC20InvalidSender(address _sender); + + /// @notice Thrown when the receiver address is invalid (e.g., zero address). + /// @param _receiver Invalid receiver address. + error ERC20InvalidReceiver(address _receiver); + /// @notice Thrown when the spender address is invalid (e.g., zero address). /// @param _spender Invalid spender address. error ERC20InvalidSpender(address _spender); + /// @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); + /// @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. @@ -52,7 +78,92 @@ contract ERC20PermitFacet { } } - // EIP-2612 Permit Extension + /** + * @notice Returns the name of the token. + * @return The token name. + */ + function name() external view returns (string memory) { + return getStorage().name; + } + + /** + * @notice Returns the total supply of tokens. + * @return The total token supply. + */ + function totalSupply() external view returns (uint256) { + return getStorage().totalSupply; + } + + /** + * @notice Returns the balance of a specific account. + * @param _account The address of the account. + * @return The account balance. + */ + function balanceOf(address _account) external view returns (uint256) { + return getStorage().balanceOf[_account]; + } + + /** + * @notice Transfers tokens on behalf of another account, provided sufficient allowance exists. + * @dev Emits a {Transfer} event and decreases the spender's allowance. + * @param _from The address to transfer tokens from. + * @param _to The address to transfer tokens to. + * @param _value The amount of tokens to transfer. + * @return True if the transfer was successful. + */ + function transferFrom(address _from, address _to, uint256 _value) external returns (bool) { + ERC20PermitStorage storage s = getStorage(); + if (_from == address(0)) { + revert ERC20InvalidSender(address(0)); + } + if (_to == address(0)) { + revert ERC20InvalidReceiver(address(0)); + } + uint256 currentAllowance = s.allowances[_from][msg.sender]; + if (currentAllowance < _value) { + revert ERC20InsufficientAllowance(msg.sender, currentAllowance, _value); + } + uint256 fromBalance = s.balanceOf[_from]; + if (fromBalance < _value) { + revert ERC20InsufficientBalance(_from, fromBalance, _value); + } + unchecked { + if (currentAllowance != type(uint256).max) { + s.allowances[_from][msg.sender] = currentAllowance - _value; + } + s.balanceOf[_from] = fromBalance - _value; + } + s.balanceOf[_to] += _value; + emit Transfer(_from, _to, _value); + return true; + } + + /** + * @notice Returns the remaining number of tokens that a spender is allowed to spend on behalf of an owner. + * @param _owner The address of the token owner. + * @param _spender The address of the spender. + * @return The remaining allowance. + */ + function allowance(address _owner, address _spender) external view returns (uint256) { + return getStorage().allowances[_owner][_spender]; + } + + /** + * @notice Approves a spender to transfer up to a certain amount of tokens on behalf of the caller. + * @dev Emits an {Approval} event. + * @param _spender The address approved to spend tokens. + * @param _value The number of tokens to approve. + * @return True if the approval was successful. + */ + function approve(address _spender, uint256 _value) external returns (bool) { + ERC20PermitStorage storage s = getStorage(); + if (_spender == address(0)) { + revert ERC20InvalidSpender(address(0)); + } + s.allowances[msg.sender][_spender] = _value; + emit Approval(msg.sender, _spender, _value); + return true; + } /** * @notice Returns the current nonce for an owner. diff --git a/test/token/ERC20/ERC20/ERC20BurnFacet.t.sol b/test/token/ERC20/ERC20/ERC20BurnFacet.t.sol index 2bf1c1e2..5433a183 100644 --- a/test/token/ERC20/ERC20/ERC20BurnFacet.t.sol +++ b/test/token/ERC20/ERC20/ERC20BurnFacet.t.sol @@ -2,7 +2,7 @@ pragma solidity >=0.8.30; import {Test} from "forge-std/Test.sol"; -import {ERC20Facet} from "../../../../src/token/ERC20/ERC20/ERC20Facet.sol"; +import {ERC20BurnFacet} from "../../../../src/token/ERC20/ERC20/ERC20BurnFacet.sol"; import {ERC20BurnFacetHarness} from "./harnesses/ERC20BurnFacetHarness.sol"; contract ERC20BurnFacetTest is Test { @@ -13,6 +13,7 @@ contract ERC20BurnFacetTest is Test { address public charlie; uint256 constant INITIAL_SUPPLY = 1000000e18; + uint256 constant TOTAL_SUPPLY = 1000000000e18; event Transfer(address indexed _from, address indexed _to, uint256 _value); event Approval(address indexed _owner, address indexed _spender, uint256 _value); @@ -22,8 +23,8 @@ contract ERC20BurnFacetTest is Test { bob = makeAddr("bob"); charlie = makeAddr("charlie"); - token = new ERC20FacetHarness(); - token.initialize(); + token = new ERC20BurnFacetHarness(); + token.initialize(TOTAL_SUPPLY); token.mint(alice, INITIAL_SUPPLY); } @@ -62,14 +63,14 @@ contract ERC20BurnFacetTest is Test { vm.prank(alice); vm.expectRevert( - abi.encodeWithSelector(ERC20Facet.ERC20InsufficientBalance.selector, alice, INITIAL_SUPPLY, amount) + abi.encodeWithSelector(ERC20BurnFacet.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)); + vm.expectRevert(abi.encodeWithSelector(ERC20BurnFacet.ERC20InsufficientBalance.selector, bob, 0, 1)); token.burn(1); } @@ -178,7 +179,7 @@ contract ERC20BurnFacetTest is Test { vm.prank(bob); vm.expectRevert( - abi.encodeWithSelector(ERC20Facet.ERC20InsufficientAllowance.selector, bob, allowanceAmount, burnAmount) + abi.encodeWithSelector(ERC20BurnFacet.ERC20InsufficientAllowance.selector, bob, allowanceAmount, burnAmount) ); token.burnFrom(alice, burnAmount); } @@ -191,14 +192,14 @@ contract ERC20BurnFacetTest is Test { vm.prank(bob); vm.expectRevert( - abi.encodeWithSelector(ERC20Facet.ERC20InsufficientBalance.selector, alice, INITIAL_SUPPLY, amount) + 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(ERC20Facet.ERC20InsufficientAllowance.selector, bob, 0, 100e18)); + vm.expectRevert(abi.encodeWithSelector(ERC20BurnFacet.ERC20InsufficientAllowance.selector, bob, 0, 100e18)); token.burnFrom(alice, 100e18); } } \ No newline at end of file diff --git a/test/token/ERC20/ERC20/ERC20PermitFacet.t.sol b/test/token/ERC20/ERC20/ERC20PermitFacet.t.sol index 56818f92..0a1903fc 100644 --- a/test/token/ERC20/ERC20/ERC20PermitFacet.t.sol +++ b/test/token/ERC20/ERC20/ERC20PermitFacet.t.sol @@ -2,20 +2,19 @@ pragma solidity >=0.8.30; import {Test} from "forge-std/Test.sol"; -import {ERC20Facet} from "../../../../src/token/ERC20/ERC20/ERC20Facet.sol"; -import {ERC20FacetHarness} from "./harnesses/ERC20FacetHarness.sol"; +import {ERC20PermitFacet} from "../../../../src/token/ERC20/ERC20/ERC20PermitFacet.sol"; +import {ERC20PermitFacetHarness} from "./harnesses/ERC20PermitFacetHarness.sol"; contract ERC20BurnFacetTest is Test { - ERC20FacetHarness public token; + ERC20PermitFacetHarness public token; address public alice; address public bob; address public charlie; string constant TOKEN_NAME = "Test Token"; - string constant TOKEN_SYMBOL = "TEST"; - uint8 constant TOKEN_DECIMALS = 18; uint256 constant INITIAL_SUPPLY = 1000000e18; + uint256 constant TOTAL_SUPPLY = 1000000000e18; event Transfer(address indexed _from, address indexed _to, uint256 _value); event Approval(address indexed _owner, address indexed _spender, uint256 _value); @@ -25,8 +24,8 @@ contract ERC20BurnFacetTest is Test { bob = makeAddr("bob"); charlie = makeAddr("charlie"); - token = new ERC20FacetHarness(); - token.initialize(TOKEN_NAME, TOKEN_SYMBOL, TOKEN_DECIMALS); + token = new ERC20PermitFacetHarness(); + token.initialize(TOKEN_NAME, TOTAL_SUPPLY); token.mint(alice, INITIAL_SUPPLY); } @@ -162,7 +161,7 @@ contract ERC20BurnFacetTest is Test { (uint8 v, bytes32 r, bytes32 s) = vm.sign(ownerPrivateKey, hash); vm.expectRevert( - abi.encodeWithSelector(ERC20Facet.ERC2612InvalidSignature.selector, owner, bob, value, deadline, v, r, s) + abi.encodeWithSelector(ERC20PermitFacet.ERC2612InvalidSignature.selector, owner, bob, value, deadline, v, r, s) ); token.permit(owner, bob, value, deadline, v, r, s); } @@ -189,7 +188,7 @@ contract ERC20BurnFacetTest is Test { (uint8 v, bytes32 r, bytes32 s) = vm.sign(wrongPrivateKey, hash); vm.expectRevert( - abi.encodeWithSelector(ERC20Facet.ERC2612InvalidSignature.selector, owner, bob, value, deadline, v, r, s) + abi.encodeWithSelector(ERC20PermitFacet.ERC2612InvalidSignature.selector, owner, bob, value, deadline, v, r, s) ); token.permit(owner, bob, value, deadline, v, r, s); } @@ -217,7 +216,7 @@ contract ERC20BurnFacetTest is Test { token.permit(owner, bob, value, deadline, v, r, s); vm.expectRevert( - abi.encodeWithSelector(ERC20Facet.ERC2612InvalidSignature.selector, owner, bob, value, deadline, v, r, s) + abi.encodeWithSelector(ERC20PermitFacet.ERC2612InvalidSignature.selector, owner, bob, value, deadline, v, r, s) ); token.permit(owner, bob, value, deadline, v, r, s); } @@ -242,7 +241,7 @@ contract ERC20BurnFacetTest is Test { 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))); + vm.expectRevert(abi.encodeWithSelector(ERC20PermitFacet.ERC20InvalidSpender.selector, address(0))); token.permit(owner, address(0), value, deadline, v, r, s); } @@ -329,7 +328,7 @@ contract ERC20BurnFacetTest is Test { (uint8 v, bytes32 r, bytes32 s) = vm.sign(ownerPrivateKey, hash); vm.expectRevert( - abi.encodeWithSelector(ERC20Facet.ERC2612InvalidSignature.selector, owner, bob, value, deadline, v, r, s) + abi.encodeWithSelector(ERC20PermitFacet.ERC2612InvalidSignature.selector, owner, bob, value, deadline, v, r, s) ); token.permit(owner, bob, value, deadline, v, r, s); } @@ -450,14 +449,14 @@ contract ERC20BurnFacetTest is Test { // Test with invalid v value (should be 27 or 28) vm.expectRevert( - abi.encodeWithSelector(ERC20Facet.ERC2612InvalidSignature.selector, owner, bob, value, deadline, 99, r, s) + 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( - ERC20Facet.ERC2612InvalidSignature.selector, owner, bob, value, deadline, v, bytes32(0), s + ERC20PermitFacet.ERC2612InvalidSignature.selector, owner, bob, value, deadline, v, bytes32(0), s ) ); token.permit(owner, bob, value, deadline, v, bytes32(0), s); @@ -465,7 +464,7 @@ contract ERC20BurnFacetTest is Test { // Test with zero s value vm.expectRevert( abi.encodeWithSelector( - ERC20Facet.ERC2612InvalidSignature.selector, owner, bob, value, deadline, v, r, bytes32(0) + ERC20PermitFacet.ERC2612InvalidSignature.selector, owner, bob, value, deadline, v, r, bytes32(0) ) ); token.permit(owner, bob, value, deadline, v, r, bytes32(0)); diff --git a/test/token/ERC20/ERC20/harnesses/LibERC20BurnFacetHarness.t.sol b/test/token/ERC20/ERC20/harnesses/ERC20BurnFacetHarness.sol similarity index 83% rename from test/token/ERC20/ERC20/harnesses/LibERC20BurnFacetHarness.t.sol rename to test/token/ERC20/ERC20/harnesses/ERC20BurnFacetHarness.sol index 213448d5..df3ada80 100644 --- a/test/token/ERC20/ERC20/harnesses/LibERC20BurnFacetHarness.t.sol +++ b/test/token/ERC20/ERC20/harnesses/ERC20BurnFacetHarness.sol @@ -3,9 +3,9 @@ pragma solidity >=0.8.30; import {ERC20BurnFacet} from "../../../../../src/token/ERC20/ERC20/ERC20BurnFacet.sol"; -/// @title ERC20PermitFacetHarness -/// @notice Test harness for ERC20PermitFacet that adds initialization and minting for testing -contract ERC20PermitFacetHarness is ERC20PermitFacet { +/// @title ERC20BurnFacetHarness +/// @notice Test harness for ERC20BurnFacet that adds initialization and minting for testing +contract ERC20BurnFacetHarness is ERC20BurnFacet { /// @notice Initialize the ERC20 token storage /// @dev Only used for testing - production diamonds should initialize in constructor function initialize(uint256 _totalSupply) external { diff --git a/test/token/ERC20/ERC20/harnesses/LibERC20PermitFacetHarness.t.sol b/test/token/ERC20/ERC20/harnesses/ERC20PermitFacetHarness.sol similarity index 77% rename from test/token/ERC20/ERC20/harnesses/LibERC20PermitFacetHarness.t.sol rename to test/token/ERC20/ERC20/harnesses/ERC20PermitFacetHarness.sol index bb349dda..38a70321 100644 --- a/test/token/ERC20/ERC20/harnesses/LibERC20PermitFacetHarness.t.sol +++ b/test/token/ERC20/ERC20/harnesses/ERC20PermitFacetHarness.sol @@ -3,12 +3,12 @@ pragma solidity >=0.8.30; import {ERC20PermitFacet} from "../../../../../src/token/ERC20/ERC20/ERC20PermitFacet.sol"; -/// @title ERC20BurnFacetHarness -/// @notice Test harness for ERC20BurnFacet that adds initialization and minting for testing -contract ERC20BurnFacetHarness is ERC20BurnFacet { +/// @title ERC20PermitFacetHarness +/// @notice Test harness for ERC20PermitFacet that adds initialization and minting for testing +contract ERC20PermitFacetHarness is ERC20PermitFacet { /// @notice Initialize the ERC20 token storage /// @dev Only used for testing - production diamonds should initialize in constructor - function initialize(string _name, uint256 _totalSupply) external { + function initialize(string memory _name, uint256 _totalSupply) external { ERC20PermitStorage storage s = getStorage(); s.name = _name; s.totalSupply = _totalSupply; From 975245a9ca8ba00366173befaab7bb9e09ddd154 Mon Sep 17 00:00:00 2001 From: Abhivansh <31abhivanshj@gmail.com> Date: Fri, 21 Nov 2025 19:06:00 +0530 Subject: [PATCH 3/9] refactor: separate the logic of burn and permit from the ERC20Facet --- test/token/ERC20/ERC20/ERC20BurnFacet.t.sol | 2 -- test/token/ERC20/ERC20/ERC20PermitFacet.t.sol | 3 +-- test/token/ERC20/ERC20/harnesses/ERC20BurnFacetHarness.sol | 7 ------- .../ERC20/ERC20/harnesses/ERC20PermitFacetHarness.sol | 3 +-- 4 files changed, 2 insertions(+), 13 deletions(-) diff --git a/test/token/ERC20/ERC20/ERC20BurnFacet.t.sol b/test/token/ERC20/ERC20/ERC20BurnFacet.t.sol index 5433a183..8c710c44 100644 --- a/test/token/ERC20/ERC20/ERC20BurnFacet.t.sol +++ b/test/token/ERC20/ERC20/ERC20BurnFacet.t.sol @@ -13,7 +13,6 @@ contract ERC20BurnFacetTest is Test { address public charlie; uint256 constant INITIAL_SUPPLY = 1000000e18; - uint256 constant TOTAL_SUPPLY = 1000000000e18; event Transfer(address indexed _from, address indexed _to, uint256 _value); event Approval(address indexed _owner, address indexed _spender, uint256 _value); @@ -24,7 +23,6 @@ contract ERC20BurnFacetTest is Test { charlie = makeAddr("charlie"); token = new ERC20BurnFacetHarness(); - token.initialize(TOTAL_SUPPLY); token.mint(alice, INITIAL_SUPPLY); } diff --git a/test/token/ERC20/ERC20/ERC20PermitFacet.t.sol b/test/token/ERC20/ERC20/ERC20PermitFacet.t.sol index 0a1903fc..f3bc6430 100644 --- a/test/token/ERC20/ERC20/ERC20PermitFacet.t.sol +++ b/test/token/ERC20/ERC20/ERC20PermitFacet.t.sol @@ -14,7 +14,6 @@ contract ERC20BurnFacetTest is Test { string constant TOKEN_NAME = "Test Token"; uint256 constant INITIAL_SUPPLY = 1000000e18; - uint256 constant TOTAL_SUPPLY = 1000000000e18; event Transfer(address indexed _from, address indexed _to, uint256 _value); event Approval(address indexed _owner, address indexed _spender, uint256 _value); @@ -25,7 +24,7 @@ contract ERC20BurnFacetTest is Test { charlie = makeAddr("charlie"); token = new ERC20PermitFacetHarness(); - token.initialize(TOKEN_NAME, TOTAL_SUPPLY); + token.initialize(TOKEN_NAME); token.mint(alice, INITIAL_SUPPLY); } diff --git a/test/token/ERC20/ERC20/harnesses/ERC20BurnFacetHarness.sol b/test/token/ERC20/ERC20/harnesses/ERC20BurnFacetHarness.sol index df3ada80..97cc6cf3 100644 --- a/test/token/ERC20/ERC20/harnesses/ERC20BurnFacetHarness.sol +++ b/test/token/ERC20/ERC20/harnesses/ERC20BurnFacetHarness.sol @@ -6,13 +6,6 @@ import {ERC20BurnFacet} from "../../../../../src/token/ERC20/ERC20/ERC20BurnFace /// @title ERC20BurnFacetHarness /// @notice Test harness for ERC20BurnFacet that adds initialization and minting for testing contract ERC20BurnFacetHarness is ERC20BurnFacet { - /// @notice Initialize the ERC20 token storage - /// @dev Only used for testing - production diamonds should initialize in constructor - function initialize(uint256 _totalSupply) external { - ERC20BurnStorage storage s = getStorage(); - s.totalSupply = _totalSupply; - } - /// @notice Mint tokens to an address /// @dev Only used for testing - exposes internal mint functionality function mint(address _to, uint256 _value) external { diff --git a/test/token/ERC20/ERC20/harnesses/ERC20PermitFacetHarness.sol b/test/token/ERC20/ERC20/harnesses/ERC20PermitFacetHarness.sol index 38a70321..dd3681c1 100644 --- a/test/token/ERC20/ERC20/harnesses/ERC20PermitFacetHarness.sol +++ b/test/token/ERC20/ERC20/harnesses/ERC20PermitFacetHarness.sol @@ -8,10 +8,9 @@ import {ERC20PermitFacet} from "../../../../../src/token/ERC20/ERC20/ERC20Permit contract ERC20PermitFacetHarness is ERC20PermitFacet { /// @notice Initialize the ERC20 token storage /// @dev Only used for testing - production diamonds should initialize in constructor - function initialize(string memory _name, uint256 _totalSupply) external { + function initialize(string memory _name) external { ERC20PermitStorage storage s = getStorage(); s.name = _name; - s.totalSupply = _totalSupply; } /// @notice Mint tokens to an address From 8aa7b5a87ed6253fce3f2d40f106d49e1e969c7c Mon Sep 17 00:00:00 2001 From: Abhivansh <31abhivanshj@gmail.com> Date: Fri, 21 Nov 2025 19:21:10 +0530 Subject: [PATCH 4/9] format --- src/token/ERC20/ERC20/ERC20BurnFacet.sol | 7 +++--- src/token/ERC20/ERC20/ERC20Facet.sol | 6 ++--- src/token/ERC20/ERC20/ERC20PermitFacet.sol | 5 ++--- test/token/ERC20/ERC20/ERC20BurnFacet.t.sol | 2 +- test/token/ERC20/ERC20/ERC20PermitFacet.t.sol | 22 ++++++++++++++----- 5 files changed, 25 insertions(+), 17 deletions(-) diff --git a/src/token/ERC20/ERC20/ERC20BurnFacet.sol b/src/token/ERC20/ERC20/ERC20BurnFacet.sol index 83cfbcc7..d2772f6a 100644 --- a/src/token/ERC20/ERC20/ERC20BurnFacet.sol +++ b/src/token/ERC20/ERC20/ERC20BurnFacet.sol @@ -2,7 +2,6 @@ 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. @@ -46,8 +45,8 @@ contract ERC20BurnFacet { * @dev ERC-8042 compliant storage struct for ERC20 token data. * @custom:storage-location erc8042:compose.erc20burn */ - struct ERC20BurnStorage { - mapping(address owner => uint256 balance) balanceOf; + struct ERC20BurnStorage { + mapping(address owner => uint256 balance) balanceOf; mapping(address owner => mapping(address spender => uint256 allowance)) allowances; uint256 totalSupply; } @@ -186,4 +185,4 @@ contract ERC20BurnFacet { } emit Transfer(_account, address(0), _value); } -} \ No newline at end of file +} diff --git a/src/token/ERC20/ERC20/ERC20Facet.sol b/src/token/ERC20/ERC20/ERC20Facet.sol index 3603e029..4d974e89 100644 --- a/src/token/ERC20/ERC20/ERC20Facet.sol +++ b/src/token/ERC20/ERC20/ERC20Facet.sol @@ -45,13 +45,13 @@ contract ERC20Facet { * @dev ERC-8042 compliant storage struct for ERC20 token data. * @custom:storage-location erc8042:compose.erc20 */ - struct ERC20Storage { - mapping(address owner => uint256 balance) balanceOf; + struct ERC20Storage { + mapping(address owner => uint256 balance) balanceOf; mapping(address owner => mapping(address spender => uint256 allowance)) allowances; uint256 totalSupply; string name; string symbol; - uint8 decimals; + uint8 decimals; } /** diff --git a/src/token/ERC20/ERC20/ERC20PermitFacet.sol b/src/token/ERC20/ERC20/ERC20PermitFacet.sol index a3b944da..642d8af5 100644 --- a/src/token/ERC20/ERC20/ERC20PermitFacet.sol +++ b/src/token/ERC20/ERC20/ERC20PermitFacet.sol @@ -2,7 +2,6 @@ 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. @@ -63,7 +62,7 @@ contract ERC20PermitFacet { uint256 totalSupply; mapping(address owner => mapping(address spender => uint256 allowance)) allowances; mapping(address owner => uint256) nonces; - mapping(address owner => uint256 balance) balanceOf; + mapping(address owner => uint256 balance) balanceOf; } /** @@ -257,4 +256,4 @@ contract ERC20PermitFacet { s.nonces[_owner] = currentNonce + 1; emit Approval(_owner, _spender, _value); } -} \ No newline at end of file +} diff --git a/test/token/ERC20/ERC20/ERC20BurnFacet.t.sol b/test/token/ERC20/ERC20/ERC20BurnFacet.t.sol index 8c710c44..5201a8a0 100644 --- a/test/token/ERC20/ERC20/ERC20BurnFacet.t.sol +++ b/test/token/ERC20/ERC20/ERC20BurnFacet.t.sol @@ -200,4 +200,4 @@ contract ERC20BurnFacetTest is Test { vm.expectRevert(abi.encodeWithSelector(ERC20BurnFacet.ERC20InsufficientAllowance.selector, bob, 0, 100e18)); token.burnFrom(alice, 100e18); } -} \ No newline at end of file +} diff --git a/test/token/ERC20/ERC20/ERC20PermitFacet.t.sol b/test/token/ERC20/ERC20/ERC20PermitFacet.t.sol index f3bc6430..42f0a24f 100644 --- a/test/token/ERC20/ERC20/ERC20PermitFacet.t.sol +++ b/test/token/ERC20/ERC20/ERC20PermitFacet.t.sol @@ -160,7 +160,9 @@ contract ERC20BurnFacetTest is Test { (uint8 v, bytes32 r, bytes32 s) = vm.sign(ownerPrivateKey, hash); vm.expectRevert( - abi.encodeWithSelector(ERC20PermitFacet.ERC2612InvalidSignature.selector, owner, bob, value, deadline, v, r, s) + abi.encodeWithSelector( + ERC20PermitFacet.ERC2612InvalidSignature.selector, owner, bob, value, deadline, v, r, s + ) ); token.permit(owner, bob, value, deadline, v, r, s); } @@ -187,7 +189,9 @@ contract ERC20BurnFacetTest is Test { (uint8 v, bytes32 r, bytes32 s) = vm.sign(wrongPrivateKey, hash); vm.expectRevert( - abi.encodeWithSelector(ERC20PermitFacet.ERC2612InvalidSignature.selector, owner, bob, value, deadline, v, r, s) + abi.encodeWithSelector( + ERC20PermitFacet.ERC2612InvalidSignature.selector, owner, bob, value, deadline, v, r, s + ) ); token.permit(owner, bob, value, deadline, v, r, s); } @@ -215,7 +219,9 @@ contract ERC20BurnFacetTest is Test { token.permit(owner, bob, value, deadline, v, r, s); vm.expectRevert( - abi.encodeWithSelector(ERC20PermitFacet.ERC2612InvalidSignature.selector, owner, bob, value, deadline, v, r, s) + abi.encodeWithSelector( + ERC20PermitFacet.ERC2612InvalidSignature.selector, owner, bob, value, deadline, v, r, s + ) ); token.permit(owner, bob, value, deadline, v, r, s); } @@ -327,7 +333,9 @@ contract ERC20BurnFacetTest is Test { (uint8 v, bytes32 r, bytes32 s) = vm.sign(ownerPrivateKey, hash); vm.expectRevert( - abi.encodeWithSelector(ERC20PermitFacet.ERC2612InvalidSignature.selector, owner, bob, value, deadline, v, r, s) + abi.encodeWithSelector( + ERC20PermitFacet.ERC2612InvalidSignature.selector, owner, bob, value, deadline, v, r, s + ) ); token.permit(owner, bob, value, deadline, v, r, s); } @@ -448,7 +456,9 @@ contract ERC20BurnFacetTest is Test { // Test with invalid v value (should be 27 or 28) vm.expectRevert( - abi.encodeWithSelector(ERC20PermitFacet.ERC2612InvalidSignature.selector, owner, bob, value, deadline, 99, r, s) + abi.encodeWithSelector( + ERC20PermitFacet.ERC2612InvalidSignature.selector, owner, bob, value, deadline, 99, r, s + ) ); token.permit(owner, bob, value, deadline, 99, r, s); @@ -495,4 +505,4 @@ contract ERC20BurnFacetTest is Test { assertEq(token.allowance(owner, spender), value); assertEq(token.nonces(owner), 1); } -} \ No newline at end of file +} From 539907d2a522a1609372c2def05cad3d2d8964fe Mon Sep 17 00:00:00 2001 From: Abhivansh <31abhivanshj@gmail.com> Date: Fri, 21 Nov 2025 19:27:07 +0530 Subject: [PATCH 5/9] removed unused functions from facet --- src/token/ERC20/ERC20/ERC20BurnFacet.sol | 35 ---------------------- src/token/ERC20/ERC20/ERC20PermitFacet.sol | 16 ---------- 2 files changed, 51 deletions(-) diff --git a/src/token/ERC20/ERC20/ERC20BurnFacet.sol b/src/token/ERC20/ERC20/ERC20BurnFacet.sol index d2772f6a..a457b15e 100644 --- a/src/token/ERC20/ERC20/ERC20BurnFacet.sol +++ b/src/token/ERC20/ERC20/ERC20BurnFacet.sol @@ -80,41 +80,6 @@ contract ERC20BurnFacet { return getStorage().balanceOf[_account]; } - /** - * @notice Transfers tokens on behalf of another account, provided sufficient allowance exists. - * @dev Emits a {Transfer} event and decreases the spender's allowance. - * @param _from The address to transfer tokens from. - * @param _to The address to transfer tokens to. - * @param _value The amount of tokens to transfer. - * @return True if the transfer was successful. - */ - function transferFrom(address _from, address _to, uint256 _value) external returns (bool) { - ERC20BurnStorage storage s = getStorage(); - if (_from == address(0)) { - revert ERC20InvalidSender(address(0)); - } - if (_to == address(0)) { - revert ERC20InvalidReceiver(address(0)); - } - uint256 currentAllowance = s.allowances[_from][msg.sender]; - if (currentAllowance < _value) { - revert ERC20InsufficientAllowance(msg.sender, currentAllowance, _value); - } - uint256 fromBalance = s.balanceOf[_from]; - if (fromBalance < _value) { - revert ERC20InsufficientBalance(_from, fromBalance, _value); - } - unchecked { - if (currentAllowance != type(uint256).max) { - s.allowances[_from][msg.sender] = currentAllowance - _value; - } - s.balanceOf[_from] = fromBalance - _value; - } - s.balanceOf[_to] += _value; - emit Transfer(_from, _to, _value); - return true; - } - /** * @notice Returns the remaining number of tokens that a spender is allowed to spend on behalf of an owner. * @param _owner The address of the token owner. diff --git a/src/token/ERC20/ERC20/ERC20PermitFacet.sol b/src/token/ERC20/ERC20/ERC20PermitFacet.sol index 642d8af5..77335093 100644 --- a/src/token/ERC20/ERC20/ERC20PermitFacet.sol +++ b/src/token/ERC20/ERC20/ERC20PermitFacet.sol @@ -77,22 +77,6 @@ contract ERC20PermitFacet { } } - /** - * @notice Returns the name of the token. - * @return The token name. - */ - function name() external view returns (string memory) { - return getStorage().name; - } - - /** - * @notice Returns the total supply of tokens. - * @return The total token supply. - */ - function totalSupply() external view returns (uint256) { - return getStorage().totalSupply; - } - /** * @notice Returns the balance of a specific account. * @param _account The address of the account. From 11e90dc44f396ff81bcdc3439e9d2de63a7d28f7 Mon Sep 17 00:00:00 2001 From: Abhivansh <31abhivanshj@gmail.com> Date: Fri, 21 Nov 2025 19:46:00 +0530 Subject: [PATCH 6/9] refactor: refactor to match the Compose design --- src/token/ERC20/ERC20/ERC20BurnFacet.sol | 14 ++++++------- src/token/ERC20/ERC20/ERC20PermitFacet.sol | 20 +++++++++---------- .../ERC20/harnesses/ERC20BurnFacetHarness.sol | 2 +- .../harnesses/ERC20PermitFacetHarness.sol | 4 ++-- 4 files changed, 20 insertions(+), 20 deletions(-) diff --git a/src/token/ERC20/ERC20/ERC20BurnFacet.sol b/src/token/ERC20/ERC20/ERC20BurnFacet.sol index a457b15e..c867aa88 100644 --- a/src/token/ERC20/ERC20/ERC20BurnFacet.sol +++ b/src/token/ERC20/ERC20/ERC20BurnFacet.sol @@ -39,13 +39,13 @@ contract ERC20BurnFacet { event Approval(address indexed _owner, address indexed _spender, uint256 _value); // @dev Storage position determined by the keccak256 hash of the diamond storage identifier. - bytes32 constant STORAGE_POSITION = keccak256("compose.erc20burn"); + bytes32 constant STORAGE_POSITION = keccak256("compose.erc20"); /** * @dev ERC-8042 compliant storage struct for ERC20 token data. - * @custom:storage-location erc8042:compose.erc20burn + * @custom:storage-location erc8042:compose.erc20 */ - struct ERC20BurnStorage { + struct ERC20Storage { mapping(address owner => uint256 balance) balanceOf; mapping(address owner => mapping(address spender => uint256 allowance)) allowances; uint256 totalSupply; @@ -56,7 +56,7 @@ contract ERC20BurnFacet { * @dev Uses inline assembly to set the storage slot reference. * @return s The ERC20 storage struct reference. */ - function getStorage() internal pure returns (ERC20BurnStorage storage s) { + function getStorage() internal pure returns (ERC20Storage storage s) { bytes32 position = STORAGE_POSITION; assembly { s.slot := position @@ -98,7 +98,7 @@ contract ERC20BurnFacet { * @return True if the approval was successful. */ function approve(address _spender, uint256 _value) external returns (bool) { - ERC20BurnStorage storage s = getStorage(); + ERC20Storage storage s = getStorage(); if (_spender == address(0)) { revert ERC20InvalidSpender(address(0)); } @@ -113,7 +113,7 @@ contract ERC20BurnFacet { * @param _value The amount of tokens to burn. */ function burn(uint256 _value) external { - ERC20BurnStorage storage s = getStorage(); + ERC20Storage storage s = getStorage(); uint256 balance = s.balanceOf[msg.sender]; if (balance < _value) { revert ERC20InsufficientBalance(msg.sender, balance, _value); @@ -132,7 +132,7 @@ contract ERC20BurnFacet { * @param _value The amount of tokens to burn. */ function burnFrom(address _account, uint256 _value) external { - ERC20BurnStorage storage s = getStorage(); + ERC20Storage storage s = getStorage(); uint256 currentAllowance = s.allowances[_account][msg.sender]; if (currentAllowance < _value) { revert ERC20InsufficientAllowance(msg.sender, currentAllowance, _value); diff --git a/src/token/ERC20/ERC20/ERC20PermitFacet.sol b/src/token/ERC20/ERC20/ERC20PermitFacet.sol index 77335093..6d21a2e0 100644 --- a/src/token/ERC20/ERC20/ERC20PermitFacet.sol +++ b/src/token/ERC20/ERC20/ERC20PermitFacet.sol @@ -51,18 +51,18 @@ contract ERC20PermitFacet { event Approval(address indexed _owner, address indexed _spender, uint256 _value); /// @dev Storage position determined by the keccak256 hash of the diamond storage identifier. - bytes32 constant STORAGE_POSITION = keccak256("compose.erc20permit"); + bytes32 constant STORAGE_POSITION = keccak256("compose.erc20"); /** * @dev ERC-8042 compliant storage struct for ERC20 token data. - * @custom:storage-location erc8042:compose.erc20permit + * @custom:storage-location erc8042:compose.erc20 */ - struct ERC20PermitStorage { - string name; - uint256 totalSupply; + struct ERC20Storage { + mapping(address owner => uint256 balance) balanceOf; mapping(address owner => mapping(address spender => uint256 allowance)) allowances; + uint256 totalSupply; mapping(address owner => uint256) nonces; - mapping(address owner => uint256 balance) balanceOf; + string name; } /** @@ -70,7 +70,7 @@ contract ERC20PermitFacet { * @dev Uses inline assembly to set the storage slot reference. * @return s The ERC20 storage struct reference. */ - function getStorage() internal pure returns (ERC20PermitStorage storage s) { + function getStorage() internal pure returns (ERC20Storage storage s) { bytes32 position = STORAGE_POSITION; assembly { s.slot := position @@ -95,7 +95,7 @@ contract ERC20PermitFacet { * @return True if the transfer was successful. */ function transferFrom(address _from, address _to, uint256 _value) external returns (bool) { - ERC20PermitStorage storage s = getStorage(); + ERC20Storage storage s = getStorage(); if (_from == address(0)) { revert ERC20InvalidSender(address(0)); } @@ -139,7 +139,7 @@ contract ERC20PermitFacet { * @return True if the approval was successful. */ function approve(address _spender, uint256 _value) external returns (bool) { - ERC20PermitStorage storage s = getStorage(); + ERC20Storage storage s = getStorage(); if (_spender == address(0)) { revert ERC20InvalidSpender(address(0)); } @@ -202,7 +202,7 @@ contract ERC20PermitFacet { revert ERC2612InvalidSignature(_owner, _spender, _value, _deadline, _v, _r, _s); } - ERC20PermitStorage storage s = getStorage(); + ERC20Storage storage s = getStorage(); uint256 currentNonce = s.nonces[_owner]; bytes32 structHash = keccak256( abi.encode( diff --git a/test/token/ERC20/ERC20/harnesses/ERC20BurnFacetHarness.sol b/test/token/ERC20/ERC20/harnesses/ERC20BurnFacetHarness.sol index 97cc6cf3..4dae1444 100644 --- a/test/token/ERC20/ERC20/harnesses/ERC20BurnFacetHarness.sol +++ b/test/token/ERC20/ERC20/harnesses/ERC20BurnFacetHarness.sol @@ -9,7 +9,7 @@ contract ERC20BurnFacetHarness is ERC20BurnFacet { /// @notice Mint tokens to an address /// @dev Only used for testing - exposes internal mint functionality function mint(address _to, uint256 _value) external { - ERC20BurnStorage storage s = getStorage(); + ERC20Storage storage s = getStorage(); if (_to == address(0)) { revert ERC20InvalidReceiver(address(0)); } diff --git a/test/token/ERC20/ERC20/harnesses/ERC20PermitFacetHarness.sol b/test/token/ERC20/ERC20/harnesses/ERC20PermitFacetHarness.sol index dd3681c1..fd619085 100644 --- a/test/token/ERC20/ERC20/harnesses/ERC20PermitFacetHarness.sol +++ b/test/token/ERC20/ERC20/harnesses/ERC20PermitFacetHarness.sol @@ -9,14 +9,14 @@ contract ERC20PermitFacetHarness is ERC20PermitFacet { /// @notice Initialize the ERC20 token storage /// @dev Only used for testing - production diamonds should initialize in constructor function initialize(string memory _name) external { - ERC20PermitStorage storage s = getStorage(); + ERC20Storage storage s = getStorage(); 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 { - ERC20PermitStorage storage s = getStorage(); + ERC20Storage storage s = getStorage(); if (_to == address(0)) { revert ERC20InvalidReceiver(address(0)); } From c0d5ddb61eea064770b1002b31d6bdbcd582e098 Mon Sep 17 00:00:00 2001 From: Abhivansh <31abhivanshj@gmail.com> Date: Fri, 21 Nov 2025 21:39:56 +0530 Subject: [PATCH 7/9] forge update --- lib/forge-std | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/forge-std b/lib/forge-std index 100b0d75..8e40513d 160000 --- a/lib/forge-std +++ b/lib/forge-std @@ -1 +1 @@ -Subproject commit 100b0d756adda67bc70aab816fa5a1a95dcf78b6 +Subproject commit 8e40513d678f392f398620b3ef2b418648b33e89 From 9a198eed9ad2015920daafd3afcf10c87d2b69a8 Mon Sep 17 00:00:00 2001 From: Abhivansh <31abhivanshj@gmail.com> Date: Sat, 22 Nov 2025 22:41:03 +0530 Subject: [PATCH 8/9] made requested changes --- src/token/ERC20/ERC20/ERC20BurnFacet.sol | 84 ++------ src/token/ERC20/ERC20/ERC20Facet.sol | 39 +++- src/token/ERC20/ERC20/ERC20PermitFacet.sol | 197 +++++++----------- test/token/ERC20/ERC20/ERC20BurnFacet.t.sol | 50 ++++- .../ERC20/harnesses/ERC20BurnFacetHarness.sol | 35 +++- .../harnesses/ERC20PermitFacetHarness.sol | 52 ++++- 6 files changed, 242 insertions(+), 215 deletions(-) diff --git a/src/token/ERC20/ERC20/ERC20BurnFacet.sol b/src/token/ERC20/ERC20/ERC20BurnFacet.sol index c867aa88..c05a4c14 100644 --- a/src/token/ERC20/ERC20/ERC20BurnFacet.sol +++ b/src/token/ERC20/ERC20/ERC20BurnFacet.sol @@ -6,25 +6,21 @@ contract ERC20BurnFacet { /// @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); + 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 Thrown when the sender address is invalid (e.g., zero address). - /// @param _sender Invalid sender address. - error ERC20InvalidSender(address _sender); - - /// @notice Thrown when the receiver address is invalid (e.g., zero address). - /// @param _receiver Invalid receiver address. - error ERC20InvalidReceiver(address _receiver); - - /// @notice Thrown when the spender address is invalid (e.g., zero address). - /// @param _spender Invalid spender address. - error ERC20InvalidSpender(address _spender); + error ERC20InsufficientAllowance( + address _spender, + uint256 _allowance, + uint256 _needed + ); /// @notice Emitted when tokens are transferred between two addresses. /// @param _from Address sending the tokens. @@ -32,12 +28,6 @@ contract ERC20BurnFacet { /// @param _value Amount of tokens transferred. event Transfer(address indexed _from, address indexed _to, uint256 _value); - /// @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); - // @dev Storage position determined by the keccak256 hash of the diamond storage identifier. bytes32 constant STORAGE_POSITION = keccak256("compose.erc20"); @@ -47,8 +37,8 @@ contract ERC20BurnFacet { */ struct ERC20Storage { mapping(address owner => uint256 balance) balanceOf; - mapping(address owner => mapping(address spender => uint256 allowance)) allowances; uint256 totalSupply; + mapping(address owner => mapping(address spender => uint256 allowance)) allowances; } /** @@ -62,51 +52,7 @@ contract ERC20BurnFacet { s.slot := position } } - - /** - * @notice Returns the total supply of tokens. - * @return The total token supply. - */ - function totalSupply() external view returns (uint256) { - return getStorage().totalSupply; - } - - /** - * @notice Returns the balance of a specific account. - * @param _account The address of the account. - * @return The account balance. - */ - function balanceOf(address _account) external view returns (uint256) { - return getStorage().balanceOf[_account]; - } - - /** - * @notice Returns the remaining number of tokens that a spender is allowed to spend on behalf of an owner. - * @param _owner The address of the token owner. - * @param _spender The address of the spender. - * @return The remaining allowance. - */ - function allowance(address _owner, address _spender) external view returns (uint256) { - return getStorage().allowances[_owner][_spender]; - } - - /** - * @notice Approves a spender to transfer up to a certain amount of tokens on behalf of the caller. - * @dev Emits an {Approval} event. - * @param _spender The address approved to spend tokens. - * @param _value The number of tokens to approve. - * @return True if the approval was successful. - */ - function approve(address _spender, uint256 _value) external returns (bool) { - ERC20Storage storage s = getStorage(); - if (_spender == address(0)) { - revert ERC20InvalidSpender(address(0)); - } - s.allowances[msg.sender][_spender] = _value; - emit Approval(msg.sender, _spender, _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. @@ -135,7 +81,11 @@ contract ERC20BurnFacet { ERC20Storage storage s = getStorage(); uint256 currentAllowance = s.allowances[_account][msg.sender]; if (currentAllowance < _value) { - revert ERC20InsufficientAllowance(msg.sender, currentAllowance, _value); + revert ERC20InsufficientAllowance( + msg.sender, + currentAllowance, + _value + ); } uint256 balance = s.balanceOf[_account]; if (balance < _value) { diff --git a/src/token/ERC20/ERC20/ERC20Facet.sol b/src/token/ERC20/ERC20/ERC20Facet.sol index 4d974e89..cf7bae50 100644 --- a/src/token/ERC20/ERC20/ERC20Facet.sol +++ b/src/token/ERC20/ERC20/ERC20Facet.sol @@ -6,7 +6,11 @@ contract ERC20Facet { /// @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); + error ERC20InsufficientBalance( + address _sender, + uint256 _balance, + uint256 _needed + ); /// @notice Thrown when the sender address is invalid (e.g., zero address). /// @param _sender Invalid sender address. @@ -20,7 +24,11 @@ contract ERC20Facet { /// @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); + error ERC20InsufficientAllowance( + address _spender, + uint256 _allowance, + uint256 _needed + ); /// @notice Thrown when the spender address is invalid (e.g., zero address). /// @param _spender Invalid spender address. @@ -30,7 +38,11 @@ contract ERC20Facet { /// @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); + event Approval( + address indexed _owner, + address indexed _spender, + uint256 _value + ); /// @notice Emitted when tokens are transferred between two addresses. /// @param _from Address sending the tokens. @@ -47,11 +59,11 @@ contract ERC20Facet { */ struct ERC20Storage { mapping(address owner => uint256 balance) balanceOf; - mapping(address owner => mapping(address spender => uint256 allowance)) allowances; uint256 totalSupply; + mapping(address owner => mapping(address spender => uint256 allowance)) allowances; + uint8 decimals; string name; string symbol; - uint8 decimals; } /** @@ -113,7 +125,10 @@ contract ERC20Facet { * @param _spender The address of the spender. * @return The remaining allowance. */ - function allowance(address _owner, address _spender) external view returns (uint256) { + function allowance( + address _owner, + address _spender + ) external view returns (uint256) { return getStorage().allowances[_owner][_spender]; } @@ -166,7 +181,11 @@ contract ERC20Facet { * @param _value The amount of tokens to transfer. * @return True if the transfer was successful. */ - function transferFrom(address _from, address _to, uint256 _value) external returns (bool) { + function transferFrom( + address _from, + address _to, + uint256 _value + ) external returns (bool) { ERC20Storage storage s = getStorage(); if (_from == address(0)) { revert ERC20InvalidSender(address(0)); @@ -176,7 +195,11 @@ contract ERC20Facet { } uint256 currentAllowance = s.allowances[_from][msg.sender]; if (currentAllowance < _value) { - revert ERC20InsufficientAllowance(msg.sender, currentAllowance, _value); + revert ERC20InsufficientAllowance( + msg.sender, + currentAllowance, + _value + ); } uint256 fromBalance = s.balanceOf[_from]; if (fromBalance < _value) { diff --git a/src/token/ERC20/ERC20/ERC20PermitFacet.sol b/src/token/ERC20/ERC20/ERC20PermitFacet.sol index 6d21a2e0..8ab44685 100644 --- a/src/token/ERC20/ERC20/ERC20PermitFacet.sol +++ b/src/token/ERC20/ERC20/ERC20PermitFacet.sol @@ -11,141 +11,66 @@ contract ERC20PermitFacet { /// @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 + address _owner, + address _spender, + uint256 _value, + uint256 _deadline, + uint8 _v, + bytes32 _r, + bytes32 _s ); - /// @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 Thrown when the sender address is invalid (e.g., zero address). - /// @param _sender Invalid sender address. - error ERC20InvalidSender(address _sender); - - /// @notice Thrown when the receiver address is invalid (e.g., zero address). - /// @param _receiver Invalid receiver address. - error ERC20InvalidReceiver(address _receiver); - /// @notice Thrown when the spender address is invalid (e.g., zero address). /// @param _spender Invalid spender address. error ERC20InvalidSpender(address _spender); - /// @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); - /// @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); + event Approval( + address indexed _owner, + address indexed _spender, + uint256 _value + ); - /// @dev Storage position determined by the keccak256 hash of the diamond storage identifier. - bytes32 constant STORAGE_POSITION = keccak256("compose.erc20"); + bytes32 constant ERC20_STORAGE_POSITION = keccak256("compose.erc20"); - /** - * @dev ERC-8042 compliant storage struct for ERC20 token data. - * @custom:storage-location erc8042: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; - mapping(address owner => mapping(address spender => uint256 allowance)) allowances; uint256 totalSupply; - mapping(address owner => uint256) nonces; + mapping(address owner => mapping(address spender => uint256 allowance)) allowances; + uint8 decimals; string name; } - /** - * @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; + function getERC20Storage() internal pure returns (ERC20Storage storage s) { + bytes32 position = ERC20_STORAGE_POSITION; assembly { s.slot := position } } - /** - * @notice Returns the balance of a specific account. - * @param _account The address of the account. - * @return The account balance. - */ - function balanceOf(address _account) external view returns (uint256) { - return getStorage().balanceOf[_account]; - } - - /** - * @notice Transfers tokens on behalf of another account, provided sufficient allowance exists. - * @dev Emits a {Transfer} event and decreases the spender's allowance. - * @param _from The address to transfer tokens from. - * @param _to The address to transfer tokens to. - * @param _value The amount of tokens to transfer. - * @return True if the transfer was successful. - */ - function transferFrom(address _from, address _to, uint256 _value) external returns (bool) { - ERC20Storage storage s = getStorage(); - if (_from == address(0)) { - revert ERC20InvalidSender(address(0)); - } - if (_to == address(0)) { - revert ERC20InvalidReceiver(address(0)); - } - uint256 currentAllowance = s.allowances[_from][msg.sender]; - if (currentAllowance < _value) { - revert ERC20InsufficientAllowance(msg.sender, currentAllowance, _value); - } - uint256 fromBalance = s.balanceOf[_from]; - if (fromBalance < _value) { - revert ERC20InsufficientBalance(_from, fromBalance, _value); - } - unchecked { - if (currentAllowance != type(uint256).max) { - s.allowances[_from][msg.sender] = currentAllowance - _value; - } - s.balanceOf[_from] = fromBalance - _value; - } - s.balanceOf[_to] += _value; - emit Transfer(_from, _to, _value); - return true; - } + bytes32 constant STORAGE_POSITION = keccak256("compose.erc20.permit"); - /** - * @notice Returns the remaining number of tokens that a spender is allowed to spend on behalf of an owner. - * @param _owner The address of the token owner. - * @param _spender The address of the spender. - * @return The remaining allowance. - */ - function allowance(address _owner, address _spender) external view returns (uint256) { - return getStorage().allowances[_owner][_spender]; + /// @custom:storage-location erc8042:compose.erc20.permit + struct ERC20PermitStorage { + mapping(address owner => uint256) nonces; } - /** - * @notice Approves a spender to transfer up to a certain amount of tokens on behalf of the caller. - * @dev Emits an {Approval} event. - * @param _spender The address approved to spend tokens. - * @param _value The number of tokens to approve. - * @return True if the approval was successful. - */ - function approve(address _spender, uint256 _value) external returns (bool) { - ERC20Storage storage s = getStorage(); - if (_spender == address(0)) { - revert ERC20InvalidSpender(address(0)); + function getStorage() internal pure returns (ERC20PermitStorage storage s) { + bytes32 position = STORAGE_POSITION; + assembly { + s.slot := position } - s.allowances[msg.sender][_spender] = _value; - emit Approval(msg.sender, _spender, _value); - return true; } /** @@ -164,15 +89,18 @@ contract ERC20PermitFacet { * @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) - ) - ); + return + keccak256( + abi.encode( + keccak256( + "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)" + ), + keccak256(bytes(getERC20Storage().name)), + keccak256("1"), + block.chainid, + address(this) + ) + ); } /** @@ -199,14 +127,25 @@ contract ERC20PermitFacet { revert ERC20InvalidSpender(address(0)); } if (block.timestamp > _deadline) { - revert ERC2612InvalidSignature(_owner, _spender, _value, _deadline, _v, _r, _s); + revert ERC2612InvalidSignature( + _owner, + _spender, + _value, + _deadline, + _v, + _r, + _s + ); } - ERC20Storage storage s = getStorage(); + 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)"), + keccak256( + "Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)" + ), _owner, _spender, _value, @@ -220,8 +159,10 @@ contract ERC20PermitFacet { "\x19\x01", keccak256( abi.encode( - keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"), - keccak256(bytes(s.name)), + keccak256( + "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)" + ), + keccak256(bytes(sERC20.name)), keccak256("1"), block.chainid, address(this) @@ -233,10 +174,18 @@ contract ERC20PermitFacet { address signer = ecrecover(hash, _v, _r, _s); if (signer != _owner || signer == address(0)) { - revert ERC2612InvalidSignature(_owner, _spender, _value, _deadline, _v, _r, _s); + revert ERC2612InvalidSignature( + _owner, + _spender, + _value, + _deadline, + _v, + _r, + _s + ); } - s.allowances[_owner][_spender] = _value; + 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 index 5201a8a0..f26eae9a 100644 --- a/test/token/ERC20/ERC20/ERC20BurnFacet.t.sol +++ b/test/token/ERC20/ERC20/ERC20BurnFacet.t.sol @@ -15,7 +15,11 @@ contract ERC20BurnFacetTest is Test { 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); + event Approval( + address indexed _owner, + address indexed _spender, + uint256 _value + ); function setUp() public { alice = makeAddr("alice"); @@ -61,14 +65,26 @@ contract ERC20BurnFacetTest is Test { vm.prank(alice); vm.expectRevert( - abi.encodeWithSelector(ERC20BurnFacet.ERC20InsufficientBalance.selector, alice, INITIAL_SUPPLY, amount) + 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)); + vm.expectRevert( + abi.encodeWithSelector( + ERC20BurnFacet.ERC20InsufficientBalance.selector, + bob, + 0, + 1 + ) + ); token.burn(1); } @@ -164,7 +180,10 @@ contract ERC20BurnFacetTest is Test { } // Verify final balances and total supply - assertEq(token.balanceOf(alice), INITIAL_SUPPLY - (burnAmount * numBurns)); + assertEq( + token.balanceOf(alice), + INITIAL_SUPPLY - (burnAmount * numBurns) + ); assertEq(token.totalSupply(), INITIAL_SUPPLY - (burnAmount * numBurns)); } @@ -177,7 +196,12 @@ contract ERC20BurnFacetTest is Test { vm.prank(bob); vm.expectRevert( - abi.encodeWithSelector(ERC20BurnFacet.ERC20InsufficientAllowance.selector, bob, allowanceAmount, burnAmount) + abi.encodeWithSelector( + ERC20BurnFacet.ERC20InsufficientAllowance.selector, + bob, + allowanceAmount, + burnAmount + ) ); token.burnFrom(alice, burnAmount); } @@ -190,14 +214,26 @@ contract ERC20BurnFacetTest is Test { vm.prank(bob); vm.expectRevert( - abi.encodeWithSelector(ERC20BurnFacet.ERC20InsufficientBalance.selector, alice, INITIAL_SUPPLY, amount) + 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)); + vm.expectRevert( + abi.encodeWithSelector( + ERC20BurnFacet.ERC20InsufficientAllowance.selector, + bob, + 0, + 100e18 + ) + ); token.burnFrom(alice, 100e18); } } diff --git a/test/token/ERC20/ERC20/harnesses/ERC20BurnFacetHarness.sol b/test/token/ERC20/ERC20/harnesses/ERC20BurnFacetHarness.sol index 4dae1444..791a57ae 100644 --- a/test/token/ERC20/ERC20/harnesses/ERC20BurnFacetHarness.sol +++ b/test/token/ERC20/ERC20/harnesses/ERC20BurnFacetHarness.sol @@ -6,13 +6,42 @@ import {ERC20BurnFacet} from "../../../../../src/token/ERC20/ERC20/ERC20BurnFace /// @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(); - if (_to == address(0)) { - revert ERC20InvalidReceiver(address(0)); - } + require(_to != address(0), "ERC20: mint to zero address"); unchecked { s.totalSupply += _value; s.balanceOf[_to] += _value; diff --git a/test/token/ERC20/ERC20/harnesses/ERC20PermitFacetHarness.sol b/test/token/ERC20/ERC20/harnesses/ERC20PermitFacetHarness.sol index fd619085..729c1512 100644 --- a/test/token/ERC20/ERC20/harnesses/ERC20PermitFacetHarness.sol +++ b/test/token/ERC20/ERC20/harnesses/ERC20PermitFacetHarness.sol @@ -6,24 +6,64 @@ import {ERC20PermitFacet} from "../../../../../src/token/ERC20/ERC20/ERC20Permit /// @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 = getStorage(); + 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 = getStorage(); - if (_to == address(0)) { - revert ERC20InvalidReceiver(address(0)); - } + 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; + } +} \ No newline at end of file From 7bc31e56d8f4939c518bccf7263cc2f9fd60841f Mon Sep 17 00:00:00 2001 From: Abhivansh <31abhivanshj@gmail.com> Date: Sat, 22 Nov 2025 22:41:19 +0530 Subject: [PATCH 9/9] format --- src/token/ERC20/ERC20/ERC20BurnFacet.sol | 20 ++---- src/token/ERC20/ERC20/ERC20Facet.sol | 35 ++--------- src/token/ERC20/ERC20/ERC20PermitFacet.sol | 63 +++++-------------- test/token/ERC20/ERC20/ERC20BurnFacet.t.sol | 50 +++------------ .../ERC20/harnesses/ERC20BurnFacetHarness.sol | 11 +--- .../harnesses/ERC20PermitFacetHarness.sol | 6 +- 6 files changed, 37 insertions(+), 148 deletions(-) diff --git a/src/token/ERC20/ERC20/ERC20BurnFacet.sol b/src/token/ERC20/ERC20/ERC20BurnFacet.sol index c05a4c14..7cc5eca0 100644 --- a/src/token/ERC20/ERC20/ERC20BurnFacet.sol +++ b/src/token/ERC20/ERC20/ERC20BurnFacet.sol @@ -6,21 +6,13 @@ contract ERC20BurnFacet { /// @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 - ); + 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 - ); + error ERC20InsufficientAllowance(address _spender, uint256 _allowance, uint256 _needed); /// @notice Emitted when tokens are transferred between two addresses. /// @param _from Address sending the tokens. @@ -52,7 +44,7 @@ contract ERC20BurnFacet { 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. @@ -81,11 +73,7 @@ contract ERC20BurnFacet { ERC20Storage storage s = getStorage(); uint256 currentAllowance = s.allowances[_account][msg.sender]; if (currentAllowance < _value) { - revert ERC20InsufficientAllowance( - msg.sender, - currentAllowance, - _value - ); + revert ERC20InsufficientAllowance(msg.sender, currentAllowance, _value); } uint256 balance = s.balanceOf[_account]; if (balance < _value) { diff --git a/src/token/ERC20/ERC20/ERC20Facet.sol b/src/token/ERC20/ERC20/ERC20Facet.sol index cf7bae50..e4a8280f 100644 --- a/src/token/ERC20/ERC20/ERC20Facet.sol +++ b/src/token/ERC20/ERC20/ERC20Facet.sol @@ -6,11 +6,7 @@ contract ERC20Facet { /// @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 - ); + error ERC20InsufficientBalance(address _sender, uint256 _balance, uint256 _needed); /// @notice Thrown when the sender address is invalid (e.g., zero address). /// @param _sender Invalid sender address. @@ -24,11 +20,7 @@ contract ERC20Facet { /// @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 - ); + error ERC20InsufficientAllowance(address _spender, uint256 _allowance, uint256 _needed); /// @notice Thrown when the spender address is invalid (e.g., zero address). /// @param _spender Invalid spender address. @@ -38,11 +30,7 @@ contract ERC20Facet { /// @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 - ); + event Approval(address indexed _owner, address indexed _spender, uint256 _value); /// @notice Emitted when tokens are transferred between two addresses. /// @param _from Address sending the tokens. @@ -125,10 +113,7 @@ contract ERC20Facet { * @param _spender The address of the spender. * @return The remaining allowance. */ - function allowance( - address _owner, - address _spender - ) external view returns (uint256) { + function allowance(address _owner, address _spender) external view returns (uint256) { return getStorage().allowances[_owner][_spender]; } @@ -181,11 +166,7 @@ contract ERC20Facet { * @param _value The amount of tokens to transfer. * @return True if the transfer was successful. */ - function transferFrom( - address _from, - address _to, - uint256 _value - ) external returns (bool) { + function transferFrom(address _from, address _to, uint256 _value) external returns (bool) { ERC20Storage storage s = getStorage(); if (_from == address(0)) { revert ERC20InvalidSender(address(0)); @@ -195,11 +176,7 @@ contract ERC20Facet { } uint256 currentAllowance = s.allowances[_from][msg.sender]; if (currentAllowance < _value) { - revert ERC20InsufficientAllowance( - msg.sender, - currentAllowance, - _value - ); + revert ERC20InsufficientAllowance(msg.sender, currentAllowance, _value); } uint256 fromBalance = s.balanceOf[_from]; if (fromBalance < _value) { diff --git a/src/token/ERC20/ERC20/ERC20PermitFacet.sol b/src/token/ERC20/ERC20/ERC20PermitFacet.sol index 8ab44685..462283ff 100644 --- a/src/token/ERC20/ERC20/ERC20PermitFacet.sol +++ b/src/token/ERC20/ERC20/ERC20PermitFacet.sol @@ -11,13 +11,7 @@ contract ERC20PermitFacet { /// @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 + 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). @@ -28,11 +22,7 @@ contract ERC20PermitFacet { /// @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 - ); + event Approval(address indexed _owner, address indexed _spender, uint256 _value); bytes32 constant ERC20_STORAGE_POSITION = keccak256("compose.erc20"); @@ -89,18 +79,15 @@ contract ERC20PermitFacet { * @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) - ) - ); + return keccak256( + abi.encode( + keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"), + keccak256(bytes(getERC20Storage().name)), + keccak256("1"), + block.chainid, + address(this) + ) + ); } /** @@ -127,15 +114,7 @@ contract ERC20PermitFacet { revert ERC20InvalidSpender(address(0)); } if (block.timestamp > _deadline) { - revert ERC2612InvalidSignature( - _owner, - _spender, - _value, - _deadline, - _v, - _r, - _s - ); + revert ERC2612InvalidSignature(_owner, _spender, _value, _deadline, _v, _r, _s); } ERC20PermitStorage storage s = getStorage(); @@ -143,9 +122,7 @@ contract ERC20PermitFacet { uint256 currentNonce = s.nonces[_owner]; bytes32 structHash = keccak256( abi.encode( - keccak256( - "Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)" - ), + keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"), _owner, _spender, _value, @@ -159,9 +136,7 @@ contract ERC20PermitFacet { "\x19\x01", keccak256( abi.encode( - keccak256( - "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)" - ), + keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"), keccak256(bytes(sERC20.name)), keccak256("1"), block.chainid, @@ -174,15 +149,7 @@ contract ERC20PermitFacet { address signer = ecrecover(hash, _v, _r, _s); if (signer != _owner || signer == address(0)) { - revert ERC2612InvalidSignature( - _owner, - _spender, - _value, - _deadline, - _v, - _r, - _s - ); + revert ERC2612InvalidSignature(_owner, _spender, _value, _deadline, _v, _r, _s); } sERC20.allowances[_owner][_spender] = _value; diff --git a/test/token/ERC20/ERC20/ERC20BurnFacet.t.sol b/test/token/ERC20/ERC20/ERC20BurnFacet.t.sol index f26eae9a..5201a8a0 100644 --- a/test/token/ERC20/ERC20/ERC20BurnFacet.t.sol +++ b/test/token/ERC20/ERC20/ERC20BurnFacet.t.sol @@ -15,11 +15,7 @@ contract ERC20BurnFacetTest is Test { 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 - ); + event Approval(address indexed _owner, address indexed _spender, uint256 _value); function setUp() public { alice = makeAddr("alice"); @@ -65,26 +61,14 @@ contract ERC20BurnFacetTest is Test { vm.prank(alice); vm.expectRevert( - abi.encodeWithSelector( - ERC20BurnFacet.ERC20InsufficientBalance.selector, - alice, - INITIAL_SUPPLY, - amount - ) + 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 - ) - ); + vm.expectRevert(abi.encodeWithSelector(ERC20BurnFacet.ERC20InsufficientBalance.selector, bob, 0, 1)); token.burn(1); } @@ -180,10 +164,7 @@ contract ERC20BurnFacetTest is Test { } // Verify final balances and total supply - assertEq( - token.balanceOf(alice), - INITIAL_SUPPLY - (burnAmount * numBurns) - ); + assertEq(token.balanceOf(alice), INITIAL_SUPPLY - (burnAmount * numBurns)); assertEq(token.totalSupply(), INITIAL_SUPPLY - (burnAmount * numBurns)); } @@ -196,12 +177,7 @@ contract ERC20BurnFacetTest is Test { vm.prank(bob); vm.expectRevert( - abi.encodeWithSelector( - ERC20BurnFacet.ERC20InsufficientAllowance.selector, - bob, - allowanceAmount, - burnAmount - ) + abi.encodeWithSelector(ERC20BurnFacet.ERC20InsufficientAllowance.selector, bob, allowanceAmount, burnAmount) ); token.burnFrom(alice, burnAmount); } @@ -214,26 +190,14 @@ contract ERC20BurnFacetTest is Test { vm.prank(bob); vm.expectRevert( - abi.encodeWithSelector( - ERC20BurnFacet.ERC20InsufficientBalance.selector, - alice, - INITIAL_SUPPLY, - amount - ) + 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 - ) - ); + vm.expectRevert(abi.encodeWithSelector(ERC20BurnFacet.ERC20InsufficientAllowance.selector, bob, 0, 100e18)); token.burnFrom(alice, 100e18); } } diff --git a/test/token/ERC20/ERC20/harnesses/ERC20BurnFacetHarness.sol b/test/token/ERC20/ERC20/harnesses/ERC20BurnFacetHarness.sol index 791a57ae..3ac302a9 100644 --- a/test/token/ERC20/ERC20/harnesses/ERC20BurnFacetHarness.sol +++ b/test/token/ERC20/ERC20/harnesses/ERC20BurnFacetHarness.sol @@ -6,11 +6,7 @@ import {ERC20BurnFacet} from "../../../../../src/token/ERC20/ERC20/ERC20BurnFace /// @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 - ); + 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) { @@ -21,10 +17,7 @@ contract ERC20BurnFacetHarness is ERC20BurnFacet { return getStorage().totalSupply; } - function allowance( - address _owner, - address _spender - ) external view returns (uint256) { + function allowance(address _owner, address _spender) external view returns (uint256) { return getStorage().allowances[_owner][_spender]; } diff --git a/test/token/ERC20/ERC20/harnesses/ERC20PermitFacetHarness.sol b/test/token/ERC20/ERC20/harnesses/ERC20PermitFacetHarness.sol index 729c1512..21df8aff 100644 --- a/test/token/ERC20/ERC20/harnesses/ERC20PermitFacetHarness.sol +++ b/test/token/ERC20/ERC20/harnesses/ERC20PermitFacetHarness.sol @@ -54,10 +54,10 @@ contract ERC20PermitFacetHarness is ERC20PermitFacet { 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; @@ -66,4 +66,4 @@ contract ERC20PermitFacetHarness is ERC20PermitFacet { emit Transfer(_from, _to, _value); return true; } -} \ No newline at end of file +}