@perfect-abstractions/compose/token/ERC20/Permit/ERC20PermitFacet.solSet an allowance from a signed message instead of a transaction, following EIP-2612
permit sets an allowance from an owner's signature, so the owner never has to send a transaction.This facet touches three separate slots: the allowance it writes, the token name it hashes into the EIP-712 domain, and the nonce it consumes.
ERC20_METADATA_STORAGE_POSITIONbytes32keccak256("erc20.metadata"))ERC20_STORAGE_POSITIONbytes32keccak256("erc20"))STORAGE_POSITIONbytes32keccak256("nonces"))Only the name field is declared here, because the domain separator needs nothing else.
This is a layout-compatible prefix of the metadata struct, not a different slot. name is the first field either way, so this facet reads exactly the same string that ERC20MetadataFacet returns from name(). The symbol and decimals fields are simply not declared, because permit never reads them.
The nonce slot is keccak256("nonces"), a bare identifier rather than something like erc20.nonces. Any other contract in the same diamond that picks the string "nonces" for its own storage will land on this slot and corrupt permit nonces. Nothing else in Compose uses it today.
Returns the number of permits _owner has already used. The next signature must be signed with this value.
Parameters:
_owneraddress0.Returns:
-uint256Returns the EIP-712 domain separator that signatures must be built against.
It is computed on every call rather than cached at deployment, from four inputs: the token name read from metadata storage, the hardcoded version string "1", the current block.chainid, and the diamond's own address.
Returns:
-bytes32Verifies an EIP-2612 signature and sets allowance[_owner][_spender] to _value.
The call is permissionless. Whoever relays the signature pays the gas, and the allowance is recorded for _owner regardless of who submitted it. The owner's nonce increases by one, and only on success.
Parameters:
_owneraddress_spenderaddressaddress(0)._valueuint256_deadlineuint256block.timestamp equals the deadline is still accepted._vuint8_rbytes32r value of the signature._sbytes32s value of the signature.Reverts:
ERC20InvalidSpendererror_spender is address(0). Checked before anything else.ERC2612InvalidSignatureerror_owner, or recovery failed. All three cases share this one error.Emitted on every successful permit. Identical in shape to the event ERC20ApproveFacet emits, so an allowance set by signature is indistinguishable in the logs from one set by a transaction.
_owneraddress_spenderaddress_valueuint256Thrown for every signature failure: an expired deadline, a signer that does not match _owner, or a recovery that returned address(0). Because one error covers all three, a caller cannot tell an expired permit from a malformed one without checking the deadline separately.
_owneraddress_spenderaddress_valueuint256_deadlineuint256_vuint8_rbytes32r value of the rejected signature._sbytes32s value of the rejected signature.Thrown when _spender is the zero address. This check runs before the deadline and signature checks, so it fires even on an otherwise invalid permit.
_spenderaddressaddress(0).ERC20MetadataFacet and call setMetadata before anyone signs a permit. With an unset name the domain is built from an empty string, and every signature becomes invalid the moment the name is filled in.nonces(owner) when building a signature, and DOMAIN_SEPARATOR() rather than reconstructing the domain by hand.ERC20TransferFacet, since a permit is only useful if the spender can then call transferFrom.Renaming the token invalidates every outstanding permit. The domain separator hashes the name read from metadata storage, and it is recomputed on each call rather than cached. Calling setMetadata with a different name silently changes the domain, so signatures already in flight stop verifying and any integrator caching the old DOMAIN_SEPARATOR() starts producing rejected signatures.
The nonce is consumed only on success. It is incremented after the signature check passes, so failed submissions do not burn nonces and cannot be used to grief an owner's pending signatures.
Anyone can submit someone else's permit. That is the design, but it means a contract that bundles permit and a follow-up action in one transaction can be broken by a third party submitting the permit first: the bundled call then reverts on the now-stale nonce. Tolerate an already-consumed permit if you bundle.
Signature malleability is not rejected, but replay is prevented. ecrecover is called directly, with no low-s bound and no check that _v is 27 or 28, so an altered but still valid encoding of a signature recovers the same signer. This does not enable replay, because the nonce has already moved on after the first use. The one failure mode ecrecover does have is covered: a recovery that returns address(0) is rejected explicitly, so a malformed signature cannot be passed off as a permit from the zero address.
The deadline is inclusive. The check rejects only when block.timestamp is strictly greater than _deadline, so a permit remains usable during the block whose timestamp equals its deadline.