合同

实数形式的契约类似于面向对象语言中的类。它们包含状态变量中的持久数据,以及可以修改这些变量的函数。在不同的协定(实例)上调用函数将执行EVM函数调用,从而切换上下文,从而使调用协定中的状态变量不可访问。任何事情发生都需要一个契约和它的功能。在以太坊中没有“cron”的概念来自动调用特定事件中的函数。

创建合同

合同可以通过以太坊交易“从外部”创建,也可以通过Solidity合同创建。

IDES,例如 Remix ,使用ui元素使创建过程无缝。

在以太坊上以编程方式创建契约的一种方法是通过JavaScript API web3.js .它有一个函数 web3.eth.Contract 促进合同创建。

创建合同时, constructor (用 constructor 关键字)执行一次。

构造函数是可选的。只允许一个构造函数,这意味着不支持重载。

建造者执行后,合同的最终代码存储在区块链上。此代码包括所有公共和外部函数,以及通过函数调用可从那里访问的所有函数。部署的代码不包括构造函数代码或仅从构造函数调用的内部函数。

在内部,传递构造函数参数 ABI encoded 在合同代码之后,但是如果你使用 web3.js .

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

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


contract OwnedToken {
    // `TokenCreator` is a contract type that is defined below.
    // It is fine to reference it as long as it is not used
    // to create a new contract.
    TokenCreator creator;
    address owner;
    bytes32 name;

    // This is the constructor which registers the
    // creator and the assigned name.
    constructor(bytes32 _name) {
        // State variables are accessed via their name
        // and not via e.g. `this.owner`. Functions can
        // be accessed directly or through `this.f`,
        // but the latter provides an external view
        // to the function. Especially in the constructor,
        // you should not access functions externally,
        // because the function does not exist yet.
        // See the next section for details.
        owner = msg.sender;

        // We perform an explicit type conversion from `address`
        // to `TokenCreator` and assume that the type of
        // the calling contract is `TokenCreator`, there is
        // no real way to verify that.
        // This does not create a new contract.
        creator = TokenCreator(msg.sender);
        name = _name;
    }

    function changeName(bytes32 newName) public {
        // Only the creator can alter the name.
        // We compare the contract based on its
        // address which can be retrieved by
        // explicit conversion to address.
        if (msg.sender == address(creator))
            name = newName;
    }

    function transfer(address newOwner) public {
        // Only the current owner can transfer the token.
        if (msg.sender != owner) return;

        // We ask the creator contract if the transfer
        // should proceed by using a function of the
        // `TokenCreator` contract defined below. If
        // the call fails (e.g. due to out-of-gas),
        // the execution also fails here.
        if (creator.isTokenTransferOK(owner, newOwner))
            owner = newOwner;
    }
}


contract TokenCreator {
    function createToken(bytes32 name)
        public
        returns (OwnedToken tokenAddress)
    {
        // Create a new `Token` contract and return its address.
        // From the JavaScript side, the return type
        // of this function is `address`, as this is
        // the closest type available in the ABI.
        return new OwnedToken(name);
    }

    function changeName(OwnedToken tokenAddress, bytes32 name) public {
        // Again, the external type of `tokenAddress` is
        // simply `address`.
        tokenAddress.changeName(name);
    }

    // Perform checks to determine if transferring a token to the
    // `OwnedToken` contract should proceed
    function isTokenTransferOK(address currentOwner, address newOwner)
        public
        pure
        returns (bool ok)
    {
        // Check an arbitrary condition to see if transfer should proceed
        return keccak256(abi.encodePacked(currentOwner, newOwner))[0] == 0x7f;
    }
}

能见度和吸气剂

Solidity知道两种类型的函数调用:不创建实际EVM调用的内部函数调用(也称为“消息调用”)和创建实际EVM调用的外部函数调用。因此,函数和状态变量的可见性有四种类型。

必须将函数指定为 externalpublicinternalprivate .对于状态变量, external 不可能。

external

外部函数是合同接口的一部分,这意味着可以从其他合同或通过事务调用它们。外部函数 f 不能在内部调用(即 f() 不起作用,但是 this.f() 工作)。

public

公共函数是契约接口的一部分,可以在内部调用,也可以通过消息调用。对于公共状态变量,将生成一个自动getter函数(见下文)。

internal

这些函数和状态变量只能在内部访问(即,从当前合同或从当前合同派生的合同中),而不能使用 this 。这是状态变量的默认可见性级别。

private

私有函数和状态变量仅对在其中定义的契约可见,而不是在派生契约中。

注解

对于区块链外部的所有观察者来说,合同中的所有内容都是可见的。做点什么 private 只会阻止其他契约读取或修改信息,但对于区块链之外的整个世界来说,它仍然是可见的。

可见性说明符在状态变量类型之后以及函数的参数列表和返回参数列表之间给出。

// SPDX-License-Identifier: GPL-3.0
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 .

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

