--- eip: 8103 title: Permissioned Authorization Object (PPO) description: A signed, portable, revocable EIP-712 authorization struct enabling bounded pull-based token transfers. author: Mats Heming Julner (@recurmj) discussions-to: https://ethereum-magicians.org/t/erc-8103-permissioned-authorization-object/27135 status: Draft type: Standards Track category: ERC created: 2025-12-11 requires: 712 --- ## Abstract This EIP defines a **Permissioned Authorization Object (PPO)**: an [EIP-712](./eip-712.md) typed struct describing a *bounded, revocable permission to pull tokens* from a grantor. A PPO is: - signed off-chain by the **grantor**, - specifies the **grantee**, **token**, **per-call cap**, **time window**, and **nonce**, - portable across chains and executors that adopt the same type, - revocable prior to execution by invalidating the nonce. It does **not** prescribe how transfers are executed; that is defined by a separate EIP ("Permissioned Pull", [ERC-8102](./eip-8102.md)). ## Motivation Today, [ERC-20](./eip-20.md) and related standards lack a canonical primitive for *fine-grained pull permissions*. We have: - `transfer` — one-shot push - `approve/transferFrom` — persistent allowance, but: - often infinite, - not time-bounded, - not natively portable across chains, - approvals live in token contract state (harder to audit/rotate) - `permit` ([EIP-2612](./eip-2612.md)) — signed approvals, but tightly coupled to token contracts What is missing is a **chain-agnostic, revocable authorization object** that: - encodes *bounded* consent in a single signed payload, - can be verified by any compliant executor contract, - is easy for wallets and AA stacks to reason about, - can be revoked before use, - composes with any ERC-20 token without requiring token changes. This EIP specifies such an authorization struct and its hashing rules, so that: - executors (ERC-8102) can implement a common verification model, - registries and wallets can expose and revoke PPOs consistently, - systems can build higher-level coordination (subscriptions, automation) on a shared authorization primitive. ## Specification ### EIP-712 Type Definition The canonical type for a Permissioned Authorization Object is: ```text Authorization( address grantor, address grantee, address token, uint256 maxPerPull, uint256 validAfter, uint256 validBefore, bytes32 nonce ) ``` Where: - `grantor`: address providing consent (token owner). - `grantee`: address allowed to initiate pulls under this authorization. - `token`: ERC-20 token address. - `maxPerPull`: maximum number of `token` units that may be pulled per execution. - `validAfter`: unix timestamp (seconds) from which this authorization becomes valid (inclusive). - `validBefore`: unix timestamp (seconds) after which this authorization is no longer valid (exclusive). - `nonce`: unique value chosen by the grantor for replay protection. #### TYPEHASH (normative) The canonical `AUTH_TYPEHASH` MUST be: ```solidity bytes32 public constant AUTH_TYPEHASH = keccak256( "Authorization(address grantor,address grantee,address token,uint256 maxPerPull,uint256 validAfter,uint256 validBefore,bytes32 nonce)" ); ``` #### Struct Hash (normative) Given: ```solidity struct Authorization { address grantor; address grantee; address token; uint256 maxPerPull; uint256 validAfter; uint256 validBefore; bytes32 nonce; } ``` The struct hash MUST be computed as: ```solidity function _authStructHash(Authorization memory a) internal pure returns (bytes32) { return keccak256( abi.encode( AUTH_TYPEHASH, a.grantor, a.grantee, a.token, a.maxPerPull, a.validAfter, a.validBefore, a.nonce ) ); } ``` The EIP-712 digest is then: ```solidity bytes32 digest = keccak256( abi.encodePacked( "\x19\x01", domainSeparator, _authStructHash(auth) ) ); ``` where `domainSeparator` is as defined in EIP-712 by the verifying contract (typically the executor contract specified in ERC-8102). ### Revocation Model This EIP defines requirements on revocation semantics but does not prescribe storage layout. Implementations MAY use: - local per-executor storage, and/or - a shared registry contract, and/or - wallet-maintained revocation maps. Normative requirements: 1. Grantors MUST be able to revoke any *unused* `nonce` prior to execution. 2. Executors MUST check revocation status at execution time according to the mechanism adopted by the executor (e.g., local cancellation and/or shared registry). 3. Executors MUST reject revoked or canceled nonces. Recommended events (names are illustrative): ```solidity event AuthorizationGranted( address indexed grantor, address indexed grantee, address indexed token, bytes32 nonce ); event AuthorizationRevoked( address indexed grantor, bytes32 indexed nonce ); ``` Wallets and registries MAY emit these upon signing or revoking PPOs. ### Nonce Semantics - `nonce` MUST be treated as **single-use** in the context of a given grantor and domain separator. - Implementations SHOULD treat `(grantor, nonce)` as a unique pair. - Nonce consumption MUST be race-safe: an executor MUST mark the nonce as used *before* or atomically with any external calls (e.g., token transfers). ### Domain Binding This EIP intentionally separates: - the **authorization object** (defined herein), and - the **execution domain** (ERC-8102, pull executor). Domain binding is achieved in ERC-8102 by including the executor’s `domainSeparator()` in the EIP-712 digest. This prevents signatures from being replayed across executors with different domains. ### Executor Verification Requirements Executors verifying PPOs MUST: - enforce validity windows (`validAfter`, `validBefore`) before execution, - enforce [EIP-2](./eip-2.md) signature constraints (low-`s`, valid `v`), - bind verification to their own domain separator. ## Rationale - **Separation of concerns**: This proposal defines “what is consent?” while ERC-8102 defines “how is consent executed?”. This keeps wallets, AA stacks, and registries decoupled from any specific executor implementation. - **Stateless across chains**: `maxPerPull` and time windows keep each execution stateless; cumulative limits can be built on top, but are not required at the primitive level. - **Nonce as bytes32**: Allows flexible schemes (per-grantee, per-token, per-policy) while remaining compatible with simple counter-based patterns. ## Backwards Compatibility - Compatible with any ERC-20 token; no token changes required. - Orthogonal to EIP-2612; PPOs are a different authorization path (pull-by-consent vs “permit as approve”). ## Security Considerations The rules required for safe execution are specified normatively in the Specification section. This section discusses implications: - Revocation must be checked at execution time to avoid race conditions between revoke and pull. - Wallet UX should make nonce scope and domain binding clear to users. - Grantors should protect signing keys; leaked signatures remain valid until expiry or revocation. ## Copyright Copyright and related rights waived via [CC0](../LICENSE.md).