// SPDX-License-Identifier: GPL-3.0-or-later pragma solidity ^0.8.19; import "forge-std/Test.sol"; import {TestEnvironment} from "../test/helpers/TestBase.sol"; import {IUniswapV3Pool} from "@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol"; import {IUniswapV3Factory} from "@uniswap/v3-core/contracts/interfaces/IUniswapV3Factory.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 {TickMath} from "@aperture/uni-v3-lib/TickMath.sol"; import {ThreePositionStrategy} from "../src/abstracts/ThreePositionStrategy.sol"; import "../test/mocks/BullMarketOptimizer.sol"; import "../test/mocks/NeutralMarketOptimizer.sol"; import "../test/mocks/BearMarketOptimizer.sol"; import "../test/mocks/WhaleOptimizer.sol"; import "../test/mocks/MockOptimizer.sol"; import "../test/mocks/RandomScenarioOptimizer.sol"; import "./helpers/CSVManager.sol"; import "./helpers/SwapExecutor.sol"; import {LiquidityAmounts} from "@aperture/uni-v3-lib/LiquidityAmounts.sol"; /** * @title FuzzingAnalysis * @notice Fuzzing analysis to find profitable trading scenarios against LiquidityManager * @dev Configurable via environment variables: * - FUZZING_RUNS: Number of fuzzing iterations per market (default 100) * - TRACK_POSITIONS: Track detailed position data (default false) */ contract FuzzingAnalysis is Test, CSVManager { TestEnvironment testEnv; IUniswapV3Factory factory; IUniswapV3Pool pool; IWETH9 weth; Kraiken harberg; Stake stake; LiquidityManager lm; bool token0isWeth; address account = makeAddr("trader"); address feeDestination = makeAddr("fees"); // Analysis metrics uint256 public scenariosAnalyzed; uint256 public profitableScenarios; // Configuration uint256 public fuzzingRuns; bool public trackPositions; string public optimizerClass; uint256 public tradesPerRun; uint256 public seedOffset; // Optimizers BullMarketOptimizer bullOptimizer; NeutralMarketOptimizer neutralOptimizer; BearMarketOptimizer bearOptimizer; WhaleOptimizer whaleOptimizer; MockOptimizer mockOptimizer; RandomScenarioOptimizer randomOptimizer; function run() public { _loadConfiguration(); console.log("=== Fuzzing Analysis ==="); console.log(string.concat("Optimizer: ", optimizerClass)); console.log(string.concat("Fuzzing runs: ", vm.toString(fuzzingRuns))); console.log(string.concat("Trades per run: ", vm.toString(tradesPerRun))); console.log(string.concat("Position tracking: ", trackPositions ? "enabled" : "disabled")); console.log(""); testEnv = new TestEnvironment(feeDestination); // Get optimizer based on class name address optimizerAddress = _getOptimizerByClass(optimizerClass); // Initialize CSV for profitable scenarios string memory profitableCSV = "Scenario,Seed,Initial Balance,Final Balance,Profit,Profit %\n"; uint256 profitableCount; uint256 marketProfitable = 0; console.log(string.concat("=== FUZZING with ", optimizerClass, " ===")); for (uint256 seed = seedOffset; seed < seedOffset + fuzzingRuns; seed++) { if (seed % 10 == 0 && seed > 0) { console.log(string.concat("Progress: ", vm.toString(seed), "/", vm.toString(fuzzingRuns))); } // Create fresh environment for each run (factory, pool, weth, harberg, stake, lm,, token0isWeth) = testEnv.setupEnvironmentWithOptimizer(seed % 2 == 0, feeDestination, optimizerAddress); // Fund LiquidityManager with initial ETH vm.deal(address(lm), 50 ether); // Fund account with random amount (10-50 ETH) uint256 fundAmount = 10 ether + (uint256(keccak256(abi.encodePacked(seed, "fund"))) % 40 ether); vm.deal(account, fundAmount * 2); vm.prank(account); weth.deposit{value: fundAmount}(); uint256 initialBalance = weth.balanceOf(account); // Initial recenter vm.warp(block.timestamp + 5 hours); vm.prank(feeDestination); try lm.recenter() {} catch {} // Run trading scenario uint256 finalBalance = _runFuzzedScenario(optimizerClass, seed); scenariosAnalyzed++; // Calculate profit/loss bool isProfitable = finalBalance > initialBalance; uint256 profitOrLoss; uint256 profitOrLossPercentage; if (isProfitable) { profitOrLoss = finalBalance - initialBalance; profitOrLossPercentage = (profitOrLoss * 100) / initialBalance; profitableScenarios++; marketProfitable++; console.log(string.concat("PROFITABLE! Seed: ", vm.toString(seed), " Profit: ", vm.toString(profitOrLossPercentage), "%")); // Add to CSV profitableCSV = string.concat( profitableCSV, optimizerClass, ",", vm.toString(seed), ",", vm.toString(initialBalance), ",", vm.toString(finalBalance), ",", vm.toString(profitOrLoss), ",", vm.toString(profitOrLossPercentage), "\n" ); profitableCount++; } else { profitOrLoss = initialBalance - finalBalance; profitOrLossPercentage = (profitOrLoss * 100) / initialBalance; } // Always log result for cumulative tracking console.log(string.concat("RESULT|SEED:", vm.toString(seed), "|INITIAL:", vm.toString(initialBalance), "|FINAL:", vm.toString(finalBalance), "|PNL:", isProfitable ? "+" : "-", vm.toString(profitOrLoss))); } console.log(string.concat("\nResults for ", optimizerClass, ":")); console.log(string.concat("Profitable: ", vm.toString(marketProfitable), "/", vm.toString(fuzzingRuns))); console.log(""); console.log("=== ANALYSIS COMPLETE ==="); console.log(string.concat("Total scenarios analyzed: ", vm.toString(scenariosAnalyzed))); console.log(string.concat("Total profitable scenarios: ", vm.toString(profitableScenarios))); console.log(string.concat("Profitable rate: ", vm.toString((profitableScenarios * 100) / scenariosAnalyzed), "%")); // Write profitable scenarios CSV if any found if (profitableCount > 0) { console.log("Writing profitable scenarios CSV..."); string memory filename = string.concat("profitable_scenarios_", vm.toString(block.timestamp), ".csv"); vm.writeFile(filename, profitableCSV); console.log(string.concat("\nProfitable scenarios written to: ", filename)); } else { console.log("\nNo profitable scenarios found."); } console.log("Script execution complete."); } function _loadConfiguration() internal { fuzzingRuns = vm.envOr("FUZZING_RUNS", uint256(100)); trackPositions = vm.envOr("TRACK_POSITIONS", false); optimizerClass = vm.envOr("OPTIMIZER_CLASS", string("BullMarketOptimizer")); tradesPerRun = vm.envOr("TRADES_PER_RUN", uint256(20)); seedOffset = vm.envOr("SEED_OFFSET", uint256(0)); } function _runFuzzedScenario(string memory scenarioName, uint256 seed) internal returns (uint256) { // Initialize position tracking CSV if enabled if (trackPositions) { initializePositionsCSV(); _recordPositionData("Initial"); } // Use seed for randomness uint256 rand = uint256(keccak256(abi.encodePacked(seed, scenarioName, block.timestamp))); // Use configured number of trades (with some randomness) uint256 numTrades = tradesPerRun + (rand % 11) - 5; // +/- 5 trades if (numTrades < 5) numTrades = 5; // Minimum 5 trades // Initial buy if no HARB if (harberg.balanceOf(account) == 0 && weth.balanceOf(account) > 0) { uint256 initialBuy = weth.balanceOf(account) / 10; _executeBuy(initialBuy); } // Execute random trades for (uint256 i = 0; i < numTrades; i++) { rand = uint256(keccak256(abi.encodePacked(rand, i))); uint256 action = rand % 100; if (action < 25) { // 25% chance buy uint256 wethBal = weth.balanceOf(account); if (wethBal > 0) { uint256 buyPercent = 1 + (rand % 1000); // 0.1% to 100% uint256 buyAmount = (wethBal * buyPercent) / 1000; if (buyAmount > 0) { _executeBuy(buyAmount); if (trackPositions) { _recordPositionData(string.concat("Buy_", vm.toString(i))); } } } } else if (action < 50) { // 25% chance sell uint256 harbBal = harberg.balanceOf(account); if (harbBal > 0) { uint256 sellPercent = 1 + (rand % 1000); // 0.1% to 100% uint256 sellAmount = (harbBal * sellPercent) / 1000; if (sellAmount > 0) { _executeSell(sellAmount); if (trackPositions) { _recordPositionData(string.concat("Sell_", vm.toString(i))); } } } } else { // 50% chance recenter uint256 waitTime = 1 minutes + (rand % 10 hours); vm.warp(block.timestamp + waitTime); vm.prank(feeDestination); try lm.recenter() { if (trackPositions) { _recordPositionData(string.concat("Recenter_", vm.toString(i))); } } catch {} } // Skip trades at extreme ticks (, int24 currentTick, , , , , ) = pool.slot0(); if (currentTick < -887000 || currentTick > 887000) continue; } // Sell remaining HARB uint256 finalHarb = harberg.balanceOf(account); if (finalHarb > 0) { _executeSell(finalHarb); if (trackPositions) { _recordPositionData("Final_Sell"); } } // Final recenters for (uint256 j = 0; j < 1 + (rand % 3); j++) { vm.warp(block.timestamp + 5 hours); vm.prank(feeDestination); try lm.recenter() {} catch {} } // Write position tracking CSV if enabled if (trackPositions) { _recordPositionData("Final"); string memory positionFilename = string.concat( "positions_", scenarioName, "_", vm.toString(seed), ".csv" ); writeCSVToFile(positionFilename); console.log(string.concat("Position tracking CSV written to: ", positionFilename)); } return weth.balanceOf(account); } function _executeBuy(uint256 amount) internal { if (amount == 0 || weth.balanceOf(account) < amount) return; SwapExecutor executor = new SwapExecutor(pool, weth, harberg, token0isWeth); vm.prank(account); weth.transfer(address(executor), amount); try executor.executeBuy(amount, account) {} catch {} } function _executeSell(uint256 amount) internal { if (amount == 0 || harberg.balanceOf(account) < amount) return; SwapExecutor executor = new SwapExecutor(pool, weth, harberg, token0isWeth); vm.prank(account); harberg.transfer(address(executor), amount); try executor.executeSell(amount, account) {} catch {} } function _getOrCreateOptimizer(uint256 index) internal returns (address) { if (index == 0) { if (address(bullOptimizer) == address(0)) bullOptimizer = new BullMarketOptimizer(); return address(bullOptimizer); } else if (index == 1) { if (address(neutralOptimizer) == address(0)) neutralOptimizer = new NeutralMarketOptimizer(); return address(neutralOptimizer); } else { if (address(bearOptimizer) == address(0)) bearOptimizer = new BearMarketOptimizer(); return address(bearOptimizer); } } function _getOptimizerByClass(string memory className) internal returns (address) { bytes32 classHash = keccak256(abi.encodePacked(className)); if (classHash == keccak256(abi.encodePacked("BullMarketOptimizer"))) { if (address(bullOptimizer) == address(0)) bullOptimizer = new BullMarketOptimizer(); return address(bullOptimizer); } else if (classHash == keccak256(abi.encodePacked("NeutralMarketOptimizer"))) { if (address(neutralOptimizer) == address(0)) neutralOptimizer = new NeutralMarketOptimizer(); return address(neutralOptimizer); } else if (classHash == keccak256(abi.encodePacked("BearMarketOptimizer"))) { if (address(bearOptimizer) == address(0)) bearOptimizer = new BearMarketOptimizer(); return address(bearOptimizer); } else if (classHash == keccak256(abi.encodePacked("WhaleOptimizer"))) { if (address(whaleOptimizer) == address(0)) whaleOptimizer = new WhaleOptimizer(); return address(whaleOptimizer); } else if (classHash == keccak256(abi.encodePacked("MockOptimizer"))) { if (address(mockOptimizer) == address(0)) { mockOptimizer = new MockOptimizer(); mockOptimizer.initialize(address(harberg), address(stake)); } return address(mockOptimizer); } else if (classHash == keccak256(abi.encodePacked("RandomScenarioOptimizer"))) { if (address(randomOptimizer) == address(0)) randomOptimizer = new RandomScenarioOptimizer(); return address(randomOptimizer); } else { revert(string.concat("Unknown optimizer class: ", className, ". Available: BullMarketOptimizer, NeutralMarketOptimizer, BearMarketOptimizer, WhaleOptimizer, MockOptimizer, RandomScenarioOptimizer")); } } function _recordPositionData(string memory label) internal { (,int24 currentTick,,,,,) = pool.slot0(); // Cap currentTick to avoid overflow in extreme cases if (currentTick > 887000) currentTick = 887000; if (currentTick < -887000) currentTick = -887000; // Get each position (uint128 floorLiq, int24 floorLower, int24 floorUpper) = lm.positions(ThreePositionStrategy.Stage.FLOOR); (uint128 anchorLiq, int24 anchorLower, int24 anchorUpper) = lm.positions(ThreePositionStrategy.Stage.ANCHOR); (uint128 discoveryLiq, int24 discoveryLower, int24 discoveryUpper) = lm.positions(ThreePositionStrategy.Stage.DISCOVERY); // Debug: Log liquidity values if (keccak256(bytes(label)) == keccak256(bytes("Initial")) || keccak256(bytes(label)) == keccak256(bytes("Recenter_2"))) { console.log("=== LIQUIDITY VALUES ==="); console.log("Label:", label); console.log("Current tick:", uint256(int256(currentTick))); console.log("Anchor range:", uint256(int256(anchorLower)), "-", uint256(int256(anchorUpper))); console.log("Anchor liquidity:", uint256(anchorLiq)); console.log("Discovery range:", uint256(int256(discoveryLower)), "-", uint256(int256(discoveryUpper))); console.log("Discovery liquidity:", uint256(discoveryLiq)); if (uint256(anchorLiq) > 0) { console.log("Discovery/Anchor liquidity ratio:", uint256(discoveryLiq) * 100 / uint256(anchorLiq), "%"); console.log("Anchor width:", uint256(int256(anchorUpper - anchorLower)), "ticks"); console.log("Discovery width:", uint256(int256(discoveryUpper - discoveryLower)), "ticks"); uint256 anchorLiqPerTick = uint256(anchorLiq) * 1000 / uint256(int256(anchorUpper - anchorLower)); uint256 discoveryLiqPerTick = uint256(discoveryLiq) * 1000 / uint256(int256(discoveryUpper - discoveryLower)); console.log("Anchor liquidity per tick (x1000):", anchorLiqPerTick); console.log("Discovery liquidity per tick (x1000):", discoveryLiqPerTick); console.log("Discovery/Anchor per tick ratio:", discoveryLiqPerTick * 100 / anchorLiqPerTick, "%"); } } // Create position data row with liquidity values directly string memory row = string.concat( label, ", ", vm.toString(currentTick), ", ", vm.toString(floorLower), ", ", vm.toString(floorUpper), ", ", vm.toString(uint256(floorLiq)), ", ", vm.toString(anchorLower), ", ", vm.toString(anchorUpper), ", ", vm.toString(uint256(anchorLiq)), ", ", vm.toString(discoveryLower), ", ", vm.toString(discoveryUpper), ", ", vm.toString(uint256(discoveryLiq)), ", ", token0isWeth ? "true" : "false" ); appendCSVRow(row); } }