contract C {
    uint private data;

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

// This will not compile
contract D {
    function readData() public {
        C c = new C();
        uint local = c.f(7); // error: member `f` is not visible
        c.setData(3);
        local = c.getData();
        local = c.compute(3, 5); // error: member `compute` is not visible
    }
}

contract E is C {
    function g() public {
        C c = new C();
        uint val = compute(3, 5); // access to internal member (from derived to parent contract)
    }
}

getter函数

编译器自动为所有 公众的 状态变量。对于下面给出的契约,编译器将生成一个名为 data 不接受任何参数并返回 uint ,状态变量的值 data .状态变量可以在声明时初始化。

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

contract C {
    uint public data = 42;
}

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

getter函数具有外部可见性。如果在内部访问符号(即 this. ,它计算为状态变量。如果从外部访问(即 this. ,它计算为一个函数。

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

contract C {
    uint public data;
    function x() public returns (uint) {
        data = 3; // internal access
        return this.data(); // external access
    }
}

如果你有一个 public 数组类型的状态变量,则只能通过生成的getter函数检索数组的单个元素。存在该机制是为了在返回整个阵列时避免高昂的气体成本。您可以使用参数指定要返回的单个元素,例如 myArray(0) 。如果您希望在一次调用中返回整个数组,则需要编写一个函数,例如:

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.16 <0.9.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;
    }
}

现在你可以使用 getArray() 以检索整个数组,而不是 myArray(i) ,每次调用返回一个元素。

下一个例子更复杂:

// SPDX-License-Identifier: GPL-3.0
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;
}

它生成以下形式的函数。结构中的映射和数组(字节数组除外)被省略,因为没有选择单个结构成员或为映射提供键的好方法:

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

函数修饰符

可以用声明性的方式改变函数的行为。例如,可以使用修饰符在执行函数之前自动检查条件。

修饰符是协定的可继承属性,可以由派生协定重写,但前提是它们已标记 virtual . 有关详细信息,请参阅 Modifier Overriding .

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

