📜 ⬆️ ⬇️

Ethereum renewable smart contracts

Almost every programmer who writes Ethereum smart contracts has questions: “What if you need to expand the functionality of contracts? What if there is a bug in the contract that causes a loss of funds? What if there is a vulnerability in the solidity compiler (which has happened more than once)? ”After all, the contracts that we load into the network cannot be changed. At first it is quite difficult to realize: how can this code not be updated? Why? But this is partly the strength of Ethereum smart contracts - users would probably have less trust in contracts that can be changed.

We will try to make out several approaches that still allow us to change smart contracts.

This article is designed for those who have at least the basic skills of programming in the solidity language and understand the basic principles of the Ethereum network.

Split a smart contract into multiple related contracts.


In this case, you can save the addresses of currently active contracts in the storage of any of the contracts. Often allocate any one contract, which is responsible for storing and changing references to parts of the entire system.
')
As an example, we can give a contract to sell tokens, in which the rules for calculating the number of tokens that need to be sent to the eter's wallet are not clearly stated. The calculation of the amount can deal with a separate contract, which we will be able to replace in case of need. We will not dwell on this option for a long time, because this approach is often used not only in solidity.

One of the main drawbacks of this approach is that there is no way to change the interface of a contract that is external to the entire system. You cannot add or remove a function.

Use delegatecall to proxy a call to another contract.


In EIP-7 , an instruction was proposed and implemented that allows calling code from another contract, but the context of the call remains the same as that of the current contract. That is, the called contract will write to the storage of the calling contract, msg.sender and msg.value remain the same as originally.

In the network you can find several examples of the implementation of this mechanism. They all include the use of a solidity assembly. Without assembly, it is not possible to get any value returned from delegatecall.

The main idea of ​​all the methods that delegatecall uses for proxying is the implementation of the fallback function. It is necessary to read the calldata and pass on through the delegatecall.
Take a closer look at a few examples of implementation:

  1. Upgradeable stores the size of the return values ​​in the mapping.

    Here is the implementation of the fallback function from here:

     bytes4 sig; assembly { sig := calldataload(0) } var len = _sizes[sig]; var target = _dest; assembly { // return _dest.delegatecall(msg.data) calldatacopy(0x0, 0x0, calldatasize) delegatecall(sub(gas, 10000), target, 0x0, calldatasize, 0, len) return(0, len) } 

    In this case, the size of the return value (in bytes) is stored in mapping _sizes. This field in storage must be filled in when renewing a contract.

    The disadvantage of this approach is that the size of the returned value is rigidly tied to the signature of the called function, that is, to return a string of arbitrary size or an array of bytes does not work.

    In addition, access to storage is quite expensive. And in this case, we will have two calls to storage: when we access the _dest field and when we access the _size field.
  2. EVM assembly tricks : always use a response size of 32 bytes.

    The code is very similar to the previous example, but the answer size is always 32 bytes. This is a fairly balanced decision. Firstly, most types in solidity fit exactly 32 bytes, and secondly, without resorting to storage once again, we save quite a decent amount of gas. Later we estimate how much gas is spent in various embodiments.
  3. Using new resultdatasize and resultdatacopy instructions
    These instructions appeared in the main Ethereum network only after the last hard forks (Byzantium - October 17, 2017).

    The instructions allow you to get the size of the answer that is returned from call / delegatecall, as well as copy the answer itself into memory. That is, we were able to implement a full proxy for any size returndata.

    Here is the final assembly code:

             assembly {
                 let _target: = sload (0)
                 calldatacopy (0x0, 0x0, calldatasize)
                 let retval: = delegatecall (gas, _target, 0x0, calldatasize, 0x0, 0)
                 let returnsize: = returndatasize
                 returndatacopy (0x0, 0x0, returnsize)
                 switch retval case 0 {revert (0, 0)} default {return (0, returnsize)}
             }
    

Consider the use of gas. Testing shows that all 3 of the above methods increase the use of gas by a value from 1000 to 1500. Is this a lot or a little? This is about 2% of the more or less average cost of the transaction that will change storage.

Difficulty in using


Unfortunately, the use of these techniques is limited. First of all, in order for such renewal of contracts to work, the structure of data storage in the contract cannot be changed (fields cannot be rearranged, fields must be deleted). You can add fields to new contract versions.

It is also necessary to very carefully distinguish between access to a function that changes the address of the active contract.

An important fact is that users' trust in the contract will be less than in the same unchangeable. On the other hand, you can provide a test period of time during which the new version of the contract can be rolled back, and after which the version of the contract will be fixed and will no longer be able to change.

Examples of the implementation of updates


Several contracts that will help make the upgrade easier and safer.
Upgradeable - this contract implements a check that the target field (the address of the active version of the contract) is stored in the same slot as in the current version.

Similarly, you can implement checks on other storage fields (an example can be found in Target.sol )
If you are planning to implement Upgradeable contracts, then be sure to look at the tests for the Upgradeable contract.

Before deploying such contracts to the network, it is necessary to test all the options. Otherwise, after the next update, you can be left without a functioning contract and without the possibility of renewal.

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


All Articles