Lesson 24 of 25
Upgradeable Contracts
Proxy patterns (UUPS, Transparent) to make your contracts upgradeable post-deploy.
AdvancedWhy Proxies?
Deployed Solidity code is immutable. To upgrade a contract's logic, use a proxy pattern:
- Users interact with a Proxy contract (permanent address, just stores state)
- The proxy
delegatecalls to an Implementation contract (upgradeable logic) - To upgrade: deploy new implementation, point proxy to it
Solidity
// How delegatecall works:
// When proxy delegatecalls to implementation:
// - Implementation's CODE runs...
// - ...but in the PROXY's storage context
// - msg.sender and msg.value are preserved
// Minimal proxy (EIP-1167 Clone Factory)
contract MinimalProxy {
address public implementation;
constructor(address _impl) { implementation = _impl; }
fallback() external payable {
address impl = implementation;
assembly {
calldatacopy(0, 0, calldatasize())
let result := delegatecall(gas(), impl, 0, calldatasize(), 0, 0)
returndatacopy(0, 0, returndatasize())
switch result
case 0 { revert(0, returndatasize()) }
default { return(0, returndatasize()) }
}
}
}
UUPS Proxy Pattern (Recommended)
UUPS (Universal Upgradeable Proxy Standard — EIP-1822) puts the upgrade logic IN the implementation, reducing proxy gas overhead. Use OpenZeppelin's battle-tested implementation.
Solidity
// Implementation contract (with upgrade logic)
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
contract MyContractV1 is Initializable, UUPSUpgradeable, OwnableUpgradeable {
uint256 public value;
// Use initialize() instead of constructor
function initialize(uint256 _value) public initializer {
__Ownable_init(msg.sender);
__UUPSUpgradeable_init();
value = _value;
}
function setValue(uint256 _value) public onlyOwner {
value = _value;
}
// Only owner can upgrade
function _authorizeUpgrade(address) internal override onlyOwner {}
}
// Deploy script (Hardhat + OpenZeppelin Upgrades):
// const proxy = await upgrades.deployProxy(MyContractV1, [42]);
// const upgraded = await upgrades.upgradeProxy(proxy, MyContractV2);
Storage Layout Rules
When upgrading, you MUST NOT change the order or type of existing storage variables. Only add new variables at the END.
Solidity
// V1 storage layout
contract V1 {
uint256 public a; // slot 0
address public b; // slot 1
}
// ✅ V2 — safe upgrade, only adds new slot
contract V2 {
uint256 public a; // slot 0 — unchanged
address public b; // slot 1 — unchanged
uint256 public c; // slot 2 — NEW — safe to add
}
// ❌ V2 — BROKEN — reorders slots, corrupts storage
contract V2Broken {
address public b; // slot 0 — was uint256!
uint256 public a; // slot 1 — was address!
}