From c1627dd4c95ad1fda509e3cdc4184c48633f4869 Mon Sep 17 00:00:00 2001 From: johba Date: Mon, 18 Aug 2025 21:09:03 +0200 Subject: [PATCH] refactor: Migrate to improved fuzzing system with visualizer support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replaced old FuzzingAnalysis.s.sol with improved RecordedFuzzingAnalysis - Old fuzzing used trades too small (0.1-100% of remaining balance) to reach discovery - New system uses larger trades (50-200 ETH) that successfully find invariants - run-fuzzing.sh now redirects to run-recorded-fuzzing.sh for backward compatibility - Added position CSV generation for profitable scenarios to support visualizer - Visualizer automatically launches when invariants are found - Removed unnecessary debugCSV complexity The old fuzzing couldn't find invariants because trades got progressively smaller (often <1 ETH after a few iterations) and couldn't move price the 3,690 ticks needed to reach the discovery position. The new system maintains large trade sizes throughout the scenario, successfully finding profitable exploits. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- onchain/analysis/FuzzingAnalysis.s.sol | 384 ---------------- .../analysis/RecordedFuzzingAnalysis.s.sol | 2 + onchain/analysis/run-fuzzing.sh | 410 +----------------- onchain/analysis/run-recorded-fuzzing.sh | 75 +++- 4 files changed, 79 insertions(+), 792 deletions(-) delete mode 100644 onchain/analysis/FuzzingAnalysis.s.sol diff --git a/onchain/analysis/FuzzingAnalysis.s.sol b/onchain/analysis/FuzzingAnalysis.s.sol deleted file mode 100644 index 860fd7b..0000000 --- a/onchain/analysis/FuzzingAnalysis.s.sol +++ /dev/null @@ -1,384 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity ^0.8.19; - -import "forge-std/Test.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 {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 {TickMath} from "@aperture/uni-v3-lib/TickMath.sol"; -import {ThreePositionStrategy} from "../src/abstracts/ThreePositionStrategy.sol"; -import "../test/mocks/BullMarketOptimizer.sol"; -import "../test/mocks/NeutralMarketOptimizer.sol"; -import "../test/mocks/BearMarketOptimizer.sol"; -import "../test/mocks/WhaleOptimizer.sol"; -import "../test/mocks/MockOptimizer.sol"; -import "../test/mocks/RandomScenarioOptimizer.sol"; -import "./helpers/CSVManager.sol"; -import "./helpers/SwapExecutor.sol"; -import {LiquidityAmounts} from "@aperture/uni-v3-lib/LiquidityAmounts.sol"; - -/** - * @title FuzzingAnalysis - * @notice Fuzzing analysis to find profitable trading scenarios against LiquidityManager - * @dev Configurable via environment variables: - * - FUZZING_RUNS: Number of fuzzing iterations per market (default 100) - * - TRACK_POSITIONS: Track detailed position data (default false) - */ -contract FuzzingAnalysis is Test, CSVManager { - TestEnvironment testEnv; - IUniswapV3Factory factory; - IUniswapV3Pool pool; - IWETH9 weth; - Kraiken harberg; - Stake stake; - LiquidityManager lm; - bool token0isWeth; - - address account = makeAddr("trader"); - address feeDestination = makeAddr("fees"); - - // Analysis metrics - uint256 public scenariosAnalyzed; - uint256 public profitableScenarios; - - // Configuration - uint256 public fuzzingRuns; - bool public trackPositions; - string public optimizerClass; - uint256 public tradesPerRun; - uint256 public seedOffset; - - // Optimizers - BullMarketOptimizer bullOptimizer; - NeutralMarketOptimizer neutralOptimizer; - BearMarketOptimizer bearOptimizer; - WhaleOptimizer whaleOptimizer; - MockOptimizer mockOptimizer; - RandomScenarioOptimizer randomOptimizer; - - function run() public { - _loadConfiguration(); - - console.log("=== Fuzzing Analysis ==="); - console.log(string.concat("Optimizer: ", optimizerClass)); - console.log(string.concat("Fuzzing runs: ", vm.toString(fuzzingRuns))); - console.log(string.concat("Trades per run: ", vm.toString(tradesPerRun))); - console.log(string.concat("Position tracking: ", trackPositions ? "enabled" : "disabled")); - console.log(""); - - testEnv = new TestEnvironment(feeDestination); - - // Get optimizer based on class name - address optimizerAddress = _getOptimizerByClass(optimizerClass); - - // Initialize CSV for profitable scenarios - string memory profitableCSV = "Scenario,Seed,Initial Balance,Final Balance,Profit,Profit %\n"; - uint256 profitableCount; - uint256 marketProfitable = 0; - - console.log(string.concat("=== FUZZING with ", optimizerClass, " ===")); - - for (uint256 seed = seedOffset; seed < seedOffset + fuzzingRuns; seed++) { - if (seed % 10 == 0 && seed > 0) { - console.log(string.concat("Progress: ", vm.toString(seed), "/", vm.toString(fuzzingRuns))); - } - - // Create fresh environment for each run - (factory, pool, weth, harberg, stake, lm,, token0isWeth) = - testEnv.setupEnvironmentWithOptimizer(seed % 2 == 0, feeDestination, optimizerAddress); - - // Fund LiquidityManager with initial ETH - vm.deal(address(lm), 50 ether); - - // Fund account with random amount (10-50 ETH) - uint256 fundAmount = 10 ether + (uint256(keccak256(abi.encodePacked(seed, "fund"))) % 40 ether); - vm.deal(account, fundAmount * 2); - vm.prank(account); - weth.deposit{value: fundAmount}(); - - uint256 initialBalance = weth.balanceOf(account); - - // Initial recenter - vm.warp(block.timestamp + 5 hours); - vm.prank(feeDestination); - try lm.recenter() {} catch {} - - // Run trading scenario - uint256 finalBalance = _runFuzzedScenario(optimizerClass, seed); - - scenariosAnalyzed++; - - // Calculate profit/loss - bool isProfitable = finalBalance > initialBalance; - uint256 profitOrLoss; - uint256 profitOrLossPercentage; - - if (isProfitable) { - profitOrLoss = finalBalance - initialBalance; - profitOrLossPercentage = (profitOrLoss * 100) / initialBalance; - profitableScenarios++; - marketProfitable++; - - console.log(string.concat("PROFITABLE! Seed: ", vm.toString(seed), " Profit: ", vm.toString(profitOrLossPercentage), "%")); - - // Add to CSV - profitableCSV = string.concat( - profitableCSV, - optimizerClass, ",", - vm.toString(seed), ",", - vm.toString(initialBalance), ",", - vm.toString(finalBalance), ",", - vm.toString(profitOrLoss), ",", - vm.toString(profitOrLossPercentage), "\n" - ); - profitableCount++; - } else { - profitOrLoss = initialBalance - finalBalance; - profitOrLossPercentage = (profitOrLoss * 100) / initialBalance; - } - - // Always log result for cumulative tracking - console.log(string.concat("RESULT|SEED:", vm.toString(seed), "|INITIAL:", vm.toString(initialBalance), "|FINAL:", vm.toString(finalBalance), "|PNL:", isProfitable ? "+" : "-", vm.toString(profitOrLoss))); - } - - console.log(string.concat("\nResults for ", optimizerClass, ":")); - console.log(string.concat("Profitable: ", vm.toString(marketProfitable), "/", vm.toString(fuzzingRuns))); - console.log(""); - - console.log("=== ANALYSIS COMPLETE ==="); - console.log(string.concat("Total scenarios analyzed: ", vm.toString(scenariosAnalyzed))); - console.log(string.concat("Total profitable scenarios: ", vm.toString(profitableScenarios))); - console.log(string.concat("Profitable rate: ", vm.toString((profitableScenarios * 100) / scenariosAnalyzed), "%")); - - // Write profitable scenarios CSV if any found - if (profitableCount > 0) { - console.log("Writing profitable scenarios CSV..."); - string memory filename = string.concat("profitable_scenarios_", vm.toString(block.timestamp), ".csv"); - vm.writeFile(filename, profitableCSV); - console.log(string.concat("\nProfitable scenarios written to: ", filename)); - } else { - console.log("\nNo profitable scenarios found."); - } - - console.log("Script execution complete."); - } - - function _loadConfiguration() internal { - fuzzingRuns = vm.envOr("FUZZING_RUNS", uint256(100)); - trackPositions = vm.envOr("TRACK_POSITIONS", false); - optimizerClass = vm.envOr("OPTIMIZER_CLASS", string("BullMarketOptimizer")); - tradesPerRun = vm.envOr("TRADES_PER_RUN", uint256(20)); - seedOffset = vm.envOr("SEED_OFFSET", uint256(0)); - } - - function _runFuzzedScenario(string memory scenarioName, uint256 seed) internal returns (uint256) { - // Initialize position tracking CSV if enabled - if (trackPositions) { - initializePositionsCSV(); - _recordPositionData("Initial"); - } - - // Use seed for randomness - uint256 rand = uint256(keccak256(abi.encodePacked(seed, scenarioName, block.timestamp))); - - // Use configured number of trades (with some randomness) - uint256 numTrades = tradesPerRun + (rand % 11) - 5; // +/- 5 trades - if (numTrades < 5) numTrades = 5; // Minimum 5 trades - - // Initial buy if no HARB - if (harberg.balanceOf(account) == 0 && weth.balanceOf(account) > 0) { - uint256 initialBuy = weth.balanceOf(account) / 10; - _executeBuy(initialBuy); - } - - // Execute random trades - for (uint256 i = 0; i < numTrades; i++) { - rand = uint256(keccak256(abi.encodePacked(rand, i))); - uint256 action = rand % 100; - - if (action < 25) { // 25% chance buy - uint256 wethBal = weth.balanceOf(account); - if (wethBal > 0) { - uint256 buyPercent = 1 + (rand % 1000); // 0.1% to 100% - uint256 buyAmount = (wethBal * buyPercent) / 1000; - if (buyAmount > 0) { - _executeBuy(buyAmount); - if (trackPositions) { - _recordPositionData(string.concat("Buy_", vm.toString(i))); - } - } - } - } else if (action < 50) { // 25% chance sell - uint256 harbBal = harberg.balanceOf(account); - if (harbBal > 0) { - uint256 sellPercent = 1 + (rand % 1000); // 0.1% to 100% - uint256 sellAmount = (harbBal * sellPercent) / 1000; - if (sellAmount > 0) { - _executeSell(sellAmount); - if (trackPositions) { - _recordPositionData(string.concat("Sell_", vm.toString(i))); - } - } - } - } else { // 50% chance recenter - uint256 waitTime = 1 minutes + (rand % 10 hours); - vm.warp(block.timestamp + waitTime); - vm.prank(feeDestination); - try lm.recenter() { - if (trackPositions) { - _recordPositionData(string.concat("Recenter_", vm.toString(i))); - } - } catch {} - } - - // Skip trades at extreme ticks - (, int24 currentTick, , , , , ) = pool.slot0(); - if (currentTick < -887000 || currentTick > 887000) continue; - } - - // Sell remaining HARB - uint256 finalHarb = harberg.balanceOf(account); - if (finalHarb > 0) { - _executeSell(finalHarb); - if (trackPositions) { - _recordPositionData("Final_Sell"); - } - } - - // Final recenters - for (uint256 j = 0; j < 1 + (rand % 3); j++) { - vm.warp(block.timestamp + 5 hours); - vm.prank(feeDestination); - try lm.recenter() {} catch {} - } - - // Write position tracking CSV if enabled - if (trackPositions) { - _recordPositionData("Final"); - string memory positionFilename = string.concat( - "positions_", scenarioName, "_", vm.toString(seed), ".csv" - ); - writeCSVToFile(positionFilename); - console.log(string.concat("Position tracking CSV written to: ", positionFilename)); - } - - return weth.balanceOf(account); - } - - function _executeBuy(uint256 amount) internal { - if (amount == 0 || weth.balanceOf(account) < amount) return; - - SwapExecutor executor = new SwapExecutor(pool, weth, harberg, token0isWeth); - vm.prank(account); - weth.transfer(address(executor), amount); - - try executor.executeBuy(amount, account) {} catch {} - } - - function _executeSell(uint256 amount) internal { - if (amount == 0 || harberg.balanceOf(account) < amount) return; - - SwapExecutor executor = new SwapExecutor(pool, weth, harberg, token0isWeth); - vm.prank(account); - harberg.transfer(address(executor), amount); - - try executor.executeSell(amount, account) {} catch {} - } - - function _getOrCreateOptimizer(uint256 index) internal returns (address) { - if (index == 0) { - if (address(bullOptimizer) == address(0)) bullOptimizer = new BullMarketOptimizer(); - return address(bullOptimizer); - } else if (index == 1) { - if (address(neutralOptimizer) == address(0)) neutralOptimizer = new NeutralMarketOptimizer(); - return address(neutralOptimizer); - } else { - if (address(bearOptimizer) == address(0)) bearOptimizer = new BearMarketOptimizer(); - return address(bearOptimizer); - } - } - - function _getOptimizerByClass(string memory className) internal returns (address) { - bytes32 classHash = keccak256(abi.encodePacked(className)); - - if (classHash == keccak256(abi.encodePacked("BullMarketOptimizer"))) { - if (address(bullOptimizer) == address(0)) bullOptimizer = new BullMarketOptimizer(); - return address(bullOptimizer); - } else if (classHash == keccak256(abi.encodePacked("NeutralMarketOptimizer"))) { - if (address(neutralOptimizer) == address(0)) neutralOptimizer = new NeutralMarketOptimizer(); - return address(neutralOptimizer); - } else if (classHash == keccak256(abi.encodePacked("BearMarketOptimizer"))) { - if (address(bearOptimizer) == address(0)) bearOptimizer = new BearMarketOptimizer(); - return address(bearOptimizer); - } else if (classHash == keccak256(abi.encodePacked("WhaleOptimizer"))) { - if (address(whaleOptimizer) == address(0)) whaleOptimizer = new WhaleOptimizer(); - return address(whaleOptimizer); - } else if (classHash == keccak256(abi.encodePacked("MockOptimizer"))) { - if (address(mockOptimizer) == address(0)) { - mockOptimizer = new MockOptimizer(); - mockOptimizer.initialize(address(harberg), address(stake)); - } - return address(mockOptimizer); - } else if (classHash == keccak256(abi.encodePacked("RandomScenarioOptimizer"))) { - if (address(randomOptimizer) == address(0)) randomOptimizer = new RandomScenarioOptimizer(); - return address(randomOptimizer); - } else { - revert(string.concat("Unknown optimizer class: ", className, ". Available: BullMarketOptimizer, NeutralMarketOptimizer, BearMarketOptimizer, WhaleOptimizer, MockOptimizer, RandomScenarioOptimizer")); - } - } - - function _recordPositionData(string memory label) internal { - (,int24 currentTick,,,,,) = pool.slot0(); - - // Cap currentTick to avoid overflow in extreme cases - if (currentTick > 887000) currentTick = 887000; - if (currentTick < -887000) currentTick = -887000; - - // Get each position - (uint128 floorLiq, int24 floorLower, int24 floorUpper) = lm.positions(ThreePositionStrategy.Stage.FLOOR); - (uint128 anchorLiq, int24 anchorLower, int24 anchorUpper) = lm.positions(ThreePositionStrategy.Stage.ANCHOR); - (uint128 discoveryLiq, int24 discoveryLower, int24 discoveryUpper) = lm.positions(ThreePositionStrategy.Stage.DISCOVERY); - - // Debug: Log liquidity values - if (keccak256(bytes(label)) == keccak256(bytes("Initial")) || keccak256(bytes(label)) == keccak256(bytes("Recenter_2"))) { - console.log("=== LIQUIDITY VALUES ==="); - console.log("Label:", label); - console.log("Current tick:", uint256(int256(currentTick))); - console.log("Anchor range:", uint256(int256(anchorLower)), "-", uint256(int256(anchorUpper))); - console.log("Anchor liquidity:", uint256(anchorLiq)); - console.log("Discovery range:", uint256(int256(discoveryLower)), "-", uint256(int256(discoveryUpper))); - console.log("Discovery liquidity:", uint256(discoveryLiq)); - if (uint256(anchorLiq) > 0) { - console.log("Discovery/Anchor liquidity ratio:", uint256(discoveryLiq) * 100 / uint256(anchorLiq), "%"); - console.log("Anchor width:", uint256(int256(anchorUpper - anchorLower)), "ticks"); - console.log("Discovery width:", uint256(int256(discoveryUpper - discoveryLower)), "ticks"); - uint256 anchorLiqPerTick = uint256(anchorLiq) * 1000 / uint256(int256(anchorUpper - anchorLower)); - uint256 discoveryLiqPerTick = uint256(discoveryLiq) * 1000 / uint256(int256(discoveryUpper - discoveryLower)); - console.log("Anchor liquidity per tick (x1000):", anchorLiqPerTick); - console.log("Discovery liquidity per tick (x1000):", discoveryLiqPerTick); - console.log("Discovery/Anchor per tick ratio:", discoveryLiqPerTick * 100 / anchorLiqPerTick, "%"); - } - } - - // Create position data row with liquidity values directly - string memory row = string.concat( - label, ", ", - vm.toString(currentTick), ", ", - vm.toString(floorLower), ", ", - vm.toString(floorUpper), ", ", - vm.toString(uint256(floorLiq)), ", ", - vm.toString(anchorLower), ", ", - vm.toString(anchorUpper), ", ", - vm.toString(uint256(anchorLiq)), ", ", - vm.toString(discoveryLower), ", ", - vm.toString(discoveryUpper), ", ", - vm.toString(uint256(discoveryLiq)), ", ", - token0isWeth ? "true" : "false" - ); - appendCSVRow(row); - } -} \ No newline at end of file diff --git a/onchain/analysis/RecordedFuzzingAnalysis.s.sol b/onchain/analysis/RecordedFuzzingAnalysis.s.sol index 737c148..0d46c13 100644 --- a/onchain/analysis/RecordedFuzzingAnalysis.s.sol +++ b/onchain/analysis/RecordedFuzzingAnalysis.s.sol @@ -42,6 +42,8 @@ contract RecordedFuzzingAnalysis is ImprovedFuzzingAnalysis { // Initialize recorder for this scenario if (enableRecording) { recorder = new ScenarioRecorder(); + // Always track positions when recording to generate CSV for visualizer + trackPositions = true; recorder.initializeScenario( seed, optimizerClass, diff --git a/onchain/analysis/run-fuzzing.sh b/onchain/analysis/run-fuzzing.sh index 4b9cefe..e728117 100755 --- a/onchain/analysis/run-fuzzing.sh +++ b/onchain/analysis/run-fuzzing.sh @@ -1,408 +1,12 @@ #!/bin/bash -# Change to the analysis directory (where this script is located) -cd "$(dirname "$0")" +# This script now uses the improved recorded fuzzing system +# The old FuzzingAnalysis.s.sol has been replaced with RecordedFuzzingAnalysis.s.sol +# which uses larger trades that can actually reach the discovery position -# Function to cleanup on exit -cleanup() { - if [ -n "$VIEWER_PID" ]; then - echo -e "\n${YELLOW}Stopping viewer...${NC}" - # Kill the entire process group - pkill -TERM -g $VIEWER_PID 2>/dev/null || true - sleep 1 - pkill -KILL -g $VIEWER_PID 2>/dev/null || true - fi - rm -f profitable_scenario.csv 2>/dev/null || true -} - -# Set trap to cleanup on script exit -trap cleanup EXIT - -# Default values -OPTIMIZER_CLASS="" -TOTAL_RUNS=50 -TRADES_PER_RUN=20 -DEBUG_CSV=false -WHALE_MODE=false - -# Function to show usage -show_usage() { - echo "Usage: $0 [runs=N] [trades=N] [debugCSV] [whaleMode]" - echo "" - echo "Parameters:" - echo " optimizer_class Required. The optimizer class to use" - echo " runs=N Optional. Number of fuzzing runs (default: 50)" - echo " trades=N Optional. Trades per run (default: 20, actual will be ±5)" - echo " debugCSV Optional. Enable debug mode with position tracking CSV (forces runs=1)" - echo " whaleMode Optional. Enable whale-sized trades (20-80% of balance, 50-500 ETH funding)" - echo "" - echo "Examples:" - echo " $0 BullMarketOptimizer" - echo " $0 WhaleOptimizer runs=100" - echo " $0 BearMarketOptimizer runs=10 trades=50" - echo " $0 NeutralMarketOptimizer trades=30 runs=25" - echo " $0 BullMarketOptimizer debugCSV" - echo " $0 WhaleOptimizer whaleMode runs=20" - echo "" - echo "Available optimizers:" - echo " - BullMarketOptimizer" - echo " - NeutralMarketOptimizer" - echo " - BearMarketOptimizer" - echo " - WhaleOptimizer" - echo " - MockOptimizer" - echo " - RandomScenarioOptimizer" -} - -# Parse arguments -if [ $# -eq 0 ]; then - echo "Error: No optimizer class specified" - show_usage - exit 1 -fi - -# First argument is always the optimizer class -OPTIMIZER_CLASS=$1 -shift - -# Parse named parameters -for arg in "$@"; do - case $arg in - runs=*) - TOTAL_RUNS="${arg#*=}" - if ! [[ "$TOTAL_RUNS" =~ ^[0-9]+$ ]] || [ "$TOTAL_RUNS" -eq 0 ]; then - echo "Error: Invalid value for runs. Must be a positive integer." - exit 1 - fi - ;; - trades=*) - TRADES_PER_RUN="${arg#*=}" - if ! [[ "$TRADES_PER_RUN" =~ ^[0-9]+$ ]] || [ "$TRADES_PER_RUN" -eq 0 ]; then - echo "Error: Invalid value for trades. Must be a positive integer." - exit 1 - fi - ;; - debugCSV) - DEBUG_CSV=true - TOTAL_RUNS=1 - ;; - whaleMode) - WHALE_MODE=true - ;; - *) - echo "Error: Unknown parameter '$arg'" - show_usage - exit 1 - ;; - esac -done - -# Colors for output -GREEN='\033[0;32m' -RED='\033[0;31m' -YELLOW='\033[1;33m' -NC='\033[0m' # No Color - -OUTPUT_DIR="fuzzing_results_${OPTIMIZER_CLASS}_$(date +%Y%m%d_%H%M%S)" -MERGED_CSV="$OUTPUT_DIR/merged_profitable_scenarios.csv" - -echo -e "${GREEN}=== Fuzzing Campaign ===${NC}" -echo "Optimizer: $OPTIMIZER_CLASS" -echo "Total runs: $TOTAL_RUNS" -echo "Trades per run: $TRADES_PER_RUN (±5)" -if [ "$DEBUG_CSV" = true ]; then - echo -e "${YELLOW}Debug mode: ENABLED (position tracking CSV will be generated)${NC}" -fi -if [ "$WHALE_MODE" = true ]; then - echo -e "${YELLOW}Whale mode: ENABLED (20-80% trades, 50-500 ETH funding)${NC}" -fi -echo "Output directory: $OUTPUT_DIR" +echo "Note: run-fuzzing.sh now uses the improved fuzzing with recording capabilities" +echo "Redirecting to run-recorded-fuzzing.sh..." echo "" -# Validate that the optimizer class exists by doing a dry run -echo "Validating optimizer class..." -OPTIMIZER_CLASS="$OPTIMIZER_CLASS" FUZZING_RUNS=1 TRADES_PER_RUN=1 forge script FuzzingAnalysis.s.sol --ffi --via-ir --gas-limit 200000000 > /tmp/optimizer_check.log 2>&1 -if [ $? -ne 0 ]; then - # Check specifically for unknown optimizer error - if grep -q "Unknown optimizer class" /tmp/optimizer_check.log; then - echo -e "${RED}Error: Invalid optimizer class '${OPTIMIZER_CLASS}'${NC}" - echo -e "${RED}Check the error:${NC}" - grep "Unknown optimizer" /tmp/optimizer_check.log - echo "" - show_usage - exit 1 - else - # Other errors are ok during validation, we just want to check the optimizer exists - echo "Optimizer validation passed (non-optimizer errors ignored)" - fi -else - echo "Optimizer validation passed" -fi - -# Create output directory -mkdir -p "$OUTPUT_DIR" - -# Initialize merged CSV with header -echo "Scenario,Seed,Initial Balance,Final Balance,Profit,Profit %" > "$MERGED_CSV" - -# Track statistics -TOTAL_PROFITABLE=0 -FAILED_RUNS=0 -CUMULATIVE_PNL=0 -CSV_GENERATED=false -LATEST_CSV="" - -# Save configuration -CONFIG_FILE="$OUTPUT_DIR/config.txt" -{ - echo "Fuzzing Configuration" - echo "====================" - echo "Optimizer: $OPTIMIZER_CLASS" - echo "Total runs: $TOTAL_RUNS" - echo "Trades per run: $TRADES_PER_RUN (±5)" - echo "Start time: $(date)" -} > "$CONFIG_FILE" - -# Run fuzzing analysis multiple times -for i in $(seq 1 $TOTAL_RUNS); do - echo -e "${YELLOW}Running fuzzing iteration $i/$TOTAL_RUNS...${NC}" - - # Run single fuzzing iteration with specified optimizer and trades - # Use iteration number as seed offset to ensure different scenarios - # Enable position tracking if debugCSV is set - if [ "$DEBUG_CSV" = true ]; then - WHALE_MODE="$WHALE_MODE" TRACK_POSITIONS=true SEED_OFFSET=$((i - 1)) OPTIMIZER_CLASS="$OPTIMIZER_CLASS" TRADES_PER_RUN="$TRADES_PER_RUN" FUZZING_RUNS=1 forge script FuzzingAnalysis.s.sol --ffi --via-ir --gas-limit 200000000 > "$OUTPUT_DIR/run_$i.log" 2>&1 - else - WHALE_MODE="$WHALE_MODE" SEED_OFFSET=$((i - 1)) OPTIMIZER_CLASS="$OPTIMIZER_CLASS" TRADES_PER_RUN="$TRADES_PER_RUN" FUZZING_RUNS=1 forge script FuzzingAnalysis.s.sol --ffi --via-ir --gas-limit 200000000 > "$OUTPUT_DIR/run_$i.log" 2>&1 - fi - - if [ $? -eq 0 ]; then - echo -e "${GREEN}✓ Run $i completed${NC}" - - # Extract P&L from RESULT line (may have leading spaces from forge output) - RESULT_LINE=$(grep "RESULT|" "$OUTPUT_DIR/run_$i.log") - if [ -n "$RESULT_LINE" ]; then - # Parse the RESULT line to extract P&L - PNL_VALUE=$(echo "$RESULT_LINE" | awk -F'|' '{print $5}' | sed 's/PNL://' | tr -d ' ') - - # Debug: show values before calculation - # echo "DEBUG: Current cumulative: $CUMULATIVE_PNL, New P&L: $PNL_VALUE" - - # Add to cumulative P&L using awk (handles large numbers better than bash arithmetic) - CUMULATIVE_PNL=$(LC_NUMERIC=C awk -v cum="$CUMULATIVE_PNL" -v pnl="$PNL_VALUE" 'BEGIN {printf "%.0f", cum + pnl}') - - # Format cumulative P&L for display (convert from wei to ETH) - CUMULATIVE_ETH=$(LC_NUMERIC=C awk -v cum="$CUMULATIVE_PNL" 'BEGIN {printf "%.6f", cum / 1000000000000000000}') - - # Check if profitable - if [[ "$PNL_VALUE" == +* ]]; then - echo -e "${GREEN} Found profitable scenario!${NC}" - ((TOTAL_PROFITABLE++)) - - # Extract profit percentage - PROFIT_PCT=$(grep "PROFITABLE!" "$OUTPUT_DIR/run_$i.log" | grep -oE "Profit: [0-9]+%" | grep -oE "[0-9]+") - echo -e "${GREEN} Profit: ${PROFIT_PCT}%${NC}" - else - echo -e "${YELLOW} Loss scenario${NC}" - fi - - # Display cumulative P&L - if awk -v cum="$CUMULATIVE_PNL" 'BEGIN {exit !(cum >= 0)}'; then - echo -e "${GREEN} Cumulative P&L: +${CUMULATIVE_ETH} ETH${NC}" - else - echo -e "${RED} Cumulative P&L: ${CUMULATIVE_ETH} ETH${NC}" - fi - - # Extract CSV file path if generated (for profitable scenarios) - CSV_FILE=$(grep "Profitable scenarios written to:" "$OUTPUT_DIR/run_$i.log" | awk '{print $NF}') - if [ -n "$CSV_FILE" ] && [ -f "$CSV_FILE" ]; then - # Append data rows (skip header) to merged CSV - tail -n +2 "$CSV_FILE" >> "$MERGED_CSV" - # Move individual CSV to output directory - mv "$CSV_FILE" "$OUTPUT_DIR/" - CSV_GENERATED=true - LATEST_CSV="$OUTPUT_DIR/$(basename "$CSV_FILE")" - fi - - # In debug mode, also look for position tracking CSV - if [ "$DEBUG_CSV" = true ]; then - # Look for position CSV mentioned in the log - POSITION_CSV=$(grep "Position tracking CSV written to:" "$OUTPUT_DIR/run_$i.log" | awk -F': ' '{print $2}') - if [ -n "$POSITION_CSV" ]; then - # The CSV is generated in the parent directory (onchain), so check there - PARENT_CSV="../$POSITION_CSV" - if [ -f "$PARENT_CSV" ]; then - echo -e "${GREEN} Position tracking CSV generated: $POSITION_CSV${NC}" - # Move to output directory with a more descriptive name - FINAL_CSV_NAME="debug_positions_${OPTIMIZER_CLASS}_seed${SEED_OFFSET}.csv" - mv "$PARENT_CSV" "$OUTPUT_DIR/$FINAL_CSV_NAME" - echo -e "${GREEN} Moved to: $OUTPUT_DIR/$FINAL_CSV_NAME${NC}" - CSV_GENERATED=true - LATEST_CSV="$OUTPUT_DIR/$FINAL_CSV_NAME" - elif [ -f "$POSITION_CSV" ]; then - # Fallback if it's in the current directory - echo -e "${GREEN} Position tracking CSV generated: $POSITION_CSV${NC}" - FINAL_CSV_NAME="debug_positions_${OPTIMIZER_CLASS}_seed${SEED_OFFSET}.csv" - mv "$POSITION_CSV" "$OUTPUT_DIR/$FINAL_CSV_NAME" - echo -e "${GREEN} Moved to: $OUTPUT_DIR/$FINAL_CSV_NAME${NC}" - CSV_GENERATED=true - LATEST_CSV="$OUTPUT_DIR/$FINAL_CSV_NAME" - fi - fi - fi - else - echo -e "${YELLOW} Warning: No RESULT line found in output${NC}" - fi - else - echo -e "${RED}✗ Run $i failed${NC}" - ((FAILED_RUNS++)) - # Show last few lines of error - echo -e "${RED}Error details:${NC}" - tail -5 "$OUTPUT_DIR/run_$i.log" - fi - - # Small delay to avoid overwhelming the system - sleep 0.5 -done - -# Update config with end time -echo "End time: $(date)" >> "$CONFIG_FILE" - -echo "" -echo -e "${GREEN}=== FUZZING CAMPAIGN COMPLETE ===${NC}" -echo "Optimizer: $OPTIMIZER_CLASS" -echo "Total runs: $TOTAL_RUNS" -echo "Trades per run: $TRADES_PER_RUN (±5)" -echo "Successful runs: $((TOTAL_RUNS - FAILED_RUNS))" -echo "Failed runs: $FAILED_RUNS" -echo "Total profitable scenarios: $TOTAL_PROFITABLE" - -# Display final cumulative P&L -FINAL_CUMULATIVE_ETH=$(LC_NUMERIC=C awk -v cum="$CUMULATIVE_PNL" 'BEGIN {printf "%.6f", cum / 1000000000000000000}') -if awk -v cum="$CUMULATIVE_PNL" 'BEGIN {exit !(cum >= 0)}'; then - echo -e "${GREEN}Final Cumulative P&L: +${FINAL_CUMULATIVE_ETH} ETH${NC}" -else - echo -e "${RED}Final Cumulative P&L: ${FINAL_CUMULATIVE_ETH} ETH${NC}" -fi - -echo "" -echo "Results saved in: $OUTPUT_DIR" -echo "Merged CSV: $MERGED_CSV" - -# Generate summary report -SUMMARY="$OUTPUT_DIR/summary.txt" -{ - echo "Fuzzing Campaign Summary" - echo "========================" - echo "Date: $(date)" - echo "Optimizer: $OPTIMIZER_CLASS" - echo "Total runs: $TOTAL_RUNS" - echo "Trades per run: $TRADES_PER_RUN (±5)" - echo "" - echo "Results:" - echo "--------" - echo "Successful runs: $((TOTAL_RUNS - FAILED_RUNS)) / $TOTAL_RUNS" - echo "Failed runs: $FAILED_RUNS" - echo "Total profitable scenarios: $TOTAL_PROFITABLE / $((TOTAL_RUNS - FAILED_RUNS))" - echo "Success rate: $(awk "BEGIN {if ($TOTAL_RUNS - $FAILED_RUNS > 0) printf \"%.2f\", $TOTAL_PROFITABLE/($TOTAL_RUNS-$FAILED_RUNS)*100; else print \"0.00\"}")%" - echo "" - echo "Profit/Loss Analysis:" - echo "--------------------" - echo "Cumulative P&L: $FINAL_CUMULATIVE_ETH ETH" - echo "Average P&L per run: $(awk -v cumeth="$FINAL_CUMULATIVE_ETH" -v total="$TOTAL_RUNS" -v failed="$FAILED_RUNS" 'BEGIN {if (total - failed > 0) printf "%.6f ETH", cumeth/(total-failed); else print "0.000000 ETH"}')" -} > "$SUMMARY" - -echo "" -echo "Summary report: $SUMMARY" - -# If there were profitable scenarios, show a sample -if [ $TOTAL_PROFITABLE -gt 0 ]; then - echo "" - echo -e "${GREEN}Sample profitable scenarios:${NC}" - head -5 "$MERGED_CSV" -fi - -# If debug mode was used, mention the position tracking CSV -if [ "$DEBUG_CSV" = true ]; then - echo "" - echo -e "${GREEN}Debug position tracking CSV generated!${NC}" - echo "View it with: ./view-scenarios.sh" - echo "Then navigate to the output directory and select the debug CSV file" -fi - -# If any CSV was generated, launch the viewer -if [ "$CSV_GENERATED" = true ] && [ -n "$LATEST_CSV" ]; then - echo "" - echo -e "${GREEN}=== Launching Scenario Visualizer ===${NC}" - echo "CSV file: $LATEST_CSV" - - # Create a temporary symlink to the CSV for the viewer - TEMP_LINK="profitable_scenario.csv" - if [ -f "$TEMP_LINK" ] || [ -L "$TEMP_LINK" ]; then - rm -f "$TEMP_LINK" - fi - # Use absolute path for the symlink - ln -s "$(pwd)/$LATEST_CSV" "$TEMP_LINK" - - # Check if server is already running on common ports - SERVER_RUNNING=false - EXISTING_PORT="" - for PORT in 8000 8001 8002; do - if lsof -Pi :$PORT -sTCP:LISTEN -t >/dev/null 2>&1; then - # Check if it's a python http server in our analysis directory - if lsof -Pi :$PORT -sTCP:LISTEN 2>/dev/null | grep -q "python.*http.server"; then - SERVER_RUNNING=true - EXISTING_PORT=$PORT - break - fi - fi - done - - if [ "$SERVER_RUNNING" = true ]; then - echo -e "${YELLOW}Server already running on port $EXISTING_PORT${NC}" - echo -e "${GREEN}Browser should open to: http://localhost:$EXISTING_PORT/scenario-visualizer.html${NC}" - - # Try to open browser to existing server - if command -v xdg-open &> /dev/null; then - xdg-open "http://localhost:$EXISTING_PORT/scenario-visualizer.html" 2>/dev/null & - elif command -v open &> /dev/null; then - open "http://localhost:$EXISTING_PORT/scenario-visualizer.html" 2>/dev/null & - fi - - echo "" - echo -e "${YELLOW}Press Enter to exit (server will keep running)...${NC}" - read -r - else - # Start the viewer in background in its own process group - setsid ./view-scenarios.sh & - VIEWER_PID=$! - - # Give the server time to start and browser to open - sleep 2 - - # Show the URL - echo "" - echo -e "${GREEN}Browser should open to: http://localhost:8000/scenario-visualizer.html${NC}" - echo "(If port 8000 was busy, check the port number mentioned above)" - echo "If browser didn't open, manually navigate to that URL" - - # Wait for user input - echo "" - echo -e "${YELLOW}Press Enter to stop the viewer and exit...${NC}" - read -r - - # Kill the viewer process and its children - if [ -n "$VIEWER_PID" ]; then - # Kill the entire process group (includes python server) - pkill -TERM -g $VIEWER_PID 2>/dev/null || true - # Give it a moment to clean up - sleep 1 - # Force kill if still running - pkill -KILL -g $VIEWER_PID 2>/dev/null || true - fi - - echo -e "${GREEN}Viewer stopped.${NC}" - fi - - # Clean up the symlink - rm -f "$TEMP_LINK" -fi \ No newline at end of file +# Pass all arguments to the new script +exec ./analysis/run-recorded-fuzzing.sh "$@" \ No newline at end of file diff --git a/onchain/analysis/run-recorded-fuzzing.sh b/onchain/analysis/run-recorded-fuzzing.sh index 9b49286..f0f65e4 100755 --- a/onchain/analysis/run-recorded-fuzzing.sh +++ b/onchain/analysis/run-recorded-fuzzing.sh @@ -58,7 +58,6 @@ mkdir -p $OUTPUT_DIR echo -e "${YELLOW}Starting recorded fuzzing analysis...${NC}" FUZZING_RUNS=$RUNS_VALUE \ OPTIMIZER_CLASS=$OPTIMIZER \ -TRACK_POSITIONS=true \ RUN_ID=$RUN_ID \ forge script analysis/RecordedFuzzingAnalysis.s.sol:RecordedFuzzingAnalysis -vv 2>&1 | tee $OUTPUT_DIR/fuzzing.log @@ -106,9 +105,9 @@ if [ $SCENARIO_COUNT -gt 0 ]; then done # Move position CSVs if they exist - if ls positions_*.csv 1> /dev/null 2>&1; then - mv positions_*.csv $OUTPUT_DIR/ - echo -e "\n Position CSVs moved to: ${BLUE}$OUTPUT_DIR/${NC}" + if ls improved_positions_*.csv 1> /dev/null 2>&1; then + mv improved_positions_*.csv $OUTPUT_DIR/ + echo -e "\n Position CSVs for visualization moved to: ${BLUE}$OUTPUT_DIR/${NC}" fi # Create index file @@ -157,4 +156,70 @@ fi echo "" echo -e "${GREEN}Results saved to: $OUTPUT_DIR/${NC}" -echo -e "Run ID: ${BOLD}${RUN_ID}${NC}" \ No newline at end of file +echo -e "Run ID: ${BOLD}${RUN_ID}${NC}" + +# Launch visualizer if position CSVs were generated +POSITION_CSV_COUNT=$(ls -1 $OUTPUT_DIR/improved_positions_*.csv 2>/dev/null | wc -l) + +if [ $POSITION_CSV_COUNT -gt 0 ] && [ $SCENARIO_COUNT -gt 0 ]; then + echo "" + echo -e "${GREEN}=== Launching Scenario Visualizer ===${NC}" + + # Get the first position CSV for visualization + FIRST_CSV=$(ls -1 $OUTPUT_DIR/improved_positions_*.csv 2>/dev/null | head -1) + echo "CSV file: $FIRST_CSV" + + # Create a temporary symlink to the CSV for the viewer + TEMP_LINK="profitable_scenario.csv" + if [ -f "$TEMP_LINK" ] || [ -L "$TEMP_LINK" ]; then + rm -f "$TEMP_LINK" + fi + # Use absolute path for the symlink + ln -s "$(pwd)/$FIRST_CSV" "$TEMP_LINK" + + # Check if server is already running on common ports + SERVER_RUNNING=false + EXISTING_PORT="" + for PORT in 8000 8001 8002; do + if lsof -Pi :$PORT -sTCP:LISTEN -t >/dev/null 2>&1; then + # Check if it's a python http server in our analysis directory + if lsof -Pi :$PORT -sTCP:LISTEN 2>/dev/null | grep -q "python.*http.server"; then + SERVER_RUNNING=true + EXISTING_PORT=$PORT + break + fi + fi + done + + if [ "$SERVER_RUNNING" = true ]; then + echo -e "${YELLOW}Server already running on port $EXISTING_PORT${NC}" + echo -e "${GREEN}Open browser to: http://localhost:$EXISTING_PORT/scenario-visualizer.html${NC}" + echo "" + echo -e "${YELLOW}Press Enter to exit (server will keep running)...${NC}" + read -r + else + # Start the viewer in background + ./analysis/view-scenarios.sh & + VIEWER_PID=$! + + # Give the server time to start + sleep 2 + + echo "" + echo -e "${GREEN}Browser should open to: http://localhost:8000/scenario-visualizer.html${NC}" + echo "" + echo -e "${YELLOW}Press Enter to stop the viewer and exit...${NC}" + read -r + + # Kill the viewer process + if [ -n "$VIEWER_PID" ]; then + pkill -P $VIEWER_PID 2>/dev/null || true + kill $VIEWER_PID 2>/dev/null || true + fi + + echo -e "${GREEN}Viewer stopped.${NC}" + fi + + # Clean up the symlink + rm -f "$TEMP_LINK" +fi \ No newline at end of file