Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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
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