本节是《Solidity by Example》的中文翻译与深入讲解,专为零基础或刚接触区块链开发的小白朋友打造。我们将通过“示例 + 解说 + 提示”的方式,带你逐步理解每一段 Solidity 代码的实际用途与背后的逻辑。
Solidity 是以太坊等智能合约平台使用的主要编程语言,就像写网页要用 HTML 和 JavaScript,写智能合约就需要会 Solidity。
如果你从没写过区块链代码也没关系,只要你了解一点点编程概念,比如“变量”“函数”“条件判断”,我们就能从最简单的例子开始,一步步建立你的 Solidity 编程思维。
Transient Storage
瞬态存储
瞬态存储中的数据在交易结束后会被清除。
什么是瞬态存储(Transient Storage)?
瞬态存储(Transient Storage)是 Solidity 在以太坊 Cancun 升级(EIP-1153)中引入的一种新数据存储位置,介于storage(永久存储)和memory(函数级临时存储)之间。
- 特点:
- 数据存储在交易(transaction)期间,交易结束后自动清除。
- 使用
tstore
(存储)和tload
(读取)操作,通过内联汇编(assembly
)访问。 - 比
storage
操作(如sstore
/sload
)更节省 Gas,因为数据不持久化到区块链。
- 比喻:
- 瞬态存储像会议室的临时白板,会议(交易)结束后自动擦除。
- 相比之下,
storage
像刻在石碑上的记录(永久且昂贵),memory
像便签纸(函数结束即丢弃)。
- 特点:
用途:
- 临时状态:适合需要跨函数调用但仅在交易内有效的状态(如锁状态)。
- 重入保护:在重入保护(reentrancy guard)中替代
storage
,降低 Gas 成本。 - 优化 Gas:瞬态存储的操作(
tstore
/tload
)比storage
操作便宜,适合高频临时数据操作。
// SPDX-License-Identifier: MIT
// 使用 MIT 许可证,允许自由使用、修改和分发代码。
pragma solidity ^0.8.26;
// 指定 Solidity 编译器版本,必须为 0.8.26 或更高(但低于 0.9.0)。
// Make sure EVM version and VM set to Cancun
// 确保 EVM 版本和虚拟机设置为 Cancun(支持 EIP-1153 的瞬态存储)。
// Storage - data is stored on the blockchain
// Memory - data is cleared out after a function call
// Transient storage - data is cleared out after a transaction
// 存储(Storage) - 数据存储在区块链上,永久存在。
// 内存(Memory) - 数据在函数调用后清除。
// 瞬态存储(Transient Storage) - 数据在交易结束后清除。
interface ITest {
function val() external view returns (uint256);
function test() external;
}
// 定义一个接口 ITest,包含两个函数:
// - val():视图函数,返回 uint256 类型的值(读取状态,免费)。
// - test():外部函数,执行测试逻辑(可能修改状态,消耗 Gas)。
// 接口用于跨合约调用,测试存储行为。
contract Callback {
uint256 public val;
// 声明一个公共的状态变量 val,存储在 storage(区块链上),记录测试值。
// public 自动生成 getter 函数(val()),消耗 Gas 修改。
fallback() external {
val = ITest(msg.sender).val();
// 定义一个 fallback 函数,在接收未定义函数调用时触发。
// 调用调用者的 val() 函数(通过 ITest 接口),获取其状态并存储到本合约的 val。
// msg.sender 是触发调用的合约地址(如 TestStorage 或 TestTransientStorage)。
}
function test(address target) external {
ITest(target).test();
// 定义一个公共函数 test,接受目标合约地址 target。
// 通过 ITest 接口调用目标合约的 test() 函数,触发目标合约逻辑。
// 若目标合约调用本合约的 fallback,val 会被更新。
}
}
contract TestStorage {
uint256 public val;
// 声明一个公共的状态变量 val,存储在 storage(区块链上),记录测试值。
function test() public {
val = 123;
// 将 storage 变量 val 设置为 123,修改区块链状态,消耗 Gas(约 20,000+)。
bytes memory b = "";
// 创建一个空的 memory 字节数组 b,临时存储,不消耗 Gas。
msg.sender.call(b);
// 调用 msg.sender(调用者,通常是 Callback 合约)的 fallback 函数,传递空字节数组 b。
// 触发 Callback 的 fallback,Callback 会读取本合约的 val(123)。
}
}
contract TestTransientStorage {
bytes32 constant SLOT = 0;
// 定义一个常量 SLOT(值为 0),表示瞬态存储的槽位。
// 瞬态存储使用固定槽位(bytes32 类型)来存储数据。
function test() public {
assembly {
tstore(SLOT, 321)
// 使用内联汇编的 tstore 操作,将值 321 存储到瞬态存储的 SLOT(槽位 0)。
// 瞬态存储仅在交易期间有效,交易结束后清除。
// tstore 比 sstore(storage 操作)更节省 Gas。
}
bytes memory b = "";
// 创建一个空的 memory 字节数组 b,临时存储,不消耗 Gas。
msg.sender.call(b);
// 调用 msg.sender(调用者,通常是 Callback 合约)的 fallback 函数,传递空字节数组 b。
// 触发 Callback 的 fallback,Callback 会读取本合约的 val(瞬态存储中的 321)。
}
function val() public view returns (uint256 v) {
assembly {
v := tload(SLOT)
// 使用内联汇编的 tload 操作,读取瞬态存储 SLOT(槽位 0)的值。
// 返回瞬态存储中的值(交易内为 321,交易结束后为 0)。
}
}
}
contract MaliciousCallback {
uint256 public count = 0;
// 声明一个公共的状态变量 count,存储在 storage,记录重入次数。
fallback() external {
count++;
// 在 fallback 函数中增加 count,表示被调用一次。
ITest(msg.sender).test();
// 尝试重新调用调用者的 test() 函数,模拟重入攻击。
}
function attack(address _target) external {
// 定义一个公共函数 attack,接受目标合约地址 _target。
ITest(_target).test();
// 调用目标合约的 test() 函数,触发重入攻击。
// 若目标合约未防止重入,MaliciousCallback 的 fallback 会反复调用 test()。
}
}
contract ReentrancyGuard {
bool private locked;
// 声明一个私有状态变量 locked,存储在 storage,表示锁状态(true 表示锁定,false 表示未锁定)。
// private 防止外部直接访问。
modifier lock() {
require(!locked);
// 检查 locked 是否为 false,若为 true 则抛出错误,防止重入。
locked = true;
// 设置 locked 为 true,锁定合约。
_;
// 执行函数主体。
locked = false;
// 函数结束后,设置 locked 为 false,释放锁。
}
// 27587 gas
function test() public lock {
// 定义一个公共函数 test,使用 lock 修饰符防止重入。
// 注释表明此函数消耗约 27,587 Gas(因 storage 操作)。
bytes memory b = "";
// 创建一个空的 memory 字节数组 b,临时存储。
msg.sender.call(b);
// 调用 msg.sender 的 fallback 函数,传递空字节数组 b。
// 若调用者是 MaliciousCallback,lock 修饰符会防止重入。
}
}
contract ReentrancyGuardTransient {
bytes32 constant SLOT = 0;
// 定义一个常量 SLOT(值为 0),表示瞬态存储的槽位。
modifier lock() {
assembly {
if tload(SLOT) { revert(0, 0) }
// 检查瞬态存储 SLOT 的值,若非 0(已锁定),抛出错误,防止重入。
tstore(SLOT, 1)
// 设置瞬态存储 SLOT 的值为 1,表示锁定。
}
_;
// 执行函数主体。
assembly {
tstore(SLOT, 0)
// 函数结束后,设置瞬态存储 SLOT 为 0,释放锁(交易结束会自动清除)。
}
}
// 4909 gas
function test() external lock {
// 定义一个外部函数 test,使用 lock 修饰符防止重入。
// 注释表明此函数消耗约 4,909 Gas(因瞬态存储操作比 storage 便宜)。
bytes memory b = "";
// 创建一个空的 memory 字节数组 b,临时存储。
msg.sender.call(b);
// 调用 msg.sender 的 fallback 函数,传递空字节数组 b。
// 若调用者是 MaliciousCallback,lock 修饰符会防止重入。
}
}
代码整体说明
代码包含多个合约,展示瞬态存储(transient storage
)与传统存储(storage
)的对比,以及在重入保护中的应用:
- 接口
ITest
:定义测试函数签名,用于跨合约调用。 - 合约
Callback
:测试普通存储和瞬态存储的差异,通过fallback
捕获调用者的状态。 - 合约
TestStorage
:使用普通存储(storage
)保存数据,展示持久化行为。 - 合约
TestTransientStorage
:使用瞬态存储(tstore
/tload
)保存数据,展示交易后清除行为。 - 合约
MaliciousCallback
:模拟重入攻击,测试重入保护。 - 合约
ReentrancyGuard
:使用storage
实现重入保护,展示高 Gas 成本。 - 合约
ReentrancyGuardTransient
:使用瞬态存储实现重入保护,展示低 Gas 成本。
瞬态存储的本质
- 瞬态存储(
transient storage
)是一种在交易期间存储数据的机制,交易结束后数据自动清除。 - 比喻:
- 瞬态存储像会议室的临时白板,会议(交易)结束后自动擦除,记录成本低。
Storage
像刻在石碑上的记录,永久但昂贵(高 Gas)。Memory
像便签纸,函数结束即丢弃,免费但无法跨函数调用。
- 核心特点:
- 生命周期:仅在交易内有效,结束后清除(无需手动清理)。
- 操作方式:通过内联汇编的
tstore
(存储)和tload
(读取)访问,使用槽位(bytes32
)存储数据。 - 低 Gas 成本:
tstore
和tload
比sstore
和sload
(storage
操作)便宜,适合临时状态。
- 优势:
- 节省 Gas:瞬态存储操作成本低(如
ReentrancyGuardTransient
的 4,909 Gas 对比ReentrancyGuard
的 27,587 Gas)。 - 自动清理:交易结束后数据自动清除,无需手动重置。
- 跨函数调用:数据可在交易内的多次调用中共享(不像
memory
局限于单函数)。
- 节省 Gas:瞬态存储操作成本低(如
代码功能
- 接口
ITest
:- 定义
val()
和test()
函数,用于跨合约调用,测试存储行为。
- 定义
- 合约
Callback
:- 接收目标合约的
val()
返回值,存储到storage
变量val
,展示持久化存储。
- 接收目标合约的
- 合约
TestStorage
:- 使用
storage
变量val
存储 123,调用Callback
的fallback
,持久保存数据。
- 使用
- 合约
TestTransientStorage
:- 使用瞬态存储(
tstore
/tload
)存储 321,调用Callback
的fallback
,数据在交易结束后清除。
- 使用瞬态存储(
- 合约
MaliciousCallback
:- 模拟重入攻击,通过
fallback
反复调用目标合约的test()
。
- 模拟重入攻击,通过
- 合约
ReentrancyGuard
:- 使用
storage
变量locked
实现重入保护,消耗高 Gas(27,587)。
- 使用
- 合约
ReentrancyGuardTransient
:- 使用瞬态存储(槽位 0)实现重入保护,消耗低 Gas(4,909)。
瞬态存储的注意事项
- 生命周期:
- 瞬态存储仅在交易内有效,交易结束后自动清零。
- 不适合持久数据存储(如用户余额)。
- 操作方式:
- 仅通过内联汇编(
tstore
/tload
)访问,需熟悉汇编语法。 - 槽位(
bytes32
)需明确定义,避免冲突。
- 仅通过内联汇编(
- Gas 成本:
tstore
和tload
比sstore
和sload
便宜,适合高频临时操作。- 仍需注意交易内多次操作的累积成本。
- 兼容性:
- 瞬态存储依赖 Cancun 升级(EIP-1153),需确保 EVM 支持。
- 测试时使用 Hardhat 或 Remix 的 Cancun 环境。
- 安全性:
- 检查槽位状态(
tload
),防止逻辑错误。 - 避免在关键逻辑中依赖瞬态存储的持久性(会丢失)。
- 检查槽位状态(