diff --git a/.changeset/small-cars-appear.md b/.changeset/small-cars-appear.md new file mode 100644 index 00000000000..0263bcd1854 --- /dev/null +++ b/.changeset/small-cars-appear.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': patch +--- + +`ECDSA`: Add a function `toDataWithIntendedValidatorHash` that encodes data with version 0x00 following EIP-191. diff --git a/contracts/utils/cryptography/ECDSA.sol b/contracts/utils/cryptography/ECDSA.sol index a499b546b91..77279eb4f18 100644 --- a/contracts/utils/cryptography/ECDSA.sol +++ b/contracts/utils/cryptography/ECDSA.sol @@ -204,4 +204,14 @@ library ECDSA { data := keccak256(ptr, 0x42) } } + + /** + * @dev Returns an Ethereum Signed Data with intended validator, created from a + * `validator` and `data` according to the version 0 of EIP-191. + * + * See {recover}. + */ + function toDataWithIntendedValidatorHash(address validator, bytes memory data) internal pure returns (bytes32) { + return keccak256(abi.encodePacked("\x19\x00", validator, data)); + } } diff --git a/test/helpers/sign.js b/test/helpers/sign.js index 417ef591dbb..d537116bbb8 100644 --- a/test/helpers/sign.js +++ b/test/helpers/sign.js @@ -4,6 +4,21 @@ function toEthSignedMessageHash(messageHex) { return web3.utils.sha3(Buffer.concat([prefix, messageBuffer])); } +/** + * Create a signed data with intended validator according to the version 0 of EIP-191 + * @param validatorAddress The address of the validator + * @param dataHex The data to be concatenated with the prefix and signed + */ +function toDataWithIntendedValidatorHash(validatorAddress, dataHex) { + const validatorBuffer = Buffer.from(web3.utils.hexToBytes(validatorAddress)); + const dataBuffer = Buffer.from(web3.utils.hexToBytes(dataHex)); + const preambleBuffer = Buffer.from('\x19'); + const versionBuffer = Buffer.from('\x00'); + const ethMessage = Buffer.concat([preambleBuffer, versionBuffer, validatorBuffer, dataBuffer]); + + return web3.utils.sha3(ethMessage); +} + /** * Create a signer between a contract and a signer for a voucher of method, args, and redeemer * Note that `method` is the web3 method, not the truffle-contract method @@ -43,5 +58,6 @@ const getSignFor = module.exports = { toEthSignedMessageHash, + toDataWithIntendedValidatorHash, getSignFor, }; diff --git a/test/utils/cryptography/ECDSA.test.js b/test/utils/cryptography/ECDSA.test.js index 3b19cde60dd..ae737086b12 100644 --- a/test/utils/cryptography/ECDSA.test.js +++ b/test/utils/cryptography/ECDSA.test.js @@ -1,5 +1,5 @@ const { expectRevert } = require('@openzeppelin/test-helpers'); -const { toEthSignedMessageHash } = require('../../helpers/sign'); +const { toEthSignedMessageHash, toDataWithIntendedValidatorHash } = require('../../helpers/sign'); const { expect } = require('chai'); @@ -8,6 +8,7 @@ const ECDSA = artifacts.require('$ECDSA'); const TEST_MESSAGE = web3.utils.sha3('OpenZeppelin'); const WRONG_MESSAGE = web3.utils.sha3('Nope'); const NON_HASH_MESSAGE = '0x' + Buffer.from('abcd').toString('hex'); +const RANDOM_ADDRESS = web3.utils.toChecksumAddress(web3.utils.randomHex(20)); function to2098Format(signature) { const long = web3.utils.hexToBytes(signature); @@ -248,4 +249,12 @@ contract('ECDSA', function (accounts) { ); }); }); + + context('toDataWithIntendedValidatorHash', function () { + it('returns the hash correctly', async function () { + expect( + await this.ecdsa.methods['$toDataWithIntendedValidatorHash(address,bytes)'](RANDOM_ADDRESS, NON_HASH_MESSAGE), + ).to.equal(toDataWithIntendedValidatorHash(RANDOM_ADDRESS, NON_HASH_MESSAGE)); + }); + }); });