Solidity 学习

RT。

Solidity 源文件结构

版本杂注

版本杂注使用如下:

1
2
// 版本号必须 >= 0.4.0,且第一位非 0 数字不能增加,即小于 0.5.0 (遵循 npm 版本号规范)
pragma solidity ^0.4.0;

导入其他源文件

语法与语义

1
import "filename";

此语句将从 “filename” 中导入所有的全局符号到当前全局作用域中(不同于 ES6,Solidity 是向后兼容的)。

1
import * as symbolName from "filename";

创建一个新的全局符号 symbolName,其成员均来自 "filename" 中全局符号。

1
import {symbol1 as alias, symbol2} from "filename";

创建新的全局符号 aliassymbol2,分别从 "filename" 引用 symbol1symbol2
这里 as 的结合优先级要高于 , ,也就是说,第一句 import 是 symbol1 as alias,第二句是 symbol2,一起 from "filename"

另一种语法不属于 ES6,但或许更简便:

1
import "filename" as symbolName;

这条语句等同于 import * as symbolName from "filename";

路径

用 import “./x” as x; 语句导入当前源文件同目录下的文件 x 。 如果用 import “x” as x; 代替,可能会引入不同的文件(在全局 include directory 中)。

最终导入哪个文件取决于编译器(见下文)到底是怎样解析路径的。 通常,目录层次不必严格映射到本地文件系统, 它也可以映射到能通过诸如 ipfs,http 或者 git 发现的资源。

Remix 提供一个为 github 源代码平台的自动重映射,它将通过网络自动获取文件: 比如,你可以使用 import "github.com/ethereum/dapp-bin/library/iterable_mapping.sol" as it_mapping; 导入一个 map 迭代器。

注释

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

在下面的例子中,我们记录了合约的标题、两个入参和两个返回值的说明:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
pragma solidity ^0.4.0;

/** @title 形状计算器。 */
contract shapeCalculator {
    /** @dev 求矩形表明面积与周长。
    * @param w 矩形宽度。
    * @param h 矩形高度。
    * @return s 求得表面积。
    * @return p 求得周长。
    */
    function rectangle(uint w, uint h) returns (uint s, uint p) {
        s = w * h;
        p = 2 * (w + h);
    }
}

变量与类型

值类型有:bool、整型 int/uint、定点小数 fixed/ufixed、address、byte、enum、function;
引用类型有:bytes、string、数据位置(memory 或 storage)、数组、结构体;
mapping 既不是值类型也不是引用类型。

布尔

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

支持的运算符:
* ! (逻辑非)
* && (逻辑与, “and” )
* || (逻辑或, “or” )
* == (等于)
* != (不等于)

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

整型

  • int, uint: 支持关键字 uint8uint256 (无符号,从 8 位到 256 位)以及 int8int256,以 8 位为步长递增。 uintint 分别是 uint256int256 的别名。

运算符:

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

定长小数

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

地址

address:地址类型存储一个 20 字节的值(以太坊地址的大小)

  • address.balance 用来获取 eth 的余额,单位为 wei
  • address.transfer(amount) 用来向 address 发送数量为 amount 的 eth,单位为 wei
  • address.send(amount)address.transfer 的 low level api, 如果调用栈深度是 1024 会导致发送失败(这总是可以被调用者强制),如果接收者用光了 gas 也会导致发送失败。 所以为了保证发送的安全,一定要检查 send 的返回值,使用 transfer 或者更好的办法: 使用一种接收者可以取回资金的模式。
  • address.call()
    • nameReg.call(“register”, “MyName”);
    • nameReg.call(bytes4(keccak256(“fun(uint256)”)), a);
    • call 返回的布尔值表明了被调用的函数已经执行完毕(true)或者引发了一个 EVM 异常(false);
    • 无法访问返回的真实数据(为此我们需要事先知道编码和大小);
    • namReg.call.gas(1000000)(“register”, “MyName”); 可以使用 .gas() 修饰器modifier 调整提供的 gas 数量
  • address.delegatecall()address.call()区别在于只使用给定地址的代码,其它属性(存储,余额,……)都取自当前合约。用户必须确保两个合约中的存储结构都适用于 delegatecall。

byte 与 bytes

定长字节数组

关键字有:bytes1, bytes2, bytes3, …, bytes32。byte 是 bytes1 的别名。
.length 表示这个字节数组的长度(只读).

变长字节数组

bytes: 变长字节数组,是引用类型。
string: 变长 UTF-8 编码字符串类型,是引用类型。

枚举

1
2
3
4
contract test {
    enum ActionChoices { GoLeft, GoRight, GoStraight, SitStill }
    ...
}

函数

函数类型表示成如下的形式:

1
function (<parameter types>) {internal|external} [pure|constant|view|payable] [returns (<return types>)]

内部函数只能在当前合约内被调用(更具体来说,在当前代码块内,包括内部库函数和继承的函数中),因为它们不能在当前合约上下文的外部被执行。 调用一个内部函数是通过跳转到它的入口标签来实现的,就像在当前合约的内部调用一个函数。

外部函数由一个地址和一个函数签名组成,可以通过外部函数调用传递或者返回。

函数类型默认是内部函数,因此不需要声明 internal 关键字。 与此相反的是,合约中的函数本身默认是 public 的,只有当它被当做类型名称时,默认才是内部函数。

除此之外,public(或 external)函数也有一个特殊的成员变量称作 selector,可以返回 ABI 函数选择器:

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

contract Selector {
  function f() public view returns (bytes4) {
    return this.f.selector;
  }
}

使用内部函数类型的例子,range 生成 [1, 2, 3 … n], map 生成 [1, 4, 9 … n2], reduce 求和:

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
pragma solidity ^0.4.16;

library ArrayUtils {
  // 内部函数可以在内部库函数中使用,
  // 因为它们会成为同一代码上下文的一部分
  function map(uint[] memory self, function (uint) pure returns (uint) f)
    internal
    pure
    returns (uint[] memory r)
  {
    r = new uint[](self.length);
    for (uint i = 0; i < self.length; i++) {
      r[i] = f(self[i]);
    }
  }
  function reduce(
    uint[] memory self,
    function (uint, uint) pure returns (uint) f
  )
    internal
    pure
    returns (uint r)
  {
    r = self[0];
    for (uint i = 1; i < self.length; i++) {
      r = f(r, self[i]);
    }
  }
  function range(uint length) internal pure returns (uint[] memory r) {
    r = new uint[](length);
    for (uint i = 0; i < r.length; i++) {
      r[i] = i;
    }
  }
}

contract Pyramid {
  using ArrayUtils for *;
  function pyramid(uint l) public pure returns (uint) {
    return ArrayUtils.range(l).map(square).reduce(sum);
  }
  function square(uint x) internal pure returns (uint) {
    return x * x;
  }
  function sum(uint x, uint y) internal pure returns (uint) {
    return x + y;
  }
}

一个使用外部函数类型的例子,预言机发请求,获得回调:

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
pragma solidity ^0.4.11;

contract Oracle {
  struct Request {
    bytes data;
    function(bytes memory) external callback;
  }
  Request[] requests;
  event NewRequest(uint);
  function query(bytes data, function(bytes memory) external callback) public {
    requests.push(Request(data, callback));
    NewRequest(requests.length - 1);
  }
  function reply(uint requestID, bytes response) public {
    // 这里要验证 reply 来自可信的源
    requests[requestID].callback(response);
  }
}

contract OracleUser {
  Oracle constant oracle = Oracle(0x1234567); // 已知的合约
  function buySomething() {
    oracle.query("USD", this.oracleResponse);
  }
  function oracleResponse(bytes response) public {
    require(msg.sender == address(oracle));
    // 使用数据
  }
}

数据位置

本段落中的 状态变量 也就是合约的成员变量。状态变量负责持久化数据,而且一定会持久化。

所有的复杂类型,即 数组 和 结构 类型,都有一个额外属性,“数据位置”,说明数据是保存在 内存 memory 中还是 存储storage 中。

  • 根据上下文不同,大多数时候数据有默认的位置,但也可以通过在类型名后增加关键字 storage 或 memory 进行修改;
  • 函数参数(包括返回的参数)的数据位置默认是 memory;
  • 局部变量的数据位置默认是 storage;
  • 状态变量的数据位置强制是 storage;
  • 外部函数的参数(非返回参数)的数据位置被强制指定为 calldata ,效果跟 memory 差不多。

  • 在 storage 和 memory 之间两两赋值,会创建一份独立的拷贝;

  • storage 向状态变量(或从其它状态变量)赋值,会创建一份独立的拷贝;
  • 状态变量向局部变量赋值时仅仅传递一个引用,而且这个引用总是指向状态变量,因此后者改变的同时前者也会发生改变;
  • 从一个 memory 存储的引用类型向另一个 memory 存储的引用类型赋值并不会创建拷贝。

