Solidity

Table of Contents

1. Solidity 简介

Solidity 是区块链智能合约开发语言,由 Ethereum 核心开发者 Gavin Wood 于 2014 年创造。

2. 源文件结构

Solidity 源文件由下面几个部分组成:

  1. pragma directives
  2. import directives
  3. contract definitions
  4. struct definitions
  5. enum definitions

2.1. Pragma

关键字 pragma 用于告诉编译器启用哪些 features 或者 checks。下面是一些例子:

pragma solidity ^0.5.2;             // 指定编译器版本,格式和 npm semver 相同
pragma experimental ABIEncoderV2;   // enable ABIEncoderV2
pragma experimental SMTChecker;     // enable SMTChecker

需要说明的是, pragma 仅仅适用于当前文件 ,就算 import 了其它文件,那么其它文件中的 pragma 不会在当前文件中生效。

2.2. Import

import 和 ES6 中类似,如:

import "filename";                       // 导入 "filename" 中所有符号
import * as symbolName from "filename";  // 导入 "filename" 中所有符号,使用时要加上symbolName,如symbolName.symbol1
import "filename" as symbolName;         // 和上一行相同
import {symbol1 as alias, symbol2} from "filename"; // 导入 "filename" 中的 symbol1(且改为 alias)和 symbol2

如果你确定导入的文件和当前文件在同一个目录中,那么最好这样写:

import "./filename"           # 导入和当前文件在同一个目录中的文件

而不推荐这样写:

import "filename"             # 可能会导入 global “include directory” 中的文件

2.3. 注释

和 JS 一样,Solidity 中单行注释为 // ,多行注释为 /* */

采用 /// (或者 /** */ )时,表示采用 Ethereum Natural Language Specification Format (NatSpec) 所识别的注释。如:

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.5.0 <0.7.0;

/// @title A simulator for trees
/// @author Larry A. Gardner
/// @notice You can use this contract for only the most basic simulation
/// @dev All function calls are currently implemented without side effects
contract Tree {
    /// @notice Calculate tree age in years, rounded up, for live trees
    /// @dev The Alexandr N. Tetearing algorithm could increase precision
    /// @param rings The number of rings from dendrochronological sample
    /// @return age in years, rounded up for partial years
    function age(uint256 rings) external pure returns (uint256) {
        return rings + 1;
    }
}

2.4. 合约的结构

用关键字 contract 可以定义合约,合约类似于其它语言中的“类”。合约中可以包含:State Variables, Functions, Function Modifiers, Events, Struct Types and Enum Types。

关键字 libraryinterface 可以定义特殊的合约。

2.4.1. 状态变量

状态变量会永久地保存在合约的存储中,如:

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.0 <0.7.0;

contract SimpleStorage {
    uint storedData;          // State variable
    // ...
}

2.4.2. 函数

函数可以定义在合约内,也可以定义在合约外,如:

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.1 <0.9.0;

contract SimpleAuction {
    function bid() public payable { // Function
        // ...
    }
}

// Helper function defined outside of a contract
function helper(uint x) pure returns (uint) {
    return x * 2;
}

2.4.3. 函数修饰器

函数修饰器可以用来以声明的方式改良函数语义,如:

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.22 <0.7.0;

contract Purchase {
    address public seller;

    modifier onlySeller() {        // 这是函数修饰器的定义
        require(
            msg.sender == seller,
            "Only seller can call this."
        );
        _;
    }

    function abort() public view onlySeller { // 使用函数修饰器
        // ...
    }
}

2.4.4. 事件

通过事件(Event)可以使用以太坊虚拟机的日志功能。

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.21 <0.7.0;

