美文网首页
[Solidity] view pure receive 函数

[Solidity] view pure receive 函数

作者: 朱慢慢 | 来源:发表于2022-08-05 17:55 被阅读0次

View 视图函数

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

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

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

Pure 纯函数

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

  • 特别是,应该可以在编译时确定一个 pure 函数,它仅处理输入参数和 msg.data ,对当前区块链状态没有任何了解。 这也意味着读取 immutable 变量也不是一个 pure 操作。
除了上面解释的状态修改语句列表之外,以下被认为是读取状态:
  1. 读取状态变量。
  2. 访问 address(this).balance 或者 <address>.balance。
  3. 访问 block,tx, msg 中任意成员 (除 msg.sig 和 msg.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()` 在 [发生错误](https://learnblockchain.cn/docs/solidity/control-structures.html#assert-and-require) 时去还原潜在状态更改。

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

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

receive 接收以太函数

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

不需要 function 关键字,也没有参数和返回值并且必须是 external 可见性和 payable 修饰. 它可以是 virtual 的,可以被重载也可以有 <ruby style="box-sizing: border-box;">修改器<rt style="box-sizing: border-box;">modifier</rt> 。</ruby>

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

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

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

  • 写入存储
  • 创建合约
  • 调用消耗大量 gas 的外部函数
  • 发送以太币
  1. 一个没有定义 fallback 函数或  receive 函数的合约,直接接收以太币(没有函数调用,即使用 send 或 transfer)会抛出一个异常, 并返还以太币(在 Solidity v0.4.0 之前行为会有所不同)。 所以如果你想让你的合约接收以太币,必须实现receive函数(使用 payable fallback 函数不再推荐,因为它会让借口混淆)。
  2. 一个没有receive函数的合约,可以作为 coinbase 交易 (又名 矿工区块回报 )的接收者或者作为 selfdestruct 的目标来接收以太币。
  3. 一个合约不能对这种以太币转移做出反应,因此也不能拒绝它们。这是 EVM 在设计时就决定好的,而且 Solidity 无法绕过这个问题。
  4. 这也意味着 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 传递给它,回退函数就可以执行复杂的操作。

  1. payable 的fallback函数也可以在纯以太转账的时候执行, 如果没有 receive 以太函数 推荐总是定义一个receive函数,而不是定义一个payable 的fallback函数,
// 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;
    }

}

相关文章

网友评论

      本文标题:[Solidity] view pure receive 函数

      本文链接:https://www.haomeiwen.com/subject/bmipwrtx.html