This commit is contained in:
dexorder
2024-10-17 02:42:28 -04:00
commit 25def69c66
878 changed files with 112489 additions and 0 deletions

View File

@@ -0,0 +1,280 @@
const { ethers } = require('hardhat');
const { expect } = require('chai');
const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
const { PANIC_CODES } = require('@nomicfoundation/hardhat-chai-matchers/panic');
const coder = ethers.AbiCoder.defaultAbiCoder();
async function fixture() {
const [recipient, other] = await ethers.getSigners();
const mock = await ethers.deployContract('$Address');
const target = await ethers.deployContract('CallReceiverMock');
const targetEther = await ethers.deployContract('EtherReceiverMock');
return { recipient, other, mock, target, targetEther };
}
describe('Address', function () {
beforeEach(async function () {
Object.assign(this, await loadFixture(fixture));
});
describe('sendValue', function () {
describe('when sender contract has no funds', function () {
it('sends 0 wei', async function () {
await expect(this.mock.$sendValue(this.other, 0n)).to.changeEtherBalance(this.recipient, 0n);
});
it('reverts when sending non-zero amounts', async function () {
await expect(this.mock.$sendValue(this.other, 1n))
.to.be.revertedWithCustomError(this.mock, 'InsufficientBalance')
.withArgs(0n, 1n);
});
});
describe('when sender contract has funds', function () {
const funds = ethers.parseEther('1');
beforeEach(async function () {
await this.other.sendTransaction({ to: this.mock, value: funds });
});
describe('with EOA recipient', function () {
it('sends 0 wei', async function () {
await expect(this.mock.$sendValue(this.recipient, 0n)).to.changeEtherBalance(this.recipient, 0n);
});
it('sends non-zero amounts', async function () {
await expect(this.mock.$sendValue(this.recipient, funds - 1n)).to.changeEtherBalance(
this.recipient,
funds - 1n,
);
});
it('sends the whole balance', async function () {
await expect(this.mock.$sendValue(this.recipient, funds)).to.changeEtherBalance(this.recipient, funds);
expect(await ethers.provider.getBalance(this.mock)).to.equal(0n);
});
it('reverts when sending more than the balance', async function () {
await expect(this.mock.$sendValue(this.recipient, funds + 1n))
.to.be.revertedWithCustomError(this.mock, 'InsufficientBalance')
.withArgs(funds, funds + 1n);
});
});
describe('with contract recipient', function () {
it('sends funds', async function () {
await this.targetEther.setAcceptEther(true);
await expect(this.mock.$sendValue(this.targetEther, funds)).to.changeEtherBalance(this.targetEther, funds);
});
it('reverts on recipient revert', async function () {
await this.targetEther.setAcceptEther(false);
await expect(this.mock.$sendValue(this.targetEther, funds)).to.be.revertedWithCustomError(
this.mock,
'FailedCall',
);
});
});
});
});
describe('functionCall', function () {
describe('with valid contract receiver', function () {
it('calls the requested function', async function () {
const call = this.target.interface.encodeFunctionData('mockFunction');
await expect(this.mock.$functionCall(this.target, call))
.to.emit(this.target, 'MockFunctionCalled')
.to.emit(this.mock, 'return$functionCall')
.withArgs(coder.encode(['string'], ['0x1234']));
});
it('calls the requested empty return function', async function () {
const call = this.target.interface.encodeFunctionData('mockFunctionEmptyReturn');
await expect(this.mock.$functionCall(this.target, call)).to.emit(this.target, 'MockFunctionCalled');
});
it('reverts when the called function reverts with no reason', async function () {
const call = this.target.interface.encodeFunctionData('mockFunctionRevertsNoReason');
await expect(this.mock.$functionCall(this.target, call)).to.be.revertedWithCustomError(this.mock, 'FailedCall');
});
it('reverts when the called function reverts, bubbling up the revert reason', async function () {
const call = this.target.interface.encodeFunctionData('mockFunctionRevertsReason');
await expect(this.mock.$functionCall(this.target, call)).to.be.revertedWith('CallReceiverMock: reverting');
});
it('reverts when the called function runs out of gas', async function () {
const call = this.target.interface.encodeFunctionData('mockFunctionOutOfGas');
await expect(this.mock.$functionCall(this.target, call, { gasLimit: 120_000n })).to.be.revertedWithCustomError(
this.mock,
'FailedCall',
);
});
it('reverts when the called function throws', async function () {
const call = this.target.interface.encodeFunctionData('mockFunctionThrows');
await expect(this.mock.$functionCall(this.target, call)).to.be.revertedWithPanic(PANIC_CODES.ASSERTION_ERROR);
});
it('reverts when function does not exist', async function () {
const interface = new ethers.Interface(['function mockFunctionDoesNotExist()']);
const call = interface.encodeFunctionData('mockFunctionDoesNotExist');
await expect(this.mock.$functionCall(this.target, call)).to.be.revertedWithCustomError(this.mock, 'FailedCall');
});
});
describe('with non-contract receiver', function () {
it('reverts when address is not a contract', async function () {
const call = this.target.interface.encodeFunctionData('mockFunction');
await expect(this.mock.$functionCall(this.recipient, call))
.to.be.revertedWithCustomError(this.mock, 'AddressEmptyCode')
.withArgs(this.recipient);
});
});
});
describe('functionCallWithValue', function () {
describe('with zero value', function () {
it('calls the requested function', async function () {
const call = this.target.interface.encodeFunctionData('mockFunction');
await expect(this.mock.$functionCallWithValue(this.target, call, 0n))
.to.emit(this.target, 'MockFunctionCalled')
.to.emit(this.mock, 'return$functionCallWithValue')
.withArgs(coder.encode(['string'], ['0x1234']));
});
});
describe('with non-zero value', function () {
const value = ethers.parseEther('1.2');
it('reverts if insufficient sender balance', async function () {
const call = this.target.interface.encodeFunctionData('mockFunction');
await expect(this.mock.$functionCallWithValue(this.target, call, value))
.to.be.revertedWithCustomError(this.mock, 'InsufficientBalance')
.withArgs(0n, value);
});
it('calls the requested function with existing value', async function () {
await this.other.sendTransaction({ to: this.mock, value });
const call = this.target.interface.encodeFunctionData('mockFunction');
const tx = await this.mock.$functionCallWithValue(this.target, call, value);
await expect(tx).to.changeEtherBalance(this.target, value);
await expect(tx)
.to.emit(this.target, 'MockFunctionCalled')
.to.emit(this.mock, 'return$functionCallWithValue')
.withArgs(coder.encode(['string'], ['0x1234']));
});
it('calls the requested function with transaction funds', async function () {
expect(await ethers.provider.getBalance(this.mock)).to.equal(0n);
const call = this.target.interface.encodeFunctionData('mockFunction');
const tx = await this.mock.connect(this.other).$functionCallWithValue(this.target, call, value, { value });
await expect(tx).to.changeEtherBalance(this.target, value);
await expect(tx)
.to.emit(this.target, 'MockFunctionCalled')
.to.emit(this.mock, 'return$functionCallWithValue')
.withArgs(coder.encode(['string'], ['0x1234']));
});
it('reverts when calling non-payable functions', async function () {
await this.other.sendTransaction({ to: this.mock, value });
const call = this.target.interface.encodeFunctionData('mockFunctionNonPayable');
await expect(this.mock.$functionCallWithValue(this.target, call, value)).to.be.revertedWithCustomError(
this.mock,
'FailedCall',
);
});
});
});
describe('functionStaticCall', function () {
it('calls the requested function', async function () {
const call = this.target.interface.encodeFunctionData('mockStaticFunction');
expect(await this.mock.$functionStaticCall(this.target, call)).to.equal(coder.encode(['string'], ['0x1234']));
});
it('reverts on a non-static function', async function () {
const call = this.target.interface.encodeFunctionData('mockFunction');
await expect(this.mock.$functionStaticCall(this.target, call)).to.be.revertedWithCustomError(
this.mock,
'FailedCall',
);
});
it('bubbles up revert reason', async function () {
const call = this.target.interface.encodeFunctionData('mockFunctionRevertsReason');
await expect(this.mock.$functionStaticCall(this.target, call)).to.be.revertedWith('CallReceiverMock: reverting');
});
it('reverts when address is not a contract', async function () {
const call = this.target.interface.encodeFunctionData('mockFunction');
await expect(this.mock.$functionStaticCall(this.recipient, call))
.to.be.revertedWithCustomError(this.mock, 'AddressEmptyCode')
.withArgs(this.recipient);
});
});
describe('functionDelegateCall', function () {
it('delegate calls the requested function', async function () {
const slot = ethers.hexlify(ethers.randomBytes(32));
const value = ethers.hexlify(ethers.randomBytes(32));
const call = this.target.interface.encodeFunctionData('mockFunctionWritesStorage', [slot, value]);
expect(await ethers.provider.getStorage(this.mock, slot)).to.equal(ethers.ZeroHash);
await expect(await this.mock.$functionDelegateCall(this.target, call))
.to.emit(this.mock, 'return$functionDelegateCall')
.withArgs(coder.encode(['string'], ['0x1234']));
expect(await ethers.provider.getStorage(this.mock, slot)).to.equal(value);
});
it('bubbles up revert reason', async function () {
const call = this.target.interface.encodeFunctionData('mockFunctionRevertsReason');
await expect(this.mock.$functionDelegateCall(this.target, call)).to.be.revertedWith(
'CallReceiverMock: reverting',
);
});
it('reverts when address is not a contract', async function () {
const call = this.target.interface.encodeFunctionData('mockFunction');
await expect(this.mock.$functionDelegateCall(this.recipient, call))
.to.be.revertedWithCustomError(this.mock, 'AddressEmptyCode')
.withArgs(this.recipient);
});
});
describe('verifyCallResult', function () {
it('returns returndata on success', async function () {
const returndata = '0x123abc';
expect(await this.mock.$verifyCallResult(true, returndata)).to.equal(returndata);
});
});
});

View File

@@ -0,0 +1,15 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import {Test} from "@forge-std/Test.sol";
import {Arrays} from "@openzeppelin/contracts/utils/Arrays.sol";
contract ArraysTest is Test {
function testSort(uint256[] memory values) public {
Arrays.sort(values);
for (uint256 i = 1; i < values.length; ++i) {
assertLe(values[i - 1], values[i]);
}
}
}

View File

@@ -0,0 +1,223 @@
const { ethers } = require('hardhat');
const { expect } = require('chai');
const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
const { generators } = require('../helpers/random');
const { capitalize } = require('../../scripts/helpers');
const { TYPES } = require('../../scripts/generate/templates/Arrays.opts');
// See https://en.cppreference.com/w/cpp/algorithm/lower_bound
const lowerBound = (array, value) => {
const i = array.findIndex(element => value <= element);
return i == -1 ? array.length : i;
};
// See https://en.cppreference.com/w/cpp/algorithm/upper_bound
const upperBound = (array, value) => {
const i = array.findIndex(element => value < element);
return i == -1 ? array.length : i;
};
const bigintSign = x => (x > 0n ? 1 : x < 0n ? -1 : 0);
const comparator = (a, b) => bigintSign(ethers.toBigInt(a) - ethers.toBigInt(b));
const hasDuplicates = array => array.some((v, i) => array.indexOf(v) != i);
describe('Arrays', function () {
const fixture = async () => {
return { mock: await ethers.deployContract('$Arrays') };
};
beforeEach(async function () {
Object.assign(this, await loadFixture(fixture));
});
describe('search', function () {
for (const [title, { array, tests }] of Object.entries({
'Even number of elements': {
array: [11n, 12n, 13n, 14n, 15n, 16n, 17n, 18n, 19n, 20n],
tests: {
'basic case': 16n,
'first element': 11n,
'last element': 20n,
'searched value is over the upper boundary': 32n,
'searched value is under the lower boundary': 2n,
},
},
'Odd number of elements': {
array: [11n, 12n, 13n, 14n, 15n, 16n, 17n, 18n, 19n, 20n, 21n],
tests: {
'basic case': 16n,
'first element': 11n,
'last element': 21n,
'searched value is over the upper boundary': 32n,
'searched value is under the lower boundary': 2n,
},
},
'Array with gap': {
array: [11n, 12n, 13n, 14n, 15n, 20n, 21n, 22n, 23n, 24n],
tests: {
'search value in gap': 17n,
},
},
'Array with duplicated elements': {
array: [0n, 10n, 10n, 10n, 10n, 10n, 10n, 10n, 20n],
tests: {
'search value is duplicated': 10n,
},
},
'Array with duplicated first element': {
array: [10n, 10n, 10n, 10n, 10n, 10n, 10n, 20n],
tests: {
'search value is duplicated first element': 10n,
},
},
'Array with duplicated last element': {
array: [0n, 10n, 10n, 10n, 10n, 10n, 10n, 10n],
tests: {
'search value is duplicated last element': 10n,
},
},
'Empty array': {
array: [],
tests: {
'always returns 0 for empty array': 10n,
},
},
})) {
describe(title, function () {
const fixture = async () => {
return { instance: await ethers.deployContract('Uint256ArraysMock', [array]) };
};
beforeEach(async function () {
Object.assign(this, await loadFixture(fixture));
});
for (const [name, input] of Object.entries(tests)) {
describe(name, function () {
it('[deprecated] findUpperBound', async function () {
// findUpperBound does not support duplicated
if (hasDuplicates(array)) {
expect(await this.instance.findUpperBound(input)).to.equal(upperBound(array, input) - 1);
} else {
expect(await this.instance.findUpperBound(input)).to.equal(lowerBound(array, input));
}
});
it('lowerBound', async function () {
expect(await this.instance.lowerBound(input)).to.equal(lowerBound(array, input));
expect(await this.instance.lowerBoundMemory(array, input)).to.equal(lowerBound(array, input));
});
it('upperBound', async function () {
expect(await this.instance.upperBound(input)).to.equal(upperBound(array, input));
expect(await this.instance.upperBoundMemory(array, input)).to.equal(upperBound(array, input));
});
});
}
});
}
});
for (const type of TYPES) {
const elements = Array.from({ length: 10 }, generators[type]);
describe(type, function () {
const fixture = async () => {
return { instance: await ethers.deployContract(`${capitalize(type)}ArraysMock`, [elements]) };
};
beforeEach(async function () {
Object.assign(this, await loadFixture(fixture));
});
describe('sort', function () {
for (const length of [0, 1, 2, 8, 32, 128]) {
describe(`${type}[] of length ${length}`, function () {
beforeEach(async function () {
this.array = Array.from({ length }, generators[type]);
});
afterEach(async function () {
const expected = Array.from(this.array).sort(comparator);
const reversed = Array.from(expected).reverse();
expect(await this.instance.sort(this.array)).to.deep.equal(expected);
expect(await this.instance.sortReverse(this.array)).to.deep.equal(reversed);
});
it('sort array', async function () {
// nothing to do here, beforeEach and afterEach already take care of everything.
});
if (length > 1) {
it('sort array for identical elements', async function () {
// duplicate the first value to all elements
this.array.fill(this.array.at(0));
});
it('sort already sorted array', async function () {
// pre-sort the elements
this.array.sort(comparator);
});
it('sort reversed array', async function () {
// pre-sort in reverse order
this.array.sort(comparator).reverse();
});
it('sort almost sorted array', async function () {
// pre-sort + rotate (move the last element to the front) for an almost sorted effect
this.array.sort(comparator);
this.array.unshift(this.array.pop());
});
}
});
}
});
describe('unsafeAccess', function () {
describe('storage', function () {
for (const i in elements) {
it(`unsafeAccess within bounds #${i}`, async function () {
expect(await this.instance.unsafeAccess(i)).to.equal(elements[i]);
});
}
it('unsafeAccess outside bounds', async function () {
await expect(this.instance.unsafeAccess(elements.length)).to.not.be.rejected;
});
it('unsafeSetLength changes the length or the array', async function () {
const newLength = generators.uint256();
expect(await this.instance.length()).to.equal(elements.length);
await expect(this.instance.unsafeSetLength(newLength)).to.not.be.rejected;
expect(await this.instance.length()).to.equal(newLength);
});
});
describe('memory', function () {
const fragment = `$unsafeMemoryAccess(${type}[] arr, uint256 pos)`;
for (const i in elements) {
it(`unsafeMemoryAccess within bounds #${i}`, async function () {
expect(await this.mock[fragment](elements, i)).to.equal(elements[i]);
});
}
it('unsafeMemoryAccess outside bounds', async function () {
await expect(this.mock[fragment](elements, elements.length)).to.not.be.rejected;
});
it('unsafeMemoryAccess loop around', async function () {
for (let i = 251n; i < 256n; ++i) {
expect(await this.mock[fragment](elements, 2n ** i - 1n)).to.equal(BigInt(elements.length));
expect(await this.mock[fragment](elements, 2n ** i + 0n)).to.equal(elements[0]);
expect(await this.mock[fragment](elements, 2n ** i + 1n)).to.equal(elements[1]);
}
});
});
});
});
}
});

View File

@@ -0,0 +1,34 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import {Test} from "@forge-std/Test.sol";
import {Base64} from "@openzeppelin/contracts/utils/Base64.sol";
contract Base64Test is Test {
function testEncode(bytes memory input) external {
assertEq(Base64.encode(input), vm.toBase64(input));
}
function testEncodeURL(bytes memory input) external {
assertEq(Base64.encodeURL(input), _removePadding(vm.toBase64URL(input)));
}
function _removePadding(string memory inputStr) internal pure returns (string memory) {
bytes memory input = bytes(inputStr);
bytes memory output;
for (uint256 i = 0; i < input.length; ++i) {
if (input[input.length - i - 1] != 0x3d) {
output = new bytes(input.length - i);
break;
}
}
for (uint256 i = 0; i < output.length; ++i) {
output[i] = input[i];
}
return string(output);
}
}

View File

@@ -0,0 +1,59 @@
const { ethers } = require('hardhat');
const { expect } = require('chai');
const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
// Replace "+/" with "-_" in the char table, and remove the padding
// see https://datatracker.ietf.org/doc/html/rfc4648#section-5
const base64toBase64Url = str => str.replaceAll('+', '-').replaceAll('/', '_').replaceAll('=', '');
async function fixture() {
const mock = await ethers.deployContract('$Base64');
return { mock };
}
describe('Strings', function () {
beforeEach(async function () {
Object.assign(this, await loadFixture(fixture));
});
describe('base64', function () {
for (const { title, input, expected } of [
{ title: 'converts to base64 encoded string with double padding', input: 'test', expected: 'dGVzdA==' },
{ title: 'converts to base64 encoded string with single padding', input: 'test1', expected: 'dGVzdDE=' },
{ title: 'converts to base64 encoded string without padding', input: 'test12', expected: 'dGVzdDEy' },
{ title: 'converts to base64 encoded string (/ case)', input: 'où', expected: 'b/k=' },
{ title: 'converts to base64 encoded string (+ case)', input: 'zs~1t8', expected: 'enN+MXQ4' },
{ title: 'empty bytes', input: '', expected: '' },
])
it(title, async function () {
const buffer = Buffer.from(input, 'ascii');
expect(await this.mock.$encode(buffer)).to.equal(ethers.encodeBase64(buffer));
expect(await this.mock.$encode(buffer)).to.equal(expected);
});
});
describe('base64url', function () {
for (const { title, input, expected } of [
{ title: 'converts to base64url encoded string with double padding', input: 'test', expected: 'dGVzdA' },
{ title: 'converts to base64url encoded string with single padding', input: 'test1', expected: 'dGVzdDE' },
{ title: 'converts to base64url encoded string without padding', input: 'test12', expected: 'dGVzdDEy' },
{ title: 'converts to base64url encoded string (_ case)', input: 'où', expected: 'b_k' },
{ title: 'converts to base64url encoded string (- case)', input: 'zs~1t8', expected: 'enN-MXQ4' },
{ title: 'empty bytes', input: '', expected: '' },
])
it(title, async function () {
const buffer = Buffer.from(input, 'ascii');
expect(await this.mock.$encodeURL(buffer)).to.equal(base64toBase64Url(ethers.encodeBase64(buffer)));
expect(await this.mock.$encodeURL(buffer)).to.equal(expected);
});
});
it('Encode reads beyond the input buffer into dirty memory', async function () {
const mock = await ethers.deployContract('Base64Dirty');
const buffer32 = ethers.id('example');
const buffer31 = buffer32.slice(0, -2);
expect(await mock.encode(buffer31)).to.equal(ethers.encodeBase64(buffer31));
expect(await mock.encode(buffer32)).to.equal(ethers.encodeBase64(buffer32));
});
});

View File

@@ -0,0 +1,48 @@
const { ethers } = require('hardhat');
const { expect } = require('chai');
const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
async function fixture() {
return { contextHelper: await ethers.deployContract('ContextMockCaller', []) };
}
function shouldBehaveLikeRegularContext() {
beforeEach(async function () {
Object.assign(this, await loadFixture(fixture));
});
describe('msgSender', function () {
it('returns the transaction sender when called from an EOA', async function () {
await expect(this.context.connect(this.sender).msgSender()).to.emit(this.context, 'Sender').withArgs(this.sender);
});
it('returns the transaction sender when called from another contract', async function () {
await expect(this.contextHelper.connect(this.sender).callSender(this.context))
.to.emit(this.context, 'Sender')
.withArgs(this.contextHelper);
});
});
describe('msgData', function () {
const args = [42n, 'OpenZeppelin'];
it('returns the transaction data when called from an EOA', async function () {
const callData = this.context.interface.encodeFunctionData('msgData', args);
await expect(this.context.msgData(...args))
.to.emit(this.context, 'Data')
.withArgs(callData, ...args);
});
it('returns the transaction sender when from another contract', async function () {
const callData = this.context.interface.encodeFunctionData('msgData', args);
await expect(this.contextHelper.callData(this.context, ...args))
.to.emit(this.context, 'Data')
.withArgs(callData, ...args);
});
});
}
module.exports = {
shouldBehaveLikeRegularContext,
};

View File

@@ -0,0 +1,18 @@
const { ethers } = require('hardhat');
const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
const { shouldBehaveLikeRegularContext } = require('./Context.behavior');
async function fixture() {
const [sender] = await ethers.getSigners();
const context = await ethers.deployContract('ContextMock', []);
return { sender, context };
}
describe('Context', function () {
beforeEach(async function () {
Object.assign(this, await loadFixture(fixture));
});
shouldBehaveLikeRegularContext();
});

View File

@@ -0,0 +1,18 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import {Test} from "@forge-std/Test.sol";
import {Create2} from "@openzeppelin/contracts/utils/Create2.sol";
contract Create2Test is Test {
function testComputeAddressSpillage(bytes32 salt, bytes32 bytecodeHash, address deployer) public {
address predicted = Create2.computeAddress(salt, bytecodeHash, deployer);
bytes32 spillage;
/// @solidity memory-safe-assembly
assembly {
spillage := and(predicted, 0xffffffffffffffffffffffff0000000000000000000000000000000000000000)
}
assertEq(spillage, bytes32(0));
}
}

View File