contract SimpleAuction {
    event HighestBidIncreased(address bidder, uint amount); // 声明 Event

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

2.4.5. 错误

在 Solidity 0.8.4 中引入了 error 关键字,可以用于定义错误,在 revert 语句中可以使用这个错误,如:

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.4;

/// Not enough funds for transfer. Requested `requested`,
/// but only `available` available.
error NotEnoughFunds(uint requested, uint available);

contract Token {
    mapping(address => uint) balances;
    function transfer(address to, uint amount) public {
        uint balance = balances[msg.sender];
        if (balance < amount)
            revert NotEnoughFunds(amount, balance);
        balances[msg.sender] -= amount;
        balances[to] += amount;
        // ...
    }
}

在引入 error 关键字之前,我们可以直接使用 revert 来报告一个错误消息,如:

revert("Not enough Ether provided.");

但这种方式只接受一个字符串参数,不方便接受额外的信息,如还想报告“请求的数量”及“当前可用余额”就很麻烦。使用 error 关键字定制错误则可以解决这个问题。

2.4.5.1. Revert reason string

下面通过两个例子来介绍 Revert reason string 的格式。

假设合约执行到了语句 revert("Not enough Ether provided."); ,那么 eth_estimateGas 或者 eth_call 会返回消息:

{
  "jsonrpc":"2.0",
  "id":1,
  "error": {
    "code":3,
    "data":"0x08c379a00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000001a4e6f7420656e6f7567682045746865722070726f76696465642e000000000000",
    "message":"execution reverted: Not enough Ether provided."
  }
}

其中 data 数据的具体说明如下:

0x08c379a0                                                         // Function selector for Error(string)
0x0000000000000000000000000000000000000000000000000000000000000020 // Data offset
0x000000000000000000000000000000000000000000000000000000000000001a // String length
0x4e6f7420656e6f7567682045746865722070726f76696465642e000000000000 // String data

假设合约执行到了语句 revert NotEnoughFunds(10,5); ,那么 eth_estimateGas 或者 eth_call 会返回消息:

{
  "jsonrpc":"2.0",
  "id":1,
  "error":{
    "code":3,
    "data":"0x8c90536800000000000000000000000000000000000000000000000000000000000000050000000000000000000000000000000000000000000000000000000000000005",
    "message":"execution reverted"
  }
}

其中 data 数据的具体说明如下:

0x8c905368                                                         // Function selector for NotEnoughFunds(uint, uint)
0x000000000000000000000000000000000000000000000000000000000000000a // First param (10)
0x0000000000000000000000000000000000000000000000000000000000000005 // Second param (5)

参考:https://docs.soliditylang.org/en/v0.8.12/control-structures.html#revert

2.4.6. 结构体

结构体可以组织多种类型为一个新类型,如:

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.0 <0.7.0;

contract Ballot {
    struct Voter {          // Struct
        uint weight;
        bool voted;
        address delegate;
        uint vote;
    }
}

除了在 contract 中定义结构体外,也可以直接在源文件中定义结构体,即和 contract 平级定义。

2.4.7. 枚举

枚举用于定义一组有限的常量,如:

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.0 <0.7.0;

contract Purchase {
    enum State { Created, Locked, Inactive }    // Enum
}

枚举和整数之间不会进行隐式地转换,不过你可以显式地在枚举和整数之间做转换。枚举中首个常量是 0(而不是 1)。

3. 类型

Solidity 是静态类型语言,需要为每个变量指定类型。Solidity 中没有 undefined 或者 null 的概念,所有新声明的变量总是“其对应类型的默认值”。

3.1. 值类型

值类型的变量总是“passed by value”,也就是作为函数参数时,它们会被复制。

3.1.1. Booleans

关键字 bool 用于声明布尔值,有两个字面常量: true, false

3.1.2. Integers

整数和无符号整数可以分别用下面关键字声明:

int8, int16, int24, int32, int40, ..., int256
uint8, uint16, uint24, uint32, uint40, ..., int256

注, intuint 分别是 int256, uint256 的别名。

每种类型都有相应的表示范围,如 int32 的范围为 -2**31 到 2**31-1;而 uint32 的范围为 0 到 2**32-1。

整数支持下面运算符:

  • 比较运算符: <=, <, ==, !=, >=, >
  • 按位运算符: &~, ~|, ^, ~
  • 移位运算符: <<, >>
  • 算术运算符: +, -, *, /, % (Remainder), ** (Exponentiation)

10**18 = 1e18, 2e5 = 2 * 10 ** 5, 123_000 等都是整数字面量的例子。整数字面量具有任意的精度,比如 (2**800 + 1) - 2**800 会得到整数 1;又如 0.5 * 8 会得到整数 4。

3.1.2.1. uint256 的最大值

怎么表示 uint256 的最大值呢?有下面这些方法:

// SPDX-License-Identifier: MIT
pragma solidity ^0.6.8;

contract Max {
    // 1) Just type in the number!
    uint256 constant public MAX_INT_NUMBER = 115792089237316195423570985008687907853269984665640564039457584007913129639935;

    // 2) Use the hex version of the number
    uint256 constant public MAX_INT_HEX = 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff;

    // 3) Casting -1
    uint256 constant public MAX_INT_CAST = uint256(-1);

    // 4) Using exponentiation
    uint256 constant public MAX_INT_EXPONENTIATION = 2**256 - 1;

    // 5) Type (Solidity 0.6.8)
    uint256 constant public MAX_INT_TYPE = type(uint256).max;
}

建议采用 type(uint256).max 方式,因为它的可读性最好,这种用法是在 Solidity 0.6.8 中引入的。

参考:https://forum.openzeppelin.com/t/using-the-maximum-integer-in-solidity/3000/7?u=abcoathup

3.1.3. Address

Solidity 支持两种地址类型:

  • address :存储 20 字节的地址(这是以太坊地址的大小);
  • address payable :和 address 相同,但支持更多的成员(transfer 和 send)。

类型 address payable 可以隐式地转换为 address ;而类型 addressaddress payable 的转换需要用 payable(<address>) 显式地进行。

类型 addressaddress payable 支持的成员如表 1 所示。

Table 1: 地址类型支持的成员
成员 address address payable 说明
.balance  
.transfer(uint256 amount) x gas limit 固定为 2300,向合约转账可能失败
.send(uint256 amount) returns (bool) x gas limit 固定为 2300,向合约转账可能失败
.call(bytes memory) returns (bool, bytes memory)  
.delegatecall(bytes memory) returns (bool, bytes memory)  
.staticcall(bytes memory) returns (bool, bytes memory) 和 call 相同,但 staticcall 不能改变合约的状态

下面是满足指定条件下从当前合约地址转账 10 wei 给地址 x 的例子:

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

地址类型也有 literal 表达,如下所示:

contract Sample {
  payable address myAddress = payable(0xdCad3a6d3569DF655070DEd06cb7A1b2Ccd1D3AF); // 必须是大小写混合形式(EIP-55)
}
3.1.3.1. 转账的三种方式(推荐使用 call)

我们知道使用 transfer/send/call 都可以进行转账。下面是分别使用这三个方法向 _to 地址转账 amount ether 的例子:

# 方法一(gas limit 固定为 2300,向合约转账可能失败):
_to.transfter(amount);

# 方法二(gas limit 固定为 2300,向合约转账可能失败):
bool success = _to.send(amount);
require(success, "Transfer failed");

# 方法三(推荐):
(bool success, ) = _to.call{value: amount}("");
require(success, "Transfer failed");

参考:https://consensys.io/diligence/blog/2019/09/stop-using-soliditys-transfer-now/

3.1.4. Fixed-size byte arrays

值类型 bytes1, bytes2, bytes3, …, bytes32 表示字节序列,其大小分别占 1 到 32 字节。

注: bytebytes1 的别名。

bytes1, bytes2, bytes3, …, bytes32 赋值时,必须宽度匹配,不过 0 或者 0x0 可以转换为任意宽度,如:

bytes2 a = 54321; // not allowed
bytes2 b = 0x12; // not allowed
bytes2 c = 0x123; // not allowed
bytes2 d = 0x1234; // fine
bytes2 e = 0x0012; // fine
bytes4 f = 0; // fine
bytes4 g = 0x0; // fine

字符串或者 hex 字符串,如果宽度匹配,则可以隐式转换为 bytes1, bytes2, bytes3, …, bytes32 ,如:

bytes2 a = hex"1234"; // fine
bytes2 b = "xy"; // fine
bytes2 c = hex"12"; // not allowed
bytes2 d = hex"123"; // not allowed
bytes2 e = "x"; // not allowed
bytes2 f = "xyz"; // not allowed

3.2. 引用类型

Solidity 中,引用类型有三种:数组、结构体、Mapping。

3.2.1. 引用类型变量的保存位置

对于引用类型的变量,需要声明数据保存在什么位置,有三个可能的保存位置:

  • memory,仅在函数调用期间有效,是易失的;
  • storage,和整个合约的生命周期一样;
  • calldata,这是个特别的地方,保存函数参数,这个位置的数据是只读的。
3.2.1.1. 保存位置会影响赋值语义

引用类型变量的保存位置会影响赋值的语义:

  1. storage 和 memory 之间的赋值,会引发底层数据的复制。
  2. 从 calldata 到 storage 的赋值,会引发底层数据的复制。
  3. memory 和 memory 之间的赋值,不会引发底层数据的复制,仅仅是一份数据有多个名称而且。
  4. 从 storage 到 local storage 的赋值,不会引发底层数据的复制。
  5. 其它所有形式的 storage 和 storage 之间的赋值,会引发底层数据的复制。

上面提到的 local storage 变量是指在函数内定义的 storage 变量,对于 local storage 变量,我们只能够把一个事先存在的 storage 变量赋值给它(这不会引发底层数据的复制)。

下面是一些例子:

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.5.0 <0.7.0;

contract C {
    // The data location of x is storage.
    // This is the only place where the
    // data location can be omitted.
    uint[] x;

    // The data location of memoryArray is memory.
    function f(uint[] memory memoryArray) public {
        x = memoryArray; // 引发底层数据复制,规则 1
        uint[] storage y = x; // 不会引发底层数据复制,y 是 local storage,规则 4
        y[7]; // fine, returns the 8th element
        y.pop(); // fine, modifies x through y

        g(x); // calls g, handing over a reference to x,会修改 x 下标为 1 的元素为 3
        h(x); // calls h and creates an independent, temporary copy in memory,不会修改 x
    }

    function g(uint[] storage z) internal {
        z[1] = 3;
    }
    function h(uint[] memory z) internal pure {
        z[1] = 4;
    }
}

下面再看一个例子,展示 storage 之间的赋值:

contract Demo {
    uint[2] stateArray1 = [1, 2];  // storage
    uint[2] stateArray2 = [3, 4];  // storage

    function getUint() returns (uint) {
        stateArray1 = stateArray2;    // 引发底层数据复制,规则 5
        stateArray2[1] = 5;           // 修改 stateArray2 时,不会影响到 stateArray1

        return stateArray1[1];        // 4
    }
}

3.2.2. 数组

数组可以在编译时就确定大小,也可以在运行时确定大小。

下面是数组的例子:

contract C {
    int[] x;       // 动态大小
    int[10] y;     // 固定大小
}

如果数组保存在 storage 中,则可以直接使用 .push() 方法往往数组后面增加新元素。如果数组保存在 memory 中,则不能使用 .push() 来改变其大小,参考节 3.2.2.2

3.2.2.1. bytes, string

bytes 是动态大小的字节数组, string 是动态大小的 UTF-8 编码字符串,它们都是动态大小的数组。

注: byte[]bytes 类似,也是动态大小的字节数组;但 byte[] 在元素之间有 padding,它比 bytes 更占资源,所以 推荐使用更轻量的 bytes 当然,如果你的大小是固定的,则应该使用值类型 bytes1bytes32 ,它们比动态数组 bytes 更轻量,参见节 3.1.4

3.2.2.2. memory 中的数组

保存在 memory 中的动态大小的数组,可以使用 new 关键字创建,一旦指定了大小,大小就固定不变了(即不能使用 .push() 方法)。

下面是 memory 中的数组的例子:

pragma solidity >=0.4.16 <0.7.0;

contract C {
    function f(uint len) public pure {
        uint[] memory a = new uint[](7);
        bytes memory b = new bytes(len);
        assert(a.length == 7);
        assert(b.length == len);
        a[6] = 8;
    }
}
3.2.2.3. 数组字面量

数组字面量用方括号表示,如 [1, 2, 3]

数组字面量 [1, 2, 3] 的类型是 uint8[3] memory ,也就是:保存在 memory 中的大小为 3 的 uint8 数组。

如果想把 [1, 2, 3] 赋值给其它类型的数组,把其首个元素进行强制类型转换即可,如:

contract C {
    function f() public pure {
        g([int8(1), 2, 3]);     // g 参数类型是 int8 数组,[1, 2, 3] 中首个元素需要强制类型转换
    }
    function g(int8[3] memory) public pure {
        // ...
    }
}
3.2.2.4. 数组成员总结

数组的成员如表 2 所示。这其中只有 length 可用于 string,其它 3 个都不能用于 string。

Table 2: 数组成员
成员 含义 说明
length 返回数组的长度  
push() 往数组末尾增加对应类型的零值元素(默认值),返回新元素引用。所以 x.push() = 6 相当于 x.push(6) 仅当数组保存在 storage,且是动态大小时才可用
push(x) 往数组末尾增加元素,不返回任何值 仅当数组保存在 storage,且是动态大小时才可用
pop() 从数组末尾移除一个元素,隐含对元素调用 delete 仅当数组保存在 storage,且是动态大小时才可用

3.2.3. 数组 Slices

Array slices are a view on a contiguous portion of an array. They are written as x[start:end], the first element of the slice is x[start] and the last element is x[end - 1].

As of now, array slices are only implemented for calldata arrays.

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.6.0 <0.7.0;

contract Proxy {
    /// @dev Address of the client contract managed by proxy i.e., this contract
    address client;

    constructor(address _client) public {
        client = _client;
    }

    /// Forward call to "setOwner(address)" that is implemented by client
    /// after doing basic validation on the address argument.
    function forward(bytes calldata _payload) external {
        // Since ABI decoding requires padded data, we cannot
        // use 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.");
    }
}

3.2.4. 结构体

下面是结构体的实例:

contract C {
    struct Person {
        string name;
        uint age;
    }

    function f() public pure {
        Person memory p1 = Person("David", 10);
    }
}

在创建结构体变量时,不用使用 new 关键字。 new 用于创建合约实例,或者动态数组。

3.2.5. Mapping

使用语法 mapping(_KeyType => _ValueType) _VariableName 可以创建 Mapping 变量。其中 _KeyType 可以是任何内置的值类型、 bytesstring 或者合约或者枚举,但 _KeyType 不能是用户定义的复杂类型,不能是 Mapping,不能是 struct,不能是其它数组类型。 _ValueType 可以是任意类型。

Mapping 可以认为是其它语言中的 Hash Table。 Mapping 中并不会保存 Key 本身,而是使用 Key 的 keccak256 哈希进行查询和定位。如果 Key 不在 Mapping 中,那么你会得到 Value 对应类型的默认值(比如整数默认就是 0)。也就是说,没有办法区分下面两种情况:1、Key 不存在于 Mapping 中,2、Key 存在,但其 Value 为对应类型的默认值。

Mapping 只能保存在 storage 中,不能保存在 memory 或者 calldata 中。

3.2.5.1. 遍历 Mapping

Solidity 中的 Mapping 不支持遍历,无法直接知道 Mapping 中保存了多少数据。

我们可以在原生的 Mapping 基础上进行封装,得到可以遍历的 Mapping,如:

pragma solidity >=0.6.0 <0.7.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;
    }
}

