Developers Forum for XinFin XDC Network

ruslan wing
ruslan wing

Posted on

Multiple ways to upgrade a Solidity smart contract deployed on XDC Network

There are situations when you want to upgrade a Solidity smart contract. You could 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 XDC Network, they cannot be changed. If you deploy a new version of the contract, you also start with empty storage. In this tutorial, we will review several methods that will allow you to upgrade a Solidity smart contract on XDC Network

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 business logic. The proxy contract will then interact with the actual contract to execute all calls. We will use a delegate call to have the proxy contract interact with the actual contract. 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 the state. To upgrade business logic, you create a new smart contract with which the proxy interacts. The proxy contract does not contain any business logic. This method separates stored data (proxy contract) and business logic (separate contract).

Image description

Proxy contract using a delegate call process flow:

  • Bob deploys smart contractV1, which contains business logic
  • Then he deploys a proxy contract that is built to call smart contractV1
  • Users interact with the proxy contract and the fallback function called smart contractV1
  • All data is stored in the proxy contract
  • Bob wants to change functionality in his smart contract, so he deploys smart contractV2
  • 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())}
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

smartContractV1

pragma solidity ^0.8.6;


contract smartContractV1 {
  uint public age;

//set
    function setAge(uint newAge) external {
        age = newAge;
    }
}
Enter fullscreen mode Exit fullscreen mode

smartContractV2

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;  
    }
}
Enter fullscreen mode Exit fullscreen mode

Upgrade Method 2 – the interface pattern

or 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 that some developers will be familiar with this concept of abstraction.

This main contract contains most 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 Dex. Some of the contracts main functions rely on other satellite contracts for execution. You can deploy a new satellite contract at any time 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 and combine 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 pattern to follow.

Image description

The smart contract interface pattern process flow:

  • Bob deploys the main contract that imports a defined interface
  • The main contract is responsible for business logic and storage
  • Then he deploys a satelliteV1 contract
  • He configures the satelliteV1 address in the main contract
  • Users interact with the main contract.
  • The main contract has functions implemented and calls additional functions in the satelliteV1 contract
  • Bob wants to change business logic in his smart contract, so he deploys the satelliteV2 contract
  • 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();
  }
}
Enter fullscreen mode Exit fullscreen mode

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;

//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;
  }
}
Enter fullscreen mode Exit fullscreen mode

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.

Image description

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

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

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 and 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;

    }
}
Enter fullscreen mode Exit fullscreen mode

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();
    }   
}
Enter fullscreen mode Exit fullscreen mode

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

Tools required for smart contract development on XDC Network
XDCPay How to use XDCPay
XinFin Remix Learn How to develop smart contract using XinFin Remix

If you are facing any issue in deploying a smart contract, please feel free to post the issue on https://xdc.dev

Discussion (0)