目录

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

什么是OpenZeppelin

OpenZeppelin是一套命令行工具,可在以太坊以及所有其他由EVM和eWASM支持的区块链上开发,部署和运营智能合约项目。包含一些已经写好的经过安全验证的智能合约,以及提供了编写可升级智能合约的方案。

简单点就是类似脚手架。

1
npm install --global @openzeppelin/cli

官方网站: https://openzeppelin.com/

官方Github: https://github.com/OpenZeppelin/openzeppelin-sdk

官方文档: https://docs.openzeppelin.com/

初始化

新建一个空目录

1
2
mkdir start
cd start

初始化

1
2
3
# 下面这一步自己去一个包名,其他默认直接回车
npm init
oz init

返回:

1
2
3
? Welcome to the OpenZeppelin SDK! Choose a name for your project first-smart-contract
? Initial project version 1.0.0
Project initialized. Write a new contract in the contracts folder and run 'openzeppelin deploy' to deploy it.

openzeppelin 命令等同于 oz

1
2
3
4
5
openzeppelin init
# 等同于
oz init
# 如果提示找不到命令使用
npx oz init

start目录下出现下列文件

https://assets.cooldev.cn/image-20200415173705130.png@!p

contracts 目录是存放合约的文件夹,此时还没有文件

编写第一个合约

contracts 目录下创建文件: Box.sol 文件内容如下

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
// contracts/Box.sol
pragma solidity ^0.5.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;
    }
}

编译

1
oz compile

如果不报错会返回:

1
2
 oz compile
✓ Compiled contracts with solc 0.5.17 (commit.d19bba13)

start 目录下会多出一个 build目录,是编译好的文件,其中 Box.json就是合约编译后的信息,包含源代码,源代码信息,ABI,字节码bytecode,以及编译器使用的版本

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
  networks.js      
  package.json     
                   
├─.openzeppelin     
      project.json 
                   
├─build             
  └─contracts      
          Box.json 
                   
└─contracts         
        .gitkeep    
        Box.sol     
                    

您还可以通过将参数传递给compile命令来配置编译,包括选择编译器版本和启用优化:

1
$ npx oz compile --solc-version=0.5.12 --optimizer on

有关这些选项的详细信息,请参阅《使用CLI编译》。

完善

1
2
3
4
5
# 安装几个npm包
# 包含openzeppelin提前写好的合同
npm install --save-dev @openzeppelin/contracts
# Ganache 能快速运行一个本地测试区块链 
npm install --save-dev ganache-cli

启动Ganache,Ganache将创建随机的一组解锁帐户,并将其分配给以太币。为了获得与本指南中将使用的地址相同的地址,可以在确定性模式下启动Ganache:

1
2
# --deterministic 参数是使用当前目录下的 networks.js为配置启动本地测试区块链
$ npx ganache-cli --deterministic

Ganache将打印出可用帐户及其私钥的列表,以及一些区块链配置值。最重要的是,它将显示其地址,我们将使用它来连接到它。默认情况下,它将为127.0.0.1:8545

请记住,每次运行Ganache时,它将创建一个全新的本地区块链- 不会保留先前运行的状态。

如果要持久化数据,可以使用--db选项,指定一个目录存储区块链生成数据

1
2
# 家目录下的data存储数据
npx ganache-cli --deterministic --db ~/data

返回(生成了10个地址和私钥,每个地址里面有100ETH,以及gas的价格和限制,HTTP的RPC端口是监听在8545端口):

 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
40
41
42
43
44
45
46
Ganache CLI v6.9.1 (ganache-core: 2.10.2)

Available Accounts
==================
(0) 0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1 (100 ETH)
(1) 0xFFcf8FDEE72ac11b5c542428B35EEF5769C409f0 (100 ETH)
(2) 0x22d491Bde2303f2f43325b2108D26f1eAbA1e32b (100 ETH)
(3) 0xE11BA2b4D45Eaed5996Cd0823791E0C93114882d (100 ETH)
(4) 0xd03ea8624C8C5987235048901fB614fDcA89b117 (100 ETH)
(5) 0x95cED938F7991cd0dFcb48F0a06a40FA1aF46EBC (100 ETH)
(6) 0x3E5e9111Ae8eB78Fe1CC3bb8915d5D461F3Ef9A9 (100 ETH)
(7) 0x28a8746e75304c0780E011BEd21C72cD78cd535E (100 ETH)
(8) 0xACa94ef8bD5ffEE41947b4585a84BdA5a3d3DA6E (100 ETH)
(9) 0x1dF62f291b2E969fB0849d99D9Ce41e2F137006e (100 ETH)

