banner
zach

zach

github
twitter
medium

Upgradeable contract solution

Logic and Storage Separation

We all know that once a smart contract is deployed on the blockchain, its code cannot be tampered with. If a contract has a bug, the project team often has no solution.

Contracts usually consist of stored variables and logical functions. Since migrating variables to a new contract incurs high costs and the main upgrade is usually in the logical functions, it is possible to isolate the stored variables and logical functions at the contract level.

Therefore, the complete calling chain is shown in the following diagram:

  • User interacts with contract-A, where A does not record the logical functions but only stores variables.
  • Contract-A calls contract-B through delegatecall, applying the function logic of B to contract-A.
  • After the call ends, from the user's perspective, only contract-A is visible, and the existence of contract-B is not perceived.

Basic Upgradable Contract Implementation

Referring to the above calling chain, if contract-B is replaced with another contract, the replacement of the logical contract can be completed without affecting the original contract storage.

Proxy Contract

  • implementation: records the address of the logical contract.
  • Use the fallback function to delegate all function calls to the logical contract through delegatecall.
  • Provide the upgrade function to replace the address of the logical contract.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;
// A simple upgradable contract, where the admin can change the address of the logical contract through the upgrade function, thereby changing the contract's logic.
contract SimpleUpgrade {
    address public implementation; // Address of the logical contract
    address public admin; // Admin address
    string public words; // String that can be changed through the logical contract's function

    // Constructor, initializes the admin and logical contract address
    constructor(address _implementation){
        admin = msg.sender;
        implementation = _implementation;
    }

    // Fallback function, delegates the call to the logical contract
    fallback() external payable {
        (bool success, bytes memory data) = implementation.delegatecall(msg.data);
    }

    // Upgrade function, changes the address of the logical contract, can only be called by the admin
    function upgrade(address newImplementation) external {
        require(msg.sender == admin);
        implementation = newImplementation;
    }
}

Logic Contract

  • Needs to maintain the same storage layout (variable types and variable order) as the proxy contract.
  • The functions in the logical contract will modify the variables in the proxy contract, so calling the foo() function will modify the words variable in the proxy contract to "old".
contract Logic1 {
    // State variables are consistent with the proxy contract to prevent slot conflicts
    address public implementation;
    address public admin;
    string public words; // String that can be changed through the logical contract's function

    // Changes the state variable in the proxy contract, selector: 0xc2985578
    function foo() public{
        words = "old";
    }
}

New Logic Contract:

  • Maintains the same storage layout.
  • Adjusts the function logic to complete the logic update. Calling the foo() function will modify the words variable in the proxy contract to "new".
contract Logic2 {
    // State variables are consistent with the proxy contract to prevent slot conflicts
    address public implementation;
    address public admin;
    string public words; // String that can be changed through the logical contract's function

    // Changes the state variable in the proxy contract, selector: 0xc2985578
    function foo() public{
        words = "new";
    }
}

Contract Upgrade

The admin account calls the upgrade function in the proxy contract and passes the address of the new logical contract to complete the upgrade.

Existing Issues

There may be selector conflicts when calling delegatecall in the fallback function.

The selector of a function is the first four bytes of the hash of the function signature. Different functions may have conflicts. If there are two conflicting functions in a contract, the contract cannot be compiled. However, conflicts may exist between the proxy contract and the logical contract, which cannot be detected during compilation.

Transparent Proxy

The transparent proxy is designed to solve the upgrade issue caused by selector conflicts. The idea of the transparent proxy is to separate the permissions of the upgrade function and the delegatecall execution in the fallback function.

  • The fallback function requires that it is not called by the admin.
  • The upgrade function requires that it is called only by the admin.

This avoids upgrade issues caused by selector conflicts.

fallback() external payable {
        require(msg.sender != admin);
        (bool success, bytes memory data) = implementation.delegatecall(msg.data);
    }

  // Upgrade function, changes the address of the logical contract, can only be called by the admin
  function upgrade(address newImplementation) external {
      if (msg.sender != admin) revert();
      implementation = newImplementation;
  }

Universal Upgradeable Proxy System (UUPS)

The transparent proxy pattern requires gas to verify the admin address every time. The UUPS pattern moves the upgrade function to the logical contract, which avoids the issue of selector conflicts with the upgrade function at compile time in a single contract.

The original upgrade function was in the proxy contract and limited to admin permissions. Now it is moved to the logic contract. Since the msg.sender remains unchanged during the delegatecall process, and the stored admin is still recorded in the proxy contract, this verification logic can also be placed in the logic contract.

UUPS Upgradeable Contract Example: Record the implementation address in the logic contract and apply it to the proxy contract.

Proxy Contract

Only includes stored variables, constructor, and fallback function. The upgrade function is removed.

contract UUPSProxy {
    address public implementation; // Address of the logical contract
    address public admin; // Admin address
    string public words; // String that can be changed through the logical contract's function

    // Constructor, initializes the admin and logical contract address
    constructor(address _implementation){
        admin = msg.sender;
        implementation = _implementation;
    }

    // Fallback function, delegates the call to the logical contract
    fallback() external payable {
        (bool success, bytes memory data) = implementation.delegatecall(msg.data);
    }
}

Logic Contract

Includes the upgrade function.

// UUPS logic contract (upgrade function is included in the logic contract)
contract UUPS1{
    // State variables are consistent with the proxy contract to prevent slot conflicts
    address public implementation;
    address public admin;
    string public words; // String that can be changed through the logical contract's function

    // Changes the state variable in the proxy contract, selector: 0xc2985578
    function foo() public{
        words = "old";
    }

    // Upgrade function, changes the address of the logical contract, can only be called by the admin. Selector: 0x0900f010
    // In UUPS, the logic function must include the upgrade function, otherwise it cannot be upgraded again.
    function upgrade(address newImplementation) external {
        require(msg.sender == admin);
        implementation = newImplementation;
    }
}
// New UUPS logic contract
contract UUPS2{
    // State variables are consistent with the proxy contract to prevent slot conflicts
    address public implementation;
    address public admin;
    string public words; // String that can be changed through the logical contract's function

    // Changes the state variable in the proxy contract, selector: 0xc2985578
    function foo() public{
        words = "new";
    }

    // Upgrade function, changes the address of the logical contract, can only be called by the admin. Selector: 0x0900f010
    // In UUPS, the logic function must include the upgrade function, otherwise it cannot be upgraded again.
    function upgrade(address newImplementation) external {
        require(msg.sender == admin);
        implementation = newImplementation;
    }
}
Loading...
Ownership of this post data is guaranteed by blockchain and smart contracts to the creator alone.