Migrate proxy folder to ethersjs (#4746)
Co-authored-by: Hadrien Croubois <hadrien.croubois@gmail.com> Co-authored-by: ernestognw <ernestognw@gmail.com>
This commit is contained in:
@ -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,
|
||||
|
||||
@ -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({
|
||||
|
||||
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -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');
|
||||
|
||||
const createProxy = (implementation, initData, opts) =>
|
||||
ethers.deployContract('ERC1967Proxy', [implementation, initData], opts);
|
||||
|
||||
return { nonContractAddress, implementation, createProxy };
|
||||
};
|
||||
|
||||
shouldBehaveLikeProxy(createProxy, accounts);
|
||||
describe('ERC1967Proxy', function () {
|
||||
beforeEach(async function () {
|
||||
Object.assign(this, await loadFixture(fixture));
|
||||
});
|
||||
|
||||
shouldBehaveLikeProxy();
|
||||
});
|
||||
|
||||
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -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;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
describe('upgrade', async function () {
|
||||
it('upgrade a proxy by upgrading its beacon', async function () {
|
||||
const beacon = await UpgradeableBeacon.new(this.implementationV0.address, upgradeableBeaconAdmin);
|
||||
|
||||
const value = '10';
|
||||
const data = this.implementationV0.contract.methods.initializeNonPayableWithValue(value).encodeABI();
|
||||
const proxy = await BeaconProxy.new(beacon.address, data);
|
||||
|
||||
const dummy = new DummyImplementation(proxy.address);
|
||||
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));
|
||||
|
||||
// test initial values
|
||||
expect(await dummy.value()).to.bignumber.eq(value);
|
||||
expect(await proxy.value()).to.equal(value);
|
||||
|
||||
// test initial version
|
||||
expect(await dummy.version()).to.eq('V1');
|
||||
expect(await proxy.version()).to.equal('V1');
|
||||
|
||||
// upgrade beacon
|
||||
await beacon.upgradeTo(this.implementationV1.address, { from: upgradeableBeaconAdmin });
|
||||
await this.beacon.connect(this.admin).upgradeTo(this.v2);
|
||||
|
||||
// test upgraded version
|
||||
expect(await dummy.version()).to.eq('V2');
|
||||
expect(await proxy.version()).to.equal('V2');
|
||||
});
|
||||
|
||||
it('upgrade 2 proxies by upgrading shared beacon', async function () {
|
||||
const value1 = '10';
|
||||
const value2 = '42';
|
||||
const value1 = 10n;
|
||||
const data1 = this.v1.interface.encodeFunctionData('initializeNonPayableWithValue', [value1]);
|
||||
const proxy1 = await this.newBeaconProxy(this.beacon, data1).then(instance => this.v1.attach(instance));
|
||||
|
||||
const beacon = await UpgradeableBeacon.new(this.implementationV0.address, upgradeableBeaconAdmin);
|
||||
|
||||
const proxy1InitializeData = this.implementationV0.contract.methods
|
||||
.initializeNonPayableWithValue(value1)
|
||||
.encodeABI();
|
||||
const proxy1 = await BeaconProxy.new(beacon.address, proxy1InitializeData);
|
||||
|
||||
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);
|
||||
const value2 = 42n;
|
||||
const data2 = this.v1.interface.encodeFunctionData('initializeNonPayableWithValue', [value2]);
|
||||
const proxy2 = await this.newBeaconProxy(this.beacon, data2).then(instance => this.v1.attach(instance));
|
||||
|
||||
// test initial values
|
||||
expect(await dummy1.value()).to.bignumber.eq(value1);
|
||||
expect(await dummy2.value()).to.bignumber.eq(value2);
|
||||
expect(await proxy1.value()).to.equal(value1);
|
||||
expect(await proxy2.value()).to.equal(value2);
|
||||
|
||||
// test initial version
|
||||
expect(await dummy1.version()).to.eq('V1');
|
||||
expect(await dummy2.version()).to.eq('V1');
|
||||
expect(await proxy1.version()).to.equal('V1');
|
||||
expect(await proxy2.version()).to.equal('V1');
|
||||
|
||||
// upgrade beacon
|
||||
await beacon.upgradeTo(this.implementationV1.address, { from: upgradeableBeaconAdmin });
|
||||
await this.beacon.connect(this.admin).upgradeTo(this.v2);
|
||||
|
||||
// test upgraded version
|
||||
expect(await dummy1.version()).to.eq('V2');
|
||||
expect(await dummy2.version()).to.eq('V2');
|
||||
expect(await proxy1.version()).to.equal('V2');
|
||||
expect(await proxy2.version()).to.equal('V2');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -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 };
|
||||
}
|
||||
|
||||
describe('UpgradeableBeacon', function () {
|
||||
beforeEach(async function () {
|
||||
Object.assign(this, await loadFixture(fixture));
|
||||
});
|
||||
|
||||
it('cannot be created with non-contract implementation', async function () {
|
||||
await expectRevertCustomError(UpgradeableBeacon.new(other, owner), 'BeaconInvalidImplementation', [other]);
|
||||
});
|
||||
|
||||
context('once deployed', async function () {
|
||||
beforeEach('deploying beacon', async function () {
|
||||
this.v1 = await Implementation1.new();
|
||||
this.beacon = await UpgradeableBeacon.new(this.v1.address, owner);
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -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');
|
||||
// createProxy, initialOwner, accounts
|
||||
module.exports = function shouldBehaveLikeTransparentUpgradeableProxy() {
|
||||
before(async function () {
|
||||
const implementationV0 = await ethers.deployContract('DummyImplementation');
|
||||
const implementationV1 = await ethers.deployContract('DummyImplementation');
|
||||
|
||||
module.exports = function shouldBehaveLikeTransparentUpgradeableProxy(createProxy, initialOwner, accounts) {
|
||||
const [anotherAccount] = accounts;
|
||||
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()]),
|
||||
);
|
||||
|
||||
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);
|
||||
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,
|
||||
proxyAdminAddress,
|
||||
proxyAdmin,
|
||||
proxyAdminAsSigner,
|
||||
tx,
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
before(async function () {
|
||||
this.implementationV0 = (await DummyImplementation.new()).address;
|
||||
this.implementationV1 = (await DummyImplementation.new()).address;
|
||||
Object.assign(this, {
|
||||
implementationV0,
|
||||
implementationV1,
|
||||
createProxyWithImpersonatedProxyAdmin,
|
||||
});
|
||||
});
|
||||
|
||||
beforeEach(async function () {
|
||||
const initializeData = Buffer.from('');
|
||||
const { proxy, proxyAdminAddress } = await createProxyWithImpersonatedProxyAdmin(
|
||||
this.implementationV0,
|
||||
initializeData,
|
||||
);
|
||||
this.proxy = proxy;
|
||||
this.proxyAdminAddress = proxyAdminAddress;
|
||||
Object.assign(this, await this.createProxyWithImpersonatedProxyAdmin(this.implementationV0, '0x'));
|
||||
});
|
||||
|
||||
describe('implementation', function () {
|
||||
it('returns the current implementation address', async function () {
|
||||
const implementationAddress = await getAddressInSlot(this.proxy, ImplementationSlot);
|
||||
expect(implementationAddress).to.be.equal(this.implementationV0);
|
||||
expect(await getAddressInSlot(this.proxy, ImplementationSlot)).to.equal(this.implementationV0.target);
|
||||
});
|
||||
|
||||
it('delegates to the implementation', async function () {
|
||||
const dummy = new DummyImplementation(this.proxy.address);
|
||||
const value = await dummy.get();
|
||||
|
||||
expect(value).to.equal(true);
|
||||
expect(await this.instance.get()).to.be.true;
|
||||
});
|
||||
});
|
||||
|
||||
describe('proxy admin', function () {
|
||||
it('emits AdminChanged event during construction', async function () {
|
||||
await expectEvent.inConstruction(this.proxy, 'AdminChanged', {
|
||||
previousAdmin: ZERO_ADDRESS,
|
||||
newAdmin: this.proxyAdminAddress,
|
||||
});
|
||||
await expect(this.tx).to.emit(this.proxy, 'AdminChanged').withArgs(ethers.ZeroAddress, this.proxyAdmin.target);
|
||||
});
|
||||
|
||||
it('sets the proxy admin in storage with the correct initial owner', async function () {
|
||||
expect(await getAddressInSlot(this.proxy, AdminSlot)).to.be.equal(this.proxyAdminAddress);
|
||||
const proxyAdmin = await Ownable.at(this.proxyAdminAddress);
|
||||
expect(await proxyAdmin.owner()).to.be.equal(initialOwner);
|
||||
expect(await getAddressInSlot(this.proxy, AdminSlot)).to.equal(this.proxyAdmin.target);
|
||||
|
||||
expect(await this.proxyAdmin.owner()).to.equal(this.owner.address);
|
||||
});
|
||||
|
||||
it('can overwrite the admin by the implementation', async function () {
|
||||
const dummy = new DummyImplementation(this.proxy.address);
|
||||
await dummy.unsafeOverrideAdmin(anotherAccount);
|
||||
await this.instance.unsafeOverrideAdmin(this.other);
|
||||
|
||||
const ERC1967AdminSlotValue = await getAddressInSlot(this.proxy, AdminSlot);
|
||||
expect(ERC1967AdminSlotValue).to.be.equal(anotherAccount);
|
||||
expect(ERC1967AdminSlotValue).to.equal(this.other.address);
|
||||
expect(ERC1967AdminSlotValue).to.not.equal(this.proxyAdmin.address);
|
||||
|
||||
// Still allows previous admin to execute admin operations
|
||||
expect(ERC1967AdminSlotValue).to.not.equal(this.proxyAdminAddress);
|
||||
expectEvent(
|
||||
await this.proxy.upgradeToAndCall(this.implementationV1, '0x', { from: this.proxyAdminAddress }),
|
||||
'Upgraded',
|
||||
{
|
||||
implementation: this.implementationV1,
|
||||
},
|
||||
);
|
||||
await expect(this.proxy.connect(this.proxyAdminAsSigner).upgradeToAndCall(this.implementationV1, '0x'))
|
||||
.to.emit(this.proxy, 'Upgraded')
|
||||
.withArgs(this.implementationV1.target);
|
||||
});
|
||||
});
|
||||
|
||||
describe('upgradeToAndCall', function () {
|
||||
describe('without migrations', function () {
|
||||
beforeEach(async function () {
|
||||
this.behavior = await InitializableMock.new();
|
||||
this.behavior = await ethers.deployContract('InitializableMock');
|
||||
});
|
||||
|
||||
describe('when the call does not fail', function () {
|
||||
const initializeData = new InitializableMock('').contract.methods['initializeWithX(uint256)'](42).encodeABI();
|
||||
beforeEach(function () {
|
||||
this.initializeData = this.behavior.interface.encodeFunctionData('initializeWithX', [42n]);
|
||||
});
|
||||
|
||||
describe('when the sender is the admin', function () {
|
||||
const value = 1e5;
|
||||
const value = 10n ** 5n;
|
||||
|
||||
beforeEach(async function () {
|
||||
this.receipt = await this.proxy.upgradeToAndCall(this.behavior.address, initializeData, {
|
||||
from: this.proxyAdminAddress,
|
||||
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,
|
||||
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,
|
||||
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,
|
||||
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);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
@ -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();
|
||||
});
|
||||
|
||||
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -1,28 +1,36 @@
|
||||
const { expectEvent } = require('@openzeppelin/test-helpers');
|
||||
const { ethers } = require('hardhat');
|
||||
const { expect } = require('chai');
|
||||
const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
|
||||
|
||||
const { getAddressInSlot, ImplementationSlot } = require('../../helpers/erc1967');
|
||||
const { expectRevertCustomError } = require('../../helpers/customError');
|
||||
|
||||
const ERC1967Proxy = artifacts.require('ERC1967Proxy');
|
||||
const UUPSUpgradeableMock = artifacts.require('UUPSUpgradeableMock');
|
||||
const UUPSUpgradeableUnsafeMock = artifacts.require('UUPSUpgradeableUnsafeMock');
|
||||
const NonUpgradeableMock = artifacts.require('NonUpgradeableMock');
|
||||
const UUPSUnsupportedProxiableUUID = artifacts.require('UUPSUnsupportedProxiableUUID');
|
||||
const Clones = artifacts.require('$Clones');
|
||||
|
||||
contract('UUPSUpgradeable', function () {
|
||||
before(async function () {
|
||||
this.implInitial = await UUPSUpgradeableMock.new();
|
||||
this.implUpgradeOk = await UUPSUpgradeableMock.new();
|
||||
this.implUpgradeUnsafe = await UUPSUpgradeableUnsafeMock.new();
|
||||
this.implUpgradeNonUUPS = await NonUpgradeableMock.new();
|
||||
this.implUnsupportedUUID = await UUPSUnsupportedProxiableUUID.new();
|
||||
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)
|
||||
this.cloneFactory = await Clones.new();
|
||||
});
|
||||
const cloneFactory = await ethers.deployContract('$Clones');
|
||||
|
||||
const instance = await ethers
|
||||
.deployContract('ERC1967Proxy', [implInitial, '0x'])
|
||||
.then(proxy => implInitial.attach(proxy.target));
|
||||
|
||||
return {
|
||||
implInitial,
|
||||
implUpgradeOk,
|
||||
implUpgradeUnsafe,
|
||||
implUpgradeNonUUPS,
|
||||
implUnsupportedUUID,
|
||||
cloneFactory,
|
||||
instance,
|
||||
};
|
||||
}
|
||||
|
||||
describe('UUPSUpgradeable', function () {
|
||||
beforeEach(async function () {
|
||||
const { address } = await ERC1967Proxy.new(this.implInitial.address, '0x');
|
||||
this.instance = await UUPSUpgradeableMock.at(address);
|
||||
Object.assign(this, await loadFixture(fixture));
|
||||
});
|
||||
|
||||
it('has an interface version', async function () {
|
||||
@ -30,102 +38,83 @@ contract('UUPSUpgradeable', function () {
|
||||
});
|
||||
|
||||
it('upgrade to upgradeable implementation', async function () {
|
||||
const { receipt } = await this.instance.upgradeToAndCall(this.implUpgradeOk.address, '0x');
|
||||
expect(receipt.logs.filter(({ event }) => event === 'Upgraded').length).to.be.equal(1);
|
||||
expectEvent(receipt, 'Upgraded', { implementation: this.implUpgradeOk.address });
|
||||
expect(await getAddressInSlot(this.instance, ImplementationSlot)).to.be.equal(this.implUpgradeOk.address);
|
||||
await expect(this.instance.upgradeToAndCall(this.implUpgradeOk, '0x'))
|
||||
.to.emit(this.instance, 'Upgraded')
|
||||
.withArgs(this.implUpgradeOk.target);
|
||||
|
||||
expect(await getAddressInSlot(this.instance, ImplementationSlot)).to.equal(this.implUpgradeOk.target);
|
||||
});
|
||||
|
||||
it('upgrade to upgradeable implementation with call', async function () {
|
||||
expect(await this.instance.current()).to.be.bignumber.equal('0');
|
||||
expect(await this.instance.current()).to.equal(0n);
|
||||
|
||||
const { receipt } = await this.instance.upgradeToAndCall(
|
||||
this.implUpgradeOk.address,
|
||||
this.implUpgradeOk.contract.methods.increment().encodeABI(),
|
||||
);
|
||||
expect(receipt.logs.filter(({ event }) => event === 'Upgraded').length).to.be.equal(1);
|
||||
expectEvent(receipt, 'Upgraded', { implementation: this.implUpgradeOk.address });
|
||||
expect(await getAddressInSlot(this.instance, ImplementationSlot)).to.be.equal(this.implUpgradeOk.address);
|
||||
await expect(
|
||||
this.instance.upgradeToAndCall(this.implUpgradeOk, this.implUpgradeOk.interface.encodeFunctionData('increment')),
|
||||
)
|
||||
.to.emit(this.instance, 'Upgraded')
|
||||
.withArgs(this.implUpgradeOk.target);
|
||||
|
||||
expect(await 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);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user