使用openzeppelin从Solidity中创建可升级智能合约

本文是对openzeppelin文档中Creating Upgradeable Contracts From Solidity的实践

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

要建立两个合约Factory.solProduct.sol,目标是通过Factory.sol去创建一个可以升级的Product.sol合约.

contracts目录下创建Factory.sol合约

 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
// contracts/Factory.sol
pragma solidity ^0.5.0;

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

contract Factory is Initializable {
    App private app;

    event InstanceCreated(address);

    function initialize(App _app) public initializer {
    	// 和之前不一样的是这里把另一个合约作为对象
        app = _app;
    }

    function createInstance(bytes memory _data) public {
    	// first-smart-contract是当前项目的包名称
        string memory packageName = "first-smart-contract";
        string memory contractName = "Product";
        address admin = msg.sender;

        address product = address(
            app.create(packageName, contractName, admin, _data)
        );

        emit InstanceCreated(product);
    }
}

其中 first-smart-contract 是之前使用npm init初始化时输入的包名,这个值也保存在.openzeppelin/project.json配置文件中,可以自己查看

contracts目录下创建Product.sol合约,合约代码使用之前Box.sol的代码稍微做一下更改

修改下初始化函数,可以指定value的值

依然保留store和retrieve函数

 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
// contracts/Product.sol
pragma solidity ^0.5.0;

import "@openzeppelin/upgrades/contracts/Initializable.sol";
import "@openzeppelin/contracts-ethereum-package/contracts/ownership/Ownable.sol";

contract Product is Initializable, Ownable {
    uint256 public value;

    event ValueChanged(uint256 newValue);

    // 修改下初始化函数,可以指定value的值
    function initialize(uint256 _value) public initializer {
        value = _value;
    }

    // 依然保留store和retrieve函数
    function store(uint256 newValue) public onlyOwner {
        value = newValue;
        emit ValueChanged(newValue);
    }

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

依然是先启动本地测试链,--db指定之前保存链数据的目录

1
npx ganache-cli --deterministic --db ~/data

再新开一个命令行窗口,进入项目目录,输入:

1
oz add Product

返回:

1
2
3
$ npx oz add Product
✓ Compiled contracts with solc 0.5.17 (commit.d19bba13)
✓ Added contract Product

此步骤是将Product.sol编译,会在build/contracts目录下生成编译后的json文件

再将Procuct添加到.openzeppelin/project.json配置文件中contract字段

 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
{
  "manifestVersion": "2.2",
  "contracts": {
    "Box": "Box",
    "Product": "Product"	<--- 就是这个位置
  },
  "dependencies": {},
  "name": "first-smart-contract",
  "version": "1.0.0",
  "compiler": {
    "compilerSettings": {
      "optimizer": {
        "enabled": false,
        "runs": "200"
      }
    },
    "typechain": {
      "enabled": false
    },
    "manager": "openzeppelin",
    "solcVersion": "0.5.17",
    "artifactsDir": "build/contracts",
    "contractsDir": "contracts"
  },
  "telemetryOptIn": false
}

执行oz push

1
oz push

返回(Box是上一篇中用到的合约,可以忽略不看):

1
2
3
4
5
$ oz push
Nothing to compile, all contracts are up to date.
? Pick a network development
✓ Contract Box deployed
✓ Contract Product deployed

此步骤是将.openzeppelin/project.json配置文件中contract字段的合约进行部署,会在.openzeppelin/dev-1587092426749.json中生成以合约名为字段的数据,dev-1587092426749.json中数字部分是时间戳,所以每个人不一样

.openzeppelin/dev-1587092426749.json大致内容:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
{
  "contracts": {
    "Box": {...省略...},
    "Product": {...省略...}
  },
  "solidityLibs": {},
  "proxies": {},
  "manifestVersion": "2.2",
  "version": "1.0.0"
}

其中Product字段内容大致如下,省略部分:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
{
      "address": "0xA94B7f0465E98609391C623d0560C5720a3f2D33",
      "constructorCode": "60806040526109f6806100136000396000f3fe",
      "bodyBytecodeHash": "9711d7e2adef036560304526efa3b5605a73565d8600a96c07f635293e00532f",
      "localBytecodeHash": "e85d71e1251c7f3ba93e4d0ec72fd80027628cc30ab70cc3a2d4bcad56c9370b",
      "deployedBytecodeHash": "e85d71e1251c7f3ba93e4d0ec72fd80027628cc30ab70cc3a2d4bcad56c9370b",
      "types": {...省略...},
      "storage": [...省略...],
      "warnings": {
        "hasConstructor": false,
        "hasSelfDestruct": false,
        "hasDelegateCall": false,
        "hasInitialValuesInDeclarations": false,
        "uninitializedBaseContracts": []
      }
    }
  }

