// SPDX-License-Identifier: MIT pragma solidity ^0.8.19; import { Optimizer } from "../src/Optimizer.sol"; import { OptimizerV2 } from "../src/OptimizerV2.sol"; import { ThreePositionStrategy } from "../src/abstracts/ThreePositionStrategy.sol"; import { BearMarketOptimizer } from "../test/mocks/BearMarketOptimizer.sol"; import { BullMarketOptimizer } from "../test/mocks/BullMarketOptimizer.sol"; import { ConfigurableOptimizer } from "../test/mocks/ConfigurableOptimizer.sol"; import { ExtremeOptimizer } from "../test/mocks/ExtremeOptimizer.sol"; import { MaliciousOptimizer } from "../test/mocks/MaliciousOptimizer.sol"; import { NeutralMarketOptimizer } from "../test/mocks/NeutralMarketOptimizer.sol"; import { WhaleOptimizer } from "../test/mocks/WhaleOptimizer.sol"; import { FuzzingBase } from "./helpers/FuzzingBase.sol"; import "forge-std/console2.sol"; /// @title StreamlinedFuzzing /// @notice Per-run CSV fuzzer: deploys a named optimizer, runs N randomized /// trade sequences, and writes one CSV per run with full position snapshots. /// Supports ConfigurableOptimizer (env-driven params), uncapped swaps, /// and random stake/unstake actions interleaved with trades. contract StreamlinedFuzzing is FuzzingBase { // CSV filename for current run string csvFilename; // Track cumulative fees uint256 totalFees0; uint256 totalFees1; // Track recentering uint256 lastRecenterBlock; uint256 recenterCount; // Staking position tracking (max 10 active positions per run) uint256[10] activePositions; uint256 numActivePositions; // Batch seed for multi-batch uniqueness uint256 bSeed; function run() public { uint256 numRuns = vm.envOr("FUZZING_RUNS", uint256(20)); uint256 tradesPerRun = vm.envOr("TRADES_PER_RUN", uint256(15)); uint256 buyBias = vm.envOr("BUY_BIAS", uint256(50)); string memory optimizerClass = vm.envOr("OPTIMIZER_CLASS", string("BullMarketOptimizer")); bool uncapped = vm.envOr("UNCAPPED_SWAPS", false); bSeed = vm.envOr("BATCH_SEED", uint256(0)); uint256 bgLpEthPerLayer = vm.envOr("BG_LP_ETH_PER_LAYER", uint256(0)); uint256 stakingLevel = vm.envOr("STAKING_LEVEL", uint256(0)); // 0-100% uint256 stakingTaxRate = vm.envOr("STAKING_TAX_RATE", uint256(3)); // tax rate index 0-29 console2.log("=== Streamlined Fuzzing Analysis ==="); console2.log("Optimizer:", optimizerClass); console2.log("Uncapped:", uncapped); if (bgLpEthPerLayer > 0) { console2.log("Background LP: ", bgLpEthPerLayer / 1e18, "ETH/layer x5"); } if (stakingLevel > 0) { console2.log("Staking level: ", stakingLevel, "% at tax rate index", stakingTaxRate); } _initInfrastructure(); string memory scenarioCode = _generateScenarioId(bSeed); for (uint256 runIndex = 0; runIndex < numRuns; runIndex++) { string memory runId = string(abi.encodePacked(scenarioCode, "-", _padNumber(runIndex, 3))); console2.log("\nRun:", runId); csvFilename = string(abi.encodePacked("analysis/fuzz-", runId, ".csv")); string memory header = "action,amount,tick,floor_lower,floor_upper,floor_liq,anchor_lower,anchor_upper,anchor_liq,discovery_lower,discovery_upper,discovery_liq,eth_balance,kraiken_balance,vwap,fees_eth,fees_kraiken,recenter,fee_dest_weth,fee_dest_krk\n"; vm.writeFile(csvFilename, header); // Deploy fresh environment per run address optimizer = _deployOptimizer(optimizerClass); _setupEnvironment(optimizer, runIndex % 2 == 0, uncapped); // Late-initialize OptimizerV2 (needs stake address from setup) if (keccak256(bytes(optimizerClass)) == keccak256(bytes("OptimizerV2"))) { OptimizerV2(optimizer).initialize(address(kraiken), address(stake)); } // Deploy background LP if configured if (bgLpEthPerLayer > 0) { _deployBackgroundLP(bgLpEthPerLayer); } // Pre-seed staking to set initial sentiment if (stakingLevel > 0) { _seedStaking(stakingLevel, uint32(stakingTaxRate)); } // Reset tracking totalFees0 = 0; totalFees1 = 0; lastRecenterBlock = block.number; recenterCount = 0; numActivePositions = 0; // Fund trader uint256 traderFund = 500 ether + (uint256(keccak256(abi.encodePacked(bSeed, runIndex, "trader"))) % 500 ether); vm.deal(trader, traderFund * 2); vm.prank(trader); weth.deposit{ value: traderFund }(); _recordState("INIT", 0); for (uint256 i = 0; i < tradesPerRun; i++) { // Recenter on average every 3 trades uint256 recenterRand = uint256(keccak256(abi.encodePacked(bSeed, runIndex, i, "recenter"))) % 3; if (recenterRand == 0) { if (_tryRecenter()) { lastRecenterBlock = block.number; recenterCount++; // Rebalance background LP every 10 recenters to track market // (every recenter is too expensive for 2000-trade runs) if (bgLpEthPerLayer > 0 && recenterCount % 10 == 0) { _rebalanceBackgroundLP(bgLpEthPerLayer); } _recordState("RECENTER", 0); } } // 10% chance of staking action instead of trade uint256 stakeRand = uint256(keccak256(abi.encodePacked(bSeed, runIndex, i, "stake"))) % 100; if (stakeRand < 10) { _runStakingAction(runIndex, i); continue; } uint256 rand = uint256(keccak256(abi.encodePacked(bSeed, runIndex, i))) % 100; if (rand < buyBias) { _runBuy(runIndex, i); } else { _runSell(runIndex, i); } } // Exit all staking positions before liquidation _exitAllPositions(); _liquidateTraderHoldings(); _recordState("FINAL", 0); // Log fee revenue (uint256 feeWeth, uint256 feeKrk) = _getFeeRevenue(); console2.log("Fee revenue: WETH=", feeWeth / 1e18, "KRK=", feeKrk / 1e18); } console2.log("\n=== Analysis Complete ==="); console2.log("Generated", numRuns, "CSV files with prefix:", scenarioCode); } function _runBuy(uint256 runIndex, uint256 tradeIndex) internal { uint256 amount = _getTradeAmount(runIndex, tradeIndex, true); if (_executeBuy(amount)) { _recordState("BUY", amount); } else { _recordState("BUY_FAIL", amount); } } function _runSell(uint256 runIndex, uint256 tradeIndex) internal { uint256 amount = _getTradeAmount(runIndex, tradeIndex, false); if (_executeSell(amount)) { _recordState("SELL", amount); } else { _recordState("SELL_FAIL", amount); } } function _getTradeAmount(uint256 runIndex, uint256 tradeIndex, bool isBuy) internal view returns (uint256) { uint256 baseAmount = 10 ether + (uint256(keccak256(abi.encodePacked(bSeed, runIndex, tradeIndex))) % 90 ether); return isBuy ? baseAmount : baseAmount * 1000; } // ── Staking Actions ────────────────────────────────────────────────── function _runStakingAction(uint256 runIndex, uint256 tradeIndex) internal { // If we have active positions, 50/50 stake vs unstake; otherwise always stake bool doStake = numActivePositions == 0 || uint256(keccak256(abi.encodePacked(bSeed, runIndex, tradeIndex, "stakeOrUnstake"))) % 2 == 0; if (doStake) { _tryStake(runIndex, tradeIndex); } else { _tryUnstake(runIndex, tradeIndex); } } function _tryStake(uint256 runIndex, uint256 tradeIndex) internal { uint256 krkBalance = kraiken.balanceOf(trader); if (krkBalance == 0 || numActivePositions >= 10) { _recordState("STAKE_SKIP", 0); return; } // Stake 5-20% of KRK holdings uint256 pct = 5 + (uint256(keccak256(abi.encodePacked(bSeed, runIndex, tradeIndex, "stakePct"))) % 16); uint256 stakeAmount = krkBalance * pct / 100; // Check minimum stake try kraiken.minStake() returns (uint256 minStake) { if (stakeAmount < minStake) stakeAmount = minStake; if (stakeAmount > krkBalance) { _recordState("STAKE_SKIP", 0); return; } } catch { _recordState("STAKE_SKIP", 0); return; } // Random tax rate index (0-29), bias toward low rates uint32 taxRate = uint32(uint256(keccak256(abi.encodePacked(bSeed, runIndex, tradeIndex, "taxRate"))) % 10); vm.startPrank(trader); kraiken.approve(address(stake), stakeAmount); uint256[] memory empty = new uint256[](0); try stake.snatch(stakeAmount, trader, taxRate, empty) returns (uint256 positionId) { vm.stopPrank(); activePositions[numActivePositions] = positionId; numActivePositions++; _recordState("STAKE", stakeAmount); } catch { vm.stopPrank(); _recordState("STAKE_FAIL", stakeAmount); } } function _tryUnstake(uint256 runIndex, uint256 tradeIndex) internal { if (numActivePositions == 0) return; // Pick a random position to exit uint256 idx = uint256(keccak256(abi.encodePacked(bSeed, runIndex, tradeIndex, "unstakeIdx"))) % numActivePositions; uint256 positionId = activePositions[idx]; // Advance time so we pass the 3-day tax floor vm.warp(block.timestamp + 4 days); vm.prank(trader); try stake.exitPosition(positionId) { // Remove from tracking by swapping with last activePositions[idx] = activePositions[numActivePositions - 1]; numActivePositions--; _recordState("UNSTAKE", positionId); } catch { _recordState("UNSTAKE_FAIL", positionId); } } function _exitAllPositions() internal { for (uint256 i = 0; i < numActivePositions; i++) { vm.warp(block.timestamp + 4 days); vm.prank(trader); try stake.exitPosition(activePositions[i]) { } catch { } } numActivePositions = 0; } function _generateScenarioId(uint256 seed) internal view returns (string memory) { uint256 rand = uint256(keccak256(abi.encodePacked(block.timestamp, block.prevrandao, seed))); bytes memory chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; bytes memory result = new bytes(4); for (uint256 i = 0; i < 4; i++) { result[i] = chars[rand % chars.length]; rand = rand / chars.length; } return string(result); } // ── Optimizer Deployment ───────────────────────────────────────────── function _deployOptimizer(string memory optimizerClass) internal returns (address) { if (keccak256(bytes(optimizerClass)) == keccak256(bytes("ConfigurableOptimizer"))) { uint256 ci = vm.envOr("CI_VALUE", uint256(0)); uint256 as_ = vm.envOr("AS_VALUE", uint256(100_000_000_000_000_000)); uint256 aw = vm.envOr("AW_VALUE", uint256(20)); uint256 dd = vm.envOr("DD_VALUE", uint256(500_000_000_000_000_000)); return address(new ConfigurableOptimizer(ci, as_, uint24(aw > 100 ? 100 : aw), dd)); } else if (keccak256(bytes(optimizerClass)) == keccak256(bytes("BullMarketOptimizer"))) { return address(new BullMarketOptimizer()); } else if (keccak256(bytes(optimizerClass)) == keccak256(bytes("BearMarketOptimizer"))) { return address(new BearMarketOptimizer()); } else if (keccak256(bytes(optimizerClass)) == keccak256(bytes("NeutralMarketOptimizer"))) { return address(new NeutralMarketOptimizer()); } else if (keccak256(bytes(optimizerClass)) == keccak256(bytes("WhaleOptimizer"))) { return address(new WhaleOptimizer()); } else if (keccak256(bytes(optimizerClass)) == keccak256(bytes("ExtremeOptimizer"))) { return address(new ExtremeOptimizer()); } else if (keccak256(bytes(optimizerClass)) == keccak256(bytes("MaliciousOptimizer"))) { return address(new MaliciousOptimizer()); } else if (keccak256(bytes(optimizerClass)) == keccak256(bytes("OptimizerV2"))) { // Deploy uninitialized — will be initialized after _setupEnvironment // when stake address is available return address(new OptimizerV2()); } else { return address(new BullMarketOptimizer()); } } // ── CSV Recording ───────────────────────────────────────────────────── function _recordState(string memory action, uint256 amount) internal { string memory row = _buildRowPart1(action, amount); row = string(abi.encodePacked(row, _buildRowPart2())); row = string(abi.encodePacked(row, _buildRowPart3())); vm.writeLine(csvFilename, row); } function _buildRowPart1(string memory action, uint256 amount) internal view returns (string memory) { (, int24 tick,,,,,) = pool.slot0(); (uint128 floorLiq, int24 floorLower, int24 floorUpper) = lm.positions(ThreePositionStrategy.Stage.FLOOR); return string( abi.encodePacked( action, ",", vm.toString(amount), ",", vm.toString(tick), ",", vm.toString(floorLower), ",", vm.toString(floorUpper), ",", vm.toString(uint256(floorLiq)), "," ) ); } function _buildRowPart2() internal view returns (string memory) { (uint128 anchorLiq, int24 anchorLower, int24 anchorUpper) = lm.positions(ThreePositionStrategy.Stage.ANCHOR); (uint128 discoveryLiq, int24 discoveryLower, int24 discoveryUpper) = lm.positions(ThreePositionStrategy.Stage.DISCOVERY); return string( abi.encodePacked( vm.toString(anchorLower), ",", vm.toString(anchorUpper), ",", vm.toString(uint256(anchorLiq)), ",", vm.toString(discoveryLower), ",", vm.toString(discoveryUpper), ",", vm.toString(uint256(discoveryLiq)), "," ) ); } function _buildRowPart3() internal view returns (string memory) { uint256 ethBalance = weth.balanceOf(trader); uint256 kraikenBalance = kraiken.balanceOf(trader); (uint128 fees0, uint128 fees1) = pool.protocolFees(); uint256 deltaFees0 = fees0 > totalFees0 ? fees0 - totalFees0 : 0; uint256 deltaFees1 = fees1 > totalFees1 ? fees1 - totalFees1 : 0; // Fee destination balances (LM fee revenue) (uint256 feeDestWeth, uint256 feeDestKrk) = _getFeeRevenue(); return string( abi.encodePacked( vm.toString(ethBalance), ",", vm.toString(kraikenBalance), ",", "0,", vm.toString(deltaFees0), ",", vm.toString(deltaFees1), ",", "0,", vm.toString(feeDestWeth), ",", vm.toString(feeDestKrk) ) ); } }