ERC: Onchain Clear Signing

Onchain Clear Signing

Kampela builds air-gapped hardware signing devices. This document describes our proposal for on-chain clear signing — making transactions human-readable and verifiable on constrained devices, without trusted third parties.


Blind signing is the state of Ethereum today: users approve transactions they cannot meaningfully read, trusting the requesting application to accurately describe what they are authorizing. In February 2025, Bybit suffered a loss of approximately $1.5 billion when Safe multisig signers approved a transaction that, contrary to its display, replaced the wallet implementation and transferred control to attacker-controlled addresses. Existing approaches — curated off-chain registries such as ERC-7730 — bring clear signing to a small set of well-known contracts, but they depend on third-party curation, cover only the most prominent protocols, and cannot scale to the long tail of contracts deployed every day.

Any solution must satisfy three requirements. Trustlessness: the display is verified by math and contract logic alone — no party needs to be trusted. Decentralization: any developer can publish their own display specification with no registry, no curator, and no approval gate. Security: the display must be cryptographically bound to execution, not advisory metadata that can be substituted.

We propose Onchain Clear Signing — a clear signing architecture defined across three interdependent ERCs that satisfies these requirements. Contract developers embed display specifications directly in their contract code. The function selector is extended with a display identifier, binding the display to on-chain execution with no external registry. Transaction requests carry the display specification alongside the calldata, making full verification possible even on air-gapped hardware wallets. The goal is that clear signing becomes the baseline expectation for every newly deployed contract.


The Architecture

The architecture is built on three layers: a semantic type system that defines what data means, an on-chain enforcement mechanism that binds the display to execution, and a transport layer that delivers the display specification to the wallet.

[Onchain Clear Signing Specification] defines the type system and display identifier. Developers write display specifications that describe how a function’s calldata should be interpreted and shown, and embed them directly in the contract. Each specification is uniquely identified by a compact 32-byte digest that any device can compute locally from the spec itself, without network access.

[Onchain Clear Signing Verification] defines how a contract binds its display specification to its own execution. The function selector is extended with a 32-byte display identifier; the contract commits to the expected identifier at deployment and verifies it on every call, ensuring the display the user approved is the display bound to that execution. If the identifier does not match, the transaction reverts. This transforms display specifications from advisory metadata into enforced preconditions of execution — “display is law”.

[wallet_sendTransaction] defines the transport. The dApp bundles the display specification with the transaction request — a push model that eliminates live network dependencies at signing time. No registry to query; no external service that can be unavailable or compromised. The method works on air-gapped hardware.

Display specifications compose. A smart account calling a multisig calling a swap contract renders each layer independently — the wallet locates the display specification for each nested call, renders it in full, and presents the composed result before the user signs. This has direct implications for account abstraction: wallets do not need built-in knowledge of every smart account entrypoint or execution framework. Any contract that embeds a display specification is fully renderable and verifiable, regardless of how it is invoked.


Transaction Flow

dApp
 │
 │  wallet_sendTransaction(tx, display_spec) (Transport)
 ▼
Wallet
 ├─ Renders calldata using the display specification (Spec)
 ├─ Verifies designated addresses against Contract Lists [optional] (Spec)
 ├─ Computes and verifies display identifier locally (Spec)
 └─ User approves the rendered display
 │
 │  clearCall(display_identifier || selector || calldata) (Verification)
 ▼
Contract
 └─ Verifies identifier matches committed value → executes (Verification)

Verification is independent at each layer; no layer trusts another. A display identifier mismatch results in wallet rejection before submission. A call submitted without a valid identifier — bypassing the wallet entirely — reverts at the contract. The mechanism carries a small, fixed gas overhead: the 32-byte display identifier prepended to calldata increases calldata cost, while the on-chain validation contributes a constant cost to execution.


Contract Lists: The Social Layer

Cryptographic binding guarantees that execution matches the committed display specification. It does not guarantee the contract’s identity — a malicious contract can commit to an accurate display of its own malicious behavior.

The primary attack vector is a transaction targeting a legitimate contract with parameters that delegate authority to a malicious address — for example, approve(maliciousSpender, maxAmount) on an ERC-20 token. To prevent this, the architecture includes Contract Lists: curated mappings of contract addresses to verified contract identities, following the same model as Token Lists.

Display specifications designate which addresses require verification using typed formats (address, token, contract). When a wallet encounters an address marked for verification, it checks trusted Contract Lists and halts rendering if the address is not recognized. Contract Lists can be published by wallet providers, DAOs, security councils, or maintained locally by users. Full details are in the Onchain Clear Signing Specification.


Request for Feedback

These three ERCs are at the idea stage. We plan to submit to the EIP repository once we have addressed community feedback. We are looking for feedback on:

Architecture. Where might the design break or create unexpected complexity?
Integration. What adoption friction do you anticipate? For dApps: build-time tooling burden. For wallets: rendering engine complexity on constrained devices. For contracts: gas overhead and proxy compatibility.
Security. Attack vectors we have not addressed?
Format coverage. Missing types or rendering requirements for your use case.
Backward compatibility. Migration paths for non-upgradeable contracts. See the Backwards Compatibility section in the Onchain Clear Signing Verification ERC for proposed options.
Early adoption. If you are building a wallet, dApp, or contract and want to be an early adopter, let us know.

