Jak psát upgradovatelné inteligentní smlouvy (verze inteligentních smluv)

Tento článek byl poprvé publikován na naší open-source platformě SimpleAsWater.com. Pokud vás zajímají IPFS, Libp2p, Ethereum, Zero-knowledge Proofs, Defi, CryptoEconomics, IPLD, Multi formats a další projekty Web 3.0, koncepty a interaktivní výukové programy, pak se určitě podívejte na SimpleAsWater.com.

Inteligentní smlouvy jsou neměnné, od návrhu. Na druhou stranu kvalita softwaru silně závisí na schopnosti upgradovat a opravovat zdrojový kód za účelem vytváření iterativních vydání. Přestože software založený na blockchainu významně těží z neměnnosti této technologie, stále je pro opravu chyb a potenciální vylepšení produktu potřebná určitá míra proměnlivosti.

V tomto příspěvku se naučíme:

  1. Proč potřebujeme upgradovat chytré smlouvy?
  2. Chápete, jak fungují upgrady pod kapotou?
  3. Používání rozhraní OpenZeppelin CLI ke snadnému psaní/správě „upgradovatelných“ inteligentních smluv.
  4. Programově upgradovat smlouvy pomocí knihovny OpenZeppelin Upgrades.
  5. Některá omezení a zástupná řešení smluv o upgradu

Pokud právě hledáte způsob, jak sepsat upgradovatelné smlouvy a nechcete procházet „jak to všechno funguje“, pak zamiřte do 3. sekce.

Proč musíme upgradovat?

Inteligentní smlouvy v Ethereu jsou ve výchozím nastavení neměnné. Jakmile je vytvoříte, neexistuje žádný způsob, jak je změnit, což účinně působí jako neporušitelná smlouva mezi účastníky.

Existuje však několik scénářů, kdy bychom si přáli, aby existoval způsob, jak upgradovat smlouvy. Existuje mnoho příkladů, kdy byl Ether ukraden/hacknut v hodnotě milionů dolarů, což by se dalo ušetřit, kdybychom mohli aktualizovat chytré smlouvy.

Jak fungují upgrady pod kapotou?

Existuje několik způsobů, jak upgradovat naše smlouvy.

Nejviditelnější způsob bude něco takového:

  • Vytvořte a nasaďte novou verzi smlouvy.
  • Ručně migrujte všechny stavy ze staré smlouvy do nové smlouvy.

Zdá se, že to funguje, ale má několik problémů.

  1. Migrace stavu smlouvy může být nákladná.
  2. Když vytváříme a nasazujeme novou smlouvu, adresu smlouvy změní se. Budete tedy muset aktualizovat všechny smlouvy, které interagovaly se starou smlouvou, aby používali adresu nové verze.
  3. Také byste museli oslovit všechny své uživatele a přesvědčit je, aby začali používat novou smlouvu a řešili používání obou smluv současně, protože uživatelé migrují pomalu.

Lepší způsob je použít proxy smlouvu s rozhraním, kde každá metoda deleguje k implementaci kontrakt (který obsahuje veškerou logiku).

volání delegáta je podobný běžnému volání, kromě toho, že veškerý kód je spuštěn v kontextu volajícího (proxy ), ne volaného (implementace ). Z tohoto důvodu transfer v kódu implementační smlouvy převede zůstatek proxy a všechna čtení nebo zápisy do úložiště smlouvy budou číst nebo zapisovat z úložiště proxy.

Tento přístup je lepší, protože uživatelé komunikují pouze s proxy smlouvy a můžeme změnit implementaci smlouvu při zachování stejného proxy smlouvy.

Zdá se to lepší než předchozí přístup, ale pokud potřebujeme provést nějaké změny v implementaci smluvních metod, museli bychom aktualizovat proxy také metody smlouvy (protože smlouva o zastoupení má metody rozhraní). Uživatelé tedy budou muset změnit adresu proxy.

K vyřešení tohoto problému můžeme použít záložní funkci v proxy smlouvě. Záložní funkce se spustí na jakýkoli požadavek a přesměruje požadavek na implementaci a vrácení výsledné hodnoty (pomocí operačních kódů). To je podobné předchozímu přístupu, ale zde smlouva o zastoupení nemá metody rozhraní, pouze záložní funkci, takže není třeba měnit adresu proxy, pokud se změní metody smlouvy.

