Why Upgradeable Contracts Exist
Immutability is a security feature, but it is also a liability: a discovered bug or a needed feature cannot be applied to an already-deployed contract. Upgradeable contracts restore the ability to change logic after deployment, at the cost of added complexity and a new trust assumption, since whoever controls the upgrade can change the rules under users' feet, so upgrade rights are typically held by a multisig or governance contract rather than a single key. The dominant technique is the proxy pattern: users always interact with a stable proxy address whose code can be pointed at successive implementation contracts.
Cricket analogy: It's like a captain who can change the bowling plan mid-innings, flexible and powerful, but that authority must be trusted not to be misused, so it rests with the captain, not any one fielder.
How Proxies Work: delegatecall
A proxy contract stores all the state and holds the user-facing address, but contains almost no business logic. When a call arrives, its fallback function uses delegatecall to run the code of a separate implementation contract in the proxy's own storage context. delegatecall is the key primitive: it executes the target's bytecode but reads and writes the caller's (proxy's) storage, and preserves msg.sender and msg.value. To upgrade, you simply store a new implementation address; the proxy's storage, and thus all user balances and state, stays intact while the logic changes.
Cricket analogy: It's like a batter following instructions relayed from the dressing room but playing with their own bat and stance, so the strategy comes from elsewhere yet executes on the player's own body.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
contract CounterV1 is Initializable, OwnableUpgradeable, UUPSUpgradeable {
uint256 public count;
/// @custom:oz-upgrades-unsafe-allow constructor
constructor() {
_disableInitializers(); // stop anyone initializing the logic contract itself
}
function initialize(address owner_) public initializer {
__Ownable_init(owner_);
__UUPSUpgradeable_init();
}
function increment() external {
count += 1;
}
// Only the owner may authorize an upgrade to a new implementation
function _authorizeUpgrade(address newImplementation) internal override onlyOwner {}
}Transparent vs UUPS Proxies
Two proxy patterns dominate. In the Transparent Proxy pattern, the proxy itself contains the upgrade logic and an admin address; it routes admin calls to upgrade functions and everyone else's calls to the implementation, avoiding function-selector clashes. In the newer UUPS (Universal Upgradeable Proxy Standard) pattern, the upgrade function lives in the implementation contract instead, making the proxy smaller and cheaper to deploy, but with a critical catch: if you ever upgrade to an implementation that omits the upgrade logic, the contract becomes permanently frozen. OpenZeppelin's Upgrades plugins automate deploying both and run safety checks against common mistakes.
Cricket analogy: It's like choosing whether the review authority sits with the on-field umpire or is delegated to each new third umpire, so if a replacement third umpire lacks the tools, reviews stop entirely.
Storage Layout and the Initializer Trap
Because a proxy runs new logic against old storage, storage-layout compatibility is the deepest hazard. Storage variables occupy fixed slots in declaration order; a new implementation must keep every existing variable in the same slot and only append new ones, since reordering, inserting, or changing a variable's type corrupts existing data. Two practices manage this: reserving uint256[50] private __gap; slots for future variables, and, because constructors do not run on the proxy's storage, using an initializer function (guarded so it runs exactly once) instead of a constructor to set initial state.
Cricket analogy: It's like a batting order fixed by numbered positions, where you can add a new player at the end but must never reshuffle existing spots, or every player's stats attach to the wrong slot.
Upgradeable contracts cannot use constructors to initialize state, because the constructor runs in the implementation's context, not the proxy's, and must not rely on immutable/constant state persisting in the proxy. Use an initializer guarded by the initializer modifier so it runs exactly once, and call _disableInitializers() in the implementation's constructor to stop anyone initializing the logic contract directly.
OpenZeppelin's Hardhat Upgrades plugin (deployProxy/upgradeProxy) automatically deploys the implementation and proxy, wires up the initializer, and runs storage-layout compatibility checks between versions, flagging unsafe changes before you deploy a broken upgrade.
- Upgradeability trades immutability's safety for the ability to fix bugs and add features, adding a trust assumption in whoever controls upgrades.
- A proxy holds the stable address and all storage; delegatecall runs a separate implementation's logic in the proxy's storage context.
- Transparent proxies keep upgrade logic and an admin in the proxy; UUPS puts upgrade logic in the implementation for a cheaper proxy.
- UUPS risks permanent freezing if an upgrade removes the upgrade function.
- Storage layout must be append-only: never reorder, insert, or retype existing variables, or you corrupt state.
- Reserve __gap slots for future variables and use a guarded initializer instead of a constructor.
- OpenZeppelin Upgrades plugins automate deployment and run storage-safety checks between versions.
Practice what you learned
1. What EVM primitive lets a proxy run another contract's code against its own storage?
2. In the UUPS pattern, where does the upgrade logic reside?
3. Why can't upgradeable contracts use a normal constructor to set initial state?
4. Which storage change is safe in a new implementation?
5. What is the purpose of a uint256[50] private __gap variable?
Was this page helpful?
You May Also Like
Deploying to Testnets and Mainnet
A practical guide to deploying Solidity contracts with Hardhat: configuring networks and keys, understanding gas and nonces, waiting for confirmations, and verifying source on Etherscan.
Auditing Smart Contracts
What a smart contract audit is and is not: the common vulnerability classes auditors hunt, the static, symbolic, and fuzzing tools that assist them, and why manual review plus layered defenses matter more than any single report.
Inheritance in Solidity
Inheritance lets contracts reuse and extend each other with the is keyword, virtual/override, super, C3-linearized multiple inheritance, and ordered constructor chaining.
Related Reading
Related Study Notes in Programming
Browse all study notesApache Spark Study Notes
Programming · 30 topics
ProgrammingApache Flink Study Notes
Programming · 30 topics
ProgrammingHadoop Study Notes
Programming · 30 topics
ProgrammingSnowflake Study Notes
Programming · 30 topics
ProgrammingApache Airflow Study Notes
Programming · 30 topics
Programmingdbt (Data Build Tool) Study Notes
Programming · 30 topics