目录

使用openzeppelin开发第一个可升级智能合约-Part2

目录

接上篇使用openzeppelin开发第一个可升级智能合约-Part1

在上一篇,我们跟着文档走,结果合约升级确报了3个错,文档更新并不及时:

1
2
3
4
5
6
7
# 1.合约中有构造函数,应当修改为initializer函数
- Contract Box or an ancestor has a constructor. Change it to an initializer function. See https://docs.openzeppelin.com/upgrades/2.6//writing-upgradeable#initializers.
# 2. 应该是附带问题
- New variable 'address _owner' was inserted in contract Ownable in @openzeppelin/contracts/ownership/Ownable.sol:1. You should only add new variables at the end of your contract.
See https://docs.openzeppelin.com/upgrades/2.6//writing-upgradeable#modifying-your-contracts for more info.
# 3.导入的合约来自@openzeppelin/contracts,使用@openzeppelin/contracts-ethereum-package替换
- Contract Box imports ownership/Ownable.sol, GSN/Context.sol from @openzeppelin/contracts. Use @openzeppelin/contracts-ethereum-package instead. See https://docs.openzeppelin.com/cli/2.6/dependencies#linking-the-contracts-ethereum-package.

WTF??? 一步步对着文档来的,你和我搞这个?

先解决第3个:

1
2
3
4
5
6
# 移除旧包,做测试的话这个包先不要移除
npm remove @openzeppelin/contracts
# 安装新包
# @openzeppelin/upgrades也要装,不然会提示你@openzeppelin/contracts-ethereum-package依赖这个包
npm install --save-dev @openzeppelin/upgrades
npm install --save-dev @openzeppelin/contracts-ethereum-package

第1个问题:

https://docs.openzeppelin.com/upgrades/2.8/writing-upgradeable

您可以在OpenZeppelin升级中使用您的Solidity合同,无需对其进行任何修改(构造函数除外)。由于基于代理的可升级性系统的要求,因此在可升级合同中不能使用构造函数。要了解此限制的原因,请访问代理

这意味着,在将合同与OpenZeppelin升级配合使用时,您需要将其构造函数更改为常规函数(通常名为)initialize,在其中运行所有设置逻辑:

由于这种模式在编写可升级合同时非常普遍,因此OpenZeppelin升级提供了一个Initializable基本合同,该合同具有一个initializer修饰符来处理此问题:

使用initializer去替代构造函数,并引入OpenZeppelin提供的Initializable来限制它只能运行一次(像构造函数一样工作)

重新编写代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// contracts/Box.sol
pragma solidity ^0.5.0;

import '@openzeppelin/upgrades/contracts/Initializable.sol';
// 引入合约拥有者权限
import "@openzeppelin/contracts-ethereum-package/contracts/ownership/Ownable.sol";

contract Box is Initializable, Ownable {

    uint256 private value;

    event ValueChanged(uint256 newValue);

    // 使用初始化函数代替构造函数
    function initialize() public initializer {
        // 要初始化Ownable合约
        Ownable.initialize(msg.sender);
    }

    function store(uint256 newValue) public onlyOwner {
        value = newValue;
        emit ValueChanged(newValue);
    }

    function retrieve() public view returns (uint256) {
        return value;
    }
}

部署:

Call a function to initialize the instance after creating it? 选择的是 Y,调用initialize函数,相当于执行了构造函数

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
$ oz deploy
✓ Compiled contracts with solc 0.5.17 (commit.d19bba13)
? Choose the kind of deployment upgradeable
? Pick a network development
? Pick a contract to deploy Box
✓ Contract Box deployed
All implementations have been deployed
? Call a function to initialize the instance after creating it? Yes
? Select which function * initialize()
✓ Setting everything up to create contract instances
✓ Instance created at 0x0290FB167208Af455bB137780163b7B7a9a10C16
To upgrade this instance run 'oz upgrade'
0x0290FB167208Af455bB137780163b7B7a9a10C16

再次使用node src/index.js去调用,不过要修改一下地址

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// src/index.js
const Web3 = require('web3');
const {setupLoader} = require('@openzeppelin/contract-loader');

async function main() {
    // 连接RPC
    const web3 = new Web3('http://localhost:8545');
    const loader = setupLoader({provider: web3}).web3;

    // 改为新的地址
    const address = '0x0290FB167208Af455bB137780163b7B7a9a10C16';
    const box = loader.fromArtifact('Box', address);

    // 调用 retrieve 函数,使用call的方式
    let value = await box.methods.retrieve().call();
    console.log("Box value Before is", value);

    // 获取账户列表
    const accounts = await web3.eth.getAccounts();

    // 使用第一个账户accounts[0]来发送交易,调用store函数,将值设为20,指定gas为50000,gasPrice为1e6
    await box.methods.store(20)
        .send({from: accounts[0], gas: 50000, gasPrice: 1e6});

    // 再次调用 retrieve 函数,查看值是否变化
    value = await box.methods.retrieve().call();
    console.log("Box value After is", value);
}