contract owned {
    constructor() { owner = payable(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 {
        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 {
        selfdestruct(owner);
    }
}

contract priced {
    // Modifiers can receive arguments:
    modifier costs(uint price) {
        if (msg.value >= price) {
            _;
        }
    }
}

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

    constructor(uint initialPrice) { price = initialPrice; }

    // It is important to also provide the
    // `payable` keyword here, otherwise the function will
    // automatically reject all Ether sent to it.
    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;
    }

    /// This function is protected by a mutex, which means that
    /// reentrant calls from within `msg.sender.call` cannot call `f` again.
    /// The `return 7` statement assigns 7 to the return value but still
    /// executes the statement `locked = false` in the modifier.
    function f() public noReentrancy returns (uint) {
        (bool success,) = msg.sender.call("");
        require(success);
        return 7;
    }
}

如果要访问修改量 m 在合同中定义的 C ,您可以使用 C.m 在不进行虚拟查找的情况下引用它。只能使用当前合同或其基础合同中定义的修改量。也可以在库中定义修饰符,但它们的使用仅限于同一库的函数。

通过在空格分隔的列表中指定多个修饰符来应用于函数,并按照呈现的顺序进行计算。

修饰符不能隐式访问或更改它们修改的函数的参数和返回值。它们的值只能在调用点显式传递给它们。

修饰符或函数体的显式返回只留下当前修饰符或函数体。返回变量被赋值,并且控制流在 _ 在前面的修饰语中。

警告

在早期版本的solidity中, return 具有修饰符的函数中的语句的行为不同。

修饰符的显式返回 return; 不影响函数返回的值。但是,修饰符可以选择根本不执行函数体,在这种情况下,返回的变量设置为其 default values 就像函数有一个空体一样。

这个 _ 符号可以在修改器中多次出现。每个匹配项都替换为函数体。

修饰符参数允许使用任意表达式,在此上下文中,函数中可见的所有符号都在修饰符中可见。在修饰符中引入的符号在函数中不可见(因为它们可能通过重写而更改)。

恒定不变的状态变量

状态变量可以声明为 constantimmutable . 在这两种情况下,在构建契约之后,变量都不能被修改。为 constant 变量时,该值必须在编译时固定,而对于 immutable ,仍可以在施工时分配。

还可以定义 constant 文件级别的变量。

编译器不会为这些变量保留存储槽,每次出现都会被相应的值替换。

与常规状态变量相比,常变量和不变变量的气体成本要低得多。对于常量变量,分配给它的表达式会复制到访问它的所有位置,并且每次都会重新计算。这允许局部优化。不可变变量在构造时计算一次,它们的值被复制到代码中访问它们的所有位置。对于这些值,保留了32个字节,即使它们可以容纳更少的字节。因此,常数值有时比不变值便宜。

并不是所有常量和不可变的类型都在此时实现。唯一支持的类型是 strings (仅适用于常量)和 value types .

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

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

常数

为了 constant 变量,则该值在编译时必须是常量,并且必须在声明变量的位置赋值。任何访问存储、区块链数据的表达式(例如。 block.timestampaddress(this).balanceblock.number )或执行数据 (msg.valuegasleft() )或者不允许调用外部合同。允许使用可能对内存分配产生副作用的表达式,但不允许使用可能对其他内存对象产生副作用的表达式。内置功能 keccak256sha256ripemd160ecrecoveraddmodmulmod 允许(即使,除了 keccak256 他们称之为外部合同)。

允许对内存分配器产生副作用的原因是,应该可以构造像查找表这样的复杂对象。此功能尚未完全可用。

不变的

声明为 immutable 比那些声明为 constant :不可变变量可以在协定的构造函数中或在其声明点被赋予任意值。它们只能分配一次,并且从那时起,即使在施工期间也可以读取。

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

注解

在声明时分配的不可变变量仅在协定的构造函数执行时才被视为已初始化。这意味着不能用依赖于另一个不可变变量的值内联初始化不可变变量。但是,您可以在约定的构造函数中执行此操作。

这可以防止对状态变量初始化和构造函数执行顺序的不同解释,特别是在继承方面。

功能

功能可以在合同内部和外部定义。

契约之外的功能,也称为“自由函数”,总是有隐含的 internal visibility . 它们的代码包含在调用它们的所有契约中,类似于内部库函数。

// 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 ,可以调用其他合同,向它们发送以太,并销毁调用它们的合同,等等。与协定中定义的函数的主要区别在于,自由函数不能直接访问其作用域之外的存储变量和函数。

函数参数和返回变量

函数将类型化参数作为输入,并且与其他许多语言不同,函数还可以返回任意数量的值作为输出。

功能参数

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

例如,如果您希望您的合同接受一种具有两个整数的外部呼叫,您可以使用类似以下内容:

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

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

函数参数可以用作任何其他局部变量,也可以分配给它们。

注解

一个 external function 无法接受多维数组作为输入参数。如果您通过添加以下命令来启用ABI编码器v2,则可以实现此功能 pragma abicoder v2; 添加到您的源文件中。

internal function 可以接受多维数组而不启用该功能。

返回变量

函数返回变量在 returns 关键字。

例如,假设您要返回两个结果:作为函数参数传递的两个整数的和和乘积,则使用如下内容:

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

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

返回变量的名称可以省略。返回变量可以用作任何其他局部变量,并用它们初始化 default value 一直保持这个值直到它们被(重新)分配。

您可以显式赋值给返回变量,然后保持函数不变,也可以提供返回值(单个或 multiple ones )直接与 return 声明:

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

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

如果你用 return 若要保留具有返回变量的函数,必须在return语句的同时提供返回值。

注解

不能从非内部函数返回某些类型,特别是多维动态数组和结构。如果您通过添加以下命令来启用ABI编码器v2 pragma abicoder v2; 添加到源文件,则有更多类型可用,但是 mapping 类型仍然限制在单个合同内,您不能转移它们。

返回多个值

当一个函数有多个返回类型时,该语句 return (v0, v1, ..., vn) 可用于返回多个值。组件的数量必须与返回变量的数量相同,并且它们的类型必须匹配,可能在 implicit conversion .

状态可突变性

查看函数

函数可以声明 view 在这种情况下,他们承诺不修改状态。

注解

如果编译器的evm目标是拜占庭或更新的(默认)操作码 STATICCALL 使用时 view 调用函数,强制状态保持不变,作为EVM执行的一部分。类库 view 功能 DELEGATECALL 使用,因为没有组合 DELEGATECALLSTATICCALL .这意味着类库 view 函数没有阻止状态修改的运行时检查。这不会对安全性产生负面影响,因为库代码通常在编译时已知,静态检查器执行编译时检查。

以下语句被视为修改状态:

  1. 写入状态变量。

  2. Emitting events .

  3. Creating other contracts .

  4. 使用 selfdestruct .

  5. 通过电话发送 Ether 。

  6. 调用任何未标记的函数 viewpure .

  7. 使用低级呼叫。

  8. 使用包含某些操作码的内联程序集。

// SPDX-License-Identifier: GPL-3.0
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版之前,编译器没有使用 STATICCALL 操作码 view 功能。启用状态修改 view 函数通过使用无效的显式类型转换。通过使用 STATICCALL 对于 view 在EVM级别上阻止对状态的修改和功能。

纯函数

函数可以声明 pure 在这种情况下,他们承诺不阅读或修改状态。

注解

如果编译器的evm目标是拜占庭或更新的(默认)操作码 STATICCALL 使用,这不保证不读取状态,但至少不修改状态。

除了上述状态修改语句列表外,还考虑从状态中读取以下内容:

  1. 从状态变量读取。

  2. 访问 address(this).balance<address>.balance .

  3. 访问的任何成员 blocktxmsg (除了 msg.sigmsg.data

  4. 调用任何未标记的函数 pure .

  5. 使用包含某些操作码的内联程序集。

// SPDX-License-Identifier: GPL-3.0
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()error occurs .

恢复状态更改不被视为“状态修改”,因为只有以前在没有 viewpure 限制被恢复,并且该代码可以选择捕获 revert 而且不能传下去。

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

警告

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

注解

在0.5.0版之前,编译器没有使用 STATICCALL 操作码 pure 功能。启用状态修改 pure 函数通过使用无效的显式类型转换。通过使用 STATICCALL 对于 pure 在EVM级别上阻止对状态的修改和功能。

注解

在0.4.17版本之前,编译器没有强制 pure 没有读取状态。这是一个编译时类型检查,可以避免在协定类型之间执行无效的显式转换,因为编译器可以验证协定的类型不执行状态更改操作,但不能检查将在运行时调用的协定实际上是是那种类型的。

特殊功能

接收乙醚功能

一份合同最多只能有一个 receive 函数,使用 receive() external payable {{ ... }} (没有 function 关键字)。此函数不能有参数,不能返回任何内容,并且必须具有 external 可见性和 payable 状态可变性。它可以是虚拟的,可以覆盖,并且可以有修饰符。

接收函数在调用具有空calldata的协定时执行。这是在普通以太网传输(例如,VIA)上执行的功能 .send().transfer() )。如果不存在此类函数,但应付款 fallback function 存在,则将在普通以太网传输中调用回退函数。如果既不存在接收Ether,也不存在Payable Fallback功能,则合同无法通过常规事务接收Ether,并引发异常。

在最坏的情况下 receive 功能只能依赖于2300气体可用(例如 sendtransfer 使用),除了基本日志记录外,几乎没有空间执行其他操作。以下操作将消耗比2300天然气津贴更多的天然气:

  • 写入存储器

  • 创建合同

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

  • 发送 Ether

警告

直接接收乙醚的合同(没有函数调用,即使用 sendtransfer )但是不要定义一个receive-Ether函数或一个payable fallback函数抛出一个异常,发送回Ether(这在Solidity v0.4.0之前是不同的)。因此,如果您希望您的契约接收Ether,那么必须实现一个receive-Ether函数(不建议使用payable fallback函数来接收Ether,因为它不会在接口混淆时失败)。

警告

没有receive-Ether函数的契约可以作为 coinbase交易 (阿卡 矿工封锁奖励 )或者作为一个目的地 selfdestruct .

合同不能对这种 Ether 转移作出反应,因此也不能拒绝它们。这是EVM的设计选择,Solidity不能解决这个问题。

这也意味着 address(this).balance 可以高于合同中执行的某些手动会计的总和(即在receive Ether函数中更新计数器)。

下面您可以看到一个使用函数的Sink契约的示例 receive .

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.6.0 <0.9.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);
    }
}