@@ -0,0 +1,134 @@
const { ethers } = require('hardhat');
const { expect } = require('chai');
const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
async function fixture() {
const [deployer, other] = await ethers.getSigners();
const factory = await ethers.deployContract('$Create2');
// Bytecode for deploying a contract that includes a constructor.
// We use a vesting wallet, with 3 constructor arguments.
const constructorByteCode = await ethers
.getContractFactory('VestingWallet')
.then(({ bytecode, interface }) => ethers.concat([bytecode, interface.encodeDeploy([other.address, 0n, 0n])]));
// Bytecode for deploying a contract that has no constructor log.
// Here we use the Create2 helper factory.
const constructorLessBytecode = await ethers
.getContractFactory('$Create2')
.then(({ bytecode, interface }) => ethers.concat([bytecode, interface.encodeDeploy([])]));
return { deployer, other, factory, constructorByteCode, constructorLessBytecode };
}
describe('Create2', function () {
const salt = 'salt message';
const saltHex = ethers.id(salt);
beforeEach(async function () {
Object.assign(this, await loadFixture(fixture));
});
describe('computeAddress', function () {
it('computes the correct contract address', async function () {
const onChainComputed = await this.factory.$computeAddress(saltHex, ethers.keccak256(this.constructorByteCode));
const offChainComputed = ethers.getCreate2Address(
this.factory.target,
saltHex,
ethers.keccak256(this.constructorByteCode),
);
expect(onChainComputed).to.equal(offChainComputed);
});
it('computes the correct contract address with deployer', async function () {
const onChainComputed = await this.factory.$computeAddress(
saltHex,
ethers.keccak256(this.constructorByteCode),
ethers.Typed.address(this.deployer),
);
const offChainComputed = ethers.getCreate2Address(
this.deployer.address,
saltHex,
ethers.keccak256(this.constructorByteCode),
);
expect(onChainComputed).to.equal(offChainComputed);
});
});
describe('deploy', function () {
it('deploys a contract without constructor', async function () {
const offChainComputed = ethers.getCreate2Address(
this.factory.target,
saltHex,
ethers.keccak256(this.constructorLessBytecode),
);
await expect(this.factory.$deploy(0n, saltHex, this.constructorLessBytecode))
.to.emit(this.factory, 'return$deploy')
.withArgs(offChainComputed);
expect(this.constructorLessBytecode).to.include((await ethers.provider.getCode(offChainComputed)).slice(2));
});
it('deploys a contract with constructor arguments', async function () {
const offChainComputed = ethers.getCreate2Address(
this.factory.target,
saltHex,
ethers.keccak256(this.constructorByteCode),
);
await expect(this.factory.$deploy(0n, saltHex, this.constructorByteCode))
.to.emit(this.factory, 'return$deploy')
.withArgs(offChainComputed);
const instance = await ethers.getContractAt('VestingWallet', offChainComputed);
expect(await instance.owner()).to.equal(this.other);
});
it('deploys a contract with funds deposited in the factory', async function () {
const value = 10n;
await this.deployer.sendTransaction({ to: this.factory, value });
const offChainComputed = ethers.getCreate2Address(
this.factory.target,
saltHex,
ethers.keccak256(this.constructorByteCode),
);
expect(await ethers.provider.getBalance(this.factory)).to.equal(value);
expect(await ethers.provider.getBalance(offChainComputed)).to.equal(0n);
await expect(this.factory.$deploy(value, saltHex, this.constructorByteCode))
.to.emit(this.factory, 'return$deploy')
.withArgs(offChainComputed);
expect(await ethers.provider.getBalance(this.factory)).to.equal(0n);
expect(await ethers.provider.getBalance(offChainComputed)).to.equal(value);
});
it('fails deploying a contract in an existent address', async function () {
await expect(this.factory.$deploy(0n, saltHex, this.constructorByteCode)).to.emit(this.factory, 'return$deploy');
await expect(this.factory.$deploy(0n, saltHex, this.constructorByteCode)).to.be.revertedWithCustomError(
this.factory,
'FailedDeployment',
);
});
it('fails deploying a contract if the bytecode length is zero', async function () {
await expect(this.factory.$deploy(0n, saltHex, '0x')).to.be.revertedWithCustomError(
this.factory,
'Create2EmptyBytecode',
);
});
it('fails deploying a contract if factory contract does not have sufficient balance', async function () {
await expect(this.factory.$deploy(1n, saltHex, this.constructorByteCode))
.to.be.revertedWithCustomError(this.factory, 'InsufficientBalance')
.withArgs(0n, 1n);
});
});
});

View File

@@ -0,0 +1,72 @@
const { ethers } = require('hardhat');
const { expect } = require('chai');
const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
async function fixture() {
const [holder, alice, bruce] = await ethers.getSigners();
const amount = 12_000n;
const helper = await ethers.deployContract('MulticallHelper');
const mock = await ethers.deployContract('$ERC20MulticallMock', ['name', 'symbol']);
await mock.$_mint(holder, amount);
return { holder, alice, bruce, amount, mock, helper };
}
describe('Multicall', function () {
beforeEach(async function () {
Object.assign(this, await loadFixture(fixture));
});
it('batches function calls', async function () {
expect(await this.mock.balanceOf(this.alice)).to.equal(0n);
expect(await this.mock.balanceOf(this.bruce)).to.equal(0n);
await expect(
this.mock.multicall([
this.mock.interface.encodeFunctionData('transfer', [this.alice.address, this.amount / 2n]),
this.mock.interface.encodeFunctionData('transfer', [this.bruce.address, this.amount / 3n]),
]),
)
.to.emit(this.mock, 'Transfer')
.withArgs(this.holder, this.alice, this.amount / 2n)
.to.emit(this.mock, 'Transfer')
.withArgs(this.holder, this.bruce, this.amount / 3n);
expect(await this.mock.balanceOf(this.alice)).to.equal(this.amount / 2n);
expect(await this.mock.balanceOf(this.bruce)).to.equal(this.amount / 3n);
});
it('returns an array with the result of each call', async function () {
await this.mock.transfer(this.helper, this.amount);
expect(await this.mock.balanceOf(this.helper)).to.equal(this.amount);
await this.helper.checkReturnValues(this.mock, [this.alice, this.bruce], [this.amount / 2n, this.amount / 3n]);
});
it('reverts previous calls', async function () {
expect(await this.mock.balanceOf(this.alice)).to.equal(0n);
await expect(
this.mock.multicall([
this.mock.interface.encodeFunctionData('transfer', [this.alice.address, this.amount]),
this.mock.interface.encodeFunctionData('transfer', [this.bruce.address, this.amount]),
]),
)
.to.be.revertedWithCustomError(this.mock, 'ERC20InsufficientBalance')
.withArgs(this.holder, 0, this.amount);
expect(await this.mock.balanceOf(this.alice)).to.equal(0n);
});
it('bubbles up revert reasons', async function () {
await expect(
this.mock.multicall([
this.mock.interface.encodeFunctionData('transfer', [this.alice.address, this.amount]),
this.mock.interface.encodeFunctionData('transfer', [this.bruce.address, this.amount]),
]),
)
.to.be.revertedWithCustomError(this.mock, 'ERC20InsufficientBalance')
.withArgs(this.holder, 0, this.amount);
});
});

View File

@@ -0,0 +1,75 @@
const { ethers } = require('hardhat');
const { expect } = require('chai');
const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
async function fixture() {
const [sender, other] = await ethers.getSigners();
const mock = await ethers.deployContract('$Nonces');
return { sender, other, mock };
}
describe('Nonces', function () {
beforeEach(async function () {
Object.assign(this, await loadFixture(fixture));
});
it('gets a nonce', async function () {
expect(await this.mock.nonces(this.sender)).to.equal(0n);
});
describe('_useNonce', function () {
it('increments a nonce', async function () {
expect(await this.mock.nonces(this.sender)).to.equal(0n);
await expect(await this.mock.$_useNonce(this.sender))
.to.emit(this.mock, 'return$_useNonce')
.withArgs(0n);
expect(await this.mock.nonces(this.sender)).to.equal(1n);
});
it("increments only sender's nonce", async function () {
expect(await this.mock.nonces(this.sender)).to.equal(0n);
expect(await this.mock.nonces(this.other)).to.equal(0n);
await this.mock.$_useNonce(this.sender);
expect(await this.mock.nonces(this.sender)).to.equal(1n);
expect(await this.mock.nonces(this.other)).to.equal(0n);
});
});
describe('_useCheckedNonce', function () {
it('increments a nonce', async function () {
const currentNonce = await this.mock.nonces(this.sender);
expect(currentNonce).to.equal(0n);
await this.mock.$_useCheckedNonce(this.sender, currentNonce);
expect(await this.mock.nonces(this.sender)).to.equal(1n);
});
it("increments only sender's nonce", async function () {
const currentNonce = await this.mock.nonces(this.sender);
expect(currentNonce).to.equal(0n);
expect(await this.mock.nonces(this.other)).to.equal(0n);
await this.mock.$_useCheckedNonce(this.sender, currentNonce);
expect(await this.mock.nonces(this.sender)).to.equal(1n);
expect(await this.mock.nonces(this.other)).to.equal(0n);
});
it('reverts when nonce is not the expected', async function () {
const currentNonce = await this.mock.nonces(this.sender);
await expect(this.mock.$_useCheckedNonce(this.sender, currentNonce + 1n))
.to.be.revertedWithCustomError(this.mock, 'InvalidAccountNonce')
.withArgs(this.sender, currentNonce);
});
});
});

View File

@@ -0,0 +1,27 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import {Test} from "@forge-std/Test.sol";
import {Packing} from "@openzeppelin/contracts/utils/Packing.sol";
contract PackingTest is Test {
using Packing for *;
// Pack a pair of arbitrary uint128, and check that split recovers the correct values
function testUint128x2(uint128 first, uint128 second) external {
Packing.Uint128x2 packed = Packing.pack(first, second);
assertEq(packed.first(), first);
assertEq(packed.second(), second);
(uint128 recoveredFirst, uint128 recoveredSecond) = packed.split();
assertEq(recoveredFirst, first);
assertEq(recoveredSecond, second);
}
// split an arbitrary bytes32 into a pair of uint128, and check that repack matches the input
function testUint128x2(bytes32 input) external {
(uint128 first, uint128 second) = input.asUint128x2().split();
assertEq(Packing.pack(first, second).asBytes32(), input);
}
}

View File

@@ -0,0 +1,27 @@
const { ethers } = require('hardhat');
const { expect } = require('chai');
const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
const { generators } = require('../helpers/random');
async function fixture() {
return { mock: await ethers.deployContract('$Packing') };
}
describe('Packing', function () {
beforeEach(async function () {
Object.assign(this, await loadFixture(fixture));
});
it('Uint128x2', async function () {
const first = generators.uint256() % 2n ** 128n;
const second = generators.uint256() % 2n ** 128n;
const packed = ethers.hexlify(ethers.toBeArray((first << 128n) | second));
expect(await this.mock.$asUint128x2(packed)).to.equal(packed);
expect(await this.mock.$asBytes32(packed)).to.equal(packed);
expect(await this.mock.$pack(first, second)).to.equal(packed);
expect(await this.mock.$split(packed)).to.deep.equal([first, second]);
expect(await this.mock.$first(packed)).to.equal(first);
expect(await this.mock.$second(packed)).to.equal(second);
});
});

View File

@@ -0,0 +1,37 @@
const { ethers } = require('hardhat');
const { expect } = require('chai');
const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
const { PANIC_CODES } = require('@nomicfoundation/hardhat-chai-matchers/panic');
async function fixture() {
return { mock: await ethers.deployContract('$Panic') };
}
describe('Panic', function () {
beforeEach(async function () {
Object.assign(this, await loadFixture(fixture));
});
for (const [name, code] of Object.entries({
GENERIC: 0x0,
ASSERT: PANIC_CODES.ASSERTION_ERROR,
UNDER_OVERFLOW: PANIC_CODES.ARITHMETIC_OVERFLOW,
DIVISION_BY_ZERO: PANIC_CODES.DIVISION_BY_ZERO,
ENUM_CONVERSION_ERROR: PANIC_CODES.ENUM_CONVERSION_OUT_OF_BOUNDS,
STORAGE_ENCODING_ERROR: PANIC_CODES.INCORRECTLY_ENCODED_STORAGE_BYTE_ARRAY,
EMPTY_ARRAY_POP: PANIC_CODES.POP_ON_EMPTY_ARRAY,
ARRAY_OUT_OF_BOUNDS: PANIC_CODES.ARRAY_ACCESS_OUT_OF_BOUNDS,
RESOURCE_ERROR: PANIC_CODES.TOO_MUCH_MEMORY_ALLOCATED,
INVALID_INTERNAL_FUNCTION: PANIC_CODES.ZERO_INITIALIZED_VARIABLE,
})) {
describe(`${name} (${ethers.toBeHex(code)})`, function () {
it('exposes panic code as constant', async function () {
expect(await this.mock.getFunction(`$${name}`)()).to.equal(code);
});
it('reverts with panic when called', async function () {
await expect(this.mock.$panic(code)).to.be.revertedWithPanic(code);
});
});
}
});

View File

@@ -0,0 +1,90 @@
const { ethers } = require('hardhat');
const { expect } = require('chai');
const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
async function fixture() {
const [pauser] = await ethers.getSigners();
const mock = await ethers.deployContract('PausableMock');
return { pauser, mock };
}
describe('Pausable', function () {
beforeEach(async function () {
Object.assign(this, await loadFixture(fixture));
});
describe('when unpaused', function () {
beforeEach(async function () {
expect(await this.mock.paused()).to.be.false;
});
it('can perform normal process in non-pause', async function () {
expect(await this.mock.count()).to.equal(0n);
await this.mock.normalProcess();
expect(await this.mock.count()).to.equal(1n);
});
it('cannot take drastic measure in non-pause', async function () {
await expect(this.mock.drasticMeasure()).to.be.revertedWithCustomError(this.mock, 'ExpectedPause');
expect(await this.mock.drasticMeasureTaken()).to.be.false;
});
describe('when paused', function () {
beforeEach(async function () {
this.tx = await this.mock.pause();
});
it('emits a Paused event', async function () {
await expect(this.tx).to.emit(this.mock, 'Paused').withArgs(this.pauser);
});
it('cannot perform normal process in pause', async function () {
await expect(this.mock.normalProcess()).to.be.revertedWithCustomError(this.mock, 'EnforcedPause');
});
it('can take a drastic measure in a pause', async function () {
await this.mock.drasticMeasure();
expect(await this.mock.drasticMeasureTaken()).to.be.true;
});
it('reverts when re-pausing', async function () {
await expect(this.mock.pause()).to.be.revertedWithCustomError(this.mock, 'EnforcedPause');
});
describe('unpausing', function () {
it('is unpausable by the pauser', async function () {
await this.mock.unpause();
expect(await this.mock.paused()).to.be.false;
});
describe('when unpaused', function () {
beforeEach(async function () {
this.tx = await this.mock.unpause();
});
it('emits an Unpaused event', async function () {
await expect(this.tx).to.emit(this.mock, 'Unpaused').withArgs(this.pauser);
});
it('should resume allowing normal process', async function () {
expect(await this.mock.count()).to.equal(0n);
await this.mock.normalProcess();
expect(await this.mock.count()).to.equal(1n);
});
it('should prevent drastic measure', async function () {
await expect(this.mock.drasticMeasure()).to.be.revertedWithCustomError(this.mock, 'ExpectedPause');
});
it('reverts when re-unpausing', async function () {
await expect(this.mock.unpause()).to.be.revertedWithCustomError(this.mock, 'ExpectedPause');
});
});
});
});
});
});

View File

@@ -0,0 +1,50 @@
const { ethers } = require('hardhat');
const { expect } = require('chai');
const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
for (const variant of ['', 'Transient']) {
describe(`Reentrancy${variant}Guard`, function () {
async function fixture() {
const name = `Reentrancy${variant}Mock`;
const mock = await ethers.deployContract(name);
return { name, mock };
}
beforeEach(async function () {
Object.assign(this, await loadFixture(fixture));
});
it('nonReentrant function can be called', async function () {
expect(await this.mock.counter()).to.equal(0n);
await this.mock.callback();
expect(await this.mock.counter()).to.equal(1n);
});
it('does not allow remote callback', async function () {
const attacker = await ethers.deployContract('ReentrancyAttack');
await expect(this.mock.countAndCall(attacker)).to.be.revertedWith('ReentrancyAttack: failed call');
});
it('_reentrancyGuardEntered should be true when guarded', async function () {
await this.mock.guardedCheckEntered();
});
it('_reentrancyGuardEntered should be false when unguarded', async function () {
await this.mock.unguardedCheckNotEntered();
});
// The following are more side-effects than intended behavior:
// I put them here as documentation, and to monitor any changes
// in the side-effects.
it('does not allow local recursion', async function () {
await expect(this.mock.countLocalRecursive(10n)).to.be.revertedWithCustomError(
this.mock,
'ReentrancyGuardReentrantCall',
);
});
it('does not allow indirect local recursion', async function () {
await expect(this.mock.countThisRecursive(10n)).to.be.revertedWith(`${this.name}: failed call`);
});
});
}

View File

@@ -0,0 +1,55 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import {Test} from "@forge-std/Test.sol";
import {ShortStrings, ShortString} from "@openzeppelin/contracts/utils/ShortStrings.sol";
contract ShortStringsTest is Test {
string _fallback;
function testRoundtripShort(string memory input) external {
vm.assume(_isShort(input));
ShortString short = ShortStrings.toShortString(input);
string memory output = ShortStrings.toString(short);
assertEq(input, output);
}
function testRoundtripWithFallback(string memory input, string memory fallbackInitial) external {
_fallback = fallbackInitial; // Make sure that the initial value has no effect
ShortString short = ShortStrings.toShortStringWithFallback(input, _fallback);
string memory output = ShortStrings.toStringWithFallback(short, _fallback);
assertEq(input, output);
}
function testRevertLong(string memory input) external {
vm.assume(!_isShort(input));
vm.expectRevert(abi.encodeWithSelector(ShortStrings.StringTooLong.selector, input));
this.toShortString(input);
}
function testLengthShort(string memory input) external {
vm.assume(_isShort(input));
uint256 inputLength = bytes(input).length;
ShortString short = ShortStrings.toShortString(input);
uint256 shortLength = ShortStrings.byteLength(short);
assertEq(inputLength, shortLength);
}
function testLengthWithFallback(string memory input, string memory fallbackInitial) external {
_fallback = fallbackInitial;
uint256 inputLength = bytes(input).length;
ShortString short = ShortStrings.toShortStringWithFallback(input, _fallback);
uint256 shortLength = ShortStrings.byteLengthWithFallback(short, _fallback);
assertEq(inputLength, shortLength);
}
function toShortString(string memory input) external pure returns (ShortString) {
return ShortStrings.toShortString(input);
}
function _isShort(string memory input) internal pure returns (bool) {
return bytes(input).length < 32;
}
}

View File

@@ -0,0 +1,64 @@
const { ethers } = require('hardhat');
const { expect } = require('chai');
const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
const FALLBACK_SENTINEL = ethers.zeroPadValue('0xFF', 32);
const length = sstr => parseInt(sstr.slice(64), 16);
const decode = sstr => ethers.toUtf8String(sstr).slice(0, length(sstr));
const encode = str =>
str.length < 32
? ethers.concat([
ethers.encodeBytes32String(str).slice(0, -2),
ethers.zeroPadValue(ethers.toBeArray(str.length), 1),
])
: FALLBACK_SENTINEL;
async function fixture() {
const mock = await ethers.deployContract('$ShortStrings');
return { mock };
}
describe('ShortStrings', function () {
beforeEach(async function () {
Object.assign(this, await loadFixture(fixture));
});
for (const str of [0, 1, 16, 31, 32, 64, 1024].map(length => 'a'.repeat(length))) {
describe(`with string length ${str.length}`, function () {
it('encode / decode', async function () {
if (str.length < 32) {
const encoded = await this.mock.$toShortString(str);
expect(encoded).to.equal(encode(str));
expect(decode(encoded)).to.equal(str);
expect(await this.mock.$byteLength(encoded)).to.equal(str.length);
expect(await this.mock.$toString(encoded)).to.equal(str);
} else {
await expect(this.mock.$toShortString(str))
.to.be.revertedWithCustomError(this.mock, 'StringTooLong')
.withArgs(str);
}
});
it('set / get with fallback', async function () {
const short = await this.mock
.$toShortStringWithFallback(str, 0)
.then(tx => tx.wait())
.then(receipt => receipt.logs.find(ev => ev.fragment.name == 'return$toShortStringWithFallback').args[0]);
expect(short).to.equal(encode(str));
const promise = this.mock.$toString(short);
if (str.length < 32) {
expect(await promise).to.equal(str);
} else {
await expect(promise).to.be.revertedWithCustomError(this.mock, 'InvalidShortString');
}
expect(await this.mock.$byteLengthWithFallback(short, 0)).to.equal(str.length);
expect(await this.mock.$toStringWithFallback(short, 0)).to.equal(str);
});
});
}
});

View File

@@ -0,0 +1,203 @@
// SPDX-License-Identifier: MIT
// This file was procedurally generated from scripts/generate/templates/SlotDerivation.t.js.
pragma solidity ^0.8.20;
import {Test} from "@forge-std/Test.sol";
import {SlotDerivation} from "@openzeppelin/contracts/utils/SlotDerivation.sol";
contract SlotDerivationTest is Test {
using SlotDerivation for bytes32;
bytes[] private _array;
function testDeriveArray(uint256 length, uint256 offset) public {
length = bound(length, 1, type(uint256).max);
offset = bound(offset, 0, length - 1);
bytes32 baseSlot;
assembly {
baseSlot := _array.slot
sstore(baseSlot, length) // store length so solidity access does not revert
}
bytes storage derived = _array[offset];
bytes32 derivedSlot;
assembly {
derivedSlot := derived.slot
}
assertEq(baseSlot.deriveArray().offset(offset), derivedSlot);
}
mapping(address => bytes) private _addressMapping;
function testDeriveMappingAddress(address key) public {
bytes32 baseSlot;
assembly {
baseSlot := _addressMapping.slot
}
bytes storage derived = _addressMapping[key];
bytes32 derivedSlot;
assembly {
derivedSlot := derived.slot
}
assertEq(baseSlot.deriveMapping(key), derivedSlot);
}
mapping(bool => bytes) private _boolMapping;
function testDeriveMappingBoolean(bool key) public {
bytes32 baseSlot;
assembly {
baseSlot := _boolMapping.slot
}
bytes storage derived = _boolMapping[key];
bytes32 derivedSlot;
assembly {
derivedSlot := derived.slot
}
assertEq(baseSlot.deriveMapping(key), derivedSlot);
}
mapping(bytes32 => bytes) private _bytes32Mapping;
function testDeriveMappingBytes32(bytes32 key) public {
bytes32 baseSlot;
assembly {
baseSlot := _bytes32Mapping.slot
}
bytes storage derived = _bytes32Mapping[key];
bytes32 derivedSlot;
assembly {
derivedSlot := derived.slot
}
assertEq(baseSlot.deriveMapping(key), derivedSlot);
}
mapping(bytes4 => bytes) private _bytes4Mapping;
function testDeriveMappingBytes4(bytes4 key) public {
bytes32 baseSlot;
assembly {
baseSlot := _bytes4Mapping.slot
}
bytes storage derived = _bytes4Mapping[key];
bytes32 derivedSlot;
assembly {
derivedSlot := derived.slot
}
assertEq(baseSlot.deriveMapping(key), derivedSlot);
}
mapping(uint256 => bytes) private _uint256Mapping;
function testDeriveMappingUint256(uint256 key) public {
bytes32 baseSlot;
assembly {
baseSlot := _uint256Mapping.slot
}
bytes storage derived = _uint256Mapping[key];
bytes32 derivedSlot;
assembly {
derivedSlot := derived.slot
}
assertEq(baseSlot.deriveMapping(key), derivedSlot);
}
mapping(uint32 => bytes) private _uint32Mapping;
function testDeriveMappingUint32(uint32 key) public {
bytes32 baseSlot;
assembly {
baseSlot := _uint32Mapping.slot
}
bytes storage derived = _uint32Mapping[key];
bytes32 derivedSlot;
assembly {
derivedSlot := derived.slot
}
assertEq(baseSlot.deriveMapping(key), derivedSlot);
}
mapping(int256 => bytes) private _int256Mapping;
function testDeriveMappingInt256(int256 key) public {
bytes32 baseSlot;
assembly {
baseSlot := _int256Mapping.slot
}
bytes storage derived = _int256Mapping[key];
bytes32 derivedSlot;
assembly {
derivedSlot := derived.slot
}
assertEq(baseSlot.deriveMapping(key), derivedSlot);
}
mapping(int32 => bytes) private _int32Mapping;
function testDeriveMappingInt32(int32 key) public {
bytes32 baseSlot;
assembly {
baseSlot := _int32Mapping.slot
}
bytes storage derived = _int32Mapping[key];
bytes32 derivedSlot;
assembly {
derivedSlot := derived.slot
}
assertEq(baseSlot.deriveMapping(key), derivedSlot);
}
mapping(string => bytes) private _stringMapping;
function testDeriveMappingString(string memory key) public {
bytes32 baseSlot;
assembly {
baseSlot := _stringMapping.slot
}
bytes storage derived = _stringMapping[key];
bytes32 derivedSlot;
assembly {
derivedSlot := derived.slot
}
assertEq(baseSlot.deriveMapping(key), derivedSlot);
}
mapping(bytes => bytes) private _bytesMapping;
function testDeriveMappingBytes(bytes memory key) public {
bytes32 baseSlot;
assembly {
baseSlot := _bytesMapping.slot
}
bytes storage derived = _bytesMapping[key];
bytes32 derivedSlot;
assembly {
derivedSlot := derived.slot
}
assertEq(baseSlot.deriveMapping(key), derivedSlot);
}
}

View File

@@ -0,0 +1,58 @@
const { ethers } = require('hardhat');
const { expect } = require('chai');
const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
const { erc7201Slot } = require('../helpers/storage');
const { generators } = require('../helpers/random');
async function fixture() {
const [account] = await ethers.getSigners();
const mock = await ethers.deployContract('$SlotDerivation');
return { mock, account };
}
describe('SlotDerivation', function () {
beforeEach(async function () {
Object.assign(this, await loadFixture(fixture));
});
describe('namespaces', function () {
const namespace = 'example.main';
it('erc-7201', async function () {
expect(await this.mock.$erc7201Slot(namespace)).to.equal(erc7201Slot(namespace));
});
});
describe('derivation', function () {
it('offset', async function () {
const base = generators.bytes32();
const offset = generators.uint256();
expect(await this.mock.$offset(base, offset)).to.equal((ethers.toBigInt(base) + offset) & ethers.MaxUint256);
});
it('array', async function () {
const base = generators.bytes32();
expect(await this.mock.$deriveArray(base)).to.equal(ethers.keccak256(base));
});
describe('mapping', function () {
for (const { type, key, isValueType } of [
{ type: 'bool', key: true, isValueType: true },
{ type: 'address', key: generators.address(), isValueType: true },
{ type: 'bytes32', key: generators.bytes32(), isValueType: true },
{ type: 'uint256', key: generators.uint256(), isValueType: true },
{ type: 'int256', key: generators.int256(), isValueType: true },
{ type: 'bytes', key: generators.hexBytes(128), isValueType: false },
{ type: 'string', key: 'lorem ipsum', isValueType: false },
]) {
it(type, async function () {
const base = generators.bytes32();
const expected = isValueType
? ethers.keccak256(ethers.AbiCoder.defaultAbiCoder().encode([type, 'bytes32'], [key, base]))
: ethers.solidityPackedKeccak256([type, 'bytes32'], [key, base]);
expect(await this.mock[`$deriveMapping(bytes32,${type})`](base, key)).to.equal(expected);
});
}
});
});
});

View File

@@ -0,0 +1,106 @@
const { ethers } = require('hardhat');
const { expect } = require('chai');
const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
const { generators } = require('../helpers/random');
const slot = ethers.id('some.storage.slot');
const otherSlot = ethers.id('some.other.storage.slot');
const TYPES = [
{ name: 'Boolean', type: 'bool', value: true, isValueType: true, zero: false },
{ name: 'Address', type: 'address', value: generators.address(), isValueType: true, zero: generators.address.zero },
{ name: 'Bytes32', type: 'bytes32', value: generators.bytes32(), isValueType: true, zero: generators.bytes32.zero },
{ name: 'Uint256', type: 'uint256', value: generators.uint256(), isValueType: true, zero: generators.uint256.zero },
{ name: 'Int256', type: 'int256', value: generators.int256(), isValueType: true, zero: generators.int256.zero },
{ name: 'Bytes', type: 'bytes', value: generators.hexBytes(128), isValueType: false, zero: generators.hexBytes.zero },
{ name: 'String', type: 'string', value: 'lorem ipsum', isValueType: false, zero: '' },
];
async function fixture() {
return { mock: await ethers.deployContract('StorageSlotMock') };
}
describe('StorageSlot', function () {
beforeEach(async function () {
Object.assign(this, await loadFixture(fixture));
});
for (const { name, type, value, zero } of TYPES) {
describe(`${type} storage slot`, function () {
it('set', async function () {
await this.mock.getFunction(`set${name}Slot`)(slot, value);
});
describe('get', function () {
beforeEach(async function () {
await this.mock.getFunction(`set${name}Slot`)(slot, value);
});
it('from right slot', async function () {
expect(await this.mock.getFunction(`get${name}Slot`)(slot)).to.equal(value);
});
it('from other slot', async function () {
expect(await this.mock.getFunction(`get${name}Slot`)(otherSlot)).to.equal(zero);
});
});
});
}
for (const { name, type, value, zero } of TYPES.filter(type => !type.isValueType)) {
describe(`${type} storage pointer`, function () {
it('set', async function () {
await this.mock.getFunction(`set${name}Storage`)(slot, value);
});
describe('get', function () {
beforeEach(async function () {
await this.mock.getFunction(`set${name}Storage`)(slot, value);
});
it('from right slot', async function () {
expect(await this.mock.getFunction(`${type}Map`)(slot)).to.equal(value);
expect(await this.mock.getFunction(`get${name}Storage`)(slot)).to.equal(value);
});
it('from other slot', async function () {
expect(await this.mock.getFunction(`${type}Map`)(otherSlot)).to.equal(zero);
expect(await this.mock.getFunction(`get${name}Storage`)(otherSlot)).to.equal(zero);
});
});
});
}
for (const { name, type, value, zero } of TYPES.filter(type => type.isValueType)) {
describe(`${type} transient slot`, function () {
const load = `tload${name}(bytes32)`;
const store = `tstore(bytes32,${type})`;
const event = `${name}Value`;
it('load', async function () {
await expect(this.mock[load](slot)).to.emit(this.mock, event).withArgs(slot, zero);
});
it('store and load (2 txs)', async function () {
await this.mock[store](slot, value);
await expect(this.mock[load](slot)).to.emit(this.mock, event).withArgs(slot, zero);
});
it('store and load (batched)', async function () {
await expect(
this.mock.multicall([
this.mock.interface.encodeFunctionData(store, [slot, value]),
this.mock.interface.encodeFunctionData(load, [slot]),
this.mock.interface.encodeFunctionData(load, [otherSlot]),
]),
)
.to.emit(this.mock, event)
.withArgs(slot, value)
.to.emit(this.mock, event)
.withArgs(otherSlot, zero);
await expect(this.mock[load](slot)).to.emit(this.mock, event).withArgs(slot, zero);
});
});
}
});

View File

@@ -0,0 +1,153 @@
const { ethers } = require('hardhat');
const { expect } = require('chai');
const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
async function fixture() {
const mock = await ethers.deployContract('$Strings');
return { mock };
}
describe('Strings', function () {
before(async function () {
Object.assign(this, await loadFixture(fixture));
});
describe('toString', function () {
const values = [
0n,
7n,
10n,
99n,
100n,
101n,
123n,
4132n,
12345n,
1234567n,
1234567890n,
123456789012345n,
12345678901234567890n,
123456789012345678901234567890n,
1234567890123456789012345678901234567890n,
12345678901234567890123456789012345678901234567890n,
123456789012345678901234567890123456789012345678901234567890n,
1234567890123456789012345678901234567890123456789012345678901234567890n,
];
describe('uint256', function () {
it('converts MAX_UINT256', async function () {
const value = ethers.MaxUint256;
expect(await this.mock.$toString(value)).to.equal(value.toString(10));
});
for (const value of values) {
it(`converts ${value}`, async function () {
expect(await this.mock.$toString(value)).to.equal(value);
});
}
});
describe('int256', function () {
it('converts MAX_INT256', async function () {
const value = ethers.MaxInt256;
expect(await this.mock.$toStringSigned(value)).to.equal(value.toString(10));
});
it('converts MIN_INT256', async function () {
const value = ethers.MinInt256;
expect(await this.mock.$toStringSigned(value)).to.equal(value.toString(10));
});
for (const value of values) {
it(`convert ${value}`, async function () {
expect(await this.mock.$toStringSigned(value)).to.equal(value);
});
it(`convert negative ${value}`, async function () {
const negated = -value;
expect(await this.mock.$toStringSigned(negated)).to.equal(negated.toString(10));
});
}
});
});
describe('toHexString', function () {
it('converts 0', async function () {
expect(await this.mock.getFunction('$toHexString(uint256)')(0n)).to.equal('0x00');
});
it('converts a positive number', async function () {
expect(await this.mock.getFunction('$toHexString(uint256)')(0x4132n)).to.equal('0x4132');
});
it('converts MAX_UINT256', async function () {
expect(await this.mock.getFunction('$toHexString(uint256)')(ethers.MaxUint256)).to.equal(
`0x${ethers.MaxUint256.toString(16)}`,
);
});
});
describe('toHexString fixed', function () {
it('converts a positive number (long)', async function () {
expect(await this.mock.getFunction('$toHexString(uint256,uint256)')(0x4132n, 32n)).to.equal(
'0x0000000000000000000000000000000000000000000000000000000000004132',
);
});
it('converts a positive number (short)', async function () {
const length = 1n;
await expect(this.mock.getFunction('$toHexString(uint256,uint256)')(0x4132n, length))
.to.be.revertedWithCustomError(this.mock, `StringsInsufficientHexLength`)
.withArgs(0x4132, length);
});
it('converts MAX_UINT256', async function () {
expect(await this.mock.getFunction('$toHexString(uint256,uint256)')(ethers.MaxUint256, 32n)).to.equal(
`0x${ethers.MaxUint256.toString(16)}`,
);
});
});
describe('toHexString address', function () {
it('converts a random address', async function () {
const addr = '0xa9036907dccae6a1e0033479b12e837e5cf5a02f';
expect(await this.mock.getFunction('$toHexString(address)')(addr)).to.equal(addr);
});
it('converts an address with leading zeros', async function () {
const addr = '0x0000e0ca771e21bd00057f54a68c30d400000000';
expect(await this.mock.getFunction('$toHexString(address)')(addr)).to.equal(addr);
});
});
describe('equal', function () {
it('compares two empty strings', async function () {
expect(await this.mock.$equal('', '')).to.be.true;
});
it('compares two equal strings', async function () {
expect(await this.mock.$equal('a', 'a')).to.be.true;
});
it('compares two different strings', async function () {
expect(await this.mock.$equal('a', 'b')).to.be.false;
});
it('compares two different strings of different lengths', async function () {
expect(await this.mock.$equal('a', 'aa')).to.be.false;
expect(await this.mock.$equal('aa', 'a')).to.be.false;
});
it('compares two different large strings', async function () {
const str1 = 'a'.repeat(201);
const str2 = 'a'.repeat(200) + 'b';
expect(await this.mock.$equal(str1, str2)).to.be.false;
});
it('compares two equal large strings', async function () {
const str1 = 'a'.repeat(201);
const str2 = 'a'.repeat(201);
expect(await this.mock.$equal(str1, str2)).to.be.true;
});
});
});

View File

@@ -0,0 +1,213 @@
const { ethers } = require('hardhat');
const { expect } = require('chai');
const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
const TEST_MESSAGE = ethers.id('OpenZeppelin');
const WRONG_MESSAGE = ethers.id('Nope');
const NON_HASH_MESSAGE = '0xabcd';
async function fixture() {
const [signer] = await ethers.getSigners();
const mock = await ethers.deployContract('$ECDSA');
return { signer, mock };
}
describe('ECDSA', function () {
beforeEach(async function () {
Object.assign(this, await loadFixture(fixture));
});
describe('recover with invalid signature', function () {
it('with short signature', async function () {
await expect(this.mock.$recover(TEST_MESSAGE, '0x1234'))
.to.be.revertedWithCustomError(this.mock, 'ECDSAInvalidSignatureLength')
.withArgs(2);
});
it('with long signature', async function () {
await expect(
// eslint-disable-next-line max-len
this.mock.$recover(
TEST_MESSAGE,
'0x01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789',
),
)
.to.be.revertedWithCustomError(this.mock, 'ECDSAInvalidSignatureLength')
.withArgs(85);
});
});
describe('recover with valid signature', function () {
describe('using <signer>.sign', function () {
it('returns signer address with correct signature', async function () {
// Create the signature
const signature = await this.signer.signMessage(TEST_MESSAGE);
// Recover the signer address from the generated message and signature.
expect(await this.mock.$recover(ethers.hashMessage(TEST_MESSAGE), signature)).to.equal(this.signer);
});
it('returns signer address with correct signature for arbitrary length message', async function () {
// Create the signature
const signature = await this.signer.signMessage(NON_HASH_MESSAGE);
// Recover the signer address from the generated message and signature.
expect(await this.mock.$recover(ethers.hashMessage(NON_HASH_MESSAGE), signature)).to.equal(this.signer);
});
it('returns a different address', async function () {
const signature = await this.signer.signMessage(TEST_MESSAGE);
expect(await this.mock.$recover(WRONG_MESSAGE, signature)).to.not.be.equal(this.signer);
});
it('reverts with invalid signature', async function () {
// eslint-disable-next-line max-len
const signature =
'0x332ce75a821c982f9127538858900d87d3ec1f9f737338ad67cad133fa48feff48e6fa0c18abc62e42820f05943e47af3e9fbe306ce74d64094bdf1691ee53e01c';
await expect(this.mock.$recover(TEST_MESSAGE, signature)).to.be.revertedWithCustomError(
this.mock,
'ECDSAInvalidSignature',
);
});
});
describe('with v=27 signature', function () {
const signer = '0x2cc1166f6212628A0deEf2B33BEFB2187D35b86c';
// eslint-disable-next-line max-len
const signatureWithoutV =
'0x5d99b6f7f6d1f73d1a26497f2b1c89b24c0993913f86e9a2d02cd69887d9c94f3c880358579d811b21dd1b7fd9bb01c1d81d10e69f0384e675c32b39643be892';
it('works with correct v value', async function () {
const v = '0x1b'; // 27 = 1b.
const signature = ethers.concat([signatureWithoutV, v]);
expect(await this.mock.$recover(TEST_MESSAGE, signature)).to.equal(signer);
const { r, s, yParityAndS: vs } = ethers.Signature.from(signature);
expect(await this.mock.getFunction('$recover(bytes32,uint8,bytes32,bytes32)')(TEST_MESSAGE, v, r, s)).to.equal(
signer,
);
expect(await this.mock.getFunction('$recover(bytes32,bytes32,bytes32)')(TEST_MESSAGE, r, vs)).to.equal(signer);
});
it('rejects incorrect v value', async function () {
const v = '0x1c'; // 28 = 1c.
const signature = ethers.concat([signatureWithoutV, v]);
expect(await this.mock.$recover(TEST_MESSAGE, signature)).to.not.equal(signer);
const { r, s, yParityAndS: vs } = ethers.Signature.from(signature);
expect(
await this.mock.getFunction('$recover(bytes32,uint8,bytes32,bytes32)')(TEST_MESSAGE, v, r, s),
).to.not.equal(signer);
expect(await this.mock.getFunction('$recover(bytes32,bytes32,bytes32)')(TEST_MESSAGE, r, vs)).to.not.equal(
signer,
);
});
it('reverts wrong v values', async function () {
for (const v of ['0x00', '0x01']) {
const signature = ethers.concat([signatureWithoutV, v]);
await expect(this.mock.$recover(TEST_MESSAGE, signature)).to.be.revertedWithCustomError(
this.mock,
'ECDSAInvalidSignature',
);
const { r, s } = ethers.Signature.from(signature);
await expect(
this.mock.getFunction('$recover(bytes32,uint8,bytes32,bytes32)')(TEST_MESSAGE, v, r, s),
).to.be.revertedWithCustomError(this.mock, 'ECDSAInvalidSignature');
}
});
it('rejects short EIP2098 format', async function () {
const v = '0x1b'; // 27 = 1b.
const signature = ethers.concat([signatureWithoutV, v]);
const { compactSerialized } = ethers.Signature.from(signature);
await expect(this.mock.$recover(TEST_MESSAGE, compactSerialized))
.to.be.revertedWithCustomError(this.mock, 'ECDSAInvalidSignatureLength')
.withArgs(64);
});
});
describe('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 = '0x1c'; // 28 = 1c.
const signature = ethers.concat([signatureWithoutV, v]);
expect(await this.mock.$recover(TEST_MESSAGE, signature)).to.equal(signer);
const { r, s, yParityAndS: vs } = ethers.Signature.from(signature);
expect(await this.mock.getFunction('$recover(bytes32,uint8,bytes32,bytes32)')(TEST_MESSAGE, v, r, s)).to.equal(
signer,
);
expect(await this.mock.getFunction('$recover(bytes32,bytes32,bytes32)')(TEST_MESSAGE, r, vs)).to.equal(signer);
});
it('rejects incorrect v value', async function () {
const v = '0x1b'; // 27 = 1b.
const signature = ethers.concat([signatureWithoutV, v]);
expect(await this.mock.$recover(TEST_MESSAGE, signature)).to.not.equal(signer);
const { r, s, yParityAndS: vs } = ethers.Signature.from(signature);
expect(
await this.mock.getFunction('$recover(bytes32,uint8,bytes32,bytes32)')(TEST_MESSAGE, v, r, s),
).to.not.equal(signer);
expect(await this.mock.getFunction('$recover(bytes32,bytes32,bytes32)')(TEST_MESSAGE, r, vs)).to.not.equal(
signer,
);
});
it('reverts invalid v values', async function () {
for (const v of ['0x00', '0x01']) {
const signature = ethers.concat([signatureWithoutV, v]);
await expect(this.mock.$recover(TEST_MESSAGE, signature)).to.be.revertedWithCustomError(
this.mock,
'ECDSAInvalidSignature',
);
const { r, s } = ethers.Signature.from(signature);
await expect(
this.mock.getFunction('$recover(bytes32,uint8,bytes32,bytes32)')(TEST_MESSAGE, v, r, s),
).to.be.revertedWithCustomError(this.mock, 'ECDSAInvalidSignature');
}
});
it('rejects short EIP2098 format', async function () {
const v = '0x1b'; // 28 = 1b.
const signature = ethers.concat([signatureWithoutV, v]);
const { compactSerialized } = ethers.Signature.from(signature);
await expect(this.mock.$recover(TEST_MESSAGE, compactSerialized))
.to.be.revertedWithCustomError(this.mock, 'ECDSAInvalidSignatureLength')
.withArgs(64);
});
});
it('reverts with high-s value signature', async function () {
const message = '0xb94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9';
// eslint-disable-next-line max-len
const highSSignature =
'0xe742ff452d41413616a5bf43fe15dd88294e983d3d36206c2712f39083d638bde0a0fc89be718fbc1033e1d30d78be1c68081562ed2e97af876f286f3453231d1b';
const r = ethers.dataSlice(highSSignature, 0, 32);
const s = ethers.dataSlice(highSSignature, 32, 64);
const v = ethers.dataSlice(highSSignature, 64, 65);
await expect(this.mock.$recover(message, highSSignature))
.to.be.revertedWithCustomError(this.mock, 'ECDSAInvalidSignatureS')
.withArgs(s);
await expect(this.mock.getFunction('$recover(bytes32,uint8,bytes32,bytes32)')(TEST_MESSAGE, v, r, s))
.to.be.revertedWithCustomError(this.mock, 'ECDSAInvalidSignatureS')
.withArgs(s);
expect(() => ethers.Signature.from(highSSignature)).to.throw('non-canonical s');
});
});
});

View File

@@ -0,0 +1,105 @@
const { ethers } = require('hardhat');
const { expect } = require('chai');
const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
const { getDomain, domainSeparator, hashTypedData } = require('../../helpers/eip712');
const { formatType } = require('../../helpers/eip712-types');
const LENGTHS = {
short: ['A Name', '1'],
long: ['A'.repeat(40), 'B'.repeat(40)],
};
const fixture = async () => {
const [from, to] = await ethers.getSigners();
const lengths = {};
for (const [shortOrLong, [name, version]] of Object.entries(LENGTHS)) {
lengths[shortOrLong] = { name, version };
lengths[shortOrLong].eip712 = await ethers.deployContract('$EIP712Verifier', [name, version]);
lengths[shortOrLong].domain = {
name,
version,
chainId: await ethers.provider.getNetwork().then(({ chainId }) => chainId),
verifyingContract: lengths[shortOrLong].eip712.target,
};
}
return { from, to, lengths };
};
describe('EIP712', function () {
for (const [shortOrLong, [name, version]] of Object.entries(LENGTHS)) {
describe(`with ${shortOrLong} name and version`, function () {
beforeEach('deploying', async function () {
Object.assign(this, await loadFixture(fixture));
Object.assign(this, this.lengths[shortOrLong]);
});
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(rebuildDomain).to.be.deep.equal(this.domain);
});
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 ethers.deployContract('$Clones');
const clone = await factory
.$clone(this.eip712)
.then(tx => tx.wait())
.then(receipt => receipt.logs.find(ev => ev.fragment.name == 'return$clone_address').args.instance)
.then(address => ethers.getContractAt('$EIP712Verifier', address));
const expectedDomain = { ...this.domain, verifyingContract: clone.target };
expect(await getDomain(clone)).to.be.deep.equal(expectedDomain);
const expectedSeparator = await domainSeparator(expectedDomain);
expect(await clone.$_domainSeparatorV4()).to.equal(expectedSeparator);
});
}
});
it('hash digest', async function () {
const structhash = ethers.hexlify(ethers.randomBytes(32));
expect(await this.eip712.$_hashTypedDataV4(structhash)).to.equal(hashTypedData(this.domain, structhash));
});
it('digest', async function () {
const types = {
Mail: formatType({
to: 'address',
contents: 'string',
}),
};
const message = {
to: this.to.address,
contents: 'very interesting',
};
const signature = await this.from.signTypedData(this.domain, types, message);
await expect(this.eip712.verify(signature, this.from.address, message.to, message.contents)).to.not.be.reverted;
});
it('name', async function () {
expect(await this.eip712.$_EIP712Name()).to.equal(name);
});
it('version', async function () {
expect(await this.eip712.$_EIP712Version()).to.equal(version);
});
});
}
});