Thanks for reading — looking forward to the discussion.

Links and Images

Presentation at AllWalletDevs #38, February 18, 2026



Onchain Clear Signing Specification

Table of Contents

Abstract

This standard defines a structured display specification for smart contract functions, associating ABI-decoded calldata parameters with semantic display fields covering types such as token amounts, date and time values, percentages, and addresses. Each display specification is uniquely identified by a 32-byte digest computed as an EIP-712 structured data hash. This compact identifier enables resource-constrained devices to deterministically compute and verify the integrity of a specification without network access.

Motivation

The Ethereum ABI encodes function call parameters as typed byte sequences but carries no semantic meaning: a Unix timestamp and a token amount are indistinguishable representations of uint256, and a bytes parameter encoding an inner token transfer is displayed as an opaque hex string. This absence of machine-parseable semantics produces blind signing — users authorize transactions whose effects they cannot independently verify, relying entirely on the originating application interface to describe what they are approving. This trust model is incompatible with the security properties expected of self-custodial wallets and hardware signing devices, where the integrity of displayed information cannot be delegated to connected software.

Hardware signing devices are the most constrained signing environment: limited memory and no network connectivity preclude fetching or validating external metadata at signing time. Existing off-chain metadata registries require live network access and provide no cryptographic binding between displayed metadata and the contract being called — absent such a link, the description presented to the signer carries no protocol-level guarantee. This standard operates within hardware constraints and binds display to execution by construction, providing uniform integrity guarantees across all wallet environments.

This standard defines an expressive semantic type system — covering token amounts, timestamps, durations, percentages, addresses, and nested calls — and a compact, deterministic display identifier that any device can derive locally from a complete specification without network access. The companion Onchain Clear Signing Verification standard (EIP-TBD) defines how display identifiers are committed to by contracts at deployment and verified on every call, making the displayed specification an enforced precondition of execution.

Specification

The keywords “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.

Type Definitions

A display specification is composed of four EIP-712 compatible structs: Display, Field, Labels, and Entry. Implementations MUST use the type strings below verbatim — any deviation produces a different display identifier.

Display — root type for one function’s display specification.

  • abi — Solidity function signature for selector matching, inspired by the ethers.js Human-Readable ABI format
  • title — label reference or literal string for transaction title
  • description — label reference or literal string for human-readable operation description
  • fields — ordered array of Field definitions
  • labels — array of Labels bundles
{
  "Display": [
    {"name": "abi", "type": "string"},
    {"name": "title", "type": "string"},
    {"name": "description", "type": "string"},
    {"name": "fields", "type": "Field[]"},
    {"name": "labels", "type": "Labels[]"}
  ]
}

Field — single display item definition.

  • title — label reference or literal for field name
  • description — label reference or literal string for field description; an empty string indicates no description
  • format — display format identifier
  • case — array of discriminant values for conditional rendering; an empty array indicates unconditional rendering
  • paramsEntry array supplying formatter arguments (format-specific keys, variable references, or literals)
  • fields — nested Field definitions for structural formats
{
  "Field": [
    {"name": "title", "type": "string"},
    {"name": "description", "type": "string"},
    {"name": "format", "type": "string"},
    {"name": "case", "type": "string[]"},
    {"name": "params", "type": "Entry[]"},
    {"name": "fields", "type": "Field[]"}
  ]
}

Labels — locale-specific string bundle.

  • locale — locale identifier (e.g., en, fr)
  • itemsEntry array mapping label keys to translated strings
{
  "Labels": [
    {"name": "locale", "type": "string"},
    {"name": "items", "type": "Entry[]"}
  ]
}

Entry — generic key-value pair.

  • key — string identifier
  • value — string value; interpreted as a variable reference or literal depending on context
{
  "Entry": [
    {"name": "key", "type": "string"},
    {"name": "value", "type": "string"}
  ]
}

Display Identifier

The display identifier is the 32-byte value produced by hashStruct(Display) as defined in EIP-712, computed from a complete display specification. Because hashStruct is deterministic and collision-resistant, the identifier uniquely commits to every field, format, label, and parameter in the specification. Any modification — including whitespace in the abi string or reordering of labels entries — produces a different identifier.

Function Signature Format

The Display.abi field MUST be a function signature of the form <name>(<type> <name>, ...) [<modifier>].

Each parameter MUST include a type and SHOULD include a name. Named parameters are accessible via $args.<name> (e.g., $args.amount); unnamed parameters are accessible by zero-based positional index via $args.<index> (e.g., $args.0). The optional state mutability modifier MUST be payable or nonpayable (default if omitted). Wallets MUST reject calls where msg.value > 0 and the modifier is not payable.

The following are valid function signatures:

transfer(address to, uint256 amount)
approve(address spender, uint256 amount) nonpayable
deposit() payable

The function selector for matching derives from the canonical ABI signature (types only, names and modifier stripped) via keccak256.

Variable References

String fields are resolved at render time as either a literal (static string, no $ prefix) or a variable reference ($-prefixed path resolving to a runtime value). The resolved value is then type-cast to what the consuming formatter expects; resolution MUST halt on failure.

