diff --git "a/10 If Else\346\235\241\344\273\266\350\257\255\345\217\245.md" "b/10 If Else\346\235\241\344\273\266\350\257\255\345\217\245.md" new file mode 100644 index 0000000..f677858 --- /dev/null +++ "b/10 If Else\346\235\241\344\273\266\350\257\255\345\217\245.md" @@ -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 + } +} +``` diff --git "a/15 \347\224\250\346\210\267\350\207\252\345\256\232\344\271\211\345\200\274\347\261\273\345\236\213.md" "b/15 \347\224\250\346\210\267\350\207\252\345\256\232\344\271\211\345\200\274\347\261\273\345\236\213.md" new file mode 100644 index 0000000..f54a9f4 --- /dev/null +++ "b/15 \347\224\250\346\210\267\350\207\252\345\256\232\344\271\211\345\200\274\347\261\273\345\236\213.md" @@ -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); // 编译器会报类型不匹配错误 + } +} +``` diff --git "a/18 \347\236\254\346\200\201\345\255\230\345\202\250.md" "b/18 \347\236\254\346\200\201\345\255\230\345\202\250.md" new file mode 100644 index 0000000..a4c41da --- /dev/null +++ "b/18 \347\236\254\346\200\201\345\255\230\345\202\250.md" @@ -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); // 向调用者发送空调用 + } +} +``` diff --git "a/24 \344\272\213\344\273\266\350\277\233\351\230\266.md" "b/24 \344\272\213\344\273\266\350\277\233\351\230\266.md" new file mode 100644 index 0000000..924a4b9 --- /dev/null +++ "b/24 \344\272\213\344\273\266\350\277\233\351\230\266.md" @@ -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 消耗。 diff --git "a/Payable \345\207\275\346\225\260.md" "b/31 Payable \345\207\275\346\225\260.md" similarity index 100% rename from "Payable \345\207\275\346\225\260.md" rename to "31 Payable \345\207\275\346\225\260.md" diff --git "a/32 \345\217\221\351\200\201\344\273\245\345\244\252\345\270\201.md" "b/32 \345\217\221\351\200\201\344\273\245\345\244\252\345\270\201.md" new file mode 100644 index 0000000..c42e68a --- /dev/null +++ "b/32 \345\217\221\351\200\201\344\273\245\345\244\252\345\270\201.md" @@ -0,0 +1,88 @@ +# 发送以太币(transfer, send, call) + +## 如何发送以太币? + +你可以通过以下方式向其他合约发送以太币: + +- `transfer`(固定 2300 gas,失败时抛出异常) +- `send`(固定 2300 gas,返回 bool 值) +- `call`(转发所有 gas 或自定义 gas,返回 bool 值) + +## 如何接收以太币? + +接收以太币的合约必须至少包含以下函数之一: + +- `receive() external payable` +- `fallback() external payable` + +当 `msg.data` 为空时,调用 `receive()`;否则调用 `fallback()`。 + +## 应该使用哪种方法? + +自 2019 年 12 月以后,**`call` 结合重入保护**是推荐的方法。 + +防止重入攻击的方法: + +- 在调用其他合约之前完成所有状态更改 +- 使用重入保护修饰符 + +```solidity +// SPDX-License-Identifier: MIT // SPDX 许可证标识符:MIT 许可证 +pragma solidity ^0.8.26; // 版本指令,要求使用 Solidity 0.8.26 及以上版本 + +// ReceiveEther 合约:演示如何接收以太币 +contract ReceiveEther { + /* + 哪个函数会被调用,fallback() 还是 receive()? + + 发送以太币 + | + msg.data 是否为空? + / \ + 是 否 + / \ + receive() 存在? fallback() + / \ + 是 否 + / \ + receive() fallback() + */ + + // receive 函数:接收以太币,要求 msg.data 必须为空 + receive() external payable {} // external payable 使其可接收以太币 + + // fallback 函数:当 msg.data 不为空时被调用 + fallback() external payable {} // 备用函数,也可接收以太币 + + // getBalance 函数:查询合约余额 + function getBalance() public view returns (uint256) { // 返回合约当前以太币余额 + return address(this).balance; // 返回本合约地址的余额 + } +} + +// SendEther 合约:演示三种发送以太币的方法 +contract SendEther { + // sendViaTransfer 函数:使用 transfer 方法发送以太币 + function sendViaTransfer(address payable _to) public payable { // 接收目标地址,函数本身可接收以太币 + // 此方法已不再推荐用于发送以太币 + // transfer 固定转发 2300 gas,失败时自动 revert + _to.transfer(msg.value); // 将 msg.value 数量的以太币发送到 _to 地址 + } + + // sendViaSend 函数:使用 send 方法发送以太币 + function sendViaSend(address payable _to) public payable { // 接收目标地址 + // send 返回一个布尔值表示成功或失败 + // 此方法已不再推荐用于发送以太币 + bool sent = _to.send(msg.value); // 发送以太币,返回是否成功 + require(sent, "Failed to send Ether"); // 如果发送失败,回滚交易 + } + + // sendViaCall 函数:使用 call 方法发送以太币(当前推荐方式) + function sendViaCall(address payable _to) public payable { // 接收目标地址 + // call 返回一个布尔值表示成功或失败 + // 这是当前推荐的发送以太币方法 + (bool sent, bytes memory data) = _to.call{value: msg.value}(""); // 使用 call 发送以太币,返回成功标志和返回数据 + require(sent, "Failed to send Ether"); // 如果发送失败,回滚交易 + } +} +``` diff --git "a/33 Fallback\345\207\275\346\225\260.md" "b/33 Fallback\345\207\275\346\225\260.md" new file mode 100644 index 0000000..776da49 --- /dev/null +++ "b/33 Fallback\345\207\275\346\225\260.md" @@ -0,0 +1,126 @@ +# Fallback函数 + +`fallback` 是一个特殊函数,在以下情况下执行: + +- 调用了不存在的函数,或 +- 直接向合约发送以太币,但 `receive()` 不存在或 `msg.data` 不为空 + +为了更好地理解 Solidity 在什么条件下调用 `receive` 或 `fallback` 函数,请参考以下流程图: + +``` + 发送以太币 + | + msg.data 是否为空? + / \ + 是 否 + | | + receive() 存在? fallback() + / \ + 是 否 + | | + receive() fallback() +``` + +当通过 `transfer` 或 `send` 调用时,`fallback` 只能使用 2300 gas。 + +```solidity +// SPDX-License-Identifier: MIT // SPDX 许可证标识符:MIT 许可证 +pragma solidity ^0.8.26; // 版本指令,要求使用 Solidity 0.8.26 及以上版本 + +// Fallback 合约:演示 fallback 和 receive 函数的行为 +contract Fallback { + // Log 事件:记录被调用的函数名和剩余 gas 量 + event Log(string func, uint256 gas); // func 为函数名,gas 为剩余 gas 量 + + // fallback 函数必须声明为 external + // 当调用了不存在的函数,或 msg.data 不为空时触发 + fallback() external payable { + // send / transfer 会将 2300 gas 转发到此 fallback 函数 + // call 会转发所有剩余的 gas + emit Log("fallback", gasleft()); // 记录 "fallback" 和当前剩余 gas 量 + } + + // receive 是 fallback 的变体,当 msg.data 为空时触发 + receive() external payable { + emit Log("receive", gasleft()); // 记录 "receive" 和当前剩余 gas 量 + } + + // getBalance 辅助函数:查询本合约的余额 + function getBalance() public view returns (uint256) { + return address(this).balance; // 返回本合约地址的以太币余额 + } +} + +// SendToFallback 合约:演示向 Fallback 合约发送以太币时 fallback/receive 的行为 +contract SendToFallback { + // transferToFallback 函数:使用 transfer 方法发送以太币 + function transferToFallback(address payable _to) public payable { // 接收目标地址 + _to.transfer(msg.value); // transfer 仅转发 2300 gas + } + + // callFallback 函数:使用 call 方法发送以太币 + function callFallback(address payable _to) public payable { // 接收目标地址 + (bool sent,) = _to.call{value: msg.value}(""); // call 转发所有剩余 gas + require(sent, "Failed to send Ether"); // 如果发送失败则回滚 + } +} +``` + +`fallback` 可以选择性地接收 `bytes` 类型的输入和输出: + +```solidity +// SPDX-License-Identifier: MIT // SPDX 许可证标识符:MIT 许可证 +pragma solidity ^0.8.26; // 版本指令,要求使用 Solidity 0.8.26 及以上版本 + +// FallbackInputOutput 合约:演示带输入输出的 fallback 函数 +// 调用链:TestFallbackInputOutput -> FallbackInputOutput -> Counter +contract FallbackInputOutput { + address immutable target; // 不可变的目标合约地址 + + // 构造函数:设置代理目标地址 + constructor(address _target) { + target = _target; // 初始化目标地址,部署后不可更改 + } + + // fallback 函数:接收 calldata 并转发给目标合约 + fallback(bytes calldata data) external payable returns (bytes memory) { // 接收 calldata,返回 bytes + (bool ok, bytes memory res) = target.call{value: msg.value}(data); // 将调用转发到目标合约 + require(ok, "call failed"); // 如果调用失败则回滚 + return res; // 返回目标合约的返回值 + } +} + +// Counter 合约:一个简单的计数器合约,作为代理的目标合约 +contract Counter { + uint256 public count; // 公共计数器变量 + + // get 函数:获取当前计数值 + function get() external view returns (uint256) { + return count; // 返回当前计数 + } + + // inc 函数:计数器加一并返回新值 + function inc() external returns (uint256) { + count += 1; // 计数器自增 1 + return count; // 返回更新后的计数值 + } +} + +// TestFallbackInputOutput 合约:测试带输入输出 fallback 的调用 +contract TestFallbackInputOutput { + event Log(bytes res); // Log 事件:记录返回数据 + + // test 函数:向 fallback 合约发送调用 + function test(address _fallback, bytes calldata data) external { // 接收 fallback 合约地址和调用数据 + (bool ok, bytes memory res) = _fallback.call(data); // 向 fallback 合约发送调用 + require(ok, "call failed"); // 如果调用失败则回滚 + emit Log(res); // 记录返回数据 + } + + // getTestData 函数:获取用于测试的编码数据 + function getTestData() external pure returns (bytes memory, bytes memory) { + return + (abi.encodeCall(Counter.get, ()), abi.encodeCall(Counter.inc, ())); // 返回 Counter.get 和 Counter.inc 的 ABI 编码调用数据 + } +} +``` diff --git "a/34 Call\350\260\203\347\224\250.md" "b/34 Call\350\260\203\347\224\250.md" new file mode 100644 index 0000000..6cfb8fe --- /dev/null +++ "b/34 Call\350\260\203\347\224\250.md" @@ -0,0 +1,67 @@ +# Call调用 + +`call` 是一种与其他合约交互的低级函数。 + +当仅通过调用 `fallback` 函数来发送以太币时,这是推荐的方法。但不推荐用它来调用已存在的函数。 + +## 不推荐使用低级 call 的几个原因 + +- 错误不会向上冒泡(revert 不会传播) +- 绕过了类型检查 +- 跳过了函数存在性检查 + +```solidity +// SPDX-License-Identifier: MIT // SPDX 许可证标识符:MIT 许可证 +pragma solidity ^0.8.26; // 版本指令,要求使用 Solidity 0.8.26 及以上版本 + +// Receiver 合约:被调用的目标合约 +contract Receiver { + // Received 事件:记录调用者和调用信息 + event Received(address caller, uint256 amount, string message); + + // fallback 函数:当调用了不存在的函数时触发 + fallback() external payable { + emit Received(msg.sender, msg.value, "Fallback was called"); // 记录 fallback 被调用 + } + + // foo 函数:一个测试函数,接收字符串和数字参数 + function foo(string memory _message, uint256 _x) + public + payable + returns (uint256) // 返回 _x + 1 + { + emit Received(msg.sender, msg.value, _message); // 记录调用信息 + + return _x + 1; // 返回参数 _x 加 1 + } +} + +// Caller 合约:使用 call 调用 Receiver 合约 +contract Caller { + // Response 事件:记录调用是否成功及返回数据 + event Response(bool success, bytes data); + + // 假设 Caller 合约没有 Receiver 合约的源码, + // 但我们知道 Receiver 的地址和要调用的函数签名。 + // testCallFoo 函数:使用 call 调用 Receiver 的 foo 函数 + function testCallFoo(address payable _addr) public payable { // 接收目标地址 + // 可以通过 call 发送以太币并指定自定义 gas 量 + // abi.encodeWithSignature 将函数签名和参数编码为 calldata + (bool success, bytes memory data) = _addr.call{ + value: msg.value, // 发送的以太币数量 + gas: 5000 // 指定的 gas 上限 + }(abi.encodeWithSignature("foo(string,uint256)", "call foo", 123)); // 编码函数签名和参数 + + emit Response(success, data); // 记录调用结果 + } + + // testCallDoesNotExist 函数:调用不存在的函数,将触发 fallback + function testCallDoesNotExist(address payable _addr) public payable { // 接收目标地址 + (bool success, bytes memory data) = _addr.call{value: msg.value}( + abi.encodeWithSignature("doesNotExist()") // 这个函数在 Receiver 中不存在 + ); + + emit Response(success, data); // 记录调用结果(success 仍为 true,因为 fallback 执行成功) + } +} +``` diff --git "a/35 Delegatecall\345\247\224\346\211\230\350\260\203\347\224\250.md" "b/35 Delegatecall\345\247\224\346\211\230\350\260\203\347\224\250.md" new file mode 100644 index 0000000..aaf62b2 --- /dev/null +++ "b/35 Delegatecall\345\247\224\346\211\230\350\260\203\347\224\250.md" @@ -0,0 +1,67 @@ +# Delegatecall委托调用 + +`delegatecall` 是一种类似于 `call` 的低级函数。 + +当合约 `A` 对合约 `B` 执行 `delegatecall` 时,`B` 的代码会在 **`A` 的存储上下文**中执行,同时保留 `A` 的 `msg.sender` 和 `msg.value`。 + +这意味着 `delegatecall` 执行的是目标合约的**逻辑**,但读写的是调用合约的**存储**。 + +```solidity +// SPDX-License-Identifier: MIT // SPDX 许可证标识符:MIT 许可证 +pragma solidity ^0.8.26; // 版本指令,要求使用 Solidity 0.8.26 及以上版本 + +// 注意:请先部署合约 B +// B 合约:被委托调用的合约(逻辑合约) +contract B { + // 注意:存储布局必须与合约 A 相同 + uint256 public num; // 插槽 0:数值变量 + address public sender; // 插槽 1:发送者地址 + uint256 public value; // 插槽 2:发送的以太币数量 + + // setVars 函数:设置三个状态变量的值 + function setVars(uint256 _num) public payable { // 接收数值参数,可附带以太币 + num = _num; // 设置 num + sender = msg.sender; // 设置 sender 为当前调用者 + value = msg.value; // 设置 value 为附带的以太币数量 + } +} + +// A 合约:调用 delegatecall 的合约(代理合约) +contract A { + // 存储布局必须与合约 B 完全一致 + uint256 public num; // 插槽 0:数值变量(与 B 对应) + address public sender; // 插槽 1:发送者地址(与 B 对应) + uint256 public value; // 插槽 2:发送的以太币数量(与 B 对应) + + // DelegateResponse 事件:记录 delegatecall 的结果 + event DelegateResponse(bool success, bytes data); + // CallResponse 事件:记录普通 call 的结果 + event CallResponse(bool success, bytes data); + + // setVarsDelegateCall 函数:使用 delegatecall 调用合约 B 的 setVars + // delegatecall 会修改 A 的存储,B 的存储不受影响 + function setVarsDelegateCall(address _contract, uint256 _num) + public + payable + { + // A 的存储被修改;B 的存储保持不变 + // delegatecall 使用 A 的存储上下文执行 B 的代码 + (bool success, bytes memory data) = _contract.delegatecall( + abi.encodeWithSignature("setVars(uint256)", _num) // 编码函数签名和参数 + ); + + emit DelegateResponse(success, data); // 记录 delegatecall 结果 + } + + // setVarsCall 函数:使用普通 call 调用合约 B 的 setVars(用于对比) + // 普通 call 会修改 B 的存储,A 的存储不受影响 + function setVarsCall(address _contract, uint256 _num) public payable { + // B 的存储被修改;A 的存储保持不变 + (bool success, bytes memory data) = _contract.call{value: msg.value}( + abi.encodeWithSignature("setVars(uint256)", _num) // 编码函数签名和参数 + ); + + emit CallResponse(success, data); // 记录 call 结果 + } +} +``` diff --git "a/36 \345\207\275\346\225\260\351\200\211\346\213\251\345\231\250.md" "b/36 \345\207\275\346\225\260\351\200\211\346\213\251\345\231\250.md" new file mode 100644 index 0000000..fe1f97b --- /dev/null +++ "b/36 \345\207\275\346\225\260\351\200\211\346\213\251\345\231\250.md" @@ -0,0 +1,44 @@ +# 函数选择器 + +当调用一个函数时,`calldata` 的前 4 个字节指定了要调用哪个函数。这 4 个字节被称为**函数选择器**(Function Selector)。 + +例如,下面的代码使用 `call` 在地址 `addr` 的合约上执行 `transfer` 函数: + +```solidity +addr.call(abi.encodeWithSignature("transfer(address,uint256)", 0xSomeAddress, 123)) +``` + +`abi.encodeWithSignature(....)` 返回数据的前 4 个字节就是函数选择器。 + +如果你在代码中预先计算并内联函数选择器,也许可以节省少量的 gas。下面展示函数选择器是如何计算的。 + +```solidity +// SPDX-License-Identifier: MIT // SPDX 许可证标识符:MIT 许可证 +pragma solidity ^0.8.26; // 版本指令,要求使用 Solidity 0.8.26 及以上版本 + +contract FunctionSelector { // 定义名为 FunctionSelector 的智能合约 + /* + 函数选择器的计算方式: + 对函数签名进行 keccak256 哈希,取前 4 个字节。 + + 例如: + "transfer(address,uint256)" + 其 keccak256 哈希的前 4 字节(十六进制):0xa9059cbb + + "transferFrom(address,address,uint256)" + 其 keccak256 哈希的前 4 字节(十六进制):0x23b872dd + */ + + // getSelector 函数:根据函数签名计算函数选择器 + function getSelector(string calldata _func) // 接收函数签名字符串 + external + pure + returns (bytes4) // 返回 4 字节的函数选择器 + { + // 1. bytes(_func) 将签名字符串转换为 bytes 类型 + // 2. keccak256() 对 bytes 进行 keccak256 哈希 + // 3. bytes4() 取哈希值的前 4 个字节 + return bytes4(keccak256(bytes(_func))); // 计算并返回函数选择器 + } +} +``` diff --git "a/37 \350\260\203\347\224\250\345\205\266\344\273\226\345\220\210\347\272\246.md" "b/37 \350\260\203\347\224\250\345\205\266\344\273\226\345\220\210\347\272\246.md" new file mode 100644 index 0000000..8de555d --- /dev/null +++ "b/37 \350\260\203\347\224\250\345\205\266\344\273\226\345\220\210\347\272\246.md" @@ -0,0 +1,57 @@ +# 调用其他合约 + +合约可以通过两种方式调用其他合约。 + +最简单的方式是直接调用,如 `A.foo(x, y, z)`。 + +另一种方式是使用低级 `call` 函数。这种方法不推荐使用。 + +```solidity +// SPDX-License-Identifier: MIT // SPDX 许可证标识符:MIT 许可证 +pragma solidity ^0.8.26; // 版本指令,要求使用 Solidity 0.8.26 及以上版本 + +// Callee 合约:被调用的目标合约 +contract Callee { + uint256 public x; // 公共状态变量 x + uint256 public value; // 公共状态变量 value,记录收到的以太币 + + // setX 函数:设置 x 的值 + function setX(uint256 _x) public returns (uint256) { // 接收一个 uint256 参数 + x = _x; // 将参数 _x 赋值给状态变量 x + return x; // 返回更新后的 x 值 + } + + // setXandSendEther 函数:设置 x 并同时接收以太币 + function setXandSendEther(uint256 _x) + public + payable // payable 关键字使函数可以接收以太币 + returns (uint256, uint256) // 返回 x 和收到的以太币数量 + { + x = _x; // 设置 x 的值 + value = msg.value; // 记录本次调用附带的以太币数量 + + return (x, value); // 返回 x 和 value + } +} + +// Caller 合约:调用 Callee 合约的调用方 +contract Caller { + // setX 函数:通过合约引用直接调用 Callee.setX + function setX(Callee _callee, uint256 _x) public { // 接收 Callee 实例和参数 + uint256 x = _callee.setX(_x); // 直接调用 Callee 合约的 setX 方法 + } + + // setXFromAddress 函数:通过地址调用(需要先转换为合约类型) + function setXFromAddress(address _addr, uint256 _x) public { // 接收合约地址和参数 + Callee callee = Callee(_addr); // 将地址强制转换为 Callee 合约类型 + callee.setX(_x); // 调用 setX 方法 + } + + // setXandSendEther 函数:调用时附带发送以太币 + function setXandSendEther(Callee _callee, uint256 _x) public payable { // payable 使其可发送以太币 + // 使用 {value: msg.value} 在调用时附加以太币 + (uint256 x, uint256 value) = + _callee.setXandSendEther{value: msg.value}(_x); // 发送以太币并调用 setXandSendEther + } +} +``` diff --git "a/38 \345\220\210\347\272\246\345\210\233\345\273\272\345\220\210\347\272\246.md" "b/38 \345\220\210\347\272\246\345\210\233\345\273\272\345\220\210\347\272\246.md" new file mode 100644 index 0000000..fe016a5 --- /dev/null +++ "b/38 \345\220\210\347\272\246\345\210\233\345\273\272\345\220\210\347\272\246.md" @@ -0,0 +1,81 @@ +# 合约创建合约 + +合约可以通过 `new` 关键字创建其他合约。自 Solidity 0.8.0 起,`new` 关键字支持通过指定 `salt` 选项来使用 `create2` 功能。 + +```solidity +// SPDX-License-Identifier: MIT // SPDX 许可证标识符:MIT 许可证 +pragma solidity ^0.8.26; // 版本指令,要求使用 Solidity 0.8.26 及以上版本 + +// Car 合约:将被 CarFactory 创建的汽车合约 +contract Car { + address public owner; // 公共变量:汽车所有者地址 + string public model; // 公共变量:汽车型号 + address public carAddr; // 公共变量:本合约自身地址 + + // 构造函数:在合约创建时初始化所有者、型号和自身地址 + constructor(address _owner, string memory _model) payable { // payable 使其可在部署时接收以太币 + owner = _owner; // 设置所有者为传入的 _owner 地址 + model = _model; // 设置型号为传入的 _model 字符串 + carAddr = address(this); // 记录本合约的地址 + } +} + +// CarFactory 合约:批量创建 Car 合约的工厂合约 +contract CarFactory { + Car[] public cars; // 存储所有创建的 Car 合约实例的数组 + + // create 函数:使用 new 关键字创建一个新的 Car 合约 + function create(address _owner, string memory _model) public { // 接收所有者和型号 + Car car = new Car(_owner, _model); // 使用 new 关键字部署新的 Car 合约 + cars.push(car); // 将新创建的 Car 实例存入数组 + } + + // createAndSendEther 函数:创建合约的同时发送以太币 + function createAndSendEther(address _owner, string memory _model) + public + payable // payable 使工厂函数可接收以太币 + { + // {value: msg.value} 将收到的以太币转发给新创建的 Car 合约 + Car car = (new Car){value: msg.value}(_owner, _model); // 创建 Car 并发送以太币 + cars.push(car); // 将新实例存入数组 + } + + // create2 函数:使用 CREATE2 操作码创建合约 + // CREATE2 可以根据 salt 预计算合约地址 + function create2(address _owner, string memory _model, bytes32 _salt) + public // 接收 salt 参数用于确定合约地址 + { + // {salt: _salt} 使用 CREATE2 操作码而非 CREATE + Car car = (new Car){salt: _salt}(_owner, _model); // 使用 CREATE2 创建合约 + cars.push(car); // 将新实例存入数组 + } + + // create2AndSendEther 函数:同时使用 CREATE2 和发送以太币 + function create2AndSendEther( + address _owner, + string memory _model, + bytes32 _salt + ) public payable { // 接收所有者和型号,可发送以太币 + // 同时指定 value 和 salt + Car car = (new Car){value: msg.value, salt: _salt}(_owner, _model); // CREATE2 + 附带以太币 + cars.push(car); // 将新实例存入数组 + } + + // getCar 函数:根据索引获取指定 Car 的信息 + function getCar(uint256 _index) + public + view + returns ( + address owner, // 所有者地址 + string memory model, // 型号 + address carAddr, // 合约地址 + uint256 balance // 合约余额 + ) + { + Car car = cars[_index]; // 从数组中取出指定索引的 Car 实例 + + // 返回该 Car 的完整信息 + return (car.owner(), car.model(), car.carAddr(), address(car).balance); + } +} +``` diff --git a/39 Try Catch.md b/39 Try Catch.md new file mode 100644 index 0000000..2851f3b --- /dev/null +++ b/39 Try Catch.md @@ -0,0 +1,72 @@ +# Try Catch + +`try / catch` 只能捕获来自**外部函数调用**和**合约创建**的错误。 + +```solidity +// SPDX-License-Identifier: MIT // SPDX 许可证标识符:MIT 许可证 +pragma solidity ^0.8.26; // 版本指令,要求使用 Solidity 0.8.26 及以上版本 + +// Foo 合约:用于 try / catch 示例的外部合约 +contract Foo { + address public owner; // 公共变量:合约所有者地址 + + // 构造函数:创建合约时设置所有者 + constructor(address _owner) { // 接收一个地址作为所有者 + require(_owner != address(0), "invalid address"); // require 检查:所有者不能是零地址 + // assert 检查:所有者不能是特定地址(0x000...001) + // 使用 assert 触发 Panic 错误,区别于 require 的 Error 错误 + assert(_owner != 0x0000000000000000000000000000000000000001); + owner = _owner; // 设置所有者 + } + + // myFunc 函数:一个测试函数 + function myFunc(uint256 x) public pure returns (string memory) { // 接收一个 uint256 参数 + require(x != 0, "require failed"); // 如果 x 为 0,触发 require 错误 + return "my func was called"; // 成功时返回此字符串 + } +} + +// Bar 合约:演示 try / catch 的用法 +contract Bar { + event Log(string message); // 记录字符串消息的事件 + event LogBytes(bytes data); // 记录原始字节数据的事件 + + Foo public foo; // Foo 合约实例 + + constructor() { // 构造函数 + // 在部署 Bar 合约时,同时创建一个 Foo 合约实例 + // 此 Foo 实例用于 try catch 外部调用示例 + foo = new Foo(msg.sender); // 创建以当前部署者为所有者的 Foo 合约 + } + + // tryCatchExternalCall 函数:演示 try / catch 捕获外部调用错误 + // tryCatchExternalCall(0) => 输出 Log("external call failed"),因为 myFunc 中 require(x != 0) 失败 + // tryCatchExternalCall(1) => 输出 Log("my func was called"),调用成功 + function tryCatchExternalCall(uint256 _i) public { // 接收一个测试参数 + try foo.myFunc(_i) returns (string memory result) { // 尝试调用 foo.myFunc(_i) + // 如果外部调用成功,执行此代码块 + emit Log(result); // 记录返回的字符串 + } catch { // 如果外部调用失败(任何类型的错误) + // 执行此备用代码块 + emit Log("external call failed"); // 记录外部调用失败 + } + } + + // tryCatchNewContract 函数:演示 try / catch 捕获合约创建错误 + // tryCatchNewContract(0x000...000) => Log("invalid address"),Require 失败 + // tryCatchNewContract(0x000...001) => LogBytes(""),Assert 失败(返回空数据) + // tryCatchNewContract(0x000...002) => Log("Foo created"),创建成功 + function tryCatchNewContract(address _owner) public { // 接收所有者地址参数 + try new Foo(_owner) returns (Foo foo) { // 尝试创建新的 Foo 合约 + // 如果创建成功,可以在此使用 foo 变量 + emit Log("Foo created"); // 记录合约创建成功 + } catch Error(string memory reason) { // 捕获 require() 和 revert() 产生的错误 + // 此类错误带有错误消息字符串 + emit Log(reason); // 记录错误消息(如 "invalid address") + } catch (bytes memory reason) { // 捕获 assert() 产生的 Panic 错误等其他低级错误 + // assert 失败时,错误数据通常为空 + emit LogBytes(reason); // 记录原始错误数据 + } + } +} +``` diff --git "a/40 Import\345\257\274\345\205\245.md" "b/40 Import\345\257\274\345\205\245.md" new file mode 100644 index 0000000..882afef --- /dev/null +++ "b/40 Import\345\257\274\345\205\245.md" @@ -0,0 +1,78 @@ +# Import导入 + +在 Solidity 中,你可以导入本地和外部文件。 + +## 本地导入 + +以下是我们的文件夹结构: + +``` +├── Import.sol +└── Foo.sol +``` + +Foo.sol —— 被导入的源文件: + +```solidity +// SPDX-License-Identifier: MIT // SPDX 许可证标识符:MIT 许可证 +pragma solidity ^0.8.26; // 版本指令,要求使用 Solidity 0.8.26 及以上版本 + +// Point 结构体:定义一个二维坐标点 +struct Point { // struct 关键字定义结构体类型 + uint256 x; // x 坐标 + uint256 y; // y 坐标 +} + +// Unauthorized 错误:自定义错误类型,携带调用者地址 +error Unauthorized(address caller); // error 关键字定义自定义错误 + +// add 函数:文件级别的自由函数(不属于任何合约) +function add(uint256 x, uint256 y) pure returns (uint256) { // pure 表示不读取也不修改状态 + return x + y; // 返回两数之和 +} + +// Foo 合约:被导入的合约 +contract Foo { // 定义名为 Foo 的智能合约 + string public name = "Foo"; // 公共字符串变量,默认值为 "Foo" +} +``` + +Import.sol —— 导入 Foo.sol 的文件: + +```solidity +// SPDX-License-Identifier: MIT // SPDX 许可证标识符:MIT 许可证 +pragma solidity ^0.8.26; // 版本指令,要求使用 Solidity 0.8.26 及以上版本 + +// 从当前目录导入 Foo.sol +import "./Foo.sol"; // 导入同一目录下的 Foo.sol 文件 + +// 命名导入:使用 {symbol1 as alias, symbol2} 语法 +// 导入 Unauthorized 错误、add 函数(别名为 func)和 Point 结构体 +import {Unauthorized, add as func, Point} from "./Foo.sol"; // 选择性导入并重命名 + +// Import 合约:使用导入的内容 +contract Import { // 定义名为 Import 的智能合约 + // 初始化 Foo.sol 中定义的合约 + Foo public foo = new Foo(); // 创建 Foo 合约的新实例 + + // getFooName 函数:通过调用 Foo 合约的 name() 来测试导入是否成功 + function getFooName() public view returns (string memory) { // 返回 Foo 合约的 name + return foo.name(); // 调用 foo 实例的 name 函数 + } +} +``` + +## 外部导入 + +你也可以通过简单地复制 URL 从 [GitHub](https://github.com) 导入: + +```solidity +// 基本格式:从 GitHub 仓库导入 +// https://github.com/owner/repo/blob/branch/path/to/Contract.sol +import "https://github.com/owner/repo/blob/branch/path/to/Contract.sol"; + +// 示例:从 OpenZeppelin 的 openzeppelin-contracts 仓库导入 ECDSA.sol +// 仓库地址:https://github.com/OpenZeppelin/openzeppelin-contracts +// 分支:release-v4.5 +import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/release-v4.5/contracts/utils/cryptography/ECDSA.sol"; +``` diff --git "a/41 Library\345\272\223.md" "b/41 Library\345\272\223.md" new file mode 100644 index 0000000..64ce394 --- /dev/null +++ "b/41 Library\345\272\223.md" @@ -0,0 +1,71 @@ +# Library库 + +库(Library)与合约类似,但不能声明任何状态变量,也不能发送以太币。 + +如果库中的所有函数都是 `internal` 的,则库会被嵌入到合约中。否则库必须被部署,并在合约部署前进行链接。 + +```solidity +// SPDX-License-Identifier: MIT // SPDX 许可证标识符:MIT 许可证 +pragma solidity ^0.8.26; // 版本指令,要求使用 Solidity 0.8.26 及以上版本 + +// Math 库:提供数学工具函数 +library Math { // library 关键字定义库,类似合约但不能有状态变量 + // sqrt 函数:使用牛顿迭代法计算平方根 + function sqrt(uint256 y) internal pure returns (uint256 z) { // internal 函数会被内嵌到调用合约中 + if (y > 3) { // 如果 y 大于 3,使用牛顿迭代法 + z = y; // 初始估计值设为 y + uint256 x = y / 2 + 1; // 初始猜测值 + while (x < z) { // 当猜测值小于当前估计值时继续迭代 + z = x; // 更新估计值 + x = (y / x + x) / 2; // 牛顿迭代公式:x = (y/x + x) / 2 + } + } else if (y != 0) { // 如果 y 为 1、2 或 3 + z = 1; // 平方根为 1(1²=1, 2²=4中的整数部分, 3²=9) + } + // 否则 z = 0(y 为 0 时返回默认值 0) + } +} + +// TestMath 合约:测试 Math 库的使用 +contract TestMath { // 定义测试合约 + // testSquareRoot 函数:调用 Math 库的 sqrt 函数 + function testSquareRoot(uint256 x) public pure returns (uint256) { + return Math.sqrt(x); // 通过库名直接调用函数 + } +} + +// Array 库:提供数组操作函数 +// 用于删除指定索引处的元素并重新整理数组,使元素之间没有空隙 +library Array { // 定义 Array 库 + // remove 函数:从数组中删除指定索引的元素(交换删除法) + function remove(uint256[] storage arr, uint256 index) public { // storage 引用,直接修改原数组 + // 将最后一个元素移到要删除的位置 + require(arr.length > 0, "Can't remove from empty array"); // 确保数组非空 + arr[index] = arr[arr.length - 1]; // 用最后一个元素覆盖要删除的位置 + arr.pop(); // 删除最后一个元素(因为已被移走) + } +} + +// TestArray 合约:测试 Array 库的使用 +contract TestArray { // 定义测试合约 + using Array for uint256[]; // using for 语法:将 Array 库附加到 uint256[] 类型 + + uint256[] public arr; // 声明一个公共 uint256 数组 + + // testArrayRemove 函数:测试数组删除功能 + function testArrayRemove() public { + // 循环添加 3 个元素 + for (uint256 i = 0; i < 3; i++) { // i = 0, 1, 2 + arr.push(i); // arr = [0, 1, 2] + } + + // 调用 Array 库的 remove 函数删除索引 1 的元素(值为 1) + arr.remove(1); // arr 变为 [0, 2](元素 1 被最后的 2 替换,然后 pop) + + // 验证结果 + assert(arr.length == 2); // 数组长度应为 2 + assert(arr[0] == 0); // 索引 0 应为 0 + assert(arr[1] == 2); // 索引 1 应为 2 + } +} +``` diff --git "a/42 ABI Encode\347\274\226\347\240\201.md" "b/42 ABI Encode\347\274\226\347\240\201.md" new file mode 100644 index 0000000..3b8de32 --- /dev/null +++ "b/42 ABI Encode\347\274\226\347\240\201.md" @@ -0,0 +1,71 @@ +# ABI Encode编码 + +ABI 编码是将函数调用参数编码为以太坊交易所需的 `bytes` 格式。 + +Solidity 提供了三种方式进行 ABI 编码: + +| 方式 | 优点 | 缺点 | +|------|------|------| +| `abi.encodeWithSignature` | 简洁直观 | 不检查拼写错误和类型 | +| `abi.encodeWithSelector` | 使用选择器 | 不检查参数类型 | +| `abi.encodeCall` | 编译时检查拼写和类型 | 需要合约接口 | + +```solidity +// SPDX-License-Identifier: MIT // SPDX 许可证标识符:MIT 许可证 +pragma solidity ^0.8.26; // 版本指令,要求使用 Solidity 0.8.26 及以上版本 + +// IERC20 接口:定义 ERC20 代币的 transfer 函数签名 +interface IERC20 { // interface 关键字定义接口 + function transfer(address, uint256) external; // transfer 函数签名 +} + +// Token 合约:一个简单的代币合约(仅用于示例) +contract Token { // 定义 Token 合约 + function transfer(address, uint256) external {} // transfer 函数实现(空实现,仅用于演示) +} + +// AbiEncode 合约:演示三种 ABI 编码方式 +contract AbiEncode { // 定义 AbiEncode 合约 + // test 函数:执行任意编码调用(低级 call) + function test(address _contract, bytes calldata data) external { // 接收目标地址和编码后的调用数据 + (bool ok,) = _contract.call(data); // 对目标合约执行低级 call + require(ok, "call failed"); // 如果调用失败则回滚 + } + + // encodeWithSignature 函数:使用函数签名字符串编码 + // 注意:签名中的拼写错误不会被编译器检查! + function encodeWithSignature(address to, uint256 amount) // 接收目标地址和金额 + external + pure + returns (bytes memory) // 返回编码后的字节数据 + { + // 拼写错误 "transfer(address, uint)" 不会被检测到 + // 因为 encodeWithSignature 只接收字符串,编译器不会验证 + return abi.encodeWithSignature("transfer(address,uint256)", to, amount); // 按函数签名编码 + } + + // encodeWithSelector 函数:使用函数选择器编码 + // 注意:参数类型不会被编译器检查! + function encodeWithSelector(address to, uint256 amount) + external + pure + returns (bytes memory) // 返回编码后的字节数据 + { + // 类型不会被检查 - 例如 (IERC20.transfer.selector, true, amount) 也能编译通过 + // 因为 encodeWithSelector 接受任意参数 + return abi.encodeWithSelector(IERC20.transfer.selector, to, amount); // 按函数选择器编码 + } + + // encodeCall 函数:使用类型安全的编码方式(推荐) + // 拼写错误和类型错误都会在编译时被捕获! + function encodeCall(address to, uint256 amount) + external + pure + returns (bytes memory) // 返回编码后的字节数据 + { + // 使用 encodeCall 时,拼写错误和类型错误都将导致编译失败 + // 这是类型安全的方式,也是推荐的做法 + return abi.encodeCall(IERC20.transfer, (to, amount)); // 编译时检查的编码方式(推荐) + } +} +``` diff --git "a/43 ABI Decode\350\247\243\347\240\201.md" "b/43 ABI Decode\350\247\243\347\240\201.md" new file mode 100644 index 0000000..22651b6 --- /dev/null +++ "b/43 ABI Decode\350\247\243\347\240\201.md" @@ -0,0 +1,50 @@ +# ABI Decode解码 + +`abi.encode` 将数据编码为 `bytes` 格式。 + +`abi.decode` 将 `bytes` 解码回原始数据。 + +```solidity +// SPDX-License-Identifier: MIT // SPDX 许可证标识符:MIT 许可证 +pragma solidity ^0.8.26; // 版本指令,要求使用 Solidity 0.8.26 及以上版本 + +// AbiDecode 合约:演示 ABI 编码和解码 +contract AbiDecode { // 定义 AbiDecode 合约 + // MyStruct 结构体:自定义复杂数据类型 + struct MyStruct { // struct 定义包含多种类型字段的结构体 + string name; // 字符串名称 + uint256[2] nums; // 长度为 2 的 uint256 定长数组 + } + + // encode 函数:将多种类型的数据编码为 bytes + function encode( // 接收四种不同类型的参数 + uint256 x, // uint256 类型 + address addr, // address 类型 + uint256[] calldata arr, // uint256 动态数组(calldata 位置) + MyStruct calldata myStruct // 自定义结构体(calldata 位置) + ) external pure returns (bytes memory) { // 返回编码后的 bytes + // abi.encode 将所有参数紧密打包编码为 bytes + return abi.encode(x, addr, arr, myStruct); // 编码所有参数 + } + + // decode 函数:将 bytes 解码回原始数据类型 + function decode(bytes calldata data) // 接收编码后的 bytes 数据 + external + pure + returns ( + uint256 x, // 解码出的 uint256 + address addr, // 解码出的地址 + uint256[] memory arr, // 解码出的动态数组(memory 位置) + MyStruct memory myStruct // 解码出的结构体(memory 位置) + ) + { + // 以下注解展示了解构赋值的等效写法: + // (uint x, address addr, uint[] memory arr, MyStruct myStruct) = ... + + // abi.decode 将 bytes 解码回原始类型 + // 必须指定与编码时完全一致的类型列表 + (x, addr, arr, myStruct) = + abi.decode(data, (uint256, address, uint256[], MyStruct)); // 按指定类型解码 data + } +} +``` diff --git "a/44 Keccak256\345\223\210\345\270\214.md" "b/44 Keccak256\345\223\210\345\270\214.md" new file mode 100644 index 0000000..c5afd5a --- /dev/null +++ "b/44 Keccak256\345\223\210\345\270\214.md" @@ -0,0 +1,68 @@ +# Keccak256哈希 + +`keccak256` 用于计算输入的 Keccak-256 哈希值。 + +一些用例包括: + +- 从输入创建确定性的唯一 ID +- 提交-揭示(Commit-Reveal)方案 +- 紧凑的加密签名(通过签名哈希值而不是较大的原始输入) + +Solidity 提供了两种数据编码方法: + +| 方法 | 特点 | 优势 | 劣势 | +|------|------|------|------| +| `abi.encode` | 带填充的编码,保留所有数据信息 | 处理动态类型时更安全 | 输出较长 | +| `abi.encodePacked` | 紧凑编码(压缩),Gas 更高效 | 输出更短,节省 Gas | 动态类型存在哈希碰撞风险 | + +```solidity +// SPDX-License-Identifier: MIT // SPDX 许可证标识符:MIT 许可证 +pragma solidity ^0.8.26; // 版本指令,要求使用 Solidity 0.8.26 及以上版本 + +// HashFunction 合约:演示 keccak256 哈希的用法及碰撞风险 +contract HashFunction { // 定义 HashFunction 合约 + // hash 函数:对输入进行 keccak256 哈希 + function hash( // 接收字符串、uint 和地址三种类型参数 + string memory _text, // 文本字符串 + uint256 _num, // 数值 + address _addr // 地址 + ) public pure returns (bytes32) { // 返回 32 字节的哈希值 + // abi.encodePacked 执行紧凑编码后进行哈希 + // 注意:多个动态类型组合时存在哈希碰撞风险 + return keccak256(abi.encodePacked(_text, _num, _addr)); // 打包编码后计算哈希 + } + + // collision 函数:演示 abi.encodePacked 的哈希碰撞漏洞 + // 当你使用 abi.encodePacked 打包多个动态类型(如字符串)时, + // 不同的输入组合可能产生相同的打包结果,从而导致相同的哈希值 + function collision(string memory _text, string memory _anotherText) // 接收两个字符串 + public + pure + returns (bytes32) // 返回哈希值 + { + // encodePacked("AAA", "BBB") -> "AAABBB" + // encodePacked("AA", "ABBB") -> "AAABBB" + // 以上两种不同的输入产生了相同的打包结果! + // 因此它们的 keccak256 哈希值也完全相同,这就是哈希碰撞 + return keccak256(abi.encodePacked(_text, _anotherText)); // 存在碰撞风险的写法 + } + + // 修复方法:使用 abi.encode 替代 abi.encodePacked + // abi.encode 会在每个参数之间添加 32 字节的填充,避免边界模糊 + // keccak256(abi.encode(_text, _anotherText)) 是更安全的写法 +} + +// GuessTheMagicWord 合约:演示 Commit-Reveal 模式 +contract GuessTheMagicWord { // 定义猜词合约 + // 预先存储正确答案的哈希值 + // 正确答案是 "Solidity",但哈希值不会泄露原始信息 + bytes32 public answer = + 0x60298f78cc0b47170ba79c10aa3851d7648bd96f2f22e46facf0db0fa7c77189; // keccak256(abi.encodePacked("Solidity")) + + // guess 函数:用户提交猜测,验证是否匹配 + function guess(string memory _word) public view returns (bool) { // 接收用户猜的词 + // 对用户的输入进行哈希,与预设的 answer 进行比较 + return keccak256(abi.encodePacked(_word)) == answer; // 匹配则返回 true + } +} +``` diff --git "a/45 \351\252\214\350\257\201\347\255\276\345\220\215.md" "b/45 \351\252\214\350\257\201\347\255\276\345\220\215.md" new file mode 100644 index 0000000..458ac0a --- /dev/null +++ "b/45 \351\252\214\350\257\201\347\255\276\345\220\215.md" @@ -0,0 +1,104 @@ +# 验证签名 + +消息可以在链下签名,然后通过智能合约在链上进行验证。 + +## 工作流程 + +1. **创建消息哈希** —— 将地址、金额、消息字符串和 nonce 打包后计算 `keccak256` 哈希 +2. **添加以太坊签名前缀** —— 在哈希前加上 `"\x19Ethereum Signed Message:\n32"` 后再哈希,这与 MetaMask 和 web3 库的签名格式一致 +3. **链下签名** —— 用户通过 `personal_sign`(浏览器钱包)或 `web3.eth.personal.sign` 进行签名 +4. **链上验证** —— 合约重新计算哈希,拆分签名 `(r, s, v)`,通过 `ecrecover` 恢复签名者地址 + +```solidity +// SPDX-License-Identifier: MIT // SPDX 许可证标识符:MIT 许可证 +pragma solidity ^0.8.26; // 版本指令,要求使用 Solidity 0.8.26 及以上版本 + +// VerifySignature 合约:演示链下签名、链上验证的完整流程 +contract VerifySignature { // 定义 VerifySignature 合约 + // 第一步:创建消息哈希 + // getMessageHash 函数:将消息参数打包后计算 keccak256 哈希 + function getMessageHash( + address _to, // 接收方地址 + uint256 _amount, // 金额 + string memory _message, // 消息文本 + uint256 _nonce // 随机数(防止重放攻击) + ) public pure returns (bytes32) { // 返回消息哈希 + // 使用 abi.encodePacked 打包所有参数后哈希 + // 注意:这里只有一个动态类型(string),所以碰撞风险可控 + return keccak256(abi.encodePacked(_to, _amount, _message, _nonce)); // 计算消息哈希 + } + + // 第二步:添加以太坊签名前缀 + // getEthSignedMessageHash 函数:按以太坊标准格式化消息哈希 + function getEthSignedMessageHash(bytes32 _messageHash) // 接收消息哈希 + public + pure + returns (bytes32) // 返回带前缀的签名哈希 + { + // 以太坊签名标准:在消息哈希前加上 "\x19Ethereum Signed Message:\n32" + // 这是 MetaMask 和 web3 库通用的签名格式 + return + keccak256( + abi.encodePacked("\x19Ethereum Signed Message:\n32", _messageHash) // 添加前缀后再次哈希 + ); + } + + // 第三步在链下完成(使用钱包签名) + + // 第四步:链上验证签名 + // verify 函数:验证签名是否由声明的签名者签署 + function verify( + address _signer, // 声明的签名者地址 + address _to, // 接收方地址(与签名时一致) + uint256 _amount, // 金额(与签名时一致) + string memory _message, // 消息(与签名时一致) + uint256 _nonce, // 随机数(与签名时一致) + bytes memory signature // 链下签名的 65 字节签名数据 + ) public pure returns (bool) { // 返回验证结果 + // 1. 重新计算消息哈希 + bytes32 messageHash = getMessageHash(_to, _amount, _message, _nonce); + // 2. 添加以太坊签名前缀 + bytes32 ethSignedMessageHash = getEthSignedMessageHash(messageHash); + + // 3. 使用 ecrecover 从签名中恢复签名者地址 + // 4. 比较恢复的地址与声明的签名者地址是否一致 + return recoverSigner(ethSignedMessageHash, signature) == _signer; // 地址匹配则验证通过 + } + + // recoverSigner 函数:从签名和消息哈希中恢复签名者地址 + function recoverSigner( + bytes32 _ethSignedMessageHash, // 带前缀的消息哈希 + bytes memory _signature // 65 字节的签名数据 + ) public pure returns (address) { // 返回恢复出的签名者地址 + // 拆分 65 字节签名为 r、s、v 三个部分 + (bytes32 r, bytes32 s, uint8 v) = splitSignature(_signature); + + // ecrecover 是 EVM 预编译合约,用于从签名恢复公钥地址 + // 参数:消息哈希、v(恢复标识符)、r(签名前半部分)、s(签名后半部分) + return ecrecover(_ethSignedMessageHash, v, r, s); // 恢复并返回签名者地址 + } + + // splitSignature 函数:使用内联汇编拆分 65 字节签名 + function splitSignature(bytes memory sig) // 接收完整的 65 字节签名 + public + pure + returns ( + bytes32 r, // 签名的 r 部分(前 32 字节) + bytes32 s, // 签名的 s 部分(中间 32 字节) + uint8 v // 恢复标识符 v(最后 1 字节) + ) + { + // 验证签名长度必须为 65 字节 + require(sig.length == 65, "invalid signature length"); // 签名必须是 65 字节 + + assembly { // 使用内联汇编读取签名数据 + // r 位于 sig 偏移 32 字节处(前 32 字节是长度前缀) + r := mload(add(sig, 32)) // 读取 bytes 数据的 r(32 字节) + // s 位于 sig 偏移 64 字节处 + s := mload(add(sig, 64)) // 读取 s(32 字节) + // v 位于 sig 偏移 96 字节处,取最后一个字节 + v := byte(0, mload(add(sig, 96))) // 读取 v(1 字节) + } + } +} +``` diff --git "a/46 Gas\344\274\230\345\214\226\346\212\200\345\267\247.md" "b/46 Gas\344\274\230\345\214\226\346\212\200\345\267\247.md" new file mode 100644 index 0000000..6661145 --- /dev/null +++ "b/46 Gas\344\274\230\345\214\226\346\212\200\345\267\247.md" @@ -0,0 +1,61 @@ +# Gas优化技巧 + +一些节省 Gas 的技巧: + +- 使用 `calldata` 替代 `memory` +- 将状态变量加载到内存中缓存 +- 将 `i++` 替换为 `++i` +- 缓存数组元素 +- 短路求值(Short Circuit) + +```solidity +// SPDX-License-Identifier: MIT // SPDX 许可证标识符:MIT 许可证 +pragma solidity ^0.8.26; // 版本指令,要求使用 Solidity 0.8.26 及以上版本 + +// GasGolf 合约:演示 Gas 优化技巧 +contract GasGolf { // 定义 GasGolf 合约 + uint256 public total; // 总计数器(状态变量,存储在 storage 中) + + // 未优化版本(基准线:约 50908 gas) + // sumIfEvenAndLessThan99 函数:对数组中满足条件的元素求和 + // 条件:偶数(num % 2 == 0)且小于 99 + function sumIfEvenAndLessThan99(uint256[] memory nums) external { // 接收数组(memory) + for (uint256 i = 0; i < nums.length; i++) { // 遍历数组 + bool isEven = nums[i] % 2 == 0; // 检查是否为偶数 + bool isLessThan99 = nums[i] < 99; // 检查是否小于 99 + if (isEven && isLessThan99) { // 同时满足两个条件 + total += nums[i]; // 累加到 total(SSTORE 操作) + } + } + } + + // 优化后的版本(约 47309 gas,节省约 7%) + function sumIfEvenAndLessThan99Optimized(uint256[] calldata nums) external { // 使用 calldata 替代 memory + // 优化 1:将状态变量加载到内存中缓存,避免在循环中反复 SLOAD + uint256 _total = total; // 读取一次 storage 到内存,后续使用内存变量 + + // 优化 2:缓存数组长度,避免每次循环都读取 + uint256 len = nums.length; // 长度只读取一次 + + // 优化 3:使用 ++i 替代 i++,并在 unchecked 中跳过溢出检查 + for (uint256 i = 0; i < len;) { // 循环变量 i + // 优化 4:将数组元素缓存到内存变量中 + uint256 num = nums[i]; // 避免重复从 calldata 读取 + + // 优化 5:短路求值 —— 第一个条件为假时,第二个条件不计算 + // num % 2 == 0 为假时,直接跳过 && 后面的计算 + if (num % 2 == 0 && num < 99) { // 短路求值节省不必要的比较 + _total += num; // 累加到内存变量(不是 storage) + } + + // 优化 3(续):在 unchecked 块中自增,跳过溢出检查 + unchecked { + ++i; // i 不会溢出,跳过 SafeMath 检查以节省 gas + } + } + + // 循环结束后一次性写回 storage(仅一次 SSTORE) + total = _total; // 将最终结果写入状态变量 + } +} +``` diff --git "a/47 \344\275\215\350\277\220\347\256\227\347\254\246.md" "b/47 \344\275\215\350\277\220\347\256\227\347\254\246.md" new file mode 100644 index 0000000..1f9a42b --- /dev/null +++ "b/47 \344\275\215\350\277\220\347\256\227\347\254\246.md" @@ -0,0 +1,200 @@ +# 位运算符 + +Solidity 支持对整数进行按位运算。以下是基本的位运算符和用法。 + +```solidity +// SPDX-License-Identifier: MIT // SPDX 许可证标识符:MIT 许可证 +pragma solidity ^0.8.26; // 版本指令,要求使用 Solidity 0.8.26 及以上版本 + +// BitwiseOps 合约:演示各种位运算操作 +contract BitwiseOps { // 定义 BitwiseOps 合约 + // and 函数:按位与运算(对应位都为 1 时结果为 1) + // x = 12 (1100), y = 10 (1010), x & y = 1000 = 8 + function and(uint256 x, uint256 y) external pure returns (uint256) { // 接收两个 uint256 + return x & y; // 按位与 + } + + // or 函数:按位或运算(对应位有一个为 1 时结果为 1) + // x = 12 (1100), y = 10 (1010), x | y = 1110 = 14 + function or(uint256 x, uint256 y) external pure returns (uint256) { + return x | y; // 按位或 + } + + // xor 函数:按位异或运算(对应位不同时结果为 1) + // x = 12 (1100), y = 10 (1010), x ^ y = 0110 = 6 + function xor(uint256 x, uint256 y) external pure returns (uint256) { + return x ^ y; // 按位异或 + } + + // not 函数:按位取反运算(每位翻转,1 变 0,0 变 1) + // x = 12 (00001100), ~x = 11110011 = 243(uint8 范围内) + function not(uint8 x) external pure returns (uint8) { // 仅操作 uint8 类型 + return ~x; // 按位取反 + } + + // shiftLeft 函数:左移运算(相当于乘以 2^bits) + // x = 1 (0001), bits = 2, 1 << 2 = 0100 = 4 + function shiftLeft(uint256 x, uint256 bits) external pure returns (uint256) { + return x << bits; // 将 x 左移 bits 位 + } + + // shiftRight 函数:右移运算(相当于除以 2^bits) + // x = 4 (0100), bits = 1, 4 >> 1 = 0010 = 2 + function shiftRight(uint256 x, uint256 bits) external pure returns (uint256) { + return x >> bits; // 将 x 右移 bits 位 + } + + // getLastNBits 函数:获取最低 n 位(使用位掩码方式) + // x = 13 (1101), n = 2, 掩码 = (1 << 2) - 1 = 0011, 结果 = 1101 & 0011 = 0001 = 1 + function getLastNBits(uint256 x, uint256 n) // 接收数值和要提取的位数 + external + pure + returns (uint256) // 返回低 n 位的值 + { + // 创建掩码:(1 << n) - 1,例如 n=2 时掩码为 000...0011 + uint256 mask = (1 << n) - 1; // 构造低 n 位全为 1 的掩码 + return x & mask; // 按位与提取低 n 位 + } + + // getLastNBitsUsingMod 函数:使用取模方式获取最低 n 位 + // (1 << n) = 2^n,对 2^n 取模等价于取低 n 位 + function getLastNBitsUsingMod(uint256 x, uint256 n) + external + pure + returns (uint256) + { + return x % (1 << n); // x % 2^n 等于 x 的低 n 位 + } + + // mostSignificantBit 函数:找到最高有效位(MSB)的位置 + // 通过不断右移,计数直到 x 变为 0 + function mostSignificantBit(uint256 x) external pure returns (uint256 msb) { + while (x > 0) { // 当 x 还有位时继续 + x >>= 1; // 右移 1 位 + msb++; // 计数器加一 + } + // 注意:当 x=0 时,msb 为 0 + // 当 x=8 (1000) 时,循环 4 次,msb=4(最高位在第 4 位,0 索引为 3) + } + + // getFirstNBits 函数:获取最高 n 位 + // 需要先知道 x 的总位宽 len,通常通过 mostSignificantBit(x) 获得 + function getFirstNBits( + uint256 x, // 要操作的数值 + uint256 n, // 要提取的位数 + uint256 len // x 的总位宽(mostSignificantBit(x)) + ) external pure returns (uint256) { + // 创建掩码:((1 << n) - 1) << (len - n) + // 例如:x=13 (1101), len=4, n=2 + // 掩码 = ((1 << 2) - 1) << 2 = 0011 << 2 = 1100 + uint256 mask = ((1 << n) - 1) << (len - n); // 将 n 个 1 移到高位 + return x & mask; // 按位与提取最高 n 位 + } +} +``` + +## 最高有效位(MSB)—— 二分查找算法 + +```solidity +// SPDX-License-Identifier: MIT // SPDX 许可证标识符:MIT 许可证 +pragma solidity ^0.8.26; // 版本指令 + +// MostSignificantBitFunction 合约:使用二分查找高效找到 MSB +contract MostSignificantBitFunction { // 定义合约 + // mostSignificantBit 函数:通过二分查找确定最高有效位位置 + function mostSignificantBit(uint256 x) external pure returns (uint256 msb) { + // 每次检查 x 是否 >= 2 的某个幂次,如果是则右移并累加 + // 从大到小依次检查 2^128, 2^64, 2^32, 2^16, 2^8, 2^4, 2^2, 2^1 + + if (x >= 0x100000000000000000000000000000000) { // 2^128,如果 x 的最高位在 128 以上 + x >>= 128; // 右移 128 位 + msb += 128; // 累加 128 + } + if (x >= 0x10000000000000000) { // 2^64 + x >>= 64; // 右移 64 位 + msb += 64; // 累加 64 + } + if (x >= 0x100000000) { // 2^32 + x >>= 32; // 右移 32 位 + msb += 32; // 累加 32 + } + if (x >= 0x10000) { // 2^16 + x >>= 16; // 右移 16 位 + msb += 16; // 累加 16 + } + if (x >= 0x100) { // 2^8 + x >>= 8; // 右移 8 位 + msb += 8; // 累加 8 + } + if (x >= 0x10) { // 2^4 + x >>= 4; // 右移 4 位 + msb += 4; // 累加 4 + } + if (x >= 0x4) { // 2^2 + x >>= 2; // 右移 2 位 + msb += 2; // 累加 2 + } + if (x >= 0x2) { // 2^1,最后一步不右移,只检查 + msb += 1; // 累加 1 + } + // 例如:x=42(二进制 101010),最高位位置为 5(2^5=32) + } +} +``` + +## 最高有效位 —— 汇编实现 + +```solidity +// SPDX-License-Identifier: MIT // SPDX 许可证标识符:MIT 许可证 +pragma solidity ^0.8.26; // 版本指令 + +// MostSignificantBitAssembly 合约:使用内联汇编高效计算 MSB +contract MostSignificantBitAssembly { // 定义合约 + // mostSignificantBit 函数:汇编实现的最高有效位查找 + function mostSignificantBit(uint256 x) external pure returns (uint256 msb) { + assembly { // Yul 内联汇编块 + // 检查 x 是否 >= 2^128 范围 + // 如果 >= 则右移 128 位,并将 128 累加到 msb + let f := shl(7, gt(x, 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF)) // 如果 x > 2^128-1,f = 1<<7 = 128 + x := shr(f, x) // 将 x 右移 f 位 + msb := or(msb, f) // 累加 f 到结果(or 可替换为 add,因为无重叠位) + } + assembly { + let f := shl(6, gt(x, 0xFFFFFFFFFFFFFFFF)) // 如果 x > 2^64-1,f = 1<<6 = 64 + x := shr(f, x) + msb := or(msb, f) + } + assembly { + let f := shl(5, gt(x, 0xFFFFFFFF)) // 如果 x > 2^32-1,f = 1<<5 = 32 + x := shr(f, x) + msb := or(msb, f) + } + assembly { + let f := shl(4, gt(x, 0xFFFF)) // 如果 x > 2^16-1,f = 1<<4 = 16 + x := shr(f, x) + msb := or(msb, f) + } + assembly { + let f := shl(3, gt(x, 0xFF)) // 如果 x > 2^8-1,f = 1<<3 = 8 + x := shr(f, x) + msb := or(msb, f) + } + assembly { + let f := shl(2, gt(x, 0xF)) // 如果 x > 2^4-1,f = 1<<2 = 4 + x := shr(f, x) + msb := or(msb, f) + } + assembly { + let f := shl(1, gt(x, 0x3)) // 如果 x > 2^2-1,f = 1<<1 = 2 + x := shr(f, x) + msb := or(msb, f) + } + assembly { + let f := gt(x, 0x1) // 如果 x > 1,f = 1 + // 最后一步不需要右移 x + msb := or(msb, f) // 累加最后 1 位 + } + // 当 x=0 时 msb 为 0(所有检查均失败) + } +} +``` diff --git "a/48 Unchecked\346\225\260\345\255\246\350\277\220\347\256\227.md" "b/48 Unchecked\346\225\260\345\255\246\350\277\220\347\256\227.md" new file mode 100644 index 0000000..b97ccfb --- /dev/null +++ "b/48 Unchecked\346\225\260\345\255\246\350\277\220\347\256\227.md" @@ -0,0 +1,46 @@ +# Unchecked数学运算 + +在 Solidity 0.8 及以上版本中,数字的溢出和下溢默认会抛出错误。可以通过使用 `unchecked` 来禁用此检查。 + +禁用溢出/下溢检查可以节省 Gas。 + +```solidity +// SPDX-License-Identifier: MIT // SPDX 许可证标识符:MIT 许可证 +pragma solidity ^0.8.26; // 版本指令,要求使用 Solidity 0.8.26 及以上版本 + +// UncheckedMath 合约:演示使用 unchecked 绕过溢出检查以节省 Gas +contract UncheckedMath { // 定义 UncheckedMath 合约 + // add 函数:使用 unchecked 进行加法运算 + function add(uint256 x, uint256 y) public pure returns (uint256) { + // unchecked 块内的数学运算不会检查溢出 + unchecked { // 禁用溢出检查 + return x + y; // 约 22103 gas(比默认的 22291 gas 节省约 188 gas) + } + // 注意:如果溢出发生,结果会静默回绕(wrapping),不会 revert + } + + // sub 函数:使用 unchecked 进行减法运算 + function sub(uint256 x, uint256 y) public pure returns (uint256) { + unchecked { // 禁用溢出/下溢检查 + return x - y; // 约 22147 gas(比默认的 22329 gas 节省约 182 gas) + } + // 注意:如果 y > x,结果会下溢回绕为一个大数 + } + + // sumOfCubes 函数:在 unchecked 中进行多次数学运算 + // 当开发者确定不会发生溢出时,将复杂数学逻辑放入 unchecked 可以显著节省 Gas + function sumOfCubes(uint256 x, uint256 y) public pure returns (uint256) { + // 将复杂的数学逻辑包装在 unchecked 块中 + unchecked { // 整个表达式在禁用溢出检查的情况下计算 + // 计算 x³ + y³ + uint256 x3 = x * x * x; // x 的三次方(不检查溢出) + uint256 y3 = y * y * y; // y 的三次方(不检查溢出) + return x3 + y3; // 返回 x³ + y³(不检查溢出) + } + } + + // 风险提示:如果 unchecked 块内确实发生了溢出,结果会静默回绕, + // 不会像正常 Solidity 代码那样抛出错误并回滚交易。 + // 因此,只有在开发者 100% 确定不会溢出时才应使用 unchecked。 +} +``` diff --git "a/49 \346\261\207\347\274\226\345\217\230\351\207\217.md" "b/49 \346\261\207\347\274\226\345\217\230\351\207\217.md" new file mode 100644 index 0000000..e58914a --- /dev/null +++ "b/49 \346\261\207\347\274\226\345\217\230\351\207\217.md" @@ -0,0 +1,24 @@ +# 汇编变量 + +在 Solidity 的内联汇编(Yul)中声明和使用变量。 + +内联汇编使用的语言称为 Yul。通过 `assembly` 关键字可以在 Solidity 中嵌入 Yul 代码块。 + +```solidity +// SPDX-License-Identifier: MIT // SPDX 许可证标识符:MIT 许可证 +pragma solidity ^0.8.26; // 版本指令,要求使用 Solidity 0.8.26 及以上版本 + +// AssemblyVariable 合约:演示在 Yul 汇编中声明和使用变量 +contract AssemblyVariable { // 定义 AssemblyVariable 合约 + // yul_let 函数:演示 Yul 中的变量声明和赋值 + function yul_let() public pure returns (uint256 z) { // 返回 uint256 类型变量 z + assembly { // 进入内联汇编(Yul)代码块 + let x := 123 // 使用 let 声明局部变量 x,并初始化为 123 + z := 456 // 将 456 赋值给函数的返回值变量 z + // 函数最终返回 456(因为 z 在汇编块中被直接设置) + } + // 注意:Yul 中的变量是局部作用域的,仅在当前 assembly 块内有效 + // Solidity 的变量(如 z)可以在汇编块中直接访问和修改 + } +} +``` diff --git "a/50 \346\261\207\347\274\226\346\235\241\344\273\266\350\257\255\345\217\245.md" "b/50 \346\261\207\347\274\226\346\235\241\344\273\266\350\257\255\345\217\245.md" new file mode 100644 index 0000000..47d09c6 --- /dev/null +++ "b/50 \346\261\207\347\274\226\346\235\241\344\273\266\350\257\255\345\217\245.md" @@ -0,0 +1,41 @@ +# 汇编条件语句 + +Yul 汇编支持两种条件控制流:`if` 和 `switch`。 + +```solidity +// SPDX-License-Identifier: MIT // SPDX 许可证标识符:MIT 许可证 +pragma solidity ^0.8.26; // 版本指令,要求使用 Solidity 0.8.26 及以上版本 + +// AssemblyIf 合约:演示 Yul 汇编中的 if 和 switch 语句 +contract AssemblyIf { // 定义 AssemblyIf 合约 + // yul_if 函数:演示 Yul 中的 if 语句 + function yul_if(uint256 x) public pure returns (uint256 z) { // 接收 uint256 参数 + assembly { // 进入 Yul 汇编块 + // Yul 的 if 语句格式:if condition { code } + // 注意:Yul 中的 if 没有 else 分支! + if lt(x, 10) { // lt(x, 10) 等价于 x < 10,如果条件为真 + z := 99 // 将 z 设置为 99 + } + // 如果 x >= 10,z 保持默认值 0 + } + } + + // yul_switch 函数:演示 Yul 中的 switch 语句 + function yul_switch(uint256 x) public pure returns (uint256 z) { // 接收 uint256 参数 + assembly { // 进入 Yul 汇编块 + // Yul 的 switch 语句格式:switch value case val1 { ... } case val2 { ... } default { ... } + switch x // 对 x 的值进行分支判断 + case 1 { // 如果 x == 1 + z := 10 // 将 z 设置为 10 + } + case 2 { // 如果 x == 2 + z := 20 // 将 z 设置为 20 + } + default { // 默认分支(所有其他情况) + z := 0 // 将 z 设置为 0 + } + // switch 在处理多个离散值时比嵌套 if 更清晰 + } + } +} +``` diff --git "a/51 \346\261\207\347\274\226\345\276\252\347\216\257.md" "b/51 \346\261\207\347\274\226\345\276\252\347\216\257.md" new file mode 100644 index 0000000..fd8e333 --- /dev/null +++ "b/51 \346\261\207\347\274\226\345\276\252\347\216\257.md" @@ -0,0 +1,47 @@ +# 汇编循环 + +Yul 汇编使用 `for` 语句实现循环。Yul 没有专门的 `while` 关键字,但可以通过 `for` 来模拟。 + +```solidity +// SPDX-License-Identifier: MIT // SPDX 许可证标识符:MIT 许可证 +pragma solidity ^0.8.26; // 版本指令,要求使用 Solidity 0.8.26 及以上版本 + +// AssemblyLoop 合约:演示 Yul 汇编中的 for 循环和 while 循环 +contract AssemblyLoop { // 定义 AssemblyLoop 合约 + // yul_for_loop 函数:演示 Yul 中的 for 循环 + function yul_for_loop() public pure returns (uint256 z) { // 返回计数器结果 + assembly { // 进入 Yul 汇编块 + // Yul for 循环格式: + // for { 初始化 } 条件 { 每次迭代后执行 } { 循环体 } + + // 经典 C 风格 for 循环:for (i=0; i<10; i++) { z++ } + for { let i := 0 } // 初始化:声明 i 并赋值为 0 + lt(i, 10) // 条件:当 i < 10 时继续循环 + { i := add(i, 1) } // 迭代递增:i = i + 1 + { // 循环体开始 + z := add(z, 1) // z = z + 1 + } + // 循环执行 10 次(i = 0, 1, 2, ..., 9),z 最终为 10 + } + } + + // yul_while_loop 函数:演示如何用 for 模拟 while 循环 + function yul_while_loop() public pure returns (uint256 z) { + assembly { // 进入 Yul 汇编块 + let i := 0 // 在循环外初始化 i = 0 + // 模拟 while (i < 5) { i++; z++; } + for {} // 空的初始化块(相当于 while 没有初始化) + lt(i, 5) // 条件:当 i < 5 时继续 + {} // 空的迭代递增块 + { // 循环体:手动递增 i 和 z + i := add(i, 1) // i = i + 1 + z := add(z, 1) // z = z + 1 + } + // 循环执行 5 次,z 最终为 5 + } + + // 总结:Yul 只有 for 循环,没有 while 关键字 + // 通过将 for 的初始化和递增块留空,可以模拟 while 循环的行为 + } +} +``` diff --git "a/52 \346\261\207\347\274\226\351\224\231\350\257\257\345\244\204\347\220\206.md" "b/52 \346\261\207\347\274\226\351\224\231\350\257\257\345\244\204\347\220\206.md" new file mode 100644 index 0000000..979a3fc --- /dev/null +++ "b/52 \346\261\207\347\274\226\351\224\231\350\257\257\345\244\204\347\220\206.md" @@ -0,0 +1,27 @@ +# 汇编错误处理 + +在 Yul 汇编中,使用 `revert` 来回滚交易并撤销所有状态更改。 + +```solidity +// SPDX-License-Identifier: MIT // SPDX 许可证标识符:MIT 许可证 +pragma solidity ^0.8.26; // 版本指令,要求使用 Solidity 0.8.26 及以上版本 + +// AssemblyError 合约:演示 Yul 汇编中的错误处理 +contract AssemblyError { // 定义 AssemblyError 合约 + // yul_revert 函数:如果 x > 10 则回滚交易 + function yul_revert(uint256 x) public pure { // 接收 uint256 参数 + assembly { // 进入 Yul 汇编块 + // gt(x, 10) 等价于 x > 10 + if gt(x, 10) { // 如果 x 大于 10 + // revert(offset, size) 回滚交易 + // offset=0, size=0 表示不返回任何错误数据 + // 这相当于一个不包含错误消息的回滚 + revert(0, 0) // 终止执行,撤销所有状态更改,不返回错误数据 + } + // 如果 x <= 10,函数正常结束,不执行任何操作 + // 使用 revert(0, 0) 可以节省编码错误字符串的 Gas, + // 但代价是不提供任何错误信息给调用者 + } + } +} +``` diff --git "a/53 \346\261\207\347\274\226\346\225\260\345\255\246\350\277\220\347\256\227.md" "b/53 \346\261\207\347\274\226\346\225\260\345\255\246\350\277\220\347\256\227.md" new file mode 100644 index 0000000..6214edc --- /dev/null +++ "b/53 \346\261\207\347\274\226\346\225\260\345\255\246\350\277\220\347\256\227.md" @@ -0,0 +1,63 @@ +# 汇编数学运算 + +Yul 汇编中的数学运算,包括带溢出检查的加法、乘法和定点数舍入。 + +```solidity +// SPDX-License-Identifier: MIT // SPDX 许可证标识符:MIT 许可证 +pragma solidity ^0.8.26; // 版本指令,要求使用 Solidity 0.8.26 及以上版本 + +// AssemblyMath 合约:演示 Yul 汇编中的数学运算 +contract AssemblyMath { // 定义 AssemblyMath 合约 + // yul_add 函数:带溢出检查的 Yul 加法 + function yul_add(uint256 x, uint256 y) public pure returns (uint256 z) { + assembly { // 进入 Yul 汇编块 + z := add(x, y) // z = x + y(Yul 中的加法不会自动检查溢出) + // 溢出检查:如果溢出,z 会小于 x(因为结果回绕) + if lt(z, x) { // 如果 z < x,说明发生了溢出 + revert(0, 0) // 回滚交易 + } + // 如果没有溢出,正常返回 z + } + } + + // yul_mul 函数:带溢出检查的 Yul 乘法 + function yul_mul(uint256 x, uint256 y) public pure returns (uint256 z) { + assembly { // 进入 Yul 汇编块 + // 特殊情况:如果 x 为 0,结果一定为 0 + switch x // 对 x 的值进行判断 + case 0 { // 如果 x == 0 + z := 0 // 结果为 0 + } + default { // x != 0 的情况 + z := mul(x, y) // z = x * y + // 溢出检查:将 z 除以 x,如果不等于 y 则说明溢出 + // iszero(eq(div(z, x), y)) 等价于 (z / x) != y + if iszero(eq(div(z, x), y)) { // 如果除法验证不通过 + revert(0, 0) // 回滚交易(乘法溢出) + } + } + } + } + + // yul_fixed_point_round 函数:定点数舍入到最近的倍数 + // 例如:x=90, b=100 → 90 + 50 = 140, 140/100 = 1, 1*100 = 100 + function yul_fixed_point_round(uint256 x, uint256 b) + public + pure + returns (uint256 z) + { + assembly { // 进入 Yul 汇编块 + // 公式:((x + b/2) / b) * b + // 通过先加 b/2 实现四舍五入: + // x=90, b=100 → (90+50)/100*100 = 140/100*100 = 1*100 = 100 + // x=40, b=100 → (40+50)/100*100 = 90/100*100 = 0*100 = 0 + // x=150, b=100 → (150+50)/100*100 = 200/100*100 = 2*100 = 200 + + // 注意:此实现假设 b 不会溢出 + z := add(x, div(b, 2)) // step 1: x + b/2 + z := div(z, b) // step 2: (x + b/2) / b + z := mul(z, b) // step 3: ((x + b/2) / b) * b + } + } +} +``` diff --git "a/Unit2 \345\272\224\347\224\250/ERC1155\345\244\232\344\273\243\345\270\201.md" "b/Unit2 \345\272\224\347\224\250/ERC1155\345\244\232\344\273\243\345\270\201.md" new file mode 100644 index 0000000..c2501c9 --- /dev/null +++ "b/Unit2 \345\272\224\347\224\250/ERC1155\345\244\232\344\273\243\345\270\201.md" @@ -0,0 +1,287 @@ +# ERC1155 + +ERC1155 是多代币标准,允许一个合约同时管理同质化代币(FT)和非同质化代币(NFT)。每个代币类型由唯一的 `id` 标识,同一 `id` 下可以有多个余额。 + +```solidity +// SPDX-License-Identifier: MIT // SPDX 许可证标识符:MIT 许可证 +pragma solidity ^0.8.26; // 版本指令,要求使用 Solidity 0.8.26 及以上版本 + +// IERC1155 接口:ERC1155 多代币标准接口 +interface IERC1155 { // 定义 ERC1155 标准接口 + // safeTransferFrom 函数:安全转移单个代币类型 + function safeTransferFrom( + address from, // 发送者 + address to, // 接收者 + uint256 id, // 代币类型 ID + uint256 value, // 数量 + bytes calldata data // 附加数据 + ) external; + // safeBatchTransferFrom 函数:安全批量转移多种代币类型 + function safeBatchTransferFrom( + address from, + address to, + uint256[] calldata ids, // 代币 ID 数组 + uint256[] calldata values, // 对应数量数组 + bytes calldata data + ) external; + function balanceOf(address owner, uint256 id) // 查询某地址拥有某代币类型的数量 + external + view + returns (uint256); + function balanceOfBatch(address[] calldata owners, uint256[] calldata ids) // 批量查询余额 + external + view + returns (uint256[] memory); + function setApprovalForAll(address operator, bool approved) external; // 批量授权 + function isApprovedForAll(address owner, address operator) // 检查批量授权 + external + view + returns (bool); +} + +// IERC1155TokenReceiver 接口:安全接收 ERC1155 代币的回调接口 +interface IERC1155TokenReceiver { // 接收者必须实现此接口 + // onERC1155Received 回调:单次接收代币时被调用 + function onERC1155Received( + address operator, + address from, + uint256 id, + uint256 value, + bytes calldata data + ) external returns (bytes4); // 返回函数选择器确认接收 + + // onERC1155BatchReceived 回调:批量接收代币时被调用 + function onERC1155BatchReceived( + address operator, + address from, + uint256[] calldata ids, + uint256[] calldata values, + bytes calldata data + ) external returns (bytes4); +} + +// ERC1155 合约:实现 ERC1155 多代币标准 +contract ERC1155 is IERC1155 { // 实现 IERC1155 接口 + // TransferSingle 事件:单次转移时触发 + event TransferSingle( + address indexed operator, + address indexed from, + address indexed to, + uint256 id, + uint256 value + ); + // TransferBatch 事件:批量转移时触发 + event TransferBatch( + address indexed operator, + address indexed from, + address indexed to, + uint256[] ids, + uint256[] values + ); + // ApprovalForAll 事件:批量授权时触发 + event ApprovalForAll( + address indexed owner, address indexed operator, bool approved + ); + // URI 事件:元数据 URI 变更时触发 + event URI(string value, uint256 indexed id); + + // balanceOf 映射:owner => id => balance(所有者 => 代币 ID => 余额) + mapping(address => mapping(uint256 => uint256)) public balanceOf; + // isApprovedForAll 映射:owner => operator => approved + mapping(address => mapping(address => bool)) public isApprovedForAll; + + // balanceOfBatch 函数:批量查询余额 + function balanceOfBatch(address[] calldata owners, uint256[] calldata ids) + external + view + returns (uint256[] memory balances) // 返回余额数组 + { + require(owners.length == ids.length, "owners length != ids length"); // 数组长度必须一致 + + balances = new uint256[](owners.length); // 创建结果数组 + + unchecked { // 使用 unchecked 跳过溢出检查以节省 gas + for (uint256 i = 0; i < owners.length; i++) { // 遍历查询 + balances[i] = balanceOf[owners[i]][ids[i]]; // 获取每个 owner 对应 id 的余额 + } + } + } + + // setApprovalForAll 函数:设置批量授权 + function setApprovalForAll(address operator, bool approved) external { + isApprovedForAll[msg.sender][operator] = approved; // 设置授权状态 + emit ApprovalForAll(msg.sender, operator, approved); // 触发事件 + } + + // safeTransferFrom 函数:安全转移单个代币类型 + function safeTransferFrom( + address from, + address to, + uint256 id, + uint256 value, + bytes calldata data + ) external { + require( + msg.sender == from || isApprovedForAll[from][msg.sender], // 调用者必须是所有者或已被授权 + "not approved" + ); + require(to != address(0), "to = 0 address"); + + balanceOf[from][id] -= value; // 减少发送者余额 + balanceOf[to][id] += value; // 增加接收者余额 + + emit TransferSingle(msg.sender, from, to, id, value); // 触发 TransferSingle 事件 + + // 如果接收者是合约,检查是否实现了安全的接收回调 + if (to.code.length > 0) { // code.length > 0 表示 to 是合约地址 + require( + IERC1155TokenReceiver(to).onERC1155Received( + msg.sender, from, id, value, data + ) == IERC1155TokenReceiver.onERC1155Received.selector, // 必须返回正确的选择器 + "unsafe transfer" + ); + } + } + + // safeBatchTransferFrom 函数:安全批量转移 + function safeBatchTransferFrom( + address from, + address to, + uint256[] calldata ids, + uint256[] calldata values, + bytes calldata data + ) external { + require( + msg.sender == from || isApprovedForAll[from][msg.sender], // 权限检查 + "not approved" + ); + require(to != address(0), "to = 0 address"); + require(ids.length == values.length, "ids length != values length"); // 数组长度校验 + + // 批量更新余额 + for (uint256 i = 0; i < ids.length; i++) { // 遍历所有代币 ID + balanceOf[from][ids[i]] -= values[i]; // 减少发送者余额 + balanceOf[to][ids[i]] += values[i]; // 增加接收者余额 + } + + emit TransferBatch(msg.sender, from, to, ids, values); // 触发 TransferBatch 事件 + + // 安全检查:如果接收者是合约,调用批量接收回调 + if (to.code.length > 0) { + require( + IERC1155TokenReceiver(to).onERC1155BatchReceived( + msg.sender, from, ids, values, data + ) == IERC1155TokenReceiver.onERC1155BatchReceived.selector, + "unsafe transfer" + ); + } + } + + // supportsInterface 函数:ERC165 接口检测 + function supportsInterface(bytes4 interfaceId) + external + pure + returns (bool) + { + return interfaceId == 0x01ffc9a7 // ERC165 接口 ID(ERC165 自身) + || interfaceId == 0xd9b67a26 // ERC165 接口 ID(ERC1155) + || interfaceId == 0x0e89341c; // ERC165 接口 ID(ERC1155MetadataURI) + } + + // uri 函数:返回代币的元数据 URI(子合约可重写) + function uri(uint256 id) public view virtual returns (string memory) {} // 虚函数,由子合约实现 + + // ===== 内部函数 ===== + + // _mint 内部函数:铸造单个代币类型 + function _mint(address to, uint256 id, uint256 value, bytes memory data) + internal + { + require(to != address(0), "to = 0 address"); + balanceOf[to][id] += value; // 增加目标地址的余额 + emit TransferSingle(msg.sender, address(0), to, id, value); // 从零地址转移 = 铸造 + // 安全检查 + if (to.code.length > 0) { + require( + IERC1155TokenReceiver(to).onERC1155Received( + msg.sender, address(0), id, value, data + ) == IERC1155TokenReceiver.onERC1155Received.selector, + "unsafe transfer" + ); + } + } + + // _batchMint 内部函数:批量铸造 + function _batchMint( + address to, + uint256[] calldata ids, + uint256[] calldata values, + bytes calldata data + ) internal { + require(to != address(0), "to = 0 address"); + require(ids.length == values.length, "ids length != values length"); + for (uint256 i = 0; i < ids.length; i++) { + balanceOf[to][ids[i]] += values[i]; // 铸造每种代币 + } + emit TransferBatch(msg.sender, address(0), to, ids, values); // 批量铸造事件 + if (to.code.length > 0) { + require( + IERC1155TokenReceiver(to).onERC1155BatchReceived( + msg.sender, address(0), ids, values, data + ) == IERC1155TokenReceiver.onERC1155BatchReceived.selector, + "unsafe transfer" + ); + } + } + + // _burn 内部函数:销毁单个代币类型 + function _burn(address from, uint256 id, uint256 value) internal { + require(from != address(0), "from = 0 address"); + balanceOf[from][id] -= value; // 减少余额 + emit TransferSingle(msg.sender, from, address(0), id, value); // 转移到零地址 = 销毁 + } + + // _batchBurn 内部函数:批量销毁 + function _batchBurn( + address from, + uint256[] calldata ids, + uint256[] calldata values + ) internal { + require(from != address(0), "from = 0 address"); + require(ids.length == values.length, "ids length != values length"); + for (uint256 i = 0; i < ids.length; i++) { + balanceOf[from][ids[i]] -= values[i]; // 批量减少余额 + } + emit TransferBatch(msg.sender, from, address(0), ids, values); // 批量销毁事件 + } +} + +// MyMultiToken 合约:继承 ERC1155 的多代币合约 +contract MyMultiToken is ERC1155 { // 继承 ERC1155 合约 + // mint 函数:铸造单种代币 + function mint(uint256 id, uint256 value, bytes memory data) external { + _mint(msg.sender, id, value, data); // 为调用者铸造指定类型和数量的代币 + } + + // batchMint 函数:批量铸造多种代币 + function batchMint( + uint256[] calldata ids, + uint256[] calldata values, + bytes calldata data + ) external { + _batchMint(msg.sender, ids, values, data); // 批量铸造 + } + + // burn 函数:销毁单种代币 + function burn(uint256 id, uint256 value) external { + _burn(msg.sender, id, value); // 销毁调用者自己的代币 + } + + // batchBurn 函数:批量销毁多种代币 + function batchBurn(uint256[] calldata ids, uint256[] calldata values) + external + { + _batchBurn(msg.sender, ids, values); // 批量销毁 + } +} +``` diff --git "a/Unit2 \345\272\224\347\224\250/ERC20\344\273\243\345\270\201.md" "b/Unit2 \345\272\224\347\224\250/ERC20\344\273\243\345\270\201.md" new file mode 100644 index 0000000..c67ed19 --- /dev/null +++ "b/Unit2 \345\272\224\347\224\250/ERC20\344\273\243\345\270\201.md" @@ -0,0 +1,228 @@ +# ERC20 + +遵循 ERC20 标准 的任何合约都是 ERC20 代币。 + +ERC20 代币提供以下功能: +- 转移代币 +- 允许他人代表代币持有者转移代币 + +## ERC20 接口 + +```solidity +// SPDX-License-Identifier: MIT // SPDX 许可证标识符:MIT 许可证 +pragma solidity ^0.8.26; // 版本指令,要求使用 Solidity 0.8.26 及以上版本 + +// IERC20 接口:定义 ERC20 代币的标准函数签名 +interface IERC20 { // interface 关键字定义接口 + function totalSupply() external view returns (uint256); // 返回总供应量 + function balanceOf(address account) external view returns (uint256); // 查询某地址的余额 + function transfer(address recipient, uint256 amount) // 转移代币 + external + returns (bool); // 返回是否成功 + function allowance(address owner, address spender) // 查询授权额度 + external + view + returns (uint256); + function approve(address spender, uint256 amount) external returns (bool); // 授权某人代为转账的额度 + function transferFrom(address sender, address recipient, uint256 amount) // 代表发送者转账 + external + returns (bool); +} +``` + +## ERC20 代币合约 + +```solidity +// SPDX-License-Identifier: MIT // SPDX 许可证标识符:MIT 许可证 +pragma solidity ^0.8.26; // 版本指令,要求使用 Solidity 0.8.26 及以上版本 + +import "./IERC20.sol"; // 导入 IERC20 接口 + +// ERC20 合约:实现 ERC20 标准的代币合约 +contract ERC20 is IERC20 { // 继承 IERC20 接口 + // Transfer 事件:转账时触发 + event Transfer(address indexed from, address indexed to, uint256 value); + // Approval 事件:授权时触发 + event Approval( + address indexed owner, address indexed spender, uint256 value + ); + + uint256 public totalSupply; // 代币总供应量 + mapping(address => uint256) public balanceOf; // 地址到余额的映射 + // allowance 嵌套映射:owner 授权 spender 可使用的代币数量 + mapping(address => mapping(address => uint256)) public allowance; + string public name; // 代币名称 + string public symbol; // 代币符号 + uint8 public decimals; // 小数位数 + + // 构造函数:初始化代币的名称、符号和小数位数 + constructor(string memory _name, string memory _symbol, uint8 _decimals) { + name = _name; // 设置代币名称 + symbol = _symbol; // 设置代币符号 + decimals = _decimals; // 设置小数位数 + } + + // transfer 函数:将代币从调用者转移到接收者 + function transfer(address recipient, uint256 amount) + external + returns (bool) // 返回是否成功 + { + balanceOf[msg.sender] -= amount; // 减少发送者的余额(Solidity 0.8+ 自动检查下溢) + balanceOf[recipient] += amount; // 增加接收者的余额 + emit Transfer(msg.sender, recipient, amount); // 触发 Transfer 事件 + return true; // 返回成功 + } + + // approve 函数:授权 spender 可以从调用者账户中转移一定数量的代币 + function approve(address spender, uint256 amount) external returns (bool) { + allowance[msg.sender][spender] = amount; // 设置授权额度 + emit Approval(msg.sender, spender, amount); // 触发 Approval 事件 + return true; + } + + // transferFrom 函数:代表发送者将代币转移给接收者(需要发送者预先授权) + function transferFrom(address sender, address recipient, uint256 amount) + external + returns (bool) + { + allowance[sender][msg.sender] -= amount; // 减少授权额度 + balanceOf[sender] -= amount; // 减少发送者余额 + balanceOf[recipient] += amount; // 增加接收者余额 + emit Transfer(sender, recipient, amount); // 触发 Transfer 事件 + return true; + } + + // _mint 内部函数:铸造新代币(内部调用,增加总供应量) + function _mint(address to, uint256 amount) internal { // internal 只能被合约内部或子合约调用 + balanceOf[to] += amount; // 增加目标地址的余额 + totalSupply += amount; // 增加总供应量 + emit Transfer(address(0), to, amount); // 从零地址转账表示铸造 + } + + // _burn 内部函数:销毁代币(内部调用,减少总供应量) + function _burn(address from, uint256 amount) internal { + balanceOf[from] -= amount; // 减少来源地址的余额 + totalSupply -= amount; // 减少总供应量 + emit Transfer(from, address(0), amount); // 转移到零地址表示销毁 + } + + // mint 外部函数:公开的铸造接口 + function mint(address to, uint256 amount) external { + _mint(to, amount); // 调用内部 _mint 函数 + } + + // burn 外部函数:公开的销毁接口 + function burn(address from, uint256 amount) external { + _burn(from, amount); // 调用内部 _burn 函数 + } +} +``` + +## 创建自己的 ERC20 代币 + +使用 OpenZeppelin 可以非常容易地创建自己的 ERC20 代币。以下是使用上述 ERC20 合约创建自定义代币的示例: + +```solidity +// SPDX-License-Identifier: MIT // SPDX 许可证标识符:MIT 许可证 +pragma solidity ^0.8.26; // 版本指令 + +import "./ERC20.sol"; // 导入自定义的 ERC20 合约 + +// MyToken 合约:继承 ERC20,创建自定义代币 +contract MyToken is ERC20 { // 继承 ERC20 合约 + // 构造函数:设置代币名称、符号和小数位数 + constructor(string memory name, string memory symbol, uint8 decimals) + ERC20(name, symbol, decimals) // 调用父合约 ERC20 的构造函数 + { + // 为 msg.sender 铸造 100 个代币 + // 类似于: + // 1 美元 = 100 美分 + // 1 个代币 = 1 * (10 ** decimals) 最小单位 + _mint(msg.sender, 100 * 10 ** uint256(decimals)); // 铸造 100 个代币 + } +} +``` + +## 代币交换合约 + +以下是一个 `TokenSwap` 合约,用于将一种 ERC20 代币交换为另一种。它通过调用 `transferFrom` 来完成交换。 + +在 `TokenSwap` 调用 `transferFrom` 之前,发送者必须: +- 余额中有足够的代币 +- 通过调用 `approve` 允许 `TokenSwap` 提取 `amount` 数量的代币 + +```solidity +// SPDX-License-Identifier: MIT // SPDX 许可证标识符:MIT 许可证 +pragma solidity ^0.8.26; // 版本指令 + +import "./IERC20.sol"; // 导入 IERC20 接口 + +/* +代币交换流程: + +1. Alice 拥有 100 枚 AliceCoin(一种 ERC20 代币) +2. Bob 拥有 100 枚 BobCoin(也是 ERC20 代币) +3. Alice 和 Bob 希望用 10 枚 AliceCoin 交换 20 枚 BobCoin +4. Alice 或 Bob 部署 TokenSwap 合约 +5. Alice 调用 approve 授权 TokenSwap 从 AliceCoin 中提取 10 枚代币 +6. Bob 调用 approve 授权 TokenSwap 从 BobCoin 中提取 20 枚代币 +7. Alice 或 Bob 调用 TokenSwap.swap() 执行交换 +8. Alice 和 Bob 成功完成代币互换 +*/ + +// TokenSwap 合约:原子化地交换两种 ERC20 代币 +contract TokenSwap { // 定义 TokenSwap 合约 + IERC20 public token1; // 第一种代币的合约引用 + address public owner1; // 第一种代币的拥有者 + uint256 public amount1; // 第一种代币的交换数量 + IERC20 public token2; // 第二种代币的合约引用 + address public owner2; // 第二种代币的拥有者 + uint256 public amount2; // 第二种代币的交换数量 + + // 构造函数:设置交换双方的代币、拥有者和数量 + constructor( + address _token1, + address _owner1, + uint256 _amount1, + address _token2, + address _owner2, + uint256 _amount2 + ) { + token1 = IERC20(_token1); // 将地址转换为 IERC20 接口类型 + owner1 = _owner1; // 设置代币1的拥有者 + amount1 = _amount1; // 设置代币1的数量 + token2 = IERC20(_token2); // 将地址转换为 IERC20 接口类型 + owner2 = _owner2; // 设置代币2的拥有者 + amount2 = _amount2; // 设置代币2的数量 + } + + // swap 函数:执行原子交换 + function swap() public { // 任意一方都可以调用 + require(msg.sender == owner1 || msg.sender == owner2, "Not authorized"); // 只有交换双方可以调用 + require( + token1.allowance(owner1, address(this)) >= amount1, // 检查代币1的授权额度是否足够 + "Token 1 allowance too low" + ); + require( + token2.allowance(owner2, address(this)) >= amount2, // 检查代币2的授权额度是否足够 + "Token 2 allowance too low" + ); + + // 将代币1从 owner1 转给 owner2 + _safeTransferFrom(token1, owner1, owner2, amount1); // Alice → Bob + // 将代币2从 owner2 转给 owner1 + _safeTransferFrom(token2, owner2, owner1, amount2); // Bob → Alice + } + + // _safeTransferFrom 私有函数:安全地执行 transferFrom,并检查返回值 + function _safeTransferFrom( + IERC20 token, // 代币合约 + address sender, // 发送者 + address recipient, // 接收者 + uint256 amount // 数量 + ) private { + bool sent = token.transferFrom(sender, recipient, amount); // 执行转账 + require(sent, "Token transfer failed"); // 检查转账是否成功 + } +} +``` diff --git "a/Unit2 \345\272\224\347\224\250/ERC721\351\235\236\345\220\214\350\264\250\345\214\226\344\273\243\345\270\201.md" "b/Unit2 \345\272\224\347\224\250/ERC721\351\235\236\345\220\214\350\264\250\345\214\226\344\273\243\345\270\201.md" new file mode 100644 index 0000000..28e67fa --- /dev/null +++ "b/Unit2 \345\272\224\347\224\250/ERC721\351\235\236\345\220\214\350\264\250\345\214\226\344\273\243\345\270\201.md" @@ -0,0 +1,222 @@ +# ERC721 + +ERC721 是非同质化代币(NFT)的标准。与 ERC20 不同,每个 ERC721 代币都是独一无二的,具有自己的 `tokenId`。 + +```solidity +// SPDX-License-Identifier: MIT // SPDX 许可证标识符:MIT 许可证 +pragma solidity ^0.8.26; // 版本指令,要求使用 Solidity 0.8.26 及以上版本 + +// IERC165 接口:用于检测合约支持的接口 +interface IERC165 { // ERC165 标准接口 + function supportsInterface(bytes4 interfaceID) // 检查是否支持某个接口 + external + view + returns (bool); +} + +// IERC721 接口:ERC721 NFT 标准接口(继承 IERC165) +interface IERC721 is IERC165 { // 继承 IERC165 + function balanceOf(address owner) external view returns (uint256 balance); // 查询某地址拥有的 NFT 数量 + function ownerOf(uint256 tokenId) external view returns (address owner); // 查询某 tokenId 的拥有者 + function safeTransferFrom(address from, address to, uint256 tokenId) // 安全转移 + external; + function safeTransferFrom( // 带数据的安全转移 + address from, + address to, + uint256 tokenId, + bytes calldata data + ) external; + function transferFrom(address from, address to, uint256 tokenId) external; // 普通转移 + function approve(address to, uint256 tokenId) external; // 授权某人操作指定 NFT + function getApproved(uint256 tokenId) // 获取某 NFT 的授权地址 + external + view + returns (address operator); + function setApprovalForAll(address operator, bool _approved) external; // 批量授权 + function isApprovedForAll(address owner, address operator) // 检查批量授权 + external + view + returns (bool); +} + +// IERC721Receiver 接口:接收 ERC721 代币时必须实现的回调接口 +interface IERC721Receiver { // 安全转账的回调接口 + function onERC721Received( // 接收到 NFT 时被调用 + address operator, // 操作者地址 + address from, // 发送者地址 + uint256 tokenId, // 代币 ID + bytes calldata data // 附加数据 + ) external returns (bytes4); // 返回函数选择器以确认接收 +} + +// ERC721 合约:实现 ERC721 标准的 NFT 合约 +contract ERC721 is IERC721 { // 实现 IERC721 接口 + // Transfer 事件:NFT 转移时触发 + event Transfer( + address indexed from, address indexed to, uint256 indexed id + ); + // Approval 事件:单个 NFT 授权时触发 + event Approval( + address indexed owner, address indexed spender, uint256 indexed id + ); + // ApprovalForAll 事件:批量授权时触发 + event ApprovalForAll( + address indexed owner, address indexed operator, bool approved + ); + + // _ownerOf 映射:从 tokenId 到所有者地址的映射 + mapping(uint256 => address) internal _ownerOf; + + // _balanceOf 映射:从所有者地址到 NFT 数量的映射 + mapping(address => uint256) internal _balanceOf; + + // _approvals 映射:从 tokenId 到被授权地址的映射(单个 NFT 授权) + mapping(uint256 => address) internal _approvals; + + // isApprovedForAll 映射:从 owner 到 operator 的批量授权状态 + mapping(address => mapping(address => bool)) public isApprovedForAll; + + // supportsInterface 函数:实现 ERC165 接口检测 + function supportsInterface(bytes4 interfaceId) + external + pure + returns (bool) + { + // 检查是否匹配 IERC721 或 IERC165 的接口 ID + return interfaceId == type(IERC721).interfaceId + || interfaceId == type(IERC165).interfaceId; // type(X).interfaceId 返回接口的 4 字节选择器 + } + + // ownerOf 函数:查询某 tokenId 的拥有者 + function ownerOf(uint256 id) external view returns (address owner) { + owner = _ownerOf[id]; // 获取所有者 + require(owner != address(0), "token doesn't exist"); // 零地址表示不存在 + } + + // balanceOf 函数:查询某地址拥有的 NFT 数量 + function balanceOf(address owner) external view returns (uint256) { + require(owner != address(0), "owner = zero address"); // 零地址无效 + return _balanceOf[owner]; // 返回该地址的 NFT 数量 + } + + // setApprovalForAll 函数:设置批量授权(授权 operator 操作调用者的所有 NFT) + function setApprovalForAll(address operator, bool approved) external { + isApprovedForAll[msg.sender][operator] = approved; // 设置授权状态 + emit ApprovalForAll(msg.sender, operator, approved); // 触发事件 + } + + // approve 函数:授权 spender 操作指定的 NFT + function approve(address spender, uint256 id) external { + address owner = _ownerOf[id]; // 获取 NFT 的所有者 + require( + msg.sender == owner || isApprovedForAll[owner][msg.sender], // 调用者必须是所有者或已被批量授权 + "not authorized" + ); + + _approvals[id] = spender; // 设置单个 NFT 的授权 + emit Approval(owner, spender, id); // 触发 Approval 事件 + } + + // getApproved 函数:获取某 NFT 的授权地址 + function getApproved(uint256 id) external view returns (address) { + require(_ownerOf[id] != address(0), "token doesn't exist"); // 确保 NFT 存在 + return _approvals[id]; // 返回授权地址 + } + + // _isApprovedOrOwner 内部函数:检查 spender 是否为所有者或已授权 + function _isApprovedOrOwner(address owner, address spender, uint256 id) + internal + view + returns (bool) + { + return ( + spender == owner // 就是所有者本人 + || isApprovedForAll[owner][spender] // 已被批量授权 + || spender == _approvals[id] // 已被单独授权 + ); + } + + // transferFrom 函数:转移 NFT(需要授权) + function transferFrom(address from, address to, uint256 id) public { + require(from == _ownerOf[id], "from != owner"); // 发送者必须是所有者 + require(to != address(0), "transfer to zero address"); // 不能转移到零地址 + + require(_isApprovedOrOwner(from, msg.sender, id), "not authorized"); // 调用者需要被授权 + + _balanceOf[from]--; // 减少发送者的 NFT 数量 + _balanceOf[to]++; // 增加接收者的 NFT 数量 + _ownerOf[id] = to; // 更新 NFT 的所有者 + + delete _approvals[id]; // 清除旧的授权 + + emit Transfer(from, to, id); // 触发 Transfer 事件 + } + + // safeTransferFrom 函数:安全转移 NFT(无附加数据),检查接收者能否处理 NFT + function safeTransferFrom(address from, address to, uint256 id) external { + transferFrom(from, to, id); // 先执行普通转移 + + require( + to.code.length == 0 // 如果接收者是 EOA(外部账户),code 为空 + // 如果接收者是合约,检查是否实现了 onERC721Received 回调 + || IERC721Receiver(to).onERC721Received(msg.sender, from, id, "") + == IERC721Receiver.onERC721Received.selector, // 必须返回正确的选择器 + "unsafe recipient" // 接收者不能安全处理 NFT + ); + } + + // safeTransferFrom 函数:带附加数据的安全转移 + function safeTransferFrom( + address from, + address to, + uint256 id, + bytes calldata data // 附加数据,传递给接收者的回调函数 + ) external { + transferFrom(from, to, id); // 先执行普通转移 + + require( + to.code.length == 0 + || IERC721Receiver(to).onERC721Received(msg.sender, from, id, data) + == IERC721Receiver.onERC721Received.selector, + "unsafe recipient" + ); + } + + // _mint 内部函数:铸造新 NFT + function _mint(address to, uint256 id) internal { + require(to != address(0), "mint to zero address"); // 不能铸造到零地址 + require(_ownerOf[id] == address(0), "already minted"); // 该 ID 不能已被铸造 + + _balanceOf[to]++; // 增加接收者的 NFT 数量 + _ownerOf[id] = to; // 设置 NFT 所有者 + emit Transfer(address(0), to, id); // 从零地址转移表示铸造 + } + + // _burn 内部函数:销毁 NFT + function _burn(uint256 id) internal { + address owner = _ownerOf[id]; // 获取 NFT 所有者 + require(owner != address(0), "not minted"); // 确保 NFT 存在 + + _balanceOf[owner] -= 1; // 减少所有者的 NFT 数量 + + delete _ownerOf[id]; // 清除所有权记录 + delete _approvals[id]; // 清除授权记录 + + emit Transfer(owner, address(0), id); // 转移到零地址表示销毁 + } +} + +// MyNFT 合约:使用 ERC721 创建的简单 NFT 合约 +contract MyNFT is ERC721 { // 继承 ERC721 合约 + // mint 函数:公开的铸造接口 + function mint(address to, uint256 id) external { // 接收目标地址和 tokenId + _mint(to, id); // 调用内部 _mint 铸造 NFT + } + + // burn 函数:销毁自己的 NFT + function burn(uint256 id) external { + require(msg.sender == _ownerOf[id], "not owner"); // 只有所有者可以销毁 + _burn(id); // 调用内部 _burn 销毁 NFT + } +} +``` diff --git "a/Unit2 \345\272\224\347\224\250/\344\273\245\345\244\252\345\270\201\351\222\261\345\214\205.md" "b/Unit2 \345\272\224\347\224\250/\344\273\245\345\244\252\345\270\201\351\222\261\345\214\205.md" new file mode 100644 index 0000000..9797de4 --- /dev/null +++ "b/Unit2 \345\272\224\347\224\250/\344\273\245\345\244\252\345\270\201\351\222\261\345\214\205.md" @@ -0,0 +1,32 @@ +# 以太币钱包 + +一个简单的以太币钱包合约:任何人都可以向其发送 ETH,但只有合约所有者可以提取资金。 + +```solidity +// SPDX-License-Identifier: MIT // SPDX 许可证标识符:MIT 许可证 +pragma solidity ^0.8.26; // 版本指令,要求使用 Solidity 0.8.26 及以上版本 + +// EtherWallet 合约:一个简单的以太币钱包 +contract EtherWallet { // 定义 EtherWallet 合约 + address payable public owner; // 钱包所有者地址(payable 使其可接收以太币) + + // 构造函数:部署合约时,将部署者设为钱包所有者 + constructor() { + owner = payable(msg.sender); // 将部署合约的地址设置为所有者 + } + + // receive 函数:允许合约接收以太币转账 + receive() external payable {} // 当 msg.data 为空且收到以太币时自动调用 + + // withdraw 函数:仅所有者可以提取以太币 + function withdraw(uint256 _amount) external { // 接收提款金额 + require(msg.sender == owner, "caller is not owner"); // 确保调用者是所有者 + payable(msg.sender).transfer(_amount); // 使用 transfer 方法发送以太币(2300 gas) + } + + // getBalance 函数:查询合约当前余额 + function getBalance() external view returns (uint256) { // 返回合约的以太币余额 + return address(this).balance; // 返回本合约地址的余额 + } +} +``` diff --git "a/Unit2 \345\272\224\347\224\250/\345\206\231\345\205\245\344\273\273\346\204\217\345\255\230\345\202\250\346\247\275.md" "b/Unit2 \345\272\224\347\224\250/\345\206\231\345\205\245\344\273\273\346\204\217\345\255\230\345\202\250\346\247\275.md" new file mode 100644 index 0000000..e61f7e2 --- /dev/null +++ "b/Unit2 \345\272\224\347\224\250/\345\206\231\345\205\245\344\273\273\346\204\217\345\255\230\345\202\250\346\247\275.md" @@ -0,0 +1,47 @@ +# 写入任意存储槽 + +通过内联汇编,可以将数据直接写入任意指定的存储槽位,实现精确的存储位置控制。 + +```solidity +// SPDX-License-Identifier: MIT // SPDX 许可证标识符:MIT 许可证 +pragma solidity ^0.8.26; // 版本指令,要求使用 Solidity 0.8.26 及以上版本 + +// StorageSlot 库:用于在特定存储槽位读写数据的工具库 +library StorageSlot { // 定义 StorageSlot 库 + // 将 address 封装在结构体中,使其可以作为 storage 指针传递 + struct AddressSlot { // 地址槽结构体 + address value; // 地址值 + } + + // getAddressSlot 函数:通过内联汇编将结构体指针映射到指定存储槽位 + function getAddressSlot(bytes32 slot) // 接收存储槽位哈希 + internal + pure + returns (AddressSlot storage pointer) // 返回指向指定槽位的结构体引用 + { + assembly { // 使用内联汇编设置存储槽位 + // 将结构体的存储槽位设置为 slot + pointer.slot := slot // 直接修改 storage 指针的槽位 + } + } +} + +// TestSlot 合约:演示 StorageSlot 库的用法 +contract TestSlot { // 定义 TestSlot 合约 + bytes32 public constant TEST_SLOT = keccak256("TEST_SLOT"); // 计算测试槽位的哈希(EIP-1967 风格) + + // write 函数:将地址写入指定槽位 + function write(address _addr) external { // 接收要写入的地址 + StorageSlot.AddressSlot storage data = // 获取槽位引用 + StorageSlot.getAddressSlot(TEST_SLOT); // 指向 TEST_SLOT 槽位 + data.value = _addr; // 将地址写入该槽位 + } + + // get 函数:从指定槽位读取地址 + function get() external view returns (address) { // 返回存储的地址 + StorageSlot.AddressSlot storage data = // 获取槽位引用 + StorageSlot.getAddressSlot(TEST_SLOT); // 指向 TEST_SLOT 槽位 + return data.value; // 读取槽位中的地址值 + } +} +``` diff --git "a/Unit2 \345\272\224\347\224\250/\345\215\225\345\220\221\346\224\257\344\273\230\346\270\240\351\201\223.md" "b/Unit2 \345\272\224\347\224\250/\345\215\225\345\220\221\346\224\257\344\273\230\346\270\240\351\201\223.md" new file mode 100644 index 0000000..0a24e70 --- /dev/null +++ "b/Unit2 \345\272\224\347\224\250/\345\215\225\345\220\221\346\224\257\344\273\230\346\270\240\351\201\223.md" @@ -0,0 +1,273 @@ +# 单向支付渠道 + +单向支付渠道(Unidirectional Payment Channel)允许发送方向接收方进行多次链下支付,仅在最终结算时才上链。通过链下签名进行授权,接收方可以在任何时候(或在截止时间后)上链领取最终金额,剩余的 ETH 会退还给发送方。 + +本示例包含 ECDSA 签名验证库和完整的支付渠道合约。 + +```solidity +// SPDX-License-Identifier: MIT // SPDX 许可证标识符:MIT 许可证 +pragma solidity ^0.8.26; // 版本指令,要求使用 Solidity 0.8.26 及以上版本 + +// ===== ECDSA 签名验证库(OpenZeppelin 参考实现)===== + +// ECDSA 库:椭圆曲线数字签名算法库,用于签名验证 +library ECDSA { // 定义 ECDSA 库 + // 签名恢复错误枚举 + enum RecoverError { // 恢复错误的类型 + NoError, // 无错误 + InvalidSignature, // 无效签名 + InvalidSignatureLength, // 无效签名长度 + InvalidSignatureS, // 无效的 s 值 + InvalidSignatureV // 无效的 v 值 + } + + // _throwError 内部函数:将错误码转为 revert 消息 + function _throwError(RecoverError error) private pure { + if (error == RecoverError.NoError) { + return; // 无错误,直接返回 + } else if (error == RecoverError.InvalidSignature) { + revert("ECDSA: invalid signature"); + } else if (error == RecoverError.InvalidSignatureLength) { + revert("ECDSA: invalid signature length"); + } else if (error == RecoverError.InvalidSignatureS) { + revert("ECDSA: invalid signature 's' value"); + } else if (error == RecoverError.InvalidSignatureV) { + revert("ECDSA: invalid signature 'v' value"); + } + } + + // recover 函数(字符串签名版本):从签名恢复地址 + function recover(bytes32 hash, bytes memory signature) // 哈希和签名 + internal + pure + returns (address) // 返回签名者地址 + { + (address recovered, RecoverError error) = tryRecover(hash, signature); // 尝试恢复 + _throwError(error); // 如果有错误则回滚 + return recovered; // 返回恢复的地址 + } + + // tryRecover 函数:尝试从签名(65 或 64 字节)中恢复地址 + function tryRecover(bytes32 hash, bytes memory signature) // 哈希和签名 + internal + pure + returns (address, RecoverError) // 返回地址和错误码 + { + // 检查签名长度 + // 65 字节:r、s、v 签名(标准格式) + // 64 字节:r、vs 签名(EIP-2098 格式) + if (signature.length == 65) { // 标准 65 字节签名 + bytes32 r; + bytes32 s; + uint8 v; + assembly { // 从内存中提取 r、s、v + r := mload(add(signature, 0x20)) // 偏移 32 字节读 r + s := mload(add(signature, 0x40)) // 偏移 64 字节读 s + v := byte(0, mload(add(signature, 0x60))) // 偏移 96 字节读 v 的最低字节 + } + return tryRecover(hash, v, r, s); // 使用标准参数恢复 + } else if (signature.length == 64) { // EIP-2098 压缩签名 + bytes32 r; + bytes32 vs; + assembly { // 从内存中提取 r、vs + r := mload(add(signature, 0x20)) // 偏移 32 字节读 r + vs := mload(add(signature, 0x40)) // 偏移 64 字节读 vs + } + return tryRecover(hash, r, vs); // 使用压缩参数恢复 + } else { + return (address(0), RecoverError.InvalidSignatureLength); // 长度不合法 + } + } + + // tryRecover 函数(r、vs 版本):从压缩签名恢复地址 + function tryRecover(bytes32 hash, bytes32 r, bytes32 vs) // 哈希、r、vs + internal + pure + returns (address, RecoverError) // 返回地址和错误码 + { + // 从 vs 中提取 s(低 255 位)和 v(最高位 + 27) + bytes32 s = vs + & bytes32( + 0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff + ); // 清除最高位获得 s + uint8 v = uint8((uint256(vs) >> 255) + 27); // 从最高位提取 v(27 或 28) + return tryRecover(hash, v, r, s); // 使用标准参数恢复 + } + + // recover 函数(r、vs 版本):从压缩签名恢复的便利函数 + function recover(bytes32 hash, bytes32 r, bytes32 vs) // 哈希、r、vs + internal + pure + returns (address) // 返回签名者地址 + { + (address recovered, RecoverError error) = tryRecover(hash, r, vs); // 尝试恢复 + _throwError(error); // 如果有错误则回滚 + return recovered; + } + + // tryRecover 函数(v、r、s 版本):使用 ecrecover 恢复签名的核心逻辑 + function tryRecover(bytes32 hash, uint8 v, bytes32 r, bytes32 s) // 标准签名参数 + internal + pure + returns (address, RecoverError) // 返回地址和错误码 + { + // EIP-2 仍然允许 ecrecover() 的签名可塑性。移除此可能性使签名唯一。 + // 以太坊黄皮书附录 F 定义了 s 的有效范围和 v 的有效范围。 + if ( + uint256(s) + > 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0 + ) { + return (address(0), RecoverError.InvalidSignatureS); // s 超出有效范围 + } + if (v != 27 && v != 28) { + return (address(0), RecoverError.InvalidSignatureV); // v 不是 27 或 28 + } + + // 使用 ecrecover 预编译合约恢复签名者地址 + address signer = ecrecover(hash, v, r, s); // 椭圆曲线恢复 + if (signer == address(0)) { + return (address(0), RecoverError.InvalidSignature); // 恢复失败 + } + + return (signer, RecoverError.NoError); // 成功恢复 + } + + // recover 函数(v、r、s 版本):从签名恢复的便利函数 + function recover(bytes32 hash, uint8 v, bytes32 r, bytes32 s) // 标准签名参数 + internal + pure + returns (address) // 返回签名者地址 + { + (address recovered, RecoverError error) = tryRecover(hash, v, r, s); // 尝试恢复 + _throwError(error); // 如果有错误则回滚 + return recovered; + } + + // toEthSignedMessageHash 函数:为签名生成以太坊签名消息哈希 + function toEthSignedMessageHash(bytes32 hash) // 原始哈希 + internal + pure + returns (bytes32) // 返回带前缀的哈希 + { + // 32 是哈希的字节长度,由类型签名强制确保 + return keccak256( // 计算 keccak256 哈希 + abi.encodePacked("\x19Ethereum Signed Message:\n32", hash) // 以太坊签名消息前缀 + ); + } +} + +// ===== 单向支付渠道合约 ===== + +// ReentrancyGuard 合约:防重入锁 +contract ReentrancyGuard { // 定义 ReentrancyGuard 合约 + bool private locked; // 锁状态变量 + + // guard 修饰符:防止重入攻击 + modifier guard() { + require(!locked); // 检查锁状态 + locked = true; // 上锁 + _; // 执行函数逻辑 + locked = false; // 解锁 + } +} + +// UniDirectionalPaymentChannel 合约:单向支付渠道实现 +contract UniDirectionalPaymentChannel is ReentrancyGuard { // 继承防重入保护 + using ECDSA for bytes32; // 为 bytes32 类型附加 ECDSA 库方法 + + address payable public sender; // 支付发送方 + address payable public receiver; // 支付接收方 + + uint256 private constant DURATION = 7 * 24 * 60 * 60; // 渠道有效期限(7 天) + uint256 public expiresAt; // 渠道过期时间 + + // 构造函数:部署支付渠道 + constructor(address payable _receiver) payable { // 可接收 ETH + require(_receiver != address(0), "receiver = zero address"); // 接收方不能为零地址 + sender = payable(msg.sender); // 部署者即为发送方 + receiver = _receiver; // 设置接收方 + expiresAt = block.timestamp + DURATION; // 设置 7 天后过期 + } + + // _getHash 内部函数:计算金额的哈希(用于签名) + function _getHash(uint256 _amount) private view returns (bytes32) { + // 注意:合约地址也参与哈希,防止签名在其他合约上被重放 + return keccak256(abi.encodePacked(address(this), _amount)); // 合约地址 + 金额 + } + + // getHash 公开函数:供外部查询哈希 + function getHash(uint256 _amount) external view returns (bytes32) { + return _getHash(_amount); // 返回哈希 + } + + // _getEthSignedHash 内部函数:生成以太坊签名消息哈希 + function _getEthSignedHash(uint256 _amount) // 金额 + private + view + returns (bytes32) + { + return _getHash(_amount).toEthSignedMessageHash(); // 添加以太坊签名前缀 + } + + // getEthSignedHash 公开函数:供外部查询签名消息哈希 + function getEthSignedHash(uint256 _amount) // 金额 + external + view + returns (bytes32) + { + return _getEthSignedHash(_amount); + } + + // _verify 内部函数:验证签名 + function _verify(uint256 _amount, bytes memory _sig) // 金额和签名 + private + view + returns (bool) + { + return _getEthSignedHash(_amount).recover(_sig) == sender; // 从签名恢复的地址必须为发送方 + } + + // verify 公开函数:供外部验证签名是否有效 + function verify(uint256 _amount, bytes memory _sig) // 金额和签名 + external + view + returns (bool) + { + return _verify(_amount, _sig); + } + + // close 函数:接收方用签名关闭渠道,领取金额 + function close(uint256 _amount, bytes memory _sig) // 金额和发送方的签名 + external + guard // 防重入保护 + { + require(msg.sender == receiver, "!receiver"); // 只有接收方可关闭 + require(_verify(_amount, _sig), "invalid sig"); // 验证签名 + + // 将指定金额发送给接收方 + (bool sent,) = receiver.call{value: _amount}(""); // 发送 ETH 给接收方 + require(sent, "Failed to send Ether"); // 发送失败则回滚 + selfdestruct(sender); // 销毁合约,剩余 ETH 返还发送方 + } + + // cancel 函数:发送方在渠道过期后取消渠道,取回所有 ETH + function cancel() external { // 只有发送方可调用 + require(msg.sender == sender, "!sender"); // 只有发送方 + require(block.timestamp >= expiresAt, "!expired"); // 必须在过期后 + selfdestruct(sender); // 销毁合约,ETH 全部返还发送方 + } +} +``` + +## 工作流程 + +1. **部署渠道**:发送方部署 `UniDirectionalPaymentChannel`,传入接收方地址并附带一定 ETH +2. **链下支付**:发送方在链下使用 ECDSA 签名对(金额)进行签名,将签名发送给接收方 +3. **接收方上链结算**:接收方在渠道过期前调用 `close`,提供最终金额和发送方签名,领取 ETH +4. **发送方取回**:如果渠道过期且接收方未结算,发送方调用 `cancel` 取回所有 ETH + +## 关键安全要点 + +- 每次签名的金额应递增,接收方仅会选择最大的一笔来结算 +- 签名中包含合约地址,防止同一签名在其他合约上被重放 +- 使用防重入锁保护 `close` 函数 diff --git "a/Unit2 \345\272\224\347\224\250/\345\217\257\345\215\207\347\272\247\344\273\243\347\220\206.md" "b/Unit2 \345\272\224\347\224\250/\345\217\257\345\215\207\347\272\247\344\273\243\347\220\206.md" new file mode 100644 index 0000000..f6afa18 --- /dev/null +++ "b/Unit2 \345\272\224\347\224\250/\345\217\257\345\215\207\347\272\247\344\273\243\347\220\206.md" @@ -0,0 +1,298 @@ +# 可升级代理 + +可升级代理合约的示例。此代码仅供参考学习,请勿在生产环境中使用。 + +本示例展示了: +- 如何在 `fallback` 被调用时使用 `delegatecall` 并返回数据 +- 如何将 `admin` 和 `implementation` 地址存储在 EIP-1967 规定的特定存储槽位中 + +```solidity +// SPDX-License-Identifier: MIT // SPDX 许可证标识符:MIT 许可证 +pragma solidity ^0.8.26; // 版本指令,要求使用 Solidity 0.8.26 及以上版本 + +// ===== 逻辑合约(实现合约)===== + +// CounterV1 合约:第一版计数器逻辑 +contract CounterV1 { // 定义 CounterV1 合约 + uint256 public count; // 公共计数变量 + + // inc 函数:计数器加一 + function inc() external { + count += 1; // count = count + 1 + } +} + +// CounterV2 合约:第二版计数器逻辑(新增减一功能) +contract CounterV2 { // 定义 CounterV2 合约 + uint256 public count; // 公共计数变量(存储布局必须与 V1 一致) + + // inc 函数:计数器加一 + function inc() external { + count += 1; + } + + // dec 函数:计数器减一(V2 新增功能) + function dec() external { + count -= 1; // count = count - 1 + } +} + +// ===== 有 Bug 的代理合约(供对比学习)===== + +// BuggyProxy 合约:一个存在存储冲突问题的代理合约 +contract BuggyProxy { // 定义 BuggyProxy 合约 + address public implementation; // 插槽 0:实现合约地址 + address public admin; // 插槽 1:管理员地址 + + constructor() { // 构造函数 + admin = msg.sender; // 部署者成为管理员 + } + + // _delegate 私有函数:将调用委托给实现合约 + function _delegate() private { + (bool ok,) = implementation.delegatecall(msg.data); // 使用 delegatecall 转发调用 + require(ok, "delegatecall failed"); // 如果调用失败则回滚 + } + + // fallback 函数:所有未匹配的函数调用和以太币发送都进入这里 + fallback() external payable { + _delegate(); // 委托给实现合约 + } + + receive() external payable { // 接收纯以太币转账 + _delegate(); + } + + // upgradeTo 函数:升级实现合约 + function upgradeTo(address _implementation) external { + require(msg.sender == admin, "not authorized"); // 只有管理员可以升级 + implementation = _implementation; // 更新实现合约地址 + } + // BuggyProxy 的问题: + // 当调用 upgradeTo 时,代理合约的 implementation 被更新 + // 但如果 CounterV1 的 inc() 被 delegatecall 调用, + // 它会使用代理合约的存储,修改插槽 0 的值(即 implementation 地址) + // storage 冲突 → 操作失败! +} + +// Dev 合约:用于查询代理合约的函数选择器 +contract Dev { // 定义 Dev 合约 + function selectors() external view returns (bytes4, bytes4, bytes4) { + return ( + Proxy.admin.selector, // admin() 的函数选择器 + Proxy.implementation.selector, // implementation() 的函数选择器 + Proxy.upgradeTo.selector // upgradeTo() 的函数选择器 + ); + } +} + +// ===== 正确的透明可升级代理合约 ===== + +// Proxy 合约:使用 EIP-1967 存储槽位避免冲突的透明代理 +contract Proxy { // 定义 Proxy 合约 + // 所有函数和变量都应是私有的,将所有调用转发到 fallback + + // 实现合约存储槽位(EIP-1967 标准) + // keccak256("eip1967.proxy.implementation") - 1 + // 结果:0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc + bytes32 private constant IMPLEMENTATION_SLOT = + bytes32(uint256(keccak256("eip1967.proxy.implementation")) - 1); + + // 管理员存储槽位(EIP-1967 标准) + // keccak256("eip1967.proxy.admin") - 1 + // 结果:0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103 + bytes32 private constant ADMIN_SLOT = + bytes32(uint256(keccak256("eip1967.proxy.admin")) - 1); + + // 注意:使用随机存储槽位(EIP-1967)而非固定插槽 0,1, + // 避免了与逻辑合约的存储变量冲突 + + constructor() { // 构造函数 + _setAdmin(msg.sender); // 设置部署者为管理员 + } + + // ifAdmin 修饰符:如果是管理员则正常执行,否则走 fallback 委托 + modifier ifAdmin() { + if (msg.sender == _getAdmin()) { // 如果调用者是管理员 + _; // 执行代理自身的逻辑 + } else { + _fallback(); // 否则委托给实现合约 + } + } + + // _getAdmin 私有函数:从 EIP-1967 槽位读取管理员地址 + function _getAdmin() private view returns (address) { + return StorageSlot.getAddressSlot(ADMIN_SLOT).value; // 从指定槽位读取地址 + } + + // _setAdmin 私有函数:将管理员地址写入 EIP-1967 槽位 + function _setAdmin(address _admin) private { + require(_admin != address(0), "admin = zero address"); // 不能为零地址 + StorageSlot.getAddressSlot(ADMIN_SLOT).value = _admin; // 写入槽位 + } + + // _getImplementation 私有函数:从 EIP-1967 槽位读取实现合约地址 + function _getImplementation() private view returns (address) { + return StorageSlot.getAddressSlot(IMPLEMENTATION_SLOT).value; // 从指定槽位读取 + } + + // _setImplementation 私有函数:写入实现合约地址 + function _setImplementation(address _implementation) private { + require( + _implementation.code.length > 0, "implementation is not a contract" // 必须是有代码的合约 + ); + StorageSlot.getAddressSlot(IMPLEMENTATION_SLOT).value = _implementation; // 写入槽位 + } + + // ===== 管理员接口 ===== + + // changeAdmin 函数:更改管理员 + function changeAdmin(address _admin) external ifAdmin { // 仅管理员可调用 + _setAdmin(_admin); + } + + // upgradeTo 函数:升级实现合约 + function upgradeTo(address _implementation) external ifAdmin { // 仅管理员可调用 + _setImplementation(_implementation); + } + + // admin 函数:查询管理员地址 + function admin() external ifAdmin returns (address) { + return _getAdmin(); + } + + // implementation 函数:查询实现合约地址 + function implementation() external ifAdmin returns (address) { + return _getImplementation(); + } + + // ===== 用户接口 ===== + + // _delegate 内部函数:将调用委托给实现合约(使用内联汇编实现) + function _delegate(address _implementation) internal virtual { + assembly { // 使用内联汇编完全控制内存和调用 + // 将 msg.data 复制到内存位置 0 + // calldatacopy(to, from, size) - 将 calldata 复制到内存 + calldatacopy(0, 0, calldatasize()) // 复制全部 calldata 到内存偏移 0 + + // 调用实现合约 + // delegatecall(gas, address, in_offset, in_size, out_offset, out_size) + // - 返回 0 表示错误,返回 1 表示成功 + let result := + delegatecall(gas(), _implementation, 0, calldatasize(), 0, 0) // 无输出缓冲区 + + // 将返回数据复制到内存 + returndatacopy(0, 0, returndatasize()) // 复制返回数据到内存偏移 0 + + switch result // 根据 delegatecall 结果进行分支 + case 0 { // delegatecall 返回 0 = 失败 + // revert(p, s) - 回滚状态变更,返回数据 + revert(0, returndatasize()) // 回滚整笔交易 + } + default { // delegatecall 返回 1 = 成功 + // return(p, s) - 结束执行,返回数据 + return(0, returndatasize()) // 返回数据给调用者 + } + } + } + + // _fallback 私有函数:委托调用给实现合约 + function _fallback() private { + _delegate(_getImplementation()); // 委托给当前实现合约 + } + + // fallback 函数:接收所有未匹配的函数调用 + fallback() external payable { + _fallback(); // 走 fallback 委托流程 + } + + // receive 函数:接收纯以太币转账 + receive() external payable { + _fallback(); // 同样走 fallback 委托 + } +} + +// ProxyAdmin 合约:代理管理合约,管理 Proxy 的管理员和升级操作 +contract ProxyAdmin { // 定义 ProxyAdmin 合约 + address public owner; // ProxyAdmin 的所有者 + + constructor() { // 构造函数 + owner = msg.sender; // 部署者成为所有者 + } + + // onlyOwner 修饰符:仅所有者可调用 + modifier onlyOwner() { + require(msg.sender == owner, "not owner"); + _; + } + + // getProxyAdmin 函数:通过静态调用查询代理合约的管理员 + function getProxyAdmin(address proxy) external view returns (address) { + (bool ok, bytes memory res) = + proxy.staticcall(abi.encodeCall(Proxy.admin, ())); // 静态调用 proxy.admin() + require(ok, "call failed"); + return abi.decode(res, (address)); // 解码返回的管理员地址 + } + + // getProxyImplementation 函数:查询代理合约的实现合约 + function getProxyImplementation(address proxy) + external + view + returns (address) + { + (bool ok, bytes memory res) = + proxy.staticcall(abi.encodeCall(Proxy.implementation, ())); // 静态调用 proxy.implementation() + require(ok, "call failed"); + return abi.decode(res, (address)); // 解码返回的地址 + } + + // changeProxyAdmin 函数:更改代理合约的管理员 + function changeProxyAdmin(address payable proxy, address admin) + external + onlyOwner + { + Proxy(proxy).changeAdmin(admin); // 调用 Proxy 的 changeAdmin + } + + // upgrade 函数:升级代理合约的实现 + function upgrade(address payable proxy, address implementation) + external + onlyOwner + { + Proxy(proxy).upgradeTo(implementation); // 调用 Proxy 的 upgradeTo + } +} + +// StorageSlot 库:用于在特定存储槽位读写数据 +library StorageSlot { // 定义 StorageSlot 库 + struct AddressSlot { // 地址槽结构体 + address value; // 地址值 + } + + // getAddressSlot 函数:使用汇编将结构体指针映射到特定槽位 + function getAddressSlot(bytes32 slot) + internal + pure + returns (AddressSlot storage r) // 返回指向指定槽位的结构体引用 + { + assembly { + r.slot := slot // 将结构体的存储槽位设置为指定槽位 + } + } +} + +// TestSlot 合约:演示 StorageSlot 库的用法 +contract TestSlot { // 定义 TestSlot 合约 + bytes32 public constant slot = keccak256("TEST_SLOT"); // 计算测试槽位哈希 + + // getSlot 函数:从指定槽位读取地址值 + function getSlot() external view returns (address) { + return StorageSlot.getAddressSlot(slot).value; // 从 slot 槽位读取 + } + + // writeSlot 函数:将地址值写入指定槽位 + function writeSlot(address _addr) external { + StorageSlot.getAddressSlot(slot).value = _addr; // 写入 slot 槽位 + } +} +``` diff --git "a/Unit2 \345\272\224\347\224\250/\345\217\257\350\277\255\344\273\243\346\230\240\345\260\204.md" "b/Unit2 \345\272\224\347\224\250/\345\217\257\350\277\255\344\273\243\346\230\240\345\260\204.md" new file mode 100644 index 0000000..2fa7f15 --- /dev/null +++ "b/Unit2 \345\272\224\347\224\250/\345\217\257\350\277\255\344\273\243\346\230\240\345\260\204.md" @@ -0,0 +1,103 @@ +# 可迭代映射 + +Solidity 中的 `mapping` 默认无法遍历。本示例展示如何构建一个可迭代的映射——通过维护一个密钥数组和辅助映射来实现遍历,同时保持 O(1) 的查找效率。 + +```solidity +// SPDX-License-Identifier: MIT // SPDX 许可证标识符:MIT 许可证 +pragma solidity ^0.8.26; // 版本指令,要求使用 Solidity 0.8.26 及以上版本 + +// IterableMapping 库:实现从 address 到 uint256 的可迭代映射 +library IterableMapping { // library 关键字定义库 + // Map 结构体:可迭代映射的核心数据结构 + struct Map { // 包含键数组和多个辅助映射 + address[] keys; // 存储所有键的数组(用于迭代) + mapping(address => uint256) values; // 键到值的映射 + mapping(address => uint256) indexOf; // 键到数组索引的映射 + mapping(address => bool) inserted; // 键是否已存在的标记 + } + + // get 函数:根据键获取值 + function get(Map storage map, address key) public view returns (uint256) { + return map.values[key]; // 返回键对应的值 + } + + // getKeyAtIndex 函数:根据数组索引获取键 + function getKeyAtIndex(Map storage map, uint256 index) + public + view + returns (address) // 返回该索引处的地址键 + { + return map.keys[index]; // 从 keys 数组中按索引取值 + } + + // size 函数:获取映射中键值对的数量 + function size(Map storage map) public view returns (uint256) { + return map.keys.length; // 返回 keys 数组的长度 + } + + // set 函数:设置键值对(如果键已存在则更新,不存在则新增) + function set(Map storage map, address key, uint256 val) public { // 接收键和值 + if (map.inserted[key]) { // 如果键已存在 + map.values[key] = val; // 直接更新值 + } else { // 如果键不存在 + map.inserted[key] = true; // 标记为已插入 + map.values[key] = val; // 存储值 + map.indexOf[key] = map.keys.length; // 记录键在数组中的索引 + map.keys.push(key); // 将键添加到 keys 数组末尾 + } + } + + // remove 函数:删除键值对(使用交换删除法保持数组紧凑) + function remove(Map storage map, address key) public { // 接收要删除的键 + if (!map.inserted[key]) { // 如果键不存在 + return; // 直接返回,不做任何操作 + } + + delete map.inserted[key]; // 清除插入标记 + delete map.values[key]; // 清除值 + + // 交换删除法:用最后一个元素覆盖要删除的元素,然后 pop + uint256 index = map.indexOf[key]; // 获取要删除键在数组中的索引 + address lastKey = map.keys[map.keys.length - 1]; // 获取数组中最后一个键 + + map.indexOf[lastKey] = index; // 更新最后一个键的索引为被删除的位置 + delete map.indexOf[key]; // 清除被删除键的索引记录 + + map.keys[index] = lastKey; // 用最后一个键覆盖要删除的位置 + map.keys.pop(); // 删除数组最后一个元素 + } +} + +// TestIterableMap 合约:测试可迭代映射的使用 +contract TestIterableMap { // 定义测试合约 + // using for 语法:将 IterableMapping 库附加到 Map 类型 + using IterableMapping for IterableMapping.Map; + + IterableMapping.Map private map; // 声明私有 Map 变量 + + // setInMapping 函数:调用者在映射中设置自己的值 + function setInMapping(uint256 val) public { // 接收要设置的值 + map.set(msg.sender, val); // 以 msg.sender 为键设置值 + } + + // getFromMap 函数:调用者获取自己的值 + function getFromMap() public view returns (uint256) { + return map.get(msg.sender); // 返回 msg.sender 对应的值 + } + + // getKeyAtIndex 函数:根据索引获取键 + function getKeyAtIndex(uint256 index) public view returns (address) { + return map.getKeyAtIndex(index); // 返回指定索引处的地址 + } + + // sizeOfMapping 函数:获取映射的大小 + function sizeOfMapping() public view returns (uint256) { + return map.size(); // 返回映射中的键值对数量 + } + + // removeFromMapping 函数:调用者从映射中删除自己的条目 + function removeFromMapping() public { // 删除 msg.sender 对应的条目 + map.remove(msg.sender); // 从映射中移除 + } +} +``` diff --git "a/Unit2 \345\272\224\347\224\250/\345\244\232\347\255\276\351\222\261\345\214\205.md" "b/Unit2 \345\272\224\347\224\250/\345\244\232\347\255\276\351\222\261\345\214\205.md" new file mode 100644 index 0000000..2f4f067 --- /dev/null +++ "b/Unit2 \345\272\224\347\224\250/\345\244\232\347\255\276\351\222\261\345\214\205.md" @@ -0,0 +1,236 @@ +# 多签钱包 + +多签钱包允许多个所有者共同管理资金。所有者可以: +- 提交交易 +- 批准和撤销对待处理交易的批准 +- 在足够多的所有者批准后,任何人都可以执行交易 + +```solidity +// SPDX-License-Identifier: MIT // SPDX 许可证标识符:MIT 许可证 +pragma solidity ^0.8.26; // 版本指令,要求使用 Solidity 0.8.26 及以上版本 + +// MultiSigWallet 合约:多签钱包,需要多个所有者确认才能执行交易 +contract MultiSigWallet { // 定义 MultiSigWallet 合约 + // Deposit 事件:收到存款时触发 + event Deposit(address indexed sender, uint256 amount, uint256 balance); + // SubmitTransaction 事件:交易提交时触发 + event SubmitTransaction( + address indexed owner, // 提交交易的所有者地址(已索引) + uint256 indexed txIndex, // 交易索引(已索引) + address indexed to, // 交易目标地址(已索引) + uint256 value, // 发送的以太币数量 + bytes data // 交易附带的数据 + ); + // ConfirmTransaction 事件:确认交易时触发 + event ConfirmTransaction(address indexed owner, uint256 indexed txIndex); + // RevokeConfirmation 事件:撤销确认时触发 + event RevokeConfirmation(address indexed owner, uint256 indexed txIndex); + // ExecuteTransaction 事件:交易执行时触发 + event ExecuteTransaction(address indexed owner, uint256 indexed txIndex); + + address[] public owners; // 所有者地址数组 + mapping(address => bool) public isOwner; // 快速检查地址是否为所有者 + uint256 public numConfirmationsRequired; // 执行交易所需的最小确认数 + + // Transaction 结构体:定义交易的数据结构 + struct Transaction { // 存储每个交易的详细信息 + address to; // 目标地址 + uint256 value; // 发送的以太币数量 + bytes data; // 交易附带的数据(用于调用合约函数) + bool executed; // 交易是否已执行 + uint256 numConfirmations; // 当前确认数 + } + + // isConfirmed 嵌套映射:记录某笔交易的某个所有者是否已确认 + // 映射结构:交易索引 => 所有者地址 => 是否已确认 + mapping(uint256 => mapping(address => bool)) public isConfirmed; + + Transaction[] public transactions; // 所有交易的数组 + + // onlyOwner 修饰符:确保只有所有者可以调用 + modifier onlyOwner() { + require(isOwner[msg.sender], "not owner"); // 检查调用者是否为所有者 + _; + } + + // txExists 修饰符:确保交易存在 + modifier txExists(uint256 _txIndex) { // 接收交易索引 + require(_txIndex < transactions.length, "tx does not exist"); // 检查索引是否有效 + _; + } + + // notExecuted 修饰符:确保交易尚未执行 + modifier notExecuted(uint256 _txIndex) { + require(!transactions[_txIndex].executed, "tx already executed"); // 防止重复执行 + _; + } + + // notConfirmed 修饰符:确保当前调用者尚未确认该交易 + modifier notConfirmed(uint256 _txIndex) { + require(!isConfirmed[_txIndex][msg.sender], "tx already confirmed"); // 防止重复确认 + _; + } + + // 构造函数:设置所有者列表和所需确认数 + constructor(address[] memory _owners, uint256 _numConfirmationsRequired) { + require(_owners.length > 0, "owners required"); // 至少需要一个所有者 + require( + _numConfirmationsRequired > 0 + && _numConfirmationsRequired <= _owners.length, // 确认数要在 1 到所有者数之间 + "invalid number of required confirmations" + ); + + // 遍历所有者列表,初始化所有者数据 + for (uint256 i = 0; i < _owners.length; i++) { // 逐个处理所有者地址 + address owner = _owners[i]; + + require(owner != address(0), "invalid owner"); // 不允许零地址 + require(!isOwner[owner], "owner not unique"); // 不允许重复所有者 + + isOwner[owner] = true; // 标记为所有者 + owners.push(owner); // 加入所有者列表 + } + + numConfirmationsRequired = _numConfirmationsRequired; // 设置所需确认数 + } + + // receive 函数:接收以太币存款 + receive() external payable { + emit Deposit(msg.sender, msg.value, address(this).balance); // 记录存款事件 + } + + // submitTransaction 函数:提交新的待处理交易 + function submitTransaction(address _to, uint256 _value, bytes memory _data) + public + onlyOwner // 只有所有者可以提交交易 + { + uint256 txIndex = transactions.length; // 新交易的索引 + + transactions.push( // 将新交易添加到数组 + Transaction({ + to: _to, // 目标地址 + value: _value, // 以太币数量 + data: _data, // 调用数据 + executed: false, // 初始状态:未执行 + numConfirmations: 0 // 初始确认数为 0 + }) + ); + + emit SubmitTransaction(msg.sender, txIndex, _to, _value, _data); // 记录交易提交事件 + } + + // confirmTransaction 函数:确认一笔交易 + function confirmTransaction(uint256 _txIndex) + public + onlyOwner // 只有所有者可以确认 + txExists(_txIndex) // 交易必须存在 + notExecuted(_txIndex) // 交易不能已执行 + notConfirmed(_txIndex) // 当前调用者不能已确认 + { + Transaction storage transaction = transactions[_txIndex]; // 获取交易引用(storage) + transaction.numConfirmations += 1; // 确认数加 1 + isConfirmed[_txIndex][msg.sender] = true; // 标记当前所有者已确认 + + emit ConfirmTransaction(msg.sender, _txIndex); // 记录确认事件 + } + + // executeTransaction 函数:执行一笔已获得足够确认的交易 + function executeTransaction(uint256 _txIndex) + public + onlyOwner // 只有所有者可以执行 + txExists(_txIndex) // 交易必须存在 + notExecuted(_txIndex) // 交易不能已执行 + { + Transaction storage transaction = transactions[_txIndex]; // 获取交易引用 + + require( + transaction.numConfirmations >= numConfirmationsRequired, // 确认数必须达标 + "cannot execute tx" + ); + + transaction.executed = true; // 先标记为已执行(防止重入攻击) + + // 使用低级 call 执行交易 + (bool success,) = + transaction.to.call{value: transaction.value}(transaction.data); // 发送以太币并附带数据 + require(success, "tx failed"); // 执行失败则回滚 + + emit ExecuteTransaction(msg.sender, _txIndex); // 记录执行事件 + } + + // revokeConfirmation 函数:撤销对交易的确认 + function revokeConfirmation(uint256 _txIndex) + public + onlyOwner // 只有所有者可以撤销 + txExists(_txIndex) // 交易必须存在 + notExecuted(_txIndex) // 交易不能已执行 + { + Transaction storage transaction = transactions[_txIndex]; // 获取交易引用 + + require(isConfirmed[_txIndex][msg.sender], "tx not confirmed"); // 必须已确认才能撤销 + + transaction.numConfirmations -= 1; // 确认数减 1 + isConfirmed[_txIndex][msg.sender] = false; // 清除确认标记 + + emit RevokeConfirmation(msg.sender, _txIndex); // 记录撤销事件 + } + + // getOwners 查询函数:获取所有所有者列表 + function getOwners() public view returns (address[] memory) { + return owners; // 返回所有者数组 + } + + // getTransactionCount 查询函数:获取交易总数 + function getTransactionCount() public view returns (uint256) { + return transactions.length; // 返回交易数组长度 + } + + // getTransaction 查询函数:获取指定索引的交易详情 + function getTransaction(uint256 _txIndex) + public + view + returns ( + address to, // 目标地址 + uint256 value, // 以太币数量 + bytes memory data, // 调用数据 + bool executed, // 是否已执行 + uint256 numConfirmations // 当前确认数 + ) + { + Transaction storage transaction = transactions[_txIndex]; // 获取指定交易 + + return ( + transaction.to, + transaction.value, + transaction.data, + transaction.executed, + transaction.numConfirmations // 返回交易的全部信息 + ); + } +} +``` + +## 测试合约 + +以下是一个简单的测试合约,用于演示通过多签钱包调用其他合约函数: + +```solidity +// SPDX-License-Identifier: MIT // SPDX 许可证标识符:MIT 许可证 +pragma solidity ^0.8.26; // 版本指令,要求使用 Solidity 0.8.26 及以上版本 + +// TestContract 合约:用于测试多签钱包的调用功能 +contract TestContract { // 定义 TestContract 测试合约 + uint256 public i; // 公共状态变量 i + + // callMe 函数:接收一个参数并累加到 i + function callMe(uint256 j) public { // 接收 uint256 参数 + i += j; // 将 j 加到 i 上 + } + + // getData 函数:返回调用 callMe(uint256) 的 ABI 编码调用数据 + function getData() public pure returns (bytes memory) { // 返回编码后的 bytes + // 使用 abi.encodeWithSignature 编码函数调用 + return abi.encodeWithSignature("callMe(uint256)", 123); // 编码 callMe(123) 的调用数据 + } +} +``` diff --git "a/Unit2 \345\272\224\347\224\250/\346\227\240Gas\344\273\243\345\270\201\350\275\254\350\264\246.md" "b/Unit2 \345\272\224\347\224\250/\346\227\240Gas\344\273\243\345\270\201\350\275\254\350\264\246.md" new file mode 100644 index 0000000..92dad92 --- /dev/null +++ "b/Unit2 \345\272\224\347\224\250/\346\227\240Gas\344\273\243\345\270\201\350\275\254\350\264\246.md" @@ -0,0 +1,245 @@ +# 无Gas代币转账 + +使用元交易(Meta Transaction)实现免 Gas 的 ERC20 代币转账。通过 `permit` 签名授权,由第三方代为支付 Gas 费用。 + +```solidity +// SPDX-License-Identifier: MIT // SPDX 许可证标识符:MIT 许可证 +pragma solidity ^0.8.26; // 版本指令,要求使用 Solidity 0.8.26 及以上版本 + +// IERC20Permit 接口:支持 permit 签名的 ERC20 接口 +interface IERC20Permit { // 定义支持 permit 的 ERC20 接口 + function totalSupply() external view returns (uint256); + function balanceOf(address account) external view returns (uint256); + function transfer(address recipient, uint256 amount) + external + returns (bool); + function allowance(address owner, address spender) + external + view + returns (uint256); + function approve(address spender, uint256 amount) external returns (bool); + function transferFrom(address sender, address recipient, uint256 amount) + external + returns (bool); + // permit 函数:通过链下签名授权 transferFrom,无需用户支付 Gas + function permit( + address owner, // 代币所有者地址 + address spender, // 被授权者地址 + uint256 value, // 授权金额 + uint256 deadline, // 截止时间(Unix 时间戳) + uint8 v, // 签名恢复标识符 + bytes32 r, // 签名的 r 部分 + bytes32 s // 签名的 s 部分 + ) external; +} + +// GaslessTokenTransfer 合约:无 Gas 代币转账的中继合约 +contract GaslessTokenTransfer { // 定义 GaslessTokenTransfer 合约 + // send 函数:执行无 Gas 代币转账 + // 流程: + // 1. 用户链下签名 permit 消息 + // 2. 中继者(支付 Gas 的人)调用此函数 + // 3. 合约先执行 permit 授权,再执行转账 + function send( + address token, // 代币合约地址 + address sender, // 代币发送者(签名的用户) + address receiver, // 代币接收者 + uint256 amount, // 转账金额 + uint256 fee, // 给中继者的费用 + uint256 deadline, // 签名截止时间 + // Permit 签名参数(由发送者链下生成) + uint8 v, // 签名 v 值 + bytes32 r, // 签名 r 值 + bytes32 s // 签名 s 值 + ) external { + // 第一步:执行 Permit + // 授权本合约从 sender 账户中转移 amount + fee 数量的代币 + IERC20Permit(token).permit( + sender, address(this), amount + fee, deadline, v, r, s // 通过签名完成链上授权 + ); + // 第二步:将 amount 数量的代币从 sender 转给 receiver + IERC20Permit(token).transferFrom(sender, receiver, amount); // 转账给接收者 + // 第三步:将 fee 数量的代币从 sender 转给 msg.sender(中继者费用) + IERC20Permit(token).transferFrom(sender, msg.sender, fee); // 支付中继费用 + } +} +``` + +## 带 Permit 的 ERC20 合约 + +以下是一个实现了 `permit`(EIP-2612)的现代高效 ERC20 合约,参考自 Solmate: + +```solidity +// SPDX-License-Identifier: AGPL-3.0-only // AGPL 许可证 +pragma solidity >=0.8.0; // 版本指令(为了兼容性使用了 >=) + +// 参考来源: +// @notice 现代且 Gas 高效的 ERC20 + EIP-2612 实现 +// @author Solmate (https://github.com/transmissions11/solmate/blob/main/src/tokens/ERC20.sol) +// @author 修改自 Uniswap (https://github.com/Uniswap/uniswap-v2-core/blob/master/contracts/UniswapV2ERC20.sol) +abstract contract ERC20 { // 抽象合约,需要被子合约继承使用 + event Transfer(address indexed from, address indexed to, uint256 amount); + event Approval( + address indexed owner, address indexed spender, uint256 amount + ); + + string public name; // 代币名称 + string public symbol; // 代币符号 + uint8 public immutable decimals; // 小数位数(不可变) + uint256 public totalSupply; // 总供应量 + mapping(address => uint256) public balanceOf; // 余额映射 + mapping(address => mapping(address => uint256)) public allowance; // 授权额度映射 + uint256 internal immutable INITIAL_CHAIN_ID; // 部署时的链 ID + bytes32 internal immutable INITIAL_DOMAIN_SEPARATOR; // 初始域分隔符 + mapping(address => uint256) public nonces; // 每个地址的 nonce(用于 permit 防重放) + + constructor(string memory _name, string memory _symbol, uint8 _decimals) { + name = _name; + symbol = _symbol; + decimals = _decimals; + INITIAL_CHAIN_ID = block.chainid; // 记录部署时的链 ID + INITIAL_DOMAIN_SEPARATOR = computeDomainSeparator(); // 计算域分隔符并缓存 + } + + function approve(address spender, uint256 amount) + public + virtual + returns (bool) + { + allowance[msg.sender][spender] = amount; + emit Approval(msg.sender, spender, amount); + return true; + } + + function transfer(address to, uint256 amount) + public + virtual + returns (bool) + { + balanceOf[msg.sender] -= amount; + unchecked { // unchecked 节省 gas + balanceOf[to] += amount; + } + emit Transfer(msg.sender, to, amount); + return true; + } + + function transferFrom(address from, address to, uint256 amount) + public + virtual + returns (bool) + { + uint256 allowed = allowance[from][msg.sender]; // 获取授权额度 + if (allowed != type(uint256).max) { // 如果不是无限授权(节省存储写入 gas) + allowance[from][msg.sender] = allowed - amount; // 扣除额度 + } + balanceOf[from] -= amount; + unchecked { + balanceOf[to] += amount; + } + emit Transfer(from, to, amount); + return true; + } + + // permit 函数:EIP-2612 签名授权 + // 用户通过链下签名 approve 消息,避免了两次交易(先 approve 再 transferFrom) + function permit( + address owner, + address spender, + uint256 value, + uint256 deadline, + uint8 v, + bytes32 r, + bytes32 s + ) public virtual { + require(deadline >= block.timestamp, "PERMIT_DEADLINE_EXPIRED"); // 检查截止时间 + + unchecked { // 节省 nonce 递增的溢出检查 gas + // 使用 ecrecover 从签名恢复地址 + address recoveredAddress = ecrecover( + keccak256( + abi.encodePacked( + "\x19\x01", // EIP-712 前缀 + DOMAIN_SEPARATOR(), // 域分隔符 + keccak256( // Permit 类型哈希 + abi.encode( + keccak256( + "Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)" + ), + owner, // 所有者地址 + spender, // 被授权者地址 + value, // 授权金额 + nonces[owner]++, // current nonce,使用后递增 + deadline // 截止时间 + ) + ) + ) + ), + v, // 签名 v + r, // 签名 r + s // 签名 s + ); + + require( + recoveredAddress != address(0) && recoveredAddress == owner, // 恢复的地址必须与 owner 一致 + "INVALID_SIGNER" + ); + + allowance[recoveredAddress][spender] = value; // 设置授权额度 + } + + emit Approval(owner, spender, value); // 触发 Approval 事件 + } + + // DOMAIN_SEPARATOR 函数:返回 EIP-712 域分隔符 + function DOMAIN_SEPARATOR() public view virtual returns (bytes32) { + // 如果链 ID 没有变化,返回缓存的域分隔符(节省 gas) + return block.chainid == INITIAL_CHAIN_ID + ? INITIAL_DOMAIN_SEPARATOR + : computeDomainSeparator(); // 链 ID 变化时重新计算 + } + + // computeDomainSeparator 内部函数:计算 EIP-712 域分隔符 + function computeDomainSeparator() internal view virtual returns (bytes32) { + return keccak256( + abi.encode( + keccak256( + "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)" + ), + keccak256(bytes(name)), // 代币名称哈希 + keccak256("1"), // 版本号 + block.chainid, // 当前链 ID + address(this) // 本合约地址(验证合约) + ) + ); + } + + function _mint(address to, uint256 amount) internal virtual { + totalSupply += amount; + unchecked { + balanceOf[to] += amount; + } + emit Transfer(address(0), to, amount); + } + + function _burn(address from, uint256 amount) internal virtual { + balanceOf[from] -= amount; + unchecked { + totalSupply -= amount; + } + emit Transfer(from, address(0), amount); + } +} + +// ERC20Permit 合约:继承 ERC20 的可铸造代币 +contract ERC20Permit is ERC20 { // 继承 ERC20 抽象合约 + constructor(string memory _name, string memory _symbol, uint8 _decimals) + ERC20(_name, _symbol, _decimals) // 调用父合约构造函数 + {} + + // mint 函数:铸造新代币 + function mint(address to, uint256 amount) public { + _mint(to, amount); // 调用内部 _mint 函数 + } +} +``` diff --git "a/Unit2 \345\272\224\347\224\250/\346\234\200\345\260\217\344\273\243\347\220\206\345\220\210\347\272\246.md" "b/Unit2 \345\272\224\347\224\250/\346\234\200\345\260\217\344\273\243\347\220\206\345\220\210\347\272\246.md" new file mode 100644 index 0000000..5cf1772 --- /dev/null +++ "b/Unit2 \345\272\224\347\224\250/\346\234\200\345\260\217\344\273\243\347\220\206\345\220\210\347\272\246.md" @@ -0,0 +1,69 @@ +# 最小代理合约 + +如果你有一个需要多次部署的合约,可以使用最小代理合约(Minimal Proxy,也称 EIP-1167 Clone)来廉价地部署它们。 + +最小代理合约本身是一段固定的字节码,它将所有调用通过 `delegatecall` 转发到实现合约(目标合约)。 + +```solidity +// SPDX-License-Identifier: MIT // SPDX 许可证标识符:MIT 许可证 +pragma solidity ^0.8.26; // 版本指令,要求使用 Solidity 0.8.26 及以上版本 + +// 原始代码参考: +// https://github.com/optionality/clone-factory/blob/master/contracts/CloneFactory.sol + +// MinimalProxy 合约:通过字节码克隆部署最小代理合约 +contract MinimalProxy { // 定义 MinimalProxy 合约 + // clone 函数:部署一个代理合约,将所有调用委托给 target + function clone(address target) external returns (address result) { // 接收目标地址 + // 将 address(20 字节)转换为 bytes20 类型 + bytes20 targetBytes = bytes20(target); // 提取 20 字节的目标地址 + + // 字节码结构: + // 创建代码(复制运行时代码到内存并返回): + // 3d602d80600a3d3981f3 + // 运行时代码(delegatecall 到目标地址): + // 363d3d373d3d3d363d73 [20 字节地址] 5af43d82803e903d91602b57fd5bf3 + + assembly { // 使用内联汇编构建最小代理字节码 + // 读取 0x40 槽位的 32 字节内存 + // 在 Solidity 中,0x40 内存槽是特殊的:包含"空闲内存指针" + // 指向当前已分配内存的末尾 + let clone := mload(0x40) // 获取空闲内存指针 + + // 第一部分:前 32 字节(创建代码 + 运行时代码前半部分) + // 3d602d80600a3d3981f3363d3d373d3d3d363d73 后跟 12 字节的零填充 + mstore( + clone, // 存储到 clone 指向的位置 + 0x3d602d80600a3d3981f3363d3d373d3d3d363d73000000000000000000000000 + ) + + // 第二部分:在 clone + 0x14(20 字节)处写入目标地址 + // 0x14 = 20,覆盖上面 12 字节的零填充 + // 现在内存布局: + // | 前 20 字节 | 20 字节目标地址 | + // 3d602d80600a3d3981f3363d3d373d3d3d363d73 [目标地址20字节] + mstore(add(clone, 0x14), targetBytes) // 将目标地址写入偏移 20 字节处 + + // 第三部分:在 clone + 0x28(40 字节)处写入运行时代码的后半部分 + // 0x28 = 40 + // 运行时代码末尾: + // 5af43d82803e903d91602b57fd5bf3 + mstore( + add(clone, 0x28), // 偏移 40 字节 + 0x5af43d82803e903d91602b57fd5bf30000000000000000000000000000000000 + ) + + // 最终内存布局(55 字节): + // | 前 20 字节 | 20 字节地址 | 15 字节末尾 | + // 3d602d...363d73 [目标地址] 5af43d...5bf3 + + // 使用 create 部署新合约 + // create(value, code_offset, code_size) + // value: 0(不发送以太币) + // code_offset: clone(代码起始位置) + // code_size: 0x37(55 字节) + result := create(0, clone, 0x37) // 部署克隆合约 + } + } +} +``` diff --git "a/Unit2 \345\272\224\347\224\250/\347\256\200\345\215\225\345\255\227\350\212\202\347\240\201\345\220\210\347\272\246.md" "b/Unit2 \345\272\224\347\224\250/\347\256\200\345\215\225\345\255\227\350\212\202\347\240\201\345\220\210\347\272\246.md" new file mode 100644 index 0000000..1a79983 --- /dev/null +++ "b/Unit2 \345\272\224\347\224\250/\347\256\200\345\215\225\345\255\227\350\212\202\347\240\201\345\220\210\347\272\246.md" @@ -0,0 +1,70 @@ +# 简单字节码合约 + +以下是一个完全使用字节码编写的简单合约示例。通过内联汇编的 `create` 操作码部署一个始终返回 `255` 的合约。 + +```solidity +// SPDX-License-Identifier: MIT // SPDX 许可证标识符:MIT 许可证 +pragma solidity ^0.8.26; // 版本指令,要求使用 Solidity 0.8.26 及以上版本 + +// Factory 合约:部署一个纯字节码合约 +contract Factory { // 定义 Factory 合约 + event Log(address addr); // Log 事件:记录已部署合约的地址 + + // deploy 函数:部署一个始终返回 255 的字节码合约 + function deploy() external { + // 字节码:6960ff60005260206000f3600052600a6016f3 + // 这段字节码包含两部分: + // 1. 创建代码(Creation code):6960ff60005260206000f3 + // 2. 运行时代码(Runtime code):60ff60005260206000f3 + // + // 运行时代码的汇编: + // PUSH1 0xff // 将值 255 压入栈 + // PUSH1 0 // 将内存偏移量 0 压入栈 + // MSTORE // 将 255 存储到内存偏移量 0 处 + // PUSH1 0x20 // 返回数据大小(32 字节) + // PUSH1 0 // 返回数据起始位置(偏移量 0) + // RETURN // 返回内存中 32 字节的数据 + // + // 创建代码的职责:将运行时代码存入内存并返回 + bytes memory bytecode = hex"6960ff60005260206000f3600052600a6016f3"; // 完整的字节码(创建代码 + 运行时代码) + address addr; // 新合约的地址 + assembly { // 使用内联汇编执行部署 + // create(value, offset, size):创建新合约 + // value = 0(不发送以太币) + // offset = add(bytecode, 0x20)(跳过前 32 字节的长度前缀) + // size = 0x13(字节码大小,19 字节) + addr := create(0, add(bytecode, 0x20), 0x13) // 部署新合约 + } + require(addr != address(0)); // 确保合约部署成功(地址不为零地址) + + emit Log(addr); // 记录新合约地址 + } +} + +// IContract 接口:定义与已部署字节码合约交互的接口 +interface IContract { // 接口定义 + function getValue() external view returns (uint256); // 获取返回值(始终为 255) +} + +/* +字节码解析(参考:https://www.evm.codes/playground): + +运行时代码(返回 255)—— 60ff60005260206000f3 + +PUSH1 0xff // 值 255 +PUSH1 0 // 内存偏移量 0 +MSTORE // 将 255 存储到内存(mstore(p, v) 将 v 存储到内存 p 到 p+32) +PUSH1 0x20 // 返回大小(32 字节) +PUSH1 0 // 返回起始偏移量 +RETURN // 返回 32 字节(其中包含值 255) + +创建代码(返回运行时代码)—— 6960ff60005260206000f3600052600a6016f3 + +PUSH10 0x60ff60005260206000f3 // 将运行时代码(10 字节)压入栈 +PUSH1 0 // 内存偏移量 0 +MSTORE // 将运行时代码存储到内存 +PUSH1 0x0a // 返回大小(10 字节) +PUSH1 0x16 // 返回起始偏移量(22 字节,0x16) +RETURN // 返回运行时代码 +*/ +``` diff --git "a/Unit2 \345\272\224\347\224\250/\351\203\250\347\275\262\344\273\273\346\204\217\345\220\210\347\272\246.md" "b/Unit2 \345\272\224\347\224\250/\351\203\250\347\275\262\344\273\273\346\204\217\345\220\210\347\272\246.md" new file mode 100644 index 0000000..7e53c97 --- /dev/null +++ "b/Unit2 \345\272\224\347\224\250/\351\203\250\347\275\262\344\273\273\346\204\217\345\220\210\347\272\246.md" @@ -0,0 +1,92 @@ +# 部署任意合约 + +通过代理合约(Proxy)部署任意合约。代理合约使用内联汇编的 `create` 操作码,可以将任意字节码部署为链上新合约。 + +```solidity +// SPDX-License-Identifier: MIT // SPDX 许可证标识符:MIT 许可证 +pragma solidity ^0.8.26; // 版本指令,要求使用 Solidity 0.8.26 及以上版本 + +// Proxy 合约:部署任意字节码的代理合约 +contract Proxy { // 定义 Proxy 合约 + event Deploy(address); // Deploy 事件:记录已部署合约的地址 + + receive() external payable {} // 可接收纯以太币转账 + + // deploy 函数:使用 create 操作码部署任意字节码 + function deploy(bytes memory _code) // 接收任意字节码 + external + payable + returns (address addr) // 返回部署的合约地址 + { + assembly { // 使用内联汇编调用 create + // create(v, p, n) + // v = 发送给新合约的 wei 数量 + // p = 代码在内存中的起始指针 + // n = 代码大小 + addr := create(callvalue(), add(_code, 0x20), mload(_code)) // callvalue() 将 ETH 转发给新合约 + } + require(addr != address(0), "deploy failed"); // 如果地址为零地址则部署失败 + + emit Deploy(addr); // 触发部署事件 + } + + // execute 函数:执行对目标合约的低级调用 + function execute(address _target, bytes memory _data) // 目标地址和调用数据 + external + payable + { + (bool success,) = _target.call{value: msg.value}(_data); // 使用 call 向目标合约发送消息 + require(success, "failed"); // 调用失败则回滚 + } +} + +// TestContract1 合约:测试合约 1(无构造函数参数) +contract TestContract1 { // 定义 TestContract1 合约 + address public owner = msg.sender; // 所有者为部署者 + + // setOwner 函数:更改所有者 + function setOwner(address _owner) public { // 仅当前所有者可更改 + require(msg.sender == owner, "not owner"); // 权限检查 + owner = _owner; // 更新所有者 + } +} + +// TestContract2 合约:测试合约 2(带构造函数参数) +contract TestContract2 { // 定义 TestContract2 合约 + address public owner = msg.sender; // 所有者为部署者 + uint256 public value = msg.value; // 部署时发送的以太币数量 + uint256 public x; // 构造函数初始化的 x 值 + uint256 public y; // 构造函数初始化的 y 值 + + // 构造函数:接收 x 和 y 两个参数 + constructor(uint256 _x, uint256 _y) payable { // payable 允许部署时接收以太币 + x = _x; // 设置 x + y = _y; // 设置 y + } +} + +// Helper 合约:生成部署所需的字节码和 calldata +contract Helper { // 定义 Helper 合约 + // getBytecode1 函数:获取 TestContract1 的创建字节码(无构造函数参数) + function getBytecode1() external pure returns (bytes memory) { + bytes memory bytecode = type(TestContract1).creationCode; // 获取创建字节码 + return bytecode; // 无需拼接构造函数参数 + } + + // getBytecode2 函数:获取 TestContract2 的创建字节码(含构造函数参数) + function getBytecode2(uint256 _x, uint256 _y) // 构造函数参数 + external + pure + returns (bytes memory) + { + bytes memory bytecode = type(TestContract2).creationCode; // 获取创建字节码 + // 将创建字节码与 ABI 编码的构造函数参数拼接 + return abi.encodePacked(bytecode, abi.encode(_x, _y)); // 打包拼接 + } + + // getCalldata 函数:生成调用 setOwner 函数的 calldata + function getCalldata(address _owner) external pure returns (bytes memory) { + return abi.encodeWithSignature("setOwner(address)", _owner); // 生成函数调用数据 + } +} +``` diff --git "a/Unit2 \345\272\224\347\224\250/\351\242\204\350\256\241\347\256\227\345\220\210\347\272\246\345\234\260\345\235\200.md" "b/Unit2 \345\272\224\347\224\250/\351\242\204\350\256\241\347\256\227\345\220\210\347\272\246\345\234\260\345\235\200.md" new file mode 100644 index 0000000..3917358 --- /dev/null +++ "b/Unit2 \345\272\224\347\224\250/\351\242\204\350\256\241\347\256\227\345\220\210\347\272\246\345\234\260\345\235\200.md" @@ -0,0 +1,109 @@ +# 预计算合约地址(Create2) + +使用 `create2` 可以在合约部署之前预先计算出合约的部署地址。 + +```solidity +// SPDX-License-Identifier: MIT // SPDX 许可证标识符:MIT 许可证 +pragma solidity ^0.8.26; // 版本指令,要求使用 Solidity 0.8.26 及以上版本 + +// Factory 合约:使用新语法(无需汇编)通过 create2 部署合约 +contract Factory { // 定义 Factory 合约 + // deploy 函数:返回新部署合约的地址 + function deploy(address _owner, uint256 _foo, bytes32 _salt) // 接收部署参数和 salt + public + payable + returns (address) // 返回新合约地址 + { + // 这是使用 create2 的新式语法,无需内联汇编 + // 只需传递 salt 选项:{salt: _salt} + return address(new TestContract{salt: _salt}(_owner, _foo)); // create2 部署并返回地址 + } +} + +// FactoryAssembly 合约:使用旧式汇编方式通过 create2 部署合约 +contract FactoryAssembly { // 定义 FactoryAssembly 合约 + event Deployed(address addr, uint256 salt); // Deployed 事件:记录部署地址和 salt + + // 第一步:获取要部署合约的字节码 + // 注意:_owner 和 _foo 是 TestContract 构造函数的参数 + function getBytecode(address _owner, uint256 _foo) // 接收构造函数参数 + public + pure + returns (bytes memory) // 返回完整的创建字节码 + { + bytes memory bytecode = type(TestContract).creationCode; // 获取 TestContract 的创建字节码 + // 将创建字节码与 ABI 编码的构造函数参数拼接 + return abi.encodePacked(bytecode, abi.encode(_owner, _foo)); // 追加构造函数参数编码 + } + + // 第二步:计算待部署合约的地址 + // 注意:_salt 是用于创建地址的随机数 + function getAddress(bytes memory bytecode, uint256 _salt) // 接收字节码和 salt + public + view + returns (address) // 返回预计算的地址 + { + // CREATE2 地址计算公式: + // address = keccak256(0xff + sender + salt + keccak256(bytecode)) 的前 20 字节 + bytes32 hash = keccak256( + abi.encodePacked( + bytes1(0xff), // 常量前缀 0xff + address(this), // 部署者(本合约)地址 + _salt, // 随机盐值 + keccak256(bytecode) // 字节码的 keccak256 哈希 + ) + ); + + // 将 hash 的最后 20 字节转换为 address 类型 + return address(uint160(uint256(hash))); // 取低 160 位(20 字节)作为地址 + } + + // 第三步:部署合约 + // 注意:检查事件日志 Deployed,其中包含已部署 TestContract 的地址 + // 该地址应等于上一步计算出的地址 + function deploy(bytes memory bytecode, uint256 _salt) public payable { + address addr; + + /* + create2 调用方式: + create2(v, p, n, s) + - 创建新合约,代码位于内存 p 到 p+n + - 发送 v wei 给新合约 + - 返回新地址 + - 其中新地址 = keccak256(0xff + address(this) + s + keccak256(mem[p…(p+n))) 的前 20 字节 + - s 是大端序 256 位值(即 salt) + */ + assembly { // 使用内联汇编调用 create2 + addr := + create2( + callvalue(), // 随当前调用发送的 wei 数量 + add(bytecode, 0x20), // 实际代码起始位置(跳过前 32 字节的长度前缀) + mload(bytecode), // 从前 32 字节中加载代码大小 + _salt // salt 值 + ) + + // 如果部署后地址没有代码,表示部署失败 + if iszero(extcodesize(addr)) { revert(0, 0) } // 回滚交易 + } + + emit Deployed(addr, _salt); // 触发部署事件 + } +} + +// TestContract 合约:通过 create2 部署的测试合约 +contract TestContract { // 定义 TestContract 合约 + address public owner; // 公共变量:所有者地址 + uint256 public foo; // 公共变量:foo 值 + + // 构造函数:初始化所有者和 foo 值 + constructor(address _owner, uint256 _foo) payable { // payable 使其可在部署时接收以太币 + owner = _owner; // 设置所有者 + foo = _foo; // 设置 foo + } + + // getBalance 函数:查询合约余额 + function getBalance() public view returns (uint256) { + return address(this).balance; // 返回本合约的以太币余额 + } +} +``` diff --git "a/Unit2 \345\272\224\347\224\250/\351\273\230\345\205\213\345\260\224\346\240\221.md" "b/Unit2 \345\272\224\347\224\250/\351\273\230\345\205\213\345\260\224\346\240\221.md" new file mode 100644 index 0000000..2cd434c --- /dev/null +++ "b/Unit2 \345\272\224\347\224\250/\351\273\230\345\205\213\345\260\224\346\240\221.md" @@ -0,0 +1,96 @@ +# 默克尔树 + +默克尔树(Merkle Tree)允许你以加密方式证明某个元素存在于一个集合中,而无需透露整个集合的内容。 + +常用于空投、白名单验证等场景。 + +```solidity +// SPDX-License-Identifier: MIT // SPDX 许可证标识符:MIT 许可证 +pragma solidity ^0.8.26; // 版本指令,要求使用 Solidity 0.8.26 及以上版本 + +// MerkleProof 合约:通用的默克尔证明验证工具 +contract MerkleProof { // 定义 MerkleProof 合约 + // verify 函数:验证默克尔证明 + function verify( + bytes32[] memory proof, // 证明路径数组(兄弟节点哈希) + bytes32 root, // 默克尔树根哈希 + bytes32 leaf, // 要验证的叶子节点哈希 + uint256 index // 叶子节点在树中的索引位置 + ) public pure returns (bool) { // 返回 true 表示验证通过 + bytes32 hash = leaf; // 从叶子节点开始计算 + + // 遍历证明路径中的每个元素 + for (uint256 i = 0; i < proof.length; i++) { // 逐层向上计算 + bytes32 proofElement = proof[i]; // 当前层的兄弟节点哈希 + + // 根据索引的奇偶性决定拼接顺序 + if (index % 2 == 0) { // 如果当前索引为偶数,叶子在左边 + // hash 在前,proofElement 在后,然后做 keccak256 + hash = keccak256(abi.encodePacked(hash, proofElement)); // 左节点 + 右节点 + } else { // 如果当前索引为奇数,叶子在右边 + // proofElement 在前,hash 在后,然后做 keccak256 + hash = keccak256(abi.encodePacked(proofElement, hash)); // 左节点 + 右节点 + } + + index = index / 2; // 索引除以 2,向父节点层移动 + } + + return hash == root; // 最终计算出的哈希应与根哈希一致 + } +} + +// TestMerkleProof 合约:继承 MerkleProof,构建具体的默克尔树示例 +contract TestMerkleProof is MerkleProof { // 继承 MerkleProof 合约 + bytes32[] public hashes; // 存储默克尔树所有节点的哈希值 + + // 构造函数:构建一棵默克尔树 + constructor() { + // 定义 4 笔交易字符串作为叶子数据 + string[4] memory transactions = // 4 笔交易的数组 + ["alice -> bob", "bob -> dave", "carol -> alice", "dave -> bob"]; + + // 第一步:计算所有叶子节点的哈希值(keccak256) + for (uint256 i = 0; i < transactions.length; i++) { // 遍历每笔交易 + // 对交易字符串进行 keccak256 哈希,存入 hashes 数组 + hashes.push(keccak256(abi.encodePacked(transactions[i]))); // 叶子哈希 + } + + // 第二步:逐层构建内部节点 + // 默克尔树构建算法:从叶子层开始,每次取相邻两个节点计算父节点 + uint256 n = transactions.length; // 当前层的节点数(初始为叶子数) + uint256 offset = 0; // 当前层在 hashes 数组中的起始位置 + + while (n > 0) { // 当还有节点需要合并时继续 + // 遍历当前层的每对相邻节点 + for (uint256 i = 0; i < n - 1; i += 2) { // 每次取两个节点 + // 将两个相邻子节点的哈希拼接后计算父节点哈希 + hashes.push( + keccak256( + abi.encodePacked( + hashes[offset + i], // 左子节点 + hashes[offset + i + 1] // 右子节点 + ) + ) + ); + } + offset += n; // offset 移到下一层的起始位置 + n = n / 2; // 每层的节点数减半 + } + } + + // getRoot 函数:获取默克尔树的根哈希 + function getRoot() public view returns (bytes32) { // 返回根哈希 + return hashes[hashes.length - 1]; // 最后一个元素即为根哈希 + } + + // 验证示例(第 3 个叶子节点,索引为 2): + // 叶子哈希: + // 0xdca3326ad7e8121bf9cf9c12333e6b2271abe823ec9edfe42f813b1e768fa57b + // 根哈希: + // 0xcc086fcc038189b4641db2cc4f1de3bb132aefbd65d510d817591550937818c7 + // 索引:2 + // 证明路径(兄弟节点哈希): + // 0x8da9e1c820f9dbd1589fd6585872bc1063588625729e7ab0797cfc63a00bd950 + // 0x995788ffc103b987ad50f5e5707fd094419eb12d9552cc423bd0cd86a3861433 +} +``` diff --git "a/Unit2 \345\272\224\347\224\250/\351\273\230\345\205\213\345\260\224\347\251\272\346\212\225.md" "b/Unit2 \345\272\224\347\224\250/\351\273\230\345\205\213\345\260\224\347\251\272\346\212\225.md" new file mode 100644 index 0000000..daa5d3e --- /dev/null +++ "b/Unit2 \345\272\224\347\224\250/\351\273\230\345\205\213\345\260\224\347\251\272\346\212\225.md" @@ -0,0 +1,125 @@ +# 默克尔空投 + +使用默克尔树(Merkle Tree)实现代币空投。将空投接收地址和金额构建为默克尔树,用户只需提供默克尔证明即可领取空投,无需链上存储完整的空投列表。 + +本示例包含默克尔证明验证库(参考 OpenZeppelin 实现)和空投合约。 + +```solidity +// SPDX-License-Identifier: MIT // SPDX 许可证标识符:MIT 许可证 +pragma solidity ^0.8.26; // 版本指令,要求使用 Solidity 0.8.26 及以上版本 + +// ===== 哈希工具库(OpenZeppelin 参考实现)===== + +// Hashes 库:高效的哈希计算工具 +library Hashes { // 定义 Hashes 库 + // commutativeKeccak256 函数:顺序无关的二元 keccak256 计算 + // 对两个分支按大小排序后计算哈希,确保无论输入顺序如何结果一致 + function commutativeKeccak256(bytes32 a, bytes32 b) // 两个哈希值 + internal + pure + returns (bytes32) // 返回组合哈希 + { + return a < b ? _efficientKeccak256(a, b) : _efficientKeccak256(b, a); // 小在前,大在后 + } + + // _efficientKeccak256 内部函数:使用汇编高效计算两个 bytes32 的 keccak256 + function _efficientKeccak256(bytes32 a, bytes32 b) // 两个哈希值 + private + pure + returns (bytes32 value) // 返回组合哈希 + { + assembly { // 使用汇编直接操作内存 + mstore(0x00, a) // 将 a 写入内存偏移 0 + mstore(0x20, b) // 将 b 写入内存偏移 32 + value := keccak256(0x00, 0x40) // 对 64 字节数据计算 keccak256 + } + } +} + +// ===== 默克尔证明验证库(OpenZeppelin 参考实现)===== + +// MerkleProof 库:默克尔树证明验证 +library MerkleProof { // 定义 MerkleProof 库 + // verify 函数:验证默克尔证明 + function verify(bytes32[] memory proof, bytes32 root, bytes32 leaf) // 证明路径、根哈希、叶子节点 + internal + pure + returns (bool) // 返回验证结果 + { + return processProof(proof, leaf) == root; // 从证明计算出的根是否与预期一致 + } + + // processProof 函数:通过证明路径逐层计算最终哈希 + function processProof(bytes32[] memory proof, bytes32 leaf) // 证明路径和叶子节点 + internal + pure + returns (bytes32) // 返回最终计算出的根哈希 + { + bytes32 computedHash = leaf; // 初始哈希为叶子节点 + for (uint256 i = 0; i < proof.length; i++) { // 遍历证明路径的每一层 + computedHash = Hashes.commutativeKeccak256(computedHash, proof[i]); // 逐层向上计算哈希 + } + return computedHash; // 返回最终计算出的根 + } +} + +// ===== 默克尔空投合约 ===== + +// IToken 接口:代币合约的最小接口 +interface IToken { // 定义 IToken 接口 + function mint(address to, uint256 amount) external; // 铸造函数 +} + +// Airdrop 合约:基于默克尔树的空投合约 +contract Airdrop { // 定义 Airdrop 合约 + event Claim(address to, uint256 amount); // Claim 事件:记录领取 + + IToken public immutable token; // 空投的代币合约(不可变) + bytes32 public immutable root; // 默克尔树根(不可变) + mapping(bytes32 => bool) public claimed; // 已领取记录(通过叶子哈希防重领) + + // 构造函数:初始化代币合约地址和默克尔树根 + constructor(address _token, bytes32 _root) { // 代币地址和根哈希 + token = IToken(_token); // 绑定代币合约 + root = _root; // 设置默克尔树根 + } + + // getLeafHash 函数:计算叶子节点的哈希 + function getLeafHash(address to, uint256 amount) // 领取地址和金额 + public + pure + returns (bytes32) // 返回叶子哈希 + { + return keccak256(abi.encode(to, amount)); // 哈希(地址, 金额) + } + + // claim 函数:使用默克尔证明领取空投 + function claim(bytes32[] memory proof, address to, uint256 amount) // 证明路径、地址、金额 + external + { + // 注意:(to, amount) 必须唯一,不能重复 + bytes32 leaf = getLeafHash(to, amount); // 计算叶子哈希 + + require(!claimed[leaf], "airdrop already claimed"); // 尚未被领取 + require(MerkleProof.verify(proof, root, leaf), "invalid merkle proof"); // 验证默克尔证明 + claimed[leaf] = true; // 标记为已领取 + + token.mint(to, amount); // 铸造代币给领取者 + + emit Claim(to, amount); // 触发领取事件 + } +} +``` + +## 工作流程 + +1. **构建默克尔树**:项目方在链下将所有空投地址和金额构建为默克尔树,将根哈希部署到 `Airdrop` 合约中 +2. **生成证明**:每个用户可以计算出自己的叶子哈希以及对应的默克尔证明路径 +3. **链上领取**:用户调用 `claim`,传入证明路径、地址和金额,合约验证证明后铸造代币 +4. **防重领**:每个地址/金额组合只能领取一次 + +## 优势 + +- **Gas 效率高**:无需在链上存储完整的空投列表 +- **验证开销固定**:验证成本仅与证明路径长度(O(log n))相关 +- **可预先计算根**:树根可以在部署前确定 diff --git "a/Unit4 DeFi/03 Chainlink \344\273\267\346\240\274\351\242\204\350\250\200\346\234\272.md" "b/Unit4 DeFi/03 Chainlink \344\273\267\346\240\274\351\242\204\350\250\200\346\234\272.md" new file mode 100644 index 0000000..2e5bb5e --- /dev/null +++ "b/Unit4 DeFi/03 Chainlink \344\273\267\346\240\274\351\242\204\350\250\200\346\234\272.md" @@ -0,0 +1,53 @@ +# Chainlink 价格预言机 + +通过 Chainlink 去中心化预言机网络获取链上资产价格。本例演示如何从 Chainlink 的 ETH/USD 价格馈送合约读取最新价格。 + +```solidity +// SPDX-License-Identifier: MIT // SPDX 许可证标识符:MIT 许可证 +pragma solidity ^0.8.26; // 版本指令,要求使用 Solidity 0.8.26 及以上版本 + +// ChainlinkPriceOracle 合约:从 Chainlink 获取 ETH/USD 价格 +contract ChainlinkPriceOracle { // 定义 Chainlink 价格预言机合约 + AggregatorV3Interface internal priceFeed; // Chainlink 聚合器接口 + + constructor() { + // ETH / USD 价格馈送合约地址(以太坊主网) + priceFeed = + AggregatorV3Interface(0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419); + } + + // getLatestPrice 函数:获取最新的 ETH/USD 价格 + function getLatestPrice() public view returns (int256) { // 返回 int256 价格 + ( + uint80 roundID, // 当前轮次 ID + int256 price, // 价格(放大 10^8 倍) + uint256 startedAt, // 本轮开始时间戳 + uint256 timeStamp, // 本轮更新时间戳 + uint80 answeredInRound // 回答时所在轮次 + ) = priceFeed.latestRoundData(); // 获取最新轮次数据 + // ETH / USD 价格按 10^8 倍放大,除以 1e8 得到实际价格 + return price / 1e8; // 价格(美元,整数) + } +} + +// AggregatorV3Interface 接口:Chainlink 聚合器 V3 标准接口 +interface AggregatorV3Interface { // 定义聚合器接口 + // latestRoundData 函数:获取最新一轮的价格数据 + function latestRoundData() + external + view + returns ( + uint80 roundId, // 轮次 ID + int256 answer, // 价格答案 + uint256 startedAt, // 开始时间 + uint256 updatedAt, // 更新时间 + uint80 answeredInRound // 回答轮次 + ); +} +``` + +## 注意事项 + +- Chainlink 价格标度因喂价对而异(例如 ETH/USD 为 8 位小数) +- 价格是 `int256` 类型,可能为负数(极少情况) +- 不同网络的合约地址不同,需从 [Chainlink 文档](https://docs.chain.link/data-feeds/price-feeds/addresses) 获取 diff --git "a/Unit4 DeFi/04 Chronicle \344\273\267\346\240\274\351\242\204\350\250\200\346\234\272.md" "b/Unit4 DeFi/04 Chronicle \344\273\267\346\240\274\351\242\204\350\250\200\346\234\272.md" new file mode 100644 index 0000000..32d9569 --- /dev/null +++ "b/Unit4 DeFi/04 Chronicle \344\273\267\346\240\274\351\242\204\350\250\200\346\234\272.md" @@ -0,0 +1,59 @@ +# Chronicle 价格预言机 + +通过 Chronicle(原 Chronicle Protocol)去中心化预言机获取链上资产价格。Chronicle 是 MakerDAO 使用的预言机,具有高安全性和去中心化特性。 + +```solidity +// SPDX-License-Identifier: MIT // SPDX 许可证标识符:MIT 许可证 +pragma solidity ^0.8.16; // 版本指令,要求使用 Solidity 0.8.16 及以上版本 + +// OracleReader 合约:从 Chronicle 预言机读取数据 +// 完整仓库见 https://github.com/chronicleprotocol/OracleReader-Example +// 此合约地址硬编码为 Sepolia 测试网,其他网络请查阅 https://chroniclelabs.org/dashboard/oracles +contract OracleReader { // 定义 OracleReader 合约 + // Chronicle ETH/USD 预言机地址(Sepolia 测试网) + IChronicle public chronicle = + IChronicle(address(0xdd6D76262Fd7BdDe428dcfCd94386EbAe0151603)); + + // SelfKisser 地址:用于授权本合约访问 Chronicle 预言机 + // SelfKisser_1 地址(Sepolia 测试网) + ISelfKisser public selfKisser = + ISelfKisser(address(0x0Dcc19657007713483A5cA76e6A7bbe5f56EA37d)); + + constructor() { + // 将 address(this) 添加到 Chronicle 预言机的白名单中 + // 此操作使得本合约可以读取 Chronicle 预言机 + selfKisser.selfKiss(address(chronicle)); // 授权访问预言机 + } + + // read 函数:从 Chronicle 预言机读取最新数据 + function read() external view returns (uint256 val, uint256 age) { + (val, age) = chronicle.readWithAge(); // 读取当前值和更新时间戳 + } +} + +// IChronicle 接口:Chronicle 预言机标准接口 +// 参考: https://github.com/chronicleprotocol/chronicle-std +interface IChronicle { // 定义 IChronicle 接口 + // read 函数:返回预言机当前值(18 位小数) + function read() external view returns (uint256 value); + + // readWithAge 函数:返回当前值及其更新时间戳 + function readWithAge() external view returns (uint256 value, uint256 age); +} + +// ISelfKisser 接口:用于白名单授权 +// 参考: https://github.com/chronicleprotocol/self-kisser +interface ISelfKisser { // 定义 ISelfKisser 接口 + // selfKiss 函数:将调用者添加到指定预言机的白名单 + function selfKiss(address oracle) external; +} +``` + +## 与 Chainlink 的区别 + +| 特性 | Chronicle | Chainlink | +|------|-----------|-----------| +| 安全性模型 | 高去中心化验证者网络 | 去中心化预言机网络 | +| 授权机制 | 需要 SelfKisser 白名单授权 | 无需授权即可读取 | +| 主要用户 | MakerDAO 生态 | 通用 DeFi | +| 价格精度 | 18 位小数 | 因喂价对而异(如 8 位) | diff --git "a/Unit4 DeFi/05 \346\201\222\345\256\232\344\271\230\347\247\257 AMM.md" "b/Unit4 DeFi/05 \346\201\222\345\256\232\344\271\230\347\247\257 AMM.md" new file mode 100644 index 0000000..7f97fdf --- /dev/null +++ "b/Unit4 DeFi/05 \346\201\222\345\256\232\344\271\230\347\247\257 AMM.md" @@ -0,0 +1,163 @@ +# 恒定乘积 AMM(CPAMM) + +恒定乘积自动做市商(Constant Product AMM)是 Uniswap V2 的核心机制。遵循 `x * y = k` 的不变量,支持 swap、添加流动性和移除流动性。 + +```solidity +// SPDX-License-Identifier: MIT // SPDX 许可证标识符:MIT 许可证 +pragma solidity ^0.8.26; // 版本指令,要求使用 Solidity 0.8.26 及以上版本 + +// CPAMM 合约:恒定乘积自动做市商 +contract CPAMM { // 定义 CPAMM 合约 + IERC20 public immutable token0; // 代币 0(不可变) + IERC20 public immutable token1; // 代币 1(不可变) + + uint256 public reserve0; // 代币 0 的储备量 + uint256 public reserve1; // 代币 1 的储备量 + + uint256 public totalSupply; // LP 代币总供应量 + mapping(address => uint256) public balanceOf; // LP 代币余额 + + constructor(address _token0, address _token1) { + token0 = IERC20(_token0); + token1 = IERC20(_token1); + } + + // _mint 内部函数:铸造 LP 代币 + function _mint(address _to, uint256 _amount) private { + balanceOf[_to] += _amount; // 增加用户余额 + totalSupply += _amount; // 增加总供应量 + } + + // _burn 内部函数:销毁 LP 代币 + function _burn(address _from, uint256 _amount) private { + balanceOf[_from] -= _amount; // 减少用户余额 + totalSupply -= _amount; // 减少总供应量 + } + + // _update 内部函数:更新储备量 + function _update(uint256 _reserve0, uint256 _reserve1) private { + reserve0 = _reserve0; + reserve1 = _reserve1; + } + + // swap 函数:用代币交换另一种代币 + function swap(address _tokenIn, uint256 _amountIn) + external + returns (uint256 amountOut) // 返回换出的数量 + { + require( + _tokenIn == address(token0) || _tokenIn == address(token1), + "invalid token" + ); + require(_amountIn > 0, "amount in = 0"); + + bool isToken0 = _tokenIn == address(token0); // 判断输入是哪个代币 + (IERC20 tokenIn, IERC20 tokenOut, uint256 reserveIn, uint256 reserveOut) + = isToken0 + ? (token0, token1, reserve0, reserve1) + : (token1, token0, reserve1, reserve0); + + tokenIn.transferFrom(msg.sender, address(this), _amountIn); + + // 恒定乘积公式推导: + // xy = k + // (x + dx)(y - dy) = k + // y - dy = k / (x + dx) + // dy = ydx / (x + dx) + // 0.3% 手续费 + uint256 amountInWithFee = (_amountIn * 997) / 1000; // 扣除 0.3% 手续费 + amountOut = + (reserveOut * amountInWithFee) / (reserveIn + amountInWithFee); + + tokenOut.transfer(msg.sender, amountOut); // 将输出代币发给用户 + + _update( + token0.balanceOf(address(this)), token1.balanceOf(address(this)) + ); + } + + // addLiquidity 函数:添加流动性 + function addLiquidity(uint256 _amount0, uint256 _amount1) + external + returns (uint256 shares) // 返回铸造的 LP 代币数量 + { + token0.transferFrom(msg.sender, address(this), _amount0); + token1.transferFrom(msg.sender, address(this), _amount1); + + // 添加 dx, dy 时需保持价格不变:x/y = (x+dx)/(y+dy) → dy = y/x * dx + if (reserve0 > 0 || reserve1 > 0) { + require( + reserve0 * _amount1 == reserve1 * _amount0, "x / y != dx / dy" + ); + } + + // LP 代币铸造量推导: + // f(x, y) = sqrt(xy)(流动性价值函数) + // L0 = f(x, y), L1 = f(x+dx, y+dy) + // s = (L1 - L0) / L0 * T = dx/x * T = dy/y * T + if (totalSupply == 0) { + shares = _sqrt(_amount0 * _amount1); // 首次添加,shares = sqrt(dx*dy) + } else { + shares = _min( + (_amount0 * totalSupply) / reserve0, // s = dx/x * T + (_amount1 * totalSupply) / reserve1 // s = dy/y * T(取最小值防操纵) + ); + } + require(shares > 0, "shares = 0"); + _mint(msg.sender, shares); + + _update( + token0.balanceOf(address(this)), token1.balanceOf(address(this)) + ); + } + + // removeLiquidity 函数:移除流动性(按比例提取两种代币) + function removeLiquidity(uint256 _shares) + external + returns (uint256 amount0, uint256 amount1) + { + // 移除的流动性量:dx = s/T * x, dy = s/T * y + uint256 bal0 = token0.balanceOf(address(this)); + uint256 bal1 = token1.balanceOf(address(this)); + + amount0 = (_shares * bal0) / totalSupply; // 应提取的 token0 + amount1 = (_shares * bal1) / totalSupply; // 应提取的 token1 + require(amount0 > 0 && amount1 > 0, "amount0 or amount1 = 0"); + + _burn(msg.sender, _shares); + _update(bal0 - amount0, bal1 - amount1); + + token0.transfer(msg.sender, amount0); + token1.transfer(msg.sender, amount1); + } + + // _sqrt 内部函数:牛顿迭代法计算平方根 + function _sqrt(uint256 y) private pure returns (uint256 z) { + if (y > 3) { + z = y; + uint256 x = y / 2 + 1; + while (x < z) { + z = x; + x = (y / x + x) / 2; // 牛顿迭代 + } + } else if (y != 0) { + z = 1; + } + } + + function _min(uint256 x, uint256 y) private pure returns (uint256) { + return x <= y ? x : y; + } +} + +interface IERC20 { /* 标准 ERC20 接口 */ } +``` + +## 核心公式 + +| 操作 | 公式 | +|------|------| +| **Swap 输出量** | `dy = y * dx / (x + dx)` | +| **Swap 手续费** | 0.3%(`amountInWithFee = dx * 997 / 1000`)| +| **添加流动性** | `s = min(dx/x * T, dy/y * T)` | +| **移除流动性** | `dx = s/T * x, dy = s/T * y` | diff --git "a/Unit4 DeFi/06 \346\201\222\345\256\232\345\222\214 AMM.md" "b/Unit4 DeFi/06 \346\201\222\345\256\232\345\222\214 AMM.md" new file mode 100644 index 0000000..c09a3e9 --- /dev/null +++ "b/Unit4 DeFi/06 \346\201\222\345\256\232\345\222\214 AMM.md" @@ -0,0 +1,125 @@ +# 恒定和 AMM(CSAMM) + +恒定和自动做市商(Constant Sum AMM)遵循 `x + y = k` 的不变量。相比恒定乘积 AMM,价格始终恒定(不存在滑点),但流动性可能在极端价格时耗尽。 + +```solidity +// SPDX-License-Identifier: MIT // SPDX 许可证标识符:MIT 许可证 +pragma solidity ^0.8.26; // 版本指令,要求使用 Solidity 0.8.26 及以上版本 + +// CSAMM 合约:恒定和自动做市商(x + y = k) +contract CSAMM { // 定义 CSAMM 合约 + IERC20 public immutable token0; // 代币 0 + IERC20 public immutable token1; // 代币 1 + + uint256 public reserve0; // 代币 0 储备 + uint256 public reserve1; // 代币 1 储备 + + uint256 public totalSupply; // LP 代币总供应 + mapping(address => uint256) public balanceOf; // LP 代币余额 + + constructor(address _token0, address _token1) { + // 注意:此合约假设 token0 和 token1 的小数位相同 + token0 = IERC20(_token0); + token1 = IERC20(_token1); + } + + function _mint(address _to, uint256 _amount) private { + balanceOf[_to] += _amount; + totalSupply += _amount; + } + + function _burn(address _from, uint256 _amount) private { + balanceOf[_from] -= _amount; + totalSupply -= _amount; + } + + function _update(uint256 _res0, uint256 _res1) private { + reserve0 = _res0; + reserve1 = _res1; + } + + // swap 函数:恒定和交换(1:1 价格,仅扣手续费) + function swap(address _tokenIn, uint256 _amountIn) + external + returns (uint256 amountOut) + { + require( + _tokenIn == address(token0) || _tokenIn == address(token1), + "invalid token" + ); + + bool isToken0 = _tokenIn == address(token0); + (IERC20 tokenIn, IERC20 tokenOut, uint256 resIn, uint256 resOut) = + isToken0 + ? (token0, token1, reserve0, reserve1) + : (token1, token0, reserve1, reserve0); + + tokenIn.transferFrom(msg.sender, address(this), _amountIn); + uint256 amountIn = tokenIn.balanceOf(address(this)) - resIn; + + // 0.3% 手续费 + amountOut = (amountIn * 997) / 1000; // 扣除手续费后的输出 + + (uint256 res0, uint256 res1) = isToken0 + ? (resIn + amountIn, resOut - amountOut) + : (resOut - amountOut, resIn + amountIn); + + _update(res0, res1); + tokenOut.transfer(msg.sender, amountOut); + } + + // addLiquidity 函数:添加两种代币的流动性 + function addLiquidity(uint256 _amount0, uint256 _amount1) + external + returns (uint256 shares) + { + token0.transferFrom(msg.sender, address(this), _amount0); + token1.transferFrom(msg.sender, address(this), _amount1); + + uint256 bal0 = token0.balanceOf(address(this)); + uint256 bal1 = token1.balanceOf(address(this)); + + uint256 d0 = bal0 - reserve0; + uint256 d1 = bal1 - reserve1; + + // s = (d0 + d1) * T / (reserve0 + reserve1) + if (totalSupply > 0) { + shares = ((d0 + d1) * totalSupply) / (reserve0 + reserve1); + } else { + shares = d0 + d1; // 首次添加,shares = 总增加值 + } + + require(shares > 0, "shares = 0"); + _mint(msg.sender, shares); + _update(bal0, bal1); + } + + // removeLiquidity 函数:移除流动性 + function removeLiquidity(uint256 _shares) + external + returns (uint256 d0, uint256 d1) + { + // a = L * s / T = (reserve0 + reserve1) * s / T + // 按比例提取两种代币 + d0 = (reserve0 * _shares) / totalSupply; + d1 = (reserve1 * _shares) / totalSupply; + + _burn(msg.sender, _shares); + _update(reserve0 - d0, reserve1 - d1); + + if (d0 > 0) token0.transfer(msg.sender, d0); + if (d1 > 0) token1.transfer(msg.sender, d1); + } +} + +interface IERC20 { /* 标准 ERC20 接口 */ } +``` + +## 与 CPAMM 对比 + +| 特性 | 恒定和 (CSAMM) | 恒定乘积 (CPAMM) | +|------|-------------|---------------| +| 不变量 | `x + y = k` | `x * y = k` | +| 价格曲线 | 线性(无滑点) | 双曲线(有滑点) | +| 流动性枯竭 | 可能完全耗尽某一种代币 | 永不完全耗尽 | +| 适用场景 | 稳定币对(价格接近 1:1) | 波动资产对 | diff --git "a/Unit4 DeFi/07 DAI \344\273\243\347\220\206.md" "b/Unit4 DeFi/07 DAI \344\273\243\347\220\206.md" new file mode 100644 index 0000000..386dedc --- /dev/null +++ "b/Unit4 DeFi/07 DAI \344\273\243\347\220\206.md" @@ -0,0 +1,122 @@ +# DAI 代理合约 + +通过 DSProxy 代理与 MakerDAO 的 CDP(抵押债仓)智能合约交互,实现存入 ETH 抵押品借出 DAI 的流程。 + +```solidity +// SPDX-License-Identifier: MIT // SPDX 许可证标识符:MIT 许可证 +pragma solidity ^0.8.26; // 版本指令,要求使用 Solidity 0.8.26 及以上版本 + +// MakerDAO 主网地址常量 +address constant DAI = 0x6B175474E89094C44Da98b954EedeAC495271d0F; // DAI 代币 +address constant PROXY_REGISTRY = 0x4678f0a6958e4D2Bc4F1BAF7Bc52E8F3564f3fE4; // DSProxy 工厂 +address constant PROXY_ACTIONS = 0x82ecD135Dce65Fbc6DbdD0e4237E0AF93FFD5038; // 代理操作合约 +address constant CDP_MANAGER = 0x5ef30b9986345249bc32d8928B7ee64DE9435E39; // CDP 管理器 +address constant JUG = 0x19c0976f590D67707E62397C87829d896Dc0f1F1; // 稳定费累积器 +address constant JOIN_ETH_C = 0xF04a5cC80B1E94C69B48f5ee68a08CD2F09A7c3E; // ETH-C 抵押品接入 +address constant JOIN_DAI = 0x9759A6Ac90977b93B58547b4A71c78317f391A28; // DAI 接入 + +bytes32 constant ETH_C = 0x4554482d43000000000000000000000000000000000000000000000000000000; // "ETH-C" + +// DaiProxy 合约:通过 DSProxy 与 MakerDAO 交互 +contract DaiProxy { // 定义 DaiProxy 合约 + IERC20 private constant dai = IERC20(DAI); + address public immutable proxy; // 用户的 DSProxy 地址 + uint256 public immutable cdpId; // CDP(金库)ID + + constructor() { + // 1. 创建 DSProxy + proxy = IDssProxyRegistry(PROXY_REGISTRY).build(); + // 2. 通过 DSProxy 打开一个 ETH-C CDP + bytes32 res = IDssProxy(proxy).execute( + PROXY_ACTIONS, + abi.encodeCall(IDssProxyActions.open, (CDP_MANAGER, ETH_C, proxy)) + ); + cdpId = uint256(res); // 返回的 CDP ID + } + + receive() external payable {} // 接收 ETH + + // lockEth 函数:将 ETH 存入 CDP 作为抵押品 + function lockEth() external payable { + IDssProxy(proxy).execute{value: msg.value}( + PROXY_ACTIONS, + abi.encodeCall( + IDssProxyActions.lockETH, (CDP_MANAGER, JOIN_ETH_C, cdpId) + ) + ); + } + + // borrow 函数:从 CDP 借出 DAI + function borrow(uint256 daiAmount) external { + IDssProxy(proxy).execute( + PROXY_ACTIONS, + abi.encodeCall( + IDssProxyActions.draw, + (CDP_MANAGER, JUG, JOIN_DAI, cdpId, daiAmount) + ) + ); + } + + // repay 函数:偿还部分 DAI 借款 + function repay(uint256 daiAmount) external { + dai.approve(proxy, daiAmount); // 需先授权 DSProxy + IDssProxy(proxy).execute( + PROXY_ACTIONS, + abi.encodeCall( + IDssProxyActions.wipe, (CDP_MANAGER, JOIN_DAI, cdpId, daiAmount) + ) + ); + } + + // repayAll 函数:偿还全部 DAI 借款 + function repayAll() external { + dai.approve(proxy, type(uint256).max); // 无限授权 + IDssProxy(proxy).execute( + PROXY_ACTIONS, + abi.encodeCall( + IDssProxyActions.wipeAll, (CDP_MANAGER, JOIN_DAI, cdpId) + ) + ); + } + + // unlockEth 函数:解锁抵押的 ETH + function unlockEth(uint256 ethAmount) external { + IDssProxy(proxy).execute( + PROXY_ACTIONS, + abi.encodeCall( + IDssProxyActions.freeETH, + (CDP_MANAGER, JOIN_ETH_C, cdpId, ethAmount) + ) + ); + } +} + +// MakerDAO 接口定义 +interface IDssProxyRegistry { + function build() external returns (address proxy); +} + +interface IDssProxy { + function execute(address target, bytes memory data) + external payable returns (bytes32 res); +} + +interface IDssProxyActions { + function open(address cdpManager, bytes32 ilk, address usr) external returns (uint256 cdpId); + function lockETH(address cdpManager, address ethJoin, uint256 cdpId) external payable; + function draw(address cdpManager, address jug, address daiJoin, uint256 cdpId, uint256 daiAmount) external; + function wipe(address cdpManager, address daiJoin, uint256 cdpId, uint256 daiAmount) external; + function wipeAll(address cdpManager, address daiJoin, uint256 cdpId) external; + function freeETH(address cdpManager, address ethJoin, uint256 cdpId, uint256 collateralAmount) external; +} + +interface IERC20 { /* 标准 ERC20 接口 */ } +``` + +## 工作流程 + +1. **创建 CDP**:部署 `DaiProxy` → 自动创建 DSProxy → 打开 ETH-C CDP +2. **存入抵押品**:`lockEth()` 将 ETH 存入 CDP +3. **借出 DAI**:`borrow()` 从 CDP 借出 DAI +4. **偿还 DAI**:`repay()` / `repayAll()` 偿还借款 +5. **解锁抵押品**:`unlockEth()` 取回抵押的 ETH diff --git "a/Unit4 DeFi/08 \347\246\273\346\225\243\350\264\250\346\212\274\345\245\226\345\212\261.md" "b/Unit4 DeFi/08 \347\246\273\346\225\243\350\264\250\346\212\274\345\245\226\345\212\261.md" new file mode 100644 index 0000000..4e95289 --- /dev/null +++ "b/Unit4 DeFi/08 \347\246\273\346\225\243\350\264\250\346\212\274\345\245\226\345\212\261.md" @@ -0,0 +1,95 @@ +# 离散质押奖励 + +离散质押奖励(Discrete Staking Rewards)与连续质押奖励不同,它通过 `updateRewardIndex` 函数手动注入奖励,适合不定期发放奖励的场景。 + +```solidity +// SPDX-License-Identifier: MIT // SPDX 许可证标识符:MIT 许可证 +pragma solidity ^0.8.26; // 版本指令,要求使用 Solidity 0.8.26 及以上版本 + +// DiscreteStakingRewards 合约:离散质押奖励系统 +contract DiscreteStakingRewards { // 定义离散质押奖励合约 + IERC20 public immutable stakingToken; // 质押代币 + IERC20 public immutable rewardToken; // 奖励代币 + + mapping(address => uint256) public balanceOf; // 各用户质押量 + uint256 public totalSupply; // 总质押量 + + uint256 private constant MULTIPLIER = 1e18; // 精度乘数 + uint256 private rewardIndex; // 全局奖励指数 + mapping(address => uint256) private rewardIndexOf; // 用户上次更新时的奖励指数 + mapping(address => uint256) private earned; // 用户已赚取但未领取的奖励 + + constructor(address _stakingToken, address _rewardToken) { + stakingToken = IERC20(_stakingToken); + rewardToken = IERC20(_rewardToken); + } + + // updateRewardIndex 函数:注入新奖励并更新全局奖励指数 + function updateRewardIndex(uint256 reward) external { + rewardToken.transferFrom(msg.sender, address(this), reward); // 转入奖励 + // 全局指数 += 奖励 * 精度 / 总质押量 + rewardIndex += (reward * MULTIPLIER) / totalSupply; + } + + // _calculateRewards 函数:计算用户自上次更新以来的新奖励 + function _calculateRewards(address account) + private + view + returns (uint256) + { + uint256 shares = balanceOf[account]; + // 奖励 = 质押份额 * (当前指数 - 用户已结算指数) / 精度 + return (shares * (rewardIndex - rewardIndexOf[account])) / MULTIPLIER; + } + + // calculateRewardsEarned 函数:查询用户可领取的总奖励 + function calculateRewardsEarned(address account) + external + view + returns (uint256) + { + return earned[account] + _calculateRewards(account); // 已结算 + 未结算 + } + + // _updateRewards 函数:结算用户的待领奖励 + function _updateRewards(address account) private { + earned[account] += _calculateRewards(account); // 累计到已赚取 + rewardIndexOf[account] = rewardIndex; // 更新用户指数 + } + + function stake(uint256 amount) external { + _updateRewards(msg.sender); // 先结算奖励 + balanceOf[msg.sender] += amount; + totalSupply += amount; + stakingToken.transferFrom(msg.sender, address(this), amount); + } + + function unstake(uint256 amount) external { + _updateRewards(msg.sender); // 先结算奖励 + balanceOf[msg.sender] -= amount; + totalSupply -= amount; + stakingToken.transfer(msg.sender, amount); + } + + // claim 函数:领取已赚取的奖励 + function claim() external returns (uint256) { + _updateRewards(msg.sender); // 先结算 + uint256 reward = earned[msg.sender]; + if (reward > 0) { + earned[msg.sender] = 0; // 清零已领取 + rewardToken.transfer(msg.sender, reward); + } + return reward; + } +} + +interface IERC20 { /* 标准 ERC20 接口 */ } +``` + +## 离散 vs 连续质押奖励 + +| 特性 | 离散质押 | 连续质押 | +|------|--------|--------| +| 奖励发放方式 | 手动调用 `updateRewardIndex` | 随时间自动发放 | +| Gas 成本 | 需额外支付奖励注入 Gas | 无额外注入成本 | +| 适用场景 | 不定期空投/奖励 | 常规 LP 挖矿 | diff --git "a/Unit4 DeFi/09 \347\250\263\345\256\232\344\272\244\346\215\242 AMM.md" "b/Unit4 DeFi/09 \347\250\263\345\256\232\344\272\244\346\215\242 AMM.md" new file mode 100644 index 0000000..df9a22e --- /dev/null +++ "b/Unit4 DeFi/09 \347\250\263\345\256\232\344\272\244\346\215\242 AMM.md" @@ -0,0 +1,317 @@ +# 稳定交换 AMM(StableSwap) + +稳定交换 AMM(StableSwap)是 Curve Finance 的核心算法,结合了恒定和(CSAMM)与恒定乘积(CPAMM)的优点,在价格接近 1:1 时滑点极低。通过牛顿迭代法求解不变量方程。 + +```solidity +// SPDX-License-Identifier: MIT // SPDX 许可证标识符:MIT 许可证 +pragma solidity ^0.8.26; // 版本指令,要求使用 Solidity 0.8.26 及以上版本 + +/* +不变量方程:An^n ∑x_i + D = ADn^n + D^(n+1) / (n^n ∏x_i) + +核心知识点: +0. 牛顿迭代法: x_(n+1) = x_n - f(x_n) / f'(x_n) +1. 不变量 D 的计算 +2. 交换(Swap)- 计算 Y 和 D +3. 虚拟价格 +4. 添加流动性 - 不平衡费率 +5. 移除流动性 +6. 单币移除流动性 - getYD +*/ + +library Math { + function abs(uint256 x, uint256 y) internal pure returns (uint256) { + return x >= y ? x - y : y - x; + } +} + +contract StableSwap { // 定义 StableSwap 合约 + uint256 private constant N = 3; // 代币数量 + // 放大系数 × N^(N-1),值越大曲线越平 + uint256 private constant A = 1000 * (N ** (N - 1)); + uint256 private constant SWAP_FEE = 300; // 0.03% 交换费率 + // 流动性费率由 2 个约束推导: + // 1. 在平衡池中添加/移除流动性时费率为 0 + // 2. 在平衡池中交换 ≈ 先添加再移除流动性 + uint256 private constant LIQUIDITY_FEE = (SWAP_FEE * N) / (4 * (N - 1)); + uint256 private constant FEE_DENOMINATOR = 1e6; // 费率分母 + + address[N] public tokens; // 代币地址列表 + // 精度乘数(将不同小数的代币统一到 18 位) + uint256[N] private multipliers = [1, 1e12, 1e12]; // DAI(18)、USDC(6)、USDT(6) + uint256[N] public balances; // 各代币余额 + + uint256 private constant DECIMALS = 18; + uint256 public totalSupply; // LP 代币总供应 + mapping(address => uint256) public balanceOf; // LP 代币余额 + + constructor(address[N] memory _tokens) { + tokens = _tokens; + } + + function _mint(address _to, uint256 _amount) private { + balanceOf[_to] += _amount; + totalSupply += _amount; + } + + function _burn(address _from, uint256 _amount) private { + balanceOf[_from] -= _amount; + totalSupply -= _amount; + } + + // _xp 函数:将余额调整为精度统一的 18 位值 + function _xp() private view returns (uint256[N] memory xp) { + for (uint256 i; i < N; ++i) { + xp[i] = balances[i] * multipliers[i]; // 乘以精度乘数 + } + } + + // _getD 函数:牛顿迭代法计算 D(平衡池中各代币之和) + function _getD(uint256[N] memory xp) private pure returns (uint256) { + uint256 a = A * N; // An^n + + uint256 s; // s = ∑x_i + for (uint256 i; i < N; ++i) { + s += xp[i]; + } + + // 牛顿迭代法,初始猜测 d ≤ s + uint256 d = s; + uint256 d_prev; + for (uint256 i; i < 255; ++i) { + // p = D^(n+1) / (n^n * ∏x_i) + uint256 p = d; + for (uint256 j; j < N; ++j) { + p = (p * d) / (N * xp[j]); + } + d_prev = d; + // D_(n+1) = (ADn^n + np) * D_n / ((A-1)D_n + (n+1)p) + d = ((a * s + N * p) * d) / ((a - 1) * d + (N + 1) * p); + + if (Math.abs(d, d_prev) <= 1) { + return d; // 收敛 + } + } + revert("D didn't converge"); + } + + // _getY 函数:已知代币 i 的新余额 x,计算代币 j 的新余额 y + function _getY(uint256 i, uint256 j, uint256 x, uint256[N] memory xp) + private pure returns (uint256) + { + uint256 a = A * N; + uint256 d = _getD(xp); + uint256 s; + uint256 c = d; + + uint256 _x; + for (uint256 k; k < N; ++k) { + if (k == i) { _x = x; } + else if (k == j) { continue; } + else { _x = xp[k]; } + + s += _x; + c = (c * d) / (N * _x); + } + c = (c * d) / (N * a); + uint256 b = s + d / a; + + // 牛顿迭代法求 y,初始猜测 y ≤ d + uint256 y_prev; + uint256 y = d; + for (uint256 _i; _i < 255; ++_i) { + y_prev = y; + // y_(n+1) = (y_n² + c) / (2y_n + b - D) + y = (y * y + c) / (2 * y + b - d); + if (Math.abs(y, y_prev) <= 1) { + return y; + } + } + revert("y didn't converge"); + } + + // getVirtualPrice 函数:估算每份额代币的价值 + function getVirtualPrice() external view returns (uint256) { + uint256 d = _getD(_xp()); + uint256 _totalSupply = totalSupply; + if (_totalSupply > 0) { + return (d * 10 ** DECIMALS) / _totalSupply; + } + return 0; + } + + // swap 函数:交换 dx 数量的 token i 为 token j + function swap(uint256 i, uint256 j, uint256 dx, uint256 minDy) + external returns (uint256 dy) + { + require(i != j, "i = j"); + + IERC20(tokens[i]).transferFrom(msg.sender, address(this), dx); + + uint256[N] memory xp = _xp(); + uint256 x = xp[i] + dx * multipliers[i]; + + uint256 y0 = xp[j]; + uint256 y1 = _getY(i, j, x, xp); + // y0 >= y1(因为 x 增加了),-1 向下取整 + dy = (y0 - y1 - 1) / multipliers[j]; + + // 扣除交换费 + uint256 fee = (dy * SWAP_FEE) / FEE_DENOMINATOR; + dy -= fee; + require(dy >= minDy, "dy < min"); + + balances[i] += dx; + balances[j] -= dy; + + IERC20(tokens[j]).transfer(msg.sender, dy); + } + + function addLiquidity(uint256[N] calldata amounts, uint256 minShares) + external returns (uint256 shares) + { + uint256 _totalSupply = totalSupply; + uint256 d0; + uint256[N] memory old_xs = _xp(); + if (_totalSupply > 0) { + d0 = _getD(old_xs); // 当前流动性 D + } + + // 转入代币并计算新的精度调整余额 + uint256[N] memory new_xs; + for (uint256 i; i < N; ++i) { + uint256 amount = amounts[i]; + if (amount > 0) { + IERC20(tokens[i]).transferFrom(msg.sender, address(this), amount); + new_xs[i] = old_xs[i] + amount * multipliers[i]; + } else { + new_xs[i] = old_xs[i]; + } + } + + uint256 d1 = _getD(new_xs); // 添加后的 D + require(d1 > d0, "liquidity didn't increase"); + + // 重新计算 D 以包含不平衡费 + uint256 d2; + if (_totalSupply > 0) { + for (uint256 i; i < N; ++i) { + uint256 idealBalance = (old_xs[i] * d1) / d0; // 理想比例 + uint256 diff = Math.abs(new_xs[i], idealBalance); + new_xs[i] -= (LIQUIDITY_FEE * diff) / FEE_DENOMINATOR; // 扣费 + } + d2 = _getD(new_xs); // 扣费后的 D + } else { + d2 = d1; // 首次添加无费率 + } + + for (uint256 i; i < N; ++i) { + balances[i] += amounts[i]; // 更新余额 + } + + // shares = (d2 - d0) / d0 * totalSupply + if (_totalSupply > 0) { + shares = ((d2 - d0) * _totalSupply) / d0; + } else { + shares = d2; // 首次添加 + } + require(shares >= minShares, "shares < min"); + _mint(msg.sender, shares); + } + + function removeLiquidity(uint256 shares, uint256[N] calldata minAmountsOut) + external returns (uint256[N] memory amountsOut) + { + uint256 _totalSupply = totalSupply; + + for (uint256 i; i < N; ++i) { + uint256 amountOut = (balances[i] * shares) / _totalSupply; + require(amountOut >= minAmountsOut[i], "out < min"); + balances[i] -= amountOut; + amountsOut[i] = amountOut; + IERC20(tokens[i]).transfer(msg.sender, amountOut); + } + + _burn(msg.sender, shares); + } + + // removeLiquidityOneToken 函数:单币移除流动性 + function removeLiquidityOneToken(uint256 shares, uint256 i, uint256 minAmountOut) + external returns (uint256 amountOut) + { + (amountOut,) = _calcWithdrawOneToken(shares, i); + require(amountOut >= minAmountOut, "out < min"); + + balances[i] -= amountOut; + _burn(msg.sender, shares); + IERC20(tokens[i]).transfer(msg.sender, amountOut); + } + + function _calcWithdrawOneToken(uint256 shares, uint256 i) + private view returns (uint256 dy, uint256 fee) + { + uint256 _totalSupply = totalSupply; + uint256[N] memory xp = _xp(); + + uint256 d0 = _getD(xp); + uint256 d1 = d0 - (d0 * shares) / _totalSupply; // 取出份额后的 D + + uint256 y0 = _getYD(i, xp, d1); + uint256 dy0 = (xp[i] - y0) / multipliers[i]; + + // 计算不平衡费 + uint256 dx; + for (uint256 j; j < N; ++j) { + if (j == i) { + dx = (xp[j] * d1) / d0 - y0; + } else { + dx = xp[j] - (xp[j] * d1) / d0; + } + xp[j] -= (LIQUIDITY_FEE * dx) / FEE_DENOMINATOR; + } + + uint256 y1 = _getYD(i, xp, d1); + dy = (xp[i] - y1 - 1) / multipliers[i]; + fee = dy0 - dy; + } + + // _getYD 函数:已知 D 计算代币 i 的新余额 + function _getYD(uint256 i, uint256[N] memory xp, uint256 d) + private pure returns (uint256) + { + uint256 a = A * N; + uint256 s; + uint256 c = d; + + for (uint256 k; k < N; ++k) { + if (k != i) { + _x = xp[k]; + s += _x; + c = (c * d) / (N * _x); + } + } + c = (c * d) / (N * a); + uint256 b = s + d / a; + + uint256 y_prev; + uint256 y = d; + for (uint256 _i; _i < 255; ++_i) { + y_prev = y; + y = (y * y + c) / (2 * y + b - d); + if (Math.abs(y, y_prev) <= 1) { return y; } + } + revert("y didn't converge"); + } +} + +interface IERC20 { /* 标准 ERC20 接口 */ } +``` + +## 三种 AMM 对比 + +| 特性 | CSAMM | CPAMM | StableSwap | +|------|-------|-------|------------| +| 不变量 | `∑x = k` | `∏x = k` | 混合曲线 | +| 滑点 | 零 | 中等 | 1:1 附近极低 | +| 适用场景 | 稳定币对 | 波动资产 | 锚定资产池 | +| 算法复杂度 | O(1) | O(1) | O(n²) 牛顿迭代 | diff --git "a/Unit4 DeFi/10 \350\264\250\346\212\274\345\245\226\345\212\261.md" "b/Unit4 DeFi/10 \350\264\250\346\212\274\345\245\226\345\212\261.md" new file mode 100644 index 0000000..efde134 --- /dev/null +++ "b/Unit4 DeFi/10 \350\264\250\346\212\274\345\245\226\345\212\261.md" @@ -0,0 +1,139 @@ +# 质押奖励(Staking Rewards) + +基于 Synthetix 的连续质押奖励合约。奖励随时间线性释放,用户根据质押份额按比例获得奖励。 + +```solidity +// SPDX-License-Identifier: MIT // SPDX 许可证标识符:MIT 许可证 +pragma solidity ^0.8.26; // 版本指令,要求使用 Solidity 0.8.26 及以上版本 + +// StakingRewards 合约:连续时间质押奖励(Synthetix 风格) +contract StakingRewards { // 定义 StakingRewards 合约 + IERC20 public immutable stakingToken; // 质押代币 + IERC20 public immutable rewardsToken; // 奖励代币 + + address public owner; // 合约所有者 + uint256 public duration; // 奖励发放周期(秒) + uint256 public finishAt; // 奖励结束时间戳 + uint256 public updatedAt; // 上次更新时间戳 + uint256 public rewardRate; // 每秒发放的奖励数量 + uint256 public rewardPerTokenStored; // 每单位质押量的累计奖励 + + mapping(address => uint256) public userRewardPerTokenPaid; // 用户已结算的奖励指数 + mapping(address => uint256) public rewards; // 用户待领取奖励 + + uint256 public totalSupply; // 总质押量 + mapping(address => uint256) public balanceOf; // 各用户质押量 + + constructor(address _stakingToken, address _rewardToken) { + owner = msg.sender; + stakingToken = IERC20(_stakingToken); + rewardsToken = IERC20(_rewardToken); + } + + modifier onlyOwner() { + require(msg.sender == owner, "not authorized"); + _; + } + + // updateReward 修饰符:在执行前更新全局和用户奖励状态 + modifier updateReward(address _account) { + rewardPerTokenStored = rewardPerToken(); // 更新全局每 token 奖励 + updatedAt = lastTimeRewardApplicable(); // 更新时间戳 + + if (_account != address(0)) { + rewards[_account] = earned(_account); // 结算未领取奖励 + userRewardPerTokenPaid[_account] = rewardPerTokenStored; // 更新用户指数 + } + _; + } + + function lastTimeRewardApplicable() public view returns (uint256) { + return _min(finishAt, block.timestamp); // 取结束时间与当前时间的较小值 + } + + // rewardPerToken 函数:查询全局每单位质押量的累计奖励 + function rewardPerToken() public view returns (uint256) { + if (totalSupply == 0) { + return rewardPerTokenStored; + } + return rewardPerTokenStored + + (rewardRate * (lastTimeRewardApplicable() - updatedAt) * 1e18) // 新增奖励 * 精度 + / totalSupply; // 除以总质押量 + } + + function stake(uint256 _amount) external updateReward(msg.sender) { + require(_amount > 0, "amount = 0"); + stakingToken.transferFrom(msg.sender, address(this), _amount); + balanceOf[msg.sender] += _amount; + totalSupply += _amount; + } + + function withdraw(uint256 _amount) external updateReward(msg.sender) { + require(_amount > 0, "amount = 0"); + balanceOf[msg.sender] -= _amount; + totalSupply -= _amount; + stakingToken.transfer(msg.sender, _amount); + } + + // earned 函数:查询用户已赚取的奖励 + function earned(address _account) public view returns (uint256) { + return ( + ( + balanceOf[_account] + * (rewardPerToken() - userRewardPerTokenPaid[_account]) // 质押量 * 未结算指数差 + ) / 1e18 + ) + rewards[_account]; // + 已结算待领取 + } + + function getReward() external updateReward(msg.sender) { + uint256 reward = rewards[msg.sender]; + if (reward > 0) { + rewards[msg.sender] = 0; + rewardsToken.transfer(msg.sender, reward); + } + } + + function setRewardsDuration(uint256 _duration) external onlyOwner { + require(finishAt < block.timestamp, "reward duration not finished"); + duration = _duration; + } + + // notifyRewardAmount 函数:注入新奖励 + function notifyRewardAmount(uint256 _amount) + external + onlyOwner + updateReward(address(0)) + { + if (block.timestamp >= finishAt) { + rewardRate = _amount / duration; // 新奖励速率 + } else { + // 合并剩余奖励 + uint256 remainingRewards = (finishAt - block.timestamp) * rewardRate; + rewardRate = (_amount + remainingRewards) / duration; + } + require(rewardRate > 0, "reward rate = 0"); + require( + rewardRate * duration <= rewardsToken.balanceOf(address(this)), + "reward amount > balance" + ); + finishAt = block.timestamp + duration; + updatedAt = block.timestamp; + } + + function _min(uint256 x, uint256 y) private pure returns (uint256) { + return x <= y ? x : y; + } +} + +interface IERC20 { /* 标准 ERC20 接口 */ } +``` + +## 关键机制 + +| 变量 | 含义 | +|------|------| +| `rewardPerToken` | 每单位质押的累计奖励(不断增长) | +| `userRewardPerTokenPaid` | 用户上次结算时的 `rewardPerToken` 值 | +| `earned` | 用户未领取奖励 = 质押量 * (当前指数 - 用户指数) + 已结算 | + +**奖励释放**:`notifyRewardAmount` 设置 `rewardRate`,奖励在 `duration` 秒内线性释放。如果前一轮奖励尚未结束,剩余部分会与新一轮合并。 diff --git "a/Unit4 DeFi/11 \344\273\243\345\270\201\351\224\201.md" "b/Unit4 DeFi/11 \344\273\243\345\270\201\351\224\201.md" new file mode 100644 index 0000000..1e15627 --- /dev/null +++ "b/Unit4 DeFi/11 \344\273\243\345\270\201\351\224\201.md" @@ -0,0 +1,116 @@ +# 代币锁合约 + +代币锁(Token Lock)合约按线性释放(vesting)计划逐步解锁代币。用户可以随时查询当前可领取的代币数量。 + +```solidity +// SPDX-License-Identifier: MIT // SPDX 许可证标识符:MIT 许可证 +pragma solidity 0.8.26; // 版本指令,要求使用 Solidity 0.8.26 版本 + +import {IERC20} from "./IERC20.sol"; + +// Auth 合约:权限管理基础合约 +contract Auth { // 定义 Auth 合约 + mapping(address => bool) public authorized; // 授权映射 + + modifier auth() { + require(authorized[msg.sender], "not authorized"); + _; + } + + constructor() { + authorized[msg.sender] = true; // 部署者初始授权 + } + + function allow(address user) external auth { authorized[user] = true; } + function deny(address user) external auth { authorized[user] = false; } +} + +// TokenLock 合约:代币线性解锁合约 +contract TokenLock is Auth { // 继承 Auth 合约 + struct Lock { // 锁仓结构体 + uint256 amount; // 锁定金额 + uint32 updatedAt; // 上次更新时间戳 + uint32 expiresAt; // 过期时间戳 + uint32 duration; // 锁定期(秒) + } + + mapping(address => Lock) public locks; // 代币地址 → 锁仓信息 + mapping(address => uint256) public freed; // 代币地址 → 已释放待领取的金额 + + function get(address token) external view returns (Lock memory) { + return locks[token]; + } + + function set(address token, uint32 duration) external auth { + locks[token].duration = duration; + } + + // unlocked 函数:计算当前已解锁的代币数量(线性释放) + function unlocked(address token) public view returns (uint256) { + Lock memory l = locks[token]; + // 若已完全过期,所有金额均解锁 + if (block.timestamp >= l.expiresAt) { + return l.amount; + } + // 线性释放: 已解锁 = 总金额 * 已过时间 / 总锁定期 + return l.amount * (block.timestamp - uint256(l.updatedAt)) + / uint256(l.expiresAt - l.updatedAt); + } + + // claimable 函数:查询总共可领取的代币数量 + function claimable(address token) public view returns (uint256) { + return freed[token] + unlocked(token); // 已释放 + 已解锁 + } + + // lock 函数:锁定新代币 + function lock(address token, uint256 amount) external auth { + uint256 free = unlocked(token); // 获取已解锁部分 + + Lock storage l = locks[token]; + l.amount -= free; // 从总额中扣除已解锁部分 + l.updatedAt = uint32(block.timestamp); + l.expiresAt = uint32(block.timestamp) + l.duration; // 新的过期时间 + freed[token] += free; // 将已解锁的转入 freely 池 + + if (amount > 0) { + IERC20(token).transferFrom(msg.sender, address(this), amount); // 转入新代币 + l.amount += amount; // 增加锁定量 + } + } + + // unlock 函数:提取已解锁/已释放的代币 + function unlock(address token) external auth returns (uint256 amount) { + uint256 free = unlocked(token); + + Lock storage l = locks[token]; + l.amount -= free; + l.updatedAt = uint32(block.timestamp); + + amount = freed[token] + free; // = 已释放 + 本次解锁 + freed[token] = 0; + + if (amount > 0) { + IERC20(token).transfer(msg.sender, amount); + } + } + + // sync 函数:同步账上余额(处理直接转入合约的代币) + function sync(address token) external auth { + uint256 reserved = freed[token] + locks[token].amount; + uint256 bal = IERC20(token).balanceOf(address(this)); + if (bal > reserved) { + IERC20(token).transfer(msg.sender, bal - reserved); // 转出多余余额 + } + } +} +``` + +## 线性释放公式 + +``` +已解锁量 = 锁定总量 × (当前时间 - 开始时间) / 过期时间 +``` + +- **lock()**: 每次锁定新代币时,先结算旧的已解锁部分,然后以新金额创建新的锁定 +- **unlock()**: 提取已解锁和之前释放的代币 +- 支持同时锁定多种代币(以 token 地址为 key) diff --git "a/Unit4 DeFi/12 \351\207\221\345\272\223.md" "b/Unit4 DeFi/12 \351\207\221\345\272\223.md" new file mode 100644 index 0000000..61585e1 --- /dev/null +++ "b/Unit4 DeFi/12 \351\207\221\345\272\223.md" @@ -0,0 +1,70 @@ +# 金库合约(Vault) + +金库合约(Vault)是收益聚合器的核心组件。用户存入代币后获得份额代币,金库通过策略赚取收益,用户的份额价值随之增长。 + +```solidity +// SPDX-License-Identifier: MIT // SPDX 许可证标识符:MIT 许可证 +pragma solidity ^0.8.26; // 版本指令,要求使用 Solidity 0.8.26 及以上版本 + +// Vault 合约:收益聚合金库 +contract Vault { // 定义 Vault 合约 + IERC20 public immutable token; // 金库的底层代币 + + uint256 public totalSupply; // 份额总供应 + mapping(address => uint256) public balanceOf; // 用户份额余额 + + constructor(address _token) { + token = IERC20(_token); + } + + function _mint(address _to, uint256 _shares) private { + totalSupply += _shares; + balanceOf[_to] += _shares; + } + + function _burn(address _from, uint256 _shares) private { + totalSupply -= _shares; + balanceOf[_from] -= _shares; + } + + // deposit 函数:存入代币获取份额 + function deposit(uint256 _amount) external { + // a = 存入金额 + // B = 存入前金库代币余额 + // T = 总份额 + // s = 铸造份额 + // (T + s) / T = (a + B) / B → s = aT / B + uint256 shares; + if (totalSupply == 0) { + shares = _amount; // 首次存款,份额 = 金额 + } else { + shares = (_amount * totalSupply) / token.balanceOf(address(this)); + } + + _mint(msg.sender, shares); + token.transferFrom(msg.sender, address(this), _amount); + } + + // withdraw 函数:销毁份额取回代币 + function withdraw(uint256 _shares) external { + // a = 取出金额 + // B = 取出前金库代币余额 + // T = 总份额 + // s = 销毁份额 + // (T - s) / T = (B - a) / B → a = sB / T + uint256 amount = + (_shares * token.balanceOf(address(this))) / totalSupply; + _burn(msg.sender, _shares); + token.transfer(msg.sender, amount); + } +} + +interface IERC20 { /* 标准 ERC20 接口 */ } +``` + +## 核心机制 + +金库模型的核心优势在于: +- **份额代币升值**:收益留存于金库中(而非增发新份额),使得单位份额对应的底层资产不断增加 +- **复利效应**:收益自动累积,不需要用户定期复投 +- **公式推导**:`(T ± s) / T = (B ± a) / B` 确保份额与资产同比例变化 diff --git "a/Unit4 DeFi/13 Uniswap V2 \351\227\252\347\224\265\344\272\244\346\215\242.md" "b/Unit4 DeFi/13 Uniswap V2 \351\227\252\347\224\265\344\272\244\346\215\242.md" new file mode 100644 index 0000000..4902912 --- /dev/null +++ "b/Unit4 DeFi/13 Uniswap V2 \351\227\252\347\224\265\344\272\244\346\215\242.md" @@ -0,0 +1,92 @@ +# Uniswap V2 闪电交换 + +Uniswap V2 的闪电交换(Flash Swap)允许用户"先使用后支付",可以在不持有本金的情况下进行套利等操作。用户在一次交易中借出代币、使用代币、然后归还本金+手续费。 + +```solidity +// SPDX-License-Identifier: MIT // SPDX 许可证标识符:MIT 许可证 +pragma solidity ^0.8.26; // 版本指令,要求使用 Solidity 0.8.26 及以上版本 + +// IUniswapV2Callee 接口:闪电交换回调接口 +interface IUniswapV2Callee { + function uniswapV2Call( + address sender, uint256 amount0, uint256 amount1, bytes calldata data + ) external; +} + +// UniswapV2FlashSwap 合约:Uniswap V2 闪电交换实现 +contract UniswapV2FlashSwap is IUniswapV2Callee { // 实现回调接口 + // Uniswap V2 工厂合约地址 + address private constant UNISWAP_V2_FACTORY = + 0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f; + + address private constant DAI = 0x6B175474E89094C44Da98b954EedeAC495271d0F; + address private constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; + + IUniswapV2Factory private constant factory = + IUniswapV2Factory(UNISWAP_V2_FACTORY); + IERC20 private constant weth = IERC20(WETH); + IUniswapV2Pair private immutable pair; // DAI-WETH 交易对 + + uint256 public amountToRepay; // 需偿还的总金额(本金+手续费) + + constructor() { + pair = IUniswapV2Pair(factory.getPair(DAI, WETH)); // 获取交易对地址 + } + + // flashSwap 函数:发起闪电交换 + function flashSwap(uint256 wethAmount) external { + // 编码回调数据(借入的代币和调用者地址) + bytes memory data = abi.encode(WETH, msg.sender); + + // amount0Out = DAI 输出(0),amount1Out = WETH 输出(借入量) + pair.swap(0, wethAmount, address(this), data); + } + + // uniswapV2Call 函数:闪电交换回调(由交易对合约调用) + function uniswapV2Call( + address sender, + uint256 amount0, + uint256 amount1, + bytes calldata data + ) external { + require(msg.sender == address(pair), "not pair"); + require(sender == address(this), "not sender"); + + (address tokenBorrow, address caller) = + abi.decode(data, (address, address)); + + // 在此处编写套利或其他自定义逻辑 + require(tokenBorrow == WETH, "token borrow != WETH"); + + // 计算手续费(约 0.3%),+1 向上取整 + uint256 fee = (amount1 * 3) / 997 + 1; + amountToRepay = amount1 + fee; // 借入量 + 手续费 + + // 从调用者转入手续费(调用者需提前授权) + weth.transferFrom(caller, address(this), fee); + + // 归还本金 + 手续费给交易对 + weth.transfer(address(pair), amountToRepay); + } +} + +interface IUniswapV2Pair { + function swap(uint256 amount0Out, uint256 amount1Out, address to, bytes calldata data) external; +} +interface IUniswapV2Factory { + function getPair(address tokenA, address tokenB) external view returns (address pair); +} +interface IERC20 { /* 标准 ERC20 接口 */ } +interface IWETH is IERC20 { + function deposit() external payable; + function withdraw(uint256 amount) external; +} +``` + +## 闪电交换 vs 闪电贷 + +| 特性 | Uniswap V2 闪电交换 | 传统闪电贷 | +|------|-------------------|----------| +| 调用入口 | `pair.swap(0, amount, to, data)` | 借贷池合约 | +| 手续费 | 0.3% Uniswap 手续费 | 平台费(如 Aave 0.09%)| +| 回调函数 | `uniswapV2Call` | 合约特定函数 | diff --git "a/Unit4 DeFi/14 Uniswap V2 \346\234\200\344\274\230\345\215\225\350\276\271\344\276\233\347\273\231.md" "b/Unit4 DeFi/14 Uniswap V2 \346\234\200\344\274\230\345\215\225\350\276\271\344\276\233\347\273\231.md" new file mode 100644 index 0000000..8b8b8ff --- /dev/null +++ "b/Unit4 DeFi/14 Uniswap V2 \346\234\200\344\274\230\345\215\225\350\276\271\344\276\233\347\273\231.md" @@ -0,0 +1,113 @@ +# Uniswap V2 最优单边供给 + +当用户只持有交易对中的一种代币时,可通过计算最优交换量,将部分代币先兑换为另一种代币,再以最优比例添加流动性。 + +```solidity +// SPDX-License-Identifier: MIT // SPDX 许可证标识符:MIT 许可证 +pragma solidity ^0.8.26; // 版本指令,要求使用 Solidity 0.8.26 及以上版本 + +// TestUniswapOptimalOneSidedSupply 合约:单边最优添加流动性 +contract TestUniswapOptimalOneSidedSupply { // 定义最优单边供给合约 + address private constant FACTORY = 0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f; + address private constant ROUTER = 0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D; + address private constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; + + // sqrt 函数:牛顿迭代法求平方根 + function sqrt(uint256 y) private pure returns (uint256 z) { + if (y > 3) { + z = y; + uint256 x = y / 2 + 1; + while (x < z) { + z = x; + x = (y / x + x) / 2; + } + } else if (y != 0) { + z = 1; + } + } + + // getSwapAmount 函数:计算最优交换量 + // s = 最优交换量 + // r = 代币 a 的储备量 + // a = 用户当前持有代币 a 的数量(尚未添加到储备) + // f = 手续费百分比 (0.3%) + // s = (sqrt(((2 - f)r)^2 + 4(1 - f)ar) - (2 - f)r) / (2(1 - f)) + function getSwapAmount(uint256 r, uint256 a) + public + pure + returns (uint256) + { + // 代入 f = 0.003 化简后的常量 + return (sqrt(r * (r * 3988009 + a * 3988000)) - r * 1997) / 1994; + } + + // zap 函数:一键最优单边添加流动性 + // 步骤:1. 交换最优数量的 tokenA → tokenB + // 2. 将剩余的两种代币添加流动性 + function zap(address _tokenA, address _tokenB, uint256 _amountA) external { + require(_tokenA == WETH || _tokenB == WETH, "!weth"); + + IERC20(_tokenA).transferFrom(msg.sender, address(this), _amountA); + + address pair = IUniswapV2Factory(FACTORY).getPair(_tokenA, _tokenB); + (uint256 reserve0, uint256 reserve1,) = + IUniswapV2Pair(pair).getReserves(); + + uint256 swapAmount; + if (IUniswapV2Pair(pair).token0() == _tokenA) { + swapAmount = getSwapAmount(reserve0, _amountA); // 用 reserve0 计算 + } else { + swapAmount = getSwapAmount(reserve1, _amountA); // 用 reserve1 计算 + } + + _swap(_tokenA, _tokenB, swapAmount); // 步骤 1: 交换 + _addLiquidity(_tokenA, _tokenB); // 步骤 2: 添加流动性 + } + + function _swap(address _from, address _to, uint256 _amount) internal { + IERC20(_from).approve(ROUTER, _amount); + address[] memory path = new address[](2); + path[0] = _from; + path[1] = _to; + IUniswapV2Router(ROUTER).swapExactTokensForTokens( + _amount, 1, path, address(this), block.timestamp + ); + } + + function _addLiquidity(address _tokenA, address _tokenB) internal { + uint256 balA = IERC20(_tokenA).balanceOf(address(this)); + uint256 balB = IERC20(_tokenB).balanceOf(address(this)); + IERC20(_tokenA).approve(ROUTER, balA); + IERC20(_tokenB).approve(ROUTER, balB); + IUniswapV2Router(ROUTER).addLiquidity( + _tokenA, _tokenB, balA, balB, 0, 0, address(this), block.timestamp + ); + } +} + +interface IUniswapV2Router { + function addLiquidity(address, address, uint256, uint256, uint256, uint256, address, uint256) + external returns (uint256, uint256, uint256); + function swapExactTokensForTokens(uint256, uint256, address[] calldata, address, uint256) + external returns (uint256[] memory); +} +interface IUniswapV2Factory { + function getPair(address, address) external view returns (address); +} +interface IUniswapV2Pair { + function token0() external view returns (address); + function token1() external view returns (address); + function getReserves() external view returns (uint112, uint112, uint32); +} +interface IERC20 { /* 标准 ERC20 接口 */ } +``` + +## 最优交换量推导 + +恒定乘积 AMM 中,若只持有代币 A(数量 a),要最大化添加的流动性,需要找到最优的交换量 s: + +``` +s = (sqrt(((2-f)r)² + 4(1-f)ar) - (2-f)r) / (2(1-f)) +``` + +其中 r 是代币 A 的储备量,f = 0.003(0.3% 手续费)。 diff --git "a/Unit4 DeFi/15 Uniswap V3 \344\272\244\346\215\242.md" "b/Unit4 DeFi/15 Uniswap V3 \344\272\244\346\215\242.md" new file mode 100644 index 0000000..e0fa2d2 --- /dev/null +++ "b/Unit4 DeFi/15 Uniswap V3 \344\272\244\346\215\242.md" @@ -0,0 +1,205 @@ +# Uniswap V3 交换 + +Uniswap V3 交换示例,包括单跳交换、多跳交换(ExactInput/ExactOutput),以及使用 SwapRouter 和 SwapRouter02 的不同方式。 + +```solidity +// SPDX-License-Identifier: MIT // SPDX 许可证标识符:MIT 许可证 +pragma solidity ^0.8.26; // 版本指令,要求使用 Solidity 0.8.26 及以上版本 + +// ===== SwapRouter02 版本(推荐)===== + +address constant SWAP_ROUTER_02 = 0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45; // 通用路由器 +address constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; +address constant DAI = 0x6B175474E89094C44Da98b954EedeAC495271d0F; +address constant USDC = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48; + +// UniswapV3SingleHopSwap 合约:单跳交换(SwapRouter02) +contract UniswapV3SingleHopSwap { // 定义单跳交换合约 + ISwapRouter02 private constant router = ISwapRouter02(SWAP_ROUTER_02); + IERC20 private constant weth = IERC20(WETH); + IERC20 private constant dai = IERC20(DAI); + + // swapExactInputSingleHop 函数:精确输入的单跳交换 + function swapExactInputSingleHop(uint256 amountIn, uint256 amountOutMin) + external + { + weth.transferFrom(msg.sender, address(this), amountIn); // 转入输入代币 + weth.approve(address(router), amountIn); // 授权路由器 + + ISwapRouter02.ExactInputSingleParams memory params = ISwapRouter02 + .ExactInputSingleParams({ + tokenIn: WETH, // 输入代币 + tokenOut: DAI, // 输出代币 + fee: 3000, // 费率等级(3000 = 0.3%) + recipient: msg.sender, // 输出代币接收者 + amountIn: amountIn, // 精确输入量 + amountOutMinimum: amountOutMin, // 最小输出量(滑点保护) + sqrtPriceLimitX96: 0 // 价格限制(0 = 无限制) + }); + + router.exactInputSingle(params); + } + + // swapExactOutputSingleHop 函数:精确输出的单跳交换 + function swapExactOutputSingleHop(uint256 amountOut, uint256 amountInMax) + external + { + weth.transferFrom(msg.sender, address(this), amountInMax); + weth.approve(address(router), amountInMax); + + ISwapRouter02.ExactOutputSingleParams memory params = ISwapRouter02 + .ExactOutputSingleParams({ + tokenIn: WETH, + tokenOut: DAI, + fee: 3000, + recipient: msg.sender, + amountOut: amountOut, // 精确输出量 + amountInMaximum: amountInMax, // 最大输入量(滑点保护) + sqrtPriceLimitX96: 0 + }); + + uint256 amountIn = router.exactOutputSingle(params); + + // 退还多转入的代币 + if (amountIn < amountInMax) { + weth.approve(address(router), 0); + weth.transfer(msg.sender, amountInMax - amountIn); + } + } +} + +// UniswapV3MultiHopSwap 合约:多跳交换(通过中间代币路由) +contract UniswapV3MultiHopSwap { // 定义多跳交换合约 + ISwapRouter02 private constant router = ISwapRouter02(SWAP_ROUTER_02); + IERC20 private constant weth = IERC20(WETH); + IERC20 private constant dai = IERC20(DAI); + + // swapExactInputMultiHop 函数:精确输入的多跳交换(WETH → USDC → DAI) + function swapExactInputMultiHop(uint256 amountIn, uint256 amountOutMin) + external + { + weth.transferFrom(msg.sender, address(this), amountIn); + weth.approve(address(router), amountIn); + + // Path 编码格式: [token0, fee1, token1, fee2, token2, ...] + bytes memory path = + abi.encodePacked(WETH, uint24(3000), USDC, uint24(100), DAI); + + ISwapRouter02.ExactInputParams memory params = ISwapRouter02 + .ExactInputParams({ + path: path, + recipient: msg.sender, + amountIn: amountIn, + amountOutMinimum: amountOutMin + }); + + router.exactInput(params); + } + + // swapExactOutputMultiHop 函数:精确输出的多跳交换(DAI → USDC → WETH) + function swapExactOutputMultiHop(uint256 amountOut, uint256 amountInMax) + external + { + weth.transferFrom(msg.sender, address(this), amountInMax); + weth.approve(address(router), amountInMax); + + bytes memory path = + abi.encodePacked(DAI, uint24(100), USDC, uint24(3000), WETH); + + ISwapRouter02.ExactOutputParams memory params = ISwapRouter02 + .ExactOutputParams({ + path: path, + recipient: msg.sender, + amountOut: amountOut, + amountInMaximum: amountInMax + }); + + uint256 amountIn = router.exactOutput(params); + + // 退还多余输入 + if (amountIn < amountInMax) { + weth.approve(address(router), 0); + weth.transfer(msg.sender, amountInMax - amountIn); + } + } +} + +// ===== SwapRouter (V1) 版本 ===== + +// UniswapV3SwapExamples 合约:旧版路由器示例 +contract UniswapV3SwapExamples { // 定义旧版交换示例 + ISwapRouter constant router = ISwapRouter(0xE592427A0AEce92De3Edee1F18E0157C05861564); + + function swapExactInputSingleHop(address tokenIn, address tokenOut, uint24 poolFee, uint256 amountIn) + external returns (uint256 amountOut) + { + IERC20(tokenIn).transferFrom(msg.sender, address(this), amountIn); + IERC20(tokenIn).approve(address(router), amountIn); + // ...调用 router.exactInputSingle(params) + } + + function swapExactInputMultiHop(bytes calldata path, address tokenIn, uint256 amountIn) + external returns (uint256 amountOut) + { + IERC20(tokenIn).transferFrom(msg.sender, address(this), amountIn); + IERC20(tokenIn).approve(address(router), amountIn); + // ...调用 router.exactInput(params) + } +} + +// ===== 接口定义 ===== + +interface ISwapRouter02 { + struct ExactInputSingleParams { + address tokenIn; address tokenOut; uint24 fee; address recipient; + uint256 amountIn; uint256 amountOutMinimum; uint160 sqrtPriceLimitX96; + } + function exactInputSingle(ExactInputSingleParams calldata params) external payable returns (uint256 amountOut); + + struct ExactOutputSingleParams { + address tokenIn; address tokenOut; uint24 fee; address recipient; + uint256 amountOut; uint256 amountInMaximum; uint160 sqrtPriceLimitX96; + } + function exactOutputSingle(ExactOutputSingleParams calldata params) external payable returns (uint256 amountIn); + + struct ExactInputParams { + bytes path; address recipient; uint256 amountIn; uint256 amountOutMinimum; + } + function exactInput(ExactInputParams calldata params) external payable returns (uint256 amountOut); + + struct ExactOutputParams { + bytes path; address recipient; uint256 amountOut; uint256 amountInMaximum; + } + function exactOutput(ExactOutputParams calldata params) external payable returns (uint256 amountIn); +} + +interface ISwapRouter { + function exactInputSingle(ExactInputSingleParams calldata) external payable returns (uint256); + function exactInput(ExactInputParams calldata) external payable returns (uint256); +} + +interface IERC20 { /* 标准 ERC20 接口 */ } +interface IWETH is IERC20 { + function deposit() external payable; + function withdraw(uint256 amount) external; +} +``` + +## Path 编码格式 + +多跳交换的路径编码方式: + +``` +[token0(20B)] [fee1(3B)] [token1(20B)] [fee2(3B)] [token2(20B)] ... +``` + +每个步骤为 `(token + fee)`,最终以输出代币结尾,解码后得到完整的交换路径。 + +## V3 费率等级 + +| 费率 | 值 | 适用场景 | +|------|-----|----------| +| 0.01% | 100 | 稳定币对 | +| 0.05% | 500 | 稳定币/蓝筹资产 | +| 0.30% | 3000 | 标准交易对 | +| 1.00% | 10000 | 波动性资产 | diff --git "a/Unit4 DeFi/16 Uniswap V3 \351\227\252\347\224\265\344\272\244\346\215\242.md" "b/Unit4 DeFi/16 Uniswap V3 \351\227\252\347\224\265\344\272\244\346\215\242.md" new file mode 100644 index 0000000..e42632e --- /dev/null +++ "b/Unit4 DeFi/16 Uniswap V3 \351\227\252\347\224\265\344\272\244\346\215\242.md" @@ -0,0 +1,128 @@ +# Uniswap V3 闪电交换 + +Uniswap V3 闪电交换(Flash Swap)利用不同费率等级池之间的价格差进行套利。在池 0 借出 WETH → 在池 1 将 WETH 换回 DAI → 归还池 0 + 赚取差价。 + +```solidity +// SPDX-License-Identifier: MIT // SPDX 许可证标识符:MIT 许可证 +pragma solidity ^0.8.26; // 版本指令,要求使用 Solidity 0.8.26 及以上版本 + +address constant SWAP_ROUTER_02 = 0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45; + +// UniswapV3FlashSwap 合约:利用 V3 不同费率池进行套利 +contract UniswapV3FlashSwap { // 定义闪电交换套利合约 + ISwapRouter02 constant router = ISwapRouter02(SWAP_ROUTER_02); + + uint160 private constant MIN_SQRT_RATIO = 4295128739; + uint160 private constant MAX_SQRT_RATIO = + 1461446703485210103287273052203988822378723970342; + + // 套利逻辑: + // 1. 在 pool0 借出 WETH(闪电交换) + // 2. 在 pool1 将 WETH 换为 DAI + // 3. 将 DAI 归还 pool0 + // 利润 = pool1 换得的 DAI - pool0 需归还的 DAI + + function flashSwap( + address pool0, + uint24 fee1, + address tokenIn, + address tokenOut, + uint256 amountIn + ) external { + bool zeroForOne = tokenIn < tokenOut; + // zeroForOne: 0→1(价格平方根下降) + // !zeroForOne: 1→0(价格平方根上升) + uint160 sqrtPriceLimitX96 = + zeroForOne ? MIN_SQRT_RATIO + 1 : MAX_SQRT_RATIO - 1; + + bytes memory data = abi.encode( + msg.sender, pool0, fee1, tokenIn, tokenOut, amountIn, zeroForOne + ); + + // 在 pool0 发起闪电交换 + IUniswapV3Pool(pool0).swap({ + recipient: address(this), + zeroForOne: zeroForOne, + amountSpecified: int256(amountIn), // 正数 = 精确输入(借入 tokenIn) + sqrtPriceLimitX96: sqrtPriceLimitX96, + data: data + }); + } + + // _swap 内部函数:通过 SwapRouter02 在 pool1 执行交换 + function _swap( + address tokenIn, address tokenOut, + uint24 fee, uint256 amountIn, uint256 amountOutMin + ) private returns (uint256 amountOut) { + IERC20(tokenIn).approve(address(router), amountIn); + ISwapRouter02.ExactInputSingleParams memory params = ISwapRouter02 + .ExactInputSingleParams({ + tokenIn: tokenIn, tokenOut: tokenOut, fee: fee, + recipient: address(this), amountIn: amountIn, + amountOutMinimum: amountOutMin, sqrtPriceLimitX96: 0 + }); + amountOut = router.exactInputSingle(params); + } + + // uniswapV3SwapCallback 函数:闪电交换回调 + function uniswapV3SwapCallback( + int256 amount0, int256 amount1, bytes calldata data + ) external { + // 解码回调数据 + (address caller, address pool0, uint24 fee1, + address tokenIn, address tokenOut, + uint256 amountIn, bool zeroForOne + ) = abi.decode( + data, (address, address, uint24, address, address, uint256, bool) + ); + + uint256 amountOut = zeroForOne ? uint256(-amount1) : uint256(-amount0); + + // 在 pool1 将借出的代币换回 + uint256 buyBackAmount = _swap({ + tokenIn: tokenOut, + tokenOut: tokenIn, + fee: fee1, + amountIn: amountOut, + amountOutMin: amountIn // 至少换回借入量 + }); + + // 归还 pool0 + uint256 profit = buyBackAmount - amountIn; + require(profit > 0, "profit = 0"); + + IERC20(tokenIn).transfer(pool0, amountIn); // 归还本金 + IERC20(tokenIn).transfer(caller, profit); // 利润转给调用者 + } +} + +interface ISwapRouter02 { + struct ExactInputSingleParams { + address tokenIn; address tokenOut; uint24 fee; address recipient; + uint256 amountIn; uint256 amountOutMinimum; uint160 sqrtPriceLimitX96; + } + function exactInputSingle(ExactInputSingleParams calldata params) external payable returns (uint256); +} + +interface IUniswapV3Pool { + function swap(address recipient, bool zeroForOne, int256 amountSpecified, + uint160 sqrtPriceLimitX96, bytes calldata data) + external returns (int256 amount0, int256 amount1); +} + +interface IERC20 { /* 标准 ERC20 接口 */ } +``` + +## 套利流程 + +``` +pool0 (0.3% fee) pool1 (0.05% fee) + |------------ flash swap --------->| + 借出 WETH 将 WETH 换为 DAI + | | + 归还 DAI <------------ DAI 换回 + | | + 利润 = 换回 DAI - 归还 DAI +``` + +利用不同费率池的价格差异,可以实现无本金套利。 diff --git "a/Unit4 DeFi/17 Uniswap V3 \351\227\252\347\224\265\350\264\267.md" "b/Unit4 DeFi/17 Uniswap V3 \351\227\252\347\224\265\350\264\267.md" new file mode 100644 index 0000000..81ded3b --- /dev/null +++ "b/Unit4 DeFi/17 Uniswap V3 \351\227\252\347\224\265\350\264\267.md" @@ -0,0 +1,82 @@ +# Uniswap V3 闪电贷 + +Uniswap V3 支持原生的闪电贷(Flash)功能,允许用户在单笔交易中借出任意数量的代币,使用后归还本金和手续费。 + +```solidity +// SPDX-License-Identifier: MIT // SPDX 许可证标识符:MIT 许可证 +pragma solidity ^0.8.26; // 版本指令,要求使用 Solidity 0.8.26 及以上版本 + +// UniswapV3Flash 合约:Uniswap V3 闪电贷示例 +contract UniswapV3Flash { // 定义 UniswapV3Flash 合约 + struct FlashCallbackData { // 回调数据结构 + uint256 amount0; // 借入的 token0 数量 + uint256 amount1; // 借入的 token1 数量 + address caller; // 发起调用者(用于归还手续费) + } + + IUniswapV3Pool private immutable pool; // Uniswap V3 池 + IERC20 private immutable token0; // 代币 0 + IERC20 private immutable token1; // 代币 1 + + constructor(address _pool) { + pool = IUniswapV3Pool(_pool); + token0 = IERC20(pool.token0()); // 从池中读取代币地址 + token1 = IERC20(pool.token1()); + } + + // flash 函数:发起闪电贷 + function flash(uint256 amount0, uint256 amount1) external { + bytes memory data = abi.encode( + FlashCallbackData({ + amount0: amount0, + amount1: amount1, + caller: msg.sender + }) + ); + // 调用池合约的 flash 函数 + IUniswapV3Pool(pool).flash(address(this), amount0, amount1, data); + } + + // uniswapV3FlashCallback 函数:闪电贷回调 + function uniswapV3FlashCallback( + uint256 fee0, // token0 的手续费(池费率 × 借入量) + uint256 fee1, // token1 的手续费 + bytes calldata data + ) external { + require(msg.sender == address(pool), "not authorized"); + + FlashCallbackData memory decoded = abi.decode(data, (FlashCallbackData)); + + // 在此编写自定义逻辑(套利、清算等) + + // 从调用者转入手续费 + if (fee0 > 0) { + token0.transferFrom(decoded.caller, address(this), fee0); + } + if (fee1 > 0) { + token1.transferFrom(decoded.caller, address(this), fee1); + } + + // 归还借入代币 + 手续费 + if (fee0 > 0) { + token0.transfer(address(pool), decoded.amount0 + fee0); + } + if (fee1 > 0) { + token1.transfer(address(pool), decoded.amount1 + fee1); + } + } +} + +interface IUniswapV3Pool { + function token0() external view returns (address); + function token1() external view returns (address); + function flash(address recipient, uint256 amount0, uint256 amount1, bytes calldata data) external; +} +interface IERC20 { /* 标准 ERC20 接口 */ } +``` + +## V3 闪电贷特点 + +- **多种费率等级**:不同的池有不同费率(0.05%、0.30%、1.00%) +- **可同时借入两种代币**:`flash(amount0, amount1)` 可借入任意组合 +- **集中流动性**:V3 的流动性集中机制使得大额闪电贷的深度更好 diff --git "a/Unit4 DeFi/18 Uniswap V3 \346\265\201\345\212\250\346\200\247.md" "b/Unit4 DeFi/18 Uniswap V3 \346\265\201\345\212\250\346\200\247.md" new file mode 100644 index 0000000..f31e069 --- /dev/null +++ "b/Unit4 DeFi/18 Uniswap V3 \346\265\201\345\212\250\346\200\247.md" @@ -0,0 +1,172 @@ +# Uniswap V3 流动性管理 + +Uniswap V3 使用 NFT 代表流动性仓位(Position),支持在自定义的价格区间内提供集中流动性。通过 `NonfungiblePositionManager` 进行铸造、增加、减少和收取手续费。 + +```solidity +// SPDX-License-Identifier: MIT // SPDX 许可证标识符:MIT 许可证 +pragma solidity ^0.8.26; // 版本指令,要求使用 Solidity 0.8.26 及以上版本 + +address constant DAI = 0x6B175474E89094C44Da98b954EedeAC495271d0F; +address constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; + +// UniswapV3Liquidity 合约:V3 流动性管理 +contract UniswapV3Liquidity is IERC721Receiver { // 实现 ERC721 接收器 + IERC20 private constant dai = IERC20(DAI); + IWETH private constant weth = IWETH(WETH); + + int24 private constant MIN_TICK = -887272; // 最小 tick + int24 private constant MAX_TICK = -MIN_TICK; // 最大 tick(对称) + int24 private constant TICK_SPACING = 60; // 费率 0.3% 的 tick 间距 + + // NonfungiblePositionManager 合约地址(主网) + INonfungiblePositionManager public nonfungiblePositionManager = + INonfungiblePositionManager(0xC36442b4a4522E871399CD717aBDD847Ab11FE88); + + // 接收 NFT 的回调(返回 selector 表示支持) + function onERC721Received(address, address, uint256, bytes calldata) + external pure returns (bytes4) { + return IERC721Receiver.onERC721Received.selector; + } + + // mintNewPosition 函数:创建新的流动性仓位 + function mintNewPosition(uint256 amount0ToAdd, uint256 amount1ToAdd) + external + returns (uint256 tokenId, uint128 liquidity, uint256 amount0, uint256 amount1) + { + dai.transferFrom(msg.sender, address(this), amount0ToAdd); + weth.transferFrom(msg.sender, address(this), amount1ToAdd); + dai.approve(address(nonfungiblePositionManager), amount0ToAdd); + weth.approve(address(nonfungiblePositionManager), amount1ToAdd); + + INonfungiblePositionManager.MintParams memory params = + INonfungiblePositionManager.MintParams({ + token0: DAI, + token1: WETH, + fee: 3000, // 0.3% 费率等级 + // 设置全范围 tick(对齐到 tickSpacing) + tickLower: (MIN_TICK / TICK_SPACING) * TICK_SPACING, + tickUpper: (MAX_TICK / TICK_SPACING) * TICK_SPACING, + amount0Desired: amount0ToAdd, amount1Desired: amount1ToAdd, + amount0Min: 0, amount1Min: 0, + recipient: address(this), + deadline: block.timestamp + }); + + (tokenId, liquidity, amount0, amount1) = + nonfungiblePositionManager.mint(params); + + // 退还多余代币 + _refundIfNeeded(amount0ToAdd, amount0, amount1ToAdd, amount1); + } + + // collectAllFees 函数:收集仓位的手续费收益 + function collectAllFees(uint256 tokenId) + external + returns (uint256 amount0, uint256 amount1) + { + INonfungiblePositionManager.CollectParams memory params = + INonfungiblePositionManager.CollectParams({ + tokenId: tokenId, + recipient: address(this), // 手续费收到本合约 + amount0Max: type(uint128).max, // 收集全部手续费 + amount1Max: type(uint128).max + }); + (amount0, amount1) = nonfungiblePositionManager.collect(params); + } + + // increaseLiquidityCurrentRange 函数:为已有仓位增加流动性 + function increaseLiquidityCurrentRange( + uint256 tokenId, uint256 amount0ToAdd, uint256 amount1ToAdd + ) external returns (uint128 liquidity, uint256 amount0, uint256 amount1) { + dai.transferFrom(msg.sender, address(this), amount0ToAdd); + weth.transferFrom(msg.sender, address(this), amount1ToAdd); + dai.approve(address(nonfungiblePositionManager), amount0ToAdd); + weth.approve(address(nonfungiblePositionManager), amount1ToAdd); + + INonfungiblePositionManager.IncreaseLiquidityParams memory params = + INonfungiblePositionManager.IncreaseLiquidityParams({ + tokenId: tokenId, + amount0Desired: amount0ToAdd, amount1Desired: amount1ToAdd, + amount0Min: 0, amount1Min: 0, + deadline: block.timestamp + }); + (liquidity, amount0, amount1) = + nonfungiblePositionManager.increaseLiquidity(params); + } + + // decreaseLiquidityCurrentRange 函数:减少仓位的流动性 + function decreaseLiquidityCurrentRange(uint256 tokenId, uint128 liquidity) + external + returns (uint256 amount0, uint256 amount1) + { + INonfungiblePositionManager.DecreaseLiquidityParams memory params = + INonfungiblePositionManager.DecreaseLiquidityParams({ + tokenId: tokenId, + liquidity: liquidity, + amount0Min: 0, amount1Min: 0, + deadline: block.timestamp + }); + (amount0, amount1) = + nonfungiblePositionManager.decreaseLiquidity(params); + } + + function _refundIfNeeded(uint256 d0, uint256 a0, uint256 d1, uint256 a1) + private + { + if (a0 < d0) { dai.approve(address(nonfungiblePositionManager), 0); dai.transfer(msg.sender, d0 - a0); } + if (a1 < d1) { weth.approve(address(nonfungiblePositionManager), 0); weth.transfer(msg.sender, d1 - a1); } + } +} + +// 核心接口 +interface INonfungiblePositionManager { + struct MintParams { + address token0; address token1; uint24 fee; + int24 tickLower; int24 tickUpper; + uint256 amount0Desired; uint256 amount1Desired; + uint256 amount0Min; uint256 amount1Min; + address recipient; uint256 deadline; + } + function mint(MintParams calldata params) external payable + returns (uint256 tokenId, uint128 liquidity, uint256 amount0, uint256 amount1); + + struct IncreaseLiquidityParams { + uint256 tokenId; uint256 amount0Desired; uint256 amount1Desired; + uint256 amount0Min; uint256 amount1Min; uint256 deadline; + } + function increaseLiquidity(IncreaseLiquidityParams calldata params) external payable + returns (uint128 liquidity, uint256 amount0, uint256 amount1); + + struct DecreaseLiquidityParams { + uint256 tokenId; uint128 liquidity; uint256 amount0Min; uint256 amount1Min; uint256 deadline; + } + function decreaseLiquidity(DecreaseLiquidityParams calldata params) external payable + returns (uint256 amount0, uint256 amount1); + + struct CollectParams { + uint256 tokenId; address recipient; uint128 amount0Max; uint128 amount1Max; + } + function collect(CollectParams calldata params) external payable + returns (uint256 amount0, uint256 amount1); +} + +interface IERC721Receiver { + function onERC721Received(address operator, address from, uint256 tokenId, bytes calldata data) + external returns (bytes4); +} +interface IERC20 { /* 标准 ERC20 接口 */ } +interface IWETH is IERC20 { + function deposit() external payable; + function withdraw(uint256 amount) external; +} +``` + +## V3 核心概念 + +| 概念 | 说明 | +|------|------| +| **Tick** | 价格的整数表示,`price = 1.0001^tick` | +| **Tick Spacing** | 可用的 tick 间隔(由费率决定:0.05%→10, 0.30%→60, 1.00%→200)| +| **区间流动性** | 仅在 tickLower ~ tickUpper 范围内提供流动性 | +| **NFT 仓位** | 每个仓位是独特的 ERC721 代币,可交易/转移 | +| **手续费自动累积** | 手续费在仓位的 token0 和 token1 中累积,需手动收集 | diff --git "a/Unit4 DeFi/19 Uniswap V4 \344\272\244\346\215\242.md" "b/Unit4 DeFi/19 Uniswap V4 \344\272\244\346\215\242.md" new file mode 100644 index 0000000..b0a2e7a --- /dev/null +++ "b/Unit4 DeFi/19 Uniswap V4 \344\272\244\346\215\242.md" @@ -0,0 +1,151 @@ +# Uniswap V4 交换 + +Uniswap V4 使用单例 PoolManager 配合 unlock/unlockCallback 模式。V4 的核心创新包括 Hooks(钩子)和 Flash Accounting(闪电记账)。 + +```solidity +// SPDX-License-Identifier: MIT // SPDX 许可证标识符:MIT 许可证 +pragma solidity ^0.8.26; // 版本指令,要求使用 Solidity 0.8.26 及以上版本 + +// Uniswap V4 PoolManager 主网地址 +address constant POOL_MANAGER = 0x000000000004444c5dc75cB358380D2e3dE08A90; +address constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; +address constant USDC = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48; + +// UniswapV4Swap 合约:V4 交换示例 +contract UniswapV4Swap is IUnlockCallback { // 实现 unlock 回调接口 + IPoolManager public immutable poolManager; + + error NotPoolManager(); + error SwapFailed(); + + constructor() { + poolManager = IPoolManager(POOL_MANAGER); + } + + // swapExactInput 函数:精确输入交换 + function swapExactInput( + PoolKey calldata key, // 池键(定义代币对 + 费率 + 钩子) + uint128 amountIn, // 输入数量 + uint128 minAmountOut // 最小输出(滑点保护) + ) external returns (uint256 amountOut) { + // 编码参数,通过 unlock 回调传递 + bytes memory data = abi.encode( + SwapParams({ + key: key, amountIn: amountIn, minAmountOut: minAmountOut, + zeroForOne: true, sender: msg.sender + }) + ); + + // unlock 触发 → unlockCallback 中执行交换 + bytes memory result = poolManager.unlock(data); + amountOut = abi.decode(result, (uint256)); + } + + // unlockCallback 函数:PoolManager 回调,真正执行交换逻辑 + function unlockCallback(bytes calldata data) + external override returns (bytes memory) + { + if (msg.sender != address(poolManager)) revert NotPoolManager(); + + SwapParams memory params = abi.decode(data, (SwapParams)); + + // 执行交换 + // zeroForOne: true = token0 → token1, false = token1 → token0 + // amountSpecified: 负数 = 精确输入, 正数 = 精确输出 + BalanceDelta delta = poolManager.swap( + params.key, + IPoolManager.SwapParams({ + zeroForOne: params.zeroForOne, + amountSpecified: -int256(uint256(params.amountIn)), // 负数 = 精确输入 + sqrtPriceLimitX96: params.zeroForOne + ? MIN_SQRT_PRICE + 1 + : MAX_SQRT_PRICE - 1 + }), + bytes("") // hook 数据(空 = 无钩子回调) + ); + + // 从 delta 中提取输出量 + // delta.amount0() < 0 → 欠池 token0 + // delta.amount1() > 0 → 池欠 token1 + uint256 amountOut = params.zeroForOne + ? uint256(int256(delta.amount1())) // token1 输出 + : uint256(int256(delta.amount0())); // token0 输出 + + if (amountOut < params.minAmountOut) revert SwapFailed(); + + // 结算(Settle):支付输入的代币 + Currency inputCurrency = params.zeroForOne + ? params.key.currency0 : params.key.currency1; + + IERC20(Currency.unwrap(inputCurrency)).transferFrom( + params.sender, address(poolManager), params.amountIn + ); + poolManager.settle(inputCurrency); // 结算输入 + + // 提取(Take):接收输出的代币 + Currency outputCurrency = params.zeroForOne + ? params.key.currency1 : params.key.currency0; + + poolManager.take(outputCurrency, params.sender, amountOut); + + return abi.encode(amountOut); + } + + struct SwapParams { + PoolKey key; uint128 amountIn; uint128 minAmountOut; + bool zeroForOne; address sender; + } +} + +// ===== V4 核心类型 ===== + +uint160 constant MIN_SQRT_PRICE = 4295128739; +uint160 constant MAX_SQRT_PRICE = 1461446703485210103287273052203988822378723970342; + +type Currency is address; // Currency 类型(address(0) = 原生 ETH) + +struct PoolKey { // 池键:唯一标识一个池 + Currency currency0; Currency currency1; + uint24 fee; int24 tickSpacing; address hooks; +} + +type BalanceDelta is int256; // 余额变化量 + +library BalanceDeltaLibrary { + function amount0(BalanceDelta delta) internal pure returns (int128) { + return int128(int256(BalanceDelta.unwrap(delta) >> 128)); + } + function amount1(BalanceDelta delta) internal pure returns (int128) { + return int128(int256(BalanceDelta.unwrap(delta))); + } +} + +using BalanceDeltaLibrary for BalanceDelta; + +interface IPoolManager { + struct SwapParams { + bool zeroForOne; int256 amountSpecified; uint160 sqrtPriceLimitX96; + } + function unlock(bytes calldata data) external returns (bytes memory); + function swap(PoolKey memory key, SwapParams memory params, bytes calldata hookData) + external returns (BalanceDelta); + function settle(Currency currency) external payable returns (uint256); + function take(Currency currency, address to, uint256 amount) external; +} + +interface IUnlockCallback { + function unlockCallback(bytes calldata data) external returns (bytes memory); +} + +interface IERC20 { /* 标准 ERC20 接口 */ } +``` + +## V4 核心创新 + +| 特性 | V3 | V4 | +|------|-----|-----| +| 架构 | 每池独立合约 | 单例 PoolManager | +| 手续费 | 需编码到路径 | PoolKey 中指定 | +| 钩子(Hooks) | 不支持 | 支持自定义回调(swap 前后) | +| 闪电记账 | 不支持 | 支持净额结算,减少转账 | +| 原生 ETH | 需包装为 WETH | 原生支持 ETH | diff --git "a/Unit4 DeFi/20 Uniswap V4 \351\227\252\347\224\265\350\264\267.md" "b/Unit4 DeFi/20 Uniswap V4 \351\227\252\347\224\265\350\264\267.md" new file mode 100644 index 0000000..9cc6284 --- /dev/null +++ "b/Unit4 DeFi/20 Uniswap V4 \351\227\252\347\224\265\350\264\267.md" @@ -0,0 +1,116 @@ +# Uniswap V4 闪电贷 + +Uniswap V4 的闪电贷**完全免费**(无手续费)。V4 采用闪电记账(Flash Accounting)机制,只需在 unlock 回调期间保持净余额为零即可。 + +```solidity +// SPDX-License-Identifier: MIT // SPDX 许可证标识符:MIT 许可证 +pragma solidity ^0.8.26; // 版本指令,要求使用 Solidity 0.8.26 及以上版本 + +// Uniswap V4 PoolManager 主网地址 +address constant POOL_MANAGER = 0x000000000004444c5dc75cB358380D2e3dE08A90; + +// UniswapV4Flash 合约:V4 免费闪电贷 +// V4 闪电贷因闪电记账机制完全免费 — 无需手续费! +contract UniswapV4Flash is IUnlockCallback { // 实现 unlock 回调接口 + IPoolManager public immutable poolManager; + + error NotPoolManager(); + error FlashLoanFailed(); + + constructor() { + poolManager = IPoolManager(POOL_MANAGER); + } + + // flash 函数:发起闪电贷 + // currency: 借入的代币(address(0) = 原生 ETH) + function flash(Currency currency, uint256 amount, bytes calldata data) + external + { + bytes memory callbackData = abi.encode( + FlashParams({ + currency: currency, + amount: amount, + sender: msg.sender, + data: data + }) + ); + + poolManager.unlock(callbackData); // 触发 unlock → unlockCallback + } + + // unlockCallback 函数:PoolManager 回调 + function unlockCallback(bytes calldata callbackData) + external override returns (bytes memory) + { + if (msg.sender != address(poolManager)) revert NotPoolManager(); + + FlashParams memory params = abi.decode(callbackData, (FlashParams)); + + // 从池中"取走"代币(产生负余额/债务) + poolManager.take(params.currency, address(this), params.amount); + + // ============================================ + // 在此执行闪电贷自定义逻辑 + // ============================================ + + // 归还闪电贷 + if (!isNative(params.currency)) { + // ERC20:先转账给 PoolManager,再调用 settle + IERC20(Currency.unwrap(params.currency)).transfer( + address(poolManager), params.amount + ); + poolManager.settle(params.currency); + } else { + // 原生 ETH:直接附带 value 调用 settle + poolManager.settle{value: params.amount}(params.currency); + } + + // 无手续费!余额差值为零,unlock 成功 + return bytes(""); + } + + function isNative(Currency currency) internal pure returns (bool) { + return Currency.unwrap(currency) == address(0); + } + + receive() external payable {} + + struct FlashParams { + Currency currency; uint256 amount; address sender; bytes data; + } +} + +// ===== V4 核心类型 ===== + +type Currency is address; + +library CurrencyLibrary { + function unwrap(Currency currency) internal pure returns (address) { + return Currency.unwrap(currency); + } +} + +using CurrencyLibrary for Currency; + +interface IPoolManager { + function unlock(bytes calldata data) external returns (bytes memory); + function settle(Currency currency) external payable returns (uint256); + function take(Currency currency, address to, uint256 amount) external; +} + +interface IUnlockCallback { + function unlockCallback(bytes calldata data) external returns (bytes memory); +} + +interface IERC20 { /* 标准 ERC20 接口 */ } +``` + +## V4 闪电贷 vs V3 闪电贷 + +| 特性 | V3 Flash | V4 Flash | +|------|----------|----------| +| 手续费 | 按池费率收取(如 0.3%)| **完全免费** | +| 机制 | `pool.flash(recipient, amount0, amount1, data)` | `poolManager.unlock(data)` | +| 借入方式 | 通过回调参数直接获得代币 | `poolManager.take()` 提取 | +| ETH 支持 | 需 WETH | 原生支持 | +| 闪电记账 | 不支持 | 支持净额结算 | diff --git "a/Unit4 DeFi/21 Uniswap V4 \351\231\220\344\273\267\345\215\225.md" "b/Unit4 DeFi/21 Uniswap V4 \351\231\220\344\273\267\345\215\225.md" new file mode 100644 index 0000000..32be441 --- /dev/null +++ "b/Unit4 DeFi/21 Uniswap V4 \351\231\220\344\273\267\345\215\225.md" @@ -0,0 +1,178 @@ +# Uniswap V4 限价单钩子 + +Uniswap V4 的 Hooks(钩子)机制允许在交换生命周期的关键节点执行自定义逻辑。本示例实现一个限价单钩子:当价格到达指定 tick 时自动执行挂单。 + +```solidity +// SPDX-License-Identifier: MIT // SPDX 许可证标识符:MIT 许可证 +pragma solidity ^0.8.26; // 版本指令,要求使用 Solidity 0.8.26 及以上版本 + +// LimitOrderHook 合约:V4 限价单钩子 +contract LimitOrderHook is IHooks { // 实现 IHooks 接口 + IPoolManager public immutable poolManager; + + // 池 → tick → zeroForOne → 总流动性 + mapping(bytes32 => mapping(int24 => mapping(bool => uint256))) public tickLiquidity; + // 池 → tick → zeroForOne → 用户 → 流动性 + mapping(bytes32 => mapping(int24 => mapping(bool => mapping(address => uint256)))) + public userPositions; + + error NotPoolManager(); + error InvalidTick(); + + constructor(IPoolManager _poolManager) { + poolManager = _poolManager; + } + + // placeLimitOrder 函数:在指定 tick 处挂限价单 + function placeLimitOrder( + PoolKey calldata key, // 池键值 + int24 tick, // 目标价格 tick + bool zeroForOne, // true = 卖 token0 买 token1 + uint256 amount // 卖出的代币数量 + ) external { + // 验证 tick 在当前价格的正确一侧 + (, int24 currentTick,,) = poolManager.getSlot0(toId(key)); + + // 卖 token0 → tick 必须高于当前(价格上升时成交) + // 卖 token1 → tick 必须低于当前(价格下降时成交) + if (zeroForOne && tick <= currentTick) revert InvalidTick(); + if (!zeroForOne && tick >= currentTick) revert InvalidTick(); + + bytes32 poolId = toId(key); + + // 从用户转入代币 + Currency currency = zeroForOne ? key.currency0 : key.currency1; + IERC20(Currency.unwrap(currency)).transferFrom(msg.sender, address(this), amount); + + // 记录仓位 + tickLiquidity[poolId][tick][zeroForOne] += amount; + userPositions[poolId][tick][zeroForOne][msg.sender] += amount; + } + + // afterSwap 钩子:每次交换后检查是否到达限价单价格 + function afterSwap( + address, // sender + PoolKey calldata key, // 池键值 + IPoolManager.SwapParams calldata params, // 交换参数 + BalanceDelta, // 交换的余额变化 + bytes calldata // hook 数据 + ) external override returns (bytes4, int128) { + if (msg.sender != address(poolManager)) revert NotPoolManager(); + + (, int24 currentTick,,) = poolManager.getSlot0(toId(key)); + bytes32 poolId = toId(key); + + // zeroForOne swap 使价格下降 → 检查卖 token1 的挂单 + // !zeroForOne swap 使价格上涨 → 检查卖 token0 的挂单 + bool checkZeroForOne = !params.zeroForOne; + + uint256 liquidity = tickLiquidity[poolId][currentTick][checkZeroForOne]; + + if (liquidity > 0) { + _executeLimitOrders(key, currentTick, checkZeroForOne, liquidity); + } + + return (IHooks.afterSwap.selector, 0); + } + + function _executeLimitOrders( + PoolKey calldata key, int24 tick, bool zeroForOne, uint256 amount + ) internal { + bytes32 poolId = toId(key); + tickLiquidity[poolId][tick][zeroForOne] = 0; // 清除挂单 + emit LimitOrderFilled(poolId, tick, zeroForOne, amount); + } + + // cancelLimitOrder 函数:取消未成交的限价单 + function cancelLimitOrder( + PoolKey calldata key, int24 tick, bool zeroForOne + ) external { + bytes32 poolId = toId(key); + uint256 amount = userPositions[poolId][tick][zeroForOne][msg.sender]; + require(amount > 0, "No position"); + + userPositions[poolId][tick][zeroForOne][msg.sender] = 0; + tickLiquidity[poolId][tick][zeroForOne] -= amount; + + Currency currency = zeroForOne ? key.currency0 : key.currency1; + IERC20(Currency.unwrap(currency)).transfer(msg.sender, amount); + } + + // getHookPermissions 函数:声明需要的钩子权限 + function getHookPermissions() public pure returns (Hooks.Permissions memory) { + return Hooks.Permissions({ + beforeInitialize: false, afterInitialize: false, + beforeAddLiquidity: false, afterAddLiquidity: false, + beforeRemoveLiquidity: false, afterRemoveLiquidity: false, + beforeSwap: false, afterSwap: true, // 仅需 afterSwap + beforeDonate: false, afterDonate: false, + beforeSwapReturnDelta: false, afterSwapReturnDelta: false, + afterAddLiquidityReturnDelta: false, afterRemoveLiquidityReturnDelta: false + }); + } + + function toId(PoolKey memory key) internal pure returns (bytes32) { + return keccak256(abi.encode(key)); + } + + // 未使用的钩子(均返回 selector) + function beforeInitialize(address, PoolKey calldata, uint160) external pure override returns (bytes4) { + return IHooks.beforeInitialize.selector; + } + // ... (其余未使用钩子同理,返回各自 selector) + + event LimitOrderFilled(bytes32 indexed poolId, int24 tick, bool zeroForOne, uint256 amount); +} + +// ===== V4 核心类型和接口 ===== + +type Currency is address; +type BalanceDelta is int256; + +struct PoolKey { + Currency currency0; Currency currency1; + uint24 fee; int24 tickSpacing; address hooks; +} + +library Hooks { + struct Permissions { + bool beforeInitialize; bool afterInitialize; + bool beforeAddLiquidity; bool afterAddLiquidity; + bool beforeRemoveLiquidity; bool afterRemoveLiquidity; + bool beforeSwap; bool afterSwap; + bool beforeDonate; bool afterDonate; + bool beforeSwapReturnDelta; bool afterSwapReturnDelta; + bool afterAddLiquidityReturnDelta; bool afterRemoveLiquidityReturnDelta; + } +} + +interface IPoolManager { + struct SwapParams { + bool zeroForOne; int256 amountSpecified; uint160 sqrtPriceLimitX96; + } + function getSlot0(bytes32 poolId) external view + returns (uint160 sqrtPriceX96, int24 tick, uint24 protocolFee, uint24 lpFee); +} + +interface IHooks { + function beforeSwap(address, PoolKey calldata, IPoolManager.SwapParams calldata, bytes calldata) + external returns (bytes4, BeforeSwapDelta, uint24); + function afterSwap(address, PoolKey calldata, IPoolManager.SwapParams calldata, BalanceDelta, bytes calldata) + external returns (bytes4, int128); + // ... 其余 12 个钩子函数 +} + +interface IERC20 { /* 标准 ERC20 接口 */ } +``` + +## V4 Hooks 机制 + +| 钩子 | 触发时机 | 典型用途 | +|------|----------|----------| +| `beforeSwap` | 交换即将执行 | 自定义费率、价格验证 | +| `afterSwap` | 交换刚执行完 | 限价单、TWAP 更新 | +| `beforeAddLiquidity` | 添加流动性前 | 白名单检查 | +| `afterAddLiquidity` | 添加流动性后 | 仓位跟踪 | +| `beforeDonate` | 捐赠前 | 费率重分配 | + +**Hooks 设计原则**:一个合约可以实现任意钩子组合,通过 `getHookPermissions` 声明所需权限,无效的钩子不可注册。 diff --git "a/Unit5 \351\273\221\345\256\242\346\224\273\345\207\273\345\217\212\351\242\204\351\230\262/63-64 Gas \350\247\204\345\210\231.md" "b/Unit5 \351\273\221\345\256\242\346\224\273\345\207\273\345\217\212\351\242\204\351\230\262/63-64 Gas \350\247\204\345\210\231.md" new file mode 100644 index 0000000..93df6d2 --- /dev/null +++ "b/Unit5 \351\273\221\345\256\242\346\224\273\345\207\273\345\217\212\351\242\204\351\230\262/63-64 Gas \350\247\204\345\210\231.md" @@ -0,0 +1,96 @@ +# 63/64 Gas 规则 + +EVM 中有一个重要的规则:外部调用最多只能接收当前合约剩余 Gas 的 63/64,剩余的 1/64 Gas 留在当前合约中。这个规则如果被恶意利用,可能导致 Gas 退还计算错误。 + +```solidity +// SPDX-License-Identifier: MIT // SPDX 许可证标识符:MIT 许可证 +pragma solidity ^0.8.26; // 版本指令,要求使用 Solidity 0.8.26 及以上版本 + +// A 合约:发起调用的一方 +contract A { // 定义 A 合约 + // f 函数:调用合约 B 的 g 函数 + function f(address b) external { + uint256 gasStart = gasleft(); // 记录起始 Gas(g0) + B(payable(b)).g(msg.sender, gasStart); // 调用 B.g,转入剩余 Gas + } +} + +// B 合约:被调用的漏洞合约 +contract B { // 定义 B 合约 + event Log(uint256 gas); // Log 事件:记录消耗的 Gas + + mapping(address => bool) public authorized; // 授权映射 + + constructor() { + authorized[msg.sender] = true; // 部署者获得授权 + } + + receive() external payable {} // 接收 ETH + + // setAuth 函数:授权管理员 + function setAuth(address addr, bool auth) external { + require(authorized[msg.sender], "not authorized"); // 权限检查 + authorized[addr] = auth; + } + + // g 函数:向接收者发送等于消耗 Gas 量的 ETH + // 发送 9000000000000000000 Gas 可以耗尽合约的 ETH + function g(address receiver, uint256 gasStart) external { + require(authorized[msg.sender], "not authorized"); // 权限检查 + + uint256 gasNow = gasleft(); // 记录当前 Gas(g1) + uint256 gasUsed = gasStart - gasNow; // 计算消耗的 Gas(存在漏洞) + // 修复方案: + // uint256 gasUsed = gasStart - (gasNow / 63) - gasNow; + + // 问题:gasStart - gasNow 包含了被 63/64 规则"扣留"在 A 中的那部分 Gas + // 攻击者可以发送超大 Gas 来放大这个差值,从而提取更多 ETH + (bool ok,) = receiver.call{value: gasUsed}(""); // 发送 ETH + require(ok, "send failed"); + + emit Log(gasUsed); // 记录 Gas 使用量 + } +} +``` + +## 63/64 规则详解 + +``` +外部调用只能接收当前合约剩余 Gas 的 63/64 +剩余的 1/64 Gas 保留在当前合约中 + +g0 = A 中调用 gasleft() 的值 +g1 = B 中调用 gasleft() 的值 +g* = 即将调用 B 前的实际剩余 Gas + + g* 63/64 g* +A |---->| B +| | +g0 g1 + +Gas 消耗: +dg = g0 - g1 - 1/64 g* ≥ 0 +``` + +## 漏洞原因 + +退还金额 = `g0 - g1`,但实际消耗只有 `g0 - g1 - 1/64 g*`。 + +问题:**退还多付了 `1/64 g*`**。攻击者可以通过发送超大 Gas(使 g* 极大)来放大这个超额退还,从而耗尽合约的 ETH。 + +## 修复方案 + +```solidity +// 正确的 Gas 消耗计算 +uint256 gasUsed = gasStart - (gasNow / 63) - gasNow; +``` + +公式推导: +``` +g1 ≤ 63/64 g* ≤ g0 +g1/63 ≤ 1/64 g* ≤ g0/63 + +g0 - g1 - g1/63 ≥ g0 - g1 - 1/64 g* ≥ g0 - g1 - g0/63 = 62/63 g0 - g1 ≥ 0 +``` + +因此实际退还额应为 `gasStart - gasNow - gasNow/63`,而非简单的 `gasStart - gasNow`。 diff --git "a/Unit6 EVM/01 \345\255\230\345\202\250.md" "b/Unit6 EVM/01 \345\255\230\345\202\250.md" new file mode 100644 index 0000000..117117f --- /dev/null +++ "b/Unit6 EVM/01 \345\255\230\345\202\250.md" @@ -0,0 +1,625 @@ +# EVM 存储(Storage) + +EVM 存储的底层原理。通过内联汇编(Yul)的 `sstore` 和 `sload` 操作码,直接操作合约的持久化存储空间。 + +EVM 存储有 2²⁵⁶ 个槽位,每个槽位最多存储 32 字节。槽位按照状态变量声明的顺序分配,小于 32 字节的数据会从右到左紧凑打包在一个槽内。 + +```solidity +// SPDX-License-Identifier: MIT // SPDX 许可证标识符:MIT 许可证 +pragma solidity ^0.8.26; // 版本指令,要求使用 Solidity 0.8.26 及以上版本 + +// ===== Yul 基础 ===== + +// YulIntro 合约:Yul(Solidity 内联汇编语言)基础介绍 +contract YulIntro { // 定义 YulIntro 合约 + // 测试 Yul 变量赋值 + function test_yul_var() public pure returns (uint256) { // 返回 uint256 + uint256 s = 0; // Solidity 变量 + + assembly { // 内联汇编块 + // 声明变量 + let x := 1 // 声明并赋值 + // 重新赋值 + x := 2 // 修改 yul 局部变量 + // 将 yul 变量赋值给 Solidity 变量 + s := 2 // 写入 Solidity 变量 + } + + return s; // 返回 2 + } + + // 测试 Yul 类型(在 Yul 中,一切都是 bytes32) + function test_yul_types() // 测试 Yul 的类型系统 + public + pure + returns (bool x, uint256 y, bytes32 z) // 返回三种不同类型 + { + assembly { + x := 1 // bool 值(0 为 false,非 0 为 true) + y := 0xaaa // uint256 值 + z := "Hello Yul" // 字符串被当作 bytes32 处理 + } + + return (x, y, z); // 返回各值 + } +} + +// ===== 单槽存储 ===== + +// EVMStorageSingleSlot 合约:演示单变量占据独立存储槽 +contract EVMStorageSingleSlot { // 定义单槽存储合约 + // EVM 存储 + // 2**256 个槽位,每个槽位最多存储 32 字节 + // 槽位按状态变量声明顺序分配 + // 小于 32 字节的数据从右到左打包 + // sstore(k, v) = 将 v 存储到槽位 k + // sload(k) = 从槽位 k 加载 32 字节 + + // 单变量占据一个槽位 + // 槽位 0 + uint256 public s_x; // 第 0 个槽位 + // 槽位 1 + uint256 public s_y; // 第 1 个槽位 + // 槽位 2 + bytes32 public s_z; // 第 2 个槽位 + + // test_sstore 函数:使用固定槽位编号写入存储 + function test_sstore() public { + assembly { + sstore(0, 111) // 向槽位 0 写入 111 + sstore(1, 222) // 向槽位 1 写入 222 + sstore(2, 0xababab) // 向槽位 2 写入 0xababab + } + } + + // test_sstore_again 函数:使用 .slot 属性获取变量槽位 + function test_sstore_again() public { + assembly { + sstore(s_x.slot, 111) // 通过 s_x.slot 获取槽位编号(0) + sstore(s_y.slot, 222) // 通过 s_y.slot 获取槽位编号(1) + sstore(s_z.slot, 0xcdcdcd) // 通过 s_z.slot 获取槽位编号(2) + } + } + + // test_sload 函数:使用固定槽位编号读取存储 + function test_sload() + public + view + returns (uint256 x, uint256 y, bytes32 z) + { + assembly { + x := sload(0) // 从槽位 0 加载(存储变量在声明时已经赋值) + y := sload(1) // 从槽位 1 加载 + z := sload(2) // 从槽位 2 加载 + } + + return (x, y, z); + } + + // test_sload_again 函数:使用 .slot 属性读取存储 + function test_sload_again() + public + view + returns (uint256 x, uint256 y, bytes32 z) + { + assembly { + x := sload(s_x.slot) // 通过 .slot 读取槽位 0 + y := sload(s_y.slot) // 通过 .slot 读取槽位 1 + z := sload(s_z.slot) // 通过 .slot 读取槽位 2 + } + + return (x, y, z); + } +} + +// ===== 紧凑打包存储(Bytes) ===== + +// EVMStoragePackedSlotBytes 合约:演示小于 32 字节的数据如何打包 +contract EVMStoragePackedSlotBytes { // 定义字节打包合约 + // 槽位 0(从右到左打包) + // | 256 bits | + // | b2(2 字节) | b4(4 字节) | + bytes4 public b4 = 0xabababab; // 4 字节在右 + bytes2 public b2 = 0xcdcd; // 2 字节在 b4 左边 + + // get 函数:查看整个槽位的原始数据 + function get() public view returns (bytes32 b32) { + assembly { + b32 := sload(0) // 加载槽位 0 的全部 32 字节 + } + } +} + +// ===== 位掩码操作 ===== + +// BitMasking 合约:演示位掩码的创建(用于紧凑存储的读写) +contract BitMasking { // 定义位掩码合约 + // test_mask 函数:创建全 1 的位掩码 + function test_mask() public pure returns (bytes32 mask) { + assembly { + // | 256 bits | + // 000 ... 000 | 111 ... 111 + // | 16 bits + // 结果: 0x000000000000000000000000000000000000000000000000000000000000ffff + mask := sub(shl(16, 1), 1) // (1 << 16) - 1 = 65535 = 0xffff + } + } + + // test_shift_mask 函数:创建偏移后的位掩码 + function test_shift_mask() public pure returns (bytes32 mask) { + assembly { + // | 256 bits | + // 000 ... 000 | 111 ... 111 | 000 ... 000 | + // | 16 bits | 32 bits + // 结果: 0x0000000000000000000000000000000000000000000000000000ffff00000000 + mask := shl(32, sub(shl(16, 1), 1)) // 将 16 bit 的掩码左移 32 bit + } + } + + // test_not_mask 函数:创建反掩码(清零用) + function test_not_mask() public pure returns (bytes32 mask) { + assembly { + // | 256 bits | + // 111 ... 111 | 000 ... 000 | 111 ... 111 | + // | 16 bits | 32 bits + // 结果: 0xffffffffffffffffffffffffffffffffffffffffffffffffffff0000ffffffff + mask := not(shl(32, sub(shl(16, 1), 1))) // NOT 翻转所有位 + } + } +} + +// ===== 紧凑打包存储 ===== + +// EVMStoragePackedSlot 合约:演示多种数据类型的紧凑存储 +contract EVMStoragePackedSlot { // 定义紧凑打包存储合约 + // 小于 32 字节的数据会被打包到同一个槽位 + // 使用位掩码进行读写 + // slot(槽位编号)、offset(字节偏移量) + + // 槽位 0 + // | s_d(32b) | s_c(32b) | s_b(64b) | s_a(128b) | + uint128 public s_a; // 128 位,占用槽位 0 的低 128 位 + uint64 public s_b; // 64 位,紧挨 s_a + uint32 public s_c; // 32 位 + uint32 public s_d; // 32 位,占用槽位 0 的最高 32 位 + // 槽位 1 + // | s_y(32b) | s_x(64b) | s_addr(160b) | + address public s_addr; // 20 字节 = 160 位,占用槽位 1 的低 160 位 + uint64 public s_x; // 64 位 + uint32 public s_y; // 32 位 + + // test_sstore 函数:使用位掩码精确写入紧凑存储的各个变量 + function test_sstore() public { + assembly { + // 从槽位 0 加载 32 字节 + let v := sload(0) + + // s_d | s_c | s_b | s_a + // 32 | 32 | 64 | 128 bits + + // 设置 s_a = 11 + // mask = 在 128 位处及左侧全为 1,右侧全为 0 + // 111 ... 111 | 000 ... 000 + // | 128 bits + let mask_a := not(sub(shl(128, 1), 1)) // NOT((1<<128)-1) + // 将左侧 128 位清零 + v := and(v, mask_a) + // 设置 s_a = 11 + v := or(v, 11) + + // 设置 s_b = 22 + // mask = 111...111 | 000 ... 000 | 111 ... 111 + // | 64 bits | 128 bits + let mask_b := not(shl(128, sub(shl(64, 1), 1))) // NOT((1<<64)-1 << 128) + // 将 128-191 位清零 + v := and(v, mask_b) + v := or(v, shl(128, 22)) // 左移 128 位到 s_b 的位置 + + // 设置 s_c = 33 + // mask = 111...111 | 000...000 | 111 ... 111 | 111 ... 111 + // | 32 bits | 64 bits | 128 bits + let mask_c := not(shl(192, sub(shl(32, 1), 1))) // NOT((1<<32)-1 << 192) + // 将 192-223 位清零 + v := and(v, mask_c) + v := or(v, shl(192, 33)) // 左移 192 位到 s_c 的位置 + + // 设置 s_d = 44 + // mask = 000...000 | 111...111 | 111 ... 111 | 111 ... 111 + // | 32 bits | 64 bits | 128 bits + let mask_d := not(shl(224, sub(shl(32, 1), 1))) // NOT((1<<32)-1 << 224) + // 将 224-255 位清零 + v := and(v, mask_d) + v := or(v, shl(224, 44)) // 左移 224 位到 s_d 的位置 + + // 将新值存储到槽位 0 + sstore(0, v) + } + } + + // test_slot_0_offset 函数:查看槽位 0 中各变量的字节偏移量 + function test_slot_0_offset() + public + pure + returns ( + uint256 a_offset, + uint256 b_offset, + uint256 c_offset, + uint256 d_offset + ) + { + // a_offset = 0 = 0 * 8 = 0 bits + // b_offset = 16 = 16 * 8 = 128 bits + // c_offset = 24 = 24 * 8 = 192 bits + // d_offset = 28 = 28 * 8 = 224 bits + assembly { + a_offset := s_a.offset // 0 + b_offset := s_b.offset // 16 + c_offset := s_c.offset // 24 + d_offset := s_d.offset // 28 + } + } + + // test_slot_1_offset 函数:查看槽位 1 中各变量的字节偏移量 + function test_slot_1_offset() + public + pure + returns (uint256 addr_offset, uint256 x_offset, uint256 y_offset) + { + // addr_offset = 0(地址在最低位) + // x_offset = 20 + // y_offset = 28 + assembly { + addr_offset := s_addr.offset // 0 + x_offset := s_x.offset // 20 + y_offset := s_y.offset // 28 + } + } + + // test_sstore_using_offset 函数:使用 .offset 属性简化紧凑存储的写入 + function test_sstore_using_offset() public { + assembly { + // 从槽位 0 加载 32 字节 + let v := sload(s_a.slot) // 使用 .slot 获取槽位 + + // 设置 s_a = 111 + let mask_a := not(sub(shl(128, 1), 1)) + v := and(v, mask_a) + v := or(v, 111) + + // 设置 s_b = 222(使用 .offset * 8 自动计算位偏移) + let mask_b := not(shl(mul(s_b.offset, 8), sub(shl(64, 1), 1))) + v := and(v, mask_b) + v := or(v, shl(mul(s_b.offset, 8), 222)) // offset*8 = 位偏移 + + // 设置 s_c = 333 + let mask_c := not(shl(mul(s_c.offset, 8), sub(shl(32, 1), 1))) + v := and(v, mask_c) + v := or(v, shl(mul(s_c.offset, 8), 333)) + + // 设置 s_d = 444 + let mask_d := not(shl(mul(s_d.offset, 8), sub(shl(32, 1), 1))) + v := and(v, mask_d) + v := or(v, shl(mul(s_d.offset, 8), 444)) + + // 存储到槽位 0 + sstore(s_a.slot, v) + } + } +} + +// ===== 结构体存储 ===== + +// EVMStorageStruct 合约:演示结构体的存储布局 +contract EVMStorageStruct { // 定义结构体存储合约 + // 单槽结构体(所有字段打包在一个槽内) + struct SingleSlot { // 单槽结构体 + uint128 x; // 128 位 + uint64 y; // 64 位 + uint64 z; // 64 位(128+64+64 = 256 位 = 32 字节,恰好一个槽) + } + + // 多槽结构体(每个字段单独占据一个槽) + struct MultipleSlots { // 多槽结构体 + uint256 a; // 256 位,占据槽 1 + uint256 b; // 256 位,占据槽 2 + uint256 c; // 256 位,占据槽 3 + } + + // 槽位 0 + SingleSlot public single = SingleSlot({x: 1, y: 2, z: 3}); + // 槽位 1, 2, 3 + MultipleSlots public multi = MultipleSlots({a: 11, b: 22, c: 33}); + + // test_get_single_slot_struct 函数:通过汇编读取单槽结构体 + function test_get_single_slot_struct() + public + view + returns (uint128 x, uint64 y, uint64 z) + { + assembly { + let s := sload(0) // 加载槽位 0 + // z(64) | y(64) | x(128) + x := s // 类型转换自动截断高位 + y := shr(128, s) // 右移 128 位获取 y + z := shr(192, s) // 右移 192 位获取 z + } + } + + // test_get_multiple_slots_struct 函数:读取多槽结构体 + function test_get_multiple_slots_struct() + public + view + returns (uint256 a, uint256 b, uint256 c) + { + assembly { + a := sload(1) // 从槽位 1 读取 a + b := sload(2) // 从槽位 2 读取 b + c := sload(3) // 从槽位 3 读取 c + } + } +} + +// ===== 常量和不可变量的存储 ===== + +// EVMStorageConstants 合约:演示常量和不可变量不使用存储 +contract EVMStorageConstants { // 定义常量存储合约 + // 槽位 0 + uint256 public s0 = 1; + // 常量和不可变量不使用存储(存储在合约字节码中) + uint256 public constant X = 123; // 常量:编译时嵌入字节码 + address public immutable owner; // 不可变量:部署时写入字节码 + // 槽位 1 + uint256 public s1 = 2; + + constructor() { + owner = msg.sender; // 不可变量在构造时赋值 + } + + // test_get_slots 函数:验证常量和不可变量不占用槽位 + function test_get_slots() public view returns (uint256 v0, uint256 v1) { + assembly { + v0 := sload(0) // 槽位 0 是 s0 + v1 := sload(1) // 槽位 1 是 s1(常量和不可变量被跳过) + } + } +} + +// ===== 定长数组存储 ===== + +// EVMStorageFixedArray 合约:演示定长数组的存储布局 +contract EVMStorageFixedArray { // 定义定长数组存储合约 + // 定长数组,元素占 32 字节:元素槽位 = 数组声明的起始槽位 + 索引 + // 槽位 0, 1, 2 + uint256[3] private arr_0 = [1, 2, 3]; // 连续占用槽位 0-2 + // 槽位 3, 4, 5 + uint256[3] private arr_1 = [4, 5, 6]; // 连续占用槽位 3-5 + // 紧凑打包的定长数组:元素小于 32 字节时,相邻元素共享槽位 + // 槽位 6(元素 0 和 1)、槽位 7(元素 2 和 3)、槽位 8(元素 4 + 零填充) + uint128[5] private arr_2 = [7, 8, 9, 10, 11]; + + // test_arr_0 函数:读取定长数组 + function test_arr_0(uint256 i) public view returns (uint256 v) { + assembly { + // arr_0 从槽位 0 开始 + v := sload(add(0, i)) // 槽位 = 起始槽位 + 索引 + } + } + + // test_arr_1 函数:读取另一个定长数组 + function test_arr_1(uint256 i) public view returns (uint256 v) { + assembly { + // arr_1 从槽位 3 开始 + v := sload(add(3, i)) // 槽位 = 3 + 索引 + } + } + + // test_arr_2 函数:读取紧凑打包的定长数组 + function test_arr_2(uint256 i) public view returns (uint128 v) { + assembly { + // arr_2 从槽位 6 开始,每个槽存两个 uint128 + let b32 := sload(add(6, div(i, 2))) // 槽位 = 6 + i/2 + // 槽位 6: 第 1 个元素 | 第 0 个元素 + // 槽位 7: 第 3 个元素 | 第 2 个元素 + // 槽位 8: 000...000 | 第 4 个元素 + + // 偶数索引取低 128 位,奇数索引取高 128 位 + switch mod(i, 2) + case 1 { v := shr(128, b32) } // 奇数:右移获取高 128 位 + default { v := b32 } // 偶数:直接截取低 128 位 + } + } +} + +// ===== 动态数组存储 ===== + +// EVMStorageDynamicArray 合约:演示动态数组的存储布局 +contract EVMStorageDynamicArray { // 定义动态数组存储合约 + // 动态数组: + // 元素槽位 = keccak256(数组声明槽位) + 元素大小 * 索引 + // 声明的槽位存储数组长度,元素存储在 keccak256(槽位) 开始的连续位置 + // keccak256(0) + 1 * 索引 + uint256[] private arr = [11, 22, 33]; + // keccak256(1) + 1 * 索引(128 位元素每槽存 2 个) + uint128[] private arr_2 = [1, 2, 3]; + + // test_arr 函数:通过汇编读取动态数组 + function test_arr(uint256 slot, uint256 i) + public + view + returns (uint256 v, bytes32 b32, uint256 len) + { + bytes32 start = keccak256(abi.encode(slot)); // 计算元素起始槽位 + + assembly { + len := sload(slot) // 读取数组长度 + v := sload(add(start, i)) // 读取第 i 个元素 + b32 := v + } + } +} + +// ===== 映射存储 ===== + +// EVMStorageMapping 合约:演示映射的存储布局 +contract EVMStorageMapping { // 定义映射存储合约 + // 映射值槽位 = keccak256(key, 映射声明槽位) + mapping(address => uint256) public map; // 声明在槽位 0 + + address public constant ADDR_1 = address(1); + address public constant ADDR_2 = address(2); + address public constant ADDR_3 = address(3); + + constructor() { + map[ADDR_1] = 11; + map[ADDR_2] = 22; + map[ADDR_3] = 33; + } + + // test_mapping 函数:通过汇编读取映射值 + function test_mapping(address key) public view returns (uint256 v) { + uint256 slot = 0; // 映射声明在槽位 0 + bytes32 slot_v = keccak256(abi.encode(key, slot)); // 计算值的槽位 + + assembly { + v := sload(slot_v) // 从计算的槽位读取 + } + } +} + +// ===== 嵌套映射存储 ===== + +// EVMStorageNestedMapping 合约:演示嵌套映射的存储布局 +contract EVMStorageNestedMapping { // 定义嵌套映射存储合约 + // key0 => key1 => val + // 值槽位 = keccak256(key1, keccak256(key0, 嵌套映射声明槽位)) + mapping(address => mapping(address => uint256)) public map; + + address public constant ADDR_1 = address(1); + address public constant ADDR_2 = address(2); + address public constant ADDR_3 = address(3); + + constructor() { + map[ADDR_1][ADDR_2] = 11; + map[ADDR_2][ADDR_3] = 22; + map[ADDR_3][ADDR_1] = 33; + } + + // test_nested_mapping 函数:通过汇编读取嵌套映射值 + function test_nested_mapping(address key_0, address key_1) + public + view + returns (uint256 v) + { + uint256 slot = 0; + bytes32 s0 = keccak256(abi.encode(key_0, slot)); // 第一次哈希 + bytes32 s1 = keccak256(abi.encode(key_1, s0)); // 第二次哈希 + + assembly { + v := sload(s1) // 从最终计算的槽位读取 + } + } +} + +// ===== 映射到数组的存储 ===== + +// EVMStorageMappingArray 合约:演示映射到动态数组的存储布局 +contract EVMStorageMappingArray { // 定义映射数组存储合约 + // 映射中值的槽位 = keccak256(key, slot) + // 数组元素的槽位 = keccak256(slot) + 索引 + // 映射 → 数组: keccak256(keccak256(key, 映射声明槽位)) + 索引 + mapping(address => uint256[]) public map; + + address public constant ADDR_1 = address(1); + address public constant ADDR_2 = address(2); + + constructor() { + map[ADDR_1].push(11); + map[ADDR_1].push(22); + map[ADDR_1].push(33); + map[ADDR_2].push(44); + map[ADDR_2].push(55); + map[ADDR_2].push(66); + } + + // test_map_arr 函数:读取映射中某个地址对应的数组元素 + function test_map_arr(address addr, uint256 i) + public + view + returns (uint256 v, uint256 len) + { + uint256 map_slot = 0; + bytes32 map_hash = keccak256(abi.encode(addr, map_slot)); // 映射值槽位(数组长度在此) + bytes32 arr_hash = keccak256(abi.encode(map_hash)); // 数组元素起始槽位 + + assembly { + len := sload(map_hash) // 读取数组长度 + v := sload(add(arr_hash, i)) // 读取第 i 个元素 + } + } +} + +// ===== 动态数组存储结构体 ===== + +// EVMStorageDynamicArrayStruct 合约:演示结构体动态数组的存储布局 +contract EVMStorageDynamicArrayStruct { // 定义结构体动态数组存储合约 + struct Point { // Point 结构体:占据 2 个槽位 + uint256 x; // 槽位 0(相对于结构体起始) + uint128 y; // 槽位 1 的低 128 位 + uint128 z; // 槽位 1 的高 128 位 + } + + // 元素槽位 = keccak256(数组声明槽位) + 索引 * 结构体槽位数 + // keccak256(0) + 索引 * 2 + Point[] private arr; + + constructor() { + arr.push(Point(11, 22, 33)); + arr.push(Point(44, 55, 66)); + arr.push(Point(77, 88, 99)); + } + + // test_struct_arr 函数:读取结构体数组中的元素 + function test_struct_arr(uint256 i) + public + view + returns (uint256 x, uint128 y, uint128 z, uint256 len) + { + uint256 slot = 0; + bytes32 start = keccak256(abi.encode(slot)); // 计算元素起始槽位 + + assembly { + len := sload(slot) // 槽位 0 存储数组长度 + + // 索引 | 槽位 | 数据 + // 0 | start + 0 | arr[0].x + // 0 | start + 1 | arr[0].z | arr[0].y + // 1 | start + 2 | arr[1].x + // 1 | start + 3 | arr[1].z | arr[1].y + x := sload(add(start, mul(i, 2))) // 索引 * 2 = 每个结构体占 2 槽 + let zy := sload(add(start, add(mul(i, 2), 1))) + y := zy // 截取低 128 位 + z := shr(128, zy) // 右移获取高 128 位 + } + } +} +``` + +## 核心知识点 + +| 概念 | 存储规则 | +|------|----------| +| 单变量 | 每个 32 字节变量占用一个独立槽位 | +| 紧凑打包 | 小于 32 字节的变量从右到左合并到同一槽位 | +| 常量和不可变量 | 不占用存储槽位,存储在合约字节码中 | +| 定长数组(32 字节元素) | 槽位 = 起始槽位 + 索引 | +| 定长数组(<32 字节元素) | 相邻元素共享槽位 | +| 动态数组 | 声明槽位存长度;元素槽位 = keccak256(声明槽位) + 索引 | +| 映射 | 值槽位 = keccak256(key, 声明槽位) | +| 嵌套映射 | 值槽位 = keccak256(key1, keccak256(key0, 声明槽位)) | +| 映射到数组 | 值槽位 = keccak256(keccak256(key, 声明槽位)) + 索引 | +| **结构体** | 紧凑打包规则与基本类型相同 | + +**`sstore(k, v)`** — 将值 v 存储到槽位 k +**`sload(k)`** — 从槽位 k 加载 32 字节 +**`.slot`** — 获取变量的存储槽位 +**`.offset`** — 获取变量在槽位中的字节偏移量 diff --git "a/Unit6 EVM/02 \345\206\205\345\255\230.md" "b/Unit6 EVM/02 \345\206\205\345\255\230.md" new file mode 100644 index 0000000..a338be6 --- /dev/null +++ "b/Unit6 EVM/02 \345\206\205\345\255\230.md" @@ -0,0 +1,590 @@ +# EVM 内存(Memory) + +EVM 内存的底层原理。通过内联汇编(Yul)的 `mstore`、`mload` 等操作码,直接操作合约的易失性内存空间。 + +EVM 内存是一个长度为 2²⁵⁶ 的字节数组,每个元素存储 1 字节(0x00 到 0xff),合约调用结束后内存数据即销毁。 + +```solidity +// SPDX-License-Identifier: MIT // SPDX 许可证标识符:MIT 许可证 +pragma solidity 0.8.26; // 版本指令,要求使用 Solidity 0.8.26 版本 + +// ===== 内存布局 ===== +// 内存是一个长度为 2**256 的字节数组 +// index 0 1 2 ... 0xfff...fff = 2**256 - 1 +// memory | 00 | 00 | 00 | ... | 00 | + +// 保留槽位 +// 0x00 - 0x3f(64 字节):哈希方法的暂存空间 +// 0x40 - 0x5f(32 字节):空闲内存指针 — 指向下一个可用的内存位置 +// 0x60 - 0x7f(32 字节):零槽 — 动态内存数组的初始值,永远不应被写入 + +// 空闲内存指针(0x40) +// 初始指向 0x80 + +// ===== 基础内存操作 ===== + +// MemBasic 合约:演示基础的内存读写操作 +contract MemBasic { // 定义 MemBasic 合约 + // mstore(p, v) = 将 32 字节存储到以 p 为起始位置的内存中 + // mload(p) = 从以 p 为起始位置的内存中加载 32 字节 + + // test_1 函数:使用空闲内存指针分配并写入内存 + function test_1() public pure returns (bytes32 b32) { + assembly { + // 读取空闲内存指针 + // p = 0x80(初始值) + let p := mload(0x40) // 从 0x40 加载当前的可用内存位置 + mstore(p, 0xababab) // 在 p 处写入 0xababab + b32 := mload(p) // 从 p 处读回 + } + } + + // test_2 函数:演示非对齐存储(mstore 按字节索引) + function test_2() public pure { + assembly { + mstore(0, 0x11) // 在字节 0 处写入 32 字节 + // index: 0 开始的 32 字节数据 + // 0x00: 0x0000000000000000000000000000000000000000000000000000000000000011 + + mstore(1, 0x22) // 在字节 1 处写入(非 32 字节对齐) + // 0 1 + // 0x00: 0x0000000000000000000000000000000000000000000000000000000000000000 + // 0x20: 0x2200000000000000000000000000000000000000000000000000000000000000 + + mstore(2, 0x33) // 在字节 2 处写入 + // 0 1 2 + // 0x00: 0x0000000000000000000000000000000000000000000000000000000000000000 + // 0x20: 0x0033000000000000000000000000000000000000000000000000000000000000 + + mstore(3, 0x44) // 在字节 3 处写入 + // 0 1 2 3 + // 0x00: 0x0000000000000000000000000000000000000000000000000000000000000000 + // 0x20: 0x0000440000000000000000000000000000000000000000000000000000000000 + } + } +} + +// ===== 结构体内存布局 ===== + +// MemStruct 合约:演示结构体在内存中的布局 +contract MemStruct { // 定义 MemStruct 合约 + // 内存数据不打包 — 所有数据都以 32 字节块为单位存储 + struct Point { // Point 结构体 + uint256 x; // 占用 32 字节 + uint32 y; // 占用 32 字节(即使实际只需 4 字节) + uint32 z; // 占用 32 字节(即使实际只需 4 字节) + } + + // test_read 函数:读取内存中的结构体字段 + function test_read() + public + pure + returns (uint256 x, uint256 y, uint256 z) + { + // Point 从 0x80 开始加载到内存 + Point memory p = Point(1, 2, 3); + + assembly { + x := mload(0x80) // 从 0x80 加载 32 字节 → x = 1 + y := mload(0xa0) // 从 0xa0(0x80+32)加载 → y 所在槽位 + z := mload(0xc0) // 从 0xc0(0xa0+32)加载 → z 所在槽位 + } + } + + // test_write 函数:向内存中的结构体写入数据 + function test_write() + public + pure + returns (bytes32 free_mem_ptr, uint256 x, uint256 y, uint256 z) + { + // 为 Point 分配 0x80 到 0xdf 的内存 + // 空闲内存指针 = 0xe0 + Point memory p; + + assembly { + mstore(p, 11) // 写入 0x80 + mstore(add(p, 0x20), 22) // 写入 0xa0 + mstore(add(p, 0x40), 33) // 写入 0xc0 + free_mem_ptr := mload(0x40) // 读取新的空闲内存指针 = 0xe0 + } + + x = p.x; // 从内存读取 + y = p.y; + z = p.z; + } +} + +// ===== 定长数组内存布局 ===== + +// MemFixedArray 合约:演示定长数组在内存中的布局 +contract MemFixedArray { // 定义 MemFixedArray 合约 + // test_read 函数:读取内存中的定长数组 + function test_read() + public + pure + returns (uint256 a0, uint256 a1, uint256 a2) + { + // arr 从 0x80 开始加载到内存 + // 每个元素占 32 字节 + uint32[3] memory arr = [uint32(1), uint32(2), uint32(3)]; + + assembly { + a0 := mload(0x80) // 元素 0 + a1 := mload(0xa0) // 元素 1 + a2 := mload(0xc0) // 元素 2 + } + } + + // test_write 函数:向内存中的定长数组写入 + function test_write() + public + pure + returns (uint256 a0, uint256 a1, uint256 a2) + { + uint32[3] memory arr; + + assembly { + mstore(arr, 11) // 写入元素 0(0x80) + mstore(add(arr, 0x20), 22) // 写入元素 1(0xa0) + mstore(add(arr, 0x40), 33) // 写入元素 2(0xc0) + } + + a0 = arr[0]; // 11 + a1 = arr[1]; // 22 + a2 = arr[2]; // 33 + } +} + +// ===== 动态数组内存布局 ===== + +// MemDynamicArray 合约:演示动态数组在内存中的布局 +contract MemDynamicArray { // 定义 MemDynamicArray 合约 + // 动态数组内存布局: + // [长度(32字节)] [元素0(32字节)] [元素1(32字节)] ... + + // test_read 函数:读取动态数组在内存中的数据 + function test_read() + public + pure + returns (bytes32 p, uint256 len, uint256 a0, uint256 a1, uint256 a2) + { + uint256[] memory arr = new uint256[](5); // 创建长度为 5 的动态数组 + arr[0] = uint256(11); + arr[1] = uint256(22); + arr[2] = uint256(33); + arr[3] = uint256(44); + arr[4] = uint256(55); + + assembly { + p := arr // 数组指针(指向长度字段) + len := mload(arr) // 0x80:长度 + a0 := mload(add(arr, 0x20)) // 0xa0:元素 0 + a1 := mload(add(arr, 0x40)) // 0xc0:元素 1 + a2 := mload(add(arr, 0x60)) // 0xe0:元素 2 + } + } + + // test_write 函数:手动在内存中构造动态数组 + function test_write() public pure returns (bytes32 p, uint256[] memory) { + uint256[] memory arr = new uint256[](0); // 创建空数组(仅分配长度字段) + + assembly { + p := arr + mstore(arr, 3) // 写入长度 = 3 + mstore(add(arr, 0x20), 11) // 写入元素 0 + mstore(add(arr, 0x40), 22) // 写入元素 1 + mstore(add(arr, 0x60), 33) // 写入元素 2 + // 更新空闲内存指针(长度 1 + 3 个元素 = 4 * 32 = 0x80) + mstore(0x40, add(arr, 0x80)) // 0x80 + 0x80 = 0x100 + } + + // arr 在返回时会被 ABI 编码 + return (p, arr); + } +} + +// ===== 内部函数返回与内存 ===== + +// MemInternalFuncReturn 合约:演示内部函数返回值的内存机制 +contract MemInternalFuncReturn { // 定义内部函数返回合约 + // internal_func_return_val 函数:返回一个值类型 + function internal_func_return_val() private pure returns (uint256) { + return uint256(0xababab); // 返回值通过栈传递 + } + + // test_val 函数:验证值类型返回值在栈顶 + function test_val() public pure { + // 0xababab 将被放在栈顶 + internal_func_return_val(); // 调用后返回值在栈顶(但未使用) + } + + // internal_func_return_mem 函数:返回内存中的动态类型 + function internal_func_return_mem() + private + pure + returns (bytes32[] memory) // 返回动态数组(通过内存) + { + bytes32[] memory arr = new bytes32[](3); + arr[0] = bytes32(uint256(0xaaa)); + arr[1] = bytes32(uint256(0xbbb)); + arr[2] = bytes32(uint256(0xccc)); + return arr; // 内存指针放在栈顶 + } + + // test_mem 函数:读取内部函数返回的动态数组 + function test_mem() + public + pure + returns (uint256 len, bytes32 a0, bytes32 a1, bytes32 a2) + { + // 将 0x80(arr 的内存指针)放在栈顶 + internal_func_return_mem(); + // 从内存中读取 arr 的数据 + assembly { + len := mload(0x80) // arr 在 0x80,读取长度 + a0 := mload(0xa0) // 元素 0 + a1 := mload(0xc0) // 元素 1 + a2 := mload(0xe0) // 元素 2 + } + } +} + +// ===== ABI 编码在内存中的布局 ===== + +// ABIEncode 合约:演示 ABI 编码在内存中的布局 +contract ABIEncode { // 定义 ABIEncode 合约 + // 值类型小于 32 字节 → 左侧零填充 + // 0x000000000000000000000000abababababababababababababababababababab + function encode_addr() public pure returns (bytes memory) { + address addr = 0xABaBaBaBABabABabAbAbABAbABabababaBaBABaB; + return abi.encode(addr); // 地址(20 字节)左侧零填充到 32 字节 + } + + // 定长字节类型 → 右侧零填充 + // 0xaabbccdd00000000000000000000000000000000000000000000000000000000 + function encode_bytes4() public pure returns (bytes memory) { + bytes4 b4 = 0xaabbccdd; + return abi.encode(b4); // bytes4 右侧零填充到 32 字节 + } + + // 动态类型编码格式:偏移量 | 长度 | 数据 + // 0x0000000000000000000000000000000000000000000000000000000000000020 ← 偏移量(32 字节) + // 0000000000000000000000000000000000000000000000000000000000000003 ← 长度 + // ababab0000000000000000000000000000000000000000000000000000000000 ← 数据(左对齐) + function encode_bytes() public pure returns (bytes memory) { + bytes memory b = new bytes(3); + b[0] = 0xab; + b[1] = 0xab; + b[2] = 0xab; + return abi.encode(b); + } + + // 动态数组编码示例 + function encode_uint8_arr() public pure returns (bytes memory) { + uint8[] memory a = new uint8[](3); + a[0] = 1; + a[1] = 2; + a[2] = 3; + return abi.encode(a); + // 编码格式:偏移量 | 长度 | 元素1 | 元素2 | 元素3 + } + + // 定长数组:直接编码每个元素,无偏移量和长度前缀 + function encode_uint256_fixed_size_arr() + public + pure + returns (bytes memory) + { + uint8[3] memory a; + a[0] = 1; + a[1] = 2; + a[2] = 3; + return abi.encode(a); + // 编码格式:元素1 | 元素2 | 元素3(直接拼接) + } + + // 结构体编码(类似定长数组,直接编码字段) + struct Point { uint256 x; uint128 y; uint128 z; } + + function encode_struct() public pure returns (bytes memory) { + Point memory p = Point(1, 2, 3); + return abi.encode(p); + // 编码格式:x(32B) | y(32B) | z(32B) + } + + // 结构体动态数组 + function encode_struct_array() public pure returns (bytes memory) { + Point[] memory arr = new Point[](3); + arr[0] = Point(1, 2, 3); + arr[1] = Point(4, 5, 6); + arr[2] = Point(7, 8, 9); + return abi.encode(arr); + // 编码格式:偏移量 | 长度 | 结构体1 | 结构体2 | 结构体3 + } +} + +// ===== return 操作码 ===== + +// MemReturn 合约:演示 return 操作码的用法 +contract MemReturn { // 定义 MemReturn 合约 + // test_return_vals 函数:使用 return 直接返回内存数据 + function test_return_vals() public pure returns (uint256, uint256) { + // return(start, len) - 停止执行,返回从 start 到 start+len 的内存数据 + assembly { + mstore(0x80, 11) // 在 0x80 写入 11 + mstore(0xa0, 22) // 在 0xa0 写入 22 + return(0x80, 0x40) // 返回 0x80 开始的 64 字节 + } + } + + // test_return_dyn_arr 函数:使用 return 返回动态数组(需手动 ABI 编码) + function test_return_dyn_arr() public pure returns (uint256[] memory) { + // ABI 编码 uint256[] 数组 [11, 22, 33] + assembly { + mstore(0x80, 0x20) // 偏移量(指向实际数据起始位置) + mstore(add(0x80, 0x20), 3) // 长度 = 3 + mstore(add(0x80, 0x40), 11) // 元素 0 + mstore(add(0x80, 0x60), 22) // 元素 1 + mstore(add(0x80, 0x80), 33) // 元素 2 + return(0x80, mul(5, 0x20)) // 返回 5 * 32 = 160 字节 + } + } + + // test_return 函数:return 之后代码不会执行 + function test_return() public pure returns (uint256, uint256) { + test_return_vals(); // 这里执行了 return(0x80, 0x40),直接返回 (11, 22) + // 下面的代码永远不会执行 + return (333, 444); + } +} + +// ===== revert 操作码 ===== + +// MemRevert 合约:演示 revert 操作码的用法 +contract MemRevert { // 定义 MemRevert 合约 + // test_revert 函数:使用 revert 回滚并返回错误数据 + function test_revert() public pure { + // revert(start, len) - 回滚执行,返回从 start 到 start+len 的内存数据 + assembly { + mstore(0x80, "ERROR HERE") // 在 0x80 写入错误消息 + revert(0x80, 0x20) // 回滚,返回 32 字节错误数据 + } + } + + // test_revert_with_error_msg 函数:构造标准 Error(string) 格式的回滚 + function test_revert_with_error_msg() public pure { + assembly { + let p := mload(0x40) // 获取空闲内存指针 + // Error(string) 的函数选择器 + // 0x08c379a0 << 224 = 选择器放在 32 字节的最高 4 字节 + mstore(p, shl(224, 0x08c379a0)) // 写入选择器 + mstore(add(p, 0x04), 0x20) // 字符串偏移量(指向实际数据) + mstore(add(p, 0x24), 5) // 字符串长度 + mstore(add(p, 0x44), "ERROR") // 字符串内容(必须小于 32 字节) + // 总大小 = 4 + 32 + 32 + 32 = 0x64 + revert(p, 0x64) // 回滚并返回 ABI 编码的错误 + } + } +} + +// ===== keccak256 在内存中的应用 ===== + +// MemKeccak 合约:演示 keccak256 操作码 +contract MemKeccak { // 定义 MemKeccak 合约 + // test_keccak 函数:使用汇编对内存数据计算 keccak256 + function test_keccak() public pure returns (bytes32) { + // keccak256(start, len) - 对 start 到 start+len 的内存数据计算 keccak256 + assembly { + mstore(0x80, 1) // 写入数据 1 + mstore(0xa0, 2) // 写入数据 2 + + let h := keccak256(0x80, 0x40) // 对 0x80 开始的 64 字节计算哈希 + mstore(0xc0, h) // 存储哈希结果 + + return(0xc0, 0x20) // 返回 32 字节哈希值 + } + } + + // keccak 函数:Solidity 原生方式作为对比 + function keccak() public pure returns (bytes32) { + return keccak256(abi.encodePacked(uint256(1), uint256(2))); + } +} + +// ===== staticcall 通过汇编调用外部合约 ===== + +// Target 合约:被调用的目标合约 +contract Target { // 定义 Target 合约 + function return_uint256(uint256 x) public pure returns (uint256) { + return x; // 直接返回 + } + + function return_bytes(uint256 n) public pure returns (bytes memory) { + bytes memory out = new bytes(n); + for (uint256 i; i < n; i++) { + out[i] = 0xab; // 填充 0xab + } + return out; + } + + function return_uint256_arr(uint256 n) + public + pure + returns (uint256[] memory) + { + uint256[] memory out = new uint256[](n); + for (uint256 i = 0; i < n; i++) { + out[i] = i + 1; + } + return out; + } +} + +// YulStaticCall 合约:演示 staticcall 操作码 +contract YulStaticCall { // 定义 YulStaticCall 合约 + // test_staticcall 函数:使用汇编执行 staticcall + function test_staticcall(address a, bytes calldata data) public view { + assembly { + let p := mload(0x40) // 内存起始位置 + // 将 calldata 复制到内存 + calldatacopy(p, data.offset, data.length) + + // staticcall(gas, addr, in_offset, in_size, out_offset, out_size) + let ok := staticcall(gas(), a, p, data.length, 0, 0) + + if iszero(ok) { revert(0, 0) } // 失败则回滚 + + let return_data_size := returndatasize() // 获取返回数据大小 + // 将返回数据复制到内存 + returndatacopy(p, 0, return_data_size) + return(p, return_data_size) // 返回数据给调用者 + } + } + + // 封装函数:解码返回的 uint256 + function test_abi_decode_uint256(address a, bytes calldata data) + public view returns (uint256) { test_staticcall(a, data); } + + // 封装函数:解码返回的 bytes + function test_abi_decode_bytes(address a, bytes calldata data) + public view returns (bytes memory) { test_staticcall(a, data); } + + // 封装函数:解码返回的 uint256[] 动态数组 + function test_abi_decode_uint256_arr(address a, bytes calldata data) + public view returns (uint256[] memory) { test_staticcall(a, data); } + + // test_staticcall_return_abi_encoded_bytes 函数:手动处理返回数据 + function test_staticcall_return_abi_encoded_bytes( + address addr, + bytes calldata data // 编码后的函数调用数据 + ) public view returns (bytes memory out, uint256 return_data_size) { + assembly { + let p := mload(0x40) + calldatacopy(p, data.offset, data.length) // 复制 calldata 到内存 + mstore(0x40, add(p, data.length)) // 更新空闲内存指针 + + let ok := staticcall(gas(), addr, p, data.length, 0, 0) + if iszero(ok) { revert(0, 0) } + + return_data_size := returndatasize() + // 将返回数据大小存入 out 的 length 字段 + mstore(out, return_data_size) + // 将返回数据复制到 out + returndatacopy(add(out, 0x20), 0, return_data_size) + // 更新空闲内存指针 + mstore(0x40, add(out, add(0x20, return_data_size))) + } + } +} + +// Counter 合约:用于测试 call 的目标合约 +contract Counter { // 定义 Counter 合约 + uint256 public count; // 计数器 + + function inc() public returns (uint256) { + count += 1; // 状态变更(需 call 而非 staticcall) + return count; + } +} + +// YulCall 合约:演示 call 操作码 +contract YulCall { // 定义 YulCall 合约 + // test_call 函数:使用汇编执行 call(可修改状态 + 可发送 ETH) + function test_call(address a, bytes memory data) + public + payable // 可接收 ETH 转发 + returns (bytes memory out) + { + assembly { + let data_ptr := data + let out_ptr := out + let data_size := mload(data) // 读取 data 的长度 + let data_start := add(data, 0x20) // 跳过长度前缀 + // call(gas, addr, value, in_offset, in_size, out_offset, out_size) + let ok := call(gas(), a, callvalue(), data_start, data_size, 0, 0) + + if iszero(ok) { revert(0, 0) } + + let return_data_size := returndatasize() + mstore(out, return_data_size) // 写入返回数据长度 + returndatacopy(add(out, 0x20), 0, return_data_size) // 复制返回数据 + mstore(0x40, add(out, add(0x20, return_data_size))) // 更新空闲内存指针 + } + } + + // test_inc 函数:调用 Counter 合约的 inc 并解码返回值 + function test_inc(address counter) public returns (uint256 count) { + bytes memory res = test_call(counter, abi.encodeCall(Counter.inc, ())); + count = abi.decode(res, (uint256)); + } +} + +// ===== 内存扩展的 Gas 消耗 ===== + +// MemExp 合约:演示内存扩展的 Gas 消耗(与分配量呈二次方关系) +contract MemExp { // 定义 MemExp 合约 + function alloc_mem(uint256 n) external view returns (uint256) { + uint256 gas_start = gasleft(); // 记录起始 Gas + uint256[] memory arr = new uint256[](n); // 分配 n 个元素 + uint256 gas_end = gasleft(); // 记录结束 Gas + return gas_start - gas_end; // 返回 Gas 消耗 + } +} + +// 内存分配量与 Gas 消耗的关系: +// arr size | gas cost +// 0 | 120 +// 1 | 178 +// 10 | 232 +// 20 | 293 +// 50 | 477 +// 100 | 793 +// 200 | 1454 +// 1000 | 8144 +// 2000 | 20023 +// 5000 | 79097 +// 10000 | 255679 +// 20000 | 901812 +// Gas 消耗随内存分配呈二次方增长! +``` + +## 核心知识点 + +| 概念 | 内存规则 | +|------|----------| +| **内存布局** | 0x00-0x3f 暂存空间、0x40 空闲指针、0x60 零槽 | +| **mstore(p, v)** | 将 32 字节值 v 写入以字节 p 为起始位置的内存 | +| **mload(p)** | 从以字节 p 为起始位置的内存加载 32 字节 | +| **结构体** | 内存中不紧凑打包,每个字段占 32 字节 | +| **定长数组** | 连续存储,每个元素占 32 字节 | +| **动态数组** | [长度(32B)] + [元素连续存储],指针指向长度字段 | +| **ABI 编码** | 值类型左零填充,动态类型使用偏移量+长度+数据格式 | +| **return(p, s)** | 停止执行并返回 p 开始的 s 字节内存数据 | +| **revert(p, s)** | 回滚执行并返回 p 开始的 s 字节内存数据 | +| **keccak256(p, s)** | 对 p 开始的 s 字节内存数据计算哈希 | +| **calldatacopy(p, o, s)** | 将 calldata 中偏移 o 开始的 s 字节复制到内存 p | +| **returndatasize()** | 获取外部调用返回数据的大小 | +| **returndatacopy(p, o, s)** | 将返回数据复制到内存 | +| **staticcall(g, a, i, is, o, os)** | 只读外部调用 | +| **call(g, a, v, i, is, o, os)** | 可修改状态的外部调用,可发送 ETH | +| **内存扩展 Gas** | 消耗与分配量呈二次方关系 | diff --git "a/Unit7 Foundry/01 \346\216\210\346\235\203.md" "b/Unit7 Foundry/01 \346\216\210\346\235\203.md" new file mode 100644 index 0000000..c983145 --- /dev/null +++ "b/Unit7 Foundry/01 \346\216\210\346\235\203.md" @@ -0,0 +1,63 @@ +# Foundry 授权测试 + +Foundry 提供了 `vm.prank` 和 `vm.startPrank`/`vm.stopPrank` 作弊码,用于在测试中模拟指定地址发送消息。 + +```solidity +// SPDX-License-Identifier: MIT // SPDX 许可证标识符:MIT 许可证 +pragma solidity 0.8.26; // 版本指令,要求使用 Solidity 0.8.26 版本 + +import {Test, console2, stdError} from "forge-std/Test.sol"; // 导入 Foundry 标准测试库 + +// Auth 合约:简单的权限控制合约 +contract Auth { // 定义 Auth 合约 + address public owner; // 合约所有者 + + constructor() { + owner = msg.sender; // 部署者成为所有者 + } + + // setOwner 函数:更改所有者(仅当前所有者可调用) + function setOwner(address _owner) external { + require(msg.sender == owner, "not authorized"); // 权限检查 + owner = _owner; // 更新所有者 + } +} + +// AuthTest 合约:Auth 的 Foundry 测试 +contract AuthTest is Test { // 继承 Test 合约,获得作弊码和断言 + Auth private auth; // 测试目标合约 + + // setUp 函数:每个测试用例执行前自动调用 + function setUp() public { + auth = new Auth(); // 部署新合约,owner = 本测试合约 + } + + // testSetOwner 函数:测试正常更改所有者 + function testSetOwner() public { + auth.setOwner(address(1)); // 调用 setOwner + assertEq(auth.owner(), address(1)); // 断言 owner 已更新 + } + + // testFailNotOwner 函数:测试非所有者调用会失败 + function testFailNotOwner() public { + // vm.prank(address):设置下一次调用的 msg.sender 为指定地址 + vm.prank(address(1)); // 下一次调用 msg.sender = address(1) + auth.setOwner(address(1)); // 非所有者调用,应 revert + + // vm.startPrank(address) ... vm.stopPrank():设置区间内的所有 msg.sender + vm.startPrank(address(1)); // 从此开始所有调用的 msg.sender = address(1) + auth.setOwner(address(1)); + auth.setOwner(address(1)); + auth.setOwner(address(1)); + vm.stopPrank(); // 恢复 msg.sender 为测试合约 + } +} +``` + +## Foundry 作弊码速查 + +| 作弊码 | 功能 | +|--------|------| +| `vm.prank(addr)` | 设置**下一次**外部调用的 `msg.sender` 为 `addr` | +| `vm.startPrank(addr)` | 设置**后续所有**调用(直到 `stopPrank`)的 `msg.sender` | +| `vm.stopPrank()` | 停止 `startPrank` 的模拟 | diff --git "a/Unit7 Foundry/02 \345\237\272\347\241\200\346\265\213\350\257\225.md" "b/Unit7 Foundry/02 \345\237\272\347\241\200\346\265\213\350\257\225.md" new file mode 100644 index 0000000..c4090bd --- /dev/null +++ "b/Unit7 Foundry/02 \345\237\272\347\241\200\346\265\213\350\257\225.md" @@ -0,0 +1,67 @@ +# Foundry 基础测试 + +Foundry 基础测试示例,演示 `setUp`、`test*`、`testFail*` 和 `vm.expectRevert` 的用法。 + +```solidity +// SPDX-License-Identifier: MIT // SPDX 许可证标识符:MIT 许可证 +pragma solidity 0.8.26; // 版本指令,要求使用 Solidity 0.8.26 版本 + +import {Test, console2, stdError} from "forge-std/Test.sol"; // 导入 Foundry 标准测试库 + +// Counter 合约:简单的计数器合约 +contract Counter { // 定义 Counter 合约 + uint256 public count; // 计数器值 + + // inc 函数:计数器加一 + function inc() external { + count += 1; // count = count + 1 + } + + // dec 函数:计数器减一 + function dec() external { + count -= 1; // count = count - 1 + } +} + +// CounterTest 合约:Counter 的 Foundry 测试 +contract CounterTest is Test { // 继承 Test 合约 + Counter public counter; // 测试目标 + + // setUp 函数:每个测试用例执行前调用 + function setUp() public { + counter = new Counter(); // 部署全新的 Counter 实例 + } + + // testInc 函数:测试 inc 函数 + function testInc() public { + counter.inc(); // 调用 inc + assertEq(counter.count(), 1); // 断言 count == 1 + } + + // testFailDec 函数:预期此测试会失败(因为 count=0 时 dec 会下溢) + function testFailDec() public { + counter.dec(); // 0 - 1 = 下溢,solidity 0.8+ 会 revert + } + + // testDecUnderflow 函数:使用 expectRevert 明确断言会 revert + function testDecUnderflow() public { + vm.expectRevert(stdError.arithmeticError); // 预期算术溢出错误 + counter.dec(); // 此调用应 revert + } + + // testDec 函数:正常测试 dec + function testDec() public { + counter.inc(); // count = 1 + counter.inc(); // count = 2 + counter.dec(); // count = 1 + assertEq(counter.count(), 1); // 断言 count == 1 + } +} +``` + +## 测试命名规则 + +| 函数前缀 | 含义 | +|----------|------| +| `test*` | 正常的测试用例,不 revert 视为通过 | +| `testFail*` | **预期会 revert**,revert 视为通过(不 revert 为失败) | diff --git "a/Unit7 Foundry/03 \351\224\231\350\257\257.md" "b/Unit7 Foundry/03 \351\224\231\350\257\257.md" new file mode 100644 index 0000000..6ee69f1 --- /dev/null +++ "b/Unit7 Foundry/03 \351\224\231\350\257\257.md" @@ -0,0 +1,73 @@ +# Foundry 错误测试 + +Foundry 中对 require 错误信息和自定义错误的测试。使用 `vm.expectRevert` 可以精确匹配预期的错误。 + +```solidity +// SPDX-License-Identifier: MIT // SPDX 许可证标识符:MIT 许可证 +pragma solidity 0.8.26; // 版本指令,要求使用 Solidity 0.8.26 版本 + +import "forge-std/Test.sol"; // 导入 Foundry 标准测试库 + +// Error 合约:包含不同类型的错误抛出 +contract Error { // 定义 Error 合约 + // NotAuthorized 自定义错误(Gas 效率比 require string 更高) + error NotAuthorized(); // 自定义错误声明 + + // throwError 函数:抛出 require 错误 + function throwError() external { + require(false, "not authorized"); // require(false) 强制 revert + } + + // throwCustomError 函数:抛出自定义错误 + function throwCustomError() external { + revert NotAuthorized(); // 使用 revert 抛出自定义错误 + } +} + +// ErrorTest 合约:Error 的 Foundry 测试 +contract ErrorTest is Test { // 继承 Test 合约 + Error public err; // 测试目标 + + function setUp() public { + err = new Error(); // 部署 Error 合约 + } + + // testFail 函数:testFail 前缀表示预期此测试会 revert + function testFail() public { + err.throwError(); // 会 revert,因此测试通过 + } + + // testRevert 函数:使用 expectRevert 断言会 revert + function testRevert() public { + vm.expectRevert(); // 预期接下来的调用会 revert(不检查错误消息) + err.throwError(); // 实际执行 + } + + // testRequireMessage 函数:精确检查 require 的错误信息 + function testRequireMessage() public { + vm.expectRevert(bytes("not authorized")); // 预期此具体错误消息 + err.throwError(); // 要求 revert 消息为 "not authorized" + } + + // testCustomError 函数:精确匹配自定义错误 + function testCustomError() public { + vm.expectRevert(Error.NotAuthorized.selector); // 用选择器匹配自定义错误 + err.throwCustomError(); // 要求抛出 NotAuthorized 错误 + } + + // testErrorLabel 函数:为断言添加标签(便于定位失败原因) + function testErrorLabel() public { + assertEq(uint256(1), uint256(1), "test 1"); // 断言带标签 + assertEq(uint256(1), uint256(1), "test 2"); + assertEq(uint256(1), uint256(1), "test 3"); + } +} +``` + +## vm.expectRevert 用法 + +| 用法 | 匹配范围 | +|------|----------| +| `vm.expectRevert()` | 匹配任意 revert | +| `vm.expectRevert(bytes("msg"))` | 精确匹配 require 消息 | +| `vm.expectRevert(Error.selector)` | 精确匹配自定义错误选择器 | diff --git "a/Unit7 Foundry/04 \344\272\213\344\273\266.md" "b/Unit7 Foundry/04 \344\272\213\344\273\266.md" new file mode 100644 index 0000000..7f3926a --- /dev/null +++ "b/Unit7 Foundry/04 \344\272\213\344\273\266.md" @@ -0,0 +1,97 @@ +# Foundry 事件测试 + +Foundry 中使用 `vm.expectEmit` 可以精确验证合约是否发出了预期的事件。 + +```solidity +// SPDX-License-Identifier: MIT // SPDX 许可证标识符:MIT 许可证 +pragma solidity 0.8.26; // 版本指令,要求使用 Solidity 0.8.26 版本 + +import "forge-std/Test.sol"; // 导入 Foundry 标准测试库 + +// Event 合约:包含事件发射的测试合约 +contract Event { // 定义 Event 合约 + // Transfer 事件:记录转账信息 + event Transfer(address indexed from, address indexed to, uint256 amount); + + // transfer 函数:单次转账 + function transfer(address from, address to, uint256 amount) external { + emit Transfer(from, to, amount); // 发射 Transfer 事件 + } + + // transferMany 函数:批量转账 + function transferMany( + address from, + address[] calldata to, // 接收者列表 + uint256[] calldata amounts // 金额列表 + ) external { + for (uint256 i = 0; i < to.length; i++) { + emit Transfer(from, to[i], amounts[i]); // 逐个发射事件 + } + } +} + +// EventTest 合约:Event 的 Foundry 测试 +contract EventTest is Test { // 继承 Test 合约 + Event public e; // 测试目标 + + // 声明同名事件,用于 expectEmit 的"预期事件" + event Transfer(address indexed from, address indexed to, uint256 amount); + + function setUp() public { + e = new Event(); // 部署 Event 合约 + } + + // testEmitTransferEvent 函数:测试单次事件发射 + function testEmitTransferEvent() public { + // vm.expectEmit(checkTopic1, checkTopic2, checkTopic3, checkData) + // checkTopic1: 是否检查索引参数 1 + // checkTopic2: 是否检查索引参数 2 + // checkTopic3: 是否检查索引参数 3 + // checkData: 是否检查非索引数据(data 字段) + + // 1. 告知 Foundry 检查哪些字段(topic1, topic2, data) + vm.expectEmit(true, true, false, true); // 检查 from, to 和 amount + // 2. 在本合约中发出"预期的事件" + emit Transfer(address(this), address(123), 456); + // 3. 调用被测试函数,对比实际事件与预期事件 + e.transfer(address(this), address(123), 456); + + // 仅检查 topic1(from),topic2 和 data 不检查 + vm.expectEmit(true, false, false, false); + emit Transfer(address(this), address(123), 456); + // 注意:topic2(to)和 data(amount)不匹配,但测试仍然通过 + e.transfer(address(this), address(111), 222); + } + + // testEmitManyTransferEvent 函数:测试批量事件发射 + function testEmitManyTransferEvent() public { + address[] memory to = new address[](2); + to[0] = address(111); + to[1] = address(222); + + uint256[] memory amounts = new uint256[](2); + amounts[0] = 1; + amounts[1] = 2; + + // 可以预先设置多个 expectEmit,按顺序匹配 + for (uint256 i = 0; i < to.length; i++) { + vm.expectEmit(true, true, false, true); // 1. 设置检查规则 + emit Transfer(address(this), to[i], amounts[i]); // 2. 发出预期事件 + } + + // 3. 调用被测试函数 + e.transferMany(address(this), to, amounts); + } +} +``` + +## vm.expectEmit 参数说明 + +| 参数 | 含义 | +|------|------| +| `checkTopic1` | 是否检查事件的第一个索引参数 | +| `checkTopic2` | 是否检查事件的第二个索引参数 | +| `checkTopic3` | 是否检查事件的第三个索引参数 | +| `checkData` | 是否检查事件的非索引数据部分 | + +**使用流程**:`vm.expectEmit` → 发出预期事件 → 调用被测试函数(三步不能乱) diff --git "a/Unit7 Foundry/05 \346\240\207\347\255\276.md" "b/Unit7 Foundry/05 \346\240\207\347\255\276.md" new file mode 100644 index 0000000..e6e5dcf --- /dev/null +++ "b/Unit7 Foundry/05 \346\240\207\347\255\276.md" @@ -0,0 +1,44 @@ +# Foundry 标签 + +使用 `vm.label(address, string)` 给地址打标签,标签会显示在 Foundry 的堆栈跟踪中,极大提升调试体验。 + +```solidity +// SPDX-License-Identifier: MIT // SPDX 许可证标识符:MIT 许可证 +pragma solidity 0.8.26; // 版本指令,要求使用 Solidity 0.8.26 版本 + +import "forge-std/Test.sol"; // 导入 Foundry 标准测试库 + +// WETH 主网合约地址(常量) +address constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; + +// IERC20 接口:最小 ERC20 接口 +interface IERC20 { // 定义 IERC20 接口 + function balanceOf(address) external view returns (uint256); // 查询余额 +} + +// LabelTest 合约:演示 vm.label 的用法 +contract LabelTest is Test { // 继承 Test 合约 + IERC20 weth; // WETH 代币接口 + + function setUp() public { + weth = IERC20(WETH); // 绑定 WETH 主网地址 + // vm.label:为该地址打上标签,堆栈跟踪中会显示 "WETH" 而非裸地址 + vm.label(WETH, "WETH"); // 标记 WETH 合约地址 + } + + // test 函数:查询 WETH 余额并打印 + function test() public { + // %e 格式打印整数(带逗号分隔)或地址 + console.log("%e", weth.balanceOf(address(this))); // 打印本合约的 WETH 余额 + } +} +``` + +## console.log 格式化说明 + +| 格式化符 | 含义 | +|----------|------| +| `%s` | 字符串 | +| `%d` | 十进制整数 | +| `%e` | 地址或带格式的整数 | +| `%x` | 十六进制 | diff --git "a/Unit7 Foundry/06 \346\250\241\346\213\237\350\260\203\347\224\250.md" "b/Unit7 Foundry/06 \346\250\241\346\213\237\350\260\203\347\224\250.md" new file mode 100644 index 0000000..7263738 --- /dev/null +++ "b/Unit7 Foundry/06 \346\250\241\346\213\237\350\260\203\347\224\250.md" @@ -0,0 +1,61 @@ +# Foundry 模拟调用 + +使用 `vm.mockCall` 可以在测试中模拟外部合约的返回值,无需部署真实的依赖合约。 + +```solidity +// SPDX-License-Identifier: MIT // SPDX 许可证标识符:MIT 许可证 +pragma solidity 0.8.26; // 版本指令,要求使用 Solidity 0.8.26 版本 + +import "forge-std/Test.sol"; // 导入 Foundry 标准测试库 + +// Target 合约:被模拟调用的目标合约 +contract Target { // 定义 Target 合约 + // f 函数:接受两个参数,调用内部函数 g 并返回 + function f(uint256 x, uint256 y) external view returns (uint256) { + return g(); // 调用内部函数 + } + + // g 内部函数:始终返回 1 + function g() internal view returns (uint256) { + return 1; // 真实返回值为 1 + } +} + +// MockCallTest 合约:演示 mockCall 的用法 +contract MockCallTest is Test { // 继承 Test 合约 + Target target; // 被模拟的合约实例 + + function setUp() public { + target = new Target(); // 部署 Target 合约 + } + + // test 函数:使用 mockCall 拦截调用并返回自定义数据 + function test() public { + uint256 x = 1; + uint256 y = 1; + + // vm.mockCall(address, calldata, returnData) + // 当对 address 发起与 calldata 匹配的调用时,直接返回 returnData + // 注意:mockCall 也会影响内部调用 + vm.mockCall( + address(target), // 目标合约地址 + abi.encodeCall(Target.f, (x, y)), // 要匹配的 calldata + abi.encode(uint256(99)) // 模拟的返回值(99) + ); + + // 调用 f(x, y),实际返回 99 而不是 1 + uint256 res = target.f(x, y); + console.log("res", res); // 打印 99 + } +} +``` + +## vm.mockCall 参数 + +| 参数 | 说明 | +|------|------| +| `address` | 要拦截调用的合约地址 | +| `calldata` | 要匹配的调用数据(函数选择器 + 参数) | +| `returnData` | 模拟的返回值(ABI 编码格式) | + +**注意**:`mockCall` 会影响内部调用(而非仅外部调用),这意味着它可以模拟目标合约中的 `internal` 函数返回值。 diff --git "a/Unit7 Foundry/07 \345\217\221\351\200\201.md" "b/Unit7 Foundry/07 \345\217\221\351\200\201.md" new file mode 100644 index 0000000..4aaf869 --- /dev/null +++ "b/Unit7 Foundry/07 \345\217\221\351\200\201.md" @@ -0,0 +1,41 @@ +# Foundry 发送与余额设置 + +使用 `deal` 可以给任意地址设置 ETH 余额或 ERC20 代币余额。`hoax` 是 `deal` + `prank` 的组合。 + +```solidity +// SPDX-License-Identifier: MIT // SPDX 许可证标识符:MIT 许可证 +pragma solidity 0.8.26; // 版本指令,要求使用 Solidity 0.8.26 版本 + +import "forge-std/Test.sol"; // 导入 Foundry 标准测试库 + +// ERC20 合约:最小 ERC20 实现(仅用于测试演示) +contract ERC20 { // 定义 ERC20 合约 + uint256 public totalSupply; // 总供应量 + mapping(address => uint256) public balanceOf; // 余额映射 +} + +// SendTest 合约:演示 deal 和 hoax 的用法 +contract SendTest is Test { // 继承 Test 合约 + ERC20 token = new ERC20(); // 部署测试 ERC20 合约 + + // testSendEth 函数:演示设置余额 + function testSendEth() public { + // deal(address, uint256):设置地址的 ETH 余额 + deal(address(1), 100); // 给 address(1) 100 wei ETH + assertEq(address(1).balance, 100); // 断言余额为 100 + + // deal(address, address, uint256):设置 ERC20 代币余额 + // deal(token, user, amount) — 直接修改 token 合约中 user 的余额 + deal(address(token), address(1), 10); // 给 address(1) 10 个 ERC20 代币 + assertEq(token.balanceOf(address(1)), 10); // 断言 ERC20 余额为 10 + } +} +``` + +## deal / hoax 说明 + +| 作弊码 | 功能 | +|--------|------| +| `deal(addr, amount)` | 设置 `addr` 的 ETH 余额为 `amount` wei | +| `deal(token, addr, amount)` | 设置 `addr` 在 `token` 合约中的余额为 `amount`(直接写存储) | +| `hoax(addr, amount)` | `deal` + `prank`:设置余额并模拟 `addr` 为下一调用的发送者 | diff --git "a/Unit7 Foundry/08 \347\255\276\345\220\215.md" "b/Unit7 Foundry/08 \347\255\276\345\220\215.md" new file mode 100644 index 0000000..75fbf58 --- /dev/null +++ "b/Unit7 Foundry/08 \347\255\276\345\220\215.md" @@ -0,0 +1,51 @@ +# Foundry 签名测试 + +使用 `vm.sign` 可以用私钥对消息进行签名,组合 `vm.addr` 从私钥推导公钥地址,再利用 `ecrecover` 验证签名。 + +```solidity +// SPDX-License-Identifier: MIT // SPDX 许可证标识符:MIT 许可证 +pragma solidity 0.8.26; // 版本指令,要求使用 Solidity 0.8.26 版本 + +import "forge-std/Test.sol"; // 导入 Foundry 标准测试库 + +// SignTest 合约:演示 vm.sign 的用法 +contract SignTest is Test { // 继承 Test 合约 + // 签名流程: + // private key = 123 + // public key = vm.addr(privateKey) — 从私钥推导地址 + // message = "secret message" + // message hash = keccak256(message) + // vm.sign(privateKey, messageHash) — 用私钥签名消息哈希 + + // testSignature 函数:测试签名生成和验证 + function testSignature() public { + uint256 privateKey = 123; // 测试用私钥 + // vm.addr(privateKey):从私钥计算对应的地址 + address alice = vm.addr(privateKey); // 推导 Alice 的地址 + + // 测试有效签名 + bytes32 messageHash = keccak256("Signed by Alice"); // 消息哈希 + + // vm.sign(privateKey, digest):用私钥签名 + (uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey, messageHash); // 生成签名 + // ecrecover 预编译:从签名恢复地址 + address signer = ecrecover(messageHash, v, r, s); // 恢复签名者 + + assertEq(signer, alice); // 恢复的地址应等于 Alice + + // 测试无效消息 + bytes32 invalidHash = keccak256("Not signed by Alice"); // 不同的消息 + signer = ecrecover(invalidHash, v, r, s); // 使用同一签名尝试恢复 + + assertTrue(signer != alice); // 签名者不应等于 Alice + } +} +``` + +## 签名相关操作 + +| 操作 | 说明 | +|------|------| +| `vm.addr(privateKey)` | 从私钥推导以太坊地址 | +| `vm.sign(privateKey, digest)` | 用私钥对消息哈希签名,返回 `(v, r, s)` | +| `ecrecover(digest, v, r, s)` | 以太坊预编译,从签名恢复地址 | diff --git "a/Unit7 Foundry/09 \346\227\266\351\227\264.md" "b/Unit7 Foundry/09 \346\227\266\351\227\264.md" new file mode 100644 index 0000000..d0fe001 --- /dev/null +++ "b/Unit7 Foundry/09 \346\227\266\351\227\264.md" @@ -0,0 +1,52 @@ +# Foundry 时间操作 + +使用 `vm.warp`、`vm.roll`、`skip` 和 `rewind` 可以操控区块时间和区块号。 + +```solidity +// SPDX-License-Identifier: MIT // SPDX 许可证标识符:MIT 许可证 +pragma solidity 0.8.26; // 版本指令,要求使用 Solidity 0.8.26 版本 + +import "forge-std/Test.sol"; // 导入 Foundry 标准测试库 + +// TimeTest 合约:演示时间操控作弊码 +contract TimeTest is Test { // 继承 Test 合约 + // vm.warp(timestamp) - 设置 block.timestamp 为指定时间戳 + // vm.roll(blockNum) - 设置 block.number 为指定区块号 + // skip(seconds) - 当前时间戳增加 N 秒 + // rewind(seconds) - 当前时间戳减少 N 秒 + + function test() public { + console.log("timestamp", block.timestamp); // 打印当前时间戳 + console.log("block number", block.number); // 打印当前区块号 + + // vm.warp:跳跃到指定时间戳 + console.log("warp"); + vm.warp(block.timestamp + 10); // 时间戳 + 10 秒 + console.log("timestamp", block.timestamp); // 打印新时间戳 + + // skip:时间戳跳跃 N 秒(相对操作) + console.log("skip"); + skip(10); // 时间戳再 + 10 秒 + console.log("timestamp", block.timestamp); + + // vm.roll:跳跃到指定区块号 + console.log("roll"); + vm.roll(10); // 区块号直接设为 10 + console.log("block number", block.number); + + // rewind:时间戳回退 N 秒 + console.log("rewind"); + rewind(10); // 时间戳 - 10 秒 + console.log("timestamp", block.timestamp); + } +} +``` + +## 时间操控作弊码速查 + +| 作弊码 | 功能 | 类型 | +|--------|------|------| +| `vm.warp(ts)` | 设置 `block.timestamp = ts` | 绝对设置 | +| `vm.roll(num)` | 设置 `block.number = num` | 绝对设置 | +| `skip(n)` | `block.timestamp += n` | 相对增加 | +| `rewind(n)` | `block.timestamp -= n` | 相对减少 | diff --git "a/Unit7 Foundry/10 \345\255\230\345\202\250\345\206\231\345\205\245.md" "b/Unit7 Foundry/10 \345\255\230\345\202\250\345\206\231\345\205\245.md" new file mode 100644 index 0000000..032038a --- /dev/null +++ "b/Unit7 Foundry/10 \345\255\230\345\202\250\345\206\231\345\205\245.md" @@ -0,0 +1,97 @@ +# Foundry 存储写入 + +使用 `vm.store` 可以直接修改任何合约的存储槽位。这在测试中可以绕过权限检查,直接设置状态。 + +```solidity +// SPDX-License-Identifier: MIT // SPDX 许可证标识符:MIT 许可证 +pragma solidity 0.8.26; // 版本指令,要求使用 Solidity 0.8.26 版本 + +import {Test, console2} from "forge-std/Test.sol"; // 导入 Foundry 标准测试库 + +// Vault 合约:一个需要密码才能提款的保险库合约 +contract Vault { // 定义 Vault 合约 + // 槽位 0 + address public owner; // 合约所有者 + // 槽位 1 + uint256 public password; // 提款密码 + // 槽位 2 + mapping(address => uint256) public balances; // 用户余额映射 + + constructor(uint256 _password) { + owner = msg.sender; // 部署者成为所有者 + password = _password; // 设置密码 + } + + // withdraw 函数:提取余额(仅所有者) + function withdraw() external { + require(msg.sender == owner, "not owner"); // 权限检查 + uint256 bal = balances[msg.sender]; // 读取余额 + balances[msg.sender] = 0; // 清零 + payable(msg.sender).transfer(bal); // 转账 ETH + } +} + +// StoreTest 合约:演示 vm.store 的用法 +contract StoreTest is Test { // 继承 Test 合约 + Vault vault; // 测试目标 + + function setUp() public { + vault = new Vault(12345); // 部署 Vault,密码为 12345 + } + + // vm.store(account, slot, value) 参数说明: + // account: 目标合约地址 + // slot: 要写入的存储槽位(bytes32) + // value: 要写入的值(bytes32) + + // test_store_simple_slot 函数:直接修改简单槽位 + function test_store_simple_slot() public { + // 槽位 0 存储 owner 地址 + // 将 owner 改为 address(1) + vm.store( + address(vault), // 目标合约 + bytes32(uint256(0)), // 槽位 0 + bytes32(uint256(uint160(address(1)))) // address(1) 左填充到 32 字节 + ); + assertEq(vault.owner(), address(1)); // 验证 owner 已变更 + + // 槽位 1 存储 password + // 将 password 改为 999 + vm.store( + address(vault), // 目标合约 + bytes32(uint256(1)), // 槽位 1 + bytes32(uint256(999)) // 新密码 999 + ); + assertEq(vault.password(), 999); // 验证 password 已变更 + } + + // test_store_mapping 函数:修改映射中的值 + function test_store_mapping() public { + // 映射值的槽位计算公式:keccak256(abi.encode(key, mappingSlot)) + address user = address(0xBEEF); // 目标用户 + uint256 mappingSlot = 2; // balances 声明在槽位 2 + + // 计算 balances[user] 的存储槽位 + bytes32 slot = keccak256(abi.encode(user, mappingSlot)); + + // 将 user 的余额设置为 100 ether + vm.store( + address(vault), // 目标合约 + slot, // 计算出的槽位 + bytes32(uint256(100 ether)) // 100 ether 转换为 32 字节 + ); + + assertEq(vault.balances(user), 100 ether); // 验证余额已设置 + } +} +``` + +## vm.store 参数说明 + +| 参数 | 说明 | +|------|------| +| `account` | 目标合约地址 | +| `slot` | 存储槽位编号(bytes32) | +| `value` | 要写入的值(bytes32) | + +对于映射类型,槽位需要通过 `keccak256(abi.encode(key, mappingSlot))` 计算。这是 EVM 存储布局的标准规则。