Skip to main content

Add clear signing to your protocol with ERC-7730

ERC-7730
security
signing
smart contracts
wallets
Intermediate
Hester Bruikman
May 11, 2026
7 minute read

Most major Ethereum exploits had the same final step: a user approving a transaction they could not meaningfully understand. Hardware wallets show raw hex calldata, and worse force you to have blind signing on. Software wallets show decoded fields, but only when they recognise the contract. When they don't, whether because the protocol is new, the app is compromised, or the device is offline, users sign blind.

ERC-7730 (opens in a new tab) defines a standard JSON format for describing what your contract's function calls mean.

A wallet that supports ERC-7730 reads your descriptor and shows:

Swap
Send: 1,000 USDC
Receive minimum: 0.42 WETH
Protocol: Uniswap V3

Or a single constructed sentence readable by humans and agents alike:

Swap 1,000 USDC for at least 0.42 WETH

Instead of a function selector and a list of raw integer values.

This is clear signing (opens in a new tab) — "What You See Is What You Sign." This tutorial walks you through writing a descriptor for your own contract, validating it with the official CLI tool, and submitting it to the open registry.

Prerequisites

  • Familiarity with Solidity and smart contract ABIs
  • A deployed smart contract with a verified ABI (Sourcify (opens in a new tab) verification is required before a descriptor is accepted to the registry)
  • Python 3.12+ for the validation CLI
  • Basic JSON knowledge

What is an ERC-7730 descriptor?

A descriptor is a single JSON file with three sections:

SectionPurpose
contextBinds the descriptor to specific contract deployments by chain ID and address
metadataNames the project and defines reusable constants
displayMaps each function signature to human-readable labels and field formats

Because the descriptor is separate from the contract itself, you can add clear signing support to any existing protocol without redeployment. Wallets retrieve descriptors from the registry and use them at signing time.

Step 1: Create the file skeleton

Create a file named calldata-<contractname>-<descriptorversion>.json. The calldata- prefix tells the registry this descriptor covers contract function calls, as opposed to eip712- for typed-data messages. The descriptorversion tells the registry the version of the descriptor file, 0 by default if no version is provided.

1{
2 "$schema": "https://eips.ethereum.org/assets/eip-7730/erc7730-v2.schema.json",
3 "context": {},
4 "metadata": {},
5 "display": {
6 "formats": {}
7 }
8}

Step 2: Write the context section

The context section binds the descriptor to one or more contract deployments. Wallets use this to match an incoming transaction to the correct descriptor.

1"context": {
2 "$id": "uniswap-v3-router-mainnet",
3 "contract": {
4 "deployments": [
5 { "chainId": 1, "address": "0xYourContractAddressOnMainnet" },
6 { "chainId": 137, "address": "0xYourContractAddressOnPolygon" }
7 ]
8 }
9}

Context fields

  • context.$id — A unique identifier for this descriptor document or deployment configuration.
  • contract.deployments — The set of deployments this descriptor applies to.
  • deployments[].chainId — The EVM chain ID for a deployment. Include every chain where your contract is deployed.
  • deployments[].address — The contract address wallets should associate with this descriptor. Use the implementation address that holds the execution logic.

Step 3: Write the metadata section

The metadata section provides human-readable information about the project and contract described by this file. Wallets may use this information to display protocol names, links, and other contextual details during signing.

1"metadata": {
2 "owner": "Example Swap Protocol",
3 "info": { "url": "https://example.xyz" },
4 "contractName": "SwapRouter"
5}

Metadata fields

  • owner — The project, protocol, organization, or maintainer responsible for this descriptor.
  • info.url — A canonical project or documentation URL wallets may display to users for additional context.
  • contractName — The contract or implementation name described by this file, typically matching the verified source code or ABI.

If your ERC-7730 file describes an ERC-20 contract, you should add a token object too.

Step 4: Write the display formats section

The display.formats object maps function signatures to human-readable signing instructions. This is how wallets show your function to users before they approve a transaction!

Each key is a human-readable ABI fragment — the function signature including both parameter names and parameter types exactly as they appear in your ABI.

Example: Describing a token swap

1"display": {
2 "formats": {
3 "swapExactTokensForTokens(uint256 amountIn,uint256 amountOutMin,address[] path,address to,uint256 deadline)": {
4 "intent": "Swap",
5 "interpolatedIntent": "Swap {amountIn} for at least {amountOutMin}",
6 "fields": [
7 {
8 "path": "#.amountIn",
9 "label": "Send",
10 "format": "tokenAmount",
11 "params": {
12 "tokenPath": "#.path[0]"
13 }
14 },
15 {
16 "path": "#.amountOutMin",
17 "label": "Receive minimum",
18 "format": "tokenAmount",
19 "params": {
20 "tokenPath": "#.path[1]"
21 }
22 },
23 {
24 "path": "#.to",
25 "label": "Recipient",
26 "format": "addressName",
27 "params": {
28 "types": ["eoa", "contract"],
29 "sources": ["local", "ens"]
30 }
31 },
32 {
33 "path": "#.deadline",
34 "label": "Expires",
35 "format": "date",
36 "params": {
37 "encoding": "timestamp"
38 }
39 }
40 ]
41 }
42 }
43}
44

