|
|
|
@ -1,261 +1,223 @@ |
|
|
|
|
const { BN, expectRevert, expectEvent, constants } = require('@openzeppelin/test-helpers'); |
|
|
|
|
const { ZERO_ADDRESS } = constants; |
|
|
|
|
const { getAddressInSlot, ImplementationSlot, AdminSlot } = require('../../helpers/erc1967'); |
|
|
|
|
const { expectRevertCustomError } = require('../../helpers/customError'); |
|
|
|
|
|
|
|
|
|
const { ethers } = require('hardhat'); |
|
|
|
|
const { expect } = require('chai'); |
|
|
|
|
const { ethers, web3 } = require('hardhat'); |
|
|
|
|
|
|
|
|
|
const { getAddressInSlot, ImplementationSlot, AdminSlot } = require('../../helpers/erc1967'); |
|
|
|
|
const { impersonate } = require('../../helpers/account'); |
|
|
|
|
|
|
|
|
|
const Implementation1 = artifacts.require('Implementation1'); |
|
|
|
|
const Implementation2 = artifacts.require('Implementation2'); |
|
|
|
|
const Implementation3 = artifacts.require('Implementation3'); |
|
|
|
|
const Implementation4 = artifacts.require('Implementation4'); |
|
|
|
|
const MigratableMockV1 = artifacts.require('MigratableMockV1'); |
|
|
|
|
const MigratableMockV2 = artifacts.require('MigratableMockV2'); |
|
|
|
|
const MigratableMockV3 = artifacts.require('MigratableMockV3'); |
|
|
|
|
const InitializableMock = artifacts.require('InitializableMock'); |
|
|
|
|
const DummyImplementation = artifacts.require('DummyImplementation'); |
|
|
|
|
const ClashingImplementation = artifacts.require('ClashingImplementation'); |
|
|
|
|
const Ownable = artifacts.require('Ownable'); |
|
|
|
|
|
|
|
|
|
module.exports = function shouldBehaveLikeTransparentUpgradeableProxy(createProxy, initialOwner, accounts) { |
|
|
|
|
const [anotherAccount] = accounts; |
|
|
|
|
|
|
|
|
|
async function createProxyWithImpersonatedProxyAdmin(logic, initData, opts = undefined) { |
|
|
|
|
const proxy = await createProxy(logic, initData, opts); |
|
|
|
|
|
|
|
|
|
// Expect proxy admin to be the first and only contract created by the proxy
|
|
|
|
|
const proxyAdminAddress = ethers.getCreateAddress({ from: proxy.address, nonce: 1 }); |
|
|
|
|
await impersonate(proxyAdminAddress); |
|
|
|
|
|
|
|
|
|
return { |
|
|
|
|
proxy, |
|
|
|
|
proxyAdminAddress, |
|
|
|
|
// 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, |
|
|
|
|
}; |
|
|
|
|
}; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
before(async function () { |
|
|
|
|
this.implementationV0 = (await DummyImplementation.new()).address; |
|
|
|
|
this.implementationV1 = (await DummyImplementation.new()).address; |
|
|
|
|
Object.assign(this, { |
|
|
|
|
implementationV0, |
|
|
|
|
implementationV1, |
|
|
|
|
createProxyWithImpersonatedProxyAdmin, |
|
|
|
|
}); |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
beforeEach(async function () { |
|
|
|
|
const initializeData = Buffer.from(''); |
|
|
|
|
const { proxy, proxyAdminAddress } = await createProxyWithImpersonatedProxyAdmin( |
|
|
|
|
this.implementationV0, |
|
|
|
|
initializeData, |
|
|
|
|
); |
|
|
|
|
this.proxy = proxy; |
|
|
|
|
this.proxyAdminAddress = proxyAdminAddress; |
|
|
|
|
Object.assign(this, await this.createProxyWithImpersonatedProxyAdmin(this.implementationV0, '0x')); |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
describe('implementation', function () { |
|
|
|
|
it('returns the current implementation address', async function () { |
|
|
|
|
const implementationAddress = await getAddressInSlot(this.proxy, ImplementationSlot); |
|
|
|
|
expect(implementationAddress).to.be.equal(this.implementationV0); |
|
|
|
|
expect(await getAddressInSlot(this.proxy, ImplementationSlot)).to.equal(this.implementationV0.target); |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
it('delegates to the implementation', async function () { |
|
|
|
|
const dummy = new DummyImplementation(this.proxy.address); |
|
|
|
|
const value = await dummy.get(); |
|
|
|
|
|
|
|
|
|
expect(value).to.equal(true); |
|
|
|
|
expect(await this.instance.get()).to.be.true; |
|
|
|
|
}); |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
describe('proxy admin', function () { |
|
|
|
|
it('emits AdminChanged event during construction', async function () { |
|
|
|
|
await expectEvent.inConstruction(this.proxy, 'AdminChanged', { |
|
|
|
|
previousAdmin: ZERO_ADDRESS, |
|
|
|
|
newAdmin: this.proxyAdminAddress, |
|
|
|
|
}); |
|
|
|
|
await expect(this.tx).to.emit(this.proxy, 'AdminChanged').withArgs(ethers.ZeroAddress, this.proxyAdmin.target); |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
it('sets the proxy admin in storage with the correct initial owner', async function () { |
|
|
|
|
expect(await getAddressInSlot(this.proxy, AdminSlot)).to.be.equal(this.proxyAdminAddress); |
|
|
|
|
const proxyAdmin = await Ownable.at(this.proxyAdminAddress); |
|
|
|
|
expect(await proxyAdmin.owner()).to.be.equal(initialOwner); |
|
|
|
|
expect(await getAddressInSlot(this.proxy, AdminSlot)).to.equal(this.proxyAdmin.target); |
|
|
|
|
|
|
|
|
|
expect(await this.proxyAdmin.owner()).to.equal(this.owner.address); |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
it('can overwrite the admin by the implementation', async function () { |
|
|
|
|
const dummy = new DummyImplementation(this.proxy.address); |
|
|
|
|
await dummy.unsafeOverrideAdmin(anotherAccount); |
|
|
|
|
await this.instance.unsafeOverrideAdmin(this.other); |
|
|
|
|
|
|
|
|
|
const ERC1967AdminSlotValue = await getAddressInSlot(this.proxy, AdminSlot); |
|
|
|
|
expect(ERC1967AdminSlotValue).to.be.equal(anotherAccount); |
|
|
|
|
expect(ERC1967AdminSlotValue).to.equal(this.other.address); |
|
|
|
|
expect(ERC1967AdminSlotValue).to.not.equal(this.proxyAdmin.address); |
|
|
|
|
|
|
|
|
|
// Still allows previous admin to execute admin operations
|
|
|
|
|
expect(ERC1967AdminSlotValue).to.not.equal(this.proxyAdminAddress); |
|
|
|
|
expectEvent( |
|
|
|
|
await this.proxy.upgradeToAndCall(this.implementationV1, '0x', { from: this.proxyAdminAddress }), |
|
|
|
|
'Upgraded', |
|
|
|
|
{ |
|
|
|
|
implementation: this.implementationV1, |
|
|
|
|
}, |
|
|
|
|
); |
|
|
|
|
await expect(this.proxy.connect(this.proxyAdminAsSigner).upgradeToAndCall(this.implementationV1, '0x')) |
|
|
|
|
.to.emit(this.proxy, 'Upgraded') |
|
|
|
|
.withArgs(this.implementationV1.target); |
|
|
|
|
}); |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
describe('upgradeToAndCall', function () { |
|
|
|
|
describe('without migrations', function () { |
|
|
|
|
beforeEach(async function () { |
|
|
|
|
this.behavior = await InitializableMock.new(); |
|
|
|
|
this.behavior = await ethers.deployContract('InitializableMock'); |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
describe('when the call does not fail', function () { |
|
|
|
|
const initializeData = new InitializableMock('').contract.methods['initializeWithX(uint256)'](42).encodeABI(); |
|
|
|
|
beforeEach(function () { |
|
|
|
|
this.initializeData = this.behavior.interface.encodeFunctionData('initializeWithX', [42n]); |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
describe('when the sender is the admin', function () { |
|
|
|
|
const value = 1e5; |
|
|
|
|
const value = 10n ** 5n; |
|
|
|
|
|
|
|
|
|
beforeEach(async function () { |
|
|
|
|
this.receipt = await this.proxy.upgradeToAndCall(this.behavior.address, initializeData, { |
|
|
|
|
from: this.proxyAdminAddress, |
|
|
|
|
value, |
|
|
|
|
}); |
|
|
|
|
this.tx = await this.proxy |
|
|
|
|
.connect(this.proxyAdminAsSigner) |
|
|
|
|
.upgradeToAndCall(this.behavior, this.initializeData, { |
|
|
|
|
value, |
|
|
|
|
}); |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
it('upgrades to the requested implementation', async function () { |
|
|
|
|
const implementationAddress = await getAddressInSlot(this.proxy, ImplementationSlot); |
|
|
|
|
expect(implementationAddress).to.be.equal(this.behavior.address); |
|
|
|
|
expect(await getAddressInSlot(this.proxy, ImplementationSlot)).to.equal(this.behavior.target); |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
it('emits an event', function () { |
|
|
|
|
expectEvent(this.receipt, 'Upgraded', { implementation: this.behavior.address }); |
|
|
|
|
it('emits an event', async function () { |
|
|
|
|
await expect(this.tx).to.emit(this.proxy, 'Upgraded').withArgs(this.behavior.target); |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
it('calls the initializer function', async function () { |
|
|
|
|
const migratable = new InitializableMock(this.proxy.address); |
|
|
|
|
const x = await migratable.x(); |
|
|
|
|
expect(x).to.be.bignumber.equal('42'); |
|
|
|
|
expect(await this.behavior.attach(this.proxy).x()).to.equal(42n); |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
it('sends given value to the proxy', async function () { |
|
|
|
|
const balance = await web3.eth.getBalance(this.proxy.address); |
|
|
|
|
expect(balance.toString()).to.be.bignumber.equal(value.toString()); |
|
|
|
|
expect(await ethers.provider.getBalance(this.proxy)).to.equal(value); |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
it('uses the storage of the proxy', async function () { |
|
|
|
|
// storage layout should look as follows:
|
|
|
|
|
// - 0: Initializable storage ++ initializerRan ++ onlyInitializingRan
|
|
|
|
|
// - 1: x
|
|
|
|
|
const storedValue = await web3.eth.getStorageAt(this.proxy.address, 1); |
|
|
|
|
expect(parseInt(storedValue)).to.eq(42); |
|
|
|
|
expect(await ethers.provider.getStorage(this.proxy, 1n)).to.equal(42n); |
|
|
|
|
}); |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
describe('when the sender is not the admin', function () { |
|
|
|
|
it('reverts', async function () { |
|
|
|
|
await expectRevert.unspecified( |
|
|
|
|
this.proxy.upgradeToAndCall(this.behavior.address, initializeData, { from: anotherAccount }), |
|
|
|
|
); |
|
|
|
|
await expect(this.proxy.connect(this.other).upgradeToAndCall(this.behavior, this.initializeData)).to.be |
|
|
|
|
.reverted; |
|
|
|
|
}); |
|
|
|
|
}); |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
describe('when the call does fail', function () { |
|
|
|
|
const initializeData = new InitializableMock('').contract.methods.fail().encodeABI(); |
|
|
|
|
beforeEach(function () { |
|
|
|
|
this.initializeData = this.behavior.interface.encodeFunctionData('fail'); |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
it('reverts', async function () { |
|
|
|
|
await expectRevert.unspecified( |
|
|
|
|
this.proxy.upgradeToAndCall(this.behavior.address, initializeData, { from: this.proxyAdminAddress }), |
|
|
|
|
); |
|
|
|
|
await expect(this.proxy.connect(this.proxyAdminAsSigner).upgradeToAndCall(this.behavior, this.initializeData)) |
|
|
|
|
.to.be.reverted; |
|
|
|
|
}); |
|
|
|
|
}); |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
describe('with migrations', function () { |
|
|
|
|
describe('when the sender is the admin', function () { |
|
|
|
|
const value = 1e5; |
|
|
|
|
const value = 10n ** 5n; |
|
|
|
|
|
|
|
|
|
describe('when upgrading to V1', function () { |
|
|
|
|
const v1MigrationData = new MigratableMockV1('').contract.methods.initialize(42).encodeABI(); |
|
|
|
|
|
|
|
|
|
beforeEach(async function () { |
|
|
|
|
this.behaviorV1 = await MigratableMockV1.new(); |
|
|
|
|
this.balancePreviousV1 = new BN(await web3.eth.getBalance(this.proxy.address)); |
|
|
|
|
this.receipt = await this.proxy.upgradeToAndCall(this.behaviorV1.address, v1MigrationData, { |
|
|
|
|
from: this.proxyAdminAddress, |
|
|
|
|
value, |
|
|
|
|
}); |
|
|
|
|
this.behaviorV1 = await ethers.deployContract('MigratableMockV1'); |
|
|
|
|
const v1MigrationData = this.behaviorV1.interface.encodeFunctionData('initialize', [42n]); |
|
|
|
|
|
|
|
|
|
this.balancePreviousV1 = await ethers.provider.getBalance(this.proxy); |
|
|
|
|
this.tx = await this.proxy |
|
|
|
|
.connect(this.proxyAdminAsSigner) |
|
|
|
|
.upgradeToAndCall(this.behaviorV1, v1MigrationData, { |
|
|
|
|
value, |
|
|
|
|
}); |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
it('upgrades to the requested version and emits an event', async function () { |
|
|
|
|
const implementation = await getAddressInSlot(this.proxy, ImplementationSlot); |
|
|
|
|
expect(implementation).to.be.equal(this.behaviorV1.address); |
|
|
|
|
expectEvent(this.receipt, 'Upgraded', { implementation: this.behaviorV1.address }); |
|
|
|
|
expect(await getAddressInSlot(this.proxy, ImplementationSlot)).to.equal(this.behaviorV1.target); |
|
|
|
|
|
|
|
|
|
await expect(this.tx).to.emit(this.proxy, 'Upgraded').withArgs(this.behaviorV1.target); |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
it("calls the 'initialize' function and sends given value to the proxy", async function () { |
|
|
|
|
const migratable = new MigratableMockV1(this.proxy.address); |
|
|
|
|
|
|
|
|
|
const x = await migratable.x(); |
|
|
|
|
expect(x).to.be.bignumber.equal('42'); |
|
|
|
|
|
|
|
|
|
const balance = await web3.eth.getBalance(this.proxy.address); |
|
|
|
|
expect(new BN(balance)).to.be.bignumber.equal(this.balancePreviousV1.addn(value)); |
|
|
|
|
expect(await this.behaviorV1.attach(this.proxy).x()).to.equal(42n); |
|
|
|
|
expect(await ethers.provider.getBalance(this.proxy)).to.equal(this.balancePreviousV1 + value); |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
describe('when upgrading to V2', function () { |
|
|
|
|
const v2MigrationData = new MigratableMockV2('').contract.methods.migrate(10, 42).encodeABI(); |
|
|
|
|
|
|
|
|
|
beforeEach(async function () { |
|
|
|
|
this.behaviorV2 = await MigratableMockV2.new(); |
|
|
|
|
this.balancePreviousV2 = new BN(await web3.eth.getBalance(this.proxy.address)); |
|
|
|
|
this.receipt = await this.proxy.upgradeToAndCall(this.behaviorV2.address, v2MigrationData, { |
|
|
|
|
from: this.proxyAdminAddress, |
|
|
|
|
value, |
|
|
|
|
}); |
|
|
|
|
this.behaviorV2 = await ethers.deployContract('MigratableMockV2'); |
|
|
|
|
const v2MigrationData = this.behaviorV2.interface.encodeFunctionData('migrate', [10n, 42n]); |
|
|
|
|
|
|
|
|
|
this.balancePreviousV2 = await ethers.provider.getBalance(this.proxy); |
|
|
|
|
this.tx = await this.proxy |
|
|
|
|
.connect(this.proxyAdminAsSigner) |
|
|
|
|
.upgradeToAndCall(this.behaviorV2, v2MigrationData, { |
|
|
|
|
value, |
|
|
|
|
}); |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
it('upgrades to the requested version and emits an event', async function () { |
|
|
|
|
const implementation = await getAddressInSlot(this.proxy, ImplementationSlot); |
|
|
|
|
expect(implementation).to.be.equal(this.behaviorV2.address); |
|
|
|
|
expectEvent(this.receipt, 'Upgraded', { implementation: this.behaviorV2.address }); |
|
|
|
|
expect(await getAddressInSlot(this.proxy, ImplementationSlot)).to.equal(this.behaviorV2.target); |
|
|
|
|
|
|
|
|
|
await expect(this.tx).to.emit(this.proxy, 'Upgraded').withArgs(this.behaviorV2.target); |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
it("calls the 'migrate' function and sends given value to the proxy", async function () { |
|
|
|
|
const migratable = new MigratableMockV2(this.proxy.address); |
|
|
|
|
|
|
|
|
|
const x = await migratable.x(); |
|
|
|
|
expect(x).to.be.bignumber.equal('10'); |
|
|
|
|
|
|
|
|
|
const y = await migratable.y(); |
|
|
|
|
expect(y).to.be.bignumber.equal('42'); |
|
|
|
|
|
|
|
|
|
const balance = new BN(await web3.eth.getBalance(this.proxy.address)); |
|
|
|
|
expect(balance).to.be.bignumber.equal(this.balancePreviousV2.addn(value)); |
|
|
|
|
expect(await this.behaviorV2.attach(this.proxy).x()).to.equal(10n); |
|
|
|
|
expect(await this.behaviorV2.attach(this.proxy).y()).to.equal(42n); |
|
|
|
|
expect(await ethers.provider.getBalance(this.proxy)).to.equal(this.balancePreviousV2 + value); |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
describe('when upgrading to V3', function () { |
|
|
|
|
const v3MigrationData = new MigratableMockV3('').contract.methods['migrate()']().encodeABI(); |
|
|
|
|
|
|
|
|
|
beforeEach(async function () { |
|
|
|
|
this.behaviorV3 = await MigratableMockV3.new(); |
|
|
|
|
this.balancePreviousV3 = new BN(await web3.eth.getBalance(this.proxy.address)); |
|
|
|
|
this.receipt = await this.proxy.upgradeToAndCall(this.behaviorV3.address, v3MigrationData, { |
|
|
|
|
from: this.proxyAdminAddress, |
|
|
|
|
value, |
|
|
|
|
}); |
|
|
|
|
this.behaviorV3 = await ethers.deployContract('MigratableMockV3'); |
|
|
|
|
const v3MigrationData = this.behaviorV3.interface.encodeFunctionData('migrate()'); |
|
|
|
|
|
|
|
|
|
this.balancePreviousV3 = await ethers.provider.getBalance(this.proxy); |
|
|
|
|
this.tx = await this.proxy |
|
|
|
|
.connect(this.proxyAdminAsSigner) |
|
|
|
|
.upgradeToAndCall(this.behaviorV3, v3MigrationData, { |
|
|
|
|
value, |
|
|
|
|
}); |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
it('upgrades to the requested version and emits an event', async function () { |
|
|
|
|
const implementation = await getAddressInSlot(this.proxy, ImplementationSlot); |
|
|
|
|
expect(implementation).to.be.equal(this.behaviorV3.address); |
|
|
|
|
expectEvent(this.receipt, 'Upgraded', { implementation: this.behaviorV3.address }); |
|
|
|
|
expect(await getAddressInSlot(this.proxy, ImplementationSlot)).to.equal(this.behaviorV3.target); |
|
|
|
|
|
|
|
|
|
await expect(this.tx).to.emit(this.proxy, 'Upgraded').withArgs(this.behaviorV3.target); |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
it("calls the 'migrate' function and sends given value to the proxy", async function () { |
|
|
|
|
const migratable = new MigratableMockV3(this.proxy.address); |
|
|
|
|
|
|
|
|
|
const x = await migratable.x(); |
|
|
|
|
expect(x).to.be.bignumber.equal('42'); |
|
|
|
|
|
|
|
|
|
const y = await migratable.y(); |
|
|
|
|
expect(y).to.be.bignumber.equal('10'); |
|
|
|
|
|
|
|
|
|
const balance = new BN(await web3.eth.getBalance(this.proxy.address)); |
|
|
|
|
expect(balance).to.be.bignumber.equal(this.balancePreviousV3.addn(value)); |
|
|
|
|
expect(await this.behaviorV3.attach(this.proxy).x()).to.equal(42n); |
|
|
|
|
expect(await this.behaviorV3.attach(this.proxy).y()).to.equal(10n); |
|
|
|
|
expect(await ethers.provider.getBalance(this.proxy)).to.equal(this.balancePreviousV3 + value); |
|
|
|
|
}); |
|
|
|
|
}); |
|
|
|
|
}); |
|
|
|
@ -263,12 +225,10 @@ module.exports = function shouldBehaveLikeTransparentUpgradeableProxy(createProx |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
describe('when the sender is not the admin', function () { |
|
|
|
|
const from = anotherAccount; |
|
|
|
|
|
|
|
|
|
it('reverts', async function () { |
|
|
|
|
const behaviorV1 = await MigratableMockV1.new(); |
|
|
|
|
const v1MigrationData = new MigratableMockV1('').contract.methods.initialize(42).encodeABI(); |
|
|
|
|
await expectRevert.unspecified(this.proxy.upgradeToAndCall(behaviorV1.address, v1MigrationData, { from })); |
|
|
|
|
const behaviorV1 = await ethers.deployContract('MigratableMockV1'); |
|
|
|
|
const v1MigrationData = behaviorV1.interface.encodeFunctionData('initialize', [42n]); |
|
|
|
|
await expect(this.proxy.connect(this.other).upgradeToAndCall(behaviorV1, v1MigrationData)).to.be.reverted; |
|
|
|
|
}); |
|
|
|
|
}); |
|
|
|
|
}); |
|
|
|
@ -276,137 +236,122 @@ module.exports = function shouldBehaveLikeTransparentUpgradeableProxy(createProx |
|
|
|
|
|
|
|
|
|
describe('transparent proxy', function () { |
|
|
|
|
beforeEach('creating proxy', async function () { |
|
|
|
|
const initializeData = Buffer.from(''); |
|
|
|
|
this.clashingImplV0 = (await ClashingImplementation.new()).address; |
|
|
|
|
this.clashingImplV1 = (await ClashingImplementation.new()).address; |
|
|
|
|
const { proxy, proxyAdminAddress } = await createProxyWithImpersonatedProxyAdmin( |
|
|
|
|
this.clashingImplV0, |
|
|
|
|
initializeData, |
|
|
|
|
); |
|
|
|
|
this.proxy = proxy; |
|
|
|
|
this.proxyAdminAddress = proxyAdminAddress; |
|
|
|
|
this.clashing = new ClashingImplementation(this.proxy.address); |
|
|
|
|
this.clashingImplV0 = await ethers.deployContract('ClashingImplementation'); |
|
|
|
|
this.clashingImplV1 = await ethers.deployContract('ClashingImplementation'); |
|
|
|
|
|
|
|
|
|
Object.assign(this, await this.createProxyWithImpersonatedProxyAdmin(this.clashingImplV0, '0x')); |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
it('proxy admin cannot call delegated functions', async function () { |
|
|
|
|
await expectRevertCustomError( |
|
|
|
|
this.clashing.delegatedFunction({ from: this.proxyAdminAddress }), |
|
|
|
|
const interface = await ethers.getContractFactory('TransparentUpgradeableProxy'); |
|
|
|
|
|
|
|
|
|
await expect(this.instance.connect(this.proxyAdminAsSigner).delegatedFunction()).to.be.revertedWithCustomError( |
|
|
|
|
interface, |
|
|
|
|
'ProxyDeniedAdminAccess', |
|
|
|
|
[], |
|
|
|
|
); |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
describe('when function names clash', function () { |
|
|
|
|
it('executes the proxy function if the sender is the admin', async function () { |
|
|
|
|
const receipt = await this.proxy.upgradeToAndCall(this.clashingImplV1, '0x', { |
|
|
|
|
from: this.proxyAdminAddress, |
|
|
|
|
}); |
|
|
|
|
expectEvent(receipt, 'Upgraded', { implementation: this.clashingImplV1 }); |
|
|
|
|
await expect(this.proxy.connect(this.proxyAdminAsSigner).upgradeToAndCall(this.clashingImplV1, '0x')) |
|
|
|
|
.to.emit(this.proxy, 'Upgraded') |
|
|
|
|
.withArgs(this.clashingImplV1.target); |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
it('delegates the call to implementation when sender is not the admin', async function () { |
|
|
|
|
const receipt = await this.proxy.upgradeToAndCall(this.clashingImplV1, '0x', { |
|
|
|
|
from: anotherAccount, |
|
|
|
|
}); |
|
|
|
|
expectEvent.notEmitted(receipt, 'Upgraded'); |
|
|
|
|
expectEvent.inTransaction(receipt.tx, this.clashing, 'ClashingImplementationCall'); |
|
|
|
|
await expect(this.proxy.connect(this.other).upgradeToAndCall(this.clashingImplV1, '0x')) |
|
|
|
|
.to.emit(this.instance, 'ClashingImplementationCall') |
|
|
|
|
.to.not.emit(this.proxy, 'Upgraded'); |
|
|
|
|
}); |
|
|
|
|
}); |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
describe('regression', () => { |
|
|
|
|
const initializeData = Buffer.from(''); |
|
|
|
|
describe('regression', function () { |
|
|
|
|
const initializeData = '0x'; |
|
|
|
|
|
|
|
|
|
it('should add new function', async () => { |
|
|
|
|
const instance1 = await Implementation1.new(); |
|
|
|
|
const { proxy, proxyAdminAddress } = await createProxyWithImpersonatedProxyAdmin( |
|
|
|
|
instance1.address, |
|
|
|
|
it('should add new function', async function () { |
|
|
|
|
const impl1 = await ethers.deployContract('Implementation1'); |
|
|
|
|
const impl2 = await ethers.deployContract('Implementation2'); |
|
|
|
|
const { instance, proxy, proxyAdminAsSigner } = await this.createProxyWithImpersonatedProxyAdmin( |
|
|
|
|
impl1, |
|
|
|
|
initializeData, |
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
const proxyInstance1 = new Implementation1(proxy.address); |
|
|
|
|
await proxyInstance1.setValue(42); |
|
|
|
|
await instance.setValue(42n); |
|
|
|
|
|
|
|
|
|
// `getValue` is not available in impl1
|
|
|
|
|
await expect(impl2.attach(instance).getValue()).to.be.reverted; |
|
|
|
|
|
|
|
|
|
const instance2 = await Implementation2.new(); |
|
|
|
|
await proxy.upgradeToAndCall(instance2.address, '0x', { from: proxyAdminAddress }); |
|
|
|
|
// do upgrade
|
|
|
|
|
await proxy.connect(proxyAdminAsSigner).upgradeToAndCall(impl2, '0x'); |
|
|
|
|
|
|
|
|
|
const proxyInstance2 = new Implementation2(proxy.address); |
|
|
|
|
const res = await proxyInstance2.getValue(); |
|
|
|
|
expect(res.toString()).to.eq('42'); |
|
|
|
|
// `getValue` is available in impl2
|
|
|
|
|
expect(await impl2.attach(instance).getValue()).to.equal(42n); |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
it('should remove function', async () => { |
|
|
|
|
const instance2 = await Implementation2.new(); |
|
|
|
|
const { proxy, proxyAdminAddress } = await createProxyWithImpersonatedProxyAdmin( |
|
|
|
|
instance2.address, |
|
|
|
|
it('should remove function', async function () { |
|
|
|
|
const impl1 = await ethers.deployContract('Implementation1'); |
|
|
|
|
const impl2 = await ethers.deployContract('Implementation2'); |
|
|
|
|
const { instance, proxy, proxyAdminAsSigner } = await this.createProxyWithImpersonatedProxyAdmin( |
|
|
|
|
impl2, |
|
|
|
|
initializeData, |
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
const proxyInstance2 = new Implementation2(proxy.address); |
|
|
|
|
await proxyInstance2.setValue(42); |
|
|
|
|
const res = await proxyInstance2.getValue(); |
|
|
|
|
expect(res.toString()).to.eq('42'); |
|
|
|
|
await instance.setValue(42n); |
|
|
|
|
|
|
|
|
|
// `getValue` is available in impl2
|
|
|
|
|
expect(await impl2.attach(instance).getValue()).to.equal(42n); |
|
|
|
|
|
|
|
|
|
const instance1 = await Implementation1.new(); |
|
|
|
|
await proxy.upgradeToAndCall(instance1.address, '0x', { from: proxyAdminAddress }); |
|
|
|
|
// do downgrade
|
|
|
|
|
await proxy.connect(proxyAdminAsSigner).upgradeToAndCall(impl1, '0x'); |
|
|
|
|
|
|
|
|
|
const proxyInstance1 = new Implementation2(proxy.address); |
|
|
|
|
await expectRevert.unspecified(proxyInstance1.getValue()); |
|
|
|
|
// `getValue` is not available in impl1
|
|
|
|
|
await expect(impl2.attach(instance).getValue()).to.be.reverted; |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
it('should change function signature', async () => { |
|
|
|
|
const instance1 = await Implementation1.new(); |
|
|
|
|
const { proxy, proxyAdminAddress } = await createProxyWithImpersonatedProxyAdmin( |
|
|
|
|
instance1.address, |
|
|
|
|
it('should change function signature', async function () { |
|
|
|
|
const impl1 = await ethers.deployContract('Implementation1'); |
|
|
|
|
const impl3 = await ethers.deployContract('Implementation3'); |
|
|
|
|
const { instance, proxy, proxyAdminAsSigner } = await this.createProxyWithImpersonatedProxyAdmin( |
|
|
|
|
impl1, |
|
|
|
|
initializeData, |
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
const proxyInstance1 = new Implementation1(proxy.address); |
|
|
|
|
await proxyInstance1.setValue(42); |
|
|
|
|
await instance.setValue(42n); |
|
|
|
|
|
|
|
|
|
const instance3 = await Implementation3.new(); |
|
|
|
|
await proxy.upgradeToAndCall(instance3.address, '0x', { from: proxyAdminAddress }); |
|
|
|
|
const proxyInstance3 = new Implementation3(proxy.address); |
|
|
|
|
await proxy.connect(proxyAdminAsSigner).upgradeToAndCall(impl3, '0x'); |
|
|
|
|
|
|
|
|
|
const res = await proxyInstance3.getValue(8); |
|
|
|
|
expect(res.toString()).to.eq('50'); |
|
|
|
|
expect(await impl3.attach(instance).getValue(8n)).to.equal(50n); |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
it('should add fallback function', async () => { |
|
|
|
|
const initializeData = Buffer.from(''); |
|
|
|
|
const instance1 = await Implementation1.new(); |
|
|
|
|
const { proxy, proxyAdminAddress } = await createProxyWithImpersonatedProxyAdmin( |
|
|
|
|
instance1.address, |
|
|
|
|
it('should add fallback function', async function () { |
|
|
|
|
const impl1 = await ethers.deployContract('Implementation1'); |
|
|
|
|
const impl4 = await ethers.deployContract('Implementation4'); |
|
|
|
|
const { instance, proxy, proxyAdminAsSigner } = await this.createProxyWithImpersonatedProxyAdmin( |
|
|
|
|
impl1, |
|
|
|
|
initializeData, |
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
const instance4 = await Implementation4.new(); |
|
|
|
|
await proxy.upgradeToAndCall(instance4.address, '0x', { from: proxyAdminAddress }); |
|
|
|
|
const proxyInstance4 = new Implementation4(proxy.address); |
|
|
|
|
await proxy.connect(proxyAdminAsSigner).upgradeToAndCall(impl4, '0x'); |
|
|
|
|
|
|
|
|
|
const data = '0x'; |
|
|
|
|
await web3.eth.sendTransaction({ to: proxy.address, from: anotherAccount, data }); |
|
|
|
|
await this.other.sendTransaction({ to: proxy }); |
|
|
|
|
|
|
|
|
|
const res = await proxyInstance4.getValue(); |
|
|
|
|
expect(res.toString()).to.eq('1'); |
|
|
|
|
expect(await impl4.attach(instance).getValue()).to.equal(1n); |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
it('should remove fallback function', async () => { |
|
|
|
|
const instance4 = await Implementation4.new(); |
|
|
|
|
const { proxy, proxyAdminAddress } = await createProxyWithImpersonatedProxyAdmin( |
|
|
|
|
instance4.address, |
|
|
|
|
it('should remove fallback function', async function () { |
|
|
|
|
const impl2 = await ethers.deployContract('Implementation2'); |
|
|
|
|
const impl4 = await ethers.deployContract('Implementation4'); |
|
|
|
|
const { instance, proxy, proxyAdminAsSigner } = await this.createProxyWithImpersonatedProxyAdmin( |
|
|
|
|
impl4, |
|
|
|
|
initializeData, |
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
const instance2 = await Implementation2.new(); |
|
|
|
|
await proxy.upgradeToAndCall(instance2.address, '0x', { from: proxyAdminAddress }); |
|
|
|
|
await proxy.connect(proxyAdminAsSigner).upgradeToAndCall(impl2, '0x'); |
|
|
|
|
|
|
|
|
|
const data = '0x'; |
|
|
|
|
await expectRevert.unspecified(web3.eth.sendTransaction({ to: proxy.address, from: anotherAccount, data })); |
|
|
|
|
await expect(this.other.sendTransaction({ to: proxy })).to.be.reverted; |
|
|
|
|
|
|
|
|
|
const proxyInstance2 = new Implementation2(proxy.address); |
|
|
|
|
const res = await proxyInstance2.getValue(); |
|
|
|
|
expect(res.toString()).to.eq('0'); |
|
|
|
|
expect(await impl2.attach(instance).getValue()).to.equal(0n); |
|
|
|
|
}); |
|
|
|
|
}); |
|
|
|
|
}; |
|
|
|
|