Writing scalable smart contracts

Original link

When writing upgradable contracts with OpenZeppelin Upgrades, there are a few things to keep in mind when writing Solidity code.

It's worth mentioning that these limitations stem from the way the Ethereum Virtual Machine works, and apply to all projects using upgradeable contracts, not just OpenZeppelin Upgrades.

Initializers

When writing Solidity contracts using OpenZeppelin Upgrades, no modification is required, only the constructor needs to be modified. Constructors cannot be used in upgradable contracts due to the requirements of the agent-based upgradability system. To understand the reasoning behind this limitation, see Proxies .

This means that when using an OpenZeppelin upgradeable contract, you need to change its constructor to a regular function, usually named initialize, where all the initialization logic is performed.

// NOTE: Do not use this code snippet, it's incomplete and has a critical vulnerability!

pragma solidity ^0.6.0;


contract MyContract {
    uint256 public x;

    function initialize(uint256 _x) public {
        x = _x;
    }
}

However, while Solidity ensures that a constructor constructoris only called once during the lifetime of the contract, a normal function can be called multiple times. To prevent a contract from being initialized multiple times, you need to add a check to ensure that the initialization function is only called once.

// contracts/MyContract.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;


contract MyContract {
    uint256 public x;
    bool private initialized;

    function initialize(uint256 _x) public {
        require(!initialized, "Contract instance has already been initialized");
        initialized = true;
        x = _x;
    }
}

Since this pattern is so common when writing upgradeable contracts, OpenZeppelin Upgrades provides a Initializablebase contract that has a initializer modifier to handle this.

// contracts/MyContract.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;

import "@openzeppelin/upgrades/contracts/Initializable.sol";


contract MyContract is Initializable {
    uint256 public x;

    function initialize(uint256 _x) public initializer {
        x = _x;
    }
}

Another difference between constructors constructorand normal functions is that Solidity is responsible for automatically calling the constructors of all base classes of a contract. When writing initializers, you need to pay special attention to manually calling the initializers of all parent contracts.

// contracts/MyContract.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;

import "@openzeppelin/upgrades/contracts/Initializable.sol";


contract BaseContract is Initializable {
    uint256 public y;

    function initialize() public initializer {
        y = 42;
    }
}


contract MyContract is BaseContract {
    uint256 public x;

    function initialize(uint256 _x) public initializer {
        BaseContract.initialize(); // Do not forget this call!
        x = _x;
    }
}

Use an upgradable smart contract library

Keep in mind that this limitation affects not only your contracts, but also contracts you import from the library. Consider, for example, ERC20 in an OpenZeppelin contract : the contract initializes the token's name, sign and scale in its constructor.

// @openzeppelin/contracts/token/ERC20/ERC20.sol
pragma solidity ^0.6.0;

  ...

contract ERC20 is Context, IERC20 {

  ...

    string private _name;
    string private _symbol;
    uint8 private _decimals;

    constructor (string memory name, string memory symbol) public {
        _name = name;
        _symbol = symbol;
        _decimals = 18;
    }

  ...
}

This means that you should not use these contracts in your OpenZeppelin Upgrades project. Instead, make sure to use @openzeppelin/contracts-upgradeable, which is an official fork of OpenZeppelin contracts that has been modified to use initializers instead of constructors. Take a look at what @openzeppelin/contracts-upgradeablethe ERC20Upgradeable looks like in:

// @openzeppelin/contracts-upgradeable/contracts/token/ERC20/ERC20Upgradeable.sol
pragma solidity ^0.6.0;
  ...
contract ERC20Upgradeable is Initializable, ContextUpgradeable, IERC20Upgradeable {
  ...
    string private _name;
    string private _symbol;
    uint8 private _decimals;

    function __ERC20_init(string memory name, string memory symbol) internal initializer {
        __Context_init_unchained();
        __ERC20_init_unchained(name, symbol);
    }

    function __ERC20_init_unchained(string memory name, string memory symbol) internal initializer {
        _name = name;
        _symbol = symbol;
        _decimals = 18;
    }
  ...
}

Whether using OpenZeppelin contracts or another smart contract library, make sure the package is set up to handle upgradable contracts.

Learn more about OpenZeppelin contract upgradeability in Contracts:  Contracts: Using with Upgrades .

Avoid using initial values ​​in field declarations

Solidity allows defining initial values ​​for fields when they are declared in a contract.

contract MyContract {
    uint256 public hasInitialValue = 42; // equivalent to setting in the constructor
}

This is equivalent to setting these values ​​in the constructor, and as such, is not valid for upgradable contracts. Make sure that all initial values ​​are set in the initialization function as shown below; otherwise, none of the upgradable instances will have these fields set.

contract MyContract is Initializable {
    uint256 public hasInitialValue;

    function initialize() public initializer {
        hasInitialValue = 42; // set initial value in initializer
    }
}

