Update docs
This commit is contained in:
245
test/utils/cryptography/ECDSA.test.js
Normal file
245
test/utils/cryptography/ECDSA.test.js
Normal file
@ -0,0 +1,245 @@
|
||||
require('@openzeppelin/test-helpers');
|
||||
const { expectRevertCustomError } = require('../../helpers/customError');
|
||||
const { toEthSignedMessageHash } = require('../../helpers/sign');
|
||||
|
||||
const { expect } = require('chai');
|
||||
|
||||
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');
|
||||
|
||||
function to2098Format(signature) {
|
||||
const long = web3.utils.hexToBytes(signature);
|
||||
if (long.length !== 65) {
|
||||
throw new Error('invalid signature length (expected long format)');
|
||||
}
|
||||
if (long[32] >> 7 === 1) {
|
||||
throw new Error("invalid signature 's' value");
|
||||
}
|
||||
const short = long.slice(0, 64);
|
||||
short[32] |= long[64] % 27 << 7; // set the first bit of the 32nd byte to the v parity bit
|
||||
return web3.utils.bytesToHex(short);
|
||||
}
|
||||
|
||||
function split(signature) {
|
||||
const raw = web3.utils.hexToBytes(signature);
|
||||
switch (raw.length) {
|
||||
case 64:
|
||||
return [
|
||||
web3.utils.bytesToHex(raw.slice(0, 32)), // r
|
||||
web3.utils.bytesToHex(raw.slice(32, 64)), // vs
|
||||
];
|
||||
case 65:
|
||||
return [
|
||||
raw[64], // v
|
||||
web3.utils.bytesToHex(raw.slice(0, 32)), // r
|
||||
web3.utils.bytesToHex(raw.slice(32, 64)), // s
|
||||
];
|
||||
default:
|
||||
expect.fail('Invalid signature length, cannot split');
|
||||
}
|
||||
}
|
||||
|
||||
contract('ECDSA', function (accounts) {
|
||||
const [other] = accounts;
|
||||
|
||||
beforeEach(async function () {
|
||||
this.ecdsa = await ECDSA.new();
|
||||
});
|
||||
|
||||
context('recover with invalid signature', function () {
|
||||
it('with short signature', async function () {
|
||||
await expectRevertCustomError(this.ecdsa.$recover(TEST_MESSAGE, '0x1234'), 'ECDSAInvalidSignatureLength', [2]);
|
||||
});
|
||||
|
||||
it('with long signature', async function () {
|
||||
await expectRevertCustomError(
|
||||
// eslint-disable-next-line max-len
|
||||
this.ecdsa.$recover(
|
||||
TEST_MESSAGE,
|
||||
'0x01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789',
|
||||
),
|
||||
'ECDSAInvalidSignatureLength',
|
||||
[85],
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
context('recover with valid signature', function () {
|
||||
context('using web3.eth.sign', function () {
|
||||
it('returns signer address with correct signature', async function () {
|
||||
// Create the signature
|
||||
const signature = await web3.eth.sign(TEST_MESSAGE, other);
|
||||
|
||||
// Recover the signer address from the generated message and signature.
|
||||
expect(await this.ecdsa.$recover(toEthSignedMessageHash(TEST_MESSAGE), signature)).to.equal(other);
|
||||
});
|
||||
|
||||
it('returns signer address with correct signature for arbitrary length message', async function () {
|
||||
// Create the signature
|
||||
const signature = await web3.eth.sign(NON_HASH_MESSAGE, other);
|
||||
|
||||
// Recover the signer address from the generated message and signature.
|
||||
expect(await this.ecdsa.$recover(toEthSignedMessageHash(NON_HASH_MESSAGE), signature)).to.equal(other);
|
||||
});
|
||||
|
||||
it('returns a different address', async function () {
|
||||
const signature = await web3.eth.sign(TEST_MESSAGE, other);
|
||||
expect(await this.ecdsa.$recover(WRONG_MESSAGE, signature)).to.not.equal(other);
|
||||
});
|
||||
|
||||
it('reverts with invalid signature', async function () {
|
||||
// eslint-disable-next-line max-len
|
||||
const signature =
|
||||
'0x332ce75a821c982f9127538858900d87d3ec1f9f737338ad67cad133fa48feff48e6fa0c18abc62e42820f05943e47af3e9fbe306ce74d64094bdf1691ee53e01c';
|
||||
await expectRevertCustomError(this.ecdsa.$recover(TEST_MESSAGE, signature), 'ECDSAInvalidSignature', []);
|
||||
});
|
||||
});
|
||||
|
||||
context('with v=27 signature', function () {
|
||||
// Signature generated outside ganache with method web3.eth.sign(signer, message)
|
||||
const signer = '0x2cc1166f6212628A0deEf2B33BEFB2187D35b86c';
|
||||
// eslint-disable-next-line max-len
|
||||
const signatureWithoutV =
|
||||
'0x5d99b6f7f6d1f73d1a26497f2b1c89b24c0993913f86e9a2d02cd69887d9c94f3c880358579d811b21dd1b7fd9bb01c1d81d10e69f0384e675c32b39643be892';
|
||||
|
||||
it('works with correct v value', async function () {
|
||||
const v = '1b'; // 27 = 1b.
|
||||
const signature = signatureWithoutV + v;
|
||||
expect(await this.ecdsa.$recover(TEST_MESSAGE, signature)).to.equal(signer);
|
||||
|
||||
expect(
|
||||
await this.ecdsa.methods['$recover(bytes32,uint8,bytes32,bytes32)'](TEST_MESSAGE, ...split(signature)),
|
||||
).to.equal(signer);
|
||||
|
||||
expect(
|
||||
await this.ecdsa.methods['$recover(bytes32,bytes32,bytes32)'](
|
||||
TEST_MESSAGE,
|
||||
...split(to2098Format(signature)),
|
||||
),
|
||||
).to.equal(signer);
|
||||
});
|
||||
|
||||
it('rejects incorrect v value', async function () {
|
||||
const v = '1c'; // 28 = 1c.
|
||||
const signature = signatureWithoutV + v;
|
||||
expect(await this.ecdsa.$recover(TEST_MESSAGE, signature)).to.not.equal(signer);
|
||||
|
||||
expect(
|
||||
await this.ecdsa.methods['$recover(bytes32,uint8,bytes32,bytes32)'](TEST_MESSAGE, ...split(signature)),
|
||||
).to.not.equal(signer);
|
||||
|
||||
expect(
|
||||
await this.ecdsa.methods['$recover(bytes32,bytes32,bytes32)'](
|
||||
TEST_MESSAGE,
|
||||
...split(to2098Format(signature)),
|
||||
),
|
||||
).to.not.equal(signer);
|
||||
});
|
||||
|
||||
it('reverts wrong v values', async function () {
|
||||
for (const v of ['00', '01']) {
|
||||
const signature = signatureWithoutV + v;
|
||||
await expectRevertCustomError(this.ecdsa.$recover(TEST_MESSAGE, signature), 'ECDSAInvalidSignature', []);
|
||||
|
||||
await expectRevertCustomError(
|
||||
this.ecdsa.methods['$recover(bytes32,uint8,bytes32,bytes32)'](TEST_MESSAGE, ...split(signature)),
|
||||
'ECDSAInvalidSignature',
|
||||
[],
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it('rejects short EIP2098 format', async function () {
|
||||
const v = '1b'; // 27 = 1b.
|
||||
const signature = signatureWithoutV + v;
|
||||
await expectRevertCustomError(
|
||||
this.ecdsa.$recover(TEST_MESSAGE, to2098Format(signature)),
|
||||
'ECDSAInvalidSignatureLength',
|
||||
[64],
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
context('with v=28 signature', function () {
|
||||
const signer = '0x1E318623aB09Fe6de3C9b8672098464Aeda9100E';
|
||||
// eslint-disable-next-line max-len
|
||||
const signatureWithoutV =
|
||||
'0x331fe75a821c982f9127538858900d87d3ec1f9f737338ad67cad133fa48feff48e6fa0c18abc62e42820f05943e47af3e9fbe306ce74d64094bdf1691ee53e0';
|
||||
|
||||
it('works with correct v value', async function () {
|
||||
const v = '1c'; // 28 = 1c.
|
||||
const signature = signatureWithoutV + v;
|
||||
expect(await this.ecdsa.$recover(TEST_MESSAGE, signature)).to.equal(signer);
|
||||
|
||||
expect(
|
||||
await this.ecdsa.methods['$recover(bytes32,uint8,bytes32,bytes32)'](TEST_MESSAGE, ...split(signature)),
|
||||
).to.equal(signer);
|
||||
|
||||
expect(
|
||||
await this.ecdsa.methods['$recover(bytes32,bytes32,bytes32)'](
|
||||
TEST_MESSAGE,
|
||||
...split(to2098Format(signature)),
|
||||
),
|
||||
).to.equal(signer);
|
||||
});
|
||||
|
||||
it('rejects incorrect v value', async function () {
|
||||
const v = '1b'; // 27 = 1b.
|
||||
const signature = signatureWithoutV + v;
|
||||
expect(await this.ecdsa.$recover(TEST_MESSAGE, signature)).to.not.equal(signer);
|
||||
|
||||
expect(
|
||||
await this.ecdsa.methods['$recover(bytes32,uint8,bytes32,bytes32)'](TEST_MESSAGE, ...split(signature)),
|
||||
).to.not.equal(signer);
|
||||
|
||||
expect(
|
||||
await this.ecdsa.methods['$recover(bytes32,bytes32,bytes32)'](
|
||||
TEST_MESSAGE,
|
||||
...split(to2098Format(signature)),
|
||||
),
|
||||
).to.not.equal(signer);
|
||||
});
|
||||
|
||||
it('reverts invalid v values', async function () {
|
||||
for (const v of ['00', '01']) {
|
||||
const signature = signatureWithoutV + v;
|
||||
await expectRevertCustomError(this.ecdsa.$recover(TEST_MESSAGE, signature), 'ECDSAInvalidSignature', []);
|
||||
|
||||
await expectRevertCustomError(
|
||||
this.ecdsa.methods['$recover(bytes32,uint8,bytes32,bytes32)'](TEST_MESSAGE, ...split(signature)),
|
||||
'ECDSAInvalidSignature',
|
||||
[],
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it('rejects short EIP2098 format', async function () {
|
||||
const v = '1c'; // 27 = 1b.
|
||||
const signature = signatureWithoutV + v;
|
||||
await expectRevertCustomError(
|
||||
this.ecdsa.$recover(TEST_MESSAGE, to2098Format(signature)),
|
||||
'ECDSAInvalidSignatureLength',
|
||||
[64],
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('reverts with high-s value signature', async function () {
|
||||
const message = '0xb94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9';
|
||||
// eslint-disable-next-line max-len
|
||||
const highSSignature =
|
||||
'0xe742ff452d41413616a5bf43fe15dd88294e983d3d36206c2712f39083d638bde0a0fc89be718fbc1033e1d30d78be1c68081562ed2e97af876f286f3453231d1b';
|
||||
const [r, v, s] = split(highSSignature);
|
||||
await expectRevertCustomError(this.ecdsa.$recover(message, highSSignature), 'ECDSAInvalidSignatureS', [s]);
|
||||
await expectRevertCustomError(
|
||||
this.ecdsa.methods['$recover(bytes32,uint8,bytes32,bytes32)'](TEST_MESSAGE, r, v, s),
|
||||
'ECDSAInvalidSignatureS',
|
||||
[s],
|
||||
);
|
||||
expect(() => to2098Format(highSSignature)).to.throw("invalid signature 's' value");
|
||||
});
|
||||
});
|
||||
});
|
||||
111
test/utils/cryptography/EIP712.test.js
Normal file
111
test/utils/cryptography/EIP712.test.js
Normal file
@ -0,0 +1,111 @@
|
||||
const ethSigUtil = require('eth-sig-util');
|
||||
const Wallet = require('ethereumjs-wallet').default;
|
||||
|
||||
const { getDomain, domainType, domainSeparator, hashTypedData } = require('../../helpers/eip712');
|
||||
const { getChainId } = require('../../helpers/chainid');
|
||||
const { mapValues } = require('../../helpers/iterate');
|
||||
|
||||
const EIP712Verifier = artifacts.require('$EIP712Verifier');
|
||||
const Clones = artifacts.require('$Clones');
|
||||
|
||||
contract('EIP712', function (accounts) {
|
||||
const [mailTo] = accounts;
|
||||
|
||||
const shortName = 'A Name';
|
||||
const shortVersion = '1';
|
||||
|
||||
const longName = 'A'.repeat(40);
|
||||
const longVersion = 'B'.repeat(40);
|
||||
|
||||
const cases = [
|
||||
['short', shortName, shortVersion],
|
||||
['long', longName, longVersion],
|
||||
];
|
||||
|
||||
for (const [shortOrLong, name, version] of cases) {
|
||||
describe(`with ${shortOrLong} name and version`, function () {
|
||||
beforeEach('deploying', async function () {
|
||||
this.eip712 = await EIP712Verifier.new(name, version);
|
||||
|
||||
this.domain = {
|
||||
name,
|
||||
version,
|
||||
chainId: await getChainId(),
|
||||
verifyingContract: this.eip712.address,
|
||||
};
|
||||
this.domainType = domainType(this.domain);
|
||||
});
|
||||
|
||||
describe('domain separator', function () {
|
||||
it('is internally available', async function () {
|
||||
const expected = await domainSeparator(this.domain);
|
||||
|
||||
expect(await this.eip712.$_domainSeparatorV4()).to.equal(expected);
|
||||
});
|
||||
|
||||
it("can be rebuilt using EIP-5267's eip712Domain", async function () {
|
||||
const rebuildDomain = await getDomain(this.eip712);
|
||||
expect(mapValues(rebuildDomain, String)).to.be.deep.equal(mapValues(this.domain, String));
|
||||
});
|
||||
|
||||
if (shortOrLong === 'short') {
|
||||
// Long strings are in storage, and the proxy will not be properly initialized unless
|
||||
// the upgradeable contract variant is used and the initializer is invoked.
|
||||
|
||||
it('adjusts when behind proxy', async function () {
|
||||
const factory = await Clones.new();
|
||||
const cloneReceipt = await factory.$clone(this.eip712.address);
|
||||
const cloneAddress = cloneReceipt.logs.find(({ event }) => event === 'return$clone').args.instance;
|
||||
const clone = new EIP712Verifier(cloneAddress);
|
||||
|
||||
const cloneDomain = { ...this.domain, verifyingContract: clone.address };
|
||||
|
||||
const reportedDomain = await getDomain(clone);
|
||||
expect(mapValues(reportedDomain, String)).to.be.deep.equal(mapValues(cloneDomain, String));
|
||||
|
||||
const expectedSeparator = await domainSeparator(cloneDomain);
|
||||
expect(await clone.$_domainSeparatorV4()).to.equal(expectedSeparator);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
it('hash digest', async function () {
|
||||
const structhash = web3.utils.randomHex(32);
|
||||
expect(await this.eip712.$_hashTypedDataV4(structhash)).to.be.equal(hashTypedData(this.domain, structhash));
|
||||
});
|
||||
|
||||
it('digest', async function () {
|
||||
const message = {
|
||||
to: mailTo,
|
||||
contents: 'very interesting',
|
||||
};
|
||||
|
||||
const data = {
|
||||
types: {
|
||||
EIP712Domain: this.domainType,
|
||||
Mail: [
|
||||
{ name: 'to', type: 'address' },
|
||||
{ name: 'contents', type: 'string' },
|
||||
],
|
||||
},
|
||||
domain: this.domain,
|
||||
primaryType: 'Mail',
|
||||
message,
|
||||
};
|
||||
|
||||
const wallet = Wallet.generate();
|
||||
const signature = ethSigUtil.signTypedMessage(wallet.getPrivateKey(), { data });
|
||||
|
||||
await this.eip712.verify(signature, wallet.getAddressString(), message.to, message.contents);
|
||||
});
|
||||
|
||||
it('name', async function () {
|
||||
expect(await this.eip712.$_EIP712Name()).to.be.equal(name);
|
||||
});
|
||||
|
||||
it('version', async function () {
|
||||
expect(await this.eip712.$_EIP712Version()).to.be.equal(version);
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
207
test/utils/cryptography/MerkleProof.test.js
Normal file
207
test/utils/cryptography/MerkleProof.test.js
Normal file
@ -0,0 +1,207 @@
|
||||
const { expectRevert } = require('@openzeppelin/test-helpers');
|
||||
|
||||
const { MerkleTree } = require('merkletreejs');
|
||||
const keccak256 = require('keccak256');
|
||||
|
||||
const { expect } = require('chai');
|
||||
const { expectRevertCustomError } = require('../../helpers/customError');
|
||||
|
||||
const MerkleProof = artifacts.require('$MerkleProof');
|
||||
|
||||
contract('MerkleProof', function () {
|
||||
beforeEach(async function () {
|
||||
this.merkleProof = await MerkleProof.new();
|
||||
});
|
||||
|
||||
describe('verify', function () {
|
||||
it('returns true for a valid Merkle proof', async function () {
|
||||
const elements = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='.split('');
|
||||
const merkleTree = new MerkleTree(elements, keccak256, { hashLeaves: true, sortPairs: true });
|
||||
|
||||
const root = merkleTree.getHexRoot();
|
||||
|
||||
const leaf = keccak256(elements[0]);
|
||||
|
||||
const proof = merkleTree.getHexProof(leaf);
|
||||
|
||||
expect(await this.merkleProof.$verify(proof, root, leaf)).to.equal(true);
|
||||
expect(await this.merkleProof.$verifyCalldata(proof, root, leaf)).to.equal(true);
|
||||
|
||||
// For demonstration, it is also possible to create valid proofs for certain 64-byte values *not* in elements:
|
||||
const noSuchLeaf = keccak256(
|
||||
Buffer.concat([keccak256(elements[0]), keccak256(elements[1])].sort(Buffer.compare)),
|
||||
);
|
||||
expect(await this.merkleProof.$verify(proof.slice(1), root, noSuchLeaf)).to.equal(true);
|
||||
expect(await this.merkleProof.$verifyCalldata(proof.slice(1), root, noSuchLeaf)).to.equal(true);
|
||||
});
|
||||
|
||||
it('returns false for an invalid Merkle proof', async function () {
|
||||
const correctElements = ['a', 'b', 'c'];
|
||||
const correctMerkleTree = new MerkleTree(correctElements, keccak256, { hashLeaves: true, sortPairs: true });
|
||||
|
||||
const correctRoot = correctMerkleTree.getHexRoot();
|
||||
|
||||
const correctLeaf = keccak256(correctElements[0]);
|
||||
|
||||
const badElements = ['d', 'e', 'f'];
|
||||
const badMerkleTree = new MerkleTree(badElements);
|
||||
|
||||
const badProof = badMerkleTree.getHexProof(badElements[0]);
|
||||
|
||||
expect(await this.merkleProof.$verify(badProof, correctRoot, correctLeaf)).to.equal(false);
|
||||
expect(await this.merkleProof.$verifyCalldata(badProof, correctRoot, correctLeaf)).to.equal(false);
|
||||
});
|
||||
|
||||
it('returns false for a Merkle proof of invalid length', async function () {
|
||||
const elements = ['a', 'b', 'c'];
|
||||
const merkleTree = new MerkleTree(elements, keccak256, { hashLeaves: true, sortPairs: true });
|
||||
|
||||
const root = merkleTree.getHexRoot();
|
||||
|
||||
const leaf = keccak256(elements[0]);
|
||||
|
||||
const proof = merkleTree.getHexProof(leaf);
|
||||
const badProof = proof.slice(0, proof.length - 5);
|
||||
|
||||
expect(await this.merkleProof.$verify(badProof, root, leaf)).to.equal(false);
|
||||
expect(await this.merkleProof.$verifyCalldata(badProof, root, leaf)).to.equal(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('multiProofVerify', function () {
|
||||
it('returns true for a valid Merkle multi proof', async function () {
|
||||
const leaves = ['a', 'b', 'c', 'd', 'e', 'f'].map(keccak256).sort(Buffer.compare);
|
||||
const merkleTree = new MerkleTree(leaves, keccak256, { sort: true });
|
||||
|
||||
const root = merkleTree.getRoot();
|
||||
const proofLeaves = ['b', 'f', 'd'].map(keccak256).sort(Buffer.compare);
|
||||
const proof = merkleTree.getMultiProof(proofLeaves);
|
||||
const proofFlags = merkleTree.getProofFlags(proofLeaves, proof);
|
||||
|
||||
expect(await this.merkleProof.$multiProofVerify(proof, proofFlags, root, proofLeaves)).to.equal(true);
|
||||
expect(await this.merkleProof.$multiProofVerifyCalldata(proof, proofFlags, root, proofLeaves)).to.equal(true);
|
||||
});
|
||||
|
||||
it('returns false for an invalid Merkle multi proof', async function () {
|
||||
const leaves = ['a', 'b', 'c', 'd', 'e', 'f'].map(keccak256).sort(Buffer.compare);
|
||||
const merkleTree = new MerkleTree(leaves, keccak256, { sort: true });
|
||||
|
||||
const root = merkleTree.getRoot();
|
||||
const badProofLeaves = ['g', 'h', 'i'].map(keccak256).sort(Buffer.compare);
|
||||
const badMerkleTree = new MerkleTree(badProofLeaves);
|
||||
const badProof = badMerkleTree.getMultiProof(badProofLeaves);
|
||||
const badProofFlags = badMerkleTree.getProofFlags(badProofLeaves, badProof);
|
||||
|
||||
expect(await this.merkleProof.$multiProofVerify(badProof, badProofFlags, root, badProofLeaves)).to.equal(false);
|
||||
expect(await this.merkleProof.$multiProofVerifyCalldata(badProof, badProofFlags, root, badProofLeaves)).to.equal(
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
it('revert with invalid multi proof #1', async function () {
|
||||
const fill = Buffer.alloc(32); // This could be anything, we are reconstructing a fake branch
|
||||
const leaves = ['a', 'b', 'c', 'd'].map(keccak256).sort(Buffer.compare);
|
||||
const badLeaf = keccak256('e');
|
||||
const merkleTree = new MerkleTree(leaves, keccak256, { sort: true });
|
||||
|
||||
const root = merkleTree.getRoot();
|
||||
|
||||
await expectRevertCustomError(
|
||||
this.merkleProof.$multiProofVerify(
|
||||
[leaves[1], fill, merkleTree.layers[1][1]],
|
||||
[false, false, false],
|
||||
root,
|
||||
[leaves[0], badLeaf], // A, E
|
||||
),
|
||||
'MerkleProofInvalidMultiproof',
|
||||
[],
|
||||
);
|
||||
await expectRevertCustomError(
|
||||
this.merkleProof.$multiProofVerifyCalldata(
|
||||
[leaves[1], fill, merkleTree.layers[1][1]],
|
||||
[false, false, false],
|
||||
root,
|
||||
[leaves[0], badLeaf], // A, E
|
||||
),
|
||||
'MerkleProofInvalidMultiproof',
|
||||
[],
|
||||
);
|
||||
});
|
||||
|
||||
it('revert with invalid multi proof #2', async function () {
|
||||
const fill = Buffer.alloc(32); // This could be anything, we are reconstructing a fake branch
|
||||
const leaves = ['a', 'b', 'c', 'd'].map(keccak256).sort(Buffer.compare);
|
||||
const badLeaf = keccak256('e');
|
||||
const merkleTree = new MerkleTree(leaves, keccak256, { sort: true });
|
||||
|
||||
const root = merkleTree.getRoot();
|
||||
|
||||
await expectRevert(
|
||||
this.merkleProof.$multiProofVerify(
|
||||
[leaves[1], fill, merkleTree.layers[1][1]],
|
||||
[false, false, false, false],
|
||||
root,
|
||||
[badLeaf, leaves[0]], // A, E
|
||||
),
|
||||
'reverted with panic code 0x32',
|
||||
);
|
||||
|
||||
await expectRevert(
|
||||
this.merkleProof.$multiProofVerifyCalldata(
|
||||
[leaves[1], fill, merkleTree.layers[1][1]],
|
||||
[false, false, false, false],
|
||||
root,
|
||||
[badLeaf, leaves[0]], // A, E
|
||||
),
|
||||
'reverted with panic code 0x32',
|
||||
);
|
||||
});
|
||||
|
||||
it('limit case: works for tree containing a single leaf', async function () {
|
||||
const leaves = ['a'].map(keccak256).sort(Buffer.compare);
|
||||
const merkleTree = new MerkleTree(leaves, keccak256, { sort: true });
|
||||
|
||||
const root = merkleTree.getRoot();
|
||||
const proofLeaves = ['a'].map(keccak256).sort(Buffer.compare);
|
||||
const proof = merkleTree.getMultiProof(proofLeaves);
|
||||
const proofFlags = merkleTree.getProofFlags(proofLeaves, proof);
|
||||
|
||||
expect(await this.merkleProof.$multiProofVerify(proof, proofFlags, root, proofLeaves)).to.equal(true);
|
||||
expect(await this.merkleProof.$multiProofVerifyCalldata(proof, proofFlags, root, proofLeaves)).to.equal(true);
|
||||
});
|
||||
|
||||
it('limit case: can prove empty leaves', async function () {
|
||||
const leaves = ['a', 'b', 'c', 'd'].map(keccak256).sort(Buffer.compare);
|
||||
const merkleTree = new MerkleTree(leaves, keccak256, { sort: true });
|
||||
|
||||
const root = merkleTree.getRoot();
|
||||
expect(await this.merkleProof.$multiProofVerify([root], [], root, [])).to.equal(true);
|
||||
expect(await this.merkleProof.$multiProofVerifyCalldata([root], [], root, [])).to.equal(true);
|
||||
});
|
||||
|
||||
it('reverts processing manipulated proofs with a zero-value node at depth 1', async function () {
|
||||
// Create a merkle tree that contains a zero leaf at depth 1
|
||||
const leaves = [keccak256('real leaf'), Buffer.alloc(32, 0)];
|
||||
const merkleTree = new MerkleTree(leaves, keccak256, { sortPairs: true });
|
||||
|
||||
const root = merkleTree.getRoot();
|
||||
|
||||
// Now we can pass any **malicious** fake leaves as valid!
|
||||
const maliciousLeaves = ['malicious', 'leaves'].map(keccak256).sort(Buffer.compare);
|
||||
const maliciousProof = [leaves[0], leaves[0]];
|
||||
const maliciousProofFlags = [true, true, false];
|
||||
|
||||
await expectRevertCustomError(
|
||||
this.merkleProof.$multiProofVerify(maliciousProof, maliciousProofFlags, root, maliciousLeaves),
|
||||
'MerkleProofInvalidMultiproof',
|
||||
[],
|
||||
);
|
||||
|
||||
await expectRevertCustomError(
|
||||
this.merkleProof.$multiProofVerifyCalldata(maliciousProof, maliciousProofFlags, root, maliciousLeaves),
|
||||
'MerkleProofInvalidMultiproof',
|
||||
[],
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
55
test/utils/cryptography/MessageHashUtils.test.js
Normal file
55
test/utils/cryptography/MessageHashUtils.test.js
Normal file
@ -0,0 +1,55 @@
|
||||
require('@openzeppelin/test-helpers');
|
||||
const { toEthSignedMessageHash, toDataWithIntendedValidatorHash } = require('../../helpers/sign');
|
||||
const { domainSeparator, hashTypedData } = require('../../helpers/eip712');
|
||||
|
||||
const { expect } = require('chai');
|
||||
|
||||
const MessageHashUtils = artifacts.require('$MessageHashUtils');
|
||||
|
||||
contract('MessageHashUtils', function () {
|
||||
beforeEach(async function () {
|
||||
this.messageHashUtils = await MessageHashUtils.new();
|
||||
|
||||
this.message = '0x' + Buffer.from('abcd').toString('hex');
|
||||
this.messageHash = web3.utils.sha3(this.message);
|
||||
this.verifyingAddress = web3.utils.toChecksumAddress(web3.utils.randomHex(20));
|
||||
});
|
||||
|
||||
context('toEthSignedMessageHash', function () {
|
||||
it('prefixes bytes32 data correctly', async function () {
|
||||
expect(await this.messageHashUtils.methods['$toEthSignedMessageHash(bytes32)'](this.messageHash)).to.equal(
|
||||
toEthSignedMessageHash(this.messageHash),
|
||||
);
|
||||
});
|
||||
|
||||
it('prefixes dynamic length data correctly', async function () {
|
||||
expect(await this.messageHashUtils.methods['$toEthSignedMessageHash(bytes)'](this.message)).to.equal(
|
||||
toEthSignedMessageHash(this.message),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
context('toDataWithIntendedValidatorHash', function () {
|
||||
it('returns the digest correctly', async function () {
|
||||
expect(
|
||||
await this.messageHashUtils.$toDataWithIntendedValidatorHash(this.verifyingAddress, this.message),
|
||||
).to.equal(toDataWithIntendedValidatorHash(this.verifyingAddress, this.message));
|
||||
});
|
||||
});
|
||||
|
||||
context('toTypedDataHash', function () {
|
||||
it('returns the digest correctly', async function () {
|
||||
const domain = {
|
||||
name: 'Test',
|
||||
version: 1,
|
||||
chainId: 1,
|
||||
verifyingContract: this.verifyingAddress,
|
||||
};
|
||||
const structhash = web3.utils.randomHex(32);
|
||||
const expectedDomainSeparator = await domainSeparator(domain);
|
||||
expect(await this.messageHashUtils.$toTypedDataHash(expectedDomainSeparator, structhash)).to.equal(
|
||||
hashTypedData(domain, structhash),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
87
test/utils/cryptography/SignatureChecker.test.js
Normal file
87
test/utils/cryptography/SignatureChecker.test.js
Normal file
@ -0,0 +1,87 @@
|
||||
const { toEthSignedMessageHash } = require('../../helpers/sign');
|
||||
|
||||
const { expect } = require('chai');
|
||||
|
||||
const SignatureChecker = artifacts.require('$SignatureChecker');
|
||||
const ERC1271WalletMock = artifacts.require('ERC1271WalletMock');
|
||||
const ERC1271MaliciousMock = artifacts.require('ERC1271MaliciousMock');
|
||||
|
||||
const TEST_MESSAGE = web3.utils.sha3('OpenZeppelin');
|
||||
const WRONG_MESSAGE = web3.utils.sha3('Nope');
|
||||
|
||||
contract('SignatureChecker (ERC1271)', function (accounts) {
|
||||
const [signer, other] = accounts;
|
||||
|
||||
before('deploying', async function () {
|
||||
this.signaturechecker = await SignatureChecker.new();
|
||||
this.wallet = await ERC1271WalletMock.new(signer);
|
||||
this.malicious = await ERC1271MaliciousMock.new();
|
||||
this.signature = await web3.eth.sign(TEST_MESSAGE, signer);
|
||||
});
|
||||
|
||||
context('EOA account', function () {
|
||||
it('with matching signer and signature', async function () {
|
||||
expect(
|
||||
await this.signaturechecker.$isValidSignatureNow(signer, toEthSignedMessageHash(TEST_MESSAGE), this.signature),
|
||||
).to.equal(true);
|
||||
});
|
||||
|
||||
it('with invalid signer', async function () {
|
||||
expect(
|
||||
await this.signaturechecker.$isValidSignatureNow(other, toEthSignedMessageHash(TEST_MESSAGE), this.signature),
|
||||
).to.equal(false);
|
||||
});
|
||||
|
||||
it('with invalid signature', async function () {
|
||||
expect(
|
||||
await this.signaturechecker.$isValidSignatureNow(signer, toEthSignedMessageHash(WRONG_MESSAGE), this.signature),
|
||||
).to.equal(false);
|
||||
});
|
||||
});
|
||||
|
||||
context('ERC1271 wallet', function () {
|
||||
for (const signature of ['isValidERC1271SignatureNow', 'isValidSignatureNow']) {
|
||||
context(signature, function () {
|
||||
it('with matching signer and signature', async function () {
|
||||
expect(
|
||||
await this.signaturechecker[`$${signature}`](
|
||||
this.wallet.address,
|
||||
toEthSignedMessageHash(TEST_MESSAGE),
|
||||
this.signature,
|
||||
),
|
||||
).to.equal(true);
|
||||
});
|
||||
|
||||
it('with invalid signer', async function () {
|
||||
expect(
|
||||
await this.signaturechecker[`$${signature}`](
|
||||
this.signaturechecker.address,
|
||||
toEthSignedMessageHash(TEST_MESSAGE),
|
||||
this.signature,
|
||||
),
|
||||
).to.equal(false);
|
||||
});
|
||||
|
||||
it('with invalid signature', async function () {
|
||||
expect(
|
||||
await this.signaturechecker[`$${signature}`](
|
||||
this.wallet.address,
|
||||
toEthSignedMessageHash(WRONG_MESSAGE),
|
||||
this.signature,
|
||||
),
|
||||
).to.equal(false);
|
||||
});
|
||||
|
||||
it('with malicious wallet', async function () {
|
||||
expect(
|
||||
await this.signaturechecker[`$${signature}`](
|
||||
this.malicious.address,
|
||||
toEthSignedMessageHash(TEST_MESSAGE),
|
||||
this.signature,
|
||||
),
|
||||
).to.equal(false);
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user