数组

数组可以在声明时指定长度,也可以动态调整大小。
对于 storage 的数组来说,元素类型可以是任意的(即元素也可以是数组类型,映射类型或者结构体)。
对于 memory 的数组来说,元素类型不能是映射类型,如果作为 public 函数的参数,它只能是 ABI 类型。

.length:

数组有 length 成员变量表示当前数组的长度。 动态数组可以在 存储storage (而不是 内存memory )中通过改变成员变量 .length 改变数组大小。 并不能通过访问超出当前数组长度的方式实现自动扩展数组的长度。 一经创建,内存memory 数组的大小就是固定的(但却是动态的,也就是说,它依赖于运行时的参数)。

.push():

变长的 存储storage 数组以及 bytes 类型(而不是 string 类型)都有一个叫做 push 的成员函数,它用来附加新的元素到数组末尾。 这个函数将返回新的数组长度。

一个元素类型为 T,固定长度为 k 的数组可以声明为 T[k],而动态数组声明为 T[]。 举个例子,一个长度为 5,元素类型为 uint 的动态数组的数组,应声明为 uint[][5] (注意这里跟其它语言比,数组长度的声明位置是反的)。 要访问第三个动态数组的第二个元素,你应该使用 x[2][1](数组下标是从 0 开始的,且访问数组时的下标顺序与声明时相反,也就是说,x[2] 是从右边减少了一级)。

bytes 和 string 类型的变量是特殊的数组。 bytes 类似于 byte[],但它在 calldata 中会被“紧打包”(译者注:将元素连续地存在一起,不会按每 32 字节一单元的方式来存放)。 string 与 bytes 相同,但(暂时)不允许用长度或索引来访问。

如果想要访问以字节表示的字符串 s,请使用 bytes(s).length / bytes(s)[7] = ‘x’;。 注意这时你访问的是 UTF-8 形式的低级 bytes 类型,而不是单个的字符。

可以将数组标识为 public,从而让 Solidity 创建一个 getter。 之后必须使用数字下标作为参数来访问 getter。

可以使用 new 在内存中创建变长数组。但是内存数组不能通过 .length 更改长度:

1
2
uint[] memory a = new uint[](7);
bytes memory b = new bytes(len);

结构体

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
contract CrowdFunding {
    // 声明
    struct Funder {
        address addr;
        uint amount;
    }

    struct Campaign {
        address beneficiary;
        uint fundingGoal;
        uint numFunders;
        uint amount;
        mapping (uint => Funder) funders;
    }

    // 存储
    uint numCampaigns;
    mapping (uint => Campaign) campaigns;

    function newCampaign(address beneficiary, uint goal) public returns (uint campaignID) {
        campaignID = numCampaigns++;
        // 创建
        campaigns[campaignID] = Campaign(beneficiary, goal, 0, 0);
    }
    ...

mapping

mapping 为 solidity 中的 hash table。声明时的形式为 mapping(_KeyType => _ValueType)。 其中 _KeyType 可以是除了映射、变长数组、合约、枚举以及结构体以外的几乎所有类型。 _ValueType 可以是包括映射类型在内的任何类型。

映射是没有长度的,也没有 key 的集合或 value 的集合的概念。
只有状态变量(或者在 internal 函数中的对于存储变量的引用)可以使用映射类型。

可以将映射声明为 public,然后来让 Solidity 创建一个 getter。 _KeyType 将成为 getter 的必须参数,并且 getter 会返回 _ValueType

_ValueType 也可以是一个映射。这时在使用 getter 时将将需要递归地传入每个 _KeyType 参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
pragma solidity ^0.4.0;

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

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

contract MappingUser {
    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
//基于返回的元组来声明变量并赋值
(uint x, bool b, uint y) = f();
//交换两个值的通用窍门——但不适用于非值类型的存储 (storage) 变量。
(x, y) = (y, x);
//元组的末尾元素可以省略(这也适用于变量声明)。
(data.length,,) = f(); // 将长度设置为 7
//省略元组中末尾元素的写法,仅可以在赋值操作的左侧使用,除了这个例外:
(x,) = (1,);
//(1,) 是指定单元素元组的唯一方法,因为 (1)
//相当于 1。

显式类型转换

1
2
int8 y = -3;
uint x = uint(y);

如果一个类型显式转换成更小的类型,相应的高位将被舍弃:

1
2
uint32 a = 0x12345678;
uint16 b = uint16(a); // 此时 b 的值是 0x5678

合约结构

为了后面两个章节的背景知识,本章节将简述合约结构。详细的合约声明及使用将在 《合约》章节中介绍。

状态变量

状态变量是永久地存储在合约存储中的值。
相对其他语言,contract 是一个类,状态变量则是类的成员变量:

1
2
3
4
5
6
pragma solidity ^0.4.0;

contract SimpleStorage {
    uint storedData; // 状态变量
    // ...
}

constant 状态变量

状态变量可使用 constant 修饰,在这种情况下,只能使用那些在编译时有确定值的表达式来给它们赋值。任何通过访问 storage,区块链数据(例如 now, this.balance 或者 block.number)或执行数据( msg.gas ) 或对外部合约的调用来给它们赋值都是不允许的。 在内存分配上有边界效应(side-effect)的表达式是允许的,但对其他内存对象产生边界效应的表达式则不行。 内建(built-in)函数 keccak256,sha256,ripemd160,ecrecover,addmod 和 mulmod 是允许的(即使他们确实会调用外部合约)。

编译器不会为这些变量预留存储,它们的每次出现都会被替换为相应的常量表达式(这将可能被优化器计算为实际的某个值)。

不是所有类型的状态变量都支持用 constant 来修饰,当前支持的仅有值类型和字符串。

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

contract C {
    uint constant x = 32**22 + 8;
    string constant text = "abc";
    bytes32 constant myHash = keccak256("abc");
}

函数

1
2
3
4
5
contract SimpleAuction {
    function bid() public payable { // 函数
        // ...
    }
}

详细用法将在后续《表达式与控制结构》章节讲解。

修饰器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
pragma solidity ^0.4.22;

contract Purchase {
    address public seller;

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

    function abort() public onlySeller { // Modifier usage
        // ...
    }
}

事件

emit 事件可以使得轻钱包得到通知。在底层,事件和其他调用的日志等级不同,而轻钱包可以观测到事件的日志。

1
2
3
4
5
6
7
8
9
pragma solidity ^0.4.21;
contract SimpleAuction {
    event HighestBidIncreased(address bidder, uint amount); // 声明

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

表达式和控制结构

delete 运算符

delete a 的结果是将 a 的类型在初始化时的值赋值给 a。即对于整型变量来说,相当于 a = 0, 但 delete 也适用于数组,对于动态数组来说,是将数组的长度设为 0,而对于静态数组来说,是将数组中的所有元素重置。 如果对象是结构体,则将结构体中的所有属性重置。

delete 对整个映射是无效的(因为映射的键可以是任意的,通常也是未知的)。 因此在你删除一个结构体时,结果将重置所有的非映射属性,这个过程是递归进行的,除非它们是映射。 然而,单个的键及其映射的值是可以被删除的。

理解 delete a 的效果就像是给 a 赋值很重要,换句话说,这相当于在 a 中存储了一个新的对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
pragma solidity ^0.4.0;

contract DeleteExample {
    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 对象赋值。
    }
}

控制结构

JavaScript 中的大部分控制结构在 Solidity 中都是可用的,除了 switch 和 goto。 因此 Solidity 中有 if,else,while,do,for,break,continue,return,? : 这些与在 C 或者 JavaScript 中表达相同语义的关键词。

用于表示条件的括号 不可以 被省略,单语句体两边的花括号可以被省略。

注意,与 C 和 JavaScript 不同, Solidity 中非布尔类型数值不能转换为布尔类型,因此 if (1) { … } 的写法在 Solidity 中 无效 。

函数的声明

输入参数

输入参数的声明方式与变量相同。但是有一个例外,未使用的参数可以省略参数名。 例如,如果我们希望合约接受有两个整数形参的函数的外部调用,我们会像下面这样写:

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

contract Simple {
    function taker(uint _a, uint _b) public pure {
        // 用 _a 和 _b 实现相关功能.
    }
}

输出参数

输出参数的声明方式在关键词 returns 之后,与输入参数的声明方式相同。 例如,如果我们需要返回两个结果:两个给定整数的和与积,我们应该写作:

1
2
3
4
5
6
7
8
9
10
11
12
pragma solidity ^0.4.16;

contract Simple {
    function arithmetics(uint _a, uint _b)
        public
        pure
        returns (uint o_sum, uint o_product)
    {
        o_sum = _a + _b;
        o_product = _a * _b;
    }
}

输出参数名可以被省略。输出值也可以使用 return 语句指定。 return 语句也可以返回多值,参阅:ref:multi-return。 返回的输出参数被初始化为 0;如果它们没有被显式赋值,它们就会一直为 0。

输入参数和输出参数可以在函数体中用作表达式。因此,它们也可用在等号左边被赋值。

当一个函数有多个输出参数时, return (v0, v1, …,vn) 写法可以返回多个值。不过元素的个数必须与输出参数的个数相同。

函数的调用

内部函数调用

当前合约中的函数可以直接(“从内部”)调用,也可以递归调用,就像下边的例子一样:

1
2
3
4
5
6
pragma solidity ^0.4.16;

contract C {
    function g(uint a) public pure returns (uint ret) { return f(); }
    function f() internal pure returns (uint ret) { return g(7) + f(); }
}

这些函数调用在 EVM 中被解释为简单的跳转。这样做的效果就是当前内存不会被清除,也就是说,通过内部调用在函数之间传递内存引用是非常有效的。

外部函数调用

表达式 this.g(8); 和 c.g(2); (其中 c 是合约实例)也是有效的函数调用,但是这种情况下,函数将会通过一个消息调用来被“外部调用”,而不是直接的跳转。 请注意,不可以在构造函数中通过 this 来调用函数,因为此时真实的合约实例还没有被创建。

如果想要调用其他合约的函数,需要外部调用。对于一个外部调用,所有的函数参数都需要被复制到内存。

当调用其他合约的函数时,随函数调用发送的 Wei 和 gas 的数量可以分别由特定选项 .value() 和 .gas() 指定:

1
2
3
4
5
6
7
8
9
10
11
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)(); }
}

