Browse Source

Migrate proxy folder to ethersjs (#4746)

Co-authored-by: Hadrien Croubois <hadrien.croubois@gmail.com>
Co-authored-by: ernestognw <ernestognw@gmail.com>
Renan Souza 1 năm trước cách đây
mục cha
commit
ae69142379

+ 17 - 22
test/helpers/erc1967.js

@@ -5,37 +5,32 @@ const ImplementationLabel = 'eip1967.proxy.implementation';
 const AdminLabel = 'eip1967.proxy.admin';
 const BeaconLabel = 'eip1967.proxy.beacon';
 
-function labelToSlot(label) {
-  return ethers.toBeHex(BigInt(ethers.keccak256(ethers.toUtf8Bytes(label))) - 1n);
-}
+const erc1967slot = label => ethers.toBeHex(ethers.toBigInt(ethers.id(label)) - 1n);
+const erc7201slot = label => ethers.toBeHex(ethers.toBigInt(ethers.keccak256(erc1967slot(label))) & ~0xffn);
 
-function getSlot(address, slot) {
-  return getStorageAt(
-    ethers.isAddress(address) ? address : address.address,
-    ethers.isBytesLike(slot) ? slot : labelToSlot(slot),
+const getSlot = (address, slot) =>
+  (ethers.isAddressable(address) ? address.getAddress() : Promise.resolve(address)).then(address =>
+    getStorageAt(address, ethers.isBytesLike(slot) ? slot : erc1967slot(slot)),
   );
-}
 
-function setSlot(address, slot, value) {
-  return setStorageAt(
-    ethers.isAddress(address) ? address : address.address,
-    ethers.isBytesLike(slot) ? slot : labelToSlot(slot),
-    value,
-  );
-}
+const setSlot = (address, slot, value) =>
+  Promise.all([
+    ethers.isAddressable(address) ? address.getAddress() : Promise.resolve(address),
+    ethers.isAddressable(value) ? value.getAddress() : Promise.resolve(value),
+  ]).then(([address, value]) => setStorageAt(address, ethers.isBytesLike(slot) ? slot : erc1967slot(slot), value));
 
-async function getAddressInSlot(address, slot) {
-  const slotValue = await getSlot(address, slot);
-  return ethers.getAddress(slotValue.substring(slotValue.length - 40));
-}
+const getAddressInSlot = (address, slot) =>
+  getSlot(address, slot).then(slotValue => ethers.AbiCoder.defaultAbiCoder().decode(['address'], slotValue)[0]);
 
 module.exports = {
   ImplementationLabel,
   AdminLabel,
   BeaconLabel,
-  ImplementationSlot: labelToSlot(ImplementationLabel),
-  AdminSlot: labelToSlot(AdminLabel),
-  BeaconSlot: labelToSlot(BeaconLabel),
+  ImplementationSlot: erc1967slot(ImplementationLabel),
+  AdminSlot: erc1967slot(AdminLabel),
+  BeaconSlot: erc1967slot(BeaconLabel),
+  erc1967slot,
+  erc7201slot,
   setSlot,
   getSlot,
   getAddressInSlot,

+ 41 - 36
test/proxy/Clones.behaviour.js

@@ -1,33 +1,29 @@
-const { expectRevert } = require('@openzeppelin/test-helpers');
-
+const { ethers } = require('hardhat');
 const { expect } = require('chai');
 
-const DummyImplementation = artifacts.require('DummyImplementation');
-
-module.exports = function shouldBehaveLikeClone(createClone) {
-  before('deploy implementation', async function () {
-    this.implementation = web3.utils.toChecksumAddress((await DummyImplementation.new()).address);
-  });
-
+module.exports = function shouldBehaveLikeClone() {
   const assertProxyInitialization = function ({ value, balance }) {
     it('initializes the proxy', async function () {
-      const dummy = new DummyImplementation(this.proxy);
-      expect(await dummy.value()).to.be.bignumber.equal(value.toString());
+      const dummy = await ethers.getContractAt('DummyImplementation', this.proxy);
+      expect(await dummy.value()).to.equal(value);
     });
 
     it('has expected balance', async function () {
-      expect(await web3.eth.getBalance(this.proxy)).to.be.bignumber.equal(balance.toString());
+      expect(await ethers.provider.getBalance(this.proxy)).to.equal(balance);
     });
   };
 
   describe('initialization without parameters', function () {
     describe('non payable', function () {
-      const expectedInitializedValue = 10;
-      const initializeData = new DummyImplementation('').contract.methods['initializeNonPayable()']().encodeABI();
+      const expectedInitializedValue = 10n;
+
+      beforeEach(async function () {
+        this.initializeData = await this.implementation.interface.encodeFunctionData('initializeNonPayable');
+      });
 
       describe('when not sending balance', function () {
         beforeEach('creating proxy', async function () {
-          this.proxy = (await createClone(this.implementation, initializeData)).address;
+          this.proxy = await this.createClone(this.initializeData);
         });
 
         assertProxyInitialization({
@@ -37,21 +33,24 @@ module.exports = function shouldBehaveLikeClone(createClone) {
       });
 
       describe('when sending some balance', function () {
-        const value = 10e5;
+        const value = 10n ** 6n;
 
         it('reverts', async function () {
-          await expectRevert.unspecified(createClone(this.implementation, initializeData, { value }));
+          await expect(this.createClone(this.initializeData, { value })).to.be.reverted;
         });
       });
     });
 
     describe('payable', function () {
-      const expectedInitializedValue = 100;
-      const initializeData = new DummyImplementation('').contract.methods['initializePayable()']().encodeABI();
+      const expectedInitializedValue = 100n;
+
+      beforeEach(async function () {
+        this.initializeData = await this.implementation.interface.encodeFunctionData('initializePayable');
+      });
 
       describe('when not sending balance', function () {
         beforeEach('creating proxy', async function () {
-          this.proxy = (await createClone(this.implementation, initializeData)).address;
+          this.proxy = await this.createClone(this.initializeData);
         });
 
         assertProxyInitialization({
@@ -61,10 +60,10 @@ module.exports = function shouldBehaveLikeClone(createClone) {
       });
 
       describe('when sending some balance', function () {
-        const value = 10e5;
+        const value = 10n ** 6n;
 
         beforeEach('creating proxy', async function () {
-          this.proxy = (await createClone(this.implementation, initializeData, { value })).address;
+          this.proxy = await this.createClone(this.initializeData, { value });
         });
 
         assertProxyInitialization({
@@ -77,14 +76,17 @@ module.exports = function shouldBehaveLikeClone(createClone) {
 
   describe('initialization with parameters', function () {
     describe('non payable', function () {
-      const expectedInitializedValue = 10;
-      const initializeData = new DummyImplementation('').contract.methods
-        .initializeNonPayableWithValue(expectedInitializedValue)
-        .encodeABI();
+      const expectedInitializedValue = 10n;
+
+      beforeEach(async function () {
+        this.initializeData = await this.implementation.interface.encodeFunctionData('initializeNonPayableWithValue', [
+          expectedInitializedValue,
+        ]);
+      });
 
       describe('when not sending balance', function () {
         beforeEach('creating proxy', async function () {
-          this.proxy = (await createClone(this.implementation, initializeData)).address;
+          this.proxy = await this.createClone(this.initializeData);
         });
 
         assertProxyInitialization({
@@ -94,23 +96,26 @@ module.exports = function shouldBehaveLikeClone(createClone) {
       });
 
       describe('when sending some balance', function () {
-        const value = 10e5;
+        const value = 10n ** 6n;
 
         it('reverts', async function () {
-          await expectRevert.unspecified(createClone(this.implementation, initializeData, { value }));
+          await expect(this.createClone(this.initializeData, { value })).to.be.reverted;
         });
       });
     });
 
     describe('payable', function () {
-      const expectedInitializedValue = 42;
-      const initializeData = new DummyImplementation('').contract.methods
-        .initializePayableWithValue(expectedInitializedValue)
-        .encodeABI();
+      const expectedInitializedValue = 42n;
+
+      beforeEach(function () {
+        this.initializeData = this.implementation.interface.encodeFunctionData('initializePayableWithValue', [
+          expectedInitializedValue,
+        ]);
+      });
 
       describe('when not sending balance', function () {
         beforeEach('creating proxy', async function () {
-          this.proxy = (await createClone(this.implementation, initializeData)).address;
+          this.proxy = await this.createClone(this.initializeData);
         });
 
         assertProxyInitialization({
@@ -120,10 +125,10 @@ module.exports = function shouldBehaveLikeClone(createClone) {
       });
 
       describe('when sending some balance', function () {
-        const value = 10e5;
+        const value = 10n ** 6n;
 
         beforeEach('creating proxy', async function () {
-          this.proxy = (await createClone(this.implementation, initializeData, { value })).address;
+          this.proxy = await this.createClone(this.initializeData, { value });
         });
 
         assertProxyInitialization({

+ 62 - 37
test/proxy/Clones.test.js

@@ -1,62 +1,87 @@
-const { ethers } = require('ethers');
-const { expectEvent } = require('@openzeppelin/test-helpers');
+const { ethers } = require('hardhat');
 const { expect } = require('chai');
-const { expectRevertCustomError } = require('../helpers/customError');
+const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
 
 const shouldBehaveLikeClone = require('./Clones.behaviour');
 
-const Clones = artifacts.require('$Clones');
+async function fixture() {
+  const [deployer] = await ethers.getSigners();
 
-contract('Clones', function (accounts) {
-  const [deployer] = accounts;
+  const factory = await ethers.deployContract('$Clones');
+  const implementation = await ethers.deployContract('DummyImplementation');
+
+  const newClone = async (initData, opts = {}) => {
+    const clone = await factory.$clone.staticCall(implementation).then(address => implementation.attach(address));
+    await factory.$clone(implementation);
+    await deployer.sendTransaction({ to: clone, value: opts.value ?? 0n, data: initData ?? '0x' });
+    return clone;
+  };
+
+  const newCloneDeterministic = async (initData, opts = {}) => {
+    const salt = opts.salt ?? ethers.randomBytes(32);
+    const clone = await factory.$cloneDeterministic
+      .staticCall(implementation, salt)
+      .then(address => implementation.attach(address));
+    await factory.$cloneDeterministic(implementation, salt);
+    await deployer.sendTransaction({ to: clone, value: opts.value ?? 0n, data: initData ?? '0x' });
+    return clone;
+  };
+
+  return { deployer, factory, implementation, newClone, newCloneDeterministic };
+}
+
+describe('Clones', function () {
+  beforeEach(async function () {
+    Object.assign(this, await loadFixture(fixture));
+  });
 
   describe('clone', function () {
-    shouldBehaveLikeClone(async (implementation, initData, opts = {}) => {
-      const factory = await Clones.new();
-      const receipt = await factory.$clone(implementation);
-      const address = receipt.logs.find(({ event }) => event === 'return$clone').args.instance;
-      await web3.eth.sendTransaction({ from: deployer, to: address, value: opts.value, data: initData });
-      return { address };
+    beforeEach(async function () {
+      this.createClone = this.newClone;
     });
+
+    shouldBehaveLikeClone();
   });
 
   describe('cloneDeterministic', function () {
-    shouldBehaveLikeClone(async (implementation, initData, opts = {}) => {
-      const salt = web3.utils.randomHex(32);
-      const factory = await Clones.new();
-      const receipt = await factory.$cloneDeterministic(implementation, salt);
-      const address = receipt.logs.find(({ event }) => event === 'return$cloneDeterministic').args.instance;
-      await web3.eth.sendTransaction({ from: deployer, to: address, value: opts.value, data: initData });
-      return { address };
+    beforeEach(async function () {
+      this.createClone = this.newCloneDeterministic;
     });
 
-    it('address already used', async function () {
-      const implementation = web3.utils.randomHex(20);
-      const salt = web3.utils.randomHex(32);
-      const factory = await Clones.new();
+    shouldBehaveLikeClone();
+
+    it('revert if address already used', async function () {
+      const salt = ethers.randomBytes(32);
+
       // deploy once
-      expectEvent(await factory.$cloneDeterministic(implementation, salt), 'return$cloneDeterministic');
+      await expect(this.factory.$cloneDeterministic(this.implementation, salt)).to.emit(
+        this.factory,
+        'return$cloneDeterministic',
+      );
+
       // deploy twice
-      await expectRevertCustomError(factory.$cloneDeterministic(implementation, salt), 'ERC1167FailedCreateClone', []);
+      await expect(this.factory.$cloneDeterministic(this.implementation, salt)).to.be.revertedWithCustomError(
+        this.factory,
+        'ERC1167FailedCreateClone',
+      );
     });
 
     it('address prediction', async function () {
-      const implementation = web3.utils.randomHex(20);
-      const salt = web3.utils.randomHex(32);
-      const factory = await Clones.new();
-      const predicted = await factory.$predictDeterministicAddress(implementation, salt);
+      const salt = ethers.randomBytes(32);
 
-      const creationCode = [
+      const creationCode = ethers.concat([
         '0x3d602d80600a3d3981f3363d3d373d3d3d363d73',
-        implementation.replace(/0x/, '').toLowerCase(),
-        '5af43d82803e903d91602b57fd5bf3',
-      ].join('');
+        this.implementation.target,
+        '0x5af43d82803e903d91602b57fd5bf3',
+      ]);
 
-      expect(ethers.getCreate2Address(factory.address, salt, ethers.keccak256(creationCode))).to.be.equal(predicted);
+      const predicted = await this.factory.$predictDeterministicAddress(this.implementation, salt);
+      const expected = ethers.getCreate2Address(this.factory.target, salt, ethers.keccak256(creationCode));
+      expect(predicted).to.equal(expected);
 
-      expectEvent(await factory.$cloneDeterministic(implementation, salt), 'return$cloneDeterministic', {
-        instance: predicted,
-      });
+      await expect(this.factory.$cloneDeterministic(this.implementation, salt))
+        .to.emit(this.factory, 'return$cloneDeterministic')
+        .withArgs(predicted);
     });
   });
 });

+ 18 - 7
test/proxy/ERC1967/ERC1967Proxy.test.js

@@ -1,12 +1,23 @@
+const { ethers } = require('hardhat');
+
 const shouldBehaveLikeProxy = require('../Proxy.behaviour');
+const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
+
+const fixture = async () => {
+  const [nonContractAddress] = await ethers.getSigners();
+
+  const implementation = await ethers.deployContract('DummyImplementation');
+
+  const createProxy = (implementation, initData, opts) =>
+    ethers.deployContract('ERC1967Proxy', [implementation, initData], opts);
 
-const ERC1967Proxy = artifacts.require('ERC1967Proxy');
+  return { nonContractAddress, implementation, createProxy };
+};
 
-contract('ERC1967Proxy', function (accounts) {
-  // `undefined`, `null` and other false-ish opts will not be forwarded.
-  const createProxy = async function (implementation, initData, opts) {
-    return ERC1967Proxy.new(implementation, initData, ...[opts].filter(Boolean));
-  };
+describe('ERC1967Proxy', function () {
+  beforeEach(async function () {
+    Object.assign(this, await loadFixture(fixture));
+  });
 
-  shouldBehaveLikeProxy(createProxy, accounts);
+  shouldBehaveLikeProxy();
 });

+ 64 - 74
test/proxy/ERC1967/ERC1967Utils.test.js

@@ -1,70 +1,65 @@
-const { expectEvent, constants } = require('@openzeppelin/test-helpers');
-const { expectRevertCustomError } = require('../../helpers/customError');
-const { getAddressInSlot, setSlot, ImplementationSlot, AdminSlot, BeaconSlot } = require('../../helpers/erc1967');
+const { ethers } = require('hardhat');
+const { expect } = require('chai');
+const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
 
-const { ZERO_ADDRESS } = constants;
+const { getAddressInSlot, setSlot, ImplementationSlot, AdminSlot, BeaconSlot } = require('../../helpers/erc1967');
 
-const ERC1967Utils = artifacts.require('$ERC1967Utils');
+async function fixture() {
+  const [, admin, anotherAccount] = await ethers.getSigners();
 
-const V1 = artifacts.require('DummyImplementation');
-const V2 = artifacts.require('CallReceiverMock');
-const UpgradeableBeaconMock = artifacts.require('UpgradeableBeaconMock');
-const UpgradeableBeaconReentrantMock = artifacts.require('UpgradeableBeaconReentrantMock');
+  const utils = await ethers.deployContract('$ERC1967Utils');
+  const v1 = await ethers.deployContract('DummyImplementation');
+  const v2 = await ethers.deployContract('CallReceiverMock');
 
-contract('ERC1967Utils', function (accounts) {
-  const [, admin, anotherAccount] = accounts;
-  const EMPTY_DATA = '0x';
+  return { admin, anotherAccount, utils, v1, v2 };
+}
 
+describe('ERC1967Utils', function () {
   beforeEach('setup', async function () {
-    this.utils = await ERC1967Utils.new();
-    this.v1 = await V1.new();
-    this.v2 = await V2.new();
+    Object.assign(this, await loadFixture(fixture));
   });
 
   describe('IMPLEMENTATION_SLOT', function () {
     beforeEach('set v1 implementation', async function () {
-      await setSlot(this.utils, ImplementationSlot, this.v1.address);
+      await setSlot(this.utils, ImplementationSlot, this.v1);
     });
 
     describe('getImplementation', function () {
       it('returns current implementation and matches implementation slot value', async function () {
-        expect(await this.utils.$getImplementation()).to.equal(this.v1.address);
-        expect(await getAddressInSlot(this.utils.address, ImplementationSlot)).to.equal(this.v1.address);
+        expect(await this.utils.$getImplementation()).to.equal(this.v1.target);
+        expect(await getAddressInSlot(this.utils, ImplementationSlot)).to.equal(this.v1.target);
       });
     });
 
     describe('upgradeToAndCall', function () {
       it('sets implementation in storage and emits event', async function () {
-        const newImplementation = this.v2.address;
-        const receipt = await this.utils.$upgradeToAndCall(newImplementation, EMPTY_DATA);
+        const newImplementation = this.v2;
+        const tx = await this.utils.$upgradeToAndCall(newImplementation, '0x');
 
-        expect(await getAddressInSlot(this.utils.address, ImplementationSlot)).to.equal(newImplementation);
-        expectEvent(receipt, 'Upgraded', { implementation: newImplementation });
+        expect(await getAddressInSlot(this.utils, ImplementationSlot)).to.equal(newImplementation.target);
+        await expect(tx).to.emit(this.utils, 'Upgraded').withArgs(newImplementation.target);
       });
 
       it('reverts when implementation does not contain code', async function () {
-        await expectRevertCustomError(
-          this.utils.$upgradeToAndCall(anotherAccount, EMPTY_DATA),
-          'ERC1967InvalidImplementation',
-          [anotherAccount],
-        );
+        await expect(this.utils.$upgradeToAndCall(this.anotherAccount, '0x'))
+          .to.be.revertedWithCustomError(this.utils, 'ERC1967InvalidImplementation')
+          .withArgs(this.anotherAccount.address);
       });
 
       describe('when data is empty', function () {
         it('reverts when value is sent', async function () {
-          await expectRevertCustomError(
-            this.utils.$upgradeToAndCall(this.v2.address, EMPTY_DATA, { value: 1 }),
+          await expect(this.utils.$upgradeToAndCall(this.v2, '0x', { value: 1 })).to.be.revertedWithCustomError(
+            this.utils,
             'ERC1967NonPayable',
-            [],
           );
         });
       });
 
       describe('when data is not empty', function () {
         it('delegates a call to the new implementation', async function () {
-          const initializeData = this.v2.contract.methods.mockFunction().encodeABI();
-          const receipt = await this.utils.$upgradeToAndCall(this.v2.address, initializeData);
-          await expectEvent.inTransaction(receipt.tx, await V2.at(this.utils.address), 'MockFunctionCalled');
+          const initializeData = this.v2.interface.encodeFunctionData('mockFunction');
+          const tx = await this.utils.$upgradeToAndCall(this.v2, initializeData);
+          await expect(tx).to.emit(await ethers.getContractAt('CallReceiverMock', this.utils), 'MockFunctionCalled');
         });
       });
     });
@@ -72,99 +67,94 @@ contract('ERC1967Utils', function (accounts) {
 
   describe('ADMIN_SLOT', function () {
     beforeEach('set admin', async function () {
-      await setSlot(this.utils, AdminSlot, admin);
+      await setSlot(this.utils, AdminSlot, this.admin);
     });
 
     describe('getAdmin', function () {
       it('returns current admin and matches admin slot value', async function () {
-        expect(await this.utils.$getAdmin()).to.equal(admin);
-        expect(await getAddressInSlot(this.utils.address, AdminSlot)).to.equal(admin);
+        expect(await this.utils.$getAdmin()).to.equal(this.admin.address);
+        expect(await getAddressInSlot(this.utils, AdminSlot)).to.equal(this.admin.address);
       });
     });
 
     describe('changeAdmin', function () {
       it('sets admin in storage and emits event', async function () {
-        const newAdmin = anotherAccount;
-        const receipt = await this.utils.$changeAdmin(newAdmin);
+        const newAdmin = this.anotherAccount;
+        const tx = await this.utils.$changeAdmin(newAdmin);
 
-        expect(await getAddressInSlot(this.utils.address, AdminSlot)).to.equal(newAdmin);
-        expectEvent(receipt, 'AdminChanged', { previousAdmin: admin, newAdmin: newAdmin });
+        expect(await getAddressInSlot(this.utils, AdminSlot)).to.equal(newAdmin.address);
+        await expect(tx).to.emit(this.utils, 'AdminChanged').withArgs(this.admin.address, newAdmin.address);
       });
 
       it('reverts when setting the address zero as admin', async function () {
-        await expectRevertCustomError(this.utils.$changeAdmin(ZERO_ADDRESS), 'ERC1967InvalidAdmin', [ZERO_ADDRESS]);
+        await expect(this.utils.$changeAdmin(ethers.ZeroAddress))
+          .to.be.revertedWithCustomError(this.utils, 'ERC1967InvalidAdmin')
+          .withArgs(ethers.ZeroAddress);
       });
     });
   });
 
   describe('BEACON_SLOT', function () {
     beforeEach('set beacon', async function () {
-      this.beacon = await UpgradeableBeaconMock.new(this.v1.address);
-      await setSlot(this.utils, BeaconSlot, this.beacon.address);
+      this.beacon = await ethers.deployContract('UpgradeableBeaconMock', [this.v1]);
+      await setSlot(this.utils, BeaconSlot, this.beacon);
     });
 
     describe('getBeacon', function () {
       it('returns current beacon and matches beacon slot value', async function () {
-        expect(await this.utils.$getBeacon()).to.equal(this.beacon.address);
-        expect(await getAddressInSlot(this.utils.address, BeaconSlot)).to.equal(this.beacon.address);
+        expect(await this.utils.$getBeacon()).to.equal(this.beacon.target);
+        expect(await getAddressInSlot(this.utils, BeaconSlot)).to.equal(this.beacon.target);
       });
     });
 
     describe('upgradeBeaconToAndCall', function () {
       it('sets beacon in storage and emits event', async function () {
-        const newBeacon = await UpgradeableBeaconMock.new(this.v2.address);
-        const receipt = await this.utils.$upgradeBeaconToAndCall(newBeacon.address, EMPTY_DATA);
+        const newBeacon = await ethers.deployContract('UpgradeableBeaconMock', [this.v2]);
+        const tx = await this.utils.$upgradeBeaconToAndCall(newBeacon, '0x');
 
-        expect(await getAddressInSlot(this.utils.address, BeaconSlot)).to.equal(newBeacon.address);
-        expectEvent(receipt, 'BeaconUpgraded', { beacon: newBeacon.address });
+        expect(await getAddressInSlot(this.utils, BeaconSlot)).to.equal(newBeacon.target);
+        await expect(tx).to.emit(this.utils, 'BeaconUpgraded').withArgs(newBeacon.target);
       });
 
       it('reverts when beacon does not contain code', async function () {
-        await expectRevertCustomError(
-          this.utils.$upgradeBeaconToAndCall(anotherAccount, EMPTY_DATA),
-          'ERC1967InvalidBeacon',
-          [anotherAccount],
-        );
+        await expect(this.utils.$upgradeBeaconToAndCall(this.anotherAccount, '0x'))
+          .to.be.revertedWithCustomError(this.utils, 'ERC1967InvalidBeacon')
+          .withArgs(this.anotherAccount.address);
       });
 
       it("reverts when beacon's implementation does not contain code", async function () {
-        const newBeacon = await UpgradeableBeaconMock.new(anotherAccount);
+        const newBeacon = await ethers.deployContract('UpgradeableBeaconMock', [this.anotherAccount]);
 
-        await expectRevertCustomError(
-          this.utils.$upgradeBeaconToAndCall(newBeacon.address, EMPTY_DATA),
-          'ERC1967InvalidImplementation',
-          [anotherAccount],
-        );
+        await expect(this.utils.$upgradeBeaconToAndCall(newBeacon, '0x'))
+          .to.be.revertedWithCustomError(this.utils, 'ERC1967InvalidImplementation')
+          .withArgs(this.anotherAccount.address);
       });
 
       describe('when data is empty', function () {
         it('reverts when value is sent', async function () {
-          const newBeacon = await UpgradeableBeaconMock.new(this.v2.address);
-          await expectRevertCustomError(
-            this.utils.$upgradeBeaconToAndCall(newBeacon.address, EMPTY_DATA, { value: 1 }),
+          const newBeacon = await ethers.deployContract('UpgradeableBeaconMock', [this.v2]);
+          await expect(this.utils.$upgradeBeaconToAndCall(newBeacon, '0x', { value: 1 })).to.be.revertedWithCustomError(
+            this.utils,
             'ERC1967NonPayable',
-            [],
           );
         });
       });
 
       describe('when data is not empty', function () {
         it('delegates a call to the new implementation', async function () {
-          const initializeData = this.v2.contract.methods.mockFunction().encodeABI();
-          const newBeacon = await UpgradeableBeaconMock.new(this.v2.address);
-          const receipt = await this.utils.$upgradeBeaconToAndCall(newBeacon.address, initializeData);
-          await expectEvent.inTransaction(receipt.tx, await V2.at(this.utils.address), 'MockFunctionCalled');
+          const initializeData = this.v2.interface.encodeFunctionData('mockFunction');
+          const newBeacon = await ethers.deployContract('UpgradeableBeaconMock', [this.v2]);
+          const tx = await this.utils.$upgradeBeaconToAndCall(newBeacon, initializeData);
+          await expect(tx).to.emit(await ethers.getContractAt('CallReceiverMock', this.utils), 'MockFunctionCalled');
         });
       });
 
       describe('reentrant beacon implementation() call', function () {
         it('sees the new beacon implementation', async function () {
-          const newBeacon = await UpgradeableBeaconReentrantMock.new();
-          await expectRevertCustomError(
-            this.utils.$upgradeBeaconToAndCall(newBeacon.address, '0x'),
-            'BeaconProxyBeaconSlotAddress',
-            [newBeacon.address],
-          );
+          const newBeacon = await ethers.deployContract('UpgradeableBeaconReentrantMock');
+          await expect(this.utils.$upgradeBeaconToAndCall(newBeacon, '0x'))
+            .to.be.revertedWithCustomError(newBeacon, 'BeaconProxyBeaconSlotAddress')
+            .withArgs(newBeacon.target);
         });
       });
     });

+ 57 - 55
test/proxy/Proxy.behaviour.js

@@ -1,100 +1,94 @@
-const { expectRevert } = require('@openzeppelin/test-helpers');
-const { getSlot, ImplementationSlot } = require('../helpers/erc1967');
-
+const { ethers } = require('hardhat');
 const { expect } = require('chai');
-const { expectRevertCustomError } = require('../helpers/customError');
 
-const DummyImplementation = artifacts.require('DummyImplementation');
+const { getAddressInSlot, ImplementationSlot } = require('../helpers/erc1967');
 
-module.exports = function shouldBehaveLikeProxy(createProxy, accounts) {
+module.exports = function shouldBehaveLikeProxy() {
   it('cannot be initialized with a non-contract address', async function () {
-    const nonContractAddress = accounts[0];
-    const initializeData = Buffer.from('');
-    await expectRevert.unspecified(createProxy(nonContractAddress, initializeData));
-  });
-
-  before('deploy implementation', async function () {
-    this.implementation = web3.utils.toChecksumAddress((await DummyImplementation.new()).address);
+    const initializeData = '0x';
+    await expect(this.createProxy(this.nonContractAddress, initializeData))
+      .to.be.revertedWithCustomError(await ethers.getContractFactory('ERC1967Proxy'), 'ERC1967InvalidImplementation')
+      .withArgs(this.nonContractAddress.address);
   });
 
   const assertProxyInitialization = function ({ value, balance }) {
     it('sets the implementation address', async function () {
-      const implementationSlot = await getSlot(this.proxy, ImplementationSlot);
-      const implementationAddress = web3.utils.toChecksumAddress(implementationSlot.substr(-40));
-      expect(implementationAddress).to.be.equal(this.implementation);
+      expect(await getAddressInSlot(this.proxy, ImplementationSlot)).to.equal(this.implementation.target);
     });
 
     it('initializes the proxy', async function () {
-      const dummy = new DummyImplementation(this.proxy);
-      expect(await dummy.value()).to.be.bignumber.equal(value.toString());
+      const dummy = this.implementation.attach(this.proxy);
+      expect(await dummy.value()).to.equal(value);
     });
 
     it('has expected balance', async function () {
-      expect(await web3.eth.getBalance(this.proxy)).to.be.bignumber.equal(balance.toString());
+      expect(await ethers.provider.getBalance(this.proxy)).to.equal(balance);
     });
   };
 
   describe('without initialization', function () {
-    const initializeData = Buffer.from('');
+    const initializeData = '0x';
 
     describe('when not sending balance', function () {
       beforeEach('creating proxy', async function () {
-        this.proxy = (await createProxy(this.implementation, initializeData)).address;
+        this.proxy = await this.createProxy(this.implementation, initializeData);
       });
 
-      assertProxyInitialization({ value: 0, balance: 0 });
+      assertProxyInitialization({ value: 0n, balance: 0n });
     });
 
     describe('when sending some balance', function () {
-      const value = 10e5;
+      const value = 10n ** 5n;
 
       it('reverts', async function () {
-        await expectRevertCustomError(
-          createProxy(this.implementation, initializeData, { value }),
-          'ERC1967NonPayable',
-          [],
-        );
+        await expect(this.createProxy(this.implementation, initializeData, { value })).to.be.reverted;
       });
     });
   });
 
   describe('initialization without parameters', function () {
     describe('non payable', function () {
-      const expectedInitializedValue = 10;
-      const initializeData = new DummyImplementation('').contract.methods['initializeNonPayable()']().encodeABI();
+      const expectedInitializedValue = 10n;
+
+      beforeEach(function () {
+        this.initializeData = this.implementation.interface.encodeFunctionData('initializeNonPayable');
+      });
 
       describe('when not sending balance', function () {
         beforeEach('creating proxy', async function () {
-          this.proxy = (await createProxy(this.implementation, initializeData)).address;
+          this.proxy = await this.createProxy(this.implementation, this.initializeData);
         });
 
         assertProxyInitialization({
           value: expectedInitializedValue,
-          balance: 0,
+          balance: 0n,
         });
       });
 
       describe('when sending some balance', function () {
-        const value = 10e5;
+        const value = 10n ** 5n;
 
         it('reverts', async function () {
-          await expectRevert.unspecified(createProxy(this.implementation, initializeData, { value }));
+          await expect(this.createProxy(this.implementation, this.initializeData, { value })).to.be.reverted;
         });
       });
     });
 
     describe('payable', function () {
-      const expectedInitializedValue = 100;
-      const initializeData = new DummyImplementation('').contract.methods['initializePayable()']().encodeABI();
+      const expectedInitializedValue = 100n;
+
+      beforeEach(function () {
+        this.initializeData = this.implementation.interface.encodeFunctionData('initializePayable');
+      });
 
       describe('when not sending balance', function () {
         beforeEach('creating proxy', async function () {
-          this.proxy = (await createProxy(this.implementation, initializeData)).address;
+          this.proxy = await this.createProxy(this.implementation, this.initializeData);
         });
 
         assertProxyInitialization({
           value: expectedInitializedValue,
-          balance: 0,
+          balance: 0n,
         });
       });
 
@@ -102,7 +96,7 @@ module.exports = function shouldBehaveLikeProxy(createProxy, accounts) {
         const value = 10e5;
 
         beforeEach('creating proxy', async function () {
-          this.proxy = (await createProxy(this.implementation, initializeData, { value })).address;
+          this.proxy = await this.createProxy(this.implementation, this.initializeData, { value });
         });
 
         assertProxyInitialization({
@@ -115,14 +109,17 @@ module.exports = function shouldBehaveLikeProxy(createProxy, accounts) {
 
   describe('initialization with parameters', function () {
     describe('non payable', function () {
-      const expectedInitializedValue = 10;
-      const initializeData = new DummyImplementation('').contract.methods
-        .initializeNonPayableWithValue(expectedInitializedValue)
-        .encodeABI();
+      const expectedInitializedValue = 10n;
+
+      beforeEach(function () {
+        this.initializeData = this.implementation.interface.encodeFunctionData('initializeNonPayableWithValue', [
+          expectedInitializedValue,
+        ]);
+      });
 
       describe('when not sending balance', function () {
         beforeEach('creating proxy', async function () {
-          this.proxy = (await createProxy(this.implementation, initializeData)).address;
+          this.proxy = await this.createProxy(this.implementation, this.initializeData);
         });
 
         assertProxyInitialization({
@@ -135,33 +132,36 @@ module.exports = function shouldBehaveLikeProxy(createProxy, accounts) {
         const value = 10e5;
 
         it('reverts', async function () {
-          await expectRevert.unspecified(createProxy(this.implementation, initializeData, { value }));
+          await expect(this.createProxy(this.implementation, this.initializeData, { value })).to.be.reverted;
         });
       });
     });
 
     describe('payable', function () {
-      const expectedInitializedValue = 42;
-      const initializeData = new DummyImplementation('').contract.methods
-        .initializePayableWithValue(expectedInitializedValue)
-        .encodeABI();
+      const expectedInitializedValue = 42n;
+
+      beforeEach(function () {
+        this.initializeData = this.implementation.interface.encodeFunctionData('initializePayableWithValue', [
+          expectedInitializedValue,
+        ]);
+      });
 
       describe('when not sending balance', function () {
         beforeEach('creating proxy', async function () {
-          this.proxy = (await createProxy(this.implementation, initializeData)).address;
+          this.proxy = await this.createProxy(this.implementation, this.initializeData);
         });
 
         assertProxyInitialization({
           value: expectedInitializedValue,
-          balance: 0,
+          balance: 0n,
         });
       });
 
       describe('when sending some balance', function () {
-        const value = 10e5;
+        const value = 10n ** 5n;
 
         beforeEach('creating proxy', async function () {
-          this.proxy = (await createProxy(this.implementation, initializeData, { value })).address;
+          this.proxy = await this.createProxy(this.implementation, this.initializeData, { value });
         });
 
         assertProxyInitialization({
@@ -172,10 +172,12 @@ module.exports = function shouldBehaveLikeProxy(createProxy, accounts) {
     });
 
     describe('reverting initialization', function () {
-      const initializeData = new DummyImplementation('').contract.methods.reverts().encodeABI();
+      beforeEach(function () {
+        this.initializeData = this.implementation.interface.encodeFunctionData('reverts');
+      });
 
       it('reverts', async function () {
-        await expectRevert(createProxy(this.implementation, initializeData), 'DummyImplementation reverted');
+        await expect(this.createProxy(this.implementation, this.initializeData)).to.be.reverted;
       });
     });
   });

+ 88 - 102
test/proxy/beacon/BeaconProxy.test.js

@@ -1,152 +1,138 @@
-const { expectRevert } = require('@openzeppelin/test-helpers');
-const { getSlot, BeaconSlot } = require('../../helpers/erc1967');
+const { ethers } = require('hardhat');
+const { expect } = require('chai');
+const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
+const { getAddressInSlot, BeaconSlot } = require('../../helpers/erc1967');
 
-const { expectRevertCustomError } = require('../../helpers/customError');
+async function fixture() {
+  const [admin, other] = await ethers.getSigners();
 
-const { expect } = require('chai');
+  const v1 = await ethers.deployContract('DummyImplementation');
+  const v2 = await ethers.deployContract('DummyImplementationV2');
+  const factory = await ethers.getContractFactory('BeaconProxy');
+  const beacon = await ethers.deployContract('UpgradeableBeacon', [v1, admin]);
+
+  const newBeaconProxy = (beacon, data, opts = {}) => factory.deploy(beacon, data, opts);
 
-const UpgradeableBeacon = artifacts.require('UpgradeableBeacon');
-const BeaconProxy = artifacts.require('BeaconProxy');
-const DummyImplementation = artifacts.require('DummyImplementation');
-const DummyImplementationV2 = artifacts.require('DummyImplementationV2');
-const BadBeaconNoImpl = artifacts.require('BadBeaconNoImpl');
-const BadBeaconNotContract = artifacts.require('BadBeaconNotContract');
+  return { admin, other, factory, beacon, v1, v2, newBeaconProxy };
+}
 
-contract('BeaconProxy', function (accounts) {
-  const [upgradeableBeaconAdmin, anotherAccount] = accounts;
+describe('BeaconProxy', function () {
+  beforeEach(async function () {
+    Object.assign(this, await loadFixture(fixture));
+  });
 
   describe('bad beacon is not accepted', async function () {
     it('non-contract beacon', async function () {
-      await expectRevertCustomError(BeaconProxy.new(anotherAccount, '0x'), 'ERC1967InvalidBeacon', [anotherAccount]);
+      const notBeacon = this.other;
+
+      await expect(this.newBeaconProxy(notBeacon, '0x'))
+        .to.be.revertedWithCustomError(this.factory, 'ERC1967InvalidBeacon')
+        .withArgs(notBeacon.address);
     });
 
     it('non-compliant beacon', async function () {
-      const beacon = await BadBeaconNoImpl.new();
-      await expectRevert.unspecified(BeaconProxy.new(beacon.address, '0x'));
+      const badBeacon = await ethers.deployContract('BadBeaconNoImpl');
+
+      await expect(this.newBeaconProxy(badBeacon, '0x')).to.be.revertedWithoutReason;
     });
 
     it('non-contract implementation', async function () {
-      const beacon = await BadBeaconNotContract.new();
-      const implementation = await beacon.implementation();
-      await expectRevertCustomError(BeaconProxy.new(beacon.address, '0x'), 'ERC1967InvalidImplementation', [
-        implementation,
-      ]);
-    });
-  });
+      const badBeacon = await ethers.deployContract('BadBeaconNotContract');
 
-  before('deploy implementation', async function () {
-    this.implementationV0 = await DummyImplementation.new();
-    this.implementationV1 = await DummyImplementationV2.new();
+      await expect(this.newBeaconProxy(badBeacon, '0x'))
+        .to.be.revertedWithCustomError(this.factory, 'ERC1967InvalidImplementation')
+        .withArgs(await badBeacon.implementation());
+    });
   });
 
   describe('initialization', function () {
-    before(function () {
-      this.assertInitialized = async ({ value, balance }) => {
-        const beaconSlot = await getSlot(this.proxy, BeaconSlot);
-        const beaconAddress = web3.utils.toChecksumAddress(beaconSlot.substr(-40));
-        expect(beaconAddress).to.equal(this.beacon.address);
+    async function assertInitialized({ value, balance }) {
+      const beaconAddress = await getAddressInSlot(this.proxy, BeaconSlot);
+      expect(beaconAddress).to.equal(this.beacon.target);
 
-        const dummy = new DummyImplementation(this.proxy.address);
-        expect(await dummy.value()).to.bignumber.eq(value);
-
-        expect(await web3.eth.getBalance(this.proxy.address)).to.bignumber.eq(balance);
-      };
-    });
+      const dummy = this.v1.attach(this.proxy);
+      expect(await dummy.value()).to.equal(value);
 
-    beforeEach('deploy beacon', async function () {
-      this.beacon = await UpgradeableBeacon.new(this.implementationV0.address, upgradeableBeaconAdmin);
-    });
+      expect(await ethers.provider.getBalance(this.proxy)).to.equal(balance);
+    }
 
     it('no initialization', async function () {
-      const data = Buffer.from('');
-      this.proxy = await BeaconProxy.new(this.beacon.address, data);
-      await this.assertInitialized({ value: '0', balance: '0' });
+      this.proxy = await this.newBeaconProxy(this.beacon, '0x');
+      await assertInitialized.bind(this)({ value: 0n, balance: 0n });
     });
 
     it('non-payable initialization', async function () {
-      const value = '55';
-      const data = this.implementationV0.contract.methods.initializeNonPayableWithValue(value).encodeABI();
-      this.proxy = await BeaconProxy.new(this.beacon.address, data);
-      await this.assertInitialized({ value, balance: '0' });
+      const value = 55n;
+      const data = this.v1.interface.encodeFunctionData('initializeNonPayableWithValue', [value]);
+
+      this.proxy = await this.newBeaconProxy(this.beacon, data);
+      await assertInitialized.bind(this)({ value, balance: 0n });
     });
 
     it('payable initialization', async function () {
-      const value = '55';
-      const data = this.implementationV0.contract.methods.initializePayableWithValue(value).encodeABI();
-      const balance = '100';
-      this.proxy = await BeaconProxy.new(this.beacon.address, data, { value: balance });
-      await this.assertInitialized({ value, balance });
+      const value = 55n;
+      const data = this.v1.interface.encodeFunctionData('initializePayableWithValue', [value]);
+      const balance = 100n;
+
+      this.proxy = await this.newBeaconProxy(this.beacon, data, { value: balance });
+      await assertInitialized.bind(this)({ value, balance });
     });
 
     it('reverting initialization due to value', async function () {
-      const data = Buffer.from('');
-      await expectRevertCustomError(
-        BeaconProxy.new(this.beacon.address, data, { value: '1' }),
+      await expect(this.newBeaconProxy(this.beacon, '0x', { value: 1n })).to.be.revertedWithCustomError(
+        this.factory,
         'ERC1967NonPayable',
-        [],
       );
     });
 
     it('reverting initialization function', async function () {
-      const data = this.implementationV0.contract.methods.reverts().encodeABI();
-      await expectRevert(BeaconProxy.new(this.beacon.address, data), 'DummyImplementation reverted');
+      const data = this.v1.interface.encodeFunctionData('reverts');
+      await expect(this.newBeaconProxy(this.beacon, data)).to.be.revertedWith('DummyImplementation reverted');
     });
   });
 
-  it('upgrade a proxy by upgrading its beacon', async function () {
-    const beacon = await UpgradeableBeacon.new(this.implementationV0.address, upgradeableBeaconAdmin);
+  describe('upgrade', async function () {
+    it('upgrade a proxy by upgrading its beacon', async function () {
+      const value = 10n;
+      const data = this.v1.interface.encodeFunctionData('initializeNonPayableWithValue', [value]);
+      const proxy = await this.newBeaconProxy(this.beacon, data).then(instance => this.v1.attach(instance));
 
-    const value = '10';
-    const data = this.implementationV0.contract.methods.initializeNonPayableWithValue(value).encodeABI();
-    const proxy = await BeaconProxy.new(beacon.address, data);
+      // test initial values
+      expect(await proxy.value()).to.equal(value);
 
-    const dummy = new DummyImplementation(proxy.address);
+      // test initial version
+      expect(await proxy.version()).to.equal('V1');
 
-    // test initial values
-    expect(await dummy.value()).to.bignumber.eq(value);
+      // upgrade beacon
+      await this.beacon.connect(this.admin).upgradeTo(this.v2);
 
-    // test initial version
-    expect(await dummy.version()).to.eq('V1');
-
-    // upgrade beacon
-    await beacon.upgradeTo(this.implementationV1.address, { from: upgradeableBeaconAdmin });
-
-    // test upgraded version
-    expect(await dummy.version()).to.eq('V2');
-  });
-
-  it('upgrade 2 proxies by upgrading shared beacon', async function () {
-    const value1 = '10';
-    const value2 = '42';
-
-    const beacon = await UpgradeableBeacon.new(this.implementationV0.address, upgradeableBeaconAdmin);
-
-    const proxy1InitializeData = this.implementationV0.contract.methods
-      .initializeNonPayableWithValue(value1)
-      .encodeABI();
-    const proxy1 = await BeaconProxy.new(beacon.address, proxy1InitializeData);
+      // test upgraded version
+      expect(await proxy.version()).to.equal('V2');
+    });
 
-    const proxy2InitializeData = this.implementationV0.contract.methods
-      .initializeNonPayableWithValue(value2)
-      .encodeABI();
-    const proxy2 = await BeaconProxy.new(beacon.address, proxy2InitializeData);
+    it('upgrade 2 proxies by upgrading shared beacon', async function () {
+      const value1 = 10n;
+      const data1 = this.v1.interface.encodeFunctionData('initializeNonPayableWithValue', [value1]);
+      const proxy1 = await this.newBeaconProxy(this.beacon, data1).then(instance => this.v1.attach(instance));
 
-    const dummy1 = new DummyImplementation(proxy1.address);
-    const dummy2 = new DummyImplementation(proxy2.address);
+      const value2 = 42n;
+      const data2 = this.v1.interface.encodeFunctionData('initializeNonPayableWithValue', [value2]);
+      const proxy2 = await this.newBeaconProxy(this.beacon, data2).then(instance => this.v1.attach(instance));
 
-    // test initial values
-    expect(await dummy1.value()).to.bignumber.eq(value1);
-    expect(await dummy2.value()).to.bignumber.eq(value2);
+      // test initial values
+      expect(await proxy1.value()).to.equal(value1);
+      expect(await proxy2.value()).to.equal(value2);
 
-    // test initial version
-    expect(await dummy1.version()).to.eq('V1');
-    expect(await dummy2.version()).to.eq('V1');
+      // test initial version
+      expect(await proxy1.version()).to.equal('V1');
+      expect(await proxy2.version()).to.equal('V1');
 
-    // upgrade beacon
-    await beacon.upgradeTo(this.implementationV1.address, { from: upgradeableBeaconAdmin });
+      // upgrade beacon
+      await this.beacon.connect(this.admin).upgradeTo(this.v2);
 
-    // test upgraded version
-    expect(await dummy1.version()).to.eq('V2');
-    expect(await dummy2.version()).to.eq('V2');
+      // test upgraded version
+      expect(await proxy1.version()).to.equal('V2');
+      expect(await proxy2.version()).to.equal('V2');
+    });
   });
 });

+ 32 - 31
test/proxy/beacon/UpgradeableBeacon.test.js

@@ -1,54 +1,55 @@
-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 [admin, other] = await ethers.getSigners();
 
-const UpgradeableBeacon = artifacts.require('UpgradeableBeacon');
-const Implementation1 = artifacts.require('Implementation1');
-const Implementation2 = artifacts.require('Implementation2');
+  const v1 = await ethers.deployContract('Implementation1');
+  const v2 = await ethers.deployContract('Implementation2');
+  const beacon = await ethers.deployContract('UpgradeableBeacon', [v1, admin]);
 
-contract('UpgradeableBeacon', function (accounts) {
-  const [owner, other] = accounts;
+  return { admin, other, beacon, v1, v2 };
+}
 
-  it('cannot be created with non-contract implementation', async function () {
-    await expectRevertCustomError(UpgradeableBeacon.new(other, owner), 'BeaconInvalidImplementation', [other]);
+describe('UpgradeableBeacon', function () {
+  beforeEach(async function () {
+    Object.assign(this, await loadFixture(fixture));
   });
 
-  context('once deployed', async function () {
-    beforeEach('deploying beacon', async function () {
-      this.v1 = await Implementation1.new();
-      this.beacon = await UpgradeableBeacon.new(this.v1.address, owner);
-    });
+  it('cannot be created with non-contract implementation', async function () {
+    await expect(ethers.deployContract('UpgradeableBeacon', [this.other, this.admin]))
+      .to.be.revertedWithCustomError(this.beacon, 'BeaconInvalidImplementation')
+      .withArgs(this.other.address);
+  });
 
+  describe('once deployed', async function () {
     it('emits Upgraded event to the first implementation', async function () {
-      const beacon = await UpgradeableBeacon.new(this.v1.address, owner);
-      await expectEvent.inTransaction(beacon.contract.transactionHash, beacon, 'Upgraded', {
-        implementation: this.v1.address,
-      });
+      await expect(this.beacon.deploymentTransaction()).to.emit(this.beacon, 'Upgraded').withArgs(this.v1.target);
     });
 
     it('returns implementation', async function () {
-      expect(await this.beacon.implementation()).to.equal(this.v1.address);
+      expect(await this.beacon.implementation()).to.equal(this.v1.target);
     });
 
-    it('can be upgraded by the owner', async function () {
-      const v2 = await Implementation2.new();
-      const receipt = await this.beacon.upgradeTo(v2.address, { from: owner });
-      expectEvent(receipt, 'Upgraded', { implementation: v2.address });
-      expect(await this.beacon.implementation()).to.equal(v2.address);
+    it('can be upgraded by the admin', async function () {
+      await expect(this.beacon.connect(this.admin).upgradeTo(this.v2))
+        .to.emit(this.beacon, 'Upgraded')
+        .withArgs(this.v2.target);
+
+      expect(await this.beacon.implementation()).to.equal(this.v2.target);
     });
 
     it('cannot be upgraded to a non-contract', async function () {
-      await expectRevertCustomError(this.beacon.upgradeTo(other, { from: owner }), 'BeaconInvalidImplementation', [
-        other,
-      ]);
+      await expect(this.beacon.connect(this.admin).upgradeTo(this.other))
+        .to.be.revertedWithCustomError(this.beacon, 'BeaconInvalidImplementation')
+        .withArgs(this.other.address);
     });
 
     it('cannot be upgraded by other account', async function () {
-      const v2 = await Implementation2.new();
-      await expectRevertCustomError(this.beacon.upgradeTo(v2.address, { from: other }), 'OwnableUnauthorizedAccount', [
-        other,
-      ]);
+      await expect(this.beacon.connect(this.other).upgradeTo(this.v2))
+        .to.be.revertedWithCustomError(this.beacon, 'OwnableUnauthorizedAccount')
+        .withArgs(this.other.address);
     });
   });
 });

+ 33 - 55
test/proxy/transparent/ProxyAdmin.test.js

@@ -1,36 +1,33 @@
 const { ethers } = require('hardhat');
-const { expectRevert } = require('@openzeppelin/test-helpers');
 const { expect } = require('chai');
-const ImplV1 = artifacts.require('DummyImplementation');
-const ImplV2 = artifacts.require('DummyImplementationV2');
-const ProxyAdmin = artifacts.require('ProxyAdmin');
-const TransparentUpgradeableProxy = artifacts.require('TransparentUpgradeableProxy');
-const ITransparentUpgradeableProxy = artifacts.require('ITransparentUpgradeableProxy');
-
+const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
 const { getAddressInSlot, ImplementationSlot } = require('../../helpers/erc1967');
-const { expectRevertCustomError } = require('../../helpers/customError');
 
-contract('ProxyAdmin', function (accounts) {
-  const [proxyAdminOwner, anotherAccount] = accounts;
+async function fixture() {
+  const [admin, other] = await ethers.getSigners();
 
-  before('set implementations', async function () {
-    this.implementationV1 = await ImplV1.new();
-    this.implementationV2 = await ImplV2.new();
-  });
+  const v1 = await ethers.deployContract('DummyImplementation');
+  const v2 = await ethers.deployContract('DummyImplementationV2');
 
-  beforeEach(async function () {
-    const initializeData = Buffer.from('');
-    const proxy = await TransparentUpgradeableProxy.new(this.implementationV1.address, proxyAdminOwner, initializeData);
+  const proxy = await ethers
+    .deployContract('TransparentUpgradeableProxy', [v1, admin, '0x'])
+    .then(instance => ethers.getContractAt('ITransparentUpgradeableProxy', instance));
+
+  const proxyAdmin = await ethers.getContractAt(
+    'ProxyAdmin',
+    ethers.getCreateAddress({ from: proxy.target, nonce: 1n }),
+  );
 
-    const proxyNonce = await web3.eth.getTransactionCount(proxy.address);
-    const proxyAdminAddress = ethers.getCreateAddress({ from: proxy.address, nonce: proxyNonce - 1 }); // Nonce already used
-    this.proxyAdmin = await ProxyAdmin.at(proxyAdminAddress);
+  return { admin, other, v1, v2, proxy, proxyAdmin };
+}
 
-    this.proxy = await ITransparentUpgradeableProxy.at(proxy.address);
+describe('ProxyAdmin', function () {
+  beforeEach(async function () {
+    Object.assign(this, await loadFixture(fixture));
   });
 
   it('has an owner', async function () {
-    expect(await this.proxyAdmin.owner()).to.equal(proxyAdminOwner);
+    expect(await this.proxyAdmin.owner()).to.equal(this.admin.address);
   });
 
   it('has an interface version', async function () {
@@ -40,24 +37,16 @@ contract('ProxyAdmin', function (accounts) {
   describe('without data', function () {
     context('with unauthorized account', function () {
       it('fails to upgrade', async function () {
-        await expectRevertCustomError(
-          this.proxyAdmin.upgradeAndCall(this.proxy.address, this.implementationV2.address, '0x', {
-            from: anotherAccount,
-          }),
-          'OwnableUnauthorizedAccount',
-          [anotherAccount],
-        );
+        await expect(this.proxyAdmin.connect(this.other).upgradeAndCall(this.proxy, this.v2, '0x'))
+          .to.be.revertedWithCustomError(this.proxyAdmin, 'OwnableUnauthorizedAccount')
+          .withArgs(this.other.address);
       });
     });
 
     context('with authorized account', function () {
       it('upgrades implementation', async function () {
-        await this.proxyAdmin.upgradeAndCall(this.proxy.address, this.implementationV2.address, '0x', {
-          from: proxyAdminOwner,
-        });
-
-        const implementationAddress = await getAddressInSlot(this.proxy, ImplementationSlot);
-        expect(implementationAddress).to.be.equal(this.implementationV2.address);
+        await this.proxyAdmin.connect(this.admin).upgradeAndCall(this.proxy, this.v2, '0x');
+        expect(await getAddressInSlot(this.proxy, ImplementationSlot)).to.be.equal(this.v2.target);
       });
     });
   });
@@ -65,37 +54,26 @@ contract('ProxyAdmin', function (accounts) {
   describe('with data', function () {
     context('with unauthorized account', function () {
       it('fails to upgrade', async function () {
-        const callData = new ImplV1('').contract.methods.initializeNonPayableWithValue(1337).encodeABI();
-        await expectRevertCustomError(
-          this.proxyAdmin.upgradeAndCall(this.proxy.address, this.implementationV2.address, callData, {
-            from: anotherAccount,
-          }),
-          'OwnableUnauthorizedAccount',
-          [anotherAccount],
-        );
+        const data = this.v1.interface.encodeFunctionData('initializeNonPayableWithValue', [1337n]);
+        await expect(this.proxyAdmin.connect(this.other).upgradeAndCall(this.proxy, this.v2, data))
+          .to.be.revertedWithCustomError(this.proxyAdmin, 'OwnableUnauthorizedAccount')
+          .withArgs(this.other.address);
       });
     });
 
     context('with authorized account', function () {
       context('with invalid callData', function () {
         it('fails to upgrade', async function () {
-          const callData = '0x12345678';
-          await expectRevert.unspecified(
-            this.proxyAdmin.upgradeAndCall(this.proxy.address, this.implementationV2.address, callData, {
-              from: proxyAdminOwner,
-            }),
-          );
+          const data = '0x12345678';
+          await expect(this.proxyAdmin.connect(this.admin).upgradeAndCall(this.proxy, this.v2, data)).to.be.reverted;
         });
       });
 
       context('with valid callData', function () {
         it('upgrades implementation', async function () {
-          const callData = new ImplV1('').contract.methods.initializeNonPayableWithValue(1337).encodeABI();
-          await this.proxyAdmin.upgradeAndCall(this.proxy.address, this.implementationV2.address, callData, {
-            from: proxyAdminOwner,
-          });
-          const implementationAddress = await getAddressInSlot(this.proxy, ImplementationSlot);
-          expect(implementationAddress).to.be.equal(this.implementationV2.address);
+          const data = this.v2.interface.encodeFunctionData('initializeNonPayableWithValue', [1337n]);
+          await this.proxyAdmin.connect(this.admin).upgradeAndCall(this.proxy, this.v2, data);
+          expect(await getAddressInSlot(this.proxy, ImplementationSlot)).to.be.equal(this.v2.target);
         });
       });
     });

+ 183 - 238
test/proxy/transparent/TransparentUpgradeableProxy.behaviour.js

@@ -1,261 +1,223 @@
-const { BN, expectRevert, expectEvent, constants } = require('@openzeppelin/test-helpers');
-const { ZERO_ADDRESS } = constants;
-const { getAddressInSlot, ImplementationSlot, AdminSlot } = require('../../helpers/erc1967');
-const { expectRevertCustomError } = require('../../helpers/customError');
-
+const { ethers } = require('hardhat');
 const { expect } = require('chai');
-const { ethers, web3 } = require('hardhat');
+
+const { getAddressInSlot, ImplementationSlot, AdminSlot } = require('../../helpers/erc1967');
 const { impersonate } = require('../../helpers/account');
 
-const Implementation1 = artifacts.require('Implementation1');
-const Implementation2 = artifacts.require('Implementation2');
-const Implementation3 = artifacts.require('Implementation3');
-const Implementation4 = artifacts.require('Implementation4');
-const MigratableMockV1 = artifacts.require('MigratableMockV1');
-const MigratableMockV2 = artifacts.require('MigratableMockV2');
-const MigratableMockV3 = artifacts.require('MigratableMockV3');
-const InitializableMock = artifacts.require('InitializableMock');
-const DummyImplementation = artifacts.require('DummyImplementation');
-const ClashingImplementation = artifacts.require('ClashingImplementation');
-const Ownable = artifacts.require('Ownable');
-
-module.exports = function shouldBehaveLikeTransparentUpgradeableProxy(createProxy, initialOwner, accounts) {
-  const [anotherAccount] = accounts;
-
-  async function createProxyWithImpersonatedProxyAdmin(logic, initData, opts = undefined) {
-    const proxy = await createProxy(logic, initData, opts);
-
-    // Expect proxy admin to be the first and only contract created by the proxy
-    const proxyAdminAddress = ethers.getCreateAddress({ from: proxy.address, nonce: 1 });
-    await impersonate(proxyAdminAddress);
-
-    return {
-      proxy,
-      proxyAdminAddress,
+// createProxy, initialOwner, accounts
+module.exports = function shouldBehaveLikeTransparentUpgradeableProxy() {
+  before(async function () {
+    const implementationV0 = await ethers.deployContract('DummyImplementation');
+    const implementationV1 = await ethers.deployContract('DummyImplementation');
+
+    const createProxyWithImpersonatedProxyAdmin = async (logic, initData, opts = undefined) => {
+      const [proxy, tx] = await this.createProxy(logic, initData, opts).then(instance =>
+        Promise.all([ethers.getContractAt('ITransparentUpgradeableProxy', instance), instance.deploymentTransaction()]),
+      );
+
+      const proxyAdmin = await ethers.getContractAt(
+        'ProxyAdmin',
+        ethers.getCreateAddress({ from: proxy.target, nonce: 1n }),
+      );
+      const proxyAdminAsSigner = await proxyAdmin.getAddress().then(impersonate);
+
+      return {
+        instance: logic.attach(proxy.target), // attaching proxy directly works well for everything except for event resolution
+        proxy,
+        proxyAdmin,
+        proxyAdminAsSigner,
+        tx,
+      };
     };
-  }
 
-  before(async function () {
-    this.implementationV0 = (await DummyImplementation.new()).address;
-    this.implementationV1 = (await DummyImplementation.new()).address;
+    Object.assign(this, {
+      implementationV0,
+      implementationV1,
+      createProxyWithImpersonatedProxyAdmin,
+    });
   });
 
   beforeEach(async function () {
-    const initializeData = Buffer.from('');
-    const { proxy, proxyAdminAddress } = await createProxyWithImpersonatedProxyAdmin(
-      this.implementationV0,
-      initializeData,
-    );
-    this.proxy = proxy;
-    this.proxyAdminAddress = proxyAdminAddress;
+    Object.assign(this, await this.createProxyWithImpersonatedProxyAdmin(this.implementationV0, '0x'));
   });
 
   describe('implementation', function () {
     it('returns the current implementation address', async function () {
-      const implementationAddress = await getAddressInSlot(this.proxy, ImplementationSlot);
-      expect(implementationAddress).to.be.equal(this.implementationV0);
+      expect(await getAddressInSlot(this.proxy, ImplementationSlot)).to.equal(this.implementationV0.target);
     });
 
     it('delegates to the implementation', async function () {
-      const dummy = new DummyImplementation(this.proxy.address);
-      const value = await dummy.get();
-
-      expect(value).to.equal(true);
+      expect(await this.instance.get()).to.be.true;
     });
   });
 
   describe('proxy admin', function () {
     it('emits AdminChanged event during construction', async function () {
-      await expectEvent.inConstruction(this.proxy, 'AdminChanged', {
-        previousAdmin: ZERO_ADDRESS,
-        newAdmin: this.proxyAdminAddress,
-      });
+      await expect(this.tx).to.emit(this.proxy, 'AdminChanged').withArgs(ethers.ZeroAddress, this.proxyAdmin.target);
     });
 
     it('sets the proxy admin in storage with the correct initial owner', async function () {
-      expect(await getAddressInSlot(this.proxy, AdminSlot)).to.be.equal(this.proxyAdminAddress);
-      const proxyAdmin = await Ownable.at(this.proxyAdminAddress);
-      expect(await proxyAdmin.owner()).to.be.equal(initialOwner);
+      expect(await getAddressInSlot(this.proxy, AdminSlot)).to.equal(this.proxyAdmin.target);
+
+      expect(await this.proxyAdmin.owner()).to.equal(this.owner.address);
     });
 
     it('can overwrite the admin by the implementation', async function () {
-      const dummy = new DummyImplementation(this.proxy.address);
-      await dummy.unsafeOverrideAdmin(anotherAccount);
+      await this.instance.unsafeOverrideAdmin(this.other);
+
       const ERC1967AdminSlotValue = await getAddressInSlot(this.proxy, AdminSlot);
-      expect(ERC1967AdminSlotValue).to.be.equal(anotherAccount);
+      expect(ERC1967AdminSlotValue).to.equal(this.other.address);
+      expect(ERC1967AdminSlotValue).to.not.equal(this.proxyAdmin.address);
 
       // Still allows previous admin to execute admin operations
-      expect(ERC1967AdminSlotValue).to.not.equal(this.proxyAdminAddress);
-      expectEvent(
-        await this.proxy.upgradeToAndCall(this.implementationV1, '0x', { from: this.proxyAdminAddress }),
-        'Upgraded',
-        {
-          implementation: this.implementationV1,
-        },
-      );
+      await expect(this.proxy.connect(this.proxyAdminAsSigner).upgradeToAndCall(this.implementationV1, '0x'))
+        .to.emit(this.proxy, 'Upgraded')
+        .withArgs(this.implementationV1.target);
     });
   });
 
   describe('upgradeToAndCall', function () {
     describe('without migrations', function () {
       beforeEach(async function () {
-        this.behavior = await InitializableMock.new();
+        this.behavior = await ethers.deployContract('InitializableMock');
       });
 
       describe('when the call does not fail', function () {
-        const initializeData = new InitializableMock('').contract.methods['initializeWithX(uint256)'](42).encodeABI();
+        beforeEach(function () {
+          this.initializeData = this.behavior.interface.encodeFunctionData('initializeWithX', [42n]);
+        });
 
         describe('when the sender is the admin', function () {
-          const value = 1e5;
+          const value = 10n ** 5n;
 
           beforeEach(async function () {
-            this.receipt = await this.proxy.upgradeToAndCall(this.behavior.address, initializeData, {
-              from: this.proxyAdminAddress,
-              value,
-            });
+            this.tx = await this.proxy
+              .connect(this.proxyAdminAsSigner)
+              .upgradeToAndCall(this.behavior, this.initializeData, {
+                value,
+              });
           });
 
           it('upgrades to the requested implementation', async function () {
-            const implementationAddress = await getAddressInSlot(this.proxy, ImplementationSlot);
-            expect(implementationAddress).to.be.equal(this.behavior.address);
+            expect(await getAddressInSlot(this.proxy, ImplementationSlot)).to.equal(this.behavior.target);
           });
 
-          it('emits an event', function () {
-            expectEvent(this.receipt, 'Upgraded', { implementation: this.behavior.address });
+          it('emits an event', async function () {
+            await expect(this.tx).to.emit(this.proxy, 'Upgraded').withArgs(this.behavior.target);
           });
 
           it('calls the initializer function', async function () {
-            const migratable = new InitializableMock(this.proxy.address);
-            const x = await migratable.x();
-            expect(x).to.be.bignumber.equal('42');
+            expect(await this.behavior.attach(this.proxy).x()).to.equal(42n);
           });
 
           it('sends given value to the proxy', async function () {
-            const balance = await web3.eth.getBalance(this.proxy.address);
-            expect(balance.toString()).to.be.bignumber.equal(value.toString());
+            expect(await ethers.provider.getBalance(this.proxy)).to.equal(value);
           });
 
           it('uses the storage of the proxy', async function () {
             // storage layout should look as follows:
             //  - 0: Initializable storage ++ initializerRan ++ onlyInitializingRan
             //  - 1: x
-            const storedValue = await web3.eth.getStorageAt(this.proxy.address, 1);
-            expect(parseInt(storedValue)).to.eq(42);
+            expect(await ethers.provider.getStorage(this.proxy, 1n)).to.equal(42n);
           });
         });
 
         describe('when the sender is not the admin', function () {
           it('reverts', async function () {
-            await expectRevert.unspecified(
-              this.proxy.upgradeToAndCall(this.behavior.address, initializeData, { from: anotherAccount }),
-            );
+            await expect(this.proxy.connect(this.other).upgradeToAndCall(this.behavior, this.initializeData)).to.be
+              .reverted;
           });
         });
       });
 
       describe('when the call does fail', function () {
-        const initializeData = new InitializableMock('').contract.methods.fail().encodeABI();
+        beforeEach(function () {
+          this.initializeData = this.behavior.interface.encodeFunctionData('fail');
+        });
 
         it('reverts', async function () {
-          await expectRevert.unspecified(
-            this.proxy.upgradeToAndCall(this.behavior.address, initializeData, { from: this.proxyAdminAddress }),
-          );
+          await expect(this.proxy.connect(this.proxyAdminAsSigner).upgradeToAndCall(this.behavior, this.initializeData))
+            .to.be.reverted;
         });
       });
     });
 
     describe('with migrations', function () {
       describe('when the sender is the admin', function () {
-        const value = 1e5;
+        const value = 10n ** 5n;
 
         describe('when upgrading to V1', function () {
-          const v1MigrationData = new MigratableMockV1('').contract.methods.initialize(42).encodeABI();
-
           beforeEach(async function () {
-            this.behaviorV1 = await MigratableMockV1.new();
-            this.balancePreviousV1 = new BN(await web3.eth.getBalance(this.proxy.address));
-            this.receipt = await this.proxy.upgradeToAndCall(this.behaviorV1.address, v1MigrationData, {
-              from: this.proxyAdminAddress,
-              value,
-            });
+            this.behaviorV1 = await ethers.deployContract('MigratableMockV1');
+            const v1MigrationData = this.behaviorV1.interface.encodeFunctionData('initialize', [42n]);
+
+            this.balancePreviousV1 = await ethers.provider.getBalance(this.proxy);
+            this.tx = await this.proxy
+              .connect(this.proxyAdminAsSigner)
+              .upgradeToAndCall(this.behaviorV1, v1MigrationData, {
+                value,
+              });
           });
 
           it('upgrades to the requested version and emits an event', async function () {
-            const implementation = await getAddressInSlot(this.proxy, ImplementationSlot);
-            expect(implementation).to.be.equal(this.behaviorV1.address);
-            expectEvent(this.receipt, 'Upgraded', { implementation: this.behaviorV1.address });
+            expect(await getAddressInSlot(this.proxy, ImplementationSlot)).to.equal(this.behaviorV1.target);
+
+            await expect(this.tx).to.emit(this.proxy, 'Upgraded').withArgs(this.behaviorV1.target);
           });
 
           it("calls the 'initialize' function and sends given value to the proxy", async function () {
-            const migratable = new MigratableMockV1(this.proxy.address);
-
-            const x = await migratable.x();
-            expect(x).to.be.bignumber.equal('42');
-
-            const balance = await web3.eth.getBalance(this.proxy.address);
-            expect(new BN(balance)).to.be.bignumber.equal(this.balancePreviousV1.addn(value));
+            expect(await this.behaviorV1.attach(this.proxy).x()).to.equal(42n);
+            expect(await ethers.provider.getBalance(this.proxy)).to.equal(this.balancePreviousV1 + value);
           });
 
           describe('when upgrading to V2', function () {
-            const v2MigrationData = new MigratableMockV2('').contract.methods.migrate(10, 42).encodeABI();
-
             beforeEach(async function () {
-              this.behaviorV2 = await MigratableMockV2.new();
-              this.balancePreviousV2 = new BN(await web3.eth.getBalance(this.proxy.address));
-              this.receipt = await this.proxy.upgradeToAndCall(this.behaviorV2.address, v2MigrationData, {
-                from: this.proxyAdminAddress,
-                value,
-              });
+              this.behaviorV2 = await ethers.deployContract('MigratableMockV2');
+              const v2MigrationData = this.behaviorV2.interface.encodeFunctionData('migrate', [10n, 42n]);
+
+              this.balancePreviousV2 = await ethers.provider.getBalance(this.proxy);
+              this.tx = await this.proxy
+                .connect(this.proxyAdminAsSigner)
+                .upgradeToAndCall(this.behaviorV2, v2MigrationData, {
+                  value,
+                });
             });
 
             it('upgrades to the requested version and emits an event', async function () {
-              const implementation = await getAddressInSlot(this.proxy, ImplementationSlot);
-              expect(implementation).to.be.equal(this.behaviorV2.address);
-              expectEvent(this.receipt, 'Upgraded', { implementation: this.behaviorV2.address });
+              expect(await getAddressInSlot(this.proxy, ImplementationSlot)).to.equal(this.behaviorV2.target);
+
+              await expect(this.tx).to.emit(this.proxy, 'Upgraded').withArgs(this.behaviorV2.target);
             });
 
             it("calls the 'migrate' function and sends given value to the proxy", async function () {
-              const migratable = new MigratableMockV2(this.proxy.address);
-
-              const x = await migratable.x();
-              expect(x).to.be.bignumber.equal('10');
-
-              const y = await migratable.y();
-              expect(y).to.be.bignumber.equal('42');
-
-              const balance = new BN(await web3.eth.getBalance(this.proxy.address));
-              expect(balance).to.be.bignumber.equal(this.balancePreviousV2.addn(value));
+              expect(await this.behaviorV2.attach(this.proxy).x()).to.equal(10n);
+              expect(await this.behaviorV2.attach(this.proxy).y()).to.equal(42n);
+              expect(await ethers.provider.getBalance(this.proxy)).to.equal(this.balancePreviousV2 + value);
             });
 
             describe('when upgrading to V3', function () {
-              const v3MigrationData = new MigratableMockV3('').contract.methods['migrate()']().encodeABI();
-
               beforeEach(async function () {
-                this.behaviorV3 = await MigratableMockV3.new();
-                this.balancePreviousV3 = new BN(await web3.eth.getBalance(this.proxy.address));
-                this.receipt = await this.proxy.upgradeToAndCall(this.behaviorV3.address, v3MigrationData, {
-                  from: this.proxyAdminAddress,
-                  value,
-                });
+                this.behaviorV3 = await ethers.deployContract('MigratableMockV3');
+                const v3MigrationData = this.behaviorV3.interface.encodeFunctionData('migrate()');
+
+                this.balancePreviousV3 = await ethers.provider.getBalance(this.proxy);
+                this.tx = await this.proxy
+                  .connect(this.proxyAdminAsSigner)
+                  .upgradeToAndCall(this.behaviorV3, v3MigrationData, {
+                    value,
+                  });
               });
 
               it('upgrades to the requested version and emits an event', async function () {
-                const implementation = await getAddressInSlot(this.proxy, ImplementationSlot);
-                expect(implementation).to.be.equal(this.behaviorV3.address);
-                expectEvent(this.receipt, 'Upgraded', { implementation: this.behaviorV3.address });
+                expect(await getAddressInSlot(this.proxy, ImplementationSlot)).to.equal(this.behaviorV3.target);
+
+                await expect(this.tx).to.emit(this.proxy, 'Upgraded').withArgs(this.behaviorV3.target);
               });
 
               it("calls the 'migrate' function and sends given value to the proxy", async function () {
-                const migratable = new MigratableMockV3(this.proxy.address);
-
-                const x = await migratable.x();
-                expect(x).to.be.bignumber.equal('42');
-
-                const y = await migratable.y();
-                expect(y).to.be.bignumber.equal('10');
-
-                const balance = new BN(await web3.eth.getBalance(this.proxy.address));
-                expect(balance).to.be.bignumber.equal(this.balancePreviousV3.addn(value));
+                expect(await this.behaviorV3.attach(this.proxy).x()).to.equal(42n);
+                expect(await this.behaviorV3.attach(this.proxy).y()).to.equal(10n);
+                expect(await ethers.provider.getBalance(this.proxy)).to.equal(this.balancePreviousV3 + value);
               });
             });
           });
@@ -263,12 +225,10 @@ module.exports = function shouldBehaveLikeTransparentUpgradeableProxy(createProx
       });
 
       describe('when the sender is not the admin', function () {
-        const from = anotherAccount;
-
         it('reverts', async function () {
-          const behaviorV1 = await MigratableMockV1.new();
-          const v1MigrationData = new MigratableMockV1('').contract.methods.initialize(42).encodeABI();
-          await expectRevert.unspecified(this.proxy.upgradeToAndCall(behaviorV1.address, v1MigrationData, { from }));
+          const behaviorV1 = await ethers.deployContract('MigratableMockV1');
+          const v1MigrationData = behaviorV1.interface.encodeFunctionData('initialize', [42n]);
+          await expect(this.proxy.connect(this.other).upgradeToAndCall(behaviorV1, v1MigrationData)).to.be.reverted;
         });
       });
     });
@@ -276,137 +236,122 @@ module.exports = function shouldBehaveLikeTransparentUpgradeableProxy(createProx
 
   describe('transparent proxy', function () {
     beforeEach('creating proxy', async function () {
-      const initializeData = Buffer.from('');
-      this.clashingImplV0 = (await ClashingImplementation.new()).address;
-      this.clashingImplV1 = (await ClashingImplementation.new()).address;
-      const { proxy, proxyAdminAddress } = await createProxyWithImpersonatedProxyAdmin(
-        this.clashingImplV0,
-        initializeData,
-      );
-      this.proxy = proxy;
-      this.proxyAdminAddress = proxyAdminAddress;
-      this.clashing = new ClashingImplementation(this.proxy.address);
+      this.clashingImplV0 = await ethers.deployContract('ClashingImplementation');
+      this.clashingImplV1 = await ethers.deployContract('ClashingImplementation');
+
+      Object.assign(this, await this.createProxyWithImpersonatedProxyAdmin(this.clashingImplV0, '0x'));
     });
 
     it('proxy admin cannot call delegated functions', async function () {
-      await expectRevertCustomError(
-        this.clashing.delegatedFunction({ from: this.proxyAdminAddress }),
+      const interface = await ethers.getContractFactory('TransparentUpgradeableProxy');
+
+      await expect(this.instance.connect(this.proxyAdminAsSigner).delegatedFunction()).to.be.revertedWithCustomError(
+        interface,
         'ProxyDeniedAdminAccess',
-        [],
       );
     });
 
     describe('when function names clash', function () {
       it('executes the proxy function if the sender is the admin', async function () {
-        const receipt = await this.proxy.upgradeToAndCall(this.clashingImplV1, '0x', {
-          from: this.proxyAdminAddress,
-        });
-        expectEvent(receipt, 'Upgraded', { implementation: this.clashingImplV1 });
+        await expect(this.proxy.connect(this.proxyAdminAsSigner).upgradeToAndCall(this.clashingImplV1, '0x'))
+          .to.emit(this.proxy, 'Upgraded')
+          .withArgs(this.clashingImplV1.target);
       });
 
       it('delegates the call to implementation when sender is not the admin', async function () {
-        const receipt = await this.proxy.upgradeToAndCall(this.clashingImplV1, '0x', {
-          from: anotherAccount,
-        });
-        expectEvent.notEmitted(receipt, 'Upgraded');
-        expectEvent.inTransaction(receipt.tx, this.clashing, 'ClashingImplementationCall');
+        await expect(this.proxy.connect(this.other).upgradeToAndCall(this.clashingImplV1, '0x'))
+          .to.emit(this.instance, 'ClashingImplementationCall')
+          .to.not.emit(this.proxy, 'Upgraded');
       });
     });
   });
 
-  describe('regression', () => {
-    const initializeData = Buffer.from('');
+  describe('regression', function () {
+    const initializeData = '0x';
 
-    it('should add new function', async () => {
-      const instance1 = await Implementation1.new();
-      const { proxy, proxyAdminAddress } = await createProxyWithImpersonatedProxyAdmin(
-        instance1.address,
+    it('should add new function', async function () {
+      const impl1 = await ethers.deployContract('Implementation1');
+      const impl2 = await ethers.deployContract('Implementation2');
+      const { instance, proxy, proxyAdminAsSigner } = await this.createProxyWithImpersonatedProxyAdmin(
+        impl1,
         initializeData,
       );
 
-      const proxyInstance1 = new Implementation1(proxy.address);
-      await proxyInstance1.setValue(42);
+      await instance.setValue(42n);
+
+      // `getValue` is not available in impl1
+      await expect(impl2.attach(instance).getValue()).to.be.reverted;
 
-      const instance2 = await Implementation2.new();
-      await proxy.upgradeToAndCall(instance2.address, '0x', { from: proxyAdminAddress });
+      // do upgrade
+      await proxy.connect(proxyAdminAsSigner).upgradeToAndCall(impl2, '0x');
 
-      const proxyInstance2 = new Implementation2(proxy.address);
-      const res = await proxyInstance2.getValue();
-      expect(res.toString()).to.eq('42');
+      // `getValue` is available in impl2
+      expect(await impl2.attach(instance).getValue()).to.equal(42n);
     });
 
-    it('should remove function', async () => {
-      const instance2 = await Implementation2.new();
-      const { proxy, proxyAdminAddress } = await createProxyWithImpersonatedProxyAdmin(
-        instance2.address,
+    it('should remove function', async function () {
+      const impl1 = await ethers.deployContract('Implementation1');
+      const impl2 = await ethers.deployContract('Implementation2');
+      const { instance, proxy, proxyAdminAsSigner } = await this.createProxyWithImpersonatedProxyAdmin(
+        impl2,
         initializeData,
       );
 
-      const proxyInstance2 = new Implementation2(proxy.address);
-      await proxyInstance2.setValue(42);
-      const res = await proxyInstance2.getValue();
-      expect(res.toString()).to.eq('42');
+      await instance.setValue(42n);
+
+      // `getValue` is available in impl2
+      expect(await impl2.attach(instance).getValue()).to.equal(42n);
 
-      const instance1 = await Implementation1.new();
-      await proxy.upgradeToAndCall(instance1.address, '0x', { from: proxyAdminAddress });
+      // do downgrade
+      await proxy.connect(proxyAdminAsSigner).upgradeToAndCall(impl1, '0x');
 
-      const proxyInstance1 = new Implementation2(proxy.address);
-      await expectRevert.unspecified(proxyInstance1.getValue());
+      // `getValue` is not available in impl1
+      await expect(impl2.attach(instance).getValue()).to.be.reverted;
     });
 
-    it('should change function signature', async () => {
-      const instance1 = await Implementation1.new();
-      const { proxy, proxyAdminAddress } = await createProxyWithImpersonatedProxyAdmin(
-        instance1.address,
+    it('should change function signature', async function () {
+      const impl1 = await ethers.deployContract('Implementation1');
+      const impl3 = await ethers.deployContract('Implementation3');
+      const { instance, proxy, proxyAdminAsSigner } = await this.createProxyWithImpersonatedProxyAdmin(
+        impl1,
         initializeData,
       );
 
-      const proxyInstance1 = new Implementation1(proxy.address);
-      await proxyInstance1.setValue(42);
+      await instance.setValue(42n);
 
-      const instance3 = await Implementation3.new();
-      await proxy.upgradeToAndCall(instance3.address, '0x', { from: proxyAdminAddress });
-      const proxyInstance3 = new Implementation3(proxy.address);
+      await proxy.connect(proxyAdminAsSigner).upgradeToAndCall(impl3, '0x');
 
-      const res = await proxyInstance3.getValue(8);
-      expect(res.toString()).to.eq('50');
+      expect(await impl3.attach(instance).getValue(8n)).to.equal(50n);
     });
 
-    it('should add fallback function', async () => {
-      const initializeData = Buffer.from('');
-      const instance1 = await Implementation1.new();
-      const { proxy, proxyAdminAddress } = await createProxyWithImpersonatedProxyAdmin(
-        instance1.address,
+    it('should add fallback function', async function () {
+      const impl1 = await ethers.deployContract('Implementation1');
+      const impl4 = await ethers.deployContract('Implementation4');
+      const { instance, proxy, proxyAdminAsSigner } = await this.createProxyWithImpersonatedProxyAdmin(
+        impl1,
         initializeData,
       );
 
-      const instance4 = await Implementation4.new();
-      await proxy.upgradeToAndCall(instance4.address, '0x', { from: proxyAdminAddress });
-      const proxyInstance4 = new Implementation4(proxy.address);
+      await proxy.connect(proxyAdminAsSigner).upgradeToAndCall(impl4, '0x');
 
-      const data = '0x';
-      await web3.eth.sendTransaction({ to: proxy.address, from: anotherAccount, data });
+      await this.other.sendTransaction({ to: proxy });
 
-      const res = await proxyInstance4.getValue();
-      expect(res.toString()).to.eq('1');
+      expect(await impl4.attach(instance).getValue()).to.equal(1n);
     });
 
-    it('should remove fallback function', async () => {
-      const instance4 = await Implementation4.new();
-      const { proxy, proxyAdminAddress } = await createProxyWithImpersonatedProxyAdmin(
-        instance4.address,
+    it('should remove fallback function', async function () {
+      const impl2 = await ethers.deployContract('Implementation2');
+      const impl4 = await ethers.deployContract('Implementation4');
+      const { instance, proxy, proxyAdminAsSigner } = await this.createProxyWithImpersonatedProxyAdmin(
+        impl4,
         initializeData,
       );
 
-      const instance2 = await Implementation2.new();
-      await proxy.upgradeToAndCall(instance2.address, '0x', { from: proxyAdminAddress });
+      await proxy.connect(proxyAdminAsSigner).upgradeToAndCall(impl2, '0x');
 
-      const data = '0x';
-      await expectRevert.unspecified(web3.eth.sendTransaction({ to: proxy.address, from: anotherAccount, data }));
+      await expect(this.other.sendTransaction({ to: proxy })).to.be.reverted;
 
-      const proxyInstance2 = new Implementation2(proxy.address);
-      const res = await proxyInstance2.getValue();
-      expect(res.toString()).to.eq('0');
+      expect(await impl2.attach(instance).getValue()).to.equal(0n);
     });
   });
 };

+ 22 - 18
test/proxy/transparent/TransparentUpgradeableProxy.test.js

@@ -1,24 +1,28 @@
+const { ethers } = require('hardhat');
+const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
+
 const shouldBehaveLikeProxy = require('../Proxy.behaviour');
 const shouldBehaveLikeTransparentUpgradeableProxy = require('./TransparentUpgradeableProxy.behaviour');
 
-const TransparentUpgradeableProxy = artifacts.require('TransparentUpgradeableProxy');
-const ITransparentUpgradeableProxy = artifacts.require('ITransparentUpgradeableProxy');
-
-contract('TransparentUpgradeableProxy', function (accounts) {
-  const [owner, ...otherAccounts] = accounts;
-
-  // `undefined`, `null` and other false-ish opts will not be forwarded.
-  const createProxy = async function (logic, initData, opts = undefined) {
-    const { address, transactionHash } = await TransparentUpgradeableProxy.new(
-      logic,
-      owner,
-      initData,
-      ...[opts].filter(Boolean),
-    );
-    const instance = await ITransparentUpgradeableProxy.at(address);
-    return { ...instance, transactionHash };
+async function fixture() {
+  const [owner, other, ...accounts] = await ethers.getSigners();
+
+  const implementation = await ethers.deployContract('DummyImplementation');
+
+  const createProxy = function (logic, initData, opts = undefined) {
+    return ethers.deployContract('TransparentUpgradeableProxy', [logic, owner, initData], opts);
   };
 
-  shouldBehaveLikeProxy(createProxy, otherAccounts);
-  shouldBehaveLikeTransparentUpgradeableProxy(createProxy, owner, otherAccounts);
+  return { nonContractAddress: owner, owner, other, accounts, implementation, createProxy };
+}
+
+describe('TransparentUpgradeableProxy', function () {
+  beforeEach(async function () {
+    Object.assign(this, await loadFixture(fixture));
+  });
+
+  shouldBehaveLikeProxy();
+
+  // createProxy, owner, otherAccounts
+  shouldBehaveLikeTransparentUpgradeableProxy();
 });

+ 97 - 99
test/proxy/utils/Initializable.test.js

@@ -1,220 +1,218 @@
-const { expectEvent } = require('@openzeppelin/test-helpers');
+const { ethers } = require('hardhat');
 const { expect } = require('chai');
-const { expectRevertCustomError } = require('../../helpers/customError');
-const { MAX_UINT64 } = require('../../helpers/constants');
-
-const InitializableMock = artifacts.require('InitializableMock');
-const ConstructorInitializableMock = artifacts.require('ConstructorInitializableMock');
-const ChildConstructorInitializableMock = artifacts.require('ChildConstructorInitializableMock');
-const ReinitializerMock = artifacts.require('ReinitializerMock');
-const SampleChild = artifacts.require('SampleChild');
-const DisableBad1 = artifacts.require('DisableBad1');
-const DisableBad2 = artifacts.require('DisableBad2');
-const DisableOk = artifacts.require('DisableOk');
-
-contract('Initializable', function () {
+const {
+  bigint: { MAX_UINT64 },
+} = require('../../helpers/constants');
+
+describe('Initializable', function () {
   describe('basic testing without inheritance', function () {
     beforeEach('deploying', async function () {
-      this.contract = await InitializableMock.new();
+      this.mock = await ethers.deployContract('InitializableMock');
     });
 
     describe('before initialize', function () {
       it('initializer has not run', async function () {
-        expect(await this.contract.initializerRan()).to.equal(false);
+        expect(await this.mock.initializerRan()).to.be.false;
       });
 
       it('_initializing returns false before initialization', async function () {
-        expect(await this.contract.isInitializing()).to.equal(false);
+        expect(await this.mock.isInitializing()).to.be.false;
       });
     });
 
     describe('after initialize', function () {
       beforeEach('initializing', async function () {
-        await this.contract.initialize();
+        await this.mock.initialize();
       });
 
       it('initializer has run', async function () {
-        expect(await this.contract.initializerRan()).to.equal(true);
+        expect(await this.mock.initializerRan()).to.be.true;
       });
 
       it('_initializing returns false after initialization', async function () {
-        expect(await this.contract.isInitializing()).to.equal(false);
+        expect(await this.mock.isInitializing()).to.be.false;
       });
 
       it('initializer does not run again', async function () {
-        await expectRevertCustomError(this.contract.initialize(), 'InvalidInitialization', []);
+        await expect(this.mock.initialize()).to.be.revertedWithCustomError(this.mock, 'InvalidInitialization');
       });
     });
 
     describe('nested under an initializer', function () {
       it('initializer modifier reverts', async function () {
-        await expectRevertCustomError(this.contract.initializerNested(), 'InvalidInitialization', []);
+        await expect(this.mock.initializerNested()).to.be.revertedWithCustomError(this.mock, 'InvalidInitialization');
       });
 
       it('onlyInitializing modifier succeeds', async function () {
-        await this.contract.onlyInitializingNested();
-        expect(await this.contract.onlyInitializingRan()).to.equal(true);
+        await this.mock.onlyInitializingNested();
+        expect(await this.mock.onlyInitializingRan()).to.be.true;
       });
     });
 
     it('cannot call onlyInitializable function outside the scope of an initializable function', async function () {
-      await expectRevertCustomError(this.contract.initializeOnlyInitializing(), 'NotInitializing', []);
+      await expect(this.mock.initializeOnlyInitializing()).to.be.revertedWithCustomError(this.mock, 'NotInitializing');
     });
   });
 
   it('nested initializer can run during construction', async function () {
-    const contract2 = await ConstructorInitializableMock.new();
-    expect(await contract2.initializerRan()).to.equal(true);
-    expect(await contract2.onlyInitializingRan()).to.equal(true);
+    const mock = await ethers.deployContract('ConstructorInitializableMock');
+    expect(await mock.initializerRan()).to.be.true;
+    expect(await mock.onlyInitializingRan()).to.be.true;
   });
 
   it('multiple constructor levels can be initializers', async function () {
-    const contract2 = await ChildConstructorInitializableMock.new();
-    expect(await contract2.initializerRan()).to.equal(true);
-    expect(await contract2.childInitializerRan()).to.equal(true);
-    expect(await contract2.onlyInitializingRan()).to.equal(true);
+    const mock = await ethers.deployContract('ChildConstructorInitializableMock');
+    expect(await mock.initializerRan()).to.be.true;
+    expect(await mock.childInitializerRan()).to.be.true;
+    expect(await mock.onlyInitializingRan()).to.be.true;
   });
 
   describe('reinitialization', function () {
     beforeEach('deploying', async function () {
-      this.contract = await ReinitializerMock.new();
+      this.mock = await ethers.deployContract('ReinitializerMock');
     });
 
     it('can reinitialize', async function () {
-      expect(await this.contract.counter()).to.be.bignumber.equal('0');
-      await this.contract.initialize();
-      expect(await this.contract.counter()).to.be.bignumber.equal('1');
-      await this.contract.reinitialize(2);
-      expect(await this.contract.counter()).to.be.bignumber.equal('2');
-      await this.contract.reinitialize(3);
-      expect(await this.contract.counter()).to.be.bignumber.equal('3');
+      expect(await this.mock.counter()).to.equal(0n);
+      await this.mock.initialize();
+      expect(await this.mock.counter()).to.equal(1n);
+      await this.mock.reinitialize(2);
+      expect(await this.mock.counter()).to.equal(2n);
+      await this.mock.reinitialize(3);
+      expect(await this.mock.counter()).to.equal(3n);
     });
 
     it('can jump multiple steps', async function () {
-      expect(await this.contract.counter()).to.be.bignumber.equal('0');
-      await this.contract.initialize();
-      expect(await this.contract.counter()).to.be.bignumber.equal('1');
-      await this.contract.reinitialize(128);
-      expect(await this.contract.counter()).to.be.bignumber.equal('2');
+      expect(await this.mock.counter()).to.equal(0n);
+      await this.mock.initialize();
+      expect(await this.mock.counter()).to.equal(1n);
+      await this.mock.reinitialize(128);
+      expect(await this.mock.counter()).to.equal(2n);
     });
 
     it('cannot nest reinitializers', async function () {
-      expect(await this.contract.counter()).to.be.bignumber.equal('0');
-      await expectRevertCustomError(this.contract.nestedReinitialize(2, 2), 'InvalidInitialization', []);
-      await expectRevertCustomError(this.contract.nestedReinitialize(2, 3), 'InvalidInitialization', []);
-      await expectRevertCustomError(this.contract.nestedReinitialize(3, 2), 'InvalidInitialization', []);
+      expect(await this.mock.counter()).to.equal(0n);
+      await expect(this.mock.nestedReinitialize(2, 2)).to.be.revertedWithCustomError(
+        this.mock,
+        'InvalidInitialization',
+      );
+      await expect(this.mock.nestedReinitialize(2, 3)).to.be.revertedWithCustomError(
+        this.mock,
+        'InvalidInitialization',
+      );
+      await expect(this.mock.nestedReinitialize(3, 2)).to.be.revertedWithCustomError(
+        this.mock,
+        'InvalidInitialization',
+      );
     });
 
     it('can chain reinitializers', async function () {
-      expect(await this.contract.counter()).to.be.bignumber.equal('0');
-      await this.contract.chainReinitialize(2, 3);
-      expect(await this.contract.counter()).to.be.bignumber.equal('2');
+      expect(await this.mock.counter()).to.equal(0n);
+      await this.mock.chainReinitialize(2, 3);
+      expect(await this.mock.counter()).to.equal(2n);
     });
 
     it('_getInitializedVersion returns right version', async function () {
-      await this.contract.initialize();
-      expect(await this.contract.getInitializedVersion()).to.be.bignumber.equal('1');
-      await this.contract.reinitialize(12);
-      expect(await this.contract.getInitializedVersion()).to.be.bignumber.equal('12');
+      await this.mock.initialize();
+      expect(await this.mock.getInitializedVersion()).to.equal(1n);
+      await this.mock.reinitialize(12);
+      expect(await this.mock.getInitializedVersion()).to.equal(12n);
     });
 
     describe('contract locking', function () {
       it('prevents initialization', async function () {
-        await this.contract.disableInitializers();
-        await expectRevertCustomError(this.contract.initialize(), 'InvalidInitialization', []);
+        await this.mock.disableInitializers();
+        await expect(this.mock.initialize()).to.be.revertedWithCustomError(this.mock, 'InvalidInitialization');
       });
 
       it('prevents re-initialization', async function () {
-        await this.contract.disableInitializers();
-        await expectRevertCustomError(this.contract.reinitialize(255), 'InvalidInitialization', []);
+        await this.mock.disableInitializers();
+        await expect(this.mock.reinitialize(255n)).to.be.revertedWithCustomError(this.mock, 'InvalidInitialization');
       });
 
       it('can lock contract after initialization', async function () {
-        await this.contract.initialize();
-        await this.contract.disableInitializers();
-        await expectRevertCustomError(this.contract.reinitialize(255), 'InvalidInitialization', []);
+        await this.mock.initialize();
+        await this.mock.disableInitializers();
+        await expect(this.mock.reinitialize(255n)).to.be.revertedWithCustomError(this.mock, 'InvalidInitialization');
       });
     });
   });
 
   describe('events', function () {
     it('constructor initialization emits event', async function () {
-      const contract = await ConstructorInitializableMock.new();
-
-      await expectEvent.inTransaction(contract.transactionHash, contract, 'Initialized', { version: '1' });
+      const mock = await ethers.deployContract('ConstructorInitializableMock');
+      await expect(mock.deploymentTransaction()).to.emit(mock, 'Initialized').withArgs(1n);
     });
 
     it('initialization emits event', async function () {
-      const contract = await ReinitializerMock.new();
-
-      const { receipt } = await contract.initialize();
-      expect(receipt.logs.filter(({ event }) => event === 'Initialized').length).to.be.equal(1);
-      expectEvent(receipt, 'Initialized', { version: '1' });
+      const mock = await ethers.deployContract('ReinitializerMock');
+      await expect(mock.initialize()).to.emit(mock, 'Initialized').withArgs(1n);
     });
 
     it('reinitialization emits event', async function () {
-      const contract = await ReinitializerMock.new();
-
-      const { receipt } = await contract.reinitialize(128);
-      expect(receipt.logs.filter(({ event }) => event === 'Initialized').length).to.be.equal(1);
-      expectEvent(receipt, 'Initialized', { version: '128' });
+      const mock = await ethers.deployContract('ReinitializerMock');
+      await expect(mock.reinitialize(128)).to.emit(mock, 'Initialized').withArgs(128n);
     });
 
     it('chained reinitialization emits multiple events', async function () {
-      const contract = await ReinitializerMock.new();
+      const mock = await ethers.deployContract('ReinitializerMock');
 
-      const { receipt } = await contract.chainReinitialize(2, 3);
-      expect(receipt.logs.filter(({ event }) => event === 'Initialized').length).to.be.equal(2);
-      expectEvent(receipt, 'Initialized', { version: '2' });
-      expectEvent(receipt, 'Initialized', { version: '3' });
+      await expect(mock.chainReinitialize(2, 3))
+        .to.emit(mock, 'Initialized')
+        .withArgs(2n)
+        .to.emit(mock, 'Initialized')
+        .withArgs(3n);
     });
   });
 
   describe('complex testing with inheritance', function () {
-    const mother = '12';
+    const mother = 12n;
     const gramps = '56';
-    const father = '34';
-    const child = '78';
+    const father = 34n;
+    const child = 78n;
 
     beforeEach('deploying', async function () {
-      this.contract = await SampleChild.new();
-    });
-
-    beforeEach('initializing', async function () {
-      await this.contract.initialize(mother, gramps, father, child);
+      this.mock = await ethers.deployContract('SampleChild');
+      await this.mock.initialize(mother, gramps, father, child);
     });
 
     it('initializes human', async function () {
-      expect(await this.contract.isHuman()).to.be.equal(true);
+      expect(await this.mock.isHuman()).to.be.true;
     });
 
     it('initializes mother', async function () {
-      expect(await this.contract.mother()).to.be.bignumber.equal(mother);
+      expect(await this.mock.mother()).to.equal(mother);
     });
 
     it('initializes gramps', async function () {
-      expect(await this.contract.gramps()).to.be.bignumber.equal(gramps);
+      expect(await this.mock.gramps()).to.equal(gramps);
     });
 
     it('initializes father', async function () {
-      expect(await this.contract.father()).to.be.bignumber.equal(father);
+      expect(await this.mock.father()).to.equal(father);
     });
 
     it('initializes child', async function () {
-      expect(await this.contract.child()).to.be.bignumber.equal(child);
+      expect(await this.mock.child()).to.equal(child);
     });
   });
 
   describe('disabling initialization', function () {
     it('old and new patterns in bad sequence', async function () {
-      await expectRevertCustomError(DisableBad1.new(), 'InvalidInitialization', []);
-      await expectRevertCustomError(DisableBad2.new(), 'InvalidInitialization', []);
+      const DisableBad1 = await ethers.getContractFactory('DisableBad1');
+      await expect(DisableBad1.deploy()).to.be.revertedWithCustomError(DisableBad1, 'InvalidInitialization');
+
+      const DisableBad2 = await ethers.getContractFactory('DisableBad2');
+      await expect(DisableBad2.deploy()).to.be.revertedWithCustomError(DisableBad2, 'InvalidInitialization');
     });
 
     it('old and new patterns in good sequence', async function () {
-      const ok = await DisableOk.new();
-      await expectEvent.inConstruction(ok, 'Initialized', { version: '1' });
-      await expectEvent.inConstruction(ok, 'Initialized', { version: MAX_UINT64 });
+      const ok = await ethers.deployContract('DisableOk');
+      await expect(ok.deploymentTransaction())
+        .to.emit(ok, 'Initialized')
+        .withArgs(1n)
+        .to.emit(ok, 'Initialized')
+        .withArgs(MAX_UINT64);
     });
   });
 });

+ 73 - 84
test/proxy/utils/UUPSUpgradeable.test.js

@@ -1,28 +1,36 @@
-const { expectEvent } = require('@openzeppelin/test-helpers');
+const { ethers } = require('hardhat');
+const { expect } = require('chai');
+const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
+
 const { getAddressInSlot, ImplementationSlot } = require('../../helpers/erc1967');
-const { expectRevertCustomError } = require('../../helpers/customError');
-
-const ERC1967Proxy = artifacts.require('ERC1967Proxy');
-const UUPSUpgradeableMock = artifacts.require('UUPSUpgradeableMock');
-const UUPSUpgradeableUnsafeMock = artifacts.require('UUPSUpgradeableUnsafeMock');
-const NonUpgradeableMock = artifacts.require('NonUpgradeableMock');
-const UUPSUnsupportedProxiableUUID = artifacts.require('UUPSUnsupportedProxiableUUID');
-const Clones = artifacts.require('$Clones');
-
-contract('UUPSUpgradeable', function () {
-  before(async function () {
-    this.implInitial = await UUPSUpgradeableMock.new();
-    this.implUpgradeOk = await UUPSUpgradeableMock.new();
-    this.implUpgradeUnsafe = await UUPSUpgradeableUnsafeMock.new();
-    this.implUpgradeNonUUPS = await NonUpgradeableMock.new();
-    this.implUnsupportedUUID = await UUPSUnsupportedProxiableUUID.new();
-    // Used for testing non ERC1967 compliant proxies (clones are proxies that don't use the ERC1967 implementation slot)
-    this.cloneFactory = await Clones.new();
-  });
 
+async function fixture() {
+  const implInitial = await ethers.deployContract('UUPSUpgradeableMock');
+  const implUpgradeOk = await ethers.deployContract('UUPSUpgradeableMock');
+  const implUpgradeUnsafe = await ethers.deployContract('UUPSUpgradeableUnsafeMock');
+  const implUpgradeNonUUPS = await ethers.deployContract('NonUpgradeableMock');
+  const implUnsupportedUUID = await ethers.deployContract('UUPSUnsupportedProxiableUUID');
+  // Used for testing non ERC1967 compliant proxies (clones are proxies that don't use the ERC1967 implementation slot)
+  const cloneFactory = await ethers.deployContract('$Clones');
+
+  const instance = await ethers
+    .deployContract('ERC1967Proxy', [implInitial, '0x'])
+    .then(proxy => implInitial.attach(proxy.target));
+
+  return {
+    implInitial,
+    implUpgradeOk,
+    implUpgradeUnsafe,
+    implUpgradeNonUUPS,
+    implUnsupportedUUID,
+    cloneFactory,
+    instance,
+  };
+}
+
+describe('UUPSUpgradeable', function () {
   beforeEach(async function () {
-    const { address } = await ERC1967Proxy.new(this.implInitial.address, '0x');
-    this.instance = await UUPSUpgradeableMock.at(address);
+    Object.assign(this, await loadFixture(fixture));
   });
 
   it('has an interface version', async function () {
@@ -30,102 +38,83 @@ contract('UUPSUpgradeable', function () {
   });
 
   it('upgrade to upgradeable implementation', async function () {
-    const { receipt } = await this.instance.upgradeToAndCall(this.implUpgradeOk.address, '0x');
-    expect(receipt.logs.filter(({ event }) => event === 'Upgraded').length).to.be.equal(1);
-    expectEvent(receipt, 'Upgraded', { implementation: this.implUpgradeOk.address });
-    expect(await getAddressInSlot(this.instance, ImplementationSlot)).to.be.equal(this.implUpgradeOk.address);
+    await expect(this.instance.upgradeToAndCall(this.implUpgradeOk, '0x'))
+      .to.emit(this.instance, 'Upgraded')
+      .withArgs(this.implUpgradeOk.target);
+
+    expect(await getAddressInSlot(this.instance, ImplementationSlot)).to.equal(this.implUpgradeOk.target);
   });
 
   it('upgrade to upgradeable implementation with call', async function () {
-    expect(await this.instance.current()).to.be.bignumber.equal('0');
+    expect(await this.instance.current()).to.equal(0n);
 
-    const { receipt } = await this.instance.upgradeToAndCall(
-      this.implUpgradeOk.address,
-      this.implUpgradeOk.contract.methods.increment().encodeABI(),
-    );
-    expect(receipt.logs.filter(({ event }) => event === 'Upgraded').length).to.be.equal(1);
-    expectEvent(receipt, 'Upgraded', { implementation: this.implUpgradeOk.address });
-    expect(await getAddressInSlot(this.instance, ImplementationSlot)).to.be.equal(this.implUpgradeOk.address);
+    await expect(
+      this.instance.upgradeToAndCall(this.implUpgradeOk, this.implUpgradeOk.interface.encodeFunctionData('increment')),
+    )
+      .to.emit(this.instance, 'Upgraded')
+      .withArgs(this.implUpgradeOk.target);
+
+    expect(await getAddressInSlot(this.instance, ImplementationSlot)).to.equal(this.implUpgradeOk.target);
 
-    expect(await this.instance.current()).to.be.bignumber.equal('1');
+    expect(await this.instance.current()).to.equal(1n);
   });
 
   it('calling upgradeTo on the implementation reverts', async function () {
-    await expectRevertCustomError(
-      this.implInitial.upgradeToAndCall(this.implUpgradeOk.address, '0x'),
+    await expect(this.implInitial.upgradeToAndCall(this.implUpgradeOk, '0x')).to.be.revertedWithCustomError(
+      this.implInitial,
       'UUPSUnauthorizedCallContext',
-      [],
     );
   });
 
   it('calling upgradeToAndCall on the implementation reverts', async function () {
-    await expectRevertCustomError(
+    await expect(
       this.implInitial.upgradeToAndCall(
-        this.implUpgradeOk.address,
-        this.implUpgradeOk.contract.methods.increment().encodeABI(),
+        this.implUpgradeOk,
+        this.implUpgradeOk.interface.encodeFunctionData('increment'),
       ),
-      'UUPSUnauthorizedCallContext',
-      [],
-    );
-  });
-
-  it('calling upgradeTo from a contract that is not an ERC1967 proxy (with the right implementation) reverts', async function () {
-    const receipt = await this.cloneFactory.$clone(this.implUpgradeOk.address);
-    const instance = await UUPSUpgradeableMock.at(
-      receipt.logs.find(({ event }) => event === 'return$clone').args.instance,
-    );
-
-    await expectRevertCustomError(
-      instance.upgradeToAndCall(this.implUpgradeUnsafe.address, '0x'),
-      'UUPSUnauthorizedCallContext',
-      [],
-    );
+    ).to.be.revertedWithCustomError(this.implUpgradeOk, 'UUPSUnauthorizedCallContext');
   });
 
   it('calling upgradeToAndCall from a contract that is not an ERC1967 proxy (with the right implementation) reverts', async function () {
-    const receipt = await this.cloneFactory.$clone(this.implUpgradeOk.address);
-    const instance = await UUPSUpgradeableMock.at(
-      receipt.logs.find(({ event }) => event === 'return$clone').args.instance,
-    );
+    const instance = await this.cloneFactory.$clone
+      .staticCall(this.implUpgradeOk)
+      .then(address => this.implInitial.attach(address));
+    await this.cloneFactory.$clone(this.implUpgradeOk);
 
-    await expectRevertCustomError(
-      instance.upgradeToAndCall(this.implUpgradeUnsafe.address, '0x'),
+    await expect(instance.upgradeToAndCall(this.implUpgradeUnsafe, '0x')).to.be.revertedWithCustomError(
+      instance,
       'UUPSUnauthorizedCallContext',
-      [],
     );
   });
 
   it('rejects upgrading to an unsupported UUID', async function () {
-    await expectRevertCustomError(
-      this.instance.upgradeToAndCall(this.implUnsupportedUUID.address, '0x'),
-      'UUPSUnsupportedProxiableUUID',
-      [web3.utils.keccak256('invalid UUID')],
-    );
+    await expect(this.instance.upgradeToAndCall(this.implUnsupportedUUID, '0x'))
+      .to.be.revertedWithCustomError(this.instance, 'UUPSUnsupportedProxiableUUID')
+      .withArgs(ethers.id('invalid UUID'));
   });
 
   it('upgrade to and unsafe upgradeable implementation', async function () {
-    const { receipt } = await this.instance.upgradeToAndCall(this.implUpgradeUnsafe.address, '0x');
-    expectEvent(receipt, 'Upgraded', { implementation: this.implUpgradeUnsafe.address });
-    expect(await getAddressInSlot(this.instance, ImplementationSlot)).to.be.equal(this.implUpgradeUnsafe.address);
+    await expect(this.instance.upgradeToAndCall(this.implUpgradeUnsafe, '0x'))
+      .to.emit(this.instance, 'Upgraded')
+      .withArgs(this.implUpgradeUnsafe.target);
+
+    expect(await getAddressInSlot(this.instance, ImplementationSlot)).to.equal(this.implUpgradeUnsafe.target);
   });
 
   // delegate to a non existing upgradeTo function causes a low level revert
   it('reject upgrade to non uups implementation', async function () {
-    await expectRevertCustomError(
-      this.instance.upgradeToAndCall(this.implUpgradeNonUUPS.address, '0x'),
-      'ERC1967InvalidImplementation',
-      [this.implUpgradeNonUUPS.address],
-    );
+    await expect(this.instance.upgradeToAndCall(this.implUpgradeNonUUPS, '0x'))
+      .to.be.revertedWithCustomError(this.instance, 'ERC1967InvalidImplementation')
+      .withArgs(this.implUpgradeNonUUPS.target);
   });
 
   it('reject proxy address as implementation', async function () {
-    const { address } = await ERC1967Proxy.new(this.implInitial.address, '0x');
-    const otherInstance = await UUPSUpgradeableMock.at(address);
+    const otherInstance = await ethers
+      .deployContract('ERC1967Proxy', [this.implInitial, '0x'])
+      .then(proxy => this.implInitial.attach(proxy.target));
 
-    await expectRevertCustomError(
-      this.instance.upgradeToAndCall(otherInstance.address, '0x'),
-      'ERC1967InvalidImplementation',
-      [otherInstance.address],
-    );
+    await expect(this.instance.upgradeToAndCall(otherInstance, '0x'))
+      .to.be.revertedWithCustomError(this.instance, 'ERC1967InvalidImplementation')
+      .withArgs(otherInstance.target);
   });
 });