使用OpenZeppelin Truffle升級外掛升級合約

買賣虛擬貨幣
使用 OpenZeppelin 升級外掛部署的合約具備可升級的特性:可以升級以修改其程式碼,同時保留其地址,狀態和餘額。這使你可以迭代地向專案中新增新功能,或修復線上上版本中可能發現的任何錯誤。在本文中,我們將展示使用 OpenZeppelin Truffle 升級外掛和 Gnosis Safe 的生命週期,包含從建立合約,測試合約、部署合約一直到使用 Gnosis Safe 進行升級整個過程:· 建立可升級合約· 本地測試合約· 將合約部署到公共網路· 轉移升級許可權到 Gnosis Safe 多籤
· 實現一個新的升級版本· 本地測試升級版本· 部署新的升級版本· 升級合約配置開發環境我們將從建立一個新的 npm 專案開始:
mkdir mycontract && cd mycontractnpm init -y安裝並初始化 Truffle。注意:我們需要使用 Truffle 5.1.35 或更高版本。npm i --save-dev trufflenpx truffle init安裝 Truffle 升級外掛。
npm i --save-dev @openzeppelin/truffle-upgrades建立可升級合約我們將使用OpenZeppelin 學習指南[3]中最受歡迎的 Box 合約。使用以下Solidity[4]程式碼在你的Contracts目錄中建立 Box.sol。注意,可升級合約使用`initialize`函式而不是建構函式[5]來初始化狀態。為了保持簡單,任何帳戶都可以多次呼叫的公開的store函式來初始化狀態(而不是受保護的一次性 initialize 函式)。Box.sol// contracts/Box.sol
// SPDX-License-Identifier: MITpragma solidity ^0.7.0;contract Box {    uint256 private value;    // Emitted when the stored value changes    event ValueChanged(uint256 newValue);
    // Stores a new value in the contract    function store(uint256 newValue) public {        value = newValue;        emit ValueChanged(newValue);    }    // Reads the last stored value
    function retrieve() public view returns (uint256) {        return value;    }}本地測試合約我們的合約應該始終有相應的測試。要測試合約,我們應該為合約實現建立單元測試。
