From 52eedac4a85832a23b5c07a94783fd4e6d7efc1f Mon Sep 17 00:00:00 2001 From: michael88888888 Date: Fri, 1 May 2026 19:25:46 +0800 Subject: [PATCH] Sync up to date with 'solidity-by-example.github.io' --- ...41\344\273\266\350\257\255\345\217\245.md" | 35 + ...11\345\200\274\347\261\273\345\236\213.md" | 113 ++++ ...54\346\200\201\345\255\230\345\202\250.md" | 136 ++++ ...13\344\273\266\350\277\233\351\230\266.md" | 109 +++ ...> "31 Payable \345\207\275\346\225\260.md" | 0 ...01\344\273\245\345\244\252\345\270\201.md" | 88 +++ "33 Fallback\345\207\275\346\225\260.md" | 126 ++++ "34 Call\350\260\203\347\224\250.md" | 67 ++ ...24\346\211\230\350\260\203\347\224\250.md" | 67 ++ ...60\351\200\211\346\213\251\345\231\250.md" | 44 ++ ...66\344\273\226\345\220\210\347\272\246.md" | 57 ++ ...33\345\273\272\345\220\210\347\272\246.md" | 81 +++ 39 Try Catch.md | 72 ++ "40 Import\345\257\274\345\205\245.md" | 78 +++ "41 Library\345\272\223.md" | 71 ++ "42 ABI Encode\347\274\226\347\240\201.md" | 71 ++ "43 ABI Decode\350\247\243\347\240\201.md" | 50 ++ "44 Keccak256\345\223\210\345\270\214.md" | 68 ++ ...14\350\257\201\347\255\276\345\220\215.md" | 104 +++ ...30\345\214\226\346\212\200\345\267\247.md" | 61 ++ ...15\350\277\220\347\256\227\347\254\246.md" | 200 ++++++ ...60\345\255\246\350\277\220\347\256\227.md" | 46 ++ ...07\347\274\226\345\217\230\351\207\217.md" | 24 + ...41\344\273\266\350\257\255\345\217\245.md" | 41 ++ ...07\347\274\226\345\276\252\347\216\257.md" | 47 ++ ...31\350\257\257\345\244\204\347\220\206.md" | 27 + ...60\345\255\246\350\277\220\347\256\227.md" | 63 ++ ...55\345\244\232\344\273\243\345\270\201.md" | 287 ++++++++ .../ERC20\344\273\243\345\270\201.md" | 228 +++++++ ...50\345\214\226\344\273\243\345\270\201.md" | 222 +++++++ ...52\345\270\201\351\222\261\345\214\205.md" | 32 + ...17\345\255\230\345\202\250\346\247\275.md" | 47 ++ ...57\344\273\230\346\270\240\351\201\223.md" | 273 ++++++++ ...07\347\272\247\344\273\243\347\220\206.md" | 298 +++++++++ ...55\344\273\243\346\230\240\345\260\204.md" | 103 +++ ...32\347\255\276\351\222\261\345\214\205.md" | 236 +++++++ ...43\345\270\201\350\275\254\350\264\246.md" | 245 +++++++ ...43\347\220\206\345\220\210\347\272\246.md" | 69 ++ ...02\347\240\201\345\220\210\347\272\246.md" | 70 ++ ...73\346\204\217\345\220\210\347\272\246.md" | 92 +++ ...10\347\272\246\345\234\260\345\235\200.md" | 109 +++ ...30\345\205\213\345\260\224\346\240\221.md" | 96 +++ ...13\345\260\224\347\251\272\346\212\225.md" | 125 ++++ ...74\351\242\204\350\250\200\346\234\272.md" | 53 ++ ...74\351\242\204\350\250\200\346\234\272.md" | 59 ++ ...45\256\232\344\271\230\347\247\257 AMM.md" | 163 +++++ ...46\201\222\345\256\232\345\222\214 AMM.md" | 125 ++++ .../07 DAI \344\273\243\347\220\206.md" | 122 ++++ ...50\346\212\274\345\245\226\345\212\261.md" | 95 +++ ...45\256\232\344\272\244\346\215\242 AMM.md" | 317 +++++++++ ...50\346\212\274\345\245\226\345\212\261.md" | 139 ++++ ...1 \344\273\243\345\270\201\351\224\201.md" | 116 ++++ "Unit4 DeFi/12 \351\207\221\345\272\223.md" | 70 ++ ...52\347\224\265\344\272\244\346\215\242.md" | 92 +++ ...25\350\276\271\344\276\233\347\273\231.md" | 113 ++++ ...15 Uniswap V3 \344\272\244\346\215\242.md" | 205 ++++++ ...52\347\224\265\344\272\244\346\215\242.md" | 128 ++++ ...3 \351\227\252\347\224\265\350\264\267.md" | 82 +++ ...3 \346\265\201\345\212\250\346\200\247.md" | 172 +++++ ...19 Uniswap V4 \344\272\244\346\215\242.md" | 151 +++++ ...4 \351\227\252\347\224\265\350\264\267.md" | 116 ++++ ...4 \351\231\220\344\273\267\345\215\225.md" | 178 +++++ .../63-64 Gas \350\247\204\345\210\231.md" | 96 +++ "Unit6 EVM/01 \345\255\230\345\202\250.md" | 625 ++++++++++++++++++ "Unit6 EVM/02 \345\206\205\345\255\230.md" | 590 +++++++++++++++++ .../01 \346\216\210\346\235\203.md" | 63 ++ ...72\347\241\200\346\265\213\350\257\225.md" | 67 ++ .../03 \351\224\231\350\257\257.md" | 73 ++ .../04 \344\272\213\344\273\266.md" | 97 +++ .../05 \346\240\207\347\255\276.md" | 44 ++ ...41\346\213\237\350\260\203\347\224\250.md" | 61 ++ .../07 \345\217\221\351\200\201.md" | 41 ++ .../08 \347\255\276\345\220\215.md" | 51 ++ .../09 \346\227\266\351\227\264.md" | 52 ++ ...30\345\202\250\345\206\231\345\205\245.md" | 97 +++ 75 files changed, 8931 insertions(+) create mode 100644 "10 If Else\346\235\241\344\273\266\350\257\255\345\217\245.md" create mode 100644 "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" create mode 100644 "18 \347\236\254\346\200\201\345\255\230\345\202\250.md" create mode 100644 "24 \344\272\213\344\273\266\350\277\233\351\230\266.md" rename "Payable \345\207\275\346\225\260.md" => "31 Payable \345\207\275\346\225\260.md" (100%) create mode 100644 "32 \345\217\221\351\200\201\344\273\245\345\244\252\345\270\201.md" create mode 100644 "33 Fallback\345\207\275\346\225\260.md" create mode 100644 "34 Call\350\260\203\347\224\250.md" create mode 100644 "35 Delegatecall\345\247\224\346\211\230\350\260\203\347\224\250.md" create mode 100644 "36 \345\207\275\346\225\260\351\200\211\346\213\251\345\231\250.md" create mode 100644 "37 \350\260\203\347\224\250\345\205\266\344\273\226\345\220\210\347\272\246.md" create mode 100644 "38 \345\220\210\347\272\246\345\210\233\345\273\272\345\220\210\347\272\246.md" create mode 100644 39 Try Catch.md create mode 100644 "40 Import\345\257\274\345\205\245.md" create mode 100644 "41 Library\345\272\223.md" create mode 100644 "42 ABI Encode\347\274\226\347\240\201.md" create mode 100644 "43 ABI Decode\350\247\243\347\240\201.md" create mode 100644 "44 Keccak256\345\223\210\345\270\214.md" create mode 100644 "45 \351\252\214\350\257\201\347\255\276\345\220\215.md" create mode 100644 "46 Gas\344\274\230\345\214\226\346\212\200\345\267\247.md" create mode 100644 "47 \344\275\215\350\277\220\347\256\227\347\254\246.md" create mode 100644 "48 Unchecked\346\225\260\345\255\246\350\277\220\347\256\227.md" create mode 100644 "49 \346\261\207\347\274\226\345\217\230\351\207\217.md" create mode 100644 "50 \346\261\207\347\274\226\346\235\241\344\273\266\350\257\255\345\217\245.md" create mode 100644 "51 \346\261\207\347\274\226\345\276\252\347\216\257.md" create mode 100644 "52 \346\261\207\347\274\226\351\224\231\350\257\257\345\244\204\347\220\206.md" create mode 100644 "53 \346\261\207\347\274\226\346\225\260\345\255\246\350\277\220\347\256\227.md" create mode 100644 "Unit2 \345\272\224\347\224\250/ERC1155\345\244\232\344\273\243\345\270\201.md" create mode 100644 "Unit2 \345\272\224\347\224\250/ERC20\344\273\243\345\270\201.md" create mode 100644 "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" create mode 100644 "Unit2 \345\272\224\347\224\250/\344\273\245\345\244\252\345\270\201\351\222\261\345\214\205.md" create mode 100644 "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" create mode 100644 "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" create mode 100644 "Unit2 \345\272\224\347\224\250/\345\217\257\345\215\207\347\272\247\344\273\243\347\220\206.md" create mode 100644 "Unit2 \345\272\224\347\224\250/\345\217\257\350\277\255\344\273\243\346\230\240\345\260\204.md" create mode 100644 "Unit2 \345\272\224\347\224\250/\345\244\232\347\255\276\351\222\261\345\214\205.md" create mode 100644 "Unit2 \345\272\224\347\224\250/\346\227\240Gas\344\273\243\345\270\201\350\275\254\350\264\246.md" create mode 100644 "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" create mode 100644 "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" create mode 100644 "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" create mode 100644 "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" create mode 100644 "Unit2 \345\272\224\347\224\250/\351\273\230\345\205\213\345\260\224\346\240\221.md" create mode 100644 "Unit2 \345\272\224\347\224\250/\351\273\230\345\205\213\345\260\224\347\251\272\346\212\225.md" create mode 100644 "Unit4 DeFi/03 Chainlink \344\273\267\346\240\274\351\242\204\350\250\200\346\234\272.md" create mode 100644 "Unit4 DeFi/04 Chronicle \344\273\267\346\240\274\351\242\204\350\250\200\346\234\272.md" create mode 100644 "Unit4 DeFi/05 \346\201\222\345\256\232\344\271\230\347\247\257 AMM.md" create mode 100644 "Unit4 DeFi/06 \346\201\222\345\256\232\345\222\214 AMM.md" create mode 100644 "Unit4 DeFi/07 DAI \344\273\243\347\220\206.md" create mode 100644 "Unit4 DeFi/08 \347\246\273\346\225\243\350\264\250\346\212\274\345\245\226\345\212\261.md" create mode 100644 "Unit4 DeFi/09 \347\250\263\345\256\232\344\272\244\346\215\242 AMM.md" create mode 100644 "Unit4 DeFi/10 \350\264\250\346\212\274\345\245\226\345\212\261.md" create mode 100644 "Unit4 DeFi/11 \344\273\243\345\270\201\351\224\201.md" create mode 100644 "Unit4 DeFi/12 \351\207\221\345\272\223.md" create mode 100644 "Unit4 DeFi/13 Uniswap V2 \351\227\252\347\224\265\344\272\244\346\215\242.md" create mode 100644 "Unit4 DeFi/14 Uniswap V2 \346\234\200\344\274\230\345\215\225\350\276\271\344\276\233\347\273\231.md" create mode 100644 "Unit4 DeFi/15 Uniswap V3 \344\272\244\346\215\242.md" create mode 100644 "Unit4 DeFi/16 Uniswap V3 \351\227\252\347\224\265\344\272\244\346\215\242.md" create mode 100644 "Unit4 DeFi/17 Uniswap V3 \351\227\252\347\224\265\350\264\267.md" create mode 100644 "Unit4 DeFi/18 Uniswap V3 \346\265\201\345\212\250\346\200\247.md" create mode 100644 "Unit4 DeFi/19 Uniswap V4 \344\272\244\346\215\242.md" create mode 100644 "Unit4 DeFi/20 Uniswap V4 \351\227\252\347\224\265\350\264\267.md" create mode 100644 "Unit4 DeFi/21 Uniswap V4 \351\231\220\344\273\267\345\215\225.md" create mode 100644 "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" create mode 100644 "Unit6 EVM/01 \345\255\230\345\202\250.md" create mode 100644 "Unit6 EVM/02 \345\206\205\345\255\230.md" create mode 100644 "Unit7 Foundry/01 \346\216\210\346\235\203.md" create mode 100644 "Unit7 Foundry/02 \345\237\272\347\241\200\346\265\213\350\257\225.md" create mode 100644 "Unit7 Foundry/03 \351\224\231\350\257\257.md" create mode 100644 "Unit7 Foundry/04 \344\272\213\344\273\266.md" create mode 100644 "Unit7 Foundry/05 \346\240\207\347\255\276.md" create mode 100644 "Unit7 Foundry/06 \346\250\241\346\213\237\350\260\203\347\224\250.md" create mode 100644 "Unit7 Foundry/07 \345\217\221\351\200\201.md" create mode 100644 "Unit7 Foundry/08 \347\255\276\345\220\215.md" create mode 100644 "Unit7 Foundry/09 \346\227\266\351\227\264.md" create mode 100644 "Unit7 Foundry/10 \345\255\230\345\202\250\345\206\231\345\205\245.md" 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 存储布局的标准规则。