Skip to content

Commit 2798a2f

Browse files
committed
Add study notes for 2025-08-18
1 parent 4385fb3 commit 2798a2f

1 file changed

Lines changed: 195 additions & 0 deletions

File tree

XXXJCSAMA.md

Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,201 @@ rust solana
1515
## Notes
1616

1717
<!-- Content_START -->
18+
# 2025-08-18
19+
20+
在 Currency.sol 的开始部分,我们就可以看到如下代码:
21+
22+
type Currency is address;
23+
24+
using {greaterThan as >, lessThan as <, greaterThanOrEqualTo as >=, equals as ==} for Currency global;
25+
using CurrencyLibrary for Currency global;
26+
27+
function equals(Currency currency, Currency other) pure returns (bool) {
28+
return Currency.unwrap(currency) == Currency.unwrap(other);
29+
}
30+
31+
function greaterThan(Currency currency, Currency other) pure returns (bool) {
32+
return Currency.unwrap(currency) > Currency.unwrap(other);
33+
}
34+
35+
function lessThan(Currency currency, Currency other) pure returns (bool) {
36+
return Currency.unwrap(currency) < Currency.unwrap(other);
37+
}
38+
39+
function greaterThanOrEqualTo(Currency currency, Currency other) pure returns (bool) {
40+
return Currency.unwrap(currency) >= Currency.unwrap(other);
41+
}
42+
此处我们首先使用了 type Currency is address; 为 address 类型创建别名。然后使用 using A for B 的语法为 Currency 类型增加了一些特性。需要注意的,此处使用了操作运算符的定义,所以在最后增加了 global 关键词。
43+
44+
之后的代码中出现了 Currency.unwrap 函数,该函数也是新版本 solidity 为用户自定义类型增加的新特性。在较新版本的 solidity 中,假如用户定义了 type C is V ,其中 C 是用户自定义类型,而 V 是 solidity 提供的原始类型,那么用户可以使用 C.wrap 将 V 类型转化为 C 类型,也可以使用 C.unwrap 将 V 类型转化为 C 类型。使用这种方法的好处是,solidity 会提供默认的类型检查,避免错误的类型转化发生。
45+
46+
Uniswap v4 此处的代码就显示了在新版本 solidity 下,开发者该如何定义自己的类型以及为类型重载运算符。如果读者希望进一步了解相关内容,可以参考 User-defined Value Types 部分的文档。
47+
48+
在 CurrencyLibrary 内部,Uniswap V4 定义了以下函数:
49+
50+
function toId(Currency currency) internal pure returns (uint256) {
51+
return uint160(Currency.unwrap(currency));
52+
}
53+
54+
// If the upper 12 bytes are non-zero, they will be zero-ed out
55+
// Therefore, fromId() and toId() are not inverses of each other
56+
function fromId(uint256 id) internal pure returns (Currency) {
57+
return Currency.wrap(address(uint160(id)));
58+
}
59+
这两段函数较为简单,但注释指出 fromId() 和 toId() 并不是完全互逆的。这是因为假如用户给出了一个特殊的 id,比如给定 0x1000000000000000000000001f9840a85d5af5bf1d1762f925bdaddc4201f984 的 ID,那么由于 uint160(id) 会截断大于 160 bit 的部分,所以最终返回的结果会是 0x1f9840a85d5af5bf1d1762f925bdaddc4201f984 代币地址。所以存在多个 ID 对应同一种代币的情况。故此,Uniswap v4 注释内指出 fromId 和 toId 并不是完全互逆关系。
60+
61+
ERC 7751 和 Custom Revert
62+
为了优化异常的抛出,Uniswap v4 引入了 ERC 7751 协议,并编写了 CustomRevert 库来处理 Revert。所谓的 ERC 7751 是一种带有上下文的错误抛出方法。一个简单的应用场景是当 Uniswap V4 出现报错后,在之前的情况下,我们不知道是 Uniswap v4 的主合约给出的报错还是因为 ERC20 代币实现给出的报错,我们只能通过 trace 的方式发现调用出错的位置。但 ERC 7751 允许我们给出报错的合约地址以及相关的上下文,这可以使得开发者不在 trace 的情况下就可以快速判断问题。
63+
64+
上述功能在 src/libraries/CustomRevert.sol 内的 bubbleUpAndRevertWith 函数内进行了实现:
65+
66+
/// @notice bubble up the revert message returned by a call and revert with a wrapped ERC-7751 error
67+
/// @dev this method can be vulnerable to revert data bombs
68+
function bubbleUpAndRevertWith(
69+
address revertingContract,
70+
bytes4 revertingFunctionSelector,
71+
bytes4 additionalContext
72+
) internal pure {
73+
bytes4 wrappedErrorSelector = WrappedError.selector;
74+
assembly ("memory-safe") {
75+
// Ensure the size of the revert data is a multiple of 32 bytes
76+
let encodedDataSize := mul(div(add(returndatasize(), 31), 32), 32)
77+
78+
let fmp := mload(0x40)
79+
80+
// Encode wrapped error selector, address, function selector, offset, additional context, size, revert reason
81+
mstore(fmp, wrappedErrorSelector)
82+
mstore(add(fmp, 0x04), and(revertingContract, 0xffffffffffffffffffffffffffffffffffffffff))
83+
mstore(
84+
add(fmp, 0x24),
85+
and(revertingFunctionSelector, 0xffffffff00000000000000000000000000000000000000000000000000000000)
86+
)
87+
// offset revert reason
88+
mstore(add(fmp, 0x44), 0x80)
89+
// offset additional context
90+
mstore(add(fmp, 0x64), add(0xa0, encodedDataSize))
91+
// size revert reason
92+
mstore(add(fmp, 0x84), returndatasize())
93+
// revert reason
94+
returndatacopy(add(fmp, 0xa4), 0, returndatasize())
95+
// size additional context
96+
mstore(add(fmp, add(0xa4, encodedDataSize)), 0x04)
97+
// additional context
98+
mstore(
99+
add(fmp, add(0xc4, encodedDataSize)),
100+
and(additionalContext, 0xffffffff00000000000000000000000000000000000000000000000000000000)
101+
)
102+
revert(fmp, add(0xe4, encodedDataSize))
103+
}
104+
}
105+
上述代码使用了 yul 汇编完成,虽然看上去很复杂,但实际上很简单。上述代码本质上是抛出了以下错误的 ABI 编码版本:
106+
107+
error WrappedError(address target, bytes4 selector, bytes reason, bytes details);
108+
在 bubbleUpAndRevertWith 函数内,details 只是一个 bytes4 additionalContext 类型。上述代码本质上是在完成了 WrappedError 的 ABI 编码工作。我们可以分部分来研究上述代码的功能:
109+
110+
第一部分是用来计算调用合约的返回值占据的字节数:
111+
112+
let encodedDataSize := mul(div(add(returndatasize(), 31), 32), 32)
113+
在 WrappedError 内,我们会给出调用其他合约返回的报错内容,这部分报错内容都存储在 call 之后的返回值内,我们可以通过 returndatasize 获得这部分错误返回值的大小。由于 solidity 内都以 32 bytes 作为基础单位,所以此处我们需要将 returndatasize 计算为 32 的倍数。具体逻辑实现上,我们首先增加将 returndatasize 增加 31 ,这是为了处理 returndatasize = 1 这种情况。即使 returndatasize = 1,使用上述代码仍可以计算出 32 的大小。其他的先除(div )后乘(mul) 是一种典型的将每一个数字重整为另一个数字倍数的方法。我们可以在 Uniswap v3 内的 tickSpacingToMaxLiquidityPerTick 函数内看到类似的操作。
114+
115+
第二部分使用 let fmp := mload(0x40) 语句获取当前 solidity 的空闲内存地址的起点。solidity 编译器使用了动态的内存布局方案,每次使用或者释放内存后,都会修改 0x40 内的数据,将其指向当前未使用的空闲内存的起点。在大部分内存安全的代码方案内,我们都会 mload(0x40) 读取空闲内存起点,避免后续占用到已被其他函数使用到的内存造成安全问题。
116+
117+
第三部分用来写入 WrappedError 错误选择器以及报错的合约地址。
118+
119+
mstore(fmp, wrappedErrorSelector)
120+
mstore(add(fmp, 0x04), and(revertingContract, 0xffffffffffffffffffffffffffffffffffffffff))
121+
与 solidity 定义的 log 方法一致,solidity 内抛出错误实际上也会计算错误的选择器,然后将其作为最初的 4 bytes。此处的 wrappedErrorSelector 就首先被写入了内存,然后跳过 wrappedErrorSelector 占据的最初 4 bytes(add(fmp, 0x04) 就是跳过 wrappedErrorSelector 占据的字节),我们接下来需要写入给出报错的地址。根据 solidity 的彬吗规范,给出报错的地址应该占据 256 bit。此处使用 and(revertingContract, 0xffffffffffffffffffffffffffffffffffffffff)) 清理了 revertingContract 变量可能存在的高位垃圾,然后将其写入内存。
122+
123+
此时的内存结构如下:
124+
125+
wrappedErrorSelector (4 bytes) + revertingContract(32 bytes)
126+
第四部分写入出现报错的 selector 和错误函数的返回值。比如 Uniswap V4 调用 ERC20 代币的 transfer 失败,那么此处就写入 transfer 的选择器和返回的错误信息:
127+
128+
mstore(
129+
add(fmp, 0x24),
130+
and(revertingFunctionSelector, 0xffffffff00000000000000000000000000000000000000000000000000000000)
131+
)
132+
上述代码完成了 selector 的写入,因为 selector 的类型是 bytes4,所以此处直接写入内存即可,但注意 bytes4 在 ABI 编码后会转化为 uint256 。我们需要计算写入时需要跳过的内存长度。我们需要跳过 wrappedErrorSelector (4 bytes) + revertingContract(32 bytes) 的内存结构,该结构的长度为 36(0x24),所以我们此处使用 add(fmp, 0x24) 确定了起始位置,然后写入了 revertingFunctionSelector,此处也需要注意使用与0xffffffff00000000000000000000000000000000000000000000000000000000 进行 and 操作去掉其他垃圾位。
133+
134+
完成上述操作后,内存结构如下:
135+
136+
wrappedErrorSelector (4 bytes)
137+
+ revertingContract(32 bytes)
138+
+ revertingFunctionSelector(32 bytes)
139+
第五部分内,我们写入动态类型的 offset。因为我们最后写入的 reason 和 detail 都是 bytes 类型,该类型要求写入动态类型的起始位置。在后文内,我们称之为 reason offset 和 detail offset 。代码如下:
140+
141+
// offset revert reason
142+
mstore(add(fmp, 0x44), 0x80)
143+
// offset additional context
144+
mstore(add(fmp, 0x64), add(0xa0, encodedDataSize))
145+
此处,我们首先确定 reason offset 的写入位置 ,我们目前的内存已占用了 4 + 32 + 32 = 68,所以使用 add(fmp, 0x44) 跳过之前已写入的数据。而关于 bytes 动态类型的起始位置计算则需要排除 wrappedErrorSelector (4 bytes),这一点额外重要,即计算动态类型的 offset 不需要包括选择器的占用部分。所以计算获得的 reason offset 应该为 revertingContract(32 bytes) + revertingFunctionSelector(32 bytes) + reason offset(32 bytes) + details offset(32 bytes)。我们使用 mstore(add(fmp, 0x44), 0x80) 写入 reason offset。
146+
147+
接下来,我们需要 detail offset,该变量的写入位置为 wrappedErrorSelector (4 bytes) + revertingContract(32 bytes) + revertingFunctionSelector(32 bytes) + reason offset(32 bytes) = 0x64,而写入的数值是 add(0xa0, encodedDataSize)。该数值中的 0xa0 包含以下部分 revertingContract(32 bytes) + revertingFunctionSelector(32 bytes) + reason offset(32 bytes) + reason length(32 bytes) + detail offset(32 bytes) = 160,除此之外还包含 encodedDataSize 部分。
148+
149+
在具体的编码过程中,我们往往最后确认 offset 的数值,所以动态类型的 offset 并没有非常难以确认
150+
151+
上述代码编写完成后,内存布局如下:
152+
153+
wrappedErrorSelector (4 bytes)
154+
+ revertingContract(32 bytes)
155+
+ revertingFunctionSelector(32 bytes)
156+
+ reason offset(32 bytes)
157+
+ detail offset(32 bytes)
158+
接下来,我们可以写入 reason 的真实内容。此处需要注意写入 bytes 类型的内容,需要在原有内容前增加长度信息。我们先完成 reason 的长度写入然后完成具体的内容写入:
159+
160+
// size revert reason
161+
mstore(add(fmp, 0x84), returndatasize())
162+
// revert reason
163+
returndatacopy(add(fmp, 0xa4), 0, returndatasize())
164+
此处使用 returndatacopy 将失败的调用的返回结果写入内存。读者可以自行阅读 文档 获得 returndatacopy 的参数。
165+
166+
完成上述操作后内存布局为:
167+
168+
wrappedErrorSelector (4 bytes)
169+
+ revertingContract(32 bytes)
170+
+ revertingFunctionSelector(32 bytes)
171+
+ reason offset(32 bytes)
172+
+ detail offset(32 bytes)
173+
+ reason length(32 bytes)
174+
+ return data(encodedDataSize)
175+
最后,我们写入 detail 的内容。Uniswap v4 约定 detail 是一个 bytes4 additionalContext 参数,所以长度确定为 4。相关代码如下:
176+
177+
// size additional context
178+
mstore(add(fmp, add(0xa4, encodedDataSize)), 0x04)
179+
// additional context
180+
mstore(
181+
add(fmp, add(0xc4, encodedDataSize)),
182+
and(additionalContext, 0xffffffff00000000000000000000000000000000000000000000000000000000)
183+
)
184+
此处也使用了 and 去掉垃圾位。完成上述所有步骤后,我们的内存布局如下:
185+
186+
wrappedErrorSelector (4 bytes)
187+
+ revertingContract(32 bytes)
188+
+ revertingFunctionSelector(32 bytes)
189+
+ reason offset(32 bytes)
190+
+ detail offset(32 bytes)
191+
+ reason length(32 bytes)
192+
+ return data(encodedDataSize)
193+
+ detail length(32 bytes)
194+
+ detail data(32 bytes)
195+
完成上述所有工作后,我们直接使用 revert(fmp, add(0xe4, encodedDataSize)) 抛出报错。代码内的 0xe4 实际上就是除了 return data 外,其他固定长度部分之和。
196+
197+
由于上述代码最后使用 revert 结束,所以此处我们没有进行内存的清理工作,但其他函数内,开发者如果使用内联汇编操作内存,应当考虑内存的清理工作。
198+
199+
在 CurrencyLibrary 库内,我们使用了上述函数报错 transfer 的错误:
200+
201+
if (!success) {
202+
CustomRevert.bubbleUpAndRevertWith(
203+
Currency.unwrap(currency), IERC20Minimal.transfer.selector, ERC20TransferFailed.selector
204+
);
205+
}
206+
考虑到文章的长度,此处不会详细介绍 CurrencyLibrary 内给出的 transfer 函数的构造方法,读者可以自行阅读 yul 源代码。该函数最后就使用了以下代码清理内存:
207+
208+
// Now clean the memory we used
209+
mstore(fmp, 0) // 4 byte `selector` and 28 bytes of `to` were stored here
210+
mstore(add(fmp, 0x20), 0) // 4 bytes of `to` and 28 bytes of `amount` were stored here
211+
mstore(add(fmp, 0x40), 0) // 4 bytes of `amount` were stored here
212+
18213
# 2025-08-16
19214

20215
借贷平台 (DeFi Lending Platform) 项目需求文档

0 commit comments

Comments
 (0)