Toto bylo základní vysvětlení, které nám stačí pro práci s upgradovatelnými smlouvami. V případě, že se chcete ponořit hluboko do kódu smlouvy proxy a různých vzorů proxy, pak se podívejte na tento příspěvek.

Jak upgradovatelné chytré smlouvy fungují pod pokličkou

Upgrady OpenZeppelin

Jak jsme viděli výše, při psaní upgradovatelných smluv je potřeba spravovat spoustu věcí.

Naštěstí projekty jako OpenZeppelin vytvořily nástroje a knihovny CLI, které poskytují snadno použitelný, jednoduchý, robustní a volitelný mechanismus upgradu pro chytré smlouvy, který může být řízen jakýmkoliv typem řízení, ať už se jedná o multi- sig wallet, jednoduchá adresa nebo složité DAO.

Pojďme nejprve vytvořit základní upgradovatelnou smlouvu pomocí nástroje OpenZeppelin CLI. Můžete najít kód pro níže uvedenou implementaci zde .

OpenZeppelin upgraduje CLI

Práce s OpenZeppelin CLI vyžaduje pro vývoj Node.js. Pokud jej ještě nemáte, nainstalujte uzel pomocí libovolného správce balíčků nebo pomocí oficiálního instalačního programu.

Nastavení projektu

Vytvořte složku s názvem upgradable-smart-contracts a přejděte do složky.

$ mkdir upgradable-smart-contracts && cd upgradable-smart-contracts 

Pro tento tutoriál použijeme místní blockchain. Nejoblíbenějším místním blockchainem je Ganache. Chcete-li jej nainstalovat a spustit ve svém projektu, spusťte:

$ npm install --save-dev ganache-cli && npx ganache-cli --deterministic

Nyní spusťte nový shell/terminál ve stejné složce a spusťte následující příkaz pro instalaci nástroje CLI:

$ npm install --save-dev @openzeppelin/cli

Chcete-li spravovat své nasazené smlouvy, musíte vytvořit nový projekt CLI. Spusťte následující příkaz a po zobrazení výzvy mu poskytněte název a číslo verze projektu:

$ npx openzeppelin init

Během inicializace se stanou dvě věci. Nejprve .openzeppelin bude vytvořen adresář obsahující informace specifické pro projekt. Tento adresář bude spravován CLI:nebudete muset nic ručně upravovat. Některé z těchto souborů byste však měli odevzdat Gitu.

Za druhé, CLI uloží konfiguraci sítě do souboru s názvem networks.js . Pro usnadnění je již vyplněn záznamem nazvaným development s konfigurací odpovídající výchozímu nastavení Ganache.

Všechny odemčené účty můžete zobrazit spuštěním následujícího příkazu:

$ npx openzeppelin accounts

Seznam odemčených účtů

Psaní a nasazení smluv

Nyní vytvoříme smlouvu s názvem TodoList v contracts složku.

// contracts/TodoList.sol
pragma solidity ^0.6.3;

contract TodoList {
    string[] private list;

    // Emitted when the storeda new item is added to the list
    event ItemAdded(string item);

    // Adds a new item in the list
    function addItem(string memory newItem) public {
        list.push(newItem);
        emit ItemAdded(newItem);
    }

    // Gets the item from the list according to index
    function getListItem(uint256 index)
        public
        view
        returns (string memory item)
    {
        return list[index];
    }
}

Nyní nasadíme tuto smlouvu na místní blockchain.

$ npx openzeppelin create

Jak vidíme, naše smlouva je nasazena na 0xD833215cBcc3f914bD1C9ece3EE7BF8B14f841bb .

Pojďme přidat položku („odpovídání na e-maily“) do list pole pomocí addItem() spuštěním npx openzeppelin send-tx .

Nyní předpokládejme, že potřebujeme přidat novou funkci s názvem getListSize() získat velikost seznamu. Stačí přidat novou funkci do TodoList smlouva.

// contracts/TodoList.sol
pragma solidity ^0.6.3;

contract TodoList {
    // ...


    // Gets the size of the list
    function getListSize() public view returns (uint256 size) {
        return list.length;
    }
}

