Multiple ways to upgrade a Solidity smart contract

There are situations when you want to upgrade a Solidity smart contract. You might want to fix a software vulnerability, change the logic of the contract or add a new feature. As you are aware smart contracts are immutable and once they are deployed to the blockchain they cannot be changed. If you deploy a new version of the contract you also start with an empty storage. In this tutorial we will review several methods that will allow you to upgrade a Solidity smart contract. These methods will work on Ethereum, Binance Smart Chain, Polygon, or any other EVM compatible blockchain.

Upgrade Method 1 – Proxy contract using a delegate call

For this first upgrade method users will interact with a proxy contract that does not contain any business logic. The proxy contract will then interact with the actual contract to execute all calls. To have the proxy contract interact with the actual contract we will use a delegate call. A delegate call will allow you to execute a function in the context of another contract.

In the event of an upgrade the proxy contract is still used. Users will interact with the same proxy contract and all data remains stored in state. To upgrade business logic you would create a new smart contract that the proxy interacts with. The proxy contract does not contain any business logic. This method separates stored data (proxy contract) and business logic (separate contract).

upgrade a Solidity smart contract. Proxy contract using a delegate call

Proxy contract using a delegate call process flow:

  1. Bob deploys smart contractV1 which contains business logic
  2. Then he deploys a proxy contract that is built to call smart contractV1
  3. Users interact with the proxy contract and fallback function calls smart contractV1
  4. All data is stored in the proxy contract
  5. Bob wants to change functionality in his smart contract so he deploys smart contractV2
  6. He updates his Proxy contract to point to the new contractV2 address

To test a proxy contract using a delegate call perform the following steps:

  • Deploy the smartContractV1 contract below
  • Deploy the proxy contract and set the SMARTCONTRACTWITHLOGIC to the smartContractV1 address
  • Test the V1 contract
  • Then deploy the smartContractV2 contract below
  • Using the Proxy upgrade function to set the address to the smartContractV2 contract
  • Test the V2 contract

Proxy contract

pragma solidity ^0.8.6;

contract sampleProxy {

  //two assembly memory slots locations
  bytes32 private constant _OWNER_SLOT = 0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103;
  bytes32 private constant _SMARTCONTRACTWITHLOGIC_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc;

 
  constructor() {
    bytes32 slot = _OWNER_SLOT;
    address _admin = msg.sender;
    assembly {
    //allows you to store a value in storage
      sstore(slot, _admin)
    }
  }

 
  //address of the owner
  function admin() public view returns (address owner) {
    bytes32 slot = _OWNER_SLOT;
    assembly {
      //read a value in storage
      owner := sload(slot)
    }
  }

 
  //address of the contract with business logic
  function SMARTCONTRACTWITHLOGIC() public view returns (address contractwithlogic) {
    bytes32 slot = _SMARTCONTRACTWITHLOGIC_SLOT;
    assembly {
      contractwithlogic := sload(slot)
    }
  }


  //function used to change the address of the contract containing business logic
  function upgrade(address newContract) external {
    //verify the sender is the admin
    require(msg.sender == admin(), 'You must be an owner only');
    bytes32 slot = _SMARTCONTRACTWITHLOGIC_SLOT;
    assembly {
      //store in memory the new address
      sstore(slot, newContract)
    }
  }

 
//user calls a function that does not exist in this contract so the fallback function is called
//assembly is used


  fallback() external payable {
    assembly {
      //get the address of the contract that contains business logic
      //save address in temporary memory
      let _target := sload(_SMARTCONTRACTWITHLOGIC_SLOT)
      //copy the function call in memory
      //first parameter is the memory slot we want to copy the function call to
      //second parameter is the memory slot we want to copy from
      //third parameter is the size we want to copy which is all data
      calldatacopy(0x0, 0x0, calldatasize())
      //forward the call to the smart contract that contains the business logic
      //specify the gas, address of contract, function we want to call and size
      //if the call is successful it will be stored in the bool result
      let result := delegatecall(gas(), _target, 0x0, calldatasize(), 0x0, 0)
      //copy the return data into memory
      returndatacopy(0x0, 0x0, returndatasize())
      //if the result is 0 and failed then revert
      switch result case 0 {revert(0, 0)} default {return (0, returndatasize())}
    }
  }
}

Try on Remix

smartContractV1 and smartContractV2

pragma solidity ^0.8.6;


contract smartContractV1 {
  uint public age;

//set
    function setAge(uint newAge) external {
        age = newAge;
    }
}



pragma solidity ^0.8.6;

//in the upgraded contract you need to keep all existing state variables in the same order
//add new state variables below existing state variables or you will overwrite data 

contract smartContractV2 {
  uint public age1;
  uint public age2;

//set
    function setAge1(uint newAge1) external {
        age1 = newAge1;
    }

//set
    function setAge2(uint newAge2) external {
        age2 = newAge2;  
    }
}