我們將在測試中使用 chai(一個 Js 測試框架),因此首先需要安裝它。npm i --save-dev chai我們將為合約實現建立單元測試。使用以下 JavaScript 在你的test目錄中建立Box.test.js。Box.test.js// test/Box.test.js
// 載入依賴const { expect } = require('chai');// Load compiled artifactsconst Box = artifacts.require('Box');// Start test blockcontract('Box', function () {
  beforeEach(async function () {    // 為每個測試部署一個新的Box合約    this.box = await Box.new();  });  // 測試用  it('retrieve returns a value previously stored', async function () {
    // Store a value    await this.box.store(42);    // 測試是否返回了同一個設定的值    // Note that we need to use strings to compare the 256 bit integers    expect((await this.box.retrieve()).toString()).to.equal('42');  });
});我們還可以透過(升級)代理建立測試進行互動。注意:我們不需要在此處重複單元測試,這是為了測試代理互動和測試升級。使用以下 JavaScript 在你的test目錄中建立Box.proxy.test.js。Box.proxy.test.js// test/Box.proxy.test.js
// Load dependenciesconst { expect } = require('chai');const { deployProxy } = require('@openzeppelin/truffle-upgrades');// Load compiled artifactsconst Box = artifacts.require('Box');// Start test block
contract('Box (proxy)', function () {  beforeEach(async function () {    // 為每個測試部署一個新的Box合約    this.box = await deployProxy(Box, [42], {initializer: 'store'});  });  // 測試用例
  it('retrieve returns a value previously initialized', async function () {    // 測試是否返回了同一個設定的值    // 注意需要使用字串去對比256位的整數    expect((await this.box.retrieve()).toString()).to.equal('42');  });});
在我們編譯合約之前,我們需要在truffle-config.js中將 solc 版本更改為^0.7.0,因為我們的合約標記為pragma solidity ^0.7.0然後,我們可以執行測試。$ npx truffle test...  Contract: Box (proxy)    ✓ retrieve returns a value previously initialized (43ms)
  Contract: Box    ✓ retrieve returns a value previously stored (100ms)  2 passing (3s)將合約部署到公共網路我們將使用Truffle 遷移[6]來部署 Box 合約。Truffle 升級外掛提供了一個 deployProxy功能來部署可升級合約。它將部署我們實現的合約,ProxyAdmin 會作為專案代理和代理管理員,並呼叫(任何的)初始化函式。在 migrations 目錄中建立以下2_deploy_box.js指令碼。
在本文中,我們還沒有initialize函式,因此我們將使用store 函式來初始化狀態。2_deploy_box.js// migrations/2_deploy_box.jsconst Box = artifacts.require('Box');const { deployProxy } = require('@openzeppelin/truffle-upgrades');
module.exports = async function (deployer) {  await deployProxy(Box, [42], { deployer, initializer: 'store' });};我們通常先將合約部署到本地測試環境(例如ganache-cli),然後手動與之互動。為了節省時間,我們將跳過這步,而直接部署到公共測試網路。在本文中,我們將部署到 Rinkeby。如果需要配置方面的幫助,請參閱使用 Truffle 連線到公共測試網路[7]。注意:助記符或 Infura 專案 ID 之類的機密內容都不應提交給版本控制。使用 Rinkeby 網路執行truffle migration進行部署。我們可以看到 3 個合約:Box.sol、ProxyAdmin 和 代理合約
AdminUpgradeabilityProxy。$ npx truffle migrate --network rinkeby...2_deploy_box.js===============   Deploying 'Box'
   ---------------   > transaction hash:    0x3263d01ce2e3eb4ba51abf882abbdd9252364b51eb972f82958719d60a8b9ebe   > Blocks: 0            Seconds: 5   > contract address:    0xd568071213Ea31B01AA2247BC9eC7285087cf882...   Deploying 'ProxyAdmin'
   ----------------------   > transaction hash:    0xf39e8cb97c332b8bbdf0c66b13f26a9a3dc97b207d2caec73ba6df8d5bb6b211   > Blocks: 1            Seconds: 17   > contract address:    0x2A210B6d5EffC0A3BB47dD3791a4C26B8E31f161...   Deploying 'AdminUpgradeabilityProxy'
   ------------------------------------   > transaction hash:    0x439711597b694f03b1065582ab44ac0bea5e22b0c6e3c460ae7b4536f004c355   > Blocks: 1            Seconds: 17   > contract address:    0xF325bB49f91445F97241Ec5C286f90215a7E3BC6...我們可以使用 Truffle 控制檯(truffle console)與我們的合約進行互動。
注意: Box.deployed() 是我們的代理合約的地址。$ npx truffle console --network rinkebytruffle(rinkeby)> box = await Box.deployed()truffle(rinkeby)> box.address'0xF325bB49f91445F97241Ec5C286f90215a7E3BC6'truffle(rinkeby)> (await box.retrieve()).toString()
'42'轉移升級許可權到 Gnosis Safe 多籤我們將使用 Gnosis Safe 來控制合約的升級。譯者注:Gnosis Safe 是一款多簽名錢包,可設定滿足 n/m (例如:2/3)的簽名才可以進行交易。首先,我們需要在 Rinkeby 網路上為自己建立一個 Gnosis Safe。可參考文件:建立 Safe Multisig[8]的說明。為簡單起見,在本文中,本例使用 1/1,在正式產品中,你應考慮使用至少 2/3。

在 Rinkeby 上建立 Gnosis Safe 之後,請複製地址,以便我們轉移所有權。

當前代理的管理員(可以執行升級)是 ProxyAdmin 合約。只有 ProxyAdmin 的所有者可以升級代理。警告:ProxyAdmin 所有權轉移時請確保轉到我們控制的地址上。

使用以下 JavaScript 在migrations目錄中建立3_transfer_ownership.js。將 gnosisSafe 的值更改為你的 Gnosis Safe 地址。

3_transfer_ownership.js
// migrations/3_transfer_ownership.js
const { admin } = require('@openzeppelin/truffle-upgrades');

module.exports = async function (deployer, network) {
  // 使用你的 Gnosis Safe 地址
  const gnosisSafe = '0x1c14600daeca8852BA559CC8EdB1C383B8825906';

  // Don't change ProxyAdmin ownership for our test network
  if (network !== 'test') {
    // The owner of the ProxyAdmin can upgrade our contracts
    await admin.transferProxyAdminOwnership(gnosisSafe);
  }
};

我們可以在 Rinkeby 網路上執行遷移。

$ npx truffle migrate --network rinkeby
...
3_transfer_ownership.js
=======================

   > Saving migration to chain.
   -------------------------------------
...

實現一個新的升級版本

一段時間後,我們決定要向合約新增功能。在本文中,我們將新增一個increment函式。

注意:我們無法更改之前合約實現的儲存佈局,有關技術限制的更多詳細資訊,請參閱升級[9]。

使用以下 Solidity 程式碼在你的contracts目錄中建立新的實現BoxV2.sol 。

BoxV2.sol
// contracts/BoxV2.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.7.0;

contract BoxV2 {
    uint256 private value;

    // Emitted when the stored value changes
    event ValueChanged(uint256 newValue);

    // Stores a new value in the contract
    function store(uint256 newValue) public {
        value = newValue;
        emit ValueChanged(newValue);
    }

    // Reads the last stored value
    function retrieve() public view returns (uint256) {
        return value;
    }

    // Increments the stored value by 1
    function increment() public {
        value = value + 1;
        emit ValueChanged(value);
    }
}

本地測試升級的版本

為了測試我們的升級版本,我們應該為新的合約建立單元測試,併為透過代理測試互動,並檢查升級之間是否保持狀態。

我們將為新的合約實現建立單元測試。我們可以在已經建立的單元測試中新增新測試,以確保高覆蓋率。使用以下 JavaScript 在你的test目錄中建立BoxV2.test.js。

BoxV2.test.js
// test/BoxV2.test.js
// Load dependencies
const { expect } = require('chai');

// Load compiled artifacts
const BoxV2 = artifacts.require('BoxV2');

// Start test block
contract('BoxV2', function () {
  beforeEach(async function () {
    // Deploy a new BoxV2 contract for each test
    this.boxV2 = await BoxV2.new();
  });

  // Test case
  it('retrieve returns a value previously stored', async function () {
    // Store a value
    await this.boxV2.store(42);

    // 測試是否返回了同一個設定的值
    // 注意需要使用字串去對比256位的整數
    expect((await this.boxV2.retrieve()).toString()).to.equal('42');
  });

  // Test case
  it('retrieve returns a value previously incremented', async function () {
    // Increment
    await this.boxV2.increment();

     // 測試是否返回了同一個設定的值
    // 注意需要使用字串去對比256位的整數
    expect((await this.boxV2.retrieve()).toString()).to.equal('1');
  });
});

升級後,我們還可以透過代理進行互動來建立測試。注意:我們不需要在此處重複單元測試,這是為了測試代理互動和測試升級後的狀態。

使用以下 JavaScript 在你的test目錄中建立BoxV2.proxy.test.js。

BoxV2.proxy.test.js
// test/Box.proxy.test.js
// Load dependencies
const { expect } = require('chai');
const { deployProxy, upgradeProxy} = require('@openzeppelin/truffle-upgrades');

// Load compiled artifacts
const Box = artifacts.require('Box');
const BoxV2 = artifacts.require('BoxV2');

// Start test block
contract('BoxV2 (proxy)', function () {

  beforeEach(async function () {
    // Deploy a new Box contract for each test
    this.box = await deployProxy(Box, [42], {initializer: 'store'});
    this.boxV2 = await upgradeProxy(this.box.address, BoxV2);
  });

  // Test case
  it('retrieve returns a value previously incremented', async function () {
    // Increment
    await this.boxV2.increment();

    // Test if the returned value is the same one
    // Note that we need to use strings to compare the 256 bit integers
    expect((await this.boxV2.retrieve()).toString()).to.equal('43');
  });
});

然後,我們可以執行測試。

$ npx truffle test
Using network 'test'.
...
  Contract: Box (proxy)
    ✓ retrieve returns a value previously initialized (38ms)

  Contract: Box
    ✓ retrieve returns a value previously stored (87ms)

  Contract: BoxV2 (proxy)
    ✓ retrieve returns a value previously incremented (90ms)

  Contract: BoxV2
    ✓ retrieve returns a value previously stored (91ms)
    ✓ retrieve returns a value previously incremented (86ms)

  5 passing (1s)

部署新的升級版本

一旦測試了新的實現,就可以準備升級。這將驗證並部署新合約。注意:我們僅是準備升級。我們將使用 Gnosis Safe 執行實際升級。

使用以下 JavaScript 在migrations目錄中建立4_prepare_upgrade_boxv2.js。

4_prepare_upgrade_boxv2.js
// migrations/4_prepare_upgrade_boxv2.js
const Box = artifacts.require('Box');
const BoxV2 = artifacts.require('BoxV2');

const { prepareUpgrade } = require('@openzeppelin/truffle-upgrades');

module.exports = async function (deployer) {
  const box = await Box.deployed();
  await prepareUpgrade(box.address, BoxV2, { deployer });
};

我們可以在 Rinkeby 網路上執行遷移,以部署新的合約實現。注意:執行此遷移時,我們需要跳過之前執行過的遷移。

$ npx truffle migrate --network rinkeby
...
4_prepare_upgrade_boxv2.js
==========================

   Deploying 'BoxV2'
   -----------------
   > transaction hash:    0x078c4c4454bb15e3791bc80396975e6e8fc8efb76c6f54c321cdaa01f5b960a7
   > Blocks: 1            Seconds: 17
   > contract address:    0xEc784bE1CC7F5deA6976f61f578b328E856FB72c
...

升級合約

要管理我們在 Gnosis Safe 中的升級,我們使用 OpenZeppelin 應用程式(找一下 OpenZeppelin 的 logo)。

首先,我們需要代理的地址(box.address)和新實現的地址(boxV2.address)。我們可以從 truffle 遷移的輸出或 truffle console 中獲得。

$ npx truffle console --network rinkeby
truffle(rinkeby)> box = await Box.deployed()
truffle(rinkeby)> boxV2 = await BoxV2.deployed()
truffle(rinkeby)> box.address
'0xF325bB49f91445F97241Ec5C286f90215a7E3BC6'
truffle(rinkeby)> boxV2.address
'0xEc784bE1CC7F5deA6976f61f578b328E856FB72c'

在“應用程式(APPS)”選項卡中,選擇“ OpenZeppelin”應用程式,然後將代理地址貼上到“合約地址(Contract address)”欄位中,然後將新實現的地址貼上到“新實現的地址( New implementation address)”欄位中。

該應用程式應顯示該合約是EIP1967[10]相容的。

仔細檢查地址,然後按“升級(Upgrade)”按鈕。

確認顯示對話方塊以提交交易。

然後,我們需要在 MetaMask(或你正使用的錢包)中籤署交易。

現在,我們可以與升級後的合約進行互動。我們需要使用代理地址與 BoxV2 進行互動。然後,我們可以呼叫新的“增量”功能,觀察到整個升級過程中都保持了狀態。

$ npx truffle console --network rinkeby
truffle(rinkeby)> box = await Box.deployed()
truffle(rinkeby)> boxV2 = await BoxV2.at(box.address)
truffle(rinkeby)> (await boxV2.retrieve()).toString()
'42'
truffle(rinkeby)> await boxV2.increment()
{ tx:
...
truffle(rinkeby)> (await boxV2.retrieve()).toString()
'43'

接下來

我們已經建立了一個可升級的合約,將升級的控制權轉移到了 Gnosis Safe,並升級了我們的合約。

可以在主網上執行相同的過程。注意:我們應該始終首先在公共測試網上測試升級。

如果你對本文有任何疑問或建議的改進,請釋出在openzeppelin 社羣論壇[11]中。

參考資料

[1]登鏈翻譯計劃: https://github.com/lbc-team/Pioneer
[2]Tiny 熊: https://learnblockchain.cn/people/15
[3]OpenZeppelin 學習指南: https://docs.openzeppelin.com/learn/developing-smart-contracts#setting-up-a-solidity-project
[4]Solidity: https://learnblockchain.cn/docs/solidity/
[5]initialize函式而不是建構函式: https://docs.openzeppelin.com/learn/upgrading-smart-contracts#initialization
[6]Truffle 遷移: https://learnblockchain.cn/docs/truffle/getting-started/running-migrations.html
[7]使用 Truffle 連線到公共測試網路: https://forum.openzeppelin.com/t/connecting-to-public-test-networks-with-truffle/2960
[8]建立 Safe Multisig: https://help.gnosis-safe.io/en/articles/3876461-create-a-safe-multisig
[9]升級: https://docs.openzeppelin.com/learn/upgrading-smart-contracts#upgrading
[10]EIP1967: https://learnblockchain.cn/docs/eips/eip-1967.html
[11]openzeppelin 社羣論壇: https://forum.openzeppelin.com/
[12]Cell Network: https://www.cellnetwork.io/?utm_souce=learnblockchain
[13]abcoathup: https://forum.openzeppelin.com/u/abcoathup

免責聲明:

  1. 本文版權歸原作者所有,僅代表作者本人觀點,不代表鏈報觀點或立場。
  2. 如發現文章、圖片等侵權行爲,侵權責任將由作者本人承擔。
  3. 鏈報僅提供相關項目信息,不構成任何投資建議

推荐阅读

;