solidity学习笔记

前言

做区块链相关的创业已经有4年了,都是做ETL方面的,对于真正的合约以及Trading,其实没有真正的使用,技术就是这样,用了就会发现有意思,从最早的没有信仰,到现在的信心满满,虽然BTC的价格还是3年前BTC的价格,但是市场已经今非昔比了,从DEFI的火热,到各种POOL的出现,区块链的运营方式和目标人群都巨大的变化了。在大经济形势周期的转变下,我也从数据,转战到了DEFI,因为DEFI才是代表了未来真正的放下,大环境下,随着疫情,越来越多的政府和国家地区更深入的隔绝,对于崇尚自由的人来说,我们更应该获得真正意义上的金融自由。虽然现在Blockchain还是被美帝把持着。

闲话不多说,我们开始真正的来玩转合约,合约从Ether起,也和Ether的创始团队息息相关,从V神,还有WOOD博士开始,Ether的出现也带来了Solidity和Vyper,智能合约语言,目前来看Vyper更安全,而Solidity使用更广泛,本文主要讲Solidity的开发和使用。

Solidity文档: https://learnblockchain.cn/docs/solidity/introduction-to-smart-contracts.html

Solidity

正文

Solidity语法

Solidity 是一门面向合约的、为实现智能合约而创建的高级编程语言。这门语言受到了 C++,Python 和 Javascript 语言的影响,设计的目的是能在 以太坊虚拟机(EVM) 上运行。Solidity 是静态类型语言,支持继承、库和复杂的用户定义类型等特性。在部署合约时,应该尽量使用最新版本,因为新版本会有一些重大的新特性以及bug修复。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// SPDX-License-Identifier: GPL-3.0 
pragma solidity >=0.4.16 <0.8.0;

contract SimpleStorage {
uint storedData;

function set(uint x) public {
storedData = x;
}

function get() public view returns (uint) {
return storedData;
}
}

pragmas(编译指令)是告知编译器如何处理源代码的指令, Solidity中合约的含义就是一组代码(它的 函数 )和数据(它的 状态 ),它们位于以太坊区块链的一个特定地址上。该合约能完成的事情并不多(由于以太坊构建的基础架构的原因):它能允许任何人在合约中存储一个单独的数字,并且这个数字可以被世界上任何人访问,且没有可行的办法阻止你发布这个数字。当然,任何人都可以再次调用 set ,传入不同的值,覆盖你的数字,但是这个数字仍会被存储在区块链的历史记录中。随后,我们会看到怎样施加访问限制,以确保只有你才能改变这个数字。 所有的标识符(合约名称,函数名称和变量名称)都只能使用ASCII字符集。UTF-8编码的数据可以用字符串变量的形式存储。

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
// SPDX-License-Identifier: GPL-3.0
pragma solidity >0.5.99 <0.8.0;

contract Coin {
// 关键字“public”让这些变量可以从外部读取
address public minter;
mapping (address => uint) public balances;

// 轻客户端可以通过事件针对变化作出高效的反应
event Sent(address from, address to, uint amount);

// 这是构造函数,只有当合约创建时运行
constructor() {
minter = msg.sender;
}
// 铸币操作
function mint(address receiver, uint amount) public {
require(msg.sender == minter);
require(amount < 1e60);
balances[receiver] += amount;
}
// 转币操作
function send(address receiver, uint amount) public {
require(amount <= balances[msg.sender], "Insufficient balance.");
balances[msg.sender] -= amount;
balances[receiver] += amount;
emit Sent(msg.sender, receiver, amount);
}
}