View File

@@ -0,0 +1,173 @@
const { ethers } = require('hardhat');
const { expect } = require('chai');
const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
const { StandardMerkleTree } = require('@openzeppelin/merkle-tree');
const toElements = str => str.split('').map(e => [e]);
const hashPair = (a, b) => ethers.keccak256(Buffer.concat([a, b].sort(Buffer.compare)));
async function fixture() {
const mock = await ethers.deployContract('$MerkleProof');
return { mock };
}
describe('MerkleProof', function () {
beforeEach(async function () {
Object.assign(this, await loadFixture(fixture));
});
describe('verify', function () {
it('returns true for a valid Merkle proof', async function () {
const merkleTree = StandardMerkleTree.of(
toElements('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='),
['string'],
);
const root = merkleTree.root;
const hash = merkleTree.leafHash(['A']);
const proof = merkleTree.getProof(['A']);
expect(await this.mock.$verify(proof, root, hash)).to.be.true;
expect(await this.mock.$verifyCalldata(proof, root, hash)).to.be.true;
// For demonstration, it is also possible to create valid proofs for certain 64-byte values *not* in elements:
const noSuchLeaf = hashPair(
ethers.toBeArray(merkleTree.leafHash(['A'])),
ethers.toBeArray(merkleTree.leafHash(['B'])),
);
expect(await this.mock.$verify(proof.slice(1), root, noSuchLeaf)).to.be.true;
expect(await this.mock.$verifyCalldata(proof.slice(1), root, noSuchLeaf)).to.be.true;
});
it('returns false for an invalid Merkle proof', async function () {
const correctMerkleTree = StandardMerkleTree.of(toElements('abc'), ['string']);
const otherMerkleTree = StandardMerkleTree.of(toElements('def'), ['string']);
const root = correctMerkleTree.root;
const hash = correctMerkleTree.leafHash(['a']);
const proof = otherMerkleTree.getProof(['d']);
expect(await this.mock.$verify(proof, root, hash)).to.be.false;
expect(await this.mock.$verifyCalldata(proof, root, hash)).to.be.false;
});
it('returns false for a Merkle proof of invalid length', async function () {
const merkleTree = StandardMerkleTree.of(toElements('abc'), ['string']);
const root = merkleTree.root;
const hash = merkleTree.leafHash(['a']);
const proof = merkleTree.getProof(['a']);
const badProof = proof.slice(0, -1);
expect(await this.mock.$verify(badProof, root, hash)).to.be.false;
expect(await this.mock.$verifyCalldata(badProof, root, hash)).to.be.false;
});
});
describe('multiProofVerify', function () {
it('returns true for a valid Merkle multi proof', async function () {
const merkleTree = StandardMerkleTree.of(toElements('abcdef'), ['string']);
const root = merkleTree.root;
const { proof, proofFlags, leaves } = merkleTree.getMultiProof(toElements('bdf'));
const hashes = leaves.map(e => merkleTree.leafHash(e));
expect(await this.mock.$multiProofVerify(proof, proofFlags, root, hashes)).to.be.true;
expect(await this.mock.$multiProofVerifyCalldata(proof, proofFlags, root, hashes)).to.be.true;
});
it('returns false for an invalid Merkle multi proof', async function () {
const merkleTree = StandardMerkleTree.of(toElements('abcdef'), ['string']);
const otherMerkleTree = StandardMerkleTree.of(toElements('ghi'), ['string']);
const root = merkleTree.root;
const { proof, proofFlags, leaves } = otherMerkleTree.getMultiProof(toElements('ghi'));
const hashes = leaves.map(e => merkleTree.leafHash(e));
expect(await this.mock.$multiProofVerify(proof, proofFlags, root, hashes)).to.be.false;
expect(await this.mock.$multiProofVerifyCalldata(proof, proofFlags, root, hashes)).to.be.false;
});
it('revert with invalid multi proof #1', async function () {
const merkleTree = StandardMerkleTree.of(toElements('abcd'), ['string']);
const root = merkleTree.root;
const hashA = merkleTree.leafHash(['a']);
const hashB = merkleTree.leafHash(['b']);
const hashCD = hashPair(
ethers.toBeArray(merkleTree.leafHash(['c'])),
ethers.toBeArray(merkleTree.leafHash(['d'])),
);
const hashE = merkleTree.leafHash(['e']); // incorrect (not part of the tree)
const fill = ethers.randomBytes(32);
await expect(
this.mock.$multiProofVerify([hashB, fill, hashCD], [false, false, false], root, [hashA, hashE]),
).to.be.revertedWithCustomError(this.mock, 'MerkleProofInvalidMultiproof');
await expect(
this.mock.$multiProofVerifyCalldata([hashB, fill, hashCD], [false, false, false], root, [hashA, hashE]),
).to.be.revertedWithCustomError(this.mock, 'MerkleProofInvalidMultiproof');
});
it('revert with invalid multi proof #2', async function () {
const merkleTree = StandardMerkleTree.of(toElements('abcd'), ['string']);
const root = merkleTree.root;
const hashA = merkleTree.leafHash(['a']);
const hashB = merkleTree.leafHash(['b']);
const hashCD = hashPair(
ethers.toBeArray(merkleTree.leafHash(['c'])),
ethers.toBeArray(merkleTree.leafHash(['d'])),
);
const hashE = merkleTree.leafHash(['e']); // incorrect (not part of the tree)
const fill = ethers.randomBytes(32);
await expect(
this.mock.$multiProofVerify([hashB, fill, hashCD], [false, false, false, false], root, [hashE, hashA]),
).to.be.revertedWithPanic(0x32);
await expect(
this.mock.$multiProofVerifyCalldata([hashB, fill, hashCD], [false, false, false, false], root, [hashE, hashA]),
).to.be.revertedWithPanic(0x32);
});
it('limit case: works for tree containing a single leaf', async function () {
const merkleTree = StandardMerkleTree.of(toElements('a'), ['string']);
const root = merkleTree.root;
const { proof, proofFlags, leaves } = merkleTree.getMultiProof(toElements('a'));
const hashes = leaves.map(e => merkleTree.leafHash(e));
expect(await this.mock.$multiProofVerify(proof, proofFlags, root, hashes)).to.be.true;
expect(await this.mock.$multiProofVerifyCalldata(proof, proofFlags, root, hashes)).to.be.true;
});
it('limit case: can prove empty leaves', async function () {
const merkleTree = StandardMerkleTree.of(toElements('abcd'), ['string']);
const root = merkleTree.root;
expect(await this.mock.$multiProofVerify([root], [], root, [])).to.be.true;
expect(await this.mock.$multiProofVerifyCalldata([root], [], root, [])).to.be.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 leave = ethers.id('real leaf');
const root = hashPair(ethers.toBeArray(leave), Buffer.alloc(32, 0));
// Now we can pass any **malicious** fake leaves as valid!
const maliciousLeaves = ['malicious', 'leaves'].map(ethers.id).map(ethers.toBeArray).sort(Buffer.compare);
const maliciousProof = [leave, leave];
const maliciousProofFlags = [true, true, false];
await expect(
this.mock.$multiProofVerify(maliciousProof, maliciousProofFlags, root, maliciousLeaves),
).to.be.revertedWithCustomError(this.mock, 'MerkleProofInvalidMultiproof');
await expect(
this.mock.$multiProofVerifyCalldata(maliciousProof, maliciousProofFlags, root, maliciousLeaves),
).to.be.revertedWithCustomError(this.mock, 'MerkleProofInvalidMultiproof');
});
});
});

View File

@@ -0,0 +1,68 @@
const { ethers } = require('hardhat');
const { expect } = require('chai');
const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
const { domainSeparator, hashTypedData } = require('../../helpers/eip712');
async function fixture() {
const mock = await ethers.deployContract('$MessageHashUtils');
return { mock };
}
describe('MessageHashUtils', function () {
beforeEach(async function () {
Object.assign(this, await loadFixture(fixture));
});
describe('toEthSignedMessageHash', function () {
it('prefixes bytes32 data correctly', async function () {
const message = ethers.randomBytes(32);
const expectedHash = ethers.hashMessage(message);
expect(await this.mock.getFunction('$toEthSignedMessageHash(bytes32)')(message)).to.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);
});
it('version match for bytes32', async function () {
const message = ethers.randomBytes(32);
const fixed = await this.mock.getFunction('$toEthSignedMessageHash(bytes32)')(message);
const dynamic = await this.mock.getFunction('$toEthSignedMessageHash(bytes)')(message);
expect(fixed).to.equal(dynamic);
});
});
describe('toDataWithIntendedValidatorHash', function () {
it('returns the digest correctly', async function () {
const verifier = ethers.Wallet.createRandom().address;
const message = ethers.randomBytes(128);
const expectedHash = ethers.solidityPackedKeccak256(
['string', 'address', 'bytes'],
['\x19\x00', verifier, message],
);
expect(await this.mock.$toDataWithIntendedValidatorHash(verifier, message)).to.equal(expectedHash);
});
});
describe('toTypedDataHash', function () {
it('returns the digest correctly', async function () {
const domain = {
name: 'Test',
version: '1',
chainId: 1n,
verifyingContract: ethers.Wallet.createRandom().address,
};
const structhash = ethers.randomBytes(32);
const expectedHash = hashTypedData(domain, structhash);
expect(await this.mock.$toTypedDataHash(domainSeparator(domain), structhash)).to.equal(expectedHash);
});
});
});

View File

@@ -0,0 +1,61 @@
const { ethers } = require('hardhat');
const { expect } = require('chai');
const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
const TEST_MESSAGE = ethers.id('OpenZeppelin');
const TEST_MESSAGE_HASH = ethers.hashMessage(TEST_MESSAGE);
const WRONG_MESSAGE = ethers.id('Nope');
const WRONG_MESSAGE_HASH = ethers.hashMessage(WRONG_MESSAGE);
async function fixture() {
const [signer, other] = await ethers.getSigners();
const mock = await ethers.deployContract('$SignatureChecker');
const wallet = await ethers.deployContract('ERC1271WalletMock', [signer]);
const malicious = await ethers.deployContract('ERC1271MaliciousMock');
const signature = await signer.signMessage(TEST_MESSAGE);
return { signer, other, mock, wallet, malicious, signature };
}
describe('SignatureChecker (ERC1271)', function () {
before('deploying', async function () {
Object.assign(this, await loadFixture(fixture));
});
describe('EOA account', function () {
it('with matching signer and signature', async function () {
expect(await this.mock.$isValidSignatureNow(this.signer, TEST_MESSAGE_HASH, this.signature)).to.be.true;
});
it('with invalid signer', async function () {
expect(await this.mock.$isValidSignatureNow(this.other, TEST_MESSAGE_HASH, this.signature)).to.be.false;
});
it('with invalid signature', async function () {
expect(await this.mock.$isValidSignatureNow(this.signer, WRONG_MESSAGE_HASH, this.signature)).to.be.false;
});
});
describe('ERC1271 wallet', function () {
for (const fn of ['isValidERC1271SignatureNow', 'isValidSignatureNow']) {
describe(fn, function () {
it('with matching signer and signature', async function () {
expect(await this.mock.getFunction(`$${fn}`)(this.wallet, TEST_MESSAGE_HASH, this.signature)).to.be.true;
});
it('with invalid signer', async function () {
expect(await this.mock.getFunction(`$${fn}`)(this.mock, TEST_MESSAGE_HASH, this.signature)).to.be.false;
});
it('with invalid signature', async function () {
expect(await this.mock.getFunction(`$${fn}`)(this.wallet, WRONG_MESSAGE_HASH, this.signature)).to.be.false;
});
it('with malicious wallet', async function () {
expect(await this.mock.getFunction(`$${fn}`)(this.malicious, TEST_MESSAGE_HASH, this.signature)).to.be.false;
});
});
}
});
});

View File

@@ -0,0 +1,18 @@
const { ethers } = require('hardhat');
const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
const { shouldSupportInterfaces } = require('./SupportsInterface.behavior');
async function fixture() {
return {
mock: await ethers.deployContract('$ERC165'),
};
}
describe('ERC165', function () {
beforeEach(async function () {
Object.assign(this, await loadFixture(fixture));
});
shouldSupportInterfaces();
});

View File

@@ -0,0 +1,245 @@
const { ethers } = require('hardhat');
const { expect } = require('chai');
const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
const DUMMY_ID = '0xdeadbeef';
const DUMMY_ID_2 = '0xcafebabe';
const DUMMY_ID_3 = '0xdecafbad';
const DUMMY_UNSUPPORTED_ID = '0xbaddcafe';
const DUMMY_UNSUPPORTED_ID_2 = '0xbaadcafe';
const DUMMY_ACCOUNT = '0x1111111111111111111111111111111111111111';
async function fixture() {
return { mock: await ethers.deployContract('$ERC165Checker') };
}
describe('ERC165Checker', function () {
beforeEach(async function () {
Object.assign(this, await loadFixture(fixture));
});
describe('ERC165 missing return data', function () {
before(async function () {
this.target = await ethers.deployContract('ERC165MissingData');
});
it('does not support ERC165', async function () {
expect(await this.mock.$supportsERC165(this.target)).to.be.false;
});
it('does not support mock interface via supportsInterface', async function () {
expect(await this.mock.$supportsInterface(this.target, DUMMY_ID)).to.be.false;
});
it('does not support mock interface via supportsAllInterfaces', async function () {
expect(await this.mock.$supportsAllInterfaces(this.target, [DUMMY_ID])).to.be.false;
});
it('does not support mock interface via getSupportedInterfaces', async function () {
expect(await this.mock.$getSupportedInterfaces(this.target, [DUMMY_ID])).to.deep.equal([false]);
});
it('does not support mock interface via supportsERC165InterfaceUnchecked', async function () {
expect(await this.mock.$supportsERC165InterfaceUnchecked(this.target, DUMMY_ID)).to.be.false;
});
});
describe('ERC165 malicious return data', function () {
beforeEach(async function () {
this.target = await ethers.deployContract('ERC165MaliciousData');
});
it('does not support ERC165', async function () {
expect(await this.mock.$supportsERC165(this.target)).to.be.false;
});
it('does not support mock interface via supportsInterface', async function () {
expect(await this.mock.$supportsInterface(this.target, DUMMY_ID)).to.be.false;
});
it('does not support mock interface via supportsAllInterfaces', async function () {
expect(await this.mock.$supportsAllInterfaces(this.target, [DUMMY_ID])).to.be.false;
});
it('does not support mock interface via getSupportedInterfaces', async function () {
expect(await this.mock.$getSupportedInterfaces(this.target, [DUMMY_ID])).to.deep.equal([false]);
});
it('does not support mock interface via supportsERC165InterfaceUnchecked', async function () {
expect(await this.mock.$supportsERC165InterfaceUnchecked(this.target, DUMMY_ID)).to.be.true;
});
});
describe('ERC165 not supported', function () {
beforeEach(async function () {
this.target = await ethers.deployContract('ERC165NotSupported');
});
it('does not support ERC165', async function () {
expect(await this.mock.$supportsERC165(this.target)).to.be.false;
});
it('does not support mock interface via supportsInterface', async function () {
expect(await this.mock.$supportsInterface(this.target, DUMMY_ID)).to.be.false;
});
it('does not support mock interface via supportsAllInterfaces', async function () {
expect(await this.mock.$supportsAllInterfaces(this.target, [DUMMY_ID])).to.be.false;
});
it('does not support mock interface via getSupportedInterfaces', async function () {
expect(await this.mock.$getSupportedInterfaces(this.target, [DUMMY_ID])).to.deep.equal([false]);
});
it('does not support mock interface via supportsERC165InterfaceUnchecked', async function () {
expect(await this.mock.$supportsERC165InterfaceUnchecked(this.target, DUMMY_ID)).to.be.false;
});
});
describe('ERC165 supported', function () {
beforeEach(async function () {
this.target = await ethers.deployContract('ERC165InterfacesSupported', [[]]);
});
it('supports ERC165', async function () {
expect(await this.mock.$supportsERC165(this.target)).to.be.true;
});
it('does not support mock interface via supportsInterface', async function () {
expect(await this.mock.$supportsInterface(this.target, DUMMY_ID)).to.be.false;
});
it('does not support mock interface via supportsAllInterfaces', async function () {
expect(await this.mock.$supportsAllInterfaces(this.target, [DUMMY_ID])).to.be.false;
});
it('does not support mock interface via getSupportedInterfaces', async function () {
expect(await this.mock.$getSupportedInterfaces(this.target, [DUMMY_ID])).to.deep.equal([false]);
});
it('does not support mock interface via supportsERC165InterfaceUnchecked', async function () {
expect(await this.mock.$supportsERC165InterfaceUnchecked(this.target, DUMMY_ID)).to.be.false;
});
});
describe('ERC165 and single interface supported', function () {
beforeEach(async function () {
this.target = await ethers.deployContract('ERC165InterfacesSupported', [[DUMMY_ID]]);
});
it('supports ERC165', async function () {
expect(await this.mock.$supportsERC165(this.target)).to.be.true;
});
it('supports mock interface via supportsInterface', async function () {
expect(await this.mock.$supportsInterface(this.target, DUMMY_ID)).to.be.true;
});
it('supports mock interface via supportsAllInterfaces', async function () {
expect(await this.mock.$supportsAllInterfaces(this.target, [DUMMY_ID])).to.be.true;
});
it('supports mock interface via getSupportedInterfaces', async function () {
expect(await this.mock.$getSupportedInterfaces(this.target, [DUMMY_ID])).to.deep.equal([true]);
});
it('supports mock interface via supportsERC165InterfaceUnchecked', async function () {
expect(await this.mock.$supportsERC165InterfaceUnchecked(this.target, DUMMY_ID)).to.be.true;
});
});
describe('ERC165 and many interfaces supported', function () {
const supportedInterfaces = [DUMMY_ID, DUMMY_ID_2, DUMMY_ID_3];
beforeEach(async function () {
this.target = await ethers.deployContract('ERC165InterfacesSupported', [supportedInterfaces]);
});
it('supports ERC165', async function () {
expect(await this.mock.$supportsERC165(this.target)).to.be.true;
});
it('supports each interfaceId via supportsInterface', async function () {
for (const interfaceId of supportedInterfaces) {
expect(await this.mock.$supportsInterface(this.target, interfaceId)).to.be.true;
}
});
it('supports all interfaceIds via supportsAllInterfaces', async function () {
expect(await this.mock.$supportsAllInterfaces(this.target, supportedInterfaces)).to.be.true;
});
it('supports none of the interfaces queried via supportsAllInterfaces', async function () {
const interfaceIdsToTest = [DUMMY_UNSUPPORTED_ID, DUMMY_UNSUPPORTED_ID_2];
expect(await this.mock.$supportsAllInterfaces(this.target, interfaceIdsToTest)).to.be.false;
});
it('supports not all of the interfaces queried via supportsAllInterfaces', async function () {
const interfaceIdsToTest = [...supportedInterfaces, DUMMY_UNSUPPORTED_ID];
expect(await this.mock.$supportsAllInterfaces(this.target, interfaceIdsToTest)).to.be.false;
});
it('supports all interfaceIds via getSupportedInterfaces', async function () {
expect(await this.mock.$getSupportedInterfaces(this.target, supportedInterfaces)).to.deep.equal(
supportedInterfaces.map(i => supportedInterfaces.includes(i)),
);
});
it('supports none of the interfaces queried via getSupportedInterfaces', async function () {
const interfaceIdsToTest = [DUMMY_UNSUPPORTED_ID, DUMMY_UNSUPPORTED_ID_2];
expect(await this.mock.$getSupportedInterfaces(this.target, interfaceIdsToTest)).to.deep.equal(
interfaceIdsToTest.map(i => supportedInterfaces.includes(i)),
);
});
it('supports not all of the interfaces queried via getSupportedInterfaces', async function () {
const interfaceIdsToTest = [...supportedInterfaces, DUMMY_UNSUPPORTED_ID];
expect(await this.mock.$getSupportedInterfaces(this.target, interfaceIdsToTest)).to.deep.equal(
interfaceIdsToTest.map(i => supportedInterfaces.includes(i)),
);
});
it('supports each interfaceId via supportsERC165InterfaceUnchecked', async function () {
for (const interfaceId of supportedInterfaces) {
expect(await this.mock.$supportsERC165InterfaceUnchecked(this.target, interfaceId)).to.be.true;
}
});
});
describe('account address does not support ERC165', function () {
it('does not support ERC165', async function () {
expect(await this.mock.$supportsERC165(DUMMY_ACCOUNT)).to.be.false;
});
it('does not support mock interface via supportsInterface', async function () {
expect(await this.mock.$supportsInterface(DUMMY_ACCOUNT, DUMMY_ID)).to.be.false;
});
it('does not support mock interface via supportsAllInterfaces', async function () {
expect(await this.mock.$supportsAllInterfaces(DUMMY_ACCOUNT, [DUMMY_ID])).to.be.false;
});
it('does not support mock interface via getSupportedInterfaces', async function () {
expect(await this.mock.$getSupportedInterfaces(DUMMY_ACCOUNT, [DUMMY_ID])).to.deep.equal([false]);
});
it('does not support mock interface via supportsERC165InterfaceUnchecked', async function () {
expect(await this.mock.$supportsERC165InterfaceUnchecked(DUMMY_ACCOUNT, DUMMY_ID)).to.be.false;
});
});
it('Return bomb resistance', async function () {
this.target = await ethers.deployContract('ERC165ReturnBombMock');
const { gasUsed: gasUsed1 } = await this.mock.$supportsInterface.send(this.target, DUMMY_ID).then(tx => tx.wait());
expect(gasUsed1).to.be.lessThan(120_000n); // 3*30k + 21k + some margin
const { gasUsed: gasUsed2 } = await this.mock.$getSupportedInterfaces
.send(this.target, [DUMMY_ID, DUMMY_ID_2, DUMMY_ID_3, DUMMY_UNSUPPORTED_ID, DUMMY_UNSUPPORTED_ID_2])
.then(tx => tx.wait());
expect(gasUsed2).to.be.lessThan(250_000n); // (2+5)*30k + 21k + some margin
});
});

View File