.value() 选项只能用于被 payable 修饰的函数。

注意,表达式 InfoFeed(addr) 进行了一个的显式类型转换,说明”我们知道给定地址的合约类型是 InfoFeed “并且这不会执行构造函数。 显式类型转换需要谨慎处理。绝对不要在一个你不清楚类型的合约上执行函数调用。

我们也可以直接使用 function setFeed(InfoFeed _feed) { feed = _feed; } 。 注意一个事实,feed.info.value(10).gas(800) 只(局部地)设置了与函数调用一起发送的 Wei 值和 gas 的数量,只有最后的圆括号执行了真正的调用。

如果被调函数所在合约不存在(也就是账户中不包含代码)或者被调用合约本身抛出异常或者 gas 用完等,函数调用会抛出异常。

防范重入攻击

任何与其他合约的交互都会强加潜在危险,在调用时,当前合约将控制权移交给被调用合约,而被调用合约可能做任何事。
此外,请小心被调用的合约再次调用你的其他合约,或者甚至在第一次调用返回之前返回到你的当前合约。这意味着被调用合约可以通过它自己的函数改变调用合约的状态变量。
一个建议的函数写法是,例如,在你合约中状态变量进行各种变化后再调用外部函数,这样,你的合约就不会轻易被滥用的重入 (reentrancy) 所影响。

创建合约

使用关键字 new 可以创建一个新合约。待创建合约的完整代码必须事先知道,因此递归的创建依赖是不可能的。

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.0;

contract D {
    uint x;
    function D(uint a) public payable {
        x = a;
    }
}

contract C {
    D d = new D(4); // 将作为合约 C 构造函数的一部分执行

    function createD(uint arg) public {
        D newD = new D(arg);
    }

    function createAndEndowD(uint arg, uint amount) public payable {
                //随合约的创建发送 ether
        D newD = (new D).value(amount)(arg);
    }
}

如示例中所示,使用 .value() 选项创建 D 的实例时可以转发 Ether,但是不可能限制 gas 的数量。如果创建失败(可能因为栈溢出,或没有足够的余额或其他问题),会引发异常。

操作符优先级

参见 操作符优先级

作用域和声明

变量声明后将有默认初始值,其初始值字节表示全部为零。任何类型变量的“默认值”是其对应类型的典型“零状态”。例如, bool 类型的默认值是 false 。 uint 或 int 类型的默认值是 0 。对于静态大小的数组和 bytes1 到 bytes32 ,每个单独的元素将被初始化为与其类型相对应的默认值。 最后,对于动态大小的数组, bytes 和 string 类型,其默认缺省值是一个空数组或字符串。

Solidity 中的作用域规则遵循了 C99(与其他很多语言一样):变量将会从它们被声明之后可见,直到一对 { } 块的结束。作为一个例外,在 for 循环语句中初始化的变量,其可见性仅维持到 for 循环的结束。

那些定义在代码块之外的变量,比如函数、合约、自定义类型等等,并不会影响它们的作用域特性。这意味着你可以在实际声明状态变量的语句之前就使用它们,并且递归地调用函数。

错误处理:Assert, Require, Revert and Exceptions

Solidity 使用状态恢复异常来处理错误。这种异常将撤消对当前调用(及其所有子调用)中的状态所做的所有更改,并且还向调用者标记错误。 便利函数 assert 和 require 可用于检查条件并在条件不满足时抛出异常。assert 函数只能用于测试内部错误,并检查非变量。 require 函数用于确认条件有效性,例如输入变量,或合约状态变量是否满足条件,或验证外部合约调用返回的值。 如果使用得当,分析工具可以评估你的合约,并标示出那些会使 assert 失败的条件和函数调用。 正常工作的代码不会导致一个 assert 语句的失败;如果这发生了,那就说明出现了一个需要你修复的 bug。

require 可以指定消息字符串,而 assert 不行:

1
2
3
4
5
6
7
8
9
10
11
12
pragma solidity ^0.4.22;

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;
    }
}

revert 搭配条件判断可以达到和 require 同样的效果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
pragma solidity ^0.4.22;

contract VendingMachine {
    function buy(uint amount) payable {
        if (amount > msg.value / 2 ether)
            revert("Not enough Ether provided.");
        // 下边是等价的方法来做同样的检查:
        require(
            amount <= msg.value / 2 ether,
            "Not enough Ether provided."
        );
        // 执行购买操作
    }
}

下列情况将会产生一个 assert 式异常:

1
2
3
4
5
6
7
如果你访问数组的索引太大或为负数(例如 x[i] 其中 i >= x.length  i < 0)。
如果你访问固定长度 bytesN 的索引太大或为负数。
如果你用零当除数做除法或模运算(例如 5 / 0  23 % 0 )。
如果你移位负数位。
如果你将一个太大或负数值转换为一个枚举类型。
如果你调用内部函数类型的零初始化变量。
如果你调用 assert 的参数(表达式)最终结算为 false

下列情况将会产生一个 require 式异常:

1
2
3
4
5
6
7
8
调用 throw 
如果你调用 require 的参数(表达式)最终结算为 false 
如果你通过消息调用调用某个函数,但该函数没有正确结束(它耗尽了 gas,没有匹配函数,或者本身抛出一个异常),上述函数不包括低级别的操作 call  send  delegatecall 或者 callcode 。低级操作不会抛出异常,而通过返回 false 来指示失败。
如果你使用 new 关键字创建合约,但合约没有正确创建(请参阅上条有关”未正确完成“的定义)。
如果你对不包含代码的合约执行外部函数调用。
如果你的合约通过一个没有 payable 修饰符的公有函数(包括构造函数和 fallback 函数)接收 Ether
如果你的合约通过公有 getter 函数接收 Ether 
如果 .transfer() 失败。