回退函数

一份合同最多只能有一个 fallback 函数,使用以下任一方法声明 fallback () external [payable]fallback (bytes calldata _input) external [payable] returns (bytes memory _output) (两者都没有 function 关键字)。此函数必须具有 external 可见性。回退函数可以是虚拟的,可以重写,并且可以有修饰符。

如果没有其他函数与给定的函数签名匹配,或者如果根本没有提供数据且没有提供任何数据,则在调用协定时执行回退函数 receive Ether function 。回退功能始终接收数据,但为了也接收Ether,必须将其标记为 payable

如果使用带参数的版本, _input 将包含发送到合同的完整数据(等于 msg.data ),并且可以将数据返回到 _output 。返回的数据将不会进行ABI编码。取而代之的是,它将在没有修改的情况下返回(甚至不需要填充)。

在最坏的情况下,如果还使用应付回退功能来代替接收功能,则只能依赖2300个可用气体(参见 receive Ether function 以获得对其含义的简要描述)。

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

警告

A payable 如果没有,也会对纯以太传输执行回退功能 receive Ether function 存在。如果您定义了一个应付回退函数来区分以太网传输和接口混淆,那么建议您始终定义一个接收以太函数。

注解

如果要对输入数据进行解码,可以检查函数选择器的前四个字节,然后可以使用 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 {
    uint x;
    // This function is called for all messages sent to
    // this contract (there is no other function).
    // Sending Ether to this contract will cause an exception,
    // because the fallback function does not have the `payable`
    // modifier.
    fallback() external { x = 1; }
}

contract TestPayable {
    uint x;
    uint y;
    // This function is called for all messages sent to
    // this contract, except plain Ether transfers
    // (there is no other function except the receive function).
    // Any call with non-empty calldata to this contract will execute
    // the fallback function (even if Ether is sent along with the call).
    fallback() external payable { x = 1; y = msg.value; }

    // This function is called for plain Ether transfers, i.e.
    // for every call with 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);
        // results in test.x becoming == 1.

        // address(test) will not allow to call ``send`` directly, since ``test`` has no payable
        // fallback function.
        // It has to be converted to the ``address payable`` type to even allow calling ``send`` on it.
        address payable testPayable = payable(address(test));

        // If someone sends Ether to that contract,
        // the transfer will fail, i.e. this returns false here.
        return testPayable.send(2 ether);
    }

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

        // If someone sends Ether to that contract, the receive function in TestPayable will be called.
        // Since that function writes to storage, it takes more gas than is available with a
        // simple ``send`` or ``transfer``. Because of that, we have to use a low-level call.
        (success,) = address(test).call{value: 2 ether}("");
        require(success);
        // results in test.x becoming == 2 and test.y becoming 2 ether.

        return true;
    }
}

函数得载

合同可以有多个同名但参数类型不同的函数。这个过程称为“重载”,也适用于继承的函数。下面的示例显示函数的重载 f 在合同范围内 A .

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.16 <0.9.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;
    }
}

重载函数也存在于外部接口中。如果两个外部可见的函数的solidity类型不同,但外部类型不同,则为错误。

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

// This will not compile
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 上面的函数重载最终接受ABI的地址类型,尽管它们在solidity中被认为是不同的。

过载分辨率和参数匹配

通过将当前作用域中的函数声明与函数调用中提供的参数匹配来选择重载函数。如果所有参数都可以隐式转换为预期类型,则将选择函数作为重载候选者。如果没有一个候选人,决议就失败了。

注解

重载解决方案不考虑返回参数。

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

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

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

调用 f(50) 将创建类型错误,因为 50 无法将两者隐式转换为 uint8uint256 类型。另一方面 f(256) 会下决心 f(uint256) 过载组件 256 无法隐式转换为 uint8 .

事件

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

事件是契约的可继承成员。当你调用它们时,它们会导致参数存储在交易日志中——区块链中的一种特殊数据结构。这些日志与合同的地址相关联,被合并到区块链中,只要区块可以访问,就一直保持在那里(直到现在,这可能会随着平静而改变)。无法从合同中访问日志及其事件数据(甚至不能从创建它们的合同中访问)。

