合约

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

创建合约

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

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

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

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

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

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

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

contract OwnedToken {

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

    // 这是注册 creator 和设置名称的构造函数。
    constructor(bytes32 name_) {

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

    function changeName(bytes32 newName) public {

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

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

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

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

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

可见性和 getter 函数

状态变量可见性

状态变量有 3 种可见性:

public
对于 public 状态变量会自动生成一个 getter hanshu 函数(见下面)。 以便其他的合约读取他们的值。 当在用一个合约里使用是,外部方式访问 (如: this.x) 会调用getter 函数,而内部方式访问 (如: x) 会直接从存储中获取值。 Setter函数则不会被生成,所以其他合约不能直接修改其值。
internal
内部可见性状态变量只能在它们所定义的合约和派生合同中访问。 它们不能被外部访问。 这是状态变量的默认可见性。
private
私有状态变量就像内部变量一样,但它们在派生合约中是不可见的。

警告

设置为 private``或 ``internal,只能防止其他合约读取或修改信息,但它仍然可以在链外查看到。

函数可见性

由于 Solidity 有两种函数调用:外部调用则会产生一个 EVM 调用,而内部调用不会, 更进一步, 函数可以确定器被内部及派生合约的可访问性,这里有 4 种可见性:

external
外部可见性函数作为合约接口的一部分,意味着我们可以从其他合约和交易中调用。 一个外部函数 f 不能从内部调用(即 f 不起作用,但 this.f() 可以)。
public
public 函数是合约接口的一部分,可以在内部或通过消息调用。
internal
内部可见性函数访问可以在当前合约或派生的合约访问,不可以外部访问。 由于它们没有通过合约的ABI向外部公开,它们可以接受内部可见性类型的参数:比如映射或存储引用。
private
private 函数和状态变量仅在当前定义它们的合约中使用,并且不能被派生合约使用。

警告

设置为 private``或 ``internal,只能防止其他合约读取或修改信息,但它仍然可以在链外查看到。

可见性标识符的定义位置,对于状态变量来说是在类型后面,对于函数是在参数列表和返回关键字中间。

pragma solidity  >=0.4.16 <0.9.0;

contract C {
    function f(uint a) private pure returns (uint b) { return a + 1; }
    function setData(uint a) internal { data = a; }
    uint public data;
}

在下面的例子中,D 可以调用 c.getData() 来获取状态存储中 data 的值,但不能调用 f 。 合约 E 继承自 C ,因此可以调用 compute

pragma solidity >=0.4.16 <0.9.0;

contract C {
    uint private data;

    function f(uint a) private returns(uint b) { return a + 1; }
    function setData(uint a) public { data = a; }
    function getData() public returns(uint) { return data; }
    function compute(uint a, uint b) internal returns (uint) { return a+b; }
}

// 下面代码编译错误
contract D {
    function readData() public {
        C c = new C();
        uint local = c.f(7); // 错误:成员 `f` 不可见
        c.setData(3);
        local = c.getData();
        local = c.compute(3, 5); // 错误:成员 `compute` 不可见
    }
}

contract E is C {
    function g() public {
        C c = new C();
        uint val = compute(3, 5); // 访问内部成员(从继承合约访问父合约成员)
    }
}

Getter 函数

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

pragma solidity  >=0.4.16 <0.9.0;

contract C {
    uint public data = 42;
}

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

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

pragma solidity >=0.4.16 <0.9.0;

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

如果你有一个数组类型的 public 状态变量,那么你只能通过生成的 getter 函数访问数组的单个元素。 这个机制以避免返回整个数组时的高成本gas。 可以使用如 myArray(0) 用于指定参数要返回的单个元素。 如果要在一次调用中返回整个数组,则需要写一个函数,例如:

pragma solidity >=0.4.0 <0.9.0;

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

  // 指定生成的Getter 函数
  /*
  function myArray(uint i) public view returns (uint) {
      return myArray[i];
  }
  */

  // 返回整个数组
  function getArray() public view returns (uint[] memory) {
      return myArray;
  }
}

现在可以使用 getArray() 获得整个数组,而 myArray(i) 是返回单个元素。

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

pragma solidity ^0.4.0 <0.9.0;

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

这将会生成以下形式的函数,在结构体内的映射和数组(byte 数组除外)被省略了,因为没有好办法为单个结构成员或为映射提供一个键。

function data(uint arg1, bool arg2, uint arg3)
    public
    returns (uint a, bytes3 b, bytes memory e)
{
    a = data[arg1][arg2][arg3].a;
    b = data[arg1][arg2][arg3].b;
    e = data[arg1][arg2][arg3].e;
}

函数 修改器modifier

使用 修改器modifier 可以轻松改变函数的行为。 例如,它们可以在执行函数之前自动检查某个条件。 修改器modifier 是合约的可继承属性,并可能被派生合约覆盖 , 但前提是它们被标记为 virtual.。 有关详细信息,请参见 Modifier 重载.

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

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

    address owner;

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

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

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

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

    constructor(uint initialPrice) { price = initialPrice; }

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

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

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

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

如果你想访问定义在合约 C修改器modifier m , 可以使用 C.m 去引用它,而不需要使用虚拟表查找。

只能使用在当前合约或在基类合约中定义的 修改器modifier , 修改器modifier 也可以定义在库里面,但是他们被限定在库函数使用。

如果同一个函数有多个 修改器modifier,它们之间以空格隔开,修改器modifier 会依次检查执行。

修改器不能隐式地访问或改变它们所修饰的函数的参数和返回值。 这些值只能在调用时明确地以参数传递。

在函数修改器中,指定何时运行被修改器应用的函数是有必要。占位符语句(用单个下划线字符 _ 表示)用于表示被修改的函数的主体应该被插入的位置。 请注意,占位符运算符与在变量名称中使用下划线作为前导或尾随字符不同,后者是一种风格上的选择。

修改器modifier 或函数体中显式的 return 语句仅仅跳出当前的 修改器modifier 和函数体。 返回变量会被赋值,但整个执行逻辑会从前一个 修改器modifier 中的定义的 _ 之后继续执行。

警告

在早期的 Solidity 版本中,有 修改器modifier 的函数, return 语句的行为表现不同。

return; 从修改器中显式返回并不影响函数返回值。 然而,修改器可以选择完全不执行函数体,在这种情况下,返回的变量被设置为:ref:默认值<default-value>,就像该函数是空函数体一样。

_ 符号可以在修改器中出现多次,每处都会替换为函数体。

修改器modifier 的参数可以是任意表达式,在此上下文中,所有在函数中可见的符号,在 修改器modifier 中均可见。 在 修改器modifier 中引入的符号在函数中不可见(可能被重载改变)。

Constant 和 Immutable 状态变量

状态变量声明为 constant (常量)或者 immutable (不可变量),在这两种情况下,合约一旦部署之后,变量将不在修改。

对于 constant 常量, 他的值在编译器确定,而对于 immutable, 它的值在部署时确定。

也可以在文件级别定义 constant 变量(注:0.7.2 之后加入的特性)。

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

与常规状态变量相比,常量和不可变量的gas成本要低得多。 对于常量,赋值给它的表达式将复制到所有访问该常量的位置,并且每次都会对其进行重新求值。 这样可以进行本地优化。

不可变变量在构造时进行一次求值,并将其值复制到代码中访问它们的所有位置。 对于这些值,将保留32个字节,即使它们适合较少的字节也是如此。 因此,常量有时可能比不可变量更便宜。

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

// SPDX-License-Identifier: GPL-3.0
pragma solidity >0.7.4;
uint constant X = 32**22 + 8;

contract C {

    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 ref) {
        decimals = decimals_;
        // Assignments to immutables can even access the environment.
        maxBalance = ref.balance;
    }

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

Constant

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

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

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

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

Immutable

声明为不可变量(immutable)的变量的限制要比声明为常量(constant) 的变量的限制少:可以在合约的构造函数中或声明时为不可变的变量分配任意值。 不可变量只能赋值一次,并且在赋值之后才可以读取。

编译器生成的合约创建代码将在返回合约之前修改合约的运行时代码,方法是将对不可变量的所有引用替换为分配给它们的值。 如果要将编译器生成的运行时代码与实际存储在区块链中的代码进行比较,则这一点很重要。

注解

不可变量可以在声明时赋值,不过只有在合约的构造函数执行时才被视为视为初始化。 这意味着,你不能用一个依赖于不可变量的值在行内初始化另一个不可变量。 不过,你可以在合约的构造函数中这样做。

这是为了防止对状态变量初始化和构造函数顺序的不同解释,特别是继承时,出现问题。

注解

译者注:不可变量(Immutable) 是 Solidity 0.6.5 引入的,因此0.6.5 之前的版本不可用。

函数

可以在合约内部和外部定义函数。

注解

译者注:函数可以在合约外部定义是从 0.7.0 之后才开始支持的。

合约之外的函数(也称为“自由函数”)始终具有隐式的 internal 可见性。 它们的代码包含在所有调用它们合约中,类似于内部库函数。

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

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

contract ArrayExample {
    bool found;
    function f(uint[] memory arr) public {
        // This calls the free function internally.
        // The compiler will add its code to the contract.
        uint s = sum(arr);
        require(s >= 10);
        found = true;
    }
}

注解

在合约之外定义的函数仍然在合约的上下文内执行。 他们仍然可以调用其他合约,将其发送以太币或销毁调用它们的合约等其他事情。 与在合约中定义的函数的主要区别为:自由函数不能直接访问存储变量 this 、存储和不在他们的作用域范围内函数。

函数参数及返回值

与 Javascript 一样,函数可能需要参数作为输入; 而与 Javascript 和 C 不同的是,它们可能返回任意数量的参数作为输出。

函数参数(输入参数)

函数参数的声明方式与变量相同。不过未使用的参数可以省略参数名。

例如,如果我们希望合约接受有两个整数形参的函数的外部调用,可以像下面这样写:

pragma solidity >=0.4.16 <0.9.0;

contract Simple {
    uint sum;
    function taker(uint a, uint b) public {
        sum = a + b;
    }
}

函数参数可以当作为本地变量,也可用在等号左边被赋值。

返回变量

函数返回变量的声明方式在关键词 returns 之后,与参数的声明方式相同。

例如,如果我们需要返回两个结果:两个给定整数的和与积,我们应该写作:

pragma solidity >=0.4.16 <0.9.0;

contract Simple {
    function arithmetic(uint a, uint b)
        public
        pure
        returns (uint sum, uint product)
    {
        sum = a + b;
        product = a * b;
    }
}

返回变量名可以被省略。 返回变量可以当作为函数中的本地变量,没有显式设置的话,会使用 :ref:` 默认值 <default-value>` 返回变量可以显式给它附一个值(像上面),也可以使用 return 语句指定,使用 return 语句可以一个或多个值,参阅 multiple ones

pragma solidity >=0.4.16 <0.9.0;

contract Simple {
    function arithmetic(uint a, uint b)
        public
        pure
        returns (uint sum, uint product)
    {
        return (a + b, a * b);
    }
}

这个形式等同于赋值给返回参数,然后用 return; 退出。

如果使用 return 提前退出有返回值的函数, 必须在用 return 时提供返回值。

注解

非内部函数有些类型没法返回,这些类型如下,以及把他们的组合:

  • mappings (映射),
  • 内部函数类型,
  • 指向存储 storage 的引用类型,
  • 多维数组 (仅适用于 ABI coder v1),
  • 机构体 (仅适用于 ABI coder v1).

这些限制不使用与库函数,因为他们是不同的 internal ABI.

返回多个值

当函数需要使用多个值,可以用语句 return (v0, v1, ..., vn) 。 参数的数量需要和声明时候一致。

状态可变性

View 视图函数

可以将函数声明为 view 类型,这种情况下要保证不修改状态。

注解

如果编译器的 EVM 目标是拜占庭硬分叉( 译者注:Byzantium 分叉发生在2017年10月,这次分叉进加入了4个操作符: REVERT 、RETURNDATASIZE、RETURNDATACOPY 、STATICCALL) 或更新的 (默认), 则操作码 STATICCALL 将用于视图函数, 这些函数强制在 EVM 执行过程中保持不修改状态。 对于库视图函数, 使用 DELLEGATECALL, 因为没有组合的 DELEGATECALLSTATICALL。这意味着库视图函数不会在运行时检查进而阻止状态修改。 这不会对安全性产生负面影响, 因为库代码通常在编译时知道, 并且静态检查器会执行编译时检查。

下面的语句被认为是修改状态:

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

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

注解

constant 之前是 view 的别名,不过在0.5.0之后移除了。

注解

Getter 方法自动被标记为 view

注解

在0.5.0 版本之前, 编译器没有对 view 函数使用 STATICCALL 操作码。 这样通过使用无效的显式类型转换会启用视图函数中的状态修改。 通过对 view 函数使用 STATICCALL , 可以防止在 EVM 级别上对状态进行修改。

Pure 纯函数

函数可以声明为 pure ,在这种情况下,承诺不读取也不修改状态变量。

特别是,应该可以在编译时确定一个 pure 函数,它仅处理输入参数和 msg.data ,对当前区块链状态没有任何了解。 这也意味着读取 immutable 变量也不是一个 pure 操作。

注解

如果编译器的 EVM 编译目标设置为 Byzantium 或之后的版本 (默认), 则使用操作码 STATICCALL , 这并不保证状态未被读取, 但至少不被修改。

除了上面解释的状态修改语句列表之外,以下被认为是读取状态:

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

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

纯函数能够使用 revert()require()发生错误 时去还原潜在状态更改。

还原状态更改不被视为 “状态修改”, 因为它只还原以前在没有``view`` 或 pure 限制的代码中所做的状态更改, 并且代码可以选择捕获 revert 并不传递还原。

这种行为也符合 STATICCALL 操作码。

警告

不可能在 EVM 级别阻止函数读取状态, 只能阻止它们写入状态 (即只能在 EVM 级别强制执行 view , 而 pure 不能强制)。

注解

在0.5.0 版本之前, 编译器没有对 pure 函数使用 STATICCALL 操作码。这样通过使用无效的显式类型转换启用 pure 函数中的状态修改。 通过对 pure 函数使用 STATICCALL , 可以防止在 EVM 级别上对状态进行修改。

注解

在0.4.17版本之前,编译器不会强制 pure 函数不读取状态。它是一个编译时类型检查, 可以避免在合约类型之间进行无效的显式转换, 因为编译器可以验证合约类型没有状态更改操作, 但它不会在运行时能检查调用实际的类型。

特别的函数

receive 接收以太函数

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

不需要 function 关键字,也没有参数和返回值并且必须是 external 可见性和 payable 修饰. 它可以是 virtual 的,可以被重载也可以有 修改器modifier

在对合约没有任何附加数据调用(通常是对合约转账)是会执行 receive 函数. 例如 通过 .send() or .transfer() 如果 receive 函数不存在, 但是有payable 的 fallback 回退函数 那么在进行纯以太转账时,fallback 函数会调用.

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

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

  • 写入存储
  • 创建合约
  • 调用消耗大量 gas 的外部函数
  • 发送以太币

警告

一个没有定义 fallback 函数或  receive 函数的合约,直接接收以太币(没有函数调用,即使用 sendtransfer)会抛出一个异常, 并返还以太币(在 Solidity v0.4.0 之前行为会有所不同)。 所以如果你想让你的合约接收以太币,必须实现receive函数(使用 payable fallback 函数不再推荐,因为payable fallback功能被调用,不会因为发送方的接口混乱而失败)。

警告

一个没有receive函数的合约,可以作为 coinbase 交易 (又名 矿工区块回报 )的接收者或者作为 selfdestruct 的目标来接收以太币。

一个合约不能对这种以太币转移做出反应,因此也不能拒绝它们。这是 EVM 在设计时就决定好的,而且 Solidity 无法绕过这个问题。

这也意味着 address(this).balance 可以高于合约中实现的一些手工记帐的总和(例如在receive 函数中更新的累加器记帐)。

下面是一个例子:

pragma solidity ^0.6.0;

// 这个合约会保留所有发送给它的以太币,没有办法取回。
contract Sink {
    event Received(address, uint);
    receive() external payable {
        emit Received(msg.sender, msg.value);
    }
}

Fallback 回退函数

合约可以最多有一个回退函数。函数声明为: fallback () external [payable]fallback (bytes calldata input) external [payable] returns (bytes memory output)

没有 function 关键字。 必须是 external 可见性,它可以是 virtual 的,可以被重载也可以有 修改器modifier

如果在一个对合约调用中,没有其他函数与给定的函数标识符匹配fallback会被调用. 或者在没有 receive 函数 时,而没有提供附加数据对合约调用,那么fallback 函数会被执行。

fallback 函数始终会接收数据,但为了同时接收以太时,必须标记为 payable

如果使用了带参数的版本, input 将包含发送到合约的完整数据(等于 msg.data ),并且通过 output 返回数据。 返回数据不是 ABI 编码过的数据,相反,它返回不经过修改的数据。

更糟的是,如果回退函数在接收以太时调用,可能只有 2300 gas 可以使用,参考 receive接收函数

与任何其他函数一样,只要有足够的 gas 传递给它,回退函数就可以执行复杂的操作。

警告

payable 的fallback函数也可以在纯以太转账的时候执行, 如果没有 receive 以太函数 推荐总是定义一个receive函数,而不是定义一个``payable`` 的fallback函数,

注解

如果想要解码输入数据,那么前四个字节用作函数选择器,然后用 abi.decode 与数组切片语法一起使用来解码ABI编码的数据:

(c, d) = abi.decode(_input[4:], (uint256, uint256));

请注意,这仅应作为最后的手段,而应使用对应的函数。

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

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


// 这个合约会保留所有发送给它的以太币,没有办法返还。
contract TestPayable {
    uint x;
    uint y;

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

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

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

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


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

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

        // 发送以太币, TestPayable 的 receive 函数被调用.

        // 因为函数有存储写入, 会比简单的使用 ``send`` or ``transfer``消耗更多的 gas。
        // 因此使用底层的call调用
        (success,) = address(test).call{value: 2 ether}("");
        require(success);

        // 结果 test.x 为 2 而 test.y 为 2 ether.

        return true;
    }

}

函数重载

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

pragma solidity >=0.4.16 <0.9.0;

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

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

重载函数也存在于外部接口中。如果两个外部可见函数仅区别于 Solidity 内的类型而不是它们的外部类型则会导致错误。

// 以下代码无法编译
pragma solidity >=0.4.16 <0.9.0;

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

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

contract B {
}

以上两个 f 函数重载都接受了 ABI 的地址类型,虽然它们在 Solidity 中被认为是不同的。

重载解析和参数匹配

通过将当前范围内的函数声明与函数调用中提供的参数相匹配,可以选择重载函数。 如果所有参数都可以隐式地转换为预期类型,则选择函数作为重载候选项。如果一个候选都没有,解析失败。

注解

返回参数不作为重载解析的依据。

pragma solidity >=0.4.16 <0.9.0;

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

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

调用 f(50) 会导致类型错误,因为 50 既可以被隐式转换为 uint8 也可以被隐式转换为 uint256。 另一方面,调用 f(256) 则会解析为 f(uint256) 重载,因为 256 不能隐式转换为 uint8

事件 Events

Solidity 事件是EVM的日志功能之上的抽象。 应用程序可以通过以太坊客户端的RPC接口订阅和监听这些事件。

事件在合约中可被继承。当他们被调用时,会使参数被存储到交易的日志中 —— 一种区块链中的特殊数据结构。 这些日志与地址相关联,被并入区块链中,只要区块可以访问就一直存在(现在开始会被永久保存,在 Serenity 版本中可能会改动)。 日志和事件在合约内不可直接被访问(甚至是创建日志的合约也不能访问)。

如果外部实体需要该日志实际上存在于区块链中的证明,可以请求日志的Merkle证明. 但需要留意的是,由于合约中仅能访问最近的 256 个区块哈希,所以还需要提供区块头信息。

对日志的证明是可能的,如果一个外部实体提供了一个带有这种证明的合约,它可以检查日志是否真实存在于区块链中。

最多三个参数可以接收 indexed 属性(它是一个特殊的名为:ref:”主题” <abi_events> 的数据结构, 而不作为日志的数据部分)。 主题仅有 32 字节, 因此如果 引用类型 标记为索引项,则它们的 keccak-256 哈希值会被作为 主题topic 保存。

所有非索引 indexed 参数是 ABI-encoded 都将存储在日志的数据部分中。

主题topic 让我们可以可以搜索事件,比如在为某些事件过滤一些区块,还可以按发起事件的合同地址来过滤事件。

例如, 使用如下的 web3.js subscribe("logs") 方法 去过滤符合特定地址的|topic| :

var options = {
    fromBlock: 0,
    address: web3.eth.defaultAccount,
    topics: ["0x0000000000000000000000000000000000000000000000000000000000000000", null, null]
};
web3.eth.subscribe('logs', options, function (error, result) {
    if (!error)
        console.log(result);
})
    .on("data", function (log) {
        console.log(log);
    })
    .on("changed", function (log) {
});

除非你用 anonymous 声明事件,否则事件签名的哈希值是一个 主题topic 。 同时也意味着对于匿名事件无法通过名字来过滤,仅能按合约地址过滤。 匿名事件的优势是他们部署和调用的成本更低。它也允许你声明 4 个索引参与而不是 3 个。

注解

由于交易日志只存储事件数据而不存储类型。你必须知道事件的类型,包括哪个参数被索引,以及该事件是否是匿名的,以便正确解释数据。 尤其是,有可能使用一个匿名事件来”伪造”另一个事件的签名。

事件成员

  • event.selector: 对于非匿名事件,这是一个 bytes32 值,包含事件签名的 keccak256 哈希值,在默认主题中使用。

示例

pragma solidity  >=0.4.21 <0.9.0;

contract ClientReceipt {
    event Deposit(
        address indexed from,
        bytes32 indexed id,
        uint value
    );

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

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

var abi = /* abi 由编译器产生 */;
var ClientReceipt = web3.eth.contract(abi);
var clientReceipt = ClientReceipt.at("0x1234...xlb67" /* 地址 */);

var depositEvent = clientReceipt.Deposit();

// 监听变化
depositEvent.watch(function(error, result) {
    // 结果包含 非索引参数 以及 主题 topic
    if (!error)
        console.log(result);
});

// 或者通过传入回调函数,立即开始听监
var depositEvent = clientReceipt.Deposit(function(error, result) {
    if (!error)
        console.log(result);
});

上面的输出如下所示(有删减):

{
   "returnValues": {
       "from": "0x1111…FFFFCCCC",
       "id": "0x50…sd5adb20",
       "value": "0x420042"
   },
   "raw": {
       "data": "0x7f…91385",
       "topics": ["0xfd4…b4ead7", "0x7f…1a91385"]
   }
}

错误和回退语句

Solidity 中的错误(关键字error)提供了一种方便且省gas的方式来向用户解释为什么一个操作会失败。它们可以被定义在合约(包括接口和库)内部和外部。

错误必须与 revert 语句 一起使用。它会还原当前调用中的发生的所有变化,并将错误数据传回给调用者。

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

/// 转账时,没有足够的余额。
/// @param available balance available.
/// @param required requested amount to transfer.
error InsufficientBalance(uint256 available, uint256 required);

contract TestToken {
    mapping(address => uint) balance;
    function transfer(address to, uint256 amount) public {
        if (amount > balance[msg.sender])
            revert InsufficientBalance({
                available: balance[msg.sender],
                required: amount
            });
        balance[msg.sender] -= amount;
        balance[to] += amount;
    }
    // ...
}

错误不能被重写或覆盖,但是可以继承。只要作用域不同,同一个错误可以在多个地方定义。 只能使用 revert 语句创建错误实例。

错误产生的数据,会通过revert操作传递给调用者,可以交由链外组件处理或在 try/catch 语句 中捕获它。 注意,只有外部调用的错误才能被捕获。发生在内部调用或同一函数内的 revert 不能被捕获。

如果错误没有任何参数,错误只需要四个字节的数据,你可以使用:ref:NatSpec <natspec>,来进一步解释错误背后的原因,NatSpec 不会存储在链上。 这个方式使得它同时也是一个非常便宜和方便的错误报告功能。

更具体地说,一个错误实例的ABI编码方式与调用相同名称和类型的函数的方式相同,它作为 revert 操作码的返回数据使用。 这意味着错误数据由一个4字节的选择器和 ABI-encoded 数据组成。 选择器是错误的签名的keccak256哈希的前四个字节组成。

注解

合约有可能出现以下情况:在不同地方定义的相同名称的错误。而这些错误对调用者来说是无法区分的。 对于外部来说,如 ABI 仅关联了错误的名字,而没有定义它的合约或文件。

如果你可以定义 error Error(string) 的话, require(condition, "description");if (!condition) revert Error("description") 等效。 不过, Error 是内建类型,不可以通过用户代码定义。

同样的,一个 assert 的错误或类似的失败将以 Panic(uint256) 类型的错误的方式回退。

注解

错误数据应该只用于指示错误,而不是作为控制流的一种手段。原因是默认情况下内部调用的获得的错误数据可能是通过外部调用链冒泡过来。

当这意味着,一个内部调用可以 “伪造”错误数据,使它看起来像是来自被调用的合约。

Errors 成员

  • error.selector: 包含错误选择器的 bytes4 值。

继承

Solidity 支持多重继承包括多态。

所有的函数调用都是虚拟的,这意味着最终派生的函数会被调用,除非明确给出合约名称或者使用super关键字。

当一个合约从多个合约继承时,在区块链上只有一个合约被创建,所有基类合约(或称为父合约)的代码被编译到创建的合约中。这意味着对基类合约函数的所有内部调用也只是使用内部函数调用(super.f(..)将使用JUMP跳转而不是消息调用)。

状态变量覆盖被视为错误。 派生合约不可以在声明已经是基类合约中可见的状态变量具有相同的名称 x

总的来说,Solidity 的继承系统与 Python的继承系统 非常 相似,特别是多重继承方面, 但是也有一些 不同

下面的例子进行了详细的说明。

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    uint info;
}

注意,在上边的代码中,我们调用 Destructible.destroy() 来“转发”销毁请求。 这样做法是有问题的,在下面的例子中可以看到:

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

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

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

contract Base1 is Destructible {
    function destroy() public virtual override  { /* 清除操作 1 */ Destructible.destroy(); }
}

contract Base2 is Destructible {
    function destroy() public { /* 清除操作 2 */ Destructible.destroy(); }
}

contract Final is Base1, Base2 {
    function destroy() public override(Base1, Base2) { Base2.destroy(); }
}

。 解决此问题的方法是使用超级:

调用 Final.destroy() 时会调用 Base2.destroy, 因为我们在最终重写中显式指定了它。 但是此函数将绕过 Base1.destroy, 解决这个问题的方法是使用 super

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

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

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

contract Base1 is Destructible {
    function destroy() public virtual override { /* 清除操作 1 */ super.destroy(); }
}


contract Base2 is Destructible {
    function destroy() public  virtual override { /* 清除操作 2 */ super.destroy(); }
}

contract Final is Base1, Base2 {
    function destroy() public override(Base1, Base2) { super.destroy(); }
}

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

函数重写(Overriding)

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

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

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

以下示例演示了可变性和可见性的变化:

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

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

contract Middle is Base {}

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

对于多重继承,如果有多个父合约有相同定义的函数, override 关键字后必须指定所有父合约名。

例如:

pragma solidity >=0.7.0 <0.9.0;

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

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

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

不过如果(重写的)函数继承自一个公共的父合约, override 是可以不用显示指定的。 例如:

pragma solidity >=0.7.0 <0.9.0;

contract A { function f() public pure{} }
contract B is A {}
contract C is A {}
// 不用显示  override
contract D is B, C {}

更正式地说,如果存在父合约是签名函数的所有重写路径的一部分,则不需要重写(直接或间接)从多个基础继承的函数,并且(1)父合约实现了该函数,从当前合约到父合约的路径都没有提到具有该签名的函数,或者(2)父合约没有实现该函数,并且存在从当前合约到该父合约的所有路径中,最多只能提及该函数。

从这个意义上说,签名函数的重写路径是通过继承图的路径,该路径始于所考虑的合约,并终止于提及具有该签名的函数的合约。

如果函数没有标记为 virtual , 那么派生合约将不能更改函数的行为(即不能重写)。

注解

private 的函数是不可以标记为 virtual 的。

注解

除接口之外(因为接口会自动作为 virtual ),没有实现的函数必须标记为 virtual

注解

从 Solidity 0.8.8 开始, 在重写接口函数时不再要求 override 关键字,除非函数在多个父合约定义。

如果getter 函数的参数和返回值都和外部函数一致时,外部(external)函数是可以被 public 的状态变量被重写的,例如:

pragma solidity >=0.7.0 <0.9.0;

contract A
{
    function f() external view virtual returns(uint) { return 5; }
}

contract B is A
{
    uint public override f;
}

注解

尽管public 的状态变量可以重写外部函数,但是public 的状态变量不能被重写。

修改器重写

修改器重写也可以被重写,工作方式和 函数重写 类似。 需要被重写的修改器也需要使用 virtual 修饰, override 则同样修饰重载,例如:

pragma solidity >=0.7.0 <0.9.0;

contract Base
{
    modifier foo() virtual {_;}
}

contract Inherited is Base
{
    modifier foo() override {_;}
}

如果是多重继承,所有直接父合约必须显示指定override, 例如:

pragma solidity >=0.7.0 <0.9.0;

contract Base1
{
    modifier foo() virtual {_;}
}

contract Base2
{
    modifier foo() virtual {_;}
}

contract Inherited is Base1, Base2
{
    modifier foo() override(Base1, Base2) {_;}
}

构造函数

构造函数是使用 constructor 关键字声明的一个可选函数, 它在创建合约时执行, 可以在其中运行合约初始化代码。

在执行构造函数代码之前, 如果状态变量可以初始化为指定值; 如果不初始化, 则为 默认值

构造函数运行后, 将合约的最终代码部署到区块链。代码的部署需要 gas 与代码的长度线性相关。 此代码包括所有函数部分是公有接口以及可以通过函数调用访问的所有函数。它不包括构造函数代码或仅从构造函数调用的内部函数。

如果没有构造函数, 合约将假定采用默认构造函数, 它等效于 constructor() {}

举例:

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

abstract contract A {
    uint public a;

    constructor(uint a) {
        a = a;
    }
}

contract B is A(1) {
    constructor() {}
}

构造函数可以使用内部(internal)参数(例如指向存储的指针),在这个例子中,合约必须标记为 抽象合约 ,因为参数不能赋值被外部赋值,而仅能通过派生的合约。

警告

在 0.4.22 版本之前, 构造函数定义为合约的同名函数,不过语法在0.5.0之后弃用了。

警告

在 0.7.0 版本之前, 你需要通过 internalpublic 指定构造函数的可见性。

基类构造函数的参数

所有基类合约的构造函数将在下面解释的线性化规则被调用。如果基构造函数有参数, 派生合约需要指定所有参数。这可以通过两种方式来实现:

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

contract Base {
    uint x;
    constructor(uint x) { x = x; }
}

// 直接在继承列表中指定参数
contract Derived1 is Base(7) {
    constructor() {}
}

// 或通过派生的构造函数中用 修饰符 "modifier"
contract Derived2 is Base {
    constructor(uint y) Base(y * y) {}
}

// or declare abstract...
abstract contract Derived3 is Base {
}

// and have the next concrete derived contract initialize it.
contract DerivedFromDerived is Derived3 {
    constructor() Base(10 + 10) {}
}

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

参数必须在两种方式中(继承列表或派生构造函数修饰符样式)使用一种 。 在这两个位置都指定参数则会发生错误。

如果派生合约没有给所有基类合约指定参数,则这个合约必须声明为抽象合约。 在这种情况下,当另一个合约从它派生出来时,另一个合约的继承列表或构造函数必须为所有还没有指定参数的基类提供必要的参数(否则,其他合约也必须被声明为抽象的)。 例如,在上面的代码片段中,可以看到的 Derived3DerivedFromDerived

多重继承与线性化

编程语言实现多重继承需要解决几个问题。 一个问题是 钻石问题。 Solidity 借鉴了 Python 的方式并且使用“ C3 线性化 ”强制一个由基类构成的 DAG(有向无环图)保持一个特定的顺序。 这最终反映为我们所希望的唯一化的结果,但也使某些继承方式变为无效。尤其是,基类在 is 后面的顺序很重要:列出基类合约的 顺序从 “最基类” 到 “最派生类” 。请注意, 此顺序与 Python 中使用的顺序相反。

另一种简化的解释是, 当一个在不同的合约中多次定义函数被调用时, , 给定的基类以从右到左 (Python 中从左到右) 按深度优先的方式进行搜索,在第一次匹配的时候停止。 如果基类合约已经搜索过, 则跳过该合约。

在下面的代码中,Solidity 会给出“ Linearization of inheritance graph impossible ”这样的错误。

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

contract X {}
contract A is X {}
// 编译出错
contract C is A, X {}

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

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

由于必须显式覆盖从多个基类继承的函数,因此C3线性化在实践中并不是太重要。

当继承层次结构中有多个构造函数时,继承线性化特别重要。 构造函数将始终以线性化顺序执行,无论在继承合约的构造函数中提供其参数的顺序如何。 例如:

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

contract Base1 {
    constructor() {}
}

contract Base2 {
    constructor() {}
}

//  构造函数以以下顺序执行:
//  1 - Base1
//  2 - Base2
//  3 - Derived1
contract Derived1 is Base1, Base2 {
    constructor() Base1() Base2() {}
}

// 构造函数以以下顺序执行:
//  1 - Base2
//  2 - Base1
//  3 - Derived2
contract Derived2 is Base2, Base1 {
    constructor() Base2() Base1() {}
}

// 构造函数仍然以以下顺序执行:
//  1 - Base2
//  2 - Base1
//  3 - Derived3
contract Derived3 is Base2, Base1 {
    constructor() Base1() Base2() {}
}

继承有相同名字的不同类型成员

当继承时合约出现了一下相同名字会被认为是一个错误:

  • 函数和 修改器modifier 同名
  • 函数和事件同名
  • 事件和 修改器modifier 同名

有一种例外情况,状态变量的 getter 函数可以覆盖 external 函数。

抽象合约

如果未实现合约中的至少一个函数,则必须将合约标记为 abstract。 即使实现了所有功能,合约也可能被标记为abstract。

当合约中至少有一个函数没有被实现,或者合约没有为其所有的基类合约构造函数提供参数时,必须将合约标记为 abstract 。 即使不是这种情况,合约仍然可以被标记为抽象的,例如,当你不打算直接创建合约时。抽象合约类似于 接口 ,但是interface可以声明的内容更加有限。

如下例所示,可以使用关键字 abstract 定义抽象合约合约, 由于 utterance() 函数没有具体的实现(没有实现体 { } , 而是以 ; 结尾:

pragma solidity >=0.6.0 <0.9.0;

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

这样的抽象合约不能直接实例化。 如果抽象合约本身确实都有实现所有定义的函数,也是正确的。 下例显示了抽象合约作为基类的用法:

pragma solidity >=0.6.0 <0.9.0;

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

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

如果合约继承自抽象合约,并且没有通过重写来实现所有未实现的函数, 它依然需要标记为抽象 abstract 合约.

请注意,没有实现的函数与 Function Type 不同,即使它们的语法看起来非常相似。

没有实现的函数示例(函数声明):

function foo(address) external returns (address);

函数类型的示例(变量声明,其中变量的类型为“函数”):

function(address) external returns (address) foo;

抽象合约将合约的定义与其实现脱钩,从而提供了更好的可扩展性和自文档性,并简化了诸如 Template方法 的模式并消除了代码重复。抽象合约的使用方式与接口 interface 中定义方法的使用方式相同。 抽象合约的设计者可以这样说“我的任何继承都必须实施此方法”。

注解

抽象合约不能用一个无实现的函数重写一个实现了的虚函数。

接口

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

  • 无法继承其他合约,不过可以继承其他接口。
  • 接口中所有的函数都需要是 external,尽管在合约里可以是public
  • 无法定义构造函数。
  • 无法定义状态变量。
  • 不可以声明修改器。

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

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

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

pragma solidity >=0.6.2 <0.9.0;

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

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

接口可以继承其他的接口,遵循同样继承规则。

pragma solidity >=0.6.2 <0.9.0;

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

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

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

定义在接口或其他类合约( contract-like)结构体里的类型,可以在其他的合约里用这样的方式访问: Token.TokenTypeToken.Coin.

库与合约类似,库的目的是只需要在特定的地址部署一次,而它们的代码可以通过 EVM 的 DELEGATECALL (Homestead 之前使用 CALLCODE 关键字)特性进行重用。

这意味着如果库函数被调用,它的代码在调用合约的上下文中执行,即 this 指向调用合约,特别注意,他访问的是调用合约存储的状态。 因为每个库都是一段独立的代码,所以它仅能访问调用合约明确提供的状态变量(否则它就无法通过名字访问这些变量)。

因为我们假定库是无状态的,所以如果它们不修改状态(如果它们是 view 或者 pure 函数),库函数仅能通过直接调用来使用(即不使用 DELEGATECALL 关键字), 特别是,任何库不可能被销毁。

注解

在0.4.20版本之前,可以通过绕过Solidity的类型系统来销毁库。 从0.4.20版本开始,库包含一个 调用保护机制,该机制不允许修改状态的函数被直接调用(即不使用 DELEGATECALL )。

使用库就显示是使用基类合约方法类似。虽然它们在继承关系中不会显式可见,但调用库函数与调用显式的基类合约十分类似(可以使用 L.f() 调用)。

当然,使用内部调用约定来调用库的内部函数,这意味着所有的 internal 类型,和 保存在内存类型 都是通过引用而不是复制来传递。

EVM 为了实现这些,合约所调用的内部库函数的代码及内部调用的所有函数都在编译阶段被包含到调用合约中,然后使用一个 JUMP 指令调用来代替 DELEGATECALL

注解

当涉及到 public 函数时,继承的类比就失效了。 用 L.f() 调用一个公共库函数的与外部调用(准确的说是 DELEGATECALL 调用) 差不多。 相反,使用 A.f() 时,当 A 是合约的基类合约时, A.f() 是一个内部调用。

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

pragma solidity >=0.6.0 <0.9.0;

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

library Set {

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

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

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

contract C {
    Data knownValues;

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

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

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

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

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

struct bigint {
    uint[] limbs;
}

library BigInt {

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

    function add(bigint memory a, bigint memory b) internal pure returns (bigint memory 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 limbA = limb(a, i);
            uint limbB = limb(b, i);
            unchecked {
                r.limbs[i] = limbA + limbB + carry;

                if (limbA + limbB < limbA || (limbA + limbB == type(uint).max && carry > 0))
                    carry = 1;
                else
                    carry = 0;
            }
        }
        if (carry > 0) {
            // too bad, we have to add a limb
            uint[] memory newLimbs = new uint[](r.limbs.length + 1);
            uint i;
            for (i = 0; i < r.limbs.length; ++i)
                newLimbs[i] = r.limbs[i];
            newLimbs[i] = carry;
            r.limbs = newLimbs;
        }
    }

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

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

contract C {
    using BigInt for bigint;

    function f() public pure {
        bigint memory x = BigInt.fromUint(7);
        bigint memory y = BigInt.fromUint(type(uint).max);
        bigint memory z = x.add(y);
        assert(z.limb(1) > 0);
    }
}

可以通过类型转换, 将库类型更改为 address 类型, 例如: 使用 address(LibraryName)

由于编译器无法知道库的部署位置,编译器会生成 __$30bbc0abd4d6364515865950d3e0d10953$__ 形式的占位符,该占位符是完整的库名称的keccak256哈希的十六进制编码的34个字符的前缀,例如:如果该库存储在libraries目录中名为bigint.sol的文件中,则完整的库名称为``libraries/bigint.sol:BigInt``。

此类字节码不完整的合约,不应该部署。 占位符需要替换为实际地址。 你可以通过在编译库时将它们传递给编译器或使用链接器更新已编译的二进制文件来实现。

有关如何使用命令行编译器进行链接的信息,请参见 库链接

与合约相比,库的限制:

  • 没有状态变量
  • 不能够继承或被继承
  • 不能接收以太币
  • 不可以被销毁

(将来有可能会解除这些限制)

库的函数签名与选择器

尽管可以对 public 或 external 的库函数进行外部调用,但此类调用会被视为Solidity的内部调用,与常规的 contract ABI 规则不同。

外部库函数比外部合约函数支持更多的参数类型,例如递归结构和指向存储的指针。

因此,计算用于计算4字节选择器的函数签名遵循内部命名模式以及可对合约ABI中不支持的类型的参数使用内部编码。

以下标识符可以作为函数签名中的类型:

  • 值类型, 非存储的(non-storage) string 及非存储的 bytes 使用和合约 ABI 中同样的标识符。
  • 非存储的数组类型遵循合约 ABI 中同样的规则,例如 <type>[] 为动态数组以及 <type>[M]M 个元素的动态数组。
  • 非存储的结构体使用完整的命名引用,例如 C.S 用于 contract C { struct S { ... } }.
  • 存储的映射指针使用 mapping(<keyType> => <valueType>) storage<keyType><valueType> 是映射的键和值类型。
  • 其他的存储的指针类型使用其对应的非存储类型的类型标识符,但在其后面附加一个空格及 storage

除了指向存储的指针以外,参数编码与常规合约ABI相同,存储指针被编码为 uint256 值,指向它们所指向的存储插槽。

与合约 ABI 相似,选择器由签名的Keccak256哈希的前四个字节组成。可以使用 .selector 成员从Solidity中获取其值,如下所示:

pragma solidity >=0.5.14 <0.9.0;

library L {
    function f(uint256) external {}
}

contract C {
    function g() public pure returns (bytes4) {
        return L.f.selector;
    }
}

库的调用保护

如果库的代码是通过 CALL 来执行,而不是 DELEGATECALL 或者 CALLCODE 那么执行的结果会被回退, 除非是对 view 或者 pure 函数的调用。

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

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

这意味着库在链上存储的实际代码与编译器输出的 deployedBytecode 的编码是不同。

Using For

在当前的合约上下里, 指令 using A for B; 可用于附加库函数(从库 A)到任何类型( B)作为成员函数。 这些函数将接收到调用它们的对象作为它们的第一个参数(像 Python 的 self 变量)。

Using For 可在文件或合约内部及合约级都是有效的。

第一部分 A 可以是以下之一:

  • 一些库或文件级的函数列表(using {f, g, h, L.t} for uint;), 仅是那些函数被附加到类型。
  • 库名称 (using L for uint;) ,库里所有的函数(包括 public 和 internal 函数) 被附加到类型上。

在文件级,第二部分 B 必须是一个显式类型(不用指定数据位置)

在合约内,你可以使用 using L for *;, 表示库 L 中的函数被附加在所有类型上。

如果你指定一个库, 库内所有函数都会被加载,即使它们的第一个参数类型与对象的类型不匹配。类型检查会在函数调用和重载解析时执行。

如果你使用函数列表 (using {f, g, h, L.t} for uint;), 那么类型 (uint) 会隐式的转换为这些函数的第一个参数。 即便这些函数中没有一个被调用,这个检查也会进行。

using A for B; 指令仅在当前作用域有效(要么是合约中,或当前模块、或源码单元),包括在作用域内的所有函数,在合约或模块之外则无效。

using for 指令在文件级别使用,并应用于一个用户定义类型(在用一个文件定义的文件级别的用户类型), global 关键字可以添加到末尾。 产生的效果是,这些函数被附加到使用该类型的任何地方(包括其他文件),而不仅仅是声明处所在的作用域。

让我们重写一下来自 一节中的例子,使用文件级函数而不是库函数的方式重写。

struct Data { mapping(uint => bool) flags; }
// Now we attach functions to the type.
// The attached functions can be used throughout the rest of the module.
// If you import the module, you have to
// repeat the using directive there, for example as
//   import "flags.sol" as Flags;
//   using {Flags.insert, Flags.remove, Flags.contains}
//     for Flags.Data;
using {insert, remove, contains} for Data;

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

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

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


contract C {
    Data knownValues;

    function register(uint value) public {
        // Here, all variables of type Data have
        // corresponding member functions.
        // The following function call is identical to
        // `Set.insert(knownValues, value)`
        require(knownValues.insert(value));
    }
}

也可以像这样扩展内置基本类型,在下面的例子中,我们将使用库:

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

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 type(uint).max;
    }
}

using Search for uint[];

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

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

    function replace(uint from, uint to) public {
        // 执行库函数调用
        uint index = data.indexOf(from);
        if (index == type(uint).max)
            data.push(to);
        else
            data[index] = to;
    }
}

注意,所有 external 库调用都是实际的 EVM 函数调用。这意味着如果传递内存或值类型,都将产生一个副本,即使是 self 变量。 引用存储变量或者 internal 库调用 是唯一不会发生拷贝的情况。