Skip to content

Commit eed5222

Browse files
ernestognwAmxx
andauthored
Avoid uninitialized proxies (#5906)
Co-authored-by: Hadrien Croubois <hadrien.croubois@gmail.com>
1 parent 01b3e2a commit eed5222

File tree

9 files changed

+105
-32
lines changed

9 files changed

+105
-32
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
### Breaking changes
44

5+
- `ERC1967Proxy` and `TransparentUpgradeableProxy`: Mandate initialization during construction. Deployment now reverts with `ERC1967ProxyUninitialized` if an initialize call is not provided. Developers that rely on the previous behavior and want to disable this check can do so by overriding the internal `_unsafeAllowUninitialized` function to return true.
56
- `ERC721` and `ERC1155`: Prevent setting an operator for `address(0)`. In the case of `ERC721` this type of operator allowance could lead to obfuscated mint permission.
67
- `RLP`: The `encode(bytes32)` function now encodes `bytes32` as a fixed size item and not as a scalar in `encode(uint256)`. Users must replace calls to `encode(bytes32)` with `encode(uint256(bytes32))` to preserve the same behavior.
78

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
// SPDX-License-Identifier: MIT
2+
3+
pragma solidity ^0.8.22;
4+
5+
import {ERC1967Proxy} from "../../proxy/ERC1967/ERC1967Proxy.sol";
6+
7+
contract ERC1967ProxyUnsafe is ERC1967Proxy {
8+
constructor(address implementation, bytes memory _data) payable ERC1967Proxy(implementation, _data) {}
9+
10+
function _unsafeAllowUninitialized() internal pure override returns (bool) {
11+
return true;
12+
}
13+
}

contracts/proxy/ERC1967/ERC1967Proxy.sol

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,17 +13,28 @@ import {ERC1967Utils} from "./ERC1967Utils.sol";
1313
* implementation behind the proxy.
1414
*/
1515
contract ERC1967Proxy is Proxy {
16+
/**
17+
* @dev The proxy is left uninitialized.
18+
*/
19+
error ERC1967ProxyUninitialized();
20+
1621
/**
1722
* @dev Initializes the upgradeable proxy with an initial implementation specified by `implementation`.
1823
*
19-
* If `_data` is nonempty, it's used as data in a delegate call to `implementation`. This will typically be an
20-
* encoded function call, and allows initializing the storage of the proxy like a Solidity constructor.
24+
* Provided `_data` is passed in a delegate call to `implementation`. This will typically be an encoded function
25+
* call, and allows initializing the storage of the proxy like a Solidity constructor. By default construction
26+
* will fail if `_data` is empty. This behavior can be overridden using a custom {_unsafeAllowUninitialized} that
27+
* returns true. In that case, empty `_data` is ignored and no delegate call to the implementation is performed
28+
* during construction.
2129
*
2230
* Requirements:
2331
*
2432
* - If `data` is empty, `msg.value` must be zero.
2533
*/
2634
constructor(address implementation, bytes memory _data) payable {
35+
if (!_unsafeAllowUninitialized() && _data.length == 0) {
36+
revert ERC1967ProxyUninitialized();
37+
}
2738
ERC1967Utils.upgradeToAndCall(implementation, _data);
2839
}
2940

@@ -37,4 +48,15 @@ contract ERC1967Proxy is Proxy {
3748
function _implementation() internal view virtual override returns (address) {
3849
return ERC1967Utils.getImplementation();
3950
}
51+
52+
/**
53+
* @dev Returns whether the proxy can be left uninitialized.
54+
*
55+
* NOTE: Override this function to allow the proxy to be left uninitialized.
56+
* Consider uninitialized proxies might be susceptible to man-in-the-middle threats
57+
* where the proxy is replaced with a malicious one.
58+
*/
59+
function _unsafeAllowUninitialized() internal pure virtual returns (bool) {
60+
return false;
61+
}
4062
}

scripts/upgradeable/transpile.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ npx @openzeppelin/upgrade-safe-transpiler -D \
3232
-b "$build_info" \
3333
-i contracts/proxy/utils/Initializable.sol \
3434
-x 'contracts-exposed/**/*' \
35+
-x 'contracts/mocks/**/*Proxy*.sol' \
3536
-x 'contracts/proxy/**/*Proxy*.sol' \
3637
-x 'contracts/proxy/beacon/UpgradeableBeacon.sol' \
3738
-p 'contracts/access/manager/AccessManager.sol' \

test/proxy/ERC1967/ERC1967Proxy.test.js

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,29 @@ const fixture = async () => {
88

99
const implementation = await ethers.deployContract('DummyImplementation');
1010

11-
const createProxy = (implementation, initData, opts) =>
12-
ethers.deployContract('ERC1967Proxy', [implementation, initData], opts);
13-
14-
return { nonContractAddress, implementation, createProxy };
11+
return { nonContractAddress, implementation };
1512
};
1613

1714
describe('ERC1967Proxy', function () {
1815
beforeEach(async function () {
1916
Object.assign(this, await loadFixture(fixture));
2017
});
2118

22-
shouldBehaveLikeProxy();
19+
describe('(default) allowUninitialized is false', function () {
20+
before(function () {
21+
this.createProxy = (implementation, initData, opts) =>
22+
ethers.deployContract('ERC1967Proxy', [implementation, initData], opts);
23+
});
24+
25+
shouldBehaveLikeProxy(false);
26+
});
27+
28+
describe('(unsafe) allowUninitialized is true', function () {
29+
before(function () {
30+
this.createProxy = (implementation, initData, opts) =>
31+
ethers.deployContract('ERC1967ProxyUnsafe', [implementation, initData], opts);
32+
});
33+
34+
shouldBehaveLikeProxy(true);
35+
});
2336
});

test/proxy/Proxy.behaviour.js

Lines changed: 26 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,11 @@ const { expect } = require('chai');
33

44
const { getAddressInSlot, ImplementationSlot } = require('../helpers/storage');
55

6-
module.exports = function shouldBehaveLikeProxy() {
6+
module.exports = function shouldBehaveLikeProxy(allowUninitialized = false) {
77
it('cannot be initialized with a non-contract address', async function () {
8-
const initializeData = '0x';
8+
const initializeData = '0x00'; // non empty data to avoid uninitialized error
99
const contractFactory = await ethers.getContractFactory('ERC1967Proxy');
10+
1011
await expect(this.createProxy(this.nonContractAddress, initializeData))
1112
.to.be.revertedWithCustomError(contractFactory, 'ERC1967InvalidImplementation')
1213
.withArgs(this.nonContractAddress);
@@ -28,23 +29,35 @@ module.exports = function shouldBehaveLikeProxy() {
2829
};
2930

3031
describe('without initialization', function () {
31-
const initializeData = '0x';
32+
if (allowUninitialized) {
33+
const initializeData = '0x';
3234

33-
describe('when not sending balance', function () {
34-
beforeEach('creating proxy', async function () {
35-
this.proxy = await this.createProxy(this.implementation, initializeData);
35+
describe('when not sending balance', function () {
36+
beforeEach('creating proxy', async function () {
37+
this.proxy = await this.createProxy(this.implementation, initializeData);
38+
});
39+
40+
assertProxyInitialization({ value: 0n, balance: 0n });
3641
});
3742

38-
assertProxyInitialization({ value: 0n, balance: 0n });
39-
});
43+
describe('when sending some balance', function () {
44+
const value = 10n ** 5n;
4045

41-
describe('when sending some balance', function () {
42-
const value = 10n ** 5n;
46+
it('reverts', async function () {
47+
await expect(this.createProxy(this.implementation, initializeData, { value })).to.be.reverted;
48+
});
49+
});
50+
} else {
51+
it('reverts without initialization', async function () {
52+
const initializeData = '0x'; // empty data causes uninitialized error
53+
const contractFactory = await ethers.getContractFactory('ERC1967Proxy');
4354

44-
it('reverts', async function () {
45-
await expect(this.createProxy(this.implementation, initializeData, { value })).to.be.reverted;
55+
await expect(this.createProxy(this.implementation, initializeData)).to.be.revertedWithCustomError(
56+
contractFactory,
57+
'ERC1967ProxyUninitialized',
58+
);
4659
});
47-
});
60+
}
4861
});
4962

5063
describe('initialization without parameters', function () {

test/proxy/transparent/ProxyAdmin.test.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ async function fixture() {
1111
const v2 = await ethers.deployContract('DummyImplementationV2');
1212

1313
const proxy = await ethers
14-
.deployContract('TransparentUpgradeableProxy', [v1, admin, '0x'])
14+
.deployContract('TransparentUpgradeableProxy', [v1, admin, v1.interface.encodeFunctionData('initializeNonPayable')])
1515
.then(instance => ethers.getContractAt('ITransparentUpgradeableProxy', instance));
1616

1717
const proxyAdmin = await ethers.getContractAt(

test/proxy/transparent/TransparentUpgradeableProxy.behaviour.js

Lines changed: 19 additions & 9 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.implementationV0.interface.encodeFunctionData('initializeNonPayable'),
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('delegatedFunction'), // use a pure function with no state change as dummy initializer.
253+
),
254+
);
243255
});
244256

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

269281
describe('regression', function () {
270-
const initializeData = '0x';
271-
272282
it('should add new function', async function () {
273283
const impl1 = await ethers.deployContract('Implementation1');
274284
const impl2 = await ethers.deployContract('Implementation2');
275285
const { instance, proxy, proxyAdminAsSigner } = await this.createProxyWithImpersonatedProxyAdmin(
276286
impl1,
277-
initializeData,
287+
impl1.interface.encodeFunctionData('initialize'),
278288
);
279289

280290
await instance.setValue(42n);
@@ -294,7 +304,7 @@ module.exports = function shouldBehaveLikeTransparentUpgradeableProxy() {
294304
const impl2 = await ethers.deployContract('Implementation2');
295305
const { instance, proxy, proxyAdminAsSigner } = await this.createProxyWithImpersonatedProxyAdmin(
296306
impl2,
297-
initializeData,
307+
impl2.interface.encodeFunctionData('initialize'),
298308
);
299309

300310
await instance.setValue(42n);
@@ -314,7 +324,7 @@ module.exports = function shouldBehaveLikeTransparentUpgradeableProxy() {
314324
const impl3 = await ethers.deployContract('Implementation3');
315325
const { instance, proxy, proxyAdminAsSigner } = await this.createProxyWithImpersonatedProxyAdmin(
316326
impl1,
317-
initializeData,
327+
impl1.interface.encodeFunctionData('initialize'),
318328
);
319329

320330
await instance.setValue(42n);
@@ -329,7 +339,7 @@ module.exports = function shouldBehaveLikeTransparentUpgradeableProxy() {
329339
const impl4 = await ethers.deployContract('Implementation4');
330340
const { instance, proxy, proxyAdminAsSigner } = await this.createProxyWithImpersonatedProxyAdmin(
331341
impl1,
332-
initializeData,
342+
impl1.interface.encodeFunctionData('initialize'),
333343
);
334344

335345
await proxy.connect(proxyAdminAsSigner).upgradeToAndCall(impl4, '0x');
@@ -344,7 +354,7 @@ module.exports = function shouldBehaveLikeTransparentUpgradeableProxy() {
344354
const impl4 = await ethers.deployContract('Implementation4');
345355
const { instance, proxy, proxyAdminAsSigner } = await this.createProxyWithImpersonatedProxyAdmin(
346356
impl4,
347-
initializeData,
357+
impl4.interface.encodeFunctionData('initialize'),
348358
);
349359

350360
await proxy.connect(proxyAdminAsSigner).upgradeToAndCall(impl2, '0x');

test/proxy/utils/UUPSUpgradeable.test.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ async function fixture() {
1414
const cloneFactory = await ethers.deployContract('$Clones');
1515

1616
const instance = await ethers
17-
.deployContract('ERC1967Proxy', [implInitial, '0x'])
17+
.deployContract('ERC1967ProxyUnsafe', [implInitial, '0x'])
1818
.then(proxy => implInitial.attach(proxy.target));
1919

2020
return {
@@ -110,7 +110,7 @@ describe('UUPSUpgradeable', function () {
110110

111111
it('reject proxy address as implementation', async function () {
112112
const otherInstance = await ethers
113-
.deployContract('ERC1967Proxy', [this.implInitial, '0x'])
113+
.deployContract('ERC1967ProxyUnsafe', [this.implInitial, '0x'])
114114
.then(proxy => this.implInitial.attach(proxy.target));
115115

116116
await expect(this.instance.upgradeToAndCall(otherInstance, '0x'))

0 commit comments

Comments
 (0)