Add clear signing to your protocol with ERC-7730
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:
| Section | Purpose |
|---|---|
context | Binds the descriptor to specific contract deployments by chain ID and address |
metadata | Names the project and defines reusable constants |
display | Maps 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 alongsideintentto 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.#.fieldNamepoints to a decoded calldata parameter by the name in the ABI.@.valuerefers 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:tokenAmountaddressNamedate
Use
rawwhen no additional formatting is needed. Some formats accept additionalparamsconfiguration. For example:tokenAmountcan usetokenPathto identify which token address provides decimals and ticker metadata.datecan useencodingto 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.
- Fork the repository on GitHub
- Create a folder at
registry/<your-project-name>/ - Place your file inside it:
registry/myproject/calldata-mycontract-0_0.json - Update the
$schemafield to the relative path used within the repo:"../../specs/erc7730-v2.schema.json" - 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
- ERC-7730 specification (opens in a new tab)
- ERC-7730 registry (opens in a new tab)
- clearsigning.org (opens in a new tab) — tooling, ecosystem status, and governance
- Sourcify contract verification (opens in a new tab)
- Trillion Dollar Security initiative (opens in a new tab)