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}

image_up_163948588072438dad.jpg

选中 Compiler Tab,点击 Compile 编译。

选中 Deploy Tab,点击 Deploy 部署。

image_up_1639485945b19dec8d.jpg

默认的环境为 JavaScript VM,即浏览器内部的虚拟机。我们也可以设置为使用本机的 Ganache 提供的虚拟机。也可以直接使用本机的 geth 提供的虚拟机。

部署完毕之后,可以在下面执行合约中的方法:

image_up_1639486074290a2f19.jpg

可以看到返回值:

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.

默认我们是部署到第一个账户的。现在切换到第二个账户:

image_up_16394872958a35ff0f.jpg

然后执行 setName:

image_up_16394873169aba286e.jpg

可以看到报错: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}

部署之后点开可以看到日志:

image_up_1639493588b4932efe.jpg

 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 是一种特殊的函数。触发条件:

  1. 调用了合约上不存在的函数。

  2. 合约收到 ether 但没有携带 data。

在 Fallback 中避免以下操作,以免浪费钱:

  1. 修改状态

  2. 创建合约

  3. 调用外部函数

  4. 发送 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:

image_up_163949458829aafc55.jpg

日志:

 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

image_up_163949614051aa20fa.jpg

然后查询会发现有 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 是一个代币标准。详见:此文

例子在

参考

https://www.bilibili.com/video/BV1Ht4y197h8?