Skip to content
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/blue-mirrors-agree.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'openzeppelin-solidity': minor
---

`MessageHashUtils`: Add helper functions to build EIP-712 domain typehash and separator with fields selectivelly enabled/disabled.
129 changes: 129 additions & 0 deletions contracts/utils/cryptography/MessageHashUtils.sol
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import {Strings} from "../Strings.sol";
* specifications.
*/
library MessageHashUtils {
error ERC5267ExtensionsNotSupported();

/**
* @dev Returns the keccak256 digest of an ERC-191 signed data with version
* `0x45` (`personal_sign` messages).
Expand Down Expand Up @@ -96,4 +98,131 @@ library MessageHashUtils {
digest := keccak256(ptr, 0x42)
}
}

/**
* @dev Returns the EIP-712 domain separator constructed from an `eip712Domain`. See {IERC5267-eip712Domain}
*
* This function dynamically constructs the domain separator based on which fields are present in the
* `fields` parameter. It contains flags that indicate which domain fields are present:
*
* * Bit 0 (0x01): name
* * Bit 1 (0x02): version
* * Bit 2 (0x04): chainId
* * Bit 3 (0x08): verifyingContract
* * Bit 4 (0x10): salt
*
* Arguments that correspond to fields which are not present in `fields` are ignored. For example, if `fields` is
* `0x0f` (`0x01111`), then the `salt` parameter is ignored.
*/
function toDomainSeparator(
bytes1 fields,
string memory name,
string memory version,
uint256 chainId,
address verifyingContract,
bytes32 salt
) internal pure returns (bytes32 hash) {
return
toDomainSeparator(
fields,
keccak256(bytes(name)),
keccak256(bytes(version)),
chainId,
verifyingContract,
salt
);
}

/// @dev Variant of {toDomainSeparator-bytes1-string-string-uint256-address-bytes32} that uses hashed name and version.
function toDomainSeparator(
bytes1 fields,
bytes32 nameHash,
bytes32 versionHash,
uint256 chainId,
address verifyingContract,
bytes32 salt
) internal pure returns (bytes32 hash) {
bytes32 domainTypeHash = toDomainTypeHash(fields);

assembly ("memory-safe") {
// align fields to the right for easy processing
fields := shr(248, fields)

// FMP used as scratch space
let fmp := mload(0x40)
mstore(fmp, domainTypeHash)

let ptr := add(fmp, 0x20)
if and(fields, 0x01) {
mstore(ptr, nameHash)
ptr := add(ptr, 0x20)
}
if and(fields, 0x02) {
mstore(ptr, versionHash)
ptr := add(ptr, 0x20)
}
if and(fields, 0x04) {
mstore(ptr, chainId)
ptr := add(ptr, 0x20)
}
if and(fields, 0x08) {
mstore(ptr, verifyingContract)
ptr := add(ptr, 0x20)
}
if and(fields, 0x10) {
mstore(ptr, salt)
ptr := add(ptr, 0x20)
}

hash := keccak256(fmp, sub(ptr, fmp))
}
}

/// @dev Builds an EIP-712 domain type hash depending on the `fields` provided, following https://eips.ethereum.org/EIPS/eip-5267[ERC-5267]
function toDomainTypeHash(bytes1 fields) internal pure returns (bytes32 hash) {
if (fields & 0x20 == 0x20) revert ERC5267ExtensionsNotSupported();

assembly ("memory-safe") {
// align fields to the right for easy processing
fields := shr(248, fields)

// FMP used as scratch space
let fmp := mload(0x40)
mstore(fmp, "EIP712Domain(")

let ptr := add(fmp, 0x0d)
// name field
if and(fields, 0x01) {
mstore(ptr, "string name,")
ptr := add(ptr, 0x0c)
}
// version field
if and(fields, 0x02) {
mstore(ptr, "string version,")
ptr := add(ptr, 0x0f)
}
// chainId field
if and(fields, 0x04) {
mstore(ptr, "uint256 chainId,")
ptr := add(ptr, 0x10)
}
// verifyingContract field
if and(fields, 0x08) {
mstore(ptr, "address verifyingContract,")
ptr := add(ptr, 0x1a)
}
// salt field
if and(fields, 0x10) {
mstore(ptr, "bytes32 salt,")
ptr := add(ptr, 0x0d)
}
// if any field is enabled, remove the trailing comma
ptr := sub(ptr, iszero(iszero(and(fields, 0x1f))))
// add the closing brace
mstore8(ptr, 0x29) // add closing brace
ptr := add(ptr, 1)

hash := keccak256(fmp, sub(ptr, fmp))
}
}
}
54 changes: 53 additions & 1 deletion test/utils/cryptography/MessageHashUtils.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ const { ethers } = require('hardhat');
const { expect } = require('chai');
const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');

const { domainSeparator, hashTypedData } = require('../../helpers/eip712');
const { domainType, domainSeparator, hashTypedData } = require('../../helpers/eip712');
const { generators } = require('../../helpers/random');

async function fixture() {
const mock = await ethers.deployContract('$MessageHashUtils');
Expand Down Expand Up @@ -94,4 +95,55 @@ describe('MessageHashUtils', function () {
await expect(this.mock.$toTypedDataHash(domainSeparator(domain), structhash)).to.eventually.equal(expectedHash);
});
});

describe('ERC-5267', function () {
const fullDomain = {
name: generators.string(),
version: generators.string(),
chainId: generators.uint256(),
verifyingContract: generators.address(),
salt: generators.bytes32(),
};

for (let fields = 0; fields < 1 << Object.keys(fullDomain).length; ++fields) {
const domain = Object.fromEntries(Object.entries(fullDomain).filter((_, i) => fields & (1 << i)));
const domainTypeName = new ethers.TypedDataEncoder({ EIP712Domain: domainType(domain) }).encodeType(
'EIP712Domain',
);

describe(domainTypeName, function () {
it('toDomainSeparator(bytes1,string,string,uint256,address,bytes32)', async function () {
await expect(
this.mock.$toDomainSeparator(
ethers.toBeHex(fields),
ethers.Typed.string(fullDomain.name),
ethers.Typed.string(fullDomain.version),
fullDomain.chainId,
fullDomain.verifyingContract,
fullDomain.salt,
),
).to.eventually.equal(domainSeparator(domain));
});

it('toDomainSeparator(bytes1,bytes32,bytes32,uint256,address,bytes32)', async function () {
await expect(
this.mock.$toDomainSeparator(
ethers.toBeHex(fields),
ethers.Typed.bytes32(ethers.id(fullDomain.name)),
ethers.Typed.bytes32(ethers.id(fullDomain.version)),
fullDomain.chainId,
fullDomain.verifyingContract,
fullDomain.salt,
),
).to.eventually.equal(domainSeparator(domain));
});

it('toDomainTypeHash', async function () {
await expect(this.mock.$toDomainTypeHash(ethers.toBeHex(fields))).to.eventually.equal(
ethers.id(domainTypeName),
);
});
});
}
});
});
Loading