Solidity 语言有两类和状态读写有关的函数类型,一类是 view 函数(也称为视图函数),另一类是 pure 函数(也称为纯函数)。他们的区别是 view 函数不修改状态,pure 函数即不修改状态也不读取状态。
view 函数
可以将函数声明为 view
函数类型,这种情况下函数保证不修改状态。
如果编译器的 EVM 目标是拜占庭或更新的(默认),则在调用
view
函数时使用操作码STATICCALL
,这将强制状态在执行EVM时保持不变。对于库view
函数使用了DELEGATECALL
,因为没有组合DELEGATECALL
和STATICCALL
。这意味着库view
函数没有防止状态修改的运行时检查。这应该不会对安全性产生负面影响,因为库代码通常在编译时已知,而静态检查器执行编译时检查。
下面的语句被认为是在修改状态:
- 修改状态变量;
- 触发事件;
- 创建其他合约;
- 使用
selfdestruct
; - 通过调用发送以太币;
- 调用任何未标记
view
或pure
的函数; - 使用低级调用;
- 使用包含某些操作码的内联汇编。
// 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 版本之前,编译器没有为
view
函数使用STATICCALL
操作码。这样可以通过使用无效的显式类型转换来修改view
函数中的状态。通过对view
函数使用STATICCALL
,在EVM级别上可以防止对状态的修改。
pure 函数
在保证不读取或修改状态的情况下,函数可以被声明为 pure
函数。特别是,在编译时只给出函数输入和msg.data
,但又不知道当前区块链状态的情况下,建议使用 pure
函数。这意味着对 immutable
变量的读取可以是非纯操作。
如果编译器的 EVM 目标是拜占庭或更新的(默认),则使用操作码
STATICCALL
,这不能保证状态没有被读取,但至少它没有被修改。
除了上面解释的状态修改语句列表,下面的语句被认为是读取状态的:
- 从状态变量中读取;
- 访问
address(this).balance
或address.balance
; - 访问
block
,tx
,msg
的任何成员(除了msg.sig
和msg.data
); - 调用任何未标记为
pure
的函数; - 使用包含某些操作码的内联汇编。
// 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()
函数在发生错误时恢复潜在的状态变化。
恢复状态变化不被认为是一种“状态修改”,因为只有在以前没有 view
或 pure
限制的代码中对状态所做的更改才会被恢复,并且该代码可以选择捕获 revert
而不传递它。
这种行为也符合 STATICCALL
操作码。
在 EVM 级别不可能阻止函数读取状态,只能阻止它们写入状态(即只有
view
可以在 EVM 级别强制执行,而pure
不能)。
在 0.5.0 版本之前,编译器没有为
pure
函数使用STATICCALL
操作码。这允许通过使用无效的显式类型转换在pure
函数中修改状态。通过对pure
函数使用STATICCALL
,在 EVM 级别上可以防止对状态的修改。
在 0.4.17 版本之前,编译器没有强制
pure
函数不读取状态。这是一个编译时类型检查,可以规避做无效的合约类型之间的显式转换,因为编译器可以验证合约的类型不做状态改变操作,但不能在运行时检查调用合约的实际类型。