Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions 10 If Else条件语句.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# If Else条件语句

Solidity 支持条件语句 `if`、`else if` 和 `else`。

```solidity
// SPDX-License-Identifier: MIT // SPDX 许可证标识符:MIT 许可证
pragma solidity ^0.8.26; // 版本指令,要求使用 Solidity 0.8.26 及以上版本

contract IfElse { // 定义名为 IfElse 的智能合约
// foo 函数:演示 if / else if / else 多分支条件语句的用法
function foo(uint256 x) public pure returns (uint256) { // 接收一个 uint256 类型参数 x,返回一个 uint256 类型值
if (x < 10) { // 如果 x 小于 10
return 0; // 返回 0
} else if (x < 20) { // 否则如果 x 小于 20(即 10 <= x < 20)
return 1; // 返回 1
} else { // 否则(即 x >= 20)
return 2; // 返回 2
}
}

// ternary 函数:演示三元运算符(条件表达式)的用法
function ternary(uint256 _x) public pure returns (uint256) { // 接收一个 uint256 类型参数 _x,返回一个 uint256 类型值
// 以下是使用 if / else 语句的等价写法(已注释):
// if (_x < 10) {
// return 1;
// }
// return 2;

// 三元运算符是 if / else 条件语句的简写形式
// "?" 操作符被称为三元运算符
// 语法格式:条件 ? 为真时的值 : 为假时的值
return _x < 10 ? 1 : 2; // 如果 _x < 10 成立(为真),返回 1;否则返回 2
}
}
```
113 changes: 113 additions & 0 deletions 15 用户自定义值类型.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
# 用户自定义值类型

Solidity 允许通过 `type` 关键字创建用户自定义值类型(User Defined Value Types,简称 UDVT)。UDVT 可以将基础类型(如 `uint64`)包装为具有语义的新类型,从而在编译时提供类型安全检查,防止混淆不同类型的值。

以下示例展示了如何使用 UDVT 来避免函数参数顺序错误。