可以为日志请求Merkle证明,因此如果外部实体向合同提供此类证明,则可以检查日志是否确实存在于区块链中。必须提供块头,因为契约只能看到最后256个块散列。

您可以添加属性 indexed 添加到最多三个参数,这会将它们添加到称为 "topics" 而不是日志的数据部分。一个主题只能包含一个单词(32字节),因此如果使用 reference type 对于索引参数,取而代之的是将值的Keccak-256散列存储为主题。

所有参数没有 indexed 属性是 ABI-encoded 进入日志的数据部分。

主题允许您搜索事件,例如在筛选特定事件的块序列时。还可以按发出事件的协定的地址筛选事件。

例如,下面的代码使用web3.js subscribe("logs") method 要筛选与特定地址值匹配的主题日志:

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 说明符。这意味着,对于具体的匿名事件,过滤无法点名,只能按合同地址点名过滤。匿名事件的优势在于它们的部署和调用成本更低。它还允许您声明四个索引参数,而不是三个。

注解

由于事务日志只存储事件数据,而不存储类型,因此您必须知道事件的类型,包括哪个参数被索引,以及事件是否为匿名的,以便正确解释数据。特别是,使用匿名事件“伪造”另一个事件的签名是可能的。

// SPDX-License-Identifier: GPL-3.0
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 {
        // Events are emitted using `emit`, followed by
        // the name of the event and the arguments
        // (if any) in parentheses. Any such invocation
        // (even deeply nested) can be detected from
        // the JavaScript API by filtering for `Deposit`.
        emit Deposit(msg.sender, _id, msg.value);
    }
}

javascript API中的用法如下:

var abi = /* abi as generated by the compiler */;
var ClientReceipt = web3.eth.contract(abi);
var clientReceipt = ClientReceipt.at("0x1234...ab67" /* address */);

var depositEvent = clientReceipt.Deposit();

// watch for changes
depositEvent.watch(function(error, result){
    // result contains non-indexed arguments and topics
    // given to the `Deposit` call.
    if (!error)
        console.log(result);
});


// Or pass a callback to start watching immediately
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"]
   }
}

了解事件的其他资源

错误和REVERT语句

稠度误差提供了一种方便且省油的方式来向用户解释操作失败的原因。它们可以在契约内部和外部定义(包括接口和库)。

它们必须与 revert statement 这会导致恢复当前调用中的所有更改,并将错误数据传回调用方。

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

/// Insufficient balance for transfer. Needed `required` but only
/// `available` available.
/// @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 结算单。

该错误创建数据,然后使用还原操作将该数据传递给调用方,以返回到离链组件或在 try/catch statement 。请注意,只有在来自外部调用时才能捕获错误,无法捕获内部调用或同一函数内部发生的还原。

如果不提供任何参数,则错误只需要四个字节的数据,您可以使用 NatSpec 如上所述,以进一步解释错误背后的原因,该错误没有存储在链上。这使得这同时成为一种非常便宜和方便的错误报告功能。

更具体地说,错误实例以与对具有相同名称和类型的函数的函数调用相同的方式进行ABI编码,然后用作 revert 操作码。这意味着数据由一个4字节的选择符组成,后跟 ABI-encoded 数据。选择器由错误类型签名的keccak256散列的前四个字节组成。

注解

合同有可能还原时出现同名的不同错误,甚至可能出现调用者无法区分的在不同位置定义的错误。对于外部,即ABI,只有错误的名称是相关的,而不是定义它的合同或文件。

该声明 require(condition, "description"); 将相当于 if (!condition) revert Error("description") 如果你能定义一下 error Error(string) 。但是,请注意, Error 是内置类型,不能在用户提供的代码中定义。

同样,失败的 assert 或者类似的情况将恢复,并返回内置类型的错误 Panic(uint256)

注解

错误数据应仅用于指示故障,而不能用作控制流的手段。原因是,默认情况下,内部调用的还原数据通过外部调用链传回。这意味着内部调用可以“伪造”看起来可能来自调用它的约定的还原数据。

遗传

solidity支持包括多态性在内的多重继承。

多态性意味着函数调用(内部和外部)总是执行继承层次结构中最派生的协定中同名(和参数类型)的函数。必须使用 virtualoverride 关键词。看到了吗 Function Overriding 了解更多详细信息。

通过使用显式指定协定,可以在继承层次结构中进一步调用函数 ContractName.functionName() 或使用 super.functionName() 如果您想在平坦继承层次结构中向上调用函数一个级别(见下文)。

当一个合同从其他合同继承时,区块链上只创建一个合同,所有基础合同的代码被编译成创建的合同。这意味着对基础契约函数的所有内部调用也只使用内部函数调用。 (super.f(..) 将使用跳转而不是消息调用)。

状态变量跟踪被认为是一个错误。派生协定只能声明状态变量 x ,如果它的任何基中没有同名的可见状态变量。

一般的继承制度与 Python's 特别是关于多重继承,但也有一些 differences .

下面的示例中给出了详细信息。

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


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


// Use `is` to derive from another contract. Derived
// contracts can access all non-private members including
// internal functions and state variables. These cannot be
// accessed externally via `this`, though.
contract Destructible is Owned {
    // The keyword `virtual` means that the function can change
    // its behaviour in derived classes ("overriding").
    function destroy() virtual public {
        if (msg.sender == owner) selfdestruct(owner);
    }
}


// These abstract contracts are only provided to make the
// interface known to the compiler. Note the function
// without body. If a contract does not implement all
// functions it can only be used as an interface.
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;
}


