强扭的瓜不甜 —— SafeDollar 被黑分析

By:Kong@慢雾安全团队


据慢雾区消息,2021 年 06 月 28 日,Polygon 上算法稳定币项目 SafeDollar 遭受闪电贷攻击,慢雾安全团队第一时间介入分析,并将结果分享如下:

攻击细节分析

Phase 1

首先攻击者提前数小时创建了一个合约,并通过 Polydex 将 MATIC 兑换成 PLX 代币。

随后攻击者通过此合约将获得的 PLX 代币存入 SafeDollar 项目的 SdoRewardPool 合约中,SdoRewardPool 合约架构参考自 SUSHI 的 MasterChef 合约,用户在 SdoRewardPool 合约中抵押指定的代币即可获得 SDO 代币奖励。

而攻击者提前数小时在 SdoRewardPool 合约中抵押 PLX 代币就是为了后续获取抵押奖励做准备。

Phase 2

攻击者在 Phase 1 中抵押数小时后部署了另一个合约,这也是主要的攻击合约,在此攻击合约中,攻击者利用通过 Polydex 的多个池子闪电贷借出大量的 PLX 代币。

随后在同一笔交易中攻击合约将获得的 PLX 代币不断地在 SdoRewardPool 合约中进行“抵押 - 提现”操作。

而我们知道 SdoRewardPool 合约架构参考自 SUSHI 的 MasterChef 合约,因此攻击者在同一笔交易内不断的进行抵押提现是无法获得 SDO 代币奖励的。那么攻击者最终是怎么获利的呢?

通过链上分析我们可以发现攻击者是通过 Phase 1 中部署的第一个合约获得大量 SDO 代币奖励的。

那么为何攻击者只是在 Phase 1 中进行简单的抵押操作就可以获得大量的 SDO 代币奖励呢?这与攻击者在第二个攻击者合约中不断地进行“抵押 - 提现”操作有何联系呢?带着这些疑问我们一步步的进行分析:

首先查看攻击者获得大量代币奖励的交易,在这笔交易中攻击者只是简单的对 SdoRewardPool 合约进行 updatePool 操作,并通过 withdraw 进行提现。而正是在提现时 SdoRewardPool 合约铸造了大量的 SDO 代币给攻击者。

既然是铸造出非预期数量的代币给到攻击者,那么必然是奖励计算出现了问题。在 SdoRewardPool 合约中负责 SDO 铸币奖励的是 _harvestReward 函数,我们跟进此函数:

通过上图我们可以发现铸币数量取决于所计算的 _claimableAmount 参数,而影响此参数的就只有用户的抵押数量 user.amount、池子的奖励数量 pool.accSdoPerShare、精度 1e18 以及用户负债 user.rewardDebt。我们直接对链上数据进行分析可以发现用户抵押数量与池子的奖励数量相乘时,pool.accSdoPerShare 是一个十分巨大的数值,这导致了最后计算结果变得非常巨大。

而池子的奖励数量 pool.accSdoPerShare 是在 updatePool 函数中进行更新的,我们继续跟进 updatePool 函数:

可以很容易的看出 pool.accSdoPerShare 主要是由缓存的 accSdoPerShare、_sdoReward 以及 lpSupply 决定的。我们直接分析此段逻辑的链上数据可以发现此时的 lpSupply 只有 2!这就导致了在计算 pool.accSdoPerShare 时除了一个非常小的 lpSupply 造成结果变得十分巨大。


而 lpSupply 取的是抵押的 PLX 代币在 SdoRewardPool 合约中的余额,但既然在 SdoRewardPool 合约中有大量的 PLX 代币抵押,为何最终在获取余额的时候只有 2 呢?我们可以很容易的猜测是 PLX 代币在获取数量时出现的问题,我们跟进 PLX 代币进行分析:

我们可以发现余额获取的是 _balances 变量,而此变量是在用户进行转账时进行改变,我们跟进其转账函数可以发现在用户进行转账时会调用 _move 函数,我们跟进此函数可以发现这是一个通缩型逻辑,也就是说 A 用户在转账给 B 用户时,B 用户接收到的数量会小于 A 用户发送的数量!


而此时本次攻击核心就呼之欲出了!因为我们可以知道 SdoRewardPool 合约架构参考自 SUSHI 的 MasterChef 合约,而此架构是用户不管抵押了多少代币,在其提现时都可以取出相同数量的代币。我们跟进 SdoRewardPool 合约的 deposit 与 withdraw 函数进行验证:

其抵押与提现的逻辑不出我们所料,用户存入多少 PLX 代币,在其提现时 SdoRewardPool 合约就会发送给用户多少代币,即用户存入 100 个 PLX,提现时也能提走 100 个 PLX 代币。

而通过上面的分析我们可以知道 PLX 代币是通缩型代币,在用户抵押 100 个代币时,SdoRewardPool 合约收到的数量小于 100 个,但用户提现时 SdoRewardPool 合约却又给了用户 100 个代币。相当于用户少充多提了,所以攻击者就是利用此通缩型代币与 SdoRewardPool 合约的兼容性问题进行反复的“抵押 - 提现”操作,最终把 SdoRewardPool 合约中的 PLX 代币数量消耗至 2,导致 SdoRewardPool 合约在铸造奖励时铸造出了大量的 SDO 代币。

攻击流程

1. 攻击者先通过“攻击合约 1”在 SdoRewardPool 合约中抵押 PLX 代币,以便后续获取 SDO 代币奖励。

2. 攻击利用 PLX 通缩型代币与 SdoRewardPool 合约的兼容性问题,通过“攻击合约 2”频繁地在 SdoRewardPool 合约中反复进行“抵押 - 提现”操作,最终导致 SdoRewardPool 合约中的 PLX 代币数量消耗到一个极小的数量 2。

3. 随后攻击通过“攻击合约 1”进行在 SdoRewardPool 合约中进行提现操作以获取 SDO 代币奖励,奖励的计算会除 SdoRewardPool 合约中 PLX 代币数量,而此时 SdoRewardPool 合约中 PLX 代币数量是一个极小的数量 2,所以导致除法计算后奖励的数量变成一个巨大的值。

4. 攻击者在获得大量 SDO 奖励后,直接在市场上进行抛售,获利离场。

总结

此次攻击的核心问题在于“通缩型代币”与架构参考自 SUSHI 的 MasterChef 合约不兼容导致的。此兼容性问题造成 SafeDollar 项目的 SdoRewardPool 合约中 PLX 代币被恶意耗尽,而 SDO 代币奖励计算又依赖于 SdoRewardPool 合约中 PLX 代币数量,最终导致 SDO 代币价格闪崩的惨剧发生。由于当前 DeFi 项目需要多个合约间进行交互,因此慢雾安全团队建议在进行设计时应充分考虑不同合约间交互的兼容性问题。

参考交易:

[1] https://polygonscan.com/tx/0x55dad44a7ed31d1637e70879af66e02290d39aea54554f8411e6ec19c03a074b

[2] https://polygonscan.com/tx/0x4dda5f3338457dfb6648e8b959e70ca1513e434299eebfebeb9dc862db3722f3

[3] https://polygonscan.com/tx/0xd78ff27f33576ff7ece3a58943f3e74caaa9321bcc3238e4cf014eca2e89ce3f

[4] https://polygonscan.com/tx/0x1360315a16aec1c7403d369bd139f0fd55a99578d117cb5637b234a0a0ee5c14

写评论,请先登录