Contract Upgrades¶
Overview¶
Contract upgrades refer to the process of changing the logic of a smart contract or fixing vulnerabilities on blockchains like Ethereum through specific design patterns, while preserving the original contract state (data) and address. This is one of the most challenging techniques in blockchain development, as it attempts to achieve mutability in an immutable environment.
Core Contradiction:
The Nature of Blockchain:
- Code is permanently immutable once deployed
- Guarantees trust and determinism
Real-World Requirements:
- Fix security vulnerabilities
- Add new features
- Adapt to changing business needs
- Optimize performance and reduce Gas costs
Contract Upgrades: A compromise between immutability and flexibility
Historical Evolution:
- 2016 DAO Incident: Exposed the severe problem of non-upgradeable contracts, ultimately leading to an Ethereum hard fork
- 2017 Parity Multisig **Wallet Vulnerability**: Due to the inability to upgrade, over 500,000 ETH was permanently frozen
- Post-2018: Proxy patterns became the industry standard, with OpenZeppelin and others providing upgrade frameworks
- Post-2020: Advanced patterns like UUPS, BeaconProxy, and Diamond matured
Market Scale:
According to statistics as of 2024: - Upgradeable contract share: 85%+ of the Top 100 DeFi protocols use upgradeable contracts - Proxy contract count: Over 100,000 proxy contracts on the Ethereum mainnet - Assets under management: Over $50B in assets managed by upgradeable contracts - Security incidents: 30%+ of contract vulnerabilities are related to upgrade mechanisms
Core Features¶
Proxy Pattern¶
Fundamental Principle:
The proxy pattern is the core mechanism for implementing contract upgrades, achieving upgradeability by separating "storage" and "logic."
Traditional Contract:
┌─────────────────────┐
│ Smart Contract │
│ ├─ Business logic │
│ └─ State data │
└─────────────────────┘
Immutable: Cannot be modified
Proxy Pattern:
┌──────────────┐ delegatecall ┌──────────────┐
│ Proxy Contract│ ───────────────────> │ Logic Contract│
│ (Proxy) │ │ (Logic) │
│ │ │ │
│ - Storage │ │ - Business │
│ - Asset │ │ logic │
│ balances │ │ - Function │
│ - User │ │ code │
│ interaction│ │ - Stateless │
└──────────────┘ └──────────────┘
↓
Upgradeable: Replace logic contract address
delegatecall Mechanism:
// Proxy Contract
contract Proxy {
address public implementation; // Logic contract address
fallback() external payable {
address _impl = implementation;
assembly {
// Copy calldata
calldatacopy(0, 0, calldatasize())
// delegatecall to logic contract
let result := delegatecall(
gas(),
_impl,
0,
calldatasize(),
0,
0
)
// Copy return data
returndatacopy(0, 0, returndatasize())
// Return or revert based on result
switch result
case 0 { revert(0, returndatasize()) }
default { return(0, returndatasize()) }
}
}
}
// Logic Contract
contract Logic {
uint256 public value; // Actually stored in Proxy's slot 0
function setValue(uint256 _value) public {
value = _value; // Modifies Proxy's storage
}
}
Key Characteristics:
delegatecall vs call:
call:
- Executes in the callee contract's context
- msg.sender = caller
- Modifies the callee contract's storage
delegatecall:
- Executes in the caller contract's context
- msg.sender = original caller (passes through the proxy)
- Modifies the caller contract's storage
- Perfect for the proxy pattern
Storage Collision¶
The Most Severe Upgrade Risk:
// ❌ Incorrect example: Storage collision
// Proxy Contract
contract Proxy {
address public implementation; // slot 0
address public admin; // slot 1
// ... delegatecall logic
}
// Logic Contract V1
contract LogicV1 {
uint256 public value; // slot 0 ❌ Collision!
function setValue(uint256 _value) public {
value = _value; // Actually writes to Proxy's slot 0
// Overwrites the implementation address!
}
}
Result:
- After executing setValue, implementation is overwritten
- Proxy contract permanently corrupted
- Funds locked
Solution: ERC-1967 Standard
// ✅ Correct example: Using ERC-1967
contract Proxy {
// Use pseudo-random slot to avoid collision
bytes32 private constant IMPLEMENTATION_SLOT =
bytes32(uint256(keccak256('eip1967.proxy.implementation')) - 1);
function _getImplementation() internal view returns (address impl) {
bytes32 slot = IMPLEMENTATION_SLOT;
assembly {
impl := sload(slot)
}
}
function _setImplementation(address newImplementation) internal {
bytes32 slot = IMPLEMENTATION_SLOT;
assembly {
sstore(slot, newImplementation)
}
}
}
// Logic Contract
contract Logic {
uint256 public value; // slot 0
// No collision because Proxy uses a special slot
}
ERC-1967 Standard Slots:
Implementation Slot:
0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc
Admin Slot:
0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103
Beacon Slot:
0xa3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6cb3582b35133d50
Calculation formula:
bytes32(uint256(keccak256('eip1967.proxy.[type]')) - 1)
Upgrade Patterns¶
Transparent Proxy¶
Problem Background:
Function selector conflict:
Proxy Contract:
- upgradeTo(address) - Upgrade function
Logic Contract:
- upgradeTo(address) - Business function (happens to share the same name)
User calls upgradeTo:
- Which one should be called?
- If the wrong one is called, it may cause an unintended upgrade
Solution:
// OpenZeppelin Transparent Proxy
contract TransparentUpgradeableProxy {
address private _admin;
modifier ifAdmin() {
if (msg.sender == _getAdmin()) {
_;
} else {
_fallback();
}
}
// Admin call: execute proxy function
function upgradeTo(address newImplementation) external ifAdmin {
_upgradeTo(newImplementation);
}
// Regular user call: forward to logic contract
fallback() external payable {
require(msg.sender != _getAdmin(), "Admin cannot fallback");
_fallback();
}
function _fallback() internal {
_delegate(_getImplementation());
}
}
How It Works:
Role Separation:
Admin:
- Calls proxy management functions (upgradeTo, changeAdmin)
- Cannot call logic contract functions
- Usually a MultiSig or DAO
User (regular user):
- Calls logic contract functions
- Cannot call proxy management functions
- Forwarded via fallback
Example:
Admin calls proxy.upgradeTo(newLogic):
→ Executes proxy's upgrade function
User calls proxy.setValue(100):
→ fallback → delegatecall → Logic.setValue(100)
Pros and Cons:
Pros:
+ Secure: Completely isolates admin and business calls
+ Simple: Users don't need to worry about upgrade mechanics
+ Mature: OpenZeppelin is thoroughly audited
Cons:
- High Gas: Each call must check msg.sender
- Admin cannot call business functions (needs another address)
UUPS Proxy (Universal Upgradeable Proxy Standard)¶
EIP-1822 Standard:
UUPS is a more efficient upgrade pattern that places the upgrade logic in the logic contract rather than the proxy contract.
Core Idea:
Traditional Proxy:
- Upgrade logic is in the Proxy
- Proxy code is complex, high Gas
UUPS:
- Upgrade logic is in the Logic contract
- Proxy is minimal, low Gas
- Upgrade executed via delegatecall
Implementation:
// UUPS Proxy (minimal)
contract UUPSProxy {
bytes32 private constant IMPLEMENTATION_SLOT =
bytes32(uint256(keccak256('eip1967.proxy.implementation')) - 1);
constructor(address _logic, bytes memory _data) {
_setImplementation(_logic);
if (_data.length > 0) {
(bool success,) = _logic.delegatecall(_data);
require(success);
}
}
fallback() external payable {
_delegate(_getImplementation());
}
function _delegate(address impl) internal {
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()) }
}
}
function _getImplementation() internal view returns (address impl) {
bytes32 slot = IMPLEMENTATION_SLOT;
assembly { impl := sload(slot) }
}
function _setImplementation(address newImplementation) internal {
bytes32 slot = IMPLEMENTATION_SLOT;
assembly { sstore(slot, newImplementation) }
}
}
// UUPS Logic Contract (contains upgrade logic)
abstract contract UUPSUpgradeable {
bytes32 private constant IMPLEMENTATION_SLOT =
bytes32(uint256(keccak256('eip1967.proxy.implementation')) - 1);
event Upgraded(address indexed implementation);
// Upgrade function (in the logic contract)
function upgradeTo(address newImplementation) public virtual {
_authorizeUpgrade(newImplementation);
_upgradeToAndCall(newImplementation, bytes(""));
}
function _upgradeToAndCall(
address newImplementation,
bytes memory data
) internal {
// 1. Update implementation slot
bytes32 slot = IMPLEMENTATION_SLOT;
assembly { sstore(slot, newImplementation) }
emit Upgraded(newImplementation);
// 2. Optional: call new logic's initialization function
if (data.length > 0) {
(bool success,) = newImplementation.delegatecall(data);
require(success);
}
}
// Subclasses must implement authorization check
function _authorizeUpgrade(address newImplementation) internal virtual;
}
// Actual business contract
contract MyContract is UUPSUpgradeable {
address public owner;
uint256 public value;
function initialize(address _owner) public {
require(owner == address(0), "Already initialized");
owner = _owner;
}
function setValue(uint256 _value) public {
value = _value;
}
// Implement authorization check
function _authorizeUpgrade(address) internal override {
require(msg.sender == owner, "Not owner");
}
}
Upgrade Flow:
1. Deploy new logic contract LogicV2
2. Call proxy.upgradeTo(LogicV2)
↓
3. Proxy fallback → delegatecall → LogicV1.upgradeTo()
↓
4. LogicV1.upgradeTo() executes:
- _authorizeUpgrade(LogicV2) // Authorization check
- sstore(IMPLEMENTATION_SLOT, LogicV2) // Update address
↓
5. Subsequent calls are forwarded to LogicV2
Pros and Cons:
Pros:
+ High Gas efficiency: Minimal Proxy, saves Gas on each call
+ Flexible: Each logic contract can customize upgrade logic
+ No admin restriction: Admin can call business functions normally
Cons:
- Higher risk: If the logic contract forgets to inherit UUPSUpgradeable
→ Cannot upgrade, permanently locked
- Complex: Developers must correctly implement upgrade logic
Safety Check:
// OpenZeppelin's UUPS safety check
contract UUPSUpgradeable {
address private immutable __self = address(this);
function upgradeTo(address newImplementation) public virtual {
require(address(this) != __self, "Must be called through proxy");
// Prevents directly calling the logic contract's upgrade function
_authorizeUpgrade(newImplementation);
_upgradeToAndCall(newImplementation, bytes(""));
}
}
Beacon Proxy¶
Use Case:
Problem:
Deployed 1,000 proxy contracts with identical logic
(e.g., 1,000 users' personal wallet contracts)
Upgrade requirement:
- Fix a vulnerability in the logic contract
- Need to upgrade all 1,000 proxies
Traditional approach:
- Call upgradeTo 1,000 times
- Gas cost: 1,000 × 100,000 gas = Extremely high
- Operational complexity: High
Beacon approach:
- All proxies point to the same Beacon
- Upgrade the Beacon once
- All proxies automatically upgrade
Architecture:
┌─────────────┐
│ BeaconProxy │ ──┐
│ #1 │ │
└─────────────┘ │
│
┌─────────────┐ │ ┌─────────────┐ ┌─────────────┐
│ BeaconProxy │ ──┼─────>│ Beacon │────> │ Logic │
│ #2 │ │ │ │ │ │
└─────────────┘ │ └─────────────┘ └─────────────┘
│
┌─────────────┐ │
│ BeaconProxy │ ──┘
│ #1000 │
└─────────────┘
Upgrade: Only need to update the Logic address in the Beacon
Implementation:
// Beacon Contract
contract UpgradeableBeacon {
address private _implementation;
address private _owner;
event Upgraded(address indexed implementation);
constructor(address implementation_) {
_setImplementation(implementation_);
_owner = msg.sender;
}
// Get current logic contract address
function implementation() public view returns (address) {
return _implementation;
}
// Upgrade logic contract
function upgradeTo(address newImplementation) public {
require(msg.sender == _owner, "Not owner");
_setImplementation(newImplementation);
}
function _setImplementation(address newImplementation) private {
require(newImplementation.code.length > 0, "Not a contract");
_implementation = newImplementation;
emit Upgraded(newImplementation);
}
}
// Beacon Proxy
contract BeaconProxy {
address private immutable _beacon;
constructor(address beacon, bytes memory data) {
_beacon = beacon;
if (data.length > 0) {
(bool success,) = _implementation().delegatecall(data);
require(success);
}
}
// Get logic contract address from Beacon
function _implementation() internal view returns (address) {
return IBeacon(_beacon).implementation();
}
fallback() external payable {
_delegate(_implementation());
}
function _delegate(address impl) internal {
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()) }
}
}
}
Usage Example:
// 1. Deploy logic contract
Logic logic = new Logic();
// 2. Deploy Beacon
UpgradeableBeacon beacon = new UpgradeableBeacon(address(logic));
// 3. Deploy multiple Beacon Proxies
for (uint i = 0; i < 1000; i++) {
new BeaconProxy(address(beacon), initData);
}
// 4. Upgrade: only needed once
LogicV2 logicV2 = new LogicV2();
beacon.upgradeTo(address(logicV2));
// All 1,000 proxies automatically use the new logic
Pros and Cons:
Pros:
+ Batch upgrade: One upgrade affects all proxies
+ Gas efficient: Upgrade cost is O(1)
+ Simple management: Centralized control
Cons:
- Single point of failure: Beacon compromise affects all proxies
- Low flexibility: Cannot upgrade individual proxies separately
- Centralized: All proxies are forced to the same version
Diamond Proxy (Diamond Standard / EIP-2535)¶
Breaking the 24KB Limit:
Ethereum contract size limit:
- After Spurious Dragon upgrade: 24 KB
- Complex DeFi protocols frequently exceed this
Traditional solution:
- Split into multiple contracts, coordinate manually
- Complex and error-prone
Diamond solution:
- One proxy delegates to multiple Facets (logic contracts)
- Breaks through the size limit
- Modular upgrades
Architecture:
DiamondProxy
│
┌────────────────┼────────────────┐
│ │ │
↓ ↓ ↓
┌─────────┐ ┌─────────┐ ┌─────────┐
│ Facet A │ │ Facet B │ │ Facet C │
│ │ │ │ │ │
│ func1() │ │ func3() │ │ func5() │
│ func2() │ │ func4() │ │ func6() │
└─────────┘ └─────────┘ └─────────┘
Function selector mapping:
func1 → Facet A
func2 → Facet A
func3 → Facet B
func4 → Facet B
func5 → Facet C
func6 → Facet C
Core Data Structure:
// Diamond Storage
library LibDiamond {
bytes32 constant DIAMOND_STORAGE_POSITION =
keccak256("diamond.standard.diamond.storage");
struct FacetAddressAndSelectorPosition {
address facetAddress;
uint16 selectorPosition;
}
struct DiamondStorage {
// Function selector → Facet address mapping
mapping(bytes4 => FacetAddressAndSelectorPosition) selectorToFacet;
// Facet address → Function selector array
mapping(address => bytes4[]) facetToSelectors;
}
function diamondStorage() internal pure returns (DiamondStorage storage ds) {
bytes32 position = DIAMOND_STORAGE_POSITION;
assembly {
ds.slot := position
}
}
}
// Diamond Proxy
contract Diamond {
constructor(address _diamondCutFacet) {
// Initialize DiamondCut Facet (for managing upgrades)
LibDiamond.DiamondStorage storage ds = LibDiamond.diamondStorage();
ds.selectorToFacet[IDiamondCut.diamondCut.selector].facetAddress = _diamondCutFacet;
}
fallback() external payable {
LibDiamond.DiamondStorage storage ds = LibDiamond.diamondStorage();
// Find the corresponding Facet based on function selector
address facet = ds.selectorToFacet[msg.sig].facetAddress;
require(facet != address(0), "Function does not exist");
// delegatecall to Facet
assembly {
calldatacopy(0, 0, calldatasize())
let result := delegatecall(gas(), facet, 0, calldatasize(), 0, 0)
returndatacopy(0, 0, returndatasize())
switch result
case 0 { revert(0, returndatasize()) }
default { return(0, returndatasize()) }
}
}
}
Upgrade Operations:
interface IDiamondCut {
enum FacetCutAction { Add, Replace, Remove }
struct FacetCut {
address facetAddress;
FacetCutAction action;
bytes4[] functionSelectors;
}
function diamondCut(
FacetCut[] calldata _diamondCut,
address _init,
bytes calldata _calldata
) external;
}
// Usage example
FacetCut[] memory cuts = new FacetCut[](3);
// 1. Add new Facet
cuts[0] = FacetCut({
facetAddress: address(newFacet),
action: FacetCutAction.Add,
functionSelectors: [newFunc1.selector, newFunc2.selector]
});
// 2. Replace existing function
cuts[1] = FacetCut({
facetAddress: address(updatedFacet),
action: FacetCutAction.Replace,
functionSelectors: [existingFunc.selector]
});
// 3. Remove function
cuts[2] = FacetCut({
facetAddress: address(0),
action: FacetCutAction.Remove,
functionSelectors: [oldFunc.selector]
});
diamond.diamondCut(cuts, address(0), "");
Pros and Cons:
Pros:
+ Breaks 24KB limit: Unlimited expansion
+ Modular upgrades: Fine-grained control
+ Code reuse: Facets can be shared
+ Flexibility: Add/Replace/Remove functions
Cons:
- Extremely high complexity: Steep learning and maintenance costs
- Gas overhead: Additional cost for function lookup
- Security risks: Complexity introduces more attack surfaces
- Low adoption: Less ecosystem support than traditional proxies
Initialization Patterns¶
Constructor Problem¶
Why Constructors Cannot Be Used:
// ❌ Incorrect: Logic contract uses constructor
contract Logic {
address public owner;
constructor() {
owner = msg.sender; // ❌ Only executes when Logic contract is deployed
// owner is 0 when called via Proxy
}
}
Problem:
- Constructor executes at contract deployment time
- When Logic is deployed, owner = Logic deployer
- When Proxy calls Logic, owner is uninitialized
Solution: initialize Function
// ✅ Correct: Use initialize
contract Logic {
address public owner;
bool private initialized;
function initialize(address _owner) public {
require(!initialized, "Already initialized");
initialized = true;
owner = _owner;
}
}
// Called during Proxy deployment
proxy = new Proxy(logic, abi.encodeWithSelector(
Logic.initialize.selector,
msg.sender
));
Initializable Pattern¶
*OpenZeppelin* Implementation:**
abstract contract Initializable {
uint8 private _initialized;
bool private _initializing;
modifier initializer() {
bool isTopLevelCall = !_initializing;
require(
(isTopLevelCall && _initialized < 1) ||
(!Address.isContract(address(this)) && _initialized == 1),
"Initializable: contract is already initialized"
);
_initialized = 1;
if (isTopLevelCall) {
_initializing = true;
}
_;
if (isTopLevelCall) {
_initializing = false;
}
}
modifier reinitializer(uint8 version) {
require(!_initializing && _initialized < version, "Initializable: contract is already initialized");
_initialized = version;
_initializing = true;
_;
_initializing = false;
}
}
// Usage
contract MyContract is Initializable {
uint256 public value;
function initialize(uint256 _value) public initializer {
value = _value;
}
// Re-initialization after V2 upgrade
function initializeV2(uint256 _newValue) public reinitializer(2) {
value = _newValue * 2;
}
}
Storage Gap¶
Upgrade Compatibility:
// ❌ Dangerous: Upgrade causes storage collision
// V1
contract LogicV1 {
uint256 public value;
// slot 0: value
}
// V2 (adding new variable)
contract LogicV2 {
address public owner; // slot 0: owner ❌ Overwrites value!
uint256 public value; // slot 1
}
Result:
- V1's value data is interpreted as an address
- Data corruption
Correct Approach:
// ✅ Correct: Use storage gap
// V1
contract LogicV1 {
uint256 public value;
// Reserve 50 slots
uint256[49] private __gap;
}
// V2 (safely adding new variable)
contract LogicV2 {
uint256 public value; // slot 0
address public owner; // slot 1 (using gap space)
// Reduce gap
uint256[48] private __gap; // 49 - 1 = 48
}
Calculating Gap Size:
Goal: Maximum number of variables that may be added in the future
Example:
- Reserve 50 slots
- Currently using 5
- gap = 50 - 5 = 45
Each time N variables are added:
- gap = gap - N
Security Considerations¶
Function Selector Conflict¶
Problem:
// Proxy and Logic have same-named function
contract Proxy {
function admin() public view returns (address);
}
contract Logic {
function admin() public view returns (address); // Conflict!
}
// User calls proxy.admin()
// Which one should execute?
Transparent Proxy solution: - Admin calls the Proxy version - Regular users call the Logic version
UUPS solution: - Upgrade function is in Logic, avoiding conflicts
Delegatecall Safety¶
selfdestruct Attack:
// ❌ Dangerous logic contract
contract MaliciousLogic {
function destroy() public {
selfdestruct(payable(msg.sender));
// Executed via delegatecall
// → Destroys the Proxy contract!
}
}
Prevention:
- Never use selfdestruct in logic contracts
- Audit upgraded logic contracts
Constructor Trap:
// ❌ Logic contract's constructor is not executed by Proxy
contract Logic {
uint256 public constant IMPORTANT = 100;
constructor() {
// This code only executes when Logic is deployed
// It does NOT execute when called via Proxy!
}
}
Timelock¶
Delayed Upgrades:
contract TimelockProxy {
address public pendingImplementation;
uint256 public upgradeTime;
uint256 public constant DELAY = 2 days;
// Propose upgrade
function proposeUpgrade(address newImpl) public onlyAdmin {
pendingImplementation = newImpl;
upgradeTime = block.timestamp + DELAY;
emit UpgradeProposed(newImpl, upgradeTime);
}
// Execute upgrade (must wait)
function executeUpgrade() public {
require(block.timestamp >= upgradeTime, "Too early");
require(pendingImplementation != address(0), "No pending upgrade");
_setImplementation(pendingImplementation);
pendingImplementation = address(0);
emit Upgraded(pendingImplementation);
}
// Cancel upgrade
function cancelUpgrade() public onlyAdmin {
pendingImplementation = address(0);
emit UpgradeCancelled();
}
}
Compound Timelock Example:
// Compound's Timelock
contract Timelock {
uint public constant GRACE_PERIOD = 14 days;
uint public constant MINIMUM_DELAY = 2 days;
uint public constant MAXIMUM_DELAY = 30 days;
mapping (bytes32 => bool) public queuedTransactions;
function queueTransaction(
address target,
uint value,
string memory signature,
bytes memory data,
uint eta
) public returns (bytes32) {
require(msg.sender == admin, "Unauthorized");
require(eta >= getBlockTimestamp() + delay, "ETA must exceed delay");
bytes32 txHash = keccak256(abi.encode(target, value, signature, data, eta));
queuedTransactions[txHash] = true;
emit QueueTransaction(txHash, target, value, signature, data, eta);
return txHash;
}
function executeTransaction(
address target,
uint value,
string memory signature,
bytes memory data,
uint eta
) public payable returns (bytes memory) {
require(msg.sender == admin, "Unauthorized");
bytes32 txHash = keccak256(abi.encode(target, value, signature, data, eta));
require(queuedTransactions[txHash], "Not queued");
require(getBlockTimestamp() >= eta, "Not yet");
require(getBlockTimestamp() <= eta + GRACE_PERIOD, "Expired");
queuedTransactions[txHash] = false;
bytes memory callData = abi.encodePacked(bytes4(keccak256(bytes(signature))), data);
(bool success, bytes memory returnData) = target.call{value: value}(callData);
require(success, "Execution failed");
emit ExecuteTransaction(txHash, target, value, signature, data, eta);
return returnData;
}
}
Best Practices¶
Development Workflow¶
1. Design Phase:
Decision Tree:
Is upgradeability needed?
│
├─ No → Non-upgradeable contract
│ Pros: Simple, secure, low Gas
│ Use for: Simple utility contracts, Tokens
│
└─ Yes → Choose upgrade pattern
│
├─ Single contract → Transparent or UUPS
│ Recommended: UUPS (lower Gas)
│
├─ Batch contracts → Beacon Proxy
│ e.g.: User wallets, NFTs
│
└─ Very large contract → Diamond
e.g.: Complex DeFi protocols
2. Implementation Phase:
// Recommended structure
// 1. Import OpenZeppelin
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
// 2. Inheritance order matters
contract MyContract is
Initializable, // First
OwnableUpgradeable,
UUPSUpgradeable // Last
{
// 3. State variables
uint256 public value;
// 4. Storage Gap
uint256[49] private __gap;
// 5. Disable constructor
/// @custom:oz-upgrades-unsafe-allow constructor
constructor() {
_disableInitializers();
}
// 6. Initialize function
function initialize(uint256 _value) public initializer {
__Ownable_init();
__UUPSUpgradeable_init();
value = _value;
}
// 7. Business logic
function setValue(uint256 _value) public onlyOwner {
value = _value;
}
// 8. Upgrade authorization
function _authorizeUpgrade(address newImplementation)
internal
override
onlyOwner
{}
}
3. Testing Phase:
// Hardhat test example
const { ethers, upgrades } = require("hardhat");
describe("Upgrade", function () {
it("Should upgrade correctly", async function () {
// Deploy V1
const V1 = await ethers.getContractFactory("MyContractV1");
const proxy = await upgrades.deployProxy(V1, [100], {
kind: "uups"
});
// Verify initial state
expect(await proxy.value()).to.equal(100);
// Upgrade to V2
const V2 = await ethers.getContractFactory("MyContractV2");
const upgraded = await upgrades.upgradeProxy(proxy.address, V2);
// Verify state preservation
expect(await upgraded.value()).to.equal(100);
// Verify new functionality
await upgraded.newFunction();
});
});
Tool Support¶
Hardhat Upgrades Plugin:
// hardhat.config.js
require("@openzeppelin/hardhat-upgrades");
module.exports = {
solidity: "0.8.20",
};
// Deployment script
async function main() {
const MyContract = await ethers.getContractFactory("MyContract");
// Automatically handles proxy deployment
const proxy = await upgrades.deployProxy(MyContract, [100], {
initializer: "initialize",
kind: "uups" // or "transparent", "beacon"
});
console.log("Proxy deployed to:", proxy.address);
// Validate upgrade safety
await upgrades.validateImplementation(MyContract);
}
*Foundry* Upgrade Testing:**
// test/Upgrade.t.sol
contract UpgradeTest is Test {
Proxy proxy;
LogicV1 logicV1;
LogicV2 logicV2;
function setUp() public {
logicV1 = new LogicV1();
proxy = new Proxy(address(logicV1), abi.encodeWithSelector(
LogicV1.initialize.selector,
100
));
}
function testUpgrade() public {
LogicV1 proxyAsV1 = LogicV1(address(proxy));
assertEq(proxyAsV1.value(), 100);
// Upgrade
logicV2 = new LogicV2();
proxyAsV1.upgradeTo(address(logicV2));
// Cast to new interface
LogicV2 proxyAsV2 = LogicV2(address(proxy));
// Verify state preservation
assertEq(proxyAsV2.value(), 100);
// Test new functionality
proxyAsV2.newFunction();
}
}
Recommended Reading¶
- OpenZeppelin Upgrades Plugins - OpenZeppelin upgrade tools documentation
- OpenZeppelin Proxy Pattern - In-depth proxy pattern analysis
- EIP-1967: Standard Proxy Storage Slots - Proxy storage slot standard
- EIP-1822: UUPS - UUPS standard
- EIP-2535: Diamond Standard - Diamond standard
- Consensys: Proxy Patterns - Proxy pattern best practices
- Trail of Bits: Contract Upgrade Anti-Patterns - Upgrade anti-pattern analysis
Related Concepts¶
- Delegatecall: Core EVM opcode for implementing the proxy pattern
- Storage Layout: How contract state variables are arranged in storage
- Initializable: Initializable contract base class
- Timelock: Governance mechanism for delayed upgrade execution
- MultiSig: Multi-signature wallet, commonly used for managing upgrade permissions
- ERC-1967: Proxy storage slot standard
- Function Selector: Function selector, used for call routing
- Storage Collision: Storage collision
- Immutable: Immutability
- Diamond Pattern: Diamond pattern