在内部, Solidity 对一个 require 式的异常执行回退操作(指令 0xfd )并执行一个无效操作(指令 0xfe )来引发 assert 式异常。 在这两种情况下,都会导致 EVM 回退对状态所做的所有更改。回退的原因是不能继续安全地执行,因为没有实现预期的效果。 因为我们想保留交易的原子性,所以最安全的做法是回退所有更改并使整个交易(或至少是调用)不产生效果。 请注意, assert 式异常消耗了所有可用的调用 gas ,而从 Metropolis 版本起 require 式的异常不会消耗任何 gas。

内建 API

ETH 单位

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

时间单位

秒是缺省时间单位,在时间单位之间,数字后面带有 seconds、 minutes、 hours、 days、 weeks 和 years 的可以进行换算,基本换算关系如下:

1
2
3
4
5
6
7
1 == 1 seconds
1 minutes == 60 seconds
1 hours == 60 minutes
1 days == 24 hours
1 weeks == 7 days
1 years == 365 days
years 后缀已经不推荐使用了,因为从 0.5.0 版本开始将不再支持。

由于闰秒造成的每年不都是 365 天、每天不都是 24 小时 leap seconds,所以如果你要使用这些单位计算日期和时间,请注意这个问题。因为闰秒是无法预测的,所以需要借助外部的预言机(oracle,是一种链外数据服务,译者注)来对一个确定的日期代码库进行时间矫正。

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

1
2
3
4
5
function f(uint start, uint daysAfter) public {
    if (now >= start + daysAfter * 1 days) {
        // ...
    }
}

区块和交易属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
block.blockhash(uint blockNumber) returns (bytes32):指定区块的区块哈希——仅可用于最新的 256 个区块且不包括当前区块;而 blocks  0.4.22 版本开始已经不推荐使用,由 blockhash(uint blockNumber) 代替
block.coinbase (address): 挖出当前区块的矿工地址
block.difficulty (uint): 当前区块难度
block.gaslimit (uint): 当前区块 gas 限额
block.number (uint): 当前区块号
block.timestamp (uint):  unix epoch 起始当前区块以秒计的时间戳
gasleft() returns (uint256):剩余的 gas
msg.data (bytes): 完整的 calldata
msg.gas (uint): 剩余 gas -  0.4.21 版本开始已经不推荐使用,由 gesleft() 代替
msg.sender (address): 消息发送者(当前调用)
msg.sig (bytes4): calldata 的前 4 字节(也就是函数标识符)
msg.value (uint): 随消息发送的 wei 的数量
now (uint): 目前区块时间戳(block.timestamp
tx.gasprice (uint): 交易的 gas 价格
tx.origin (address): 交易发起者(完全的调用链)

对于每一个外部函数调用,包括 msg.sender 和 msg.value 在内所有 msg 成员的值都会变化。这里包括对库函数的调用。

不要依赖 block.timestamp、 now 和 blockhash 产生随机数,除非你知道自己在做什么。

时间戳和区块哈希在一定程度上都可能受到挖矿矿工影响。例如,挖矿社区中的恶意矿工可以用某个给定的哈希来运行赌场合约的 payout 函数,而如果他们没收到钱,还可以用一个不同的哈希重新尝试。

当前区块的时间戳必须严格大于最后一个区块的时间戳,但这里唯一能确保的只是它会是在权威链上的两个连续区块的时间戳之间的数值。

基于可扩展因素,区块哈希不是对所有区块都有效。你仅仅可以访问最近 256 个区块的哈希,其余的哈希均为零。

ABI 编码函数

1
2
3
4
abi.encode(...) returns (bytes) ABI - 对给定参数进行编码
abi.encodePacked(...) returns (bytes):对给定参数执行 紧打包编码
abi.encodeWithSelector(bytes4 selector, ...) returns (bytes) ABI - 对给定参数进行编码,并以给定的函数选择器作为起始的 4 字节数据一起返回
abi.encodeWithSignature(string signature, ...) returns (bytes):等价于 abi.encodeWithSelector(bytes4(keccak256(signature), ...)

这些编码函数可以用来构造函数调用数据,而不用实际进行调用。此外,keccak256(abi.encodePacked(a, b)) 是更准确的方法来计算在未来版本不推荐使用的 keccak256(a, b)。 更多详情请参考 ABI 和 紧打包编码。

错误处理

1
2
3
4
5
6
7
8
9
10
assert(bool condition):
如果条件不满足,则使当前交易没有效果  用于检查内部错误。
require(bool condition):
如果条件不满足则撤销状态更改 - 用于检查由输入或者外部组件引起的错误。
require(bool condition, string message):
如果条件不满足则撤销状态更改 - 用于检查由输入或者外部组件引起的错误,可以同时提供一个错误消息。
revert():
终止运行并撤销状态更改。
revert(string reason):
终止运行并撤销状态更改,可以同时提供一个解释性的字符串。

数学和密码学函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
addmod(uint x, uint y, uint k) returns (uint):
计算 (x + y) % k,加法会在任意精度下执行,并且加法的结果即使超过 2**256 也不会被截取。从 0.5.0 版本的编译器开始会加入对 k != 0 的校验(assert)。

mulmod(uint x, uint y, uint k) returns (uint):
计算 (x * y) % k,乘法会在任意精度下执行,并且乘法的结果即使超过 2**256 也不会被截取。从 0.5.0 版本的编译器开始会加入对 k != 0 的校验(assert)。

keccak256(...) returns (bytes32):
计算 (tightly packed) arguments  Ethereum-SHA-3 Keccak-256)哈希。

sha256(...) returns (bytes32):
计算 (tightly packed) arguments  SHA-256 哈希。

sha3(...) returns (bytes32):
等价于 keccak256

ripemd160(...) returns (bytes20):
计算 (tightly packed) arguments  RIPEMD-160 哈希。

ecrecover(bytes32 hash, uint8 v, bytes32 r, bytes32 s) returns (address) 
利用椭圆曲线签名恢复与公钥相关的地址,错误返回零值。 (example usage)

上文中的“tightly packed”是指不会对参数值进行 padding 处理(就是说所有参数值的字节码是连续存放的,译者注),这意味着下边这些调用都是等价的:

1
keccak256("ab", "c") keccak256("abc") keccak256(0x616263) keccak256(6382179) keccak256(97, 98, 99)

如果需要 padding,可以使用显式类型转换:keccak256(“\x00\x12”) 和 keccak256(uint16(0x12)) 是一样的。

请注意,常量值会使用存储它们所需要的最少字节数进行打包。例如:keccak256(0) == keccak256(uint8(0)),keccak256(0x12345678) == keccak256(uint32(0x12345678))。

在一个私链上,你很有可能碰到由于 sha256、ripemd160 或者 ecrecover 引起的 Out-of-Gas。原因是因为这些密码学函数在以太坊虚拟机(EVM)中以“预编译合约”形式存在的,且在第一次收到消息后才被真正存在(尽管合约代码是EVM中已存在的硬编码)。因此发送到不存在的合约的消息非常昂贵,所以实际的执行会导致 Out-of-Gas 错误。在你实际使用你的合约之前,给每个合约发送一点儿以太币,比如 1 Wei。这在官方网络或测试网络上不是问题。

合约相关

1
2
3
4
5
6
7
this (current contract's type):
当前合约,可以明确转换为 地址类型。
selfdestruct(address recipient):
销毁合约,并把余额发送到指定 地址类型。
suicide(address recipient):
 selfdestruct 等价,但已不推荐使用。
此外,当前合约内的所有函数都可以被直接调用,包括当前函数。

合约

创建合约

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

创建合约时,会执行一次构造函数(与合约同名的函数)。构造函数是可选的。只允许有一个构造函数,这意味着不支持重载。

在内部,构造函数参数在合约代码之后通过 ABI 编码 传递,但是如果你使用 web3.js 则不必关心这个问题。

如果一个合约想要创建另一个合约,那么创建者必须知晓被创建合约的源代码(和二进制代码)。 这意味着不可能循环创建依赖项。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
contract OwnedToken {
    TokenCreator creator;
    address owner;
    bytes32 name;

    function OwnedToken(bytes32 _name) public {
        owner = msg.sender;  // 在构造函数内,不能使用 this.owner,因为 this 还没被创建完成
        creator = TokenCreator(msg.sender);
        name = _name;
    }
}

contract TokenCreator {
    function createToken(bytes32 name)
       public
       returns (OwnedToken tokenAddress)
    {
        // 创建一个新的 Token 合约并且返回它的地址。
        return new OwnedToken(name);
    }
}

继承

子合约使用 is 关键字继承父合约。例子中,SubContract 继承自 SuperContract:

1
2
contract SubContract is SuperContract {
}

可见性

Solidity 共有四种可见性:

  1. external :外部函数作为合约接口的一部分,意味着我们可以从其他合约和交易中调用。 一个外部函数 f 不能从内部调用(即 f 不起作用,但 this.f() 可以)。 当收到大量数据的时候,外部函数有时候会更有效率。
  2. public :public 函数是合约接口的一部分,可以在内部或通过消息调用。对于公共状态变量, 会自动生成一个 getter 函数(见下面)。
  3. internal :这些函数和状态变量只能是内部访问(即从当前合约内部或从它派生的合约访问),不使用 this 调用。
  4. private :private 函数和状态变量仅在当前定义它们的合约中使用,并且不能被派生合约使用。

函数可见性默认为 public,继承后的子合约不能重载父合约的 private 函数;状态变量可见性默认为 internal,不能设置为 external。

可见性定义语法:

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

contract C {
    // 函数: 可见性定义在参数列表和 returns 关键字中间
    function f(uint a) private pure returns (uint b) { return a + 1; }
    function setData(uint a) internal { data = a; }
    uint public data;  // 状态变量: 可见性定义在类型后面
}

Getter

编译器自动为所有 public 状态变量创建 getter 函数。对于下面给出的合约,编译器会生成一个名为 data 的函数, 该函数不会接收任何参数并返回一个 uint ,即状态变量 data 的值。可以在声明时完成状态变量的初始化。

1
2
3
4
5
6
7
8
9
10
11
12
pragma solidity ^0.4.0;

contract C {
    uint public data = 42;
}

contract Caller {
    C c = new C();
    function f() public {
        uint local = c.data();
    }
}

getter 函数具有外部可见性。如果在内部访问 getter(即没有 this. ),它被认为一个状态变量。 如果它是外部访问的(即用 this. ),它被认为为一个函数。

1
2
3
4
5
6
7
8
9
pragma solidity ^0.4.0;

contract C {
    uint public data;
    function x() public {
        data = 3; // 内部访问
        uint val = this.data(); // 外部访问
    }
}

下一个例子稍微复杂一些:

1
2
3
4
5
6
7
8
9
10
pragma solidity ^0.4.0;

contract Complex {
    struct Data {
        uint a;
        bytes3 b;
        mapping (uint => uint) map;
    }
    mapping (uint => mapping(bool => Data[])) public data;
}

这将会生成以下形式的函数:

1
2
3
4
function data(uint arg1, bool arg2, uint arg3) public returns (uint a, bytes3 b) {
    a = data[arg1][arg2][arg3].a;
    b = data[arg1][arg2][arg3].b;
}

修饰器

修饰器 modifier 是一种特殊的函数,用于条件检查,仅在条件允许时执行被修饰的函数,可被继承,并可能被派生合约覆盖。
如果同一个函数有多个 修饰器modifier,它们之间以空格隔开,修饰器modifier 会依次检查执行。
修饰器modifier 的参数可以是任意表达式,在此上下文中,所有在函数中可见的符号,在 修饰器modifier 中均可见。 在 修饰器modifier 中引入的符号在函数中不可见(可能被重载改变)。

例子1:父合约 owned 定义了一个修饰器 onlyOwner,子合约 mortalclose() 函数使用了此修饰器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
contract owned {
    function owned() public { owner = msg.sender; }
    address owner;

    modifier onlyOwner {
        require(msg.sender == owner);
        _;
    }
}

contract mortal is owned {
    function close() public onlyOwner {
        selfdestruct(owner);
    }
}

例子2:修饰器可以传参:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
contract priced {
    modifier costs(uint price) {
        if (msg.value >= price) {
            _;
        }
    }
}

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

    function Register(uint initialPrice) public { price = initialPrice; }

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

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

例子3:使用修饰器定义 mutex,防护重入攻击:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
contract Mutex {
    bool locked;
    modifier noReentrancy() {
        require(!locked);
        locked = true;
        _;
        locked = false;
    }

    // 这个函数受互斥量保护,这意味着 `msg.sender.call` 中的重入调用不能再次调用  `f`。
    // `return 7` 语句指定返回值为 7,但修改器中的语句 `locked = false` 仍会执行。
    function f() public noReentrancy returns (uint) {
        require(msg.sender.call());
        return 7;
    }
}

函数的授权声明

view 或 constant

viewconstant(二者等价) 声明,用于承诺此函数不修改状态:

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

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

以下行为被认为是修改状态:

1
2
3
4
5
6
7
8
修改状态变量。
产生事件。
创建其它合约。
使用 selfdestruct
通过调用发送以太币。
调用任何没有标记为 view 或者 pure 的函数。
使用低级调用。
使用包含特定操作码的内联汇编。

pure

pure 声明,用于承诺此函数不修改也不读取状态:

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

contract C {
    function f(uint a, uint b) public pure returns (uint) {
        return a * (b + 42);
    }
}

以下行为被认为是读取状态:

1
2
3
4
5
读取状态变量。
访问 this.balance 或者 <address>.balance
访问 blocktx msg 中任意成员 (除 msg.sig  msg.data 之外)。
调用任何未标记为 pure 的函数。
使用包含某些操作码的内联汇编。

fallback

合约可以有一个未命名的函数,且不能有参数也不能有返回值。这个函数被称为 fallback 函数。尽管 fallback 函数不能有参数,仍然可以使用 msg.data 来获取随调用提供的任何有效数据。

如果在一个到合约的调用中,没有其他函数与给定的函数标识符匹配(或没有提供调用数据),那么这个函数(fallback 函数)会被执行。
除此之外,每当合约收到以太币(没有任何数据),这个函数就会执行。此外,为了接收以太币,fallback 函数必须标记为 payable。

一个没有定义 fallback 函数的合约,直接接收以太币(没有函数调用,即使用 send 或 transfer)会抛出一个异常, 并返还以太币(在 Solidity v0.4.0 之前行为会有所不同)。所以如果你想让你的合约接收以太币,必须实现 fallback 函数。

一个没有 payable fallback 函数的合约,可以作为 coinbase transaction (又名 miner block reward )的接收者或者作为 selfdestruct 的目标来接收以太币。合约不能拒绝此种方式传入的以太币。也是因为有这种入账形式的存在,合约中实际的 ETH 余额 this.balance 可能会高于 solidity 代码累加记录的数量。

在这样的上下文中,通常只有很少的 gas 可以用来完成这个函数调用(准确地说,是 2300 gas),所以使 fallback 函数的调用尽量廉价很重要。 请注意,调用 fallback 函数的交易(而不是内部调用)所需的 gas 要高得多,因为每次交易都会额外收取 21000 gas 或更多的费用,用于签名检查等操作。请确保您在部署合约之前彻底测试您的 fallback 函数,以确保执行成本低于 2300 个 gas。

具体来说,以下操作会消耗比 fallback 函数更多的 gas:

1
2
3
4
写入存储
创建合约
调用消耗大量 gas 的外部函数
发送以太币
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.4.0;

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


// 这个合约会保留所有发送给它的以太币,没有办法返还。
contract Sink {
    function() public payable { }
}

contract Caller {
    function callTest(Test test) public {
        test.call(0xabcdef01); // 不存在的哈希
        // 导致 test.x 变成 == 1。
        // 以下将不会编译,但如果有人向该合约发送以太币,交易将失败并拒绝以太币。
        // test.send(2 ether);
    }
}

overload 重载

合约可以具有多个不同参数的同名函数。这也适用于继承函数。以下示例展示了合约 A 中的重载函数 f。

1
2
3
4
5
6
7
8
9
10
11
pragma solidity ^0.4.16;

contract A {
    function f(uint _in) public pure returns (uint out) {
        out = 1;
    }

    function f(uint _in, bytes32 _key) public pure returns (uint out) {
        out = 2;
    }
}

重载函数也存在于外部接口中。如果两个外部可见函数,参数数量相同,区别仅为本文件内的类型,而不是它们的外部类型,则会编译错误:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 以下代码无法编译
pragma solidity ^0.4.16;

contract A {
    function f(B _in) public pure returns (B out) {
        out = _in;
    }

    function f(address _in) public pure returns (address out) {
        out = _in;
    }
}

contract B {
}

匹配过程:如果所有参数都可以隐式地转换为函数入参的预期类型,则此函数作为重载候选项。如果调用时找不到任何候选项,则解析失败。返回值不作为匹配的依据。

下例展示了一个由于重载的二义性,导致调用可能失败,也可能成功的例子:f(50)会调用失败,因为 50 既可以被隐式转换为 uint8 也可以被隐式转换为 uint256;f(256) 则会调用成功,因为因为 256 不能隐式转换为 uint8:

1
2
3
4
5
6
7
8
9
10
11
pragma solidity ^0.4.16;

contract A {
    function f(uint8 _in) public pure returns (uint8 out) {
        out = _in;
    }

    function f(uint256 _in) public pure returns (uint256 out) {
        out = _in;
    }
}

事件与日志

事件允许在 dapp 的前端界面中监听事件,事件发生时,监听事件的 Javascript 回调函数会被调用。日志和事件在合约内不可直接被访问(甚至是创建日志的合约也不能访问)。

事件索引

事件中可以定义成员变量,成员变量可以被 indexed 关键字修饰从而称为索引,在前端可以使用索引的特定 value 来过滤事件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
pragma solidity ^0.4.0;

contract ClientReceipt {
    event Deposit(
        address indexed _from,
        bytes32 indexed _id,
        uint _value
    );

    function deposit(bytes32 _id) public payable {
        // 我们可以过滤对 `Deposit` 的调用,从而用 Javascript API 来查明对这个函数的任何调用(甚至是深度嵌套调用)。
        Deposit(msg.sender, _id, msg.value);
    }
}

使用 JavaScript API 调用事件的用法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var abi = /* abi 由编译器产生 */;
var ClientReceipt = web3.eth.contract(abi);
var clientReceipt = ClientReceipt.at("0x1234...ab67" /* 地址 */);

var event = clientReceipt.Deposit();

// 监视变化
event.watch(function(error, result){
    // 结果包括对 `Deposit` 的调用参数在内的各种信息。
    if (!error)
        console.log(result);
});

// 或者通过回调立即开始观察
var event = clientReceipt.Deposit(function(error, result) {
    if (!error)
        console.log(result);
});
  • event 最多有三个成员变量可以设置为索引;
  • 如果数组(包括 string 和 bytes)类型被标记为索引项,则它们的 keccak-256 哈希值会被作为 topic 保存;
  • 事件签名的哈希值是 topic 之一
  • 如果用 anonymous 声明匿名事件,则上条无效。匿名事件无法通过名字来过滤;
  • 所有非索引参数都将存储在日志的数据部分中;
  • 索引参数本身不会被保存。你只能搜索它们的值(来确定相应的日志数据是否存在),而不能获取它们的值本身。

事件的底层日志接口

log0log1log2log3log4 为日志的底层接口。logi 接受 i + 1 个 bytes32 类型的参数。其中第一个参数会被用来做为日志的数据部分,其它的会做为 topic:

1
2
3
4
5
6
7
8
9
10
11
12
13
pragma solidity ^0.4.10;

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

其中的长十六进制数的计算方法是 keccak256("Deposit(address,hash256,uint256)"),即事件的签名。

继承

合约通过 is 关键字继承自另一个合约:

1
2
3
contract SubContract is SuperContract {
    ...
}

构造函数的继承:

派生合约需要提供基类构造函数需要的所有参数。这可以通过两种方式来完成:

1
2
3
4
5
6
7
8
9
10
11
pragma solidity ^0.4.0;

contract Base {
    uint x;
    function Base(uint _x) public { x = _x; }
}

contract Derived is Base(7) {
    function Derived(uint _y) Base(_y * _y) public {
    }
}

一种方法直接在继承列表中调用基类构造函数 is Base(7)。 另一种方法是像 modifier 使用方法一样, 作为派生合约构造函数定义头的一部分,Base(_y * _y)。 如果构造函数参数是常量并且定义或描述了合约的行为,使用第一种方法比较方便。 如果基类构造函数的参数依赖于派生合约,那么必须使用第二种方法。
如果同时使用了两种方式调用了基类的构造函数,优先使用 modifier 风格的传参。

多重继承

编程语言实现多重继承需要解决几个问题。 一个问题是 钻石问题。 Solidity 借鉴了 Python 的方式并且使用 C3 线性化 强制一个由基类构成的 DAG(有向无环图)保持一个特定的顺序。 这最终保证了唯一化的结果,但也使某些继承方式变为无效。
基类在 is 后面的顺序很重要:

1
2
3
4
5
6
7
// 以下代码编译出错

pragma solidity ^0.4.0;

contract X {}
contract A is X {}
contract C is A, X {}

代码编译出错的原因是 C 要求 X 重写 A (因为定义的顺序是 A, X ), 但是 A 本身要求重写 X,无法解决这种冲突。

可以通过一个简单的规则来记忆: 以从“最接近的基类”(most base-like)到“最远的继承”(most derived)的顺序来指定所有的基类。

当继承导致一个合约具有相同名字的函数和 modifier 时,这会被认为是一个错误。 当事件和 modifier 同名,或者函数和事件同名时,同样会被认为是一个错误。 有一种例外情况,状态变量的 getter 可以覆盖一个 public 函数。

重载的执行顺序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
pragma solidity ^0.4.0;

contract owned {
    function owned() public { owner = msg.sender; }
    address owner;
}

contract mortal is owned {
    function kill() public {
        if (msg.sender == owner) selfdestruct(owner);
    }
}

contract Base1 is mortal {
    function kill() public { /* 清除操作 1 */ super.kill(); }
}


contract Base2 is mortal {
    function kill() public { /* 清除操作 2 */ super.kill(); }
}

contract Final is Base1, Base2 {
}

如果 Base2 调用 super 的函数,它不会简单在其基类合约上调用该函数。 相反,它在最终的继承关系图谱的下一个基类合约中调用这个函数,所以它会调用 Base1.kill() (注意最终的继承序列是——从最远派生合约开始:Final, Base2, Base1, mortal, ownerd)。 在类中使用 super 调用的实际函数在当前类的上下文中是未知的,尽管它的类型是已知的。 这与普通的虚拟方法查找类似。

一个完整的例子

  • owned 合约可以检查所有者;
  • mortal 合约可以自毁;
  • Config 和 NameReg 是两个接口,Config 用来查找,NameReg 用来注册和取消注册;
  • named 合约多重继承了 owned, mortal。在创建时,named 合约首先从 0xD5f9D8 内查找了编号为 1 的合约 config,随后在 config 中注册了 name,又重写了自毁函数 kill,在自毁之前取消注册了 name,取消注册后,调用被重载的 kill;
  • PriceFeed 多重继承自 owned, mortal, named, 并在声明列表直接调用了 named 的构造函数,传入了 GoldFeed;
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
pragma solidity ^0.4.16;

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

// 使用 is 从另一个合约派生。派生合约可以访问所有非私有成员,包括内部函数和状态变量,
// 但无法通过 this 来外部访问。
contract mortal is owned {
    function kill() {
        if (msg.sender == owner) selfdestruct(owner);
    }
}

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

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

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

    // 函数可以被另一个具有相同名称和相同数量/类型输入的函数重载。
    // 如果重载函数有不同类型的输出参数,会导致错误。
    // 本地和基于消息的函数调用都会考虑这些重载。
    function kill() public {
        if (msg.sender == owner) {
            Config config = Config(0xD5f9D8D94886E70b06E474c3fB14Fd43E2f23970);
            NameReg(config.lookup(1)).unregister();
            // 仍然可以调用特定的重载函数:
            mortal.kill();
            // 但是建议使用 super 调用上一个重载函数:
            super.kill();
        }
    }
}

// 如果构造函数接受参数,
// 则需要在声明(合约的构造函数)时提供,
// 或在派生合约的构造函数位置以修饰器调用风格提供(见下文)。
contract PriceFeed is owned, mortal, named("GoldFeed") {
    ...
}

抽象合约

合约函数可以缺少实现,如下例所示(请注意函数声明头由 ; 结尾):

1
2
3
4
5
pragma solidity ^0.4.0;

contract Feline {
    function utterance() public returns (bytes32);
}

这些合约无法成功编译(即使它们除了未实现的函数还包含其他已经实现了的函数),但他们可以用作基类合约:

1
2
3
4
5
6
7
8
9
pragma solidity ^0.4.0;

contract Feline {
    function utterance() public returns (bytes32);
}

contract Cat is Feline {
    function utterance() public returns (bytes32) { return "miaow"; }
}

如果合约继承自抽象合约,并且没有通过重写来实现所有未实现的函数,那么它本身就是抽象的。

接口

接口类似于抽象合约,合约可以继承接口。但是接口不能实现任何函数。相比于抽象类,接口还有进一步的限制:

1
2
3
4
5
无法继承其他合约或接口。
无法定义构造函数。
无法定义变量。
无法定义结构体
无法定义枚举。

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

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

1
2
3
4
5
pragma solidity ^0.4.11;

interface Token {
    function transfer(address recipient, uint amount) public;
}

库与合约类似,它们只需要在特定的地址部署一次,并且它们的代码可以通过 EVM 的 DELEGATECALL 特性进行重用。
这意味着如果库函数被调用,它的代码在调用合约的上下文中执行,即 this 指向调用合约,特别是可以访问调用合约的存储。 因为每个库都是一段独立的代码,所以它仅能访问调用合约明确提供的状态变量(否则它就无法通过名字访问这些变量)。 因为我们假定库是无状态的,所以如果它们不修改状态(也就是说,如果它们是 view 或者 pure 函数),库函数仅可以通过直接调用来使用(即不使用 DELEGATECALL 关键字), 特别是,除非能规避 Solidity 的类型系统,否则是不可能销毁任何库的。

库可以看作是使用他们的合约的隐式的基类合约。虽然它们在继承关系中不会显式可见,但调用库函数与调用显式的基类合约十分类似 (如果 L 是库的话,可以使用 L.f() 调用库函数)。此外,就像库是基类合约一样,对所有使用库的合约,库的 internal 函数都是可见的。 当然,需要使用内部调用约定来调用内部函数,这意味着所有内部类型,内存类型都是通过引用而不是复制来传递。 为了在 EVM 中实现这些,内部库函数的代码和从其中调用的所有函数都在编译阶段被拉取到调用合约中,然后使用一个 JUMP 调用来代替 DELEGATECALL。

下面的示例说明如何使用库(但也请务必看看 using for 有一个实现 set 更好的例子):

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
pragma solidity ^0.4.16;

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

  // 注意第一个参数是“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 {
    Set.Data knownValues;

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

当然,你不必按照这种方式去使用库:它们也可以在不定义结构数据类型的情况下使用。 函数也不需要任何存储引用参数,库可以出现在任何位置并且可以有多个存储引用参数。

调用 Set.contains,Set.insert 和 Set.remove 都被编译为外部调用( DELEGATECALL )。 如果使用库,请注意实际执行的是外部函数调用。 msg.sender, msg.value 和 this 在调用中将保留它们的值, (在 Homestead 之前,因为使用了 CALLCODE,改变了 msg.sender 和 msg.value)。

以下示例展示了如何在库中使用内存类型和内部函数来实现自定义类型,而无需支付外部函数调用的开销:

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.4.16;

library BigInt {
    struct bigint {
        uint[] limbs;
    }

    function fromUint(uint x) internal pure returns (bigint r) {
        r.limbs = new uint[](1);
        r.limbs[0] = x;
    }

    function add(bigint _a, bigint _b) internal pure returns (bigint r) {
        r.limbs = new uint[](max(_a.limbs.length, _b.limbs.length));
        uint carry = 0;
        for (uint i = 0; i < r.limbs.length; ++i) {
            uint a = limb(_a, i);
            uint b = limb(_b, i);
            r.limbs[i] = a + b + carry;
            if (a + b < a || (a + b == uint(-1) && carry > 0))
                carry = 1;
            else
                carry = 0;
        }
        if (carry > 0) {
            // 太差了,我们需要增加一个 limb
            uint[] memory newLimbs = new uint[](r.limbs.length + 1);
            for (i = 0; i < r.limbs.length; ++i)
                newLimbs[i] = r.limbs[i];
            newLimbs[i] = carry;
            r.limbs = newLimbs;
        }
    }

    function limb(bigint _a, uint _limb) internal pure returns (uint) {
        return _limb < _a.limbs.length ? _a.limbs[_limb] : 0;
    }

    function max(uint a, uint b) private pure returns (uint) {
        return a > b ? a : b;
    }
}

contract C {
    using BigInt for BigInt.bigint;

    function f() public pure {
        var x = BigInt.fromUint(7);
        var y = BigInt.fromUint(uint(-1));
        var z = x.add(y);
    }
}

由于编译器无法知道库的部署位置,我们需要通过链接器将这些地址填入最终的字节码中 (请参阅 使用命令行编译器 以了解如何使用命令行编译器来链接字节码)。 如果这些地址没有作为参数传递给编译器,编译后的十六进制代码将包含 __Set______ 形式的占位符(其中 Set 是库的名称)。 可以手动填写地址来将那 40 个字符替换为库合约地址的十六进制编码。

与合约相比,库的限制:

1
2
3
没有状态变量
不能够继承或被继承
不能接收以太币

库的调用保护

如果库的方法有状态(不是 view 函数也不是 pure 函数),通过 CALL 来执行,而不是 DELEGATECALL 或者 CALLCODE,那么执行的结果会被回退。

EVM 没有为合约提供检测是否使用 CALL 的直接方式,但是合约可以使用 ADDRESS 操作码找出正在运行的“位置”。 生成的代码通过比较这个地址和构造时的地址来确定调用模式。

更具体地说,库的运行时代码总是从一个 push 指令开始,它在编译时是 20 字节的零。当部署代码运行时,这个常数 被内存中的当前地址替换,修改后的代码存储在合约中。在运行时,这导致部署时地址是第一个被 push 到堆栈上的常数, 对于任何 non-view 和 non-pure 函数,调度器代码都将对比当前地址与这个常数是否一致。

合约的函数扩充:using * for *

下例展示了为内建的 uint[] 类型增加了 indexOf 函数:

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
pragma solidity ^0.4.16;

library Search {
    function indexOf(uint[] storage self, uint value)
        public
        view
        returns (uint)
    {
        for (uint i = 0; i < self.length; i++)
            if (self[i] == value) return i;
        return uint(-1);
    }
}

contract C {
    using Search for uint[];
    uint[] data;

    function append(uint value) public {
        data.push(value);
    }

    function replace(uint _old, uint _new) public {
        // 执行库函数调用
        uint index = data.indexOf(_old);
        if (index == uint(-1))
            data.push(_new);
        else
            data[index] = _new;
    }
}

安全考量

随机性

在智能合约中你所用的一切都是公开可见的,即便是局部变量和被标记成 private 的状态变量也是如此。

译者注:在智能合约中使用随机数很难保证节点不作弊, 这是因为智能合约中的随机数一般要依赖计算节点的本地时间得到, 而本地时间是可以被恶意节点伪造的,因此这种方法并不安全。 通行的做法是采用 off-chain 的第三方服务,比如 预言机oracle 来获取随机数)。

