소스 검색

Migrate utils to ethersjs v6 (#4736)

Co-authored-by: Hadrien Croubois <hadrien.croubois@gmail.com>
Co-authored-by: ernestognw <ernestognw@gmail.com>
Renan Souza 1 년 전
부모
커밋
78d5708340

+ 1 - 1
contracts/mocks/MulticallTest.sol → contracts/mocks/MulticallHelper.sol

@@ -4,7 +4,7 @@ pragma solidity ^0.8.20;
 
 import {ERC20MulticallMock} from "./token/ERC20MulticallMock.sol";
 
-contract MulticallTest {
+contract MulticallHelper {
     function checkReturnValues(
         ERC20MulticallMock multicallToken,
         address[] calldata recipients,

+ 12 - 12
contracts/mocks/StorageSlotMock.sol

@@ -7,41 +7,41 @@ import {StorageSlot} from "../utils/StorageSlot.sol";
 contract StorageSlotMock {
     using StorageSlot for *;
 
-    function setBoolean(bytes32 slot, bool value) public {
+    function setBooleanSlot(bytes32 slot, bool value) public {
         slot.getBooleanSlot().value = value;
     }
 
-    function setAddress(bytes32 slot, address value) public {
+    function setAddressSlot(bytes32 slot, address value) public {
         slot.getAddressSlot().value = value;
     }
 
-    function setBytes32(bytes32 slot, bytes32 value) public {
+    function setBytes32Slot(bytes32 slot, bytes32 value) public {
         slot.getBytes32Slot().value = value;
     }
 
-    function setUint256(bytes32 slot, uint256 value) public {
+    function setUint256Slot(bytes32 slot, uint256 value) public {
         slot.getUint256Slot().value = value;
     }
 
-    function getBoolean(bytes32 slot) public view returns (bool) {
+    function getBooleanSlot(bytes32 slot) public view returns (bool) {
         return slot.getBooleanSlot().value;
     }
 
-    function getAddress(bytes32 slot) public view returns (address) {
+    function getAddressSlot(bytes32 slot) public view returns (address) {
         return slot.getAddressSlot().value;
     }
 
-    function getBytes32(bytes32 slot) public view returns (bytes32) {
+    function getBytes32Slot(bytes32 slot) public view returns (bytes32) {
         return slot.getBytes32Slot().value;
     }
 
-    function getUint256(bytes32 slot) public view returns (uint256) {
+    function getUint256Slot(bytes32 slot) public view returns (uint256) {
         return slot.getUint256Slot().value;
     }
 
     mapping(uint256 key => string) public stringMap;
 
-    function setString(bytes32 slot, string calldata value) public {
+    function setStringSlot(bytes32 slot, string calldata value) public {
         slot.getStringSlot().value = value;
     }
 
@@ -49,7 +49,7 @@ contract StorageSlotMock {
         stringMap[key].getStringSlot().value = value;
     }
 
-    function getString(bytes32 slot) public view returns (string memory) {
+    function getStringSlot(bytes32 slot) public view returns (string memory) {
         return slot.getStringSlot().value;
     }
 
@@ -59,7 +59,7 @@ contract StorageSlotMock {
 
     mapping(uint256 key => bytes) public bytesMap;
 
-    function setBytes(bytes32 slot, bytes calldata value) public {
+    function setBytesSlot(bytes32 slot, bytes calldata value) public {
         slot.getBytesSlot().value = value;
     }
 
@@ -67,7 +67,7 @@ contract StorageSlotMock {
         bytesMap[key].getBytesSlot().value = value;
     }
 
-    function getBytes(bytes32 slot) public view returns (bytes memory) {
+    function getBytesSlot(bytes32 slot) public view returns (bytes memory) {
         return slot.getBytesSlot().value;
     }
 

+ 1 - 0
test/helpers/random.js

@@ -6,6 +6,7 @@ const generators = {
   address: () => ethers.Wallet.createRandom().address,
   bytes32: () => ethers.hexlify(ethers.randomBytes(32)),
   uint256: () => ethers.toBigInt(ethers.randomBytes(32)),
+  hexBytes: length => ethers.hexlify(ethers.randomBytes(length)),
 };
 
 module.exports = {

+ 105 - 106
test/utils/Arrays.test.js

@@ -1,121 +1,120 @@
-require('@openzeppelin/test-helpers');
-
+const { ethers } = require('hardhat');
 const { expect } = require('chai');
+const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
 
-const AddressArraysMock = artifacts.require('AddressArraysMock');
-const Bytes32ArraysMock = artifacts.require('Bytes32ArraysMock');
-const Uint256ArraysMock = artifacts.require('Uint256ArraysMock');
-
-contract('Arrays', function () {
-  describe('findUpperBound', function () {
-    context('Even number of elements', function () {
-      const EVEN_ELEMENTS_ARRAY = [11, 12, 13, 14, 15, 16, 17, 18, 19, 20];
-
-      beforeEach(async function () {
-        this.arrays = await Uint256ArraysMock.new(EVEN_ELEMENTS_ARRAY);
-      });
-
-      it('returns correct index for the basic case', async function () {
-        expect(await this.arrays.findUpperBound(16)).to.be.bignumber.equal('5');
-      });
-
-      it('returns 0 for the first element', async function () {
-        expect(await this.arrays.findUpperBound(11)).to.be.bignumber.equal('0');
-      });
-
-      it('returns index of the last element', async function () {
-        expect(await this.arrays.findUpperBound(20)).to.be.bignumber.equal('9');
-      });
-
-      it('returns first index after last element if searched value is over the upper boundary', async function () {
-        expect(await this.arrays.findUpperBound(32)).to.be.bignumber.equal('10');
-      });
-
-      it('returns 0 for the element under the lower boundary', async function () {
-        expect(await this.arrays.findUpperBound(2)).to.be.bignumber.equal('0');
-      });
-    });
-
-    context('Odd number of elements', function () {
-      const ODD_ELEMENTS_ARRAY = [11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21];
-
-      beforeEach(async function () {
-        this.arrays = await Uint256ArraysMock.new(ODD_ELEMENTS_ARRAY);
-      });
-
-      it('returns correct index for the basic case', async function () {
-        expect(await this.arrays.findUpperBound(16)).to.be.bignumber.equal('5');
-      });
-
-      it('returns 0 for the first element', async function () {
-        expect(await this.arrays.findUpperBound(11)).to.be.bignumber.equal('0');
-      });
-
-      it('returns index of the last element', async function () {
-        expect(await this.arrays.findUpperBound(21)).to.be.bignumber.equal('10');
-      });
-
-      it('returns first index after last element if searched value is over the upper boundary', async function () {
-        expect(await this.arrays.findUpperBound(32)).to.be.bignumber.equal('11');
-      });
+const { randomArray, generators } = require('../helpers/random');
 
-      it('returns 0 for the element under the lower boundary', async function () {
-        expect(await this.arrays.findUpperBound(2)).to.be.bignumber.equal('0');
-      });
-    });
+// See https://en.cppreference.com/w/cpp/algorithm/ranges/lower_bound
+const lowerBound = (array, value) => {
+  const i = array.findIndex(element => value <= element);
+  return i == -1 ? array.length : i;
+};
 
-    context('Array with gap', function () {
-      const WITH_GAP_ARRAY = [11, 12, 13, 14, 15, 20, 21, 22, 23, 24];
+// 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;
+// };
 
-      beforeEach(async function () {
-        this.arrays = await Uint256ArraysMock.new(WITH_GAP_ARRAY);
-      });
+const hasDuplicates = array => array.some((v, i) => array.indexOf(v) != i);
 
-      it('returns index of first element in next filled range', async function () {
-        expect(await this.arrays.findUpperBound(17)).to.be.bignumber.equal('5');
-      });
-    });
-
-    context('Empty array', function () {
-      beforeEach(async function () {
-        this.arrays = await Uint256ArraysMock.new([]);
-      });
-
-      it('always returns 0 for empty array', async function () {
-        expect(await this.arrays.findUpperBound(10)).to.be.bignumber.equal('0');
+describe('Arrays', function () {
+  describe('findUpperBound', 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 { mock: await ethers.deployContract('Uint256ArraysMock', [array]) };
+        };
+
+        beforeEach(async function () {
+          Object.assign(this, await loadFixture(fixture));
+        });
+
+        for (const [name, input] of Object.entries(tests)) {
+          it(name, async function () {
+            // findUpperBound does not support duplicated
+            if (hasDuplicates(array)) this.skip();
+            expect(await this.mock.findUpperBound(input)).to.be.equal(lowerBound(array, input));
+          });
+        }
       });
-    });
+    }
   });
 
   describe('unsafeAccess', function () {
-    for (const { type, artifact, elements } of [
-      {
-        type: 'address',
-        artifact: AddressArraysMock,
-        elements: Array(10)
-          .fill()
-          .map(() => web3.utils.randomHex(20)),
-      },
-      {
-        type: 'bytes32',
-        artifact: Bytes32ArraysMock,
-        elements: Array(10)
-          .fill()
-          .map(() => web3.utils.randomHex(32)),
-      },
-      {
-        type: 'uint256',
-        artifact: Uint256ArraysMock,
-        elements: Array(10)
-          .fill()
-          .map(() => web3.utils.randomHex(32)),
-      },
-    ]) {
-      it(type, async function () {
-        const contract = await artifact.new(elements);
+    const contractCases = {
+      address: { artifact: 'AddressArraysMock', elements: randomArray(generators.address, 10) },
+      bytes32: { artifact: 'Bytes32ArraysMock', elements: randomArray(generators.bytes32, 10) },
+      uint256: { artifact: 'Uint256ArraysMock', elements: randomArray(generators.uint256, 10) },
+    };
+
+    const fixture = async () => {
+      const contracts = {};
+      for (const [name, { artifact, elements }] of Object.entries(contractCases)) {
+        contracts[name] = await ethers.deployContract(artifact, [elements]);
+      }
+      return { contracts };
+    };
+
+    beforeEach(async function () {
+      Object.assign(this, await loadFixture(fixture));
+    });
 
+    for (const [name, { elements }] of Object.entries(contractCases)) {
+      it(name, async function () {
         for (const i in elements) {
-          expect(await contract.unsafeAccess(i)).to.be.bignumber.equal(elements[i]);
+          expect(await this.contracts[name].unsafeAccess(i)).to.be.equal(elements[i]);
         }
       });
     }

+ 19 - 23
test/utils/Base64.test.js

@@ -1,33 +1,29 @@
+const { ethers } = require('hardhat');
 const { expect } = require('chai');
+const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
 
-const Base64 = artifacts.require('$Base64');
+async function fixture() {
+  const mock = await ethers.deployContract('$Base64');
+  return { mock };
+}
 
-contract('Strings', function () {
+describe('Strings', function () {
   beforeEach(async function () {
-    this.base64 = await Base64.new();
+    Object.assign(this, await loadFixture(fixture));
   });
 
   describe('from bytes - base64', function () {
-    it('converts to base64 encoded string with double padding', async function () {
-      const TEST_MESSAGE = 'test';
-      const input = web3.utils.asciiToHex(TEST_MESSAGE);
-      expect(await this.base64.$encode(input)).to.equal('dGVzdA==');
-    });
+    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: 'empty bytes', input: '0x', expected: '' },
+    ])
+      it(title, async function () {
+        const raw = ethers.isBytesLike(input) ? input : ethers.toUtf8Bytes(input);
 
-    it('converts to base64 encoded string with single padding', async function () {
-      const TEST_MESSAGE = 'test1';
-      const input = web3.utils.asciiToHex(TEST_MESSAGE);
-      expect(await this.base64.$encode(input)).to.equal('dGVzdDE=');
-    });
-
-    it('converts to base64 encoded string without padding', async function () {
-      const TEST_MESSAGE = 'test12';
-      const input = web3.utils.asciiToHex(TEST_MESSAGE);
-      expect(await this.base64.$encode(input)).to.equal('dGVzdDEy');
-    });
-
-    it('empty bytes', async function () {
-      expect(await this.base64.$encode([])).to.equal('');
-    });
+        expect(await this.mock.$encode(raw)).to.equal(ethers.encodeBase64(raw));
+        expect(await this.mock.$encode(raw)).to.equal(expected);
+      });
   });
 });

+ 67 - 55
test/utils/Create2.test.js

@@ -1,35 +1,42 @@
 const { ethers } = require('hardhat');
 const { expect } = require('chai');
-const { balance, ether, expectEvent, expectRevert, send } = require('@openzeppelin/test-helpers');
-const { expectRevertCustomError } = require('../helpers/customError');
+const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
 
-const Create2 = artifacts.require('$Create2');
-const VestingWallet = artifacts.require('VestingWallet');
-// This should be a contract that:
-// - has no constructor arguments
-// - has no immutable variable populated during construction
-const ConstructorLessContract = Create2;
+async function fixture() {
+  const [deployer, other] = await ethers.getSigners();
 
-contract('Create2', function (accounts) {
-  const [deployerAccount, other] = accounts;
+  const factory = await ethers.deployContract('$Create2');
 
-  const salt = 'salt message';
-  const saltHex = web3.utils.soliditySha3(salt);
+  // 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([])]));
 
-  const encodedParams = web3.eth.abi.encodeParameters(['address', 'uint64', 'uint64'], [other, 0, 0]).slice(2);
+  return { deployer, other, factory, constructorByteCode, constructorLessBytecode };
+}
 
-  const constructorByteCode = `${VestingWallet.bytecode}${encodedParams}`;
+describe('Create2', function () {
+  const salt = 'salt message';
+  const saltHex = ethers.id(salt);
 
   beforeEach(async function () {
-    this.factory = await Create2.new();
+    Object.assign(this, await loadFixture(fixture));
   });
+
   describe('computeAddress', function () {
     it('computes the correct contract address', async function () {
-      const onChainComputed = await this.factory.$computeAddress(saltHex, web3.utils.keccak256(constructorByteCode));
+      const onChainComputed = await this.factory.$computeAddress(saltHex, ethers.keccak256(this.constructorByteCode));
       const offChainComputed = ethers.getCreate2Address(
-        this.factory.address,
+        this.factory.target,
         saltHex,
-        ethers.keccak256(constructorByteCode),
+        ethers.keccak256(this.constructorByteCode),
       );
       expect(onChainComputed).to.equal(offChainComputed);
     });
@@ -37,13 +44,13 @@ contract('Create2', function (accounts) {
     it('computes the correct contract address with deployer', async function () {
       const onChainComputed = await this.factory.$computeAddress(
         saltHex,
-        web3.utils.keccak256(constructorByteCode),
-        deployerAccount,
+        ethers.keccak256(this.constructorByteCode),
+        ethers.Typed.address(this.deployer),
       );
       const offChainComputed = ethers.getCreate2Address(
-        deployerAccount,
+        this.deployer.address,
         saltHex,
-        ethers.keccak256(constructorByteCode),
+        ethers.keccak256(this.constructorByteCode),
       );
       expect(onChainComputed).to.equal(offChainComputed);
     });
@@ -52,71 +59,76 @@ contract('Create2', function (accounts) {
   describe('deploy', function () {
     it('deploys a contract without constructor', async function () {
       const offChainComputed = ethers.getCreate2Address(
-        this.factory.address,
+        this.factory.target,
         saltHex,
-        ethers.keccak256(ConstructorLessContract.bytecode),
+        ethers.keccak256(this.constructorLessBytecode),
       );
 
-      expectEvent(await this.factory.$deploy(0, saltHex, ConstructorLessContract.bytecode), 'return$deploy', {
-        addr: offChainComputed,
-      });
+      await expect(this.factory.$deploy(0n, saltHex, this.constructorLessBytecode))
+        .to.emit(this.factory, 'return$deploy')
+        .withArgs(offChainComputed);
 
-      expect(ConstructorLessContract.bytecode).to.include((await web3.eth.getCode(offChainComputed)).slice(2));
+      expect(this.constructorLessBytecode).to.include((await web3.eth.getCode(offChainComputed)).slice(2));
     });
 
     it('deploys a contract with constructor arguments', async function () {
       const offChainComputed = ethers.getCreate2Address(
-        this.factory.address,
+        this.factory.target,
         saltHex,
-        ethers.keccak256(constructorByteCode),
+        ethers.keccak256(this.constructorByteCode),
       );
 
-      expectEvent(await this.factory.$deploy(0, saltHex, constructorByteCode), 'return$deploy', {
-        addr: offChainComputed,
-      });
+      await expect(this.factory.$deploy(0n, saltHex, this.constructorByteCode))
+        .to.emit(this.factory, 'return$deploy')
+        .withArgs(offChainComputed);
 
-      const instance = await VestingWallet.at(offChainComputed);
+      const instance = await ethers.getContractAt('VestingWallet', offChainComputed);
 
-      expect(await instance.owner()).to.be.equal(other);
+      expect(await instance.owner()).to.equal(this.other.address);
     });
 
     it('deploys a contract with funds deposited in the factory', async function () {
-      const deposit = ether('2');
-      await send.ether(deployerAccount, this.factory.address, deposit);
-      expect(await balance.current(this.factory.address)).to.be.bignumber.equal(deposit);
+      const value = 10n;
+
+      await this.deployer.sendTransaction({ to: this.factory, value });
 
       const offChainComputed = ethers.getCreate2Address(
-        this.factory.address,
+        this.factory.target,
         saltHex,
-        ethers.keccak256(constructorByteCode),
+        ethers.keccak256(this.constructorByteCode),
       );
 
-      expectEvent(await this.factory.$deploy(deposit, saltHex, constructorByteCode), 'return$deploy', {
-        addr: offChainComputed,
-      });
+      expect(await ethers.provider.getBalance(this.factory)).to.equal(value);
+      expect(await ethers.provider.getBalance(offChainComputed)).to.equal(0n);
 
-      expect(await balance.current(offChainComputed)).to.be.bignumber.equal(deposit);
+      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 () {
-      expectEvent(await this.factory.$deploy(0, saltHex, constructorByteCode), 'return$deploy');
+      await expect(this.factory.$deploy(0n, saltHex, this.constructorByteCode)).to.emit(this.factory, 'return$deploy');
 
-      // TODO: Make sure it actually throws "Create2FailedDeployment".
-      // For some unknown reason, the revert reason sometimes return:
-      // `revert with unrecognized return data or custom error`
-      await expectRevert.unspecified(this.factory.$deploy(0, saltHex, constructorByteCode));
+      await expect(this.factory.$deploy(0n, saltHex, this.constructorByteCode)).to.be.revertedWithCustomError(
+        this.factory,
+        'Create2FailedDeployment',
+      );
     });
 
     it('fails deploying a contract if the bytecode length is zero', async function () {
-      await expectRevertCustomError(this.factory.$deploy(0, saltHex, '0x'), 'Create2EmptyBytecode', []);
+      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 expectRevertCustomError(
-        this.factory.$deploy(1, saltHex, constructorByteCode),
-        'Create2InsufficientBalance',
-        [0, 1],
-      );
+      await expect(this.factory.$deploy(1n, saltHex, this.constructorByteCode))
+        .to.be.revertedWithCustomError(this.factory, 'Create2InsufficientBalance')
+        .withArgs(0n, 1n);
     });
   });
 });

+ 49 - 46
test/utils/Multicall.test.js

@@ -1,69 +1,72 @@
-const { BN } = require('@openzeppelin/test-helpers');
-const { expectRevertCustomError } = require('../helpers/customError');
+const { ethers } = require('hardhat');
+const { expect } = require('chai');
+const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
 
-const ERC20MulticallMock = artifacts.require('$ERC20MulticallMock');
+async function fixture() {
+  const [holder, alice, bruce] = await ethers.getSigners();
 
-contract('Multicall', function (accounts) {
-  const [deployer, alice, bob] = accounts;
-  const amount = 12000;
+  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 () {
-    this.multicallToken = await ERC20MulticallMock.new('name', 'symbol');
-    await this.multicallToken.$_mint(deployer, amount);
+    Object.assign(this, await loadFixture(fixture));
   });
 
   it('batches function calls', async function () {
-    expect(await this.multicallToken.balanceOf(alice)).to.be.bignumber.equal(new BN('0'));
-    expect(await this.multicallToken.balanceOf(bob)).to.be.bignumber.equal(new BN('0'));
+    expect(await this.mock.balanceOf(this.alice)).to.equal(0n);
+    expect(await this.mock.balanceOf(this.bruce)).to.equal(0n);
 
-    await this.multicallToken.multicall(
-      [
-        this.multicallToken.contract.methods.transfer(alice, amount / 2).encodeABI(),
-        this.multicallToken.contract.methods.transfer(bob, amount / 3).encodeABI(),
-      ],
-      { from: deployer },
-    );
+    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.address, this.alice.address, this.amount / 2n)
+      .to.emit(this.mock, 'Transfer')
+      .withArgs(this.holder.address, this.bruce.address, this.amount / 3n);
 
-    expect(await this.multicallToken.balanceOf(alice)).to.be.bignumber.equal(new BN(amount / 2));
-    expect(await this.multicallToken.balanceOf(bob)).to.be.bignumber.equal(new BN(amount / 3));
+    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 () {
-    const MulticallTest = artifacts.require('MulticallTest');
-    const multicallTest = await MulticallTest.new({ from: deployer });
-    await this.multicallToken.transfer(multicallTest.address, amount, { from: deployer });
-    expect(await this.multicallToken.balanceOf(multicallTest.address)).to.be.bignumber.equal(new BN(amount));
-
-    const recipients = [alice, bob];
-    const amounts = [amount / 2, amount / 3].map(n => new BN(n));
+    await this.mock.transfer(this.helper, this.amount);
+    expect(await this.mock.balanceOf(this.helper)).to.equal(this.amount);
 
-    await multicallTest.checkReturnValues(this.multicallToken.address, recipients, amounts);
+    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.multicallToken.balanceOf(alice)).to.be.bignumber.equal(new BN('0'));
+    expect(await this.mock.balanceOf(this.alice)).to.equal(0n);
 
-    const call = this.multicallToken.multicall(
-      [
-        this.multicallToken.contract.methods.transfer(alice, amount).encodeABI(),
-        this.multicallToken.contract.methods.transfer(bob, amount).encodeABI(),
-      ],
-      { from: deployer },
-    );
+    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.address, 0, this.amount);
 
-    await expectRevertCustomError(call, 'ERC20InsufficientBalance', [deployer, 0, amount]);
-    expect(await this.multicallToken.balanceOf(alice)).to.be.bignumber.equal(new BN('0'));
+    expect(await this.mock.balanceOf(this.alice)).to.equal(0n);
   });
 
   it('bubbles up revert reasons', async function () {
-    const call = this.multicallToken.multicall(
-      [
-        this.multicallToken.contract.methods.transfer(alice, amount).encodeABI(),
-        this.multicallToken.contract.methods.transfer(bob, amount).encodeABI(),
-      ],
-      { from: deployer },
-    );
-
-    await expectRevertCustomError(call, 'ERC20InsufficientBalance', [deployer, 0, amount]);
+    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.address, 0, this.amount);
   });
 });

+ 37 - 33
test/utils/Nonces.test.js

@@ -1,71 +1,75 @@
-const expectEvent = require('@openzeppelin/test-helpers/src/expectEvent');
-const { expectRevertCustomError } = require('../helpers/customError');
+const { ethers } = require('hardhat');
+const { expect } = require('chai');
+const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
 
-require('@openzeppelin/test-helpers');
+async function fixture() {
+  const [sender, other] = await ethers.getSigners();
 
-const Nonces = artifacts.require('$Nonces');
+  const mock = await ethers.deployContract('$Nonces');
 
-contract('Nonces', function (accounts) {
-  const [sender, other] = accounts;
+  return { sender, other, mock };
+}
 
+describe('Nonces', function () {
   beforeEach(async function () {
-    this.nonces = await Nonces.new();
+    Object.assign(this, await loadFixture(fixture));
   });
 
   it('gets a nonce', async function () {
-    expect(await this.nonces.nonces(sender)).to.be.bignumber.equal('0');
+    expect(await this.mock.nonces(this.sender)).to.equal(0n);
   });
 
   describe('_useNonce', function () {
     it('increments a nonce', async function () {
-      expect(await this.nonces.nonces(sender)).to.be.bignumber.equal('0');
+      expect(await this.mock.nonces(this.sender)).to.equal(0n);
 
-      const { receipt } = await this.nonces.$_useNonce(sender);
-      expectEvent(receipt, 'return$_useNonce', ['0']);
+      await expect(await this.mock.$_useNonce(this.sender))
+        .to.emit(this.mock, 'return$_useNonce')
+        .withArgs(0n);
 
-      expect(await this.nonces.nonces(sender)).to.be.bignumber.equal('1');
+      expect(await this.mock.nonces(this.sender)).to.equal(1n);
     });
 
     it("increments only sender's nonce", async function () {
-      expect(await this.nonces.nonces(sender)).to.be.bignumber.equal('0');
-      expect(await this.nonces.nonces(other)).to.be.bignumber.equal('0');
+      expect(await this.mock.nonces(this.sender)).to.equal(0n);
+      expect(await this.mock.nonces(this.other)).to.equal(0n);
 
-      await this.nonces.$_useNonce(sender);
+      await this.mock.$_useNonce(this.sender);
 
-      expect(await this.nonces.nonces(sender)).to.be.bignumber.equal('1');
-      expect(await this.nonces.nonces(other)).to.be.bignumber.equal('0');
+      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.nonces.nonces(sender);
-      expect(currentNonce).to.be.bignumber.equal('0');
+      const currentNonce = await this.mock.nonces(this.sender);
 
-      await this.nonces.$_useCheckedNonce(sender, currentNonce);
+      expect(currentNonce).to.equal(0n);
 
-      expect(await this.nonces.nonces(sender)).to.be.bignumber.equal('1');
+      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.nonces.nonces(sender);
+      const currentNonce = await this.mock.nonces(this.sender);
 
-      expect(currentNonce).to.be.bignumber.equal('0');
-      expect(await this.nonces.nonces(other)).to.be.bignumber.equal('0');
+      expect(currentNonce).to.equal(0n);
+      expect(await this.mock.nonces(this.other)).to.equal(0n);
 
-      await this.nonces.$_useCheckedNonce(sender, currentNonce);
+      await this.mock.$_useCheckedNonce(this.sender, currentNonce);
 
-      expect(await this.nonces.nonces(sender)).to.be.bignumber.equal('1');
-      expect(await this.nonces.nonces(other)).to.be.bignumber.equal('0');
+      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.nonces.nonces(sender);
-      await expectRevertCustomError(
-        this.nonces.$_useCheckedNonce(sender, currentNonce.addn(1)),
-        'InvalidAccountNonce',
-        [sender, currentNonce],
-      );
+      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.address, currentNonce);
     });
   });
 });

+ 36 - 32
test/utils/Pausable.test.js

@@ -1,83 +1,87 @@
-const { expectEvent } = require('@openzeppelin/test-helpers');
+const { ethers } = require('hardhat');
 const { expect } = require('chai');
+const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
 
-const { expectRevertCustomError } = require('../helpers/customError');
+async function fixture() {
+  const [pauser] = await ethers.getSigners();
 
-const PausableMock = artifacts.require('PausableMock');
+  const mock = await ethers.deployContract('PausableMock');
 
-contract('Pausable', function (accounts) {
-  const [pauser] = accounts;
+  return { pauser, mock };
+}
 
+describe('Pausable', function () {
   beforeEach(async function () {
-    this.pausable = await PausableMock.new();
+    Object.assign(this, await loadFixture(fixture));
   });
 
-  context('when unpaused', function () {
+  describe('when unpaused', function () {
     beforeEach(async function () {
-      expect(await this.pausable.paused()).to.equal(false);
+      expect(await this.mock.paused()).to.be.false;
     });
 
     it('can perform normal process in non-pause', async function () {
-      expect(await this.pausable.count()).to.be.bignumber.equal('0');
+      expect(await this.mock.count()).to.equal(0n);
 
-      await this.pausable.normalProcess();
-      expect(await this.pausable.count()).to.be.bignumber.equal('1');
+      await this.mock.normalProcess();
+      expect(await this.mock.count()).to.equal(1n);
     });
 
     it('cannot take drastic measure in non-pause', async function () {
-      await expectRevertCustomError(this.pausable.drasticMeasure(), 'ExpectedPause', []);
-      expect(await this.pausable.drasticMeasureTaken()).to.equal(false);
+      await expect(this.mock.drasticMeasure()).to.be.revertedWithCustomError(this.mock, 'ExpectedPause');
+
+      expect(await this.mock.drasticMeasureTaken()).to.be.false;
     });
 
-    context('when paused', function () {
+    describe('when paused', function () {
       beforeEach(async function () {
-        this.receipt = await this.pausable.pause({ from: pauser });
+        this.tx = await this.mock.pause();
       });
 
-      it('emits a Paused event', function () {
-        expectEvent(this.receipt, 'Paused', { account: pauser });
+      it('emits a Paused event', async function () {
+        await expect(this.tx).to.emit(this.mock, 'Paused').withArgs(this.pauser.address);
       });
 
       it('cannot perform normal process in pause', async function () {
-        await expectRevertCustomError(this.pausable.normalProcess(), 'EnforcedPause', []);
+        await expect(this.mock.normalProcess()).to.be.revertedWithCustomError(this.mock, 'EnforcedPause');
       });
 
       it('can take a drastic measure in a pause', async function () {
-        await this.pausable.drasticMeasure();
-        expect(await this.pausable.drasticMeasureTaken()).to.equal(true);
+        await this.mock.drasticMeasure();
+        expect(await this.mock.drasticMeasureTaken()).to.be.true;
       });
 
       it('reverts when re-pausing', async function () {
-        await expectRevertCustomError(this.pausable.pause(), 'EnforcedPause', []);
+        await expect(this.mock.pause()).to.be.revertedWithCustomError(this.mock, 'EnforcedPause');
       });
 
       describe('unpausing', function () {
         it('is unpausable by the pauser', async function () {
-          await this.pausable.unpause();
-          expect(await this.pausable.paused()).to.equal(false);
+          await this.mock.unpause();
+          expect(await this.mock.paused()).to.be.false;
         });
 
-        context('when unpaused', function () {
+        describe('when unpaused', function () {
           beforeEach(async function () {
-            this.receipt = await this.pausable.unpause({ from: pauser });
+            this.tx = await this.mock.unpause();
           });
 
-          it('emits an Unpaused event', function () {
-            expectEvent(this.receipt, 'Unpaused', { account: pauser });
+          it('emits an Unpaused event', async function () {
+            await expect(this.tx).to.emit(this.mock, 'Unpaused').withArgs(this.pauser.address);
           });
 
           it('should resume allowing normal process', async function () {
-            expect(await this.pausable.count()).to.be.bignumber.equal('0');
-            await this.pausable.normalProcess();
-            expect(await this.pausable.count()).to.be.bignumber.equal('1');
+            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 expectRevertCustomError(this.pausable.drasticMeasure(), 'ExpectedPause', []);
+            await expect(this.mock.drasticMeasure()).to.be.revertedWithCustomError(this.mock, 'ExpectedPause');
           });
 
           it('reverts when re-unpausing', async function () {
-            await expectRevertCustomError(this.pausable.unpause(), 'ExpectedPause', []);
+            await expect(this.mock.unpause()).to.be.revertedWithCustomError(this.mock, 'ExpectedPause');
           });
         });
       });

+ 20 - 17
test/utils/ReentrancyGuard.test.js

@@ -1,44 +1,47 @@
-const { expectRevert } = require('@openzeppelin/test-helpers');
+const { ethers } = require('hardhat');
 const { expect } = require('chai');
+const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
 
-const { expectRevertCustomError } = require('../helpers/customError');
+async function fixture() {
+  const mock = await ethers.deployContract('ReentrancyMock');
+  return { mock };
+}
 
-const ReentrancyMock = artifacts.require('ReentrancyMock');
-const ReentrancyAttack = artifacts.require('ReentrancyAttack');
-
-contract('ReentrancyGuard', function () {
+describe('ReentrancyGuard', function () {
   beforeEach(async function () {
-    this.reentrancyMock = await ReentrancyMock.new();
-    expect(await this.reentrancyMock.counter()).to.be.bignumber.equal('0');
+    Object.assign(this, await loadFixture(fixture));
   });
 
   it('nonReentrant function can be called', async function () {
-    expect(await this.reentrancyMock.counter()).to.be.bignumber.equal('0');
-    await this.reentrancyMock.callback();
-    expect(await this.reentrancyMock.counter()).to.be.bignumber.equal('1');
+    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 ReentrancyAttack.new();
-    await expectRevert(this.reentrancyMock.countAndCall(attacker.address), 'ReentrancyAttack: failed call', []);
+    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.reentrancyMock.guardedCheckEntered();
+    await this.mock.guardedCheckEntered();
   });
 
   it('_reentrancyGuardEntered should be false when unguarded', async function () {
-    await this.reentrancyMock.unguardedCheckNotEntered();
+    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 expectRevertCustomError(this.reentrancyMock.countLocalRecursive(10), 'ReentrancyGuardReentrantCall', []);
+    await expect(this.mock.countLocalRecursive(10n)).to.be.revertedWithCustomError(
+      this.mock,
+      'ReentrancyGuardReentrantCall',
+    );
   });
 
   it('does not allow indirect local recursion', async function () {
-    await expectRevert(this.reentrancyMock.countThisRecursive(10), 'ReentrancyMock: failed call', []);
+    await expect(this.mock.countThisRecursive(10n)).to.be.revertedWith('ReentrancyMock: failed call');
   });
 });

+ 36 - 27
test/utils/ShortStrings.test.js

@@ -1,19 +1,27 @@
+const { ethers } = require('hardhat');
 const { expect } = require('chai');
-const { expectRevertCustomError } = require('../helpers/customError');
+const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
 
-const ShortStrings = artifacts.require('$ShortStrings');
+const FALLBACK_SENTINEL = ethers.zeroPadValue('0xFF', 32);
 
-function length(sstr) {
-  return parseInt(sstr.slice(64), 16);
-}
+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;
 
-function decode(sstr) {
-  return web3.utils.toUtf8(sstr).slice(0, length(sstr));
+async function fixture() {
+  const mock = await ethers.deployContract('$ShortStrings');
+  return { mock };
 }
 
-contract('ShortStrings', function () {
-  before(async function () {
-    this.mock = await ShortStrings.new();
+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))) {
@@ -21,34 +29,35 @@ contract('ShortStrings', function () {
       it('encode / decode', async function () {
         if (str.length < 32) {
           const encoded = await this.mock.$toShortString(str);
-          expect(decode(encoded)).to.be.equal(str);
-
-          const length = await this.mock.$byteLength(encoded);
-          expect(length.toNumber()).to.be.equal(str.length);
+          expect(encoded).to.equal(encode(str));
+          expect(decode(encoded)).to.equal(str);
 
-          const decoded = await this.mock.$toString(encoded);
-          expect(decoded).to.be.equal(str);
+          expect(await this.mock.$byteLength(encoded)).to.equal(str.length);
+          expect(await this.mock.$toString(encoded)).to.equal(str);
         } else {
-          await expectRevertCustomError(this.mock.$toShortString(str), 'StringTooLong', [str]);
+          await expect(this.mock.$toShortString(str))
+            .to.be.revertedWithCustomError(this.mock, 'StringTooLong')
+            .withArgs(str);
         }
       });
 
       it('set / get with fallback', async function () {
-        const { logs } = await this.mock.$toShortStringWithFallback(str, 0);
-        const { ret0 } = logs.find(({ event }) => event == 'return$toShortStringWithFallback').args;
+        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]);
 
-        const promise = this.mock.$toString(ret0);
+        expect(short).to.equal(encode(str));
+
+        const promise = this.mock.$toString(short);
         if (str.length < 32) {
-          expect(await promise).to.be.equal(str);
+          expect(await promise).to.equal(str);
         } else {
-          await expectRevertCustomError(promise, 'InvalidShortString', []);
+          await expect(promise).to.be.revertedWithCustomError(this.mock, 'InvalidShortString');
         }
 
-        const length = await this.mock.$byteLengthWithFallback(ret0, 0);
-        expect(length.toNumber()).to.be.equal(str.length);
-
-        const recovered = await this.mock.$toStringWithFallback(ret0, 0);
-        expect(recovered).to.be.equal(str);
+        expect(await this.mock.$byteLengthWithFallback(short, 0)).to.equal(str.length);
+        expect(await this.mock.$toStringWithFallback(short, 0)).to.equal(str);
       });
     });
   }

+ 53 - 189
test/utils/StorageSlot.test.js

@@ -1,210 +1,74 @@
-const { constants, BN } = require('@openzeppelin/test-helpers');
-
+const { ethers } = require('hardhat');
 const { expect } = require('chai');
+const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
+const { generators } = require('../helpers/random');
 
-const StorageSlotMock = artifacts.require('StorageSlotMock');
+const slot = ethers.id('some.storage.slot');
+const otherSlot = ethers.id('some.other.storage.slot');
 
-const slot = web3.utils.keccak256('some.storage.slot');
-const otherSlot = web3.utils.keccak256('some.other.storage.slot');
+async function fixture() {
+  const [account] = await ethers.getSigners();
+  const mock = await ethers.deployContract('StorageSlotMock');
+  return { mock, account };
+}
 
-contract('StorageSlot', function (accounts) {
+describe('StorageSlot', function () {
   beforeEach(async function () {
-    this.store = await StorageSlotMock.new();
+    Object.assign(this, await loadFixture(fixture));
   });
 
-  describe('boolean storage slot', function () {
-    beforeEach(async function () {
-      this.value = true;
-    });
-
-    it('set', async function () {
-      await this.store.setBoolean(slot, this.value);
-    });
-
-    describe('get', function () {
-      beforeEach(async function () {
-        await this.store.setBoolean(slot, this.value);
+  for (const { type, value, zero } of [
+    { type: 'Boolean', value: true, zero: false },
+    { type: 'Address', value: generators.address(), zero: ethers.ZeroAddress },
+    { type: 'Bytes32', value: generators.bytes32(), zero: ethers.ZeroHash },
+    { type: 'String', value: 'lorem ipsum', zero: '' },
+    { type: 'Bytes', value: generators.hexBytes(128), zero: '0x' },
+  ]) {
+    describe(`${type} storage slot`, function () {
+      it('set', async function () {
+        await this.mock.getFunction(`set${type}Slot`)(slot, value);
       });
 
-      it('from right slot', async function () {
-        expect(await this.store.getBoolean(slot)).to.be.equal(this.value);
-      });
+      describe('get', function () {
+        beforeEach(async function () {
+          await this.mock.getFunction(`set${type}Slot`)(slot, value);
+        });
 
-      it('from other slot', async function () {
-        expect(await this.store.getBoolean(otherSlot)).to.be.equal(false);
-      });
-    });
-  });
-
-  describe('address storage slot', function () {
-    beforeEach(async function () {
-      this.value = accounts[1];
-    });
+        it('from right slot', async function () {
+          expect(await this.mock.getFunction(`get${type}Slot`)(slot)).to.equal(value);
+        });
 
-    it('set', async function () {
-      await this.store.setAddress(slot, this.value);
-    });
-
-    describe('get', function () {
-      beforeEach(async function () {
-        await this.store.setAddress(slot, this.value);
-      });
-
-      it('from right slot', async function () {
-        expect(await this.store.getAddress(slot)).to.be.equal(this.value);
-      });
-
-      it('from other slot', async function () {
-        expect(await this.store.getAddress(otherSlot)).to.be.equal(constants.ZERO_ADDRESS);
+        it('from other slot', async function () {
+          expect(await this.mock.getFunction(`get${type}Slot`)(otherSlot)).to.equal(zero);
+        });
       });
     });
-  });
-
-  describe('bytes32 storage slot', function () {
-    beforeEach(async function () {
-      this.value = web3.utils.keccak256('some byte32 value');
-    });
-
-    it('set', async function () {
-      await this.store.setBytes32(slot, this.value);
-    });
-
-    describe('get', function () {
-      beforeEach(async function () {
-        await this.store.setBytes32(slot, this.value);
-      });
+  }
 
-      it('from right slot', async function () {
-        expect(await this.store.getBytes32(slot)).to.be.equal(this.value);
+  for (const { type, value, zero } of [
+    { type: 'String', value: 'lorem ipsum', zero: '' },
+    { type: 'Bytes', value: generators.hexBytes(128), zero: '0x' },
+  ]) {
+    describe(`${type} storage pointer`, function () {
+      it('set', async function () {
+        await this.mock.getFunction(`set${type}Storage`)(slot, value);
       });
 
-      it('from other slot', async function () {
-        expect(await this.store.getBytes32(otherSlot)).to.be.equal(constants.ZERO_BYTES32);
-      });
-    });
-  });
+      describe('get', function () {
+        beforeEach(async function () {
+          await this.mock.getFunction(`set${type}Storage`)(slot, value);
+        });
 
-  describe('uint256 storage slot', function () {
-    beforeEach(async function () {
-      this.value = new BN(1742);
-    });
-
-    it('set', async function () {
-      await this.store.setUint256(slot, this.value);
-    });
-
-    describe('get', function () {
-      beforeEach(async function () {
-        await this.store.setUint256(slot, this.value);
-      });
+        it('from right slot', async function () {
+          expect(await this.mock.getFunction(`${type.toLowerCase()}Map`)(slot)).to.equal(value);
+          expect(await this.mock.getFunction(`get${type}Storage`)(slot)).to.equal(value);
+        });
 
-      it('from right slot', async function () {
-        expect(await this.store.getUint256(slot)).to.be.bignumber.equal(this.value);
+        it('from other slot', async function () {
+          expect(await this.mock.getFunction(`${type.toLowerCase()}Map`)(otherSlot)).to.equal(zero);
+          expect(await this.mock.getFunction(`get${type}Storage`)(otherSlot)).to.equal(zero);
+        });
       });
-
-      it('from other slot', async function () {
-        expect(await this.store.getUint256(otherSlot)).to.be.bignumber.equal('0');
-      });
-    });
-  });
-
-  describe('string storage slot', function () {
-    beforeEach(async function () {
-      this.value = 'lorem ipsum';
     });
-
-    it('set', async function () {
-      await this.store.setString(slot, this.value);
-    });
-
-    describe('get', function () {
-      beforeEach(async function () {
-        await this.store.setString(slot, this.value);
-      });
-
-      it('from right slot', async function () {
-        expect(await this.store.getString(slot)).to.be.equal(this.value);
-      });
-
-      it('from other slot', async function () {
-        expect(await this.store.getString(otherSlot)).to.be.equal('');
-      });
-    });
-  });
-
-  describe('string storage pointer', function () {
-    beforeEach(async function () {
-      this.value = 'lorem ipsum';
-    });
-
-    it('set', async function () {
-      await this.store.setStringStorage(slot, this.value);
-    });
-
-    describe('get', function () {
-      beforeEach(async function () {
-        await this.store.setStringStorage(slot, this.value);
-      });
-
-      it('from right slot', async function () {
-        expect(await this.store.stringMap(slot)).to.be.equal(this.value);
-        expect(await this.store.getStringStorage(slot)).to.be.equal(this.value);
-      });
-
-      it('from other slot', async function () {
-        expect(await this.store.stringMap(otherSlot)).to.be.equal('');
-        expect(await this.store.getStringStorage(otherSlot)).to.be.equal('');
-      });
-    });
-  });
-
-  describe('bytes storage slot', function () {
-    beforeEach(async function () {
-      this.value = web3.utils.randomHex(128);
-    });
-
-    it('set', async function () {
-      await this.store.setBytes(slot, this.value);
-    });
-
-    describe('get', function () {
-      beforeEach(async function () {
-        await this.store.setBytes(slot, this.value);
-      });
-
-      it('from right slot', async function () {
-        expect(await this.store.getBytes(slot)).to.be.equal(this.value);
-      });
-
-      it('from other slot', async function () {
-        expect(await this.store.getBytes(otherSlot)).to.be.equal(null);
-      });
-    });
-  });
-
-  describe('bytes storage pointer', function () {
-    beforeEach(async function () {
-      this.value = web3.utils.randomHex(128);
-    });
-
-    it('set', async function () {
-      await this.store.setBytesStorage(slot, this.value);
-    });
-
-    describe('get', function () {
-      beforeEach(async function () {
-        await this.store.setBytesStorage(slot, this.value);
-      });
-
-      it('from right slot', async function () {
-        expect(await this.store.bytesMap(slot)).to.be.equal(this.value);
-        expect(await this.store.getBytesStorage(slot)).to.be.equal(this.value);
-      });
-
-      it('from other slot', async function () {
-        expect(await this.store.bytesMap(otherSlot)).to.be.equal(null);
-        expect(await this.store.getBytesStorage(otherSlot)).to.be.equal(null);
-      });
-    });
-  });
+  }
 });

+ 56 - 56
test/utils/Strings.test.js

@@ -1,69 +1,71 @@
-const { BN, constants } = require('@openzeppelin/test-helpers');
-const { expectRevertCustomError } = require('../helpers/customError');
-
+const { ethers } = require('hardhat');
 const { expect } = require('chai');
+const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
 
-const Strings = artifacts.require('$Strings');
+async function fixture() {
+  const mock = await ethers.deployContract('$Strings');
+  return { mock };
+}
 
-contract('Strings', function () {
+describe('Strings', function () {
   before(async function () {
-    this.strings = await Strings.new();
+    Object.assign(this, await loadFixture(fixture));
   });
 
   describe('toString', function () {
     const values = [
-      '0',
-      '7',
-      '10',
-      '99',
-      '100',
-      '101',
-      '123',
-      '4132',
-      '12345',
-      '1234567',
-      '1234567890',
-      '123456789012345',
-      '12345678901234567890',
-      '123456789012345678901234567890',
-      '1234567890123456789012345678901234567890',
-      '12345678901234567890123456789012345678901234567890',
-      '123456789012345678901234567890123456789012345678901234567890',
-      '1234567890123456789012345678901234567890123456789012345678901234567890',
+      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 = constants.MAX_UINT256;
-        expect(await this.strings.methods['$toString(uint256)'](value)).to.equal(value.toString(10));
+        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.strings.methods['$toString(uint256)'](value)).to.equal(value);
+          expect(await this.mock.$toString(value)).to.equal(value);
         });
       }
     });
 
     describe('int256', function () {
       it('converts MAX_INT256', async function () {
-        const value = constants.MAX_INT256;
-        expect(await this.strings.methods['$toStringSigned(int256)'](value)).to.equal(value.toString(10));
+        const value = ethers.MaxInt256;
+        expect(await this.mock.$toStringSigned(value)).to.equal(value.toString(10));
       });
 
       it('converts MIN_INT256', async function () {
-        const value = constants.MIN_INT256;
-        expect(await this.strings.methods['$toStringSigned(int256)'](value)).to.equal(value.toString(10));
+        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.strings.methods['$toStringSigned(int256)'](value)).to.equal(value);
+          expect(await this.mock.$toStringSigned(value)).to.equal(value);
         });
 
         it(`convert negative ${value}`, async function () {
-          const negated = new BN(value).neg();
-          expect(await this.strings.methods['$toStringSigned(int256)'](negated)).to.equal(negated.toString(10));
+          const negated = -value;
+          expect(await this.mock.$toStringSigned(negated)).to.equal(negated.toString(10));
         });
       }
     });
@@ -71,39 +73,37 @@ contract('Strings', function () {
 
   describe('toHexString', function () {
     it('converts 0', async function () {
-      expect(await this.strings.methods['$toHexString(uint256)'](0)).to.equal('0x00');
+      expect(await this.mock.getFunction('$toHexString(uint256)')(0n)).to.equal('0x00');
     });
 
     it('converts a positive number', async function () {
-      expect(await this.strings.methods['$toHexString(uint256)'](0x4132)).to.equal('0x4132');
+      expect(await this.mock.getFunction('$toHexString(uint256)')(0x4132n)).to.equal('0x4132');
     });
 
     it('converts MAX_UINT256', async function () {
-      expect(await this.strings.methods['$toHexString(uint256)'](constants.MAX_UINT256)).to.equal(
-        web3.utils.toHex(constants.MAX_UINT256),
+      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.strings.methods['$toHexString(uint256,uint256)'](0x4132, 32)).to.equal(
+      expect(await this.mock.getFunction('$toHexString(uint256,uint256)')(0x4132n, 32n)).to.equal(
         '0x0000000000000000000000000000000000000000000000000000000000004132',
       );
     });
 
     it('converts a positive number (short)', async function () {
-      const length = 1;
-      await expectRevertCustomError(
-        this.strings.methods['$toHexString(uint256,uint256)'](0x4132, length),
-        `StringsInsufficientHexLength`,
-        [0x4132, length],
-      );
+      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.strings.methods['$toHexString(uint256,uint256)'](constants.MAX_UINT256, 32)).to.equal(
-        web3.utils.toHex(constants.MAX_UINT256),
+      expect(await this.mock.getFunction('$toHexString(uint256,uint256)')(ethers.MaxUint256, 32n)).to.equal(
+        `0x${ethers.MaxUint256.toString(16)}`,
       );
     });
   });
@@ -111,43 +111,43 @@ contract('Strings', function () {
   describe('toHexString address', function () {
     it('converts a random address', async function () {
       const addr = '0xa9036907dccae6a1e0033479b12e837e5cf5a02f';
-      expect(await this.strings.methods['$toHexString(address)'](addr)).to.equal(addr);
+      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.strings.methods['$toHexString(address)'](addr)).to.equal(addr);
+      expect(await this.mock.getFunction('$toHexString(address)')(addr)).to.equal(addr);
     });
   });
 
   describe('equal', function () {
     it('compares two empty strings', async function () {
-      expect(await this.strings.methods['$equal(string,string)']('', '')).to.equal(true);
+      expect(await this.mock.$equal('', '')).to.be.true;
     });
 
     it('compares two equal strings', async function () {
-      expect(await this.strings.methods['$equal(string,string)']('a', 'a')).to.equal(true);
+      expect(await this.mock.$equal('a', 'a')).to.be.true;
     });
 
     it('compares two different strings', async function () {
-      expect(await this.strings.methods['$equal(string,string)']('a', 'b')).to.equal(false);
+      expect(await this.mock.$equal('a', 'b')).to.be.false;
     });
 
     it('compares two different strings of different lengths', async function () {
-      expect(await this.strings.methods['$equal(string,string)']('a', 'aa')).to.equal(false);
-      expect(await this.strings.methods['$equal(string,string)']('aa', 'a')).to.equal(false);
+      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.strings.methods['$equal(string,string)'](str1, str2)).to.equal(false);
+      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.strings.methods['$equal(string,string)'](str1, str2)).to.equal(true);
+      expect(await this.mock.$equal(str1, str2)).to.be.true;
     });
   });
 });