Three reference containers are available:

  • $msg — read-only transaction context: $msg.sender, $msg.to, $msg.value, $msg.data.
  • $args — ABI-decoded function arguments for the current scope. Supports named ($args.amount), positional ($args.0), nested ($args.order.token), indexed ($args.items[0]), and slice ($args.data[1:3]) access.
  • $labels — localized string bundle for the active locale. $labels.<key> resolves to the translated string; rendering MUST halt if the key is not found.

Rendering

A wallet renders a display specification by executing the following steps in order. Any failure at any step MUST halt rendering.

Step 1 — Specification Location and Context Initialization

The display specification is located either by display identifier (trustless) or by chain, address, and selector (trusted registry). Once located, two rendering contexts are initialized:

  • $msg is populated from the transaction envelope: sender, to, value, and data. This context is read-only and constant for the entire top-level rendering scope.
  • $args is initialized by ABI-decoding $msg.data per Display.abi. Named parameters are accessible by name (e.g., $args.amount); unnamed parameters are accessible by zero-based positional index (e.g., $args.0).

Step 2 — Native Value Check

If $msg.value > 0, the wallet MUST display a warning to the user that includes the exact native value amount being transferred.

Step 3 — Field Iteration

Iterate Display.fields in declaration order. For each Field:

  1. Conditional visibility: If Field.case is non-empty, the field is rendered only if the enclosing switch value matches at least one case entry after type casting; otherwise the field is skipped. Fields with an empty case are always rendered.
  2. Reference resolution: Resolve all title, description, and params values per Variable References.
  3. Formatting: Cast and format the resolved parameter values per the rules of Field.format defined in Field Formats.
  4. Structural recursion: For structural formats (map, array, switch), process nested fields with the scope rules specified for each format in Field Formats.
  5. Nested call: The call format is a special case and does not process nested fields within the current specification. Instead, the wallet constructs a new $msg from the call field’s to, value, and data parameters — with $msg.sender set to the parent $msg.to — and locates an independent display specification for the inner call. The inner call uses its own spec’s labels bundles; it does not inherit the outer $labels context. Outer rendering is paused; rendering restarts from Step 1 for the inner specification. Once the inner rendering completes, outer rendering resumes from where it was paused. Wallets MUST enforce a maximum recursion depth. Rendering MUST halt if the limit is exceeded.

Field Formats

Raw Solidity Types

All raw formats accept a single value parameter.

Format Description
boolean Displays a bool as a wallet-localized yes/no string.
string Displays a UTF-8 string as-is.
bytes Displays bytes or bytesN as hex-encoded bytes.
int Displays any signed integer width (int8int256).
uint Displays any unsigned integer width (uint8uint256).

Rich Formats

Format Description
datetime Displays a Unix timestamp as a locale-formatted date and time. Optional units param (default: "seconds").
duration Displays a relative time span as a human-readable duration (e.g. “2 weeks”). Optional units param (default: "seconds").
percentage Displays value / basis as a percentage. basis is required (e.g. "10000" for basis points).
bitmask Displays an unsigned integer as a list of labels for each set bit. Bit labels supplied as #N params.
units Displays an unsigned integer scaled by a decimals decimal exponent (e.g. USDC with 6 decimals).

Address Formats

token and contract require verification against a Token List and Contract List respectively; rendering MUST halt if the address is not found. address is informational only.

Format Description
address 20-byte address with best-effort name resolution (contacts, ENS). Informational only.
token Token address verified against a trusted Token List. Optional tokenId for NFT identity display.
contract Contract address verified against a trusted Contract List. Use for spenders and delegatees.

Value Formats

Format Description
nativeAmount Native currency amount (e.g. ETH in wei). Optional direction: "in" or "out".
tokenAmount Token amount resolved against a trusted Token List. Required: token, amount. Optional: tokenId (NFT), direction.

Structural Formats

Structural formats carry nested fields and modify the rendering scope:

  • map, array — create an isolated $args scope ($msg remains constant)
  • call — creates a new $msg context with independent rendering
  • switch — no new scope; child fields inherit the parent $args
Format Description
map Creates an isolated $args scope and renders nested fields. Scope populated via $<name> bindings or ABI-decoded value+abi.
array Iterates parallel arrays (all $<name> params must be equal-length); renders nested fields per element with a fresh isolated $args scope.
call Renders a nested contract call. Creates a new $msg from to, value, data; outer rendering pauses until inner completes.
switch Conditionally renders nested fields based on discriminant value. No new scope; child fields inherit parent $args.

Wallets MUST reject any Field whose format value is not defined in this standard or a recognized extension. This prevents downgrade attacks via unrecognised format identifiers.

Localization

User-facing strings in title and description fields MAY use $labels.<key> references for internationalization. Specifications that omit localization MAY use literal strings directly.

Label Resolution

The wallet selects the active Labels bundle from Display.labels using the following priority order:

  1. Exact locale match (e.g., en-US matches en-US).
  2. Language-only fallback (e.g., en-US falls back to en).
  3. Default to the en bundle if present.

If no matching bundle is found, rendering MUST halt. Once a bundle is selected, the wallet searches its items array for an Entry whose key matches the reference key. Rendering MUST halt if the key is not found in the selected bundle.

Contract Lists

