diff --git a/.changeset/quiet-shrimps-kiss.md b/.changeset/quiet-shrimps-kiss.md new file mode 100644 index 000000000..6c833e118 --- /dev/null +++ b/.changeset/quiet-shrimps-kiss.md @@ -0,0 +1,5 @@ +--- +"openzeppelin-solidity": patch +--- + +`MessageHashUtils`: Add `toDataWithIntendedValidatorHash(address, bytes32)`. diff --git a/contracts/utils/cryptography/MessageHashUtils.sol b/contracts/utils/cryptography/MessageHashUtils.sol index 2f12fb517..99ec150d9 100644 --- a/contracts/utils/cryptography/MessageHashUtils.sol +++ b/contracts/utils/cryptography/MessageHashUtils.sol @@ -63,6 +63,21 @@ library MessageHashUtils { return keccak256(abi.encodePacked(hex"19_00", validator, data)); } + /** + * @dev Variant of {toDataWithIntendedValidatorHash-address-bytes} optimized for cases where `data` is a bytes32. + */ + function toDataWithIntendedValidatorHash( + address validator, + bytes32 messageHash + ) internal pure returns (bytes32 digest) { + assembly ("memory-safe") { + mstore(0x00, hex"19_00") + mstore(0x02, shl(96, validator)) + mstore(0x16, messageHash) + digest := keccak256(0x00, 0x36) + } + } + /** * @dev Returns the keccak256 digest of an EIP-712 typed data (ERC-191 version `0x01`). * diff --git a/test/utils/cryptography/MessageHashUtils.t.sol b/test/utils/cryptography/MessageHashUtils.t.sol new file mode 100644 index 000000000..4259c8839 --- /dev/null +++ b/test/utils/cryptography/MessageHashUtils.t.sol @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import {Test} from "forge-std/Test.sol"; +import {MessageHashUtils} from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; + +contract MessageHashUtilsTest is Test { + function testToDataWithIntendedValidatorHash(address validator, bytes memory data) external pure { + assertEq( + MessageHashUtils.toDataWithIntendedValidatorHash(validator, data), + MessageHashUtils.toDataWithIntendedValidatorHash(_dirty(validator), data) + ); + } + + function testToDataWithIntendedValidatorHash(address validator, bytes32 messageHash) external pure { + assertEq( + MessageHashUtils.toDataWithIntendedValidatorHash(validator, messageHash), + MessageHashUtils.toDataWithIntendedValidatorHash(_dirty(validator), messageHash) + ); + + assertEq( + MessageHashUtils.toDataWithIntendedValidatorHash(validator, messageHash), + MessageHashUtils.toDataWithIntendedValidatorHash(validator, abi.encodePacked(messageHash)) + ); + } + + function _dirty(address input) private pure returns (address output) { + assembly ("memory-safe") { + output := or(input, shl(160, not(0))) + } + } +} diff --git a/test/utils/cryptography/MessageHashUtils.test.js b/test/utils/cryptography/MessageHashUtils.test.js index f20f5a3ca..57e82867e 100644 --- a/test/utils/cryptography/MessageHashUtils.test.js +++ b/test/utils/cryptography/MessageHashUtils.test.js @@ -19,14 +19,16 @@ describe('MessageHashUtils', function () { const message = ethers.randomBytes(32); const expectedHash = ethers.hashMessage(message); - expect(await this.mock.getFunction('$toEthSignedMessageHash(bytes32)')(message)).to.equal(expectedHash); + await expect(this.mock.getFunction('$toEthSignedMessageHash(bytes32)')(message)).to.eventually.equal( + expectedHash, + ); }); it('prefixes dynamic length data correctly', async function () { const message = ethers.randomBytes(128); const expectedHash = ethers.hashMessage(message); - expect(await this.mock.getFunction('$toEthSignedMessageHash(bytes)')(message)).to.equal(expectedHash); + await expect(this.mock.getFunction('$toEthSignedMessageHash(bytes)')(message)).to.eventually.equal(expectedHash); }); it('version match for bytes32', async function () { @@ -39,7 +41,20 @@ describe('MessageHashUtils', function () { }); describe('toDataWithIntendedValidatorHash', function () { - it('returns the digest correctly', async function () { + it('returns the digest of `bytes32 messageHash` correctly', async function () { + const verifier = ethers.Wallet.createRandom().address; + const message = ethers.randomBytes(32); + const expectedHash = ethers.solidityPackedKeccak256( + ['string', 'address', 'bytes32'], + ['\x19\x00', verifier, message], + ); + + await expect( + this.mock.getFunction('$toDataWithIntendedValidatorHash(address,bytes32)')(verifier, message), + ).to.eventually.equal(expectedHash); + }); + + it('returns the digest of `bytes memory message` correctly', async function () { const verifier = ethers.Wallet.createRandom().address; const message = ethers.randomBytes(128); const expectedHash = ethers.solidityPackedKeccak256( @@ -47,7 +62,21 @@ describe('MessageHashUtils', function () { ['\x19\x00', verifier, message], ); - expect(await this.mock.$toDataWithIntendedValidatorHash(verifier, message)).to.equal(expectedHash); + await expect( + this.mock.getFunction('$toDataWithIntendedValidatorHash(address,bytes)')(verifier, message), + ).to.eventually.equal(expectedHash); + }); + + it('version match for bytes32', async function () { + const verifier = ethers.Wallet.createRandom().address; + const message = ethers.randomBytes(32); + const fixed = await this.mock.getFunction('$toDataWithIntendedValidatorHash(address,bytes)')(verifier, message); + const dynamic = await this.mock.getFunction('$toDataWithIntendedValidatorHash(address,bytes32)')( + verifier, + message, + ); + + expect(fixed).to.equal(dynamic); }); }); @@ -62,7 +91,7 @@ describe('MessageHashUtils', function () { const structhash = ethers.randomBytes(32); const expectedHash = hashTypedData(domain, structhash); - expect(await this.mock.$toTypedDataHash(domainSeparator(domain), structhash)).to.equal(expectedHash); + await expect(this.mock.$toTypedDataHash(domainSeparator(domain), structhash)).to.eventually.equal(expectedHash); }); }); });