【漏洞分析】Reflection Token 反射型代币攻击事件通用分析思路
在本篇文章中,我将通过一个攻击事件引出 Reflection Token 攻击事件的一个通用分析思路。
关于 Reflection Token 的其他案例分析,可以参考BEVO代币攻击事件分析及复现一文。
TomInu 攻击事件
TomInu Token 是一个反射型代币 reflection token,于2023-01-26遭到黑客攻击,攻击者获利35577美元。
TomInu(被攻击合约): 0x2d0e64b6bf13660a4c0de42a0b88144a7c10991f
攻击交易: https://phalcon.blocksec.com/tx/eth/0x6200bf5c43c214caa1177c3676293442059b4f39eb5dbae6cfd4e6ad16305668
攻击过程较为简单,攻击者通过几个常规操作就完成获利。
- 攻击者借出闪电贷
- 用 WETH swap 出 1465904852700232013011 TINU
- deliver 1465904852700232013011 TINU
- skim 得到 1733770910894426471783 TINU(在这一步已经完成了获利)
- 把 TINU swap 为 WETH
- 归还闪电贷
本次攻击事件通过推文告警并且进行了分析,但是很可惜分析的结论略显含糊的。红框标注的部分并不是攻击者获利的真正原因。(为什么不是真的)
推文:https://twitter.com/QuintenDes/status/1618730379447508998
并且,目前在在网络上搜索到的所有关于 reflection token 攻击事件的成因分析中提到:“由于攻击者 deliver 了一笔 token ,导致了 pair 中的 token 升值,从而能够 skim 出更多的 token 进行获利”。这类分析大多是理所当然地下结论,没有通过实际的计算推导,妄下结论误导读者。(为什么这么说)
攻击过程很简单,先 deliver 然后 skim,就能够获利了。根据这个攻击过程的特征,我们直接找 rToken增发代码,定位漏洞点。(为什么可以这么做)
以上的几个为什么都将会在后面“为什么”这一章节进行解释,读者可以先带着疑问进行阅读。
漏洞分析
在 _transferStandard
函数中可以看出,TomInu 代币在进行转账 rAmount
时需要收取 team
和 fee
两种手续费,并将扣除了手续费后的 rTransferAmount
转给收款人。
其中 team 手续费 rteam
留存在本合约中,fee 手续费 rfee
则是直接销毁。
此时他们的数量关系应该为:rTransferAmount = rAmount - rteam - rfee
问题出现在 _getRValues
函数中,该函数在计算 rTransferAmount
的过程中忽略了 rteam
参数,计算 rTransferAmount = rAmount - rfee
得到的结果比实际结果要大,造成了 rToken 的增发。也就是说,因为这个计算问题,市场上实际流通的 rAmount
总和是要大于 rTotal
的值的。
此时,随着用户不断交易,整个市场上流转的总额就不断增发。在代币进行流转的过程中以下两个操作使得增发的代币累积到 pair 合约中:
-
用户自行在 pair 合约中进行 TomInu → WETH 的 swap 操作。
-
TomInu 代币合约在执行
_transfer()
函数的时候,会将本合约的代币 swap 成 WETH。
可以通过下面的这个例子来说明增发部分确实随着 rTransferAmount
发送给 to 地址。首先输入 tAmount 数量为 1470,然后扣除手续费后的 tAmount 为 1352。
其次,event 事件中记录的数额为 tTransferAmount
,这个值是减去了两个手续费的,是计算正确的值。所以 event 事件中记录的值 tAmount 1352 应该为 to 地址实际的收款金额。
但是由于增发部分(数值为 rteam)的 rAmount 随着 rTransferAmount
发送给 to 地址,在 to 地址收到增发的 rTransferAmount
后,再计算得出的 tAmount 为 1465,要大于 event 事件中记录的值 tAmount 1352。
为什么
在这个章节中,会对前面的暴言暴论进行解释
- 为什么推文中的关于漏洞成因的结论是含糊的不准确的。
- 什么说关于 reflection token 攻击事件的成因分析中提到“由于攻击者 deliver 了一笔 token ,导致了 pair 中的 token 升值,从而能够 skim 出更多的 token 进行获利”的分析结论都是错误的。
- 为什么看到攻击过程先 deliver 然后 skim 就完成获利之后,我得出的结论是直接找 rToken增发代码定位漏洞点。
首先我将举几个例子来模拟整个 deliver-skim 的过程,为了使得这个例子尽可能的简单,这个过程中将不考虑任何手续费的收取。
场景1:
只有 attacker 和 pair 持有所有的 token
rTotal 1000, tTotal 100, rate 10
pair: rAmount 500, tAmount 50
attacker: rAmount 500, tAmount 50
attacker deliver 500 rAmount
rTotal 500, tTotal 100, rate 5
pair: rAmount 500, tAmount 100
attacker: rAmount 0, tAmount 0
此时,pair 的 tAmount 从 50 变成了 100。接下来 attacker 将调用 skim 来获利了是不是?
attacker calls pair.skim()
rTotal 500, tTotal 100, rate 5
pair: rAmount 250, tAmount 50
attacker: rAmount 250, tAmount 50
attacker 如愿以偿获利了吗?没有,attacker 和 pair 又回到了最初的 50 tAmount,并不能通过这个操作来进行获利。
场景2:
attacker, pair 以及一些其他用户共同持有所有的 token
rTotal 1000, tTotal 100, rate 10
pair: rAmount 250, tAmount 25
attacker: rAmount 500, tAmount 50
others: rAmount 250, tAmount 25
attacker deliver 500 rAmount
rTotal 500, tTotal 100, rate 5
pair: rAmount 250, tAmount 50
attacker: rAmount 0, tAmount 0
others: rAmount 250, tAmount 50
attacker calls pair.skim()
rTotal 500, tTotal 100, rate 5
pair: rAmount 125, tAmount 25
attacker: rAmount 125, tAmount 25
others: rAmount 250, tAmount 50
pair 回到了原始的 25 tAmount,而 attacker 由原来的 50 亏损到了 25 tAmount。坚定持有的 others 由 25 上涨到了 50 tAmount。
通过上面的两个例子,我们可以得出结论,只有当 attacker 和 pair 所持有的代币份额合计 100% 的情况下,deliver-skim 的操作 attacker 才不会亏损。而两者份额不足 100% 的情况下,deliver-skim 的操作反而会导致 attacker 遭受损失。也就是说 attacker 通过 deliver-skim 的操作无论怎么样都是不赚的,最好的情况是 attacker 和 pair 所持有的代币份额合计 100% 的情况下才不至于亏损。
那么…有没有更好的情况呢?好到…两者持有的代币份额合计起来…超过100%?
比如,发生了代币增发?
场景3:
由于代码存在 rToken 相关的计算错误,导致代币增发的发生,具体表现为 rToken 的实际流通量大于 rTotal 的数量。
rTotal 1000, tTotal 100, rate 10
pair: rAmount 400, tAmount 40
attacker: rAmount 800, tAmount 80
others: rAmount 400, tAmount 40
sum_rAmount = 1600 > rTotal = 1000
pair.rAmount + attacker.rAmount = 1200 > rTotal = 1000
attacker deliver 800 rAmount
rTotal 200, tTotal 100, rate 2
pair: rAmount 400, tAmount 200
attacker: rAmount 0, tAmount 0
others: rAmount 400, tAmount 200
attacker calls pair.skim()
rTotal 200, tTotal 100, rate 2
pair: rAmount 80, tAmount 40
attacker: rAmount 320, tAmount 160
others: rAmount 400, tAmount 200
至此,attacker 从原来的 80 tAmount,通过 deliver-skim 操作获利达到 160 tAmount。
通过这个场景,也就可以解释为什么看到攻击过程中通过 deliver-skim 操作获利时,首先想到的就是去找代码中使得 rAmount 增发的计算操作。因为只有 rAmount 发生了增发,pair 和 attacker 的份额大于 100%,且增发部分需要留存在 pair 合约中,才能够满足通过 deliver-skim 操作进行获利的基础条件。
攻击要求 pair 和 attacker 的份额大于 100%
在部署代币合约的时候,_tTotal
设为 1733820000000000000000
在攻击者 swap 获取大量代币时(进行 deliver 前),pair 中的 tAmount 为 316871513264115731249,attacker 中的 tAmount 为 1465904852700232013011
计算得出,此时 pair + attacker 的份额已经大于 100%,满足攻击要求。
>>> 316871513264115731249 + 1465904852700232013011
1782776365964347744260
>>> 1782776365964347744260 - 1733820000000000000000
48956365964347744260
>>> 1782776365964347744260 / 1733820000000000000000
1.0282361294507778
后记
在 TomInu 攻击事件发生的4个月后,存在相同漏洞的 ADU token 再次被攻击。
ADU token attack tx:https://explorer.phalcon.xyz/tx/bsc/0xc6f6b70e9e35770b699da9b60244d461d02db66859df42319c3207d76931423c
为什么会写这篇文章,因为当我想对这些攻击事件进行学习与分析的时候,我查看了网络上的分析文章,他们给出的漏洞成因含糊不清毫无根据。我读了很多篇分析文章,说辞都是大同小异地糊弄。还没分析清楚就胡乱指点,最终被忽悠的就是真心想研究清楚的人。走了不少弯路,把弯路总结成这篇文章,感谢你的阅读。
感谢某位不愿意透露ID的大佬对本文的指正,本文在大佬的指点下进行了二次修改,respect。