Abstract
This EIP defines a standard pull executor interface for executing token transfers under a Permissioned Authorization Object (PPO) as specified in ERC-8103.
It provides a common, interoperable execution surface so wallets and integrators can reason about pull permissions consistently, and observers can track usage and revocation across executor implementations.
It specifies:
- the
Authorizationstruct used at execution time, - the
pullfunction signature, - required view functions for nonce usage and domain separation,
- canonical events and errors,
- normative execution rules (validation, revocation, transfer).
Motivation
Given a shared authorization primitive (ERC-8103), we need a common way to execute pulls:
- so integrators can rely on consistent behavior across executors,
- so wallets can display and reason about pull permissions,
- so registries and observers can track usage and revocation.
Today, protocols implement their own ad-hoc execution logic. This EIP defines a minimal interface that:
- can be implemented by any executor contract,
- works with any ERC-20 token,
- supports revocation and replay protection,
- cleanly separates authorization (off-chain) from execution (on-chain).
Specification
Interface
Compliant contracts MUST implement the following Solidity interface (or an ABI-compatible equivalent):
pragma solidity ^0.8.20;
interface IPermissionedPullExecutor {
struct Authorization {
address grantor;
address grantee;
address token;
uint256 maxPerPull;
uint256 validAfter;
uint256 validBefore;
bytes32 nonce;
}
// Views
/// @notice EIP-712 domain separator used for Authorization digests.
function domainSeparator() external view returns (bytes32);
/// @notice Returns true if the given nonce has been used for this grantor.
function isNonceUsed(address grantor, bytes32 nonce) external view returns (bool);
/// @notice Returns true if the given nonce has been locally canceled for this grantor.
function isNonceCanceled(address grantor, bytes32 nonce) external view returns (bool);
// Mutations
/// @notice Executes a token pull from grantor to msg.sender (must equal auth.grantee),
/// under the constraints encoded in the Authorization.
function pull(
uint256 amount,
Authorization calldata auth,
bytes calldata signature
) external;
/// @notice Locally cancels a nonce for the caller, preventing future use.
function cancel(bytes32 nonce) external;
// Events
event PullExecuted(
address indexed grantor,
address indexed grantee,
address indexed token,
uint256 amount,
bytes32 structHash
);
event NonceUsed(
address indexed grantor,
bytes32 indexed nonce
);
event NonceCanceled(
address indexed grantor,
bytes32 indexed nonce
);
// Errors
error BadSignature();
error NotYetValid();
error Expired();
error OverCap();
error NonceAlreadyUsed();
error Revoked();
error WrongGrantee();
error ZeroAddress();
error ZeroAmount();
error TransferFailed();
}Execution Rules (Normative)
An implementation of pull MUST:
-
Compute digest
- Compute
structHashexactly as specified in ERC-8103. - Compute
digestas:
- Compute
bytes32 digest = keccak256(
abi.encodePacked(
"\x19\x01",
domainSeparator(),
structHash
)
);-
Verify signature
- Recover signer from
digestandsignature. - Signer MUST equal
auth.grantor. - Implementations MUST reject malleable signatures by enforcing EIP-2 rules (low-
svalues and validv), as specified in ERC-8103. - Otherwise, revert with
BadSignature().
Implementations MAY support:
- EOAs via
ecrecover - Smart contract wallets via EIP-1271 (
isValidSignature), but behavior MUST be documented.
- Recover signer from
-
Validate caller
msg.senderMUST equalauth.grantee.- Otherwise, revert with
WrongGrantee().
-
Validate time window
- If
block.timestamp < auth.validAfter, revertNotYetValid(). - If
block.timestamp >= auth.validBefore, revertExpired().
- If
-
Validate amount and addresses
- If
amount == 0, revertZeroAmount(). - If
auth.token == address(0), revertZeroAddress(). - If
amount > auth.maxPerPull, revertOverCap().
- If
-
Check revocation
- If
isNonceCanceled(auth.grantor, auth.nonce)returns true, revertRevoked(). - If implementation also integrates a shared registry (out of scope of this EIP), it MAY check an external
isRevokedview and MUST revertRevoked()if revoked.
- If
-
Check and mark nonce used
- If
isNonceUsed(auth.grantor, auth.nonce)is true, revertNonceAlreadyUsed(). - Otherwise, implementations MUST atomically mark
(auth.grantor, auth.nonce)as used before any external token transfer. - Nonce marking MUST follow checks-effects-interactions; state changes MUST precede external calls.
- If
-
Execute transfer
- Call
IERC20(auth.token).transferFrom(auth.grantor, auth.grantee, amount). - If the transfer fails (according to the tokenโs semantics), revert
TransferFailed().
- Call
-
Emit events
- Emit
NonceUsed(auth.grantor, auth.nonce). - Emit
PullExecuted(auth.grantor, auth.grantee, auth.token, amount, structHash).
- Emit
Domain Separation
domainSeparator()MUST return a stable EIP-712 domain separator used for all PPO digests.- Deployments on different chains SHOULD use:
nameandversionfields appropriate to the executor, and- the chainโs
chainIdper EIP-712.
This prevents cross-executor and cross-chain replay unless the same executor domain is intentionally replicated.
Rationale
- Separate executor standard: Keeps execution semantics independent of any specific registry or higher-level coordination logic.
- Single
pullentry point: Simple mental model for users and integrators. - Canonical errors: Makes integration, testing, and UX (e.g., decoding revert reasons) easier.
- Explicit nonces: Allow clear auditability and prevent ambiguous replay behavior.
Backwards Compatibility
- Works with any ERC-20 token that supports
transferFrom. - Compatible with existing allowance patterns; PPO-based pulls can coexist with traditional approvals.
Security Considerations
The rules required for secure behavior are specified normatively in the Specification section. This section discusses implications and common pitfalls:
- Implementations must consider token quirks (fee-on-transfer, non-standard return values) and should document behavior with such tokens.
- Time windows reduce exposure of leaked signatures; however, key security is still critical.
- Malicious grantees cannot exceed
maxPerPullin a single call, but they can callpullmultiple times if the authorization semantic allows. Systems requiring cumulative caps should implement them in higher layers or registries.
Copyright
Copyright and related rights waived via CC0.
