mt logoMyToken
总市值:
0%
恐慌指数:
0%
币种:--
交易所 --
ETH Gas:--
EN
USD
APP
Ap Store QR Code

Scan Download

采用延时喂价还被黑?WarpFinance被黑详解

收藏
分享

By : Kong@慢雾安全团队

背景

2020 年 12 月 18 日,据慢雾区情报 DeFi 项目 Warp Finance 遭受闪电贷攻击。以下是慢雾安全团队对整个攻击流程的详细分析。

攻击过程分析

(分析过程较多,快速了解攻击思路可以直接查看下方完整攻击流程部分)

1、通过攻击交易可以看出攻击者通过 Uniswap 和 dydx 闪电贷借出了约 290 万 DAI 和 34.5 万 WETH:

https://etherscan.io/tx/0x8bb8dc5c7c830bac85fa48acad2505e9300a91c3ff239c9517d0cae33b595090

2、接下来攻击者先使用借来的 WETH 和 DAI 向 Uniswap 的 WETH-DAI 交易对添加流动性,获得了约 9.4 万个 LP Token,为接下来在 Warp 中抵押 LP 做准备。

3、随后攻击者通过 Warp 项目 WarpVaultLP 合约的 provideCollateral 函数抵押之前获得的 LP Token。

function provideCollateral(uint256 _amount) public { require( LPtoken.allowance(msg.sender, address(this)) >= _amount, "Vault must have enough allowance." ); require( LPtoken.balanceOf(msg.sender) >= _amount, "Must have enough LP to provide" ); LPtoken.transferFrom(msg.sender, address(this), _amount); collateralizedLP[msg.sender] = collateralizedLP[msg.sender].add( _amount ); emit CollateralProvided(msg.sender, _amount); }

通过以上代码第 11 行我们可以看到合约通过 collateralizedLP[msg.sender] 记录了攻击者抵押的 LP Token 的数量。

4、之后攻击者的操作是本次攻击最关键的一步:攻击者通过 Uniswap 的 WETH-DAI 交易对将大约 34 万的 WETH 兑换成约 4762 万 DAI,此时 WETH-DAI 池子中约剩下有 43.6 万枚 WETH 和 1328.8 万枚 DAI,而在此之前池子里约有 9.5 万枚 WETH 和 6091 万枚 DAI。我们可以发现在攻击者兑换后池子中 WETH 数量大量增加了,接下来我们通过 Warp 的具体代码来分析攻击者为何这么做。

5、根据官方介绍 Warp Finance 项目是允许用户通过抵押 LP Token 来借出 DAI、USDC、USDT 这些稳定币的,接下里我们来看看 Warp 是如何计算出用户可以借出的稳定币数量:

1)在 Warp 中用户可以通过 WarpControl 合约的 borrowSC 函数借出稳定币:

function borrowSC(address _StableCoin, uint256 _amount) public { uint256 borrowedTotalInUSDC = getTotalBorrowedValue(msg.sender); uint256 borrowLimitInUSDC = getBorrowLimit(msg.sender); uint256 borrowAmountAllowedInUSDC = borrowLimitInUSDC.sub( borrowedTotalInUSDC ); uint256 borrowAmountInUSDC = getPriceOfToken(_StableCoin, _amount); //require the amount being borrowed is less than or equal to the amount they are aloud to borrow require( borrowAmountAllowedInUSDC >= borrowAmountInUSDC, "Borrowing more than allowed" ); //retreive stablecoin vault address being borrowed from and instantiate it WarpVaultSCI WV = WarpVaultSCI(instanceSCTracker[_StableCoin]); //call _borrow function on the stablecoin warp vault WV._borrow(_amount, msg.sender); emit NewBorrow(msg.sender, _StableCoin, _amount); }

2)从上方代码第 3、4 行,我们可以发现 WarpControl 合约是通过 getBorrowLimit 函数来获得用户可以借出稳定的数量,接下来我们具体看 getBorrowLimit 函数:

function getBorrowLimit(address _account) public returns (uint256) { uint256 availibleCollateralValue = getTotalAvailableCollateralValue( _account ); return calcBorrowLimit(availibleCollateralValue); }