Note that it is still okay to  define constant state variables, because the compiler does not reserve storage slots for these variables , and each occurrence will be replaced by the corresponding constant expression. So the following will still work in OpenZeppelin Upgrades:

contract MyContract {
    uint256 public constant hasInitialValue = 42; // define as constant
}

Create new instance from contract code

When creating a new contract instance from contract code, these creations are handled directly by Solidity, not by OpenZeppelin Upgrades, which means these contracts will not be able to be upgraded.

For example, in the following example, the tokencontract created is not upgradable even though MyContract is deployed as upgradable:

// contracts/MyContract.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;

import "@openzeppelin/upgrades/contracts/Initializable.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";

contract MyContract is Initializable {
    ERC20 public token;

    function initialize() public initializer {
        token = new ERC20("Test", "TST"); // This contract will not be upgradeable
    }
}

If you want ERC20the instance to be upgradable, the easiest way to do it is to directly accept the instance of the contract as a parameter and take over it after creation:

// contracts/MyContract.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;

import "@openzeppelin/contracts-upgradeable/contracts/proxy/Initializable.sol";
import "@openzeppelin/contracts-upgradeable/contracts/token/ERC20/IERC20Upgradeable.sol";

contract MyContract is Initializable {
    IERC20 public token;

    function initialize(IERC20Upgradeable _token) public initializer {
        token = _token;
    }
}

Potentially unsafe operation

When using upgradeable smart contracts, you will always be interacting with a (proxy) contract instance, not the underlying logic contract. However, we cannot prevent malicious actors from sending transactions directly to logic contracts. This is not a threat because any changes to the logic contract state will not affect your (proxy) contract instance since the storage of the logic contract is never used in your project.

However, there is one exception. If a direct call to the logic contract triggers a self-destruct operation selfdestruct, then the logic contract will be destroyed and all your contract instances will eventually delegate all calls to an address without any code. This will destroy all contract instances in your project.

A similar effect can also be achieved if the logical contract contains delegated invocation delegatecalloperations. If it can be delegatecallturned into a malicious contract that contains self-destruction, the calling contract will be broken.

selfdestructTherefore, using or is not allowed in your contract delegatecall.

modify your contract

When writing a new version of a contract, whether due to a new feature or a bug fix, there is an additional restriction to obey: you cannot change the order in which the contract state variables are declared, nor their type. You can  read more about the reasoning behind this limitation by learning about Proxies .

WARNING  Violation of any of these storage layout restrictions will result in obfuscated storage values ​​for upgraded contracts and potentially lead to critical bugs in your application.
This means, if the initial contract looks like this:

contract MyContract {
    uint256 private x;
    string private y;
}

Then the contract variable type cannot be modified:

contract MyContract {
    string private x;
    string private y;
}

There is also no way to change the declaration order of variables:

contract MyContract {
    string private y;
    uint256 private x;
}

New variables cannot be introduced before existing variables:

contract MyContract {
    bytes private a;
    uint256 private x;
    string private y;
}

Nor can you delete existing variables:

contract MyContract {
    string private y;
}

If you need to introduce a new variable, make sure to add it after the original variable:

contract MyContract {
    uint256 private x;
    string private y;
    bytes private z;
}

Note that if you rename a variable, after the upgrade it will keep the same value as before. If the semantics of the new variable and the old variable are the same, then this might be the desired behavior:

contract MyContract {
    uint256 private x;
    string private z; // starts with the value from `y`
}

And if you delete a variable at the end of the contract, please note that the storage will not be cleared. Adding a new variable in subsequent updates will cause the variable to read the remaining value from the deleted variable:

contract MyContract {
    uint256 private x;
}

upgrade to:

contract MyContract {
    uint256 private x;
    string private z; // starts with the value from `y`
}

Note that you may also inadvertently change the contract's storage variables by changing the contract's parent contract. For example, if you have the following contract:

contract A {
    uint256 a;
}


contract B {
    uint256 b;
}


contract MyContract is A, B {}

Then modifying it by swapping the declaration order of the base contract or introducing a new base contract MyContractwill change how the variables are actually stored:

contract MyContract is B, A {}

You also cannot add new variables to the base contract if the integration contract has any of its own variables. Given the following circumstances:

contract Base {
    uint256 base1;
}


contract Child is Base {
    uint256 child;
}

If modified Base, add an extra variable:

contract Base {
    uint256 base1;
    uint256 base2;
}

The variable base2will then be assigned to that childslot in the previous version. A workaround is to declare unused variables on the base contract, which you may want to extend in the future as a means of "reserving" these slots. Note that this trick does not increase gas usage

Guess you like

Origin blog.csdn.net/weixin_39842528/article/details/122505523