Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
221 changes: 221 additions & 0 deletions ERCS/erc-8102.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
---
eip: 8102
title: Permissioned Pull
description: Pull-based token transfers authorized by signed, time-bounded Permissioned Authorization Objects (PPOs).
author: Mats Heming Julner (@recurmj)
discussions-to: https://ethereum-magicians.org/t/erc-8102-permissioned-pull/25931
status: Draft
type: Standards Track
category: ERC
created: 2025-12-11
requires: 712, 8103
---

## Abstract

This EIP defines a standard **pull executor interface** for executing token transfers under a Permissioned Authorization Object (PPO) as specified in [ERC-8103](./eip-8103.md).
Comment thread
SamWilsn marked this conversation as resolved.

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).
Comment on lines +16 to +26

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This does a good job covering what your proposal is, but doesn't really include the purpose. Doesn't have to be long (that's what the motivation is for), but a sentence or two about why we need this proposal is usually good in the abstract.


## 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](./eip-20.md) 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):

```solidity
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:

```solidity
bytes32 digest = keccak256(
abi.encodePacked(
"\x19\x01",
domainSeparator(),
structHash
)
);

```

2. **Verify signature**

- Recover signer from `digest` and `signature`.
- Signer MUST equal `auth.grantor`.
- Implementations MUST reject malleable signatures by enforcing [EIP-2](./eip-2.md) 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](./eip-1271.md) (`isValidSignature`), but behavior MUST be documented.

3. **Validate caller**

- `msg.sender` MUST equal `auth.grantee`.
- Otherwise, revert with `WrongGrantee()`.

4. **Validate time window**

- If `block.timestamp < auth.validAfter`, revert `NotYetValid()`.
- If `block.timestamp >= auth.validBefore`, revert `Expired()`.

5. **Validate amount and addresses**

- If `amount == 0`, revert `ZeroAmount()`.
- If `auth.token == address(0)`, revert `ZeroAddress()`.
- If `amount > auth.maxPerPull`, revert `OverCap()`.

6. **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.

7. **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.

8. **Execute transfer**

- Call `IERC20(auth.token).transferFrom(auth.grantor, auth.grantee, amount)`.
- If the transfer fails (according to the token’s semantics), revert `TransferFailed()`.

9. **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](./eip-712.md) 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

Copyright and related rights waived via [CC0](../LICENSE.md).
Loading
Loading