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 V2
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 (the pinned
erc7730package does not install on Python 3.13 or later). If your system Python is a different version, uv (opens in a new tab) can download and run Python 3.12 for you - 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>.json, for example calldata-SwapRouter.json. The calldata- prefix tells the registry this descriptor covers contract function calls, as opposed to eip712- for typed-data messages. If you need to describe several versions of a contract, include the contract version in the name, as in the registry's calldata-UniswapV3Router02.json.
{
"$schema": "https://eips.ethereum.org/assets/eip-7730/erc7730-v2.schema.json",
"context": {},
"metadata": {},
"display": {
"formats": {}
}
}
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.
"context": {
"$id": "example-swap-router",
"contract": {
"deployments": [
{ "chainId": 1, "address": "0xYourContractAddressOnMainnet" },
{ "chainId": 137, "address": "0xYourContractAddressOnPolygon" }
]
}
}
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. This is the address users actually send transactions to. If your contract is upgradeable, that is the proxy address, not the implementation address, while the function signatures indisplay.formatscome from the implementation's ABI.
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.
"metadata": {
"owner": "Example Swap Protocol",
"info": { "url": "https://example.xyz" },
"contractName": "SwapRouter"
}
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
This example describes swapExactTokensForTokens from the Uniswap V2 Router02 contract. The input token is the first element of path, and the output token is the last one (path can have more than two elements for a multi-hop swap).
"display": {
"formats": {
"swapExactTokensForTokens(uint256 amountIn,uint256 amountOutMin,address[] path,address to,uint256 deadline)": {
"intent": "Swap",
"interpolatedIntent": "Swap {amountIn} for at least {amountOutMin}",
"fields": [
{
"path": "#.amountIn",
"label": "Send",
"format": "tokenAmount",
"params": {
"tokenPath": "#.path.[0]"
}
},
{
"path": "#.amountOutMin",
"label": "Receive minimum",
"format": "tokenAmount",
"params": {
"tokenPath": "#.path.[-1]"
}
},
{
"path": "#.to",
"label": "Recipient",
"format": "addressName",
"params": {
"types": ["eoa", "contract"],
"sources": ["local", "ens"]
}
},
{
"path": "#.deadline",
"label": "Expires",
"format": "date",
"params": {
"encoding": "timestamp"
}
}
]
}
}
}
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. Array elements are written with a.before the brackets:#.path.[0]is the first element and#.path.[-1]the last.#.path[0](without the dot) is rejected by the linter. -
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
Replace the placeholder addresses with the addresses of your deployed contract. The linter checks the descriptor against the verified ABI at those addresses, so it reports errors until you do.
{
"$schema": "https://eips.ethereum.org/assets/eip-7730/erc7730-v2.schema.json",
"context": {
"$id": "example-swap-router",
"contract": {
"deployments": [
{
"chainId": 1,
"address": "0xYourContractAddressOnMainnet"
},
{
"chainId": 137,
"address": "0xYourContractAddressOnPolygon"
}
]
}
},
"metadata": {
"owner": "Example Swap Protocol",
"info": {
"url": "https://example.xyz"
},
"contractName": "SwapRouter"
},
"display": {
"formats": {
"swapExactTokensForTokens(uint256 amountIn,uint256 amountOutMin,address[] path,address to,uint256 deadline)": {
"intent": "Swap",
"interpolatedIntent": "Swap {amountIn} for at least {amountOutMin}",
"fields": [
{
"path": "#.amountIn",
"label": "Send",
"format": "tokenAmount",
"params": {
"tokenPath": "#.path.[0]"
}
},
{
"path": "#.amountOutMin",
"label": "Receive minimum",
"format": "tokenAmount",
"params": {
"tokenPath": "#.path.[-1]"
}
},
{
"path": "#.to",
"label": "Recipient",
"format": "addressName",
"params": {
"types": ["eoa", "contract"],
"sources": ["local", "ens"]
}
},
{
"path": "#.deadline",
"label": "Expires",
"format": "date",
"params": {
"encoding": "timestamp"
}
}
]
}
}
}
}
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-SwapRouter.json - Update the
$schemafield to the relative path used within the repo:"../../specs/erc7730-v2.schema.json". CI rejects a URL here. - Add a test file (see below)
- Validate your files locally (see below)
- Open a pull request. Each pull request can change only one folder under
registry/
Add a test file
Every descriptor you add or change needs a test file, so the clear signing implementations can be checked against it. Create registry/<your-project-name>/testsv2/calldata-<ContractName>.tests.json with at least one test case for every function in display.formats. CI derives the function selector of each format and fails if no test transaction calls it.
{
"$schema": "../../../specs/erc7730-tests-v2.schema.json",
"descriptor": "../calldata-SwapRouter.json",
"dataProvider": {
"tokens": {
"0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48": { "symbol": "USDC", "decimals": 6, "name": "USD Coin" },
"0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2": { "symbol": "WETH", "decimals": 18, "name": "Wrapped Ether" }
}
},
"tests": [
{
"description": "Swap 1,000 USDC for at least 0.42 WETH",
"rawTx": "0x02f9012f0180843b9aca00...",
"expected": {
"intent": "Swap",
"interpolatedIntent": "Swap 1000 USDC for at least 0.42 WETH",
"owner": "Example Swap Protocol",
"fields": [
{ "label": "Send", "value": "1000 USDC" },
{ "label": "Receive minimum", "value": "0.42 WETH" },
{ "label": "Recipient", "value": "0x1234567890123456789012345678901234567890" },
{ "label": "Expires", "value": "2026-01-01 00:00:00Z" }
]
}
}
]
}
rawTxis the full serialized unsigned transaction (shortened here), not only the calldata. Real transactions to your contract are the best test cases.dataProvidersupplies the token metadata (and optionally address and ENS names) so the tests do not need network access.expectedis what a wallet should display. CI renders each test case with the clear signing implementations and reports any differences on the pull request.
See Reference test cases (opens in a new tab) in the registry README for the full format, including test files for EIP-712 descriptors.
Validate locally
The registry checks descriptors with the erc7730 (opens in a new tab) command-line tool, pinned in .github/requirements.txt. Run it from the root of your fork, in a Python 3.12 environment:
python3.12 -m venv .venv
source .venv/bin/activate
pip install -r .github/requirements.txt
erc7730 lint registry/myproject/calldata-SwapRouter.json
If you use uv (opens in a new tab), you can skip the virtual environment, and uv downloads Python 3.12 if needed:
uvx --python 3.12 --from "$(cat .github/requirements.txt)" erc7730 lint registry/myproject/calldata-SwapRouter.json
The linter checks the descriptor against the schema and against your contract's verified ABI. Fix every error. Warnings such as Missing display format mean the contract has functions your descriptor does not describe yet. To also check the $schema paths and the test file the way CI does, see How to validate (opens in a new tab) in the registry README.
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.