diff --git a/docs/utils/eip712.md b/docs/utils/eip712.md index 4d0347828c..f362ea330a 100644 --- a/docs/utils/eip712.md +++ b/docs/utils/eip712.md @@ -36,6 +36,27 @@ bytes32 internal constant _DOMAIN_TYPEHASH_SANS_CHAIN_ID = `keccak256("EIP712Domain(string name,string version,address verifyingContract)")`. This is only used in `_hashTypedDataSansChainId`. +### _DOMAIN_TYPEHASH_SANS_CHAIN_ID_AND_VERIFYING_CONTRACT + +```solidity +bytes32 internal constant + _DOMAIN_TYPEHASH_SANS_CHAIN_ID_AND_VERIFYING_CONTRACT = + 0xb03948446334eb9b2196d5eb166f69b9d49403eb4a12f36de8d3f9f3cb8e15c3 +``` + +`keccak256("EIP712Domain(string name,string version)")`. +This is only used in `_hashTypedDataSansChainIdAndVerifyingContract`. + +### _DOMAIN_TYPEHASH_SANS_VERIFYING_CONTRACT + +```solidity +bytes32 internal constant _DOMAIN_TYPEHASH_SANS_VERIFYING_CONTRACT = + 0xc2f8787176b8ac6bf7215b4adcc1e069bf4ab82d9ab1df05a57a91d425935b6e +``` + +`keccak256("EIP712Domain(string name,string version,uint256 chainId)")`. +This is only used in `_hashTypedDataSansVerifyingContract`. + ## Hashing Operations ### _domainSeparator() @@ -84,9 +105,33 @@ function _hashTypedDataSansChainId(bytes32 structHash) ``` Variant of `_hashTypedData` that excludes the chain ID. -We expect that most contracts will use `_hashTypedData` as the main hash, -and `_hashTypedDataSansChainId` only occasionally for cross-chain workflows. -Thus this is optimized for smaller bytecode size over runtime gas. +Included for the niche use case of cross-chain workflows. + +### _hashTypedDataSansChainIdAndVerifyingContract(bytes32) + +```solidity +function _hashTypedDataSansChainIdAndVerifyingContract(bytes32 structHash) + internal + view + virtual + returns (bytes32 digest) +``` + +Variant of `_hashTypedData` that excludes the chain ID and verifying contract. +Included for the niche use case of cross-chain and multi-verifier workflows. + +### _hashTypedDataSansVerifyingContract(bytes32) + +```solidity +function _hashTypedDataSansVerifyingContract(bytes32 structHash) + internal + view + virtual + returns (bytes32 digest) +``` + +Variant of `_hashTypedData` that excludes the chain ID and verifying contract. +Included for the niche use case of multi-verifier workflows. ## EIP-5267 Operations diff --git a/src/utils/EIP712.sol b/src/utils/EIP712.sol index 54a28a3258..9277c2926a 100644 --- a/src/utils/EIP712.sol +++ b/src/utils/EIP712.sol @@ -26,6 +26,16 @@ abstract contract EIP712 { bytes32 internal constant _DOMAIN_TYPEHASH_SANS_CHAIN_ID = 0x91ab3d17e3a50a9d89e63fd30b92be7f5336b03b287bb946787a83a9d62a2766; + /// @dev `keccak256("EIP712Domain(string name,string version)")`. + /// This is only used in `_hashTypedDataSansChainIdAndVerifyingContract`. + bytes32 internal constant _DOMAIN_TYPEHASH_SANS_CHAIN_ID_AND_VERIFYING_CONTRACT = + 0xb03948446334eb9b2196d5eb166f69b9d49403eb4a12f36de8d3f9f3cb8e15c3; + + /// @dev `keccak256("EIP712Domain(string name,string version,uint256 chainId)")`. + /// This is only used in `_hashTypedDataSansVerifyingContract`. + bytes32 internal constant _DOMAIN_TYPEHASH_SANS_VERIFYING_CONTRACT = + 0xc2f8787176b8ac6bf7215b4adcc1e069bf4ab82d9ab1df05a57a91d425935b6e; + uint256 private immutable _cachedThis; uint256 private immutable _cachedChainId; bytes32 private immutable _cachedNameHash; @@ -147,9 +157,7 @@ abstract contract EIP712 { } /// @dev Variant of `_hashTypedData` that excludes the chain ID. - /// We expect that most contracts will use `_hashTypedData` as the main hash, - /// and `_hashTypedDataSansChainId` only occasionally for cross-chain workflows. - /// Thus this is optimized for smaller bytecode size over runtime gas. + /// Included for the niche use case of cross-chain workflows. function _hashTypedDataSansChainId(bytes32 structHash) internal view @@ -174,6 +182,57 @@ abstract contract EIP712 { } } + /// @dev Variant of `_hashTypedData` that excludes the chain ID and verifying contract. + /// Included for the niche use case of cross-chain and multi-verifier workflows. + function _hashTypedDataSansChainIdAndVerifyingContract(bytes32 structHash) + internal + view + virtual + returns (bytes32 digest) + { + (string memory name, string memory version) = _domainNameAndVersion(); + /// @solidity memory-safe-assembly + assembly { + let m := mload(0x40) // Load the free memory pointer. + mstore(0x00, _DOMAIN_TYPEHASH_SANS_CHAIN_ID_AND_VERIFYING_CONTRACT) + mstore(0x20, keccak256(add(name, 0x20), mload(name))) + mstore(0x40, keccak256(add(version, 0x20), mload(version))) + // Compute the digest. + mstore(0x20, keccak256(0x00, 0x60)) // Store the domain separator. + mstore(0x00, 0x1901) // Store "\x19\x01". + mstore(0x40, structHash) // Store the struct hash. + digest := keccak256(0x1e, 0x42) + mstore(0x40, m) // Restore the free memory pointer. + mstore(0x60, 0) // Restore the zero pointer. + } + } + + /// @dev Variant of `_hashTypedData` that excludes the chain ID and verifying contract. + /// Included for the niche use case of multi-verifier workflows. + function _hashTypedDataSansVerifyingContract(bytes32 structHash) + internal + view + virtual + returns (bytes32 digest) + { + (string memory name, string memory version) = _domainNameAndVersion(); + /// @solidity memory-safe-assembly + assembly { + let m := mload(0x40) // Load the free memory pointer. + mstore(0x00, _DOMAIN_TYPEHASH_SANS_VERIFYING_CONTRACT) + mstore(0x20, keccak256(add(name, 0x20), mload(name))) + mstore(0x40, keccak256(add(version, 0x20), mload(version))) + mstore(0x60, chainid()) + // Compute the digest. + mstore(0x20, keccak256(0x00, 0x80)) // Store the domain separator. + mstore(0x00, 0x1901) // Store "\x19\x01". + mstore(0x40, structHash) // Store the struct hash. + digest := keccak256(0x1e, 0x42) + mstore(0x40, m) // Restore the free memory pointer. + mstore(0x60, 0) // Restore the zero pointer. + } + } + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ /* EIP-5267 OPERATIONS */ /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ diff --git a/test/EIP712.t.sol b/test/EIP712.t.sol index 29a995f079..e39b77aae0 100644 --- a/test/EIP712.t.sol +++ b/test/EIP712.t.sol @@ -238,4 +238,107 @@ contract EIP712Test is SoladyTest { function testHashTypedDataSansChainIdOnDynamicClone() public { _testHashTypedDataSansChainId(MockEIP712(address(mockDynamicClone))); } + + function _testHashTypedDataSansChainIdAndVerifyingContract(MockEIP712 mockToTest) public { + _TestTemps memory t; + (, t.name, t.version,,,,) = mockToTest.eip712Domain(); + + (t.signer, t.privateKey) = _randomSigner(); + + (t.to,) = _randomSigner(); + + t.message = "Hello Milady!"; + + t.structHash = keccak256(abi.encode("Message(address to,string message)", t.to, t.message)); + t.expectedDigest = keccak256( + abi.encodePacked( + "\x19\x01", + keccak256( + abi.encode( + keccak256("EIP712Domain(string name,string version)"), + keccak256(bytes(t.name)), + keccak256(bytes(t.version)) + ) + ), + t.structHash + ) + ); + + assertEq( + mockToTest.hashTypedDataSansChainIdAndVerifyingContract(t.structHash), t.expectedDigest + ); + + (t.v, t.r, t.s) = vm.sign(t.privateKey, t.expectedDigest); + + t.recoveredAddress = ecrecover(t.expectedDigest, t.v, t.r, t.s); + + assertEq(t.recoveredAddress, t.signer); + } + + function testHashTypedDataSansChainIdAndVerifyingContract() public { + _testHashTypedDataSansChainIdAndVerifyingContract(mock); + } + + function testHashTypedDataSansChainIdAndVerifyingContractOnClone() public { + _testHashTypedDataSansChainIdAndVerifyingContract(mockClone); + } + + function testHashTypedDataSansChainIdAndVerifyingContractOnDynamic() public { + _testHashTypedDataSansChainIdAndVerifyingContract(MockEIP712(address(mockDynamic))); + } + + function testHashTypedDataSansChainIdAndVerifyingContractOnDynamicClone() public { + _testHashTypedDataSansChainIdAndVerifyingContract(MockEIP712(address(mockDynamicClone))); + } + + function _testHashTypedDataSansVerifyingContract(MockEIP712 mockToTest) public { + _TestTemps memory t; + (, t.name, t.version,,,,) = mockToTest.eip712Domain(); + + (t.signer, t.privateKey) = _randomSigner(); + + (t.to,) = _randomSigner(); + + t.message = "Hello Milady!"; + + t.structHash = keccak256(abi.encode("Message(address to,string message)", t.to, t.message)); + t.expectedDigest = keccak256( + abi.encodePacked( + "\x19\x01", + keccak256( + abi.encode( + keccak256("EIP712Domain(string name,string version,uint256 chainId)"), + keccak256(bytes(t.name)), + keccak256(bytes(t.version)), + block.chainid + ) + ), + t.structHash + ) + ); + + assertEq(mockToTest.hashTypedDataSansVerifyingContract(t.structHash), t.expectedDigest); + + (t.v, t.r, t.s) = vm.sign(t.privateKey, t.expectedDigest); + + t.recoveredAddress = ecrecover(t.expectedDigest, t.v, t.r, t.s); + + assertEq(t.recoveredAddress, t.signer); + } + + function testHashTypedDataSansVerifyingContract() public { + _testHashTypedDataSansVerifyingContract(mock); + } + + function testHashTypedDataSansVerifyingContractOnClone() public { + _testHashTypedDataSansVerifyingContract(mockClone); + } + + function testHashTypedDataSansVerifyingContractOnDynamic() public { + _testHashTypedDataSansVerifyingContract(MockEIP712(address(mockDynamic))); + } + + function testHashTypedDataSansVerifyingContractOnDynamicClone() public { + _testHashTypedDataSansVerifyingContract(MockEIP712(address(mockDynamicClone))); + } } diff --git a/test/utils/mocks/MockEIP712.sol b/test/utils/mocks/MockEIP712.sol index aa3fed8235..821da9cd7e 100644 --- a/test/utils/mocks/MockEIP712.sol +++ b/test/utils/mocks/MockEIP712.sol @@ -27,4 +27,20 @@ contract MockEIP712 is EIP712 { function hashTypedDataSansChainId(bytes32 structHash) external view returns (bytes32) { return _hashTypedDataSansChainId(structHash); } + + function hashTypedDataSansChainIdAndVerifyingContract(bytes32 structHash) + external + view + returns (bytes32) + { + return _hashTypedDataSansChainIdAndVerifyingContract(structHash); + } + + function hashTypedDataSansVerifyingContract(bytes32 structHash) + external + view + returns (bytes32) + { + return _hashTypedDataSansVerifyingContract(structHash); + } } diff --git a/test/utils/mocks/MockEIP712Dynamic.sol b/test/utils/mocks/MockEIP712Dynamic.sol index 126b4f3637..d040a7d7d2 100644 --- a/test/utils/mocks/MockEIP712Dynamic.sol +++ b/test/utils/mocks/MockEIP712Dynamic.sol @@ -44,4 +44,20 @@ contract MockEIP712Dynamic is EIP712 { function hashTypedDataSansChainId(bytes32 structHash) external view returns (bytes32) { return _hashTypedDataSansChainId(structHash); } + + function hashTypedDataSansChainIdAndVerifyingContract(bytes32 structHash) + external + view + returns (bytes32) + { + return _hashTypedDataSansChainIdAndVerifyingContract(structHash); + } + + function hashTypedDataSansVerifyingContract(bytes32 structHash) + external + view + returns (bytes32) + { + return _hashTypedDataSansVerifyingContract(structHash); + } }