A Contract List is a JSON document maintained by a trusted party that maps contract addresses to their verified names and identities. It answers the question “is this address the contract I think it is?” — not “is this contract safe?”. When a wallet processes a contract field, it MUST verify the resolved address against at least one trusted Contract List. If the address is not found, the wallet MUST halt rendering.

List Sources

Since any party may publish a Contract List, wallets determine which lists to trust. Which sources a wallet accepts is an implementation decision. Common sources include:

  • Contact List: A Contract List maintained locally by the wallet on behalf of the user, populated through explicit user action. The wallet MUST treat the Contact List as trusted for verification purposes.
  • Wallet Provider: A list curated and maintained by the wallet’s own team.
  • Community: Lists curated by DAOs, security councils, or open governance processes.
  • Auditor or Security Firm: Lists published by professional security organizations based on contract review.
  • Block Explorer: Lists derived from verified contract metadata published by block explorer operators.

User-Initiated Verification

If a resolved address is not present in any trusted Contract List, the wallet MAY offer the user an explicit manual verification flow. If the user confirms the contract’s identity through this flow, the wallet MAY add the address to the user’s Contact List. Subsequent interactions with this address MUST then pass verification against the Contact List.

JSON Representation

Display specifications MAY be represented in JSON format for transmission via wallet_sendTransaction (EIP-TBD). The JSON representation omits EIP-712 type definitions (which are static across all displays) and uses the following normalization rules:

Normalization Rules:

  • Empty arrays: Absent case and fields arrays MUST be represented as []
  • Empty strings: Absent description fields MUST be represented as ""

The JSON structure maps directly to the EIP-712 structs:

{
  "abi": "transfer(address to, uint256 amount)",
  "title": "$labels.title",
  "description": "$labels.description",
  "fields": [
    {
      "title": "$labels.sender",
      "description": "",
      "format": "address",
      "case": [],
      "params": [{"key": "value", "value": "$msg.sender"}],
      "fields": []
    },
    {
      "title": "$labels.amount",
      "description": "",
      "format": "tokenAmount",
      "case": [],
      "params": [
        {"key": "token", "value": "$msg.to"},
        {"key": "amount", "value": "$args.amount"}
      ],
      "fields": []
    },
    {
      "title": "$labels.recipient",
      "description": "",
      "format": "address",
      "case": [],
      "params": [{"key": "value", "value": "$args.to"}],
      "fields": []
    }
  ],
  "labels": [
    {
      "locale": "en",
      "items": [
        {"key": "title", "value": "Transfer"},
        {"key": "description", "value": "Transfer ERC-20 tokens"},
        {"key": "sender", "value": "From"},
        {"key": "amount", "value": "Amount"},
        {"key": "recipient", "value": "To"}
      ]
    }
  ]
}

Implementations MUST compute the display identifier by converting the JSON to EIP-712 structs and applying hashStruct(Display) as defined in EIP-712.

Rationale

Design Goals

The specification addresses calldata interpretation through local decoding without network dependencies, verifiable display identifiers via EIP-712, and censorship-resistant metadata access. The core principle is “display is law”: display specifications are security-critical artifacts that cryptographically commit developers to the semantics shown to users.

This standard is bounded by what is present in the calldata at signing time. Contracts whose execution is driven by on-chain state rather than calldata parameters — such as a bare execute() — can hold a display specification but cannot surface dynamic values. Additionally, some rendering requires on-chain metadata absent from calldata: ERC-20 symbol and decimal precision must be queried from the token contract by the wallet. Both cases are outside the scope of this specification.

EIP-712 Display Identifier

The EIP-712 hashStruct provides a compact, 32-byte identifier compatible with established ecosystem infrastructure and resource-constrained devices. This mechanism enables deterministic verification through both static precomputation and dynamic, on-chain generation.

Adopting EIP-712 for identifier computation means that improvements to the EIP-712 algorithm and its Solidity tooling accrue to this standard without requiring specification changes. Currently, display identifiers must be expressed as nested keccak256(abi.encode(...)) chains — correct but verbose. Proposed Solidity compiler enhancements, including native type(S).typehash and type(S).hashStruct(s) support, will allow these to be replaced with direct type-level expressions evaluated at compile time. Solidity does not yet support compile-time constant evaluation (constexpr); as this capability is added to the compiler, display identifier constants will be expressible as simple compiler-verified declarations rather than manually assembled hash computations, further reducing boilerplate and eliminating a class of encoding errors.

Semantic vs Visual Separation

The specification defines semantic meaning and data hierarchy, not visual presentation. This separation ensures specifications remain valid across different wallet implementations and device form factors while allowing wallets to optimize rendering for their specific constraints.

Interpolated string templates (e.g., "Transfer {amount} to {destination}") are deliberately absent. They hard-code presentation, obscure type information ({amount} carries no indication that decimal scaling and symbol resolution are required), and are incompatible with natural language rendering, where grammatical agreement, word endings, and noun cases depend on the numeric value and surrounding context. Typed, named field definitions delegate all string composition to the wallet, keeping the specification language-agnostic and presentation-agnostic.

Structural Formats

Structural formats (map, array, switch, call) enable display specifications to cover transaction patterns that cannot be expressed as flat field listings:

map decodes bytes-encoded subcommands. Universal routers encode complex swap paths as packed bytes; map decodes them using an ABI signature, making nested fields (token addresses, amounts, slippage) accessible by name instead of appearing as opaque hex.

