Skip to content

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();
    }
}
  • 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