// Multiple inheritance is possible. Note that `owned` is
// also a base class of `Destructible`, yet there is only a single
// instance of `owned` (as for virtual inheritance in C++).
contract Named is Owned, Destructible {
    constructor(bytes32 name) {
        Config config = Config(0xD5f9D8D94886E70b06E474c3fB14Fd43E2f23970);
        NameReg(config.lookup(1)).register(name);
    }

    // Functions can be overridden by another function with the same name and
    // the same number/types of inputs.  If the overriding function has different
    // types of output parameters, that causes an error.
    // Both local and message-based function calls take these overrides
    // into account.
    // If you want the function to override, you need to use the
    // `override` keyword. You need to specify the `virtual` keyword again
    // if you want this function to be overridden again.
    function destroy() public virtual override {
        if (msg.sender == owner) {
            Config config = Config(0xD5f9D8D94886E70b06E474c3fB14Fd43E2f23970);
            NameReg(config.lookup(1)).unregister();
            // It is still possible to call a specific
            // overridden function.
            Destructible.destroy();
        }
    }
}


// If a constructor takes an argument, it needs to be
// provided in the header or modifier-invocation-style at
// the constructor of the derived contract (see below).
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 payable owner;
}

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

contract Base1 is Destructible {
    function destroy() public virtual override { /* do cleanup 1 */ Destructible.destroy(); }
}

contract Base2 is Destructible {
    function destroy() public virtual override { /* do cleanup 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 payable owner;
}

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

contract Base1 is Destructible {
    function destroy() public virtual override { /* do cleanup 1 */ super.destroy(); }
}


contract Base2 is Destructible {
    function destroy() public virtual override { /* do cleanup 2 */ super.destroy(); }
}

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

如果 Base2 调用的函数 super ,它不简单地在它的一个基础契约上调用这个函数。相反,它在最终继承图中的下一个基契约上调用这个函数,因此它将调用 Base1.destroy() (注意,最后的继承序列是——从最派生的契约开始:final、Base2、Base1、destructable、owned)。在使用super时调用的实际函数在使用它的类的上下文中是未知的,尽管它的类型是已知的。这与普通的虚拟方法查找类似。

函数重写

如果基函数标记为,则可以通过继承协定来更改其行为来重写基函数 virtual . 重写函数必须使用 override 函数头中的关键字。重写函数只能从中更改重写函数的可见性 externalpublic . 可变性可按以下顺序改为更严格的可变性: 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 关键字。换言之,您必须指定所有定义同一函数且尚未被另一个基协定覆盖(在继承图的某个路径上)的基协定。另外,如果一个契约从多个(不相关的)基继承同一个函数,它必须显式地重写它:

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

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

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

contract Inherited is Base1, Base2
{
    // Derives from multiple bases defining foo(), so we must explicitly
    // override it
    function foo() public override(Base1, Base2) {}
}

如果函数是在公共基协定中定义的,或者如果公共基协定中存在已重写所有其他函数的唯一函数,则不需要显式重写说明符。

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

contract A { function f() public pure{} }
contract B is A {}
contract C is A {}
// No explicit override required
contract D is B, C {}

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

从这个意义上说,签名的重写路径是一条通过继承图的路径,该路径从考虑中的契约开始,到引用了一个没有重写的签名的函数的契约结束。

如果未将覆盖为的函数标记为 virtual ,派生契约不再能够改变该函数的行为。

注解

函数 private 可见性不能 virtual .

注解

必须标记没有实现的函数 virtual 在接口之外。在接口中,所有函数都被自动考虑 virtual .

注解

从坚固度0.8.8开始, override 重写接口函数时不需要关键字,但函数定义在多个基础上的情况除外。

如果函数的参数和返回类型与变量的getter函数匹配,则公共状态变量可以重写外部函数:

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

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

contract B is A
{
    uint public override f;
}

注解

虽然公共状态变量可以覆盖外部函数,但它们本身不能被重写。

修改器替代

函数修饰符可以互相重写。其工作原理与 function overriding (除了修饰符没有重载)。这个 virtual 关键字必须用于重写的修饰符和 override 必须在重写修饰符中使用关键字:

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

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

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

在多重继承的情况下,必须明确指定所有直接基础契约:

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.6.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 关键字,在合同创建时执行,并且可以在其中运行合同初始化代码。

在执行构造函数代码之前,如果以内联方式将状态变量初始化为其指定值,则会将状态变量初始化为其指定值,或者将状态变量的 default value 如果你不那么做。

在构造函数运行之后,合同的最终代码被部署到区块链中。代码的部署需要额外的与代码长度成线性的气体。此代码包括作为公共接口一部分的所有函数以及通过函数调用从公共接口可访问的所有函数。它不包括仅从构造函数调用的构造函数代码或内部函数。

如果没有构造函数,则契约将假定默认构造函数,相当于 constructor() {{}} . 例如:

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