3)通过分析我们可以发现 getBorrowLimit 函数先通过 getTotalAvailableCollateralValue 函数计算出 availibleCollateralValue,再将计算结果作为参数传入 calcBorrowLimit 函数中,最后返回具体的数量。我们先分析 getTotalAvailableCollateralValue 函数:

function getTotalAvailableCollateralValue(address _account) public returns (uint256){ //get the number of LP vaults the platform has uint256 numVaults = lpVaults.length; //initialize the totalCollateral variable to zero uint256 totalCollateral = 0; //loop through each lp wapr vault for (uint256 i = 0; i < numVaults; ++i) { //instantiate warp vault at that position WarpVaultLPI vault = WarpVaultLPI(lpVaults[i]); //retreive the address of its asset address asset = vault.getAssetAdd(); //retrieve USD price of this asset uint256 assetPrice = Oracle.getUnderlyingPrice(asset); uint256 accountCollateral = vault.collateralOfAccount(_account); //emit DebugValues(accountCollateral, assetPrice); //multiply the amount of collateral by the asset price and return it uint256 accountAssetsValue = accountCollateral.mul(assetPrice); //add value to total collateral totalCollateral = totalCollateral.add(accountAssetsValue); } //return total USDC value of all collateral return totalCollateral.div(1e18); }

4)对 getTotalAvailableCollateralValue 函数进行具体的分析我们可以看到此函数通过 for 循环来获得 DAI、USDT、USDC 的可借数量总和。我们可以发现在 for 循环的逻辑中通过 Oracle.getUnderlyingPrice(asset) 获取 LP 的价格,并通过 vault.collateralOfAccount(_account) 来获取用户抵押的 LP 数量,最后将两者相乘即得到可借数量。

在这里我们可以猜测:既然抵押数量是确定的,那必然是价格计算出现问题,接下来我们具体分析价格获取的过程:

function getUnderlyingPrice(address _lpToken) public returns (uint256) { address[] memory oracleAdds = LPAssetTracker[_lpToken]; //retreives the oracle contract addresses for each asset that makes up a LP UniswapLPOracleInstance oracle1 = UniswapLPOracleInstance( oracleAdds[0] ); UniswapLPOracleInstance oracle2 = UniswapLPOracleInstance( oracleAdds[1] ); (uint256 reserveA, uint256 reserveB) = UniswapV2Library.getReserves( factory, instanceTracker[oracleAdds[0]], instanceTracker[oracleAdds[1]] ); uint256 priceAsset1 = oracle1.consult( //SlowMist// instanceTracker[oracleAdds[0]], reserveA ); uint256 priceAsset2 = oracle2.consult( instanceTracker[oracleAdds[1]], reserveB ); // Get the total supply of the pool IERC20 lpToken = IERC20(_lpToken); uint256 totalSupplyOfLP = lpToken.totalSupply(); //SlowMist// return _calculatePriceOfLP( totalSupplyOfLP, priceAsset1, priceAsset2, lpToken.decimals() ); //return USDC price of the pool divided by totalSupply of its LPs to get price //of one LP }

5)通过分析 getUnderlyingPrice 函数我们可以发现其先通过 UniswapV2Library.getReserves 来获取 Uniswap LP 池子中两种代币 (如 WETH、DAI) 的实时数量,这里的 LP 池子我们可以通过 WarpControl 合约来依次获取(以下只列举第 1 个),分别是 WETH-DAI、WETH-WBTC、WETH-USDT、WETH-USDC。

6)在获取完 LP 池子中两种代币 (如 WETH、DAI) 的数量后再通过 oracle1.consult() 分别获取这两种代币的价格,而这里的价格获取使用的是 Uniswap 预言机的实现方式,而 Uniswap 预言机价格获取是有经过时间加权的,也就是延时喂价方式,用户获取的价格是上一个区块的价格,这是无法被操控的。因此我们可以知道代币价格获取的是正确的价格。

Uniswap 的预言机介绍:

https://uniswap.org/docs/v2/core-concepts/oracles/

