📜 ⬆️ ⬇️

Ethereum Smart Contracts: What to do when you make a mistake 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 corrected! 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 possible option is to download a new contract with the corrected code. But what if there are already tokens issued on the basis of the existing contract? Migration comes to the rescue! Over the past year, I have tried many different techniques for its implementation, analyzed the projects used in other major blockchain projects and invented something myself. Details under the cut.


At once I will make a reservation that within the framework of this post I will not bring sheets of ready-made smart contracts, and I will only consider and analyze various techniques. Almost all of them in one form or another have been implemented by me in contracts for projects in which I have been involved, and much can be taken from my GitHub .

Migration from ERC20-compatible contract


Let us begin with the simplest and most common case, when the original contract, already loaded in 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 we can take from the original contract is the balance sheets of all holders of tokens 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 know the list of all holders of tokens, so during migration we will have to find out the full list of holders from some other source, for example, by unloading it from etherscan.io . An example of the contract to which the migration is carried out is given 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 contract constructor receives as parameters the address of the original ERC20-compatible contract, as well as the list of token holders manually downloaded via etherscan.io. It should be noted that in the last term of the designer we check that the number of tokens has not changed after the migration, and therefore, no token holder is forgotten. It is necessary to take into account that such migration is possible only if the number of token holders is small and the cycle for them is all possible within one transaction (the gas limit set in Ethereum for one transaction). If, however, the number of holders of tokens does not allow migrating in one transaction, then this functionality will have to be put into a separate function that can be called the necessary number of times, and the contract in this case 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 memorized, and the owner field is initialized to remember the address of the contract owner, so that only he has the right to call the migrate () function, which, once called, can migrate any number of tokens holders from the original contract.

The disadvantages of this solution are as follows:

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

But with this, unfortunately, nothing can be done, and for a more comfortable and convenient migration, you should provide some kind of auxiliary means in the original contract.

Migration between crowdsale stages


In the world of modern ICO, the practice is quite common when individual contracts are made for different stages of fundraising, migrating issued tokens to new contracts of a new stage. This, of course, can be done as we have discussed above, but if we know for sure that we will have to migrate, then why not just simplify our life for ourselves? To do this, simply enter the public field

  address [] public holders; 

All tokens holders must be added to this field. If the contract already at the early stages of collection allows the holders to transfer tokens, i.e. implements transfer (), you need to ensure that the array is updated, for example, somehow

  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 same migration technology discussed earlier, but now there is no need to transfer the array as a parameter, just refer to the already prepared array in the original contract. It should also be remembered that the size of the array may not allow to iterate it in one transaction due to the restriction of gas to one transaction, and therefore, it is necessary to provide the function migrate (), which will receive two indices - the numbers of the initial and final elements of the array for processing within this transactions.

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

Migration with burning source tokens


After all, since we are talking about migration, and not about duplication of tokens in a new smart contract, it is necessary to attend to the issue of the destruction (burning) of tokens on the original contract when creating their copy on the new one. Obviously, it is unacceptable to leave a “hole” in the smart contract, which would allow anyone, even if he were the owner of a smart contract, to burn tokens of other holders. Such a smart contract will be just scam! Perform this kind of manipulation of their tokens can only their holder, and hence the holder must carry out the migration. The owner of the smart contract in this case can only start this migration (transfer the smart contract to the migration state). I met an example of such a migration in the GOLEM project (link to their github at the end of the post), then implemented it in several of my projects.

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

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

The following additional functionality must be implemented in the original 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 source smart contract should call setMigrationAgent (), passing to it the address of the smart contract to which the migration is performed as a parameter. After that, all holders of tokens of the original smart contract should call the migrate () function, which will destroy their tokens in the original smart contract and add a new one (by calling the migrateFrom () function of the 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 great in this decision! In addition, the user must call the function migrate (). The situation is significantly complicated by the fact that function calls are supported by only a few wallets and, as a rule, they are not the most convenient. Therefore, believe me, if among the holders of your tokens there are not only cryptotogics, but also mere 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 kind of function (thank God, at least without parameters). How to be?

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

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

Conclusion


The considered solutions conceptually cover all possible ways to implement token migration, although variations are possible in specific implementations. Separate attention is worthy of the “distillery vessel” approach (link at the end of the post). Regardless of the migration approach you use, remember that a smart contract is not just a program running inside an Ethereum virtual machine, but a kind of alienated independent contract, but any migration assumes that you change the terms of this contract. Are you sure that tokens holders want to change the terms of the contract that they signed when they acquired 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. I implemented migration through voting in the smart contract of my PROVER project, 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


Source: https://habr.com/ru/post/339102/


All Articles