防范重入风险

重入

任何从合约 A 到合约 B 的交互以及任何从合约 A 到合约 B 的 ETH 的转移,都会将控制权交给合约 B。 这使得合约 B 能够在交互结束前回调 A 中的代码。 举个例子,下面的代码中有一个 bug(这只是一个代码段,不是完整的合约):

1
2
3
4
5
6
7
8
9
10
11
12
pragma solidity ^0.4.0;

// 不要使用这个合约,其中包含一个 bug。
contract Fund {
    /// 合约中 |ether| 分成的映射。
    mapping(address => uint) shares;
    /// 提取你的分成。
    function withdraw() public {
        if (msg.sender.send(shares[msg.sender]))
            shares[msg.sender] = 0;
    }
}

这里的问题不是很严重,因为有限的 gas 也作为 send 的一部分,但仍然暴露了一个缺陷: ETH 的传输过程中总是可以包含代码执行,所以接收者可以是一个回调进入 withdraw 的合约。 这就会使其多次得到退款,从而将合约中的全部 ETH 提取。 特别地,下面的合约将允许一个攻击者多次得到退款,因为它使用了 call ,默认发送所有剩余的 gas。

1
2
3
4
5
6
7
8
9
10
11
12
pragma solidity ^0.4.0;

// 不要使用这个合约,其中包含一个 bug。
contract Fund {
    /// 合约中 |ether| 分成的映射。
    mapping(address => uint) shares;
    /// 提取你的分成。
    function withdraw() public {
        if (msg.sender.call.value(shares[msg.sender])())
            shares[msg.sender] = 0;
    }
}

