|
| 1 | +// SPDX-License-Identifier: AGPL-3.0-only |
| 2 | +pragma solidity ^0.8.0; |
| 3 | + |
| 4 | +import {IERC712} from "./interfaces/IERC712.sol"; |
| 5 | + |
| 6 | +/// @dev The prefix used for EIP-712 signature. |
| 7 | +string constant ERC712_MSG_PREFIX = "\x19\x01"; |
| 8 | + |
| 9 | +/// @dev The domain typehash used for the EIP-712 signature. |
| 10 | +bytes32 constant ERC712_DOMAIN_TYPEHASH = |
| 11 | + keccak256("ERC712Domain(string name,uint256 chainId,address verifyingContract)"); |
| 12 | + |
| 13 | +/// @dev The highest valid value for s in an ECDSA signature pair (0 < s < secp256k1n ÷ 2 + 1). |
| 14 | +uint256 constant MAX_VALID_ECDSA_S = 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0; |
| 15 | + |
| 16 | +/// @notice ERC712 helpers. |
| 17 | +/// @dev Maintains cross-chain replay protection in the event of a fork. |
| 18 | +/// @dev Reference: https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/cryptography/ERC712.sol |
| 19 | +contract ERC712 is IERC712 { |
| 20 | + /// @dev The reference chainid. Used to check whether the chain forked and offer replay protection. |
| 21 | + uint256 private immutable _CACHED_CHAIN_ID; |
| 22 | + |
| 23 | + /// @dev The cached domain separator to use if chainid didnt change. |
| 24 | + bytes32 private immutable _CACHED_DOMAIN_SEPARATOR; |
| 25 | + |
| 26 | + /// @dev The name used for EIP-712 signature. |
| 27 | + bytes32 private immutable _NAMEHASH; |
| 28 | + |
| 29 | + /// @dev The nonce used inside by signers to offer signature replay protection. |
| 30 | + mapping(address => uint256) private _nonces; |
| 31 | + |
| 32 | + constructor(string memory name) { |
| 33 | + _NAMEHASH = keccak256(bytes(name)); |
| 34 | + |
| 35 | + _CACHED_CHAIN_ID = block.chainid; |
| 36 | + _CACHED_DOMAIN_SEPARATOR = _buildDomainSeparator(); |
| 37 | + } |
| 38 | + |
| 39 | + /* PUBLIC */ |
| 40 | + |
| 41 | + /// @inheritdoc IERC712 |
| 42 | + function DOMAIN_SEPARATOR() public view virtual returns (bytes32) { |
| 43 | + return block.chainid == _CACHED_CHAIN_ID ? _CACHED_DOMAIN_SEPARATOR : _buildDomainSeparator(); |
| 44 | + } |
| 45 | + |
| 46 | + /// @inheritdoc IERC712 |
| 47 | + function nonce(address user) public view virtual returns (uint256) { |
| 48 | + return _nonces[user]; |
| 49 | + } |
| 50 | + |
| 51 | + /* INTERNAL */ |
| 52 | + |
| 53 | + /// @dev Verifies a signature components against the provided data hash, nonce, deadline and signer. |
| 54 | + /// @param signature The signature to verify. |
| 55 | + /// @param dataHash The ERC712 message hash the signature should correspond to. |
| 56 | + /// @param signedNonce The nonce used along with the provided signature. Must not be an end-user input and must be proven to be signed by the signer. |
| 57 | + /// @param deadline The signature's maximum valid timestamp. Must not be an end-user input and must be proven to be signed by the signer. |
| 58 | + /// @param signer The expected signature's signer. |
| 59 | + function _verify( |
| 60 | + Signature calldata signature, |
| 61 | + bytes32 dataHash, |
| 62 | + uint256 signedNonce, |
| 63 | + uint256 deadline, |
| 64 | + address signer |
| 65 | + ) internal virtual { |
| 66 | + if (block.timestamp > deadline) revert SignatureExpired(); |
| 67 | + if (uint256(signature.s) > MAX_VALID_ECDSA_S) revert InvalidValueS(); |
| 68 | + // v ∈ {27, 28} (source: https://ethereum.github.io/yellowpaper/paper.pdf #308) |
| 69 | + if (signature.v != 27 && signature.v != 28) revert InvalidValueV(); |
| 70 | + |
| 71 | + bytes32 digest = _hashTypedData(dataHash); |
| 72 | + address recovered = ecrecover(digest, signature.v, signature.r, signature.s); |
| 73 | + |
| 74 | + if (recovered == address(0) || signer != recovered) revert InvalidSignature(recovered); |
| 75 | + |
| 76 | + uint256 usedNonce = _useNonce(signer); |
| 77 | + if (signedNonce != usedNonce) revert InvalidNonce(usedNonce); |
| 78 | + } |
| 79 | + |
| 80 | + /// @dev Increments and returns the nonce that should have been used in the corresponding signature. |
| 81 | + function _useNonce(address signer) internal virtual returns (uint256 usedNonce) { |
| 82 | + usedNonce = _nonces[signer]++; |
| 83 | + |
| 84 | + emit NonceUsed(msg.sender, signer, usedNonce); |
| 85 | + } |
| 86 | + |
| 87 | + /* PRIVATE */ |
| 88 | + |
| 89 | + /// @notice Builds a domain separator using the current chainId and contract address. |
| 90 | + function _buildDomainSeparator() private view returns (bytes32) { |
| 91 | + return keccak256(abi.encode(ERC712_DOMAIN_TYPEHASH, _NAMEHASH, block.chainid, address(this))); |
| 92 | + } |
| 93 | + |
| 94 | + /// @notice Creates an EIP-712 typed data hash |
| 95 | + function _hashTypedData(bytes32 dataHash) private view returns (bytes32) { |
| 96 | + return keccak256(abi.encodePacked(ERC712_MSG_PREFIX, DOMAIN_SEPARATOR(), dataHash)); |
| 97 | + } |
| 98 | +} |
0 commit comments