address public minter; 这一行声明了一个可以被公开访问的 address 类型的状态变量。 address 类型是一个160位的值,且不允许任何算数操作。mapping (address => uint) public balances;` 也创建一个公共状态变量,但它是一个更复杂的数据类型。 该类型将address映射为无符号整数。 Mappings 可以看作是一个 哈希表 它会执行虚拟初始化,以使所有可能存在的键都映射到一个字节表示为全零的值。 但是,这种类比并不太恰当,因为它既不能获得映射的所有键的列表,也不能获得所有值的列表。

event Sent(address from, address to, uint amount); 这行声明了一个所谓的“事件(event)”,它会在 send 函数的最后一行被发出。

对于程序员来说,区块链这个概念并不难理解,这是因为大多数难懂的东西 (挖矿, 哈希椭圆曲线密码学点对点网络(P2P) 等) 都只是用于提供特定的功能和承诺。

区块,交易,事务

区块链是全球共享的事务性数据库,这意味着每个人都可加入网络来阅读数据库中的记录。如果你想改变数据库中的某些东西,你必须创建一个被所有其他人所接受的事务。此外,交易总是由发送人(创建者)签名。

在比特币中,要解决的一个主要难题,被称为“双花攻击 (double-spend attack)”:如果网络存在两笔交易,都想花光同一个账户的钱时(即所谓的冲突)会发生什么情况?交易互相冲突?

以太仿虚拟机EVM

以太坊虚拟机 EVM 是智能合约的运行环境。它不仅是沙盒封装的,而且是完全隔离的,也就是说在 EVM 中运行代码是无法访问网络、文件系统和其他进程的。甚至智能合约之间的访问也是受限的。以太坊中有两类账户(它们共用同一个地址空间): 外部账户 由公钥-私钥对(也就是人)控制; 合约账户 由和账户一起存储的代码控制.每个账户都有一个键值对形式的持久化存储。其中 key 和 value 的长度都是256位,我们称之为 存储 。此外,每个账户有一个以太币余额( balance )(单位是“Wei”, 1 ether10**18 wei),余额会因为发送包含以太币的交易而改变。交易可以看作是从一个帐户发送到另一个帐户的消息(这里的账户,可能是相同的或特殊的零帐户,请参阅下文)。它能包含一个二进制数据(合约负载)和以太币。如果目标账户含有代码,此代码会被执行,并以 payload 作为入参。如果目标账户是零账户(账户地址为 0 ),此交易将创建一个 新合约 。 如前文所述,合约的地址不是零地址,而是通过合约创建者的地址和从该地址发出过的交易数量计算得到的(所谓的“nonce”)。 这个用来创建合约的交易的 payload 会被转换为 EVM 字节码并执行。执行的输出将作为合约代码被永久存储。这意味着,为创建一个合约,你不需要发送实际的合约代码,而是发送能够产生合约代码的代码。

一经创建,每笔交易都收取一定数量的 gas ,目的是限制执行交易所需要的工作量和为交易支付手续费。EVM 执行交易时,gas 将按特定规则逐渐耗尽。gas price 是交易发送者设置的一个值,发送者账户需要预付的手续费= gas_price * gas 。如果交易执行后还有剩余, gas 会原路返还。无论执行到什么位置,一旦 gas 被耗尽(比如降为负值),将会触发一个 out-of-gas 异常。当前调用帧(call frame)所做的所有状态修改都将被回滚。译者注:调用帧(call frame),指的是下文讲到的EVM的运行栈(stack)中当前操作所需要的若干元素。

每个账户有一块持久化内存区称为 存储 。 存储是将256位字映射到256位字的键值存储区。 在合约中枚举存储是不可能的,且读存储的相对开销很高,修改存储的开销甚至更高。合约只能读写存储区内属于自己的部分。

第二个内存区称为 内存 ,合约会试图为每一次消息调用获取一块被重新擦拭干净的内存实例。 内存是线性的,可按字节级寻址,但读的长度被限制为256位,而写的长度可以是8位或256位。当访问(无论是读还是写)之前从未访问过的内存字(word)时(无论是偏移到该字内的任何位置),内存将按字进行扩展(每个字是256位)。扩容也将消耗一定的gas。 随着内存使用量的增长,其费用也会增高(以平方级别)。

EVM 不是基于寄存器的,而是基于栈的,因此所有的计算都在一个被称为 栈(stack) 的区域执行。 栈最大有1024个元素,每个元素长度是一个字(256位)。对栈的访问只限于其顶端,限制方式为:允许拷贝最顶端的16个元素中的一个到栈顶,或者是交换栈顶元素和下面16个元素中的一个。所有其他操作都只能取最顶的两个(或一个,或更多,取决于具体的操作)元素,运算后,把结果压入栈顶。当然可以把栈上的元素放到存储或内存中。但是无法只访问栈上指定深度的那个元素,除非先从栈顶移除其他元素。

EVM的指令集量应尽量少,以最大限度地避免可能导致共识问题的错误实现。所有的指令都是针对”256位的字(word)”这个基本的数据类型来进行操作。具备常用的算术、位、逻辑和比较操作。也可以做到有条件和无条件跳转。此外,合约可以访问当前区块的相关属性,比如它的编号和时间戳。

有一种特殊类型的消息调用,被称为 委托调用(delegatecall) 。它和一般的消息调用的区别在于,目标地址的代码将在发起调用的合约的上下文中执行,并且 msg.sendermsg.value 不变。 这意味着一个合约可以在运行时从另外一个地址动态加载代码。存储、当前地址和余额都指向发起调用的合约,只有代码是从被调用地址获取的。 这使得 Solidity 可以实现”库“能力:可复用的代码库可以放在一个合约的存储上,如用来实现复杂的数据结构的库。

有一种特殊的可索引的数据结构,其存储的数据可以一路映射直到区块层级。这个特性被称为 日志(logs) ,Solidity用它来实现 事件(events) 。合约创建之后就无法访问日志数据,但是这些数据可以从区块链外高效的访问。因为部分日志数据被存储在 布隆过滤器(Bloom filter) 中,我们可以高效并且加密安全地搜索日志,所以那些没有下载整个区块链的网络节点(轻客户端)也可以找到这些日志。

合约代码从区块链上移除的唯一方式是合约在合约地址上的执行自毁操作 selfdestruct 。合约账户上剩余的以太币会发送给指定的目标,然后其存储和代码从状态中被移除。移除一个合约听上去不错,但其实有潜在的危险,如果有人发送以太币到移除的合约,这些以太币将永远提丢失。

安装Solidity

1
2
3
4
5
6
7
8
9
10
11
### 最常用的还是通过nodejs来安装
npm install -g solc

docker run ethereum/solc:stable solc --version

sudo add-apt-repository ppa:ethereum/ethereum
sudo apt-get update
sudo apt-get install solc

## 从源码安装
sudo xcodebuild -license accept

Solidity源文件结构

源文件中可以包含任意多个 合约定义导入源文件指令版本标识 指令、 结构体 , 枚举函数 定义.

1
pragma solidity ^0.5.2;

这样,源文件将既不允许低于 0.5.2 版本的编译器编译, 也不允许高于(包含) 0.6.0 版本的编译器编译(第二个条件因使用 ^ 被添加)。 这种做法的考虑是,编译器在 0.6.0 版本之前不会有重大变更,所以可确保源代码始终按预期被编译。 上面例子中不固定编译器的具体版本号,因此编译器的补丁版也可以使用。

Pragma 是 pragmatic information 的简称,微软 Visual C++ 文档 中译为标识。 Solidity 中沿用 C ,C++ 等中的编译指令概念,用于告知编译器 如何 编译。 ——译者注

第2个标注是用来标注实验性阶段的功能,它可以用来启用一些新的编译器功能或语法特性。 当前支持下面的一些实验性标注: ABIEncoderV2

新的 ABI 编码器可以用来编码和解码嵌套的数组和结构体,当然这部分代码还在优化之中,他没有像之前 ABI 编码器 那样经过严格的测试,我们可以使用下面的语法来启用它

1
pragma experimental ABIEncoderV2;
1
import "filename";

ES6 即 ECMAScript 6.0,ES6是 JavaScript 语言的下一代标准,已经在 2015 年 6 月正式发布。 ——译者注

通常使用相对引用 import "./filename.sol"; 并且避免使用 .. ,后面这种方式可以使用全局路径并设置映射,下面会有解释。

可以使用单行注释(//)和多行注释(/*...*/

在 Solidity 语言中,合约类似于其他面向对象编程语言中的

每个合约中可以包含 状态变量函数事件 Event结构体、 和 枚举类型 的声明,且合约可以从其他合约继承。

还有一些特殊的合约,如: 接口.

1
2
3
4
5
6
pragma solidity >=0.4.0 <0.8.0;

contract TinyStorage {
uint storedXlbData; // 状态变量
// ...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
// SPDX-License-Identifier: GPL-3.0
pragma solidity >0.7.0 <0.8.0;

contract TinyAuction {
function Mybid() public payable { // 定义函数
// ...
}
}

// Helper function defined outside of a contract
function helper(uint x) pure returns (uint) {
return x * 2;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
pragma solidity >=0.4.22 <0.8.0;

contract MyPurchase {
address public seller;

modifier onlySeller() { // 修改器
require(
msg.sender == seller,
"Only seller can call this."
);
_;
}

function abort() public onlySeller { // 修改器用法
// ...
}
}

函数 修改器modifier 可以用来以声明的方式修改函数语义(参阅合约章节中 函数修改器)。

1
2
3
4
5
6
7
8
9
pragma solidity >=0.4.21 <0.8.0;
contract TinyAuction {
event HighestBidIncreased(address bidder, uint amount); // 事件

function bid() public payable {
// ...
emit HighestBidIncreased(msg.sender, msg.value); // 触发事件
}
}

事件是能方便地调用以太坊虚拟机日志功能的接口。

1
2
3
4
5
6
7
8
9
10
pragma solidity >=0.4.0 <0.8.0;

contract TinyBallot {
struct Voter { // 结构体 结构体是可以将几个变量分组的自定义类型
uint weight;
bool voted;
address delegate;
uint vote;
}
}
1
2
3
4
5
pragma solidity >=0.4.0 <0.8.0;

contract Upchain {
enum State { Created, Locked, InValid } // 枚举 枚举可用来创建由一定数量的“常量值”构成的自定义类型
}

值类型

bool :可能的取值为字面常量值 truefalse

运算符:

  • ! (逻辑非)

  • && (逻辑与, “and” )

  • || (逻辑或, “or” )

  • == (等于)

  • != (不等于)

    运算符 ||&& 都遵循同样的短路( short-circuiting )规则。就是说在表达式 f(x) || g(y) 中, 如果 f(x) 的值为 true ,那么 g(y) 就不会被执行,即使会出现一些副作用。

int / uint :分别表示有符号和无符号的不同位数的整型变量。 支持关键字 uint8uint256 (无符号,从 8 位到 256 位)以及 int8int256,以 8 位为步长递增。 uintint 分别是 uint256int256 的别名。

运算符:

  • 比较运算符: <=<==!=>=> (返回布尔值)
  • 位运算符: &|^ (异或), ~ (位取反)
  • 移位运算符: << (左移位) , >> (右移位)
  • 算数运算符: +- , 一元运算 - , 一元运算 +*/% (取余或叫模运算) , ** (幂)

对于整形 X,可以使用 type(X).mintype(X).max 去获取这个类型的最小值与最大值。

加法,减法和乘法具有通常的语义,值用两进制补码表示,意思是比如:uint256(0) - uint256(1)== 2 ** 256 - 1 。 我们在设计和编写智能合约时必须考虑到溢出问题。

表达式 -x 相当于 (T(0) - x) 这里 T 是指 x 的类型。 这意味着如果 x 的类型的类型是无符号整数类型 -x 不会是负数。 另外,如果 x 为负数, -x 也可以为正数。 由于两进制补码表示还需要小心:

除法运算结果的类型始终是其中一个操作数的类型,整数除法总是产生整数。 在Solidity中,分数会取零。 这意味着 int256(-5) / int256(2) == int256(-2)

注意在智能合约中,在 字面常量 上进行除法会保留精度(保留小数位)。

除以0 会发生错误(assert 类型错误)。

模运算 a%n 是在操作数 a 的除以 n 之后产生余数 r ,其中 q = int(a / n)r = a - (n * q) 。 这意味着模运算结果与左操作数相同的符号相同(或零)。 对于 负数的a : a % n == -(a % n), 几个例子:

  • int256(5) % int256(2) == int256(1)
  • int256(5) % int256(-2) == int256(1)
  • int256(-5) % int256(2) == int256(-1)
  • int256(-5) % int256(-2) == int256(-1)

注解, 对0取模会发生错误(assert 类型错误)。

注意 `00在EVM中定义为1` 。**

fixed / ufixed:表示各种大小的有符号和无符号的定长浮点型。 在关键字 ufixedMxNfixedMxN 中,M 表示该类型占用的位数,N 表示可用的小数位数。 M 必须能整除 8,即 8 到 256 位。 N 则可以是从 0 到 80 之间的任意数。 ufixedfixed 分别是 ufixed128x19fixed128x19 的别名。

地址类型有两种形式,他们大致相同:

  • address:保存一个20字节的值(以太坊地址的大小)。
  • ddress payable :可支付地址,与 address 相同,不过有成员函数 transfersend

这种区别背后的思想是 address payable 可以接受以太币的地址,而一个普通的 address 则不能

可以使用 balance 属性来查询一个地址的余额, 也可以使用 transfer 函数向一个可支付地址(payable address)发送 以太币Ether (以 wei 为单位):

1
2
3
address x = 0x123;
address myAddress = this;
if (x.balance < 10 && myAddress.balance >= 10) x.transfer(10);

如果当前合约的余额不够多,则 transfer 函数会执行失败,或者如果以太转移被接收帐户拒绝, transfer 函数同样会失败而进行回退。

如果 x 是一个合约地址,它的代码(更具体来说是, 如果有receive函数, 执行 receive 接收以太函数, 或者存在fallback函数,执行 Fallback 回退函数 函数)会跟 transfer 函数调用一起执行(这是 EVM 的一个特性,无法阻止)。 如果在执行过程中用光了 gas 或者因为任何原因执行失败,以太币Ether 交易会被打回,当前的合约也会在终止的同时抛出异常。

1
2
警告⚠️:
在使用 send 的时候会有些风险:如果调用栈深度是 1024 会导致发送失败(这总是可以被调用者强制),如果接收者用光了 gas 也会导致发送失败。 所以为了保证 以太币Ether 发送的安全,一定要检查 send 的返回值,使用 transfer 或者更好的办法: 使用接收者自己取回资金的模式。
1
2
3
4
5
6
7
bytes memory payload = abi.encodeWithSignature("register(string)", "MyName");
(bool success, bytes memory returnData) = address(nameReg).call(payload);
require(success);

address nameReg = 0x72ba7d8e73fe8eb666ea66babc8116a41bfb10e2;
nameReg.call("register", "MyName");
nameReg.call(bytes4(keccak256("fun(uint256)")), a);

所有这些函数都是低级函数,应谨慎使用。 具体来说,任何未知的合约都可能是恶意的,我们在调用一个合约的同时就将控制权交给了它,而合约又可以回调合约,所以要准备好在调用返回时改变相应的状态变量(可参考 可重入 ), 与其他合约交互的常规方法是在合约对象上调用函数(x.f())。

所有三个函数 calldelegatecallstaticcall 都是非常低级的函数,应该只把它们当作 最后一招 来使用,因为它们破坏了 Solidity 的类型安全性。

1
2
3
4
5
6
uint128 a = 1;
uint128 b = 2.5 + a + 0.5;

bytes32 samevar = "stringliteral";
string memory a = unicode"Hello 😃";
hex"0011223344556677"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.16 <0.8.0;

contract test {
enum ActionChoices { GoLeft, GoRight, GoStraight, SitStill }
ActionChoices choice;
ActionChoices constant defaultChoice = ActionChoices.GoStraight;

function setGoStraight() public {
choice = ActionChoices.GoStraight;
}

// 由于枚举类型不属于 |ABI| 的一部分,因此对于所有来自 Solidity 外部的调用,
// "getChoice" 的签名会自动被改成 "getChoice() returns (uint8)"。
// 整数类型的大小已经足够存储所有枚举类型的值,随着值的个数增加,
// 可以逐渐使用 `uint16` 或更大的整数类型。
function getChoice() public view returns (ActionChoices) {
return choice;
}

function getDefaultChoice() public pure returns (uint) {
return uint(defaultChoice);
}
}
1
function (<parameter types>) {internal|external} [pure|constant|view|payable] [returns (<return types>)]
  • 内存memory 即数据在内存中,因此数据仅在其生命周期内(函数调用期间)有效。不能用于外部调用。

  • 存储storage 状态变量保存的位置,只要合约存在就一直存储.

  • 调用数据calldata 用来保存函数参数的特殊数据位置,是一个只读位置。

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
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.5.0 <0.8.0;

contract Tiny {
uint[] x; // x 的数据存储位置是 storage, 位置可以忽略

// memoryArray 的数据存储位置是 memory
function f(uint[] memory memoryArray) public {
x = memoryArray; // 将整个数组拷贝到 storage 中,可行
uint[] storage y = x; // 分配一个指针(其中 y 的数据存储位置是 storage),可行
y[7]; // 返回第 8 个元素,可行
y.pop(); // 通过 y 修改 x,可行
delete x; // 清除数组,同时修改 y,可行

// 下面的就不可行了;需要在 storage 中创建新的未命名的临时数组,
// 但 storage 是“静态”分配的:
// y = memoryArray;
// 下面这一行也不可行,因为这会“重置”指针,
// 但并没有可以让它指向的合适的存储位置。
// delete y;

g(x); // 调用 g 函数,同时移交对 x 的引用
h(x); // 调用 h 函数,同时在 memory 中创建一个独立的临时拷贝
}

function g(uint[] storage ) internal pure {}
function h(uint[] memory) public pure {}
}
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
pragma solidity >=0.6.99 <0.8.0;

contract Proxy {
/// 被当前合约管理的 客户端合约地址
address client;

constructor(address _client) {
client = _client;
}

/// 在进行参数验证之后,转发到由client实现的 "setOwner(address)"
function forward(bytes calldata _payload) external {
// 由于 ABI 解码要求填充的数据(padded data)不能使用
// abi.decode(_payload[:4], (bytes4)).
bytes4 sig =
_payload[0] |
(bytes4(_payload[1]) >> 8) |
(bytes4(_payload[2]) >> 16) |
(bytes4(_payload[3]) >> 24);

if (sig == bytes4(keccak256("setOwner(address)"))) {
address owner = abi.decode(_payload[4:], (address));
require(owner != address(0), "Address of owner cannot be zero.");
}
(bool status,) = client.delegatecall(_payload);
require(status, "Forwarded call failed.");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.0 <0.8.0;

contract MappingExample {
mapping(address => uint) public balances;

function update(uint newBalance) public {
balances[msg.sender] = newBalance;
}
}

contract MappingLBC {
function f() public returns (uint) {
MappingExample m = new MappingExample();
m.update(100);
return m.balances(this);
}
}
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
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.22 <0.8.0;

contract MappingExample {

mapping (address => uint256) private _balances;
mapping (address => mapping (address => uint256)) private _allowances;

event Transfer(address indexed from, address indexed to, uint256 value);
event Approval(address indexed owner, address indexed spender, uint256 value);

function allowance(address owner, address spender) public view returns (uint256) {
return _allowances[owner][spender];
}

function transferFrom(address sender, address recipient, uint256 amount) public returns (bool) {
_transfer(sender, recipient, amount);
approve(sender, msg.sender, amount);
return true;
}

function approve(address owner, address spender, uint256 amount) public returns (bool) {
require(owner != address(0), "ERC20: approve from the zero address");
require(spender != address(0), "ERC20: approve to the zero address");

_allowances[owner][spender] = amount;
emit Approval(owner, spender, amount);
return true;
}

function _transfer(address sender, address recipient, uint256 amount) internal {
require(sender != address(0), "ERC20: transfer from the zero address");
require(recipient != address(0), "ERC20: transfer to the zero address");

_balances[sender] -= amount;
_balances[recipient] += amount;
emit Transfer(sender, recipient, amount);
}
}
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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.6.0 <0.8.0;

struct IndexValue { uint keyIndex; uint value; }
struct KeyFlag { uint key; bool deleted; }

struct itmap {
mapping(uint => IndexValue) data;
KeyFlag[] keys;
uint size;
}

library IterableMapping {
function insert(itmap storage self, uint key, uint value) internal returns (bool replaced) {
uint keyIndex = self.data[key].keyIndex;
self.data[key].value = value;
if (keyIndex > 0)
return true;
else {
keyIndex = self.keys.length;

self.keys.push();
self.data[key].keyIndex = keyIndex + 1;
self.keys[keyIndex].key = key;
self.size++;
return false;
}
}

function remove(itmap storage self, uint key) internal returns (bool success) {
uint keyIndex = self.data[key].keyIndex;
if (keyIndex == 0)
return false;
delete self.data[key];
self.keys[keyIndex - 1].deleted = true;
self.size --;
}

function contains(itmap storage self, uint key) internal view returns (bool) {
return self.data[key].keyIndex > 0;
}

function iterate_start(itmap storage self) internal view returns (uint keyIndex) {
return iterate_next(self, uint(-1));
}

function iterate_valid(itmap storage self, uint keyIndex) internal view returns (bool) {
return keyIndex < self.keys.length;
}

function iterate_next(itmap storage self, uint keyIndex) internal view returns (uint r_keyIndex) {
keyIndex++;
while (keyIndex < self.keys.length && self.keys[keyIndex].deleted)
keyIndex++;
return keyIndex;
}

function iterate_get(itmap storage self, uint keyIndex) internal view returns (uint key, uint value) {
key = self.keys[keyIndex].key;
value = self.data[key].value;
}
}

// 如何使用
contract User {
// Just a struct holding our data.
itmap data;
// Apply library functions to the data type.
using IterableMapping for itmap;

// Insert something
function insert(uint k, uint v) public returns (uint size) {
// This calls IterableMapping.insert(data, k, v)
data.insert(k, v);
// We can still access members of the struct,
// but we should take care not to mess with them.
return data.size;
}

// Computes the sum of all stored data.
function sum() public view returns (uint s) {
for (
uint i = data.iterate_start();
data.iterate_valid(i);
i = data.iterate_next(i)
) {
(, uint value) = data.iterate_get(i);
s += value;
}
}
}

映射本身是无法遍历的,即无法枚举所有的键。不过,可以在它们之上实现一个数据结构来进行迭代。 例如,以下代码实现了 IterableMapping 库,然后 User 合约可以添加数据, sum 函数迭代求和所有值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.0 <0.8.0;

contract DeleteLBC {
uint data;
uint[] dataArray;

function f() public {
uint x = data;
delete x; // 将 x 设为 0,并不影响数据
delete data; // 将 data 设为 0,并不影响 x,因为它仍然有个副本
uint[] storage y = dataArray;
delete dataArray;
// 将 dataArray.length 设为 0,但由于 uint[] 是一个复杂的对象,y 也将受到影响,
// 因为它是一个存储位置是 storage 的对象的别名。
// 另一方面:"delete y" 是非法的,引用了 storage 对象的局部变量只能由已有的 storage 对象赋值。
assert(y.length == 0);
}
}
1
2
3
4
5
6
7
8
9
10
uint8 y;
uint16 z;
uint32 x = y + z;
int8 y = -3;
uint x = uint(y);
uint32 a = 0x12345678;
uint16 b = uint16(a); // 此时 b 的值是 0x5678
uint16 a = 0x1234;
uint32 b = uint32(a); // b 为 0x00001234 now
assert(a == b);

单位和全局变量

以太币Ether 单位之间的换算就是在数字后边加上 weigweiether 来实现的,如果后面没有单位,缺省为 wei。例如 2 ether == 2000 finney 的逻辑判断值为 true

1
2
3
assert(1 wei == 1);
assert(1 gwei == 1e9);
assert(1 ether == 1e18);

这些后缀不能直接用在变量后边。如果想用时间单位(例如 days)来将输入变量换算为时间,你可以用如下方式来完成:

1
2
3
4
5
6
7
8
9
10
11
1 == 1 seconds
1 minutes == 60 seconds
1 hours == 60 minutes
1 days == 24 hours
1 weeks == 7 days

function f(uint start, uint daysAfter) public {
if (block.timestamp >= start + daysAfter * 1 days) {
// ...
}
}
1
这些编码函数可以用来构造函数调用数据,而不用实际进行调用。此外,keccak256(abi.encodePacked(a, b)) 是一种计算结构化数据的哈希值(尽管我们也应该关注到:使用不同的函数参数类型也有可能会引起“哈希冲突” )的方式,不推荐使用的 keccak256(a, b) 。

错误处理

可以参阅专门的章节 assert and require 参阅有关错误处理以及何时使用哪个函数的更多详细信息。

  • assert(bool condition)

    如果不满足条件,则会导致无效的操作码,则撤销状态更改 - 用于检查内部错误。

  • require(bool condition)

    如果条件不满足则撤销状态更改 - 用于检查由输入或者外部组件引起的错误。

  • require(bool condition, string memory message)

    如果条件不满足则撤销状态更改 - 用于检查由输入或者外部组件引起的错误,可以同时提供一个错误消息。

  • revert()

    终止运行并撤销状态更改。

  • revert(string memory reason)

    终止运行并撤销状态更改,可以同时提供一个解释性的字符串。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
pragma solidity >=0.5.0 <0.8.0;

contract C {
uint index;

function f() public pure returns (uint, bool, uint) {
return (7, true, 2);
}

function g() public {
//基于返回的元组来声明变量并赋值
(uint x, bool b, uint y) = f();
//交换两个值的通用窍门——但不适用于非值类型的存储 (storage) 变量。
(x, y) = (y, x);
//元组的末尾元素可以省略(这也适用于变量声明)。
(index,,) = f(); // 设置 index 为 7
}
}
1
2
3
4
5
6
7
8
9
10
11
12
pragma solidity >=0.5.0 <0.8.0;

contract Sharer {
function sendHalf(address addr) public payable returns (uint balance) {
require(msg.value % 2 == 0, "Even value required.");
uint balanceBeforeTransfer = this.balance;
addr.transfer(msg.value / 2);
//由于转移函数在失败时抛出异常并且不能在这里回调,因此我们应该没有办法仍然有一半的钱。
assert(this.balance == balanceBeforeTransfer - msg.value / 2);
return this.balance;
}
}

在内部, Solidity 对一个 require 式的异常执行回退操作(指令 0xfd )并执行一个无效操作(指令 0xfe )来引发 assert 式异常。 在这两种情况下,都会导致 EVM 回退对状态所做的所有更改。回退的原因是不能继续安全地执行,因为没有实现预期的效果。

在这两种情况下,调用者都可以使用 try/catch 来应对此类失败(在assert类型的异常中,仅在剩余足够gas的情况下才行 ),但是调用者中的更改将始终被还原。

请注意, assert 式异常消耗了所有可用的调用 gas ,而从 Metropolis 版本起 require 式的异常不会消耗任何 gas。 这里还是尽量使用require.

合约

Solidity 合约类似于面向对象语言中的类。合约中有用于数据持久化的状态变量,和可以修改状态变量的函数。 调用另一个合约实例的函数时,会执行一个 EVM 函数调用,这个操作会切换执行时的上下文,这样,前一个合约的状态变量就不能访问了。

可以通过以太坊交易“从外部”或从 Solidity 合约内部创建合约。

一些集成开发环境,例如 Remix, 通过使用一些UI用户界面使创建合约的过程更加顺畅。 在以太坊上通过编程创建合约最好使用 JavaScript API web3.js。 现在,我们已经有了一个叫做 web3.eth.Contract 的方法能够更容易的创建合约。

创建合约时, 合约的 构造函数 (一个用关键字 constructor 声明的函数)会执行一次。 构造函数是可选的。只允许有一个构造函数,这意味着不支持重载。

构造函数执行完毕后,合约的最终代码将部署到区块链上。此代码包括所有公共和外部函数以及所有可以通过函数调用访问的函数。 部署的代码没有 包括构造函数代码或构造函数调用的内部函数。

在内部,构造函数参数在合约代码之后通过 ABI 编码 传递,但是如果你使用 web3.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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
pragma solidity >=0.4.22 <0.8.0;

contract OwnedToken {
// TokenCreator 是如下定义的合约类型.
// 不创建新合约的话,也可以引用它。
TokenCreator creator;
address owner;
bytes32 name;

// 这是注册 creator 和设置名称的构造函数。
constructor(bytes32 _name) {
// 状态变量通过其名称访问,而不是通过例如 this.owner 的方式访问。
// 这也适用于函数,特别是在构造函数中,你只能像这样(“内部地”)调用它们,
// 因为合约本身还不存在。
owner = msg.sender;
// 从 `address` 到 `TokenCreator` ,是做显式的类型转换
// 并且假定调用合约的类型是 TokenCreator,没有真正的方法来检查这一点。
creator = TokenCreator(msg.sender);
name = _name;
}

function changeName(bytes32 newName) public {
// 只有 creator (即创建当前合约的合约)能够更改名称 —— 因为合约是隐式转换为地址的,
// 所以这里的比较是可行的。
if (msg.sender == address(creator))
name = newName;
}

function transfer(address newOwner) public {
// 只有当前所有者才能发送 token。
if (msg.sender != owner) return;
// 我们也想询问 creator 是否可以发送。
// 请注意,这里调用了一个下面定义的合约中的函数。
// 如果调用失败(比如,由于 gas 不足),会立即停止执行。
if (creator.isTokenTransferOK(owner, newOwner))
owner = newOwner;
}
}

contract TokenCreator {
function createToken(bytes32 name)
public
returns (OwnedToken tokenAddress)
{
// 创建一个新的 Token 合约并且返回它的地址。
// 从 JavaScript 方面来说,返回类型是简单的 `address` 类型,因为
// 这是在 ABI 中可用的最接近的类型。
return new OwnedToken(name);
}

function changeName(OwnedToken tokenAddress, bytes32 name) public {
// 同样,`tokenAddress` 的外部类型也是 `address` 。
tokenAddress.changeName(name);
}

function isTokenTransferOK(address currentOwner, address newOwner)
public
view
returns (bool ok)
{
// 检查一些任意的情况。
address tokenAddress = msg.sender;
return (keccak256(newOwner) & 0xff) == (bytes20(tokenAddress) & 0xff);
}
}

由于 Solidity 有两种函数调用(内部调用不会产生实际的 EVM 调用或称为“消息调用”,而外部调用则会产生一个 EVM 调用), 函数和状态变量有四种可见性类型。 函数可以指定为 externalpublicinternal 或者 private。 对于状态变量,不能设置为 external ,默认是 internal

1
external

外部函数作为合约接口的一部分,意味着我们可以从其他合约和交易中调用。 一个外部函数 f 不能从内部调用(即 f 不起作用,但 this.f() 可以)。 当收到大量数据的时候,外部函数有时候会更有效率,因为数据不会从calldata复制到内存.

1
public

public 函数是合约接口的一部分,可以在内部或通过消息调用。对于 public 状态变量, 会自动生成一个 getter 函数(见下面)。

1
internal

这些函数和状态变量只能是内部访问(即从当前合约内部或从它派生的合约访问),不使用 this 调用。

1
private

private 函数和状态变量仅在当前定义它们的合约中使用,并且不能被派生合约使用。

合约中的所有内容对外部观察者都是可见的。设置一些 private 类型只能阻止其他合约访问和修改这些信息, 但是对于区块链外的整个世界它仍然是可见的。

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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
// SPDX-License-Identifier: GPL-3.0
pragma solidity >0.7.0 <0.8.0;

contract owned {
constructor() { owner = msg.sender; }

address owner;

// 这个合约只定义一个修改器,但并未使用: 它将会在派生合约中用到。
// 修改器所修饰的函数体会被插入到特殊符号 _; 的位置。
// 这意味着如果是 owner 调用这个函数,则函数会被执行,否则会抛出异常。
modifier onlyOwner {
require(
msg.sender == owner,
"Only owner can call this function."
);
_;
}
}

contract destructible is owned {
// 这个合约从 `owned` 继承了 `onlyOwner` 修饰符,并将其应用于 `destroy` 函数,
// 只有在合约里保存的 owner 调用 `destroy` 函数,才会生效。
function destroy() public onlyOwner {
selfdestruct(owner);
}
}

contract priced {
// 修改器可以接收参数:
modifier costs(uint price) {
if (msg.value >= price) {
_;
}
}
}

contract Register is priced, destructible {
mapping (address => bool) registeredAddresses;
uint price;

constructor(uint initialPrice) { price = initialPrice; }

// 在这里也使用关键字 `payable` 非常重要,否则函数会自动拒绝所有发送给它的以太币。
function register() public payable costs(price) {
registeredAddresses[msg.sender] = true;
}

function changePrice(uint _price) public onlyOwner {
price = _price;
}
}

contract Mutex {
bool locked;
modifier noReentrancy() {
require(
!locked,
"Reentrant call."
);
locked = true;
_;
locked = false;
}

// 这个函数受互斥量保护,这意味着 `msg.sender.call` 中的重入调用不能再次调用 `f`。
// `return 7` 语句指定返回值为 7,但修改器中的语句 `locked = false` 仍会执行。
function f() public noReentrancy returns (uint) {
(bool success,) = msg.sender.call("");
require(success);
return 7;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// SPDX-License-Identifier: GPL-3.0
pragma solidity >0.6.99 <0.8.0;

contract C {
uint constant X = 32**22 + 8;
string constant TEXT = "abc";
bytes32 constant MY_HASH = keccak256("abc");
uint immutable decimals;
uint immutable maxBalance;
address immutable owner = msg.sender;

constructor(uint _decimals, address _reference) {
decimals = _decimals;
// Assignments to immutables can even access the environment.
maxBalance = _reference.balance;
}

function isBalanceTooHigh(address _other) public view returns (bool) {
return _other.balance > maxBalance;
}
}

如果状态变量声明为 constant (常量)。在这种情况下,只能使用那些在编译时有确定值的表达式来给它们赋值。 任何通过访问 storage,区块链数据(例如 block.timestamp, address(this).balance 或者 block.number)或执行数据( msg.valuegasleft() ) 或对外部合约的调用来给它们赋值都是不允许的。

允许可能对内存分配产生副作用(side-effect)的表达式,但那些可能对其他内存对象产生副作用的表达式则不允许。

内建(built-in)函数 keccak256sha256ripemd160ecrecoveraddmodmulmod 是允许的(即使他们确实会调用外部合约, keccak256 除外)。

允许内存分配器的副作用的原因是它可以构造复杂的对象,例如: 查找表(lookup-table)。 此功能尚不完全可用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// SPDX-License-Identifier: GPL-3.0
pragma solidity >0.8.0 <0.8.0;

function sum(uint[] memory _arr) pure returns (uint s) {
for (uint i = 0; i < _arr.length; i++)
s += _arr[i];
}

contract ArrayExample {
bool found;
function f(uint[] memory _arr) public {
// This calls the free function internally.
// The compiler will add its code to the contract.
uint s = sum(_arr);
require(s >= 10);
found = true;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
pragma solidity >=0.4.16 <0.8.0;

contract Simple {
uint sum;
function taker(uint _a, uint _b) public {
sum = _a + _b;
}
}

pragma solidity >=0.4.16 <0.8.0;

contract Simple {
function arithmetic(uint _a, uint _b)
public
pure
returns (uint o_sum, uint o_product)
{
o_sum = _a + _b;
o_product = _a * _b;
}
}
1
2
3
4
5
6
7
pragma solidity  >=0.5.0 <0.8.0;

contract C {
function f(uint a, uint b) public view returns (uint) {
return a * (b + 42) + block.timestamp;
}
}

Getter 方法自动被标记为 viewconstant 之前是 view 的别名,不过在0.5.0之后移除了。函数可以声明为 pure ,在这种情况下,承诺不读取也不修改状态。

一个合约最多有一个 receive 函数, 声明函数为: receive() external payable { ... }

不需要 function 关键字,也没有参数和返回值并且必须是 external 可见性和 payable 修饰. 在对合约没有任何附加数据调用(通常是对合约转账)是会执行 receive 函数. 例如 通过 .send() or .transfer() 如果 receive 函数不存在, 但是有payable 的 fallback 回退函数 那么在进行纯以太转账时,fallback 函数会调用.

如果两个函数都没有,这个合约就没法通过常规的转账交易接收以太(会抛出异常).

更糟的是,fallback函数可能只有 2300 gas 可以使用(如,当使用 sendtransfer 时), 除了基础的日志输出之外,进行其他操作的余地很小。下面的操作消耗会操作 2300 gas :

  • 写入存储

  • 创建合约

  • 调用消耗大量 gas 的外部函数

  • 发送以太币

一个没有定义 fallback 函数或  receive 函数的合约,直接接收以太币(没有函数调用,即使用 sendtransfer)会抛出一个异常, 并返还以太币(在 Solidity v0.4.0 之前行为会有所不同)。 所以如果你想让你的合约接收以太币,必须实现receive函数(使用 payable fallback 函数不再推荐,因为它会让借口混淆)。 这个之前调试老版本的合约中出现过异常的情况。

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
47
48
49
50
51
52
pragma solidity >=0.6.2 <0.8.0;

contract Test {
// 发送到这个合约的所有消息都会调用此函数(因为该合约没有其它函数)。
// 向这个合约发送以太币会导致异常,因为 fallback 函数没有 `payable` 修饰符
fallback() external { x = 1; }
uint x;
}


// 这个合约会保留所有发送给它的以太币,没有办法返还。
contract TestPayable {
// 除了纯转账外,所有的调用都会调用这个函数.
// (因为除了 receive 函数外,没有其他的函数).
// 任何对合约非空calldata 调用会执行回退函数(即使是调用函数附加以太).
fallback() external payable { x = 1; y = msg.value; }

// 纯转账调用这个函数,例如对每个空empty calldata的调用
receive() external payable { x = 2; y = msg.value; }
uint x;
uint y;
}

contract Caller {
function callTest(Test test) public returns (bool) {
(bool success,) = address(test).call(abi.encodeWithSignature("nonExistingFunction()"));
require(success);
// test.x 结果变成 == 1。

// address(test) 不允许直接调用 ``send`` , 因为 ``test`` 没有 payable 回退函数
// 转化为 ``address payable`` 类型 , 然后才可以调用 ``send``
address payable testPayable = payable(address(test));


// 以下将不会编译,但如果有人向该合约发送以太币,交易将失败并拒绝以太币。
// test.send(2 ether);
}

function callTestPayable(TestPayable test) public returns (bool) {
(bool success,) = address(test).call(abi.encodeWithSignature("nonExistingFunction()"));
require(success);
// 结果 test.x 为 1 test.y 为 0.
(success,) = address(test).call{value: 1}(abi.encodeWithSignature("nonExistingFunction()"));
require(success);
// 结果test.x 为1 and test.y 为 1.

// 发送以太币, TestPayable 的 receive 函数被调用.
require(address(test).send(2 ether));
// 结果 in test.x 为 2 and test.y 为 2 ether.
}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
var options = {
fromBlock: 0,
address: web3.eth.defaultAccount,
topics: ["0x0000000000000000000000000000000000000000000000000000000000000000", null, null]
};
web3.eth.subscribe('logs', options, function (error, result) {
if (!error)
console.log(result);
})
.on("data", function (log) {
console.log(log);
})
.on("changed", function (log) {
});

关于web3 subscribe的描述

1
2
3
4
5
6
7
8
9
10
11
12
13
pragma solidity >=0.4.10 <0.8.0;

contract C {
function f() public payable {
bytes32 _id = 0x420042;
log3(
bytes32(msg.value),
bytes32(0x50cb9fe53daa9737b786ab3646f04d0150dc50ef4e75f59509d83667ad5adb20),
bytes32(msg.sender),
_id
);
}
}

日志的底层接口

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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
   // SPDX-License-Identifier: GPL-3.0
pragma solidity >0.6.99 <0.8.0;

contract Owned {
constructor() public { owner = msg.sender; }
address payable owner;
}

// 使用 is 从另一个合约派生。派生合约可以访问所有非私有成员,包括内部(internal)函数和状态变量,
// 但无法通过 this 来外部访问。
contract Destructible is Owned {

// 关键字`virtual`表示该函数可以在派生类中“overriding”。

function destroy() virtual public {
if (msg.sender == owner) selfdestruct(owner);
}
}

// 这些抽象合约仅用于给编译器提供接口。
// 注意函数没有函数体。
// 如果一个合约没有实现所有函数,则只能用作接口。
abstract contract Config {
function lookup(uint id) public virtual returns (address adr);
}

abstract contract NameReg {
function register(bytes32 name) public virtual;
function unregister() public virtual;
}

// 可以多重继承。请注意,owned 也是 Destructible 的基类,
// 但只有一个 owned 实例(就像 C++ 中的虚拟继承)。
contract Named is Owned, Destructible {
constructor(bytes32 name) {
Config config = Config(0xD5f9D8D94886E70b06E474c3fB14Fd43E2f23970);
NameReg(config.lookup(1)).register(name);
}

// 函数可以被另一个具有相同名称和相同数量/类型输入的函数重载。
// 如果重载函数有不同类型的输出参数,会导致错误。
// 本地和基于消息的函数调用都会考虑这些重载。

//如果要覆盖函数,则需要使用 `override` 关键字。 如果您想再次覆盖此函数,则需要再次指定`virtual`关键字。

function destroy() public virtual override {
if (msg.sender == owner) {
Config config = Config(0xD5f9D8D94886E70b06E474c3fB14Fd43E2f23970);
NameReg(config.lookup(1)).unregister();
// 仍然可以调用特定的重载函数。
Destructible.destroy();
}
}
}

// 如果构造函数接受参数,
// 则需要在声明(合约的构造函数)时提供,
// 或在派生合约的构造函数位置以修改器调用风格提供(见下文)。
contract PriceFeed is Owned, Destructible, Named("GoldFeed") {
function updateInfo(uint newInfo) public {
if (msg.sender == owner) info = newInfo;
}

// Here, we only specify `override` and not `virtual`.
// This means that contracts deriving from `PriceFeed`
// cannot change the behaviour of `destroy` anymore.
function destroy() public override(Destructible, Named) { Named.destroy(); }

function get() public view returns(uint r) { return info; }

uint info;
}

父合约标记为 virtual 函数可以在继承合约里重写(overridden)以更改他们的行为。重写的函数需要使用关键字 override 修饰。

重写函数只能将覆盖函数的可见性从 external 更改为 public

可变性可以按照以下顺序更改为更严格的一种: nonpayable 可以被 viewpure 覆盖。 view 可以被 pure 覆盖。 payable 是一个例外,不能更改为任何其他可变性。

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
// SPDX-License-Identifier: GPL-3.0
pragma solidity >0.6.99 <0.8.0;

contract Base
{
function foo() virtual external view {}
}

contract Middle is Base {}

contract Inherited is Middle
{
function foo() override public pure {}
}





pragma solidity >=0.6.0 <0.8.0;

contract Base1
{
function foo() virtual public {}
}

contract Base2
{
function foo() virtual public {}
}

contract Inherited is Base1, Base2
{
// 继承自两个基类合约定义的foo(), 必须显示的指定 override
function foo() public override(Base1, Base2) {}
}

接口类似于抽象合约,但是它们不能实现任何函数。还有进一步的限制:

  • 无法继承其他合约,不过可以继承其他接口。
  • 所有的函数都需要是 external
  • 无法定义构造函数。
  • 无法定义状态变量。

将来可能会解除这里的某些限制。

接口基本上基本上仅限于合约 ABI 可以表示的内容,并且 ABI 和接口之间的转换应该不会丢失任何信息。

接口由它们自己的关键字表示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
pragma solidity >=0.6.2 <0.8.0;

interface Token {
enum TokenType { Fungible, NonFungible }
struct Coin { string obverse; string reverse; }
function transfer(address recipient, uint amount) external;
}

//-------
pragma solidity >=0.6.2 <0.8.0;

interface ParentA {
function test() external returns (uint256);
}

interface ParentB {
function test() external returns (uint256);
}

interface SubInterface is ParentA, ParentB {
// 必须重新定义 test 函数,以表示兼容父合约含义
function test() external override(ParentA, ParentB) returns (uint256);
}

就像继承其他合约一样,合约可以继承接口。接口中的函数都会隐式的标记为 virtual ,意味着他们会被重写。 但是不表示重写(overriding)函数可以再次重写,仅仅当重写的函数标记为 virtual 才可以再次重写。

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
47
48
49
50
pragma solidity >=0.6.0 <0.7.0;

// 我们定义了一个新的结构体数据类型,用于在调用合约中保存数据。
struct Data {
mapping(uint => bool) flags;
}

library Set {

// 注意第一个参数是“storage reference”类型,因此在调用中参数传递的只是它的存储地址而不是内容。
// 这是库函数的一个特性。如果该函数可以被视为对象的方法,则习惯称第一个参数为 `self` 。
function insert(Data storage self, uint value)
public
returns (bool)
{
if (self.flags[value])
return false; // 已经存在
self.flags[value] = true;
return true;
}

function remove(Data storage self, uint value)
public
returns (bool)
{
if (!self.flags[value])
return false; // 不存在
self.flags[value] = false;
return true;
}

function contains(Data storage self, uint value)
public
view
returns (bool)
{
return self.flags[value];
}
}

contract C {
Data knownValues;

function register(uint value) public {
// 不需要库的特定实例就可以调用库函数,
// 因为当前合约就是“instance”。
require(Set.insert(knownValues, value));
}
// 如果我们愿意,我们也可以在这个合约中直接访问 knownValues.flags。
}

这个我们没咋个看到过

合约元数据

Solidity编译器自动生成JSON文件,即合约的元数据,其中包含了当前合约的相关信息。 它可以用于查询编译器版本,所使用的源代码,|ABI||natspec| 文档,以便更安全地与合约进行交互并验证其源代码。

编译器会将元数据文件的 Swarm 哈希值附加到每个合约的字节码末尾(详情请参阅下文), 以便你可以以认证的方式获取该文件,而不必求助于中心化的数据提供者。

当然,你必须将元数据文件发布到 Swarm (或其他服务),以便其他人可以访问它。 该文件可以通过使用 solc --metadata 来生成,并被命名为 ContractName_meta.json 。 它将包含源代码的在 Swarm 上的引用,因此你必须上传所有源文件和元数据文件。

元数据文件具有以下格式。 下面的例子将以人类可读的方式呈现。 正确格式化的元数据应正确使用引号,将空白减少到最小,并对所有对象的键值进行排序以得到唯一的格式。 代码注释当然也是不允许的,这里仅用于解释目的。

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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
{
// 必选:元数据格式的版本
version: "1",
// 必选:源代码的编程语言,一般会选择规范的“子版本”
language: "Solidity",
// 必选:编译器的细节,内容视语言而定。
compiler: {
// 对 Solidity 来说是必须的:编译器的版本
version: "0.4.6+commit.2dabbdf0.Emscripten.clang",
// 可选: 生成此输出的编译器二进制文件的哈希值
keccak256: "0x123..."
},
// 必选:编译的源文件/源单位,键值为文件名
sources:
{
"myFile.sol": {
// 必选:源文件的 keccak256 哈希值
"keccak256": "0x123...",
// 必选(除非定义了“content”,详见下文):
// 已排序的源文件的URL,URL的协议可以是任意的,但建议使用 Swarm 的URL
"urls": [ "bzzr://56ab..." ]
// Optional: 在源文件中定义的 SPDX license 标识
"license": "MIT"
},
"mortal": {
// 必选:源文件的 keccak256 哈希值
"keccak256": "0x234...",
// 必选(除非定义了“urls”): 源文件的字面内容
"content": "contract mortal is owned { function kill() { if (msg.sender == owner) selfdestruct(owner); } }"
}
},
// 必选:编译器的设置
settings:
{
// 对 Solidity 来说是必须的: 已排序的重定向列表
remappings: [ ":g/dir" ],
// 可选: 优化器的设置( enabled 默认设为 false )
optimizer: {
enabled: true,
runs: 500,
details: {
// peephole defaults to "true"
peephole: true,
// jumpdestRemover defaults to "true"
jumpdestRemover: true,
orderLiterals: false,
deduplicate: false,
cse: false,
constantOptimizer: false,
yul: true,
// Optional: Only present if "yul" is "true"
yulDetails: {
stackAllocation: false,
optimizerSteps: "dhfoDgvulfnTUtnIf..."
}
}
}
},
metadata: {
// Reflects the setting used in the input json, defaults to false
useLiteralContent: true,
// Reflects the setting used in the input json, defaults to "ipfs"
bytecodeHash: "ipfs"
}
// Required for Solidity: File and name of the contract or library this
// metadata is created for.
compilationTarget: {
"myFile.sol": "MyContract"
},
// Required for Solidity: Addresses for libraries used
libraries: {
"MyLib": "0x123123..."
}
},
// 必选:合约的生成信息
output:
{
// 必选:合约的 ABI 定义
abi: [ ... ],
// 必选:合约的 NatSpec 用户文档
userdoc: [ ... ],
// 必选:合约的 NatSpec 开发者文档
devdoc: [ ... ],
}
}

由于生成的合约的字节码包含元数据的哈希值,因此对元数据的任何更改都会导致字节码的更改。 此外,由于元数据包含所有使用的源代码的哈希值,所以任何源代码中的, 哪怕是一个空格的变化都将导致不同的元数据,并随后产生不同的字节代码。需注意,上面的 ABI 没有固定的顺序,随编译器的版本而不同。尽管从 Solidity 0.5.12 开始,数组保持了一定的顺序。

源代码验证方法

为了验证编译,可以通过元数据文件中的链接从 Swarm 中获取源代码。 获取到的源码,会根据元数据中指定的设置,被正确版本的编译器(应该为“官方”编译器之一)所处理。 处理得到的字节码会与创建交易的数据或者 CREATE 操作码使用的数据进行比较。 这会自动验证元数据,因为它的哈希值是字节码的一部分。 而额外的数据,则是与基于接口进行编码并展示给用户的构造输入数据相符的。

ABI

在 以太坊Ethereum 生态系统中, 应用二进制接口Application Binary Interface(ABI) 是从区块链外部与合约进行交互以及合约与合约间进行交互的一种标准方式。 数据会根据其类型按照这份手册中说明的方法进行编码。这种编码并不是可以自描述的,而是需要一种特定的概要(schema)来进行解码。

除了 元组tuple 以外,Solidity 支持以上所有类型的名称。ABI 元组tuple 是利用 Solidity 的 structs 编码得到的。

1
2
3
4
5
6
7
pragma solidity ^0.4.16;

contract Foo {
function bar(bytes3[2]) public pure {}
function baz(uint32 x, bool y) public pure returns (bool r) { r = x > 32 || y; }
function sam(bytes, bool, uint[]) public pure {}
}

这样,对于我们的例子 Foo,如果我们想用 69true 做参数调用 baz,我们总共需要传送 68 字节,可以分解为:

  • 0xcdcd77c0:方法ID。这源自ASCII格式的 baz(uint32,bool) 签名的 Keccak 哈希的前 4 字节。
  • 0x0000000000000000000000000000000000000000000000000000000000000045:第一个参数,一个被用 0 值字节补充到 32 字节的 uint32 值 69
  • 0x0000000000000000000000000000000000000000000000000000000000000001:第二个参数,一个被用 0 值字节补充到 32 字节的 boolean 值 true

合起来就是:

1
0xcdcd77c000000000000000000000000000000000000000000000000000000000000000450000000000000000000000000000000000000000000000000000000000000001

它返回一个 bool。比如它返回 false,那么它的输出将是一个字节数组 0x0000000000000000000000000000000000000000000000000000000000000000,一个bool值。

事件,是 以太坊Ethereum 的日志/事件监视协议的一个抽象。日志项提供了合约的地址、一系列的主题(最高 4 项)和一些任意长度的二进制数据。为了使用合适的类型数据结构来演绎这些功能(与接口定义一起),事件沿用了既存的 ABI 函数。

给定了事件名称和事件参数之后,我们将其分解为两个子集:已索引的和未索引的。已索引的部分,最多有 3 个,被用来与事件签名的 Keccak 哈希一起组成日志项的主题。未索引的部分就组成了事件的字节数组。

一个事件描述是一个有极其相似字段的 JSON 对象:

  • type:总是 "event"

  • name:事件名称;

  • 1
    inputs

    :对象数组,每个数组对象会包含:

    • name:参数名称;
    • type:参数的权威类型(相见下文);
    • components:供 元组tuple 类型使用(详见下文);
    • indexed:如果此字段是日志的一个主题,则为 true;否则为 false
  • anonymous:如果事件被声明为 anonymous,则为 true

1
2
3
4
5
6
7
8
9
10
// SPDX-License-Identifier: GPL-3.0
pragma solidity >0.6.99 <0.8.0;

contract Test {
constructor () { b = 0x12345678901234567890123456789012; }
event Event(uint indexed a, bytes32 b);
event Event2(uint indexed a, bytes32 b);
function foo(uint a) public { Event(a, b); }
bytes32 b;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
[{
"type":"event",
"inputs": [{"name":"a","type":"uint256","indexed":true},{"name":"b","type":"bytes32","indexed":false}],
"name":"Event"
}, {
"type":"event",
"inputs": [{"name":"a","type":"uint256","indexed":true},{"name":"b","type":"bytes32","indexed":false}],
"name":"Event2"
}, {
"type":"function",
"inputs": [{"name":"a","type":"uint256"}],
"name":"foo",
"outputs": []
}]

结语

Solidity的合约开始真正的意义上的普及还需要一些时间,但是对于真正的使用,还是相当的有意思。

接下来的使用中,在一步一步更新这个文档,感觉solidity蛮有意思的。

坚持原创技术分享,您的支持将鼓励xinqiyang继续创作!