Solidity 瞬态存储的使用
瞬态存储(Transient storage)是除了内存、存储、calldata(以及返回数据和代码)之外的另一种数据位置,它是通过 EIP-1153 引入的,并伴随着其相应的操作码 TSTORE
和 TLOAD
。这种新的数据位置表现为类似于存储的键值存储,主要区别在于瞬态存储中的数据不是永久的,而仅限于当前交易,在此之后将被重置为零。由于瞬态存储的内容具有非常有限的生命周期和大小,因此不需要作为状态的一部分永久存储,其相关的 gas 成本比存储要低得多。需要 EVM 版本 cancun
或更新版本才能使用瞬态存储。
瞬态存储变量不能在声明时初始化,即不能在声明时赋值,因为值将在创建交易结束时被清除,使初始化无效。瞬态变量将根据其底层类型进行默认值初始化。constant
和 immutable
变量与瞬态存储冲突,因为它们的值要么内联,要么直接存储在代码中。
瞬态存储变量与存储有完全独立的地址空间,因此瞬态状态变量的顺序不会影响存储状态变量的布局,反之亦然。不过,它们确实需要不同的名称,因为所有状态变量共享相同的命名空间。还需要注意的是,瞬态存储中的值与持久存储中的值以相同的方式打包。有关更多信息,请参见存储布局 。
此外,瞬态变量也可以具有可见性,public
的变量将像往常一样自动生成一个 getter 函数。
请注意,目前,transient
作为数据位置的这种用法仅允许用于值类型状态变量声明。引用类型,如数组、映射和结构体,以及局部或参数变量尚不支持。
瞬态存储的一个预期标准用例是更便宜的重入锁,可以通过操作码轻松实现,如下所示。
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.28;
contract Generosity {
mapping(address => bool) sentGifts;
bool transient locked;
modifier nonReentrant {
require(!locked, "Reentrancy attempt");
locked = true;
_;
// Unlocks the guard, making the pattern composable.
// 函数退出后,可以再次调用它,即使是在同一交易中。
locked = false;
}
function claimGift() nonReentrant public {
require(address(this).balance >= 1 ether);
require(!sentGifts[msg.sender]);
(bool success, ) = msg.sender.call{value: 1 ether}("");
require(success);
// In a reentrant function, doing this last would open up the vulnerability
sentGifts[msg.sender] = true;
}
}
瞬态存储对于拥有它的合约是私有的,与持久存储相同。只有拥有合约的帧(frames)可以访问其瞬态存储,并且当它们访问时,所有帧访问相同的瞬态存储。
瞬态存储是 EVM 状态的一部分,并且与持久存储一样受到相同的可变性强制。因此,任何对其的读取访问都不是 pure
,写入访问也不是 view
。
如果在 STATICCALL
的上下文中调用 TSTORE
操作码,将导致异常而不是执行修改。在 STATICCALL
的上下文中允许使用 TLOAD
。
当在 DELEGATECALL
或 CALLCODE
的上下文中使用瞬态存储时,瞬态存储的拥有合约是发出 DELEGATECALL
或 CALLCODE
指令的合约(调用者),与持久存储一样。当在 CALL
或 STATICCALL
的上下文中使用瞬态存储时,瞬态存储的拥有合约是 CALL
或 STATICCALL
指令的目标合约(被调用者)。
在 DELEGATECALL
的情况下,由于目前不支持对瞬态存储变量的引用,因此无法将其传递到库调用中。在库中,只有使用内联汇编才能访问瞬态存储。
如果一个帧回滚,则在进入帧和返回之间发生的所有对瞬态存储的写入都将回滚,包括在内部调用中发生的那些。外部调用的调用者可以使用 try ... catch
块来防止内部调用的回滚冒泡。
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!