EIP.tools

EIP.tools

⚠️ DraftStandards Track: ERC

ERC-8284: Wallet-Scoped Token Pull Execution

Wallet primitive for temporary ERC-20 pull authorization during one external call.

Authors
Created2026-06-03
Discussion Linkhttps://ethereum-magicians.org/t/erc-8284-wallet-scoped-token-pull-execution/28691
Requires

Markdown

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

EIP-GPT summary

Contents
AbstractMotivationSpecificationDefinitionsInterfaceExecution SemanticsPull SemanticsERC-20 Transfer BehaviorRationaleBackwards CompatibilityTest CasesReference ImplementationSecurity ConsiderationsCopyright

Abstract

This ERC defines a wallet-native primitive for temporary ERC-20 pull authorization during one external call.

A compliant account exposes executeWithTokenPull, which opens a transient pull context for one target, one asset, and one maximum amount. During that call, the bound target may pull tokens by calling tokenPullToCaller. Outside the active call window, pull attempts MUST fail.

Motivation

Many ERC-20 settlement flows need something narrower than approve and more execution-local than a separate signature-based transfer path. Persistent allowances are broader than users usually intend, while a plain wallet-side transfer is often too inflexible for targets that determine the final amount during execution.

The common approve-then-call pattern is also cumbersome and gas-expensive. It often requires a separate approval transaction before settlement, plus a later revocation if the user wants to remove the allowance.

This ERC standardizes a narrow pattern:

  • the wallet wants to call an external target;
  • the target may need to collect one specific ERC-20 token during that call;
  • the wallet wants temporary, target-bound, asset-bound, and amount-capped authorization; and
  • the wallet does not want to leave behind a reusable allowance.

Specification

The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in RFC 2119 and RFC 8174.

Definitions

  • Account: a smart contract wallet or equivalent smart account that implements this ERC.
  • Target: the external contract that is called through executeWithTokenPull.
  • Pull context: the active execution-local authorization tuple (target, asset, remainingAmount).

Interface

Accounts implementing this ERC MUST expose the following interface:

pragma solidity ^0.8.23;

interface IERCWalletTokenPullExecutor {
    function executeWithTokenPull(
        address target,
        bytes calldata data,
        address asset,
        uint256 maxAmount
    ) external;

    function tokenPullToCaller(address asset, uint256 amount) external;
}

Accounts implementing this ERC MUST also implement ERC-165.

  • supportsInterface(type(IERCWalletTokenPullExecutor).interfaceId) MUST return true.
  • supportsInterface(type(IERC165).interfaceId) MUST return true.

Execution Semantics

executeWithTokenPull(target, data, asset, maxAmount) performs one external call while exposing a temporary ERC-20 pull capability to target.

An implementation of executeWithTokenPull:

  1. MUST create an active pull context bound to:
    • target as the only authorized caller of tokenPullToCaller;
    • asset as the only transferable ERC-20 asset; and
    • maxAmount as the maximum total amount that may be pulled during the active execution.
  2. MUST perform exactly one external call to target using data.
  3. MUST clear the active pull context before returning.
  4. MUST clear the active pull context before propagating a revert from the external call.
  5. MUST return successfully if the external call succeeds.
  6. MUST bubble revert data from the external call when revert data is available. If no revert data is available, it MUST revert with an implementation-defined error.

An implementation MAY reject unsupported or unsafe parameters or execution shapes, such as zero-address targets, zero-address assets, unsupported token contracts, concurrent pull contexts, or active runtime contexts. Those restrictions are outside the scope of this ERC and SHOULD be documented for integrators.

The authorization model for who may call executeWithTokenPull is outside the scope of this ERC. An account MAY restrict it to owners, to the account itself, to an entry point, or to another account-defined authority model.

Pull Semantics

tokenPullToCaller(asset, amount) transfers the requested ERC-20 amount from the implementing account to the caller, subject to the active pull context.

An implementation of tokenPullToCaller:

  1. MUST revert if no token-pull context is active.
  2. MUST revert unless msg.sender equals the target bound in the active pull context.
  3. MUST revert unless asset equals the asset bound in the active pull context.
  4. MUST revert unless amount is less than or equal to the remaining authorized amount in the active pull context.
  5. MUST reduce the remaining authorized amount by amount before transferring tokens.
  6. MUST transfer exactly amount of the bound token from the implementing account to msg.sender.

After executeWithTokenPull returns or reverts, subsequent calls to tokenPullToCaller for that context MUST revert.

ERC-20 Transfer Behavior

The asset contract is expected to follow ERC-20 transfer semantics.

Implementations:

  • MUST treat a failed or reverting token transfer as a failure of tokenPullToCaller;
  • SHOULD use a transfer helper equivalent to safeTransfer;
  • MUST NOT silently ignore transfer failure; and
  • MUST NOT permit the bound target to exceed the active cap by splitting requests across multiple tokenPullToCaller calls.