abstract contract A {
    uint public a;

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

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

可以在构造函数中使用内部参数(例如存储指针)。在这种情况下,合同必须标记 abstract ,因为这些参数不能从外部赋值,只能通过派生协定的构造函数赋值。

警告

在0.4.22版本之前,构造函数被定义为与合同同名的函数。此语法已被弃用,在0.5.0版中不再允许使用。

警告

在版本0.7.0之前,必须将构造函数的可见性指定为 internalpublic .

基本构造函数的参数

所有基本合同的构造函数都将按照下面解释的线性化规则进行调用。如果基构造器有参数,派生契约需要指定所有参数。这可以通过两种方式完成:

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

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

// Either directly specify in the inheritance list...
contract Derived1 is Base(7) {
    constructor() {}
}

// or through a "modifier" of the derived constructor.
contract Derived2 is Base {
    constructor(uint _y) Base(_y * _y) {}
}

一种方法是直接在继承列表中 (is Base(7) )。另一种方法是将修饰符作为派生构造函数的一部分调用。 (Base(_y * _y) )。如果构造函数参数是一个常量,并且定义了契约的行为或对其进行了描述,那么第一种方法就更方便了。如果基的构造函数参数依赖于派生协定的参数,则必须使用第二种方法。必须在继承列表中或在派生构造函数的修饰符样式中提供参数。在两处指定参数都是错误的。

如果派生协定没有为其所有基本协定的构造函数指定参数,那么它将是抽象的。

多重继承与线性化

允许多重继承的语言必须处理几个问题。一个是 Diamond Problem .solidity类似于python,它使用“c3 linearization<https://en.wikipedia.org/wiki/c3_linearization>”,强制基类的有向非循环图(dag)中的特定顺序。这导致了单调性的理想性质,但不允许某些继承图。特别是,基类在 is 指令很重要:您必须按照从“最基本的类似”到“最派生的”的顺序列出直接基本合同。请注意,这个顺序与Python中使用的顺序相反。

解释这一点的另一种简化方法是,当一个函数在不同的契约中被多次定义时,将以深度优先的方式从右到左(在python中从左到右)搜索给定的基,并在第一次匹配时停止。如果已搜索到基本合同,则跳过它。

在下面的代码中,solidity将给出错误“继承图不可能线性化”。

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

contract X {}
contract A is X {}
// This will not compile
contract C is A, X {}

原因是 C 请求 X 覆盖 A (通过指定 A, X 按照这个顺序),但是 A 自身请求重写 X 这是一个无法解决的矛盾。

由于您必须显式重写从多个基继承的函数而没有唯一重写,因此C3线性化在实践中并不太重要。

继承线性化特别重要的一个领域是,当继承层次结构中存在多个构造函数时,这一点可能不那么清楚。无论继承合同的构造函数中参数的提供顺序如何,构造函数都将始终按线性化顺序执行。例如:

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

contract Base1 {
    constructor() {}
}

contract Base2 {
    constructor() {}
}

// Constructors are executed in the following order:
//  1 - Base1
//  2 - Base2
//  3 - Derived1
contract Derived1 is Base1, Base2 {
    constructor() Base1() Base2() {}
}

// Constructors are executed in the following order:
//  1 - Base2
//  2 - Base1
//  3 - Derived2
contract Derived2 is Base2, Base1 {
    constructor() Base2() Base1() {}
}

// Constructors are still executed in the following order:
//  1 - Base2
//  2 - Base1
//  3 - Derived3
contract Derived3 is Base2, Base1 {
    constructor() Base1() Base2() {}
}

继承不同类型的同名成员

如果协定中的以下任何一对由于继承而具有相同的名称,则会出错:
  • 函数和修改器

  • 函数和事件

  • 事件和修饰符

作为例外,状态变量getter可以重写外部函数。

摘要合同

当至少有一个功能没有实现时,合同需要标记为抽象的。即使实现了所有的功能,合同也可以标记为抽象的。

这可以通过使用 abstract 关键字,如下例所示。请注意,此约定需要定义为抽象的,因为函数 utterance() 已定义,但未提供任何实现(没有实现主体 {{ }} 已给予)。

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

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

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

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

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

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

如果契约继承自抽象契约,并且没有通过重写实现所有未实现的函数,则还需要将其标记为抽象。

注意,没有实现的函数与 Function Type 尽管它们的语法看起来非常相似。

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

function foo(address) external returns (address);

类型为函数类型的变量声明示例:

function(address) external returns (address) foo;

抽象契约将契约的定义与实现分离,提供更好的可扩展性和自文档,并促进类似 Template method 以及消除代码重复。抽象契约的使用方式与在接口中定义方法的使用方式相同。抽象合同的设计者可以说“我的任何一个孩子都必须实现这个方法”。

注解

抽象契约不能用未实现的虚函数重写已实现的虚函数。

界面

接口类似于抽象契约,但不能实现任何函数。还有其他限制:

  • 它们不能从其他契约继承,但可以从其他接口继承。

  • 所有声明的函数都必须是外部的。

  • 它们不能声明构造函数。

  • 它们不能声明状态变量。

  • 它们不能声明修饰符。

其中一些限制可能会在未来解除。

接口基本上仅限于ABI可以表示的契约,ABI和接口之间的转换应该是可能的,而不会丢失任何信息。

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

// SPDX-License-Identifier: GPL-3.0
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 关键字。这并不自动意味着可以再次重写重写函数-只有在重写函数被标记时才有可能 virtual

接口可以从其他接口继承。这与普通继承具有相同的规则。

// SPDX-License-Identifier: GPL-3.0
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 {
    // Must redefine test in order to assert that the parent
    // meanings are compatible.
    function test() external override(ParentA, ParentB) returns (uint256);
}

接口内部定义的类型和其他类似契约的结构可以从其他契约访问: Token.TokenTypeToken.Coin .

类库

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. As a library is an isolated piece of source code, it can only access state variables of the calling contract if they are explicitly supplied (it would have no way to name them, otherwise). Library functions can only be called directly (i.e. without the use of DELEGATECALL) if they do not modify the state (i.e. if they are view or pure functions), because libraries are assumed to be stateless. In particular, it is not possible to destroy a library.

注解

在0.4.20版本之前,可以绕过solidity的类型系统来销毁库。从该版本开始,库包含 mechanism 这不允许直接调用状态修改函数(即 DELEGATECALL

库可以看作是使用它们的契约的隐式基本契约。它们在继承层次结构中不会显式可见,但对库函数的调用看起来就像对显式基协定的函数的调用(使用限定的访问,如 L.f() ). 当然,对内部函数的调用使用内部调用约定,这意味着可以传递所有内部类型和类型 stored in memory 将通过引用传递而不是复制。为了在EVM中实现这一点,内部库函数的代码以及从中调用的所有函数将在编译时包含在调用契约中,并且 JUMP 将使用呼叫而不是 DELEGATECALL .

注解

当涉及到公共函数时,继承类比就失效了。使用调用公共库函数 L.f() 导致外部呼叫 (DELEGATECALL 准确地说)。相比之下, A.f() 在以下情况下是内部呼叫 A 是当前合同的基础合同。

下面的示例演示了如何使用库(但是使用手动方法,请务必签出 using for 对于实现集合的更高级示例)。

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.6.0 <0.9.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 C {
    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.
}

当然,使用库不必遵循这种方法:也可以在不定义结构数据类型的情况下使用库。函数也可以在没有任何存储引用参数的情况下工作,并且它们可以在任何位置具有多个存储引用参数。

呼叫 Set.containsSet.insertSet.remove 都编译为调用 (DELEGATECALL )外部合同/类库。如果使用库,请注意实际执行的外部函数调用。 msg.sendermsg.valuethis 但是,由于使用了 CALLCODEmsg.sendermsg.value 但是改变了)。

下面的示例演示如何使用 types stored in memory 以及库中的内部函数,以实现自定义类型而不需要外部函数调用的开销:

// 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 memory 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 a = limb(_a, i);
            uint b = limb(_b, i);
            unchecked {
                r.limbs[i] = a + b + carry;

                if (a + b < a || (a + b == 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 _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;

    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:BigInt 如果库存储在名为 bigint.sol 在一个 libraries/ 目录。这样的字节码是不完整的,不应该部署。占位符需要替换为实际地址。您可以通过在编译库时将它们传递给编译器,或者通过使用链接器更新已编译的二进制文件来实现这一点。看见 库链接 有关如何使用命令行编译器进行链接的信息。

与合同相比,类库在以下方面受到限制:

  • 它们不能有状态变量

  • 他们既不能继承也不能被继承

  • 他们不能接收乙醚

  • 它们不能被摧毁

(稍后可能会将其提起。)

库中的函数签名和选择器

虽然可以对公共或外部库函数进行外部调用,但此类调用的调用约定被认为是Solidity的内部调用,与为常规函数指定的调用约定不同 contract ABI . 与外部协定函数相比,外部库函数支持更多的参数类型,例如递归结构和存储指针。因此,用于计算4字节选择器的函数签名是按照内部命名模式计算的,契约ABI中不支持的类型的参数使用内部编码。

以下标识符用于签名中的类型:

  • 值类型,非存储 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 成员如下:

// SPDX-License-Identifier: GPL-3.0
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 而不是 DELEGATECALLCALLCODE ,它将还原,除非 viewpure 调用函数。

EVM不提供合同直接检测是否使用 CALL 或者不是,但是合同可以使用 ADDRESS 操作码,找出它当前运行的位置。生成的代码将此地址与构造时用于确定调用模式的地址进行比较。

更具体地说,库的运行时代码总是以一个push指令开始,该指令在编译时为20字节的零。当部署代码运行时,这个常量在内存中被当前地址替换,修改后的代码存储在协定中。在运行时,这会导致部署时间地址成为第一个被推送到堆栈上的常量,并且对于任何非视图和非纯函数,调度程序代码会将当前地址与该常量进行比较。

这意味着存储在库的链上的实际代码与编译器报告的代码不同 deployedBytecode .

用于

指令 using A for B; 可用于附加库函数(从库 A )任何类型 (B )在合同的上下文中。这些函数将接收被调用的对象作为第一个参数(如 self python中的变量)。

影响 using A for *; 是类库的功能吗 A 附加到 any 类型。

在这两种情况下, all 库中的函数被附加,即使是第一个参数的类型与对象的类型不匹配的函数。在调用函数时检查类型,并执行函数重载解决方案。

这个 using A for B; 指令仅在当前合同内有效,包括在其所有功能内,并且在使用指令的合同外无效。指令只能在合同中使用,不能在其任何功能中使用。

让我们重写 类库 通过这种方式:

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


// This is the same code as before, just without comments
struct Data { mapping(uint => bool) flags; }

library Set {
    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 C {
    using Set for Data; // this is the crucial change
    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.6.8 <0.9.0;

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

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

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

    function replace(uint _old, uint _new) public {
        // This performs the library function call
        uint index = data.indexOf(_old);
        if (index == type(uint).max)
            data.push(_new);
        else
            data[index] = _new;
    }
}

请注意,所有外部库调用都是实际的EVM函数调用。这意味着,如果传递内存或值类型,将执行复制,甚至 self 变量。不执行复制的唯一情况是使用存储引用变量或调用内部库函数时。