Skip to content

Add ERC: Composite EIP-712 Signatures #993

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 38 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 30 commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
a9ad9c8
Add ERC-XXXX: Composite EIP-712 Signatures
sola92 Mar 25, 2025
e2e4d3f
lints
sola92 Mar 25, 2025
0022d4d
updates
sola92 Mar 25, 2025
95ce3fc
Simplify signature by removing CompositeMessage type
sola92 Mar 27, 2025
dea21b4
fix lints
sola92 Mar 28, 2025
f5d16c3
fixes
sola92 Mar 28, 2025
0b5df96
updates
sola92 Mar 28, 2025
11d12e7
verify backwards compatibility
sola92 Mar 28, 2025
3a98549
eth_signCompositeTypedData -> eth_signTypedData_v5
sola92 Mar 28, 2025
1a095e5
fix lints
sola92 Mar 28, 2025
4ed988c
fix lints
sola92 Mar 28, 2025
6e674dd
fix lints
sola92 Mar 28, 2025
cf19e4e
fix lints
sola92 Mar 28, 2025
22828ba
fix lints
sola92 Mar 28, 2025
991df70
fix lints
sola92 Mar 28, 2025
12a62b3
fix lints
sola92 Mar 28, 2025
0294163
updates
sola92 Mar 28, 2025
7d14469
updates
sola92 Mar 28, 2025
fefafd4
updates
sola92 Mar 28, 2025
9e7b14f
updates
sola92 Mar 28, 2025
c611106
updates
sola92 Mar 28, 2025
54b260a
updates
sola92 Mar 28, 2025
3882a89
updates
sola92 Mar 28, 2025
490386c
updates
sola92 Mar 28, 2025
e125758
updates
sola92 Mar 28, 2025
b0cb002
updates
sola92 Mar 28, 2025
11b00c6
updates
sola92 Mar 28, 2025
473672a
updates
sola92 Mar 28, 2025
0705223
updates
sola92 Mar 28, 2025
47a76f8
updates
sola92 Mar 31, 2025
f23f0a4
- Fix typos
sola92 Apr 12, 2025
e306df0
updates
sola92 Apr 12, 2025
35ad9fc
updates
sola92 Apr 12, 2025
171c1cc
updates
sola92 Apr 18, 2025
bf41d3e
updates
sola92 May 4, 2025
bd08bc7
updates
sola92 May 4, 2025
5beb332
updates
sola92 May 4, 2025
54bae6a
updates
sola92 May 5, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
299 changes: 299 additions & 0 deletions ERCS/erc-7920.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,299 @@
---
eip: 7920
title: Composite EIP-712 Signatures
description: Composite EIP-712 signatures using merkle trees.
author: Sola Ogunsakin (@sola92)
discussions-to: https://ethereum-magicians.org/t/composite-eip-712-signatures/23266
status: Draft
type: Standards Track
category: ERC
created: 2025-03-20
requires: 20, 712
---

## Abstract

This ERC provides a standard for signing multiple typed-data messages with a single signature by encoding them into a Merkle tree. This allows components to independently verify messages, without requiring full knowledge of the others. It provides a significant UX improvement by reducing the number of signature prompts to one, while preserving the security and flexibility of the [EIP-712](./eip-712.md).

This ERC also gives applications the flexibility to verify messages in isolation, or in aggregate. This opens up new verification modalities: for e.g, an application can require that message (`x`) is only valid when signed in combination message (`y`).

## Motivation

As the ecosystem moves towards ETH-less transactions, users are often required to sign multiple off-chain messages in quick succession. Typically, a first signature is needed for a precise spend allowance (via Permit2, [ERC-2612](./eip-2612.md), etc.), followed by subsequent messages to direct the use of funds. This creates a frictional user experience as each signature requires a separate wallet interaction and creates confusion about what, in aggregate, is being approved.

Current solutions have significant drawbacks:

- **Pre-approving [ERC-20](./eip-20.md) allowance:** spend creates security vulnerabilities
- **Merging multiple messages into a single message:** prevents independent verifiability. Each message cannot be verified without knowledge of the entire batch
- **Separate signature requests:** creates friction in the user experience

## Specification

### Overview

The composite signature scheme uses a Merkle tree to hash multiple typed-data data messages together under a single root. The user signs only the Merkle root. The process is described below.

### Generating a Composite Signature