Po změně souboru Solidity můžeme nyní pouze upgradovat instanci, kterou jsme nasadili dříve, spuštěním openzeppelin upgrade příkaz.

Hotovo! Naše TodoList instance byla upgradována na nejnovější verzi kódu *při zachování svého stavu a stejné adresy jako dříve *. Nepotřebovali jsme vytvářet a nasazovat proxy uzavřít smlouvu nebo propojit proxy na TodoList . Vše se děje pod kapotou!

Vyzkoušejte to vyvoláním nového getListSize() a kontrola velikosti seznamu v nové smlouvě:

A je to! Všimněte si, jak size z list byla zachována po celou dobu aktualizace, stejně jako její adresa. A tento proces je stejný bez ohledu na to, zda pracujete na lokálním blockchainu, testovací síti nebo hlavní síti.

Programově upgradovat smlouvy

Pokud chcete vytvářet a upgradovat smlouvy z kódu JavaScript namísto pomocí příkazového řádku, můžete použít *Upgrady OpenZeppelin * knihovna místo CLI.

Kód níže uvedené implementace naleznete zde .

V případě, že jste nepostupovali podle výše uvedené části OpenZeppelin CLI, musíte nainstalovat NodeJs &Ganache podle pokynů zde.

Vaším prvním krokem bude instalace knihovny do vašeho projektu a pravděpodobně budete chtít nainstalovat web3 k interakci s našimi smlouvami pomocí JavaScriptu a @openzeppelin/contract-loader k načtení smluv z artefaktů JSON.

$ npm install web3 @openzeppelin/upgrades @openzeppelin/contract-loader

Nyní vytvořte soubor index.js uvnitř upgradable-smart-contracts složku a vložte tento standardní kód.

// index.js
const Web3 = require("web3");
const {
  ZWeb3,
  Contracts,
  ProxyAdminProject
} = require("@openzeppelin/upgrades");

async function main() {
  // Set up web3 object, connected to the local development network, initialize the Upgrades library
  const web3 = new Web3("http://localhost:8545");
  ZWeb3.initialize(web3.currentProvider);
  const loader = setupLoader({ provider: web3 }).web3;
}

main();

Zde nastavíme web3 objekt, připojený k místní rozvojové síti, inicializujte Upgrades knihovny přes ZWeb3.initialize a inicializujte smlouvu loader .

Nyní přidejte tento následující fragment do main() vytvořit nový project , abychom mohli spravovat naše upgradovatelné smlouvy.

async function main() {
  // ...

  //Fetch the default account
  const from = await ZWeb3.defaultAccount();

  //creating a new project, to manage our upgradeable contracts.
  const project = new ProxyAdminProject("MyProject", null, null, {
    from,
    gas: 1e6,
    gasPrice: 1e9
  });
}

Nyní pomocí tohoto project , můžeme vytvořit instanci jakékoli smlouvy. project se postará o jeho nasazení tak, aby bylo možné jej později upgradovat.

Vytvoříme 2 smlouvy, TodoList1 a jeho aktualizovanou verzi TodoList2 uvnitř upgradable-smart-contracts/contracts složku.

// contracts/TodoList1.sol
pragma solidity ^0.6.3;

contract TodoList1 {
    string[] private list;

    // Emitted when the storeda new item is added to the list
    event ItemAdded(string item);

    // Adds a new item in the list
    function addItem(string memory newItem) public {
        list.push(newItem);
        emit ItemAdded(newItem);
    }

    // Gets the item from the list according to index
    function getListItem(uint256 index)
        public
        view
        returns (string memory item)
    {
        return list[index];
    }
}

Chcete-li vytvořit TodoList2 , stačí přidat nový getListSize() funkce ve výše uvedené smlouvě.

// contracts/TodoList2.sol
pragma solidity ^0.6.3;

contract TodoList2 {
    string[] private list;

    // Emitted when the storeda new item is added to the list
    event ItemAdded(string item);

    // Adds a new item in the list
    function addItem(string memory newItem) public {
        list.push(newItem);
        emit ItemAdded(newItem);
    }

    // Gets the item from the list according to index
    function getListItem(uint256 index)
        public
        view
        returns (string memory item)
    {
        return list[index];
    }

    // Gets the size of the list
    function getListSize() public view returns (uint256 size) {
        return list.length;
    }
}