为了避免重入,你可以使用下面撰写的“检查-生效-交互”(Checks-Effects-Interactions)模式:

1
2
3
4
5
6
7
8
9
10
11
12
pragma solidity ^0.4.11;

contract Fund {
    /// 合约中 |ether| 分成的映射。
    mapping(address => uint) shares;
    /// 提取你的分成。
    function withdraw() public {
        var share = shares[msg.sender];
        shares[msg.sender] = 0;
        msg.sender.transfer(share);
    }
}

请注意重入不仅是 ETH 传输的其中一个影响,还包括任何对另一个合约的函数调用。 更进一步说,你也不得不考虑多合约的情况。 一个被调用的合约可以修改你所依赖的另一个合约的状态。

gas 限制和循环

必须谨慎使用没有固定迭代次数的循环,例如依赖于 存储storage 值的循环:由于区块 gas 有限,交易只能消耗一定数量的 gas。 无论是明确指出的还是正常运行过程中的,循环中的数次迭代操作所消耗的 gas 都有可能超出区块的 gas 限制,从而导致整个合约在某个时刻骤然停止。 这可能不适用于只被用来从区块链中读取数据的 view 函数。 尽管如此,这些函数仍然可能会被其它合约当作 链上on-chain 操作的一部分来调用,并使那些操作骤然停止。 请在合约代码的说明文档中明确说明这些情况。

