// SPDX-License-Identifier: MIT pragma solidity ^0.8.19; import "forge-std/Script.sol"; import "forge-std/console2.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 {UniswapHelpers} from "../src/helpers/UniswapHelpers.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 {SwapExecutor} from "./helpers/SwapExecutor.sol"; import {Optimizer} from "../src/Optimizer.sol"; import {BullMarketOptimizer} from "../test/mocks/BullMarketOptimizer.sol"; import {BearMarketOptimizer} from "../test/mocks/BearMarketOptimizer.sol"; import {NeutralMarketOptimizer} from "../test/mocks/NeutralMarketOptimizer.sol"; import {WhaleOptimizer} from "../test/mocks/WhaleOptimizer.sol"; import {ExtremeOptimizer} from "../test/mocks/ExtremeOptimizer.sol"; import {MaliciousOptimizer} from "../test/mocks/MaliciousOptimizer.sol"; contract StreamlinedFuzzing is Script { // Test environment TestEnvironment testEnv; IUniswapV3Factory factory; IUniswapV3Pool pool; IWETH9 weth; Kraiken kraiken; Stake stake; LiquidityManager lm; SwapExecutor swapExecutor; bool token0isWeth; // Actors address trader = makeAddr("trader"); address fees = makeAddr("fees"); // Staking tracking mapping(address => uint256[]) public activePositions; uint256[] public allPositionIds; uint256 totalStakesAttempted; uint256 totalStakesSucceeded; uint256 totalSnatchesAttempted; uint256 totalSnatchesSucceeded; // CSV filename for current run string csvFilename; // Track cumulative fees uint256 totalFees0; uint256 totalFees1; // Track recentering uint256 lastRecenterBlock; function run() public { // Get configuration from environment uint256 numRuns = vm.envOr("FUZZING_RUNS", uint256(20)); uint256 tradesPerRun = vm.envOr("TRADES_PER_RUN", uint256(15)); bool enableStaking = vm.envOr("ENABLE_STAKING", true); uint256 buyBias = vm.envOr("BUY_BIAS", uint256(50)); uint256 stakingBias = vm.envOr("STAKING_BIAS", uint256(80)); string memory optimizerClass = vm.envOr("OPTIMIZER_CLASS", string("BullMarketOptimizer")); console2.log("=== Streamlined Fuzzing Analysis ==="); console2.log("Optimizer:", optimizerClass); // Deploy factory once for all runs (gas optimization) testEnv = new TestEnvironment(fees); factory = UniswapHelpers.deployUniswapFactory(); // Generate unique 4-character scenario ID string memory scenarioCode = _generateScenarioId(); // Run fuzzing scenarios for (uint256 runIndex = 0; runIndex < numRuns; runIndex++) { string memory runId = string(abi.encodePacked(scenarioCode, "-", _padNumber(runIndex, 3))); console2.log("\nRun:", runId); // Initialize CSV file for this run // Always write to analysis directory relative to project root 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\n"; vm.writeFile(csvFilename, header); // Setup fresh environment for each run _setupEnvironment(optimizerClass, runIndex % 2 == 0); // Reset tracking variables totalFees0 = 0; totalFees1 = 0; lastRecenterBlock = block.number; // Fund trader based on run seed - increased for longer campaigns uint256 traderFund = 500 ether + (uint256(keccak256(abi.encodePacked(runIndex, "trader"))) % 500 ether); vm.deal(trader, traderFund * 2); vm.prank(trader); weth.deposit{value: traderFund}(); // Initial state _recordState("INIT", 0); // Execute trades for (uint256 i = 0; i < tradesPerRun; i++) { // Check for recenter opportunity on average every 3 trades uint256 recenterRand = uint256(keccak256(abi.encodePacked(runIndex, i, "recenter"))) % 3; if (recenterRand == 0) { _tryRecenter(); } // Determine trade based on bias uint256 rand = uint256(keccak256(abi.encodePacked(runIndex, i))) % 100; if (rand < buyBias) { _executeBuy(runIndex, i); } else { _executeSell(runIndex, i); } // Staking operations if enabled if (enableStaking && i % 3 == 0) { _executeStakingOperation(runIndex, i, stakingBias); } } // Final state _liquidateTraderHoldings(); _recordState("FINAL", 0); } console2.log("\n=== Analysis Complete ==="); console2.log("Generated", numRuns, "CSV files with prefix:", scenarioCode); } function _setupEnvironment(string memory optimizerClass, bool wethIsToken0) internal { // Get optimizer address address optimizer = _deployOptimizer(optimizerClass); // Setup new environment (factory, pool, weth, kraiken, stake, lm,, token0isWeth) = testEnv.setupEnvironmentWithExistingFactory(factory, wethIsToken0, fees, optimizer); // Deploy swap executor with liquidity boundary checks swapExecutor = new SwapExecutor(pool, weth, kraiken, token0isWeth, lm); // Fund liquidity manager vm.deal(address(lm), 200 ether); // Initialize liquidity positions // First need to give LM some WETH vm.prank(address(lm)); weth.deposit{value: 100 ether}(); // Now try recenter from fee destination vm.prank(fees); try lm.recenter() returns (bool isUp) { console2.log("Initial recenter successful, isUp:", isUp); } catch Error(string memory reason) { console2.log("Initial recenter failed:", reason); } catch { console2.log("Initial recenter failed with unknown error"); } // Clear staking state delete allPositionIds; totalStakesAttempted = 0; totalStakesSucceeded = 0; totalSnatchesAttempted = 0; totalSnatchesSucceeded = 0; } function _deployOptimizer(string memory optimizerClass) internal returns (address) { 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 { // Default to bull market return address(new BullMarketOptimizer()); } } function _executeBuy(uint256 runIndex, uint256 tradeIndex) internal { uint256 amount = _getTradeAmount(runIndex, tradeIndex, true); if (amount == 0 || weth.balanceOf(trader) < amount) return; vm.startPrank(trader); weth.transfer(address(swapExecutor), amount); try swapExecutor.executeBuy(amount, trader) returns (uint256 actualAmount) { if (actualAmount == 0) { console2.log("Buy returned 0, requested:", amount); } _recordState("BUY", actualAmount); } catch Error(string memory reason) { console2.log("Buy failed:", reason); _recordState("BUY_FAIL", amount); } vm.stopPrank(); } function _executeSell(uint256 runIndex, uint256 tradeIndex) internal { uint256 amount = _getTradeAmount(runIndex, tradeIndex, false); if (amount == 0 || kraiken.balanceOf(trader) < amount) return; vm.startPrank(trader); kraiken.transfer(address(swapExecutor), amount); try swapExecutor.executeSell(amount, trader) returns (uint256 actualAmount) { _recordState("SELL", actualAmount); } catch { _recordState("SELL_FAIL", amount); } vm.stopPrank(); } function _executeStakingOperation(uint256, uint256, uint256) internal { // Staking operations disabled for now - interface needs updating // TODO: Update to use correct Stake contract interface } function _tryRecenter() internal { vm.warp(block.timestamp + 1 hours); vm.roll(block.number + 1); // Advance block vm.prank(fees); try lm.recenter{gas: 50_000_000}() { lastRecenterBlock = block.number; _recordState("RECENTER", 0); } catch {} } function _getTradeAmount(uint256 runIndex, uint256 tradeIndex, bool isBuy) internal pure returns (uint256) { uint256 baseAmount = 10 ether + (uint256(keccak256(abi.encodePacked(runIndex, tradeIndex))) % 90 ether); return isBuy ? baseAmount : baseAmount * 1000; } function _liquidateTraderHoldings() internal { uint256 remaining = kraiken.balanceOf(trader); uint256 attempts; // Repeatedly sell down inventory, respecting liquidity limits in SwapExecutor while (remaining > 0 && attempts < 10) { uint256 prevRemaining = remaining; vm.startPrank(trader); kraiken.transfer(address(swapExecutor), remaining); try swapExecutor.executeSell(remaining, trader) returns (uint256 actualAmount) { if (actualAmount == 0) { vm.stopPrank(); console2.log("Liquidity liquidation halted: sell returned 0"); break; } } catch Error(string memory reason) { vm.stopPrank(); console2.log("Liquidity liquidation failed:", reason); break; } catch { vm.stopPrank(); console2.log("Liquidity liquidation failed with unknown error"); break; } vm.stopPrank(); remaining = kraiken.balanceOf(trader); if (remaining >= prevRemaining) { console2.log("Liquidity liquidation made no progress; remaining KRAIKEN:", remaining); break; } unchecked { attempts++; } } if (kraiken.balanceOf(trader) > 0) { console2.log("Warning: trader still holds KRAIKEN after liquidation:", kraiken.balanceOf(trader)); } } function _recordState(string memory action, uint256 amount) internal { // Build CSV row in parts to avoid stack too deep 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(); // Get floor position (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) { // Get anchor and discovery positions (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) { // Get balances and fees 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; return string(abi.encodePacked( vm.toString(ethBalance), ",", vm.toString(kraikenBalance), ",", "0,", // vwap placeholder vm.toString(deltaFees0), ",", vm.toString(deltaFees1), ",", "0" // recenter flag placeholder - no newline here )); } function _generateScenarioId() internal view returns (string memory) { uint256 rand = uint256(keccak256(abi.encodePacked(block.timestamp, block.prevrandao))); 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); } function _padNumber(uint256 num, uint256 digits) internal pure returns (string memory) { string memory numStr = vm.toString(num); bytes memory numBytes = bytes(numStr); if (numBytes.length >= digits) { return numStr; } bytes memory result = new bytes(digits); uint256 padding = digits - numBytes.length; for (uint256 i = 0; i < padding; i++) { result[i] = '0'; } for (uint256 i = 0; i < numBytes.length; i++) { result[padding + i] = numBytes[i]; } return string(result); } }