Nyní musíme sestavit tyto 2 smlouvy pomocí:

$ npx openzeppelin compile

Tím se vytvoří artefakty smlouvy JSON v build/contracts složku. Tyto soubory artefaktů obsahují všechny informace o smlouvách, které bychom potřebovali k nasazení a interakci se smlouvami.

Nyní vytvoříme instanci TodoList1 pomocí project jsme vytvořili výše.

async function main() {
//...


//Using this project, we can now create an instance of any contract.
  //The project will take care of deploying it in such a way it can be upgraded later.
  const TodoList1 = Contracts.getFromLocal("TodoList1");
  const instance = await project.createProxy(TodoList1);
  const address = instance.options.address;
  console.log("Proxy Contract Address 1: ", address);
}

Zde dostáváme TodoList1 podrobnosti smlouvy z artefaktů smlouvy, které jsme vytvořili výše pomocí Contracts.getFromLocal . Poté vytvoříme a nasadíme pár proxy a implementace (TodoList1 ) smlouvy a propojit smlouvu o zastoupení s TodoList1 přes project.createProxy metoda. Nakonec vytiskneme adresu naší smlouvy o zmocnění.

Nyní přidejte položku do list pomocí addItem() a poté načtěte přidanou položku pomocí getListItem() .

async function main() {
//...

  // Send a transaction to add a new item in the TodoList1
  await todoList1.methods
    .addItem("go to class")
    .send({ from: from, gas: 100000, gasPrice: 1e6 });

  // Call the getListItem() function to fetch the added item from TodoList1
  var item = await todoList1.methods.getListItem(0).call();
  console.log("TodoList1: List Item 0: ", item);
}

Nyní aktualizujme naše TodoList1 smlouvu na TodoList2 .

async function main() {
//...


//After deploying the contract, you can upgrade it to a new version of
  //the code using the upgradeProxy method, and providing the instance address.
  const TodoList2 = Contracts.getFromLocal("TodoList2");
  const updatedInstance = await project.upgradeProxy(address, TodoList2);
  console.log("Proxy Contract Address 2: ", updatedInstance.options.address);
}

Zde dostáváme TodoList2 podrobnosti smlouvy z artefaktů smlouvy. Poté aktualizujeme naši smlouvu prostřednictvím project.upgradeProxy metoda, která přebírá 2 parametry, address smlouvy proxy, kterou jsme nasadili v předchozím kroku, a TodoList2 předmět smlouvy. Po aktualizaci pak vytiskneme adresu smlouvy o zastoupení.

Nyní přidáme novou položku do TodoList2 a vyzvedněte předměty.

async function main() {
//...


  // Send a transaction to add a new item in the TodoList2
  await todoList2.methods
    .addItem("code")
    .send({ from: from, gas: 100000, gasPrice: 1e6 });

  // Call the getListItem() function to fetch the added items from TodoList2
  var item0 = await todoList2.methods.getListItem(0).call();
  var item1 = await todoList2.methods.getListItem(1).call();
  console.log("TodoList2: List Item 0: ", item0);
  console.log("TodoList2: List Item 1: ", item1);
}

Nyní spustíme index.js pomocí node index.js .

Zde můžeme pozorovat 2 věci:

  • Adresa proxy smlouva se nezměnila ani poté, co jsme aktualizovali TodoList1 na TodoList2 .
  • Získali jsme 2 položky z TodoList2 , to ukazuje, že stav byl během aktualizace zachován.

Můžeme tedy říci, že TodoList1 instance byla upgradována na nejnovější verzi kódu (TodoList2 ), *při zachování svého stavu a stejné adresy jako dříve *.

Nyní, když jsme viděli, jak upgradovat smlouvy, pojďme se podívat na několik omezení a náhradních řešení, o kterých musíte vědět při psaní složitějších smluv.

Několik věcí, které je třeba mít na paměti:omezení a náhradní řešení

Při práci s upgradovatelnými smlouvami pomocí upgradů OpenZeppelin je třeba mít při psaní kódu Solidity na paměti několik drobných upozornění.