Display fields

  • intent(Required) A short, user-friendly description of the action, such as "Swap".
  • interpolatedIntent(Recommended) A richer sentence template that embeds formatted field values, such as "Swap {amountIn} for at least {amountOutMin}". Include this alongside intent to provide an even more user friendly descriptor that wallets can choose to show provided any display constraints.
  • fields(Required) The ordered list of transaction fields wallets should display to users.
    • path(Required) A reference to the transaction data. #.fieldName points to a decoded calldata parameter by the name in the ABI. @.value refers to the ETH value sent with the transaction.

    • label(Required) The human-readable label shown beside the value.

    • format(Recommended) Controls how the value should be rendered. Common formats include:

      • tokenAmount
      • addressName
      • date

      Use raw when no additional formatting is needed. Some formats accept additional params configuration. For example:

      • tokenAmount can use tokenPath to identify which token address provides decimals and ticker metadata.
      • date can use encoding to describe how the timestamp is encoded.

      If the selected format does not require extra information, omit params.

The complete descriptor

1{
2 "$schema": "https://eips.ethereum.org/assets/eip-7730/erc7730-v2.schema.json",
3 "context": {
4 "$id": "uniswap-v3-router-mainnet",
5 "contract": {
6 "deployments": [
7 {
8 "chainId": 1,
9 "address": "0xYourContractAddressOnMainnet"
10 },
11 {
12 "chainId": 137,
13 "address": "0xYourContractAddressOnPolygon"
14 }
15 ]
16 }
17 },
18 "metadata": {
19 "owner": "Example Swap Protocol",
20 "info": {
21 "url": "https://example.xyz"
22 },
23 "contractName": "SwapRouter"
24 },
25 "display": {
26 "formats": {
27 "swapExactTokensForTokens(uint256 amountIn,uint256 amountOutMin,address[] path,address to,uint256 deadline)": {
28 "intent": "Swap",
29 "interpolatedIntent": "Swap {amountIn} for at least {amountOutMin}",
30 "fields": [
31 {
32 "path": "#.amountIn",
33 "label": "Send",
34 "format": "tokenAmount",
35 "params": {
36 "tokenPath": "#.path[0]"
37 }
38 },
39 {
40 "path": "#.amountOutMin",
41 "label": "Receive minimum",
42 "format": "tokenAmount",
43 "params": {
44 "tokenPath": "#.path[1]"
45 }
46 },
47 {
48 "path": "#.to",
49 "label": "Recipient",
50 "format": "addressName",
51 "params": {
52 "types": ["eoa", "contract"],
53 "sources": ["local", "ens"]
54 }
55 },
56 {
57 "path": "#.deadline",
58 "label": "Expires",
59 "format": "date",
60 "params": {
61 "encoding": "timestamp"
62 }
63 }
64 ]
65 }
66 }
67 }
68}

Step 5: Submit to the registry

The ERC-7730 registry (opens in a new tab) is an open repository hosted by the Ethereum Foundation as a neutral steward. Anyone is free to clone and self-host it — wallets independently decide which registry instances they trust.

  1. Fork the repository on GitHub
  2. Create a folder at registry/<your-project-name>/
  3. Place your file inside it: registry/myproject/calldata-mycontract-0_0.json
  4. Update the $schema field to the relative path used within the repo: "../../specs/erc7730-v2.schema.json"
  5. Open a pull request

When you open the PR, CI automatically runs schema validation, checks that function signatures produce valid selectors, confirms the contract address is verified on Sourcify, and flags ABI inconsistencies. The check results appear inline on the PR. Registry maintainers screen submissions for malformed or potentially malicious descriptors. Inclusion in the registry does not imply audit or endorsement.

What happens after merging?

All descriptors in the registry are open to auditors. After your PR is merged, any auditor can review your descriptor and publish a cryptographic attestation (under ERC-8176 (opens in a new tab)) confirming its accuracy.

These attestation signals let wallets apply their own trust policies — a descriptor with multiple independent attestations carries more weight than one without. You can reach the auditor community through clearsigning.org (opens in a new tab).

Wallets choose which registry they will support. Once your descriptor is in the registry, wallets that support ERC-7730 will start fetching it if it is in their registry and will display human-readable data when users interact with your contract.

Further reading

Was this tutorial helpful?