array handles batch operations. Multicall contracts and batch transfers process variable-length lists; array renders them with a single field template that iterates over parallel arrays (recipients and amounts) without per-element duplication.

switch supports command dispatch. Universal routers multiplex operations via command bytes or enums; switch renders different fields based on the command value using case matching, covering opcode branching and mode selection without separate display specifications per command.

call renders nested execution. Smart contract wallets, multisigs, and DAOs wrap inner transactions as ABI parameters; call recursively renders the inner call using its own display specification, enabling complete call tree visualization for account abstraction (ERC-4337), multisig execution, and DAO proposals.

Scope Isolation

map and array create isolated $args scopes; child fields access only explicitly passed parameters. switch does not create new scopes. call creates entirely new rendering contexts. Scope isolation prevents variable shadowing attacks, makes data flow auditable, and improves specification readability.

Forward Compatibility

Field parameters use generic Entry key-value pairs and string-based format identifiers, allowing new semantic types to be added in future proposals without modifying core EIP-712 type definitions. Rejection of unknown format identifiers, enforced in the Specification, prevents downgrade attacks.

Localization

Labels are included in the display identifier hash to prevent tampering and ensure translations carry the same cryptographic guarantees as field definitions. The bundle-based design separates translatable strings from format specifications and provides locale fallback mechanisms. Missing label keys halt rendering to prevent corrupted displays.

Error Handling

The specification adopts halt-on-error behavior: any resolution failure, type mismatch, verification failure, or missing key halts rendering immediately. This prevents misleading displays where partial information could lead users to approve malicious transactions. A specification that is incomplete or incorrect will produce halt-on-error failures at render time.

Backwards Compatibility

This ERC introduces a new standard and does not modify any existing Ethereum protocol, ABI encoding, or ERC. It has no backward compatibility requirements with respect to previously deployed contracts or existing wallet implementations. Wallets that do not implement this standard continue to operate under existing behavior; this standard defines an opt-in display layer.

Dependency on EIP-712 is additive: this standard reuses hashStruct solely for identifier computation and does not alter any EIP-712 behavior or interfere with existing EIP-712 signed data flows.

Security Considerations

Binding Display Specifications to Contracts

This specification does not define how display identifiers bind to contracts. The companion Onchain Clear Signing Verification standard (EIP-TBD) addresses on-chain verification mechanisms.

Without verification, users face specification substitution attacks, phishing via stolen specifications, and downgrade attacks. The on-chain verification mechanism is defined in the companion Onchain Clear Signing Verification standard (EIP-TBD); wallet implementations SHOULD NOT render display specifications without a verified binding to the target contract.

Native Value Transfer Omission

Payable functions accepting msg.value > 0 may omit native transfer display fields, hiding value transfers. Wallet implementations MUST display a prominent warning that includes the exact native value amount being transferred, so the user can assess the transfer independently of the display specification. Wallets MAY additionally require that $msg.to is present in a trusted Contract List when $msg.value > 0, ensuring native value is only transferred to a contract with a verified identity.

Developer Responsibilities

This standard guarantees that execution matches the committed display specification; it cannot verify that the specification truthfully describes the contract’s behavior. Developers MUST ensure specifications accurately represent behavior and SHOULD apply security review processes equivalent to smart contract code.

Malicious Displays

A malicious developer may author a specification that misrepresents an operation through misleading labels or omitted parameters. Directly calling a malicious contract has limited damage scope: contracts are isolated, and a malicious contract cannot access a user’s assets in other contracts without permissions the user has previously granted. The primary attack surface is not the malicious contract itself, but the act of granting it rights.

The principal phishing vector is a transaction targeting a legitimate contract — for example, an ERC-20 token — with parameters that delegate authority to a malicious address (e.g., approve(maliciousSpender, maxAmount)). The contract field format addresses this directly: the resolved spender address is verified against trusted Contract Lists, and rendering MUST halt if the address is absent. A malicious address cannot appear in a reputable Contract List without the list maintainer’s knowledge, making this class of attack detectable before the user signs.

Wallets MAY additionally restrict clear signing display to transactions where the $msg.to address is itself verified by a trusted Contract List. This reduces the social engineering surface further, at the cost of limiting which contracts users can interact with.

Denial of Service

Malicious specifications can exhaust wallet resources via excessive recursion (call), large arrays (array), deep nesting, or oversized labels. Wallet implementations MUST enforce platform-appropriate limits on recursion depth, array sizes, and computational complexity. Rendering MUST halt when limits are exceeded.

Onchain Clear Signing Verification

Table of Contents

Abstract

This standard defines clearCall() — a contract entry point that enforces a cryptographic binding between on-chain execution and the display specification presented to the signer. The standard extends the conventional Ethereum call format from selector || calldata to clearCall_selector || display_identifier || selector || calldata, embedding the display identifier as an explicit, verifiable component of every call. A contract embeds the expected display identifier for each supported function in its bytecode at deployment and verifies the provided value before delegating execution, reverting on mismatch. This transforms display specifications from advisory metadata into enforced preconditions of execution.

Motivation