// How to use it
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;
        }
    }
}

3.2.6. delete

delete a 的作用是把 a 赋值为其对应类型的默认值。

如果 a 是整数,则 delete a 相当于 a=0 (0 是整数的默认值)。
如果 a 是动态数组,则 delete a 是把 a 赋值为大小为 0 的动态数组。
如果 a 是固定大小的数组,则 delete a 是把 a 中的每个元素都赋值为对应类型的默认值。
如果 a 是数组, delete a[x] 的作用是删除下标为 x 的元素,但整个数组的长度不变,其元素也不变。这相当于数组出现了空洞。注:如果你想删除元素,mapping 是更好的选择。
如果 a 是结构体,则 delete a 会让其每个成员(mapping 类型的成员除外)都赋值为对应类型的默认值。
如果 a 是 mapping,则 delete a 没有效果,编译器会报错。
如果 a 是 mapping,则 delete a[x] 相当于设置 a 中 Key x 对应的 Value 为类型默认值。

下面再看一个例子:

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.0 <0.7.0;

contract DeleteExample {
    uint data;
    uint[] dataArray;

    function f() public {
        uint x = data;
        delete x; // sets x to 0, does not affect data
        delete data; // sets data to 0, does not affect x
        uint[] storage y = dataArray;
        delete dataArray; // this sets dataArray.length to zero, but as uint[] is a complex object, also
        // y is affected which is an alias to the storage object
        // On the other hand: "delete y" is not valid, as assignments to local variables
        // referencing storage objects can only be made from existing storage objects.
        assert(y.length == 0);
    }
}

