$100如何撬动$650万?Sonne Finance攻击分析
Sonne Finance 攻击分析:$100 如何撬动 $650 万?本次攻击事件的本质是 market (soToken) 被创建出来时,攻击者进行了**笔抵押铸造的操作,以少量 underlying token 铸造了很少的 soToken,导致 soToken 的「 totalSupply 」数值太小。攻击者继而利用了 Solidity 合约精度损失这个漏洞,再搭配直接往 soToken 合约发送 underlying token(不会铸造 soToken,也就意味着「 totalSupply 」不变,「 totalCash 」变大),而不是抵押 铸造的方式存入 underlying token。这样的操作使得合约中「 totalCash 」 变量变大,但是「 totalSupply 」 保持不变,从而导致 exchangeRate 变大。**攻击者在赎回 underlying token 时,需要销毁的 soToken 少于抵押时铸造的 soToken,攻击者利用赚取的 soToken 去其他的 soToken(比如 soWETH、soUSDC)中借出 underlying token WETH、USDC,**获利高达 2000 万美元。
2024 年 5 月 15 日,Sonne Finance 在 Optimi** 链上遭受攻击,损失高达 2 千万美元。攻击发生后,X 上 @tonyke_bot 用户发推表示,其用约 100 美元保护了 Sonne Finance 的**抵押池(也称为 market,类似于 Compound 中的 cToken)中剩余的约 650 万美元。
(https://twitter.com/tonyke_bot/status/1790547461611860182)
Sonne Finance 项目方发现攻击之后,迅速暂停了 Optimi**上的所有 markets,并表示 Base 上的 markets 是安全的。
(https://twitter.com/SonneFinance/status/1790535383005966554)
攻击简述
Sonne Finance 是 Optimi** 上的一个 fork 了 Compound V2 的去**化借贷协议,供个人、机构和协议访问金融服务。Sonne Finance 协议将用户的 token 资产聚合起来,形成了借贷流动性池,为用户提供了一个类似银行的借贷业务。与 Compound 一样,协议参与者们可以将其持有的 token 抵押到 Sonne Finance 的借贷流动性池中,同时获得凭证 soToken(与 cToken 一样)。而 soToken 是一种生息资产凭证,随着区块的推进会产生**的收益,同时还会获得 SONNE token 激励。而参与者凭借着手里的 soToken 还能从 Sonne 借贷资产池中借出其他 token,例如参与者可以抵押**数量的 USDC 获得 soUSDC 凭证,随后借贷出WETH用于经一步的流通。Sonne Finance 协议中的抵押借贷可以是多对多的资产关系,在抵押借贷的过程中,协议会自动计算参与者地址的健康度(Health Factor),当健康度低于 1 时,该地址的抵押品将支持被清算,而清算者也能获得**的清算奖励。
用户存入的 underlying token 与铸造的 soToken 的数量关系,主要与一个叫做 exchangeRate 的变量有关,这个变量粗略可以用来表示每个 soToken 价值多少 underlying token。exchangeRate 的计算公式如下:
在上述公式中,totalCash 是指 soToken 持有的 underlying token 的数量,totalBorrows 是指某 market 中被借出去的 underlying token 的数量,totalReserves 是指总储备金数量(其中包含借款人支付的利息),totalSupply 是指铸造的 soToken 的数量。
在赎回时,用户可以指定想要赎回的 underlying token 的数量redeemAmount,来计算需要销毁掉的soToken的数量redeemTokens,计算方式大概为「 redeemTokens = redeemAmount / exchangeRat 」,注意这里并没有对精度损失做处理。
本次攻击事件的本质是 market (soToken) 被创建出来时,攻击者进行了**笔抵押铸造的操作,以少量 underlying token 铸造了很少的 soToken,导致 soToken 的「 totalSupply 」数值太小。攻击者继而利用了 Solidity 合约精度损失这个漏洞,再搭配直接往 soToken 合约发送 underlying token(不会铸造 soToken,也就意味着「 totalSupply 」不变,「 totalCash 」变大),而不是抵押 铸造的方式存入 underlying token。这样的操作使得合约中「 totalCash 」 变量变大,但是「 totalSupply 」 保持不变,从而导致 exchangeRate 变大。**攻击者在赎回 underlying token 时,需要销毁的 soToken 少于抵押时铸造的 soToken,攻击者利用赚取的 soToken 去其他的 soToken(比如 soWETH、soUSDC)中借出 underlying token WETH、USDC,**获利高达 2000 万美元。
攻击中涉及的关键地址
攻击准备交易:
https://optimistic.etherscan.io/tx/0x45c0ccfd3ca1b4a937feebcb0f5a166c409c9e403070808835d41da40732db96
攻击获利交易:
https://optimistic.etherscan.io/tx/0x9312ae377d7ebdf3c7c3a86f80514878deb5df51aad38b6191d55db53e42b7f0
攻击 EOA 相关地址:
0x5d0d99e9886581ff8fcb01f35804317f5ed80bbb
0xae4a7cde7c99fb98b0d5fa414aa40f0300531f43
攻击者(合约)相关地址:
0xa78aefd483ce3919c0ad55c8a2e5c97cbac1caf8
0x02fa2625825917e9b1f8346a465de1bbc150c5b9
underlying token(VELO Token V2):
0x9560e827af36c94d2ac33a39bce1fe78631088db
漏洞合约(soVELO,类似于 Compound 的 cToken):
0xe3b81318b1b6776f0877c3770afddff97b9f5fe5
X 上 @tonyke_bot 用户救援 交易:
https://optimistic.etherscan.io/tx/0x816f9e289d8b9dee9a94086c200c0470c6456603c967f82ab559a5931fd181c2
攻击流程分析
前情提要
Sonne Finance 项目方最近通过了一项将 VELO market 添加到 Sonne Finance 的提案(https://twitter.com/SonneFinance/status/1786871066075206044),并通过多签钱包安排了五笔在两天之后执行的交易(https://optimistic.etherscan.io/tx/0x18ebeb958b50579ce76528ed812025949dfcff8c2673eb0c8bc78b12ba6377b7),这五笔交易是用来创建 VELO market(soVELO 合约),并设置该 market 的一些关键配置,比如设置利率模型,设置价格预言机,设置抵押因子等。VELO market 创建之后,用户可以存入 VELO **,以铸造 soVELO **,soVELO **又可以用来借贷其他 soToken。
攻击准备
攻击准备阶段主要是攻击者在提案两天锁定时间结束后,根据 Sonne Finance 项目方提案中的信息,创建 VELO market(soVELO 合约),设置关键的配置,并通过抵押 VELO **进 soVELO 合约来铸造 soVELO **,同时也将自己持有的 VELO **以直接发送给 soVELO 合约的方式,来增大 exchangeRate,为后续攻击获利做准备。
具体步骤如下:
攻击者在两天锁定时间结束后,首先将提案中安排的前四笔交易的操作打包到一笔交易中(交易 0x45c0cc),用来创建 VELO market(soVELO 合约),并设置好关键的配置。VELO market 初始化时,exchangeRate 被设置为「 200,000,000,000,000,000,000,000,000 」。
攻击者调用 soVELO 合约的「 mint 」函数来存入 VELO **,并铸造 soVELO **,攻击者指定「 mintAmount 」为「 400,000,001 」(VELO **的数量)。从函数「 exchangeRateStoredInternal 」可以看出,由于此时 soVELO **的「 _totalSuppl 」是 0,因此 exchangeRate 即为第 1 步中设置的值。根据公式「 mintTokens = actualMintAmount / exchangeRate 」,此时计算出的应该铸造的 soVELO **的数量为 2。简而言之,这一步攻击者向 soVELO 合约中存入数值为「 400,000,001 」 的 VELO **,攻击者获得数值为 2 的 soVELO **。
soVELO.mint:
攻击者以直接给 soVELO 合约发送 VELO **的方式,给 soVELO 合约发送了数值为「 2,552,964,259,704,265,837,526 」的 VELO **,此时 soVELO 合约持有的 VELO **增多,但是由于没有新的 soVELO **的铸造,因此 totalSupply 保持不变,也就意味着此时根据 exchangeRate 计算公式计算出的 exchangeRate 会变大。
攻击者将持有的 soVELO **转移多次,**转移给了另一个攻击 EOA 0xae4a。
攻击获利
攻击获利阶段主要是攻击者执行提案的第五笔交易,并通过闪电贷借出 VELO **直接发送给 soVELO 合约,以进一步增大 exchangeRate。然后攻击者利用自己手里的数值为 2 的 soVELO **,去其他的 soToken(比如 soWETH,soUSDC 等)合约中借出了 WETH、USDC 等 underlying token,这些部分成为了攻击者获利。紧接着攻击者去 soVELO 合约中赎回自己的 underlying token,由于 exchangeRate 变大,以及计算赎回需要销毁的 soVELO **时的精度损失问题,**使得攻击者仅仅使用数值为 1 的 soVELO **就赎回了此前存入的几乎**的 VELO **,可以理解为攻击者利用多得的数值为 1 的soVELO **,通过从其他 soToken 借贷赚取了 WETH、USDC 等 underlying token。攻击者使用同样的手法多次重复攻击,**获利巨大。
具体步骤如下:
攻击者执行题案中的第五笔交易,设置提案中规定的借贷因子。
攻击者从 VolatileV2 AMM - USDC/VELO 池子中闪电贷出数值为「 35,469,150,965,253,049,864,450,449 」的 VELO **,这会触发攻击者的 hook 函数。在 hook 函数中,攻击者继续执行攻击操作。
攻击者将自己持有的 VELO **发送给 soVELO 合约,以进一步增大 exchangeRate。目前 soVELO 合约中一共有数值为「 35,471,703,929,512,754,530,287,976 」的 VELO **(攻击者三次转入的 VELO **和)。
攻击者创建新的合约0xa16388a6210545b27f669d5189648c1722300b8b,在构造函数中,将持有的 2 个 soVELO **转给新创建的合约 0xa163(以下称为攻击者 0xa163)。
攻击者 0xa163 以持有的 soVELO **,从 soWETH 中借出数值为「 265,842,857,910,985,546,929 」的 WETH。
攻击者 0xa163 调用 soVELO 的「 redeemUnderlying 」函数,指定赎回 VELO **的数值为「 35,471,603,929,512,754,530,287,976 」(几乎是所有攻击者此前转入或者抵押进 soVELO 合约的 VELO **数量),此时需要根据公式「 redeemTokens = redeemAmountIn / exchangeRate 」来计算赎回所需要销毁的 soVELO **的数量。
从「 exchangeRateStoredInternal 」函数可以看出,由于此时 _totalSupply 是 2 不是 0,因此需要计算 exchangeRate 的值,通过公式「 exchangeRate = (totalCash totalBorrows - totalReserves) / totalSupply 」计算出,目前的 exchangeRate 为「 17,735,851,964,756,377,265,143,988,000,000,000,000,000,000 」,这个值远远大于设置的初始 exchangeRate 「 200,000,000,000,000,000,000,000,00 」。
根据新的 exchangeRate 计算出的「 redeemTokens 」的值为「 1.99 」,由于 Solidity 向下取整的特性,「 redeemTokens 」的值**为 1。也就意味着攻击者 0xa163 使用数值为 1 的 soVELO **,赎回了此前存入的几乎所有的 VELO **。同时攻击者 0xa163 也赚取了从 soWETH 中借出的数值为「 265,842,857,910,985,546,929 」的 WETH。
soVELO.redeemUnderlying:
soVELO.exchangeRateStoredInternal:
攻击者 0xa163 将借到的 WETH 和赎回的 VELO ****转给了上层攻击者,然后自毁。
攻击者调用 soWETH 的「 liquidateBorrow 」函数,用来清算前面新创建的合约 0xa163 借贷的部分资产,目的是拿回锁定住的数值为 1 的 soVELO **。目前攻击者只持有数值为 1 的 soVELO **。
攻击者调用 soVELO 的「 mint 」函数,再一次抵押铸造 soVELO **,目的是凑够数值为 2 的 soVELO **,然后再次执行上述第 3-8 步,获利其他的 undeylying token。
攻击者执行数次第 9 步的操作,还掉闪电贷,获利离场。
$100 如何撬动 $650 万
攻击发生后,X 上 @tonyke_bot 用户在交易 0x0a284cd 中,通过抵押 1144 个 VELO **到 soVELO 合约中,铸造了 0.00000011 个 soVELO。这样操作之所以能够阻止攻击者进一步攻击,是因为这笔交易改变了 soVELO 中 totalSupply 的大小和持有的 VELO **的数量 totalCash,而 totalSupply 增长对于计算 exchangeRate 产生的影响大于 totalCash 增长产生的影响,因此 exchangeRate 变小,从而导致攻击者进行攻击时,无法再利用精度损失赚取 soVELO,导致攻击无法再进行。
资金追踪
攻击者攫取非法收益后不久便将资金进行了转移,大部分资金转移到了以下 4 个地址当中,有的是为了换个地址继续攻击,有的是为了**:
0x4ab93fc50b82d4dc457db85888dfdae28d29b98d
攻击者将 198 WETH 转入了该地址,然后该地址采用了相同的攻击手法,在下列交易中获得非法收益:
攻击结束后,该地址将上述非法所得转给了0x5d0d99e9886581ff8fcb01f35804317f5ed80bbb。
0x5d0d99e9886581ff8fcb01f35804317f5ed80bbb
攻击者将 724277 USDC、2353 VELO 转入了该地址,并将 USDC 兑换成了Ether。随后立即将部分资金转入了 Stargate 跨链桥,剩下大部分非法资金残留在该地址中:
0xbd18100a168321701955e348f03d0df4f517c13b
攻击者将 33 WETH 转入了该地址,并采用 peel chain 的方式尝试**,**链路如下:
0xbd18100a168321701955e348f03d0df4f517c13b->0x7e97b74252b6df53caf386fb4c54d4fb59cb6928->0xc521bde5e53f537ff208970152b75a003093c2b4->0x9f09ec563222fe52712dc413d0b7b66cb5c7c795。
0x4fac0651bcc837bf889f6a7d79c1908419fe1770
攻击者将 563 WETH 转入了该地址,随后转给了0x1915F77A116dcE7E9b8F4C4E43CDF81e2aCf9C68,目前没有进一步行为。
攻击者本次**的手段相对来说较为专业,手法呈现多样性趋势。因此对于我们 Web3 参与者来说,在安全方面要持续不断地提高我们的反**能力,通过 KYT、AML 等相关区块链交易安全产品来提高 Defi 项目的安全性。
安全建议
精度损失需重视。精度损失导致的安全问题层出不穷,尤其是在Defi项目中,精度损失往往导致严重的资金损失。建议项目方和安全审计人员仔细审查项目中存在精度损失的代码,并做好测试,尽量规避该漏洞。
建议类似于 Compound 中 cToken 这种 market 的创建和**抵押铸造操作由特权用户来执行,避免被攻击者操作,从而操作汇率。
当合约中存在关键变量依赖于「 this.balance 」或者「 token.balanceOf() 」的值时,需要慎重考虑该关键变量改变的条件,比如是否允许直接通过给合约转原生币或者**的方式改变该变量的值,还是只能通过调用某特定函数才能改变该变量的值。