Unverified Commit 5748034c by Francisco Giordano Committed by GitHub

Add EIP 712 helpers (#2418)

parent 061e7f0d
...@@ -2,7 +2,8 @@ ...@@ -2,7 +2,8 @@
## Unreleased ## Unreleased
* Add beacon proxy. ([#2411](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2411)) * `BeaconProxy`: added new kind of proxy that allows simultaneous atomic upgrades. ([#2411](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2411))
* `EIP712`: added helpers to verify EIP712 typed data signatures on chain. ([#2418](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2418))
* `Address`: added `functionDelegateCall`, similar to the existing `functionCall`. ([#2333](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2333)) * `Address`: added `functionDelegateCall`, similar to the existing `functionCall`. ([#2333](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2333))
## 3.3.0 (2020-11-26) ## 3.3.0 (2020-11-26)
......
...@@ -5,6 +5,10 @@ NOTE: This document is better viewed at https://docs.openzeppelin.com/contracts/ ...@@ -5,6 +5,10 @@ NOTE: This document is better viewed at https://docs.openzeppelin.com/contracts/
This collection of libraries provides simple and safe ways to use different cryptographic primitives. This collection of libraries provides simple and safe ways to use different cryptographic primitives.
The following related EIPs are in draft status and can be found in the drafts directory.
- {EIP712}
== Libraries == Libraries
{{ECDSA}} {{ECDSA}}
......
// SPDX-License-Identifier: MIT
pragma solidity >=0.6.0 <0.8.0;
/**
* @dev https://eips.ethereum.org/EIPS/eip-712[EIP 712] is a standard for hashing and signing of typed structured data.
*
* The encoding specified in the EIP is very generic, and such a generic implementation in Solidity is not feasible,
* thus this contract does not implement the encoding itself. Protocols need to implement the type-specific encoding
* they need in their contracts using a combination of `abi.encode` and `keccak256`.
*
* This contract implements the EIP 712 domain separator ({_domainSeparatorV4}) that is used as part of the encoding
* scheme, and the final step of the encoding to obtain the message digest that is then signed via ECDSA
* ({_hashTypedDataV4}).
*
* The implementation of the domain separator was designed to be as efficient as possible while still properly updating
* the chain id to protect against replay attacks on an eventual fork of the chain.
*
* NOTE: This contract implements the version of the encoding known as "v4", as implemented by the JSON RPC method
* https://docs.metamask.io/guide/signing-data.html[`eth_signTypedDataV4` in MetaMask].
*/
abstract contract EIP712 {
/* solhint-disable var-name-mixedcase */
// Cache the domain separator as an immutable value, but also store the chain id that it corresponds to, in order to
// invalidate the cached domain separator if the chain id changes.
bytes32 private immutable _CACHED_DOMAIN_SEPARATOR;
uint256 private immutable _CACHED_CHAIN_ID;
bytes32 private immutable _HASHED_NAME;
bytes32 private immutable _HASHED_VERSION;
bytes32 private immutable _TYPE_HASH;
/* solhint-enable var-name-mixedcase */
/**
* @dev Initializes the domain separator and parameter caches.
*
* The meaning of `name` and `version` is specified in
* https://eips.ethereum.org/EIPS/eip-712#definition-of-domainseparator[EIP 712]:
*
* - `name`: the user readable name of the signing domain, i.e. the name of the DApp or the protocol.
* - `version`: the current major version of the signing domain.
*
* NOTE: These parameters cannot be changed except through a xref:learn::upgrading-smart-contracts.adoc[smart
* contract upgrade].
*/
constructor(string memory name, string memory version) internal {
bytes32 hashedName = keccak256(bytes(name));
bytes32 hashedVersion = keccak256(bytes(version));
bytes32 typeHash = keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)");
_HASHED_NAME = hashedName;
_HASHED_VERSION = hashedVersion;
_CACHED_CHAIN_ID = _getChainId();
_CACHED_DOMAIN_SEPARATOR = _buildDomainSeparator(typeHash, hashedName, hashedVersion);
_TYPE_HASH = typeHash;
}
/**
* @dev Returns the domain separator for the current chain.
*/
function _domainSeparatorV4() internal view returns (bytes32) {
if (_getChainId() == _CACHED_CHAIN_ID) {
return _CACHED_DOMAIN_SEPARATOR;
} else {
return _buildDomainSeparator(_TYPE_HASH, _HASHED_NAME, _HASHED_VERSION);
}
}
function _buildDomainSeparator(bytes32 typeHash, bytes32 name, bytes32 version) private view returns (bytes32) {
return keccak256(
abi.encode(
typeHash,
name,
version,
_getChainId(),
address(this)
)
);
}
/**
* @dev Given an already https://eips.ethereum.org/EIPS/eip-712#definition-of-hashstruct[hashed struct], this
* function returns the hash of the fully encoded EIP712 message for this domain.
*
* This hash can be used together with {ECDSA-recover} to obtain the signer of a message. For example:
*
* ```solidity
* bytes32 digest = _hashTypedDataV4(keccak256(abi.encode(
* keccak256("Mail(address to,string contents)"),
* mailTo,
* keccak256(bytes(mailContents))
* )));
* address signer = ECDSA.recover(digest, signature);
* ```
*/
function _hashTypedDataV4(bytes32 structHash) internal view returns (bytes32) {
return keccak256(abi.encodePacked("\x19\x01", _domainSeparatorV4(), structHash));
}
function _getChainId() private pure returns (uint256 chainId) {
// solhint-disable-next-line no-inline-assembly
assembly {
chainId := chainid()
}
}
}
= Draft EIPS
This directory contains implementations of EIPs that are still in Draft status.
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.
== Cryptography
{{EIP712}}
// SPDX-License-Identifier: MIT
pragma solidity >=0.6.0 <0.8.0;
import "../drafts/EIP712.sol";
import "../cryptography/ECDSA.sol";
contract EIP712External is EIP712 {
constructor(string memory name, string memory version) public EIP712(name, version) {}
function domainSeparator() external view returns (bytes32) {
return _domainSeparatorV4();
}
function verify(bytes memory signature, address signer, address mailTo, string memory mailContents) external view {
bytes32 digest = _hashTypedDataV4(keccak256(abi.encode(
keccak256("Mail(address to,string contents)"),
mailTo,
keccak256(bytes(mailContents))
)));
address recoveredSigner = ECDSA.recover(digest, signature);
require(recoveredSigner == signer);
}
function getChainId() external pure returns (uint256 chainId) {
// solhint-disable-next-line no-inline-assembly
assembly {
chainId := chainid()
}
}
}
...@@ -555,6 +555,62 @@ ...@@ -555,6 +555,62 @@
"integrity": "sha512-LEHHyuhlPY3TmuUYMh2oz89lTShfvgbmzaBcxve9t/9Wuy7Dwf4yoAKcND7KFT1HAQfqZ12qtc+DUrBMeKF9nw==", "integrity": "sha512-LEHHyuhlPY3TmuUYMh2oz89lTShfvgbmzaBcxve9t/9Wuy7Dwf4yoAKcND7KFT1HAQfqZ12qtc+DUrBMeKF9nw==",
"dev": true "dev": true
}, },
"eth-sig-util": {
"version": "2.5.2",
"resolved": "https://registry.npmjs.org/eth-sig-util/-/eth-sig-util-2.5.2.tgz",
"integrity": "sha512-xvDojS/4reXsw8Pz/+p/qcM5rVB61FOdPbEtMZ8FQ0YHnPEzPy5F8zAAaZ+zj5ud0SwRLWPfor2Cacjm7EzMIw==",
"dev": true,
"requires": {
"buffer": "^5.2.1",
"elliptic": "^6.4.0",
"ethereumjs-abi": "0.6.5",
"ethereumjs-util": "^5.1.1",
"tweetnacl": "^1.0.0",
"tweetnacl-util": "^0.15.0"
},
"dependencies": {
"ethereumjs-abi": {
"version": "0.6.5",
"resolved": "https://registry.npmjs.org/ethereumjs-abi/-/ethereumjs-abi-0.6.5.tgz",
"integrity": "sha1-WmN+8Wq0NHP6cqKa2QhxQFs/UkE=",
"dev": true,
"requires": {
"bn.js": "^4.10.0",
"ethereumjs-util": "^4.3.0"
},
"dependencies": {
"ethereumjs-util": {
"version": "4.5.1",
"resolved": "https://registry.npmjs.org/ethereumjs-util/-/ethereumjs-util-4.5.1.tgz",
"integrity": "sha512-WrckOZ7uBnei4+AKimpuF1B3Fv25OmoRgmYCpGsP7u8PFxXAmAgiJSYT2kRWnt6fVIlKaQlZvuwXp7PIrmn3/w==",
"dev": true,
"requires": {
"bn.js": "^4.8.0",
"create-hash": "^1.1.2",
"elliptic": "^6.5.2",
"ethereum-cryptography": "^0.1.3",
"rlp": "^2.0.0"
}
}
}
},
"ethereumjs-util": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/ethereumjs-util/-/ethereumjs-util-5.2.1.tgz",
"integrity": "sha512-v3kT+7zdyCm1HIqWlLNrHGqHGLpGYIhjeHxQjnDXjLT2FyGJDsd3LWMYUo7pAFRrk86CR3nUJfhC81CCoJNNGQ==",
"dev": true,
"requires": {
"bn.js": "^4.11.0",
"create-hash": "^1.1.2",
"elliptic": "^6.5.2",
"ethereum-cryptography": "^0.1.3",
"ethjs-util": "^0.1.3",
"rlp": "^2.0.0",
"safe-buffer": "^5.1.1"
}
}
}
},
"ethereumjs-util": { "ethereumjs-util": {
"version": "6.2.1", "version": "6.2.1",
"resolved": "https://registry.npmjs.org/ethereumjs-util/-/ethereumjs-util-6.2.1.tgz", "resolved": "https://registry.npmjs.org/ethereumjs-util/-/ethereumjs-util-6.2.1.tgz",
...@@ -1458,12 +1514,74 @@ ...@@ -1458,12 +1514,74 @@
"web3-utils": "^1.2.1" "web3-utils": "^1.2.1"
}, },
"dependencies": { "dependencies": {
"aes-js": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/aes-js/-/aes-js-3.1.2.tgz",
"integrity": "sha512-e5pEa2kBnBOgR4Y/p20pskXI74UEz7de8ZGVo58asOtvSVG5YAbJeELPZxOmt+Bnz3rX753YKhfIn4X4l1PPRQ==",
"dev": true
},
"bignumber.js": { "bignumber.js": {
"version": "9.0.1", "version": "9.0.1",
"resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.0.1.tgz", "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.0.1.tgz",
"integrity": "sha512-IdZR9mh6ahOBv/hYGiXyVuyCetmGJhtYkqLBpTStdhEGjegpPlUawydyaF3pbIOFynJTpllEs+NP+CS9jKFLjA==", "integrity": "sha512-IdZR9mh6ahOBv/hYGiXyVuyCetmGJhtYkqLBpTStdhEGjegpPlUawydyaF3pbIOFynJTpllEs+NP+CS9jKFLjA==",
"dev": true "dev": true
}, },
"eth-sig-util": {
"version": "2.5.2",
"resolved": "https://registry.npmjs.org/eth-sig-util/-/eth-sig-util-2.5.2.tgz",
"integrity": "sha512-xvDojS/4reXsw8Pz/+p/qcM5rVB61FOdPbEtMZ8FQ0YHnPEzPy5F8zAAaZ+zj5ud0SwRLWPfor2Cacjm7EzMIw==",
"dev": true,
"requires": {
"buffer": "^5.2.1",
"elliptic": "^6.4.0",
"ethereumjs-abi": "0.6.5",
"ethereumjs-util": "^5.1.1",
"tweetnacl": "^1.0.0",
"tweetnacl-util": "^0.15.0"
},
"dependencies": {
"ethereumjs-util": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/ethereumjs-util/-/ethereumjs-util-5.2.1.tgz",
"integrity": "sha512-v3kT+7zdyCm1HIqWlLNrHGqHGLpGYIhjeHxQjnDXjLT2FyGJDsd3LWMYUo7pAFRrk86CR3nUJfhC81CCoJNNGQ==",
"dev": true,
"requires": {
"bn.js": "^4.11.0",
"create-hash": "^1.1.2",
"elliptic": "^6.5.2",
"ethereum-cryptography": "^0.1.3",
"ethjs-util": "^0.1.3",
"rlp": "^2.0.0",
"safe-buffer": "^5.1.1"
}
}
}
},
"ethereumjs-abi": {
"version": "0.6.5",
"resolved": "https://registry.npmjs.org/ethereumjs-abi/-/ethereumjs-abi-0.6.5.tgz",
"integrity": "sha1-WmN+8Wq0NHP6cqKa2QhxQFs/UkE=",
"dev": true,
"requires": {
"bn.js": "^4.10.0",
"ethereumjs-util": "^4.3.0"
},
"dependencies": {
"ethereumjs-util": {
"version": "4.5.1",
"resolved": "https://registry.npmjs.org/ethereumjs-util/-/ethereumjs-util-4.5.1.tgz",
"integrity": "sha512-WrckOZ7uBnei4+AKimpuF1B3Fv25OmoRgmYCpGsP7u8PFxXAmAgiJSYT2kRWnt6fVIlKaQlZvuwXp7PIrmn3/w==",
"dev": true,
"requires": {
"bn.js": "^4.8.0",
"create-hash": "^1.1.2",
"elliptic": "^6.5.2",
"ethereum-cryptography": "^0.1.3",
"rlp": "^2.0.0"
}
}
}
},
"ethereumjs-tx": { "ethereumjs-tx": {
"version": "1.3.7", "version": "1.3.7",
"resolved": "https://registry.npmjs.org/ethereumjs-tx/-/ethereumjs-tx-1.3.7.tgz", "resolved": "https://registry.npmjs.org/ethereumjs-tx/-/ethereumjs-tx-1.3.7.tgz",
...@@ -1505,6 +1623,23 @@ ...@@ -1505,6 +1623,23 @@
"ethjs-util": "0.1.6", "ethjs-util": "0.1.6",
"rlp": "^2.2.3" "rlp": "^2.2.3"
} }
},
"ethereumjs-wallet": {
"version": "0.6.5",
"resolved": "https://registry.npmjs.org/ethereumjs-wallet/-/ethereumjs-wallet-0.6.5.tgz",
"integrity": "sha512-MDwjwB9VQVnpp/Dc1XzA6J1a3wgHQ4hSvA1uWNatdpOrtCbPVuQSKSyRnjLvS0a+KKMw2pvQ9Ybqpb3+eW8oNA==",
"dev": true,
"requires": {
"aes-js": "^3.1.1",
"bs58check": "^2.1.2",
"ethereum-cryptography": "^0.1.3",
"ethereumjs-util": "^6.0.0",
"randombytes": "^2.0.6",
"safe-buffer": "^5.1.2",
"scryptsy": "^1.2.1",
"utf8": "^3.0.0",
"uuid": "^3.3.2"
}
} }
} }
}, },
...@@ -5440,9 +5575,9 @@ ...@@ -5440,9 +5575,9 @@
} }
}, },
"eth-sig-util": { "eth-sig-util": {
"version": "2.5.2", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/eth-sig-util/-/eth-sig-util-2.5.2.tgz", "resolved": "https://registry.npmjs.org/eth-sig-util/-/eth-sig-util-3.0.0.tgz",
"integrity": "sha512-xvDojS/4reXsw8Pz/+p/qcM5rVB61FOdPbEtMZ8FQ0YHnPEzPy5F8zAAaZ+zj5ud0SwRLWPfor2Cacjm7EzMIw==", "integrity": "sha512-4eFkMOhpGbTxBQ3AMzVf0haUX2uTur7DpWiHzWyTURa28BVJJtOkcb9Ok5TV0YvEPG61DODPW7ZUATbJTslioQ==",
"dev": true, "dev": true,
"requires": { "requires": {
"buffer": "^5.2.1", "buffer": "^5.2.1",
...@@ -5824,18 +5959,17 @@ ...@@ -5824,18 +5959,17 @@
} }
}, },
"ethereumjs-wallet": { "ethereumjs-wallet": {
"version": "0.6.5", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/ethereumjs-wallet/-/ethereumjs-wallet-0.6.5.tgz", "resolved": "https://registry.npmjs.org/ethereumjs-wallet/-/ethereumjs-wallet-1.0.1.tgz",
"integrity": "sha512-MDwjwB9VQVnpp/Dc1XzA6J1a3wgHQ4hSvA1uWNatdpOrtCbPVuQSKSyRnjLvS0a+KKMw2pvQ9Ybqpb3+eW8oNA==", "integrity": "sha512-3Z5g1hG1das0JWU6cQ9HWWTY2nt9nXCcwj7eXVNAHKbo00XAZO8+NHlwdgXDWrL0SXVQMvTWN8Q/82DRH/JhPw==",
"dev": true, "dev": true,
"requires": { "requires": {
"aes-js": "^3.1.1", "aes-js": "^3.1.1",
"bs58check": "^2.1.2", "bs58check": "^2.1.2",
"ethereum-cryptography": "^0.1.3", "ethereum-cryptography": "^0.1.3",
"ethereumjs-util": "^6.0.0", "ethereumjs-util": "^7.0.2",
"randombytes": "^2.0.6", "randombytes": "^2.0.6",
"safe-buffer": "^5.1.2", "scrypt-js": "^3.0.1",
"scryptsy": "^1.2.1",
"utf8": "^3.0.0", "utf8": "^3.0.0",
"uuid": "^3.3.2" "uuid": "^3.3.2"
}, },
...@@ -5845,21 +5979,6 @@ ...@@ -5845,21 +5979,6 @@
"resolved": "https://registry.npmjs.org/aes-js/-/aes-js-3.1.2.tgz", "resolved": "https://registry.npmjs.org/aes-js/-/aes-js-3.1.2.tgz",
"integrity": "sha512-e5pEa2kBnBOgR4Y/p20pskXI74UEz7de8ZGVo58asOtvSVG5YAbJeELPZxOmt+Bnz3rX753YKhfIn4X4l1PPRQ==", "integrity": "sha512-e5pEa2kBnBOgR4Y/p20pskXI74UEz7de8ZGVo58asOtvSVG5YAbJeELPZxOmt+Bnz3rX753YKhfIn4X4l1PPRQ==",
"dev": true "dev": true
},
"ethereumjs-util": {
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/ethereumjs-util/-/ethereumjs-util-6.2.1.tgz",
"integrity": "sha512-W2Ktez4L01Vexijrm5EB6w7dg4n/TgpoYU4avuT5T3Vmnw/eCRtiBrJfQYS/DCSvDIOLn2k57GcHdeBcgVxAqw==",
"dev": true,
"requires": {
"@types/bn.js": "^4.11.3",
"bn.js": "^4.11.0",
"create-hash": "^1.1.2",
"elliptic": "^6.5.2",
"ethereum-cryptography": "^0.1.3",
"ethjs-util": "0.1.6",
"rlp": "^2.2.3"
}
} }
} }
}, },
......
...@@ -60,7 +60,9 @@ ...@@ -60,7 +60,9 @@
"eslint-plugin-node": "^10.0.0", "eslint-plugin-node": "^10.0.0",
"eslint-plugin-promise": "^4.2.1", "eslint-plugin-promise": "^4.2.1",
"eslint-plugin-standard": "^4.0.1", "eslint-plugin-standard": "^4.0.1",
"eth-sig-util": "^3.0.0",
"ethereumjs-util": "^7.0.7", "ethereumjs-util": "^7.0.7",
"ethereumjs-wallet": "^1.0.1",
"lodash.startcase": "^4.4.0", "lodash.startcase": "^4.4.0",
"lodash.zip": "^4.2.0", "lodash.zip": "^4.2.0",
"micromatch": "^4.0.2", "micromatch": "^4.0.2",
......
const ethSigUtil = require('eth-sig-util');
const Wallet = require('ethereumjs-wallet').default;
const EIP712 = artifacts.require('EIP712External');
const EIP712Domain = [
{ name: 'name', type: 'string' },
{ name: 'version', type: 'string' },
{ name: 'chainId', type: 'uint256' },
{ name: 'verifyingContract', type: 'address' },
];
async function domainSeparator (name, version, chainId, verifyingContract) {
return '0x' + ethSigUtil.TypedDataUtils.hashStruct(
'EIP712Domain',
{ name, version, chainId, verifyingContract },
{ EIP712Domain },
).toString('hex');
}
contract('EIP712', function (accounts) {
const [mailTo] = accounts;
const name = 'A Name';
const version = '1';
beforeEach('deploying', async function () {
this.eip712 = await EIP712.new(name, version);
// We get the chain id from the contract because Ganache (used for coverage) does not return the same chain id
// from within the EVM as from the JSON RPC interface.
// See https://github.com/trufflesuite/ganache-core/issues/515
this.chainId = await this.eip712.getChainId();
});
it('domain separator', async function () {
expect(
await this.eip712.domainSeparator(),
).to.equal(
await domainSeparator(name, version, this.chainId, this.eip712.address),
);
});
it('digest', async function () {
const chainId = this.chainId;
const verifyingContract = this.eip712.address;
const message = {
to: mailTo,
contents: 'very interesting',
};
const data = {
types: {
EIP712Domain,
Mail: [
{ name: 'to', type: 'address' },
{ name: 'contents', type: 'string' },
],
},
domain: { name, version, chainId, verifyingContract },
primaryType: 'Mail',
message,
};
const wallet = Wallet.generate();
const signature = ethSigUtil.signTypedMessage(wallet.getPrivateKey(), { data });
await this.eip712.verify(signature, wallet.getAddressString(), message.to, message.contents);
});
});
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment