How to Write Upgradeable Smart Contracts
Proxy patterns, storage layout rules, the UUPS vs Transparent Proxy debate, and the security implications of upgradeable contracts.
Upgradeable smart contracts let you change a contract's logic after deployment. They're powerful — and dangerous. This guide covers how they work, which patterns to use, and what security implications you must understand before deploying.
Why Upgradeability Is Controversial
Immutability is a security property. A smart contract whose code can't change is predictable — users know exactly what it will do forever. An upgradeable contract is only as trustworthy as whoever controls the upgrade key. If that's a 2-of-3 multisig controlled by a small team, users are trusting the team not to rug them. This is a real risk that users and auditors take seriously.
How Proxy Patterns Work
The proxy pattern uses delegatecall — a special EVM opcode that executes the called contract's code in the caller's context (same storage, same msg.sender). A proxy contract contains no logic — it just forwards all calls to an implementation contract via delegatecall. State lives in the proxy; logic lives in the implementation. Upgrading means pointing the proxy at a new implementation.
The Storage Layout Rule
The most common proxy vulnerability: if the implementation contract defines storage variables in different slots than the proxy, or if you add variables in the middle of a struct in a new implementation, storage is corrupted. Variables in Solidity are assigned to storage slots in declaration order. New variables must always be added at the end of existing storage. Never insert variables between existing ones in an implementation upgrade.
Transparent Proxy Pattern
OpenZeppelin's Transparent Proxy keeps upgrade logic in the proxy itself. The proxy routes admin calls (upgrade, admin changes) to itself and all other calls to the implementation. The admin address check prevents function selector clashes. Simple, well-understood, but the proxy is more complex and costs more to deploy.
UUPS (Universal Upgradeable Proxy Standard)
UUPS puts the upgrade logic in the implementation contract, making the proxy simpler and cheaper. The critical risk: if you upgrade to an implementation that doesn't include the upgrade function, you permanently lose upgradeability. The implementation must always include the upgrade functionality. UUPS is now preferred by most developers for its efficiency.
Beacon Proxy
Useful when you want to upgrade many proxy instances simultaneously. All proxies point to a beacon contract that holds the implementation address. Upgrading the beacon upgrades all proxies at once. Used by protocols that deploy many instances of the same contract (e.g., a factory that creates user-specific vaults).
Initialization Instead of Constructors
Constructors don't work in upgradeable contracts — the proxy is the contract, not the implementation, so the implementation's constructor runs in the implementation's context, not the proxy's. Instead, use an initialize() function protected by an initializer modifier (from OpenZeppelin). Critical: the implementation contract can be initialized directly by anyone if not protected. Always call _disableInitializers() in the implementation's constructor.
Security Requirements
Protect upgrade functions with a minimum 48-hour time-lock. Use a multisig for the upgrade admin. Communicate upgrades publicly before executing. Audit new implementation code with the same rigor as the original. Document all storage changes in the upgrade commit. Run the full test suite against the new implementation before proposing the upgrade.
Frequently Asked Questions
Related Guides
Ready to build your Web3 project?
Tell us about your project and get a precise quote.
Get a Project Quote