```solidity
// SPDX-License-Identifier: MIT // SPDX 许可证标识符:MIT 许可证
pragma solidity ^0.8.26; // 版本指令,要求使用 Solidity 0.8.26 及以上版本

// 以下代码参考自 Optimism 项目
// https://github.com/ethereum-optimism/optimism/blob/develop/packages/contracts-bedrock/src/dispute/lib/LibUDT.sol

// 使用 type 关键字创建用户自定义值类型
// Duration 表示时长,底层类型为 uint64
type Duration is uint64;

// Timestamp 表示时间戳,底层类型为 uint64
// 注意:虽然 Duration 和 Timestamp 底层都是 uint64,但它们是不同的类型,不能混用
type Timestamp is uint64;

// Clock 表示时钟值,底层类型为 uint128
// Clock 由 Duration 和 Timestamp 组合而成:高 64 位存 Duration,低 64 位存 Timestamp
type Clock is uint128;

// LibClock 库:使用用户自定义值类型操作 Clock
library LibClock {
// wrap 函数:将 Duration 和 Timestamp 打包为一个 Clock 值
function wrap(Duration _duration, Timestamp _timestamp)
internal
pure
returns (Clock clock_) // 返回一个 Clock 类型的值
{
assembly { // 使用内联汇编进行位操作
// 数据布局:| Duration | Timestamp |
// 位范围:| 0 ... 63 | 64 ... 127 |
// 将 _duration 左移 64 位(0x40),然后与 _timestamp 进行按位或运算
clock_ := or(shl(0x40, _duration), _timestamp)
}
}

// duration 函数:从 Clock 中提取 Duration 部分(高 64 位)
function duration(Clock _clock)
internal
pure
returns (Duration duration_) // 返回提取的 Duration 值
{
assembly {
// 将 _clock 右移 64 位(0x40),得到高 64 位的 Duration
duration_ := shr(0x40, _clock)
}
}

// timestamp 函数:从 Clock 中提取 Timestamp 部分(低 64 位)
function timestamp(Clock _clock)
internal
pure
returns (Timestamp timestamp_) // 返回提取的 Timestamp 值
{
assembly {
// 先左移 192 位(0xC0)清除高位,再右移 192 位恢复,得到低 64 位的 Timestamp
timestamp_ := shr(0xC0, shl(0xC0, _clock))
}
}
}

// LibClockBasic 库:不使用用户自定义值类型的等价实现
// 用于对比展示 UDVT 的优势
library LibClockBasic {
// wrap 函数:使用原生 uint64 类型,无法区分 Duration 和 Timestamp
function wrap(uint64 _duration, uint64 _timestamp)
internal
pure
returns (uint128 clock) // 返回打包后的 uint128 值
{
assembly {
// 将 _duration 左移 64 位后与 _timestamp 合并
clock := or(shl(0x40, _duration), _timestamp)
}
}
}

// Examples 合约:对比展示有无 UDVT 的差异
contract Examples {
// example_no_uvdt 函数:不使用 UDVT 的示例——参数顺序错误不会被编译器发现
function example_no_uvdt() external view { // external view 函数,只读外部调用
// 不使用 UDVT 的情况
uint128 clock; // 声明一个 uint128 变量 clock
uint64 d = 1; // 初始化 duration 为 1
uint64 t = uint64(block.timestamp); // 获取当前区块时间戳并转换为 uint64
clock = LibClockBasic.wrap(d, t); // 正确调用:duration 在前,timestamp 在后
// 错误!参数顺序颠倒,但编译器不会报错
clock = LibClockBasic.wrap(t, d); // timestamp 和 duration 位置反了,编译通过但语义错误
}

// example_uvdt 函数:使用 UDVT 的示例——编译器会检查类型匹配
function example_uvdt() external view { // external view 函数,只读外部调用
// 将基础类型值转换为用户自定义值类型
Duration d = Duration.wrap(1); // 使用 Duration.wrap 将 uint64 包装为 Duration 类型
Timestamp t = Timestamp.wrap(uint64(block.timestamp)); // 使用 Timestamp.wrap 将区块时间戳包装为 Timestamp 类型
// 将用户自定义值类型转换为基础类型
uint64 d_u64 = Duration.unwrap(d); // 使用 Duration.unwrap 解包
uint64 t_u64 = Timestamp.unwrap(t); // 使用 Timestamp.unwrap 解包

// 使用 LibClock 库的示例
Clock clock = Clock.wrap(0); // 初始化 Clock 值为 0
clock = LibClock.wrap(d, t); // 正确调用:Duration 在前,Timestamp 在后
// 错误!参数顺序颠倒——下面的代码将无法通过编译
// 因为 LibClock.wrap 要求第一个参数为 Duration 类型,第二个为 Timestamp 类型
// clock = LibClock.wrap(t, d); // 编译器会报类型不匹配错误
}
}
```
136 changes: 136 additions & 0 deletions 18 瞬态存储.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
# 瞬态存储

瞬态存储(Transient Storage)是 Solidity 在 Cancun 升级中引入的新型数据存储方式。与普通存储(Storage)和内存(Memory)不同,存储在瞬态存储中的数据会在交易结束后自动清除。瞬态存储通过 `tstore` 和 `tload` 操作码实现,特别适用于重入锁等场景,能显著节省 Gas。

注意:使用瞬态存储需要 EVM 版本设置为 Cancun 及以上。

