// SPDX-License-Identifier: MIT pragma solidity ^0.8.19; import { ThreePositionStrategy } from "../src/abstracts/ThreePositionStrategy.sol"; import { ConfigurableOptimizer } from "../test/mocks/ConfigurableOptimizer.sol"; import { FuzzingBase } from "./helpers/FuzzingBase.sol"; import "forge-std/console2.sol"; /// @title BullBearSweep /// @notice Measures LM resilience during bull->bear sentiment transitions. /// @dev For each parameter combo: runs a deterministic bull phase (buys + recenters), /// then a bear phase (sell all + recenters). Tracks enhanced metrics: /// - Trader PnL (proxy for LM ETH loss) /// - Bull retention (how much of trader ETH the LM absorbs during buys) /// - Bear drawdown (worst LM ETH drop during sell-off) /// - Sell attempts needed to liquidate (liquidity depth indicator) contract BullBearSweep is FuzzingBase { ConfigurableOptimizer optimizer; string summaryFile; // Scenario config uint256 cfgBullBuys; uint256 cfgBuySize; uint256 cfgLmFunding; struct ComboResult { uint256 lmEthStart; uint256 lmEthAfterBull; uint256 lmEthEnd; uint256 lmEthTrough; // lowest LM ETH during bear sell-off uint256 actualSpent; int256 traderPnl; uint256 retentionPct; uint256 drawdownBps; // (peak - trough) / peak * 10000 uint256 sellAttempts; // how many sell rounds needed to liquidate int256 lmNetGain; // lmEthEnd - lmEthStart bool floorMoved; uint256 feeWethBull; // WETH fees accumulated during bull phase uint256 feeKrkBull; // KRK fees accumulated during bull phase uint256 feeWethTotal; // WETH fees accumulated during full cycle uint256 feeKrkTotal; // KRK fees accumulated during full cycle } function run() public { cfgBullBuys = vm.envOr("BULL_BUYS", uint256(10)); cfgBuySize = vm.envOr("BUY_SIZE_ETH", uint256(15)); cfgLmFunding = vm.envOr("LM_FUNDING_ETH", uint256(200)); string memory tag = vm.envOr("SWEEP_TAG", string("BULLBEAR")); uint256[] memory ciValues = _parseUintArray(vm.envOr("CI_VALUES", string("0,300000000000000000,500000000000000000,800000000000000000,1000000000000000000"))); uint256[] memory asValues = _parseUintArray(vm.envOr("AS_VALUES", string("100000000000000000,300000000000000000,500000000000000000,700000000000000000,1000000000000000000"))); uint256[] memory awValues = _parseUintArray(vm.envOr("AW_VALUES", string("20,50,80"))); uint256[] memory ddValues = _parseUintArray(vm.envOr("DD_VALUES", string("200000000000000000,500000000000000000,1000000000000000000"))); uint256 totalCombos = ciValues.length * asValues.length * awValues.length * ddValues.length; console2.log("=== Bull-Bear Sweep ==="); console2.log("Combinations:", totalCombos); console2.log("Bull buys:", cfgBullBuys); console2.log("Buy size (ETH):", cfgBuySize); console2.log("LM funding (ETH):", cfgLmFunding); _initInfrastructure(); summaryFile = string(abi.encodePacked("analysis/sweep-", tag, "-summary.csv")); vm.writeFile( summaryFile, "ci,anchor_share,anchor_width,discovery_depth,trader_pnl,bull_spent,lm_eth_start,lm_eth_after_bull,lm_eth_end,lm_retention_pct,floor_moved,lm_eth_trough,drawdown_bps,sell_attempts,lm_net_gain,fee_weth_bull,fee_krk_bull,fee_weth_total,fee_krk_total\n" ); uint256 comboIndex; 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++) { comboIndex++; _runCombo(ciValues[ci_i], asValues[as_i], uint24(awValues[aw_i] > 100 ? 100 : awValues[aw_i]), ddValues[dd_i], comboIndex); } } } } console2.log("=== Sweep Complete ==="); console2.log("Combos:", comboIndex); console2.log("Results:", summaryFile); } function _runCombo(uint256 ci, uint256 as_, uint24 aw, uint256 dd, uint256 comboIdx) internal { optimizer = new ConfigurableOptimizer(ci, as_, aw, dd); _setupEnvironment(address(optimizer), false, true, cfgLmFunding * 1 ether); ComboResult memory r; r.lmEthStart = _getLmSystemEth(); // Record initial floor position (, int24 floorTickInit,) = lm.positions(ThreePositionStrategy.Stage.FLOOR); // Fund trader uint256 totalBullEth = cfgBullBuys * cfgBuySize * 1 ether; vm.deal(trader, totalBullEth); vm.prank(trader); weth.deposit{ value: totalBullEth }(); uint256 traderInit = weth.balanceOf(trader); // === BULL PHASE: deterministic buys with periodic recenters === for (uint256 i = 0; i < cfgBullBuys; i++) { uint256 buyAmt = cfgBuySize * 1 ether; if (weth.balanceOf(trader) < buyAmt) break; vm.startPrank(trader); weth.transfer(address(swapExecutor), buyAmt); vm.stopPrank(); try swapExecutor.executeBuy(buyAmt, trader) { r.actualSpent += buyAmt; } catch { } _recoverStuckTokens(); if ((i + 1) % LIQUIDATION_RECENTER_INTERVAL == 0) { _tryRecenter(); } } _tryRecenter(); r.lmEthAfterBull = _getLmSystemEth(); r.feeWethBull = weth.balanceOf(fees); r.feeKrkBull = kraiken.balanceOf(fees); // === BEAR PHASE: sell everything with drawdown tracking === r.lmEthTrough = r.lmEthAfterBull; _tryRecenter(); // Instrumented liquidation: sell in chunks, track trough at each step uint256 remaining = kraiken.balanceOf(trader); uint256 attempts; while (remaining > 0 && attempts < LIQUIDATION_MAX_ATTEMPTS) { uint256 prev = remaining; uint256 wethBefore = weth.balanceOf(trader); vm.startPrank(trader); kraiken.transfer(address(swapExecutor), remaining); vm.stopPrank(); try swapExecutor.executeSell(remaining, trader) { if (weth.balanceOf(trader) <= wethBefore) { _recoverStuckTokens(); break; } } catch { _recoverStuckTokens(); break; } _recoverStuckTokens(); attempts++; // Measure LM ETH after this sell — track trough uint256 lmNow = _getLmSystemEth(); if (lmNow < r.lmEthTrough) { r.lmEthTrough = lmNow; } if (attempts % LIQUIDATION_RECENTER_INTERVAL == 0) { _tryRecenter(); // Re-measure after recenter lmNow = _getLmSystemEth(); if (lmNow < r.lmEthTrough) { r.lmEthTrough = lmNow; } } remaining = kraiken.balanceOf(trader); if (remaining >= prev) break; } _recoverStuckTokens(); r.sellAttempts = attempts; r.lmEthEnd = _getLmSystemEth(); r.feeWethTotal = weth.balanceOf(fees); r.feeKrkTotal = kraiken.balanceOf(fees); r.traderPnl = int256(weth.balanceOf(trader)) - int256(traderInit); r.lmNetGain = int256(r.lmEthEnd) - int256(r.lmEthStart); (, int24 floorTickEnd,) = lm.positions(ThreePositionStrategy.Stage.FLOOR); r.floorMoved = (floorTickEnd != floorTickInit); if (r.lmEthAfterBull > 0) { r.retentionPct = r.lmEthEnd * 10_000 / r.lmEthAfterBull; r.drawdownBps = (r.lmEthAfterBull - r.lmEthTrough) * 10_000 / r.lmEthAfterBull; } _writeRow(ci, as_, aw, dd, r); if (comboIdx % 10 == 0) { console2.log("Progress:", comboIdx); console2.log(" traderPnl:", r.traderPnl); console2.log(" drawdown:", r.drawdownBps); } } function _writeRow(uint256 ci, uint256 as_, uint24 aw, uint256 dd, ComboResult memory r) internal { 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(r.traderPnl), ",", vm.toString(r.actualSpent), ",")); row = string(abi.encodePacked(row, vm.toString(r.lmEthStart), ",", vm.toString(r.lmEthAfterBull), ",")); row = string(abi.encodePacked(row, vm.toString(r.lmEthEnd), ",", vm.toString(r.retentionPct), ",", r.floorMoved ? "true" : "false", ",")); row = string(abi.encodePacked(row, vm.toString(r.lmEthTrough), ",", vm.toString(r.drawdownBps), ",")); row = string(abi.encodePacked(row, vm.toString(r.sellAttempts), ",", vm.toString(r.lmNetGain), ",")); row = string(abi.encodePacked(row, vm.toString(r.feeWethBull), ",", vm.toString(r.feeKrkBull), ",")); row = string(abi.encodePacked(row, vm.toString(r.feeWethTotal), ",", vm.toString(r.feeKrkTotal))); vm.writeLine(summaryFile, row); } }