dexorder
This commit is contained in:
160
lib_openzeppelin_contracts/test/proxy/Clones.behaviour.js
Normal file
160
lib_openzeppelin_contracts/test/proxy/Clones.behaviour.js
Normal file
@@ -0,0 +1,160 @@
|
||||
const { ethers } = require('hardhat');
|
||||
const { expect } = require('chai');
|
||||
|
||||
module.exports = function shouldBehaveLikeClone() {
|
||||
const assertProxyInitialization = function ({ value, balance }) {
|
||||
it('initializes the proxy', async function () {
|
||||
const dummy = await ethers.getContractAt('DummyImplementation', this.proxy);
|
||||
expect(await dummy.value()).to.equal(value);
|
||||
});
|
||||
|
||||
it('has expected balance', async function () {
|
||||
expect(await ethers.provider.getBalance(this.proxy)).to.equal(balance);
|
||||
});
|
||||
};
|
||||
|
||||
describe('construct with value', function () {
|
||||
const value = 10n;
|
||||
|
||||
it('factory has enough balance', async function () {
|
||||
await this.deployer.sendTransaction({ to: this.factory, value });
|
||||
|
||||
const instance = await this.createClone({ deployValue: value });
|
||||
await expect(instance.deploymentTransaction()).to.changeEtherBalances([this.factory, instance], [-value, value]);
|
||||
|
||||
expect(await ethers.provider.getBalance(instance)).to.equal(value);
|
||||
});
|
||||
|
||||
it('factory does not have enough balance', async function () {
|
||||
await expect(this.createClone({ deployValue: value }))
|
||||
.to.be.revertedWithCustomError(this.factory, 'InsufficientBalance')
|
||||
.withArgs(0n, value);
|
||||
});
|
||||
});
|
||||
|
||||
describe('initialization without parameters', function () {
|
||||
describe('non payable', function () {
|
||||
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 this.createClone({ initData: this.initializeData });
|
||||
});
|
||||
|
||||
assertProxyInitialization({
|
||||
value: expectedInitializedValue,
|
||||
balance: 0,
|
||||
});
|
||||
});
|
||||
|
||||
describe('when sending some balance', function () {
|
||||
const value = 10n ** 6n;
|
||||
|
||||
it('reverts', async function () {
|
||||
await expect(this.createClone({ initData: this.initializeData, initValue: value })).to.be.reverted;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('payable', function () {
|
||||
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 this.createClone({ initData: this.initializeData });
|
||||
});
|
||||
|
||||
assertProxyInitialization({
|
||||
value: expectedInitializedValue,
|
||||
balance: 0,
|
||||
});
|
||||
});
|
||||
|
||||
describe('when sending some balance', function () {
|
||||
const value = 10n ** 6n;
|
||||
|
||||
beforeEach('creating proxy', async function () {
|
||||
this.proxy = await this.createClone({ initData: this.initializeData, initValue: value });
|
||||
});
|
||||
|
||||
assertProxyInitialization({
|
||||
value: expectedInitializedValue,
|
||||
balance: value,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('initialization with parameters', function () {
|
||||
describe('non payable', function () {
|
||||
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 this.createClone({ initData: this.initializeData });
|
||||
});
|
||||
|
||||
assertProxyInitialization({
|
||||
value: expectedInitializedValue,
|
||||
balance: 0,
|
||||
});
|
||||
});
|
||||
|
||||
describe('when sending some balance', function () {
|
||||
const value = 10n ** 6n;
|
||||
|
||||
it('reverts', async function () {
|
||||
await expect(this.createClone({ initData: this.initializeData, initValue: value })).to.be.reverted;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('payable', function () {
|
||||
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 this.createClone({ initData: this.initializeData });
|
||||
});
|
||||
|
||||
assertProxyInitialization({
|
||||
value: expectedInitializedValue,
|
||||
balance: 0,
|
||||
});
|
||||
});
|
||||
|
||||
describe('when sending some balance', function () {
|
||||
const value = 10n ** 6n;
|
||||
|
||||
beforeEach('creating proxy', async function () {
|
||||
this.proxy = await this.createClone({ initData: this.initializeData, initValue: value });
|
||||
});
|
||||
|
||||
assertProxyInitialization({
|
||||
value: expectedInitializedValue,
|
||||
balance: value,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
18
lib_openzeppelin_contracts/test/proxy/Clones.t.sol
Normal file
18
lib_openzeppelin_contracts/test/proxy/Clones.t.sol
Normal file
@@ -0,0 +1,18 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
pragma solidity ^0.8.20;
|
||||
|
||||
import {Test} from "@forge-std/Test.sol";
|
||||
import {Clones} from "@openzeppelin/contracts/proxy/Clones.sol";
|
||||
|
||||
contract ClonesTest is Test {
|
||||
function testPredictDeterministicAddressSpillage(address implementation, bytes32 salt) public {
|
||||
address predicted = Clones.predictDeterministicAddress(implementation, salt);
|
||||
bytes32 spillage;
|
||||
/// @solidity memory-safe-assembly
|
||||
assembly {
|
||||
spillage := and(predicted, 0xffffffffffffffffffffffff0000000000000000000000000000000000000000)
|
||||
}
|
||||
assertEq(spillage, bytes32(0));
|
||||
}
|
||||
}
|
||||
95
lib_openzeppelin_contracts/test/proxy/Clones.test.js
Normal file
95
lib_openzeppelin_contracts/test/proxy/Clones.test.js
Normal file
@@ -0,0 +1,95 @@
|
||||
const { ethers } = require('hardhat');
|
||||
const { expect } = require('chai');
|
||||
const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
|
||||
|
||||
const shouldBehaveLikeClone = require('./Clones.behaviour');
|
||||
|
||||
async function fixture() {
|
||||
const [deployer] = await ethers.getSigners();
|
||||
|
||||
const factory = await ethers.deployContract('$Clones');
|
||||
const implementation = await ethers.deployContract('DummyImplementation');
|
||||
|
||||
const newClone = async (opts = {}) => {
|
||||
const clone = await factory.$clone.staticCall(implementation).then(address => implementation.attach(address));
|
||||
const tx = await (opts.deployValue
|
||||
? factory.$clone(implementation, ethers.Typed.uint256(opts.deployValue))
|
||||
: factory.$clone(implementation));
|
||||
if (opts.initData || opts.initValue) {
|
||||
await deployer.sendTransaction({ to: clone, value: opts.initValue ?? 0n, data: opts.initData ?? '0x' });
|
||||
}
|
||||
return Object.assign(clone, { deploymentTransaction: () => tx });
|
||||
};
|
||||
|
||||
const newCloneDeterministic = async (opts = {}) => {
|
||||
const salt = opts.salt ?? ethers.randomBytes(32);
|
||||
const clone = await factory.$cloneDeterministic
|
||||
.staticCall(implementation, salt)
|
||||
.then(address => implementation.attach(address));
|
||||
const tx = await (opts.deployValue
|
||||
? factory.$cloneDeterministic(implementation, salt, ethers.Typed.uint256(opts.deployValue))
|
||||
: factory.$cloneDeterministic(implementation, salt));
|
||||
if (opts.initData || opts.initValue) {
|
||||
await deployer.sendTransaction({ to: clone, value: opts.initValue ?? 0n, data: opts.initData ?? '0x' });
|
||||
}
|
||||
return Object.assign(clone, { deploymentTransaction: () => tx });
|
||||
};
|
||||
|
||||
return { deployer, factory, implementation, newClone, newCloneDeterministic };
|
||||
}
|
||||
|
||||
describe('Clones', function () {
|
||||
beforeEach(async function () {
|
||||
Object.assign(this, await loadFixture(fixture));
|
||||
});
|
||||
|
||||
describe('clone', function () {
|
||||
beforeEach(async function () {
|
||||
this.createClone = this.newClone;
|
||||
});
|
||||
|
||||
shouldBehaveLikeClone();
|
||||
});
|
||||
|
||||
describe('cloneDeterministic', function () {
|
||||
beforeEach(async function () {
|
||||
this.createClone = this.newCloneDeterministic;
|
||||
});
|
||||
|
||||
shouldBehaveLikeClone();
|
||||
|
||||
it('revert if address already used', async function () {
|
||||
const salt = ethers.randomBytes(32);
|
||||
|
||||
// deploy once
|
||||
await expect(this.factory.$cloneDeterministic(this.implementation, salt)).to.emit(
|
||||
this.factory,
|
||||
'return$cloneDeterministic_address_bytes32',
|
||||
);
|
||||
|
||||
// deploy twice
|
||||
await expect(this.factory.$cloneDeterministic(this.implementation, salt)).to.be.revertedWithCustomError(
|
||||
this.factory,
|
||||
'FailedDeployment',
|
||||
);
|
||||
});
|
||||
|
||||
it('address prediction', async function () {
|
||||
const salt = ethers.randomBytes(32);
|
||||
|
||||
const creationCode = ethers.concat([
|
||||
'0x3d602d80600a3d3981f3363d3d373d3d3d363d73',
|
||||
this.implementation.target,
|
||||
'0x5af43d82803e903d91602b57fd5bf3',
|
||||
]);
|
||||
|
||||
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);
|
||||
|
||||
await expect(this.factory.$cloneDeterministic(this.implementation, salt))
|
||||
.to.emit(this.factory, 'return$cloneDeterministic_address_bytes32')
|
||||
.withArgs(predicted);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,23 @@
|
||||
const { ethers } = require('hardhat');
|
||||
|
||||
const shouldBehaveLikeProxy = require('../Proxy.behaviour');
|
||||
const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
|
||||
|
||||
const fixture = async () => {
|
||||
const [nonContractAddress] = await ethers.getSigners();
|
||||
|
||||
const implementation = await ethers.deployContract('DummyImplementation');
|
||||
|
||||
const createProxy = (implementation, initData, opts) =>
|
||||
ethers.deployContract('ERC1967Proxy', [implementation, initData], opts);
|
||||
|
||||
return { nonContractAddress, implementation, createProxy };
|
||||
};
|
||||
|
||||
describe('ERC1967Proxy', function () {
|
||||
beforeEach(async function () {
|
||||
Object.assign(this, await loadFixture(fixture));
|
||||
});
|
||||
|
||||
shouldBehaveLikeProxy();
|
||||
});
|
||||
@@ -0,0 +1,162 @@
|
||||
const { ethers } = require('hardhat');
|
||||
const { expect } = require('chai');
|
||||
const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
|
||||
|
||||
const { getAddressInSlot, setSlot, ImplementationSlot, AdminSlot, BeaconSlot } = require('../../helpers/storage');
|
||||
|
||||
async function fixture() {
|
||||
const [, admin, anotherAccount] = await ethers.getSigners();
|
||||
|
||||
const utils = await ethers.deployContract('$ERC1967Utils');
|
||||
const v1 = await ethers.deployContract('DummyImplementation');
|
||||
const v2 = await ethers.deployContract('CallReceiverMock');
|
||||
|
||||
return { admin, anotherAccount, utils, v1, v2 };
|
||||
}
|
||||
|
||||
describe('ERC1967Utils', function () {
|
||||
beforeEach('setup', async function () {
|
||||
Object.assign(this, await loadFixture(fixture));
|
||||
});
|
||||
|
||||
describe('IMPLEMENTATION_SLOT', function () {
|
||||
beforeEach('set v1 implementation', async function () {
|
||||
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);
|
||||
expect(await getAddressInSlot(this.utils, ImplementationSlot)).to.equal(this.v1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('upgradeToAndCall', function () {
|
||||
it('sets implementation in storage and emits event', async function () {
|
||||
const newImplementation = this.v2;
|
||||
const tx = await this.utils.$upgradeToAndCall(newImplementation, '0x');
|
||||
|
||||
expect(await getAddressInSlot(this.utils, ImplementationSlot)).to.equal(newImplementation);
|
||||
await expect(tx).to.emit(this.utils, 'Upgraded').withArgs(newImplementation);
|
||||
});
|
||||
|
||||
it('reverts when implementation does not contain code', async function () {
|
||||
await expect(this.utils.$upgradeToAndCall(this.anotherAccount, '0x'))
|
||||
.to.be.revertedWithCustomError(this.utils, 'ERC1967InvalidImplementation')
|
||||
.withArgs(this.anotherAccount);
|
||||
});
|
||||
|
||||
describe('when data is empty', function () {
|
||||
it('reverts when value is sent', async function () {
|
||||
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.interface.encodeFunctionData('mockFunction');
|
||||
const tx = await this.utils.$upgradeToAndCall(this.v2, initializeData);
|
||||
await expect(tx).to.emit(await ethers.getContractAt('CallReceiverMock', this.utils), 'MockFunctionCalled');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('ADMIN_SLOT', function () {
|
||||
beforeEach('set admin', async function () {
|
||||
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(this.admin);
|
||||
expect(await getAddressInSlot(this.utils, AdminSlot)).to.equal(this.admin);
|
||||
});
|
||||
});
|
||||
|
||||
describe('changeAdmin', function () {
|
||||
it('sets admin in storage and emits event', async function () {
|
||||
const newAdmin = this.anotherAccount;
|
||||
const tx = await this.utils.$changeAdmin(newAdmin);
|
||||
|
||||
expect(await getAddressInSlot(this.utils, AdminSlot)).to.equal(newAdmin);
|
||||
await expect(tx).to.emit(this.utils, 'AdminChanged').withArgs(this.admin, newAdmin);
|
||||
});
|
||||
|
||||
it('reverts when setting the address zero as admin', async function () {
|
||||
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 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);
|
||||
expect(await getAddressInSlot(this.utils, BeaconSlot)).to.equal(this.beacon);
|
||||
});
|
||||
});
|
||||
|
||||
describe('upgradeBeaconToAndCall', function () {
|
||||
it('sets beacon in storage and emits event', async function () {
|
||||
const newBeacon = await ethers.deployContract('UpgradeableBeaconMock', [this.v2]);
|
||||
const tx = await this.utils.$upgradeBeaconToAndCall(newBeacon, '0x');
|
||||
|
||||
expect(await getAddressInSlot(this.utils, BeaconSlot)).to.equal(newBeacon);
|
||||
await expect(tx).to.emit(this.utils, 'BeaconUpgraded').withArgs(newBeacon);
|
||||
});
|
||||
|
||||
it('reverts when beacon does not contain code', async function () {
|
||||
await expect(this.utils.$upgradeBeaconToAndCall(this.anotherAccount, '0x'))
|
||||
.to.be.revertedWithCustomError(this.utils, 'ERC1967InvalidBeacon')
|
||||
.withArgs(this.anotherAccount);
|
||||
});
|
||||
|
||||
it("reverts when beacon's implementation does not contain code", async function () {
|
||||
const newBeacon = await ethers.deployContract('UpgradeableBeaconMock', [this.anotherAccount]);
|
||||
|
||||
await expect(this.utils.$upgradeBeaconToAndCall(newBeacon, '0x'))
|
||||
.to.be.revertedWithCustomError(this.utils, 'ERC1967InvalidImplementation')
|
||||
.withArgs(this.anotherAccount);
|
||||
});
|
||||
|
||||
describe('when data is empty', function () {
|
||||
it('reverts when value is sent', async function () {
|
||||
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.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 ethers.deployContract('UpgradeableBeaconReentrantMock');
|
||||
await expect(this.utils.$upgradeBeaconToAndCall(newBeacon, '0x'))
|
||||
.to.be.revertedWithCustomError(newBeacon, 'BeaconProxyBeaconSlotAddress')
|
||||
.withArgs(newBeacon);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
185
lib_openzeppelin_contracts/test/proxy/Proxy.behaviour.js
Normal file
185
lib_openzeppelin_contracts/test/proxy/Proxy.behaviour.js
Normal file
@@ -0,0 +1,185 @@
|
||||
const { ethers } = require('hardhat');
|
||||
const { expect } = require('chai');
|
||||
|
||||
const { getAddressInSlot, ImplementationSlot } = require('../helpers/storage');
|
||||
|
||||
module.exports = function shouldBehaveLikeProxy() {
|
||||
it('cannot be initialized with a non-contract address', async function () {
|
||||
const initializeData = '0x';
|
||||
const contractFactory = await ethers.getContractFactory('ERC1967Proxy');
|
||||
await expect(this.createProxy(this.nonContractAddress, initializeData))
|
||||
.to.be.revertedWithCustomError(contractFactory, 'ERC1967InvalidImplementation')
|
||||
.withArgs(this.nonContractAddress);
|
||||
});
|
||||
|
||||
const assertProxyInitialization = function ({ value, balance }) {
|
||||
it('sets the implementation address', async function () {
|
||||
expect(await getAddressInSlot(this.proxy, ImplementationSlot)).to.equal(this.implementation);
|
||||
});
|
||||
|
||||
it('initializes the proxy', async function () {
|
||||
const dummy = this.implementation.attach(this.proxy);
|
||||
expect(await dummy.value()).to.equal(value);
|
||||
});
|
||||
|
||||
it('has expected balance', async function () {
|
||||
expect(await ethers.provider.getBalance(this.proxy)).to.equal(balance);
|
||||
});
|
||||
};
|
||||
|
||||
describe('without initialization', function () {
|
||||
const initializeData = '0x';
|
||||
|
||||
describe('when not sending balance', function () {
|
||||
beforeEach('creating proxy', async function () {
|
||||
this.proxy = await this.createProxy(this.implementation, initializeData);
|
||||
});
|
||||
|
||||
assertProxyInitialization({ value: 0n, balance: 0n });
|
||||
});
|
||||
|
||||
describe('when sending some balance', function () {
|
||||
const value = 10n ** 5n;
|
||||
|
||||
it('reverts', async function () {
|
||||
await expect(this.createProxy(this.implementation, initializeData, { value })).to.be.reverted;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('initialization without parameters', function () {
|
||||
describe('non payable', function () {
|
||||
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 this.createProxy(this.implementation, this.initializeData);
|
||||
});
|
||||
|
||||
assertProxyInitialization({
|
||||
value: expectedInitializedValue,
|
||||
balance: 0n,
|
||||
});
|
||||
});
|
||||
|
||||
describe('when sending some balance', function () {
|
||||
const value = 10n ** 5n;
|
||||
|
||||
it('reverts', async function () {
|
||||
await expect(this.createProxy(this.implementation, this.initializeData, { value })).to.be.reverted;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('payable', function () {
|
||||
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 this.createProxy(this.implementation, this.initializeData);
|
||||
});
|
||||
|
||||
assertProxyInitialization({
|
||||
value: expectedInitializedValue,
|
||||
balance: 0n,
|
||||
});
|
||||
});
|
||||
|
||||
describe('when sending some balance', function () {
|
||||
const value = 10e5;
|
||||
|
||||
beforeEach('creating proxy', async function () {
|
||||
this.proxy = await this.createProxy(this.implementation, this.initializeData, { value });
|
||||
});
|
||||
|
||||
assertProxyInitialization({
|
||||
value: expectedInitializedValue,
|
||||
balance: value,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('initialization with parameters', function () {
|
||||
describe('non payable', function () {
|
||||
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 this.createProxy(this.implementation, this.initializeData);
|
||||
});
|
||||
|
||||
assertProxyInitialization({
|
||||
value: expectedInitializedValue,
|
||||
balance: 0,
|
||||
});
|
||||
});
|
||||
|
||||
describe('when sending some balance', function () {
|
||||
const value = 10e5;
|
||||
|
||||
it('reverts', async function () {
|
||||
await expect(this.createProxy(this.implementation, this.initializeData, { value })).to.be.reverted;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('payable', function () {
|
||||
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 this.createProxy(this.implementation, this.initializeData);
|
||||
});
|
||||
|
||||
assertProxyInitialization({
|
||||
value: expectedInitializedValue,
|
||||
balance: 0n,
|
||||
});
|
||||
});
|
||||
|
||||
describe('when sending some balance', function () {
|
||||
const value = 10n ** 5n;
|
||||
|
||||
beforeEach('creating proxy', async function () {
|
||||
this.proxy = await this.createProxy(this.implementation, this.initializeData, { value });
|
||||
});
|
||||
|
||||
assertProxyInitialization({
|
||||
value: expectedInitializedValue,
|
||||
balance: value,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('reverting initialization', function () {
|
||||
beforeEach(function () {
|
||||
this.initializeData = this.implementation.interface.encodeFunctionData('reverts');
|
||||
});
|
||||
|
||||
it('reverts', async function () {
|
||||
await expect(this.createProxy(this.implementation, this.initializeData)).to.be.reverted;
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
141
lib_openzeppelin_contracts/test/proxy/beacon/BeaconProxy.test.js
Normal file
141
lib_openzeppelin_contracts/test/proxy/beacon/BeaconProxy.test.js
Normal file
@@ -0,0 +1,141 @@
|
||||
const { ethers } = require('hardhat');
|
||||
const { expect } = require('chai');
|
||||
const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
|
||||
|
||||
const { getAddressInSlot, BeaconSlot } = require('../../helpers/storage');
|
||||
|
||||
async function fixture() {
|
||||
const [admin, other] = await ethers.getSigners();
|
||||
|
||||
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', function () {
|
||||
it('non-contract beacon', async function () {
|
||||
const notBeacon = this.other;
|
||||
|
||||
await expect(this.newBeaconProxy(notBeacon, '0x'))
|
||||
.to.be.revertedWithCustomError(this.factory, 'ERC1967InvalidBeacon')
|
||||
.withArgs(notBeacon);
|
||||
});
|
||||
|
||||
it('non-compliant beacon', async function () {
|
||||
const badBeacon = await ethers.deployContract('BadBeaconNoImpl');
|
||||
|
||||
// BadBeaconNoImpl does not provide `implementation()` has no fallback.
|
||||
// This causes ERC1967Utils._setBeacon to revert.
|
||||
await expect(this.newBeaconProxy(badBeacon, '0x')).to.be.revertedWithoutReason();
|
||||
});
|
||||
|
||||
it('non-contract implementation', async function () {
|
||||
const badBeacon = await ethers.deployContract('BadBeaconNotContract');
|
||||
|
||||
await expect(this.newBeaconProxy(badBeacon, '0x'))
|
||||
.to.be.revertedWithCustomError(this.factory, 'ERC1967InvalidImplementation')
|
||||
.withArgs(await badBeacon.implementation());
|
||||
});
|
||||
});
|
||||
|
||||
describe('initialization', function () {
|
||||
async function assertInitialized({ value, balance }) {
|
||||
const beaconAddress = await getAddressInSlot(this.proxy, BeaconSlot);
|
||||
expect(beaconAddress).to.equal(this.beacon);
|
||||
|
||||
const dummy = this.v1.attach(this.proxy);
|
||||
expect(await dummy.value()).to.equal(value);
|
||||
|
||||
expect(await ethers.provider.getBalance(this.proxy)).to.equal(balance);
|
||||
}
|
||||
|
||||
it('no initialization', async function () {
|
||||
this.proxy = await this.newBeaconProxy(this.beacon, '0x');
|
||||
await assertInitialized.bind(this)({ value: 0n, balance: 0n });
|
||||
});
|
||||
|
||||
it('non-payable initialization', async function () {
|
||||
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 = 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 () {
|
||||
await expect(this.newBeaconProxy(this.beacon, '0x', { value: 1n })).to.be.revertedWithCustomError(
|
||||
this.factory,
|
||||
'ERC1967NonPayable',
|
||||
);
|
||||
});
|
||||
|
||||
it('reverting initialization function', async function () {
|
||||
const data = this.v1.interface.encodeFunctionData('reverts');
|
||||
await expect(this.newBeaconProxy(this.beacon, data)).to.be.revertedWith('DummyImplementation reverted');
|
||||
});
|
||||
});
|
||||
|
||||
describe('upgrade', function () {
|
||||
it('upgrade a proxy by upgrading its beacon', async function () {
|
||||
const value = 10n;
|
||||
const data = this.v1.interface.encodeFunctionData('initializeNonPayableWithValue', [value]);
|
||||
const proxy = await this.newBeaconProxy(this.beacon, data).then(instance => this.v1.attach(instance));
|
||||
|
||||
// test initial values
|
||||
expect(await proxy.value()).to.equal(value);
|
||||
|
||||
// test initial version
|
||||
expect(await proxy.version()).to.equal('V1');
|
||||
|
||||
// upgrade beacon
|
||||
await this.beacon.connect(this.admin).upgradeTo(this.v2);
|
||||
|
||||
// test upgraded version
|
||||
expect(await proxy.version()).to.equal('V2');
|
||||
});
|
||||
|
||||
it('upgrade 2 proxies by upgrading shared beacon', async function () {
|
||||
const value1 = 10n;
|
||||
const data1 = this.v1.interface.encodeFunctionData('initializeNonPayableWithValue', [value1]);
|
||||
const proxy1 = await this.newBeaconProxy(this.beacon, data1).then(instance => this.v1.attach(instance));
|
||||
|
||||
const 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 proxy1.value()).to.equal(value1);
|
||||
expect(await proxy2.value()).to.equal(value2);
|
||||
|
||||
// test initial version
|
||||
expect(await proxy1.version()).to.equal('V1');
|
||||
expect(await proxy2.version()).to.equal('V1');
|
||||
|
||||
// upgrade beacon
|
||||
await this.beacon.connect(this.admin).upgradeTo(this.v2);
|
||||
|
||||
// test upgraded version
|
||||
expect(await proxy1.version()).to.equal('V2');
|
||||
expect(await proxy2.version()).to.equal('V2');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,55 @@
|
||||
const { ethers } = require('hardhat');
|
||||
const { expect } = require('chai');
|
||||
const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
|
||||
|
||||
async function fixture() {
|
||||
const [admin, other] = await ethers.getSigners();
|
||||
|
||||
const v1 = await ethers.deployContract('Implementation1');
|
||||
const v2 = await ethers.deployContract('Implementation2');
|
||||
const beacon = await ethers.deployContract('UpgradeableBeacon', [v1, admin]);
|
||||
|
||||
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 expect(ethers.deployContract('UpgradeableBeacon', [this.other, this.admin]))
|
||||
.to.be.revertedWithCustomError(this.beacon, 'BeaconInvalidImplementation')
|
||||
.withArgs(this.other);
|
||||
});
|
||||
|
||||
describe('once deployed', function () {
|
||||
it('emits Upgraded event to the first implementation', async function () {
|
||||
await expect(this.beacon.deploymentTransaction()).to.emit(this.beacon, 'Upgraded').withArgs(this.v1);
|
||||
});
|
||||
|
||||
it('returns implementation', async function () {
|
||||
expect(await this.beacon.implementation()).to.equal(this.v1);
|
||||
});
|
||||
|
||||
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);
|
||||
|
||||
expect(await this.beacon.implementation()).to.equal(this.v2);
|
||||
});
|
||||
|
||||
it('cannot be upgraded to a non-contract', async function () {
|
||||
await expect(this.beacon.connect(this.admin).upgradeTo(this.other))
|
||||
.to.be.revertedWithCustomError(this.beacon, 'BeaconInvalidImplementation')
|
||||
.withArgs(this.other);
|
||||
});
|
||||
|
||||
it('cannot be upgraded by other account', async function () {
|
||||
await expect(this.beacon.connect(this.other).upgradeTo(this.v2))
|
||||
.to.be.revertedWithCustomError(this.beacon, 'OwnableUnauthorizedAccount')
|
||||
.withArgs(this.other);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,82 @@
|
||||
const { ethers } = require('hardhat');
|
||||
const { expect } = require('chai');
|
||||
const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
|
||||
|
||||
const { getAddressInSlot, ImplementationSlot } = require('../../helpers/storage');
|
||||
|
||||
async function fixture() {
|
||||
const [admin, other] = await ethers.getSigners();
|
||||
|
||||
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 () {
|
||||
Object.assign(this, await loadFixture(fixture));
|
||||
});
|
||||
|
||||
it('has an owner', async function () {
|
||||
expect(await this.proxyAdmin.owner()).to.equal(this.admin);
|
||||
});
|
||||
|
||||
it('has an interface version', async function () {
|
||||
expect(await this.proxyAdmin.UPGRADE_INTERFACE_VERSION()).to.equal('5.0.0');
|
||||
});
|
||||
|
||||
describe('without data', function () {
|
||||
describe('with unauthorized account', function () {
|
||||
it('fails to upgrade', async function () {
|
||||
await expect(this.proxyAdmin.connect(this.other).upgradeAndCall(this.proxy, this.v2, '0x'))
|
||||
.to.be.revertedWithCustomError(this.proxyAdmin, 'OwnableUnauthorizedAccount')
|
||||
.withArgs(this.other);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with authorized account', function () {
|
||||
it('upgrades implementation', async function () {
|
||||
await this.proxyAdmin.connect(this.admin).upgradeAndCall(this.proxy, this.v2, '0x');
|
||||
expect(await getAddressInSlot(this.proxy, ImplementationSlot)).to.equal(this.v2);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('with data', function () {
|
||||
describe('with unauthorized account', function () {
|
||||
it('fails to upgrade', async function () {
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with authorized account', function () {
|
||||
describe('with invalid callData', function () {
|
||||
it('fails to upgrade', async function () {
|
||||
const data = '0x12345678';
|
||||
await expect(this.proxyAdmin.connect(this.admin).upgradeAndCall(this.proxy, this.v2, data)).to.be.reverted;
|
||||
});
|
||||
});
|
||||
|
||||
describe('with valid callData', function () {
|
||||
it('upgrades implementation', async function () {
|
||||
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.equal(this.v2);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,357 @@
|
||||
const { ethers } = require('hardhat');
|
||||
const { expect } = require('chai');
|
||||
|
||||
const { impersonate } = require('../../helpers/account');
|
||||
const { getAddressInSlot, ImplementationSlot, AdminSlot } = require('../../helpers/storage');
|
||||
|
||||
// createProxy, initialOwner, accounts
|
||||
module.exports = function shouldBehaveLikeTransparentUpgradeableProxy() {
|
||||
before(async function () {
|
||||
const implementationV0 = await ethers.deployContract('DummyImplementation');
|
||||
const implementationV1 = await ethers.deployContract('DummyImplementation');
|
||||
|
||||
const createProxyWithImpersonatedProxyAdmin = async (logic, initData, opts = undefined) => {
|
||||
const [proxy, tx] = await this.createProxy(logic, initData, opts).then(instance =>
|
||||
Promise.all([ethers.getContractAt('ITransparentUpgradeableProxy', instance), instance.deploymentTransaction()]),
|
||||
);
|
||||
|
||||
const proxyAdmin = await ethers.getContractAt(
|
||||
'ProxyAdmin',
|
||||
ethers.getCreateAddress({ from: proxy.target, nonce: 1n }),
|
||||
);
|
||||
const proxyAdminAsSigner = await proxyAdmin.getAddress().then(impersonate);
|
||||
|
||||
return {
|
||||
instance: logic.attach(proxy.target), // attaching proxy directly works well for everything except for event resolution
|
||||
proxy,
|
||||
proxyAdmin,
|
||||
proxyAdminAsSigner,
|
||||
tx,
|
||||
};
|
||||
};
|
||||
|
||||
Object.assign(this, {
|
||||
implementationV0,
|
||||
implementationV1,
|
||||
createProxyWithImpersonatedProxyAdmin,
|
||||
});
|
||||
});
|
||||
|
||||
beforeEach(async function () {
|
||||
Object.assign(this, await this.createProxyWithImpersonatedProxyAdmin(this.implementationV0, '0x'));
|
||||
});
|
||||
|
||||
describe('implementation', function () {
|
||||
it('returns the current implementation address', async function () {
|
||||
expect(await getAddressInSlot(this.proxy, ImplementationSlot)).to.equal(this.implementationV0);
|
||||
});
|
||||
|
||||
it('delegates to the implementation', async function () {
|
||||
expect(await this.instance.get()).to.be.true;
|
||||
});
|
||||
});
|
||||
|
||||
describe('proxy admin', function () {
|
||||
it('emits AdminChanged event during construction', async function () {
|
||||
await expect(this.tx).to.emit(this.proxy, 'AdminChanged').withArgs(ethers.ZeroAddress, this.proxyAdmin);
|
||||
});
|
||||
|
||||
it('sets the proxy admin in storage with the correct initial owner', async function () {
|
||||
expect(await getAddressInSlot(this.proxy, AdminSlot)).to.equal(this.proxyAdmin);
|
||||
|
||||
expect(await this.proxyAdmin.owner()).to.equal(this.owner);
|
||||
});
|
||||
|
||||
it('can overwrite the admin by the implementation', async function () {
|
||||
await this.instance.unsafeOverrideAdmin(this.other);
|
||||
|
||||
const ERC1967AdminSlotValue = await getAddressInSlot(this.proxy, AdminSlot);
|
||||
expect(ERC1967AdminSlotValue).to.equal(this.other);
|
||||
expect(ERC1967AdminSlotValue).to.not.equal(this.proxyAdmin);
|
||||
|
||||
// Still allows previous admin to execute admin operations
|
||||
await expect(this.proxy.connect(this.proxyAdminAsSigner).upgradeToAndCall(this.implementationV1, '0x'))
|
||||
.to.emit(this.proxy, 'Upgraded')
|
||||
.withArgs(this.implementationV1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('upgradeToAndCall', function () {
|
||||
describe('without migrations', function () {
|
||||
beforeEach(async function () {
|
||||
this.behavior = await ethers.deployContract('InitializableMock');
|
||||
});
|
||||
|
||||
describe('when the call does not fail', function () {
|
||||
beforeEach(function () {
|
||||
this.initializeData = this.behavior.interface.encodeFunctionData('initializeWithX', [42n]);
|
||||
});
|
||||
|
||||
describe('when the sender is the admin', function () {
|
||||
const value = 10n ** 5n;
|
||||
|
||||
beforeEach(async function () {
|
||||
this.tx = await this.proxy
|
||||
.connect(this.proxyAdminAsSigner)
|
||||
.upgradeToAndCall(this.behavior, this.initializeData, {
|
||||
value,
|
||||
});
|
||||
});
|
||||
|
||||
it('upgrades to the requested implementation', async function () {
|
||||
expect(await getAddressInSlot(this.proxy, ImplementationSlot)).to.equal(this.behavior);
|
||||
});
|
||||
|
||||
it('emits an event', async function () {
|
||||
await expect(this.tx).to.emit(this.proxy, 'Upgraded').withArgs(this.behavior);
|
||||
});
|
||||
|
||||
it('calls the initializer function', async function () {
|
||||
expect(await this.behavior.attach(this.proxy).x()).to.equal(42n);
|
||||
});
|
||||
|
||||
it('sends given value to the proxy', async function () {
|
||||
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
|
||||
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 expect(this.proxy.connect(this.other).upgradeToAndCall(this.behavior, this.initializeData)).to.be
|
||||
.reverted;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the call does fail', function () {
|
||||
beforeEach(function () {
|
||||
this.initializeData = this.behavior.interface.encodeFunctionData('fail');
|
||||
});
|
||||
|
||||
it('reverts', async function () {
|
||||
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 = 10n ** 5n;
|
||||
|
||||
describe('when upgrading to V1', function () {
|
||||
beforeEach(async function () {
|
||||
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 () {
|
||||
expect(await getAddressInSlot(this.proxy, ImplementationSlot)).to.equal(this.behaviorV1);
|
||||
|
||||
await expect(this.tx).to.emit(this.proxy, 'Upgraded').withArgs(this.behaviorV1);
|
||||
});
|
||||
|
||||
it("calls the 'initialize' function and sends given value to the proxy", async function () {
|
||||
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 () {
|
||||
beforeEach(async function () {
|
||||
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 () {
|
||||
expect(await getAddressInSlot(this.proxy, ImplementationSlot)).to.equal(this.behaviorV2);
|
||||
|
||||
await expect(this.tx).to.emit(this.proxy, 'Upgraded').withArgs(this.behaviorV2);
|
||||
});
|
||||
|
||||
it("calls the 'migrate' function and sends given value to the proxy", async function () {
|
||||
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 () {
|
||||
beforeEach(async function () {
|
||||
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 () {
|
||||
expect(await getAddressInSlot(this.proxy, ImplementationSlot)).to.equal(this.behaviorV3);
|
||||
|
||||
await expect(this.tx).to.emit(this.proxy, 'Upgraded').withArgs(this.behaviorV3);
|
||||
});
|
||||
|
||||
it("calls the 'migrate' function and sends given value to the proxy", async function () {
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the sender is not the admin', function () {
|
||||
it('reverts', async function () {
|
||||
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;
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('transparent proxy', function () {
|
||||
beforeEach('creating proxy', async function () {
|
||||
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 () {
|
||||
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 () {
|
||||
await expect(this.proxy.connect(this.proxyAdminAsSigner).upgradeToAndCall(this.clashingImplV1, '0x'))
|
||||
.to.emit(this.proxy, 'Upgraded')
|
||||
.withArgs(this.clashingImplV1);
|
||||
});
|
||||
|
||||
it('delegates the call to implementation when sender is not the admin', async function () {
|
||||
await expect(this.proxy.connect(this.other).upgradeToAndCall(this.clashingImplV1, '0x'))
|
||||
.to.emit(this.instance, 'ClashingImplementationCall')
|
||||
.to.not.emit(this.proxy, 'Upgraded');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('regression', function () {
|
||||
const initializeData = '0x';
|
||||
|
||||
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,
|
||||
);
|
||||
|
||||
await instance.setValue(42n);
|
||||
|
||||
// `getValue` is not available in impl1
|
||||
await expect(impl2.attach(instance).getValue()).to.be.reverted;
|
||||
|
||||
// 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 function () {
|
||||
const impl1 = await ethers.deployContract('Implementation1');
|
||||
const impl2 = await ethers.deployContract('Implementation2');
|
||||
const { instance, proxy, proxyAdminAsSigner } = await this.createProxyWithImpersonatedProxyAdmin(
|
||||
impl2,
|
||||
initializeData,
|
||||
);
|
||||
|
||||
await instance.setValue(42n);
|
||||
|
||||
// `getValue` is available in impl2
|
||||
expect(await impl2.attach(instance).getValue()).to.equal(42n);
|
||||
|
||||
// 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 function () {
|
||||
const impl1 = await ethers.deployContract('Implementation1');
|
||||
const impl3 = await ethers.deployContract('Implementation3');
|
||||
const { instance, proxy, proxyAdminAsSigner } = await this.createProxyWithImpersonatedProxyAdmin(
|
||||
impl1,
|
||||
initializeData,
|
||||
);
|
||||
|
||||
await instance.setValue(42n);
|
||||
|
||||
await proxy.connect(proxyAdminAsSigner).upgradeToAndCall(impl3, '0x');
|
||||
|
||||
expect(await impl3.attach(instance).getValue(8n)).to.equal(50n);
|
||||
});
|
||||
|
||||
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,
|
||||
);
|
||||
|
||||
await proxy.connect(proxyAdminAsSigner).upgradeToAndCall(impl4, '0x');
|
||||
|
||||
await this.other.sendTransaction({ to: proxy });
|
||||
|
||||
expect(await impl4.attach(instance).getValue()).to.equal(1n);
|
||||
});
|
||||
|
||||
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,
|
||||
);
|
||||
|
||||
await proxy.connect(proxyAdminAsSigner).upgradeToAndCall(impl2, '0x');
|
||||
|
||||
await expect(this.other.sendTransaction({ to: proxy })).to.be.reverted;
|
||||
|
||||
expect(await impl2.attach(instance).getValue()).to.equal(0n);
|
||||
});
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
const { ethers } = require('hardhat');
|
||||
const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
|
||||
|
||||
const shouldBehaveLikeProxy = require('../Proxy.behaviour');
|
||||
const shouldBehaveLikeTransparentUpgradeableProxy = require('./TransparentUpgradeableProxy.behaviour');
|
||||
|
||||
async function fixture() {
|
||||
const [owner, other, ...accounts] = await ethers.getSigners();
|
||||
|
||||
const implementation = await ethers.deployContract('DummyImplementation');
|
||||
|
||||
const createProxy = function (logic, initData, opts = undefined) {
|
||||
return ethers.deployContract('TransparentUpgradeableProxy', [logic, owner, initData], opts);
|
||||
};
|
||||
|
||||
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();
|
||||
});
|
||||
@@ -0,0 +1,216 @@
|
||||
const { ethers } = require('hardhat');
|
||||
const { expect } = require('chai');
|
||||
const { MAX_UINT64 } = require('../../helpers/constants');
|
||||
|
||||
describe('Initializable', function () {
|
||||
describe('basic testing without inheritance', function () {
|
||||
beforeEach('deploying', async function () {
|
||||
this.mock = await ethers.deployContract('InitializableMock');
|
||||
});
|
||||
|
||||
describe('before initialize', function () {
|
||||
it('initializer has not run', async function () {
|
||||
expect(await this.mock.initializerRan()).to.be.false;
|
||||
});
|
||||
|
||||
it('_initializing returns false before initialization', async function () {
|
||||
expect(await this.mock.isInitializing()).to.be.false;
|
||||
});
|
||||
});
|
||||
|
||||
describe('after initialize', function () {
|
||||
beforeEach('initializing', async function () {
|
||||
await this.mock.initialize();
|
||||
});
|
||||
|
||||
it('initializer has run', async function () {
|
||||
expect(await this.mock.initializerRan()).to.be.true;
|
||||
});
|
||||
|
||||
it('_initializing returns false after initialization', async function () {
|
||||
expect(await this.mock.isInitializing()).to.be.false;
|
||||
});
|
||||
|
||||
it('initializer does not run again', async function () {
|
||||
await expect(this.mock.initialize()).to.be.revertedWithCustomError(this.mock, 'InvalidInitialization');
|
||||
});
|
||||
});
|
||||
|
||||
describe('nested under an initializer', function () {
|
||||
it('initializer modifier reverts', async function () {
|
||||
await expect(this.mock.initializerNested()).to.be.revertedWithCustomError(this.mock, 'InvalidInitialization');
|
||||
});
|
||||
|
||||
it('onlyInitializing modifier succeeds', async function () {
|
||||
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 expect(this.mock.initializeOnlyInitializing()).to.be.revertedWithCustomError(this.mock, 'NotInitializing');
|
||||
});
|
||||
});
|
||||
|
||||
it('nested initializer can run during construction', async function () {
|
||||
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 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.mock = await ethers.deployContract('ReinitializerMock');
|
||||
});
|
||||
|
||||
it('can reinitialize', async function () {
|
||||
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.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.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.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.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.mock.disableInitializers();
|
||||
await expect(this.mock.initialize()).to.be.revertedWithCustomError(this.mock, 'InvalidInitialization');
|
||||
});
|
||||
|
||||
it('prevents re-initialization', async function () {
|
||||
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.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 mock = await ethers.deployContract('ConstructorInitializableMock');
|
||||
await expect(mock.deploymentTransaction()).to.emit(mock, 'Initialized').withArgs(1n);
|
||||
});
|
||||
|
||||
it('initialization emits event', async function () {
|
||||
const mock = await ethers.deployContract('ReinitializerMock');
|
||||
await expect(mock.initialize()).to.emit(mock, 'Initialized').withArgs(1n);
|
||||
});
|
||||
|
||||
it('reinitialization emits event', async function () {
|
||||
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 mock = await ethers.deployContract('ReinitializerMock');
|
||||
|
||||
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 = 12n;
|
||||
const gramps = '56';
|
||||
const father = 34n;
|
||||
const child = 78n;
|
||||
|
||||
beforeEach('deploying', async function () {
|
||||
this.mock = await ethers.deployContract('SampleChild');
|
||||
await this.mock.initialize(mother, gramps, father, child);
|
||||
});
|
||||
|
||||
it('initializes human', async function () {
|
||||
expect(await this.mock.isHuman()).to.be.true;
|
||||
});
|
||||
|
||||
it('initializes mother', async function () {
|
||||
expect(await this.mock.mother()).to.equal(mother);
|
||||
});
|
||||
|
||||
it('initializes gramps', async function () {
|
||||
expect(await this.mock.gramps()).to.equal(gramps);
|
||||
});
|
||||
|
||||
it('initializes father', async function () {
|
||||
expect(await this.mock.father()).to.equal(father);
|
||||
});
|
||||
|
||||
it('initializes child', async function () {
|
||||
expect(await this.mock.child()).to.equal(child);
|
||||
});
|
||||
});
|
||||
|
||||
describe('disabling initialization', function () {
|
||||
it('old and new patterns in bad sequence', async function () {
|
||||
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 ethers.deployContract('DisableOk');
|
||||
await expect(ok.deploymentTransaction())
|
||||
.to.emit(ok, 'Initialized')
|
||||
.withArgs(1n)
|
||||
.to.emit(ok, 'Initialized')
|
||||
.withArgs(MAX_UINT64);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,120 @@
|
||||
const { ethers } = require('hardhat');
|
||||
const { expect } = require('chai');
|
||||
const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
|
||||
|
||||
const { getAddressInSlot, ImplementationSlot } = require('../../helpers/storage');
|
||||
|
||||
async function fixture() {
|
||||
const implInitial = await ethers.deployContract('UUPSUpgradeableMock');
|
||||
const implUpgradeOk = await ethers.deployContract('UUPSUpgradeableMock');
|
||||
const implUpgradeUnsafe = await ethers.deployContract('UUPSUpgradeableUnsafeMock');
|
||||
const implUpgradeNonUUPS = await ethers.deployContract('NonUpgradeableMock');
|
||||
const implUnsupportedUUID = await ethers.deployContract('UUPSUnsupportedProxiableUUID');
|
||||
// Used for testing non ERC1967 compliant proxies (clones are proxies that don't use the ERC1967 implementation slot)
|
||||
const cloneFactory = await ethers.deployContract('$Clones');
|
||||
|
||||
const instance = await ethers
|
||||
.deployContract('ERC1967Proxy', [implInitial, '0x'])
|
||||
.then(proxy => implInitial.attach(proxy.target));
|
||||
|
||||
return {
|
||||
implInitial,
|
||||
implUpgradeOk,
|
||||
implUpgradeUnsafe,
|
||||
implUpgradeNonUUPS,
|
||||
implUnsupportedUUID,
|
||||
cloneFactory,
|
||||
instance,
|
||||
};
|
||||
}
|
||||
|
||||
describe('UUPSUpgradeable', function () {
|
||||
beforeEach(async function () {
|
||||
Object.assign(this, await loadFixture(fixture));
|
||||
});
|
||||
|
||||
it('has an interface version', async function () {
|
||||
expect(await this.instance.UPGRADE_INTERFACE_VERSION()).to.equal('5.0.0');
|
||||
});
|
||||
|
||||
it('upgrade to upgradeable implementation', async function () {
|
||||
await expect(this.instance.upgradeToAndCall(this.implUpgradeOk, '0x'))
|
||||
.to.emit(this.instance, 'Upgraded')
|
||||
.withArgs(this.implUpgradeOk);
|
||||
|
||||
expect(await getAddressInSlot(this.instance, ImplementationSlot)).to.equal(this.implUpgradeOk);
|
||||
});
|
||||
|
||||
it('upgrade to upgradeable implementation with call', async function () {
|
||||
expect(await this.instance.current()).to.equal(0n);
|
||||
|
||||
await expect(
|
||||
this.instance.upgradeToAndCall(this.implUpgradeOk, this.implUpgradeOk.interface.encodeFunctionData('increment')),
|
||||
)
|
||||
.to.emit(this.instance, 'Upgraded')
|
||||
.withArgs(this.implUpgradeOk);
|
||||
|
||||
expect(await getAddressInSlot(this.instance, ImplementationSlot)).to.equal(this.implUpgradeOk);
|
||||
|
||||
expect(await this.instance.current()).to.equal(1n);
|
||||
});
|
||||
|
||||
it('calling upgradeTo on the implementation reverts', async function () {
|
||||
await expect(this.implInitial.upgradeToAndCall(this.implUpgradeOk, '0x')).to.be.revertedWithCustomError(
|
||||
this.implInitial,
|
||||
'UUPSUnauthorizedCallContext',
|
||||
);
|
||||
});
|
||||
|
||||
it('calling upgradeToAndCall on the implementation reverts', async function () {
|
||||
await expect(
|
||||
this.implInitial.upgradeToAndCall(
|
||||
this.implUpgradeOk,
|
||||
this.implUpgradeOk.interface.encodeFunctionData('increment'),
|
||||
),
|
||||
).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 instance = await this.cloneFactory.$clone
|
||||
.staticCall(this.implUpgradeOk)
|
||||
.then(address => this.implInitial.attach(address));
|
||||
await this.cloneFactory.$clone(this.implUpgradeOk);
|
||||
|
||||
await expect(instance.upgradeToAndCall(this.implUpgradeUnsafe, '0x')).to.be.revertedWithCustomError(
|
||||
instance,
|
||||
'UUPSUnauthorizedCallContext',
|
||||
);
|
||||
});
|
||||
|
||||
it('rejects upgrading to an unsupported UUID', async function () {
|
||||
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 () {
|
||||
await expect(this.instance.upgradeToAndCall(this.implUpgradeUnsafe, '0x'))
|
||||
.to.emit(this.instance, 'Upgraded')
|
||||
.withArgs(this.implUpgradeUnsafe);
|
||||
|
||||
expect(await getAddressInSlot(this.instance, ImplementationSlot)).to.equal(this.implUpgradeUnsafe);
|
||||
});
|
||||
|
||||
// delegate to a non existing upgradeTo function causes a low level revert
|
||||
it('reject upgrade to non uups implementation', async function () {
|
||||
await expect(this.instance.upgradeToAndCall(this.implUpgradeNonUUPS, '0x'))
|
||||
.to.be.revertedWithCustomError(this.instance, 'ERC1967InvalidImplementation')
|
||||
.withArgs(this.implUpgradeNonUUPS);
|
||||
});
|
||||
|
||||
it('reject proxy address as implementation', async function () {
|
||||
const otherInstance = await ethers
|
||||
.deployContract('ERC1967Proxy', [this.implInitial, '0x'])
|
||||
.then(proxy => this.implInitial.attach(proxy.target));
|
||||
|
||||
await expect(this.instance.upgradeToAndCall(otherInstance, '0x'))
|
||||
.to.be.revertedWithCustomError(this.instance, 'ERC1967InvalidImplementation')
|
||||
.withArgs(otherInstance);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user