```solidity
// SPDX-License-Identifier: MIT // SPDX 许可证标识符:MIT 许可证
pragma solidity ^0.8.26; // 版本指令,要求使用 Solidity 0.8.26 及以上版本

// 确保 EVM 版本和虚拟机设置为 Cancun

// 三种数据存储位置的对比:
// Storage(存储)- 数据永久存储在区块链上
// Memory(内存)- 函数调用结束后数据被清除
// Transient storage(瞬态存储)- 交易结束后数据被清除

// ITest 接口:定义测试合约需要实现的函数
interface ITest {
function val() external view returns (uint256); // 返回当前值
function test() external; // 执行测试操作
}

// Callback 合约:用于测试 TestStorage 和 TestTransientStorage 的差异
// 展示普通存储与瞬态存储之间的区别
contract Callback {
uint256 public val; // 公共状态变量 val,存储在 storage 中

// fallback 函数:当合约收到调用时自动触发
fallback() external {
// 从调用者(msg.sender)获取 val 值并存储到本合约的 val 中
val = ITest(msg.sender).val();
}

// test 函数:对目标合约发起测试调用
function test(address target) external { // 接收目标合约地址作为参数
ITest(target).test(); // 调用目标合约的 test 函数
}
}

// TestStorage 合约:使用普通存储的测试合约
contract TestStorage {
uint256 public val; // 公共状态变量 val,存储在 storage 中

// test 函数:设置 val 然后进行外部调用
function test() public {
val = 123; // 将 val 设置为 123,此值会持久保存在区块链上
bytes memory b = ""; // 创建空字节数组用于底层调用
msg.sender.call(b); // 向调用者发送空调用,触发其 fallback 函数
}
}

// TestTransientStorage 合约:使用瞬态存储的测试合约
contract TestTransientStorage {
bytes32 constant SLOT = 0; // 定义瞬态存储的槽位常量,值为 0

// test 函数:使用瞬态存储设置值
function test() public {
assembly { // 使用内联汇编操作瞬态存储
tstore(SLOT, 321) // 使用 tstore 将 321 存入槽位 SLOT 的瞬态存储中
}
bytes memory b = ""; // 创建空字节数组用于底层调用
msg.sender.call(b); // 向调用者发送空调用,触发其 fallback 函数
}

// val 函数:从瞬态存储中读取值
function val() public view returns (uint256 v) { // 返回瞬态存储中的值
assembly {
v := tload(SLOT) // 使用 tload 从槽位 SLOT 的瞬态存储中读取值
}
}
}

// MaliciousCallback 合约:模拟恶意合约,用于测试重入攻击防护
contract MaliciousCallback {
uint256 public count = 0; // 计数器,记录重入次数

// fallback 函数:尝试对目标合约进行多次重入攻击
fallback() external {
ITest(msg.sender).test(); // 每次收到调用时,再次调用目标合约的 test 函数
}

// attack 函数:发起重入攻击
function attack(address _target) external { // 接收目标合约地址作为参数
// 第一次调用目标合约的 test()
ITest(_target).test(); // 启动攻击链
}
}

// ReentrancyGuard 合约:使用普通存储实现重入锁
contract ReentrancyGuard {
bool private locked; // 私有布尔变量,用作重入锁标志

// lock 修饰符:防止重入攻击
modifier lock() {
require(!locked); // 要求 locked 为 false,即当前未锁定
locked = true; // 设置锁标志为 true,进入锁定状态
_; // 执行被修饰的函数体
locked = false; // 函数执行完毕后释放锁
}

// 消耗约 27587 gas
// test 函数:使用 lock 修饰符保护,防止重入
function test() public lock { // 应用 lock 修饰符
// 忽略调用错误
bytes memory b = ""; // 创建空字节数组
msg.sender.call(b); // 向调用者发送空调用
}
}

// ReentrancyGuardTransient 合约:使用瞬态存储实现重入锁(更省 Gas)
contract ReentrancyGuardTransient {
bytes32 constant SLOT = 0; // 定义瞬态存储的槽位常量

// lock 修饰符:使用瞬态存储实现重入保护
modifier lock() {
assembly {
// 如果瞬态存储中已有值,表示已锁定,则回滚交易
if tload(SLOT) { revert(0, 0) } // 检查锁标志,如果非零则回滚
tstore(SLOT, 1) // 设置锁标志为 1,进入锁定状态
}
_; // 执行被修饰的函数体
assembly {
tstore(SLOT, 0) // 函数执行完毕后,清除锁标志(置零)
}
}

// 仅消耗约 4909 gas(比普通存储方案节省约 82% Gas)
// test 函数:使用瞬态存储 lock 修饰符保护
function test() external lock { // 应用 lock 修饰符
// 忽略调用错误
bytes memory b = ""; // 创建空字节数组
msg.sender.call(b); // 向调用者发送空调用
}
}
```
109 changes: 109 additions & 0 deletions 24 事件进阶.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
# 事件进阶

本页涵盖 Solidity 中事件的高级主题和用例,建立在[事件](../21%20事件.md)基础知识之上。

事件在 Solidity 中是一个强大的工具,可以实现各种高级功能和架构。一些高级用例包括:

- 事件过滤和监控,用于实时更新和分析
- 事件日志分析和解码,用于数据提取和处理
- 事件驱动架构,用于去中心化应用(dApps)
- 事件订阅,用于实时通知和更新

