--- eip: 8009 title: Proxy Clear Signing description: Permissionless singleton contract that proxies smart-contract calls and enforces transaction constraints specified by the end user author: Mark Virchenko (@borseno), Ilya Kubariev (@gymnasy55), Daniel Nagy (@nagydani) discussions-to: https://ethereum-magicians.org/t/erc-8009-proxy-clear-signing/25199 status: Draft type: Standards Track category: ERC created: 2025-08-19 requires: 20 --- ## 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](./eip-20.md) 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](./eip-20.md) 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](./eip-20.md) 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: ```solidity // 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](./eip-20.md) 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](./eip-20.md)) 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: ```solidity 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.md) **[EIP-6120](./eip-6120.md)** imposes a key limitation by requiring that the target contract cannot be a token (such as an [ERC-20](./eip-20.md)), 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](../assets/eip-8009/test/BalanceProxy.test.ts). ## Reference Implementation A reference implementation of the **Core** `BalanceProxy` contract can be found [here](../assets/eip-8009/BalanceProxy.sol) and reference implementations of the **Periphery** contracts `ApproveRouter`, `PermitRouter`, and `SafeRouter` can be found [here](../assets/eip-8009/ApproveRouter.sol), [here](../assets/eip-8009/PermitRouter.sol), and [here](../assets/eip-8009/SafeRouter.sol) 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 Copyright and related rights waived via [CC0](../LICENSE.md).