发送和接收 ETH

  • 目前无论是合约还是“外部账户”都不能阻止有人给它们发送 ETH。合约可以对一个正常的转账做出反应并拒绝它,但还有些方法可以不通过创建消息来发送 ETH。其中一种方法就是单纯地向合约地址“挖矿”,另一种方法就是使用 selfdestruct(x) 。
  • 如果一个合约收到了 ETH,且没有函数被调用,就会执行 fallback 函数。如果没有 fallback 函数,那么 ETH 会被拒收,同时会抛出异常。在 fallback 函数执行过程中,合约只能依靠此时可用的 gas 津贴(2300 gas)来执行。这笔津贴并不足以用来完成任何方式的 storage 访问。为了确保你的合约可以通过这种方式收到 ETH,请你核对 fallback 函数所需的 gas 数量 (在 Remix 的“详细”章节会举例说明)。
  • 有一种方法可以通过使用 addr.call.value(x)() 向接收合约发送更多的 gas。 这本质上跟 addr.transfer(x) 是一样的, 只不过前者发送所有剩余的 gas,并且使得接收者有能力执行更加昂贵的操作 (它只会返回一个错误代码,而且也不会自动传播这个错误)。这可能包括回调发送合约或者你想不到的其它状态改变的情况。 因此这种方法无论是给诚实用户还是恶意行为者都提供了极大的灵活性。
  • 如果你想要使用 address.transfer 发送 ETH,你需要注意以下几个细节:
    • 如果接收者是一个合约,它会执行自己的 fallback 函数,从而可以回调发送 ETH 的合约;
    • 如果调用的深度超过 1024,发送 ETH 也会失败。由于调用者对调用深度有完全的控制权,他们可以强制使这次发送失败;请考虑这种可能性,或者使用 send 并且确保每次都核对它的返回值。 更好的方法是使用一种接收者可以取回 ETH 的方式编写你的合约;
    • 发送 ETH 也可能因为接收方合约的执行所需的 gas 多于分配的 gas 数量而失败 (确切地说,是使用了 require , assert, revert , throw 或者因为这个操作过于昂贵) - “gas 不够用了”。 如果你使用 transfer 或者 send 的同时带有返回值检查,这就为接收者提供了在发送合约中阻断进程的方法。再次说明,最佳实践是使用 “取回”模式而不是“发送”模式。

调用栈深度

外部函数调用随时会失败,因为它们超过了调用栈的上限 1024。 在这种情况下,Solidity 会抛出一个异常。 恶意行为者也许能够在与你的合约交互之前强制将调用栈设置成一个比较高的值。

请注意,使用 .send() 时如果超出调用栈并不会抛出异常,而是会返回 false。低级的函数比如 .call(),.callcode() 和 .delegatecall() 也都是这样的。

tx.origin

永远不要使用 tx.origin 做身份认证。假设你有一个如下的钱包合约:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
pragma solidity ^0.4.11;

// 不要使用这个合约,其中包含一个 bug。
contract TxUserWallet {
    address owner;

    function TxUserWallet() public {
        owner = msg.sender;
    }

    function transferTo(address dest, uint amount) public {
        require(tx.origin == owner);
        dest.transfer(amount);
    }
}

现在攻击者构造了如下攻击合约:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
pragma solidity ^0.4.11;

interface TxUserWallet {
    function transferTo(address dest, uint amount) public;
}

contract TxAttackWallet {
    address owner;

    function TxAttackWallet() public {
        owner = msg.sender;
    }

    function() public {
        TxUserWallet(msg.sender).transferTo(owner, msg.sender.balance);
    }
}

如果你的钱包通过核查 msg.sender 来验证发送方身份,你就会得到恶意钱包的地址,而不是所有者的地址。但是通过核查 tx.origin ,得到的就会是启动交易的原始地址,它仍然会是所有者的地址。恶意钱包会立即将你的资金抽出。

脏高位问题

for (var i = 0; i < arrayName.length; i++) { ... } 中, i 的类型会变为 uint8 , 因为这是保存 0 值所需的最小类型。如果数组超过 255 个元素,则循环不会终止。 不占用完整 32 字节的类型可能包含“脏高位”。这在当你访问 msg.data 的时候尤为重要 —— 它带来了延展性风险: 你既可以用原始字节 0xff000001 也可以用 0x00000001 作为参数来调用函数 f(uint8 x) 以构造交易。 这两个参数都会被正常提供给合约,并且 x 的值看起来都像是数字 1, 但 msg.data 会不一样,所以如果你无论怎么使用 keccak256(msg.data),你都会得到不同的结果。

认真对待警告

最好修复编译器给出的警告。即使你不认为这个特定的警告不会产生安全隐患,因为那也有可能埋藏着其他的问题。
任何编译器警告,都可以通过轻微的修改来去掉。

同时也请尽早添加 pragma experimental "v0.5.0"; 来允许 0.5.0 版本的安全特性。注意在这种情况下,experimental 并不意味着任何有风险的安全特性, 它只是可以允许一些在当前版本还不支持的 Solidity 特性,来提供向后的兼容。

限定 ETH 的数量

限定 storage 在一个智能合约中 ETH(或者其它 token)的数量。 如果你的源代码、编译器或者平台出现了 bug,可能会导致这些资产丢失。如果你想控制你的损失,就要限定 以太币Ether 的数量。

保持合约简练且模块化

保持你的合约短小精炼且易于理解。找出无关于其它合约或库的功能。有关源码质量可以采用的一般建议:限制局部变量的数量以及函数的长度等等。 将实现的函数文档化,这样别人看到代码的时候就可以理解你的意图,并判断代码是否按照正确的意图实现。

使用“检查-生效-交互”(Checks-Effects-Interactions)模式

大多数函数会首先做一些检查工作(例如谁调用了函数,参数是否在取值范围之内,它们是否发送了足够的 以太币Ether ,用户是否具有通证等等)。 这些检查工作应该首先被完成。 第二步,如果所有检查都通过了,应该接着进行会影响当前合约状态变量的那些处理。 与其它合约的交互应该是任何函数的最后一步。
早期合约延迟了一些效果的产生,为了等待外部函数调用以非错误状态返回。 由于上文所述的重入问题,这通常会导致严重的后果。 请注意,对已知合约的调用反过来也可能导致对未知合约的调用,所以最好是一直保持使用这个模式编写代码。

包含故障-安全(Fail-Safe)模式

尽管将系统完全去中心化可以省去许多中间环节,但包含某种故障-安全模式仍然是好的做法,尤其是对于新的代码来说:
你可以在你的智能合约中增加一个函数实现某种程度上的自检查,比如 “ETH 是否会泄露?”, “通证的总和是否与合约的余额相等?”等等。 请记住,你不能使用太多的 gas,所以可能需要通过 off-chain 计算来辅助。
如果自检查没有通过,合约就会自动切换到某种“故障安全”模式,例如,关闭大部分功能,将控制权交给某个固定的可信第三方,或者将合约转换成一个简单的“退回我的钱”合约。

Over

Comments