Stojí za zmínku, že tato omezení mají kořeny ve způsobu fungování virtuálního počítače Ethereum a vztahují se na všechny projekty, které pracují s upgradovatelnými smlouvami, nejen na upgrady OpenZeppelin.

Abychom porozuměli omezením a řešením, vezměme si Example smlouvu, prozkoumejte omezení ve smlouvě a přidejte některá náhradní řešení, aby bylo možné smlouvu upgradovat.

// contracts/Example.sol

pragma solidity ^0.6.0;

import "github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/ERC20Capped.sol";

contract Example {
    uint256 private _cap = 1000000000000000000;
    ERC20Capped public token;

    constructor(uint8 cap) public {
        _cap = cap;
        token = new ERC20Capped(_cap);
    }
}

Omezení 1:Žádné konstruktory

Vzhledem k požadavku systému upgradovatelnosti založeného na proxy nelze v upgradovatelných smlouvách použít žádné konstruktory. Chcete-li se dozvědět o důvodech tohoto omezení, přejděte na tento příspěvek.

Řešení:Inicializátor

Řešením je nahrazení konstruktoru funkcí, obvykle pojmenovanou initialize , kde spustíte konstruktorovou logiku.

// contracts/Example.sol
pragma solidity ^0.6.0;

import "github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/ERC20Capped.sol";

contract Example {
    uint256 private _cap = 1000000000000000000;
    ERC20Capped public token;

    function initialize(uint8 cap) public {
        _cap = cap;
        token = new ERC20Capped(_cap);
    }
}

Nyní jako constructor se volá pouze jednou při inicializaci smlouvy, musíme přidat kontrolu, abychom zajistili, že initialize funkce se volá pouze jednou.

// contracts/Example.sol
pragma solidity ^0.6.0;

import "github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/ERC20Capped.sol";

contract Example {
    uint256 private _cap = 1000000000000000000;
    ERC20Capped public token;

    bool private _initialized = false;

    function initialize(uint8 cap) public {
        require(!_initialized);
        _initialized = true;
        _cap = cap;
        token = new ERC20Capped(_cap);
    }
}

Protože to bude běžná věc při psaní upgradovatelných smluv, OpenZeppelin Upgrades poskytuje Initializable základní smlouvu, která má initializer modifikátor, který se o to stará:

// contracts/Example.sol
pragma solidity ^0.6.0;

import "github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/ERC20Capped.sol";
import "@openzeppelin/upgrades/contracts/Initializable.sol";

contract Example is Initializable {
    uint256 private _cap = 1000000000000000000;
    ERC20Capped public token;

    function initialize(uint8 cap) public initializer {
        _cap = cap;
        token = new ERC20Capped(_cap);
    }
}

Další rozdíl mezi constructor a pravidelnou funkcí je, že Solidity se stará o automatické vyvolávání konstruktérů všech předků smlouvy. Při psaní inicializátoru musíte věnovat zvláštní pozornost ručnímu volání inicializátorů všech nadřazených smluv:

// contracts/Example.sol
pragma solidity ^0.6.0;

import "github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/ERC20Capped.sol";
import "@openzeppelin/upgrades/contracts/Initializable.sol";


contract BaseExample is Initializable {
    uint256 public createdAt;

    function initialize() initializer public {
        createdAt = block.timestamp;
    }

}

contract Example is BaseExample {
    uint256 private _cap = 1000000000000000000;
    ERC20Capped public token;

    function initialize(uint8 cap) initializer public {
        _cap = cap;
        token = new ERC20Capped(_cap);
    }
}

Mějte na paměti, že toto omezení neovlivňuje pouze vaše smlouvy, ale také smlouvy, které importujete z knihovny. Vezměme si například ERC20Capped ze smluv OpenZeppelin:smlouva inicializuje cap tokenu ve svém konstruktoru.

pragma solidity ^0.6.0;

import "./ERC20.sol";

/**
 * @dev Extension of {ERC20} that adds a cap to the supply of tokens.
 */
contract ERC20Capped is ERC20 {
    uint256 private _cap;

    /**
     * @dev Sets the value of the `cap`. This value is immutable, it can only be
     * set once during construction.
     */
    constructor (uint256 cap) public {
        require(cap > 0, "ERC20Capped: cap is 0");
        _cap = cap;
    }

    //...
}