7)接下来将通过 _calculatePriceOfLP 函数来计算 LP Token 的具体价格:

function _calculatePriceOfLP( uint256 supply, uint256 value0, uint256 value1, uint8 supplyDecimals ) public pure returns (uint256) { uint256 totalValue = value0 + value1; uint16 shiftAmount = supplyDecimals; uint256 valueShifted = totalValue * uint256(10)**shiftAmount; uint256 supplyShifted = supply; uint256 valuePerSupply = valueShifted / supplyShifted; return valuePerSupply; }

通过以上代码我们可以知道 LP 价格是如何得出的,以 WETH-DAI 池为例:其通过池子中 WETH 的数量乘 WETH 的价格加上池子中 DAI 的数量乘 DAI 的价格最后除以池子总的 LP Token 数量即可得到单个 LP Token 的价格。具体计算算式如下所示:

通过以上分析我们可以知道 WETH 的价格和 DAI 的价格获取是正常的,无法被恶意操纵,因此我们可以大胆猜测:攻击者通过将巨量的 WETH 打入 WETH-DAI 池子中换取 DAI,这时候池子中 WETH 的数量将大大的增加,而由于滑点的存在,这种巨量兑换操作必然是会亏损一大部分 WETH 的。所以我们再看上面 LP 单价的计算方式,由于 WETH 数量的大大增加,在巨量兑换后池子中 WETH 数量 * WETH 价格 + 池子中 DAI 数量 *DAI价格将远大于巨量兑换前的,也就是池子的总价值大大增加了。所以 LP 的单价也随之提高了,因此攻击者就可以通过其抵押的 LP Token 借出更多的稳定币了。

分析思路验证

我们可以借助 Ethtx.info 来验证我们的猜测是否正确:

https://ethtx.info/mainnet/0x8bb8dc5c7c830bac85fa48acad2505e9300a91c3ff239c9517d0cae33b595090

1、通过上文中第 4 点分析我们可以知道:攻击者通过 Uniswap 的 WETH-DAI 交易对将大约 34 万的 WETH 兑换成约 4762 万 DAI,此时 WETH-DAI 池子中约剩下有 43.6 万枚 WETH 和 1328.8 万枚 DAI,而在此之前池子里约有 9.5 万枚 WETH 和 6091 万枚 DAI。

2、我们可以在 Ethtx.info 发现在兑换前 WETH-DAI 池子的 LP Token 单价为58815427。

巨量兑换后 WETH-DAI 池子的 LP Token 单价为135470392。

我们可以看到由于 WETH 数量的增加造成兑换后池子的总价值几乎翻倍了,因此单个 LP Token 在 Warp 中可借出的稳定币就更多了。

3、接下里如我们猜测的那样攻击者在拉高 LP Token 的价格后通过 WarpControl 合约的 borrowSC 函数分别借出 DAI 和 USDC。

4、最后在 Uniwsap 的 WETH-DAI 池子总归还 DAI,重新拿回 34 万枚 WETH 完成攻击操作。最后只需按部就班的归还闪电贷即可获利。

完整的攻击流程如下

1、攻击者部署攻击合约,并通过 dydx 与 Uniswap 闪电贷借出 DAI 和 WETH。

2、攻击者拿出一小部分的 DAI 和 WETH 在 Uniswap 的 WETH-DAI 池中添加流动性,并获取 LP Token。

3、攻击者使用添加流动性获取的 LP Token 抵押到 Warp Finance 中,为借出稳定币做准备。

4、攻击者利用巨量的 WETH 在 Uniswap 兑换成 DAI 来拉高 WETH-DAI 池子的总价值,使得 Warp Finance 中 LP Token 的单价变高。(注意这里 WETH 和 DAI 价格获取是正确的并没有被操纵,被操纵的是 WETH 的数量,通过增加 WETH 的数量来拉高池子的总价值)。

5、由于 LP Token 的单价变高,导致攻击者抵押的 LP Token 可以借出更多的稳定币来进行获利。

总结

