Solidity - EIP-712 Typed Structured Data Hashing and Signing explains how smart contracts verify the signature of EIP-712. This article explains how to use Ethers.js for EIP-712 signing. This article uses Ethers.js version 6.7.1.

Sign

signer.signTypedData

Ethers.js uses the following function to sign. Refer to the previous article Using Ethers.js to Sign and Recover to obtain the Signer.

1
signer.signTypedData(domain, types, value): Promise<string>
  • domain: The data of eip712Domain in the contract.
  • types: struct definition.
  • value: Message content, Please refer to the example below directly.
  • Return: A Promise object, and the content is a signature.

Use the previous example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import "@openzeppelin/contracts/utils/cryptography/EIP712.sol";

contract Example is EIP712 {
struct Mail {
address from;
address to;
string contents;
}

// 0x536e54c54e6699204b424f41f6dea846ee38ac369afec3e7c141d2c92c65e67f
bytes32 private constant MAIL_TYPEHASH = keccak256(
"Mail(address from,address to,string contents)"
);

constructor() EIP712("Example", "1") {
}

function hashStruct(Mail memory mail) public pure returns (bytes32) {
return keccak256(
abi.encode(
MAIL_TYPEHASH,
mail.from,
mail.to,
keccak256(abi.encodePacked(mail.contents))
)
);
}

function recover(Mail memory mail, uint8 v, bytes32 r, bytes32 s) public view returns (address) {
bytes32 structHash = hashStruct(mail);
bytes32 hash = _hashTypedDataV4(structHash);
return ecrecover(hash , v, r, s);
}
}

The corresponding Ethers.js usage is as follows

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// Use actual chainId. For example, mainnet is 1
const chainId = 1;

// Deployed contract address
const verifyingContract = '0xd8b934580fcE35a11B58C6D73aDeE468a2833fa8';

// Also fill in the name and version in the contract
const domain = {
chainId,
verifyingContract,
name: 'Example',
version: '1'
};

const types = {
// The following is custom struct
Mail: [
{ name: 'from', type: 'address' },
{ name: 'to', type: 'address' },
{ name: 'contents', type: 'string' }
]
};

// actual content
const value = {
from: '0xBE0eB53F46cd790Cd13851d5EFf43D12404d33E8',
to: '0xDA9dfA130Df4dE4673b89022EE50ff26f6EA73Cf',
contents: 'Hello'
};

// 0x78c03866a5efa253157e62596194631b4c9757d38b3556994d6b879d79d713e666f9c0a3e21e48fd061c5cf5217fd7e3c3ced521aa39650fda179ce61e6763b11c
const signature = await signer.signTypedData(domain, types, value);

We will get a not spiltted signature. We can refer to the previous article Using Ethers.js to Sign and Recover to handle it.

Other Examples

struct array

1
2
3
4
5
6
7
8
9
10
struct Mail {
Person from;
Person[] to;
string contents;
}

struct Person {
address wallet;
string name;
}

types and message change to the following

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
const types = {
Mail: [
{ name: 'from', type: 'Person' },
{ name: 'to', type: 'Person[]' },
{ name: 'contents', type: 'string' }
],
Person: [
{ name: 'wallet', type: 'address' },
{ name: 'name', type: 'string' }
]
};

const value = {
from: {
wallet: '0xBE0eB53F46cd790Cd13851d5EFf43D12404d33E8',
name: 'Binance 7'
},
to: [
{
wallet: '0xDA9dfA130Df4dE4673b89022EE50ff26f6EA73Cf',
name: 'Kraken 13'
}
],
contents: 'Hello'
};

Recover Signature

If you want to recover and verify the signature in the backend for login purposes, you can also use the following functions:

1
verifyTypedData(domain, types, value, signature: SignatureLike): string
  • domain: The data of eip712Domain in the contract.
  • types: struct definition.
  • value: Message content, Please refer to the example below directly.
  • signature: Signature. It can be a not splitted string or an object containing vrs.
  • Return: Signer address.

For example:

1
2
3
4
5
6
7
8
9
10
11
12
13
import { verifyTypedData } from "ethers";

const signature = '0x78c03866a5efa253157e62596194631b4c9757d38b3556994d6b879d79d713e666f9c0a3e21e48fd061c5cf5217fd7e3c3ced521aa39650fda179ce61e6763b11c';

// or vrs
// const signature = {
// r: '0x78c03866a5efa253157e62596194631b4c9757d38b3556994d6b879d79d713e6',
// s: '0x66f9c0a3e21e48fd061c5cf5217fd7e3c3ced521aa39650fda179ce61e6763b1',
// v: 28
// };

// 0xDD980c315dFA75682F04381E98ea38BD2A151540
const address = verifyTypedData(domain, types, value, signature);

Further Reading

Solidity - EIP-712 Typed Structured Data Hashing and Signing
Using Web3.js to Sign and Recover EIP-712 Typed Structured Data
Solidity - ecrecover
Using Ethers.js to Sign and Recover