This commit is contained in:
dexorder
2024-10-17 02:42:28 -04:00
commit 25def69c66
878 changed files with 112489 additions and 0 deletions

View 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,
});
});
});
});
};

View 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));
}
}

View 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);
});
});
});

View File

@@ -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();
});

View File

@@ -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);
});
});
});
});
});

View 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;
});
});
});
};

View 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');
});
});
});

View File

@@ -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);
});
});
});

View File

@@ -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);
});
});
});
});
});

View File

@@ -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);
});
});
};

View File

@@ -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();
});

View File

@@ -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);
});
});
});

View File

@@ -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);
});
});