Tutorial: Upgradable Contracts

Discover how to write upgradable smart contracts, allowing you to fix bugs and add new features after deployment.

The Need for Upgradability

Smart contracts on the blockchain are immutable by default. While this is a key feature for security and trust, it also means that bugs cannot be fixed, and new features cannot be added after deployment. Upgradable contracts solve this problem by separating the contract's logic from its data. This is typically achieved using a "proxy" pattern.

The Proxy Pattern

The proxy pattern involves two contracts:

  • Proxy Contract: This is the contract that users interact with. It holds the contract's state (data) but does not contain the application logic. Instead, it delegates all calls to a logic contract.
  • Logic Contract: This contract contains the application logic. It is stateless and can be replaced with a new version to upgrade the contract.

When a user calls a function on the proxy contract, the proxy forwards the call to the current logic contract. The logic contract executes the function and returns the result to the proxy, which then returns it to the user. To upgrade the contract, the owner of the proxy contract simply points it to a new logic contract.

Example: An Upgradable Counter

Let's illustrate this with a simple counter contract. We'll have a `Proxy` contract and a `CounterV1` logic contract. Later, we'll create a `CounterV2` with new functionality and upgrade our proxy to point to it.


// Note: This is a conceptual Rust example for illustrative purposes.

// --- CounterV1: The initial logic contract ---
struct CounterV1 {
    // This contract is stateless. The state is in the Proxy.
}

impl CounterV1 {
    // Reads the count from the Proxy's storage
    fn get_count(storage: &Storage) -> u64 {
        storage.get("count").unwrap_or(0)
    }

    // Increments the count in the Proxy's storage
    fn increment(storage: &mut Storage) {
        let count = Self::get_count(storage);
        storage.set("count", count + 1);
    }
}

// --- CounterV2: The upgraded logic contract ---
struct CounterV2 {
    // Also stateless
}

impl CounterV2 {
    // Reads the count (compatible with V1)
    fn get_count(storage: &Storage) -> u64 {
        storage.get("count").unwrap_or(0)
    }

    // New function to decrement the count
    fn decrement(storage: &mut Storage) {
        let count = Self::get_count(storage);
        storage.set("count", count - 1);
    }
}