const { ethers } = require('hardhat'); const { expect } = require('chai'); const { getAddressInSlot, ImplementationSlot, AdminSlot } = require('../../helpers/erc1967'); const { impersonate } = require('../../helpers/account'); // 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.target); }); 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.target); }); it('sets the proxy admin in storage with the correct initial owner', async function () { expect(await getAddressInSlot(this.proxy, AdminSlot)).to.equal(this.proxyAdmin.target); expect(await this.proxyAdmin.owner()).to.equal(this.owner.address); }); it('can overwrite the admin by the implementation', async function () { await this.instance.unsafeOverrideAdmin(this.other); const ERC1967AdminSlotValue = await getAddressInSlot(this.proxy, AdminSlot); expect(ERC1967AdminSlotValue).to.equal(this.other.address); expect(ERC1967AdminSlotValue).to.not.equal(this.proxyAdmin.address); // 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.target); }); }); 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.target); }); it('emits an event', async function () { await expect(this.tx).to.emit(this.proxy, 'Upgraded').withArgs(this.behavior.target); }); it('calls the initializer function', async function () { 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.target); await expect(this.tx).to.emit(this.proxy, 'Upgraded').withArgs(this.behaviorV1.target); }); it("calls the 'initialize' function and sends given value to the proxy", async function () { 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.target); await expect(this.tx).to.emit(this.proxy, 'Upgraded').withArgs(this.behaviorV2.target); }); it("calls the 'migrate' function and sends given value to the proxy", async function () { 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.target); await expect(this.tx).to.emit(this.proxy, 'Upgraded').withArgs(this.behaviorV3.target); }); it("calls the 'migrate' function and sends given value to the proxy", async function () { 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.target); }); 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); }); }); };