// SPDX-License-Identifier: MIT pragma solidity ^0.8.19; import { Kraiken } from "../../src/Kraiken.sol"; import { LiquidityManager } from "../../src/LiquidityManager.sol"; import { Stake } from "../../src/Stake.sol"; import { ThreePositionStrategy } from "../../src/abstracts/ThreePositionStrategy.sol"; import { UniswapHelpers } from "../../src/helpers/UniswapHelpers.sol"; import { IWETH9 } from "../../src/interfaces/IWETH9.sol"; import { TestEnvironment } from "../../test/helpers/TestBase.sol"; import { BackgroundLP } from "./BackgroundLP.sol"; import { SwapExecutor } from "./SwapExecutor.sol"; import { IUniswapV3Factory } from "@uniswap/v3-core/contracts/interfaces/IUniswapV3Factory.sol"; import { IUniswapV3Pool } from "@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol"; import "forge-std/Script.sol"; import "forge-std/console2.sol"; /// @title FuzzingBase /// @notice Shared infrastructure for all fuzzing/analysis scripts. /// @dev Provides environment setup, trade execution, liquidation, token recovery, /// and CSV parsing. Concrete scripts inherit this and implement their own run() logic. abstract contract FuzzingBase is Script { // ── Constants ────────────────────────────────────────────────────────── uint256 internal constant LM_FUNDING_ETH = 200 ether; uint256 internal constant LM_INITIAL_WETH = 100 ether; uint256 internal constant RECENTER_GAS_LIMIT = 50_000_000; uint256 internal constant RECENTER_TIME_ADVANCE = 1 hours; uint256 internal constant LIQUIDATION_MAX_ATTEMPTS = 20; uint256 internal constant LIQUIDATION_RECENTER_INTERVAL = 3; address internal constant DEAD_ADDRESS = address(0xdead); uint256 internal constant SELL_PCT_MIN = 20; uint256 internal constant SELL_PCT_RANGE = 60; // ── Environment ─────────────────────────────────────────────────────── TestEnvironment internal testEnv; IUniswapV3Factory internal factory; IUniswapV3Pool internal pool; IWETH9 internal weth; Kraiken internal kraiken; Stake internal stake; LiquidityManager internal lm; SwapExecutor internal swapExecutor; bool internal token0isWeth; // ── Actors ──────────────────────────────────────────────────────────── address internal trader = makeAddr("trader"); address internal fees = makeAddr("fees"); BackgroundLP internal backgroundLP; // ══════════════════════════════════════════════════════════════════════ // Environment Setup // ══════════════════════════════════════════════════════════════════════ /// @notice Deploy TestEnvironment and Uniswap factory (call once per script) function _initInfrastructure() internal { testEnv = new TestEnvironment(fees); factory = UniswapHelpers.deployUniswapFactory(); } /// @notice Deploy a fresh pool/LM/optimizer environment /// @param optimizer Address of the optimizer contract to use /// @param wethIsToken0 Token ordering for the pool /// @param uncapped Whether to deploy SwapExecutor in uncapped mode function _setupEnvironment(address optimizer, bool wethIsToken0, bool uncapped) internal { (factory, pool, weth, kraiken, stake, lm,, token0isWeth) = testEnv.setupEnvironmentWithExistingFactory(factory, wethIsToken0, fees, optimizer); swapExecutor = new SwapExecutor(pool, weth, kraiken, token0isWeth, lm, uncapped); vm.deal(address(lm), LM_FUNDING_ETH); vm.prank(address(lm)); weth.deposit{ value: LM_INITIAL_WETH }(); vm.prank(fees); try lm.recenter() { } catch { } } /// @notice Overload: setup with custom LM funding function _setupEnvironment(address optimizer, bool wethIsToken0, bool uncapped, uint256 lmFundingEth) internal { (factory, pool, weth, kraiken, stake, lm,, token0isWeth) = testEnv.setupEnvironmentWithExistingFactory(factory, wethIsToken0, fees, optimizer); swapExecutor = new SwapExecutor(pool, weth, kraiken, token0isWeth, lm, uncapped); vm.deal(address(lm), lmFundingEth); vm.prank(address(lm)); weth.deposit{ value: lmFundingEth / 2 }(); vm.prank(fees); try lm.recenter() { } catch { } } // ══════════════════════════════════════════════════════════════════════ // Background Liquidity (Gaussian competition) // ══════════════════════════════════════════════════════════════════════ /// @notice Deploy background LP with Gaussian-approximating stacked positions. /// KRK is acquired by buying from the pool (realistic — no free minting). /// @param ethPerLayer ETH per layer (5 layers total). Half goes to buying KRK, /// half stays as WETH for the liquidity positions. function _deployBackgroundLP(uint256 ethPerLayer) internal { backgroundLP = new BackgroundLP(pool, weth, kraiken, token0isWeth); // Fund with ETH (2× total: half to buy KRK, half for LP positions) uint256 totalEth = ethPerLayer * 5; vm.deal(address(backgroundLP), totalEth * 4); vm.prank(address(backgroundLP)); weth.deposit{ value: totalEth * 2 }(); // Buy KRK from the pool — just like a real LP would. // Buy in small chunks with recenters to avoid overshooting thin anchors. address bgAddr = address(backgroundLP); uint256 chunkSize = totalEth / 5; for (uint256 i = 0; i < 5; i++) { uint256 buyAmount = chunkSize; uint256 wethBal = weth.balanceOf(bgAddr); if (buyAmount > wethBal / 2) buyAmount = wethBal / 2; // keep some WETH for LP if (buyAmount == 0) break; vm.startPrank(bgAddr); weth.transfer(address(swapExecutor), buyAmount); vm.stopPrank(); vm.prank(bgAddr); try swapExecutor.executeBuy(buyAmount, bgAddr) { } catch { } // Recenter between chunks to rebuild positions vm.warp(block.timestamp + 1 hours); vm.roll(block.number + 1); vm.prank(fees); try lm.recenter() { } catch { } } backgroundLP.rebalance(ethPerLayer); } /// @notice Rebalance background LP positions to track current tick. /// Uses existing token balances — no new ETH or KRK injected. function _rebalanceBackgroundLP(uint256 ethPerLayer) internal { if (address(backgroundLP) == address(0)) return; backgroundLP.rebalance(ethPerLayer); } /// @notice Get fee revenue collected by feeDestination /// @return feeWeth WETH fees, feeKrk KRK fees function _getFeeRevenue() internal view returns (uint256 feeWeth, uint256 feeKrk) { feeWeth = weth.balanceOf(fees); feeKrk = kraiken.balanceOf(fees); } // ══════════════════════════════════════════════════════════════════════ // Staking Setup // ══════════════════════════════════════════════════════════════════════ /// @notice Pre-seed staking to simulate a given sentiment level. /// Buys KRK from the pool and stakes it at the given tax rate. /// @param stakingPct Target % of authorized stake to fill (0-100, in whole %) /// @param taxRateIdx Tax rate index (0-29, lower = cheaper = more bullish) function _seedStaking(uint256 stakingPct, uint32 taxRateIdx) internal { if (stakingPct == 0) return; address staker = makeAddr("staker"); // Buy KRK for staking — use 50 ETH per attempt, loop until target met uint256 maxAttempts = 20; for (uint256 attempt = 0; attempt < maxAttempts; attempt++) { // Check current staking level uint256 currentPct = stake.getPercentageStaked() * 100 / 1e18; if (currentPct >= stakingPct) break; // Fund staker with ETH vm.deal(staker, 100 ether); vm.prank(staker); weth.deposit{ value: 50 ether }(); // Buy KRK vm.startPrank(staker); weth.transfer(address(swapExecutor), 50 ether); vm.stopPrank(); vm.prank(staker); try swapExecutor.executeBuy(50 ether, staker) { } catch { break; } // Recenter to rebuild positions vm.warp(block.timestamp + 1 hours); vm.roll(block.number + 1); vm.prank(fees); try lm.recenter() { } catch { } // Stake all KRK uint256 krkBal = kraiken.balanceOf(staker); if (krkBal == 0) break; vm.startPrank(staker); kraiken.approve(address(stake), krkBal); uint256[] memory empty = new uint256[](0); try stake.snatch(krkBal, staker, taxRateIdx, empty) { } catch { vm.stopPrank(); break; } vm.stopPrank(); } // Final recenter to pick up new optimizer params vm.warp(block.timestamp + 1 hours); vm.roll(block.number + 1); vm.prank(fees); try lm.recenter() { } catch { } } // ══════════════════════════════════════════════════════════════════════ // Trade Execution // ══════════════════════════════════════════════════════════════════════ /// @notice Execute a buy trade via the SwapExecutor /// @return success Whether the buy produced a non-zero result function _executeBuy(uint256 amount) internal returns (bool success) { if (amount == 0 || weth.balanceOf(trader) < amount) return false; vm.startPrank(trader); weth.transfer(address(swapExecutor), amount); try swapExecutor.executeBuy(amount, trader) returns (uint256 actualAmount) { vm.stopPrank(); _recoverStuckTokens(); return actualAmount > 0; } catch { vm.stopPrank(); _recoverStuckTokens(); return false; } } /// @notice Execute a sell trade via the SwapExecutor /// @return success Whether the sell produced a non-zero result function _executeSell(uint256 amount) internal returns (bool success) { if (amount == 0 || kraiken.balanceOf(trader) < amount) return false; vm.startPrank(trader); kraiken.transfer(address(swapExecutor), amount); try swapExecutor.executeSell(amount, trader) returns (uint256 actualAmount) { vm.stopPrank(); _recoverStuckTokens(); return actualAmount > 0; } catch { vm.stopPrank(); _recoverStuckTokens(); return false; } } // ══════════════════════════════════════════════════════════════════════ // Recenter // ══════════════════════════════════════════════════════════════════════ /// @notice Advance time and attempt a recenter function _tryRecenter() internal returns (bool) { vm.warp(block.timestamp + RECENTER_TIME_ADVANCE); vm.roll(block.number + 1); vm.prank(fees); try lm.recenter{ gas: RECENTER_GAS_LIMIT }() { return true; } catch { return false; } } // ══════════════════════════════════════════════════════════════════════ // Liquidation // ══════════════════════════════════════════════════════════════════════ /// @notice Sell all trader's KRAIKEN, retrying with recenters between attempts function _liquidateTraderHoldings() internal { 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(); if (attempts % LIQUIDATION_RECENTER_INTERVAL == LIQUIDATION_RECENTER_INTERVAL - 1) { _tryRecenter(); } remaining = kraiken.balanceOf(trader); if (remaining >= prev) break; unchecked { attempts++; } } _recoverStuckTokens(); } // ══════════════════════════════════════════════════════════════════════ // Token Recovery & Cleanup // ══════════════════════════════════════════════════════════════════════ /// @notice Recover any tokens stuck in SwapExecutor back to the trader function _recoverStuckTokens() internal { uint256 stuckKraiken = kraiken.balanceOf(address(swapExecutor)); if (stuckKraiken > 0) { vm.prank(address(swapExecutor)); kraiken.transfer(trader, stuckKraiken); } uint256 stuckWeth = weth.balanceOf(address(swapExecutor)); if (stuckWeth > 0) { vm.prank(address(swapExecutor)); weth.transfer(trader, stuckWeth); } } /// @notice Burn all trader + SwapExecutor tokens to prevent PnL leakage between runs function _cleanupTraderTokens() internal { uint256 leftoverWeth = weth.balanceOf(trader); if (leftoverWeth > 0) { vm.prank(trader); weth.transfer(DEAD_ADDRESS, leftoverWeth); } uint256 leftoverKraiken = kraiken.balanceOf(trader); if (leftoverKraiken > 0) { vm.prank(trader); kraiken.transfer(DEAD_ADDRESS, leftoverKraiken); } uint256 stuckWeth = weth.balanceOf(address(swapExecutor)); if (stuckWeth > 0) { vm.prank(address(swapExecutor)); weth.transfer(DEAD_ADDRESS, stuckWeth); } uint256 stuckKraiken = kraiken.balanceOf(address(swapExecutor)); if (stuckKraiken > 0) { vm.prank(address(swapExecutor)); kraiken.transfer(DEAD_ADDRESS, stuckKraiken); } } // ══════════════════════════════════════════════════════════════════════ // LM ETH Measurement // ══════════════════════════════════════════════════════════════════════ /// @notice Approximate total ETH attributable to the LM (direct balance + pool WETH) /// @dev Pool WETH includes both LM position ETH and trader buy ETH in transit. /// This is a rough proxy — trader PnL is the reliable safety metric. function _getLmSystemEth() internal view returns (uint256) { return address(lm).balance + weth.balanceOf(address(lm)) + weth.balanceOf(address(pool)); } // ══════════════════════════════════════════════════════════════════════ // CSV Parsing Utilities // ══════════════════════════════════════════════════════════════════════ /// @notice Parse a comma-separated string of uint256 values function _parseUintArray(string memory csv) internal pure returns (uint256[] memory) { bytes memory b = bytes(csv); uint256 count = 1; for (uint256 i = 0; i < b.length; i++) { if (b[i] == ",") count++; } uint256[] memory result = new uint256[](count); uint256 idx; uint256 start; for (uint256 i = 0; i <= b.length; i++) { if (i == b.length || b[i] == ",") { uint256 num; for (uint256 j = start; j < i; j++) { require(b[j] >= "0" && b[j] <= "9", "Invalid number in CSV"); num = num * 10 + (uint256(uint8(b[j])) - 48); } result[idx] = num; idx++; start = i + 1; } } return result; } // ══════════════════════════════════════════════════════════════════════ // String Helpers // ══════════════════════════════════════════════════════════════════════ /// @notice Generate a random 4-character alphanumeric ID 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); } /// @notice Pad a number with leading zeros 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); } }