main();

返回(合约刚部署,value没有值,所以等于0,后面等于20):

1
2
3
$ node src/index.js
Box value Before is 0
Box value After is 20

升级合约

此时value=20,假设升级内容是增加一个函数increment,没调用一次value+1

得到新的合约代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// contracts/Box.sol
pragma solidity ^0.5.0;

import '@openzeppelin/upgrades/contracts/Initializable.sol';
// 引入合约拥有者权限
import "@openzeppelin/contracts-ethereum-package/contracts/ownership/Ownable.sol";

contract Box is Initializable, Ownable {

    uint256 private value;

    event ValueChanged(uint256 newValue);

    // 使用初始化函数代替构造函数
    function initialize() public initializer {
        // 要初始化Ownable合约
        Ownable.initialize(msg.sender);
    }

    function store(uint256 newValue) public onlyOwner {
        value = newValue;
        emit ValueChanged(newValue);
    }

    function retrieve() public view returns (uint256) {
        return value;
    }

    // 新增一个函数,每次使value值+1
    function increment() public onlyOwner {
        value = value + 1;
        emit ValueChanged(value);
    }
}

使用oz upgrade再次升级

当Call a function to initialize the instance after creating it? 再次选择 Y 时会报错

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
$ oz upgrade
? Pick a network development
? Which instances would you like to upgrade? Choose by address
? Pick an instance to upgrade Box at 0x0290FB167208Af455bB137780163b7B7a9a10C16
? Call a function on the instance after upgrading it? Yes
? Select which function * initialize()

✓ Compiled contracts with solc 0.5.17 (commit.d19bba13)
✓ Contract Box deployed
All implementations have been deployed
✖ Upgrading instance at 0x0290FB167208Af455bB137780163b7B7a9a10C16 and calling 'initialize' with no arguments
✖ Upgrading instance at 0x0290FB167208Af455bB137780163b7B7a9a10C16
Proxy first-smart-contract/Box at 0x0290FB167208Af455bB137780163b7B7a9a10C16 failed to upgrade with error: Returned error: VM Exception while processing transaction: revert

选择N,升级成功

1
2
3
4
5
6
7
8
9
$ oz upgrade
? Pick a network development
? Which instances would you like to upgrade? Choose by address
? Pick an instance to upgrade Box at 0x0290FB167208Af455bB137780163b7B7a9a10C16
? Call a function on the instance after upgrading it? No
Nothing to compile, all contracts are up to date.
All implementations are up to date
✓ Instance upgraded at 0x0290FB167208Af455bB137780163b7B7a9a10C16. Transaction receipt: 0x2b84fba55b975b97c511798eb772156aa14e7f5002cbc25d345b895d4d3dd159
✓ Instance at 0x0290FB167208Af455bB137780163b7B7a9a10C16 upgraded

看到地址没有变化,现在要验证:

  1. value是否等于20?,如果等于表示合约状态(数据)还在
  2. 新增的increment是否存在?

修改一下 src/index.js

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// src/index.js
const Web3 = require('web3');
const {setupLoader} = require('@openzeppelin/contract-loader');

async function main() {
    // 连接RPC
    const web3 = new Web3('http://localhost:8545');
    const loader = setupLoader({provider: web3}).web3;

    // 改为新的地址
    const address = '0x0290FB167208Af455bB137780163b7B7a9a10C16';
    const box = loader.fromArtifact('Box', address);

    // 调用 retrieve 函数,使用call的方式
    let value = await box.methods.retrieve().call();
    console.log("Box value Before is", value);

    // 获取账户列表
    const accounts = await web3.eth.getAccounts();

    // 使用第一个账户accounts[0]来发送交易,调用store函数,将值设为30,指定gas为50000,gasPrice为1e6
    await box.methods.store(30)
        .send({from: accounts[0], gas: 50000, gasPrice: 1e6});

    // 再次调用 retrieve 函数,查看值是否变化
    value = await box.methods.retrieve().call();
    console.log("Box value After is", value);

    // 调用 increment 函数
    await box.methods.increment()
        .send({from: accounts[0], gas: 50000, gasPrice: 1e6});

    // 再次查看value值,此时 value应该等于31
    value = await box.methods.retrieve().call();
    console.log("Box value Increment is", value);

}

main();

返回(结果符合预期):

1
2
3
4
$ node src/index.js
Box value Before is 20
Box value After is 30
Box value Increment is 31