Ethereum smart contracts: what to do when an error occurs in a smart contract or migration technique

    When writing smart contracts, it is important to remember that after downloading to the blockchain, they can no longer be changed, and therefore, no improvements can be made or any errors found can be fixed! We all know that there are errors in any program, and returning to the code written a couple of months ago, we will always find what can be improved there. How to be? The only option is to upload a new contract with the corrected code. But what if the tokens have already been issued based on the existing contract? Migration comes to the rescue! Over the past year, I tried many different techniques for its implementation, analyzed the methods used in other large blockchain projects, and invented something myself. Details under the cut.


    I’ll make a reservation right away that, within the framework of this post, I will not give sheets of ready-made smart contracts, but will only consider and analyze various techniques. Almost all of them in one form or another were implemented by me in contracts for projects in which I have participated, and much can be taken from my GitHub .

    Migration from an ERC20-compliant contract


    We start our consideration with the simplest and most common case, when the original contract, already uploaded to the blockchain, does not contain any special mechanisms to help us with migration, i.e. in fact, we have a regular ERC20-compatible contract. The only thing that we can take useful from the initial contract is the balances of all token holders and the total number of tokens issued to verify that we did not forget anyone during the migration.

    contract ERC20base {
        uint public totalSupply;
        function balanceOf(address _who) public constant returns(uint);
    }

    Unfortunately, the interface of the ERC20-compatible contract does not allow to find out a list of all token holders, so during migration we will have to find out the full list of holders from some other source, for example, by downloading it from etherscan.io . An example of a migration contract is shown in the following listing:

    contract NewContract {
        uint public totalSupply;
        mapping (address => uint) balanceOf;
        function NewContract(address _migrationSource, address [] _holders) public {
            for(uint i=0; i<_holders.length; ++i) {
                uint balance = ERC20base(_migrationSource).balanceOf(_holders[i]);
                balanceOf[_holders[i]] = balance;
                totalSupply += balance;
            }
            require(totalSupply == ERC20base(_migrationSource).totalSupply());
        }
    }

    The constructor of the contract receives as parameters the address of the source ERC20-compatible contract, as well as a list of token holders, manually unloaded via etherscan.io. It should be noted that in the last term of the constructor we verify that the number of tokens has not changed after migration, and therefore, no token holder is forgotten. It should be borne in mind that such migration is possible only if the number of token holders is small and the cycle for all of them is possible within one transaction (gas limit set in Ethereum for one transaction). If, however, the number of token holders does not allow you to migrate in one transaction, then this functionality will have to be transferred to a separate function that can be called up as many times as necessary, and in this case the contract will look like this:

    contract NewContract {
        uint public totalSupply;
        mapping (address => uint) balanceOf;
        address public migrationSource;
        address public owner;
        function NewContract(address _migrationSource) public {
            migrationSource = _migrationSource;
            owner = msg.sender;
        }
        function migrate(address [] _holders) public
            require(msg.sender == owner);
            for(uint i=0; i<_holders.length; ++i) {
                uint balance = ERC20base(_migrationSource).balanceOf(_holders[i]);
                balanceOf[_holders[i]] = balance;
                totalSupply += balance;
            }
        }
    }

    In the constructor of this contract, the address of the original contract is remembered, and the owner field is also initialized to remember the address of the contract owner, so that only he has the right to call the migrate () function, by calling it several times, we can migrate any number of token holders from the original contract.

    The disadvantages of this solution are as follows:

    1. On the old smart contract, tokens will remain with their owners, and on the new, their balances will simply be duplicated. How bad this is depends on how your Tokens sale agreement or any other document is written that describes the scope of your obligations to the token holders of your project, and whether your obligations to them will double after creating a “duplicate”.
    2. You spend your own gas on migration, but this, in general, is logical, because You decided to do the migration and in any case inconvenience your users, although it is limited to the fact that they need to rewrite the smart contract address from the old to the new in their wallets.
    3. In the process of migration, if it certainly does not fit in one transaction, token transfers may occur between the addresses of their owners, and therefore, new holders may be added and the balance of existing ones may change.

    But, unfortunately, nothing can be done about it, and for a more comfortable and convenient migration, some auxiliary means should be provided in the original contract.

    Migration between crowdsale stages


    In the world of modern ICOs, the practice is quite common when they make separate contracts for various stages of fundraising, migrating the issued tokens to new contracts of a new stage. This, of course, can be done as we examined above, but if we know for sure that we will have to migrate, then why not immediately simplify our life? To do this, just enter the public field

        address [] public holders;

    All token holders must be added to this field. If the contract already in the early stages of the collection allows the holders to move the tokens, i.e. implements transfer (), you need to make sure that the array is updated, for example, something like this

        mapping (address => bool) public isHolder;
        address [] public holders;
        ….
        if (isHolder[_who] != true) {
            holders[holders.length++] = _who;
            isHolder[_who] = true;
        }

    Now, on the side of the acceptance contract, you can use the migration technology similar to that considered earlier, but now there is no need to transfer the array as a parameter, it is enough to refer to the already prepared array in the original contract. It should also be remembered that the size of the array may not allow it to be iterated in one transaction due to the gas restriction per transaction, and therefore, it is necessary to provide the migrate () function, which will receive two indexes - the numbers of the initial and final elements of the array for processing within this transactions.

    The disadvantages of this solution are generally the same as the previous one, except that now there is no need to upload the list of token holders via etherscan.io.

    Migration with the burning of source tokens


    Still, since we are talking about migration, and not about duplication of tokens in a new smart contract, we need to take care of the issue of destroying (burning) tokens on the original contract when creating a copy of them on the new one. Obviously, it is unacceptable to leave a “hole” in the smart contract that would allow anyone, even the owner of the smart contract, to burn tokens of other holders. Such a smart contract will be just a scam! Only their holder can carry out such manipulations on their tokens, and therefore, the holder itself must carry out the migration. The owner of the smart contract in this case can only start this migration (put the smart contract in the migration state). An example of the implementation of such migration I met in the GOLEM project (a link to their github at the end of the post), then I implemented it in several of my projects.

    In the original contract, we define the MigrationAgent interface, which should subsequently be implemented in the contract to which the migration is carried out.

    contract MigrationAgent {
        function migrateFrom(address _from, uint256 _value);
    }

    The following additional functionality should be implemented in the initial token contract:

    contract TokenMigration is Token {
        address public migrationAgent;
        // Migrate tokens to the new token contract
        function migrate() external {
            require(migrationAgent != 0);
            uint value = balanceOf[msg.sender];
            balanceOf[msg.sender] -= value;
            totalSupply -= value;
            MigrationAgent(migrationAgent).migrateFrom(msg.sender, value);
        }
        function setMigrationAgent(address _agent) external {
            require(msg.sender == owner && migrationAgent == 0);
            migrationAgent = _agent;
        }
    }

    Thus, the owner of the original smart contract must call setMigrationAgent (), passing it the address of the smart contract to which the migration is performed as a parameter. After that, all token holders of the original smart contract must call the migrate () function, which will destroy their tokens in the original smart contract and add a new one in the new one (by calling the migrateFrom () function of a new contract). Well, the new contract should actually contain the implementation of the MigrationAgent interface, for example, like this:

    contract NewContact is MigrationAgent {
        uint256 public totalSupply;
        mapping (address => uint256) public balanceOf;
        address public migrationHost;
        function NewContract(address _migrationHost) {
            migrationHost = _migrationHost;
        }
        function migrateFrom(address _from, uint256 _value) public {
            require(migrationHost == msg.sender);
            require(balanceOf[_from] + _value > balanceOf[_from]); // overflow?
            balanceOf[_from] += _value;
            totalSupply += _value;
        }
    }

    Everything is perfect in this solution! In addition, the user needs to call the migrate () function. The situation is significantly complicated by the fact that only a few wallets support function calls and they, as a rule, are not the most convenient. Therefore, believe me, if among the holders of your tokens there are not only cryptogics, but also ordinary mortal people, they will just curse you when you explain to them that you need to install some kind of Mist, and then call some function (thank God, even without parameters). How to be?

    And you can do it very simply! After all, any cryptocurrency user, even the very most beginner one, can do one thing well - send the crypt from his address to some other. So let this address be the address of our smart contract, and its fallback function in the "migration" mode will simply call migrate (). Thus, it will be enough for the holder of tokens to transfer at least 1 wei to the address of the smart contract in the “migration” mode for a miracle to happen!

    function () payable {
        if (state = State.Migration) {
            migrate();
        } else { … }
    }

    Conclusion


    The considered solutions conceptually cover all possible ways of migrating tokens, although variations in specific implementations are possible. The “distillation vessel” approach deserves special attention (link at the end of the post). Regardless of your approach to migration, remember that a smart contract is not just a program running inside an Ethereum virtual machine, but a kind of alienated independent contract, and any migration assumes that you change the terms of this contract. Are you sure that the token holders want to change the terms of the contract they concluded when purchasing tokens? This is actually a good question. And there is a very good practice, “asking” token holders about whether they want to “move” to a new contract. It was the implementation of migration through voting that I implemented in the smart contract of my projectPROVER , the text of the contract can be found on my GitHub. And of course I invite you to join the ICO of my PROVER project .

    I hope that all this is useful and necessary for someone :).

    useful links



    Also popular now: