How do I upgrade a smart contract on ethereum? (Part 1)

Tram Ho

1. Why do you need to upgrade smart contract?

A smart contract once deployed to the network, it will exist permanently on it and cannot be modified. This is the biggest advantage of Blockchain, however, in some cases it is limited.

For example, a programmer discovered a flaw in his smart contract logic, it could be exploited by a hacker and caused damage to him, now he wants to find a way to both patch. The vulnerability is both keeping all the data of that smart contract.

In a normal mindset, he would back up all the data of the old smart contract somewhere off the network, deploy a new smart contract that fixed the vulnerability, and then migrate the backed up data to it, but this way. the following limitations:

  • this is only possible when the data is relatively small
  • force new smart contract to have a function to call migrate data => cause trouble for smart contract
  • consume a lot of gas

So, how does a smart contract have to be deployed network, after running for a while, a lot of data is stored on it, if suddenly discovered that there is a hole in logic, it is still possible for the smart contract to run in Other logic more secure without deploying a new version and then migrating the data? It has been studied a very nice way based on the function of delegatecall() function to do this.

2. How the delegatecall updates the storage of the contract

As you know, in solidity there are many ways to call low-level call from contract A to contract B including:

  • call ()
  • staticcall ()
  • delegatecall ()

In which, delegatecall() operates in a special way, which can be understood as follows: When a contract X performs delegatecall() to a contract Y , it will perform the logic of contract Y but under the context and storage of contract X . The example has the below source code:

Both contract A and B declare variable as _balances , in contract A whose function is delegateCallToB will delegatecall() to function setBalance() of B.

We deploy 2 contracts to the network using Remix then:

  • use some user_address (address 0x8f287eA4DAD62A3A626942d149509D6457c2516C for example) call delegateCallToB() function of A with parameters of B’s ​​deploy address and an integer of 10 for example => delegateCallToB(B_contract_address, 10)
  • querying the sender and balance of B, the result is 0x0000000000000000000000000000000000000000 and 0 .
  • query the sender and balance of A, the result is 0x8f287eA4DAD62A3A626942d149509D6457c2516C and 10 .

 

If we now deploy to add a C contract with the following source code:

Perform:

  • call delegateCallTo(B) function of A with parameters C_contract_address and 3 => delegateCallToB(C_contract_address, 3) .
  • querying sender and balance of C, the result is 0x0000000000000000000000000000000000000000 and 0 .
  • query the sender and balance of A, the result is 0x8f287eA4DAD62A3A626942d149509D6457c2516C and 16 .

Thus, that is completely true with the properties of delegatecall() that we mentioned above.

When a contract X performs delegatecall() to a contract Y , it will execute the logic of contract Y but under the context and storage of contract X

3. Deploying a upgradeable contract in the Inherited Storage way

Thus, from the properties of delegatecall() , we have the possibility to upgrade a contract. However, how to properly deploy it is another matter.

In essence, the way that delegatecall() affects the storage of contract X is that it affects the order of the corresponding slot between X and Y. For example, contract D is as follows:

the order of declaring the two variables balance and sender of D is different from A, B, C. Implementation:

  • call delegateCallToB(B) function of A with parameters D_contract_address and 10 => delegateCallToB(D_contract_address, 10)
  • querying sender and balance of D, the result is 0x0000000000000000000000000000000000000000 and 0
  • query the sender and balance of A, the result is 0x000000000000000000000000000000000000000A (the result of address (10)) and 817288742280969564811718162174206570979710357868 (the result of uint256 (0x8f287eA4DAD62A3A626942d149509D6457c). Should be sender = 0x8f287eA4DAD62A3A626942d149509D6457c2516C and balance = 10 ?

 

The reason is, between contract A and contract D due to conflicts in the order of declaring variables, resulting in false results. Thus, when we want to implement an upgradeable contract, we must agree on the order of declaring the signs holding the contract as storage (contract A) and the contract as a logic (contract B, C , D). It can be implemented in short as follows:

This implementation is called Inherited Storage . However it does have some disadvantages:

  • Logical contracts can inherit a storage in which there can be variables it does not use.
  • Logical contracts must be tightly linked with the storage contract
  • Difficulty in managing private seas
  • Easy to create storage conflicts
  • It is imperative that the storage contract has the necessary get-set functions

There is also a way that you can use to deploy an upgradeable contract similar to Inherited Storage is Eternal Storage you can refer to here .

At the end of part 1, I would like to introduce to you:

  • Why must upgrade contract
  • delegatecall () is the main technique people use to make a contract upgradeable (upgradeable contract).
  • The simplest implementation is Inherited Storage.

In part 2, I will go later on the way to Unstructured Storage, which has been standardized and published by Openzeppelin on my github.

Share the news now

Source : Viblo