Library Tokens ERC-20 Permit Module On this page
@perfect-abstractions/compose/token/ERC20/Permit/ERC20PermitMod.solCopy
Helper functions for verifying EIP-2612 permits and accessing permit storage
permit performs the full EIP-2612 flow: deadline check, signature recovery, allowance write, nonce bump.
It emits Approval itself, so your facet must not emit it again.
Exposes three storage getters, one per slot it touches.
Storage
This module touches three separate slots: the allowance it writes, the token name it hashes into the EIP-712 domain, and the nonce it consumes.
State Variables
Property Type DescriptionERC20_METADATA_STORAGE_POSITIONbytes32 Metadata storage position, read for the token name (Value: keccak256("erc20.metadata"))ERC20_STORAGE_POSITIONbytes32 ERC-20 storage position, where the allowance is written (Value: keccak256("erc20"))STORAGE_POSITIONbytes32Nonce storage position (Value: keccak256("nonces"))
Only the name field is declared here, because the domain separator needs nothing else.
Definition
struct ERC20MetadataStorage {
string name ;
}
This is a layout-compatible prefix of the metadata struct, not a different slot. name is the first field either way, so this module reads exactly the same string ERC20MetadataMod writes. The symbol and decimals fields are simply not declared, because permit never reads them.
ERC20Storage
Definition
struct ERC20Storage {
mapping ( address owner => uint256 balance ) balanceOf ;
uint256 totalSupply ;
mapping ( address owner => mapping ( address spender => uint256 allowance ) ) allowance ;
}
NoncesStorage
Definition
struct NoncesStorage {
mapping ( address owner => uint256 ) nonces ;
}
Functions
Returns a pointer to the one-field ERC20MetadataStorage struct, used to read the token name for the domain separator.
function getERC20MetadataStorage ( ) pure returns ( ERC20MetadataStorage storage s ) ;
Returns:
Property Type DescriptionsERC20MetadataStorage storageThe struct in storage, with name only.
getERC20Storage
Returns a pointer to the ERC20Storage struct, where allowances live.
function getERC20Storage ( ) pure returns ( ERC20Storage storage s ) ;
Returns:
Property Type DescriptionsERC20Storage storageThe struct in storage.
getPermitStorage
Returns a pointer to the NoncesStorage struct.
Read an owner's nonce with getPermitStorage().nonces[_owner]. This module has no nonces() view of its own, unlike ERC20PermitFacet .
function getPermitStorage ( ) pure returns ( NoncesStorage storage s ) ;
Returns:
Property Type DescriptionsNoncesStorage storageThe struct in storage.
DOMAIN_SEPARATOR
Returns the EIP-712 domain separator that signatures must be built against.
It is computed on every call rather than cached, from the token name in metadata storage, the hardcoded version string "1", the current block.chainid, and the address of the contract executing the code.
function DOMAIN_SEPARATOR ( ) view returns ( bytes32 ) ;
Returns:
Property Type Description-bytes32The domain separator for this token on this chain.
A file-level free function has no this, so the module cannot write address(this) the way the facet does. It reads the executing address with a one-line assembly block instead. Under delegatecall both forms resolve to the same value, the diamond's address, so the module and the facet produce identical domain separators.
permit
Verifies an EIP-2612 signature and sets allowance[_owner][_spender] to _value, then increments the owner's nonce and emits Approval.
The whole flow lives here. Your facet supplies the external entrypoint and nothing else, and it must not emit Approval a second time.
function permit ( address _owner , address _spender , uint256 _value , uint256 _deadline , uint8 _v , bytes32 _r , bytes32 _s ) ;
Parameters:
Property Type Description_owneraddress The address that signed the permit and whose allowance is set. Must match the recovered signer._spenderaddress The address receiving the allowance. Cannot be address(0)._valueuint256 The new allowance. It replaces any previous value rather than adding to it._deadlineuint256 Unix timestamp after which the signature is refused. A permit submitted in the block where block.timestamp equals the deadline is still accepted._vuint8 Recovery byte of the signature._rbytes32 The r value of the signature._sbytes32The s value of the signature.
Reverts:
Property Type DescriptionERC20InvalidSpendererror_spender is address(0). Checked before anything else.ERC2612InvalidSignatureerrorThe deadline has passed, or the recovered signer is not _owner, or recovery failed. All three cases share this one error.
Events
Emitted by this module on every successful permit. Your facet should not emit it again, or the allowance change will appear twice in the logs.
Signature: event Approval ( address indexed _owner , address indexed _spender , uint256 _value ) ;
Parameters:
Property Type Description_owneraddress The signer granting the allowance. Not the account that submitted the transaction._spenderaddress The address receiving the allowance._valueuint256The new allowance.
Errors
Thrown for every signature failure: an expired deadline, a signer that does not match _owner, or a recovery that returned address(0). One error covers all three, so a caller cannot tell them apart without checking the deadline separately.
Signature: error ERC2612InvalidSignature (
address _owner , address _spender , uint256 _value , uint256 _deadline , uint8 _v , bytes32 _r , bytes32 _s
) ;
Parameters:
Property Type Description_owneraddress The address the permit claimed to be from._spenderaddress The spender named in the permit._valueuint256 The allowance the permit asked for._deadlineuint256 The deadline carried by the permit._vuint8 Recovery byte of the rejected signature._rbytes32 The r value of the rejected signature._sbytes32The s value of the rejected signature. Thrown when _spender is the zero address. This check runs before the deadline and signature checks.
Signature: error ERC20InvalidSpender ( address _spender ) ;
Best Practices
Set the token name with ERC20MetadataMod.setMetadata during deployment, before anyone signs a permit. Changing the name later invalidates every signature already in circulation.
Expose DOMAIN_SEPARATOR and a nonce getter from your facet as well. Wallets and signing libraries expect to read both on-chain, and EIP-2612 requires a nonces(address) view that this module does not provide.
Do not emit Approval from your wrapper. This module already emits it.
If you only need the standard EIP-2612 surface, use ERC20PermitFacet instead. It exports nonces, DOMAIN_SEPARATOR and permit ready to install.
Integration Notes
Import the module under a namespace and call it from your facet:
Exposing permit from a custom facet
import "src/token/ERC20/Permit/ERC20PermitMod.sol" as ERC20PermitMod ;
contract MyFacet {
function permit (
address _owner ,
address _spender ,
uint256 _value ,
uint256 _deadline ,
uint8 _v ,
bytes32 _r ,
bytes32 _s
) external {
ERC20PermitMod . permit ( _owner , _spender , _value , _deadline , _v , _r , _s ) ;
}
function nonces ( address _owner ) external view returns ( uint256 ) {
return ERC20PermitMod . getPermitStorage ( ) . nonces [ _owner ] ;
}
}
The allowance is written to ERC20Storage at keccak256("erc20"), the same slot ERC20DataFacet reads and ERC20TransferFacet spends, so a permit and a plain approve are interchangeable from every other contract's point of view.
This module writes the allowance directly rather than calling ERC20ApproveMod . It has to: that helper always acts for msg.sender, and permit must act for the signer instead.
Last updated: September 16, 2026