Private Keys
==================
(0) 0x4f3edf983ac636a65a842ce7c78d9aa706d3b113bce9c46f30d7d21715b23b1d
(1) 0x6cbed15c793ce57650b9877cf6fa156fbef513c4e6134f022a85b1ffdd59b2a1
(2) 0x6370fd033278c143179d81c5526140625662b8daa446c22ee2d73db3707e620c
(3) 0x646f1ce2fdad0e6deeeb5c7e8e5543bdde65e86029e2fd9fc169899c440a7913
(4) 0xadd53f9a7e588d003326d1cbf9e4a43c061aadd9bc938c843a79e7b4fd2ad743
(5) 0x395df67f0c2d2d9fe1ad08d1bc8b6627011959b79c53d7dd6a3536a33ab8a4fd
(6) 0xe485d098507f54e7733a205420dfddbe58db035fa577fc294ebd14db90767a52
(7) 0xa453611d9419d0e56f499079478fd72c37b251a94bfde4d19872c44cf65386e3
(8) 0x829e924fdf021ba3dbbc4225edfece9aca04b929d6e75613329ca6f1d31c0bb4
(9) 0xb0057716d5917badaf911b193b12b910811c1497b5bada8d7711f758981c3773

HD Wallet
==================
Mnemonic:      myth like bonus scare over problem client lizard pioneer submit female collect
Base HD Path:  m/44'/60'/0'/0/{account_index}

Gas Price
==================
20000000000

Gas Limit
==================
6721975

Call Gas Limit
==================
9007199254740991

Listening on 127.0.0.1:8545

由于本地测试链已经启动,这个不能关闭,需要重新开一个终端命令行

使用 openzeppelin CLI 和链交互

要求区块链是在运行汇总,openzeppelin CLI 使用 networks.js配置和链交互,实际就是使用的链提供的RPC接口

1
2
# 查询有哪些地址
oz accounts

返回(结果和ganache启动时输出一致)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
? Pick a network development
Accounts for dev-1586945927338:
Default: 0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1
All:
- 0: 0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1
- 1: 0xFFcf8FDEE72ac11b5c542428B35EEF5769C409f0
- 2: 0x22d491Bde2303f2f43325b2108D26f1eAbA1e32b
- 3: 0xE11BA2b4D45Eaed5996Cd0823791E0C93114882d
- 4: 0xd03ea8624C8C5987235048901fB614fDcA89b117
- 5: 0x95cED938F7991cd0dFcb48F0a06a40FA1aF46EBC
- 6: 0x3E5e9111Ae8eB78Fe1CC3bb8915d5D461F3Ef9A9
- 7: 0x28a8746e75304c0780E011BEd21C72cD78cd535E
- 8: 0xACa94ef8bD5ffEE41947b4585a84BdA5a3d3DA6E
- 9: 0x1dF62f291b2E969fB0849d99D9Ce41e2F137006e

查询余额

1
oz balance

返回(需要手动输入一个地址):

1
2
3
4
? Enter an address to query its balance 0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1
? Pick a network development
Balance: 100 ETH
100000000000000000000

部署合约

1
2
3
4
# 建议使用deploy
oz deploy
# create已经废弃,不建议使用
oz create

使用 oz create 返回(Call a function to initialize the instance after creating it时选择否)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
The create command is deprecated. Use deploy instead.
Nothing to compile, all contracts are up to date.
? Pick a contract to instantiate Box
? Pick a network development
✓ Added contract Box
✓ Contract Box deployed
All implementations have been deployed
? Call a function to initialize the instance after creating it? No
✓ Setting everything up to create contract instances
✓ Instance created at 0xCfEB869F69431e42cdB54A4F4f105C19C080A601
To upgrade this instance run 'oz upgrade'
0xCfEB869F69431e42cdB54A4F4f105C19C080A601

使用 oz deploy 返回:

这个会让你选择哪一种部署方式

? Choose the kind of deployment (Use arrow keys) ❯ regular standard non-upgradeable contract (常规不能升级的合约) upgradeable upgradeable instance using a delegating proxy (EIP1967) (使用委托代理的可升级合约,遵循EIP1967) minimal non-upgradeable minimal proxy instance (EIP1167) (不能升级的最小代理.遵循EIP1167)

我这里选择upgradeable 方便后面测试合约升级

