DEV Community

CoinMonks
CoinMonks

Posted on • Originally published at Medium on

Upgrade contracts in Solidity

The planet from a distance with light connections on surface
Photo by NASA on Unsplash

Upgradability in Solidity — Data and App contracts

Once a smart contract is deployed it cannot be changed. However, there are good reasons one might want to change a contract after it is deployed:

  • a bug is found and needs to be fixed
  • business rules have changed and require the contract code to be updated
  • access to owner’s private key is lost or compromised
  • gas fees have increased and code optimization is required

In this post we’ll talk about separating the contract into a Data and App contracts. This works well unless you need to change data structures. There are alternatives, such as contract migration, app/proxy/data and eternal storage, to name a few. Each comes with its own benefits and drawbacks, so no one solution will fit all use cases.

Simple Contract: Num

Num.sol:

// SPDX-License-Identifier: GPL-3.0
pragma solidity >= 0.8.0 < 0.9.0;

contract Num {
    uint num;

    function set(uint n) external {
        num = n;
    }

    function get() external view returns (uint) {
        return num;
    }

}
Enter fullscreen mode Exit fullscreen mode

We’ll focus on the method of separation instead of the actual app

  1. Create App contract
// SPDX-License-Identifier: GPL-3.0
pragma solidity >= 0.8.0 < 0.9.0;

contract NumApp {

}
Enter fullscreen mode Exit fullscreen mode
  1. Move logic that can change from Data to App contract
// SPDX-License-Identifier: GPL-3.0
pragma solidity >= 0.8.0 < 0.9.0;

contract NumApp {
    function set(uint n) external {
        num = n;
    }

    function get() external view returns (uint) {
        return num;
    }

}
Enter fullscreen mode Exit fullscreen mode
  1. Create stubs for missing functions (copy function definition from data to app contract)

Here we will create a public function which uses the internal set function

// SPDX-License-Identifier: GPL-3.0
pragma solidity >= 0.8.0 < 0.9.0;

contract NumApp {
    function setNum(uint n) external {
        set(n);
    }

    function getNum() ex returns (uint) {
        return get();
    }

}
Enter fullscreen mode Exit fullscreen mode
  1. Move internal/private functions in use in App contract to Data contract interface and change to external in both contracts
// SPDX-License-Identifier: GPL-3.0
pragma solidity >= 0.8.0 < 0.9.0;

contract NumApp {
    function setNum(uint n) external {
        set(n);
    }

    function getNum() external returns (uint) {
        return get();
    }

}

interface Num {
    set(uint n) external;
    get() external returns (uint);

}
Enter fullscreen mode Exit fullscreen mode
  1. Create dataContract state variable inside App contract and initialize it in constructor
// SPDX-License-Identifier: GPL-3.0
pragma solidity >= 0.8.0 < 0.9.0;

contract NumApp {
    Num dataContract;

    constructor(address dataContractAddress) {
        dataContract = Num(dataContractAddress);
    }

    function setNum(uint n) external {
        set(n);
    }

    function getNum() external returns (uint) {
        return get();
    }
}

interface Num {
    set(uint n) external;
    get() external returns (uint);

}
Enter fullscreen mode Exit fullscreen mode
  1. Change App contract functions that are present in the Data contract to dataContract. inside the App contract
// SPDX-License-Identifier: GPL-3.0
pragma solidity >= 0.8.0 < 0.9.0;

contract NumApp {
    Num dataContract;

constructor(address dataContractAddress) {
        dataContract = Num(dataContractAddress);
    }

    function setNum(uint n) external {
        dataContract.set(n);
    }

    function getNum() external returns (uint) {
        return dataContract.get();
    }
}

interface Num {
    set(uint n) external;
    get() external returns (uint);

}
Enter fullscreen mode Exit fullscreen mode
  1. Restrict Data contract callers

Inside the Data contract:

  • create mapping(address => bool) private authorizedAddresses
  • add if missing:
address private contractOwner

constructor() {
    contractOwner = msg.sender;
}

modifier requireContractOwner() {
    require(msg.sender == contractOwner, "Caller is not contract owner");
    _;
}
Enter fullscreen mode Exit fullscreen mode
  • add a way to authorize and deauthorize contracts:
function authorizeContract(address appContract) external requireContractOwner {
    authorizedContracts[appContract] = true;
}

function deauthorizeContract(address appContract) external requireContractOwner {
    delete authorizedContracts[appContract];
}
Enter fullscreen mode Exit fullscreen mode
  • add modifier to functions that will be called externally to make sure the caller is authorized
modifier isCallerAuthorized() {
    require(authorizedContracts[msg.sender] == true, "Caller is not authorized");
    _;
}
Enter fullscreen mode Exit fullscreen mode

Final Data Contract

// SPDX-License-Identifier: GPL-3.0
pragma solidity >= 0.8.0 < 0.9.0;

contract Num {
    uint num;

    address private contractOwner;
    mapping(address => bool) private authorizedContracts;

    constructor() {
        contractOwner = msg.sender;
    }

    modifier requireContractOwner() {
        require(msg.sender == contractOwner, "Caller is not contract owner");
        _;
    }

    modifier isCallerAuthorized() {
        require(authorizedContracts[msg.sender] == true, "Caller is not authorized");
        _;
    }

    function authorizeContract(address appContract) external requireContractOwner {
        authorizedContracts[appContract] = true;
    }

    function deauthorizeContract(address appContract) external requireContractOwner {
        delete authorizedContracts[appContract];
    }

    function set(uint n) external isCallerAuthorized {
        num = n;
    }

    function get() external view isCallerAuthorized returns (uint) {
        return num;
    }
}
Enter fullscreen mode Exit fullscreen mode

Final App Contract

// SPDX-License-Identifier: GPL-3.0
pragma solidity >= 0.8.0 < 0.9.0;

contract NumApp {
    Num dataContract;

    constructor(address dataContractAddress) {
        dataContract = Num(dataContractAddress);
    }

    function setNum(uint n) external {
        dataContract.set(n);
    }

    function getNum() external view returns (uint) {
        return dataContract.get();
    }
}

interface Num {
    function set(uint n) external;
    function get() external view returns (uint);
}
Enter fullscreen mode Exit fullscreen mode

How to deploy and test

  1. Go to https://remix.ethereum.org
  2. Create Num.sol and paste Data contract
  3. Create NumApp.sol and paste App contract
  4. Compiler version needs to be at least 0.8.0
  5. Go to the deploy tab (3rd from top to bottom)
  6. Deploy Num
  7. Copy Num address, select NumApp contract paste address next to deploy button and click deploy
  8. Copy NumApp contract address, open Num contract, paste next to authorize and click button

Noteworthy:

  • NumApp should not work before being authorized
  • To change App contract, create new App contract, deploy in the same way, authorize it and deauthorize previous

Comments and suggestions are welcome.

Join Coinmonks Telegram group and learn about crypto trading and investing

Also, Read

Get Best Software Deals Directly In Your Inbox


Top comments (0)