// 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 {ThreePositionStrategy} from "../src/abstracts/ThreePositionStrategy.sol"; import {UniswapHelpers} from "../src/helpers/UniswapHelpers.sol"; import "../test/mocks/BullMarketOptimizer.sol"; import "../test/mocks/WhaleOptimizer.sol"; import "./helpers/CSVManager.sol"; import "./helpers/SwapExecutor.sol"; /** * @title ImprovedFuzzingAnalysis * @notice Enhanced fuzzing with larger trades designed to reach discovery position * @dev Uses more aggressive trading patterns to explore the full liquidity range */ contract ImprovedFuzzingAnalysis is Test, CSVManager { TestEnvironment testEnv; IUniswapV3Factory factory; IUniswapV3Pool pool; IWETH9 weth; Kraiken harberg; Stake stake; LiquidityManager lm; bool token0isWeth; // Reusable swap executor to avoid repeated deployments SwapExecutor swapExecutor; address account = makeAddr("trader"); address whale = makeAddr("whale"); address feeDestination = makeAddr("fees"); // Analysis metrics uint256 public scenariosAnalyzed; uint256 public profitableScenarios; uint256 public discoveryReachedCount; uint256 public totalStakesAttempted; uint256 public totalStakesSucceeded; uint256 public totalSnatchesAttempted; uint256 public totalSnatchesSucceeded; // OPTIMIZATION: Circular buffer for position tracking (max 50 positions) uint256 constant MAX_TRACKED_POSITIONS = 50; uint256[MAX_TRACKED_POSITIONS] public trackedPositions; uint256 public positionWriteIndex; uint256 public totalPositionsCreated; mapping(uint256 => address) public positionOwners; // Counter to limit CSV recording and prevent memory issues uint256 private csvRecordCount; // Configuration uint256 public fuzzingRuns; bool public trackPositions; bool public enableStaking; uint256 public buyBias; // 0-100, percentage bias towards buying vs selling uint256 public tradesPerRun; // Number of trades/actions per scenario uint256 public stakingBias; // 0-100, percentage bias towards staking vs unstaking string public optimizerClass; function run() public virtual { _loadConfiguration(); console.log("=== IMPROVED Fuzzing Analysis ==="); console.log("Designed to reach discovery position with larger trades"); console.log(string.concat("Optimizer: ", optimizerClass)); console.log(string.concat("Fuzzing runs: ", vm.toString(fuzzingRuns))); console.log(string.concat("Staking enabled: ", enableStaking ? "true" : "false")); console.log(""); testEnv = new TestEnvironment(feeDestination); // Deploy factory once for all runs (gas optimization) factory = UniswapHelpers.deployUniswapFactory(); // Get optimizer address optimizerAddress = _getOptimizerByClass(optimizerClass); // Track profitable scenarios (limit string concatenation to prevent memory issues) string memory profitableCSV = "Scenario,Seed,Initial Balance,Final Balance,Profit,Profit %,Discovery Reached\n"; uint256 profitableCount; uint256 maxProfitableRecords = 20; // Limit to prevent memory issues 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 with existing factory (factory, pool, weth, harberg, stake, lm,, token0isWeth) = testEnv.setupEnvironmentWithExistingFactory(factory, seed % 2 == 0, feeDestination, optimizerAddress); // Fund LiquidityManager with MORE ETH for deeper liquidity vm.deal(address(lm), 200 ether); // Increased from 50 // Fund accounts with MORE capital uint256 traderFund = 50 ether + (uint256(keccak256(abi.encodePacked(seed, "trader"))) % 150 ether); // 50-200 ETH uint256 whaleFund = 200 ether + (uint256(keccak256(abi.encodePacked(seed, "whale"))) % 300 ether); // 200-500 ETH vm.deal(account, traderFund * 2); vm.deal(whale, whaleFund * 2); vm.prank(account); weth.deposit{value: traderFund}(); vm.prank(whale); weth.deposit{value: whaleFund}(); // Create SwapExecutor once per scenario to avoid repeated deployments swapExecutor = new SwapExecutor(pool, weth, harberg, token0isWeth); uint256 initialBalance = weth.balanceOf(account); // Initial recenter vm.prank(feeDestination); try lm.recenter{gas: 50_000_000}() {} catch {} // Initialize position tracking for each seed if (trackPositions) { // Reset tracking for new scenario _resetPositionTracking(); clearCSV(); // Clear any previous data csvRecordCount = 0; // Reset counter initializePositionsCSV(); _recordPositionData("Initial"); csvRecordCount = 1; // Count the initial record } // Run improved trading scenario (uint256 finalBalance, bool reachedDiscovery) = _runImprovedScenario(seed); scenariosAnalyzed++; if (reachedDiscovery) { discoveryReachedCount++; } // Check profitability if (finalBalance > initialBalance) { uint256 profit = finalBalance - initialBalance; uint256 profitPct = (profit * 100) / initialBalance; profitableScenarios++; console.log(string.concat("PROFITABLE! Seed: ", vm.toString(seed))); console.log(string.concat(" Profit: ", vm.toString(profit / 1e15), " finney (", vm.toString(profitPct), "%)")); console.log(string.concat(" Discovery reached: ", reachedDiscovery ? "YES" : "NO")); // Only record limited profitable scenarios to prevent memory issues if (profitableCount < maxProfitableRecords) { profitableCSV = string.concat( profitableCSV, optimizerClass, ",", vm.toString(seed), ",", vm.toString(initialBalance), ",", vm.toString(finalBalance), ",", vm.toString(profit), ",", vm.toString(profitPct), ",", reachedDiscovery ? "true" : "false", "\n" ); } profitableCount++; } // Write position CSV if tracking if (trackPositions) { _recordPositionData("Final"); string memory positionFilename = string.concat( "improved_positions_", optimizerClass, "_", vm.toString(seed), ".csv" ); writeCSVToFile(positionFilename); clearCSV(); // Clear buffer for next run } } // Summary console.log("\n=== ANALYSIS COMPLETE ==="); console.log(string.concat("Total scenarios: ", vm.toString(scenariosAnalyzed))); console.log(string.concat("Profitable scenarios: ", vm.toString(profitableScenarios))); console.log(string.concat("Discovery reached: ", vm.toString(discoveryReachedCount), " times")); console.log(string.concat("Discovery rate: ", vm.toString((discoveryReachedCount * 100) / scenariosAnalyzed), "%")); console.log(string.concat("Profit rate: ", vm.toString((profitableScenarios * 100) / scenariosAnalyzed), "%")); if (enableStaking) { console.log("\n=== STAKING METRICS ==="); console.log(string.concat("Stakes attempted: ", vm.toString(totalStakesAttempted))); console.log(string.concat("Stakes succeeded: ", vm.toString(totalStakesSucceeded))); console.log(string.concat("Snatches attempted: ", vm.toString(totalSnatchesAttempted))); console.log(string.concat("Snatches succeeded: ", vm.toString(totalSnatchesSucceeded))); if (totalStakesAttempted > 0) { console.log(string.concat("Stake success rate: ", vm.toString((totalStakesSucceeded * 100) / totalStakesAttempted), "%")); } if (totalSnatchesAttempted > 0) { console.log(string.concat("Snatch success rate: ", vm.toString((totalSnatchesSucceeded * 100) / totalSnatchesAttempted), "%")); } } if (profitableCount > 0) { string memory filename = string.concat("improved_profitable_", vm.toString(block.timestamp), ".csv"); vm.writeFile(filename, profitableCSV); console.log(string.concat("\nResults written to: ", filename)); } } function _runImprovedScenario(uint256 seed) internal virtual returns (uint256 finalBalance, bool reachedDiscovery) { uint256 rand = uint256(keccak256(abi.encodePacked(seed, block.timestamp))); // Get initial discovery position (, int24 discoveryLower, int24 discoveryUpper) = lm.positions(ThreePositionStrategy.Stage.DISCOVERY); // Initial buy to generate some KRAIKEN tokens for trading/staking _executeBuy(account, weth.balanceOf(account) / 4); // Buy 25% of ETH worth _executeBuy(whale, weth.balanceOf(whale) / 4); // Whale buys 25% of ETH worth // Always use random trading strategy for consistent behavior _executeRandomLargeTrades(rand); // Check if we reached discovery (, int24 currentTick,,,,,) = pool.slot0(); reachedDiscovery = (currentTick >= discoveryLower && currentTick < discoveryUpper); if (reachedDiscovery) { console.log(" [DISCOVERY REACHED] at tick", vm.toString(currentTick)); if (trackPositions) { _recordPositionData("Discovery_Reached"); } } // Final cleanup: sell all KRAIKEN uint256 finalKraiken = harberg.balanceOf(account); if (finalKraiken > 0) { _executeSell(account, finalKraiken); } finalBalance = weth.balanceOf(account); } function _executeRandomLargeTrades(uint256 rand) internal { console.log(" Strategy: Random Large Trades"); console.log(" Buy bias:", buyBias, "%"); console.log(" Trades:", tradesPerRun); if (enableStaking) { console.log(" Staking bias:", stakingBias, "%"); } for (uint256 i = 0; i < tradesPerRun; i++) { rand = uint256(keccak256(abi.encodePacked(rand, i))); uint256 actor = rand % 2; // 0 = trader, 1 = whale // Use buy bias to determine action uint256 actionRoll = rand % 100; uint256 action; if (actionRoll < buyBias) { action = 0; // Buy (biased towards buying when buyBias > 50) } else if (actionRoll < 90) { action = 1; // Sell (reduced probability when buyBias is high) } else { action = 2; // Recenter (10% chance) } address actorAddr = actor == 0 ? account : whale; if (action == 0) { // Large buy (30-80% of balance, or more with high buy bias) uint256 buyPct = buyBias > 70 ? 50 + (rand % 40) : 30 + (rand % 51); uint256 buyAmount = weth.balanceOf(actorAddr) * buyPct / 100; if (buyAmount > 0) { _executeBuy(actorAddr, buyAmount); } } else if (action == 1) { // Large sell (reduced amounts with high buy bias) uint256 sellPct = buyBias > 70 ? 10 + (rand % 30) : 30 + (rand % 71); uint256 sellAmount = harberg.balanceOf(actorAddr) * sellPct / 100; if (sellAmount > 0) { _executeSell(actorAddr, sellAmount); } } else { // Recenter vm.warp(block.timestamp + (rand % 2 hours)); vm.prank(feeDestination); try lm.recenter{gas: 50_000_000}() {} catch {} } // Every 3rd trade, attempt staking/unstaking if enabled if (enableStaking && i % 3 == 2) { uint256 stakingRoll = uint256(keccak256(abi.encodePacked(rand, "staking", i))) % 100; if (stakingRoll < stakingBias) { // Try to stake _executeStake(rand + i * 1000); } else { // Try to unstake _executeExitPosition(rand + i * 1000); } } // Record position data for visualization (always record all trades) if (trackPositions) { _recordPositionDataSafe(string.concat("T", vm.toString(i))); } } } function _executeBuy(address buyer, uint256 amount) internal virtual { if (amount == 0 || weth.balanceOf(buyer) < amount) return; vm.prank(buyer); weth.transfer(address(swapExecutor), amount); try swapExecutor.executeBuy(amount, buyer) {} catch {} } function _executeSell(address seller, uint256 amount) internal virtual { if (amount == 0 || harberg.balanceOf(seller) < amount) return; vm.prank(seller); harberg.transfer(address(swapExecutor), amount); try swapExecutor.executeSell(amount, seller) {} catch {} } function _getOptimizerByClass(string memory class) internal returns (address) { if (keccak256(bytes(class)) == keccak256("BullMarketOptimizer")) { return address(new BullMarketOptimizer()); } else if (keccak256(bytes(class)) == keccak256("WhaleOptimizer")) { return address(new WhaleOptimizer()); } else { return address(new BullMarketOptimizer()); } } function _loadConfiguration() internal { fuzzingRuns = vm.envOr("FUZZING_RUNS", uint256(20)); trackPositions = vm.envOr("TRACK_POSITIONS", false); enableStaking = vm.envOr("ENABLE_STAKING", true); // Default to true buyBias = vm.envOr("BUY_BIAS", uint256(50)); // Default 50% (balanced) tradesPerRun = vm.envOr("TRADES_PER_RUN", uint256(15)); // Default 15 trades stakingBias = vm.envOr("STAKING_BIAS", uint256(80)); // Default 80% stake vs 20% unstake optimizerClass = vm.envOr("OPTIMIZER_CLASS", string("BullMarketOptimizer")); } // Safe wrapper that prevents gas and memory issues function _recordPositionDataSafe(string memory label) internal { // Only record if we have enough gas remaining if (gasleft() < 1_000_000) { return; // Skip recording if low on gas } // Skip if CSV is getting extremely large (increased limit for all trades) if (bytes(csv).length > 100000) { return; // Skip to avoid memory issues } csvRecordCount++; _recordPositionData(label); } function _recordPositionData(string memory label) internal { // Absolutely prevent recording if buffer is too large if (bytes(csv).length > 100000) { return; // Stop recording to prevent memory issues } (,int24 currentTick,,,,,) = pool.slot0(); // Get position data (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); // Skip staking data for trades to save memory (only get for key events) uint256 pctStaked = 0; uint256 avgTax = 0; if (enableStaking && bytes(label).length > 1 && bytes(label)[0] != 0x54) { // Not a "T" trade label try stake.getPercentageStaked{gas: 30000}() returns (uint256 pct) { pctStaked = pct; } catch {} try stake.getAverageTaxRate{gas: 30000}() returns (uint256 tax) { avgTax = tax; } catch {} } // Build CSV row with minimal concatenations string memory row = string.concat( label, ",", vm.toString(currentTick), ",", vm.toString(floorLower), "," ); row = string.concat( row, vm.toString(floorUpper), ",", vm.toString(floorLiq), "," ); row = string.concat( row, vm.toString(anchorLower), ",", vm.toString(anchorUpper), "," ); row = string.concat( row, vm.toString(anchorLiq), ",", vm.toString(discoveryLower), "," ); row = string.concat( row, vm.toString(discoveryUpper), ",", vm.toString(discoveryLiq), "," ); row = string.concat( row, token0isWeth ? "1" : "0", ",", vm.toString(pctStaked), ",", vm.toString(avgTax) ); appendCSVRow(row); } function _executeStake(uint256 rand) internal { address staker = rand % 2 == 0 ? account : whale; uint256 harbBalance = harberg.balanceOf(staker); if (harbBalance > harberg.minStake()) { uint256 amount = harbBalance * (30 + (rand % 50)) / 100; if (amount < harberg.minStake()) { amount = harberg.minStake(); } uint32 taxRate = uint32(rand % 30); vm.prank(staker); harberg.approve(address(stake), amount); totalStakesAttempted++; vm.prank(staker); try stake.snatch{gas: 10_000_000}(amount, staker, taxRate, new uint256[](0)) returns (uint256 positionId) { totalStakesSucceeded++; _addTrackedPosition(positionId, staker); console.log(" STAKED:", amount / 1e18, "KRAIKEN"); if (trackPositions) { _recordPositionDataSafe("Stake"); } } catch { // If regular stake fails, try snatching with higher tax rate if (totalPositionsCreated > 0) { _tryOptimizedSnatch(staker, amount); } } } } function _tryOptimizedSnatch(address staker, uint256 amount) internal { uint32 maxTaxRate = 29; // Find up to 3 snatchable positions quickly uint256[] memory toSnatch = _findSnatchableQuick(maxTaxRate, amount); if (toSnatch.length > 0) { totalSnatchesAttempted++; vm.prank(staker); try stake.snatch{gas: 15_000_000}(amount, staker, maxTaxRate, toSnatch) returns (uint256 positionId) { totalSnatchesSucceeded++; _addTrackedPosition(positionId, staker); console.log(" SNATCHED", toSnatch.length, "positions"); if (trackPositions) { _recordPositionDataSafe("Snatch"); } } catch {} } } function _executeExitPosition(uint256 rand) internal { address staker = rand % 2 == 0 ? account : whale; // Find a position owned by this staker uint256 positionToExit = _findOwnedPosition(staker); if (positionToExit > 0) { vm.prank(staker); try stake.exitPosition{gas: 5_000_000}(positionToExit) { _removeTrackedPosition(positionToExit); if (trackPositions) { _recordPositionDataSafe("Unstake"); } } catch { // Position might be already exited/snatched _removeTrackedPosition(positionToExit); } } } function _findSnatchableQuick(uint32 maxTaxRate, uint256 amountNeeded) internal view returns (uint256[] memory) { uint256[] memory result = new uint256[](3); // Max 3 positions uint256 count = 0; uint256 totalShares = 0; // Check only recent positions (last 20 in circular buffer) uint256 checkCount = totalPositionsCreated < 20 ? totalPositionsCreated : 20; uint256 startIdx = positionWriteIndex > checkCount ? positionWriteIndex - checkCount : 0; for (uint256 i = 0; i < checkCount && count < 3; i++) { uint256 idx = (startIdx + i) % MAX_TRACKED_POSITIONS; uint256 positionId = trackedPositions[idx]; if (positionId == 0) continue; (uint256 shares,, , , uint32 taxRate) = stake.positions(positionId); if (shares > 0 && taxRate < maxTaxRate) { result[count] = positionId; totalShares += shares; count++; if (stake.sharesToAssets(totalShares) >= amountNeeded) { break; } } } // Resize result uint256[] memory finalResult = new uint256[](count); for (uint256 i = 0; i < count; i++) { finalResult[i] = result[i]; } return finalResult; } // Helper functions for circular buffer position tracking function _addTrackedPosition(uint256 positionId, address owner) internal { trackedPositions[positionWriteIndex] = positionId; positionOwners[positionId] = owner; positionWriteIndex = (positionWriteIndex + 1) % MAX_TRACKED_POSITIONS; totalPositionsCreated++; } function _removeTrackedPosition(uint256 positionId) internal { delete positionOwners[positionId]; // Don't remove from circular buffer, just mark owner as deleted } function _resetPositionTracking() internal { // Clear circular buffer for (uint256 i = 0; i < MAX_TRACKED_POSITIONS; i++) { trackedPositions[i] = 0; } positionWriteIndex = 0; totalPositionsCreated = 0; } function _findOwnedPosition(address owner) internal view returns (uint256) { // Check last 10 positions for ownership uint256 checkCount = totalPositionsCreated < 10 ? totalPositionsCreated : 10; uint256 startIdx = positionWriteIndex > checkCount ? positionWriteIndex - checkCount : 0; for (uint256 i = 0; i < checkCount; i++) { uint256 idx = (startIdx + i) % MAX_TRACKED_POSITIONS; uint256 positionId = trackedPositions[idx]; if (positionId > 0 && positionOwners[positionId] == owner) { return positionId; } } return 0; } }