diff --git a/script/ERC20.sol b/script/ERC20.sol index a4859160..9e635f26 100644 --- a/script/ERC20.sol +++ b/script/ERC20.sol @@ -2,17 +2,17 @@ pragma solidity ^0.8.13; import {Script} from "forge-std/Script.sol"; -import {ERC20} from "../src/ERC20/ERC20/ERC20.sol"; +import {ERC20Facet} from "../src/ERC20/ERC20/ERC20Facet.sol"; contract CounterScript is Script { - ERC20 public erc20; + ERC20Facet public erc20; function setUp() public {} function run() public { vm.startBroadcast(); - erc20 = new ERC20(); + erc20 = new ERC20Facet(); vm.stopBroadcast(); } diff --git a/src/ERC20/ERC20/ERC20Facet.sol b/src/ERC20/ERC20/ERC20Facet.sol index c6fcdf99..8ec0965b 100644 --- a/src/ERC20/ERC20/ERC20Facet.sol +++ b/src/ERC20/ERC20/ERC20Facet.sol @@ -33,6 +33,16 @@ 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. @@ -61,6 +71,7 @@ contract ERC20Facet { uint256 totalSupply; mapping(address owner => uint256 balance) balanceOf; mapping(address owner => mapping(address spender => uint256 allowance)) allowances; + mapping(address owner => uint256) nonces; } /** @@ -126,6 +137,7 @@ contract ERC20Facet { return getStorage().allowances[_owner][_spender]; } + /** * @notice Approves a spender to transfer up to a certain amount of tokens on behalf of the caller. @@ -234,4 +246,96 @@ contract ERC20Facet { } emit Transfer(msg.sender, 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 (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/ERC20/ERC20/libraries/LibERC20.sol b/src/ERC20/ERC20/libraries/LibERC20.sol index 78e58217..95d514b0 100644 --- a/src/ERC20/ERC20/libraries/LibERC20.sol +++ b/src/ERC20/ERC20/libraries/LibERC20.sol @@ -34,6 +34,12 @@ library LibERC20 { /// @param _value The 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); + /// @notice Storage slot identifier, defined using keccak256 hash of the library diamond storage identifier. bytes32 constant STORAGE_POSITION = keccak256("compose.erc20"); @@ -47,6 +53,7 @@ library LibERC20 { uint256 totalSupply; mapping(address owner => uint256 balance) balanceOf; mapping(address owner => mapping(address spender => uint256 allowance)) allowances; + mapping(address owner => uint256) nonces; } @@ -61,7 +68,7 @@ library LibERC20 { } /// @notice Mints new tokens to a specified address. - /// @dev Increases both total supply and the recipient’s balance. + /// @dev Increases both total supply and the recipient's balance. /// @param _account The address receiving the newly minted tokens. /// @param _value The number of tokens to mint. function mint(address _account, uint256 _value) internal { @@ -77,7 +84,7 @@ library LibERC20 { } /// @notice Burns tokens from a specified address. - /// @dev Decreases both total supply and the sender’s balance. + /// @dev Decreases both total supply and the sender's balance. /// @param _account The address whose tokens will be burned. /// @param _value The number of tokens to burn. function burn(address _account, uint256 _value) internal { @@ -97,7 +104,7 @@ library LibERC20 { } /// @notice Transfers tokens from one address to another using an allowance. - /// @dev Deducts the spender’s allowance and updates balances. + /// @dev Deducts the spender's allowance and updates balances. /// @param _from The address to send tokens from. /// @param _to The address to send tokens to. /// @param _value The number of tokens to transfer. @@ -124,4 +131,34 @@ library LibERC20 { } emit Transfer(_from, _to, _value); } -} + + /// @notice Transfers tokens from the caller to another address. + /// @dev Updates balances directly without allowance mechanism. + /// @param _to The address to send tokens to. + /// @param _value The number of tokens to transfer. + function transfer(address _to, uint256 _value) internal { + ERC20Storage storage s = getStorage(); + if (_to == address(0)) { + revert ERC20InvalidReceiver(address(0)); + } + uint256 fromBalance = s.balanceOf[msg.sender]; + if (fromBalance < _value) { + revert ERC20InsufficientBalance(msg.sender, fromBalance, _value); + } + unchecked { + s.balanceOf[msg.sender] = fromBalance - _value; + s.balanceOf[_to] += _value; + } + emit Transfer(msg.sender, _to, _value); + } + + /// @notice Approves a spender to transfer tokens on behalf of the caller. + /// @dev Sets the allowance for the spender. + /// @param _spender The address to approve for spending. + /// @param _value The amount of tokens to approve. + function approve(address _spender, uint256 _value) internal { + ERC20Storage storage s = getStorage(); + s.allowances[msg.sender][_spender] = _value; + emit Approval(msg.sender, _spender, _value); + } +} \ No newline at end of file diff --git a/test/ERC20.sol b/test/ERC20.sol index 49982055..086f4f4e 100644 --- a/test/ERC20.sol +++ b/test/ERC20.sol @@ -2,13 +2,13 @@ pragma solidity ^0.8.13; import {Test} from "forge-std/Test.sol"; -import {ERC20} from "../src/ERC20/ERC20/ERC20.sol"; +import {ERC20Facet} from "../src/ERC20/ERC20/ERC20Facet.sol"; contract CounterTest is Test { - ERC20 public erc20; + ERC20Facet public erc20; function setUp() public { - erc20 = new ERC20(); + erc20 = new ERC20Facet(); //erc20.setNumber(0); }