Solidity 合约开发实战
环境
Ganache - Truffle Suite 是一个快速部署启动 EVM 的工具。
Remix 是在线开发环境。
HelloWorld
我们编写一个简单的合约。新建 HelloWorld.sol
1// 开源协议
2// SPDX-License-Identifier: MIT
3// 版本范围 0.8.7~0.9.0
4pragma solidity ^0.8.7;
5
6// 相当于对象名称
7contract HelloWorld {
8 // 函数 hi。public 表示公开。pure 表示无副作用。returns 表明返回值列表。返回值为 string 类型,存放于 memory
9 function hi() public pure returns(string memory) {
10 return "Hello, world!";
11 }
12}
选中 Compiler Tab,点击 Compile
编译。
选中 Deploy Tab,点击 Deploy
部署。
默认的环境为 JavaScript VM
,即浏览器内部的虚拟机。我们也可以设置为使用本机的 Ganache 提供的虚拟机。也可以直接使用本机的 geth 提供的虚拟机。
部署完毕之后,可以在下面执行合约中的方法:
可以看到返回值:
string: Hello, world!
编写有状态的合约
calldata 和 memory 的区别
memory
-
生命周期:函数调用
-
用途:临时存储(函数参数、函数逻辑)
-
可修改:是
-
持久化:否
calldata
-
生命周期:函数外
-
用途:临时存储(仅函数参数)
-
可修改:否
-
持久化:否
storage
-
声明周期:区块链上。永世长存。
-
用途:持久保存
-
可修改:是(要钱)
-
持久化:是
合约代码如下:
1// SPDX-License-Identifier: MIT
2pragma solidity ^0.8.7;
3
4contract StateVariables {
5 string name; // 状态变量
6 address owner; // 状态变量
7 constructor() {
8 name = "initial_name";
9 owner = msg.sender;
10 }
11
12 function setName(string calldata _name) public returns (string memory) {
13 // msg.sender 用于获取命令的执行者地址
14 if (msg.sender != owner){
15 revert("permission denied");
16 }
17 name = _name;
18 return name;
19 }
20 // view 是因为涉及到对状态的读取操作。但是不涉及写操作。
21 function getName() public view returns (string memory) {
22 return name;
23 }
24}
编译部署后,执行 setName 可以花费 gas 修改 name,执行 getName 可以获取 name.
默认我们是部署到第一个账户的。现在切换到第二个账户:
然后执行 setName:
可以看到报错:permission denied
。这样就确保了修改者必须是拥有者。
这里也可以用简单的 require 断言:
1 function setName(string calldata _name) public returns (string memory) {
2 require (msg.sender == owner);
3 name = _name;
4 return name;
5 }
结果都会拒绝。require 的第二个参数可以表明拒绝原因。
1require(msg.value % 2 == 0, "Even value required.");
assert, require 和 revert 的区别
require revert
功能一样,后者适合复杂条件。都用于检查用户请求合法性。
assert
用于确保程序内部运作正确,值符合期待,即程序的内在正确性。
函数修改器
每次都校验所有权很麻烦,可以抽取代码为 modifier:
1// SPDX-License-Identifier: MIT
2pragma solidity ^0.8.7;
3
4contract StateVariables {
5 string name;
6 address owner;
7 constructor() {
8 name = "initial_name";
9 owner = msg.sender;
10 }
11
12 modifier onlyOwner() {
13 require (msg.sender == owner, "permission denied");
14 _; // 表示执行完修改器后,继续其它代码
15 };
16
17 function setName(string calldata _name) public onlyOwner returns (string memory) {
18 name = _name;
19 return name;
20 }
21
22 function getName() public view returns (string memory) {
23 return name;
24 }
25}
事件与日志
事件:用于监听事情的发生。
日志:记录事情的发生历史。可以当作事件的数据。
TX Receipt:当事务发生后,会产生 Recept 接受其结果。
logs 包含:
-
address: 由哪个 contract address 所產生
-
blockHash, blockNumber, transactionHash, transactionIndex
-
logIndex:
-
data: raw data (32 bytes 为单位)
-
topics: 主题。每个 log 只能有四段,第一段为 log id 的哈希值。
各种 log 通过 topics 归类。
topics 字段可以作为 filter 的搜索目标。
Event 的定义
event name(args)
Emit
Emit 用于发送日志。
emit nameZ(params)
多主题
第一个 topic 一定是 Log id hash. 因此还剩三个槽位。
其他三个加上 indexed 修改器,则此实参作为一个 topic 存在 receipt 的 topics 中
array 形式的数据(如 string 或 bytes)会在哈希后存储。
示例代码
1// SPDX-License-Identifier: MIT
2pragma solidity ^0.8.7;
3
4contract EventAndTopic {
5 string info;
6 uint balance;
7
8 event LogCreate(string info, uint balance);
9 event LogCreateIndex(string indexed info, uint indexed balance);
10
11 constructor() {
12 info = "default info";
13 balance = 100;
14 emit LogCreate(info, balance);
15 emit LogCreateIndex(info, balance);
16 }
17}
部署之后点开可以看到日志:
1[
2 {
3 "from": "0x9bF88fAe8CF8BaB76041c1db6467E7b37b977dD7",
4 "topic": "0xa72cbe70c44be6b30e3f437947bf652049cbda9a491cee49fb220b0d7cf4238b",
5 "event": "LogCreate",
6 "args": {
7 "0": "default info",
8 "1": "100",
9 "info": "default info",
10 "balance": "100"
11 }
12 },
13 {
14 "from": "0x9bF88fAe8CF8BaB76041c1db6467E7b37b977dD7",
15 "topic": "0x1c943f78f42f6f44cd2eac3c15756890697f14be68b248014ee937ebd0dd0831",
16 "event": "LogCreateIndex",
17 "args": {
18 "0": {
19 "_isIndexed": true,
20 "hash": "0x0966507e7759ca1addb355ce5978c391001a8759581c8e950f63ba4489df0e9c"
21 },
22 "1": "100"
23 }
24 }
25]
可以看到,创建了两个日志。
对于前一个 event log
Topic 实际上就是事件签名 LogCreate(string,uint256)
的 Keccak-256 散列值:
a72cbe70c44be6b30e3f437947bf652049cbda9a491cee49fb220b0d7cf4238b
对于第二个,Topic 是 LogCreateIndex(string,uint256)
的 Keccak-256 散列值
args0
多了一个 hash
,它其实是字符串 default info
的散列值。
Fallback
Fallback 是一种特殊的函数。触发条件:
-
调用了合约上不存在的函数。
-
合约收到 ether 但没有携带 data。
在 Fallback 中避免以下操作,以免浪费钱:
-
修改状态
-
创建合约
-
调用外部函数
-
发送 Ether
如果未定义 Fallback 函数,则收到 ether 会触发异常并退回 ether
如果 Contract 能够收取 ether,则需要加上 payable 修改器,并且声明 fallback 函数。
例子:
1// SPDX-License-Identifier: MIT
2pragma solidity ^0.8.7;
3
4contract FallbackExample {
5
6 event LogFallback(string message);
7 event LogBalance(uint balance);
8
9 fallback() external {
10 emit LogFallback("Fallback");
11 emit LogBalance(address(this).balance);
12 }
13}
我们随便填写一些 CALLDATA,点击 Transact,即可触发 fallback:
日志:
1[
2 {
3 "from": "0xA831F4e5dC3dbF0e9ABA20d34C3468679205B10A",
4 "topic": "0x7be0c02701573407689a2d6c44902697d15a6181717171fb07f0270b225eaddc",
5 "event": "LogFallback",
6 "args": {
7 "0": "Fallback",
8 "message": "Fallback"
9 }
10 },
11 {
12 "from": "0xA831F4e5dC3dbF0e9ABA20d34C3468679205B10A",
13 "topic": "0x8d9fc242eead7aebf4b509c32519beb6e2975a572c06310f797da4bd7f1ffab6",
14 "event": "LogBalance",
15 "args": {
16 "0": "0",
17 "balance": "0"
18 }
19 }
20]
可以增加 payable 修改器收钱:
1 fallback() payable external {
2 emit LogFallback("Fallback");
3 emit LogBalance(address(this).balance);
4 }
地址类型
address 是一个 20 bytes 的 eth 地址。通过 <addr>.balance
获得其余额(以 Wei 为单位)
通过 <addr>.send(uint256 amout)
送钱。
失败会返回 false.,一般搭配 require 使用。
通过 <addr>.transfer(uint256 amount)
转账。会触发 fallback(),消耗 2300 gas。
失败会抛出异常。
1// SPDX-License-Identifier: MIT
2pragma solidity ^0.8.7;
3
4contract Address {
5
6 fallback() external payable {}
7
8 function Balance() public view returns(uint256) {
9 return address(this).balance;
10 }
11
12 function Transfer(uint256 amount) public payable returns(bool) {
13 payable(msg.sender).transfer(amount * 1 ether);
14 return true;
15 }
16
17 function SendWithoutCheck(uint256 amount) public payable returns (bool) {
18 payable(msg.sender).send(amount * 1 ether);
19 return true;
20 }
21
22 function SendWithCheck(uint256 amount) public payable returns (bool) {
23 require(payable(msg.sender).send(amount * 1 ether), "failed to send");
24 return true;
25 }
26}
编译部署。这个编译会产生警告,不用管。
填入 VALUE = 10,来个 Fallback
然后查询会发现有 Balance.
执行 SendWithCheck
SendWithoutCheck
Transfer
以参数 1 都能实现转账。
但是以参数 100,若是 SendWithoutCheck
则会看似成功实际失败。
映射
mapping (T1 => T2) <var>;
例子:
1// SPDX-License-Identifier: MIT
2pragma solidity ^0.8.7;
3
4contract Donation {
5 mapping(address => uint) public ledger;
6 mapping(address => bool) public donors;
7 address[] public donorList;
8
9 function isDonor(address pAddr) internal view returns (bool) {
10 return donors[pAddr];
11 }
12
13 function donate() public payable {
14 if(msg.value < 1){
15 revert(" < 1 ether");
16 }
17 if(!isDonor(msg.sender)){
18 donors[msg.sender] = true;
19 donorList.push(msg.sender);
20 }
21
22 ledger[msg.sender] += msg.value;
23 }
24}
键,实际存的使用,用的是哈希值。
mapping 无法迭代 key。
Struct
和 C++ 差不多。
internal 构造器 / is
相当于把合约变成抽象合约。配合 is 实现继承。
在新版本中,可以直接用 abstract
关键字。
1// SPDX-License-Identifier: MIT
2pragma solidity ^0.8.7;
3
4abstract contract Ownable {
5 address private owner;
6 constructor() {
7 owner = msg.sender;
8 }
9
10 modifier onlyOwner() {
11 require(isOwner());
12 _;
13 }
14
15 function isOwner() public view returns (bool) {
16 return owner == msg.sender;
17 }
18}
19
20contract Main is Ownable {
21 string public name = "";
22 function modifyName(string calldata _name) public onlyOwner {
23 name = _name;
24 }
25}
Interface
和其它 OO 语言一样,略。
Library
即共享库。只能部署一次于指定地址,可以被多处使用。
-
没有状态
-
不能继承或被继承
-
不能收钱
例子:
1// SPDX-License-Identifier: GPL-3.0
2pragma solidity >=0.6.0 <0.9.0;
3
4
5// We define a new struct datatype that will be used to
6// hold its data in the calling contract.
7struct Data {
8 mapping(uint => bool) flags;
9}
10
11library Set {
12 // Note that the first parameter is of type "storage
13 // reference" and thus only its storage address and not
14 // its contents is passed as part of the call. This is a
15 // special feature of library functions. It is idiomatic
16 // to call the first parameter `self`, if the function can
17 // be seen as a method of that object.
18 function insert(Data storage self, uint value)
19 public
20 returns (bool)
21 {
22 if (self.flags[value])
23 return false; // already there
24 self.flags[value] = true;
25 return true;
26 }
27
28 function remove(Data storage self, uint value)
29 public
30 returns (bool)
31 {
32 if (!self.flags[value])
33 return false; // not there
34 self.flags[value] = false;
35 return true;
36 }
37
38 function contains(Data storage self, uint value)
39 public
40 view
41 returns (bool)
42 {
43 return self.flags[value];
44 }
45}
46
47
48contract C {
49 Data knownValues;
50
51 function register(uint value) public {
52 // The library functions can be called without a
53 // specific instance of the library, since the
54 // "instance" will be the current contract.
55 require(Set.insert(knownValues, value));
56 }
57 // In this contract, we can also directly access knownValues.flags, if we want.
58}
SafeMath
Solidity 中数学运算必须非常小心。所以社区开发了 SafeMath 库。
地址:https://docs.openzeppelin.com/contracts/2.x/api/math
Import & Using
Import
即引入功能。
1import "/project/lib/util.sol"; // source unit name: /project/lib/util.sol
2import "lib/util.sol"; // source unit name: lib/util.sol
3import "@openzeppelin/address.sol"; // source unit name: @openzeppelin/address.sol
4import "https://example.com/token.sol"; // source unit name: https://example.com/token.sol
详见文档。
Using
using <lib> for <type>;
例子:
1using Set for Set.Data
从而创建别名 Set.Data
ERC20 接口
ERC20 是一个代币标准。详见:此文
例子在这。