Solidity - EIP-712 Typed Structured Data Hashing and Signing
This article explains the EIP-712 standard and how to use Solidity to implement EIP-712 in smart contract.
Introduction
In the previous article, we introduced some methods of signing and verification. In practice, we may sign many data. For example:
1 | function permit(address owner, address spender, uint value, uint nonce, uint deadline, uint8 v, bytes32 r, bytes32 s) external { |
Various data are used here, and the last data to be signed is messageHash. If you use a wallet such as Metamask, you will see the following pop up:
For the user, the actual content of the signature is unknown. To solve this problem, the EIP-712 standard was proposed to be able to sign and preview structured information. For example, the signed content is as follows:
Specifications
Make some slight modifications here to explain the definition in a simpler way. We know that the final content to be verified will be a hash as follows:
1 | ecrecover(hash , v, r, s); |
And EIP-712 defines how this hash comes from.
hash
The original signed information is defined as follows:
1 | hash = keccak256("\x19\x01" + domainSeparator + hashStruct(message)) |
domainSeparator
The definition is as follows:
1 | domainSeparator = hashStruct(eip712Domain) |
hashStruct
Hash structured data, the definition is as follows:
1 | hashStruct(s) = keccak256(typeHash of s + encodeData(data of s)) |
The hashed content includes the type description typeHash and all data contents.
typeHash
For different structured data, there will be different typeHash
1 | typeHash = keccak256(encodeType(type of s)) |
encodeType
The string describing the structure has a more complicated definition and the format is as follows:
1 | StructureName(type variableName, type variableName2,...) |
For example
1 | struct Mail { |
encodeType will be
1 | Mail(address from,address to,string contents) |
Notice
uintandintmust beuin256andint256.- Array are denoted by
Type[n]orType[]. For example,uint256[2]oruint256[].
When constructing a nested structure, the other structure names follow in alphabetical order. For example:
1 | struct Transaction { |
encodeType will be
1 | Transaction(Person from,Person to,Asset asset)Asset(address token,uint256 amount)Person(address wallet,string name) |
Asset comes before Person.
encodeData
Define how to encode structured data, organized into the following points:
- Each variable is converted into 32 bytes length data according to the following points.
- If the variable is
struct, it will be converted tobytes32by recursive callhashStruct(). Similarly, if it is astructarray, it will be converted tobytes32[]. - If variables are dynamic length
string,bytesand array, useabi.encodePackedto join and performkeccak256hashing. - Other general types are padded with leading zeros in front to become 32 bytes. It can be achieved by using
abi.encodein Solidity.
encodeData for the above example Mail:
1 | bytes32 internal constant TYPE_HASH = keccak256("Mail(address from,address to,string contents)"); |
There are more examples in later sections.
eip712Domain
Finally, back to the eip712Domain definition in domainSeparator. It is another set of predefined structured data that is used to distinguish signatures of different scopes and prevent replay attacks. Contains the following fields:
1 | struct EIP712Domain { |
- name: Customized name.
- version: Customized version number.
- chainId: Chain ID. Smart contracts usually use
block.chainidto obtain it. - verifyingContract: The contract address for verifying the signature. It is usually obtained by using
address(this)in smart contracts. - salt: This field is defined in the standard. If there is still a possibility of collision, you can use this field to distinguish. However, it is usually not used, and currently this field is not included in practice.
encodeType will be
1 | EIP712Domain(string name,string version,uint256 chainId,address verifyingContract) |
After setting the data such as name and version, pass this data into hashStruct to get domainSeparator.
Examples
Complete Example
A complete contract example is as follows:
1 | contract Example { |
Without Defining Struct
You don’t have to actually define struct in the contract, you can also directly pass the variables into it:
1 | function hashStruct(address from, address to, string memory contents) public pure returns (bytes32) { |
Nest Struct
Use the above Transaction as an example:
1 | bytes32 internal constant TRANSACTION_TYPE_HASH = keccak256("Transaction(Person from,Person to,Asset asset)Asset(address token,uint256 amount)Person(address wallet,string name)"); |
Array
1 | struct Mail { |
Struct Array
1 | struct Mail { |
OpenZeppelin Library
We can also use the OpenZeppelin EIP712 to handle this type of signature. First, import and inherit EIP712, and set name and version in constructor:
1 | import "@openzeppelin/contracts/utils/cryptography/EIP712.sol"; |
The library will generate domainSeparator and save it. You only need to implement the hashStruct by yourself. Finally, use the following function to generate the hash to verify:
1 | function _hashTypedDataV4(bytes32 structHash) internal view virtual returns (bytes32); |
Write the above example as follows:
1 | import "@openzeppelin/contracts/utils/cryptography/EIP712.sol"; |
Further Reading
Solidity - ecrecover
Using Web3.js to Sign and Recover EIP-712 Typed Structured Data
Using Ethers.js to Sign and Recover EIP-712 Typed Structured Data

