Upgradeable Contracts (UUPS Proxy)
Smart contracts are immutable by default. The proxy upgrade pattern separates the contract's storage (proxy) from its logic (implementation), allowing you to deploy new logic while keeping the same address and all existing data.
UUPS Proxy Pattern
How the proxy pattern works:
1. Proxy contract holds ALL storage and has a fixed address
2. Implementation contract holds ALL logic (stateless)
3. Proxy delegatecalls every user call to the current implementation
4. To upgrade: deploy new logic contract, call upgradeToAndCall() on proxy
5. Storage remains at the proxy address, unchanged
Three proxy patterns:
- Transparent Proxy: upgrade function on proxy, admin separation (OpenZeppelin v4)
- UUPS (Universal Upgradeable Proxy Standard): upgrade function on implementation
→ Cheaper deployment, no admin proxy contract needed
→ Implementation must include _authorizeUpgrade() function
- Beacon Proxy: one beacon serves many proxies (upgrade all at once)
Storage layout rules (CRITICAL):
- Never change the ORDER of existing state variables
- Never change the TYPE of existing state variables
- Only ADD new variables at the END
- Use storage gaps for extensibility: uint256[50] private __gap;
WARNING: Storage collision can corrupt ALL your data.
Rule: Proxy variables occupy storage slot 0,1,...
Implementation variables must NOT overlap.
UUPS uses EIP-1967 to store implementation address at a specific random slot.Common Mistakes
- Inserting variables in the middle of existing storage — this shifts all subsequent variable slots and corrupts all state. Only add new variables at the end (or after storage gaps).
- Not disabling initializers in implementation contract — without _disableInitializers() in the constructor, an attacker can call initialize() directly on the implementation and take ownership.
- Removing the upgradeability check — if you upgrade to a contract missing _authorizeUpgrade(), you permanently lose the ability to upgrade. Test every upgrade on a fork first.
- Using constructors in upgradeable contracts — constructors run on the implementation, not the proxy. All initialization must happen in initialize() with the initializer modifier.
Tip
Tip
Practice Upgradeable Contracts UUPS Proxy in small, isolated examples before integrating into larger projects. Breaking concepts into small experiments builds genuine understanding faster than reading alone.
Once deployed, smart contracts are immutable — code is law
Practice Task
Note
Practice Task — (1) Write a working example of Upgradeable Contracts UUPS Proxy from scratch without looking at notes. (2) Modify it to handle an edge case (empty input, null value, or error state). (3) Share your solution in the Priygop community for feedback.
Quick Quiz
Common Mistake
Warning
A common mistake with Upgradeable Contracts UUPS Proxy is skipping edge case testing — empty inputs, null values, and unexpected data types. Always validate boundary conditions to write robust, production-ready web3 code.
Key Takeaways
- Smart contracts are immutable by default.
- Inserting variables in the middle of existing storage — this shifts all subsequent variable slots and corrupts all state. Only add new variables at the end (or after storage gaps).
- Not disabling initializers in implementation contract — without _disableInitializers() in the constructor, an attacker can call initialize() directly on the implementation and take ownership.
- Removing the upgradeability check — if you upgrade to a contract missing _authorizeUpgrade(), you permanently lose the ability to upgrade. Test every upgrade on a fork first.