EIP.tools

EIP.tools

โš ๏ธ DraftStandards Track: ERC

ERC-8102: Permissioned Pull

Pull-based token transfers authorized by signed, time-bounded Permissioned Authorization Objects (PPOs).

Authors
Created2025-12-11
Discussion Linkhttps://ethereum-magicians.org/t/erc-8102-permissioned-pull/25931
Requires

Markdown

https://raw.githubusercontent.com/ethereum/ERCs/re...
Pull Request#1401PR open

EIP-GPT summary

Contents
AbstractMotivationSpecificationInterfaceExecution Rules (Normative)Domain SeparationRationaleBackwards CompatibilitySecurity ConsiderationsCopyright

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 Authorization struct used at execution time,
  • the pull function 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:

  1. Compute digest

    • Compute structHash exactly as specified in ERC-8103.
    • Compute digest as:
bytes32 digest = keccak256(
    abi.encodePacked(
        "\x19\x01",
        domainSeparator(),
        structHash
    )
);
  1. Verify signature

    • Recover signer from digest and signature.
    • Signer MUST equal auth.grantor.
    • Implementations MUST reject malleable signatures by enforcing EIP-2 rules (low-s values and valid v), 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.
  2. Validate caller

    • msg.sender MUST equal auth.grantee.
    • Otherwise, revert with WrongGrantee().
  3. Validate time window

    • If block.timestamp < auth.validAfter, revert NotYetValid().
    • If block.timestamp >= auth.validBefore, revert Expired().
  4. Validate amount and addresses

    • If amount == 0, revert ZeroAmount().
    • If auth.token == address(0), revert ZeroAddress().
    • If amount > auth.maxPerPull, revert OverCap().
  5. Check revocation

    • If isNonceCanceled(auth.grantor, auth.nonce) returns true, revert Revoked().
    • If implementation also integrates a shared registry (out of scope of this EIP), it MAY check an external isRevoked view and MUST revert Revoked() if revoked.
  6. Check and mark nonce used

    • If isNonceUsed(auth.grantor, auth.nonce) is true, revert NonceAlreadyUsed().
    • 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.
  7. Execute transfer

    • Call IERC20(auth.token).transferFrom(auth.grantor, auth.grantee, amount).
    • If the transfer fails (according to the tokenโ€™s semantics), revert TransferFailed().
  8. Emit events

    • Emit NonceUsed(auth.grantor, auth.nonce).
    • Emit PullExecuted(auth.grantor, auth.grantee, auth.token, amount, structHash).

Domain Separation

  • domainSeparator() MUST return a stable EIP-712 domain separator used for all PPO digests.
  • Deployments on different chains SHOULD use:
    • name and version fields appropriate to the executor, and
    • the chainโ€™s chainId per 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 pull entry 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 maxPerPull in a single call, but they can call pull multiple times if the authorization semantic allows. Systems requiring cumulative caps should implement them in higher layers or registries.

Copyright and related rights waived via CC0.

EIP.tools

EIP.tools

Search, read, and map Ethereum improvement proposals, ERCs, RIPs, and CAIPs from one focused interface.

Farcaster
by @apoorveth