EIP.tools

EIP.tools

⚠️ DraftStandards Track: ERC

ERC-8009: Proxy Clear Signing

Permissionless singleton contract that proxies smart-contract calls and enforces transaction constraints specified by the end user

Authors
Created2025-08-19
Discussion Linkhttps://ethereum-magicians.org/t/erc-8009-proxy-clear-signing/25199
Requires

Markdown

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

EIP-GPT summary

Contents
AbstractMotivationSpecificationContract OverviewBalanceProxySemanticsRationaleCore/Periphery Contract ModelSafe Multisig SupportComparison to Other StandardsEIP-6120Intent-Based State TransitionBackwards CompatibilityTest CasesReference ImplementationSecurity ConsiderationsCopyright

Abstract

Introduce a singleton, stateless contract that proxies arbitrary smart‑contract calls and enforces user‑supplied outcome constraints. The proxy supports absolute post‑balance thresholds and balance‑difference (delta) checks over native ETH and ERC-20 balances. Calls that violate the declared constraints are reverted.

Motivation

Current hardware wallets struggle to present meaningful transaction information when interacting with unknown or newly deployed smart contracts. Without prior knowledge of a contract’s ABI or address, these devices can only display raw hexadecimal calldata - an unreadable and unsafe experience for most users. This practice, known as blind signing, leaves users vulnerable to unintentionally approving malicious or incorrect transactions. This vulnerability has already been exploited in high-value attacks.

Instead of parsing transaction calldata, the proposed approach focuses on verifying expected balance differences after execution. This proposal introduces an alternative path to clear signing - defined here as the ability for users to verify the outcome of a transaction before signing, without requiring ABI knowledge. By routing calls through a singleton proxy contract that enforces these expectations on-chain, hardware wallets can reliably display the “balance after transaction” values to the user, taken from the smart-contract parameters. This enables a permissionless, scalable, and understandable transaction confirmation experience, even for unknown contracts.

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.

Contract Overview

A singleton, stateless permissionless proxy contract deployed once per network.

The contract accepts transaction execution requests containing:

  1. Target call data — Encoded calldata for the intended interaction.
  2. Approvals — Optional ERC-20 instructions executed before the target call to either approve the call target to spend proxy-held ERC-20 tokens or transfer proxy-held tokens directly to the call target.
  3. Withdrawals — Optional ETH/ERC-20 transfers executed after the target call (from the proxy to specified recipients).
  4. Balance rules — One of:
    • Post‑balance rules: minimum absolute balances after execution; or
    • Balance‑difference rules: minimum required deltas between pre‑ and post‑execution balances.

The contract MUST NOT store persistent state. Multiple independent users MAY reuse the same instance without interference.

BalanceProxy

The BalanceProxy MUST implement the following interface:

// SPDX-License-Identifier: CC0-1.0
pragma solidity ^0.8.27;

/// @title IBalanceProxy
/// @notice Minimal interface for the BalanceProxy core contract
/// @dev Core is source-agnostic: it never pulls tokens, only uses its own balances
interface IBalanceProxy {
    /// @notice Struct to represent balance or value of specific token by target address
    /// @param target Target address
    /// @param token Token address (address(0) for ETH)
    /// @param balance Expected absolute balance (post) or diff (signed)
    struct Balance {
        address target;
        address token;
        int256 balance;
    }

    /// @notice Approval instruction: either transfer tokens to target or approve target to spend
    struct Approval {
        Balance balance; // token, target, amount(>=0 expected)
        bool useTransfer; // true: transfer to target; false: approve target
    }

    /// @notice Error when actual diff != expected
    error ERC8009BalanceDiffConstraintViolation(
        address token,
        address target,
        int256 expected,
        int256 actual
    );

    /// @notice Error thrown when a balance is insufficient
    error ERC8009BalanceConstraintViolation(
        address token,
        address target,
        int256 balance,
        uint256 actual
    );

    /// @notice Error thrown when a call fails
    error ERC8009CallFailed(address target, bytes data, bytes returnData);

    /// @notice Proxy call to a target contract with specified post-balance checks
    function proxyCall(
        Balance[] memory postBalances,
        Approval[] memory approvals,
        address target,
        bytes memory data,
        Balance[] memory withdrawals
    ) external payable returns (bytes memory);

    /// @notice Proxy call with balance diffs
    function proxyCallDiffs(
        Balance[] memory diffs,
        Approval[] memory approvals,
        address target,
        bytes memory data,
        Balance[] memory withdrawals
    ) external payable returns (bytes memory);
}

This standard defines a singleton, stateless proxy that:

  1. Optionally approves or transfers ERC-20 amounts for a designated spender (approvals),
  2. Proxies a call to a target contract,
  3. Optionally performs withdrawals from the proxy (withdrawals),
  4. Enforces user-specified post-execution balance checks (postBalances).

If any check fails or the proxied call fails, the transaction MUST revert.

Semantics

Execution MUST proceed in this order:

  1. Approvals
  2. Target call
  3. Withdrawals
  4. Post-balance checks (if provided) OR diff checks (if provided)

For absolute rules, balances are compared using actual >= abs(required). For diff rules, deltas are computed as after - before and compared to the signed requirement.

Rationale

