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
params — Entry 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)
items — Entry 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:
- 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.
- Reference resolution: Resolve all
title, description, and params values per Variable References.
- Formatting: Cast and format the resolved parameter values per the rules of
Field.format defined in Field Formats.
- Structural recursion: For structural formats (
map, array, switch), process nested fields with the scope rules specified for each format in Field Formats.
- 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 (int8…int256). |
uint |
Displays any unsigned integer width (uint8…uint256). |
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:
- Exact locale match (e.g.,
en-US matches en-US).
- Language-only fallback (e.g.,
en-US falls back to en).
- 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.