// SPDX-License-Identifier: MIT pragma solidity ^0.8.19; import { ConfigurableOptimizer } from "../test/mocks/ConfigurableOptimizer.sol"; import { FuzzingBase } from "./helpers/FuzzingBase.sol"; import "forge-std/console2.sol"; /// @title ParameterSweepFuzzing /// @notice Runs multiple parameter combinations in a single forge execution. /// @dev Key design: environment is deployed ONCE per combo and reused across runs /// so that VWAP accumulates naturally across recenters. /// Swaps are uncapped by default (no LiquidityBoundaryHelper limits). /// LM ETH is tracked as direct balance + pool WETH (since LM is sole LP, /// pool WETH represents LM's deployed position ETH). contract ParameterSweepFuzzing is FuzzingBase { ConfigurableOptimizer optimizer; string summaryFile; uint256 cfgBuyBias; uint256 cfgTradesPerRun; uint256 cfgRunsPerCombo; uint256 cfgMinBuy; uint256 cfgMaxBuy; bool cfgUncapped; struct RunResult { uint256 lmEthInit; uint256 lmEthFinal; uint256 traderInitEth; uint256 traderFinalEth; uint256 numRecenters; uint256 numBuysFailed; } function run() public { cfgTradesPerRun = vm.envOr("TRADES_PER_RUN", uint256(30)); cfgRunsPerCombo = vm.envOr("RUNS_PER_COMBO", uint256(5)); cfgUncapped = vm.envOr("UNCAPPED_SWAPS", true); cfgMinBuy = vm.envOr("MIN_BUY_ETH", uint256(20)); cfgMaxBuy = vm.envOr("MAX_BUY_ETH", uint256(80)); string memory tag = vm.envOr("SWEEP_TAG", string("SWEEP")); uint256[] memory ciValues = _parseUintArray(vm.envOr("CI_VALUES", string("0,500000000000000000,1000000000000000000"))); uint256[] memory asValues = _parseUintArray(vm.envOr("AS_VALUES", string("100000000000000000,500000000000000000,1000000000000000000"))); uint256[] memory awValues = _parseUintArray(vm.envOr("AW_VALUES", string("30,50,80"))); uint256[] memory ddValues = _parseUintArray(vm.envOr("DD_VALUES", string("200000000000000000,1000000000000000000"))); uint256[] memory bbValues = _parseUintArray(vm.envOr("BB_VALUES", string("60,80,100"))); uint256 totalCombos = ciValues.length * asValues.length * awValues.length * ddValues.length * bbValues.length; console2.log("=== Parameter Sweep ==="); console2.log("Combinations:", totalCombos); console2.log("Runs/combo:", cfgRunsPerCombo); console2.log("Trades/run:", cfgTradesPerRun); console2.log("Uncapped:", cfgUncapped); console2.log("Trade range (ETH):", cfgMinBuy, cfgMaxBuy); _initInfrastructure(); summaryFile = string(abi.encodePacked("analysis/sweep-", tag, "-summary.csv")); vm.writeFile(summaryFile, "ci,anchor_share,anchor_width,discovery_depth,buy_bias,worst_trader_pnl,best_trader_pnl,any_lm_loss,lm_eth_delta\n"); uint256 comboIndex = 0; for (uint256 ci_i = 0; ci_i < ciValues.length; ci_i++) { for (uint256 as_i = 0; as_i < asValues.length; as_i++) { for (uint256 aw_i = 0; aw_i < awValues.length; aw_i++) { for (uint256 dd_i = 0; dd_i < ddValues.length; dd_i++) { for (uint256 bb_i = 0; bb_i < bbValues.length; bb_i++) { comboIndex++; _runCombo( ciValues[ci_i], asValues[as_i], uint24(awValues[aw_i] > 100 ? 100 : awValues[aw_i]), ddValues[dd_i], bbValues[bb_i], comboIndex ); } } } } } console2.log("=== Sweep Complete ==="); console2.log("Results:", summaryFile); } function _runCombo(uint256 ci, uint256 as_, uint24 aw, uint256 dd, uint256 bb, uint256 comboIdx) internal { optimizer = new ConfigurableOptimizer(ci, as_, aw, dd); cfgBuyBias = bb; // Setup environment once — VWAP accumulates across all runs _setupEnvironment(address(optimizer), comboIdx % 2 == 0, cfgUncapped); int256 worstPnl = type(int256).min; int256 bestPnl = type(int256).max; bool anyLoss = false; uint256 lmSystemStart = _getLmSystemEth(); for (uint256 runIdx = 0; runIdx < cfgRunsPerCombo; runIdx++) { RunResult memory result = _executeRun(runIdx); int256 pnl = int256(result.traderFinalEth) - int256(result.traderInitEth); if (pnl > worstPnl) worstPnl = pnl; if (pnl < bestPnl) bestPnl = pnl; if (pnl > 0) anyLoss = true; } uint256 lmSystemEnd = _getLmSystemEth(); int256 lmDelta = int256(lmSystemEnd) - int256(lmSystemStart); // Write summary row string memory row = string(abi.encodePacked(vm.toString(ci), ",", vm.toString(as_), ",", vm.toString(uint256(aw)), ",")); row = string(abi.encodePacked(row, vm.toString(dd), ",", vm.toString(bb), ",", vm.toString(worstPnl), ",")); row = string(abi.encodePacked(row, vm.toString(bestPnl), ",", anyLoss ? "true" : "false", ",", vm.toString(lmDelta))); vm.writeLine(summaryFile, row); if (anyLoss) { console2.log("UNSAFE combo", comboIdx); console2.log(" worstPnl:", worstPnl); console2.log(" lmDelta:", lmDelta); } else if (comboIdx % 5 == 0) { console2.log("Progress:", comboIdx); } } function _executeRun(uint256 runIndex) internal returns (RunResult memory result) { // DON'T re-setup environment — reuse existing so VWAP accumulates result.lmEthInit = _getLmSystemEth(); // Clean up leftover tokens from previous run to prevent PnL leakage _cleanupTraderTokens(); // Fund trader fresh each run vm.deal(trader, LM_FUNDING_ETH); vm.prank(trader); weth.deposit{ value: LM_FUNDING_ETH }(); result.traderInitEth = weth.balanceOf(trader); for (uint256 i = 0; i < cfgTradesPerRun; i++) { if (uint256(keccak256(abi.encodePacked(runIndex, i, "recenter"))) % 3 == 0) { if (_tryRecenter()) result.numRecenters++; } uint256 rand = uint256(keccak256(abi.encodePacked(runIndex, i))) % 100; if (rand < cfgBuyBias) { uint256 range = cfgMaxBuy - cfgMinBuy; uint256 amount = (cfgMinBuy * 1 ether) + (uint256(keccak256(abi.encodePacked(runIndex, i, "buy"))) % (range * 1 ether)); if (!_executeBuy(amount)) result.numBuysFailed++; } else { uint256 krkBal = kraiken.balanceOf(trader); if (krkBal > 0) { uint256 pct = SELL_PCT_MIN + (uint256(keccak256(abi.encodePacked(runIndex, i, "sell"))) % SELL_PCT_RANGE); uint256 amount = krkBal * pct / 100; _executeSell(amount); } } } _tryRecenter(); _liquidateTraderHoldings(); _recoverStuckTokens(); result.lmEthFinal = _getLmSystemEth(); result.traderFinalEth = weth.balanceOf(trader); } }