@@ -0,0 +1,145 @@
const { expect } = require('chai');
const { interfaceId } = require('../../helpers/methods');
const { mapValues } = require('../../helpers/iterate');
const INVALID_ID = '0xffffffff';
const SIGNATURES = {
ERC165: ['supportsInterface(bytes4)'],
ERC721: [
'balanceOf(address)',
'ownerOf(uint256)',
'approve(address,uint256)',
'getApproved(uint256)',
'setApprovalForAll(address,bool)',
'isApprovedForAll(address,address)',
'transferFrom(address,address,uint256)',
'safeTransferFrom(address,address,uint256)',
'safeTransferFrom(address,address,uint256,bytes)',
],
ERC721Enumerable: ['totalSupply()', 'tokenOfOwnerByIndex(address,uint256)', 'tokenByIndex(uint256)'],
ERC721Metadata: ['name()', 'symbol()', 'tokenURI(uint256)'],
ERC1155: [
'balanceOf(address,uint256)',
'balanceOfBatch(address[],uint256[])',
'setApprovalForAll(address,bool)',
'isApprovedForAll(address,address)',
'safeTransferFrom(address,address,uint256,uint256,bytes)',
'safeBatchTransferFrom(address,address,uint256[],uint256[],bytes)',
],
ERC1155MetadataURI: ['uri(uint256)'],
ERC1155Receiver: [
'onERC1155Received(address,address,uint256,uint256,bytes)',
'onERC1155BatchReceived(address,address,uint256[],uint256[],bytes)',
],
ERC1363: [
'transferAndCall(address,uint256)',
'transferAndCall(address,uint256,bytes)',
'transferFromAndCall(address,address,uint256)',
'transferFromAndCall(address,address,uint256,bytes)',
'approveAndCall(address,uint256)',
'approveAndCall(address,uint256,bytes)',
],
AccessControl: [
'hasRole(bytes32,address)',
'getRoleAdmin(bytes32)',
'grantRole(bytes32,address)',
'revokeRole(bytes32,address)',
'renounceRole(bytes32,address)',
],
AccessControlEnumerable: ['getRoleMember(bytes32,uint256)', 'getRoleMemberCount(bytes32)'],
AccessControlDefaultAdminRules: [
'defaultAdminDelay()',
'pendingDefaultAdminDelay()',
'defaultAdmin()',
'pendingDefaultAdmin()',
'defaultAdminDelayIncreaseWait()',
'changeDefaultAdminDelay(uint48)',
'rollbackDefaultAdminDelay()',
'beginDefaultAdminTransfer(address)',
'acceptDefaultAdminTransfer()',
'cancelDefaultAdminTransfer()',
],
Governor: [
'name()',
'version()',
'COUNTING_MODE()',
'hashProposal(address[],uint256[],bytes[],bytes32)',
'state(uint256)',
'proposalThreshold()',
'proposalSnapshot(uint256)',
'proposalDeadline(uint256)',
'proposalProposer(uint256)',
'proposalEta(uint256)',
'proposalNeedsQueuing(uint256)',
'votingDelay()',
'votingPeriod()',
'quorum(uint256)',
'getVotes(address,uint256)',
'getVotesWithParams(address,uint256,bytes)',
'hasVoted(uint256,address)',
'propose(address[],uint256[],bytes[],string)',
'queue(address[],uint256[],bytes[],bytes32)',
'execute(address[],uint256[],bytes[],bytes32)',
'cancel(address[],uint256[],bytes[],bytes32)',
'castVote(uint256,uint8)',
'castVoteWithReason(uint256,uint8,string)',
'castVoteWithReasonAndParams(uint256,uint8,string,bytes)',
'castVoteBySig(uint256,uint8,address,bytes)',
'castVoteWithReasonAndParamsBySig(uint256,uint8,address,string,bytes,bytes)',
],
ERC2981: ['royaltyInfo(uint256,uint256)'],
};
const INTERFACE_IDS = mapValues(SIGNATURES, interfaceId);
function shouldSupportInterfaces(interfaces = []) {
interfaces.unshift('ERC165');
describe('ERC165', function () {
beforeEach(function () {
this.contractUnderTest = this.mock || this.token;
});
describe('when the interfaceId is supported', function () {
it('uses less than 30k gas', async function () {
for (const k of interfaces) {
const interface = INTERFACE_IDS[k] ?? k;
expect(await this.contractUnderTest.supportsInterface.estimateGas(interface)).to.lte(30_000n);
}
});
it('returns true', async function () {
for (const k of interfaces) {
const interfaceId = INTERFACE_IDS[k] ?? k;
expect(await this.contractUnderTest.supportsInterface(interfaceId), `does not support ${k}`).to.be.true;
}
});
});
describe('when the interfaceId is not supported', function () {
it('uses less than 30k', async function () {
expect(await this.contractUnderTest.supportsInterface.estimateGas(INVALID_ID)).to.lte(30_000n);
});
it('returns false', async function () {
expect(await this.contractUnderTest.supportsInterface(INVALID_ID), `supports ${INVALID_ID}`).to.be.false;
});
});
it('all interface functions are in ABI', async function () {
for (const k of interfaces) {
// skip interfaces for which we don't have a function list
if (SIGNATURES[k] === undefined) continue;
// Check the presence of each function in the contract's interface
for (const fnSig of SIGNATURES[k]) {
expect(this.contractUnderTest.interface.hasFunction(fnSig), `did not find ${fnSig}`).to.be.true;
}
}
});
});
}
module.exports = {
shouldSupportInterfaces,
};

View File

@@ -0,0 +1,311 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import {Test, stdError} from "@forge-std/Test.sol";
import {Math} from "@openzeppelin/contracts/utils/math/Math.sol";
contract MathTest is Test {
function testSelect(bool f, uint256 a, uint256 b) public {
assertEq(Math.ternary(f, a, b), f ? a : b);
}
// MIN & MAX
function testMinMax(uint256 a, uint256 b) public {
assertEq(Math.min(a, b), a < b ? a : b);
assertEq(Math.max(a, b), a > b ? a : b);
}
// CEILDIV
function testCeilDiv(uint256 a, uint256 b) public {
vm.assume(b > 0);
uint256 result = Math.ceilDiv(a, b);
if (result == 0) {
assertEq(a, 0);
} else {
uint256 expect = a / b;
if (expect * b < a) {
expect += 1;
}
assertEq(result, expect);
}
}
// SQRT
function testSqrt(uint256 input, uint8 r) public {
Math.Rounding rounding = _asRounding(r);
uint256 result = Math.sqrt(input, rounding);
// square of result is bigger than input
if (_squareBigger(result, input)) {
assertTrue(Math.unsignedRoundsUp(rounding));
assertTrue(_squareSmaller(result - 1, input));
}
// square of result is smaller than input
else if (_squareSmaller(result, input)) {
assertFalse(Math.unsignedRoundsUp(rounding));
assertTrue(_squareBigger(result + 1, input));
}
// input is perfect square
else {
assertEq(result * result, input);
}
}
function _squareBigger(uint256 value, uint256 ref) private pure returns (bool) {
(bool noOverflow, uint256 square) = Math.tryMul(value, value);
return !noOverflow || square > ref;
}
function _squareSmaller(uint256 value, uint256 ref) private pure returns (bool) {
return value * value < ref;
}
// INV
function testInvMod(uint256 value, uint256 p) public {
_testInvMod(value, p, true);
}
function testInvMod2(uint256 seed) public {
uint256 p = 2; // prime
_testInvMod(bound(seed, 1, p - 1), p, false);
}
function testInvMod17(uint256 seed) public {
uint256 p = 17; // prime
_testInvMod(bound(seed, 1, p - 1), p, false);
}
function testInvMod65537(uint256 seed) public {
uint256 p = 65537; // prime
_testInvMod(bound(seed, 1, p - 1), p, false);
}
function testInvModP256(uint256 seed) public {
uint256 p = 0xffffffff00000001000000000000000000000000ffffffffffffffffffffffff; // prime
_testInvMod(bound(seed, 1, p - 1), p, false);
}
function _testInvMod(uint256 value, uint256 p, bool allowZero) private {
uint256 inverse = Math.invMod(value, p);
if (inverse != 0) {
assertEq(mulmod(value, inverse, p), 1);
assertLt(inverse, p);
} else {
assertTrue(allowZero);
}
}
// LOG2
function testLog2(uint256 input, uint8 r) public {
Math.Rounding rounding = _asRounding(r);
uint256 result = Math.log2(input, rounding);
if (input == 0) {
assertEq(result, 0);
} else if (_powerOf2Bigger(result, input)) {
assertTrue(Math.unsignedRoundsUp(rounding));
assertTrue(_powerOf2Smaller(result - 1, input));
} else if (_powerOf2Smaller(result, input)) {
assertFalse(Math.unsignedRoundsUp(rounding));
assertTrue(_powerOf2Bigger(result + 1, input));
} else {
assertEq(2 ** result, input);
}
}
function _powerOf2Bigger(uint256 value, uint256 ref) private pure returns (bool) {
return value >= 256 || 2 ** value > ref; // 2**256 overflows uint256
}
function _powerOf2Smaller(uint256 value, uint256 ref) private pure returns (bool) {
return 2 ** value < ref;
}
// LOG10
function testLog10(uint256 input, uint8 r) public {
Math.Rounding rounding = _asRounding(r);
uint256 result = Math.log10(input, rounding);
if (input == 0) {
assertEq(result, 0);
} else if (_powerOf10Bigger(result, input)) {
assertTrue(Math.unsignedRoundsUp(rounding));
assertTrue(_powerOf10Smaller(result - 1, input));
} else if (_powerOf10Smaller(result, input)) {
assertFalse(Math.unsignedRoundsUp(rounding));
assertTrue(_powerOf10Bigger(result + 1, input));
} else {
assertEq(10 ** result, input);
}
}
function _powerOf10Bigger(uint256 value, uint256 ref) private pure returns (bool) {
return value >= 78 || 10 ** value > ref; // 10**78 overflows uint256
}
function _powerOf10Smaller(uint256 value, uint256 ref) private pure returns (bool) {
return 10 ** value < ref;
}
// LOG256
function testLog256(uint256 input, uint8 r) public {
Math.Rounding rounding = _asRounding(r);
uint256 result = Math.log256(input, rounding);
if (input == 0) {
assertEq(result, 0);
} else if (_powerOf256Bigger(result, input)) {
assertTrue(Math.unsignedRoundsUp(rounding));
assertTrue(_powerOf256Smaller(result - 1, input));
} else if (_powerOf256Smaller(result, input)) {
assertFalse(Math.unsignedRoundsUp(rounding));
assertTrue(_powerOf256Bigger(result + 1, input));
} else {
assertEq(256 ** result, input);
}
}
function _powerOf256Bigger(uint256 value, uint256 ref) private pure returns (bool) {
return value >= 32 || 256 ** value > ref; // 256**32 overflows uint256
}
function _powerOf256Smaller(uint256 value, uint256 ref) private pure returns (bool) {
return 256 ** value < ref;
}
// MULDIV
function testMulDiv(uint256 x, uint256 y, uint256 d) public {
// Full precision for x * y
(uint256 xyHi, uint256 xyLo) = _mulHighLow(x, y);
// Assume result won't overflow (see {testMulDivDomain})
// This also checks that `d` is positive
vm.assume(xyHi < d);
// Perform muldiv
uint256 q = Math.mulDiv(x, y, d);
// Full precision for q * d
(uint256 qdHi, uint256 qdLo) = _mulHighLow(q, d);
// Add remainder of x * y / d (computed as rem = (x * y % d))
(uint256 qdRemLo, uint256 c) = _addCarry(qdLo, mulmod(x, y, d));
uint256 qdRemHi = qdHi + c;
// Full precision check that x * y = q * d + rem
assertEq(xyHi, qdRemHi);
assertEq(xyLo, qdRemLo);
}
function testMulDivDomain(uint256 x, uint256 y, uint256 d) public {
(uint256 xyHi, ) = _mulHighLow(x, y);
// Violate {testMulDiv} assumption (covers d is 0 and result overflow)
vm.assume(xyHi >= d);
// we are outside the scope of {testMulDiv}, we expect muldiv to revert
vm.expectRevert(d == 0 ? stdError.divisionError : stdError.arithmeticError);
Math.mulDiv(x, y, d);
}
// MOD EXP
function testModExp(uint256 b, uint256 e, uint256 m) public {
if (m == 0) {
vm.expectRevert(stdError.divisionError);
}
uint256 result = Math.modExp(b, e, m);
assertLt(result, m);
assertEq(result, _nativeModExp(b, e, m));
}
function testTryModExp(uint256 b, uint256 e, uint256 m) public {
(bool success, uint256 result) = Math.tryModExp(b, e, m);
assertEq(success, m != 0);
if (success) {
assertLt(result, m);
assertEq(result, _nativeModExp(b, e, m));
} else {
assertEq(result, 0);
}
}
function testModExpMemory(uint256 b, uint256 e, uint256 m) public {
if (m == 0) {
vm.expectRevert(stdError.divisionError);
}
bytes memory result = Math.modExp(abi.encodePacked(b), abi.encodePacked(e), abi.encodePacked(m));
assertEq(result.length, 0x20);
uint256 res = abi.decode(result, (uint256));
assertLt(res, m);
assertEq(res, _nativeModExp(b, e, m));
}
function testTryModExpMemory(uint256 b, uint256 e, uint256 m) public {
(bool success, bytes memory result) = Math.tryModExp(
abi.encodePacked(b),
abi.encodePacked(e),
abi.encodePacked(m)
);
if (success) {
assertEq(result.length, 0x20); // m is a uint256, so abi.encodePacked(m).length is 0x20
uint256 res = abi.decode(result, (uint256));
assertLt(res, m);
assertEq(res, _nativeModExp(b, e, m));
} else {
assertEq(result.length, 0);
}
}
function _nativeModExp(uint256 b, uint256 e, uint256 m) private pure returns (uint256) {
if (m == 1) return 0;
uint256 r = 1;
while (e > 0) {
if (e % 2 > 0) {
r = mulmod(r, b, m);
}
b = mulmod(b, b, m);
e >>= 1;
}
return r;
}
// Helpers
function _asRounding(uint8 r) private pure returns (Math.Rounding) {
vm.assume(r < uint8(type(Math.Rounding).max));
return Math.Rounding(r);
}
function _mulHighLow(uint256 x, uint256 y) private pure returns (uint256 high, uint256 low) {
(uint256 x0, uint256 x1) = (x & type(uint128).max, x >> 128);
(uint256 y0, uint256 y1) = (y & type(uint128).max, y >> 128);
// Karatsuba algorithm
// https://en.wikipedia.org/wiki/Karatsuba_algorithm
uint256 z2 = x1 * y1;
uint256 z1a = x1 * y0;
uint256 z1b = x0 * y1;
uint256 z0 = x0 * y0;
uint256 carry = ((z1a & type(uint128).max) + (z1b & type(uint128).max) + (z0 >> 128)) >> 128;
high = z2 + (z1a >> 128) + (z1b >> 128) + carry;
unchecked {
low = x * y;
}
}
function _addCarry(uint256 x, uint256 y) private pure returns (uint256 res, uint256 carry) {
unchecked {
res = x + y;
}
carry = res < x ? 1 : 0;
}
}

View File

