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