// SPDX-License-Identifier: GPL-3.0-or-later pragma solidity ^0.8.19; /** * @title Simple Scenario Analysis for LiquidityManager * @notice Lightweight analysis script for researching profitable trading scenarios * @dev Separated from unit tests to focus on research and scenario discovery * Uses the modular LiquidityManager architecture for analysis * Run with: forge script analysis/SimpleAnalysis.s.sol --ffi */ import "../test/LiquidityManager.t.sol"; import "../test/mocks/BullMarketOptimizer.sol"; import "../test/mocks/NeutralMarketOptimizer.sol"; import "../test/mocks/BearMarketOptimizer.sol"; import "./CSVManager.sol"; import "../src/helpers/UniswapHelpers.sol"; import {LiquidityAmounts} from "@aperture/uni-v3-lib/LiquidityAmounts.sol"; import "@aperture/uni-v3-lib/TickMath.sol"; import {IUniswapV3Factory} from "@uniswap/v3-core/contracts/interfaces/IUniswapV3Factory.sol"; import {WETH} from "solmate/tokens/WETH.sol"; contract SimpleAnalysis is LiquidityManagerTest, CSVManager { using UniswapHelpers for IUniswapV3Pool; uint256 public scenariosAnalyzed; uint256 public profitableScenarios; // Market condition optimizers for sentiment analysis BullMarketOptimizer bullOptimizer; NeutralMarketOptimizer neutralOptimizer; BearMarketOptimizer bearOptimizer; /// @notice Initialize setup for fuzzing without position validation function _initializeForFuzzing() internal { _skipSetup(); setUpCustomToken0(false); // WETH as token1 // Note: recenter access will be granted in _createFreshEnvironment } /// @notice Entry point for forge script execution function run() public { console.log("Starting LiquidityManager Fuzzing Analysis..."); console.log("Testing 30-action sequences across 3 market conditions for profitable scenarios"); // Initialize custom setup to skip position validation _initializeForFuzzing(); // Initialize CSV for potential profitable scenario logging initializePositionsCSV(); // Test 3 different market sentiment optimizers string[3] memory scenarioNames = ["Bull Market", "Neutral Market", "Bear Market"]; for (uint256 i = 0; i < 3; i++) { console.log(string.concat("\\n=== TESTING ", scenarioNames[i], " ===")); // Setup optimizer for this scenario _setupScenarioOptimizer(i); // Run fuzzing loop for this scenario bool foundProfit = _runFuzzingLoop(scenarioNames[i]); if (foundProfit) { console.log("Profitable scenario found! CSV written to ./analysis/profitable_scenario.csv"); console.log("Exiting analysis after finding profitable trade."); return; } else { console.log("No profitable scenarios found in this configuration."); } } console.log("\\n=== ANALYSIS COMPLETE ==="); console.log("No profitable scenarios found across all market conditions."); console.log("This indicates effective anti-arbitrage protection is working."); } /// @notice Setup complete fresh environment for specific scenario function _setupScenarioOptimizer(uint256 scenarioIndex) internal { address optimizerAddress = _getOrCreateOptimizer(scenarioIndex); _logScenarioParameters(scenarioIndex); _createFreshEnvironment(optimizerAddress); } /// @notice Deploy a fresh Uniswap factory for isolated testing function deployFreshFactory() internal returns (IUniswapV3Factory) { return UniswapHelpers.deployUniswapFactory(); } /// @notice Get or create optimizer for given scenario index function _getOrCreateOptimizer(uint256 scenarioIndex) internal returns (address) { if (scenarioIndex == 0) { if (address(bullOptimizer) == address(0)) { bullOptimizer = new BullMarketOptimizer(); } return address(bullOptimizer); } else if (scenarioIndex == 1) { if (address(neutralOptimizer) == address(0)) { neutralOptimizer = new NeutralMarketOptimizer(); } return address(neutralOptimizer); } else { if (address(bearOptimizer) == address(0)) { bearOptimizer = new BearMarketOptimizer(); } return address(bearOptimizer); } } /// @notice Log scenario parameters for given index function _logScenarioParameters(uint256 scenarioIndex) internal view { if (scenarioIndex == 0) { console.log("Bull Market: 20% cap inefficiency, 80% anchor share, 30 width, 90% discovery"); } else if (scenarioIndex == 1) { console.log("Neutral Market: 50% cap inefficiency, 50% anchor share, 50 width, 50% discovery"); } else { console.log("Bear Market: 80% cap inefficiency, 20% anchor share, 80 width, 20% discovery"); } } /// @notice Grant recenter access with proper permissions for analysis function _grantAnalysisRecenterAccess() internal { vm.prank(feeDestination); lm.setRecenterAccess(address(this)); } /// @notice Configure all contracts with proper permissions and funding function _configureContracts() internal { lm.setFeeDestination(feeDestination); harberg.setStakingPool(address(stake)); vm.prank(feeDestination); harberg.setLiquidityManager(address(lm)); vm.deal(address(lm), 50 ether); _grantAnalysisRecenterAccess(); } /// @notice Setup test account with ETH and WETH function _setupTestAccount() internal { vm.deal(account, 500 ether); vm.prank(account); weth.deposit{value: 200 ether}(); } /// @notice Perform initial recenter to establish positions function _performInitialRecenter() internal { try lm.recenter() returns (bool /* isUp */) { console.log("Initial recenter successful"); } catch Error(string memory reason) { console.log("Initial recenter failed:", reason); // Continue anyway for fuzzing analysis } _capturePositionData("initial_setup"); } /// @notice Create fresh contracts for each scenario to avoid AddressAlreadySet errors function _createFreshEnvironment(address optimizerAddress) internal { // Create new factory factory = deployFreshFactory(); // Create new WETH weth = IWETH9(address(new WETH())); // Create new Kraiken token harberg = new Kraiken("KRAIKEN", "HARB"); // Determine token order token0isWeth = address(weth) < address(harberg); // Create new pool pool = IUniswapV3Pool(factory.createPool(address(weth), address(harberg), FEE)); pool.initializePoolFor1Cent(token0isWeth); // Create new Stake contract stake = new Stake(address(harberg), feeDestination); // Create new LiquidityManager with the specific optimizer lm = new LiquidityManager(address(factory), address(weth), address(harberg), optimizerAddress); // Configure all contracts in batch _configureContracts(); } /// @notice Run 30-action fuzzing loop for a specific scenario function _runFuzzingLoop(string memory scenarioName) internal returns (bool foundProfit) { _setupTestAccount(); _performInitialRecenter(); uint256 initialBalance = weth.balanceOf(account); console.log("Starting balance:", initialBalance); // Generate seed for this scenario's randomness uint256 scenarioSeed = uint256(keccak256(abi.encodePacked(scenarioName, block.timestamp))); // Execute 30 fuzzing actions for (uint256 action = 0; action < 30; action++) { uint256 actionSeed = uint256(keccak256(abi.encodePacked(scenarioSeed, action))); // Determine if buy or sell (50/50 chance) bool isBuy = (actionSeed % 2) == 0; // Generate random amount (1-50 ETH range) uint256 amount = 1 ether + (actionSeed % (50 ether)); // Execute trade if (isBuy) { // Ensure we don't exceed available WETH balance uint256 wethBalance = weth.balanceOf(account); if (amount > wethBalance) { amount = wethBalance / 2; // Use half of available balance } if (amount > 0) { buy(amount); _capturePositionData(string.concat("buy_", vm.toString(amount))); console.log("Action", action + 1, ": Buy", amount); } } else { // Sell KRAIKEN tokens uint256 harbergBalance = harberg.balanceOf(account); if (harbergBalance > 0) { uint256 sellAmount = (actionSeed % harbergBalance) + 1; if (sellAmount > harbergBalance) sellAmount = harbergBalance; sell(sellAmount); _capturePositionData(string.concat("sell_", vm.toString(sellAmount))); console.log("Action", action + 1, ": Sell", sellAmount); } } // 30% chance to recenter after each trade if ((actionSeed % 100) < 30) { try lm.recenter() returns (bool isUp) { _capturePositionData("recenter"); console.log("Action", action + 1, ": Recenter (price moved", isUp ? "UP)" : "DOWN)"); } catch Error(string memory reason) { // Recenter can fail due to amplitude requirements - this is normal if (keccak256(bytes(reason)) != keccak256("amplitude not reached.")) { console.log("Recenter failed:", reason); } } } } // Final cleanup - sell all remaining KRAIKEN to realize profit/loss uint256 finalHarbBalance = harberg.balanceOf(account); if (finalHarbBalance > 0) { sell(finalHarbBalance); _capturePositionData(string.concat("final_sell_", vm.toString(finalHarbBalance))); console.log("Final sell of remaining KRAIKEN:", finalHarbBalance); } // Final recenter try lm.recenter() returns (bool /* isUp */) { _capturePositionData("final_recenter"); console.log("Final recenter completed"); } catch { _capturePositionData("final_recenter_failed"); console.log("Final recenter failed"); } // Check if scenario was profitable uint256 finalBalance = weth.balanceOf(account); console.log("Final balance:", finalBalance); if (finalBalance > initialBalance) { uint256 profit = finalBalance - initialBalance; console.log("\\n[ALERT] PROFITABLE SCENARIO FOUND!"); console.log("Scenario:", scenarioName); console.log("Profit:", profit, "wei"); console.log("Profit:", profit / 1e18, "ETH"); // Mark end of profitable scenario in CSV _capturePositionData("PROFITABLE_SCENARIO_END"); // Write CSV file writeCSVToFile("./analysis/profitable_scenario.csv"); scenariosAnalyzed++; profitableScenarios++; return true; } else { console.log("Loss:", initialBalance - finalBalance, "wei"); scenariosAnalyzed++; return false; } } /// @notice Capture complete position data for CSV analysis function _capturePositionData(string memory actionType) internal { Response memory liquidityResponse = checkLiquidity("analysis"); (, int24 currentTick,,,,,) = pool.slot0(); // Build CSV row in chunks to avoid stack too deep string memory part1 = string(abi.encodePacked( actionType, ",", vm.toString(currentTick), ",", vm.toString(liquidityResponse.floorTickLower), ",", vm.toString(liquidityResponse.floorTickUpper), ",", vm.toString(liquidityResponse.ethFloor), ",", vm.toString(liquidityResponse.harbergFloor) )); string memory part2 = string(abi.encodePacked( ",", vm.toString(liquidityResponse.anchorTickLower), ",", vm.toString(liquidityResponse.anchorTickUpper), ",", vm.toString(liquidityResponse.ethAnchor), ",", vm.toString(liquidityResponse.harbergAnchor) )); string memory part3 = string(abi.encodePacked( ",", vm.toString(liquidityResponse.discoveryTickLower), ",", vm.toString(liquidityResponse.discoveryTickUpper), ",", vm.toString(liquidityResponse.ethDiscovery), ",", vm.toString(liquidityResponse.harbergDiscovery), ",", token0isWeth ? "true" : "false" )); string memory row = string(abi.encodePacked(part1, part2, part3)); appendCSVRow(row); } /// @notice Get actual token amounts from pool position data using proper Uniswap V3 math function _getPositionTokenAmounts( uint128 liquidity, int24 tickLower, int24 tickUpper ) internal view returns (uint256 token0Amount, uint256 token1Amount) { if (liquidity == 0) { return (0, 0); } // Get current price from pool (, int24 currentTick,,,,,) = pool.slot0(); // Calculate sqrt prices for the position bounds and current price uint160 sqrtPriceAX96 = TickMath.getSqrtRatioAtTick(tickLower); uint160 sqrtPriceBX96 = TickMath.getSqrtRatioAtTick(tickUpper); uint160 sqrtPriceX96 = TickMath.getSqrtRatioAtTick(currentTick); // Use LiquidityAmounts library for proper Uniswap V3 calculations if (currentTick < tickLower) { // Current price is below the position range - position only contains token0 token0Amount = LiquidityAmounts.getAmount0ForLiquidity(sqrtPriceAX96, sqrtPriceBX96, liquidity); token1Amount = 0; } else if (currentTick >= tickUpper) { // Current price is above the position range - position only contains token1 token0Amount = 0; token1Amount = LiquidityAmounts.getAmount1ForLiquidity(sqrtPriceAX96, sqrtPriceBX96, liquidity); } else { // Current price is within the position range - position contains both tokens token0Amount = LiquidityAmounts.getAmount0ForLiquidity(sqrtPriceX96, sqrtPriceBX96, liquidity); token1Amount = LiquidityAmounts.getAmount1ForLiquidity(sqrtPriceAX96, sqrtPriceX96, liquidity); } } /// @notice Get analysis statistics function getStats() public view returns (uint256 total, uint256 profitable) { return (scenariosAnalyzed, profitableScenarios); } }