View 视图函数
可以将函数声明为 view 类型,这种情况下要保证不修改状态。
- 如果编译器的 EVM 目标是拜占庭硬分叉( 译者注:Byzantium 分叉发生在2017年10月,这次分叉进加入了4个操作符: REVERT 、RETURNDATASIZE、RETURNDATACOPY 、STATICCALL) 或更新的 (默认), 则操作码 STATICCALL 将用于视图函数, 这些函数强制在 EVM 执行过程中保持不修改状态。 对于库视图函数, 使用 DELLEGATECALL, 因为没有组合的 DELEGATECALL 和 STATICALL。这意味着库视图函数不会在运行时检查进而阻止状态修改。 这不会对安全性产生负面影响, 因为库代码通常在编译时知道, 并且静态检查器会执行编译时检查
下面的语句被认为是修改状态:
- 修改状态变量。
- 产生事件。
- 创建其它合约。
- 使用
selfdestruct
。 - 通过调用发送以太币。
- 调用任何没有标记为
view
或者pure
的函数。 - 使用低级调用。
- 使用包含特定操作码的内联汇编。
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 操作。
除了上面解释的状态修改语句列表之外,以下被认为是读取状态:
- 读取状态变量。
- 访问 address(this).balance 或者 <address>.balance。
- 访问 block,tx, msg 中任意成员 (除 msg.sig 和 msg.data 之外)。
- 调用任何未标记为 pure 的函数。
- 使用包含某些操作码的内联汇编。
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 可以使用(如,当使用 send
或 transfer
时), 除了基础的日志输出之外,进行其他操作的余地很小。下面的操作消耗会操作 2300 gas :
- 写入存储
- 创建合约
- 调用消耗大量 gas 的外部函数
- 发送以太币
- 一个没有定义 fallback 函数或 receive 函数的合约,直接接收以太币(没有函数调用,即使用 send 或 transfer)会抛出一个异常, 并返还以太币(在 Solidity v0.4.0 之前行为会有所不同)。 所以如果你想让你的合约接收以太币,必须实现receive函数(使用 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函数,
// 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;
}
}