本次攻击的本质是通过操纵 LP Token 的单价来获取更多的稳定币可借贷数量进行获利的。这是由于在 Warp Finance 中 LP Token 的价格是通过 LP 池子的总价值除 以 LP Token 的总数量得到的,虽然代币价格获取正确,但代币数量是可被操纵的,因此 LP 的单价就是可被操纵的,这就形成了攻击的必要条件了。最终项目方损失约 800 万美元,但攻击者抵押的 LP 也留在了 Vault 中,如果抵押的这部分 LP 后续可被清算的话可以一定程度上的弥补项目方的损失。

相关参考链接如下:

Uniswap 预言机实现介绍:

https://uniswap.org/docs/v2/core-concepts/oracles/

本次分析的攻击交易:

https://etherscan.io/tx/0x8bb8dc5c7c830bac85fa48acad2505e9300a91c3ff239c9517d0cae33b595090

往期回顾

往期回顾

Hacking Time 区块链安全攻防峰会第二期来啦!

以小博大,简析 Sushi Swap 攻击事件始末

假钱换真钱,揭秘 Pickle Finance 被黑过程

闪电贷+重入攻击,OUSD 损失 700 万美金技术简析

如何使用闪电贷从 0 撬动百万美元?Value DeFi 协议闪电贷攻击简要分析

Hacking Time 区块链安全攻防峰会第二期来啦!以小博大,简析 Sushi Swap 攻击事件始末假钱换真钱,揭秘 Pickle Finance 被黑过程闪电贷+重入攻击,OUSD 损失 700 万美金技术简析如何使用闪电贷从 0 撬动百万美元?Value DeFi 协议闪电贷攻击简要分析

免责声明:本文版权归原作者所有,不代表MyToken(www.mytokencap.com)观点和立场;如有关于内容、版权等问题,请与我们联系。
相关阅读

DeFi潮流新风口:从链上数据看跨链桥的发展新方向

总锁仓额突破131亿美元,9月独立地址总数超12万个

Bitwise 向美SEC提交比特币策略ETF申请,旨在投资比特币期货和其他金融产品

PANews 9月15日消息,根据一份公开的监管文件,资产管理公司Bitwise 下属部门 Bitwise Index Services 向美国证券交易委员会(SEC)递交了比特币期货交易所交易基金 ETF申请,新基金名为Bitwise Bitcoin Strategy ETF。旨在投资比特币期货和其他金融产品。该文件称:“该基金不会直接投资于比特币,虽然该基金主要通过间接投资于在 CFTC 注册的商品交易所交易的标准化、现金结算的比特币期货合约来获得比特币敞口,但它也可能投资于集合投资工具和加拿大上市的提供比特币敞口的基金”。文件显示,ETF 还可能投资于现金、美国政府证券或货币市场基金。US Bancorp Fund Services 将担任转账代理和管理人,而美国银行将担任托管方。据了解,美国证券交易委员会(SEC)至今还未批准任何比特币 ETF 基金。此外,美证监会主席 Gary Gensler 表示该机构更有可能批准比特币期货 ETF 而不是现货 ETF,因为期货 ETF 将投资于芝加哥商品交易所(CME)提供监管的比特币期货产品,而比特币现货则不受监管。来源链接

知情人士:因需求强烈,Coinbase计划发行的债券或增加至20亿美元

PANews 9月15日消息,有知情人士称,此前计划发行15亿美元债券的Coinbase会将交易规模提升至20亿美元,因为至少已经有70亿美元的订单涌入。其他知情人士表示,等额的7年期和10年期债券将分别以3.375%和3.625%的利率发行,低于最初讨论的借贷成本。彭博社表示,固定收益投资者对该产品的热捧,代表了加密货币不再是一个专属于风险资本的行业,因为养老基金和对冲基金在内的专注投资债务的投资者都希望参与到此次的投资中。此前根据 Coinbase 提交给美国证券交易委员会(SEC)文件显示,Coinbase 将通过私募发行 15 亿美元于 2028 年和 2031 年到期的有担保高级票据,这些票据将由 Coinbase 的全资子公司 Coinbase, Inc. 提供全额无条件担保。来源链接