Rationale

The core design goal is to authorize one transfer window rather than a reusable allowance relationship. Binding the pull to one target, one asset, and one remaining amount keeps the wallet authorization surface narrow while still allowing the target to determine the final amount during execution.

tokenPullToCaller transfers only to msg.sender, so the address authorized to pull is also the recipient. Nested pull contexts, runtime-context interactions, and account authorization models are left to implementations.

Backwards Compatibility

This ERC is additive. It does not modify ERC-20 transfer behavior and does not require changes to existing token contracts. It introduces a new wallet-native settlement path rather than changing allowance semantics.

Test Cases

Implementations SHOULD cover at least the following cases:

  • executeWithTokenPull succeeds when target pulls one amount within the bound cap.
  • executeWithTokenPull succeeds when target pulls multiple amounts whose sum stays within the bound cap.
  • tokenPullToCaller reverts after the outer execution has finished.
  • tokenPullToCaller reverts when called by a contract other than the bound target.
  • tokenPullToCaller reverts when the requested asset does not match the bound asset.
  • tokenPullToCaller reverts when the requested amount exceeds the remaining authorized amount.
  • executeWithTokenPull clears context and bubbles revert data when the external call fails.
  • the account reports support for this ERC through ERC-165.

Reference Implementation

The following example uses transient storage for the pull context:

pragma solidity ^0.8.23;

import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";

interface IERCWalletTokenPullExecutor {
    function executeWithTokenPull(
        address target,
        bytes calldata data,
        address asset,
        uint256 maxAmount
    ) external;

    function tokenPullToCaller(address asset, uint256 amount) external;
}

abstract contract ERCWalletTokenPullReference is IERC165, IERCWalletTokenPullExecutor {
    using SafeERC20 for IERC20;

    bytes32 private constant TARGET_SLOT = bytes32(uint256(keccak256("walletTokenPull.target")) - 1);
    bytes32 private constant ASSET_SLOT = bytes32(uint256(keccak256("walletTokenPull.asset")) - 1);
    bytes32 private constant REMAINING_SLOT = bytes32(uint256(keccak256("walletTokenPull.remaining")) - 1);

    error PullInactive();
    error UnauthorizedPull();
    error AssetMismatch();
    error AmountExceeded();

    function executeWithTokenPull(
        address target,
        bytes calldata data,
        address asset,
        uint256 maxAmount
    ) external override {
        _authorizeExecuteWithTokenPull();
        _setPullContext(target, asset, maxAmount);
        (bool success, bytes memory response) = target.call(data);
        _clearPullContext();

        if (!success) {
            assembly ("memory-safe") {
                revert(add(response, 32), mload(response))
            }
        }
    }

    function tokenPullToCaller(address asset, uint256 amount) external override {
        (address target, address configuredAsset, uint256 remaining) = _pullContext();
        if (target == address(0)) revert PullInactive();
        if (msg.sender != target) revert UnauthorizedPull();
        if (asset != configuredAsset) revert AssetMismatch();
        if (amount > remaining) revert AmountExceeded();

        _setRemaining(remaining - amount);
        IERC20(asset).safeTransfer(msg.sender, amount);
    }

    function supportsInterface(bytes4 interfaceId) external pure override returns (bool) {
        return interfaceId == type(IERC165).interfaceId
            || interfaceId == type(IERCWalletTokenPullExecutor).interfaceId;
    }

    function _authorizeExecuteWithTokenPull() internal view virtual;

    function _pullContext() internal view returns (address target, address asset, uint256 remaining) {
        assembly {
            target := tload(TARGET_SLOT)
            asset := tload(ASSET_SLOT)
            remaining := tload(REMAINING_SLOT)
        }
    }

    function _setPullContext(address target, address asset, uint256 remaining) internal {
        assembly {
            tstore(TARGET_SLOT, target)
            tstore(ASSET_SLOT, asset)
            tstore(REMAINING_SLOT, remaining)
        }
    }

    function _setRemaining(uint256 remaining) internal {
        assembly {
            tstore(REMAINING_SLOT, remaining)
        }
    }

    function _clearPullContext() internal {
        _setPullContext(address(0), address(0), 0);
    }
}

Security Considerations

The bound target is intentionally allowed to decide whether and how much token balance to collect during the call, subject to the cap. Integrators SHOULD treat target as a real trust boundary.

Implementations MUST clear the pull context on both success and failure and reason carefully about reentrancy into account code, target code, and token code.

Some ERC-20 tokens have non-standard return values or fee-on-transfer behavior. Implementers SHOULD use transfer helpers that correctly handle transfer failure and SHOULD document any exact-debit assumptions.

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