[Ethereum] What design patterns are appropriate for data structure modification within Ethereum smart contracts

contract-designdesign-patternssolidity

I am looking to hear about design patterns that people have implemented within Ethereum smart contracts to allow for post deploy modification of data structures.

For example, say I have a contract which contains a struct defining an Address. Should I down the line realise that I want to add an email address property to the Address.. Is there any pre-deployment design pattern consideration that would allow for me to do this.

A little more complex of an example.. If I had two properties of a contract named 'question' and 'answer', and all of a sudden I want to have multiple possible answers.. how would one go about making such a change?

My issue/concern is that if you interface with said contract through a browser for example, you could simply update the contract that the front end points to (post update).. BUT how do you resolve the problem of maintaining any contract data across update deployments..?

Thanks
T

Best Answer

You need to put in consideration the following:

This has to be planed from the start. You will need to design your smart contract taking in consideration the following 5 points:

  1. You have to have a good testing strategies and tactics. Because the cost of updating the your smart contract can really ruin your life.
  2. Keep your smart contracts modular and fairly separate rules and logic from data structure. So if you will need to change something, you will change just the related contract and you will not need to change many or all contracts.
  3. You should be prepared by having an emergency stop or circuit-breaker to be able to stop all operations during any migration. Because you do not want to be in a situation where people can still update/insert data to the old version of the smart contract while you are migrating and thereafter.
  4. You should previously provided the ability to read all the data from your smart contract. Of course you can do a permissioned read by restricting reading all data to the owner or any other trusted user or even another smart contract. You will need to read from the old version of your smart contract and insert in the new version.
  5. You will use of of the following strategies to communicate with your smart contract. I copied them from Smart Contact Best Practices:

Upgrading Broken Contracts

Code will need to be changed if errors are discovered or if improvements need to be made. It is no good to discover a bug, but have no way to deal with it

...

However, there are two basic approaches that are most commonly used. The simpler of the two is to have a registry contract that holds the address of the latest version of the contract. A more seamless approach for contract users is to have a contract that forwards calls and data onto the latest version of the contract.

Example 1: Use a registry contract to store latest version of a contract

In this example, the calls aren't forwarded, so users should fetch the current address each time before interacting with it.

contract SomeRegister {
    address backendContract;
    address[] previousBackends;
    address owner;

    function SomeRegister() {
        owner = msg.sender;
    }

    modifier onlyOwner() {
        require(msg.sender == owner)
        _;
    }

    function changeBackend(address newBackend) public
    onlyOwner()
    returns (bool)
    {
        if(newBackend != backendContract) {
            previousBackends.push(backendContract);
            backendContract = newBackend;
            return true;
        }

        return false;
    }
}

There are two main disadvantages to this approach:

  1. Users must always look up the current address, and anyone who fails to do so risks using an old version of the contract

  2. You will need to think carefully about how to deal with the contract data when you replace the contract

The alternate approach is to have a contract forward calls and data to the latest version of the contract:

Example 2: Use a DELEGATECALL to forward data and calls

contract Relay {
    address public currentVersion;
    address public owner;

    modifier onlyOwner() {
        require(msg.sender == owner);
        _;
    }

    function Relay(address initAddr) {
        currentVersion = initAddr;
        owner = msg.sender; // this owner may be another contract with multisig, not a single contract owner
    }

    function changeContract(address newVersion) public
    onlyOwner()
    {
        currentVersion = newVersion;
    }

    function() {
        require(currentVersion.delegatecall(msg.data));
    }
}

This approach avoids the previous problems but has problems of its own. You must be extremely careful with how you store data in this contract. If your new contract has a different storage layout than the first, your data may end up corrupted. Additionally, this simple version of the pattern cannot return values from functions, only forward them, which limits its applicability. (More complex implementations attempt to solve this with in-line assembly code and a registry of return sizes.)

Regardless of your approach, it is important to have some way to upgrade your contracts, or they will become unusable when the inevitable bugs are discovered in them.

I created a story on Medium for this with title: Essential Design Consideration for Ethereum dApps (1): Upgradeable Smart Contracts

Related Topic