Overview
ERC-7920 brings a significant UX improvement to off-chain signatures.
It allows users to sign multiple EIP-712 messages with a single, verifiable signature. This eliminates consecutive signature prompts, reducing friction and boosting UX.
Motivation
Off-chain signatures are the hallmark of modern Ethereum apps: Hyperliquid, Polymarket, Wallet Sign In, Payments, UniswapV4 (Permit2), etc. They allow developers to safely interact with EOAs without relying on state-modification, which is often costly, slow, and requires the EOA to hold ETH.
Single Signature
A single signature attests to multiple messages.
Isolated Verification
Signed messages are independently verifiable, without knowledge of other messages included in the signature.
Human-readable
Know what you're signing: all the visibility benefits of EIP-712 are preserved. Giving wallets and EOAs insight into what is being signed.
Compatible with regular EIP-712 signatures
Regular EIP-712 signatures remain valid as "batch messages of 1". This allows ERC7920-aware apps to validate signatures regardless of if they signed with ERC-7920 or regular EIP-712.
New Verification Modes
Messages can be verified in isolation or aggregate. Apps can require that message (x
) is only valid when signed in combination with message (y
).
Specification
ERC-7920 uses a Merkle tree to hash multiple typed-data messages together under a single root. The user signs only the Merkle root. The process is described below.
Generating a Composite Signature
-
For a set of messages
[m₁, m₂, ..., mₙ]
, encode each using EIP-712'sencode
and compute its hash:hashₙ = keccak256(encode(mₙ))
-
Use these message hashes as leaf nodes in a Merkle tree and compute a
merkleRoot
-
Sign the merkle root.
signature = sign(merkleRoot)
Verification Process
To verify that an individual message mₓ
was included in a composite signature:
-
Verify the signature on the
merkleRoot
:recoveredSigner = ecrecover(merkleRoot, signature) isValidSignature = (recoveredSigner == expectedSigner)
-
Compute the leaf node for message
mₓ
and verify its path to the Merkle root, using the proof:leaf = keccak256(encode(mₓ)) isValidProof = _verifyMerkleProof(leaf, merkleProof, merkleRoot)
Where _verifyMerkleProof()
is defined as:
function _verifyMerkleProof(
bytes32 leaf,
bytes32[] calldata proof,
bytes32 merkleRoot
) internal pure returns (bool) {
bytes32 computedRoot = leaf;
for (uint256 i = 0; i < proof.length; ++i) {
if (computedRoot < proof[i]) {
computedRoot = keccak256(abi.encode(computedRoot, proof[i]));
} else {
computedRoot = keccak256(abi.encode(proof[i], computedRoot));
}
}
return computedRoot == merkleRoot;
}
The message is verified if and only if (1) and (2) succeed.
isVerified = isValidSignature && isValidProof
eth_signTypedData_v5
This ERC adds a new method eth_signTypedData_v5
to Ethereum JSON-RPC. This method allows signing multiple typed data messages with a single signature using the specification described above. The signing account must be unlocked prior.
This method returns: the signature, merkle root, and an array of proofs (each corresponding to an input message).
Parameters
Address
- Signing accountTypedData | TypedDataArray
- A single TypedData object or Array ofTypedData
objects from EIP-712.
Returns
{
signature: `0x${string}`; // Hex encoded 65 byte signature (same format as eth_sign)
merkleRoot: `0x${string}`; // 32 byte Merkle root as hex string
proofs: Array<Array<`0x${string}`>>; // Array of Merkle proofs (one for each input message)
}