From ae69142379cd5f20b79c1f864d89f8fe955898da Mon Sep 17 00:00:00 2001 From: Renan Souza Date: Wed, 29 Nov 2023 21:51:08 +0000 Subject: [PATCH] Migrate proxy folder to ethersjs (#4746) Co-authored-by: Hadrien Croubois Co-authored-by: ernestognw --- test/helpers/erc1967.js | 39 +- test/proxy/Clones.behaviour.js | 77 ++-- test/proxy/Clones.test.js | 99 ++-- test/proxy/ERC1967/ERC1967Proxy.test.js | 25 +- test/proxy/ERC1967/ERC1967Utils.test.js | 138 +++--- test/proxy/Proxy.behaviour.js | 112 ++--- test/proxy/beacon/BeaconProxy.test.js | 192 ++++---- test/proxy/beacon/UpgradeableBeacon.test.js | 63 +-- test/proxy/transparent/ProxyAdmin.test.js | 90 ++-- .../TransparentUpgradeableProxy.behaviour.js | 423 ++++++++---------- .../TransparentUpgradeableProxy.test.js | 36 +- test/proxy/utils/Initializable.test.js | 194 ++++---- test/proxy/utils/UUPSUpgradeable.test.js | 153 +++---- 13 files changed, 785 insertions(+), 856 deletions(-) diff --git a/test/helpers/erc1967.js b/test/helpers/erc1967.js index 50542c89a..88a87d661 100644 --- a/test/helpers/erc1967.js +++ b/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, diff --git a/test/proxy/Clones.behaviour.js b/test/proxy/Clones.behaviour.js index b5fd3c51b..861fae8a2 100644 --- a/test/proxy/Clones.behaviour.js +++ b/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({ diff --git a/test/proxy/Clones.test.js b/test/proxy/Clones.test.js index ad3dd537c..626b1e564 100644 --- a/test/proxy/Clones.test.js +++ b/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); }); }); }); diff --git a/test/proxy/ERC1967/ERC1967Proxy.test.js b/test/proxy/ERC1967/ERC1967Proxy.test.js index 81cc43507..b22280046 100644 --- a/test/proxy/ERC1967/ERC1967Proxy.test.js +++ b/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 ERC1967Proxy = artifacts.require('ERC1967Proxy'); +const fixture = async () => { + const [nonContractAddress] = await ethers.getSigners(); -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)); - }; + const implementation = await ethers.deployContract('DummyImplementation'); - shouldBehaveLikeProxy(createProxy, accounts); + const createProxy = (implementation, initData, opts) => + ethers.deployContract('ERC1967Proxy', [implementation, initData], opts); + + return { nonContractAddress, implementation, createProxy }; +}; + +describe('ERC1967Proxy', function () { + beforeEach(async function () { + Object.assign(this, await loadFixture(fixture)); + }); + + shouldBehaveLikeProxy(); }); diff --git a/test/proxy/ERC1967/ERC1967Utils.test.js b/test/proxy/ERC1967/ERC1967Utils.test.js index 975b08d81..f733e297f 100644 --- a/test/proxy/ERC1967/ERC1967Utils.test.js +++ b/test/proxy/ERC1967/ERC1967Utils.test.js @@ -1,70 +1,65 @@ -const { expectEvent, constants } = require('@openzeppelin/test-helpers'); -const { expectRevertCustomError } = require('../../helpers/customError'); +const { ethers } = require('hardhat'); +const { expect } = require('chai'); +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); + const { getAddressInSlot, setSlot, ImplementationSlot, AdminSlot, BeaconSlot } = require('../../helpers/erc1967'); -const { ZERO_ADDRESS } = constants; +async function fixture() { + const [, admin, anotherAccount] = await ethers.getSigners(); -const ERC1967Utils = artifacts.require('$ERC1967Utils'); + const utils = await ethers.deployContract('$ERC1967Utils'); + const v1 = await ethers.deployContract('DummyImplementation'); + const v2 = await ethers.deployContract('CallReceiverMock'); -const V1 = artifacts.require('DummyImplementation'); -const V2 = artifacts.require('CallReceiverMock'); -const UpgradeableBeaconMock = artifacts.require('UpgradeableBeaconMock'); -const UpgradeableBeaconReentrantMock = artifacts.require('UpgradeableBeaconReentrantMock'); - -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); }); }); }); diff --git a/test/proxy/Proxy.behaviour.js b/test/proxy/Proxy.behaviour.js index acce6d188..84cd93b51 100644 --- a/test/proxy/Proxy.behaviour.js +++ b/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; }); }); }); diff --git a/test/proxy/beacon/BeaconProxy.test.js b/test/proxy/beacon/BeaconProxy.test.js index d583d0ffb..66856ac08 100644 --- a/test/proxy/beacon/BeaconProxy.test.js +++ b/test/proxy/beacon/BeaconProxy.test.js @@ -1,152 +1,138 @@ -const { expectRevert } = require('@openzeppelin/test-helpers'); -const { getSlot, BeaconSlot } = require('../../helpers/erc1967'); - -const { expectRevertCustomError } = require('../../helpers/customError'); - +const { ethers } = require('hardhat'); const { expect } = require('chai'); +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); +const { getAddressInSlot, BeaconSlot } = require('../../helpers/erc1967'); -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'); +async function fixture() { + const [admin, other] = await ethers.getSigners(); -contract('BeaconProxy', function (accounts) { - const [upgradeableBeaconAdmin, anotherAccount] = accounts; + 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); + + return { admin, other, factory, beacon, v1, v2, newBeaconProxy }; +} + +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); + const dummy = this.v1.attach(this.proxy); + expect(await dummy.value()).to.equal(value); - expect(await web3.eth.getBalance(this.proxy.address)).to.bignumber.eq(balance); - }; - }); - - 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'); + // test upgraded version + expect(await proxy.version()).to.equal('V2'); + }); - // upgrade beacon - await beacon.upgradeTo(this.implementationV1.address, { from: upgradeableBeaconAdmin }); + 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)); - // test upgraded version - expect(await dummy.version()).to.eq('V2'); - }); + 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)); - it('upgrade 2 proxies by upgrading shared beacon', async function () { - const value1 = '10'; - const value2 = '42'; + // test initial values + expect(await proxy1.value()).to.equal(value1); + expect(await proxy2.value()).to.equal(value2); - const beacon = await UpgradeableBeacon.new(this.implementationV0.address, upgradeableBeaconAdmin); + // test initial version + expect(await proxy1.version()).to.equal('V1'); + expect(await proxy2.version()).to.equal('V1'); - const proxy1InitializeData = this.implementationV0.contract.methods - .initializeNonPayableWithValue(value1) - .encodeABI(); - const proxy1 = await BeaconProxy.new(beacon.address, proxy1InitializeData); + // upgrade beacon + await this.beacon.connect(this.admin).upgradeTo(this.v2); - const proxy2InitializeData = this.implementationV0.contract.methods - .initializeNonPayableWithValue(value2) - .encodeABI(); - const proxy2 = await BeaconProxy.new(beacon.address, proxy2InitializeData); - - const dummy1 = new DummyImplementation(proxy1.address); - const dummy2 = new DummyImplementation(proxy2.address); - - // test initial values - expect(await dummy1.value()).to.bignumber.eq(value1); - expect(await dummy2.value()).to.bignumber.eq(value2); - - // test initial version - expect(await dummy1.version()).to.eq('V1'); - expect(await dummy2.version()).to.eq('V1'); - - // upgrade beacon - await beacon.upgradeTo(this.implementationV1.address, { from: upgradeableBeaconAdmin }); - - // 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'); + }); }); }); diff --git a/test/proxy/beacon/UpgradeableBeacon.test.js b/test/proxy/beacon/UpgradeableBeacon.test.js index 0737f6fdf..be6aca754 100644 --- a/test/proxy/beacon/UpgradeableBeacon.test.js +++ b/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); }); }); }); diff --git a/test/proxy/transparent/ProxyAdmin.test.js b/test/proxy/transparent/ProxyAdmin.test.js index a3122eae3..9f137536a 100644 --- a/test/proxy/transparent/ProxyAdmin.test.js +++ b/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'); + 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 }), + ); + + return { admin, other, v1, v2, proxy, proxyAdmin }; +} + +describe('ProxyAdmin', function () { beforeEach(async function () { - const initializeData = Buffer.from(''); - const proxy = await TransparentUpgradeableProxy.new(this.implementationV1.address, proxyAdminOwner, initializeData); - - 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); - - this.proxy = await ITransparentUpgradeableProxy.at(proxy.address); + 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); }); }); }); diff --git a/test/proxy/transparent/TransparentUpgradeableProxy.behaviour.js b/test/proxy/transparent/TransparentUpgradeableProxy.behaviour.js index da4d99287..021228199 100644 --- a/test/proxy/transparent/TransparentUpgradeableProxy.behaviour.js +++ b/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 () { - this.implementationV0 = (await DummyImplementation.new()).address; - this.implementationV1 = (await DummyImplementation.new()).address; + 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, + }; + }; + + 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); - const instance2 = await Implementation2.new(); - await proxy.upgradeToAndCall(instance2.address, '0x', { from: proxyAdminAddress }); + // `getValue` is not available in impl1 + await expect(impl2.attach(instance).getValue()).to.be.reverted; - const proxyInstance2 = new Implementation2(proxy.address); - const res = await proxyInstance2.getValue(); - expect(res.toString()).to.eq('42'); + // do upgrade + await proxy.connect(proxyAdminAsSigner).upgradeToAndCall(impl2, '0x'); + + // `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); - const instance1 = await Implementation1.new(); - await proxy.upgradeToAndCall(instance1.address, '0x', { from: proxyAdminAddress }); + // `getValue` is available in impl2 + expect(await impl2.attach(instance).getValue()).to.equal(42n); - const proxyInstance1 = new Implementation2(proxy.address); - await expectRevert.unspecified(proxyInstance1.getValue()); + // do downgrade + await proxy.connect(proxyAdminAsSigner).upgradeToAndCall(impl1, '0x'); + + // `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); }); }); }; diff --git a/test/proxy/transparent/TransparentUpgradeableProxy.test.js b/test/proxy/transparent/TransparentUpgradeableProxy.test.js index f45e392f6..61e18014e 100644 --- a/test/proxy/transparent/TransparentUpgradeableProxy.test.js +++ b/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'); +async function fixture() { + const [owner, other, ...accounts] = await ethers.getSigners(); -contract('TransparentUpgradeableProxy', function (accounts) { - const [owner, ...otherAccounts] = accounts; + const implementation = await ethers.deployContract('DummyImplementation'); - // `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 }; + 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(); }); diff --git a/test/proxy/utils/Initializable.test.js b/test/proxy/utils/Initializable.test.js index b9ff3b052..bc26e6b60 100644 --- a/test/proxy/utils/Initializable.test.js +++ b/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 { + bigint: { 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 () { +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); }); }); }); diff --git a/test/proxy/utils/UUPSUpgradeable.test.js b/test/proxy/utils/UUPSUpgradeable.test.js index 0baa90520..876a64cf9 100644 --- a/test/proxy/utils/UUPSUpgradeable.test.js +++ b/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'); +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'); -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(); - }); + 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 this.instance.current()).to.be.bignumber.equal('1'); + expect(await getAddressInSlot(this.instance, ImplementationSlot)).to.equal(this.implUpgradeOk.target); + + 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); }); });