在上面例子中,不能使用 delete y ,这相同于把 y 赋值为大小为 0 的动态数组,但 y 是 local storage 变量,我们只能够把一个事先存在的 storage 变量赋值给 local storage 变量,所以不能使用 delete y

4. 单位

表示以太单位时,数字字面量后面可以加上单位 wei, gwei, finney, szabo or ether ,不加单位默认是 wei 。下面成立:

assert(1 wei == 1);
assert(1 gwei == 1e9);
assert(1 szabo == 1e12);
assert(1 finney == 1e15);
assert(1 ether == 1e18);

表示时间时,数字字面量后面可以加上单位 seconds, minutes, hours, days and weeks ,不加单位默认是 seconds 。下面成立:

1 == 1 seconds
1 minutes == 60 seconds
1 hours == 60 minutes
1 days == 24 hours
1 weeks == 7 days

不过,由于有 Leap second ,所以每天不一定是 24 小时。

5. 全局可用变量或者函数

有一些变量或者函数在全局命名空间可用,下面将介绍给它们。

5.1. Block and Transaction Properties

  • blockhash(uint blockNumber) returns (bytes32): hash of the given block - only works for 256 most recent, excluding current, blocks
  • block.basefee (uint): current block’s base fee (EIP-3198 and EIP-1559)
  • block.chainid (uint): current chain id
  • block.coinbase (address payable): current block miner’s address
  • block.difficulty (uint): current block difficulty
  • block.gaslimit (uint): current block gaslimit
  • block.number (uint): current block number
  • block.timestamp (uint): current block timestamp as seconds since unix epoch
  • gasleft() returns (uint256): remaining gas
  • msg.data (bytes calldata): complete calldata
  • msg.sender (address payable): sender of the message (current call)
  • msg.sig (bytes4): first four bytes of the calldata (i.e. function identifier)
  • msg.value (uint): number of wei sent with the message
  • now (uint): current block timestamp (alias for block.timestamp)
  • tx.gasprice (uint): gas price of the transaction
  • tx.origin (address payable): sender of the transaction (full call chain)

