Skip to content

Commit 2b051e7

Browse files
committed
Avoid uninitialized proxies
1 parent 53bb340 commit 2b051e7

File tree

5 files changed

+80
-58
lines changed

5 files changed

+80
-58
lines changed

contracts/mocks/proxy/ClashingImplementation.sol

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ pragma solidity ^0.8.20;
99
contract ClashingImplementation {
1010
event ClashingImplementationCall();
1111

12+
function initialize() external payable {
13+
// placeholder
14+
}
15+
1216
function upgradeToAndCall(address, bytes calldata) external payable {
1317
emit ClashingImplementationCall();
1418
}

contracts/proxy/ERC1967/ERC1967Proxy.sol

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,10 @@ contract ERC1967Proxy is Proxy {
2424
* - If `data` is empty, `msg.value` must be zero.
2525
*/
2626
constructor(address implementation, bytes memory _data) payable {
27-
ERC1967Utils.upgradeToAndCall(implementation, _data);
27+
require(
28+
_unsafeAllowUninitialized() || ERC1967Utils.upgradeToAndCall(implementation, _data),
29+
ERC1967Utils.ERC1967ProxyUninitialized()
30+
);
2831
}
2932

3033
/**
@@ -37,4 +40,15 @@ contract ERC1967Proxy is Proxy {
3740
function _implementation() internal view virtual override returns (address) {
3841
return ERC1967Utils.getImplementation();
3942
}
43+
44+
/**
45+
* @dev Returns whether the proxy can be left uninitialized.
46+
*
47+
* NOTE: Override this function to allow the proxy to be left uninitialized.
48+
* Consider uninitialized proxies might be susceptible to man-in-the-middle threats
49+
* where the proxy is replaced with a malicious one.
50+
*/
51+
function _unsafeAllowUninitialized() internal pure virtual returns (bool) {
52+
return false;
53+
}
4054
}

contracts/proxy/ERC1967/ERC1967Utils.sol

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,11 @@ library ERC1967Utils {
4040
*/
4141
error ERC1967NonPayable();
4242

43+
/**
44+
* @dev The proxy is left uninitialized.
45+
*/
46+
error ERC1967ProxyUninitialized();
47+
4348
/**
4449
* @dev Returns the current implementation address.
4550
*/
@@ -64,15 +69,18 @@ library ERC1967Utils {
6469
*
6570
* Emits an {IERC1967-Upgraded} event.
6671
*/
67-
function upgradeToAndCall(address newImplementation, bytes memory data) internal {
72+
function upgradeToAndCall(address newImplementation, bytes memory data) internal returns (bool) {
6873
_setImplementation(newImplementation);
6974
emit IERC1967.Upgraded(newImplementation);
75+
bool called = data.length > 0;
7076

71-
if (data.length > 0) {
77+
if (called) {
7278
Address.functionDelegateCall(newImplementation, data);
7379
} else {
7480
_checkNonPayable();
7581
}
82+
83+
return called;
7684
}
7785

7886
/**

test/proxy/Proxy.behaviour.js

Lines changed: 8 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,14 @@ module.exports = function shouldBehaveLikeProxy() {
1212
.withArgs(this.nonContractAddress);
1313
});
1414

15+
it('reverts without initialization', async function () {
16+
const contractFactory = await ethers.getContractFactory('ERC1967Proxy');
17+
await expect(this.createProxy(this.implementation, '0x')).to.be.revertedWithCustomError(
18+
contractFactory,
19+
'ERC1967ProxyUninitialized',
20+
);
21+
});
22+
1523
const assertProxyInitialization = function ({ value, balance }) {
1624
it('sets the implementation address', async function () {
1725
expect(await getAddressInSlot(this.proxy, ImplementationSlot)).to.equal(this.implementation);
@@ -27,26 +35,6 @@ module.exports = function shouldBehaveLikeProxy() {
2735
});
2836
};
2937

30-
describe('without initialization', function () {
31-
const initializeData = '0x';
32-
33-
describe('when not sending balance', function () {
34-
beforeEach('creating proxy', async function () {
35-
this.proxy = await this.createProxy(this.implementation, initializeData);
36-
});
37-
38-
assertProxyInitialization({ value: 0n, balance: 0n });
39-
});
40-
41-
describe('when sending some balance', function () {
42-
const value = 10n ** 5n;
43-
44-
it('reverts', async function () {
45-
await expect(this.createProxy(this.implementation, initializeData, { value })).to.be.reverted;
46-
});
47-
});
48-
});
49-
5038
describe('initialization without parameters', function () {
5139
describe('non payable', function () {
5240
const expectedInitializedValue = 10n;

test/proxy/transparent/TransparentUpgradeableProxy.behaviour.js

Lines changed: 43 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,13 @@ module.exports = function shouldBehaveLikeTransparentUpgradeableProxy() {
3838
});
3939

4040
beforeEach(async function () {
41-
Object.assign(this, await this.createProxyWithImpersonatedProxyAdmin(this.implementationV0, '0x'));
41+
Object.assign(
42+
this,
43+
await this.createProxyWithImpersonatedProxyAdmin(
44+
this.implementationV0,
45+
this.implementation.interface.encodeFunctionData('initialize', [100n, 'test', [1, 2, 3]]),
46+
),
47+
);
4248
});
4349

4450
describe('implementation', function () {
@@ -239,7 +245,13 @@ module.exports = function shouldBehaveLikeTransparentUpgradeableProxy() {
239245
this.clashingImplV0 = await ethers.deployContract('ClashingImplementation');
240246
this.clashingImplV1 = await ethers.deployContract('ClashingImplementation');
241247

242-
Object.assign(this, await this.createProxyWithImpersonatedProxyAdmin(this.clashingImplV0, '0x'));
248+
Object.assign(
249+
this,
250+
await this.createProxyWithImpersonatedProxyAdmin(
251+
this.clashingImplV0,
252+
this.clashingImplV0.interface.encodeFunctionData('initialize'),
253+
),
254+
);
243255
});
244256

245257
it('proxy admin cannot call delegated functions', async function () {
@@ -267,91 +279,87 @@ module.exports = function shouldBehaveLikeTransparentUpgradeableProxy() {
267279
});
268280

269281
describe('regression', function () {
270-
const initializeData = '0x';
282+
beforeEach(async function () {
283+
this.impl1 = await ethers.deployContract('Implementation1');
284+
this.impl2 = await ethers.deployContract('Implementation2');
285+
this.impl3 = await ethers.deployContract('Implementation3');
286+
this.impl4 = await ethers.deployContract('Implementation4');
287+
this.initializeData = this.impl1.interface.encodeFunctionData('initialize');
288+
});
271289

272290
it('should add new function', async function () {
273-
const impl1 = await ethers.deployContract('Implementation1');
274-
const impl2 = await ethers.deployContract('Implementation2');
275291
const { instance, proxy, proxyAdminAsSigner } = await this.createProxyWithImpersonatedProxyAdmin(
276-
impl1,
277-
initializeData,
292+
this.impl1,
293+
this.initializeData,
278294
);
279295

280296
await instance.setValue(42n);
281297

282298
// `getValue` is not available in impl1
283-
await expect(impl2.attach(instance).getValue()).to.be.reverted;
299+
await expect(this.impl2.attach(instance).getValue()).to.be.reverted;
284300

285301
// do upgrade
286-
await proxy.connect(proxyAdminAsSigner).upgradeToAndCall(impl2, '0x');
302+
await proxy.connect(proxyAdminAsSigner).upgradeToAndCall(this.impl2, '0x');
287303

288304
// `getValue` is available in impl2
289-
expect(await impl2.attach(instance).getValue()).to.equal(42n);
305+
expect(await this.impl2.attach(instance).getValue()).to.equal(42n);
290306
});
291307

292308
it('should remove function', async function () {
293-
const impl1 = await ethers.deployContract('Implementation1');
294-
const impl2 = await ethers.deployContract('Implementation2');
295309
const { instance, proxy, proxyAdminAsSigner } = await this.createProxyWithImpersonatedProxyAdmin(
296-
impl2,
297-
initializeData,
310+
this.impl2,
311+
this.initializeData,
298312
);
299313

300314
await instance.setValue(42n);
301315

302316
// `getValue` is available in impl2
303-
expect(await impl2.attach(instance).getValue()).to.equal(42n);
317+
expect(await this.impl2.attach(instance).getValue()).to.equal(42n);
304318

305319
// do downgrade
306-
await proxy.connect(proxyAdminAsSigner).upgradeToAndCall(impl1, '0x');
320+
await proxy.connect(proxyAdminAsSigner).upgradeToAndCall(this.impl1, '0x');
307321

308322
// `getValue` is not available in impl1
309-
await expect(impl2.attach(instance).getValue()).to.be.reverted;
323+
await expect(this.impl2.attach(instance).getValue()).to.be.reverted;
310324
});
311325

312326
it('should change function signature', async function () {
313-
const impl1 = await ethers.deployContract('Implementation1');
314-
const impl3 = await ethers.deployContract('Implementation3');
315327
const { instance, proxy, proxyAdminAsSigner } = await this.createProxyWithImpersonatedProxyAdmin(
316-
impl1,
317-
initializeData,
328+
this.impl1,
329+
this.initializeData,
318330
);
319331

320332
await instance.setValue(42n);
321333

322-
await proxy.connect(proxyAdminAsSigner).upgradeToAndCall(impl3, '0x');
334+
await proxy.connect(proxyAdminAsSigner).upgradeToAndCall(this.impl3, '0x');
323335

324-
expect(await impl3.attach(instance).getValue(8n)).to.equal(50n);
336+
expect(await this.impl3.attach(instance).getValue(8n)).to.equal(50n);
325337
});
326338

327339
it('should add fallback function', async function () {
328-
const impl1 = await ethers.deployContract('Implementation1');
329-
const impl4 = await ethers.deployContract('Implementation4');
330340
const { instance, proxy, proxyAdminAsSigner } = await this.createProxyWithImpersonatedProxyAdmin(
331-
impl1,
332-
initializeData,
341+
this.impl1,
342+
this.initializeData,
333343
);
334344

335-
await proxy.connect(proxyAdminAsSigner).upgradeToAndCall(impl4, '0x');
345+
await proxy.connect(proxyAdminAsSigner).upgradeToAndCall(this.impl4, '0x');
336346

337347
await this.other.sendTransaction({ to: proxy });
338348

339-
expect(await impl4.attach(instance).getValue()).to.equal(1n);
349+
expect(await this.impl4.attach(instance).getValue()).to.equal(1n);
340350
});
341351

342352
it('should remove fallback function', async function () {
343-
const impl2 = await ethers.deployContract('Implementation2');
344-
const impl4 = await ethers.deployContract('Implementation4');
345353
const { instance, proxy, proxyAdminAsSigner } = await this.createProxyWithImpersonatedProxyAdmin(
346-
impl4,
347-
initializeData,
354+
this.impl4,
355+
this.initializeData,
348356
);
349357

350-
await proxy.connect(proxyAdminAsSigner).upgradeToAndCall(impl2, '0x');
358+
await proxy.connect(proxyAdminAsSigner).upgradeToAndCall(this.impl2, '0x');
351359

352360
await expect(this.other.sendTransaction({ to: proxy })).to.be.reverted;
353361

354-
expect(await impl2.attach(instance).getValue()).to.equal(0n);
362+
expect(await this.impl2.attach(instance).getValue()).to.equal(0n);
355363
});
356364
});
357365
};

0 commit comments

Comments
 (0)