Several design decisions have been made to maximize reusability, reduce integration overhead, and simplify the verification model:

  1. Stateless – The contract holds no persistent state. All parameters, including balance requirements, approvals, and transfers, are supplied per call. This removes per-user deployment and initialization steps, avoids migrations, and keeps the execution path auditable.
  2. Protocol-agnostic – The proxy forwards arbitrary calldata without protocol-specific parsing or allowlists. It applies only generic balance and transfer rules, making it immediately usable with any newly deployed contract that transfers tokenized assets.
  3. Standardized interface and permissionless – While a canonical deployment is expected per network, this ERC primarily standardizes the IBalanceProxy interface. This is what makes coordination necessary: hardware wallets, periphery contracts, and other tooling can be built against the interface and interoperate with any conforming implementation. A well-known interface also allows independent implementations to exist for different environments (e.g. testnets, L2s, or alternative security models).
  4. UI-focused — the contract contains enough information in the parameters for hardware wallets to parse and display to the user. This includes, beyond the information required for execution, the token's decimals and symbol, which allow the hardware wallet to display the token name and format the values correctly.

These choices yield a contract that is simple to integrate, predictable to verify, and easy to reuse across applications.

The limitation of this solution is that enforcement is limited to tokenized balances (ETH or ERC-20) and cannot apply to non-transferable state stored within other contracts, such as staking positions or lending positions.

An additional "preBalances" parameter was discussed to be introduced into the contract, for the use case when a transaction is only relevant to be executed when an arbitrary address has certain balance. Because of how rare the use case is, it has been decided not to include this parameter into the contract.

A critical vulnerability was identified in the initial design: when users approve tokens to the Proxy, it was possible for an attacker to steal the tokens by crafting a transaction that calls the transferFrom function. A malicious actor could exploit the contract by sending a transaction like:

proxy.proxyCall([...], [...], tokenAddress, abi.encodeFunction("transferFrom", ...), [...]);

In response to this vulnerability, we initially considered banning the transferFrom function selector, but this approach seemed non-standard and diverged from the usual ERC token handling pattern. Instead, we decided to implement the Core/Periphery split. This split allows token transfers, including transferFrom, to be handled in the Periphery, ensuring that the Core contract remains focused on transaction execution and balance validation. This approach maintains ERC compatibility while resolving the security issue effectively.

Core/Periphery Contract Model

To address concerns around argument handling, user interface display, and support for various hardware wallets, we have split the logic into Core and Periphery contracts.

  1. Core Contract: The core contract focuses solely on executing transactions, including transferring tokens, checking balances, and enforcing balance rules. It does not deal with data formatting or UI presentation, ensuring it remains minimal, universal, and focused on performing the actual logic.
  2. Periphery Contract: The peripheral contract handles all interaction with the user interface and external hardware wallets. It takes care of token argument handling, including decimals, symbols, and other UI-related data that is specific to the hardware wallet or user preferences.

This separation ensures that the Core remains lightweight and adaptable, while the Periphery can be customized to match different wallets' needs.

Safe Multisig Support

SafeRouter is a periphery contract that adds support for Safe multisig wallets, used by teams that require multiple signers to authorize transactions. This is a good example of how periphery contracts complement the core: the BalanceProxy core remains focused solely on balance enforcement, while SafeRouter handles the Safe-specific coordination needed to make that enforcement available to multisig participants.

SafeRouter is added as an owner of the Safe, occupying one signing slot in the threshold. When the co-signers have collected their required signatures, they submit the transaction through SafeRouter. SafeRouter first verifies that the caller is an existing owner of the Safe, then makes a call to BalanceProxy, passing SafeRouter's own executeSafeTransaction function as the target call. This is because BalanceProxy needs to capture balance differences across the execution, which requires recording balances both before and after the target call. At the point of BalanceProxy executing the call, SafeRouter verifies the callback came from the expected BalanceProxy instance, provides its approval for the transaction on the multisig contract, and calls safe.execTransaction. Only after the Safe transaction has executed does BalanceProxy check the declared post-balance constraints. If any constraint is violated the entire transaction reverts, including the Safe execution.

Comparison to Other Standards

EIP-6120

EIP-6120 imposes a key limitation by requiring that the target contract cannot be a token (such as an ERC-20), which restricts its use in DeFi applications that involve token transfers. This makes it incompatible with the majority of real-world smart contracts. In contrast, our approach does not have this restriction, allowing the target contract to be any contract, including tokens, making it more flexible and compatible with existing DeFi standards. By separating the transaction logic (Core) from the user interface handling (Periphery), our solution ensures scalability and adaptability for a wide range of wallets, without compromising functionality or compatibility.

Intent-Based State Transition

The Intent-Based State Transition proposal introduces a more complex approach to contract interaction, requiring significant changes to how contracts are written and integrated. Unlike our solution, which is plug-and-play and works with existing contracts, intent-based state transitions require a complete rewrite of contracts to be compatible. This makes it less feasible for immediate adoption and integration.

Backwards Compatibility

This EIP introduces a new contract standard and does not modify existing standards. No backwards-compatibility issues are expected.

Test Cases

Test cases can be found here.

Reference Implementation

A reference implementation of the Core BalanceProxy contract can be found here and reference implementations of the Periphery contracts ApproveRouter, PermitRouter, and SafeRouter can be found here, here, and here respectively.

Please note that the reference implementation depends on the @openzeppelin/contracts v5.3.0

Security Considerations

  1. Third-party funding. Absolute balance checks only assert that an address holds at least a specified amount after execution. They do not constrain where those funds came from. An attacker (or any third party) can top up the account during the transaction to satisfy the condition. Where provenance matters, balance-difference checks SHOULD be used instead.

  2. Residual balances. Because the proxy is stateless, any assets transferred into it that are not withdrawn within the same transaction remain stuck in the contract. These residual balances are not tracked or attributed. They may later be withdrawn by anyone, intentionally or otherwise.

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