The Ethereum call format encodes function calls as a 4-byte selector followed by typed parameters. The selector identifies what to execute but says nothing about what the arguments represent, and there is no on-chain link between the calldata and the description shown to the signer.

clearCall() closes this gap by embedding a display identifier in every call — derived from the specification the contract committed to at deployment and enforced by the contract’s own logic. A wallet that renders any specification other than the committed one produces a different identifier, and the transaction reverts. No external authority is required; the binding is a property of the deployed bytecode.

Specification

The keywords “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.

Display Identifier

The display identifier is an opaque 32-byte value that uniquely identifies a display specification, computed as defined in the companion Onchain Clear Signing Specification standard (EIP-TBD).

Wallets MUST compute the display identifier locally from the exact display specification presented to the user. A wallet MUST reject a transaction before submission if the locally computed identifier does not match the display identifier present in the clearCall payload.

Contracts MUST resolve the expected display identifier for each supported function selector. Upon receiving a clearCall, the contract MUST extract the display identifier from the payload and verify it against the expected identifier. If the identifiers do not match, the contract MUST revert.

Packed Call Format

A clearCall payload MUST use the following packed byte sequence layout:

┌─────────────┬───────────────────────┬─────────────────────────────┐
│   Bytes 0-3 │       Bytes 4-35      │         Bytes 36+           │
├─────────────┼───────────────────────┼─────────────────────────────┤
│  clearCall  │   Display Identifier  │      Inner Calldata         │
│  Selector   │      (32 bytes)       │  ┌──────────┬──────────────┐│
│ 0x0ab793e2  │                       │  │ Selector │  ABI Params  ││
│             │                       │  │ (4 bytes)│              ││
└─────────────┴───────────────────────┴──┴──────────┴──────────────┘

Display Identifier Storage

Contracts MUST implement a mechanism to resolve and verify the expected display identifier for a given function selector. Developers MAY choose from the following strategies based on their requirements for gas efficiency and upgradeability.

Compile-time Constants

The most gas-efficient approach, recommended for contracts with a single, static display specification per function. This approach also works well for upgradeable proxy contracts: during a proxy upgrade, the display identifier is stored in the implementation contract’s bytecode and changes automatically when the implementation is replaced.

bytes32 constant TRANSFER_DISPLAY_ID = 0x1a2b3c...;

Deploy-time Immutables

Suitable for factory-deployed contracts where the display specification is fixed at deployment but may vary between instances (e.g., based on token parameters).

bytes32 immutable _transferDisplayId;

constructor(string memory name, string memory symbol) {
    // Display identifier computed at deploy time from token-specific parameters
    _transferDisplayId = _computeTransferDisplayId(name, symbol);
}

Runtime Storage

Used when display specifications must often be updated.

One-to-One Mapping: Maps a function selector to its current authoritative display identifier.

mapping(bytes4 => bytes32) private _displayIdentifiers;

function setDisplayIdentifier(bytes4 selector, bytes32 displayId) external onlyOwner {
    _displayIdentifiers[selector] = displayId;
}

One-to-Many Mapping: Maps multiple valid display identifiers to a function selector. This is useful for supporting multiple versions of a display specification simultaneously.

mapping(bytes32 => bytes4) private _authorizedSelectors;

function authorizeDisplay(bytes32 displayId, bytes4 selector) external onlyOwner {
    _authorizedSelectors[displayId] = selector;
}

function _verifyDisplay(bytes4 selector, bytes32 displayId) internal view returns (bool) {
    return _authorizedSelectors[displayId] == selector;
}

Nested clearCall Composition

When the inner calldata of a clearCall is itself a clearCall, the payloads are nested. Wallets MUST process nested payloads recursively: render and verify the outermost call first, pause when a nested clearCall is encountered, render the inner specification, and resume after completion. This continues until a non-clearCall inner selector is reached. Each layer’s display identifier MUST be independently verified against the specification rendered at that layer.

clearCall() Entry Point: Reference Implementation

Contracts MUST implement a function with the selector 0x0ab793e2 (corresponding to clearCall()) declared as external payable. The implementation below uses delegatecall to retrofit clear signing onto an existing contract with minimal changes: clearCall() verifies the identifier and forwards the inner calldata to the contract itself, requiring no modifications to existing function implementations. Developers MAY instead decode the inner calldata and call the target function directly.

function clearCall() external payable returns (bytes memory) {
    require(msg.data.length >= 40, "clearCall: payload too short");

    bytes32 displayId = bytes32(msg.data[4:36]);
    bytes4  selector  = bytes4(msg.data[36:40]);

    bytes32 expected = _expectedDisplayId(selector);
    require(expected != bytes32(0), "clearCall: unknown selector");
    require(displayId == expected,  "clearCall: display identifier mismatch");

    (bool success, bytes memory result) = address(this).delegatecall(msg.data[36:]);
    if (!success) {
        assembly { revert(add(32, result), mload(result)) }
    }
    return result;
}

Rationale

Packed Format

The clearCall() function uses raw packed msg.data parsing instead of explicit ABI parameters like clearCall(bytes32 displayId, bytes calldata innerCall). The packed format defines a fixed layout: bytes 0–3 contain the clearCall() selector (0x0ab793e2), bytes 4–35 contain the display identifier, and bytes 36 onward contain the inner function calldata. This layout allows the implementation to extract both the display identifier and inner calldata using fixed-offset reads from msg.data, avoiding ABI decoding overhead.

