欧易数字货币交易所 更快、更好、更强

Web3安全警示丨损失高达2000万美元,Sonne Finance攻击事件分析

2024年5月15日,SonneFinance在Optimism链上遭受攻击,损失高达2千万美元。

攻击发生后,X上@tonyke_bot用户发推表示,其用约100美元保护了SonneFinance的代币抵押池(也称为market,类似于Compound中的cToken)中剩余的约650万美元。

(https://twitter.com/tonyke_bot/status/1790547461611860182)

SonneFinance项目方发现攻击之后,迅速暂停了Optimism上的所有markets,并表示Base上的markets是安全的。

(https://twitter.com/SonneFinance/status/1790535383005966554)

攻击简述

SonneFinance是Optimism上的一个fork了CompoundV2的去中心化借贷协议,供个人、机构和协议访问金融服务。SonneFinance协议将用户的token资产聚合起来,形成了借贷流动性池,为用户提供了一个类似银行的借贷业务。与Compound一样,协议参与者们可以将其持有的token抵押到SonneFinance的借贷流动性池中,同时获得凭证soToken(与cToken一样)。而soToken是一种生息资产凭证,随着区块的推进会产生一定的收益,同时还会获得SONNEtoken激励。而参与者凭借着手里的soToken还能从Sonne借贷资产池中借出其他token,例如参与者可以抵押一定数量的USDC获得soUSDC凭证,随后借贷出WETH用于经一步的流通。SonneFinance协议中的抵押借贷可以是多对多的资产关系,在抵押借贷的过程中,协议会自动计算参与者地址的健康度(HealthFactor),当健康度低于1时,该地址的抵押品将支持被清算,而清算者也能获得一定的清算奖励。

用户存入的underlyingtoken与铸造的soToken的数量关系,主要与一个叫做exchangeRate的变量有关,这个变量粗略可以用来表示每个soToken价值多少underlyingtoken。exchangeRate的计算公式如下:

在上述公式中,totalCash是指soToken持有的underlyingtoken的数量,totalBorrows是指某market中被借出去的underlyingtoken的数量,totalReserves是指总储备金数量(其中包含借款人支付的利息),totalSupply是指铸造的soToken的数量。

在赎回时,用户可以指定想要赎回的underlyingtoken的数量redeemAmount,来计算需要销毁掉的soToken的数量redeemTokens,计算方式大概为“redeemTokens=redeemAmount/exchangeRat”,注意这里并没有对精度损失做处理。

本次攻击事件的本质是market(soToken)被创建出来时,攻击者进行了第一笔抵押铸造的操作,以少量underlyingtoken铸造了很少的soToken,导致soToken的“totalSupply”数值太小。攻击者继而利用了Solidity合约精度损失这个漏洞,再搭配直接往soToken合约发送underlyingtoken(不会铸造soToken,也就意味着“totalSupply”不变,“totalCash”变大),而不是抵押+铸造的方式存入underlyingtoken。这样的操作使得合约中“totalCash”变量变大,但是“totalSupply”保持不变,从而导致exchangeRate变大。最终攻击者在赎回underlyingtoken时,需要销毁的soToken少于抵押时铸造的soToken,攻击者利用赚取的soToken去其他的soToken(比如soWETH、soUSDC)中借出underlyingtokenWETH、USDC,最终获利高达2000万美元。

攻击中涉及的关键地址

攻击准备交易:

https://optimistic.etherscan.io/tx/0x45c0ccfd3ca1b4a937feebcb0f5a166c409c9e403070808835d41da40732db96

攻击获利交易:

https://optimistic.etherscan.io/tx/0x9312ae377d7ebdf3c7c3a86f80514878deb5df51aad38b6191d55db53e42b7f0

攻击EOA相关地址:

0x5d0d99e9886581ff8fcb01f35804317f5ed80bbb0xae4a7cde7c99fb98b0d5fa414aa40f0300531f43

攻击者(合约)相关地址:

0xa78aefd483ce3919c0ad55c8a2e5c97cbac1caf80x02fa2625825917e9b1f8346a465de1bbc150c5b9

underlyingtoken(VELOTokenV2):

0x9560e827af36c94d2ac33a39bce1fe78631088db

漏洞合约(soVELO,类似于Compound的cToken):

0xe3b81318b1b6776f0877c3770afddff97b9f5fe5

X上@tonyke_bot用户救援交易:

https://optimistic.etherscan.io/tx/0x816f9e289d8b9dee9a94086c200c0470c6456603c967f82ab559a5931fd181c2攻击流程分析前情提要

SonneFinance项目方最近通过了一项将VELOmarket添加到SonneFinance的提案(https://twitter.com/SonneFinance/status/1786871066075206044),并通过多签钱包安排了五笔在两天之后执行的交易(https://optimistic.etherscan.io/tx/0x18ebeb958b50579ce76528ed812025949dfcff8c2673eb0c8bc78b12ba6377b7),这五笔交易是用来创建VELOmarket(soVELO合约),并设置该market的一些关键配置,比如设置利率模型,设置价格预言机,设置抵押因子等。VELOmarket创建之后,用户可以存入VELO代币,以铸造soVELO代币,soVELO代币又可以用来借贷其他soToken。

攻击准备

攻击准备阶段主要是攻击者在提案两天锁定时间结束后,根据SonneFinance项目方提案中的信息,创建VELOmarket(soVELO合约),设置关键的配置,并通过抵押VELO代币进soVELO合约来铸造soVELO代币,同时也将自己持有的VELO代币以直接发送给soVELO合约的方式,来增大exchangeRate,为后续攻击获利做准备。

具体步骤如下:

攻击者在两天锁定时间结束后,首先将提案中安排的前四笔交易的操作打包到一笔交易中(交易0x45c0cc),用来创建VELOmarket(soVELO合约),并设置好关键的配置。VELOmarket初始化时,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代币转移多次,最终转移给了另一个攻击EOA0xae4a。

攻击获利

攻击获利阶段主要是攻击者执行提案的第五笔交易,并通过闪电贷借出VELO代币直接发送给soVELO合约,以进一步增大exchangeRate。然后攻击者利用自己手里的数值为2的soVELO代币,去其他的soToken(比如soWETH,soUSDC等)合约中借出了WETH、USDC等underlyingtoken,这些部分成为了攻击者获利。紧接着攻击者去soVELO合约中赎回自己的underlyingtoken,由于exchangeRate变大,以及计算赎回需要销毁的soVELO代币时的精度损失问题,最终使得攻击者仅仅使用数值为1的soVELO代币就赎回了此前存入的几乎全部的VELO代币,可以理解为攻击者利用多得的数值为1的soVELO代币,通过从其他soToken借贷赚取了WETH、USDC等underlyingtoken。攻击者使用同样的手法多次重复攻击,最终获利巨大。

具体步骤如下:

攻击者执行题案中的第五笔交易,设置提案中规定的借贷因子。

攻击者从VolatileV2AMM-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步,获利其他的undeylyingtoken。

攻击者执行数次第9步的操作,还掉闪电贷,获利离场。

$100如何撬动$650万

攻击发生后,X上@tonyke_bot用户在交易0x0a284cd中,通过抵押1144个VELO代币到soVELO合约中,铸造了0.00000011个soVELO。这样操作之所以能够阻止攻击者进一步攻击,是因为这笔交易改变了soVELO中totalSupply的大小和持有的VELO代币的数量totalCash,而totalSupply增长对于计算exchangeRate产生的影响大于totalCash增长产生的影响,因此exchangeRate变小,从而导致攻击者进行攻击时,无法再利用精度损失赚取soVELO,导致攻击无法再进行。

资金追踪

攻击者攫取非法收益后不久便将资金进行了转移,大部分资金转移到了以下4个地址当中,有的是为了换个地址继续攻击,有的是为了洗钱:

0x4ab93fc50b82d4dc457db85888dfdae28d29b98d

攻击者将198WETH转入了该地址,然后该地址采用了相同的攻击手法,在下列交易中获得非法收益:

攻击结束后,该地址将上述非法所得转给了0x5d0d99e9886581ff8fcb01f35804317f5ed80bbb。

0x5d0d99e9886581ff8fcb01f35804317f5ed80bbb

攻击者将724277USDC、2353VELO转入了该地址,并将USDC兑换成了Ether。随后立即将部分资金转入了Stargate跨链桥,剩下大部分非法资金残留在该地址中:

0xbd18100a168321701955e348f03d0df4f517c13b 

攻击者将33WETH转入了该地址,并采用peelchain的方式尝试洗钱,洗钱链路如下:

0xbd18100a168321701955e348f03d0df4f517c13b -> 0x7e97b74252b6df53caf386fb4c54d4fb59cb6928 -> 0xc521bde5e53f537ff208970152b75a003093c2b4 -> 0x9f09ec563222fe52712dc413d0b7b66cb5c7c795。

0x4fac0651bcc837bf889f6a7d79c1908419fe1770

攻击者将563WETH转入了该地址,随后转给了 0x1915F77A116dcE7E9b8F4C4E43CDF81e2aCf9C68,目前没有进一步行为。

攻击者本次洗钱的手段相对来说较为专业,手法呈现多样性趋势。因此对于我们Web3参与者来说,在安全方面要持续不断地提高我们的反洗钱能力,通过KYT、AML等相关区块链交易安全产品来提高Defi项目的安全性。

安全建议

精度损失需重视。精度损失导致的安全问题层出不穷,尤其是在Defi项目中,精度损失往往导致严重的资金损失。建议项目方和安全审计人员仔细审查项目中存在精度损失的代码,并做好测试,尽量规避该漏洞。

建议类似于Compound中cToken这种market的创建和首次抵押铸造操作由特权用户来执行,避免被攻击者操作,从而操作汇率。

当合约中存在关键变量依赖于“this.balance”或者“token.balanceOf()”的值时,需要慎重考虑该关键变量改变的条件,比如是否允许直接通过给合约转原生币或者代币的方式改变该变量的值,还是只能通过调用某特定函数才能改变该变量的值。

本文由ZANTeam的Cara(X账号@Cara6289)和XiG(X账号 @SHXiGi)共同撰写。