Skip to content

Commit 5748034

Browse files
authored
Add EIP 712 helpers (#2418)
1 parent 061e7f0 commit 5748034

File tree

8 files changed

+366
-25
lines changed

8 files changed

+366
-25
lines changed

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22

33
## Unreleased
44

5-
* Add beacon proxy. ([#2411](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2411))
5+
* `BeaconProxy`: added new kind of proxy that allows simultaneous atomic upgrades. ([#2411](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2411))
6+
* `EIP712`: added helpers to verify EIP712 typed data signatures on chain. ([#2418](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2418))
67
* `Address`: added `functionDelegateCall`, similar to the existing `functionCall`. ([#2333](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2333))
78

89
## 3.3.0 (2020-11-26)

contracts/cryptography/README.adoc

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@ NOTE: This document is better viewed at https://docs.openzeppelin.com/contracts/
55

66
This collection of libraries provides simple and safe ways to use different cryptographic primitives.
77

8+
The following related EIPs are in draft status and can be found in the drafts directory.
9+
10+
- {EIP712}
11+
812
== Libraries
913

1014
{{ECDSA}}

contracts/drafts/EIP712.sol

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
// SPDX-License-Identifier: MIT
2+
3+
pragma solidity >=0.6.0 <0.8.0;
4+
5+
/**
6+
* @dev https://eips.ethereum.org/EIPS/eip-712[EIP 712] is a standard for hashing and signing of typed structured data.
7+
*
8+
* The encoding specified in the EIP is very generic, and such a generic implementation in Solidity is not feasible,
9+
* thus this contract does not implement the encoding itself. Protocols need to implement the type-specific encoding
10+
* they need in their contracts using a combination of `abi.encode` and `keccak256`.
11+
*
12+
* This contract implements the EIP 712 domain separator ({_domainSeparatorV4}) that is used as part of the encoding
13+
* scheme, and the final step of the encoding to obtain the message digest that is then signed via ECDSA
14+
* ({_hashTypedDataV4}).
15+
*
16+
* The implementation of the domain separator was designed to be as efficient as possible while still properly updating
17+
* the chain id to protect against replay attacks on an eventual fork of the chain.
18+
*
19+
* NOTE: This contract implements the version of the encoding known as "v4", as implemented by the JSON RPC method
20+
* https://docs.metamask.io/guide/signing-data.html[`eth_signTypedDataV4` in MetaMask].
21+
*/
22+
abstract contract EIP712 {
23+
/* solhint-disable var-name-mixedcase */
24+
// Cache the domain separator as an immutable value, but also store the chain id that it corresponds to, in order to
25+
// invalidate the cached domain separator if the chain id changes.
26+
bytes32 private immutable _CACHED_DOMAIN_SEPARATOR;
27+
uint256 private immutable _CACHED_CHAIN_ID;
28+
29+
bytes32 private immutable _HASHED_NAME;
30+
bytes32 private immutable _HASHED_VERSION;
31+
bytes32 private immutable _TYPE_HASH;
32+
/* solhint-enable var-name-mixedcase */
33+
34+
/**
35+
* @dev Initializes the domain separator and parameter caches.
36+
*
37+
* The meaning of `name` and `version` is specified in
38+
* https://eips.ethereum.org/EIPS/eip-712#definition-of-domainseparator[EIP 712]:
39+
*
40+
* - `name`: the user readable name of the signing domain, i.e. the name of the DApp or the protocol.
41+
* - `version`: the current major version of the signing domain.
42+
*
43+
* NOTE: These parameters cannot be changed except through a xref:learn::upgrading-smart-contracts.adoc[smart
44+
* contract upgrade].
45+
*/
46+
constructor(string memory name, string memory version) internal {
47+
bytes32 hashedName = keccak256(bytes(name));
48+
bytes32 hashedVersion = keccak256(bytes(version));
49+
bytes32 typeHash = keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)");
50+
_HASHED_NAME = hashedName;
51+
_HASHED_VERSION = hashedVersion;
52+
_CACHED_CHAIN_ID = _getChainId();
53+
_CACHED_DOMAIN_SEPARATOR = _buildDomainSeparator(typeHash, hashedName, hashedVersion);
54+
_TYPE_HASH = typeHash;
55+
}
56+
57+
/**
58+
* @dev Returns the domain separator for the current chain.
59+
*/
60+
function _domainSeparatorV4() internal view returns (bytes32) {
61+
if (_getChainId() == _CACHED_CHAIN_ID) {
62+
return _CACHED_DOMAIN_SEPARATOR;
63+
} else {
64+
return _buildDomainSeparator(_TYPE_HASH, _HASHED_NAME, _HASHED_VERSION);
65+
}
66+
}
67+
68+
function _buildDomainSeparator(bytes32 typeHash, bytes32 name, bytes32 version) private view returns (bytes32) {
69+
return keccak256(
70+
abi.encode(
71+
typeHash,
72+
name,
73+
version,
74+
_getChainId(),
75+
address(this)
76+
)
77+
);
78+
}
79+
80+
/**
81+
* @dev Given an already https://eips.ethereum.org/EIPS/eip-712#definition-of-hashstruct[hashed struct], this
82+
* function returns the hash of the fully encoded EIP712 message for this domain.
83+
*
84+
* This hash can be used together with {ECDSA-recover} to obtain the signer of a message. For example:
85+
*
86+
* ```solidity
87+
* bytes32 digest = _hashTypedDataV4(keccak256(abi.encode(
88+
* keccak256("Mail(address to,string contents)"),
89+
* mailTo,
90+
* keccak256(bytes(mailContents))
91+
* )));
92+
* address signer = ECDSA.recover(digest, signature);
93+
* ```
94+
*/
95+
function _hashTypedDataV4(bytes32 structHash) internal view returns (bytes32) {
96+
return keccak256(abi.encodePacked("\x19\x01", _domainSeparatorV4(), structHash));
97+
}
98+
99+
function _getChainId() private pure returns (uint256 chainId) {
100+
// solhint-disable-next-line no-inline-assembly
101+
assembly {
102+
chainId := chainid()
103+
}
104+
}
105+
}

contracts/drafts/README.adoc

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
= Draft EIPS
2+
3+
This directory contains implementations of EIPs that are still in Draft status.
4+
5+
Due to their nature as drafts, the details of these contracts may change and we cannot guarantee their xref:ROOT:releases-stability.adoc[stability]. Minor releases of OpenZeppelin Contracts may contain breaking changes for the contracts in this directory, which will be duly announced in the https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/CHANGELOG.md[changelog]. The EIPs included here are used by projects in production and this may make them less likely to change significantly.
6+
7+
== Cryptography
8+
9+
{{EIP712}}

contracts/mocks/EIP712External.sol

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
// SPDX-License-Identifier: MIT
2+
3+
pragma solidity >=0.6.0 <0.8.0;
4+
5+
import "../drafts/EIP712.sol";
6+
import "../cryptography/ECDSA.sol";
7+
8+
contract EIP712External is EIP712 {
9+
constructor(string memory name, string memory version) public EIP712(name, version) {}
10+
11+
function domainSeparator() external view returns (bytes32) {
12+
return _domainSeparatorV4();
13+
}
14+
15+
function verify(bytes memory signature, address signer, address mailTo, string memory mailContents) external view {
16+
bytes32 digest = _hashTypedDataV4(keccak256(abi.encode(
17+
keccak256("Mail(address to,string contents)"),
18+
mailTo,
19+
keccak256(bytes(mailContents))
20+
)));
21+
address recoveredSigner = ECDSA.recover(digest, signature);
22+
require(recoveredSigner == signer);
23+
}
24+
25+
function getChainId() external pure returns (uint256 chainId) {
26+
// solhint-disable-next-line no-inline-assembly
27+
assembly {
28+
chainId := chainid()
29+
}
30+
}
31+
}

package-lock.json

Lines changed: 143 additions & 24 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,9 @@
6060
"eslint-plugin-node": "^10.0.0",
6161
"eslint-plugin-promise": "^4.2.1",
6262
"eslint-plugin-standard": "^4.0.1",
63+
"eth-sig-util": "^3.0.0",
6364
"ethereumjs-util": "^7.0.7",
65+
"ethereumjs-wallet": "^1.0.1",
6466
"lodash.startcase": "^4.4.0",
6567
"lodash.zip": "^4.2.0",
6668
"micromatch": "^4.0.2",

0 commit comments

Comments
 (0)