diff --git a/contracts/access/access_control/_AccessControl.sol b/contracts/access/access_control/_AccessControl.sol index eb891a4ca..ecbcc45fb 100644 --- a/contracts/access/access_control/_AccessControl.sol +++ b/contracts/access/access_control/_AccessControl.sol @@ -45,7 +45,7 @@ abstract contract _AccessControl is _IAccessControl, _Context { * @notice revert if sender does not have given role * @param role role to query */ - function _checkRole(bytes32 role) internal view virtual { + function _checkRole(bytes32 role) internal virtual { _checkRole(role, _msgSender()); } diff --git a/contracts/cryptography/ECDSA.sol b/contracts/cryptography/ECDSA.sol index 1b634daab..be6c2f807 100644 --- a/contracts/cryptography/ECDSA.sol +++ b/contracts/cryptography/ECDSA.sol @@ -13,7 +13,7 @@ library ECDSA { error ECDSA__InvalidV(); /** - * @notice recover signer of hashed message from signature + * @notice recover signer of hashed message from signature, reverting on failure * @param hash hashed data payload * @param signature signed data payload * @return signer recovered message signer @@ -22,35 +22,107 @@ library ECDSA { bytes32 hash, bytes memory signature ) internal pure returns (address signer) { - if (signature.length != 65) revert ECDSA__InvalidSignatureLength(); + function() pure errorFn; + + (signer, errorFn) = _tryRecover(hash, signature); + + if (signer == address(0)) errorFn(); + } + + /** + * @notice recover signer of hashed message from signature v, r, and s values, reverting on failure + * @param hash hashed data payload + * @param v signature "v" value + * @param r signature "r" value + * @param s signature "s" value + * @return signer recovered message signer + */ + function recover( + bytes32 hash, + uint8 v, + bytes32 r, + bytes32 s + ) internal pure returns (address signer) { + function() pure errorFn; + + (signer, errorFn) = _tryRecover(hash, v, r, s); + + if (signer == address(0)) errorFn(); + } + + /** + * @notice attempt to recover signer of hashed message from signature + * @param hash hashed data payload + * @param signature signed data payload + * @return signer recovered message signer (zero address on recovery failure) + */ + function tryRecover( + bytes32 hash, + bytes memory signature + ) internal pure returns (address signer) { + (signer, ) = _tryRecover(hash, signature); + } + + /** + * @notice attempt to recover signer of hashed message from signature v, r, and s values + * @param hash hashed data payload + * @param v signature "v" value + * @param r signature "r" value + * @param s signature "s" value + * @return signer recovered message signer (zero address on recovery failure) + */ + function tryRecover( + bytes32 hash, + uint8 v, + bytes32 r, + bytes32 s + ) internal pure returns (address signer) { + (signer, ) = _tryRecover(hash, v, r, s); + } + + /** + * @notice attempt to recover signer of hashed message from signature + * @param hash hashed data payload + * @param signature signed data payload + * @return signer recovered message signer (zero address on recovery failure) + * @return errorFn wrapper function around custom error revert + */ + function _tryRecover( + bytes32 hash, + bytes memory signature + ) private pure returns (address signer, function() pure errorFn) { + if (signature.length != 65) { + return (address(0), _revert_ECDSA__InvalidSignatureLength); + } bytes32 r; bytes32 s; uint8 v; - assembly { + assembly ('memory-safe') { r := mload(add(signature, 0x20)) s := mload(add(signature, 0x40)) v := byte(0, mload(add(signature, 0x60))) } - signer = recover(hash, v, r, s); + (signer, errorFn) = _tryRecover(hash, v, r, s); } /** - * @notice recover signer of hashed message from signature v, r, and s values + * @notice attempt to recover signer of hashed message from signature v, r, and s values * @param hash hashed data payload * @param v signature "v" value * @param r signature "r" value * @param s signature "s" value - * @return signer recovered message signer + * @return signer recovered message signer (zero address on recovery failure) + * @return errorFn wrapper function around custom error revert */ - function recover( + function _tryRecover( bytes32 hash, uint8 v, bytes32 r, bytes32 s - ) internal pure returns (address signer) { + ) private pure returns (address signer, function() pure errorFn) { // EIP-2 still allows signature malleability for ecrecover(). Remove this possibility and make the signature // unique. Appendix F in the Ethereum Yellow paper (https://ethereum.github.io/yellowpaper/paper.pdf), defines // the valid range for s in (281): 0 < s < secp256k1n ÷ 2 + 1, and for v in (282): v ∈ {27, 28}. Most @@ -63,24 +135,107 @@ library ECDSA { if ( uint256(s) > 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0 - ) revert ECDSA__InvalidS(); - if (v != 27 && v != 28) revert ECDSA__InvalidV(); + ) return (address(0), _revert_ECDSA__InvalidS); + + if (v != 27 && v != 28) return (address(0), _revert_ECDSA__InvalidV); // If the signature is valid (and not malleable), return the signer address signer = ecrecover(hash, v, r, s); - if (signer == address(0)) revert ECDSA__InvalidSignature(); + + if (signer == address(0)) + return (address(0), _revert_ECDSA__InvalidSignature); } /** * @notice generate an "Ethereum Signed Message" in the format returned by the eth_sign JSON-RPC method - * @param hash hashed data payload - * @return signedMessage signed message hash + * @param payloadHash hashed data payload + * @return recoverableHash hash to validate against signature via ECDSA function + */ + function toEthSignRecoverableHash( + bytes32 payloadHash + ) internal pure returns (bytes32 recoverableHash) { + assembly { + // assembly block equivalent to: + // + // recoverableHash = keccak256( + // abi.encodePacked( + // '\x19Ethereum Signed Message:\n32', + // payloadHash + // ) + // ); + + // load free memory pointer + let pointer := mload(64) + + mstore(pointer, '\x19Ethereum Signed Message:\n32') + mstore(add(pointer, 28), payloadHash) + + recoverableHash := keccak256(pointer, 60) + } + } + + /** + * @notice generate an EIP712 signable hash of typed structured data + * @param domainSeparator EIP712 domain separator + * @param structHash hash of underlying signed data generated through the EIP712 "hashStruct" process + * @return recoverableHash hash to validate against signature via ECDSA function + */ + function toEIP712RecoverableHash( + bytes32 domainSeparator, + bytes32 structHash + ) internal pure returns (bytes32 recoverableHash) { + assembly { + // assembly block equivalent to: + // + // recoverableHash = keccak256( + // abi.encodePacked( + // uint16(0x1901), + // domainSeparator, + // structHash + // ) + // ); + + // load free memory pointer + let pointer := mload(64) + + // this magic value is the EIP-191 signed data header, consisting of + // the hardcoded 0x19 and the one-byte version 0x01 + mstore( + pointer, + 0x1901000000000000000000000000000000000000000000000000000000000000 + ) + mstore(add(pointer, 2), domainSeparator) + mstore(add(pointer, 34), structHash) + + recoverableHash := keccak256(pointer, 66) + } + } + + /** + * @notice wrapper function for passing custom errors internally + */ + function _revert_ECDSA__InvalidS() private pure { + revert ECDSA__InvalidS(); + } + + /** + * @notice wrapper function for passing custom errors internally + */ + function _revert_ECDSA__InvalidSignature() private pure { + revert ECDSA__InvalidSignature(); + } + + /** + * @notice wrapper function for passing custom errors internally + */ + function _revert_ECDSA__InvalidSignatureLength() private pure { + revert ECDSA__InvalidSignatureLength(); + } + + /** + * @notice wrapper function for passing custom errors internally */ - function toEthSignedMessageHash( - bytes32 hash - ) internal pure returns (bytes32 signedMessage) { - signedMessage = keccak256( - abi.encodePacked('\x19Ethereum Signed Message:\n32', hash) - ); + function _revert_ECDSA__InvalidV() private pure { + revert ECDSA__InvalidV(); } } diff --git a/contracts/cryptography/EIP712.sol b/contracts/cryptography/EIP712.sol index 716949628..ac8447859 100644 --- a/contracts/cryptography/EIP712.sol +++ b/contracts/cryptography/EIP712.sol @@ -3,49 +3,757 @@ pragma solidity ^0.8.20; /** - * @title EIP-712 typed structured data hashing and signing + * @title Procedurally generated EIP-712 typed structured data hashing and signing library * @dev see https://eips.ethereum.org/EIPS/eip-712 - */ + **/ library EIP712 { - bytes32 internal constant EIP712_TYPE_HASH = + /** + * @dev ERC5267 fields value 00000 () + */ + bytes1 internal constant ERC5267_FIELDS_00000 = hex'00'; + + /** + * @dev ERC5267 fields value 00001 (name) + */ + bytes1 internal constant ERC5267_FIELDS_00001 = hex'01'; + + /** + * @dev ERC5267 fields value 00010 (version) + */ + bytes1 internal constant ERC5267_FIELDS_00010 = hex'02'; + + /** + * @dev ERC5267 fields value 00011 (name, version) + */ + bytes1 internal constant ERC5267_FIELDS_00011 = hex'03'; + + /** + * @dev ERC5267 fields value 00100 (chainId) + */ + bytes1 internal constant ERC5267_FIELDS_00100 = hex'04'; + + /** + * @dev ERC5267 fields value 00101 (name, chainId) + */ + bytes1 internal constant ERC5267_FIELDS_00101 = hex'05'; + + /** + * @dev ERC5267 fields value 00110 (version, chainId) + */ + bytes1 internal constant ERC5267_FIELDS_00110 = hex'06'; + + /** + * @dev ERC5267 fields value 00111 (name, version, chainId) + */ + bytes1 internal constant ERC5267_FIELDS_00111 = hex'07'; + + /** + * @dev ERC5267 fields value 01000 (verifyingContract) + */ + bytes1 internal constant ERC5267_FIELDS_01000 = hex'08'; + + /** + * @dev ERC5267 fields value 01001 (name, verifyingContract) + */ + bytes1 internal constant ERC5267_FIELDS_01001 = hex'09'; + + /** + * @dev ERC5267 fields value 01010 (version, verifyingContract) + */ + bytes1 internal constant ERC5267_FIELDS_01010 = hex'0a'; + + /** + * @dev ERC5267 fields value 01011 (name, version, verifyingContract) + */ + bytes1 internal constant ERC5267_FIELDS_01011 = hex'0b'; + + /** + * @dev ERC5267 fields value 01100 (chainId, verifyingContract) + */ + bytes1 internal constant ERC5267_FIELDS_01100 = hex'0c'; + + /** + * @dev ERC5267 fields value 01101 (name, chainId, verifyingContract) + */ + bytes1 internal constant ERC5267_FIELDS_01101 = hex'0d'; + + /** + * @dev ERC5267 fields value 01110 (version, chainId, verifyingContract) + */ + bytes1 internal constant ERC5267_FIELDS_01110 = hex'0e'; + + /** + * @dev ERC5267 fields value 01111 (name, version, chainId, verifyingContract) + */ + bytes1 internal constant ERC5267_FIELDS_01111 = hex'0f'; + + /** + * @dev ERC5267 fields value 10000 (salt) + */ + bytes1 internal constant ERC5267_FIELDS_10000 = hex'10'; + + /** + * @dev ERC5267 fields value 10001 (name, salt) + */ + bytes1 internal constant ERC5267_FIELDS_10001 = hex'11'; + + /** + * @dev ERC5267 fields value 10010 (version, salt) + */ + bytes1 internal constant ERC5267_FIELDS_10010 = hex'12'; + + /** + * @dev ERC5267 fields value 10011 (name, version, salt) + */ + bytes1 internal constant ERC5267_FIELDS_10011 = hex'13'; + + /** + * @dev ERC5267 fields value 10100 (chainId, salt) + */ + bytes1 internal constant ERC5267_FIELDS_10100 = hex'14'; + + /** + * @dev ERC5267 fields value 10101 (name, chainId, salt) + */ + bytes1 internal constant ERC5267_FIELDS_10101 = hex'15'; + + /** + * @dev ERC5267 fields value 10110 (version, chainId, salt) + */ + bytes1 internal constant ERC5267_FIELDS_10110 = hex'16'; + + /** + * @dev ERC5267 fields value 10111 (name, version, chainId, salt) + */ + bytes1 internal constant ERC5267_FIELDS_10111 = hex'17'; + + /** + * @dev ERC5267 fields value 11000 (verifyingContract, salt) + */ + bytes1 internal constant ERC5267_FIELDS_11000 = hex'18'; + + /** + * @dev ERC5267 fields value 11001 (name, verifyingContract, salt) + */ + bytes1 internal constant ERC5267_FIELDS_11001 = hex'19'; + + /** + * @dev ERC5267 fields value 11010 (version, verifyingContract, salt) + */ + bytes1 internal constant ERC5267_FIELDS_11010 = hex'1a'; + + /** + * @dev ERC5267 fields value 11011 (name, version, verifyingContract, salt) + */ + bytes1 internal constant ERC5267_FIELDS_11011 = hex'1b'; + + /** + * @dev ERC5267 fields value 11100 (chainId, verifyingContract, salt) + */ + bytes1 internal constant ERC5267_FIELDS_11100 = hex'1c'; + + /** + * @dev ERC5267 fields value 11101 (name, chainId, verifyingContract, salt) + */ + bytes1 internal constant ERC5267_FIELDS_11101 = hex'1d'; + + /** + * @dev ERC5267 fields value 11110 (version, chainId, verifyingContract, salt) + */ + bytes1 internal constant ERC5267_FIELDS_11110 = hex'1e'; + + /** + * @dev ERC5267 fields value 11111 (name, version, chainId, verifyingContract, salt) + */ + bytes1 internal constant ERC5267_FIELDS_11111 = hex'1f'; + + /** + * @dev EIP712Domain hash corresponding to ERC5267 fields value 00000 () + * @dev evaluates to 0x20bcc3f8105eea47d067386e42e60246e89393cd61c512edd1e87688890fb914 + */ + bytes32 internal constant EIP_712_DOMAIN_HASH_00000 = + keccak256('EIP712Domain()'); + + /** + * @dev EIP712Domain hash corresponding to ERC5267 fields value 00001 (name) + * @dev evaluates to 0xb2178a58fb1eefb359ecfdd57bb19c0bdd0f4e6eed8547f46600e500ed111af3 + */ + bytes32 internal constant EIP_712_DOMAIN_HASH_00001 = + keccak256('EIP712Domain(string name)'); + + /** + * @dev EIP712Domain hash corresponding to ERC5267 fields value 00010 (version) + * @dev evaluates to 0xbc027d3dfda1ddd4b660dee53f985a2f3b5ea30d0c0708b67f569aa0e361f302 + */ + bytes32 internal constant EIP_712_DOMAIN_HASH_00010 = + keccak256('EIP712Domain(string version)'); + + /** + * @dev EIP712Domain hash corresponding to ERC5267 fields value 00011 (name, version) + * @dev evaluates to 0xb03948446334eb9b2196d5eb166f69b9d49403eb4a12f36de8d3f9f3cb8e15c3 + */ + bytes32 internal constant EIP_712_DOMAIN_HASH_00011 = + keccak256('EIP712Domain(string name,string version)'); + + /** + * @dev EIP712Domain hash corresponding to ERC5267 fields value 00100 (chainId) + * @dev evaluates to 0xc49a8e302e3e5d6753b2bb3dbc3c28deba5e16e2572a92aef568063c963e3465 + */ + bytes32 internal constant EIP_712_DOMAIN_HASH_00100 = + keccak256('EIP712Domain(uint256 chainId)'); + + /** + * @dev EIP712Domain hash corresponding to ERC5267 fields value 00101 (name, chainId) + * @dev evaluates to 0xcc85e4a69ca54da41cc4383bb845cbd1e15ef8a13557a6bed09b8bea2a0d92ff + */ + bytes32 internal constant EIP_712_DOMAIN_HASH_00101 = + keccak256('EIP712Domain(string name,uint256 chainId)'); + + /** + * @dev EIP712Domain hash corresponding to ERC5267 fields value 00110 (version, chainId) + * @dev evaluates to 0x95166bc3984a70c39067c848833f87eaf6f7ff10e67fbe819f683dfcefb080e2 + */ + bytes32 internal constant EIP_712_DOMAIN_HASH_00110 = + keccak256('EIP712Domain(string version,uint256 chainId)'); + + /** + * @dev EIP712Domain hash corresponding to ERC5267 fields value 00111 (name, version, chainId) + * @dev evaluates to 0xc2f8787176b8ac6bf7215b4adcc1e069bf4ab82d9ab1df05a57a91d425935b6e + */ + bytes32 internal constant EIP_712_DOMAIN_HASH_00111 = + keccak256('EIP712Domain(string name,string version,uint256 chainId)'); + + /** + * @dev EIP712Domain hash corresponding to ERC5267 fields value 01000 (verifyingContract) + * @dev evaluates to 0x035aff83d86937d35b32e04f0ddc6ff469290eef2f1b692d8a815c89404d4749 + */ + bytes32 internal constant EIP_712_DOMAIN_HASH_01000 = + keccak256('EIP712Domain(address verifyingContract)'); + + /** + * @dev EIP712Domain hash corresponding to ERC5267 fields value 01001 (name, verifyingContract) + * @dev evaluates to 0xee552a4f357a6d8ecee15fed74927d873616e6da31fd672327acf0916acc174a + */ + bytes32 internal constant EIP_712_DOMAIN_HASH_01001 = + keccak256('EIP712Domain(string name,address verifyingContract)'); + + /** + * @dev EIP712Domain hash corresponding to ERC5267 fields value 01010 (version, verifyingContract) + * @dev evaluates to 0xe7cfb1b0c6cc1826928f8134ec4aaff653c53c61279b10ee7b6a1c59f3c76dd2 + */ + bytes32 internal constant EIP_712_DOMAIN_HASH_01010 = + keccak256('EIP712Domain(string version,address verifyingContract)'); + + /** + * @dev EIP712Domain hash corresponding to ERC5267 fields value 01011 (name, version, verifyingContract) + * @dev evaluates to 0x91ab3d17e3a50a9d89e63fd30b92be7f5336b03b287bb946787a83a9d62a2766 + */ + bytes32 internal constant EIP_712_DOMAIN_HASH_01011 = + keccak256( + 'EIP712Domain(string name,string version,address verifyingContract)' + ); + + /** + * @dev EIP712Domain hash corresponding to ERC5267 fields value 01100 (chainId, verifyingContract) + * @dev evaluates to 0x47e79534a245952e8b16893a336b85a3d9ea9fa8c573f3d803afb92a79469218 + */ + bytes32 internal constant EIP_712_DOMAIN_HASH_01100 = + keccak256('EIP712Domain(uint256 chainId,address verifyingContract)'); + + /** + * @dev EIP712Domain hash corresponding to ERC5267 fields value 01101 (name, chainId, verifyingContract) + * @dev evaluates to 0x8cad95687ba82c2ce50e74f7b754645e5117c3a5bec8151c0726d5857980a866 + */ + bytes32 internal constant EIP_712_DOMAIN_HASH_01101 = + keccak256( + 'EIP712Domain(string name,uint256 chainId,address verifyingContract)' + ); + + /** + * @dev EIP712Domain hash corresponding to ERC5267 fields value 01110 (version, chainId, verifyingContract) + * @dev evaluates to 0x2aef22f9d7df5f9d21c56d14029233f3fdaa91917727e1eb68e504d27072d6cd + */ + bytes32 internal constant EIP_712_DOMAIN_HASH_01110 = + keccak256( + 'EIP712Domain(string version,uint256 chainId,address verifyingContract)' + ); + + /** + * @dev EIP712Domain hash corresponding to ERC5267 fields value 01111 (name, version, chainId, verifyingContract) + * @dev evaluates to 0x8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400f + */ + bytes32 internal constant EIP_712_DOMAIN_HASH_01111 = keccak256( 'EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)' ); + /** + * @dev EIP712Domain hash corresponding to ERC5267 fields value 10000 (salt) + * @dev evaluates to 0xed46087c30783a9d27be533e9e6a1f834cec6daf2cfb016c9ab60d791039f983 + */ + bytes32 internal constant EIP_712_DOMAIN_HASH_10000 = + keccak256('EIP712Domain(bytes32 salt)'); + + /** + * @dev EIP712Domain hash corresponding to ERC5267 fields value 10001 (name, salt) + * @dev evaluates to 0xd1e3f5cf1a3ce7d7b6d652f790cb44165f3cdf0f3002d42f9f1d3e6a808e04b2 + */ + bytes32 internal constant EIP_712_DOMAIN_HASH_10001 = + keccak256('EIP712Domain(string name,bytes32 salt)'); + + /** + * @dev EIP712Domain hash corresponding to ERC5267 fields value 10010 (version, salt) + * @dev evaluates to 0x9f81c44ff68aaf167190e696336e29da4c6f2ad153d3de14f4f266b70f7cb8d0 + */ + bytes32 internal constant EIP_712_DOMAIN_HASH_10010 = + keccak256('EIP712Domain(string version,bytes32 salt)'); + + /** + * @dev EIP712Domain hash corresponding to ERC5267 fields value 10011 (name, version, salt) + * @dev evaluates to 0x599a80fcaa47b95e2323ab4d34d34e0cc9feda4b843edafcc30c7bdf60ea15bf + */ + bytes32 internal constant EIP_712_DOMAIN_HASH_10011 = + keccak256('EIP712Domain(string name,string version,bytes32 salt)'); + + /** + * @dev EIP712Domain hash corresponding to ERC5267 fields value 10100 (chainId, salt) + * @dev evaluates to 0x564d3aac36678e91beb9d11156d0a35dcedd025eea11212d2b4c45436e4a71ba + */ + bytes32 internal constant EIP_712_DOMAIN_HASH_10100 = + keccak256('EIP712Domain(uint256 chainId,bytes32 salt)'); + + /** + * @dev EIP712Domain hash corresponding to ERC5267 fields value 10101 (name, chainId, salt) + * @dev evaluates to 0x362651b35ace4088abd8ab4d0d426e15fe608272f8a9e51785f58e6621412710 + */ + bytes32 internal constant EIP_712_DOMAIN_HASH_10101 = + keccak256('EIP712Domain(string name,uint256 chainId,bytes32 salt)'); + + /** + * @dev EIP712Domain hash corresponding to ERC5267 fields value 10110 (version, chainId, salt) + * @dev evaluates to 0xc514ad1a6ba6faad885aeab076fe6d1d4f0040791a4e8130fb9c163991fcf25d + */ + bytes32 internal constant EIP_712_DOMAIN_HASH_10110 = + keccak256('EIP712Domain(string version,uint256 chainId,bytes32 salt)'); + + /** + * @dev EIP712Domain hash corresponding to ERC5267 fields value 10111 (name, version, chainId, salt) + * @dev evaluates to 0xa604fff5a27d5951f334ccda7abff3286a8af29caeeb196a6f2b40a1dce7612b + */ + bytes32 internal constant EIP_712_DOMAIN_HASH_10111 = + keccak256( + 'EIP712Domain(string name,string version,uint256 chainId,bytes32 salt)' + ); + + /** + * @dev EIP712Domain hash corresponding to ERC5267 fields value 11000 (verifyingContract, salt) + * @dev evaluates to 0x6268546d6d3d3a16ed8cfd22f4fe09a1d17f9af43838183ba533d41e284cf326 + */ + bytes32 internal constant EIP_712_DOMAIN_HASH_11000 = + keccak256('EIP712Domain(address verifyingContract,bytes32 salt)'); + + /** + * @dev EIP712Domain hash corresponding to ERC5267 fields value 11001 (name, verifyingContract, salt) + * @dev evaluates to 0xe00d3e753977caaa77095a287e170b7e5fae131a2e1b3af70a3835665255081f + */ + bytes32 internal constant EIP_712_DOMAIN_HASH_11001 = + keccak256( + 'EIP712Domain(string name,address verifyingContract,bytes32 salt)' + ); + + /** + * @dev EIP712Domain hash corresponding to ERC5267 fields value 11010 (version, verifyingContract, salt) + * @dev evaluates to 0x082f63b4da7f252440ff2be2cdc878665c088a48be3d79095973b727c93fbaec + */ + bytes32 internal constant EIP_712_DOMAIN_HASH_11010 = + keccak256( + 'EIP712Domain(string version,address verifyingContract,bytes32 salt)' + ); + + /** + * @dev EIP712Domain hash corresponding to ERC5267 fields value 11011 (name, version, verifyingContract, salt) + * @dev evaluates to 0x36c25de3e541d5d970f66e4210d728721220fff5c077cc6cd008b3a0c62adab7 + */ + bytes32 internal constant EIP_712_DOMAIN_HASH_11011 = + keccak256( + 'EIP712Domain(string name,string version,address verifyingContract,bytes32 salt)' + ); + + /** + * @dev EIP712Domain hash corresponding to ERC5267 fields value 11100 (chainId, verifyingContract, salt) + * @dev evaluates to 0x71062c282d40422f744945d587dbf4ecfd4f9cfad1d35d62c944373009d96162 + */ + bytes32 internal constant EIP_712_DOMAIN_HASH_11100 = + keccak256( + 'EIP712Domain(uint256 chainId,address verifyingContract,bytes32 salt)' + ); + + /** + * @dev EIP712Domain hash corresponding to ERC5267 fields value 11101 (name, chainId, verifyingContract, salt) + * @dev evaluates to 0xba3bbab4b37e6e20d315843d8bced25060386a557eeb60eefdbb4096f6ad6923 + */ + bytes32 internal constant EIP_712_DOMAIN_HASH_11101 = + keccak256( + 'EIP712Domain(string name,uint256 chainId,address verifyingContract,bytes32 salt)' + ); + + /** + * @dev EIP712Domain hash corresponding to ERC5267 fields value 11110 (version, chainId, verifyingContract, salt) + * @dev evaluates to 0xb90aaffa4b0fc25d6056f438f2c06198968eaf6723d182f5f928441117424b8e + */ + bytes32 internal constant EIP_712_DOMAIN_HASH_11110 = + keccak256( + 'EIP712Domain(string version,uint256 chainId,address verifyingContract,bytes32 salt)' + ); + + /** + * @dev EIP712Domain hash corresponding to ERC5267 fields value 11111 (name, version, chainId, verifyingContract, salt) + * @dev evaluates to 0xd87cd6ef79d4e2b95e15ce8abf732db51ec771f1ca2edccf22a46c729ac56472 + */ + bytes32 internal constant EIP_712_DOMAIN_HASH_11111 = + keccak256( + 'EIP712Domain(string name,string version,uint256 chainId,address verifyingContract,bytes32 salt)' + ); + + /** + * @notice calculate unique EIP-712 domain separator + * @return domainSeparator domain separator + */ + function calculateDomainSeparator_00000() + internal + pure + returns (bytes32 domainSeparator) + { + bytes32 typeHash = EIP_712_DOMAIN_HASH_00000; + + assembly { + let pointer := mload(64) + + mstore(pointer, typeHash) + + domainSeparator := keccak256(pointer, 32) + } + } + + /** + * @notice calculate unique EIP-712 domain separator + * @param nameHash hash of human-readable signing domain name + * @return domainSeparator domain separator + */ + function calculateDomainSeparator_00001( + bytes32 nameHash + ) internal pure returns (bytes32 domainSeparator) { + bytes32 typeHash = EIP_712_DOMAIN_HASH_00001; + + assembly { + let pointer := mload(64) + + mstore(pointer, typeHash) + mstore(add(pointer, 32), nameHash) + + domainSeparator := keccak256(pointer, 64) + } + } + + /** + * @notice calculate unique EIP-712 domain separator + * @param versionHash hash of signing domain version + * @return domainSeparator domain separator + */ + function calculateDomainSeparator_00010( + bytes32 versionHash + ) internal pure returns (bytes32 domainSeparator) { + bytes32 typeHash = EIP_712_DOMAIN_HASH_00010; + + assembly { + let pointer := mload(64) + + mstore(pointer, typeHash) + mstore(add(pointer, 32), versionHash) + + domainSeparator := keccak256(pointer, 64) + } + } + + /** + * @notice calculate unique EIP-712 domain separator + * @param nameHash hash of human-readable signing domain name + * @param versionHash hash of signing domain version + * @return domainSeparator domain separator + */ + function calculateDomainSeparator_00011( + bytes32 nameHash, + bytes32 versionHash + ) internal pure returns (bytes32 domainSeparator) { + bytes32 typeHash = EIP_712_DOMAIN_HASH_00011; + + assembly { + let pointer := mload(64) + + mstore(pointer, typeHash) + mstore(add(pointer, 32), nameHash) + mstore(add(pointer, 64), versionHash) + + domainSeparator := keccak256(pointer, 96) + } + } + + /** + * @notice calculate unique EIP-712 domain separator + * @return domainSeparator domain separator + */ + function calculateDomainSeparator_00100() + internal + view + returns (bytes32 domainSeparator) + { + bytes32 typeHash = EIP_712_DOMAIN_HASH_00100; + + assembly { + let pointer := mload(64) + + mstore(pointer, typeHash) + mstore(add(pointer, 32), chainid()) + + domainSeparator := keccak256(pointer, 64) + } + } + + /** + * @notice calculate unique EIP-712 domain separator + * @param nameHash hash of human-readable signing domain name + * @return domainSeparator domain separator + */ + function calculateDomainSeparator_00101( + bytes32 nameHash + ) internal view returns (bytes32 domainSeparator) { + bytes32 typeHash = EIP_712_DOMAIN_HASH_00101; + + assembly { + let pointer := mload(64) + + mstore(pointer, typeHash) + mstore(add(pointer, 32), nameHash) + mstore(add(pointer, 64), chainid()) + + domainSeparator := keccak256(pointer, 96) + } + } + + /** + * @notice calculate unique EIP-712 domain separator + * @param versionHash hash of signing domain version + * @return domainSeparator domain separator + */ + function calculateDomainSeparator_00110( + bytes32 versionHash + ) internal view returns (bytes32 domainSeparator) { + bytes32 typeHash = EIP_712_DOMAIN_HASH_00110; + + assembly { + let pointer := mload(64) + + mstore(pointer, typeHash) + mstore(add(pointer, 32), versionHash) + mstore(add(pointer, 64), chainid()) + + domainSeparator := keccak256(pointer, 96) + } + } + + /** + * @notice calculate unique EIP-712 domain separator + * @param nameHash hash of human-readable signing domain name + * @param versionHash hash of signing domain version + * @return domainSeparator domain separator + */ + function calculateDomainSeparator_00111( + bytes32 nameHash, + bytes32 versionHash + ) internal view returns (bytes32 domainSeparator) { + bytes32 typeHash = EIP_712_DOMAIN_HASH_00111; + + assembly { + let pointer := mload(64) + + mstore(pointer, typeHash) + mstore(add(pointer, 32), nameHash) + mstore(add(pointer, 64), versionHash) + mstore(add(pointer, 96), chainid()) + + domainSeparator := keccak256(pointer, 128) + } + } + + /** + * @notice calculate unique EIP-712 domain separator + * @return domainSeparator domain separator + */ + function calculateDomainSeparator_01000() + internal + view + returns (bytes32 domainSeparator) + { + bytes32 typeHash = EIP_712_DOMAIN_HASH_01000; + + assembly { + let pointer := mload(64) + + mstore(pointer, typeHash) + mstore(add(pointer, 32), address()) + + domainSeparator := keccak256(pointer, 64) + } + } + + /** + * @notice calculate unique EIP-712 domain separator + * @param nameHash hash of human-readable signing domain name + * @return domainSeparator domain separator + */ + function calculateDomainSeparator_01001( + bytes32 nameHash + ) internal view returns (bytes32 domainSeparator) { + bytes32 typeHash = EIP_712_DOMAIN_HASH_01001; + + assembly { + let pointer := mload(64) + + mstore(pointer, typeHash) + mstore(add(pointer, 32), nameHash) + mstore(add(pointer, 64), address()) + + domainSeparator := keccak256(pointer, 96) + } + } + + /** + * @notice calculate unique EIP-712 domain separator + * @param versionHash hash of signing domain version + * @return domainSeparator domain separator + */ + function calculateDomainSeparator_01010( + bytes32 versionHash + ) internal view returns (bytes32 domainSeparator) { + bytes32 typeHash = EIP_712_DOMAIN_HASH_01010; + + assembly { + let pointer := mload(64) + + mstore(pointer, typeHash) + mstore(add(pointer, 32), versionHash) + mstore(add(pointer, 64), address()) + + domainSeparator := keccak256(pointer, 96) + } + } + + /** + * @notice calculate unique EIP-712 domain separator + * @param nameHash hash of human-readable signing domain name + * @param versionHash hash of signing domain version + * @return domainSeparator domain separator + */ + function calculateDomainSeparator_01011( + bytes32 nameHash, + bytes32 versionHash + ) internal view returns (bytes32 domainSeparator) { + bytes32 typeHash = EIP_712_DOMAIN_HASH_01011; + + assembly { + let pointer := mload(64) + + mstore(pointer, typeHash) + mstore(add(pointer, 32), nameHash) + mstore(add(pointer, 64), versionHash) + mstore(add(pointer, 96), address()) + + domainSeparator := keccak256(pointer, 128) + } + } + + /** + * @notice calculate unique EIP-712 domain separator + * @return domainSeparator domain separator + */ + function calculateDomainSeparator_01100() + internal + view + returns (bytes32 domainSeparator) + { + bytes32 typeHash = EIP_712_DOMAIN_HASH_01100; + + assembly { + let pointer := mload(64) + + mstore(pointer, typeHash) + mstore(add(pointer, 32), chainid()) + mstore(add(pointer, 64), address()) + + domainSeparator := keccak256(pointer, 96) + } + } + + /** + * @notice calculate unique EIP-712 domain separator + * @param nameHash hash of human-readable signing domain name + * @return domainSeparator domain separator + */ + function calculateDomainSeparator_01101( + bytes32 nameHash + ) internal view returns (bytes32 domainSeparator) { + bytes32 typeHash = EIP_712_DOMAIN_HASH_01101; + + assembly { + let pointer := mload(64) + + mstore(pointer, typeHash) + mstore(add(pointer, 32), nameHash) + mstore(add(pointer, 64), chainid()) + mstore(add(pointer, 96), address()) + + domainSeparator := keccak256(pointer, 128) + } + } + + /** + * @notice calculate unique EIP-712 domain separator + * @param versionHash hash of signing domain version + * @return domainSeparator domain separator + */ + function calculateDomainSeparator_01110( + bytes32 versionHash + ) internal view returns (bytes32 domainSeparator) { + bytes32 typeHash = EIP_712_DOMAIN_HASH_01110; + + assembly { + let pointer := mload(64) + + mstore(pointer, typeHash) + mstore(add(pointer, 32), versionHash) + mstore(add(pointer, 64), chainid()) + mstore(add(pointer, 96), address()) + + domainSeparator := keccak256(pointer, 128) + } + } + /** * @notice calculate unique EIP-712 domain separator - * @dev name and version inputs are hashed as required by EIP-712 because they are of dynamic-length types - * @dev implementation of EIP712Domain struct type excludes the optional salt parameter * @param nameHash hash of human-readable signing domain name * @param versionHash hash of signing domain version * @return domainSeparator domain separator */ - function calculateDomainSeparator( + function calculateDomainSeparator_01111( bytes32 nameHash, bytes32 versionHash ) internal view returns (bytes32 domainSeparator) { - // execute EIP-712 hashStruct procedure + bytes32 typeHash = EIP_712_DOMAIN_HASH_01111; assembly { - // assembly block equavalent to: - // - // domainSeparator = keccak256( - // abi.encode( - // EIP712_TYPE_HASH, - // nameHash, - // versionHash, - // block.chainid, - // address(this) - // ) - // ); - - // load free memory pointer let pointer := mload(64) - mstore( - pointer, - 0x8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400f - ) + mstore(pointer, typeHash) mstore(add(pointer, 32), nameHash) mstore(add(pointer, 64), versionHash) mstore(add(pointer, 96), chainid()) @@ -54,4 +762,388 @@ library EIP712 { domainSeparator := keccak256(pointer, 160) } } + + /** + * @notice calculate unique EIP-712 domain separator + * @param salt disambiguating salt + * @return domainSeparator domain separator + */ + function calculateDomainSeparator_10000( + bytes32 salt + ) internal pure returns (bytes32 domainSeparator) { + bytes32 typeHash = EIP_712_DOMAIN_HASH_10000; + + assembly { + let pointer := mload(64) + + mstore(pointer, typeHash) + mstore(add(pointer, 32), salt) + + domainSeparator := keccak256(pointer, 64) + } + } + + /** + * @notice calculate unique EIP-712 domain separator + * @param nameHash hash of human-readable signing domain name + * @param salt disambiguating salt + * @return domainSeparator domain separator + */ + function calculateDomainSeparator_10001( + bytes32 nameHash, + bytes32 salt + ) internal pure returns (bytes32 domainSeparator) { + bytes32 typeHash = EIP_712_DOMAIN_HASH_10001; + + assembly { + let pointer := mload(64) + + mstore(pointer, typeHash) + mstore(add(pointer, 32), nameHash) + mstore(add(pointer, 64), salt) + + domainSeparator := keccak256(pointer, 96) + } + } + + /** + * @notice calculate unique EIP-712 domain separator + * @param versionHash hash of signing domain version + * @param salt disambiguating salt + * @return domainSeparator domain separator + */ + function calculateDomainSeparator_10010( + bytes32 versionHash, + bytes32 salt + ) internal pure returns (bytes32 domainSeparator) { + bytes32 typeHash = EIP_712_DOMAIN_HASH_10010; + + assembly { + let pointer := mload(64) + + mstore(pointer, typeHash) + mstore(add(pointer, 32), versionHash) + mstore(add(pointer, 64), salt) + + domainSeparator := keccak256(pointer, 96) + } + } + + /** + * @notice calculate unique EIP-712 domain separator + * @param nameHash hash of human-readable signing domain name + * @param versionHash hash of signing domain version + * @param salt disambiguating salt + * @return domainSeparator domain separator + */ + function calculateDomainSeparator_10011( + bytes32 nameHash, + bytes32 versionHash, + bytes32 salt + ) internal pure returns (bytes32 domainSeparator) { + bytes32 typeHash = EIP_712_DOMAIN_HASH_10011; + + assembly { + let pointer := mload(64) + + mstore(pointer, typeHash) + mstore(add(pointer, 32), nameHash) + mstore(add(pointer, 64), versionHash) + mstore(add(pointer, 96), salt) + + domainSeparator := keccak256(pointer, 128) + } + } + + /** + * @notice calculate unique EIP-712 domain separator + * @param salt disambiguating salt + * @return domainSeparator domain separator + */ + function calculateDomainSeparator_10100( + bytes32 salt + ) internal view returns (bytes32 domainSeparator) { + bytes32 typeHash = EIP_712_DOMAIN_HASH_10100; + + assembly { + let pointer := mload(64) + + mstore(pointer, typeHash) + mstore(add(pointer, 32), chainid()) + mstore(add(pointer, 64), salt) + + domainSeparator := keccak256(pointer, 96) + } + } + + /** + * @notice calculate unique EIP-712 domain separator + * @param nameHash hash of human-readable signing domain name + * @param salt disambiguating salt + * @return domainSeparator domain separator + */ + function calculateDomainSeparator_10101( + bytes32 nameHash, + bytes32 salt + ) internal view returns (bytes32 domainSeparator) { + bytes32 typeHash = EIP_712_DOMAIN_HASH_10101; + + assembly { + let pointer := mload(64) + + mstore(pointer, typeHash) + mstore(add(pointer, 32), nameHash) + mstore(add(pointer, 64), chainid()) + mstore(add(pointer, 96), salt) + + domainSeparator := keccak256(pointer, 128) + } + } + + /** + * @notice calculate unique EIP-712 domain separator + * @param versionHash hash of signing domain version + * @param salt disambiguating salt + * @return domainSeparator domain separator + */ + function calculateDomainSeparator_10110( + bytes32 versionHash, + bytes32 salt + ) internal view returns (bytes32 domainSeparator) { + bytes32 typeHash = EIP_712_DOMAIN_HASH_10110; + + assembly { + let pointer := mload(64) + + mstore(pointer, typeHash) + mstore(add(pointer, 32), versionHash) + mstore(add(pointer, 64), chainid()) + mstore(add(pointer, 96), salt) + + domainSeparator := keccak256(pointer, 128) + } + } + + /** + * @notice calculate unique EIP-712 domain separator + * @param nameHash hash of human-readable signing domain name + * @param versionHash hash of signing domain version + * @param salt disambiguating salt + * @return domainSeparator domain separator + */ + function calculateDomainSeparator_10111( + bytes32 nameHash, + bytes32 versionHash, + bytes32 salt + ) internal view returns (bytes32 domainSeparator) { + bytes32 typeHash = EIP_712_DOMAIN_HASH_10111; + + assembly { + let pointer := mload(64) + + mstore(pointer, typeHash) + mstore(add(pointer, 32), nameHash) + mstore(add(pointer, 64), versionHash) + mstore(add(pointer, 96), chainid()) + mstore(add(pointer, 128), salt) + + domainSeparator := keccak256(pointer, 160) + } + } + + /** + * @notice calculate unique EIP-712 domain separator + * @param salt disambiguating salt + * @return domainSeparator domain separator + */ + function calculateDomainSeparator_11000( + bytes32 salt + ) internal view returns (bytes32 domainSeparator) { + bytes32 typeHash = EIP_712_DOMAIN_HASH_11000; + + assembly { + let pointer := mload(64) + + mstore(pointer, typeHash) + mstore(add(pointer, 32), address()) + mstore(add(pointer, 64), salt) + + domainSeparator := keccak256(pointer, 96) + } + } + + /** + * @notice calculate unique EIP-712 domain separator + * @param nameHash hash of human-readable signing domain name + * @param salt disambiguating salt + * @return domainSeparator domain separator + */ + function calculateDomainSeparator_11001( + bytes32 nameHash, + bytes32 salt + ) internal view returns (bytes32 domainSeparator) { + bytes32 typeHash = EIP_712_DOMAIN_HASH_11001; + + assembly { + let pointer := mload(64) + + mstore(pointer, typeHash) + mstore(add(pointer, 32), nameHash) + mstore(add(pointer, 64), address()) + mstore(add(pointer, 96), salt) + + domainSeparator := keccak256(pointer, 128) + } + } + + /** + * @notice calculate unique EIP-712 domain separator + * @param versionHash hash of signing domain version + * @param salt disambiguating salt + * @return domainSeparator domain separator + */ + function calculateDomainSeparator_11010( + bytes32 versionHash, + bytes32 salt + ) internal view returns (bytes32 domainSeparator) { + bytes32 typeHash = EIP_712_DOMAIN_HASH_11010; + + assembly { + let pointer := mload(64) + + mstore(pointer, typeHash) + mstore(add(pointer, 32), versionHash) + mstore(add(pointer, 64), address()) + mstore(add(pointer, 96), salt) + + domainSeparator := keccak256(pointer, 128) + } + } + + /** + * @notice calculate unique EIP-712 domain separator + * @param nameHash hash of human-readable signing domain name + * @param versionHash hash of signing domain version + * @param salt disambiguating salt + * @return domainSeparator domain separator + */ + function calculateDomainSeparator_11011( + bytes32 nameHash, + bytes32 versionHash, + bytes32 salt + ) internal view returns (bytes32 domainSeparator) { + bytes32 typeHash = EIP_712_DOMAIN_HASH_11011; + + assembly { + let pointer := mload(64) + + mstore(pointer, typeHash) + mstore(add(pointer, 32), nameHash) + mstore(add(pointer, 64), versionHash) + mstore(add(pointer, 96), address()) + mstore(add(pointer, 128), salt) + + domainSeparator := keccak256(pointer, 160) + } + } + + /** + * @notice calculate unique EIP-712 domain separator + * @param salt disambiguating salt + * @return domainSeparator domain separator + */ + function calculateDomainSeparator_11100( + bytes32 salt + ) internal view returns (bytes32 domainSeparator) { + bytes32 typeHash = EIP_712_DOMAIN_HASH_11100; + + assembly { + let pointer := mload(64) + + mstore(pointer, typeHash) + mstore(add(pointer, 32), chainid()) + mstore(add(pointer, 64), address()) + mstore(add(pointer, 96), salt) + + domainSeparator := keccak256(pointer, 128) + } + } + + /** + * @notice calculate unique EIP-712 domain separator + * @param nameHash hash of human-readable signing domain name + * @param salt disambiguating salt + * @return domainSeparator domain separator + */ + function calculateDomainSeparator_11101( + bytes32 nameHash, + bytes32 salt + ) internal view returns (bytes32 domainSeparator) { + bytes32 typeHash = EIP_712_DOMAIN_HASH_11101; + + assembly { + let pointer := mload(64) + + mstore(pointer, typeHash) + mstore(add(pointer, 32), nameHash) + mstore(add(pointer, 64), chainid()) + mstore(add(pointer, 96), address()) + mstore(add(pointer, 128), salt) + + domainSeparator := keccak256(pointer, 160) + } + } + + /** + * @notice calculate unique EIP-712 domain separator + * @param versionHash hash of signing domain version + * @param salt disambiguating salt + * @return domainSeparator domain separator + */ + function calculateDomainSeparator_11110( + bytes32 versionHash, + bytes32 salt + ) internal view returns (bytes32 domainSeparator) { + bytes32 typeHash = EIP_712_DOMAIN_HASH_11110; + + assembly { + let pointer := mload(64) + + mstore(pointer, typeHash) + mstore(add(pointer, 32), versionHash) + mstore(add(pointer, 64), chainid()) + mstore(add(pointer, 96), address()) + mstore(add(pointer, 128), salt) + + domainSeparator := keccak256(pointer, 160) + } + } + + /** + * @notice calculate unique EIP-712 domain separator + * @param nameHash hash of human-readable signing domain name + * @param versionHash hash of signing domain version + * @param salt disambiguating salt + * @return domainSeparator domain separator + */ + function calculateDomainSeparator_11111( + bytes32 nameHash, + bytes32 versionHash, + bytes32 salt + ) internal view returns (bytes32 domainSeparator) { + bytes32 typeHash = EIP_712_DOMAIN_HASH_11111; + + assembly { + let pointer := mload(64) + + mstore(pointer, typeHash) + mstore(add(pointer, 32), nameHash) + mstore(add(pointer, 64), versionHash) + mstore(add(pointer, 96), chainid()) + mstore(add(pointer, 128), address()) + mstore(add(pointer, 160), salt) + + domainSeparator := keccak256(pointer, 192) + } + } } diff --git a/contracts/meta/ECDSAMetaTransactionContext.sol b/contracts/meta/ECDSAMetaTransactionContext.sol new file mode 100644 index 000000000..53ea08afc --- /dev/null +++ b/contracts/meta/ECDSAMetaTransactionContext.sol @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import { IERC5267 } from '../interfaces/IERC5267.sol'; +import { Context } from './Context.sol'; +import { _Context } from './_Context.sol'; +import { IECDSAMetaTransactionContext } from './IECDSAMetaTransactionContext.sol'; +import { _ECDSAMetaTransactionContext } from './_ECDSAMetaTransactionContext.sol'; + +abstract contract ECDSAMetaTransactionContext is + IECDSAMetaTransactionContext, + _ECDSAMetaTransactionContext, + Context +{ + /** + * @inheritdoc IERC5267 + */ + function eip712Domain() + external + view + returns ( + bytes1 fields, + string memory name, + string memory version, + uint256 chainId, + address verifyingContract, + bytes32 salt, + uint256[] memory extensions + ) + { + return _eip712Domain(); + } + + function _msgSender() + internal + override(_Context, _ECDSAMetaTransactionContext) + returns (address msgSender) + { + msgSender = super._msgSender(); + } + + function _msgData() + internal + override(_Context, _ECDSAMetaTransactionContext) + returns (bytes calldata msgData) + { + msgData = super._msgData(); + } + + function _calldataSuffixLength() + internal + view + override(_Context, _ECDSAMetaTransactionContext) + returns (uint256 length) + { + length = super._calldataSuffixLength(); + } +} diff --git a/contracts/meta/ForwardedMetaTransactionContext.sol b/contracts/meta/ForwardedMetaTransactionContext.sol index 7dc537add..4a2a09d4e 100644 --- a/contracts/meta/ForwardedMetaTransactionContext.sol +++ b/contracts/meta/ForwardedMetaTransactionContext.sol @@ -27,7 +27,6 @@ abstract contract ForwardedMetaTransactionContext is */ function _msgSender() internal - view virtual override(_Context, _ForwardedMetaTransactionContext) returns (address msgSender) @@ -40,7 +39,6 @@ abstract contract ForwardedMetaTransactionContext is */ function _msgData() internal - view virtual override(_Context, _ForwardedMetaTransactionContext) returns (bytes calldata msgData) diff --git a/contracts/meta/IECDSAMetaTransactionContext.sol b/contracts/meta/IECDSAMetaTransactionContext.sol new file mode 100644 index 000000000..97a769aa5 --- /dev/null +++ b/contracts/meta/IECDSAMetaTransactionContext.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import { IERC5267 } from '../interfaces/IERC5267.sol'; +import { IContext } from './IContext.sol'; +import { _IECDSAMetaTransactionContext } from './_IECDSAMetaTransactionContext.sol'; + +interface IECDSAMetaTransactionContext is + _IECDSAMetaTransactionContext, + IContext, + IERC5267 +{} diff --git a/contracts/meta/_Context.sol b/contracts/meta/_Context.sol index 303eab6c6..3b2f6669b 100644 --- a/contracts/meta/_Context.sol +++ b/contracts/meta/_Context.sol @@ -13,7 +13,7 @@ abstract contract _Context is _IContext { * @dev if no Context extension is in use, msg.sender is returned as-is * @return msgSender account contextualized as message sender */ - function _msgSender() internal view virtual returns (address msgSender) { + function _msgSender() internal virtual returns (address msgSender) { msgSender = msg.sender; } @@ -22,7 +22,7 @@ abstract contract _Context is _IContext { * @dev if no Context extension is in use, msg.data is returned as-is * @return msgData message data with suffix removed, if applicable */ - function _msgData() internal view virtual returns (bytes calldata msgData) { + function _msgData() internal virtual returns (bytes calldata msgData) { msgData = msg.data; } diff --git a/contracts/meta/_ECDSAMetaTransactionContext.sol b/contracts/meta/_ECDSAMetaTransactionContext.sol new file mode 100644 index 000000000..395556692 --- /dev/null +++ b/contracts/meta/_ECDSAMetaTransactionContext.sol @@ -0,0 +1,190 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import { ECDSA } from '../cryptography/ECDSA.sol'; +import { EIP712 } from '../cryptography/EIP712.sol'; +import { _Context } from './_Context.sol'; +import { _IECDSAMetaTransactionContext } from './_IECDSAMetaTransactionContext.sol'; + +abstract contract _ECDSAMetaTransactionContext is + _IECDSAMetaTransactionContext, + _Context +{ + using ECDSA for bytes32; + + bytes32 internal constant EIP_712_TYPE_HASH = + keccak256( + 'ECDSAMetaTransaction(bytes msgData,uint256 msgValue,uint256 nonce)' + ); + + bytes32 + internal constant ECDSA_META_TRANSACTION_CONTEXT_TRANSIENT_STORAGE_SLOT = + keccak256(abi.encode(uint256(EIP_712_TYPE_HASH) - 1)) & + ~bytes32(uint256(0xff)); + + function _eip712Domain() + internal + view + virtual + returns ( + bytes1 fields, + string memory name, + string memory version, + uint256 chainId, + address verifyingContract, + bytes32 salt, + uint256[] memory extensions + ) + { + return ( + EIP712.ERC5267_FIELDS_01100, + '', + '', + block.chainid, + address(this), + bytes32(0), + new uint256[](0) + ); + } + + /** + * @inheritdoc _Context + * @dev sender is read from the calldata context suffix + */ + function _msgSender() + internal + virtual + override + returns (address msgSender) + { + uint256 dataLength = msg.data.length; + uint256 suffixLength = _calldataSuffixLength(); + + if (dataLength >= suffixLength) { + // calldata is long enough that it might have a suffix + // check transient storage to see if sender has been derived already + + bytes32 slot = ECDSA_META_TRANSACTION_CONTEXT_TRANSIENT_STORAGE_SLOT; + + assembly { + msgSender := tload(slot) + } + + if (msgSender == address(0)) { + // no sender found in transient storage, so attempt to derive it from signature + + unchecked { + (msgSender, ) = _processCalldata(dataLength - suffixLength); + } + } + } else { + // calldata is too short for this to be a valid meta transaction + // return message sender as-is + msgSender = super._msgSender(); + } + } + + /** + * @inheritdoc _Context + */ + function _msgData() + internal + virtual + override + returns (bytes calldata msgData) + { + uint256 dataLength = msg.data.length; + uint256 suffixLength = _calldataSuffixLength(); + + if (dataLength >= suffixLength) { + // calldata is long enough that it might have a suffix + // check transient storage to see if msgData split index has been derived already + + uint256 split; + + bytes32 slot = ECDSA_META_TRANSACTION_CONTEXT_TRANSIENT_STORAGE_SLOT; + + assembly { + split := tload(add(slot, 1)) + } + + if (split == 0) { + // no msgData split index found in transient storage, so attempt to derive it from signature + + unchecked { + (, split) = _processCalldata(dataLength - suffixLength); + } + } + + msgData = msg.data[:split]; + } else { + // calldata is too short for this to be a valid meta transaction + // return message data as-is + msgData = super._msgData(); + } + } + + /** + * @inheritdoc _Context + * @dev this Context extension defines an address suffix with a length of 85 + */ + function _calldataSuffixLength() + internal + view + virtual + override + returns (uint256 length) + { + length = 85; + } + + function _processCalldata( + uint256 split + ) private returns (address msgSender, uint256 msgDataIndex) { + unchecked { + bytes calldata msgData = msg.data[:split]; + msgSender = address(bytes20(msg.data[split:split + 20])); + bytes calldata signature = msg.data[split + 20:]; + + // TODO: lookup and invalidate nonce + uint256 nonce = 1; + + // TODO: include msg.sender in hash to restrict forwarder? + bytes32 structHash = keccak256( + abi.encode( + EIP_712_TYPE_HASH, + keccak256(msgData), + msg.value, + nonce + ) + ); + + bytes32 recoverableHash = ECDSA.toEIP712RecoverableHash( + EIP712.calculateDomainSeparator_01100(), + structHash + ); + + // TODO: see what happens if split calldata v r s + address signer = recoverableHash.tryRecover(signature); + + if (signer == msgSender) { + msgDataIndex = split; + } else { + msgSender = super._msgSender(); + msgDataIndex = super._msgData().length; + } + } + + bytes32 slot = ECDSA_META_TRANSACTION_CONTEXT_TRANSIENT_STORAGE_SLOT; + + assembly { + // it is necessary to store metadata in transient storage because + // subsequent derivation will fail due to none invalidation + // TODO: suppress warning + // TODO: pack (msgDataIndex as bytes12) + tstore(slot, msgSender) + tstore(add(slot, 1), msgDataIndex) + } + } +} diff --git a/contracts/meta/_ForwardedMetaTransactionContext.sol b/contracts/meta/_ForwardedMetaTransactionContext.sol index 2b645f386..2fc8ec152 100644 --- a/contracts/meta/_ForwardedMetaTransactionContext.sol +++ b/contracts/meta/_ForwardedMetaTransactionContext.sol @@ -21,7 +21,6 @@ abstract contract _ForwardedMetaTransactionContext is */ function _msgSender() internal - view virtual override returns (address msgSender) @@ -48,7 +47,6 @@ abstract contract _ForwardedMetaTransactionContext is */ function _msgData() internal - view virtual override returns (bytes calldata msgData) diff --git a/contracts/meta/_IECDSAMetaTransactionContext.sol b/contracts/meta/_IECDSAMetaTransactionContext.sol new file mode 100644 index 000000000..a9abf4ea8 --- /dev/null +++ b/contracts/meta/_IECDSAMetaTransactionContext.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import { _IERC5267 } from '../interfaces/_IERC5267.sol'; +import { _IContext } from './_IContext.sol'; + +interface _IECDSAMetaTransactionContext is _IContext, _IERC5267 {} diff --git a/contracts/signature/contract_signer/ownable/_ContractSignerOwnable.sol b/contracts/signature/contract_signer/ownable/_ContractSignerOwnable.sol index c931e1edd..1f6b06f86 100644 --- a/contracts/signature/contract_signer/ownable/_ContractSignerOwnable.sol +++ b/contracts/signature/contract_signer/ownable/_ContractSignerOwnable.sol @@ -26,7 +26,7 @@ abstract contract _ContractSignerOwnable is bytes memory signature ) internal view virtual override returns (bytes4 magicValue) { return - hash.toEthSignedMessageHash().recover(signature) == _owner() + hash.toEthSignRecoverableHash().recover(signature) == _owner() ? MAGIC_VALUE : bytes4(0); } diff --git a/contracts/token/fungible/permit/_FungibleTokenPermit.sol b/contracts/token/fungible/permit/_FungibleTokenPermit.sol index f1638d3f6..5973c8835 100644 --- a/contracts/token/fungible/permit/_FungibleTokenPermit.sol +++ b/contracts/token/fungible/permit/_FungibleTokenPermit.sol @@ -35,7 +35,7 @@ abstract contract _FungibleTokenPermit is virtual returns (bytes32 domainSeparator) { - domainSeparator = EIP712.calculateDomainSeparator( + domainSeparator = EIP712.calculateDomainSeparator_01111( keccak256(bytes(_name())), keccak256(bytes(_version())) ); @@ -85,7 +85,7 @@ abstract contract _FungibleTokenPermit is ) { return ( - hex'0f', // 01111 + EIP712.ERC5267_FIELDS_01111, _name(), _version(), block.chainid, @@ -117,12 +117,12 @@ abstract contract _FungibleTokenPermit is if (deadline < block.timestamp) revert FungibleTokenPermit__ExpiredDeadline(); - // execute EIP-712 hashStruct procedure - uint256 nonce = ERC20Storage .layout(ERC20Storage.DEFAULT_STORAGE_SLOT) .erc2612Nonces[owner]++; + // execute EIP-712 hashStruct procedure + bytes32 structHash; assembly { @@ -155,41 +155,16 @@ abstract contract _FungibleTokenPermit is structHash := keccak256(pointer, 192) } - // recreate and hash data payload - - bytes32 domainSeparator = _DOMAIN_SEPARATOR(); - - bytes32 signedHash; - - assembly { - // assembly block equivalent to: - // - // signedHash = keccak256( - // abi.encodePacked( - // uint16(0x1901), - // domainSeparator, - // structHash - // ) - // ); - - // load free memory pointer - let pointer := mload(64) - - // this magic value is the EIP-191 signed data header, consisting of - // the hardcoded 0x19 and the one-byte version 0x01 - mstore( - pointer, - 0x1901000000000000000000000000000000000000000000000000000000000000 - ) - mstore(add(pointer, 2), domainSeparator) - mstore(add(pointer, 34), structHash) + // recreate final data payload hash which was signed by token owner - signedHash := keccak256(pointer, 66) - } + bytes32 recoverableHash = ECDSA.toEIP712RecoverableHash( + _DOMAIN_SEPARATOR(), + structHash + ); // validate signature - if (signedHash.recover(v, r, s) != owner) + if (recoverableHash.recover(v, r, s) != owner) revert FungibleTokenPermit__InvalidSignature(); _approve(owner, spender, amount); diff --git a/hardhat.config.ts b/hardhat.config.ts index 105c3e696..3301da640 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -1,4 +1,6 @@ +import './tasks/generate_eip_712'; import './tasks/rename_entity'; +import './tasks/scaffold_contract'; import '@nomicfoundation/hardhat-chai-matchers'; import '@nomicfoundation/hardhat-ethers'; import '@solidstate/hardhat-4byte-uploader'; @@ -16,6 +18,7 @@ const config: HardhatUserConfig = { solidity: { version: '0.8.28', settings: { + evmVersion: 'cancun', optimizer: { enabled: true, runs: 200, diff --git a/lib/erc20_permit.ts b/lib/erc20_permit.ts index 08c6ac6f6..d18defef8 100644 --- a/lib/erc20_permit.ts +++ b/lib/erc20_permit.ts @@ -1,6 +1,6 @@ import { SignerWithAddress } from '@nomicfoundation/hardhat-ethers/signers'; import { IERC2612, IERC5267 } from '@solidstate/typechain-types'; -import { Signature } from 'ethers'; +import { BytesLike, Signature } from 'ethers'; import { ethers } from 'hardhat'; interface Domain { @@ -99,9 +99,36 @@ const signERC2612Permit = async ( const signature = await owner.signTypedData(domain, types, values); - const permit = ethers.Signature.from(signature); + return ethers.Signature.from(signature); +}; + +// TODO: rename file or move to new file +const signECDSAMetaTransaction = async ( + instance: IERC5267, + signer: SignerWithAddress, + msgData: BytesLike, + msgValue: bigint, + nonce: bigint, +): Promise => { + const domain = await getDomain(instance); + + const types = { + ECDSAMetaTransaction: [ + { name: 'msgData', type: 'bytes' }, + { name: 'msgValue', type: 'uint256' }, + { name: 'nonce', type: 'uint256' }, + ], + }; + + const values = { + msgData, + msgValue, + nonce, + }; + + const signature = await signer.signTypedData(domain, types, values); - return permit; + return ethers.Signature.from(signature); }; -export { signERC2612Permit }; +export { signERC2612Permit, signECDSAMetaTransaction }; diff --git a/package.json b/package.json index 9b6b1c3a0..c82978b64 100644 --- a/package.json +++ b/package.json @@ -30,11 +30,13 @@ "@typechain/hardhat": "^9.1.0", "@types/chai": "^5.2.0", "@types/delete-empty": "^3.0.5", + "@types/ejs": "^3.1.5", "@types/git-diff": "^2.0.7", "@types/mocha": "^10.0.10", "@types/node": "^22.13.10", "chai": "^4.4.1", "delete-empty": "^3.0.0", + "ejs": "^3.1.10", "ethers": "^6.13.5", "git-diff": "^2.0.6", "hardhat": "^2.22.19", diff --git a/spec/token/fungible/FungibleTokenPermit.behavior.ts b/spec/token/fungible/FungibleTokenPermit.behavior.ts index 501962072..dcc8814f7 100644 --- a/spec/token/fungible/FungibleTokenPermit.behavior.ts +++ b/spec/token/fungible/FungibleTokenPermit.behavior.ts @@ -32,11 +32,15 @@ export function describeBehaviorOfFungibleTokenPermit( }); describe('#nonces(address)', () => { - it('todo'); + it('returns current nonce for given account', async () => { + expect( + await instance.nonces.staticCall(await holder.getAddress()), + ).to.eq(0n); + }); }); describe('#permit(address,address,uint256,uint256,uint8,bytes32,bytes32)', () => { - it('should increase allowance using permit', async () => { + it('increases allowance using permit', async () => { const timestamp = BigInt(await time.latest()); const amount = 2n; @@ -69,6 +73,36 @@ export function describeBehaviorOfFungibleTokenPermit( ); }); + it('increments nonce', async () => { + const nonce = await instance.nonces.staticCall( + await holder.getAddress(), + ); + + const permit = await signERC2612Permit( + instance, + holder, + spender, + 0n, + ethers.MaxUint256, + ); + + await instance + .connect(thirdParty) + .permit( + holder.address, + spender.address, + 0n, + ethers.MaxUint256, + permit.v, + permit.r, + permit.s, + ); + + expect( + await instance.nonces.staticCall(await holder.getAddress()), + ).to.eq(nonce + 1n); + }); + describe('reverts if', () => { it('deadline has passed', async () => { const timestamp = BigInt(await time.latest()); diff --git a/tasks/generate_eip_712.ts b/tasks/generate_eip_712.ts new file mode 100644 index 000000000..6461e15a6 --- /dev/null +++ b/tasks/generate_eip_712.ts @@ -0,0 +1,249 @@ +import ejs from 'ejs'; +import fs from 'fs'; +import { task } from 'hardhat/config'; +import path from 'path'; + +const name = 'EIP712'; +const filepath = 'cryptography'; + +const TEMPLATE_SOL = ` +pragma solidity ^0.8.20; + +/** + * @title Procedurally generated EIP-712 typed structured data hashing and signing library + * @dev see https://eips.ethereum.org/EIPS/eip-712 + **/ +library <%- name %> { + <% for (const el of fieldsConstantDefinitions) { %> + /** + * @dev ERC5267 fields value <%- el.bin %> (<%- el.fields.join(', ') %>) + */ + bytes1 internal constant <%- el.name %> = hex'<%- el.hex %>'; + <% } %> + + <% for (const el of constantDefinitions) { %> + /** + * @dev EIP712Domain hash corresponding to ERC5267 fields value <%- el.binString %> (<%- el.fields.join(', ') %>) + * @dev evaluates to <%- el.keccak %> + */ + bytes32 internal constant <%- el.name %> = keccak256('<%- el.domainString %>'); + <% } %> + + <% for (const el of functionDefinitions) { %> + /** + * @notice calculate unique EIP-712 domain separator + <%_ for (const f of el.fields.filter((f) => data[f].description)) { _%> + * @param <%- data[f].packedName ?? f %> <%- data[f].description %> + <%_ } _%> + * @return domainSeparator domain separator + */ + function <%- el.name %>(<%- el.parameters %>) internal <%- el.visibility %> returns (bytes32 domainSeparator) { + bytes32 typeHash = <%- el.hashName %>; + + assembly { + let pointer := mload(64) + + mstore(pointer, typeHash) + <%_ for (let i = 0; i < el.assemblyReferences.length; i++) { _%> + mstore(add(pointer, <%- (i + 1) * 32 %>), <%- el.assemblyReferences[i] %>) + <%_ } _%> + + domainSeparator := keccak256(pointer, <%- (el.fields.length + 1) * 32 %>) + } + } + <% } %> +} +`; + +const TEMPLATE_TS = ` +import { $<%- name %>, $<%- name %>__factory } from '@solidstate/typechain-types'; +import { expect } from 'chai'; +import { ethers } from 'hardhat'; + +describe('<%- name %>', () => { + let instance: $<%- name %>; + const nameHash = ethers.solidityPackedKeccak256(['string'], ['NAME']); + const versionHash = ethers.solidityPackedKeccak256(['string'], ['VERSION']); + let verifyingContract: string; + let chainId: string; + let salt = ethers.solidityPackedKeccak256(['string'], ['SALT']); + + before(async () => { + const [deployer] = await ethers.getSigners(); + instance = await new $<%- name %>__factory(deployer).deploy(); + verifyingContract = await instance.getAddress(); + chainId = await ethers.provider.send('eth_chainId'); + }); + + <% for (const el of fieldsConstantDefinitions) { %> + describe('#<%- el.name %>()', () => { + it('resolves to expected value', async () => { + expect(await instance.$<%- el.name %>.staticCall()).to.hexEqual('0x<%- el.hex %>'); + }); + }); + <% } %> + + <% for (const c of constantDefinitions) { %> + describe('#<%- c.name %>()', () => { + it('resolves to expected value', async () => { + expect(await instance.$<%- c.name %>.staticCall()).to.equal('<%- c.keccak %>'); + }); + }); + <% } %> + + <% for (const fn of functionDefinitions) { %> + describe('#<%- fn.name %>(<%- fn.sigTypes %>)', () => { + it('returns domain separator', async () => { + const typeHash = await instance.$<%- fn.hashName %>.staticCall(); + + const types: string[] = ['bytes32', <%- fn.hashTypes %>]; + const values: string[] = [typeHash, <%- fn.hashFields %>]; + + const domainSeparator = ethers.keccak256( + ethers.AbiCoder.defaultAbiCoder().encode(types,values)); + + expect(await instance.$<%- fn.name %>.staticCall(<%- fn.callNames %>)).to.equal(domainSeparator) + }); + }); + <% } %> +}); +`; + +task('generate-eip-712', `Generate ${name}`).setAction(async (args, hre) => { + const validFields = [ + 'name', + 'version', + 'chainId', + 'verifyingContract', + 'salt', + ] as const; + + const data: { + [field: string]: { + type: string; + packedType?: string; + packedName?: string; + description?: string; + assemblyReference?: string; + }; + } = { + name: { + packedName: 'nameHash', + type: 'string', + packedType: 'bytes32', + description: 'hash of human-readable signing domain name', + }, + version: { + packedName: 'versionHash', + type: 'string', + packedType: 'bytes32', + description: 'hash of signing domain version', + }, + chainId: { + type: 'uint256', + assemblyReference: 'chainid()', + }, + verifyingContract: { + type: 'address', + assemblyReference: 'address()', + }, + salt: { + type: 'bytes32', + description: 'disambiguating salt', + }, + }; + + const fieldsConstantDefinitions = []; + const constantDefinitions = []; + const functionDefinitions = []; + + for (let i = 0; i < 2 ** validFields.length; i++) { + const binString = i.toString(2).padStart(5, '0'); + const fields = validFields.filter((f, j) => i & (2 ** j)); + + const constantName = `EIP_712_DOMAIN_HASH_${binString}`; + const functionName = `calculateDomainSeparator_${binString}`; + + const domainString = `EIP712Domain(${fields.map((f) => `${data[f].type} ${f}`).join(',')})`; + const keccak = hre.ethers.solidityPackedKeccak256( + ['string'], + [domainString], + ); + + fieldsConstantDefinitions.push({ + fields, + name: `ERC5267_FIELDS_${binString}`, + bin: binString, + hex: i.toString(16).padStart(2, '0'), + }); + + constantDefinitions.push({ + fields, + name: constantName, + binString, + keccak, + domainString, + }); + + functionDefinitions.push({ + fields, + name: functionName, + hashName: constantName, + parameters: fields + .filter((f) => data[f].description) + .map( + (f) => + `${data[f].packedType ?? data[f].type} ${data[f].packedName ?? f}`, + ) + .join(', '), + visibility: + fields.includes('chainId') || fields.includes('verifyingContract') + ? 'view' + : 'pure', + assemblyReferences: fields.map( + (f) => data[f].assemblyReference ?? data[f].packedName ?? f, + ), + keccak, + sigTypes: fields + .filter((f) => data[f].description) + .map((f) => data[f].packedType ?? data[f].type), + hashTypes: fields + .map((f) => data[f].packedType ?? data[f].type) + .map((t) => `"${t}"`) + .join(', '), + hashFields: fields.map((f) => data[f].packedName ?? f).join(', '), + callNames: fields + .filter((f) => data[f].description) + .map((f) => data[f].packedName ?? f), + }); + } + + const templateData = { + data, + name, + fieldsConstantDefinitions, + constantDefinitions, + functionDefinitions, + }; + + const contractContent = ejs.render(TEMPLATE_SOL, templateData); + const testContent = ejs.render(TEMPLATE_TS, templateData); + + const contractPath = path.resolve( + hre.config.paths.sources, + filepath, + `${name}.sol`, + ); + const testPath = path.resolve(hre.config.paths.tests, filepath, `${name}.ts`); + + await fs.promises.mkdir(path.dirname(contractPath), { + recursive: true, + }); + + await fs.promises.mkdir(path.dirname(testPath), { + recursive: true, + }); + + await fs.promises.writeFile(contractPath, contractContent); + await fs.promises.writeFile(testPath, testContent); +}); diff --git a/tasks/scaffold_contract.ts b/tasks/scaffold_contract.ts new file mode 100644 index 000000000..4883bb509 --- /dev/null +++ b/tasks/scaffold_contract.ts @@ -0,0 +1,79 @@ +import fs from 'fs'; +import { task, types } from 'hardhat/config'; +import path from 'path'; + +task('scaffold-contract', 'Batch replace text in local filenames and contents') + .addPositionalParam( + 'name', + 'name of the external contract', + undefined, + types.string, + ) + .addPositionalParam( + 'path', + 'directory within sources directrory to create files', + undefined, + types.string, + ) + .addOptionalPositionalParam( + 'pragma', + 'solidity pragma version', + undefined, + types.string, + ) + .setAction(async (args, hre) => { + const fullpath = path.resolve(hre.config.paths.sources, args.path); + + await fs.promises.mkdir(fullpath, { recursive: true }); + + const { name } = args; + const pragma = args.pragma ?? '^0.8.20'; + + const externalContract = ` + pragma solidity ${pragma}; + + import { I${name} } from './I${name}.sol'; + import { _${name} } from './_${name}.sol'; + + abstract contract ${name} is I${name}, _${name} {} + `; + + const internalContract = ` + pragma solidity ${pragma}; + + import { _I${name} } from './_I${name}.sol'; + + abstract contract _${name} is _I${name} {} + `; + + const externalInterface = ` + pragma solidity ${pragma}; + + import { _I${name} } from './_I${name}.sol'; + + interface I${name} is _I${name} {} + `; + + const internalInterface = ` + pragma solidity ${pragma}; + + interface _I${name} {} + `; + + await fs.promises.writeFile( + path.resolve(fullpath, `${name}.sol`), + externalContract, + ); + await fs.promises.writeFile( + path.resolve(fullpath, `_${name}.sol`), + internalContract, + ); + await fs.promises.writeFile( + path.resolve(fullpath, `I${name}.sol`), + externalInterface, + ); + await fs.promises.writeFile( + path.resolve(fullpath, `_I${name}.sol`), + internalInterface, + ); + }); diff --git a/test/cryptography/ECDSA.ts b/test/cryptography/ECDSA.ts index 587304e05..459694bf7 100644 --- a/test/cryptography/ECDSA.ts +++ b/test/cryptography/ECDSA.ts @@ -162,16 +162,169 @@ describe('ECDSA', () => { }); }); - describe('#toEthSignedMessageHash(bytes32)', () => { - it('returns hash of signed message prefix and message', async () => { - const hash = ethers.keccak256(ethers.toUtf8Bytes('test')); + describe('#tryRecover(bytes32,bytes)', () => { + it('returns message signer', async () => { + const [signer] = await ethers.getSigners(); + + const data = { + types: ['uint256'], + values: [1], + nonce: 1, + address: await instance.getAddress(), + }; + + const hash = hashData(data); + const sig = await signData(signer, data); + + expect( + await instance['$tryRecover(bytes32,bytes)'].staticCall( + ethers.solidityPackedKeccak256( + ['string', 'bytes32'], + ['\x19Ethereum Signed Message:\n32', hash], + ), + sig, + ), + ).to.equal(signer.address); + }); + + it('returns zero address ifsignaure length is invalid', async () => { + expect( + await instance['$tryRecover(bytes32,bytes)'].staticCall( + ethers.randomBytes(32), + ethers.randomBytes(64), + ), + ).to.eq(ethers.ZeroAddress); + + expect( + await instance['$tryRecover(bytes32,bytes)'].staticCall( + ethers.randomBytes(32), + ethers.randomBytes(66), + ), + ).to.eq(ethers.ZeroAddress); + }); + }); + + describe('#tryRecover(bytes32,uint8,bytes32,bytes32)', () => { + it('returns message signer', async () => { + const [signer] = await ethers.getSigners(); + + const data = { + types: ['uint256'], + values: [1n], + nonce: 1n, + address: await instance.getAddress(), + }; + + const hash = hashData(data); + const sig = await signData(signer, data); + + const r = ethers.dataSlice(sig, 0, 32); + const s = ethers.dataSlice(sig, 32, 64); + const v = ethers.dataSlice(sig, 64, 65); expect( - await instance.$toEthSignedMessageHash.staticCall(hash), + await instance[ + '$tryRecover(bytes32,uint8,bytes32,bytes32)' + ].staticCall( + ethers.solidityPackedKeccak256( + ['string', 'bytes32'], + ['\x19Ethereum Signed Message:\n32', hash], + ), + v, + r, + s, + ), + ).to.equal(signer.address); + }); + + it('returns zero address if S is invalid', async () => { + const hash = ethers.randomBytes(32); + const v = 27; + const r = ethers.randomBytes(32); + + // s must be less than or equal to MAX_S_VALUE + + expect( + await instance[ + '$tryRecover(bytes32,uint8,bytes32,bytes32)' + ].staticCall(hash, v, r, ethers.toQuantity(BigInt(MAX_S_VALUE) + 1n)), + ).to.eq(ethers.ZeroAddress); + }); + + it('returns zero address if V is invalid', async () => { + const hash = ethers.randomBytes(32); + const r = ethers.randomBytes(32); + const s = MAX_S_VALUE; + + // V must be 27 or 28 + + for (let v = 0; v <= 26; v++) { + expect( + await instance[ + '$tryRecover(bytes32,uint8,bytes32,bytes32)' + ].staticCall(hash, v, r, s), + ).to.eq(ethers.ZeroAddress); + } + + for (let v = 29; v <= 255; v++) { + expect( + await instance[ + '$tryRecover(bytes32,uint8,bytes32,bytes32)' + ].staticCall(hash, v, r, s), + ).to.eq(ethers.ZeroAddress); + } + }); + + it('returns zero address if recovered signature is invalid', async () => { + const v = 27; + const s = MAX_S_VALUE; + + // hash and r generated randomly, known not to yield valid signer + + expect( + await instance[ + '$tryRecover(bytes32,uint8,bytes32,bytes32)' + ].staticCall( + '0xfb78d190a6ff9c55a28ae24c65cb006029ae15140557db9017a6474592d3fd59', + v, + '0xe1a6fa655db25741b29a03d2f8ec44fb5590d0a1ce91c789886b59e54c08f509', + s, + ), + ).to.eq(ethers.ZeroAddress); + }); + }); + + describe('#toEthSignRecoverableHash(bytes32)', () => { + it('returns recoverable hash of eth_sign message prefix and message', async () => { + const payloadHash = ethers.keccak256(ethers.toUtf8Bytes('message')); + + expect( + await instance.$toEthSignRecoverableHash.staticCall(payloadHash), ).to.equal( ethers.solidityPackedKeccak256( ['string', 'bytes32'], - ['\x19Ethereum Signed Message:\n32', hash], + ['\x19Ethereum Signed Message:\n32', payloadHash], + ), + ); + }); + }); + + describe('#toEIP712RecoverableHash(bytes32,bytes32)', () => { + it('returns recoverable hash of data in EIP712 format', async () => { + const domainSeparator = ethers.keccak256( + ethers.toUtf8Bytes('domain separator'), + ); + const structHash = ethers.keccak256(ethers.toUtf8Bytes('struct')); + + expect( + await instance.$toEIP712RecoverableHash.staticCall( + domainSeparator, + structHash, + ), + ).to.equal( + ethers.solidityPackedKeccak256( + ['bytes2', 'bytes32', 'bytes32'], + ['0x1901', domainSeparator, structHash], ), ); }); diff --git a/test/cryptography/EIP712.ts b/test/cryptography/EIP712.ts index 47ac153d1..7fd4e5d95 100644 --- a/test/cryptography/EIP712.ts +++ b/test/cryptography/EIP712.ts @@ -1,53 +1,1205 @@ -import { hashData, signData } from '@solidstate/library'; import { $EIP712, $EIP712__factory } from '@solidstate/typechain-types'; import { expect } from 'chai'; import { ethers } from 'hardhat'; describe('EIP712', () => { let instance: $EIP712; + const nameHash = ethers.solidityPackedKeccak256(['string'], ['NAME']); + const versionHash = ethers.solidityPackedKeccak256(['string'], ['VERSION']); + let verifyingContract: string; + let chainId: string; + let salt = ethers.solidityPackedKeccak256(['string'], ['SALT']); - beforeEach(async () => { + before(async () => { const [deployer] = await ethers.getSigners(); instance = await new $EIP712__factory(deployer).deploy(); + verifyingContract = await instance.getAddress(); + chainId = await ethers.provider.send('eth_chainId'); }); - describe('__internal', () => { - describe('#calculateDomainSeparator(bytes32,bytes32)', () => { - it('calculates EIP-712 domain separator', async () => { - const typeHash = ethers.solidityPackedKeccak256( - ['string'], - [ - 'EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)', - ], - ); - - const nameHash = ethers.solidityPackedKeccak256(['string'], ['name']); - const versionHash = ethers.solidityPackedKeccak256(['string'], ['1']); - - const chainId = await ethers.provider.send('eth_chainId'); - - // use keccak256 + defaultAbiCoder rather than solidityPackedKeccak256 because the latter forces packed encoding - - const domainSeparator = ethers.keccak256( - ethers.AbiCoder.defaultAbiCoder().encode( - ['bytes32', 'bytes32', 'bytes32', 'uint256', 'address'], - [ - typeHash, - nameHash, - versionHash, - chainId, - await instance.getAddress(), - ], - ), - ); - - expect( - await instance.$calculateDomainSeparator.staticCall( - nameHash, - versionHash, - ), - ).to.equal(domainSeparator); - }); + describe('#ERC5267_FIELDS_00000()', () => { + it('resolves to expected value', async () => { + expect(await instance.$ERC5267_FIELDS_00000.staticCall()).to.hexEqual( + '0x00', + ); + }); + }); + + describe('#ERC5267_FIELDS_00001()', () => { + it('resolves to expected value', async () => { + expect(await instance.$ERC5267_FIELDS_00001.staticCall()).to.hexEqual( + '0x01', + ); + }); + }); + + describe('#ERC5267_FIELDS_00010()', () => { + it('resolves to expected value', async () => { + expect(await instance.$ERC5267_FIELDS_00010.staticCall()).to.hexEqual( + '0x02', + ); + }); + }); + + describe('#ERC5267_FIELDS_00011()', () => { + it('resolves to expected value', async () => { + expect(await instance.$ERC5267_FIELDS_00011.staticCall()).to.hexEqual( + '0x03', + ); + }); + }); + + describe('#ERC5267_FIELDS_00100()', () => { + it('resolves to expected value', async () => { + expect(await instance.$ERC5267_FIELDS_00100.staticCall()).to.hexEqual( + '0x04', + ); + }); + }); + + describe('#ERC5267_FIELDS_00101()', () => { + it('resolves to expected value', async () => { + expect(await instance.$ERC5267_FIELDS_00101.staticCall()).to.hexEqual( + '0x05', + ); + }); + }); + + describe('#ERC5267_FIELDS_00110()', () => { + it('resolves to expected value', async () => { + expect(await instance.$ERC5267_FIELDS_00110.staticCall()).to.hexEqual( + '0x06', + ); + }); + }); + + describe('#ERC5267_FIELDS_00111()', () => { + it('resolves to expected value', async () => { + expect(await instance.$ERC5267_FIELDS_00111.staticCall()).to.hexEqual( + '0x07', + ); + }); + }); + + describe('#ERC5267_FIELDS_01000()', () => { + it('resolves to expected value', async () => { + expect(await instance.$ERC5267_FIELDS_01000.staticCall()).to.hexEqual( + '0x08', + ); + }); + }); + + describe('#ERC5267_FIELDS_01001()', () => { + it('resolves to expected value', async () => { + expect(await instance.$ERC5267_FIELDS_01001.staticCall()).to.hexEqual( + '0x09', + ); + }); + }); + + describe('#ERC5267_FIELDS_01010()', () => { + it('resolves to expected value', async () => { + expect(await instance.$ERC5267_FIELDS_01010.staticCall()).to.hexEqual( + '0x0a', + ); + }); + }); + + describe('#ERC5267_FIELDS_01011()', () => { + it('resolves to expected value', async () => { + expect(await instance.$ERC5267_FIELDS_01011.staticCall()).to.hexEqual( + '0x0b', + ); + }); + }); + + describe('#ERC5267_FIELDS_01100()', () => { + it('resolves to expected value', async () => { + expect(await instance.$ERC5267_FIELDS_01100.staticCall()).to.hexEqual( + '0x0c', + ); + }); + }); + + describe('#ERC5267_FIELDS_01101()', () => { + it('resolves to expected value', async () => { + expect(await instance.$ERC5267_FIELDS_01101.staticCall()).to.hexEqual( + '0x0d', + ); + }); + }); + + describe('#ERC5267_FIELDS_01110()', () => { + it('resolves to expected value', async () => { + expect(await instance.$ERC5267_FIELDS_01110.staticCall()).to.hexEqual( + '0x0e', + ); + }); + }); + + describe('#ERC5267_FIELDS_01111()', () => { + it('resolves to expected value', async () => { + expect(await instance.$ERC5267_FIELDS_01111.staticCall()).to.hexEqual( + '0x0f', + ); + }); + }); + + describe('#ERC5267_FIELDS_10000()', () => { + it('resolves to expected value', async () => { + expect(await instance.$ERC5267_FIELDS_10000.staticCall()).to.hexEqual( + '0x10', + ); + }); + }); + + describe('#ERC5267_FIELDS_10001()', () => { + it('resolves to expected value', async () => { + expect(await instance.$ERC5267_FIELDS_10001.staticCall()).to.hexEqual( + '0x11', + ); + }); + }); + + describe('#ERC5267_FIELDS_10010()', () => { + it('resolves to expected value', async () => { + expect(await instance.$ERC5267_FIELDS_10010.staticCall()).to.hexEqual( + '0x12', + ); + }); + }); + + describe('#ERC5267_FIELDS_10011()', () => { + it('resolves to expected value', async () => { + expect(await instance.$ERC5267_FIELDS_10011.staticCall()).to.hexEqual( + '0x13', + ); + }); + }); + + describe('#ERC5267_FIELDS_10100()', () => { + it('resolves to expected value', async () => { + expect(await instance.$ERC5267_FIELDS_10100.staticCall()).to.hexEqual( + '0x14', + ); + }); + }); + + describe('#ERC5267_FIELDS_10101()', () => { + it('resolves to expected value', async () => { + expect(await instance.$ERC5267_FIELDS_10101.staticCall()).to.hexEqual( + '0x15', + ); + }); + }); + + describe('#ERC5267_FIELDS_10110()', () => { + it('resolves to expected value', async () => { + expect(await instance.$ERC5267_FIELDS_10110.staticCall()).to.hexEqual( + '0x16', + ); + }); + }); + + describe('#ERC5267_FIELDS_10111()', () => { + it('resolves to expected value', async () => { + expect(await instance.$ERC5267_FIELDS_10111.staticCall()).to.hexEqual( + '0x17', + ); + }); + }); + + describe('#ERC5267_FIELDS_11000()', () => { + it('resolves to expected value', async () => { + expect(await instance.$ERC5267_FIELDS_11000.staticCall()).to.hexEqual( + '0x18', + ); + }); + }); + + describe('#ERC5267_FIELDS_11001()', () => { + it('resolves to expected value', async () => { + expect(await instance.$ERC5267_FIELDS_11001.staticCall()).to.hexEqual( + '0x19', + ); + }); + }); + + describe('#ERC5267_FIELDS_11010()', () => { + it('resolves to expected value', async () => { + expect(await instance.$ERC5267_FIELDS_11010.staticCall()).to.hexEqual( + '0x1a', + ); + }); + }); + + describe('#ERC5267_FIELDS_11011()', () => { + it('resolves to expected value', async () => { + expect(await instance.$ERC5267_FIELDS_11011.staticCall()).to.hexEqual( + '0x1b', + ); + }); + }); + + describe('#ERC5267_FIELDS_11100()', () => { + it('resolves to expected value', async () => { + expect(await instance.$ERC5267_FIELDS_11100.staticCall()).to.hexEqual( + '0x1c', + ); + }); + }); + + describe('#ERC5267_FIELDS_11101()', () => { + it('resolves to expected value', async () => { + expect(await instance.$ERC5267_FIELDS_11101.staticCall()).to.hexEqual( + '0x1d', + ); + }); + }); + + describe('#ERC5267_FIELDS_11110()', () => { + it('resolves to expected value', async () => { + expect(await instance.$ERC5267_FIELDS_11110.staticCall()).to.hexEqual( + '0x1e', + ); + }); + }); + + describe('#ERC5267_FIELDS_11111()', () => { + it('resolves to expected value', async () => { + expect(await instance.$ERC5267_FIELDS_11111.staticCall()).to.hexEqual( + '0x1f', + ); + }); + }); + + describe('#EIP_712_DOMAIN_HASH_00000()', () => { + it('resolves to expected value', async () => { + expect(await instance.$EIP_712_DOMAIN_HASH_00000.staticCall()).to.equal( + '0x20bcc3f8105eea47d067386e42e60246e89393cd61c512edd1e87688890fb914', + ); + }); + }); + + describe('#EIP_712_DOMAIN_HASH_00001()', () => { + it('resolves to expected value', async () => { + expect(await instance.$EIP_712_DOMAIN_HASH_00001.staticCall()).to.equal( + '0xb2178a58fb1eefb359ecfdd57bb19c0bdd0f4e6eed8547f46600e500ed111af3', + ); + }); + }); + + describe('#EIP_712_DOMAIN_HASH_00010()', () => { + it('resolves to expected value', async () => { + expect(await instance.$EIP_712_DOMAIN_HASH_00010.staticCall()).to.equal( + '0xbc027d3dfda1ddd4b660dee53f985a2f3b5ea30d0c0708b67f569aa0e361f302', + ); + }); + }); + + describe('#EIP_712_DOMAIN_HASH_00011()', () => { + it('resolves to expected value', async () => { + expect(await instance.$EIP_712_DOMAIN_HASH_00011.staticCall()).to.equal( + '0xb03948446334eb9b2196d5eb166f69b9d49403eb4a12f36de8d3f9f3cb8e15c3', + ); + }); + }); + + describe('#EIP_712_DOMAIN_HASH_00100()', () => { + it('resolves to expected value', async () => { + expect(await instance.$EIP_712_DOMAIN_HASH_00100.staticCall()).to.equal( + '0xc49a8e302e3e5d6753b2bb3dbc3c28deba5e16e2572a92aef568063c963e3465', + ); + }); + }); + + describe('#EIP_712_DOMAIN_HASH_00101()', () => { + it('resolves to expected value', async () => { + expect(await instance.$EIP_712_DOMAIN_HASH_00101.staticCall()).to.equal( + '0xcc85e4a69ca54da41cc4383bb845cbd1e15ef8a13557a6bed09b8bea2a0d92ff', + ); + }); + }); + + describe('#EIP_712_DOMAIN_HASH_00110()', () => { + it('resolves to expected value', async () => { + expect(await instance.$EIP_712_DOMAIN_HASH_00110.staticCall()).to.equal( + '0x95166bc3984a70c39067c848833f87eaf6f7ff10e67fbe819f683dfcefb080e2', + ); + }); + }); + + describe('#EIP_712_DOMAIN_HASH_00111()', () => { + it('resolves to expected value', async () => { + expect(await instance.$EIP_712_DOMAIN_HASH_00111.staticCall()).to.equal( + '0xc2f8787176b8ac6bf7215b4adcc1e069bf4ab82d9ab1df05a57a91d425935b6e', + ); + }); + }); + + describe('#EIP_712_DOMAIN_HASH_01000()', () => { + it('resolves to expected value', async () => { + expect(await instance.$EIP_712_DOMAIN_HASH_01000.staticCall()).to.equal( + '0x035aff83d86937d35b32e04f0ddc6ff469290eef2f1b692d8a815c89404d4749', + ); + }); + }); + + describe('#EIP_712_DOMAIN_HASH_01001()', () => { + it('resolves to expected value', async () => { + expect(await instance.$EIP_712_DOMAIN_HASH_01001.staticCall()).to.equal( + '0xee552a4f357a6d8ecee15fed74927d873616e6da31fd672327acf0916acc174a', + ); + }); + }); + + describe('#EIP_712_DOMAIN_HASH_01010()', () => { + it('resolves to expected value', async () => { + expect(await instance.$EIP_712_DOMAIN_HASH_01010.staticCall()).to.equal( + '0xe7cfb1b0c6cc1826928f8134ec4aaff653c53c61279b10ee7b6a1c59f3c76dd2', + ); + }); + }); + + describe('#EIP_712_DOMAIN_HASH_01011()', () => { + it('resolves to expected value', async () => { + expect(await instance.$EIP_712_DOMAIN_HASH_01011.staticCall()).to.equal( + '0x91ab3d17e3a50a9d89e63fd30b92be7f5336b03b287bb946787a83a9d62a2766', + ); + }); + }); + + describe('#EIP_712_DOMAIN_HASH_01100()', () => { + it('resolves to expected value', async () => { + expect(await instance.$EIP_712_DOMAIN_HASH_01100.staticCall()).to.equal( + '0x47e79534a245952e8b16893a336b85a3d9ea9fa8c573f3d803afb92a79469218', + ); + }); + }); + + describe('#EIP_712_DOMAIN_HASH_01101()', () => { + it('resolves to expected value', async () => { + expect(await instance.$EIP_712_DOMAIN_HASH_01101.staticCall()).to.equal( + '0x8cad95687ba82c2ce50e74f7b754645e5117c3a5bec8151c0726d5857980a866', + ); + }); + }); + + describe('#EIP_712_DOMAIN_HASH_01110()', () => { + it('resolves to expected value', async () => { + expect(await instance.$EIP_712_DOMAIN_HASH_01110.staticCall()).to.equal( + '0x2aef22f9d7df5f9d21c56d14029233f3fdaa91917727e1eb68e504d27072d6cd', + ); + }); + }); + + describe('#EIP_712_DOMAIN_HASH_01111()', () => { + it('resolves to expected value', async () => { + expect(await instance.$EIP_712_DOMAIN_HASH_01111.staticCall()).to.equal( + '0x8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400f', + ); + }); + }); + + describe('#EIP_712_DOMAIN_HASH_10000()', () => { + it('resolves to expected value', async () => { + expect(await instance.$EIP_712_DOMAIN_HASH_10000.staticCall()).to.equal( + '0xed46087c30783a9d27be533e9e6a1f834cec6daf2cfb016c9ab60d791039f983', + ); + }); + }); + + describe('#EIP_712_DOMAIN_HASH_10001()', () => { + it('resolves to expected value', async () => { + expect(await instance.$EIP_712_DOMAIN_HASH_10001.staticCall()).to.equal( + '0xd1e3f5cf1a3ce7d7b6d652f790cb44165f3cdf0f3002d42f9f1d3e6a808e04b2', + ); + }); + }); + + describe('#EIP_712_DOMAIN_HASH_10010()', () => { + it('resolves to expected value', async () => { + expect(await instance.$EIP_712_DOMAIN_HASH_10010.staticCall()).to.equal( + '0x9f81c44ff68aaf167190e696336e29da4c6f2ad153d3de14f4f266b70f7cb8d0', + ); + }); + }); + + describe('#EIP_712_DOMAIN_HASH_10011()', () => { + it('resolves to expected value', async () => { + expect(await instance.$EIP_712_DOMAIN_HASH_10011.staticCall()).to.equal( + '0x599a80fcaa47b95e2323ab4d34d34e0cc9feda4b843edafcc30c7bdf60ea15bf', + ); + }); + }); + + describe('#EIP_712_DOMAIN_HASH_10100()', () => { + it('resolves to expected value', async () => { + expect(await instance.$EIP_712_DOMAIN_HASH_10100.staticCall()).to.equal( + '0x564d3aac36678e91beb9d11156d0a35dcedd025eea11212d2b4c45436e4a71ba', + ); + }); + }); + + describe('#EIP_712_DOMAIN_HASH_10101()', () => { + it('resolves to expected value', async () => { + expect(await instance.$EIP_712_DOMAIN_HASH_10101.staticCall()).to.equal( + '0x362651b35ace4088abd8ab4d0d426e15fe608272f8a9e51785f58e6621412710', + ); + }); + }); + + describe('#EIP_712_DOMAIN_HASH_10110()', () => { + it('resolves to expected value', async () => { + expect(await instance.$EIP_712_DOMAIN_HASH_10110.staticCall()).to.equal( + '0xc514ad1a6ba6faad885aeab076fe6d1d4f0040791a4e8130fb9c163991fcf25d', + ); + }); + }); + + describe('#EIP_712_DOMAIN_HASH_10111()', () => { + it('resolves to expected value', async () => { + expect(await instance.$EIP_712_DOMAIN_HASH_10111.staticCall()).to.equal( + '0xa604fff5a27d5951f334ccda7abff3286a8af29caeeb196a6f2b40a1dce7612b', + ); + }); + }); + + describe('#EIP_712_DOMAIN_HASH_11000()', () => { + it('resolves to expected value', async () => { + expect(await instance.$EIP_712_DOMAIN_HASH_11000.staticCall()).to.equal( + '0x6268546d6d3d3a16ed8cfd22f4fe09a1d17f9af43838183ba533d41e284cf326', + ); + }); + }); + + describe('#EIP_712_DOMAIN_HASH_11001()', () => { + it('resolves to expected value', async () => { + expect(await instance.$EIP_712_DOMAIN_HASH_11001.staticCall()).to.equal( + '0xe00d3e753977caaa77095a287e170b7e5fae131a2e1b3af70a3835665255081f', + ); + }); + }); + + describe('#EIP_712_DOMAIN_HASH_11010()', () => { + it('resolves to expected value', async () => { + expect(await instance.$EIP_712_DOMAIN_HASH_11010.staticCall()).to.equal( + '0x082f63b4da7f252440ff2be2cdc878665c088a48be3d79095973b727c93fbaec', + ); + }); + }); + + describe('#EIP_712_DOMAIN_HASH_11011()', () => { + it('resolves to expected value', async () => { + expect(await instance.$EIP_712_DOMAIN_HASH_11011.staticCall()).to.equal( + '0x36c25de3e541d5d970f66e4210d728721220fff5c077cc6cd008b3a0c62adab7', + ); + }); + }); + + describe('#EIP_712_DOMAIN_HASH_11100()', () => { + it('resolves to expected value', async () => { + expect(await instance.$EIP_712_DOMAIN_HASH_11100.staticCall()).to.equal( + '0x71062c282d40422f744945d587dbf4ecfd4f9cfad1d35d62c944373009d96162', + ); + }); + }); + + describe('#EIP_712_DOMAIN_HASH_11101()', () => { + it('resolves to expected value', async () => { + expect(await instance.$EIP_712_DOMAIN_HASH_11101.staticCall()).to.equal( + '0xba3bbab4b37e6e20d315843d8bced25060386a557eeb60eefdbb4096f6ad6923', + ); + }); + }); + + describe('#EIP_712_DOMAIN_HASH_11110()', () => { + it('resolves to expected value', async () => { + expect(await instance.$EIP_712_DOMAIN_HASH_11110.staticCall()).to.equal( + '0xb90aaffa4b0fc25d6056f438f2c06198968eaf6723d182f5f928441117424b8e', + ); + }); + }); + + describe('#EIP_712_DOMAIN_HASH_11111()', () => { + it('resolves to expected value', async () => { + expect(await instance.$EIP_712_DOMAIN_HASH_11111.staticCall()).to.equal( + '0xd87cd6ef79d4e2b95e15ce8abf732db51ec771f1ca2edccf22a46c729ac56472', + ); + }); + }); + + describe('#calculateDomainSeparator_00000()', () => { + it('returns domain separator', async () => { + const typeHash = await instance.$EIP_712_DOMAIN_HASH_00000.staticCall(); + + const types: string[] = ['bytes32']; + const values: string[] = [typeHash]; + + const domainSeparator = ethers.keccak256( + ethers.AbiCoder.defaultAbiCoder().encode(types, values), + ); + + expect( + await instance.$calculateDomainSeparator_00000.staticCall(), + ).to.equal(domainSeparator); + }); + }); + + describe('#calculateDomainSeparator_00001(bytes32)', () => { + it('returns domain separator', async () => { + const typeHash = await instance.$EIP_712_DOMAIN_HASH_00001.staticCall(); + + const types: string[] = ['bytes32', 'bytes32']; + const values: string[] = [typeHash, nameHash]; + + const domainSeparator = ethers.keccak256( + ethers.AbiCoder.defaultAbiCoder().encode(types, values), + ); + + expect( + await instance.$calculateDomainSeparator_00001.staticCall(nameHash), + ).to.equal(domainSeparator); + }); + }); + + describe('#calculateDomainSeparator_00010(bytes32)', () => { + it('returns domain separator', async () => { + const typeHash = await instance.$EIP_712_DOMAIN_HASH_00010.staticCall(); + + const types: string[] = ['bytes32', 'bytes32']; + const values: string[] = [typeHash, versionHash]; + + const domainSeparator = ethers.keccak256( + ethers.AbiCoder.defaultAbiCoder().encode(types, values), + ); + + expect( + await instance.$calculateDomainSeparator_00010.staticCall(versionHash), + ).to.equal(domainSeparator); + }); + }); + + describe('#calculateDomainSeparator_00011(bytes32,bytes32)', () => { + it('returns domain separator', async () => { + const typeHash = await instance.$EIP_712_DOMAIN_HASH_00011.staticCall(); + + const types: string[] = ['bytes32', 'bytes32', 'bytes32']; + const values: string[] = [typeHash, nameHash, versionHash]; + + const domainSeparator = ethers.keccak256( + ethers.AbiCoder.defaultAbiCoder().encode(types, values), + ); + + expect( + await instance.$calculateDomainSeparator_00011.staticCall( + nameHash, + versionHash, + ), + ).to.equal(domainSeparator); + }); + }); + + describe('#calculateDomainSeparator_00100()', () => { + it('returns domain separator', async () => { + const typeHash = await instance.$EIP_712_DOMAIN_HASH_00100.staticCall(); + + const types: string[] = ['bytes32', 'uint256']; + const values: string[] = [typeHash, chainId]; + + const domainSeparator = ethers.keccak256( + ethers.AbiCoder.defaultAbiCoder().encode(types, values), + ); + + expect( + await instance.$calculateDomainSeparator_00100.staticCall(), + ).to.equal(domainSeparator); + }); + }); + + describe('#calculateDomainSeparator_00101(bytes32)', () => { + it('returns domain separator', async () => { + const typeHash = await instance.$EIP_712_DOMAIN_HASH_00101.staticCall(); + + const types: string[] = ['bytes32', 'bytes32', 'uint256']; + const values: string[] = [typeHash, nameHash, chainId]; + + const domainSeparator = ethers.keccak256( + ethers.AbiCoder.defaultAbiCoder().encode(types, values), + ); + + expect( + await instance.$calculateDomainSeparator_00101.staticCall(nameHash), + ).to.equal(domainSeparator); + }); + }); + + describe('#calculateDomainSeparator_00110(bytes32)', () => { + it('returns domain separator', async () => { + const typeHash = await instance.$EIP_712_DOMAIN_HASH_00110.staticCall(); + + const types: string[] = ['bytes32', 'bytes32', 'uint256']; + const values: string[] = [typeHash, versionHash, chainId]; + + const domainSeparator = ethers.keccak256( + ethers.AbiCoder.defaultAbiCoder().encode(types, values), + ); + + expect( + await instance.$calculateDomainSeparator_00110.staticCall(versionHash), + ).to.equal(domainSeparator); + }); + }); + + describe('#calculateDomainSeparator_00111(bytes32,bytes32)', () => { + it('returns domain separator', async () => { + const typeHash = await instance.$EIP_712_DOMAIN_HASH_00111.staticCall(); + + const types: string[] = ['bytes32', 'bytes32', 'bytes32', 'uint256']; + const values: string[] = [typeHash, nameHash, versionHash, chainId]; + + const domainSeparator = ethers.keccak256( + ethers.AbiCoder.defaultAbiCoder().encode(types, values), + ); + + expect( + await instance.$calculateDomainSeparator_00111.staticCall( + nameHash, + versionHash, + ), + ).to.equal(domainSeparator); + }); + }); + + describe('#calculateDomainSeparator_01000()', () => { + it('returns domain separator', async () => { + const typeHash = await instance.$EIP_712_DOMAIN_HASH_01000.staticCall(); + + const types: string[] = ['bytes32', 'address']; + const values: string[] = [typeHash, verifyingContract]; + + const domainSeparator = ethers.keccak256( + ethers.AbiCoder.defaultAbiCoder().encode(types, values), + ); + + expect( + await instance.$calculateDomainSeparator_01000.staticCall(), + ).to.equal(domainSeparator); + }); + }); + + describe('#calculateDomainSeparator_01001(bytes32)', () => { + it('returns domain separator', async () => { + const typeHash = await instance.$EIP_712_DOMAIN_HASH_01001.staticCall(); + + const types: string[] = ['bytes32', 'bytes32', 'address']; + const values: string[] = [typeHash, nameHash, verifyingContract]; + + const domainSeparator = ethers.keccak256( + ethers.AbiCoder.defaultAbiCoder().encode(types, values), + ); + + expect( + await instance.$calculateDomainSeparator_01001.staticCall(nameHash), + ).to.equal(domainSeparator); + }); + }); + + describe('#calculateDomainSeparator_01010(bytes32)', () => { + it('returns domain separator', async () => { + const typeHash = await instance.$EIP_712_DOMAIN_HASH_01010.staticCall(); + + const types: string[] = ['bytes32', 'bytes32', 'address']; + const values: string[] = [typeHash, versionHash, verifyingContract]; + + const domainSeparator = ethers.keccak256( + ethers.AbiCoder.defaultAbiCoder().encode(types, values), + ); + + expect( + await instance.$calculateDomainSeparator_01010.staticCall(versionHash), + ).to.equal(domainSeparator); + }); + }); + + describe('#calculateDomainSeparator_01011(bytes32,bytes32)', () => { + it('returns domain separator', async () => { + const typeHash = await instance.$EIP_712_DOMAIN_HASH_01011.staticCall(); + + const types: string[] = ['bytes32', 'bytes32', 'bytes32', 'address']; + const values: string[] = [ + typeHash, + nameHash, + versionHash, + verifyingContract, + ]; + + const domainSeparator = ethers.keccak256( + ethers.AbiCoder.defaultAbiCoder().encode(types, values), + ); + + expect( + await instance.$calculateDomainSeparator_01011.staticCall( + nameHash, + versionHash, + ), + ).to.equal(domainSeparator); + }); + }); + + describe('#calculateDomainSeparator_01100()', () => { + it('returns domain separator', async () => { + const typeHash = await instance.$EIP_712_DOMAIN_HASH_01100.staticCall(); + + const types: string[] = ['bytes32', 'uint256', 'address']; + const values: string[] = [typeHash, chainId, verifyingContract]; + + const domainSeparator = ethers.keccak256( + ethers.AbiCoder.defaultAbiCoder().encode(types, values), + ); + + expect( + await instance.$calculateDomainSeparator_01100.staticCall(), + ).to.equal(domainSeparator); + }); + }); + + describe('#calculateDomainSeparator_01101(bytes32)', () => { + it('returns domain separator', async () => { + const typeHash = await instance.$EIP_712_DOMAIN_HASH_01101.staticCall(); + + const types: string[] = ['bytes32', 'bytes32', 'uint256', 'address']; + const values: string[] = [typeHash, nameHash, chainId, verifyingContract]; + + const domainSeparator = ethers.keccak256( + ethers.AbiCoder.defaultAbiCoder().encode(types, values), + ); + + expect( + await instance.$calculateDomainSeparator_01101.staticCall(nameHash), + ).to.equal(domainSeparator); + }); + }); + + describe('#calculateDomainSeparator_01110(bytes32)', () => { + it('returns domain separator', async () => { + const typeHash = await instance.$EIP_712_DOMAIN_HASH_01110.staticCall(); + + const types: string[] = ['bytes32', 'bytes32', 'uint256', 'address']; + const values: string[] = [ + typeHash, + versionHash, + chainId, + verifyingContract, + ]; + + const domainSeparator = ethers.keccak256( + ethers.AbiCoder.defaultAbiCoder().encode(types, values), + ); + + expect( + await instance.$calculateDomainSeparator_01110.staticCall(versionHash), + ).to.equal(domainSeparator); + }); + }); + + describe('#calculateDomainSeparator_01111(bytes32,bytes32)', () => { + it('returns domain separator', async () => { + const typeHash = await instance.$EIP_712_DOMAIN_HASH_01111.staticCall(); + + const types: string[] = [ + 'bytes32', + 'bytes32', + 'bytes32', + 'uint256', + 'address', + ]; + const values: string[] = [ + typeHash, + nameHash, + versionHash, + chainId, + verifyingContract, + ]; + + const domainSeparator = ethers.keccak256( + ethers.AbiCoder.defaultAbiCoder().encode(types, values), + ); + + expect( + await instance.$calculateDomainSeparator_01111.staticCall( + nameHash, + versionHash, + ), + ).to.equal(domainSeparator); + }); + }); + + describe('#calculateDomainSeparator_10000(bytes32)', () => { + it('returns domain separator', async () => { + const typeHash = await instance.$EIP_712_DOMAIN_HASH_10000.staticCall(); + + const types: string[] = ['bytes32', 'bytes32']; + const values: string[] = [typeHash, salt]; + + const domainSeparator = ethers.keccak256( + ethers.AbiCoder.defaultAbiCoder().encode(types, values), + ); + + expect( + await instance.$calculateDomainSeparator_10000.staticCall(salt), + ).to.equal(domainSeparator); + }); + }); + + describe('#calculateDomainSeparator_10001(bytes32,bytes32)', () => { + it('returns domain separator', async () => { + const typeHash = await instance.$EIP_712_DOMAIN_HASH_10001.staticCall(); + + const types: string[] = ['bytes32', 'bytes32', 'bytes32']; + const values: string[] = [typeHash, nameHash, salt]; + + const domainSeparator = ethers.keccak256( + ethers.AbiCoder.defaultAbiCoder().encode(types, values), + ); + + expect( + await instance.$calculateDomainSeparator_10001.staticCall( + nameHash, + salt, + ), + ).to.equal(domainSeparator); + }); + }); + + describe('#calculateDomainSeparator_10010(bytes32,bytes32)', () => { + it('returns domain separator', async () => { + const typeHash = await instance.$EIP_712_DOMAIN_HASH_10010.staticCall(); + + const types: string[] = ['bytes32', 'bytes32', 'bytes32']; + const values: string[] = [typeHash, versionHash, salt]; + + const domainSeparator = ethers.keccak256( + ethers.AbiCoder.defaultAbiCoder().encode(types, values), + ); + + expect( + await instance.$calculateDomainSeparator_10010.staticCall( + versionHash, + salt, + ), + ).to.equal(domainSeparator); + }); + }); + + describe('#calculateDomainSeparator_10011(bytes32,bytes32,bytes32)', () => { + it('returns domain separator', async () => { + const typeHash = await instance.$EIP_712_DOMAIN_HASH_10011.staticCall(); + + const types: string[] = ['bytes32', 'bytes32', 'bytes32', 'bytes32']; + const values: string[] = [typeHash, nameHash, versionHash, salt]; + + const domainSeparator = ethers.keccak256( + ethers.AbiCoder.defaultAbiCoder().encode(types, values), + ); + + expect( + await instance.$calculateDomainSeparator_10011.staticCall( + nameHash, + versionHash, + salt, + ), + ).to.equal(domainSeparator); + }); + }); + + describe('#calculateDomainSeparator_10100(bytes32)', () => { + it('returns domain separator', async () => { + const typeHash = await instance.$EIP_712_DOMAIN_HASH_10100.staticCall(); + + const types: string[] = ['bytes32', 'uint256', 'bytes32']; + const values: string[] = [typeHash, chainId, salt]; + + const domainSeparator = ethers.keccak256( + ethers.AbiCoder.defaultAbiCoder().encode(types, values), + ); + + expect( + await instance.$calculateDomainSeparator_10100.staticCall(salt), + ).to.equal(domainSeparator); + }); + }); + + describe('#calculateDomainSeparator_10101(bytes32,bytes32)', () => { + it('returns domain separator', async () => { + const typeHash = await instance.$EIP_712_DOMAIN_HASH_10101.staticCall(); + + const types: string[] = ['bytes32', 'bytes32', 'uint256', 'bytes32']; + const values: string[] = [typeHash, nameHash, chainId, salt]; + + const domainSeparator = ethers.keccak256( + ethers.AbiCoder.defaultAbiCoder().encode(types, values), + ); + + expect( + await instance.$calculateDomainSeparator_10101.staticCall( + nameHash, + salt, + ), + ).to.equal(domainSeparator); + }); + }); + + describe('#calculateDomainSeparator_10110(bytes32,bytes32)', () => { + it('returns domain separator', async () => { + const typeHash = await instance.$EIP_712_DOMAIN_HASH_10110.staticCall(); + + const types: string[] = ['bytes32', 'bytes32', 'uint256', 'bytes32']; + const values: string[] = [typeHash, versionHash, chainId, salt]; + + const domainSeparator = ethers.keccak256( + ethers.AbiCoder.defaultAbiCoder().encode(types, values), + ); + + expect( + await instance.$calculateDomainSeparator_10110.staticCall( + versionHash, + salt, + ), + ).to.equal(domainSeparator); + }); + }); + + describe('#calculateDomainSeparator_10111(bytes32,bytes32,bytes32)', () => { + it('returns domain separator', async () => { + const typeHash = await instance.$EIP_712_DOMAIN_HASH_10111.staticCall(); + + const types: string[] = [ + 'bytes32', + 'bytes32', + 'bytes32', + 'uint256', + 'bytes32', + ]; + const values: string[] = [typeHash, nameHash, versionHash, chainId, salt]; + + const domainSeparator = ethers.keccak256( + ethers.AbiCoder.defaultAbiCoder().encode(types, values), + ); + + expect( + await instance.$calculateDomainSeparator_10111.staticCall( + nameHash, + versionHash, + salt, + ), + ).to.equal(domainSeparator); + }); + }); + + describe('#calculateDomainSeparator_11000(bytes32)', () => { + it('returns domain separator', async () => { + const typeHash = await instance.$EIP_712_DOMAIN_HASH_11000.staticCall(); + + const types: string[] = ['bytes32', 'address', 'bytes32']; + const values: string[] = [typeHash, verifyingContract, salt]; + + const domainSeparator = ethers.keccak256( + ethers.AbiCoder.defaultAbiCoder().encode(types, values), + ); + + expect( + await instance.$calculateDomainSeparator_11000.staticCall(salt), + ).to.equal(domainSeparator); + }); + }); + + describe('#calculateDomainSeparator_11001(bytes32,bytes32)', () => { + it('returns domain separator', async () => { + const typeHash = await instance.$EIP_712_DOMAIN_HASH_11001.staticCall(); + + const types: string[] = ['bytes32', 'bytes32', 'address', 'bytes32']; + const values: string[] = [typeHash, nameHash, verifyingContract, salt]; + + const domainSeparator = ethers.keccak256( + ethers.AbiCoder.defaultAbiCoder().encode(types, values), + ); + + expect( + await instance.$calculateDomainSeparator_11001.staticCall( + nameHash, + salt, + ), + ).to.equal(domainSeparator); + }); + }); + + describe('#calculateDomainSeparator_11010(bytes32,bytes32)', () => { + it('returns domain separator', async () => { + const typeHash = await instance.$EIP_712_DOMAIN_HASH_11010.staticCall(); + + const types: string[] = ['bytes32', 'bytes32', 'address', 'bytes32']; + const values: string[] = [typeHash, versionHash, verifyingContract, salt]; + + const domainSeparator = ethers.keccak256( + ethers.AbiCoder.defaultAbiCoder().encode(types, values), + ); + + expect( + await instance.$calculateDomainSeparator_11010.staticCall( + versionHash, + salt, + ), + ).to.equal(domainSeparator); + }); + }); + + describe('#calculateDomainSeparator_11011(bytes32,bytes32,bytes32)', () => { + it('returns domain separator', async () => { + const typeHash = await instance.$EIP_712_DOMAIN_HASH_11011.staticCall(); + + const types: string[] = [ + 'bytes32', + 'bytes32', + 'bytes32', + 'address', + 'bytes32', + ]; + const values: string[] = [ + typeHash, + nameHash, + versionHash, + verifyingContract, + salt, + ]; + + const domainSeparator = ethers.keccak256( + ethers.AbiCoder.defaultAbiCoder().encode(types, values), + ); + + expect( + await instance.$calculateDomainSeparator_11011.staticCall( + nameHash, + versionHash, + salt, + ), + ).to.equal(domainSeparator); + }); + }); + + describe('#calculateDomainSeparator_11100(bytes32)', () => { + it('returns domain separator', async () => { + const typeHash = await instance.$EIP_712_DOMAIN_HASH_11100.staticCall(); + + const types: string[] = ['bytes32', 'uint256', 'address', 'bytes32']; + const values: string[] = [typeHash, chainId, verifyingContract, salt]; + + const domainSeparator = ethers.keccak256( + ethers.AbiCoder.defaultAbiCoder().encode(types, values), + ); + + expect( + await instance.$calculateDomainSeparator_11100.staticCall(salt), + ).to.equal(domainSeparator); + }); + }); + + describe('#calculateDomainSeparator_11101(bytes32,bytes32)', () => { + it('returns domain separator', async () => { + const typeHash = await instance.$EIP_712_DOMAIN_HASH_11101.staticCall(); + + const types: string[] = [ + 'bytes32', + 'bytes32', + 'uint256', + 'address', + 'bytes32', + ]; + const values: string[] = [ + typeHash, + nameHash, + chainId, + verifyingContract, + salt, + ]; + + const domainSeparator = ethers.keccak256( + ethers.AbiCoder.defaultAbiCoder().encode(types, values), + ); + + expect( + await instance.$calculateDomainSeparator_11101.staticCall( + nameHash, + salt, + ), + ).to.equal(domainSeparator); + }); + }); + + describe('#calculateDomainSeparator_11110(bytes32,bytes32)', () => { + it('returns domain separator', async () => { + const typeHash = await instance.$EIP_712_DOMAIN_HASH_11110.staticCall(); + + const types: string[] = [ + 'bytes32', + 'bytes32', + 'uint256', + 'address', + 'bytes32', + ]; + const values: string[] = [ + typeHash, + versionHash, + chainId, + verifyingContract, + salt, + ]; + + const domainSeparator = ethers.keccak256( + ethers.AbiCoder.defaultAbiCoder().encode(types, values), + ); + + expect( + await instance.$calculateDomainSeparator_11110.staticCall( + versionHash, + salt, + ), + ).to.equal(domainSeparator); + }); + }); + + describe('#calculateDomainSeparator_11111(bytes32,bytes32,bytes32)', () => { + it('returns domain separator', async () => { + const typeHash = await instance.$EIP_712_DOMAIN_HASH_11111.staticCall(); + + const types: string[] = [ + 'bytes32', + 'bytes32', + 'bytes32', + 'uint256', + 'address', + 'bytes32', + ]; + const values: string[] = [ + typeHash, + nameHash, + versionHash, + chainId, + verifyingContract, + salt, + ]; + + const domainSeparator = ethers.keccak256( + ethers.AbiCoder.defaultAbiCoder().encode(types, values), + ); + + expect( + await instance.$calculateDomainSeparator_11111.staticCall( + nameHash, + versionHash, + salt, + ), + ).to.equal(domainSeparator); }); }); }); diff --git a/test/meta/ECDSAMetaTransactionContext.ts b/test/meta/ECDSAMetaTransactionContext.ts new file mode 100644 index 000000000..9b9436915 --- /dev/null +++ b/test/meta/ECDSAMetaTransactionContext.ts @@ -0,0 +1,250 @@ +import { SignerWithAddress } from '@nomicfoundation/hardhat-ethers/signers'; +import { signECDSAMetaTransaction } from '@solidstate/library'; +import { + $ECDSAMetaTransactionContext, + $ECDSAMetaTransactionContext__factory, +} from '@solidstate/typechain-types'; +import { TypedContractMethod } from '@solidstate/typechain-types/common'; +import { expect } from 'chai'; +import { BytesLike, ContractMethodArgs } from 'ethers'; +import { ethers } from 'hardhat'; + +const callMetaTransaction = async ( + signer: SignerWithAddress, + fn: TypedContractMethod<[], [string], 'nonpayable' | 'payable' | 'view'>, + data: BytesLike, + args: ContractMethodArgs<[]> = [], +) => { + const tx = await fn.populateTransaction(...args); + tx.data = ethers.concat([tx.data, data]); + + const result = await signer.call(tx); + + return new ethers.Interface([fn.fragment]).decodeFunctionResult( + fn.name, + result, + ); +}; + +// TODO: support msg.value in helper functions + +const sendMetaTransaction = async ( + signer: SignerWithAddress, + fn: TypedContractMethod<[], [string], 'nonpayable' | 'payable' | 'view'>, + data: BytesLike, + args: ContractMethodArgs<[]> = [], +) => { + const tx = await fn.populateTransaction(...args); + tx.data = ethers.concat([tx.data, data]); + + return await signer.sendTransaction(tx); +}; + +describe('ECDSAMetaTransactionContext', () => { + let instance: $ECDSAMetaTransactionContext; + let deployer: SignerWithAddress; + let forwarder: SignerWithAddress; + let signer: SignerWithAddress; + + beforeEach(async () => { + [deployer, forwarder, signer] = await ethers.getSigners(); + instance = await new $ECDSAMetaTransactionContext__factory( + deployer, + ).deploy(); + }); + + // TODO: spec + + // TODO: test multiple calls in same tx to validate that transient storage is used correctly + + describe('__internal', () => { + describe('#_msgSender()', () => { + it('returns signer if signature is valid', async () => { + const data = instance.$_msgSender.fragment.selector; + const nonce = 1n; + const value = 0n; + + const signature = await signECDSAMetaTransaction( + instance, + signer, + data, + value, + nonce, + ); + + const suffix = ethers.concat([ + await signer.getAddress(), + signature.serialized, + ]); + + expect( + await callMetaTransaction(forwarder, instance.$_msgSender, suffix), + ).to.deep.equal([await signer.getAddress()]); + }); + + it('returns message sender if signature is invalid', async () => { + const suffix = ethers.randomBytes( + Number(await instance.$_calldataSuffixLength.staticCall()), + ); + + expect( + await callMetaTransaction(forwarder, instance.$_msgSender, suffix), + ).to.deep.equal([await forwarder.getAddress()]); + }); + + it('returns message sender if message data length is less than suffix length', async () => { + // account for 4-byte selector when calculting suffix length + const suffix = ethers.randomBytes( + Number( + (await instance.$_calldataSuffixLength.staticCall()) - 4n - 1n, + ), + ); + + expect( + await callMetaTransaction(forwarder, instance.$_msgSender, suffix), + ).to.deep.equal([await forwarder.getAddress()]); + }); + + it('returns incorrect signer if message value is incorrect', async () => { + const data = instance.$_msgSender.fragment.selector; + const nonce = 1n; + const value = 1n; + + const signature = await signECDSAMetaTransaction( + instance, + signer, + data, + value, + nonce, + ); + + const suffix = ethers.concat([ + await signer.getAddress(), + signature.serialized, + ]); + + // a valid address is returned, but it is not the correct signer + + expect( + await callMetaTransaction(forwarder, instance.$_msgSender, suffix), + ).not.to.deep.equal([await signer.getAddress()]); + }); + + describe('reverts if', () => { + it('nonce has been used', async () => { + const data = instance.$_msgSender.fragment.selector; + const nonce = 1n; + const value = 0n; + + const signature = await signECDSAMetaTransaction( + instance, + signer, + data, + value, + nonce, + ); + + const suffix = ethers.concat([ + await signer.getAddress(), + signature.serialized, + ]); + + await sendMetaTransaction(forwarder, instance.$_msgSender, suffix); + + await expect( + callMetaTransaction(forwarder, instance.$_msgSender, suffix), + ).to.be.revertedWith('TODO'); + }); + }); + }); + + describe('#_msgData()', () => { + it('returns message data without suffix if signature is valid', async () => { + const nonSuffixedData = instance.$_msgData.fragment.selector; + const nonce = 1n; + const value = 0n; + + const signature = await signECDSAMetaTransaction( + instance, + signer, + nonSuffixedData, + value, + nonce, + ); + + const suffix = ethers.concat([ + await signer.getAddress(), + signature.serialized, + ]); + + expect( + await callMetaTransaction(forwarder, instance.$_msgData, suffix), + ).to.deep.equal([nonSuffixedData]); + }); + + it('returns complete message data if signature is invalid', async () => { + const nonSuffixedData = instance.$_msgData.fragment.selector; + const suffix = ethers.randomBytes( + Number(await instance.$_calldataSuffixLength.staticCall()), + ); + + // message data is returned as received, demonstrating the malleability of msg.data + + expect( + await callMetaTransaction(forwarder, instance.$_msgData, suffix), + ).to.deep.equal([ethers.concat([nonSuffixedData, suffix])]); + }); + + it('returns complete message data if message data length is less than suffix length', async () => { + const nonSuffixedData = instance.$_msgData.fragment.selector; + // account for 4-byte selector when calculting suffix length + const suffix = ethers.randomBytes( + Number( + (await instance.$_calldataSuffixLength.staticCall()) - 4n - 1n, + ), + ); + + // message data is returned as received, demonstrating the malleability of msg.data + + expect( + await callMetaTransaction(forwarder, instance.$_msgData, suffix), + ).to.deep.equal([ethers.concat([nonSuffixedData, suffix])]); + }); + + describe('reverts if', () => { + it('nonce has been used', async () => { + const data = instance.$_msgData.fragment.selector; + const nonce = 1n; + const value = 0n; + + const signature = await signECDSAMetaTransaction( + instance, + signer, + data, + value, + nonce, + ); + + const suffix = ethers.concat([ + await signer.getAddress(), + signature.serialized, + ]); + + await sendMetaTransaction(forwarder, instance.$_msgData, suffix); + + await expect( + callMetaTransaction(forwarder, instance.$_msgData, suffix), + ).to.be.revertedWith('TODO'); + }); + }); + }); + + describe('#_calldataSuffixLength()', () => { + it('returns 85', async () => { + expect(await instance.$_calldataSuffixLength.staticCall()).to.equal( + 85n, + ); + }); + }); + }); +}); diff --git a/test/token/fungible/FungibleTokenPermit.ts b/test/token/fungible/FungibleTokenPermit.ts index cee1b0f85..f7c8016e9 100644 --- a/test/token/fungible/FungibleTokenPermit.ts +++ b/test/token/fungible/FungibleTokenPermit.ts @@ -30,7 +30,7 @@ describe('FungibleTokenPermit', () => { describe('__internal', () => { describe('#_DOMAIN_SEPARATOR()', () => { - it('changes is token name is changed', async () => { + it('changes if token name is changed', async () => { const oldDomainSeparator = await instance.DOMAIN_SEPARATOR.staticCall(); await instance.$_setName(`new ${name}`); const newDomainSeparator = await instance.DOMAIN_SEPARATOR.staticCall(); diff --git a/yarn.lock b/yarn.lock index 6c530e64f..4f955444f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1513,6 +1513,11 @@ resolved "https://registry.yarnpkg.com/@types/delete-empty/-/delete-empty-3.0.5.tgz#03ae6a38f9c805d8f31ef5e5210c65e040bf56eb" integrity sha512-/7fn2o9NpN6GhAfa2enr17kgcO7Bi49j8vj08eSJ4vCh9RgSUPyQft45XLHSCBhlselWl21p7Kl5a0y79qWzqw== +"@types/ejs@^3.1.5": + version "3.1.5" + resolved "https://registry.yarnpkg.com/@types/ejs/-/ejs-3.1.5.tgz#49d738257cc73bafe45c13cb8ff240683b4d5117" + integrity sha512-nv+GSx77ZtXiJzwKdsASqi+YQ5Z7vwHsTP0JY2SiQgjGckkBRKZnk8nIM+7oUZ1VCtuTz0+By4qVR7fqzp/Dfg== + "@types/git-diff@^2.0.7": version "2.0.7" resolved "https://registry.yarnpkg.com/@types/git-diff/-/git-diff-2.0.7.tgz#533b2fc74c4739440900808e8960124de87feb24" @@ -2784,7 +2789,7 @@ eastasianwidth@^0.2.0: resolved "https://registry.yarnpkg.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb" integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA== -ejs@^3.1.7: +ejs@^3.1.10, ejs@^3.1.7: version "3.1.10" resolved "https://registry.yarnpkg.com/ejs/-/ejs-3.1.10.tgz#69ab8358b14e896f80cc39e62087b88500c3ac3b" integrity sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==