Skip to content

Commit 93ca624

Browse files
ernestognwAmxxcoderabbitai[bot]james-toussaint
authored
Add dynamic domain separator hash generation based on ERC-5267 (#5908)
Co-authored-by: Hadrien Croubois <[email protected]> Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Co-authored-by: James Toussaint <[email protected]>
1 parent d0131a9 commit 93ca624

File tree

3 files changed

+187
-1
lines changed

3 files changed

+187
-1
lines changed

.changeset/blue-mirrors-agree.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'openzeppelin-solidity': minor
3+
---
4+
5+
`MessageHashUtils`: Add helper functions to build EIP-712 domain typehash and separator with fields selectively enabled/disabled.

contracts/utils/cryptography/MessageHashUtils.sol

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ import {Strings} from "../Strings.sol";
1313
* specifications.
1414
*/
1515
library MessageHashUtils {
16+
error ERC5267ExtensionsNotSupported();
17+
1618
/**
1719
* @dev Returns the keccak256 digest of an ERC-191 signed data with version
1820
* `0x45` (`personal_sign` messages).
@@ -96,4 +98,131 @@ library MessageHashUtils {
9698
digest := keccak256(ptr, 0x42)
9799
}
98100
}
101+
102+
/**
103+
* @dev Returns the EIP-712 domain separator constructed from an `eip712Domain`. See {IERC5267-eip712Domain}
104+
*
105+
* This function dynamically constructs the domain separator based on which fields are present in the
106+
* `fields` parameter. It contains flags that indicate which domain fields are present:
107+
*
108+
* * Bit 0 (0x01): name
109+
* * Bit 1 (0x02): version
110+
* * Bit 2 (0x04): chainId
111+
* * Bit 3 (0x08): verifyingContract
112+
* * Bit 4 (0x10): salt
113+
*
114+
* Arguments that correspond to fields which are not present in `fields` are ignored. For example, if `fields` is
115+
* `0x0f` (`0b01111`), then the `salt` parameter is ignored.
116+
*/
117+
function toDomainSeparator(
118+
bytes1 fields,
119+
string memory name,
120+
string memory version,
121+
uint256 chainId,
122+
address verifyingContract,
123+
bytes32 salt
124+
) internal pure returns (bytes32 hash) {
125+
return
126+
toDomainSeparator(
127+
fields,
128+
keccak256(bytes(name)),
129+
keccak256(bytes(version)),
130+
chainId,
131+
verifyingContract,
132+
salt
133+
);
134+
}
135+
136+
/// @dev Variant of {toDomainSeparator-bytes1-string-string-uint256-address-bytes32} that uses hashed name and version.
137+
function toDomainSeparator(
138+
bytes1 fields,
139+
bytes32 nameHash,
140+
bytes32 versionHash,
141+
uint256 chainId,
142+
address verifyingContract,
143+
bytes32 salt
144+
) internal pure returns (bytes32 hash) {
145+
bytes32 domainTypeHash = toDomainTypeHash(fields);
146+
147+
assembly ("memory-safe") {
148+
// align fields to the right for easy processing
149+
fields := shr(248, fields)
150+
151+
// FMP used as scratch space
152+
let fmp := mload(0x40)
153+
mstore(fmp, domainTypeHash)
154+
155+
let ptr := add(fmp, 0x20)
156+
if and(fields, 0x01) {
157+
mstore(ptr, nameHash)
158+
ptr := add(ptr, 0x20)
159+
}
160+
if and(fields, 0x02) {
161+
mstore(ptr, versionHash)
162+
ptr := add(ptr, 0x20)
163+
}
164+
if and(fields, 0x04) {
165+
mstore(ptr, chainId)
166+
ptr := add(ptr, 0x20)
167+
}
168+
if and(fields, 0x08) {
169+
mstore(ptr, verifyingContract)
170+
ptr := add(ptr, 0x20)
171+
}
172+
if and(fields, 0x10) {
173+
mstore(ptr, salt)
174+
ptr := add(ptr, 0x20)
175+
}
176+
177+
hash := keccak256(fmp, sub(ptr, fmp))
178+
}
179+
}
180+
181+
/// @dev Builds an EIP-712 domain type hash depending on the `fields` provided, following https://eips.ethereum.org/EIPS/eip-5267[ERC-5267]
182+
function toDomainTypeHash(bytes1 fields) internal pure returns (bytes32 hash) {
183+
if (fields & 0x20 == 0x20) revert ERC5267ExtensionsNotSupported();
184+
185+
assembly ("memory-safe") {
186+
// align fields to the right for easy processing
187+
fields := shr(248, fields)
188+
189+
// FMP used as scratch space
190+
let fmp := mload(0x40)
191+
mstore(fmp, "EIP712Domain(")
192+
193+
let ptr := add(fmp, 0x0d)
194+
// name field
195+
if and(fields, 0x01) {
196+
mstore(ptr, "string name,")
197+
ptr := add(ptr, 0x0c)
198+
}
199+
// version field
200+
if and(fields, 0x02) {
201+
mstore(ptr, "string version,")
202+
ptr := add(ptr, 0x0f)
203+
}
204+
// chainId field
205+
if and(fields, 0x04) {
206+
mstore(ptr, "uint256 chainId,")
207+
ptr := add(ptr, 0x10)
208+
}
209+
// verifyingContract field
210+
if and(fields, 0x08) {
211+
mstore(ptr, "address verifyingContract,")
212+
ptr := add(ptr, 0x1a)
213+
}
214+
// salt field
215+
if and(fields, 0x10) {
216+
mstore(ptr, "bytes32 salt,")
217+
ptr := add(ptr, 0x0d)
218+
}
219+
// if any field is enabled, remove the trailing comma
220+
ptr := sub(ptr, iszero(iszero(and(fields, 0x1f))))
221+
// add the closing brace
222+
mstore8(ptr, 0x29) // add closing brace
223+
ptr := add(ptr, 1)
224+
225+
hash := keccak256(fmp, sub(ptr, fmp))
226+
}
227+
}
99228
}

test/utils/cryptography/MessageHashUtils.test.js

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@ const { ethers } = require('hardhat');
22
const { expect } = require('chai');
33
const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
44

5-
const { domainSeparator, hashTypedData } = require('../../helpers/eip712');
5+
const { domainType, domainSeparator, hashTypedData } = require('../../helpers/eip712');
6+
const { generators } = require('../../helpers/random');
67

78
async function fixture() {
89
const mock = await ethers.deployContract('$MessageHashUtils');
@@ -94,4 +95,55 @@ describe('MessageHashUtils', function () {
9495
await expect(this.mock.$toTypedDataHash(domainSeparator(domain), structhash)).to.eventually.equal(expectedHash);
9596
});
9697
});
98+
99+
describe('ERC-5267', function () {
100+
const fullDomain = {
101+
name: generators.string(),
102+
version: generators.string(),
103+
chainId: generators.uint256(),
104+
verifyingContract: generators.address(),
105+
salt: generators.bytes32(),
106+
};
107+
108+
for (let fields = 0; fields < 1 << Object.keys(fullDomain).length; ++fields) {
109+
const domain = Object.fromEntries(Object.entries(fullDomain).filter((_, i) => fields & (1 << i)));
110+
const domainTypeName = new ethers.TypedDataEncoder({ EIP712Domain: domainType(domain) }).encodeType(
111+
'EIP712Domain',
112+
);
113+
114+
describe(domainTypeName, function () {
115+
it('toDomainSeparator(bytes1,string,string,uint256,address,bytes32)', async function () {
116+
await expect(
117+
this.mock.$toDomainSeparator(
118+
ethers.toBeHex(fields),
119+
ethers.Typed.string(fullDomain.name),
120+
ethers.Typed.string(fullDomain.version),
121+
fullDomain.chainId,
122+
fullDomain.verifyingContract,
123+
fullDomain.salt,
124+
),
125+
).to.eventually.equal(domainSeparator(domain));
126+
});
127+
128+
it('toDomainSeparator(bytes1,bytes32,bytes32,uint256,address,bytes32)', async function () {
129+
await expect(
130+
this.mock.$toDomainSeparator(
131+
ethers.toBeHex(fields),
132+
ethers.Typed.bytes32(ethers.id(fullDomain.name)),
133+
ethers.Typed.bytes32(ethers.id(fullDomain.version)),
134+
fullDomain.chainId,
135+
fullDomain.verifyingContract,
136+
fullDomain.salt,
137+
),
138+
).to.eventually.equal(domainSeparator(domain));
139+
});
140+
141+
it('toDomainTypeHash', async function () {
142+
await expect(this.mock.$toDomainTypeHash(ethers.toBeHex(fields))).to.eventually.equal(
143+
ethers.id(domainTypeName),
144+
);
145+
});
146+
});
147+
}
148+
});
97149
});

0 commit comments

Comments
 (0)