```solidity
// SPDX-License-Identifier: MIT // SPDX 许可证标识符:MIT 许可证
pragma solidity ^0.8.14; // 版本指令,要求使用 Solidity 0.8.14 及以上版本

// EventDrivenArchitecture 合约:演示事件驱动架构
// 通过事件来协调和触发流程的不同阶段,例如发起和确认转账
contract EventDrivenArchitecture {
// TransferInitiated 事件:转账发起时触发
// indexed 关键字使参数可被高效过滤和搜索
event TransferInitiated(
address indexed from, // 发起方地址(已索引)
address indexed to, // 接收方地址(已索引)
uint256 value // 转账金额
);
// TransferConfirmed 事件:转账确认时触发
event TransferConfirmed(
address indexed from, // 发起方地址(已索引)
address indexed to, // 接收方地址(已索引)
uint256 value // 转账金额
);

// transferConfirmations 映射:记录转账 ID 是否已被确认
mapping(bytes32 => bool) public transferConfirmations;

// initiateTransfer 函数:发起转账
function initiateTransfer(address to, uint256 value) public { // 接收目标地址和金额
emit TransferInitiated(msg.sender, to, value); // 触发 TransferInitiated 事件
// ... (此处放置发起转账的逻辑)
}

// confirmTransfer 函数:确认转账
function confirmTransfer(bytes32 transferId) public { // 接收转账 ID
require(
!transferConfirmations[transferId], "Transfer already confirmed" // 确保该转账尚未被确认
);
transferConfirmations[transferId] = true; // 标记该转账为已确认
emit TransferConfirmed(msg.sender, address(this), 0); // 触发 TransferConfirmed 事件
// ... (此处放置确认转账的逻辑)
}
}

// IEventSubscriber 接口:定义事件订阅者必须实现的回调函数
interface IEventSubscriber {
// handleTransferEvent 函数:处理接收到的转账事件通知
function handleTransferEvent(address from, address to, uint256 value)
external; // 接收发起方、接收方和金额三个参数
}

// EventSubscription 合约:演示事件订阅和实时更新
contract EventSubscription {
// LogTransfer 事件:转账时触发,订阅者会收到此事件通知
event LogTransfer(address indexed from, address indexed to, uint256 value);

// subscribers 映射:记录地址是否已订阅
mapping(address => bool) public subscribers;
// subscriberList 数组:存储所有订阅者地址,用于遍历通知
address[] public subscriberList;

// subscribe 函数:订阅事件通知
function subscribe() public {
require(!subscribers[msg.sender], "Already subscribed"); // 确保未重复订阅
subscribers[msg.sender] = true; // 标记为已订阅
subscriberList.push(msg.sender); // 将订阅者地址加入列表
}

// unsubscribe 函数:取消订阅
function unsubscribe() public {
require(subscribers[msg.sender], "Not subscribed"); // 确保已订阅
subscribers[msg.sender] = false; // 标记为未订阅
// 从 subscriberList 数组中移除该订阅者(交换删除法)
for (uint256 i = 0; i < subscriberList.length; i++) { // 遍历订阅者列表
if (subscriberList[i] == msg.sender) { // 找到当前调用者的地址
subscriberList[i] = subscriberList[subscriberList.length - 1]; // 用最后一个元素覆盖当前位置
subscriberList.pop(); // 删除最后一个元素
break; // 退出循环
}
}
}

// transfer 函数:执行转账并通知所有订阅者
function transfer(address to, uint256 value) public { // 接收目标地址和金额
emit LogTransfer(msg.sender, to, value); // 触发 LogTransfer 事件,供链下监听
// 遍历所有订阅者,逐个通知
for (uint256 i = 0; i < subscriberList.length; i++) { // 遍历订阅者列表
IEventSubscriber(subscriberList[i]).handleTransferEvent(
msg.sender, to, value // 调用每个订阅者的 handleTransferEvent 回调
);
}
}
}
```

## 最佳实践和建议

- 为合适的事件参数添加 `indexed` 关键字,以便实现高效的过滤和搜索。地址类型通常应该被索引,而金额一般不需要索引。
- 避免重复事件——如果底层库或合约已经触发了相同的事件,则不要再次触发。
- 事件不能在 `view` 或 `pure` 函数中使用,因为它们会通过存储日志来改变区块链的状态。
- 注意触发事件的 Gas 成本,尤其是为参数添加索引时,因为这会影响合约的整体 Gas 消耗。
File renamed without changes.
Loading