// 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"; /** * @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; // 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 = 0; seed < 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 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++; // Check profitability if (finalBalance > initialBalance) { profitableScenarios++; marketProfitable++; uint256 profit = finalBalance - initialBalance; uint256 profitPercentage = (profit * 100) / initialBalance; console.log(string.concat("PROFITABLE! Seed: ", vm.toString(seed), " Profit: ", vm.toString(profitPercentage), "%")); // Add to CSV profitableCSV = string.concat( profitableCSV, optimizerClass, ",", vm.toString(seed), ",", vm.toString(initialBalance), ",", vm.toString(finalBalance), ",", vm.toString(profit), ",", vm.toString(profitPercentage), "\n" ); profitableCount++; } } 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))); // 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)); } 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 < 40) { // 40% 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); } } else if (action < 80) { // 40% 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); } } else if (action < 95) { // 15% 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 {} } else { // 5% chance wait vm.warp(block.timestamp + 1 minutes + (rand % 2 hours)); } // 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); // 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) { string memory positionFilename = string.concat( "positions_", scenarioName, "_", vm.toString(seed), ".csv" ); writeCSVToFile(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(); // 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); // Create position data row string memory row = string.concat( label, ",", vm.toString(currentTick), ",", vm.toString(floorLiq), ",", vm.toString(floorLower), ",", vm.toString(floorUpper), ",", vm.toString(anchorLiq), ",", vm.toString(anchorLower), ",", vm.toString(anchorUpper), ",", vm.toString(discoveryLiq), ",", vm.toString(discoveryLower), ",", vm.toString(discoveryUpper), ",", vm.toString(weth.balanceOf(address(lm))), ",", vm.toString(harberg.balanceOf(address(lm))) ); appendCSVRow(row); } }