The packed byte format adds approximately 3,764–3,979 gas overhead per call measured against direct calls, using the delegatecall reference implementation. The fixed-offset structure simplifies tooling: the inner calldata is always recoverable by stripping the first 36 bytes, with no dynamic offset computation required. Block explorers and indexers must implement this unwrapping (see Tooling Compatibility).

Backwards Compatibility

Opt-in Adoption

clearCall() is an additive entry point that does not conflict with existing function selectors or the Solidity fallback / receive dispatch mechanism. Contracts that implement clearCall() retain all existing ABI-defined functions, which remain callable directly via their original selectors — direct calls, internal calls, and contract-to-contract calls all bypass clearCall() entirely.

Non-Upgradeable Contracts

Contracts that cannot be modified cannot implement clearCall() and therefore cannot participate in the on-chain binding defined by this standard. For such contracts, display identifier binding must be established externally, keyed by chain ID, contract address, and function selector. Three approaches are recognised:

Embedded display specifications. Wallets SHOULD embed display specifications for well-known standard interfaces — such as ERC-20, WETH, and common staking contracts — directly in firmware. Trust is derived from the immutable behaviour of the standard interface rather than on-chain commitment.

Off-chain display repositories. A publicly accessible, community-maintained repository MAY map (chainId, contractAddress, selector) to a verified display specification. Each entry MUST be reviewed and approved by a human before publication. Wallets consuming such repositories MUST communicate to users that display specifications sourced this way carry social trust assumptions rather than cryptographic guarantees.

On-chain DisplayRegistry. A smart contract controlled by a DAO or multisig MAY maintain an on-chain mapping of (chainId, contractAddress, selector) to display identifiers, providing decentralised governance over the binding without requiring contract upgrades. Wallets MUST clearly differentiate registry-verified transactions from native clearCall() verification in the user interface.

Tooling Compatibility

Tools that do not implement clearCall unwrapping — including block explorers, wallets, and off-chain indexers — will treat such transactions as opaque fallback invocations, obscuring the inner function call and its parameters. Such tools SHOULD implement unpacking of the packed format by stripping the first 36 bytes to recover the inner calldata, preserving auditability and correct display of transaction intent.

Security Considerations

Invalid clearCall Implementation

An incorrectly implemented clearCall() entry point — one that skips or weakens the display identifier verification — undermines the security guarantee of the entire standard. A contract that accepts any display identifier or performs a partial check provides no binding between display and execution. Implementations MUST perform a strict equality check between the extracted identifier and the stored expected value. Contracts SHOULD be audited with specific attention to the verification path and all revert conditions.

wallet_sendTransaction with Verifiable Metadata

Table of Contents

Abstract

This standard defines wallet_sendTransaction, a JSON-RPC method that extends the conventional eth_sendTransaction with a metadata parameter. This parameter carries an array of verifiable context metadata for the transaction’s smart contract calls. While the method is agnostic to the specific type of metadata it carries, any supported metadata is required to be cryptographically verifiable against the original transaction.

Motivation

When a decentralized application (dApp) requests a transaction, the wallet receives calldata to present to the user for approval. The raw calldata is semantically opaque — the wallet has no built-in knowledge of what the bytes represent. Unverifiable off-chain metadata introduces significant phishing vectors, as malicious actors can supply deceptive context.

Hardware signing devices cannot query external metadata at all; they operate in strictly air-gapped environments with no network access, requiring all information needed to verify a transaction to be present in the signing request itself. wallet_sendTransaction ensures that the transaction payload includes the cryptographically verifiable context needed to safely interpret it.

Specification

The keywords “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.

Method

wallet_sendTransaction

Parameters

[
  transaction,
  metadata
]
Position Name Type Required Description
0 transaction TransactionObject yes Standard transaction object, identical to eth_sendTransaction
1 metadata MetadataObject yes Context metadata mapping for the transaction

The transaction object MUST follow the exact same structure as eth_sendTransaction. The wallet MUST reject the request if the metadata parameter is absent.

Metadata Object

The metadata object is a key-value map where each key serves as a unique identifier for a specific metadata standard or well-known format, and the value contains the structured context data specific to that standard.

Key Type Description
[standard_identifier] object|array Identifier for the type of metadata being provided. Keys SHOULD be specific to an Ethereum proposal (e.g., "eip-xyz") or a well-known identifier (e.g., "display", "abi").

Example

{
  "display": [...],
  "abi": [...]
}

Wallet Processing

Upon receiving a wallet_sendTransaction request, a wallet processes the transaction depending on its role in the signing lifecycle. In all scenarios, the user maintains the ability to confirm or reject the transaction, identical to the behavior of eth_sendTransaction.

Terminal Wallet (Consuming and Signing)

If the wallet is the final entity responsible for signing the transaction (e.g., an externally owned account on a software wallet or a hardware signer), it processes the metadata for its intended purpose:

  1. Verification and Processing: The wallet processes the metadata according to the rules of the supported standard. If the wallet does not recognize a key, or if no matching metadata is found for a specific call, it MUST treat the metadata as unavailable for that context and fall back to its default behavior.
  2. Approval: Present the complete, rendered transaction context to the user.
  3. Execution: If the user approves, sign the transaction and broadcast it to the network.
  4. Return: Return the resulting transaction hash to the calling application.

