【漏洞分析】KaoyaSwap 安全事件分析
相关信息
KaoyaSwap 是 BSC 链上的一个自动做市商 AMM。然后,现在他们的官网 https://www.kaoyaswap.com/ 已经打不开了(如果我打开方式没错的话)。所以就直接进行攻击事件的分析吧。
攻击交易:https://bscscan.com/tx/0xc8db3b620656408a5004844703aa92d895eb3527da057153f0b09f0b58208d74
攻击者在进行攻击之前,自己构建了两个代币协助完成攻击,下面将他们分别称为 TA 和 TB 。
TA address:0x74eF69Defe8bae1Fe660fB93265FC1bc79c9bDa8
TB address:0xD84379C4eeA25d05574f9F0B99E3Bf73500Ca4B4
交易流程
因为攻击是发生在 AMM 上的,所以我们可以根据代币的流向先大概分析一下攻击者在这笔交易中都做了些什么,看看能不能看出有什么奇怪的地方。下面绿色框图是 Tokens Transferred 的内容,红色框图的是 Internal Txns 的内容。
- 首先闪电贷 1800 BNB
- 调用 swapExactTokensForTokens 函数,用 672 BNB 换出 125023 KY
- 调用 swapExactTokensForTokens 函数,用 100 BNB 换出 6666 BUSD
- 调用 addLiquidity 函数,添加 1026 BNB 和 50 TA,获得 226 KALP 流动性代币
- 调用 addLiquidity 函数,添加 1 BNB 和 1 TB,获得 0.9 KALP 流动性代币
- 调用 addLiquidity 函数,添加 1 TA 和 1 TB,获得 0.9 KALP 流动性代币
- 调用 swapExactTokensForETHSupportingFeeOnTransferTokens 函数,其中 path 参数为
[TA, WBNB, TB, TA, WBNB]
的地址。这个函数还涉及到 BNB 的转账,但是函数 Output 中没有体现。(剧透一下,转出了 1019 BNB 给攻击地址。具体情况后面的代码分析会解释)。
Input:
"amountIn": "8000000000000000000000",
"amountOutMin": "1",
"path": [
"0x74ef69defe8bae1fe660fb93265fc1bc79c9bda8",
"0xbb4cdb9cbd36b01bd1cbaebf2de08d9173bc095c",
"0xd84379c4eea25d05574f9f0b99e3bf73500ca4b4",
"0x74ef69defe8bae1fe660fb93265fc1bc79c9bda8",
"0xbb4cdb9cbd36b01bd1cbaebf2de08d9173bc095c"
],
"to": "0xa722ca7bf032de8f7a675da75dfec661bc89ace9",
"deadline": "1661293930“
Output:
0x
- 调用 removeLiquidityETHSupportingFeeOnTransferTokens 函数,销毁 226 KALP,获得 8050 TA,Output "amountETH": "6392515470500594443"
- 调用 removeLiquidityETHSupportingFeeOnTransferTokens 函数,销毁 0.9 KALP,获得 0.0009 TB,Output "amountETH": "1020797089459256392608"
- 调用 removeLiquidity 函数,传入 0.9 KALP,获取 0.5 TA 和 1.9 TB
- 调用 swapExactTokensForTokens 函数,传入 83918 KY,获得 25170 BUSD
- 在 pancakeswap 中把 17740 KY 换成 5457 BUSD,把 23364 KY 换成 24 WBNB。
- 归还 1800 WBNB 闪电贷。
- 然后把 37294 BUSD 和 271 WBNB 转移到 0xd87f 地址中
总的来看,攻击者主要投入了 1026 BNB 到 WBNB 和 TA 的池子中,然后通过 swapExactTokensForETHSupportingFeeOnTransferTokens 函数以 8000 TA 获得 1019 BNB,并且通过 removeLiquidityETHSupportingFeeOnTransferTokens 函数移除 WBNB 和 TB 池子的流动性获得 1020 BNB。
投入 1026 BNB,获得 (1019 + 1020) BNB,其中必有蹊跷。
代码分析
首先来分析一下 swapExactTokensForETHSupportingFeeOnTransferTokens
函数,先调用 _transferIn
转入 TA,然后调用 _swapSupportingFeeOnTransferTokens
函数按照 path
进行一系列 swap 操作,计算 _pools[TA,WBNB][WBNB]
的变化,根据差值给 to
地址发送相等数量的 BNB。
需要关注的函数:_transferIn
和 _swapSupportingFeeOnTransferTokens
。这些函都涉及到了一个关键的变量 _pools
,它是这次攻击的关键点。
先来看 _transferIn
函数,它的作用是把 path
中的第一个代币转入合约中,并修改对应的 _pools
值。
_swapSupportingFeeOnTransferTokens
函数根据 path
所提供的代币地址进行 swap,并将对应的 _pools
变量进行修改。
其中 _transferOut
函数的作用就是向 to
地址发送 amount
数量的 token。
当 swapExactTokensForETHSupportingFeeOnTransferTokens
函数中 path = [TA, WBNB, TB, TA, WBNB]
时所发生的情况是(下面简称 [A, W, B, A, W]
):
首先在 _transferrIn
函数中:
_pool[AW][A] + amountIn
然后在 _swapSupportingFeeOnTransferTokens
函数中(注意 _pool[AW][W]
做了两次减法操作):
i = 0 , _pool[AW][W] – amountOutput1, _pool[WB][W] + amountOutput1
i = 1 , _pool[WB][B] – amountOutput2, _pool[BA][B] + amountOutput2
i = 2 , _pool[BA][A] – amountOutput3, _pool[AW][A] + amountOutput3
i = 3 , _pool[AW][W] – amountOutput4
转账的金额只通过最后一对 pair
的 _pool
数值 _pool[AW][W]
的减少量(从 AW 这个池子里换出了多少 W)来确定。正确的方法应该是只关注最后一步中 _pool[AW][W]
的减少量,而不是整个 swap 过程中 _pool[AW][W]
的总(累计)减少量。如果采用总(累计)减少量计算,则当 path 中有重复的 pair ([A, W]
)不连续出现时,会多次计算 _pool[AW][W]
的减少量,最终导致 balanceBefore.sub(balanceAfter)
的值大于实际值。
我们看到第 1 次 TA 换 WBNB 的时候,换出来 1019.797089459257413406 WBNB,第 2 次 TA 换 WBNB 的时候,换出来 0.000395070241992122 WBNB,累加得到 1019.797484529499405528 WBNB,吻合之前提到的值。而正常情况下应该转出的 WBNB 因该为 0.000395070241992122 WBNB,同时将 [WBNB, TB] 池子中 TB 的币价大幅提高(转入了大量 WBNB)。
综上所属,利用了 _pools
变量在整个 swap 过程中累计减少量的漏洞,攻击者用一笔钱,在提高了 [WBNB, TB] 池子中 TB 的币价的同时, swap 出了超量的 WBNB。然后再移除 [WBNB, TB] 池子的流动性, 获取大量的 BNB。