1
2
3
4
5
6
7
8
9
Nothing to compile, all contracts are up to date.
? Choose the kind of deployment upgradeable
? Pick a network development
? Pick a contract to deploy Box
All implementations are up to date
? Call a function to initialize the instance after creating it? No
✓ Instance created at 0x254dffcd3277C0b1660F6d42EFbB754edaBAbC2B
To upgrade this instance run 'oz upgrade'
0x254dffcd3277C0b1660F6d42EFbB754edaBAbC2B

后面我们将使用这个合约地址:0x254dffcd3277C0b1660F6d42EFbB754edaBAbC2B

使用 openzeppelin CLI 和合约交互

合约中两个函数,store设置值,retrieve 查询值

调用函数修改值

1
2
# 使用oz send-tx去发送交易
oz send-tx

返回(调用合约中的store函数,传递一个新的值,100进去)

1
2
3
4
5
6
7
? Pick a network development
? Pick an instance Box at 0x254dffcd3277C0b1660F6d42EFbB754edaBAbC2B
? Select which function store(newValue: uint256)
? newValue: uint256: 100
✓ Transaction successful. Transaction hash: 0x11076136bab3a71551099b76626a9d561e72badba6e2b0d2f1c83d97f0c6d254
Events emitted:
 - ValueChanged(100)

查询值

``retrieve 函数使用view修饰,表示是只读函数,只查询区块链不会去修改,不消耗gas,此类函数使用call方式调用

1
oz call

返回:

1
2
3
4
5
? Pick a network development
? Pick an instance Box at 0x254dffcd3277C0b1660F6d42EFbB754edaBAbC2B
? Select which function retrieve()
✓ Method 'retrieve()' returned: 100
100

使用编程的方式和合约交互

安装web3.js和 OpenZeppelin Contract Loader

1
npm install web3 @openzeppelin/contract-loader

在项目目录新建目录src并创建文件index.js,编写测试代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// 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 accounts = await web3.eth.getAccounts();
    console.log(accounts);
}

main();

运行node src/index.js,如果能打印出账户列表,表示连通没问题

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
[
  '0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1',
  '0xFFcf8FDEE72ac11b5c542428B35EEF5769C409f0',
  '0x22d491Bde2303f2f43325b2108D26f1eAbA1e32b',
  '0xE11BA2b4D45Eaed5996Cd0823791E0C93114882d',
  '0xd03ea8624C8C5987235048901fB614fDcA89b117',
  '0x95cED938F7991cd0dFcb48F0a06a40FA1aF46EBC',
  '0x3E5e9111Ae8eB78Fe1CC3bb8915d5D461F3Ef9A9',
  '0x28a8746e75304c0780E011BEd21C72cD78cd535E',
  '0xACa94ef8bD5ffEE41947b4585a84BdA5a3d3DA6E',
  '0x1dF62f291b2E969fB0849d99D9Ce41e2F137006e'
]

重新编写代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
// 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;

    // 合约地址是之前使用oz deploy部署的那一个
    const address = '0x254dffcd3277C0b1660F6d42EFbB754edaBAbC2B';
    const box = loader.fromArtifact('Box', address);

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

main();

运行node src/index.js,

1
2
$ node src/index.js
Box value is 100

发送交易,调用store函数,将值设置为20:

 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;

    // 合约地址是之前使用oz deploy部署的那一个
    const address = '0x254dffcd3277C0b1660F6d42EFbB754edaBAbC2B';
    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();

运行node src/index.js,

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

编写自动化测试

先忽略

使用公共测试网路测试

先忽略

升级智能合约

此时合约0x254dffcd3277C0b1660F6d42EFbB754edaBAbC2B中的value值为20

升级合约需要关注的点:

1.与合约交互的地址要发生变化吗?如果变化,那么要通知所有调用该合约的人,更新新的地址

2.老合约的数据怎么办?

假设现在为合约新添加了一个新函数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
// contracts/Box.sol
pragma solidity ^0.5.0;

// 引入OpenZeppelin已经写好的权限合约
import "@openzeppelin/contracts/ownership/Ownable.sol";

contract Box is Ownable {
    uint256 private value;

    event ValueChanged(uint256 newValue);


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

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

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

使用oz upgrade去升级函数

1
oz upgrade

返回:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
? Pick a network development
? Which instances would you like to upgrade? Choose by address
? Pick an instance to upgrade Box at 0x254dffcd3277C0b1660F6d42EFbB754edaBAbC2B
? Call a function on the instance after upgrading it? No
✓ Compiled contracts with solc 0.5.17 (commit.d19bba13)
- 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.
- 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.
- 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.
One or more contracts have validation errors. Please review the items listed above and fix them, or run this command again with the --force option.

报了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??? 一步步对着文档来的,你和我搞这个?

后续处理见下一篇