A terminal wallet MUST NOT submit the transaction without explicit user approval and MUST NOT modify the transaction data provided in the request.

Intermediary Wallet (Forwarding and Wrapping)

If the wallet acts as an intermediary (e.g., a service managing a smart account or multisig that intercepts a request, wraps the inner execution in its own outer call, and forwards it to a hardware signer), it MUST preserve the context:

  1. Preserve Metadata: The intermediary MUST retain the original metadata object provided in the incoming request.
  2. Append Context (Optional but Recommended): When wrapping the original transaction inside a new call (e.g., an execute call on a smart account), the intermediary SHOULD append its own verifiable metadata to the metadata object corresponding to the new wrapper calls it introduces.
  3. Forward Request: The intermediary forwards the newly wrapped transaction and the combined metadata object to the downstream wallet via wallet_sendTransaction.

This aggregation ensures that the final hardware wallet controlling the account receives the complete chain of metadata necessary to verify and display the entire sequence of execution (e.g., Smart Account Wallet → Multisig → DEX Swap).

Discovery of Supported Metadata

Wallets SHOULD expose the metadata standards they support via the wallet_getCapabilities RPC method defined in EIP-5792. This allows decentralized applications and intermediary wallets to determine whether it makes sense to include specific metadata payloads in the wallet_sendTransaction request.

For example, a wallet might indicate its supported metadata keys:

{
  "0x2105": {
    "supportedMetadata": [
      "display",
      "abi"
    ]
  }
}

If a dApp or an intermediary wallet detects that a downstream wallet does not support a specific metadata standard, it MAY choose to omit that payload to optimize the request size. If it includes it anyway, the receiving wallet MUST safely ignore the unsupported key.

Return Value

On success, the method returns the transaction hash as a 32-byte hex string, identical to the behavior of eth_sendTransaction.

"result": "0x4e3a3754410177e8842851f9b47a2db3b2b07bc8..."

Error Codes

Standard JSON-RPC error codes apply, including the following specific scenarios:

Code Message Description
-32602 Invalid params metadata parameter is missing, empty, or incorrectly formatted
-32603 Internal error Wallet failed to process metadata due to internal error

Additional error codes MAY be returned depending on the specific validation rules of the processed metadata standard.

Rationale

Push Model vs. Pull Model

Bundling metadata with the transaction request (the “Push Model”) rather than fetching them from an external registry (the “Pull Model”) eliminates runtime dependencies. A wallet that relies on pulling metadata at signing time degrades to blind signing whenever the registry is unavailable, rate-limited, or returns an incorrect entry. The push model makes availability a property of the dApp, not the registry: if the dApp can propose a transaction, it must also provide the specifications needed to display it.

Extensibility

By making the metadata parameter a generic key-value map, the standard decouples the transport mechanism from specific data formats. While the immediate motivation is to deliver Onchain Clear Signing Specifications (via the "display" key), this design allows future standards to use the same wallet_sendTransaction method to push other forms of context. For example, a dApp could provide an ABI for decoding (via "abi"), off-chain intent declarations, or alternative verification proofs using their respective EIP identifiers as keys, without requiring new RPC methods.

Backwards Compatibility

wallet_sendTransaction uses the wallet_ namespace, which is reserved for wallet-specific extensions and is not part of the standard Ethereum JSON-RPC API defined by eth_. Existing dApps using eth_sendTransaction continue to function without modification. Adoption of wallet_sendTransaction is opt-in; dApps that do not provide metadata continue to use eth_sendTransaction, and wallets continue to handle those calls with their existing display or blind-signing logic.

Wallets that do not implement wallet_sendTransaction SHOULD return a standard JSON-RPC Method Not Found error (-32601), allowing dApps to fall back to eth_sendTransaction.

ERC-4337 UserOperation flows are fully compatible; the wallet renders the metadata for the user, then signs and submits to the bundler as normal.

Security Considerations

The security guarantees of wallet_sendTransaction depend entirely on the specific metadata standard being processed. Any metadata standard transmitted via this method MUST define a cryptographic binding to the transaction. If verification fails, the wallet MUST reject the metadata and fall back to its default behavior.

Wallets MUST safely handle unknown or unsupported metadata keys by falling back to blind signing (or rejecting the transaction, depending on policy) and MUST NOT attempt to parse or render arbitrary structures that could lead to injection attacks or misleading displays.

Metadata Poisoning Attacks

A malicious dApp may bundle valid transaction data with deceptive metadata designed to mislead the user.

Wallets MUST:

  • Verify metadata authenticity before rendering
  • Reject transactions where metadata verification fails
  • Never trust metadata from unverified sources
  • Clearly distinguish verified metadata from unverified hints

Metadata Size and DoS

Malicious dApps may send extremely large metadata payloads to exhaust wallet memory or processing resources, particularly on hardware signing devices with limited RAM.

Wallets MUST:

  • Enforce reasonable metadata size limits (recommended: 1 MB maximum total)
  • Implement per-key size limits within the metadata object
  • Reject oversized metadata before attempting to parse or render it
  • Implement timeout protections for metadata verification and rendering