237 lines
No EOL
9.5 KiB
Solidity
237 lines
No EOL
9.5 KiB
Solidity
// SPDX-License-Identifier: MIT
|
|
pragma solidity ^0.8.19;
|
|
|
|
import "forge-std/Test.sol";
|
|
import {TestEnvironment} from "./helpers/TestBase.sol";
|
|
import {IUniswapV3Pool} from "@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol";
|
|
import {IWETH9} from "../src/interfaces/IWETH9.sol";
|
|
import {Kraiken} from "../src/Kraiken.sol";
|
|
import {Stake} from "../src/Stake.sol";
|
|
import {LiquidityManager} from "../src/LiquidityManager.sol";
|
|
import "../analysis/helpers/SwapExecutor.sol";
|
|
import "../test/mocks/BullMarketOptimizer.sol";
|
|
|
|
/**
|
|
* @title ReplayProfitableScenario
|
|
* @notice Replays the exact profitable scenario captured by the recorder
|
|
* @dev Demonstrates 225% profit exploit by reaching discovery position
|
|
*/
|
|
contract ReplayProfitableScenario is Test {
|
|
TestEnvironment testEnv;
|
|
IUniswapV3Pool pool;
|
|
IWETH9 weth;
|
|
Kraiken kraiken;
|
|
Stake stake;
|
|
LiquidityManager lm;
|
|
bool token0isWeth;
|
|
|
|
address trader = makeAddr("trader");
|
|
address whale = makeAddr("whale");
|
|
address feeDestination = makeAddr("fees");
|
|
|
|
function setUp() public {
|
|
// Recreate exact initial conditions from seed 1
|
|
testEnv = new TestEnvironment(feeDestination);
|
|
BullMarketOptimizer optimizer = new BullMarketOptimizer();
|
|
|
|
// Use seed 1 setup (odd seed = false for first param)
|
|
(,pool, weth, kraiken, stake, lm,, token0isWeth) =
|
|
testEnv.setupEnvironmentWithOptimizer(false, feeDestination, address(optimizer));
|
|
|
|
// Fund exactly as in the recorded scenario
|
|
vm.deal(address(lm), 200 ether);
|
|
|
|
// Trader gets specific amount based on seed 1
|
|
uint256 traderFund = 50 ether + (uint256(keccak256(abi.encodePacked(uint256(1), "trader"))) % 150 ether);
|
|
vm.deal(trader, traderFund * 2);
|
|
vm.prank(trader);
|
|
weth.deposit{value: traderFund}();
|
|
|
|
// Whale gets specific amount based on seed 1
|
|
uint256 whaleFund = 200 ether + (uint256(keccak256(abi.encodePacked(uint256(1), "whale"))) % 300 ether);
|
|
vm.deal(whale, whaleFund * 2);
|
|
vm.prank(whale);
|
|
weth.deposit{value: whaleFund}();
|
|
|
|
// Initial recenter
|
|
vm.prank(feeDestination);
|
|
lm.recenter();
|
|
}
|
|
|
|
function test_replayExactProfitableScenario() public {
|
|
console.log("=== REPLAYING PROFITABLE SCENARIO (Seed 1) ===");
|
|
console.log("Expected: 225% profit by exploiting discovery position\n");
|
|
|
|
uint256 initialTraderWeth = weth.balanceOf(trader);
|
|
uint256 initialWhaleWeth = weth.balanceOf(whale);
|
|
|
|
console.log("Initial balances:");
|
|
console.log(" Trader WETH:", initialTraderWeth / 1e18, "ETH");
|
|
console.log(" Whale WETH:", initialWhaleWeth / 1e18, "ETH");
|
|
|
|
// Log initial tick
|
|
(, int24 initialTick,,,,,) = pool.slot0();
|
|
console.log(" Initial tick:", vm.toString(initialTick));
|
|
|
|
// Execute exact sequence from recording
|
|
console.log("\n--- Executing Recorded Sequence ---");
|
|
|
|
// Step 1: Trader buys 38 ETH worth
|
|
console.log("\nStep 1: Trader BUY 38 ETH");
|
|
_executeBuy(trader, 38215432537912335624);
|
|
_logTickChange();
|
|
|
|
// Step 2: Trader sells large amount of KRAIKEN
|
|
console.log("\nStep 2: Trader SELL 2M KRAIKEN");
|
|
_executeSell(trader, 2023617577713031308513047);
|
|
_logTickChange();
|
|
|
|
// Step 3: Whale buys 132 ETH worth
|
|
console.log("\nStep 3: Whale BUY 132 ETH");
|
|
_executeBuy(whale, 132122625892942968181);
|
|
_logTickChange();
|
|
|
|
// Step 4: Trader sells
|
|
console.log("\nStep 4: Trader SELL 1.5M KRAIKEN");
|
|
_executeSell(trader, 1517713183284773481384785);
|
|
_logTickChange();
|
|
|
|
// Step 5: Whale buys 66 ETH worth
|
|
console.log("\nStep 5: Whale BUY 66 ETH");
|
|
_executeBuy(whale, 66061312946471484091);
|
|
_logTickChange();
|
|
|
|
// Step 6: Trader sells
|
|
console.log("\nStep 6: Trader SELL 1.1M KRAIKEN");
|
|
_executeSell(trader, 1138284887463580111038589);
|
|
_logTickChange();
|
|
|
|
// Step 7: Whale buys 33 ETH worth
|
|
console.log("\nStep 7: Whale BUY 33 ETH");
|
|
_executeBuy(whale, 33030656473235742045);
|
|
_logTickChange();
|
|
|
|
// Step 8: Trader sells
|
|
console.log("\nStep 8: Trader SELL 853K KRAIKEN");
|
|
_executeSell(trader, 853713665597685083278941);
|
|
_logTickChange();
|
|
|
|
// Step 9: Final trader sell
|
|
console.log("\nStep 9: Trader SELL 2.5M KRAIKEN (final)");
|
|
_executeSell(trader, 2561140996793055249836826);
|
|
_logTickChange();
|
|
|
|
// Check if we reached discovery
|
|
(, int24 currentTick,,,,,) = pool.slot0();
|
|
console.log("\n--- Position Analysis ---");
|
|
console.log("Final tick:", vm.toString(currentTick));
|
|
|
|
// The recording showed tick -119663, which should be in discovery range
|
|
// Discovery was around 109200 to 120200 in the other test
|
|
// But with token0isWeth=false, the ranges might be inverted
|
|
|
|
// Calculate final balances
|
|
uint256 finalTraderWeth = weth.balanceOf(trader);
|
|
uint256 finalTraderKraiken = kraiken.balanceOf(trader);
|
|
uint256 finalWhaleWeth = weth.balanceOf(whale);
|
|
uint256 finalWhaleKraiken = kraiken.balanceOf(whale);
|
|
|
|
console.log("\n=== FINAL RESULTS ===");
|
|
console.log("Trader:");
|
|
console.log(" Initial WETH:", initialTraderWeth / 1e18, "ETH");
|
|
console.log(" Final WETH:", finalTraderWeth / 1e18, "ETH");
|
|
console.log(" Final KRAIKEN:", finalTraderKraiken / 1e18);
|
|
|
|
// Calculate profit/loss
|
|
if (finalTraderWeth > initialTraderWeth) {
|
|
uint256 profit = finalTraderWeth - initialTraderWeth;
|
|
uint256 profitPct = (profit * 100) / initialTraderWeth;
|
|
|
|
console.log("\n[SUCCESS] INVARIANT VIOLATED!");
|
|
console.log("Trader Profit:", profit / 1e18, "ETH");
|
|
console.log("Profit Percentage:", profitPct, "%");
|
|
|
|
assertTrue(profitPct > 100, "Expected >100% profit from replay");
|
|
} else {
|
|
uint256 loss = initialTraderWeth - finalTraderWeth;
|
|
console.log("\n[UNEXPECTED] Trader lost:", loss / 1e18, "ETH");
|
|
console.log("Replay may have different initial conditions");
|
|
}
|
|
|
|
console.log("\nWhale:");
|
|
console.log(" Initial WETH:", initialWhaleWeth / 1e18, "ETH");
|
|
console.log(" Final WETH:", finalWhaleWeth / 1e18, "ETH");
|
|
console.log(" Final KRAIKEN:", finalWhaleKraiken / 1e18);
|
|
}
|
|
|
|
function test_verifyDiscoveryReached() public {
|
|
// First execute the scenario
|
|
_executeFullScenario();
|
|
|
|
// Check tick position relative to discovery
|
|
(, int24 currentTick,,,,,) = pool.slot0();
|
|
|
|
// Note: With token0isWeth=false, the tick interpretation is different
|
|
// Negative ticks mean KRAIKEN is cheap relative to WETH
|
|
|
|
console.log("=== DISCOVERY VERIFICATION ===");
|
|
console.log("Current tick after scenario:", vm.toString(currentTick));
|
|
|
|
// The scenario reached tick -119608 which was marked as discovery
|
|
// This confirms the exploit works by reaching rarely-accessed liquidity zones
|
|
|
|
if (currentTick < -119000 && currentTick > -120000) {
|
|
console.log("[CONFIRMED] Reached discovery zone around tick -119600");
|
|
console.log("This zone has massive liquidity that's rarely accessed");
|
|
console.log("Traders can exploit the liquidity imbalance for profit");
|
|
}
|
|
}
|
|
|
|
function _executeFullScenario() internal {
|
|
_executeBuy(trader, 38215432537912335624);
|
|
_executeSell(trader, 2023617577713031308513047);
|
|
_executeBuy(whale, 132122625892942968181);
|
|
_executeSell(trader, 1517713183284773481384785);
|
|
_executeBuy(whale, 66061312946471484091);
|
|
_executeSell(trader, 1138284887463580111038589);
|
|
_executeBuy(whale, 33030656473235742045);
|
|
_executeSell(trader, 853713665597685083278941);
|
|
_executeSell(trader, 2561140996793055249836826);
|
|
}
|
|
|
|
function _executeBuy(address buyer, uint256 amount) internal {
|
|
if (weth.balanceOf(buyer) < amount) {
|
|
console.log(" [WARNING] Insufficient WETH, skipping buy");
|
|
return;
|
|
}
|
|
|
|
SwapExecutor executor = new SwapExecutor(pool, weth, kraiken, token0isWeth, lm);
|
|
vm.prank(buyer);
|
|
weth.transfer(address(executor), amount);
|
|
|
|
try executor.executeBuy(amount, buyer) {} catch {
|
|
console.log(" [WARNING] Buy failed");
|
|
}
|
|
}
|
|
|
|
function _executeSell(address seller, uint256 amount) internal {
|
|
if (kraiken.balanceOf(seller) < amount) {
|
|
console.log(" [WARNING] Insufficient KRAIKEN, selling what's available");
|
|
amount = kraiken.balanceOf(seller);
|
|
if (amount == 0) return;
|
|
}
|
|
|
|
SwapExecutor executor = new SwapExecutor(pool, weth, kraiken, token0isWeth, lm);
|
|
vm.prank(seller);
|
|
kraiken.transfer(address(executor), amount);
|
|
|
|
try executor.executeSell(amount, seller) {} catch {
|
|
console.log(" [WARNING] Sell failed");
|
|
}
|
|
}
|
|
|
|
function _logTickChange() internal view {
|
|
(, int24 currentTick,,,,,) = pool.slot0();
|
|
console.log(string.concat(" Current tick: ", vm.toString(currentTick)));
|
|
}
|
|
} |