第三章:以太坊智能合约单位、表达式、全局变量及函数、控制结构
以太坊智能合约单位、表达式、全局变量及函数、控制结构
Solidity 中的单位
货币单位(Ether Unit)
一个数字常量(字面量) 后面跟随一个后缀 wei, finney、 szabo 或 ether, 这个后缀就是货币单位。
• 1 ether == 10^3 finney == 1000 finney
• 1 ether == 10^6 szabo
• 1 ether == 10^18 wei
时间单位( Time Unit)
seconds、minutes、hours、days、weeks、years
由于无法预测闰秒,所以必须由外部的预言 Coracle)来更新从而得到一个精确的日历库。
如果在合约中需要使用年、月、日等时间单位,则可以引入 DateTime 库。 比如可以通过时间戳解析出对应的年、月、日信息,DateTime 库的 GitHub 地址为 https://github.com/pipermerriam/ethereum-datetime。
Solidity 全局变量及函数
区块和交易的属性
Solidity 中有一些全局变量用来提供区块(或链)当前的信息。
- blockhash (uint blockNumber) returns (bytes32):返回给定区块号的哈希值,只支持最近的 256 个区块且不包含当前区块。 在 Solidity 0.4.22 之前这个属性是 block.Blockhash(uint blockNumber)。
- block.coinbase (address): 当前块矿工的地址,括号中表示返回值的类型。
- block.difficulty (uint): 当前块的难度。
- block.gaslimit (uint): 当前块的 gaslimit。
- block.number (uint): 当前区块的块号。
- block.timestamp (uint): 当前块的 Unix 时间戳(从 1970/1/1 00:00:00 UTC 开始所经过的秒数)。
- gasleft() (uint256): 获取剩余 gas。
- msg.data (bytes): 完整地调用数(calldata)。
- msg.gas (uint): 当前还剩的 gas。
- msg.sender (address): 当前调用发起人的地址。
- msg.sig (bytes4): 调用数据(calldata)的前四个字节(例如,函数标识 符)。
- msg.value (uint): 这个消息所附带的以太币, 单位为 wei。
- now (uint): 当前块的时间戳(block.timestamp 的别名)。
- tx.gasprice (uint): 交易的 gas 价格。
- tx.origin (address): 交易的发送者(全调用链)。
**msg 的所有成员值,如 msg.sender、 msg.value 的值可以因为每一次外部函数调用或库函数调用发生变化(因为 msg 就是和调用相关的全局变量)。对于同一个链上连续的区块来说, 当前区块的时间戳会大于上一个区块的时间戳。 为了可扩展性的原因,只能查最近的 256 个块, 其他的将返回 0。
**
ABI 编码函数
- abi.encode(...) returns (bytes): 计算参数的 ABI 编码。
- abi.encodePacked(...) returns (bytes): 计算参数的紧密打包编码。
- abi. encodeWithSelector(bytes4 selector, ... )returns (bytes): 计算函数选择器和参数的 ABI 编码。
- abi.encodeWithSignature(string signature, ... ) returns (bytes): 等价于abi.encodeWithSelector(bytes4(keccak256(signature ), ...)。
错误处理函数
- assert (bool condition): 用于判断内部错误, 条件不满足时抛出异常。
- require (bool condition): 用于判断输入或外部组件错误,条件不满足 时抛出异常。
- require(bool condition, string message): 同上, 只是多提供了一个错误信息。
- revert():终止执行并还原改变的状态。
- revert(string reason): 同上,只是多提供了一个错误信息。
数学及加密功能
Solidity 提供的有数学与加密功能的函数有:
- addmod(uint x, uint y, uint k) returns (uint): 计算(x + y) % k,加法支持任 意的精度且不会在2**256 处溢出,从 Solidity 0.5.0版本开始断言 k != 0。
- mulmod(uint x, uint y, uint k) returns (uint): 计算(x * y) % k,乘法支持任意的精度且不会在 2**256 处溢出,从 Solidity 0.5.0 版本开始断言 k!= 0。
- keccak256(...) returns (bytes32): 使用 Keccak-256 计算 hash 值,为紧密打包参数。
- sha256( ... ) returns (bytes32): 使用 SHA-256 计算 hash 值,为紧密打包参数。
- sha3( ... ) returns (bytes32): keccak256 的别名。
- ripemd160( ... ) returns (bytes20): 使用 RJPEMD-160 计算 hash 值,为紧密打包参数。
- ecrecover(bytes32 hash, uint8 v, bytes32 r, bytes32 s) returns (address): 通过椭圆曲线签名来恢复与公钥关联的地址,或者在错误时返回零。 可用于签名数据的校验,如果返回结果是签名者的公匙地址,那么说明数据是正确的。
ecrecover 函数需要四个参数,需要被签名数据的晗希结果值, r、 5、 v 分别来 自签名结果串。
r= signature[0:64]
s= signature[64:128]
v= signature[128:130] 其中 v 取出来的值或者是 00 或者是 01。 要使用时,我们先要将其转为整型, 再加上 27 ,所以我们将得到 27 或 28。 在调用函数时 v 将填入 27 或 28。
在私链(private blockchain)上运行 sha256、 ripemd160 或 ecrecover 可能会出现 out-of-gas 报错。 因为私链实现了一种预编译合约,合约要在收到第一个消息后才会真正存在(虽然他们的合约代码是硬编码的)。 而向一个不存在的合约 发送消息,是导致 out-of-gas 问题的原因。 另一种解决办法(work around)是, 在你真正使用它们之前先发送 1 wei 到这些合约上来完成初始化。在官方和测 试链上没有 out-of-gas 问题。
相关属性和函数
- .balance (uint256): address 的余额,以 wei 为单位。
- .transfer(uint256 amount): 发送给定数量的 Ether 到某个地址, 以 wei 为单位。 失败时抛出异常。
- .send(uint256 amount) returns (bool): 发送给定数量的 Ether 到 某个地址,以 wei 为单位,失败时返回 false。
- .call( ... ) returns (bool): 发起底层的 call 调用, 参数为函数选择 器。 失败时返回 false。
- .callcode( ... ) returns (bool): 发起底层的 callcode 调用, 参数为函数选择器,失败时返回 false。 不鼓励使用,未来可能会移除。
- .delegatecall( ... ) returns (bool): 发起底层的委托调用 delegatecall 调用,参数为函数选择器, 失败时返回 false。由于委托调 用的被调合约是在当前合约的环境下执行的,如果我们不知道被调合约具体做了什么事,委托调用将是一个很危险的操作。
**注意: 执行 send() 函数有一些风险。 如果调用栈的深度超过 1024 或 gas 耗光,交易都会失败。如果交易失败,会退回以太币 。而 transfer()在失败时会抛出异常。 实际上 addrA. transfer(addrB)和 require(addrA.send(y))是等价的。 **。
相关属性和函数
合约相关属性和函数有:
- this(当前合约的类型): 表示当前合约可以显式地转换为 Address。
- selfdestruct(address recipient): 销毁当前合约(也就是在第 2 章介绍的 "自毁" ),并把它的所有资金发送到给定的地址。
- suicide(address recipient): selfdestruct 的别名。
另外,当前合约里的所有自定义的函数均可支持调用 ,包括当前函数本身。 来看一个例子,了解如何使用 selfdestruct():
pragma solid ^0.4.2;
contract Steal {
address owner;
function Steal() {
owner = msg.sender;
}
function kill() { // 这是销毁的标准做法
if (msg.sender == owner) {
selfdestruct(owner);
}
}
function innocence() {
selfdestruct(owner); // 销毁合约
}
}
contract Mark {
function Deposit() payable {}
function call(address a) {
a.delegatecall(byte4(sha3("innocence()")));
}
}
在这段代码中, Steal 合约有一个 innocence 方法,这个函数虽然命名为“清白 ”,但如果 Mark 通过委托调用 innocence 方法,则 Mark 会把自己销毁掉,并且把存款发送到 Steal 合约的创建者那里。 所以我们在委托调用其他合约的函数 时, 一定要特别小心。
Solidity 表达式及控制结构
控制结构
注意,在 Solidity 中没有像 C 和JavaScript 那样从非布尔类型到布尔类型的转换, 因此不能在条件语句中使用非布尔类型, 注意:所以 if(1){...}在 Solidity 中是不合法的。
函数调用表达式
内部函数调用(Internal Function Call)
内部函数调用不会创建 EVM 消息调用 。 仅仅是在同一个合约的函数之间才可以通过内部函 数调用的方式进行调用。
外部函数调用(External Function Call)
表达式的形式为: this.g(8);和 c.g(2); (这里的 c 是一个合约实例)通过 this 或使用一个合约实例来调用是外部调用函数 的方式。它是一个消息调用,而不是 EVM 的指令跳转。 在合约的构造器中不能使用 this 来调用函数,其他合约的函数必须通过外部的方式调用 。 对于一个外部函数调用,所有函数的参数必须拷贝到内存中。可以通过选项.value()和.gas()来分别指定要发送 的以太币(以 wei 为单位)和 gas 值。
pragma solidity ^0.4.0;
contract InfoFeed {
function info() public payable returns (uint ret) {
return 42;
}
}
contract Consumer {
InfoFeed feed;
function setFeed(address addr) public {
feed = InfoFeed(addr);
}
function callFeed() public {
feed.info.value(10).gas(800)();
}
}
表达式 InfoFeed(addr)进行了一个显示的类型转换,表示给定的地址是合约 lnfoFeed 类型。
注意 feed.info.value(10).gas(800)仅仅是对发送的以太币和 gas 值进行了设置。调用callFeed 时, 需要预先存入一定量的以太币,否则会因余额不足报错。
如果我们不知道被调用的合约源代码,那么和它们交互会有潜在的风险。 虽然被调用的合约继承自一个已知的父合约 (继承仅仅要求正确实现接口,而不关注实现的内容),但是和他们交互就相当于把自己的控制权交给了被调用的合约,所以对方可以利用它做任何事。 此外,被调用的合约可以改变调用合约的状态变量( state variable ),在编写函数时我们要注意可重入性漏洞问题。
赋值表达式
解构及返回多个值
它是由一个数量固定、类型可以不同的元 素组成的一个列表。
变量声明与作用范围
函数内定义的变量,其作用域是整个函数,不管它定义的位置。 因为 Solid即使用了JavaScript 的变量作用域的规则。
错误处理
什么是错误处理
Solidity 是通过回退状态的方式来处理错误的。发生异常时会撤销当前调用(及其所有子调用)所改变的状态。 注意捕捉异常是不可能的, 因此没有 try ... catch...。
我们可以把区块链理解为全球共享的分布式事务性数据库。 全球共享意味着参与这个网络的每一个人都可以读写其中的记录。 如果想修改这个数据库中的内容,就必须创建一个事务。 事务意味着要做的修改只能被全部应用, Solidity 错误处理就是要保证每次 调用都是事务性的。
如何处理
Solidiy 提供了两个函数 assert 和 require 来进行条件检查。 assert函数通常用来检查(测试)内部错误,而 require 函数用来检查输入变量或合同状态变量 是否满足条件,以及验证调用外部合约的返回值。 Solidity 分析工具(如实现中的 SMTChecker)就可以帮我们分析出智能合约中的错误。
除可以用两个函数 assert 和 require 来进行条件检查以外,还有两种方式可以触发异常:
- revert 函数可以用来标记错误并回退当前调用,当前剩余的 gas 会返回 给调用者。
- 使用 throw 关键字抛出异常(从Solidity 0.4.13 版本开始, throw 关键字已被弃用)。
注意: 在一个不存在的地址上调用底层的函数 call、 delegatecall、 calicode 会成功返回,所以我们在进行调用时,应该优先进行函数存在性检查。
pragma solidity ^0.4.0;
contract Sharer {
function sendHalf(address addr) public payable returns (uint balance) {
require(msg.value % 2 == 0);
uint balanceBeforeTransfer = this.balance;
addr.transfer(msg.value / 2);
assert(this.balance == balanceBeforeTransfer - msg.value / 2);
return this.balance;
}
}
运行测试1:附加 1wei (奇数)去调用 sendHalf, 这时会发生异常,如下图所示。
运行测试 2:附加 2wei 去调用 sendHalf, 运行正常。
运行测试 3:附加 2wei 及 sendHalf参数为当前合约本身,在转账时发生异常,因为合约无法接收转账,错误提示与上图类似。
assert 类型异常
在下述场景中自动产生 assert 类型的异常:
1.越界或负的序号值访问数组, 如 i >= x.length 或 i<0 时访问 x[i]。
2. 序号越界或负的序号值访问一个定长的 bytesN。
3. 被除数为 0,如 5/0 或 23%0。
4. 对一个二进制数移动一个负的值,如 5<<i; i为 -1。
5. 整数进行显式转换为枚举时,将过大值、负值转为枚举类型并抛出异常。
6. 调用未初始化内部函数类型(参考函数类型章节)的变量。
7. 调用 assert的参数为 false。
require 类型异常
- 调用 throw。
- 调用 require 的参数为 false。
- 通过消息调用一个函数,但在调用的过程中并没有正确结束(例如 gas 不足没有匹配到对应的函数,或是被调用的函数出现异常)。 底层操作如 call、 send、 delegatecall 或 callcode 除外,它们不会抛出异常, 但它们会通过返回 false 来表示失败。
- 在使用 new 创建一个新合约时因为第 3 条的原因而没有正常完成。
- 调用外部函数时,被调用的对象不包含代码。
- 合约没有 payable 修饰符的 public 的函数在接收以太币时(包括构造函 数和回退函数)会出现异常。
- 合约通过一个 public 的 getter 函数接收以太币。
- transfer()执行失败。
assert 与 require 的详细对比
两种类型的相同点是异常都会撤销所有操作,这是为了保证交易的原子性(即一致性,要么全部执行,要么一点都不执行,不能只改变一部分)。
两种类型的不同点:
- assert 类型的异常会消耗掉所有剩余的 gas, 而 require 不会消耗剩余 gas(剩余 gas 会返还给调用者)。
- 操作符不同
当发生 require 类型的异常时, Solidity 会执行一个回退操作 (REVERT 指令 0xfd)。
当发生 assert 类型的异常时,Solidity 会执行一个无效操作(无效指令 0xfe)。
该使用哪一个?
下面是使用 require()的一些经验总结:
- 用于检查用户输入。
- 用于检查合约调用返回值, 如 require(external.send( amount))。
- 用于检查状态, 如 msg.send == owner。
- 通常用于函数的开头。
- 不知道使用哪一个的时候,就使用 require。
下面是使用 assert()的一些经验总结:
- 用于检查溢出错误,如“z = x + y ; assert(z >= x);”。
- 用于检查不应该发生的异常情况。
- 用于在状态改变之后,检查合约状态。
- 尽量少使用 assert;。
- 通常用于函数中间或结尾。