Skip to content

Commit cf718ae

Browse files
authored
✨ More EIP712 sans combos (#1408)
1 parent 459b53d commit cf718ae

5 files changed

Lines changed: 245 additions & 6 deletions

File tree

docs/utils/eip712.md

Lines changed: 48 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,27 @@ bytes32 internal constant _DOMAIN_TYPEHASH_SANS_CHAIN_ID =
3636
`keccak256("EIP712Domain(string name,string version,address verifyingContract)")`.
3737
This is only used in `_hashTypedDataSansChainId`.
3838

39+
### _DOMAIN_TYPEHASH_SANS_CHAIN_ID_AND_VERIFYING_CONTRACT
40+
41+
```solidity
42+
bytes32 internal constant
43+
_DOMAIN_TYPEHASH_SANS_CHAIN_ID_AND_VERIFYING_CONTRACT =
44+
0xb03948446334eb9b2196d5eb166f69b9d49403eb4a12f36de8d3f9f3cb8e15c3
45+
```
46+
47+
`keccak256("EIP712Domain(string name,string version)")`.
48+
This is only used in `_hashTypedDataSansChainIdAndVerifyingContract`.
49+
50+
### _DOMAIN_TYPEHASH_SANS_VERIFYING_CONTRACT
51+
52+
```solidity
53+
bytes32 internal constant _DOMAIN_TYPEHASH_SANS_VERIFYING_CONTRACT =
54+
0xc2f8787176b8ac6bf7215b4adcc1e069bf4ab82d9ab1df05a57a91d425935b6e
55+
```
56+
57+
`keccak256("EIP712Domain(string name,string version,uint256 chainId)")`.
58+
This is only used in `_hashTypedDataSansVerifyingContract`.
59+
3960
## Hashing Operations
4061

4162
### _domainSeparator()
@@ -84,9 +105,33 @@ function _hashTypedDataSansChainId(bytes32 structHash)
84105
```
85106

86107
Variant of `_hashTypedData` that excludes the chain ID.
87-
We expect that most contracts will use `_hashTypedData` as the main hash,
88-
and `_hashTypedDataSansChainId` only occasionally for cross-chain workflows.
89-
Thus this is optimized for smaller bytecode size over runtime gas.
108+
Included for the niche use case of cross-chain workflows.
109+
110+
### _hashTypedDataSansChainIdAndVerifyingContract(bytes32)
111+
112+
```solidity
113+
function _hashTypedDataSansChainIdAndVerifyingContract(bytes32 structHash)
114+
internal
115+
view
116+
virtual
117+
returns (bytes32 digest)
118+
```
119+
120+
Variant of `_hashTypedData` that excludes the chain ID and verifying contract.
121+
Included for the niche use case of cross-chain and multi-verifier workflows.
122+
123+
### _hashTypedDataSansVerifyingContract(bytes32)
124+
125+
```solidity
126+
function _hashTypedDataSansVerifyingContract(bytes32 structHash)
127+
internal
128+
view
129+
virtual
130+
returns (bytes32 digest)
131+
```
132+
133+
Variant of `_hashTypedData` that excludes the chain ID and verifying contract.
134+
Included for the niche use case of multi-verifier workflows.
90135

91136
## EIP-5267 Operations
92137

src/utils/EIP712.sol

Lines changed: 62 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,16 @@ abstract contract EIP712 {
2626
bytes32 internal constant _DOMAIN_TYPEHASH_SANS_CHAIN_ID =
2727
0x91ab3d17e3a50a9d89e63fd30b92be7f5336b03b287bb946787a83a9d62a2766;
2828

29+
/// @dev `keccak256("EIP712Domain(string name,string version)")`.
30+
/// This is only used in `_hashTypedDataSansChainIdAndVerifyingContract`.
31+
bytes32 internal constant _DOMAIN_TYPEHASH_SANS_CHAIN_ID_AND_VERIFYING_CONTRACT =
32+
0xb03948446334eb9b2196d5eb166f69b9d49403eb4a12f36de8d3f9f3cb8e15c3;
33+
34+
/// @dev `keccak256("EIP712Domain(string name,string version,uint256 chainId)")`.
35+
/// This is only used in `_hashTypedDataSansVerifyingContract`.
36+
bytes32 internal constant _DOMAIN_TYPEHASH_SANS_VERIFYING_CONTRACT =
37+
0xc2f8787176b8ac6bf7215b4adcc1e069bf4ab82d9ab1df05a57a91d425935b6e;
38+
2939
uint256 private immutable _cachedThis;
3040
uint256 private immutable _cachedChainId;
3141
bytes32 private immutable _cachedNameHash;
@@ -147,9 +157,7 @@ abstract contract EIP712 {
147157
}
148158

149159
/// @dev Variant of `_hashTypedData` that excludes the chain ID.
150-
/// We expect that most contracts will use `_hashTypedData` as the main hash,
151-
/// and `_hashTypedDataSansChainId` only occasionally for cross-chain workflows.
152-
/// Thus this is optimized for smaller bytecode size over runtime gas.
160+
/// Included for the niche use case of cross-chain workflows.
153161
function _hashTypedDataSansChainId(bytes32 structHash)
154162
internal
155163
view
@@ -174,6 +182,57 @@ abstract contract EIP712 {
174182
}
175183
}
176184

185+
/// @dev Variant of `_hashTypedData` that excludes the chain ID and verifying contract.
186+
/// Included for the niche use case of cross-chain and multi-verifier workflows.
187+
function _hashTypedDataSansChainIdAndVerifyingContract(bytes32 structHash)
188+
internal
189+
view
190+
virtual
191+
returns (bytes32 digest)
192+
{
193+
(string memory name, string memory version) = _domainNameAndVersion();
194+
/// @solidity memory-safe-assembly
195+
assembly {
196+
let m := mload(0x40) // Load the free memory pointer.
197+
mstore(0x00, _DOMAIN_TYPEHASH_SANS_CHAIN_ID_AND_VERIFYING_CONTRACT)
198+
mstore(0x20, keccak256(add(name, 0x20), mload(name)))
199+
mstore(0x40, keccak256(add(version, 0x20), mload(version)))
200+
// Compute the digest.
201+
mstore(0x20, keccak256(0x00, 0x60)) // Store the domain separator.
202+
mstore(0x00, 0x1901) // Store "\x19\x01".
203+
mstore(0x40, structHash) // Store the struct hash.
204+
digest := keccak256(0x1e, 0x42)
205+
mstore(0x40, m) // Restore the free memory pointer.
206+
mstore(0x60, 0) // Restore the zero pointer.
207+
}
208+
}
209+
210+
/// @dev Variant of `_hashTypedData` that excludes the chain ID and verifying contract.
211+
/// Included for the niche use case of multi-verifier workflows.
212+
function _hashTypedDataSansVerifyingContract(bytes32 structHash)
213+
internal
214+
view
215+
virtual
216+
returns (bytes32 digest)
217+
{
218+
(string memory name, string memory version) = _domainNameAndVersion();
219+
/// @solidity memory-safe-assembly
220+
assembly {
221+
let m := mload(0x40) // Load the free memory pointer.
222+
mstore(0x00, _DOMAIN_TYPEHASH_SANS_VERIFYING_CONTRACT)
223+
mstore(0x20, keccak256(add(name, 0x20), mload(name)))
224+
mstore(0x40, keccak256(add(version, 0x20), mload(version)))
225+
mstore(0x60, chainid())
226+
// Compute the digest.
227+
mstore(0x20, keccak256(0x00, 0x80)) // Store the domain separator.
228+
mstore(0x00, 0x1901) // Store "\x19\x01".
229+
mstore(0x40, structHash) // Store the struct hash.
230+
digest := keccak256(0x1e, 0x42)
231+
mstore(0x40, m) // Restore the free memory pointer.
232+
mstore(0x60, 0) // Restore the zero pointer.
233+
}
234+
}
235+
177236
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
178237
/* EIP-5267 OPERATIONS */
179238
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/

test/EIP712.t.sol

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,4 +238,107 @@ contract EIP712Test is SoladyTest {
238238
function testHashTypedDataSansChainIdOnDynamicClone() public {
239239
_testHashTypedDataSansChainId(MockEIP712(address(mockDynamicClone)));
240240
}
241+
242+
function _testHashTypedDataSansChainIdAndVerifyingContract(MockEIP712 mockToTest) public {
243+
_TestTemps memory t;
244+
(, t.name, t.version,,,,) = mockToTest.eip712Domain();
245+
246+
(t.signer, t.privateKey) = _randomSigner();
247+
248+
(t.to,) = _randomSigner();
249+
250+
t.message = "Hello Milady!";
251+
252+
t.structHash = keccak256(abi.encode("Message(address to,string message)", t.to, t.message));
253+
t.expectedDigest = keccak256(
254+
abi.encodePacked(
255+
"\x19\x01",
256+
keccak256(
257+
abi.encode(
258+
keccak256("EIP712Domain(string name,string version)"),
259+
keccak256(bytes(t.name)),
260+
keccak256(bytes(t.version))
261+
)
262+
),
263+
t.structHash
264+
)
265+
);
266+
267+
assertEq(
268+
mockToTest.hashTypedDataSansChainIdAndVerifyingContract(t.structHash), t.expectedDigest
269+
);
270+
271+
(t.v, t.r, t.s) = vm.sign(t.privateKey, t.expectedDigest);
272+
273+
t.recoveredAddress = ecrecover(t.expectedDigest, t.v, t.r, t.s);
274+
275+
assertEq(t.recoveredAddress, t.signer);
276+
}
277+
278+
function testHashTypedDataSansChainIdAndVerifyingContract() public {
279+
_testHashTypedDataSansChainIdAndVerifyingContract(mock);
280+
}
281+
282+
function testHashTypedDataSansChainIdAndVerifyingContractOnClone() public {
283+
_testHashTypedDataSansChainIdAndVerifyingContract(mockClone);
284+
}
285+
286+
function testHashTypedDataSansChainIdAndVerifyingContractOnDynamic() public {
287+
_testHashTypedDataSansChainIdAndVerifyingContract(MockEIP712(address(mockDynamic)));
288+
}
289+
290+
function testHashTypedDataSansChainIdAndVerifyingContractOnDynamicClone() public {
291+
_testHashTypedDataSansChainIdAndVerifyingContract(MockEIP712(address(mockDynamicClone)));
292+
}
293+
294+
function _testHashTypedDataSansVerifyingContract(MockEIP712 mockToTest) public {
295+
_TestTemps memory t;
296+
(, t.name, t.version,,,,) = mockToTest.eip712Domain();
297+
298+
(t.signer, t.privateKey) = _randomSigner();
299+
300+
(t.to,) = _randomSigner();
301+
302+
t.message = "Hello Milady!";
303+
304+
t.structHash = keccak256(abi.encode("Message(address to,string message)", t.to, t.message));
305+
t.expectedDigest = keccak256(
306+
abi.encodePacked(
307+
"\x19\x01",
308+
keccak256(
309+
abi.encode(
310+
keccak256("EIP712Domain(string name,string version,uint256 chainId)"),
311+
keccak256(bytes(t.name)),
312+
keccak256(bytes(t.version)),
313+
block.chainid
314+
)
315+
),
316+
t.structHash
317+
)
318+
);
319+
320+
assertEq(mockToTest.hashTypedDataSansVerifyingContract(t.structHash), t.expectedDigest);
321+
322+
(t.v, t.r, t.s) = vm.sign(t.privateKey, t.expectedDigest);
323+
324+
t.recoveredAddress = ecrecover(t.expectedDigest, t.v, t.r, t.s);
325+
326+
assertEq(t.recoveredAddress, t.signer);
327+
}
328+
329+
function testHashTypedDataSansVerifyingContract() public {
330+
_testHashTypedDataSansVerifyingContract(mock);
331+
}
332+
333+
function testHashTypedDataSansVerifyingContractOnClone() public {
334+
_testHashTypedDataSansVerifyingContract(mockClone);
335+
}
336+
337+
function testHashTypedDataSansVerifyingContractOnDynamic() public {
338+
_testHashTypedDataSansVerifyingContract(MockEIP712(address(mockDynamic)));
339+
}
340+
341+
function testHashTypedDataSansVerifyingContractOnDynamicClone() public {
342+
_testHashTypedDataSansVerifyingContract(MockEIP712(address(mockDynamicClone)));
343+
}
241344
}

test/utils/mocks/MockEIP712.sol

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,4 +27,20 @@ contract MockEIP712 is EIP712 {
2727
function hashTypedDataSansChainId(bytes32 structHash) external view returns (bytes32) {
2828
return _hashTypedDataSansChainId(structHash);
2929
}
30+
31+
function hashTypedDataSansChainIdAndVerifyingContract(bytes32 structHash)
32+
external
33+
view
34+
returns (bytes32)
35+
{
36+
return _hashTypedDataSansChainIdAndVerifyingContract(structHash);
37+
}
38+
39+
function hashTypedDataSansVerifyingContract(bytes32 structHash)
40+
external
41+
view
42+
returns (bytes32)
43+
{
44+
return _hashTypedDataSansVerifyingContract(structHash);
45+
}
3046
}

test/utils/mocks/MockEIP712Dynamic.sol

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,4 +44,20 @@ contract MockEIP712Dynamic is EIP712 {
4444
function hashTypedDataSansChainId(bytes32 structHash) external view returns (bytes32) {
4545
return _hashTypedDataSansChainId(structHash);
4646
}
47+
48+
function hashTypedDataSansChainIdAndVerifyingContract(bytes32 structHash)
49+
external
50+
view
51+
returns (bytes32)
52+
{
53+
return _hashTypedDataSansChainIdAndVerifyingContract(structHash);
54+
}
55+
56+
function hashTypedDataSansVerifyingContract(bytes32 structHash)
57+
external
58+
view
59+
returns (bytes32)
60+
{
61+
return _hashTypedDataSansVerifyingContract(structHash);
62+
}
4763
}

0 commit comments

Comments
 (0)