@@ -0,0 +1,562 @@
const { ethers } = require('hardhat');
const { expect } = require('chai');
const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
const { PANIC_CODES } = require('@nomicfoundation/hardhat-chai-matchers/panic');
const { Rounding } = require('../../helpers/enums');
const { min, max, modExp } = require('../../helpers/math');
const { generators } = require('../../helpers/random');
const { product, range } = require('../../helpers/iterate');
const RoundingDown = [Rounding.Floor, Rounding.Trunc];
const RoundingUp = [Rounding.Ceil, Rounding.Expand];
const bytes = (value, width = undefined) => ethers.Typed.bytes(ethers.toBeHex(value, width));
const uint256 = value => ethers.Typed.uint256(value);
bytes.zero = '0x';
uint256.zero = 0n;
async function testCommutative(fn, lhs, rhs, expected, ...extra) {
expect(await fn(lhs, rhs, ...extra)).to.deep.equal(expected);
expect(await fn(rhs, lhs, ...extra)).to.deep.equal(expected);
}
async function fixture() {
const mock = await ethers.deployContract('$Math');
// disambiguation, we use the version with explicit rounding
mock.$mulDiv = mock['$mulDiv(uint256,uint256,uint256,uint8)'];
mock.$sqrt = mock['$sqrt(uint256,uint8)'];
mock.$log2 = mock['$log2(uint256,uint8)'];
mock.$log10 = mock['$log10(uint256,uint8)'];
mock.$log256 = mock['$log256(uint256,uint8)'];
return { mock };
}
describe('Math', function () {
beforeEach(async function () {
Object.assign(this, await loadFixture(fixture));
});
describe('tryAdd', function () {
it('adds correctly', async function () {
const a = 5678n;
const b = 1234n;
await testCommutative(this.mock.$tryAdd, a, b, [true, a + b]);
});
it('reverts on addition overflow', async function () {
const a = ethers.MaxUint256;
const b = 1n;
await testCommutative(this.mock.$tryAdd, a, b, [false, 0n]);
});
});
describe('trySub', function () {
it('subtracts correctly', async function () {
const a = 5678n;
const b = 1234n;
expect(await this.mock.$trySub(a, b)).to.deep.equal([true, a - b]);
});
it('reverts if subtraction result would be negative', async function () {
const a = 1234n;
const b = 5678n;
expect(await this.mock.$trySub(a, b)).to.deep.equal([false, 0n]);
});
});
describe('tryMul', function () {
it('multiplies correctly', async function () {
const a = 1234n;
const b = 5678n;
await testCommutative(this.mock.$tryMul, a, b, [true, a * b]);
});
it('multiplies by zero correctly', async function () {
const a = 0n;
const b = 5678n;
await testCommutative(this.mock.$tryMul, a, b, [true, a * b]);
});
it('reverts on multiplication overflow', async function () {
const a = ethers.MaxUint256;
const b = 2n;
await testCommutative(this.mock.$tryMul, a, b, [false, 0n]);
});
});
describe('tryDiv', function () {
it('divides correctly', async function () {
const a = 5678n;
const b = 5678n;
expect(await this.mock.$tryDiv(a, b)).to.deep.equal([true, a / b]);
});
it('divides zero correctly', async function () {
const a = 0n;
const b = 5678n;
expect(await this.mock.$tryDiv(a, b)).to.deep.equal([true, a / b]);
});
it('returns complete number result on non-even division', async function () {
const a = 7000n;
const b = 5678n;
expect(await this.mock.$tryDiv(a, b)).to.deep.equal([true, a / b]);
});
it('reverts on division by zero', async function () {
const a = 5678n;
const b = 0n;
expect(await this.mock.$tryDiv(a, b)).to.deep.equal([false, 0n]);
});
});
describe('tryMod', function () {
describe('modulos correctly', function () {
it('when the dividend is smaller than the divisor', async function () {
const a = 284n;
const b = 5678n;
expect(await this.mock.$tryMod(a, b)).to.deep.equal([true, a % b]);
});
it('when the dividend is equal to the divisor', async function () {
const a = 5678n;
const b = 5678n;
expect(await this.mock.$tryMod(a, b)).to.deep.equal([true, a % b]);
});
it('when the dividend is larger than the divisor', async function () {
const a = 7000n;
const b = 5678n;
expect(await this.mock.$tryMod(a, b)).to.deep.equal([true, a % b]);
});
it('when the dividend is a multiple of the divisor', async function () {
const a = 17034n; // 17034 == 5678 * 3
const b = 5678n;
expect(await this.mock.$tryMod(a, b)).to.deep.equal([true, a % b]);
});
});
it('reverts with a 0 divisor', async function () {
const a = 5678n;
const b = 0n;
expect(await this.mock.$tryMod(a, b)).to.deep.equal([false, 0n]);
});
});
describe('max', function () {
it('is correctly detected in both position', async function () {
await testCommutative(this.mock.$max, 1234n, 5678n, max(1234n, 5678n));
});
});
describe('min', function () {
it('is correctly detected in both position', async function () {
await testCommutative(this.mock.$min, 1234n, 5678n, min(1234n, 5678n));
});
});
describe('average', function () {
it('is correctly calculated with two odd numbers', async function () {
const a = 57417n;
const b = 95431n;
expect(await this.mock.$average(a, b)).to.equal((a + b) / 2n);
});
it('is correctly calculated with two even numbers', async function () {
const a = 42304n;
const b = 84346n;
expect(await this.mock.$average(a, b)).to.equal((a + b) / 2n);
});
it('is correctly calculated with one even and one odd number', async function () {
const a = 57417n;
const b = 84346n;
expect(await this.mock.$average(a, b)).to.equal((a + b) / 2n);
});
it('is correctly calculated with two max uint256 numbers', async function () {
const a = ethers.MaxUint256;
expect(await this.mock.$average(a, a)).to.equal(a);
});
});
describe('ceilDiv', function () {
it('reverts on zero division', async function () {
const a = 2n;
const b = 0n;
// It's unspecified because it's a low level 0 division error
await expect(this.mock.$ceilDiv(a, b)).to.be.revertedWithPanic(PANIC_CODES.DIVISION_BY_ZERO);
});
it('does not round up a zero result', async function () {
const a = 0n;
const b = 2n;
const r = 0n;
expect(await this.mock.$ceilDiv(a, b)).to.equal(r);
});
it('does not round up on exact division', async function () {
const a = 10n;
const b = 5n;
const r = 2n;
expect(await this.mock.$ceilDiv(a, b)).to.equal(r);
});
it('rounds up on division with remainders', async function () {
const a = 42n;
const b = 13n;
const r = 4n;
expect(await this.mock.$ceilDiv(a, b)).to.equal(r);
});
it('does not overflow', async function () {
const a = ethers.MaxUint256;
const b = 2n;
const r = 1n << 255n;
expect(await this.mock.$ceilDiv(a, b)).to.equal(r);
});
it('correctly computes max uint256 divided by 1', async function () {
const a = ethers.MaxUint256;
const b = 1n;
const r = ethers.MaxUint256;
expect(await this.mock.$ceilDiv(a, b)).to.equal(r);
});
});
describe('mulDiv', function () {
it('divide by 0', async function () {
const a = 1n;
const b = 1n;
const c = 0n;
await expect(this.mock.$mulDiv(a, b, c, Rounding.Floor)).to.be.revertedWithPanic(PANIC_CODES.DIVISION_BY_ZERO);
});
it('reverts with result higher than 2 ^ 256', async function () {
const a = 5n;
const b = ethers.MaxUint256;
const c = 2n;
await expect(this.mock.$mulDiv(a, b, c, Rounding.Floor)).to.be.revertedWithPanic(
PANIC_CODES.ARITHMETIC_UNDER_OR_OVERFLOW,
);
});
describe('does round down', function () {
it('small values', async function () {
for (const rounding of RoundingDown) {
expect(await this.mock.$mulDiv(3n, 4n, 5n, rounding)).to.equal(2n);
expect(await this.mock.$mulDiv(3n, 5n, 5n, rounding)).to.equal(3n);
}
});
it('large values', async function () {
for (const rounding of RoundingDown) {
expect(await this.mock.$mulDiv(42n, ethers.MaxUint256 - 1n, ethers.MaxUint256, rounding)).to.equal(41n);
expect(await this.mock.$mulDiv(17n, ethers.MaxUint256, ethers.MaxUint256, rounding)).to.equal(17n);
expect(
await this.mock.$mulDiv(ethers.MaxUint256 - 1n, ethers.MaxUint256 - 1n, ethers.MaxUint256, rounding),
).to.equal(ethers.MaxUint256 - 2n);
expect(
await this.mock.$mulDiv(ethers.MaxUint256, ethers.MaxUint256 - 1n, ethers.MaxUint256, rounding),
).to.equal(ethers.MaxUint256 - 1n);
expect(await this.mock.$mulDiv(ethers.MaxUint256, ethers.MaxUint256, ethers.MaxUint256, rounding)).to.equal(
ethers.MaxUint256,
);
}
});
});
describe('does round up', function () {
it('small values', async function () {
for (const rounding of RoundingUp) {
expect(await this.mock.$mulDiv(3n, 4n, 5n, rounding)).to.equal(3n);
expect(await this.mock.$mulDiv(3n, 5n, 5n, rounding)).to.equal(3n);
}
});
it('large values', async function () {
for (const rounding of RoundingUp) {
expect(await this.mock.$mulDiv(42n, ethers.MaxUint256 - 1n, ethers.MaxUint256, rounding)).to.equal(42n);
expect(await this.mock.$mulDiv(17n, ethers.MaxUint256, ethers.MaxUint256, rounding)).to.equal(17n);
expect(
await this.mock.$mulDiv(ethers.MaxUint256 - 1n, ethers.MaxUint256 - 1n, ethers.MaxUint256, rounding),
).to.equal(ethers.MaxUint256 - 1n);
expect(
await this.mock.$mulDiv(ethers.MaxUint256, ethers.MaxUint256 - 1n, ethers.MaxUint256, rounding),
).to.equal(ethers.MaxUint256 - 1n);
expect(await this.mock.$mulDiv(ethers.MaxUint256, ethers.MaxUint256, ethers.MaxUint256, rounding)).to.equal(
ethers.MaxUint256,
);
}
});
});
});
describe('invMod', function () {
for (const factors of [
[0n],
[1n],
[2n],
[17n],
[65537n],
[0xffffffff00000001000000000000000000000000ffffffffffffffffffffffffn],
[3n, 5n],
[3n, 7n],
[47n, 53n],
]) {
const p = factors.reduce((acc, f) => acc * f, 1n);
describe(`using p=${p} which is ${p > 1 && factors.length > 1 ? 'not ' : ''}a prime`, function () {
it('trying to inverse 0 returns 0', async function () {
expect(await this.mock.$invMod(0, p)).to.equal(0n);
expect(await this.mock.$invMod(p, p)).to.equal(0n); // p is 0 mod p
});
if (p != 0) {
for (const value of Array.from({ length: 16 }, generators.uint256)) {
const isInversible = factors.every(f => value % f);
it(`trying to inverse ${value}`, async function () {
const result = await this.mock.$invMod(value, p);
if (isInversible) {
expect((value * result) % p).to.equal(1n);
} else {
expect(result).to.equal(0n);
}
});
}
}
});
}
});
describe('modExp', function () {
for (const [name, type] of Object.entries({ uint256, bytes })) {
describe(`with ${name} inputs`, function () {
it('is correctly calculating modulus', async function () {
const b = 3n;
const e = 200n;
const m = 50n;
expect(await this.mock.$modExp(type(b), type(e), type(m))).to.equal(type(b ** e % m).value);
});
it('is correctly reverting when modulus is zero', async function () {
const b = 3n;
const e = 200n;
const m = 0n;
await expect(this.mock.$modExp(type(b), type(e), type(m))).to.be.revertedWithPanic(
PANIC_CODES.DIVISION_BY_ZERO,
);
});
});
}
describe('with large bytes inputs', function () {
for (const [[b, log2b], [e, log2e], [m, log2m]] of product(
range(320, 512, 64).map(e => [2n ** BigInt(e) + 1n, e]),
range(320, 512, 64).map(e => [2n ** BigInt(e) + 1n, e]),
range(320, 512, 64).map(e => [2n ** BigInt(e) + 1n, e]),
)) {
it(`calculates b ** e % m (b=2**${log2b}+1) (e=2**${log2e}+1) (m=2**${log2m}+1)`, async function () {
const mLength = ethers.dataLength(ethers.toBeHex(m));
expect(await this.mock.$modExp(bytes(b), bytes(e), bytes(m))).to.equal(bytes(modExp(b, e, m), mLength).value);
});
}
});
});
describe('tryModExp', function () {
for (const [name, type] of Object.entries({ uint256, bytes })) {
describe(`with ${name} inputs`, function () {
it('is correctly calculating modulus', async function () {
const b = 3n;
const e = 200n;
const m = 50n;
expect(await this.mock.$tryModExp(type(b), type(e), type(m))).to.deep.equal([true, type(b ** e % m).value]);
});
it('is correctly reverting when modulus is zero', async function () {
const b = 3n;
const e = 200n;
const m = 0n;
expect(await this.mock.$tryModExp(type(b), type(e), type(m))).to.deep.equal([false, type.zero]);
});
});
}
describe('with large bytes inputs', function () {
for (const [[b, log2b], [e, log2e], [m, log2m]] of product(
range(320, 513, 64).map(e => [2n ** BigInt(e) + 1n, e]),
range(320, 513, 64).map(e => [2n ** BigInt(e) + 1n, e]),
range(320, 513, 64).map(e => [2n ** BigInt(e) + 1n, e]),
)) {
it(`calculates b ** e % m (b=2**${log2b}+1) (e=2**${log2e}+1) (m=2**${log2m}+1)`, async function () {
const mLength = ethers.dataLength(ethers.toBeHex(m));
expect(await this.mock.$tryModExp(bytes(b), bytes(e), bytes(m))).to.deep.equal([
true,
bytes(modExp(b, e, m), mLength).value,
]);
});
}
});
});
describe('sqrt', function () {
it('rounds down', async function () {
for (const rounding of RoundingDown) {
expect(await this.mock.$sqrt(0n, rounding)).to.equal(0n);
expect(await this.mock.$sqrt(1n, rounding)).to.equal(1n);
expect(await this.mock.$sqrt(2n, rounding)).to.equal(1n);
expect(await this.mock.$sqrt(3n, rounding)).to.equal(1n);
expect(await this.mock.$sqrt(4n, rounding)).to.equal(2n);
expect(await this.mock.$sqrt(144n, rounding)).to.equal(12n);
expect(await this.mock.$sqrt(999999n, rounding)).to.equal(999n);
expect(await this.mock.$sqrt(1000000n, rounding)).to.equal(1000n);
expect(await this.mock.$sqrt(1000001n, rounding)).to.equal(1000n);
expect(await this.mock.$sqrt(1002000n, rounding)).to.equal(1000n);
expect(await this.mock.$sqrt(1002001n, rounding)).to.equal(1001n);
expect(await this.mock.$sqrt(ethers.MaxUint256, rounding)).to.equal(340282366920938463463374607431768211455n);
}
});
it('rounds up', async function () {
for (const rounding of RoundingUp) {
expect(await this.mock.$sqrt(0n, rounding)).to.equal(0n);
expect(await this.mock.$sqrt(1n, rounding)).to.equal(1n);
expect(await this.mock.$sqrt(2n, rounding)).to.equal(2n);
expect(await this.mock.$sqrt(3n, rounding)).to.equal(2n);
expect(await this.mock.$sqrt(4n, rounding)).to.equal(2n);
expect(await this.mock.$sqrt(144n, rounding)).to.equal(12n);
expect(await this.mock.$sqrt(999999n, rounding)).to.equal(1000n);
expect(await this.mock.$sqrt(1000000n, rounding)).to.equal(1000n);
expect(await this.mock.$sqrt(1000001n, rounding)).to.equal(1001n);
expect(await this.mock.$sqrt(1002000n, rounding)).to.equal(1001n);
expect(await this.mock.$sqrt(1002001n, rounding)).to.equal(1001n);
expect(await this.mock.$sqrt(ethers.MaxUint256, rounding)).to.equal(340282366920938463463374607431768211456n);
}
});
});
describe('log', function () {
describe('log2', function () {
it('rounds down', async function () {
for (const rounding of RoundingDown) {
expect(await this.mock.$log2(0n, rounding)).to.equal(0n);
expect(await this.mock.$log2(1n, rounding)).to.equal(0n);
expect(await this.mock.$log2(2n, rounding)).to.equal(1n);
expect(await this.mock.$log2(3n, rounding)).to.equal(1n);
expect(await this.mock.$log2(4n, rounding)).to.equal(2n);
expect(await this.mock.$log2(5n, rounding)).to.equal(2n);
expect(await this.mock.$log2(6n, rounding)).to.equal(2n);
expect(await this.mock.$log2(7n, rounding)).to.equal(2n);
expect(await this.mock.$log2(8n, rounding)).to.equal(3n);
expect(await this.mock.$log2(9n, rounding)).to.equal(3n);
expect(await this.mock.$log2(ethers.MaxUint256, rounding)).to.equal(255n);
}
});
it('rounds up', async function () {
for (const rounding of RoundingUp) {
expect(await this.mock.$log2(0n, rounding)).to.equal(0n);
expect(await this.mock.$log2(1n, rounding)).to.equal(0n);
expect(await this.mock.$log2(2n, rounding)).to.equal(1n);
expect(await this.mock.$log2(3n, rounding)).to.equal(2n);
expect(await this.mock.$log2(4n, rounding)).to.equal(2n);
expect(await this.mock.$log2(5n, rounding)).to.equal(3n);
expect(await this.mock.$log2(6n, rounding)).to.equal(3n);
expect(await this.mock.$log2(7n, rounding)).to.equal(3n);
expect(await this.mock.$log2(8n, rounding)).to.equal(3n);
expect(await this.mock.$log2(9n, rounding)).to.equal(4n);
expect(await this.mock.$log2(ethers.MaxUint256, rounding)).to.equal(256n);
}
});
});
describe('log10', function () {
it('rounds down', async function () {
for (const rounding of RoundingDown) {
expect(await this.mock.$log10(0n, rounding)).to.equal(0n);
expect(await this.mock.$log10(1n, rounding)).to.equal(0n);
expect(await this.mock.$log10(2n, rounding)).to.equal(0n);
expect(await this.mock.$log10(9n, rounding)).to.equal(0n);
expect(await this.mock.$log10(10n, rounding)).to.equal(1n);
expect(await this.mock.$log10(11n, rounding)).to.equal(1n);
expect(await this.mock.$log10(99n, rounding)).to.equal(1n);
expect(await this.mock.$log10(100n, rounding)).to.equal(2n);
expect(await this.mock.$log10(101n, rounding)).to.equal(2n);
expect(await this.mock.$log10(999n, rounding)).to.equal(2n);
expect(await this.mock.$log10(1000n, rounding)).to.equal(3n);
expect(await this.mock.$log10(1001n, rounding)).to.equal(3n);
expect(await this.mock.$log10(ethers.MaxUint256, rounding)).to.equal(77n);
}
});
it('rounds up', async function () {
for (const rounding of RoundingUp) {
expect(await this.mock.$log10(0n, rounding)).to.equal(0n);
expect(await this.mock.$log10(1n, rounding)).to.equal(0n);
expect(await this.mock.$log10(2n, rounding)).to.equal(1n);
expect(await this.mock.$log10(9n, rounding)).to.equal(1n);
expect(await this.mock.$log10(10n, rounding)).to.equal(1n);
expect(await this.mock.$log10(11n, rounding)).to.equal(2n);
expect(await this.mock.$log10(99n, rounding)).to.equal(2n);
expect(await this.mock.$log10(100n, rounding)).to.equal(2n);
expect(await this.mock.$log10(101n, rounding)).to.equal(3n);
expect(await this.mock.$log10(999n, rounding)).to.equal(3n);
expect(await this.mock.$log10(1000n, rounding)).to.equal(3n);
expect(await this.mock.$log10(1001n, rounding)).to.equal(4n);
expect(await this.mock.$log10(ethers.MaxUint256, rounding)).to.equal(78n);
}
});
});
describe('log256', function () {
it('rounds down', async function () {
for (const rounding of RoundingDown) {
expect(await this.mock.$log256(0n, rounding)).to.equal(0n);
expect(await this.mock.$log256(1n, rounding)).to.equal(0n);
expect(await this.mock.$log256(2n, rounding)).to.equal(0n);
expect(await this.mock.$log256(255n, rounding)).to.equal(0n);
expect(await this.mock.$log256(256n, rounding)).to.equal(1n);
expect(await this.mock.$log256(257n, rounding)).to.equal(1n);
expect(await this.mock.$log256(65535n, rounding)).to.equal(1n);
expect(await this.mock.$log256(65536n, rounding)).to.equal(2n);
expect(await this.mock.$log256(65537n, rounding)).to.equal(2n);
expect(await this.mock.$log256(ethers.MaxUint256, rounding)).to.equal(31n);
}
});
it('rounds up', async function () {
for (const rounding of RoundingUp) {
expect(await this.mock.$log256(0n, rounding)).to.equal(0n);
expect(await this.mock.$log256(1n, rounding)).to.equal(0n);
expect(await this.mock.$log256(2n, rounding)).to.equal(1n);
expect(await this.mock.$log256(255n, rounding)).to.equal(1n);
expect(await this.mock.$log256(256n, rounding)).to.equal(1n);
expect(await this.mock.$log256(257n, rounding)).to.equal(2n);
expect(await this.mock.$log256(65535n, rounding)).to.equal(2n);
expect(await this.mock.$log256(65536n, rounding)).to.equal(2n);
expect(await this.mock.$log256(65537n, rounding)).to.equal(3n);
expect(await this.mock.$log256(ethers.MaxUint256, rounding)).to.equal(32n);
}
});
});
});
});

View File

@@ -0,0 +1,159 @@
const { ethers } = require('hardhat');
const { expect } = require('chai');
const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
const { range } = require('../../helpers/iterate');
async function fixture() {
const mock = await ethers.deployContract('$SafeCast');
return { mock };
}
describe('SafeCast', function () {
beforeEach(async function () {
Object.assign(this, await loadFixture(fixture));
});
for (const bits of range(8, 256, 8).map(ethers.toBigInt)) {
const maxValue = 2n ** bits - 1n;
describe(`toUint${bits}`, () => {
it('downcasts 0', async function () {
expect(await this.mock[`$toUint${bits}`](0n)).is.equal(0n);
});
it('downcasts 1', async function () {
expect(await this.mock[`$toUint${bits}`](1n)).is.equal(1n);
});
it(`downcasts 2^${bits} - 1 (${maxValue})`, async function () {
expect(await this.mock[`$toUint${bits}`](maxValue)).is.equal(maxValue);
});
it(`reverts when downcasting 2^${bits} (${maxValue + 1n})`, async function () {
await expect(this.mock[`$toUint${bits}`](maxValue + 1n))
.to.be.revertedWithCustomError(this.mock, 'SafeCastOverflowedUintDowncast')
.withArgs(bits, maxValue + 1n);
});
it(`reverts when downcasting 2^${bits} + 1 (${maxValue + 2n})`, async function () {
await expect(this.mock[`$toUint${bits}`](maxValue + 2n))
.to.be.revertedWithCustomError(this.mock, 'SafeCastOverflowedUintDowncast')
.withArgs(bits, maxValue + 2n);
});
});
}
describe('toUint256', () => {
it('casts 0', async function () {
expect(await this.mock.$toUint256(0n)).is.equal(0n);
});
it('casts 1', async function () {
expect(await this.mock.$toUint256(1n)).is.equal(1n);
});
it(`casts INT256_MAX (${ethers.MaxInt256})`, async function () {
expect(await this.mock.$toUint256(ethers.MaxInt256)).is.equal(ethers.MaxInt256);
});
it('reverts when casting -1', async function () {
await expect(this.mock.$toUint256(-1n))
.to.be.revertedWithCustomError(this.mock, 'SafeCastOverflowedIntToUint')
.withArgs(-1n);
});
it(`reverts when casting INT256_MIN (${ethers.MinInt256})`, async function () {
await expect(this.mock.$toUint256(ethers.MinInt256))
.to.be.revertedWithCustomError(this.mock, 'SafeCastOverflowedIntToUint')
.withArgs(ethers.MinInt256);
});
});
for (const bits of range(8, 256, 8).map(ethers.toBigInt)) {
const minValue = -(2n ** (bits - 1n));
const maxValue = 2n ** (bits - 1n) - 1n;
describe(`toInt${bits}`, () => {
it('downcasts 0', async function () {
expect(await this.mock[`$toInt${bits}`](0n)).is.equal(0n);
});
it('downcasts 1', async function () {
expect(await this.mock[`$toInt${bits}`](1n)).is.equal(1n);
});
it('downcasts -1', async function () {
expect(await this.mock[`$toInt${bits}`](-1n)).is.equal(-1n);
});
it(`downcasts -2^${bits - 1n} (${minValue})`, async function () {
expect(await this.mock[`$toInt${bits}`](minValue)).is.equal(minValue);
});
it(`downcasts 2^${bits - 1n} - 1 (${maxValue})`, async function () {
expect(await this.mock[`$toInt${bits}`](maxValue)).is.equal(maxValue);
});
it(`reverts when downcasting -2^${bits - 1n} - 1 (${minValue - 1n})`, async function () {
await expect(this.mock[`$toInt${bits}`](minValue - 1n))
.to.be.revertedWithCustomError(this.mock, 'SafeCastOverflowedIntDowncast')
.withArgs(bits, minValue - 1n);
});
it(`reverts when downcasting -2^${bits - 1n} - 2 (${minValue - 2n})`, async function () {
await expect(this.mock[`$toInt${bits}`](minValue - 2n))
.to.be.revertedWithCustomError(this.mock, 'SafeCastOverflowedIntDowncast')
.withArgs(bits, minValue - 2n);
});
it(`reverts when downcasting 2^${bits - 1n} (${maxValue + 1n})`, async function () {
await expect(this.mock[`$toInt${bits}`](maxValue + 1n))
.to.be.revertedWithCustomError(this.mock, 'SafeCastOverflowedIntDowncast')
.withArgs(bits, maxValue + 1n);
});
it(`reverts when downcasting 2^${bits - 1n} + 1 (${maxValue + 2n})`, async function () {
await expect(this.mock[`$toInt${bits}`](maxValue + 2n))
.to.be.revertedWithCustomError(this.mock, 'SafeCastOverflowedIntDowncast')
.withArgs(bits, maxValue + 2n);
});
});
}
describe('toInt256', () => {
it('casts 0', async function () {
expect(await this.mock.$toInt256(0)).is.equal(0n);
});
it('casts 1', async function () {
expect(await this.mock.$toInt256(1)).is.equal(1n);
});
it(`casts INT256_MAX (${ethers.MaxInt256})`, async function () {
expect(await this.mock.$toInt256(ethers.MaxInt256)).is.equal(ethers.MaxInt256);
});
it(`reverts when casting INT256_MAX + 1 (${ethers.MaxInt256 + 1n})`, async function () {
await expect(this.mock.$toInt256(ethers.MaxInt256 + 1n))
.to.be.revertedWithCustomError(this.mock, 'SafeCastOverflowedUintToInt')
.withArgs(ethers.MaxInt256 + 1n);
});
it(`reverts when casting UINT256_MAX (${ethers.MaxUint256})`, async function () {
await expect(this.mock.$toInt256(ethers.MaxUint256))
.to.be.revertedWithCustomError(this.mock, 'SafeCastOverflowedUintToInt')
.withArgs(ethers.MaxUint256);
});
});
describe('toUint (bool)', function () {
it('toUint(false) should be 0', async function () {
expect(await this.mock.$toUint(false)).to.equal(0n);
});
it('toUint(true) should be 1', async function () {
expect(await this.mock.$toUint(true)).to.equal(1n);
});
});
});

View File

@@ -0,0 +1,80 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import {Test} from "@forge-std/Test.sol";
import {Math} from "../../../contracts/utils/math/Math.sol";
import {SignedMath} from "../../../contracts/utils/math/SignedMath.sol";
contract SignedMathTest is Test {
function testSelect(bool f, int256 a, int256 b) public {
assertEq(SignedMath.ternary(f, a, b), f ? a : b);
}
// MIN & MAX
function testMinMax(int256 a, int256 b) public {
assertEq(SignedMath.min(a, b), a < b ? a : b);
assertEq(SignedMath.max(a, b), a > b ? a : b);
}
// MIN
function testMin(int256 a, int256 b) public {
int256 result = SignedMath.min(a, b);
assertLe(result, a);
assertLe(result, b);
assertTrue(result == a || result == b);
}
// MAX
function testMax(int256 a, int256 b) public {
int256 result = SignedMath.max(a, b);
assertGe(result, a);
assertGe(result, b);
assertTrue(result == a || result == b);
}
// AVERAGE
// 1. simple test, not full int256 range
function testAverage1(int256 a, int256 b) public {
a = bound(a, type(int256).min / 2, type(int256).max / 2);
b = bound(b, type(int256).min / 2, type(int256).max / 2);
int256 result = SignedMath.average(a, b);
assertEq(result, (a + b) / 2);
}
// 2. more complex test, full int256 range
function testAverage2(int256 a, int256 b) public {
(int256 result, int256 min, int256 max) = (
SignedMath.average(a, b),
SignedMath.min(a, b),
SignedMath.max(a, b)
);
// average must be between `a` and `b`
assertGe(result, min);
assertLe(result, max);
unchecked {
// must be unchecked in order to support `a = type(int256).min, b = type(int256).max`
uint256 deltaLower = uint256(result - min);
uint256 deltaUpper = uint256(max - result);
uint256 remainder = uint256((a & 1) ^ (b & 1));
assertEq(remainder, Math.max(deltaLower, deltaUpper) - Math.min(deltaLower, deltaUpper));
}
}
// ABS
function testAbs(int256 a) public {
uint256 result = SignedMath.abs(a);
unchecked {
// must be unchecked in order to support `n = type(int256).min`
assertEq(result, a < 0 ? uint256(-a) : uint256(a));
}
}
}

View File

@@ -0,0 +1,53 @@
const { ethers } = require('hardhat');
const { expect } = require('chai');
const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
const { min, max } = require('../../helpers/math');
async function testCommutative(fn, lhs, rhs, expected, ...extra) {
expect(await fn(lhs, rhs, ...extra)).to.deep.equal(expected);
expect(await fn(rhs, lhs, ...extra)).to.deep.equal(expected);
}
async function fixture() {
const mock = await ethers.deployContract('$SignedMath');
return { mock };
}
describe('SignedMath', function () {
beforeEach(async function () {
Object.assign(this, await loadFixture(fixture));
});
describe('max', function () {
it('is correctly detected in both position', async function () {
await testCommutative(this.mock.$max, -1234n, 5678n, max(-1234n, 5678n));
});
});
describe('min', function () {
it('is correctly detected in both position', async function () {
await testCommutative(this.mock.$min, -1234n, 5678n, min(-1234n, 5678n));
});
});
describe('average', function () {
it('is correctly calculated with various input', async function () {
for (const x of [ethers.MinInt256, -57417n, -42304n, -4n, -3n, 0n, 3n, 4n, 42304n, 57417n, ethers.MaxInt256]) {
for (const y of [ethers.MinInt256, -57417n, -42304n, -5n, -2n, 0n, 2n, 5n, 42304n, 57417n, ethers.MaxInt256]) {
expect(await this.mock.$average(x, y)).to.equal((x + y) / 2n);
}
}
});
});
describe('abs', function () {
const abs = x => (x < 0n ? -x : x);
for (const n of [ethers.MinInt256, ethers.MinInt256 + 1n, -1n, 0n, 1n, ethers.MaxInt256 - 1n, ethers.MaxInt256]) {
it(`correctly computes the absolute value of ${n}`, async function () {
expect(await this.mock.$abs(n)).to.equal(abs(n));
});
}
});
});

View File