To znamená, že byste tyto smlouvy neměli používat ve svém projektu Upgrady OpenZeppelin. Místo toho se ujistěte, že používáte @openzeppelin/contracts-ethereum-package , což je oficiální fork kontraktů OpenZeppelin, který byl upraven tak, aby místo konstruktorů používal inicializátory. Podívejte se, jak ERC20Capped vypadá v @openzeppelin/contracts-ethereum-package :

pragma solidity ^0.5.0;

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

/**
 * @dev Extension of {ERC20Mintable} that adds a cap to the supply of tokens.
 */
contract ERC20Capped is Initializable, ERC20Mintable {
    uint256 private _cap;

    /**
     * @dev Sets the value of the `cap`. This value is immutable, it can only be
     * set once during construction.
     */
    function initialize(uint256 cap, address sender) public initializer {
        ERC20Mintable.initialize(sender);

        require(cap > 0, "ERC20Capped: cap is 0");
        _cap = cap;
    }

    //...
}

Ať už používáte smlouvy OpenZeppelin nebo jiný balíček Ethereum, vždy se ujistěte, že je balíček nastaven tak, aby zpracovával smlouvy s možností upgradu.

// contracts/Example.sol
pragma solidity ^0.6.0;

import "@openzeppelin/contracts-ethereum-package/contracts/token/ERC20/ERC20Capped.sol";
import "@openzeppelin/upgrades/contracts/Initializable.sol";


contract BaseExample is Initializable {
    uint256 public createdAt;

    function initialize() initializer public {
        createdAt = block.timestamp;
    }

}

contract Example is BaseExample {
    uint256 private _cap = 1000000000000000000;
    ERC20Capped public token;

    function initialize(uint8 cap) initializer public {
        _cap = cap;
        token = new ERC20Capped(_cap);
    }
}

Omezení 2:Počáteční hodnoty v deklaracích polí

Solidity umožňuje definovat počáteční hodnoty polí při jejich deklaraci ve smlouvě.

contract Example is BaseExample {
    uint256 private _cap = 1000000000000000000;

    //...
}

To je ekvivalentní nastavení těchto hodnot v konstruktoru a jako takové nebude fungovat pro upgradovatelné smlouvy.

Řešení:Inicializátor

Ujistěte se, že všechny počáteční hodnoty jsou nastaveny ve funkci inicializátoru, jak je uvedeno níže; jinak nebudou mít žádné upgradovatelné instance tato pole nastavena.

//...

contract Example is BaseExample {
    uint256 private _cap;
    ERC20Capped public token;

    function initialize(uint8 cap) initializer public {
        _cap = 1000000000000000000;
        _cap = cap;
        token = new ERC20Capped(_cap);
    }
}

Všimněte si, že je stále v pořádku nastavit zde konstanty, protože kompilátor pro tyto proměnné nevyhradí úložný prostor a každý výskyt je nahrazen příslušným výrazem konstanty. Takže následující stále funguje s aktualizacemi OpenZeppelin:

//...

contract Example is BaseExample {
    uint256 constant private _cap = 1000000000000000000;

    //...
}

Omezení:Vytváření nových instancí z kódu smlouvy

Při vytváření nové instance smlouvy z kódu vaší smlouvy jsou tato vytvoření zpracovávána přímo Solidity a nikoli OpenZeppelin Upgrades, což znamená, že *tyto smlouvy nebude možné upgradovat *.

Například v následujícím příkladu, i když Example je upgradovatelný (pokud je vytvořen pomocí openzeppelin create Example ), token uzavřená smlouva není:

//...

contract Example is BaseExample {
    uint256 private _cap = 1000000000000000000;
    ERC20Capped public token;

    function initialize(uint8 cap) initializer public {
        _cap = cap;
        token = new ERC20Capped(_cap);
    }
}

Náhradní řešení:Vložení předem nasazené smlouvy z CLI

Nejjednodušší způsob, jak tento problém vyřešit, je vyhnout se vytváření smluv sami:namísto vytváření smlouvy v initialize funkce, jednoduše přijměte instanci této smlouvy jako parametr a vložte ji po jejím vytvoření z OpenZeppelin CLI:

//...

contract Example is BaseExample {
    ERC20Capped public token;

    function initialize(ERC20Capped _token) initializer public {
        token = _token;
    }
}
$ TOKEN=$(npx openzeppelin create TokenContract)
$ npx oz create Example --init --args $TOKEN

Řešení:Smlouva o aplikaci OpenZeppelin

Pokročilou alternativou, pokud potřebujete vytvořit upgradovatelné smlouvy za chodu, je ponechat instanci App vašeho projektu OpenZeppelin ve vašich smlouvách. App je smlouva, která funguje jako vstupní bod pro váš projekt OpenZeppelin, který má odkazy na vaše implementace logiky a může vytvářet nové instance smlouvy:

// contracts/Example.sol
pragma solidity ^0.6.0;

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

contract BaseExample is Initializable {
    //...
}

contract Example is BaseExample {

  App private app;

  function initialize(App _app) initializer public {
    app = _app;
  }

  function createNewToken() public returns(address) {
    return app.create("@openzeppelin/contracts-ethereum-package", "ERC20Capped");
  }
}

Potenciálně nebezpečné operace

Při práci s upgradovatelnými inteligentními smlouvami budete vždy komunikovat s instancí smlouvy proxy a nikdy se smlouvou o základní logice (implementaci). Nic však nebrání zlomyslnému aktérovi zasílat transakce přímo do logické smlouvy. To nepředstavuje hrozbu, protože jakékoli změny stavu logických kontraktů neovlivní instance vašich proxy kontraktů, protože úložiště logických kontraktů se ve vašem projektu nikdy nepoužívá.

Existuje však výjimka. Pokud přímé volání smlouvy logiky spustí selfdestruct operace, pak bude logická smlouva zničena a všechny vaše instance smlouvy skončí delegováním všech volání na adresu bez jakéhokoli kódu. To by účinně přerušilo všechny instance smluv ve vašem projektu.

Podobného efektu lze dosáhnout, pokud logický kontrakt obsahuje delegatecall úkon. Pokud lze smlouvu uzavřít na delegatecall do škodlivé smlouvy, která obsahuje selfdestruct , pak bude volací smlouva zničena.

pragma solidity ^0.6.0;

// The Exmaple contract makes a `delegatecall` to the Malicious contract. Thus, even if the Malicious contract runs the `selfdestruct` function, it is run in the context of the Example contract, thus killing the Example contract.  

contract Example {
    function testFunc(address malicious) public {
        malicious.delegatecall(abi.encodeWithSignature("kill()"));
    }
}

contract Malicious {
    function kill() public {
        address payable addr = address(uint160(address(0x4Bf8c809c898ee52Eb7fc6e1FdbB067423326B2A)));
        selfdestruct(addr);
    }
}

Proto se důrazně doporučuje vyhnout se jakémukoli použití selfdestruct nebo delegatecall ve vašich smlouvách. Pokud je potřebujete zahrnout, ujistěte se, že je nemůže zavolat útočník na neinicializované logické smlouvě.

Úprava vašich smluv

Při psaní nových verzí vašich smluv, ať už kvůli novým funkcím nebo opravám chyb, je třeba dodržet další omezení:nemůžete změnit pořadí, ve kterém jsou deklarovány proměnné stavu smlouvy, ani jejich typ. Více o důvodech tohoto omezení si můžete přečíst v článku o proxy.

To znamená, že pokud máte počáteční smlouvu, která vypadá takto:

pragma solidity ^0.6.3;

contract Example {
    string public tokenName;
    uint8 public decimals;
}

Pak nemůžete změnit typ proměnné:

pragma solidity ^0.6.3;

contract Example {
    string public tokenName;
    uint public decimals;
}

Nebo změňte pořadí, ve kterém jsou deklarovány:

pragma solidity ^0.6.3;

contract Example {
    uint public decimals;
    string public tokenName;
}

Nebo zaveďte novou proměnnou před stávajícími:

pragma solidity ^0.6.3;

contract Example {
    string public tokenSymbol;
    string public tokenName;
    uint public decimals;
}

Nebo odstraňte existující proměnnou:

