Proxy Clear Signing
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 MUST revert.
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 keywords "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in RFC 2119.
Contract Overview
A singleton, stateless permissionless proxy contract deployed once per network.
The contract accepts transaction execution requests containing:
- Target call data — Encoded calldata for the intended interaction.
- 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.
- Withdrawals — Optional ETH/ERC-20 transfers executed after the target call (from the proxy to specified recipients).
- 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: UNLICENSED
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:
- Optionally approves or transfers ERC-20 amounts for a designated spender (approvals),
- Proxies a call to a target contract,
- Optionally performs withdrawals from the proxy (withdrawals),
- 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:
- Approvals
- Target call
- Withdrawals
- 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:
- 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.
- 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.
- Singleton and permissionless – One deployment of Core per network serves all users. No “wallet contracts” or account-specific deployments are required. A single, well-known address simplifies hardware wallet support and minimizes operational complexity.
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, Core does not responsible for getting assets from user. 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.
- 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.
- 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 (e.g. does not allow checking balances for decreases). 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
-
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 pop up the account in front of the transaction to satisfy the condition. Where provenance matters, balance-difference checks SHOULD be used instead.
-
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
Copyright and related rights waived via CC0.