Try on Remix

Upgrade Method 2 – the interface pattern

For the second upgrade method a smart contract will use an interface to call a function in another contract. The goal is to abstract a contract implementation behind an interface that only defines its function signatures. This is a well known pattern in object oriented programming so some developers will be familiar with this concept of abstraction.

This main contract contains most of the business logic but interfaces with one or many contracts to perform more functions. For example a smart contract that performs a flash loan arbitrage on Uniswap and SushiSwap. Some of the contracts main functions rely on other satellite contracts for execution. At any time you can deploy a new satellite contract and update its address in the main contract.

Users call functions in the main contract which might execute the function itself or interface with another contract for execution. A contract can interface with one or many contracts are and can use a combination of their functions. This is different from a proxy contract using a delegate call because business logic exists in the main contract.

To upgrade the contract you cannot change any functionality in the main contract but you can change the functionality in a satellite contract. as long as it respects the interface. Then update the address of the satellite contract in the main contract. This is a very popular patter to follow.

upgrade a Solidity smart contract. Using an the interface pattern

The smart contract interface pattern process flow:

  1. Bob deploys main contract that imports a defined interface
  2. The main contract is responsible for business logic and storage
  3. Then he deploys a satelliteV1 contract
  4. He configures the satelliteV1 address in the main contract
  5. Users interact with the main contract.
  6. The main contract has functions implemented and calls additional functions in the satelliteV1 contract
  7. Bob wants to change business logic in his smart contract so he deploys satelliteV2 contract
  8. He updates his main contract with the satelliteV2 contract address

To test the interface pattern perform the following steps:

  • Deploy the main contract below
  • Deploy the satelliteV1 contract
  • Call the upgrade function of the main contact using the address of the satelliteV1 contract
  • Test the main contract by calling the getAge function
  • Then deploy the satelliteV2 contract below
  • Call the upgrade function of the main contact using the address of the satelliteV2 contract
  • Test the main contract by calling the getAge function

Main Contract with defined interface


pragma solidity ^0.8.6;

//defined interface needed to interact with other contract
interface Ibusinesslogic {
  function getAge() external pure returns(uint);
}

contract MainContract {
  //set an admin address
  address public admin;
  //interface contract address
  Ibusinesslogic public businesslogic;
  //the admin is the owner
  constructor() {
    admin = msg.sender;
  }

 
  //function to upgrade the contract to point to execute function
  function upgrade(address _businesslogic) external {
    require(msg.sender == admin, 'only admin');
    businesslogic = Ibusinesslogic(_businesslogic);
  }


  //call the getAge function using the businesslogic function
  function getAge() external view returns(uint) {
    return businesslogic.getAge();
  }
}

Try on Remix

Satellite contracts with defined interface

pragma solidity ^0.8.6;

//defined interface needed to interact with other contract
interface Ibusinesslogic {
  function getAge() external pure returns(uint);
}


pragma solidity ^0.8.6;

//satelliteV1 uses the Ibusinesslogic interface
contract satelliteV1 is Ibusinesslogic {
  function getAge() override external pure returns(uint) {
    return 25;
  }
}


pragma solidity ^0.8.6;

//satelliteV2 uses the Ibusinesslogic interface
contract satelliteV2 is Ibusinesslogic {
  function getAge() override external pure returns(uint) {
    return 32;
  }
}

Try on Remix

Upgrade Method 3 – store all data in a storage contract

The third upgrade method is to use one contract for all business logic and a second contract for storing data. This use case is valuable when you want to upgrade a smart contract and do not care that the address changes but want to preserve all data. Users interact with the business logic contract and data is saved in the storage contract.

When you upgrade you cannot change any of the functionality in the storage contract but you can replace the business logic contract. The business logic contract is responsible for all logic and interacting with the storage contract by getting and setting data.

upgrade a Solidity smart contract. store data in a storage contract and a contract for business logic

The using two contract (business logic and data storage) pattern process flow:

  1. Bob deploys a userStorage contract
  2. The userStorage contracts purpose is to save data
  3. Bob deploys the userContract
  4. The userContract is responsible for business logic and sends requests to the userStorage contract
  5. He configures the userStorage address in the userContract
  6. He configures the userContract address in the userStorage contract. This adds a level of security so only authorized contracts can update data.
  7. Users interact with the userContract which calls functions and stores data in the userStorage contract
  8. Bob wants to change functionality in his userContract so he deploys a new usercontractV2
  9. He updates his userContract address in the userStorage contract and the userStorage address in the userContract