@@ -0,0 +1,149 @@
const { ethers } = require('hardhat');
const { expect } = require('chai');
const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
async function fixture() {
const bitmap = await ethers.deployContract('$BitMaps');
return { bitmap };
}
describe('BitMap', function () {
const keyA = 7891n;
const keyB = 451n;
const keyC = 9592328n;
beforeEach(async function () {
Object.assign(this, await loadFixture(fixture));
});
it('starts empty', async function () {
expect(await this.bitmap.$get(0, keyA)).to.be.false;
expect(await this.bitmap.$get(0, keyB)).to.be.false;
expect(await this.bitmap.$get(0, keyC)).to.be.false;
});
describe('setTo', function () {
it('set a key to true', async function () {
await this.bitmap.$setTo(0, keyA, true);
expect(await this.bitmap.$get(0, keyA)).to.be.true;
expect(await this.bitmap.$get(0, keyB)).to.be.false;
expect(await this.bitmap.$get(0, keyC)).to.be.false;
});
it('set a key to false', async function () {
await this.bitmap.$setTo(0, keyA, true);
await this.bitmap.$setTo(0, keyA, false);
expect(await this.bitmap.$get(0, keyA)).to.be.false;
expect(await this.bitmap.$get(0, keyB)).to.be.false;
expect(await this.bitmap.$get(0, keyC)).to.be.false;
});
it('set several consecutive keys', async function () {
await this.bitmap.$setTo(0, keyA + 0n, true);
await this.bitmap.$setTo(0, keyA + 1n, true);
await this.bitmap.$setTo(0, keyA + 2n, true);
await this.bitmap.$setTo(0, keyA + 3n, true);
await this.bitmap.$setTo(0, keyA + 4n, true);
await this.bitmap.$setTo(0, keyA + 2n, false);
await this.bitmap.$setTo(0, keyA + 4n, false);
expect(await this.bitmap.$get(0, keyA + 0n)).to.be.true;
expect(await this.bitmap.$get(0, keyA + 1n)).to.be.true;
expect(await this.bitmap.$get(0, keyA + 2n)).to.be.false;
expect(await this.bitmap.$get(0, keyA + 3n)).to.be.true;
expect(await this.bitmap.$get(0, keyA + 4n)).to.be.false;
});
});
describe('set', function () {
it('adds a key', async function () {
await this.bitmap.$set(0, keyA);
expect(await this.bitmap.$get(0, keyA)).to.be.true;
expect(await this.bitmap.$get(0, keyB)).to.be.false;
expect(await this.bitmap.$get(0, keyC)).to.be.false;
});
it('adds several keys', async function () {
await this.bitmap.$set(0, keyA);
await this.bitmap.$set(0, keyB);
expect(await this.bitmap.$get(0, keyA)).to.be.true;
expect(await this.bitmap.$get(0, keyB)).to.be.true;
expect(await this.bitmap.$get(0, keyC)).to.be.false;
});
it('adds several consecutive keys', async function () {
await this.bitmap.$set(0, keyA + 0n);
await this.bitmap.$set(0, keyA + 1n);
await this.bitmap.$set(0, keyA + 3n);
expect(await this.bitmap.$get(0, keyA + 0n)).to.be.true;
expect(await this.bitmap.$get(0, keyA + 1n)).to.be.true;
expect(await this.bitmap.$get(0, keyA + 2n)).to.be.false;
expect(await this.bitmap.$get(0, keyA + 3n)).to.be.true;
expect(await this.bitmap.$get(0, keyA + 4n)).to.be.false;
});
});
describe('unset', function () {
it('removes added keys', async function () {
await this.bitmap.$set(0, keyA);
await this.bitmap.$set(0, keyB);
await this.bitmap.$unset(0, keyA);
expect(await this.bitmap.$get(0, keyA)).to.be.false;
expect(await this.bitmap.$get(0, keyB)).to.be.true;
expect(await this.bitmap.$get(0, keyC)).to.be.false;
});
it('removes consecutive added keys', async function () {
await this.bitmap.$set(0, keyA + 0n);
await this.bitmap.$set(0, keyA + 1n);
await this.bitmap.$set(0, keyA + 3n);
await this.bitmap.$unset(0, keyA + 1n);
expect(await this.bitmap.$get(0, keyA + 0n)).to.be.true;
expect(await this.bitmap.$get(0, keyA + 1n)).to.be.false;
expect(await this.bitmap.$get(0, keyA + 2n)).to.be.false;
expect(await this.bitmap.$get(0, keyA + 3n)).to.be.true;
expect(await this.bitmap.$get(0, keyA + 4n)).to.be.false;
});
it('adds and removes multiple keys', async function () {
// []
await this.bitmap.$set(0, keyA);
await this.bitmap.$set(0, keyC);
// [A, C]
await this.bitmap.$unset(0, keyA);
await this.bitmap.$unset(0, keyB);
// [C]
await this.bitmap.$set(0, keyB);
// [C, B]
await this.bitmap.$set(0, keyA);
await this.bitmap.$unset(0, keyC);
// [A, B]
await this.bitmap.$set(0, keyA);
await this.bitmap.$set(0, keyB);
// [A, B]
await this.bitmap.$set(0, keyC);
await this.bitmap.$unset(0, keyA);
// [B, C]
await this.bitmap.$set(0, keyA);
await this.bitmap.$unset(0, keyB);
// [A, C]
expect(await this.bitmap.$get(0, keyA)).to.be.true;
expect(await this.bitmap.$get(0, keyB)).to.be.false;
expect(await this.bitmap.$get(0, keyC)).to.be.true;
});
});
});

View File

@@ -0,0 +1,332 @@
// SPDX-License-Identifier: MIT
// This file was procedurally generated from scripts/generate/templates/Checkpoints.t.js.
pragma solidity ^0.8.20;
import {Test} from "@forge-std/Test.sol";
import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol";
import {Checkpoints} from "@openzeppelin/contracts/utils/structs/Checkpoints.sol";
contract CheckpointsTrace224Test is Test {
using Checkpoints for Checkpoints.Trace224;
// Maximum gap between keys used during the fuzzing tests: the `_prepareKeys` function with make sure that
// key#n+1 is in the [key#n, key#n + _KEY_MAX_GAP] range.
uint8 internal constant _KEY_MAX_GAP = 64;
Checkpoints.Trace224 internal _ckpts;
// helpers
function _boundUint32(uint32 x, uint32 min, uint32 max) internal pure returns (uint32) {
return SafeCast.toUint32(bound(uint256(x), uint256(min), uint256(max)));
}
function _prepareKeys(uint32[] memory keys, uint32 maxSpread) internal pure {
uint32 lastKey = 0;
for (uint256 i = 0; i < keys.length; ++i) {
uint32 key = _boundUint32(keys[i], lastKey, lastKey + maxSpread);
keys[i] = key;
lastKey = key;
}
}
function _assertLatestCheckpoint(bool exist, uint32 key, uint224 value) internal {
(bool _exist, uint32 _key, uint224 _value) = _ckpts.latestCheckpoint();
assertEq(_exist, exist);
assertEq(_key, key);
assertEq(_value, value);
}
// tests
function testPush(uint32[] memory keys, uint224[] memory values, uint32 pastKey) public {
vm.assume(values.length > 0 && values.length <= keys.length);
_prepareKeys(keys, _KEY_MAX_GAP);
// initial state
assertEq(_ckpts.length(), 0);
assertEq(_ckpts.latest(), 0);
_assertLatestCheckpoint(false, 0, 0);
uint256 duplicates = 0;
for (uint256 i = 0; i < keys.length; ++i) {
uint32 key = keys[i];
uint224 value = values[i % values.length];
if (i > 0 && key == keys[i - 1]) ++duplicates;
// push
_ckpts.push(key, value);
// check length & latest
assertEq(_ckpts.length(), i + 1 - duplicates);
assertEq(_ckpts.latest(), value);
_assertLatestCheckpoint(true, key, value);
}
if (keys.length > 0) {
uint32 lastKey = keys[keys.length - 1];
if (lastKey > 0) {
pastKey = _boundUint32(pastKey, 0, lastKey - 1);
vm.expectRevert();
this.push(pastKey, values[keys.length % values.length]);
}
}
}
// used to test reverts
function push(uint32 key, uint224 value) external {
_ckpts.push(key, value);
}
function testLookup(uint32[] memory keys, uint224[] memory values, uint32 lookup) public {
vm.assume(values.length > 0 && values.length <= keys.length);
_prepareKeys(keys, _KEY_MAX_GAP);
uint32 lastKey = keys.length == 0 ? 0 : keys[keys.length - 1];
lookup = _boundUint32(lookup, 0, lastKey + _KEY_MAX_GAP);
uint224 upper = 0;
uint224 lower = 0;
uint32 lowerKey = type(uint32).max;
for (uint256 i = 0; i < keys.length; ++i) {
uint32 key = keys[i];
uint224 value = values[i % values.length];
// push
_ckpts.push(key, value);
// track expected result of lookups
if (key <= lookup) {
upper = value;
}
// find the first key that is not smaller than the lookup key
if (key >= lookup && (i == 0 || keys[i - 1] < lookup)) {
lowerKey = key;
}
if (key == lowerKey) {
lower = value;
}
}
// check lookup
assertEq(_ckpts.lowerLookup(lookup), lower);
assertEq(_ckpts.upperLookup(lookup), upper);
assertEq(_ckpts.upperLookupRecent(lookup), upper);
}
}
contract CheckpointsTrace208Test is Test {
using Checkpoints for Checkpoints.Trace208;
// Maximum gap between keys used during the fuzzing tests: the `_prepareKeys` function with make sure that
// key#n+1 is in the [key#n, key#n + _KEY_MAX_GAP] range.
uint8 internal constant _KEY_MAX_GAP = 64;
Checkpoints.Trace208 internal _ckpts;
// helpers
function _boundUint48(uint48 x, uint48 min, uint48 max) internal pure returns (uint48) {
return SafeCast.toUint48(bound(uint256(x), uint256(min), uint256(max)));
}
function _prepareKeys(uint48[] memory keys, uint48 maxSpread) internal pure {
uint48 lastKey = 0;
for (uint256 i = 0; i < keys.length; ++i) {
uint48 key = _boundUint48(keys[i], lastKey, lastKey + maxSpread);
keys[i] = key;
lastKey = key;
}
}
function _assertLatestCheckpoint(bool exist, uint48 key, uint208 value) internal {
(bool _exist, uint48 _key, uint208 _value) = _ckpts.latestCheckpoint();
assertEq(_exist, exist);
assertEq(_key, key);
assertEq(_value, value);
}
// tests
function testPush(uint48[] memory keys, uint208[] memory values, uint48 pastKey) public {
vm.assume(values.length > 0 && values.length <= keys.length);
_prepareKeys(keys, _KEY_MAX_GAP);
// initial state
assertEq(_ckpts.length(), 0);
assertEq(_ckpts.latest(), 0);
_assertLatestCheckpoint(false, 0, 0);
uint256 duplicates = 0;
for (uint256 i = 0; i < keys.length; ++i) {
uint48 key = keys[i];
uint208 value = values[i % values.length];
if (i > 0 && key == keys[i - 1]) ++duplicates;
// push
_ckpts.push(key, value);
// check length & latest
assertEq(_ckpts.length(), i + 1 - duplicates);
assertEq(_ckpts.latest(), value);
_assertLatestCheckpoint(true, key, value);
}
if (keys.length > 0) {
uint48 lastKey = keys[keys.length - 1];
if (lastKey > 0) {
pastKey = _boundUint48(pastKey, 0, lastKey - 1);
vm.expectRevert();
this.push(pastKey, values[keys.length % values.length]);
}
}
}
// used to test reverts
function push(uint48 key, uint208 value) external {
_ckpts.push(key, value);
}
function testLookup(uint48[] memory keys, uint208[] memory values, uint48 lookup) public {
vm.assume(values.length > 0 && values.length <= keys.length);
_prepareKeys(keys, _KEY_MAX_GAP);
uint48 lastKey = keys.length == 0 ? 0 : keys[keys.length - 1];
lookup = _boundUint48(lookup, 0, lastKey + _KEY_MAX_GAP);
uint208 upper = 0;
uint208 lower = 0;
uint48 lowerKey = type(uint48).max;
for (uint256 i = 0; i < keys.length; ++i) {
uint48 key = keys[i];
uint208 value = values[i % values.length];
// push
_ckpts.push(key, value);
// track expected result of lookups
if (key <= lookup) {
upper = value;
}
// find the first key that is not smaller than the lookup key
if (key >= lookup && (i == 0 || keys[i - 1] < lookup)) {
lowerKey = key;
}
if (key == lowerKey) {
lower = value;
}
}
// check lookup
assertEq(_ckpts.lowerLookup(lookup), lower);
assertEq(_ckpts.upperLookup(lookup), upper);
assertEq(_ckpts.upperLookupRecent(lookup), upper);
}
}
contract CheckpointsTrace160Test is Test {
using Checkpoints for Checkpoints.Trace160;
// Maximum gap between keys used during the fuzzing tests: the `_prepareKeys` function with make sure that
// key#n+1 is in the [key#n, key#n + _KEY_MAX_GAP] range.
uint8 internal constant _KEY_MAX_GAP = 64;
Checkpoints.Trace160 internal _ckpts;
// helpers
function _boundUint96(uint96 x, uint96 min, uint96 max) internal pure returns (uint96) {
return SafeCast.toUint96(bound(uint256(x), uint256(min), uint256(max)));
}
function _prepareKeys(uint96[] memory keys, uint96 maxSpread) internal pure {
uint96 lastKey = 0;
for (uint256 i = 0; i < keys.length; ++i) {
uint96 key = _boundUint96(keys[i], lastKey, lastKey + maxSpread);
keys[i] = key;
lastKey = key;
}
}
function _assertLatestCheckpoint(bool exist, uint96 key, uint160 value) internal {
(bool _exist, uint96 _key, uint160 _value) = _ckpts.latestCheckpoint();
assertEq(_exist, exist);
assertEq(_key, key);
assertEq(_value, value);
}
// tests
function testPush(uint96[] memory keys, uint160[] memory values, uint96 pastKey) public {
vm.assume(values.length > 0 && values.length <= keys.length);
_prepareKeys(keys, _KEY_MAX_GAP);
// initial state
assertEq(_ckpts.length(), 0);
assertEq(_ckpts.latest(), 0);
_assertLatestCheckpoint(false, 0, 0);
uint256 duplicates = 0;
for (uint256 i = 0; i < keys.length; ++i) {
uint96 key = keys[i];
uint160 value = values[i % values.length];
if (i > 0 && key == keys[i - 1]) ++duplicates;
// push
_ckpts.push(key, value);
// check length & latest
assertEq(_ckpts.length(), i + 1 - duplicates);
assertEq(_ckpts.latest(), value);
_assertLatestCheckpoint(true, key, value);
}
if (keys.length > 0) {
uint96 lastKey = keys[keys.length - 1];
if (lastKey > 0) {
pastKey = _boundUint96(pastKey, 0, lastKey - 1);
vm.expectRevert();
this.push(pastKey, values[keys.length % values.length]);
}
}
}
// used to test reverts
function push(uint96 key, uint160 value) external {
_ckpts.push(key, value);
}
function testLookup(uint96[] memory keys, uint160[] memory values, uint96 lookup) public {
vm.assume(values.length > 0 && values.length <= keys.length);
_prepareKeys(keys, _KEY_MAX_GAP);
uint96 lastKey = keys.length == 0 ? 0 : keys[keys.length - 1];
lookup = _boundUint96(lookup, 0, lastKey + _KEY_MAX_GAP);
uint160 upper = 0;
uint160 lower = 0;
uint96 lowerKey = type(uint96).max;
for (uint256 i = 0; i < keys.length; ++i) {
uint96 key = keys[i];
uint160 value = values[i % values.length];
// push
_ckpts.push(key, value);
// track expected result of lookups
if (key <= lookup) {
upper = value;
}
// find the first key that is not smaller than the lookup key
if (key >= lookup && (i == 0 || keys[i - 1] < lookup)) {
lowerKey = key;
}
if (key == lowerKey) {
lower = value;
}
}
// check lookup
assertEq(_ckpts.lowerLookup(lookup), lower);
assertEq(_ckpts.upperLookup(lookup), upper);
assertEq(_ckpts.upperLookupRecent(lookup), upper);
}
}

View File

@@ -0,0 +1,146 @@
const { ethers } = require('hardhat');
const { expect } = require('chai');
const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
const { VALUE_SIZES } = require('../../../scripts/generate/templates/Checkpoints.opts');
describe('Checkpoints', function () {
for (const length of VALUE_SIZES) {
describe(`Trace${length}`, function () {
const fixture = async () => {
const mock = await ethers.deployContract('$Checkpoints');
const methods = {
at: (...args) => mock.getFunction(`$at_Checkpoints_Trace${length}`)(0, ...args),
latest: (...args) => mock.getFunction(`$latest_Checkpoints_Trace${length}`)(0, ...args),
latestCheckpoint: (...args) => mock.getFunction(`$latestCheckpoint_Checkpoints_Trace${length}`)(0, ...args),
length: (...args) => mock.getFunction(`$length_Checkpoints_Trace${length}`)(0, ...args),
push: (...args) => mock.getFunction(`$push(uint256,uint${256 - length},uint${length})`)(0, ...args),
lowerLookup: (...args) => mock.getFunction(`$lowerLookup(uint256,uint${256 - length})`)(0, ...args),
upperLookup: (...args) => mock.getFunction(`$upperLookup(uint256,uint${256 - length})`)(0, ...args),
upperLookupRecent: (...args) =>
mock.getFunction(`$upperLookupRecent(uint256,uint${256 - length})`)(0, ...args),
};
return { mock, methods };
};
beforeEach(async function () {
Object.assign(this, await loadFixture(fixture));
});
describe('without checkpoints', function () {
it('at zero reverts', async function () {
// Reverts with array out of bound access, which is unspecified
await expect(this.methods.at(0)).to.be.reverted;
});
it('returns zero as latest value', async function () {
expect(await this.methods.latest()).to.equal(0n);
const ckpt = await this.methods.latestCheckpoint();
expect(ckpt[0]).to.be.false;
expect(ckpt[1]).to.equal(0n);
expect(ckpt[2]).to.equal(0n);
});
it('lookup returns 0', async function () {
expect(await this.methods.lowerLookup(0)).to.equal(0n);
expect(await this.methods.upperLookup(0)).to.equal(0n);
expect(await this.methods.upperLookupRecent(0)).to.equal(0n);
});
});
describe('with checkpoints', function () {
beforeEach('pushing checkpoints', async function () {
this.checkpoints = [
{ key: 2n, value: 17n },
{ key: 3n, value: 42n },
{ key: 5n, value: 101n },
{ key: 7n, value: 23n },
{ key: 11n, value: 99n },
];
for (const { key, value } of this.checkpoints) {
await this.methods.push(key, value);
}
});
it('at keys', async function () {
for (const [index, { key, value }] of this.checkpoints.entries()) {
const at = await this.methods.at(index);
expect(at._value).to.equal(value);
expect(at._key).to.equal(key);
}
});
it('length', async function () {
expect(await this.methods.length()).to.equal(this.checkpoints.length);
});
it('returns latest value', async function () {
const latest = this.checkpoints.at(-1);
expect(await this.methods.latest()).to.equal(latest.value);
expect(await this.methods.latestCheckpoint()).to.deep.equal([true, latest.key, latest.value]);
});
it('cannot push values in the past', async function () {
await expect(this.methods.push(this.checkpoints.at(-1).key - 1n, 0n)).to.be.revertedWithCustomError(
this.mock,
'CheckpointUnorderedInsertion',
);
});
it('can update last value', async function () {
const newValue = 42n;
// check length before the update
expect(await this.methods.length()).to.equal(this.checkpoints.length);
// update last key
await this.methods.push(this.checkpoints.at(-1).key, newValue);
expect(await this.methods.latest()).to.equal(newValue);
// check that length did not change
expect(await this.methods.length()).to.equal(this.checkpoints.length);
});
it('lower lookup', async function () {
for (let i = 0; i < 14; ++i) {
const value = this.checkpoints.find(x => i <= x.key)?.value || 0n;
expect(await this.methods.lowerLookup(i)).to.equal(value);
}
});
it('upper lookup & upperLookupRecent', async function () {
for (let i = 0; i < 14; ++i) {
const value = this.checkpoints.findLast(x => i >= x.key)?.value || 0n;
expect(await this.methods.upperLookup(i)).to.equal(value);
expect(await this.methods.upperLookupRecent(i)).to.equal(value);
}
});
it('upperLookupRecent with more than 5 checkpoints', async function () {
const moreCheckpoints = [
{ key: 12n, value: 22n },
{ key: 13n, value: 131n },
{ key: 17n, value: 45n },
{ key: 19n, value: 31452n },
{ key: 21n, value: 0n },
];
const allCheckpoints = [].concat(this.checkpoints, moreCheckpoints);
for (const { key, value } of moreCheckpoints) {
await this.methods.push(key, value);
}
for (let i = 0; i < 25; ++i) {
const value = allCheckpoints.findLast(x => i >= x.key)?.value || 0n;
expect(await this.methods.upperLookup(i)).to.equal(value);
expect(await this.methods.upperLookupRecent(i)).to.equal(value);
}
});
});
});
}
});

View File

@@ -0,0 +1,79 @@
const { ethers } = require('hardhat');
const { expect } = require('chai');
const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
const { PANIC_CODES } = require('@nomicfoundation/hardhat-chai-matchers/panic');
const { generators } = require('../../helpers/random');
const LENGTH = 4;
async function fixture() {
const mock = await ethers.deployContract('$CircularBuffer');
await mock.$setup(0, LENGTH);
return { mock };
}
describe('CircularBuffer', function () {
beforeEach(async function () {
Object.assign(this, await loadFixture(fixture));
});
it('starts empty', async function () {
expect(await this.mock.$count(0)).to.equal(0n);
expect(await this.mock.$length(0)).to.equal(LENGTH);
expect(await this.mock.$includes(0, ethers.ZeroHash)).to.be.false;
await expect(this.mock.$last(0, 0)).to.be.revertedWithPanic(PANIC_CODES.ARRAY_ACCESS_OUT_OF_BOUNDS);
});
it('push', async function () {
const values = Array.from({ length: LENGTH + 3 }, generators.bytes32);
for (const [i, value] of values.map((v, i) => [i, v])) {
// push value
await this.mock.$push(0, value);
// view of the values
const pushed = values.slice(0, i + 1);
const stored = pushed.slice(-LENGTH);
const dropped = pushed.slice(0, -LENGTH);
// check count
expect(await this.mock.$length(0)).to.equal(LENGTH);
expect(await this.mock.$count(0)).to.equal(stored.length);
// check last
for (const j in stored) {
expect(await this.mock.$last(0, j)).to.equal(stored.at(-j - 1));
}
await expect(this.mock.$last(0, stored.length + 1)).to.be.revertedWithPanic(
PANIC_CODES.ARRAY_ACCESS_OUT_OF_BOUNDS,
);
// check included and non-included values
for (const v of stored) {
expect(await this.mock.$includes(0, v)).to.be.true;
}
for (const v of dropped) {
expect(await this.mock.$includes(0, v)).to.be.false;
}
expect(await this.mock.$includes(0, ethers.ZeroHash)).to.be.false;
}
});
it('clear', async function () {
const value = generators.bytes32();
await this.mock.$push(0, value);
expect(await this.mock.$count(0)).to.equal(1n);
expect(await this.mock.$length(0)).to.equal(LENGTH);
expect(await this.mock.$includes(0, value)).to.be.true;
await this.mock.$last(0, 0); // not revert
await this.mock.$clear(0);
expect(await this.mock.$count(0)).to.equal(0n);
expect(await this.mock.$length(0)).to.equal(LENGTH);
expect(await this.mock.$includes(0, value)).to.be.false;
await expect(this.mock.$last(0, 0)).to.be.revertedWithPanic(PANIC_CODES.ARRAY_ACCESS_OUT_OF_BOUNDS);
});
});

View File