In a call chain A->B->C->D, inside D msg.sender will be C, and tx.origin will be A. tx.origin 一定是个 EOA 帐户,如果合约中检测 msg.sender == tx.origin 成立,则说明这次调用者( msg.ender )是一个 EOA 帐户,而不是某个合约帐户。

5.2. ABI Encoding and Decoding Functions

  • abi.decode(bytes memory encodedData, (...)) returns (...): ABI-decodes the given data, while the types are given in parentheses as second argument. Example: (uint a, uint[2] memory b, bytes memory c) = abi.decode(data, (uint, uint[2], bytes))
  • abi.encode(...) returns (bytes memory): ABI-encodes the given arguments
  • abi.encodePacked(...) returns (bytes memory): Performs packed encoding of the given arguments. Note that packed encoding can be ambiguous!
  • abi.encodeWithSelector(bytes4 selector, ...) returns (bytes memory): ABI-encodes the given arguments starting from the second and prepends the given four-byte selector
  • abi.encodeWithSignature(string memory signature, ...) returns (bytes memory): Equivalent to abi.encodeWithSelector(bytes4(keccak256(bytes(signature))), ...)`

5.3. Error Handling

  • assert(bool condition) causes an invalid opcode and thus state change reversion if the condition is not met - to be used for internal errors.
  • require(bool condition) reverts if the condition is not met - to be used for errors in inputs or external components.
  • require(bool condition, string memory message) reverts if the condition is not met - to be used for errors in inputs or external components. Also provides an error message.
  • revert() abort execution and revert state changes
  • revert(string memory reason) abort execution and revert state changes, providing an explanatory string

5.4. Mathematical and Cryptographic Functions

  • addmod(uint x, uint y, uint k) returns (uint) compute (x + y) % k where the addition is performed with arbitrary precision and does not wrap around at 2**256. Assert that k != 0 starting from version 0.5.0.
  • mulmod(uint x, uint y, uint k) returns (uint) compute (x * y) % k where the multiplication is performed with arbitrary precision and does not wrap around at 2**256. Assert that k != 0 starting from version 0.5.0.
  • keccak256(bytes memory) returns (bytes32) compute the Keccak-256 hash of the input

5.5. Contract Related

  • this (current contract’s type): the current contract, explicitly convertible to Address
  • selfdestruct(address payable recipient): Destroy the current contract, sending its funds to the given Address and end execution.

6. 表达式和控制结构

6.1. 控制结构

Solidity 中支持 if, else, while, do, for, break, continue, return 这些控制结构,其语义和 C/JavaScript 中的类似。

6.2. 函数调用形式

6.2.1. 内部函数调用形式(Internal Call)

下面是“内部函数调用”的例子:

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.22 <0.7.0;

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

These function calls are translated into simple jumps inside the EVM. This has the effect that the current memory is not cleared, i.e. passing memory references to internally-called functions is very efficient. Only functions of the same contract instance can be called internally.

You should still avoid excessive recursion, as every internal function call uses up at least one stack slot and there are only 1024 slots available.

6.2.2. 外部函数调用形式(External Call)

this.g(8); 或者 c.g(2); (这里 c 是合约实例)的形式是“外部函数调用”形式。

For an external call, all function arguments have to be copied to memory.

When calling functions of other contracts, you can specify the amount of Wei or gas sent with the call with the special options {value: 10, gas: 10000}.

下面例子中, feed.info{value: 20}(); 外部函数调用,且在调用的同时转账了 20 wei 以太给被调用合约。需要说明的是 info 必须声明为 payable ,否则它不能接收以太。

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.6.2 <0.7.0;

contract InfoFeed {
    function info() public payable returns (uint ret) { return 42; }
}

contract Consumer {
    InfoFeed feed;
    function setFeed(InfoFeed addr) public { feed = addr; }
    function callFeed() public {
        feed.info{value: 20}();       // 外部函数调用,同时转账了 20 wei
    }
}

6.2.3. Named Calls

在调用函数时,可以使用 {arg1: val1, arg2: val2} 语法指定参数名称,这时参数顺序就不无谓了,如:

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.0 <0.7.0;

contract C {
    mapping(uint => uint) data;

    function f() public {
        set({value: 2, key: 3});       // 相当于 set(3, 2);
    }

    function set(uint key, uint value) public {
        data[key] = value;
    }

}

6.3. 函数返回多个值

函数返回多个值时,需要用括号括起来,如:

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.5.0 <0.7.0;

contract C {
    uint index;

    function f() public pure returns (uint, bool, uint) {
        return (7, true, 2);           // 返回多个值,小括号不能省略
    }

    function g() public {
        // Variables declared with type and assigned from the returned tuple,
        // not all elements have to be specified (but the number must match).
        (uint x, , uint y) = f();
        // Common trick to swap values -- does not work for non-value storage types.
        (x, y) = (y, x);
        // Components can be left out (also for variable declarations).
        (index, , ) = f(); // Sets the index to 7
    }
}

6.4. 错误处理

Solidity 中使用 state-reverting 来处理错误,也就是说:发现异常后,会回滚当前调用(及其所有子调用)对状态的所有变化。如果子调用中发现了异常,则会把异常 bubble up 到上层调用中,如果上层调用没有使用 try/catch 捕获( try/catch 是在 Solidity v0.6.0 中引入的),则会回滚整个交易。这个规则有两个例外,下一节将介绍。

6.4.1. 两个例外(send 和 call/delegatecall/staticcall 失败后不回滚交易)

函数执行失败会回滚整个交易。但是,这个规则有两个例外:
1. 函数 send 出现异常后,不会 bubble up 异常,而是通过 send 的返回值来告诉调用者是否执行成功
2. 函数 call/delegatecall/staticcall 出现异常,也不会 bubble up 异常,而是通过它们的第 1 个返回值来告诉调用者是否执行成功

所以,我们需要检查 send 和 call/delegatecall/staticcall 的返回值,才知道它们是否执行成功。比如:

require(address(test1).send(1 ether));  // 检查 send 的返回值,确保它执行成功,如果失败则回滚整个交易
                                        // 如果担心忘记检查,建议使用 transfer 函数,因为它失败时会 bubble up 异常(transfer 没有返回值)
                                        // 上面的 send 调用等价于 address(test1).transfer(1 ether);

(bool success,) = address(test1).call(abi.encodeWithSignature("func1()"));
require(success);                       // 检查 call 的第 1 个返回值,确保它执行成功,如果失败则回滚整个交易

参考:https://docs.soliditylang.org/en/latest/control-structures.html#error-handling-assert-require-revert-and-exceptions

6.4.2. 抛出异常 assert/require/revert

Assert should only be used to test for internal errors, and to check invariants. 换句话说, assert 如果失败了,一定是其它地方有 bug。 如:

Require 一般用于检查函数参数,或者调用完其它函数后检查它的返回值。 require(condition, "description") 等价于 if (!condition) revert Error("description") ,如:

    function buy(uint amount) public payable {
        if (amount > msg.value / 2 ether)
            revert("Not enough Ether provided.");

        // Alternative way to do it:
        require(
            amount <= msg.value / 2 ether,
            "Not enough Ether provided."
        );

        // Perform the purchase.
	// ......
    }

6.4.3. try/catch

Solidity v0.6.0 中引入了 try/catch 语句, try 后面可以跟两类语句:external function 和合约创建语句。

下面是 try 后面跟 external function 的使用例子:

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.8.1;

interface DataFeed { function getData(address token) external returns (uint value); }

contract FeedConsumer {
    DataFeed feed;
    uint errorCount;
    function rate(address token) public returns (uint value, bool success) {
        // Permanently disable the mechanism if there are
        // more than 10 errors.
        require(errorCount < 10);
        try feed.getData(token) returns (uint v) {
            return (v, true);
        } catch Error(string memory /*reason*/) {
            // This is executed in case
            // revert was called inside getData
            // and a reason string was provided.
            errorCount++;
            return (0, false);
        } catch Panic(uint /*errorCode*/) {
            // This is executed in case of a panic,
            // i.e. a serious error like division by zero
            // or overflow. The error code can be used
            // to determine the kind of error.
            errorCount++;
            return (0, false);
        } catch (bytes memory /*lowLevelData*/) {
            // This is executed in case revert() was used.
            errorCount++;
            return (0, false);
        }
    }
}

下面是 try 后面跟合约创建语句的使用例子:

pragma solidity <0.7.0;

contract CalledContract {

    constructor() public {
        // Code that reverts
        revert();
    }

    // ...
}

contract TryCatcher {

    // ...

    function execute() public {
        try new CalledContract() {
            emit SuccessEvent();
        } catch {
            emit CatchEvent();
        }
    }
}

需要注意的是, try 只会捕获它后面的 external function 调用或者合约创建语句中抛出的错误; try 语句块内部(即后面第一个大括号中的内容)的错误并不会被捕获,如:

function execute() public {
    try externalContract.someFunction() {    // someFunction 中的错误会被 try/catch 捕获
        // this will revert the execution of the transaction even if the external call succeeded
        revert();                            // 注意:这个 revert 在 try 语句第一个大括号中,并不会被捕获
    } catch {
       ...
    }
}

7. 合约

7.1. Visibility (public/external/internal/private)

函数可以声明 4 种可见性,状态变量可以声明除 external 外的另外 3 种可见性,如表 3 所示。

Table 3: 函数和状态变量的 Visibility
Visibility Meaning
public all can access
external cannot be accessed internally, only externally
internal can be accessed from this contract and derived contracts
private can be accessed only from this contract, not from derived contracts

关于函数的内部和外部调用形式,可以参考节 6.2.16.2.2

下面是不同 Visibility 的函数和状态变量的例子:

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.16 <0.7.0;

contract C {
    function f(uint a) private pure returns (uint b) { return a + 1; }  // private 函数
    function setData(uint a) internal { data = a; }    // internal 函数
    uint public data;                                  // public 状态变量
}

7.1.1. public VS. external(external 更省 gas)

当函数的参数是大数组时, external 会比 public 消耗 gas 更少。这是因为 public 可见性的函数,会把参数从 calldata 复制到 memory 中;而 external 可见性的函数,则不会进行这个复制。

下面例子中,test2 比 test 消耗 gas 更少:

pragma solidity^0.4.12;

contract Test {
    function test(uint[20] a) public returns (uint){      // 消耗 496 gas
         return a[10]*2;
    }

    function test2(uint[20] a) external returns (uint){   // 消耗 261 gas
         return a[10]*2;
    }
}

参考:https://ethereum.stackexchange.com/questions/19380/external-vs-public-best-practices

7.1.2. Getter Functions

编译器自动为所有 public 可见性的状态变量创建 getter 函数,如:

contract SimpleStorage {
    uint public storedData;

    /* 编译器自动为 public 状态变量storedData 创建的 getter 函数
    function storedData () public view returns (uint) {
        return storedData;
    }
    */
}

注意: 如果状态变量是数组,则自动生成的 getter 函数并不会返回整个数组,而是接收一个参数,返回指定参数下标的元素。 如果你想返回整个数组,则需要自己编写相应函数,如:

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.16 <0.7.0;

contract arrayExample {
    // public state variable
    uint[] public myArray;

    // Getter function generated by the compiler
    /*
    function myArray(uint i) public view returns (uint) {
        return myArray[i];
    }
    */

    // function that returns entire array
    function getArray() public view returns (uint[] memory) {
        return myArray;
    }
}

下面是一个更复杂的例子:

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.0 <0.7.0;

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

其中, data 是 public 可见性的状态变量,编译器为它生成的 getter 函数如下所示:

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

7.2. Function Modifiers

函数修饰器可以用来以声明的方式改良函数语义,如:

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.5.0 <0.7.0;

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

    // This contract only defines a modifier but does not use
    // it: it will be used in derived contracts.
    // The function body is inserted where the special symbol
    // `_;` in the definition of a modifier appears.
    // This means that if the owner calls this function, the
    // function is executed and otherwise, an exception is
    // thrown.
    modifier onlyOwner {                   // 定义函数修饰器 onlyOwner
        require(
            msg.sender == owner,
            "Only owner can call this function."
        );
        _;
    }
}

contract destructible is owned {
    // This contract inherits the `onlyOwner` modifier from
    // `owned` and applies it to the `destroy` function, which
    // causes that calls to `destroy` only have an effect if
    // they are made by the stored owner.
    function destroy() public onlyOwner {  // 使用函数修饰器 onlyOwner
        selfdestruct(owner);
    }
}

如果函数修饰器被定义为 virtual ,则它可以被它的继承合约修改。

7.3. constant 或 immutable 状态变量

状态变量可以用 constantimmutable 修饰,它们都表示这个状态变量在被赋值后不能再被修改。区别在于, constant 的状态变量必须在编译时就决定其值;而 immutable 的状态变量可以在构造函数中对其赋值。 如:

pragma solidity >0.6.4 <0.7.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) public {
        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;
    }
}

7.4. 函数

7.4.1. View 函数(不会修改状态变量)

view 声明的函数,表示其不会修改状态变量,如:

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.5.0 <0.7.0;

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

下面这些语句被认为是修改状态变量:

  1. Writing to state variables.
  2. Emitting events.
  3. Creating other contracts.
  4. Using selfdestruct.
  5. Sending Ether via calls.
  6. Calling any function not marked view or pure.
  7. Using low-level calls.
  8. Using inline assembly that contains certain opcodes.

constant 可以作为 view 的别名,不过在 Solidity 0.5.0 中被废弃了。

7.4.2. Pure 函数(不会读取、也不会修改状态变量)

pure 声明的函数,表示其读取、也不会修改状态变量,如:

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.5.0 <0.7.0;

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

下面这些语句被认为是读取状态变量:

  1. Reading from state variables.
  2. Accessing address(this).balance or <address>.balance.
  3. Accessing any of the members of block, tx, msg (with the exception of msg.sig and msg.data).
  4. Calling any function not marked pure.
  5. Using inline assembly that contains certain opcodes.

7.4.3. receive() external payable

receive() 是一个特别的函数,它不使用关键字 function

如果调用合约时, calldata 为空,则这个函数会被调用。 例如,使用 .send().transfer() 往该合约转入以太时(数量可以是 0),这个函数会被调用。

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.6.0;

// This contract keeps all Ether sent to it with no way
// to get it back.
contract Sink {
    event Received(address, uint);
    receive() external payable {
        emit Received(msg.sender, msg.value);
    }
}

在 Solidity 0.6.0 以前,合约收到以太的回调函数是下面的形式:

function() external payable {   // 这是过时的用法,从 0.6.0 开始不支持,请使用 receive()
    // ......
}

7.4.4. fallback() external payable

fallback() 是一个特别的函数,它不使用关键字 function

如果调用合约时,找不到其它合适函数,这个方法会被调用。 可以分两种情况:

  1. 调用合约时,calldata 为空,但 receive() 函数没有在合约中定义,那么 fallback() 会被调用;
  2. 调用合约时,calldata 不为空,会使用 calldata 的前 4 个字节寻找函数,如果合约中找不到匹配的函数(即调用一个不存在的函数),那么 fallback() 会被调用。

7.4.5. 函数重载

当函数参数不一样,可以实现函数的重载,如:

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.16 <0.7.0;

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

    function f(uint _in, bool _really) public pure returns (uint out) {
        if (_really)
            out = _in;
    }
}

7.5. 继承

Solidity supports multiple inheritance including polymorphism.

Polymorphism means that a function call (internal and external) always executes the function of the same name (and parameter types) in the most derived contract in the inheritance hierarchy. This has to be explicitly enabled on each function in the hierarchy using the virtual and override keywords.

参考:https://solidity.readthedocs.io/en/latest/contracts.html#inheritance

7.5.1. 函数重载

下面是函数重载的例子:

// SPDX-License-Identifier: GPL-3.0
pragma solidity >0.6.99 <0.8.0;

contract Base
{
    function foo() virtual external view {}   // virtual 关键字表示函数 foo 可以被子合约重载
}

contract Middle is Base {}

contract Inherited is Middle
{
    function foo() override public pure {}    // override 关键字表示重载函数 foo 的实现
}

7.5.2. 用 super 访问父合约

使用 super 可以访问父合约,如:

pragma solidity >=0.6.0;

contract C {
  uint public u;
  function f() virtual public {           // virtual 表示可以被子合约(即 B)重载
    u = 1;
  }
}

contract B is C {
  function f() override virtual public {  // override 表示 f 是一个重载实现,virtual 表示可以被子合约(即 A)重载
    u = 2;
  }
}

contract A is B {
  function f() override public {  // will set u to 3
    u = 3;
  }

  function f1() public {     // will set u to 2
    super.f();               // same as B.f();
  }

  function f2() public {     // will set u to 2
    B.f();
  }

  function f3() public {     // will set u to 1
    C.f();
  }
}

7.6. 抽象合约

如果合约中存在某个函数没有被实现,则合约必须声明为抽象合约。抽象合约使用关键字 abstract 表示, 抽象合约不能被直接实例化。

下面是抽象合约的例子:

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.6.0 <0.8.0;

abstract contract Feline {       // Feline 使用 abstract 修饰,是抽象合约
    function utterance() public pure virtual returns (bytes32);  // 函数 utterance 仅声明了,而没有具体实现
    function func1() public returns (int8) {return 1;}           // 函数 func1 有具体的实现
}

contract Cat is Feline {
    function utterance() public pure override returns (bytes32) { return "miaow"; }  // 重载函数 utterance
}

需要说明的是:所有方法都已经实现时,我们也可以把合约标记为抽象合约。

7.7. 接口

可以用关键字 interface 定义接口,接口和抽象合约类似,也不能直接实例化。此外,接口中的所有函数都只能声明,不能实现,而且隐含着 virtual (即接口中的函数都可以被继承的合约重载),如:

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.6.2 <0.7.0;

interface IToken {
    enum TokenType { Fungible, NonFungible }
    struct Coin { string obverse; string reverse; }
    function transfer(address recipient, uint amount) external;   // 隐含着 virtual
}

contract MyToken is IToken {
    function transfer(address recipient, uint amount) override external {
        // TODO
    }
}

接口还有下面限制:

  • They cannot inherit from other contracts, but they can inherit from other interfaces.
  • All declared functions must be external.
  • They cannot declare a constructor.
  • They cannot declare state variables.

7.8.

Libraries are similar to contracts, but their purpose is that they are deployed only once at a specific address and their code is reused using the DELEGATECALL (CALLCODE until Homestead) feature of the EVM. This means that if library functions are called, their code is executed in the context of the calling contract, i.e. this points to the calling contract, and especially the storage from the calling contract can be accessed.

库有下面限制:

  • they cannot have state variables
  • they cannot inherit nor be inherited
  • they cannot receive Ether
  • they cannot be destroyed

下面是库的例子:

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.6.0 <0.7.0;


// We define a new struct datatype that will be used to
// hold its data in the calling contract.
struct Data {
    mapping(uint => bool) flags;
}

library Set {
    // Note that the first parameter is of type "storage
    // reference" and thus only its storage address and not
    // its contents is passed as part of the call.  This is a
    // special feature of library functions.  It is idiomatic
    // to call the first parameter `self`, if the function can
    // be seen as a method of that object.
    function insert(Data storage self, uint value)
        public
        returns (bool)
    {
        if (self.flags[value])
            return false; // already there
        self.flags[value] = true;
        return true;
    }

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

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


contract C1 {
    Data knownValues;

    function register(uint value) public {
        // The library functions can be called without a
        // specific instance of the library, since the
        // "instance" will be the current contract.
        require(Set.insert(knownValues, value));
    }
    // In this contract, we can also directly access knownValues.flags, if we want.
}

执行 truffle compile 进行编译后,我们在文件 build/contracts/C1.json 中,可以看到合约 C1 的 bytecode 如下所示:

0x608060405234801561001057600080fd5b50610129806100206000396000f3fe6080604052348015600f57600080fd5b506004361060285760003560e01c8063f207564e14602d575b600080fd5b605660048036036020811015604157600080fd5b81019080803590602001909291905050506058565b005b73__Set___________________________________633ab90ad86000836040518363ffffffff1660e01b8152600401808381526020018281526020019250505060206040518083038186803b15801560af57600080fd5b505af415801560c2573d6000803e3d6000fd5b505050506040513d602081101560d757600080fd5b810190808051906020019092919050505060f057600080fd5b5056fea2646970667358221220ee5d27826d4628d525207b0b83ad872b61cf45d16df2352da4e9d928355bffd964736f6c634300060c0033

其中, __Set___________________________________ 是库 Set 的地址的占位符,使用 truffle 的 deployer.link 可以占位符换为 Set 的真实地址,部署脚本类似于:

await deployer.deploy(Set);     // 部署 Set
await deployer.link(Set, C1);   // Link 库
await deployer.deploy(C1);      // 部署 C1

前面介绍的是 Linked Library,还有一类是 Embedded Library。 如果 Library 中的函数都是 internal 或者 private 的,编译器会把库直接嵌入到使用它的合约中,调用库中的函数会使用 JUMP(而不是调用 Linked Library 函数所使用的 DELEGATECALL),这种 Library 称为 Embedded Library。 比如,OpenZeppelin 的 SafeMath 库是 Embedded Library。

7.9. Using For

使用 using lib1 for type1 可以让我们更方便地使用库。如:

library lib1{
    function subUint(uint a, uint b) public returns(uint){
        require(a >= b);
        return a - b;
    }
}

contract C {
    using lib1 for uint;           // 这样,我们可以在 uint 类型上调用 subUint 方法了

    function f1() public returns (uint) {
        uint a = 30;
        uint b = 10;

        uint c = a.subUint(b);     // 直接在 uint 类型上调用库 lib1 中的方法 subUint

        return c;
    }
}

8. 参考

Author: cig01

Created: <2019-11-02 Sat>

Last updated: <2023-10-19 Thu>

Creator: Emacs 27.1 (Org mode 9.4)