You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: src/pages/guides/upgrading-contracts.mdx
+24-8Lines changed: 24 additions & 8 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -16,10 +16,14 @@ This guide covers deploying and upgrading proxy contracts with Forge, including
16
16
17
17
### UUPS proxy deployment
18
18
19
+
In the UUPS pattern, the proxy is a thin contract that delegates all calls to an implementation contract. The implementation holds both the business logic and the upgrade logic. Because the proxy uses `delegatecall`, all state is stored on the proxy - the implementation is just code.
20
+
19
21
::::steps
20
22
21
23
#### Create the implementation
22
24
25
+
Upgradeable contracts replace constructors with `initialize` functions, since constructor logic runs in the implementation's context, not the proxy's. `_disableInitializers()` in the constructor prevents anyone from calling `initialize` directly on the implementation, which could let an attacker take ownership of it.
26
+
23
27
```solidity [src/TokenV1.sol]
24
28
// SPDX-License-Identifier: MIT
25
29
pragma solidity ^0.8.13;
@@ -52,6 +56,8 @@ contract TokenV1 is UUPSUpgradeable, OwnableUpgradeable {
52
56
53
57
#### Deploy the proxy
54
58
59
+
Deployment is a two-step process: deploy the implementation, then deploy an ERC1967 proxy pointing to it. The proxy constructor accepts initialization calldata, so the `initialize` function is called atomically during deployment.
60
+
55
61
```solidity [script/DeployToken.s.sol]
56
62
// SPDX-License-Identifier: MIT
57
63
pragma solidity ^0.8.13;
@@ -87,6 +93,8 @@ contract DeployToken is Script {
87
93
88
94
#### Create the new implementation
89
95
96
+
The new implementation inherits from V1 and adds state variables **at the end** of the storage layout. Inserting or reordering variables would corrupt existing proxy state.
97
+
90
98
```solidity [src/TokenV2.sol]
91
99
// SPDX-License-Identifier: MIT
92
100
pragma solidity ^0.8.13;
@@ -95,7 +103,7 @@ import {TokenV1} from "./TokenV1.sol";
95
103
96
104
contract TokenV2 is TokenV1 {
97
105
// New state variables must be added at the end
98
-
mapping(address => bool) public frozen;
106
+
mapping(address => bool) public frozen; // [!code ++]
99
107
100
108
function freeze(address account) external onlyOwner {
101
109
frozen[account] = true;
@@ -109,6 +117,8 @@ contract TokenV2 is TokenV1 {
109
117
110
118
#### Deploy and upgrade
111
119
120
+
To upgrade, deploy the new implementation and call `upgradeToAndCall` on the proxy. This updates the implementation address stored in the proxy's ERC1967 slot. The second argument is optional calldata for a migration function - pass empty bytes if no re-initialization is needed.
121
+
112
122
```solidity [script/UpgradeToken.s.sol]
113
123
// SPDX-License-Identifier: MIT
114
124
pragma solidity ^0.8.13;
@@ -145,7 +155,7 @@ contract UpgradeToken is Script {
145
155
146
156
### Storage layout verification
147
157
148
-
Foundry can check storage layout compatibility between implementations:
158
+
Since the proxy stores all state, the new implementation's storage layout must be compatible with the old one. Foundry can export and diff storage layouts to catch breaking changes before deployment:
Storage gaps are fixed-size arrays that reserve empty storage slots in a base contract. When you add new state variables in an upgrade, you reduce the gap size by the same number of slots, keeping the total storage layout unchanged and preventing slot collisions with inherited contracts.
179
+
168
180
```solidity
169
181
contract TokenV1 is UUPSUpgradeable, OwnableUpgradeable {
170
182
mapping(address => uint256) public balances;
171
183
uint256 public totalSupply;
172
184
173
185
// Reserve 50 slots for future variables
174
-
uint256[50] private __gap;
186
+
uint256[50] private __gap; // [!code hl]
175
187
}
176
188
177
189
contract TokenV2 is TokenV1 {
178
190
// Uses one slot from the gap
179
-
mapping(address => bool) public frozen;
191
+
mapping(address => bool) public frozen; // [!code hl]
180
192
181
193
// Update gap to maintain total slot count
182
-
uint256[49] private __gap;
194
+
uint256[49] private __gap; // [!code hl]
183
195
}
184
196
```
185
197
186
198
### Transparent proxy deployment
187
199
200
+
In the transparent proxy pattern, the proxy itself contains the upgrade logic and a separate admin address. Only the admin can call upgrade functions; all other callers are transparently delegated to the implementation. This avoids function selector clashes between the proxy and implementation but costs more gas per call due to the admin check.
201
+
188
202
```solidity [script/DeployTransparent.s.sol]
189
203
// SPDX-License-Identifier: MIT
190
204
pragma solidity ^0.8.13;
@@ -221,6 +235,8 @@ contract DeployTransparent is Script {
221
235
222
236
### Testing upgrades
223
237
238
+
Upgrade tests should verify three things: state is preserved across the upgrade, new functionality works, and only authorized accounts can trigger upgrades.
239
+
224
240
```solidity [test/TokenUpgrade.t.sol]
225
241
// SPDX-License-Identifier: MIT
226
242
pragma solidity ^0.8.13;
@@ -242,16 +258,16 @@ contract TokenUpgradeTest is Test {
0 commit comments