@@ -0,0 +1,102 @@
const { ethers } = require('hardhat');
const { expect } = require('chai');
const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
const { PANIC_CODES } = require('@nomicfoundation/hardhat-chai-matchers/panic');
async function fixture() {
const mock = await ethers.deployContract('$DoubleEndedQueue');
/** Rebuild the content of the deque as a JS array. */
const getContent = () =>
mock.$length(0).then(length => Promise.all(Array.from({ length: Number(length) }, (_, i) => mock.$at(0, i))));
return { mock, getContent };
}
describe('DoubleEndedQueue', function () {
const coder = ethers.AbiCoder.defaultAbiCoder();
const bytesA = coder.encode(['uint256'], [0xdeadbeef]);
const bytesB = coder.encode(['uint256'], [0x0123456789]);
const bytesC = coder.encode(['uint256'], [0x42424242]);
const bytesD = coder.encode(['uint256'], [0x171717]);
beforeEach(async function () {
Object.assign(this, await loadFixture(fixture));
});
describe('when empty', function () {
it('getters', async function () {
expect(await this.mock.$empty(0)).to.be.true;
expect(await this.getContent()).to.have.ordered.members([]);
});
it('reverts on accesses', async function () {
await expect(this.mock.$popBack(0)).to.be.revertedWithPanic(PANIC_CODES.POP_ON_EMPTY_ARRAY);
await expect(this.mock.$popFront(0)).to.be.revertedWithPanic(PANIC_CODES.POP_ON_EMPTY_ARRAY);
await expect(this.mock.$back(0)).to.be.revertedWithPanic(PANIC_CODES.ARRAY_ACCESS_OUT_OF_BOUNDS);
await expect(this.mock.$front(0)).to.be.revertedWithPanic(PANIC_CODES.ARRAY_ACCESS_OUT_OF_BOUNDS);
});
});
describe('when not empty', function () {
beforeEach(async function () {
await this.mock.$pushBack(0, bytesB);
await this.mock.$pushFront(0, bytesA);
await this.mock.$pushBack(0, bytesC);
this.content = [bytesA, bytesB, bytesC];
});
it('getters', async function () {
expect(await this.mock.$empty(0)).to.be.false;
expect(await this.mock.$length(0)).to.equal(this.content.length);
expect(await this.mock.$front(0)).to.equal(this.content[0]);
expect(await this.mock.$back(0)).to.equal(this.content[this.content.length - 1]);
expect(await this.getContent()).to.have.ordered.members(this.content);
});
it('out of bounds access', async function () {
await expect(this.mock.$at(0, this.content.length)).to.be.revertedWithPanic(
PANIC_CODES.ARRAY_ACCESS_OUT_OF_BOUNDS,
);
});
describe('push', function () {
it('front', async function () {
await this.mock.$pushFront(0, bytesD);
this.content.unshift(bytesD); // add element at the beginning
expect(await this.getContent()).to.have.ordered.members(this.content);
});
it('back', async function () {
await this.mock.$pushBack(0, bytesD);
this.content.push(bytesD); // add element at the end
expect(await this.getContent()).to.have.ordered.members(this.content);
});
});
describe('pop', function () {
it('front', async function () {
const value = this.content.shift(); // remove first element
await expect(this.mock.$popFront(0)).to.emit(this.mock, 'return$popFront').withArgs(value);
expect(await this.getContent()).to.have.ordered.members(this.content);
});
it('back', async function () {
const value = this.content.pop(); // remove last element
await expect(this.mock.$popBack(0)).to.emit(this.mock, 'return$popBack').withArgs(value);
expect(await this.getContent()).to.have.ordered.members(this.content);
});
});
it('clear', async function () {
await this.mock.$clear(0);
expect(await this.mock.$empty(0)).to.be.true;
expect(await this.getContent()).to.have.ordered.members([]);
});
});
});

View File

@@ -0,0 +1,151 @@
const { ethers } = require('hardhat');
const { expect } = require('chai');
const zip = (array1, array2) => array1.map((item, index) => [item, array2[index]]);
function shouldBehaveLikeMap() {
async function expectMembersMatch(methods, keys, values) {
expect(keys.length).to.equal(values.length);
expect(await methods.length()).to.equal(keys.length);
expect([...(await methods.keys())]).to.have.members(keys);
for (const [key, value] of zip(keys, values)) {
expect(await methods.contains(key)).to.be.true;
expect(await methods.get(key)).to.equal(value);
}
expect(await Promise.all(keys.map((_, index) => methods.at(index)))).to.have.deep.members(zip(keys, values));
}
it('starts empty', async function () {
expect(await this.methods.contains(this.keyA)).to.be.false;
await expectMembersMatch(this.methods, [], []);
});
describe('set', function () {
it('adds a key', async function () {
await expect(this.methods.set(this.keyA, this.valueA)).to.emit(this.mock, this.events.setReturn).withArgs(true);
await expectMembersMatch(this.methods, [this.keyA], [this.valueA]);
});
it('adds several keys', async function () {
await this.methods.set(this.keyA, this.valueA);
await this.methods.set(this.keyB, this.valueB);
await expectMembersMatch(this.methods, [this.keyA, this.keyB], [this.valueA, this.valueB]);
expect(await this.methods.contains(this.keyC)).to.be.false;
});
it('returns false when adding keys already in the set', async function () {
await this.methods.set(this.keyA, this.valueA);
await expect(this.methods.set(this.keyA, this.valueA)).to.emit(this.mock, this.events.setReturn).withArgs(false);
await expectMembersMatch(this.methods, [this.keyA], [this.valueA]);
});
it('updates values for keys already in the set', async function () {
await this.methods.set(this.keyA, this.valueA);
await this.methods.set(this.keyA, this.valueB);
await expectMembersMatch(this.methods, [this.keyA], [this.valueB]);
});
});
describe('remove', function () {
it('removes added keys', async function () {
await this.methods.set(this.keyA, this.valueA);
await expect(this.methods.remove(this.keyA)).to.emit(this.mock, this.events.removeReturn).withArgs(true);
expect(await this.methods.contains(this.keyA)).to.be.false;
await expectMembersMatch(this.methods, [], []);
});
it('returns false when removing keys not in the set', async function () {
await expect(await this.methods.remove(this.keyA))
.to.emit(this.mock, this.events.removeReturn)
.withArgs(false);
expect(await this.methods.contains(this.keyA)).to.be.false;
});
it('adds and removes multiple keys', async function () {
// []
await this.methods.set(this.keyA, this.valueA);
await this.methods.set(this.keyC, this.valueC);
// [A, C]
await this.methods.remove(this.keyA);
await this.methods.remove(this.keyB);
// [C]
await this.methods.set(this.keyB, this.valueB);
// [C, B]
await this.methods.set(this.keyA, this.valueA);
await this.methods.remove(this.keyC);
// [A, B]
await this.methods.set(this.keyA, this.valueA);
await this.methods.set(this.keyB, this.valueB);
// [A, B]
await this.methods.set(this.keyC, this.valueC);
await this.methods.remove(this.keyA);
// [B, C]
await this.methods.set(this.keyA, this.valueA);
await this.methods.remove(this.keyB);
// [A, C]
await expectMembersMatch(this.methods, [this.keyA, this.keyC], [this.valueA, this.valueC]);
expect(await this.methods.contains(this.keyA)).to.be.true;
expect(await this.methods.contains(this.keyB)).to.be.false;
expect(await this.methods.contains(this.keyC)).to.be.true;
});
});
describe('read', function () {
beforeEach(async function () {
await this.methods.set(this.keyA, this.valueA);
});
describe('get', function () {
it('existing value', async function () {
expect(await this.methods.get(this.keyA)).to.equal(this.valueA);
});
it('missing value', async function () {
await expect(this.methods.get(this.keyB))
.to.be.revertedWithCustomError(this.mock, 'EnumerableMapNonexistentKey')
.withArgs(ethers.AbiCoder.defaultAbiCoder().encode([this.keyType], [this.keyB]));
});
});
describe('tryGet', function () {
it('existing value', async function () {
expect(await this.methods.tryGet(this.keyA)).to.have.ordered.members([true, this.valueA]);
});
it('missing value', async function () {
expect(await this.methods.tryGet(this.keyB)).to.have.ordered.members([false, this.zeroValue]);
});
});
});
}
module.exports = {
shouldBehaveLikeMap,
};

View File

@@ -0,0 +1,65 @@
const { ethers } = require('hardhat');
const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
const { mapValues } = require('../../helpers/iterate');
const { generators } = require('../../helpers/random');
const { TYPES, formatType } = require('../../../scripts/generate/templates/EnumerableMap.opts');
const { shouldBehaveLikeMap } = require('./EnumerableMap.behavior');
// Add Bytes32ToBytes32Map that must be tested but is not part of the generated types.
TYPES.unshift(formatType('bytes32', 'bytes32'));
async function fixture() {
const mock = await ethers.deployContract('$EnumerableMap');
const env = Object.fromEntries(
TYPES.map(({ name, keyType, valueType }) => [
name,
{
keyType,
keys: Array.from({ length: 3 }, generators[keyType]),
values: Array.from({ length: 3 }, generators[valueType]),
zeroValue: generators[valueType].zero,
methods: mapValues(
{
set: `$set(uint256,${keyType},${valueType})`,
get: `$get_EnumerableMap_${name}(uint256,${keyType})`,
tryGet: `$tryGet_EnumerableMap_${name}(uint256,${keyType})`,
remove: `$remove_EnumerableMap_${name}(uint256,${keyType})`,
length: `$length_EnumerableMap_${name}(uint256)`,
at: `$at_EnumerableMap_${name}(uint256,uint256)`,
contains: `$contains_EnumerableMap_${name}(uint256,${keyType})`,
keys: `$keys_EnumerableMap_${name}(uint256)`,
},
fnSig =>
(...args) =>
mock.getFunction(fnSig)(0, ...args),
),
events: {
setReturn: `return$set_EnumerableMap_${name}_${keyType}_${valueType}`,
removeReturn: `return$remove_EnumerableMap_${name}_${keyType}`,
},
},
]),
);
return { mock, env };
}
describe('EnumerableMap', function () {
beforeEach(async function () {
Object.assign(this, await loadFixture(fixture));
});
for (const { name } of TYPES) {
describe(name, function () {
beforeEach(async function () {
Object.assign(this, this.env[name]);
[this.keyA, this.keyB, this.keyC] = this.keys;
[this.valueA, this.valueB, this.valueC] = this.values;
});
shouldBehaveLikeMap();
});
}
});

View File

@@ -0,0 +1,116 @@
const { expect } = require('chai');
const { PANIC_CODES } = require('@nomicfoundation/hardhat-chai-matchers/panic');
function shouldBehaveLikeSet() {
async function expectMembersMatch(methods, values) {
expect(await methods.length()).to.equal(values.length);
for (const value of values) expect(await methods.contains(value)).to.be.true;
expect(await Promise.all(values.map((_, index) => methods.at(index)))).to.have.deep.members(values);
expect([...(await methods.values())]).to.have.deep.members(values);
}
it('starts empty', async function () {
expect(await this.methods.contains(this.valueA)).to.be.false;
await expectMembersMatch(this.methods, []);
});
describe('add', function () {
it('adds a value', async function () {
await expect(this.methods.add(this.valueA)).to.emit(this.mock, this.events.addReturn).withArgs(true);
await expectMembersMatch(this.methods, [this.valueA]);
});
it('adds several values', async function () {
await this.methods.add(this.valueA);
await this.methods.add(this.valueB);
await expectMembersMatch(this.methods, [this.valueA, this.valueB]);
expect(await this.methods.contains(this.valueC)).to.be.false;
});
it('returns false when adding values already in the set', async function () {
await this.methods.add(this.valueA);
await expect(this.methods.add(this.valueA)).to.emit(this.mock, this.events.addReturn).withArgs(false);
await expectMembersMatch(this.methods, [this.valueA]);
});
});
describe('at', function () {
it('reverts when retrieving non-existent elements', async function () {
await expect(this.methods.at(0)).to.be.revertedWithPanic(PANIC_CODES.ARRAY_ACCESS_OUT_OF_BOUNDS);
});
it('retrieves existing element', async function () {
await this.methods.add(this.valueA);
expect(await this.methods.at(0)).to.equal(this.valueA);
});
});
describe('remove', function () {
it('removes added values', async function () {
await this.methods.add(this.valueA);
await expect(this.methods.remove(this.valueA)).to.emit(this.mock, this.events.removeReturn).withArgs(true);
expect(await this.methods.contains(this.valueA)).to.be.false;
await expectMembersMatch(this.methods, []);
});
it('returns false when removing values not in the set', async function () {
await expect(this.methods.remove(this.valueA)).to.emit(this.mock, this.events.removeReturn).withArgs(false);
expect(await this.methods.contains(this.valueA)).to.be.false;
});
it('adds and removes multiple values', async function () {
// []
await this.methods.add(this.valueA);
await this.methods.add(this.valueC);
// [A, C]
await this.methods.remove(this.valueA);
await this.methods.remove(this.valueB);
// [C]
await this.methods.add(this.valueB);
// [C, B]
await this.methods.add(this.valueA);
await this.methods.remove(this.valueC);
// [A, B]
await this.methods.add(this.valueA);
await this.methods.add(this.valueB);
// [A, B]
await this.methods.add(this.valueC);
await this.methods.remove(this.valueA);
// [B, C]
await this.methods.add(this.valueA);
await this.methods.remove(this.valueB);
// [A, C]
await expectMembersMatch(this.methods, [this.valueA, this.valueC]);
expect(await this.methods.contains(this.valueB)).to.be.false;
});
});
}
module.exports = {
shouldBehaveLikeSet,
};

View File

@@ -0,0 +1,61 @@
const { ethers } = require('hardhat');
const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
const { mapValues } = require('../../helpers/iterate');
const { generators } = require('../../helpers/random');
const { TYPES } = require('../../../scripts/generate/templates/EnumerableSet.opts');
const { shouldBehaveLikeSet } = require('./EnumerableSet.behavior');
const getMethods = (mock, fnSigs) => {
return mapValues(
fnSigs,
fnSig =>
(...args) =>
mock.getFunction(fnSig)(0, ...args),
);
};
async function fixture() {
const mock = await ethers.deployContract('$EnumerableSet');
const env = Object.fromEntries(
TYPES.map(({ name, type }) => [
type,
{
values: Array.from({ length: 3 }, generators[type]),
methods: getMethods(mock, {
add: `$add(uint256,${type})`,
remove: `$remove(uint256,${type})`,
contains: `$contains(uint256,${type})`,
length: `$length_EnumerableSet_${name}(uint256)`,
at: `$at_EnumerableSet_${name}(uint256,uint256)`,
values: `$values_EnumerableSet_${name}(uint256)`,
}),
events: {
addReturn: `return$add_EnumerableSet_${name}_${type}`,
removeReturn: `return$remove_EnumerableSet_${name}_${type}`,
},
},
]),
);
return { mock, env };
}
describe('EnumerableSet', function () {
beforeEach(async function () {
Object.assign(this, await loadFixture(fixture));
});
for (const { type } of TYPES) {
describe(type, function () {
beforeEach(function () {
Object.assign(this, this.env[type]);
[this.valueA, this.valueB, this.valueC] = this.values;
});
shouldBehaveLikeSet();
});
}
});

View File

@@ -0,0 +1,100 @@
const { ethers } = require('hardhat');
const { expect } = require('chai');
const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
const { PANIC_CODES } = require('@nomicfoundation/hardhat-chai-matchers/panic');
const { StandardMerkleTree } = require('@openzeppelin/merkle-tree');
const { generators } = require('../../helpers/random');
const makeTree = (leafs = [ethers.ZeroHash]) =>
StandardMerkleTree.of(
leafs.map(leaf => [leaf]),
['bytes32'],
{ sortLeaves: false },
);
const hashLeaf = leaf => makeTree().leafHash([leaf]);
const DEPTH = 4n; // 16 slots
const ZERO = hashLeaf(ethers.ZeroHash);
async function fixture() {
const mock = await ethers.deployContract('MerkleTreeMock');
await mock.setup(DEPTH, ZERO);
return { mock };
}
describe('MerkleTree', function () {
beforeEach(async function () {
Object.assign(this, await loadFixture(fixture));
});
it('sets initial values at setup', async function () {
const merkleTree = makeTree(Array.from({ length: 2 ** Number(DEPTH) }, () => ethers.ZeroHash));
expect(await this.mock.root()).to.equal(merkleTree.root);
expect(await this.mock.depth()).to.equal(DEPTH);
expect(await this.mock.nextLeafIndex()).to.equal(0n);
});
describe('push', function () {
it('tree is correctly updated', async function () {
const leafs = Array.from({ length: 2 ** Number(DEPTH) }, () => ethers.ZeroHash);
// for each leaf slot
for (const i in leafs) {
// generate random leaf and hash it
const hashedLeaf = hashLeaf((leafs[i] = generators.bytes32()));
// update leaf list and rebuild tree.
const tree = makeTree(leafs);
// push value to tree
await expect(this.mock.push(hashedLeaf)).to.emit(this.mock, 'LeafInserted').withArgs(hashedLeaf, i, tree.root);
// check tree
expect(await this.mock.root()).to.equal(tree.root);
expect(await this.mock.nextLeafIndex()).to.equal(BigInt(i) + 1n);
}
});
it('revert when tree is full', async function () {
await Promise.all(Array.from({ length: 2 ** Number(DEPTH) }).map(() => this.mock.push(ethers.ZeroHash)));
await expect(this.mock.push(ethers.ZeroHash)).to.be.revertedWithPanic(PANIC_CODES.TOO_MUCH_MEMORY_ALLOCATED);
});
});
it('reset', async function () {
// empty tree
const zeroLeafs = Array.from({ length: 2 ** Number(DEPTH) }, () => ethers.ZeroHash);
const zeroTree = makeTree(zeroLeafs);
// tree with one element
const leafs = Array.from({ length: 2 ** Number(DEPTH) }, () => ethers.ZeroHash);
const hashedLeaf = hashLeaf((leafs[0] = generators.bytes32())); // fill first leaf and hash it
const tree = makeTree(leafs);
// root should be that of a zero tree
expect(await this.mock.root()).to.equal(zeroTree.root);
expect(await this.mock.nextLeafIndex()).to.equal(0n);
// push leaf and check root
await expect(this.mock.push(hashedLeaf)).to.emit(this.mock, 'LeafInserted').withArgs(hashedLeaf, 0, tree.root);
expect(await this.mock.root()).to.equal(tree.root);
expect(await this.mock.nextLeafIndex()).to.equal(1n);
// reset tree
await this.mock.setup(DEPTH, ZERO);
expect(await this.mock.root()).to.equal(zeroTree.root);
expect(await this.mock.nextLeafIndex()).to.equal(0n);
// re-push leaf and check root
await expect(this.mock.push(hashedLeaf)).to.emit(this.mock, 'LeafInserted').withArgs(hashedLeaf, 0, tree.root);
expect(await this.mock.root()).to.equal(tree.root);
expect(await this.mock.nextLeafIndex()).to.equal(1n);
});
});

View File

@@ -0,0 +1,135 @@
const { ethers } = require('hardhat');
const { expect } = require('chai');
const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
const { product } = require('../../helpers/iterate');
const { max } = require('../../helpers/math');
const time = require('../../helpers/time');
const MAX_UINT32 = 1n << (32n - 1n);
const MAX_UINT48 = 1n << (48n - 1n);
const SOME_VALUES = [0n, 1n, 2n, 15n, 16n, 17n, 42n];
const asUint = (value, size) => {
value = ethers.toBigInt(value);
size = ethers.toBigInt(size);
expect(value).to.be.greaterThanOrEqual(0n, `value is not a valid uint${size}`);
expect(value).to.be.lessThan(1n << size, `value is not a valid uint${size}`);
return value;
};
const unpackDelay = delay => ({
valueBefore: (asUint(delay, 112) >> 32n) % (1n << 32n),
valueAfter: (asUint(delay, 112) >> 0n) % (1n << 32n),
effect: (asUint(delay, 112) >> 64n) % (1n << 48n),
});
const packDelay = ({ valueBefore, valueAfter = 0n, effect = 0n }) =>
(asUint(valueAfter, 32) << 0n) + (asUint(valueBefore, 32) << 32n) + (asUint(effect, 48) << 64n);
const effectSamplesForTimepoint = timepoint => [
0n,
timepoint,
...product([-1n, 1n], [1n, 2n, 17n, 42n])
.map(([sign, shift]) => timepoint + sign * shift)
.filter(effect => effect > 0n && effect <= MAX_UINT48),
MAX_UINT48,
];
async function fixture() {
const mock = await ethers.deployContract('$Time');
return { mock };
}
describe('Time', function () {
beforeEach(async function () {
Object.assign(this, await loadFixture(fixture));
});
describe('clocks', function () {
it('timestamp', async function () {
expect(await this.mock.$timestamp()).to.equal(await time.clock.timestamp());
});
it('block number', async function () {
expect(await this.mock.$blockNumber()).to.equal(await time.clock.blocknumber());
});
});
describe('Delay', function () {
describe('packing and unpacking', function () {
const valueBefore = 17n;
const valueAfter = 42n;
const effect = 69n;
const delay = 1272825341158973505578n;
it('pack', async function () {
expect(await this.mock.$pack(valueBefore, valueAfter, effect)).to.equal(delay);
expect(packDelay({ valueBefore, valueAfter, effect })).to.equal(delay);
});
it('unpack', async function () {
expect(await this.mock.$unpack(delay)).to.deep.equal([valueBefore, valueAfter, effect]);
expect(unpackDelay(delay)).to.deep.equal({
valueBefore,
valueAfter,
effect,
});
});
});
it('toDelay', async function () {
for (const value of [...SOME_VALUES, MAX_UINT32]) {
expect(await this.mock.$toDelay(value).then(unpackDelay)).to.deep.equal({
valueBefore: 0n,
valueAfter: value,
effect: 0n,
});
}
});
it('get & getFull', async function () {
const timepoint = await time.clock.timestamp();
const valueBefore = 24194n;
const valueAfter = 4214143n;
for (const effect of effectSamplesForTimepoint(timepoint)) {
const isPast = effect <= timepoint;
const delay = packDelay({ valueBefore, valueAfter, effect });
expect(await this.mock.$get(delay)).to.equal(isPast ? valueAfter : valueBefore);
expect(await this.mock.$getFull(delay)).to.deep.equal([
isPast ? valueAfter : valueBefore,
isPast ? 0n : valueAfter,
isPast ? 0n : effect,
]);
}
});
it('withUpdate', async function () {
const timepoint = await time.clock.timestamp();
const valueBefore = 24194n;
const valueAfter = 4214143n;
const newvalueAfter = 94716n;
for (const effect of effectSamplesForTimepoint(timepoint))
for (const minSetback of [...SOME_VALUES, MAX_UINT32]) {
const isPast = effect <= timepoint;
const expectedvalueBefore = isPast ? valueAfter : valueBefore;
const expectedSetback = max(minSetback, expectedvalueBefore - newvalueAfter, 0n);
expect(
await this.mock.$withUpdate(packDelay({ valueBefore, valueAfter, effect }), newvalueAfter, minSetback),
).to.deep.equal([
packDelay({
valueBefore: expectedvalueBefore,
valueAfter: newvalueAfter,
effect: timepoint + expectedSetback,
}),
timepoint + expectedSetback,
]);
}
});
});
});