pragma solidity ^0.6.3;

contract Example {
    string public tokenName;
}

Pokud potřebujete zavést novou proměnnou, ujistěte se, že to vždy uděláte na konci:

pragma solidity ^0.6.3;

contract Example {
    string public tokenName;
    uint public decimals;
    string public tokenSymbol;
}

Mějte na paměti, že pokud proměnnou přejmenujete, zachová si po upgradu stejnou hodnotu jako předtím. Toto může být žádoucí chování, pokud je nová proměnná sémanticky stejná jako stará:

pragma solidity ^0.6.3;

contract Example {
    string public tokenName;
    uint public decimalCount;   // starts with the value of `decimals`
}

A pokud odeberete proměnnou z konce smlouvy, mějte na paměti, že úložiště nebude vyčištěno. Následná aktualizace, která přidá novou proměnnou, způsobí, že tato proměnná načte zbývající hodnotu z odstraněné.

pragma solidity ^0.6.3;

contract Example1 {
    string public tokenName;
    uint public decimals;
}

// Updating Example1 --> Example2

contract Example2 {
    string public tokenName;
}

// Updating Example2 --> Example3

contract Example3 {
    string public tokenName;
    uint public decimalCount;   // starts with the value of `decimals`
}

Všimněte si, že můžete také neúmyslně změnit proměnné úložiště vaší smlouvy změnou jejích nadřazených (základních) smluv. Pokud máte například následující smlouvy:

pragma solidity ^0.6.3;

contract BaseExample1 {
    uint256 createdAt;
}

contract BaseExample2 {
    string version;
}

contract Example is BaseExample1, BaseExample2 {}

Poté upravte Example záměnou pořadí, ve kterém jsou základní smlouvy deklarovány, nebo přidáním nových základních smluv nebo odebráním základních smluv, se změní způsob, jakým jsou proměnné skutečně uloženy:

pragma solidity ^0.6.3;

contract BaseExample1 {
    uint256 createdAt;
}

contract BaseExample2 {
    string version;
}

//swapping the order in which the base contracts are declared
contract Example is BaseExample2, BaseExample1 {}

//Or...

//removing base contract(s)
contract Example is BaseExample1 {}

//Or...

contract BaseExample3 {} 

//adding new base contract
contract Example is BaseExample1, BaseExample2, BaseExample3 {}

Také nemůžete přidávat nové proměnné k základním kontraktům, pokud má podřízený nějaké vlastní proměnné. Vzhledem k následujícímu scénáři:

pragma solidity ^0.6.3;

contract BaseExample {}

contract Example is BaseExample {
    string tokenName;
}

//Now, if the BaseExample is updated to the following

contract BaseExample {
    string version;     // takes the value of `tokenName` 
}

contract Example is BaseExample {
    string tokenName;
}

Pak proměnná version bude přiřazen slot, který tokenName měl v předchozí verzi.

Také odeberete proměnnou ze základní smlouvy, pokud má dítě nějaké vlastní proměnné. Například:

pragma solidity ^0.6.3;

contract BaseExample {
    uint256 createdAt;
    string version;
}

contract Example is BaseExample {
    string tokenName;
}

//Now, if the BaseExample is updated to the following

contract BaseExample {
    uint256 createdAt; 
}

contract Example is BaseExample {
    string tokenName;   //takes the value of `version`
}

Zde, když odstraníme version proměnná z BaseExample , paměťový slot pro version (před aktualizací) bude nyní používán tokenName (po aktualizaci).

Řešením je deklarovat nepoužívané proměnné v základních smlouvách, které možná budete chtít v budoucnu rozšířit, jako prostředek k „rezervaci“ těchto slotů. V zásadě tedy zachování počtu a pořadí proměnných v nadřazených a podřízených smlouvách stejné pro všechny aktualizace.

pragma solidity ^0.6.3;

contract BaseExample {
    string someVar1;
    string someVar2;
    string someVar3;

    //...
}

Upozorňujeme, že tento trik ne zahrnovat zvýšenou spotřebu plynu.

Reference

  • Přístupy společnosti NuCypher ke smlouvám s možností upgradu
  • Upgrade Smart Contracts
  • Sepsání smluv s možností upgradu