To test this pattern using two contract (business logic and data storage) perform the following steps:

  • Deploy the userStorage contract below
  • Deploy the userContract below
  • In the userStorage contract call the allowAccess function using the userContract address. This will give the userContract permission to write to the userStorage contract.
  • In the userContract call the setStorageContract function using the userStorage address. This instructs the contract where to get and set data.
  • Test the userContract by setting the age variable then getting the age variable.

User Storage Contract

pragma solidity ^0.8.6;

//this contract is used to store data
 
contract UserStorage {

    //a mapping to determine which contract has access to write data to this contract
    //used in the modifier below
    mapping(address => bool) accessAllowed;
    uint private age;


    //a basic mapping that allows one to set an address and a bool value
    //for example - is this address registered on the platform?
     mapping(address => bool) addressSet;

    //function modifier checks to see if an address has permission to update data
    //bool has to be true
    modifier isAllowed() {
        require(accessAllowed[msg.sender] == true);
        _;
    }

    //access is allowed to the person that deployed the contract
    function UserStorageAccess() public {
        accessAllowed[msg.sender] = true;
    }


    //set an address to the accessAllowed map and set bool to true
    //uses the isAllowed function modifier to determine if user can change data
    //this function controls which addresses can write data to the contract
    //if you update the UserContract you would add the new address here
    function allowAccess (address _address) isAllowed public {
         accessAllowed[_address] = true;
    }

     
    //set an address to the accessAllowed map and set bool to false
    //uses the isAllowed function modifier to determine if user can change data
    //this function controls which addresses need to have thier write access removed from the contract
    //if you update the UserContract you would set the old contract address to false
    function denyAccess (address _address) isAllowed public {
         accessAllowed[_address] = false;
     }

     
    //gets an address from the addressSet map and displays true or false
    function getAddressSet (address _address) public view returns(bool) {
         return addressSet[_address];
    }

     
    //sets an address to the addressSet map and sets the bool true or false
    //uses the isAllowed function modifier to determine if user can change data
    function setAddressSet (address _address, bool _bool) isAllowed public {
         addressSet[_address] = _bool;
    }

     
    //get the age from the age variable
    function getAge () public view returns (uint) {
        return age;
    } 

    
    //set an age to the age variable
    //uses the isAllowed function modifier to determine if user can change data
    function setAge(uint newAge) isAllowed public {
        age = newAge;
      
    }
}

Try on Remix

User contract that contains business logic

pragma solidity ^0.8.6;

//logic is in the UserContract and data storage is in the UserStorage contract
//if we want to upgrade the usercontract we can and will not loose any data

contract UserContract {

    UserStorage userStorage;


    //set the address of the storage contract that this contract should user
    //all functions will read and write data to this contract
    function setStorageContract(address _userStorageAddress) public {
        userStorage = UserStorage(_userStorageAddress);    
    }


    //reads the addressSet map in the UserStorage contract
    function isMyUserNameRegistered() public view returns(bool) {
        return userStorage.getAddressSet(msg.sender);   
    }

    
    //writes to the addressSet map in the UserStorage contract
    function registerMe() public {
        userStorage.setAddressSet(msg.sender, true);
    }

    
    //set the age in the storage contract
    function setAge(uint newAge) public {
        userStorage.setAge(newAge);
    }

    
    //get the age in the storage contract
    function getAge() public view returns (uint){
        return userStorage.getAge();
    }   
}

Try on Remix

Although Solidity code is immutable one can implement a method to work around this concept and have code in multiple contracts to have mutability. This allows one to upgrade a Solidity smart contract.

Resources

Blockchain Networks

Below is a list of EVM compatible Mainnet and Testnet blockchain networks. Each link contains network configuration, links to multiple faucets for test ETH and tokens, bridge details, and technical resources for each blockchain. Basically everything you need to test and deploy smart contracts or decentralized applications on each chain. For a list of popular Ethereum forums and chat applications click here.

Ethereum test network configuration and test ETH faucet information
Optimistic Ethereum Mainnet and Testnet configuration, bridge details, etc.
Polygon network Mainnet and Testnet configuration, faucets for test MATIC tokens, bridge details, etc.
Binance Smart Chain Mainnet and Testnet configuration, faucets for test BNB tokens, bridge details, etc.
Fanton networt Mainnet and Testnet configuration, faucets for test FTM tokens, bridge details, etc.
Kucoin Chain Mainnet and Testnet configuration, faucets for test KCS tokens, bridge details, etc.

Web3 Software Libraries

You can use the following libraries to interact with an EVM compatible blockchain.

Nodes

Learn how to run a Geth node. Read getting started with Geth to run an Ethereum node.

Fix a transaction

How to fix a pending transaction stuck on Ethereum or EVM compatible chain

Next – Hide Solidity code with an external contract

Ledger Nano X - The secure hardware wallet

1 thought on “Multiple ways to upgrade a Solidity smart contract

Leave a Reply