1. For a set of messages `[m₁, m₂, ..., mₙ]`, encode each using EIP-712's `encode` and compute its hash:

```
hashₙ = keccak256(encode(mₙ))
```

2. Use these message hashes as leaf nodes in a Merkle tree and compute a `marketRoot`

3. Sign the merkle root.

```
signature = sign(marketRoot)
```

### Verification Process

To verify that an individual message `mₓ` was included in a composite signature:

1. Verify the signature on the `merkleRoot`:

```
recoveredSigner = ecrecover(merkleRoot, signature)
isValidSignature = (recoveredSigner == expectedSigner)
```

2. Compute the leaf node for message `mₓ` and verify it's path to the Merkle root, using the proof:
```
leaf = keccak256(encode(mₓ))
isValidProof = _verifyMerkleProof(leaf, merkleProof, merkleRoot)
```

The message is verified if and only if (1) and (2) succeed.

```
isVerified = isValidSignature && isValidProof
```

#### Solidity Example

Snippet below verifies a composite signature on-chain. The entrypoint, `placeOrder()` is called by a paymaster to sponsor an operation. It verifies the composite signature using the process outlined above.

```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

error Unauthorized();

contract ExampleVerifier {
bytes32 public immutable DOMAIN_SEPARATOR;
bytes32 private constant MESSAGE_TYPEHASH =
keccak256("PlaceOrder(bytes32 orderId, address user)");

constructor() {
DOMAIN_SEPARATOR = keccak256(
abi.encode(
keccak256(
"EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"
),
keccak256(bytes("MyApp")),
keccak256(bytes("1.0.0")),
block.chainid,
address(this)
)
);
}

function placeOrder(
bytes32 orderId,
address user,
bytes calldata signature,
bytes32 merkleRoot,
bytes32[] calldata proof
) public {
bytes32 message = keccak256(
abi.encodePacked(
"\x19\x01",
DOMAIN_SEPARATOR,
keccak256(abi.encode(MESSAGE_TYPEHASH, orderId, user))
)
);

if (
!_verifyCompositeSignature(
message,
proof,
merkleRoot,
signature,
user
)
) {
revert Unauthorized();
}

// DO STUFF
}

function _verifyCompositeSignature(
bytes32 message,
bytes32[] calldata proof,
bytes32 merkleRoot,
bytes calldata signature,
address expectedSigner
) internal view returns (bool) {
if (!_verifyMerkleProof(message, proof, merkleRoot)) {
return false;
}

return recover(merkleRoot, signature) == expectedSigner;
}

function _verifyMerkleProof(
bytes32 leaf,
bytes32[] calldata proof,
bytes32 root
) internal pure returns (bool) {
bytes32 computedRoot = leaf;
for (uint256 i = 0; i < proof.length; ++i) {
if (computedRoot < proof[i]) {
computedRoot = keccak256(
abi.encodePacked(computedRoot, proof[i])
);
} else {
computedRoot = keccak256(
abi.encodePacked(proof[i], computedRoot)
);
}
}
return computedRoot == root;
}

function recover(
bytes32 digest,
bytes memory signature
) internal pure returns (address) {
require(signature.length == 65, "Invalid signature length");

bytes32 r;
bytes32 s;
uint8 v;

assembly {
r := mload(add(signature, 32))
s := mload(add(signature, 64))
v := byte(0, mload(add(signature, 96)))
}

return ecrecover(digest, v, r, s);
}
}
```

### Specification of `eth_signTypedData_v5` JSON RPC method.

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 prior unlocked.

This method returns: the signature, merkle root, and an array of proofs (each corresponding to an input message).

#### Parameters

1. `Address` - Signing account
2. `TypedData | TypedDataArray` - A single TypedData object or Array of `TypedData` objects from EIP-712.

##### Returns

```JavaScript
{
signature: 'DATA', // Hex encoded 65 byte signature (same format as eth_sign)
merkleRoot: 'DATA', // 32 byte Merkle root as hex string
proofs: [ // Array of Merkle proofs (one for each input message)
['DATA', 'DATA'], // First message proof (array of 32 byte hex strings)
['DATA', 'DATA'], // Second message proof
...
]
}
```

##### Example

Request:

```shell
curl -X POST --data '{"jsonrpc":"2.0","method":"eth_signTypedData_v5","params":["0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826", [{"types":{"EIP712Domain":[{"name":"name","type":"string"},{"name":"version","type":"string"},{"name":"chainId","type":"uint256"},{"name":"verifyingContract","type":"address"}],"Person":[{"name":"name","type":"string"},{"name":"wallet","type":"address"}],"Mail":[{"name":"from","type":"Person"},{"name":"to","type":"Person"},{"name":"contents","type":"string"}]},"primaryType":"Mail","domain":{"name":"Ether Mail","version":"1","chainId":1,"verifyingContract":"0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC"},"message":{"from":{"name":"Cow","wallet":"0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826"},"to":{"name":"Bob","wallet":"0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB"},"contents":"Hello, Bob!"}}, {"types":{"EIP712Domain":[{"name":"name","type":"string"},{"name":"version","type":"string"},{"name":"chainId","type":"uint256"},{"name":"verifyingContract","type":"address"}],"Transfer":[{"name":"amount","type":"uint256"},{"name":"recipient","type":"address"}]},"primaryType":"Transfer","domain":{"name":"Ether Mail","version":"1","chainId":1,"verifyingContract":"0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC"},"message":{"amount":"1000000000000000000","recipient":"0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB"}}]],"id":1}'
```

Result:

```JavaScript
{
"id": 1,
"jsonrpc": "2.0",
"result": {
"signature": "0x4355c47d63924e8a72e509b65029052eb6c299d53a04e167c5775fd466751c9d07299936d304c153f6443dfa05f40ff007d72911b6f72307f996231605b915621c",
"merkleRoot": "0x7de103665e21d6c9d9f82ae59675443bd895ed42b571c7f952c2fdc1a5b6e8d2",
"proofs": [
["0x4bdbac3830d492ac3f4b0ef674786940fb33481b32392e88edafd45d507429f2"],
["0x95be87f8abefcddc8116061a06b18906f32298a4644882d06baff852164858c6"]
]
}
}
```

## Rationale

### UX Improvement

A single signature that covers multiple messages

### Isolated Verification

Independent verification of messages without knowledge of others

### Human-readable

This ERC preserves the readability benefits of EIP-712. Giving wallets and users insight into what is being signed.

### Efficient verification on-chain

`_verifyMerkleProof` has a runtime of `O(log(N))` where N is the number of messages that were signed.

### Improved Wallet Security

Certain messages signed in isolation may appear harmless but combined with may be harmful. Giving the full list of messages to users could help them better navigate their experience.

### Flexible Verification Modes

Applications can require combination of messages be signed together to enhance security.

## Backwards Compatibility

When the number of message is one, `eth_signTypedData_v5` produces the same signature as `eth_signTypedData_v4` since `merkleRoot == keccak256(encode(message))`. This allows `eth_signTypedData_v5` to be a drop-in replacement for `eth_signTypedData_v4` with no changes to on-chain verification.

## Reference Implementation

Reference implementation of `eth_signTypedData_v5` can be found the assets directory.

## Security Considerations

### Replay Protection

This ERC focuses on generating composite messages and verifying their signatures. It does not contain mechanisms to prevent replays. Developers **must** ensure their applications can handle receiving the same message twice.

### Partial Message Verification

During verification, care **must** be taken to ensure that **both** of these checks pass:

1. EIP-712 signature on the Merkle root is valid
2. Merkle proof is valid against the root

### User Understanding

Wallets **must** communicate to users that they are signing multiple messages at once. They should provide a way for users to review all messages included in the composite signature.

### Merkle Tree Construction

Merkle tree should be constructed in a consistent manner.

1. The hashing function **must** be `keccak256`
2. Hash pairs **must** be sorted lexicographically to simplify verification

## Copyright

Copyright and related rights waived via [CC0](../LICENSE.md).
50 changes: 50 additions & 0 deletions assets/erc-7920/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# Dependencies
node_modules
package-lock.json
yarn.lock

# Hardhat
cache
artifacts
typechain
typechain-types

# Environment variables
.env
.env.*
!.env.example

# Coverage
coverage
coverage.json

# IDE and editors
.idea
.vscode
*.swp
*.swo

# Build output
dist
build

# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# Testing
coverage
coverage.json
.nyc_output

# Deployments (optional, you might want to track these)
# deployments

# Miscellaneous
.DS_Store
.tmp
temp
.cache
Loading
Loading