执行oz publish

1
oz publish

返回:

1
2
3
4
5
6
$ oz publish
? Pick a network development
✓ Project structure deployed
✓ Registering Box at 0x0E696947A06550DEf604e82C26fd9E493e576337 in directory
✓ Registering Product at 0xA94B7f0465E98609391C623d0560C5720a3f2D33 in directory
✓ Published to dev-1587092426749!

此步骤会在.openzeppelin/project.json配置文件中增加几个字段app,package,provider3个字段

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
{
  "contracts": {
    "Box": {...省略...},
    "Product": {...省略...} ,
  },
  "solidityLibs": {},
  "proxies": {},
  "manifestVersion": "2.2",
  "version": "1.0.0",
  "app": {
    "address": "0x6eD79Aa1c71FD7BdBC515EfdA3Bd4e26394435cC"
  },
  "package": {
    "address": "0xb09bCc172050fBd4562da8b229Cf3E45Dc3045A6"
  },
  "provider": {
    "address": "0xFC628dd79137395F3C9744e33b1c5DE554D94882"
  }
}

其中app字段:

1
2
3
"app": {
    "address": "0x6eD79Aa1c71FD7BdBC515EfdA3Bd4e26394435cC"
  }

现在可以像之前那样去部署Factory合约:

1
oz deploy

返回:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
$ oz deploy
Nothing to compile, all contracts are up to date.
? Choose the kind of deployment upgradeable
? Pick a network development
? Pick a contract to deploy Factory
✓ Added contract Factory
✓ Contract Factory deployed
All implementations have been deployed
# 选择Y,调用初始化函数,函数选择initialize(_app: address)
? Call a function to initialize the instance after creating it? Yes
? Select which function initialize(_app: address)
# 复制前面app字段的地址进来
? _app: address: 0x6eD79Aa1c71FD7BdBC515EfdA3Bd4e26394435cC
✓ Setting everything up to create contract instances
✓ first-smart-contract Factory instance created at 0x86072CbFF48dA3C1F01824a6761A03F105BCC697
To upgrade this instance run 'oz upgrade'
0x86072CbFF48dA3C1F01824a6761A03F105BCC697

Factory合约地址为0x86072CbFF48dA3C1F01824a6761A03F105BCC697,这个需要用到.

用命令行调用Factory合约的createInstance函数去创建一个新的Product合约

createInstance接收一个bytes类型的_data数据,这个值需要通过计算获得

1
2
3
4
$ node
> const { encodeCall } = require('@openzeppelin/upgrades');
> encodeCall('initialize', ['uint256'], [42]);
'0xfe4b84df000000000000000000000000000000000000000000000000000000000000002a'

在使用命令行发送交易进行调用

1
oz send-tx

返回(选择createInstance(_data: bytes)函数,复制上面计算的_data进去):

1
2
3
4
5
6
7
8
$ oz send-tx
? Pick a network development
? Pick an instance Factory at 0x86072CbFF48dA3C1F01824a6761A03F105BCC697
? Select which function createInstance(_data: bytes)
? _data: bytes: 0xfe4b84df000000000000000000000000000000000000000000000000000000000000002a
✓ Transaction successful. Transaction hash: 0x2449a21c33249836420c12209eaba2dec72eb1b81ba693ef7d04aa5b7083da56
Events emitted:
 - InstanceCreated(0x067805E69e62E8bE56e8D13f4EBf53372D3dD02e)

TIP

用javascript的方式去调用Factory合约的createInstance函数,没有调试通过,还有问题待解决

待解决