diff --git a/onchain/analysis/AnalysisVisualizer.py b/onchain/analysis/AnalysisVisualizer.py deleted file mode 100755 index da853bd..0000000 --- a/onchain/analysis/AnalysisVisualizer.py +++ /dev/null @@ -1,309 +0,0 @@ -#!/usr/bin/env python3 -""" -Analysis Visualizer for LiquidityManager Risk Assessment -Processes CSV outputs from ComprehensiveAnalysis.s.sol and generates visualizations -""" - -import pandas as pd -import matplotlib.pyplot as plt -import seaborn as sns -import numpy as np -import glob -import os -from datetime import datetime - -# Set style -plt.style.use('seaborn-v0_8-darkgrid') -sns.set_palette("husl") - -def load_csv_files(pattern="analysis/comprehensive_*.csv"): - """Load all CSV files matching the pattern""" - files = glob.glob(pattern) - data = {} - - for file in files: - scenario = os.path.basename(file).replace("comprehensive_", "").replace(".csv", "") - try: - df = pd.read_csv(file) - data[scenario] = df - print(f"Loaded {scenario}: {len(df)} rows") - except Exception as e: - print(f"Error loading {file}: {e}") - - return data - -def analyze_price_impact(data): - """Analyze price impact across scenarios""" - fig, axes = plt.subplots(3, 3, figsize=(15, 12)) - axes = axes.flatten() - - for idx, (scenario, df) in enumerate(data.items()): - if idx >= 9: - break - - if 'price' in df.columns: - ax = axes[idx] - ax.plot(df.index, df['price'] / 1e18, linewidth=2) - ax.set_title(f"{scenario} - Price Movement") - ax.set_xlabel("Trade #") - ax.set_ylabel("Price (ETH)") - ax.grid(True, alpha=0.3) - - plt.tight_layout() - plt.savefig("analysis/price_impact_analysis.png", dpi=300) - plt.close() - -def analyze_lm_value(data): - """Analyze LiquidityManager value changes""" - fig, ax = plt.subplots(figsize=(12, 8)) - - for scenario, df in data.items(): - if 'lmValue' in df.columns: - lm_values = df['lmValue'] / 1e18 - initial_value = lm_values.iloc[0] if len(lm_values) > 0 else 0 - if initial_value > 0: - relative_change = ((lm_values - initial_value) / initial_value) * 100 - ax.plot(df.index, relative_change, label=scenario, linewidth=2) - - ax.set_title("LiquidityManager Value Change Over Time", fontsize=16) - ax.set_xlabel("Trade #", fontsize=14) - ax.set_ylabel("Value Change (%)", fontsize=14) - ax.legend(bbox_to_anchor=(1.05, 1), loc='upper left') - ax.grid(True, alpha=0.3) - ax.axhline(y=0, color='red', linestyle='--', alpha=0.5) - - plt.tight_layout() - plt.savefig("analysis/lm_value_changes.png", dpi=300) - plt.close() - -def analyze_trader_profits(data): - """Analyze trader profits across scenarios""" - scenarios = [] - final_profits = [] - - for scenario, df in data.items(): - if 'traderProfit' in df.columns: - total_profit = df['traderProfit'].sum() / 1e18 - elif 'sandwichProfit' in df.columns: - total_profit = df['sandwichProfit'].sum() / 1e18 - elif 'flashProfit' in df.columns: - total_profit = df['flashProfit'].sum() / 1e18 - else: - total_profit = 0 - - scenarios.append(scenario.replace("_", " ")) - final_profits.append(total_profit) - - # Create bar chart - fig, ax = plt.subplots(figsize=(10, 8)) - bars = ax.bar(scenarios, final_profits, color=['red' if p > 0 else 'green' for p in final_profits]) - - ax.set_title("Total Trader Profits by Scenario", fontsize=16) - ax.set_xlabel("Scenario", fontsize=14) - ax.set_ylabel("Total Profit (ETH)", fontsize=14) - ax.axhline(y=0, color='black', linestyle='-', alpha=0.3) - - # Add value labels on bars - for bar, profit in zip(bars, final_profits): - height = bar.get_height() - ax.text(bar.get_x() + bar.get_width()/2., height, - f'{profit:.2f}', ha='center', va='bottom' if height > 0 else 'top') - - plt.xticks(rotation=45, ha='right') - plt.tight_layout() - plt.savefig("analysis/trader_profits.png", dpi=300) - plt.close() - -def analyze_recenter_impact(data): - """Analyze impact of recenter operations""" - fig, axes = plt.subplots(2, 2, figsize=(12, 10)) - - # Count recenters per scenario - recenter_counts = {} - for scenario, df in data.items(): - if 'action' in df.columns: - recenter_count = len(df[df['action'] == 'RECENTER']) - recenter_counts[scenario] = recenter_count - - # Plot recenter frequency - ax = axes[0, 0] - scenarios = list(recenter_counts.keys()) - counts = list(recenter_counts.values()) - ax.bar(range(len(scenarios)), counts) - ax.set_xticks(range(len(scenarios))) - ax.set_xticklabels([s.replace("_", " ") for s in scenarios], rotation=45, ha='right') - ax.set_title("Recenter Frequency by Scenario") - ax.set_ylabel("Number of Recenters") - - # Analyze price volatility around recenters - ax = axes[0, 1] - volatilities = [] - - for scenario, df in data.items(): - if 'price' in df.columns and 'action' in df.columns: - recenter_indices = df[df['action'] == 'RECENTER'].index - - for idx in recenter_indices: - # Get prices around recenter (5 trades before and after) - start = max(0, idx - 5) - end = min(len(df), idx + 5) - if end > start: - prices = df.loc[start:end, 'price'] / 1e18 - if len(prices) > 1: - volatility = prices.std() / prices.mean() * 100 - volatilities.append(volatility) - - if volatilities: - ax.hist(volatilities, bins=20, alpha=0.7) - ax.set_title("Price Volatility Around Recenters") - ax.set_xlabel("Volatility (%)") - ax.set_ylabel("Frequency") - - # Hide unused subplots - axes[1, 0].axis('off') - axes[1, 1].axis('off') - - plt.tight_layout() - plt.savefig("analysis/recenter_analysis.png", dpi=300) - plt.close() - -def generate_risk_matrix(): - """Generate risk assessment matrix""" - # Risk factors based on analysis - scenarios = [ - "Bull Market", "Neutral Market", "Bear Market", - "Whale Dominance", "Sandwich Attack", "VWAP Manipulation", - "Recenter Exploit", "Liquidity Gap", "Flash Loan Attack" - ] - - risk_factors = [ - "Capital Loss Risk", - "Price Manipulation", - "MEV Vulnerability", - "Liquidity Dominance Loss", - "VWAP Oracle Attack" - ] - - # Risk scores (0-10) - risk_matrix = np.array([ - [3, 2, 5, 2, 3], # Bull - [2, 3, 3, 3, 3], # Neutral - [5, 4, 3, 5, 4], # Bear - [9, 9, 7, 8, 8], # Whale - [7, 6, 10, 4, 5], # Sandwich - [6, 8, 5, 3, 10], # VWAP - [8, 7, 9, 5, 6], # Recenter - [7, 5, 6, 6, 4], # Gap - [10, 8, 8, 7, 7] # Flash - ]) - - # Create heatmap - fig, ax = plt.subplots(figsize=(10, 8)) - sns.heatmap(risk_matrix, annot=True, fmt='d', cmap='YlOrRd', - xticklabels=risk_factors, yticklabels=scenarios, - cbar_kws={'label': 'Risk Level (0-10)'}) - - ax.set_title("LiquidityManager Risk Assessment Matrix", fontsize=16) - plt.tight_layout() - plt.savefig("analysis/risk_matrix.png", dpi=300) - plt.close() - -def generate_summary_report(data): - """Generate text summary report""" - report = [] - report.append("=" * 60) - report.append("LIQUIDITYMANAGER COMPREHENSIVE RISK ANALYSIS REPORT") - report.append("=" * 60) - report.append(f"Generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n") - - # Analyze each scenario - high_risk_scenarios = [] - - for scenario, df in data.items(): - report.append(f"\n{scenario.upper().replace('_', ' ')}:") - report.append("-" * 40) - - if 'lmValue' in df.columns: - initial_lm = df['lmValue'].iloc[0] / 1e18 if len(df) > 0 else 0 - final_lm = df['lmValue'].iloc[-1] / 1e18 if len(df) > 0 else 0 - lm_change = final_lm - initial_lm - lm_change_pct = (lm_change / initial_lm * 100) if initial_lm > 0 else 0 - - report.append(f"Initial LM Value: {initial_lm:.2f} ETH") - report.append(f"Final LM Value: {final_lm:.2f} ETH") - report.append(f"LM Change: {lm_change:.2f} ETH ({lm_change_pct:+.1f}%)") - - if lm_change < -1: # Lost more than 1 ETH - high_risk_scenarios.append((scenario, lm_change)) - - if 'price' in df.columns: - price_volatility = (df['price'].std() / df['price'].mean() * 100) if df['price'].mean() > 0 else 0 - report.append(f"Price Volatility: {price_volatility:.1f}%") - - if 'action' in df.columns: - recenter_count = len(df[df['action'] == 'RECENTER']) - report.append(f"Recenters: {recenter_count}") - - # High risk summary - report.append("\n\nHIGH RISK SCENARIOS:") - report.append("=" * 40) - for scenario, loss in sorted(high_risk_scenarios, key=lambda x: x[1]): - report.append(f"- {scenario}: {loss:.2f} ETH loss") - - # Recommendations - report.append("\n\nKEY FINDINGS & RECOMMENDATIONS:") - report.append("=" * 40) - report.append("1. Whale attacks pose the highest risk to LM capital") - report.append("2. Flash loan attacks can extract significant value quickly") - report.append("3. VWAP manipulation creates long-term positioning vulnerabilities") - report.append("4. Sandwich attacks are highly profitable during recenters") - report.append("5. Narrow liquidity positions create exploitable gaps") - - report.append("\n\nMITIGATION STRATEGIES:") - report.append("-" * 40) - report.append("• Implement position size limits relative to pool TVL") - report.append("• Add time-weighted average for recenter triggers") - report.append("• Create emergency pause mechanism for extreme volatility") - report.append("• Implement progressive fees based on trade size") - report.append("• Add VWAP decay function to limit historical influence") - report.append("• Monitor external liquidity and adjust strategy accordingly") - - # Write report - with open("analysis/comprehensive_risk_report.txt", "w") as f: - f.write("\n".join(report)) - - print("\n".join(report)) - -def main(): - """Main analysis function""" - print("Loading analysis data...") - data = load_csv_files() - - if not data: - print("No data files found. Please run ComprehensiveAnalysis.s.sol first.") - return - - print("\nGenerating visualizations...") - analyze_price_impact(data) - print("✓ Price impact analysis complete") - - analyze_lm_value(data) - print("✓ LM value analysis complete") - - analyze_trader_profits(data) - print("✓ Trader profit analysis complete") - - analyze_recenter_impact(data) - print("✓ Recenter impact analysis complete") - - generate_risk_matrix() - print("✓ Risk matrix generated") - - print("\nGenerating summary report...") - generate_summary_report(data) - print("✓ Summary report generated") - - print("\nAnalysis complete! Check the 'analysis' directory for outputs.") - -if __name__ == "__main__": - main() \ No newline at end of file diff --git a/onchain/analysis/ImprovedFuzzingAnalysis.s.sol b/onchain/analysis/ImprovedFuzzingAnalysis.s.sol deleted file mode 100644 index f002857..0000000 --- a/onchain/analysis/ImprovedFuzzingAnalysis.s.sol +++ /dev/null @@ -1,650 +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 {ThreePositionStrategy} from "../src/abstracts/ThreePositionStrategy.sol"; -import {UniswapHelpers} from "../src/helpers/UniswapHelpers.sol"; -import {TickMath} from "@aperture/uni-v3-lib/TickMath.sol"; -import {LiquidityAmounts} from "@aperture/uni-v3-lib/LiquidityAmounts.sol"; -import {Math} from "@openzeppelin/utils/math/Math.sol"; -import "../test/mocks/BullMarketOptimizer.sol"; -import "../test/mocks/WhaleOptimizer.sol"; -import "./helpers/CSVManager.sol"; -import "./helpers/SwapExecutor.sol"; - -/** - * @title ImprovedFuzzingAnalysis - * @notice Enhanced fuzzing with larger trades designed to reach discovery position - * @dev Uses more aggressive trading patterns to explore the full liquidity range - */ -contract ImprovedFuzzingAnalysis is Test, CSVManager { - TestEnvironment testEnv; - IUniswapV3Factory factory; - IUniswapV3Pool pool; - IWETH9 weth; - Kraiken harberg; - Stake stake; - LiquidityManager lm; - bool token0isWeth; - - // Reusable swap executor to avoid repeated deployments - SwapExecutor swapExecutor; - - address trader = makeAddr("trader"); - address feeDestination = makeAddr("fees"); - - // Analysis metrics - uint256 public scenariosAnalyzed; - uint256 public profitableScenarios; - uint256 public discoveryReachedCount; - uint256 public totalStakesAttempted; - uint256 public totalStakesSucceeded; - uint256 public totalSnatchesAttempted; - uint256 public totalSnatchesSucceeded; - - // Staking tracking - mapping(address => uint256[]) public activePositions; - uint256[] public allPositionIds; // Track all positions for snatching - - // Configuration - uint256 public fuzzingRuns; - bool public trackPositions; - bool public enableStaking; - uint256 public buyBias; // 0-100, percentage bias towards buying vs selling - uint256 public tradesPerRun; // Number of trades/actions per scenario - uint256 public stakingBias; // 0-100, percentage bias towards staking vs unstaking - string public optimizerClass; - - function run() public virtual { - _loadConfiguration(); - - console.log("=== IMPROVED Fuzzing Analysis ==="); - console.log(string.concat("Optimizer: ", optimizerClass)); - console.log(string.concat("Fuzzing runs: ", vm.toString(fuzzingRuns))); - - testEnv = new TestEnvironment(feeDestination); - - // Deploy factory once for all runs (gas optimization) - factory = UniswapHelpers.deployUniswapFactory(); - - // Get optimizer - address optimizerAddress = _getOptimizerByClass(optimizerClass); - - // Track profitable scenarios - string memory profitableCSV = "Scenario,Seed,Initial Balance,Final Balance,Profit,Profit %,Discovery Reached\n"; - uint256 profitableCount; - - for (uint256 seed = 0; seed < fuzzingRuns; seed++) { - // Progress tracking removed - - // Create fresh environment with existing factory - (factory, pool, weth, harberg, stake, lm,, token0isWeth) = - testEnv.setupEnvironmentWithExistingFactory(factory, seed % 2 == 0, feeDestination, optimizerAddress); - - // Fund LiquidityManager with ETH proportional to trades - uint256 lmFunding = 100 ether + (tradesPerRun * 2 ether); // Base 100 + 2 ETH per trade - vm.deal(address(lm), lmFunding); - - // Fund trader with capital proportional to number of trades - // Combine what was previously split between trader and whale - uint256 traderFund = 150 ether + (tradesPerRun * 8 ether); // 150 ETH base + 8 ETH per trade - - // Add some randomness but keep it proportional - uint256 traderRandom = uint256(keccak256(abi.encodePacked(seed, "trader"))) % (tradesPerRun * 3 ether); - traderFund += traderRandom; - - - // Deal 2x to have extra for gas - vm.deal(trader, traderFund * 2); - - vm.prank(trader); - weth.deposit{value: traderFund}(); - - // Create SwapExecutor once per scenario to avoid repeated deployments - swapExecutor = new SwapExecutor(pool, weth, harberg, token0isWeth, lm); - - // Initial recenter BEFORE recording initial balance - vm.prank(feeDestination); - try lm.recenter{gas: 50_000_000}() {} catch {} - - // Record initial balance AFTER recenter so we account for pool state - uint256 initialBalance = weth.balanceOf(trader); - - // Initialize position tracking for each seed - if (trackPositions) { - // Initialize CSV header for each seed (after clearCSV from previous run) - initializePositionsCSV(); - _recordPositionData("Initial"); - } - - // Run improved trading scenario - (uint256 finalBalance, bool reachedDiscovery) = _runImprovedScenario(seed); - - scenariosAnalyzed++; - if (reachedDiscovery) { - discoveryReachedCount++; - } - - // Check profitability - if (finalBalance > initialBalance) { - uint256 profit = finalBalance - initialBalance; - uint256 profitPct = (profit * 100) / initialBalance; - profitableScenarios++; - - console.log(string.concat("PROFITABLE! Seed: ", vm.toString(seed), " - Profit: ", vm.toString(profitPct), "%")); - - profitableCSV = string.concat( - profitableCSV, - optimizerClass, ",", - vm.toString(seed), ",", - vm.toString(initialBalance), ",", - vm.toString(finalBalance), ",", - vm.toString(profit), ",", - vm.toString(profitPct), ",", - reachedDiscovery ? "true" : "false", "\n" - ); - profitableCount++; - } - - // Write position CSV if tracking - if (trackPositions) { - _recordPositionData("Final"); - string memory positionFilename = string.concat( - "improved_positions_", optimizerClass, "_", vm.toString(seed), ".csv" - ); - writeCSVToFile(positionFilename); - clearCSV(); // Clear buffer for next run - } - } - - // Summary - console.log("\n=== ANALYSIS COMPLETE ==="); - console.log(string.concat(" Total scenarios: ", vm.toString(scenariosAnalyzed))); - console.log(string.concat(" Profitable scenarios: ", vm.toString(profitableScenarios))); - console.log(string.concat(" Discovery reached: ", vm.toString(discoveryReachedCount), " times")); - console.log(string.concat(" Discovery rate: ", vm.toString((discoveryReachedCount * 100) / scenariosAnalyzed), "%")); - console.log(string.concat(" Profit rate: ", vm.toString((profitableScenarios * 100) / scenariosAnalyzed), "%")); - - - if (enableStaking) { - // Staking metrics logged to CSV only - } - - if (profitableCount > 0) { - string memory filename = string.concat("improved_profitable_", vm.toString(block.timestamp), ".csv"); - vm.writeFile(filename, profitableCSV); - // Results written to CSV - } - } - - function _runImprovedScenario(uint256 seed) internal virtual returns (uint256 finalBalance, bool reachedDiscovery) { - uint256 rand = uint256(keccak256(abi.encodePacked(seed, block.timestamp))); - - // Get initial discovery position - (, int24 discoveryLower, int24 discoveryUpper) = lm.positions(ThreePositionStrategy.Stage.DISCOVERY); - - // Initial buy to generate KRAIKEN tokens for trading/staking - // If staking is enabled with 100% bias, buy even more initially - uint256 initialBuyPercent = (enableStaking && stakingBias >= 100) ? 60 : - (enableStaking ? 40 : 25); // 60% if 100% staking, 40% if staking, 25% otherwise - uint256 initialBuyAmount = weth.balanceOf(trader) * initialBuyPercent / 100; - _executeBuy(trader, initialBuyAmount); - - // Always use random trading strategy for consistent behavior - _executeRandomLargeTrades(rand); - - - // Check if we reached discovery - (, int24 currentTick,,,,,) = pool.slot0(); - reachedDiscovery = (currentTick >= discoveryLower && currentTick < discoveryUpper); - - if (reachedDiscovery) { - // Discovery reached - if (trackPositions) { - _recordPositionData("Discovery_Reached"); - } - } - - // Check final balances before cleanup - uint256 traderKraiken = harberg.balanceOf(trader); - - // Final cleanup: sell all KRAIKEN - if (traderKraiken > 0) { - _executeSell(trader, traderKraiken); - } - - // Calculate final balance - finalBalance = weth.balanceOf(trader); - } - function _executeRandomLargeTrades(uint256 rand) internal { - uint256 stakingAttempts = 0; - for (uint256 i = 0; i < tradesPerRun; i++) { - rand = uint256(keccak256(abi.encodePacked(rand, i))); - - // Use buy bias to determine action - uint256 actionRoll = rand % 100; - uint256 action; - - if (actionRoll < buyBias) { - action = 0; // Buy (biased towards buying when buyBias > 50) - } else if (actionRoll < 90) { - action = 1; // Sell (reduced probability when buyBias is high) - } else { - action = 2; // Recenter (10% chance) - } - - if (action == 0) { - // Large buy (30-80% of balance, or more with high buy bias) - uint256 buyPct = buyBias > 80 ? 60 + (rand % 31) : (buyBias > 70 ? 40 + (rand % 41) : 30 + (rand % 51)); - uint256 wethBalance = weth.balanceOf(trader); - uint256 buyAmount = wethBalance * buyPct / 100; - if (buyAmount > 0 && wethBalance > 0) { - _executeBuy(trader, buyAmount); - } - } else if (action == 1) { - // Large sell (significantly reduced with high buy bias to maintain KRAIKEN balance) - uint256 sellPct = buyBias > 80 ? 5 + (rand % 16) : (buyBias > 70 ? 10 + (rand % 21) : 30 + (rand % 71)); - uint256 sellAmount = harberg.balanceOf(trader) * sellPct / 100; - if (sellAmount > 0) { - _executeSell(trader, sellAmount); - } - } else { - // Recenter - vm.warp(block.timestamp + (rand % 2 hours)); - vm.prank(feeDestination); - try lm.recenter{gas: 50_000_000}() {} catch {} - } - - // Every 3rd trade, attempt staking/unstaking if enabled - if (enableStaking && i % 3 == 2) { - stakingAttempts++; - uint256 stakingRoll = uint256(keccak256(abi.encodePacked(rand, "staking", i))) % 100; - if (stakingRoll < stakingBias) { - // Before staking, ensure we have tokens - uint256 harbBalance = harberg.balanceOf(trader); - uint256 minStakeAmount = harberg.minStake(); - - // With 100% staking bias, aggressively buy tokens if needed - // We want to maintain a large KRAIKEN balance for staking - if (stakingBias >= 100 && harbBalance <= minStakeAmount * 10) { - uint256 wethBalance = weth.balanceOf(trader); - if (wethBalance > 0) { - // Buy 30-50% of ETH worth to get substantial tokens for staking - uint256 buyAmount = wethBalance * (30 + (rand % 21)) / 100; - _executeBuy(trader, buyAmount); - } - } else if (harbBalance <= minStakeAmount * 2) { - uint256 wethBalance = weth.balanceOf(trader); - if (wethBalance > 0) { - // Buy 15-25% of ETH worth to get tokens for staking - uint256 buyAmount = wethBalance * (15 + (rand % 11)) / 100; - _executeBuy(trader, buyAmount); - } - } - - // Now try to stake - _executeStake(rand + i * 1000); - } else { - // Try to unstake - _executeExitPosition(rand + i * 1000); - } - } - - // Only record every 5th trade to avoid memory issues with large trade counts - if (trackPositions && i % 5 == 0) { - _recordPositionData("Trade"); - } - } - - uint256 expectedStakeActions = tradesPerRun / 3; - uint256 expectedStakes = (expectedStakeActions * stakingBias) / 100; - // Staking actions configured - } - - function _executeBuy(address buyer, uint256 amount) internal virtual { - if (amount == 0 || weth.balanceOf(buyer) < amount) return; - - vm.prank(buyer); - weth.transfer(address(swapExecutor), amount); - - try swapExecutor.executeBuy(amount, buyer) {} catch {} - } - - function _executeSell(address seller, uint256 amount) internal virtual { - if (amount == 0 || harberg.balanceOf(seller) < amount) return; - - vm.prank(seller); - harberg.transfer(address(swapExecutor), amount); - - swapExecutor.executeSell(amount, seller); // No try-catch, let errors bubble up - } - - function _getOptimizerByClass(string memory class) internal returns (address) { - if (keccak256(bytes(class)) == keccak256("BullMarketOptimizer")) { - return address(new BullMarketOptimizer()); - } else if (keccak256(bytes(class)) == keccak256("WhaleOptimizer")) { - return address(new WhaleOptimizer()); - } else { - return address(new BullMarketOptimizer()); - } - } - - function _loadConfiguration() internal { - fuzzingRuns = vm.envOr("FUZZING_RUNS", uint256(20)); - trackPositions = vm.envOr("TRACK_POSITIONS", false); - enableStaking = vm.envOr("ENABLE_STAKING", true); // Default to true - buyBias = vm.envOr("BUY_BIAS", uint256(50)); // Default 50% (balanced) - tradesPerRun = vm.envOr("TRADES_PER_RUN", uint256(15)); // Default 15 trades - stakingBias = vm.envOr("STAKING_BIAS", uint256(80)); // Default 80% stake vs 20% unstake - optimizerClass = vm.envOr("OPTIMIZER_CLASS", string("BullMarketOptimizer")); - } - - function _recordPositionData(string memory label) internal { - // Split into separate function calls to avoid stack too deep - _recordPositionDataInternal(label); - } - - function _recordPositionDataInternal(string memory label) private { - // Disable position tracking if it causes memory issues - if (bytes(csv).length > 50000) { - // CSV is getting too large, skip recording - return; - } - - (,int24 currentTick,,,,,) = pool.slot0(); - - // Get position data - (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); - - // Use simpler row building to avoid memory issues - string memory row = label; - row = string.concat(row, ",", vm.toString(currentTick)); - row = string.concat(row, ",", vm.toString(floorLower)); - row = string.concat(row, ",", vm.toString(floorUpper)); - row = string.concat(row, ",", vm.toString(floorLiq)); - row = string.concat(row, ",", vm.toString(anchorLower)); - row = string.concat(row, ",", vm.toString(anchorUpper)); - row = string.concat(row, ",", vm.toString(anchorLiq)); - row = string.concat(row, ",", vm.toString(discoveryLower)); - row = string.concat(row, ",", vm.toString(discoveryUpper)); - row = string.concat(row, ",", vm.toString(discoveryLiq)); - row = string.concat(row, ",", token0isWeth ? "true" : "false"); - row = string.concat(row, ",", vm.toString(stake.getPercentageStaked())); - row = string.concat(row, ",", vm.toString(stake.getAverageTaxRate())); - - appendCSVRow(row); - } - - function _executeStakingAction(uint256 rand) internal { - uint256 action = rand % 100; - - // 70% chance to stake, 5% chance to exit (to fill pool faster) - if (action < 70) { - _executeStake(rand); - } else if (action < 75) { - _executeExitPosition(rand); - } - } - - function _executeStakeWithAmount(address staker, uint256 amount, uint32 taxRate) internal { - // Direct stake with specific amount and tax rate - _doStake(staker, amount, taxRate); - } - - function _executeStake(uint256 rand) internal { - address staker = trader; - uint256 harbBalance = harberg.balanceOf(staker); - uint256 minStakeAmount = harberg.minStake(); - - - if (harbBalance > minStakeAmount) { - // With high staking bias (>= 90%), stake VERY aggressively - uint256 minPct = stakingBias >= 100 ? 50 : (stakingBias >= 90 ? 30 : 10); - uint256 maxPct = stakingBias >= 100 ? 100 : (stakingBias >= 90 ? 70 : 30); - - // Stake between minPct% and maxPct% of balance - uint256 amount = harbBalance * (minPct + (rand % (maxPct - minPct + 1))) / 100; - if (amount < minStakeAmount) { - amount = minStakeAmount; - } - - // With 100% staking bias, allow staking ALL tokens - // Otherwise keep a small reserve - if (stakingBias < 100) { - uint256 maxStake = harbBalance * 90 / 100; // Keep 10% for trading if not 100% bias - if (amount > maxStake) { - amount = maxStake; - } - } - // If stakingBias == 100, no limit - can stake entire balance - - // Initial staking: use lower tax rates (0-15) to enable snatching later - uint32 taxRate = uint32(rand % 16); // 0-15 instead of 0-29 - - vm.prank(staker); - harberg.approve(address(stake), amount); - _doStake(staker, amount, taxRate); - } - // Silently skip if insufficient balance - this is expected behavior - } - - function _doStake(address staker, uint256 amount, uint32 taxRate) internal { - vm.startPrank(staker); - - // Check current pool capacity before attempting stake - uint256 currentPercentStaked = stake.getPercentageStaked(); - - // Log pool status before attempting (currentPercentStaked is in 1e18, where 1e18 = 100%) - if (currentPercentStaked > 95e16) { // > 95% - // Pool near full - } - - // First try to stake without snatching - totalStakesAttempted++; - - - try stake.snatch(amount, staker, taxRate, new uint256[](0)) returns (uint256 positionId) { - totalStakesSucceeded++; - activePositions[staker].push(positionId); - allPositionIds.push(positionId); - if (trackPositions) { - _recordPositionData("Stake"); - } - } catch Error(string memory reason) { - // Caught string error - try snatching - - // Use high tax rate (28) for snatching - can snatch anything with rate 0-27 - uint32 snatchTaxRate = 28; - - // Find positions to snatch (those with lower tax rates) - uint256[] memory positionsToSnatch = _findSnatchablePositions(snatchTaxRate, amount); - - if (positionsToSnatch.length > 0) { - totalSnatchesAttempted++; - try stake.snatch(amount, staker, snatchTaxRate, positionsToSnatch) returns (uint256 positionId) { - totalSnatchesSucceeded++; - activePositions[staker].push(positionId); - allPositionIds.push(positionId); - - // Remove snatched positions from tracking - _removeSnatchedPositions(positionsToSnatch); - - if (trackPositions) { - _recordPositionData("Snatch"); - } - } catch {} - } - } catch { - // Catch-all for non-string errors (likely ExceededAvailableStake) - // Stake failed - trying snatch - - // Now try snatching with high tax rate - uint32 snatchTaxRate = 28; - uint256[] memory positionsToSnatch = _findSnatchablePositions(snatchTaxRate, amount); - - if (positionsToSnatch.length > 0) { - totalSnatchesAttempted++; - try stake.snatch(amount, staker, snatchTaxRate, positionsToSnatch) returns (uint256 positionId) { - totalSnatchesSucceeded++; - activePositions[staker].push(positionId); - allPositionIds.push(positionId); - _removeSnatchedPositions(positionsToSnatch); - - if (trackPositions) { - _recordPositionData("Snatch"); - } - } catch {} - } - } - vm.stopPrank(); - } - - function _executeExitPosition(uint256 rand) internal { - address staker = trader; - - if (activePositions[staker].length > 0) { - uint256 index = rand % activePositions[staker].length; - uint256 positionId = activePositions[staker][index]; - - vm.prank(staker); - try stake.exitPosition(positionId) { - // Remove from array - activePositions[staker][index] = activePositions[staker][activePositions[staker].length - 1]; - activePositions[staker].pop(); - - // Also remove from allPositionIds - for (uint256 j = 0; j < allPositionIds.length; j++) { - if (allPositionIds[j] == positionId) { - allPositionIds[j] = 0; // Mark as removed - break; - } - } - - if (trackPositions) { - _recordPositionData("ExitStake"); - } - } catch { - // Exit failed (position might be liquidated), remove from tracking - activePositions[staker][index] = activePositions[staker][activePositions[staker].length - 1]; - activePositions[staker].pop(); - - // Also remove from allPositionIds - for (uint256 j = 0; j < allPositionIds.length; j++) { - if (allPositionIds[j] == positionId) { - allPositionIds[j] = 0; // Mark as removed - break; - } - } - } - } - } - - function _findSnatchablePositions(uint32 snatchTaxRate, uint256 amountNeeded) internal view returns (uint256[] memory) { - // Find positions with tax rates lower than snatchTaxRate - uint256[] memory snatchable = new uint256[](10); // Max 10 positions to snatch - uint256 count = 0; - uint256 totalShares = 0; - uint256 skippedHighTax = 0; - uint256 skippedEmpty = 0; - - for (uint256 i = 0; i < allPositionIds.length && count < 10; i++) { - uint256 positionId = allPositionIds[i]; - if (positionId == 0) continue; - - // Get position info - (uint256 shares, address owner,, , uint32 taxRate) = stake.positions(positionId); - - // Skip if position doesn't exist or tax rate is too high - if (shares == 0) { - skippedEmpty++; - continue; - } - if (taxRate >= snatchTaxRate) { - skippedHighTax++; - continue; - } - - snatchable[count] = positionId; - totalShares += shares; - count++; - - // Check if we have enough shares to cover the amount needed - uint256 assetsFromShares = stake.sharesToAssets(totalShares); - if (assetsFromShares >= amountNeeded) { - break; - } - } - - - // Resize array to actual count - uint256[] memory result = new uint256[](count); - for (uint256 i = 0; i < count; i++) { - result[i] = snatchable[i]; - } - - return result; - } - - function _removeSnatchedPositions(uint256[] memory snatchedIds) internal { - // Remove snatched positions from allPositionIds - for (uint256 i = 0; i < snatchedIds.length; i++) { - uint256 snatchedId = snatchedIds[i]; - - // Find and remove from allPositionIds - for (uint256 j = 0; j < allPositionIds.length; j++) { - if (allPositionIds[j] == snatchedId) { - allPositionIds[j] = 0; // Mark as removed - break; - } - } - - // Remove from trader's activePositions - uint256[] storage traderPositions = activePositions[trader]; - for (uint256 m = 0; m < traderPositions.length; m++) { - if (traderPositions[m] == snatchedId) { - traderPositions[m] = traderPositions[traderPositions.length - 1]; - traderPositions.pop(); - break; - } - } - } - } - - function _logCurrentPosition(int24 currentTick) internal view { - (, int24 anchorLower, int24 anchorUpper) = lm.positions(ThreePositionStrategy.Stage.ANCHOR); - (, int24 discoveryLower, int24 discoveryUpper) = lm.positions(ThreePositionStrategy.Stage.DISCOVERY); - - if (currentTick >= anchorLower && currentTick <= anchorUpper) { - // In anchor position - } else if (currentTick >= discoveryLower && currentTick <= discoveryUpper) { - // In discovery position - } else { - // Outside all positions - } - } - - function _logInitialFloor() internal view { - // Removed to reduce logging - } - - function _logFinalState() internal view { - // Removed to reduce logging - uint160 sqrtPriceUpper = TickMath.getSqrtRatioAtTick(floorUpper); - - if (tick < floorLower) { - // All liquidity is in KRAIKEN - uint256 kraikenAmount = token0isWeth ? - LiquidityAmounts.getAmount1ForLiquidity(sqrtPriceLower, sqrtPriceUpper, poolLiquidity) : - LiquidityAmounts.getAmount0ForLiquidity(sqrtPriceLower, sqrtPriceUpper, poolLiquidity); - // All liquidity in KRAIKEN - } -} \ No newline at end of file diff --git a/onchain/analysis/README.md b/onchain/analysis/README.md index 1853229..b27f725 100644 --- a/onchain/analysis/README.md +++ b/onchain/analysis/README.md @@ -18,9 +18,9 @@ This directory contains tools for fuzzing the KRAIKEN LiquidityManager to identi ## Files ### Core Scripts -- `ImprovedFuzzingAnalysis.s.sol` - Enhanced fuzzing script with staking support and memory optimizations +- `StreamlinedFuzzing.s.sol` - Streamlined fuzzing script with staking support and accurate trade recording - `run-fuzzing.sh` - Shell script to orchestrate multiple fuzzing runs with configurable parameters -- `clean.sh` - Cleanup script to remove generated files +- `clean-csvs.sh` - Cleanup script to remove generated CSV files ### Helpers - `helpers/SwapExecutor.sol` - Handles swap execution through Uniswap @@ -28,9 +28,8 @@ This directory contains tools for fuzzing the KRAIKEN LiquidityManager to identi - `helpers/CSVHelper.sol` - CSV formatting helpers ### Visualization -- `AnalysisVisualizer.py` - Python script to generate charts from CSV data -- `scenario-visualizer.html` - Interactive web visualization -- `view-scenarios.sh` - Quick script to launch web server for visualization +- `run-visualizer.html` - Interactive web visualization for analyzing individual trades from fuzzing runs +- Supports row-by-row navigation through trades with liquidity distribution charts ## Available Optimizers @@ -78,12 +77,12 @@ Each fuzzing campaign creates a timestamped directory with: To visualize results: ```bash -# Start local web server -./view-scenarios.sh +# Start local web server from analysis directory +cd analysis && python3 -m http.server 8000 +# Then open http://localhost:8000/run-visualizer.html -# Or use Python directly -python3 -m http.server 8000 -# Then open http://localhost:8000/scenario-visualizer.html +# Or use debugCSV mode which automatically launches visualizer +./analysis/run-fuzzing.sh BullMarketOptimizer debugCSV ``` ### Cleanup @@ -116,9 +115,11 @@ To add a new optimizer: ## Notes - Each run deploys a fresh Uniswap V3 environment -- Gas limit is set to 200M for script execution +- Gas limit is set to 300M for script execution - Results are deterministic based on the seed - The fuzzer tests random buy/sell patterns with periodic recenters - Supports staking operations with position snatching mechanics -- Memory-optimized with circular buffer for position tracking -- Records all trades to CSV for complete visualization \ No newline at end of file +- Only records trades that actually execute (non-zero amounts) +- Records actual traded amounts after liquidity limits are applied +- CSV files are written to the analysis/ directory +- Every trade properly updates the tick value \ No newline at end of file diff --git a/onchain/analysis/RecordedFuzzingAnalysis.s.sol b/onchain/analysis/RecordedFuzzingAnalysis.s.sol deleted file mode 100644 index 93add0e..0000000 --- a/onchain/analysis/RecordedFuzzingAnalysis.s.sol +++ /dev/null @@ -1,265 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity ^0.8.19; - -import "./ImprovedFuzzingAnalysis.s.sol"; -import "./helpers/ScenarioRecorder.sol"; - -/** - * @title RecordedFuzzingAnalysis - * @notice Enhanced fuzzing that records profitable scenarios for exact replay - * @dev Captures all actions, states, and parameters when invariants fail - */ -contract RecordedFuzzingAnalysis is ImprovedFuzzingAnalysis { - ScenarioRecorder public recorder; - bool public enableRecording = true; - string public runId; // Unique identifier for this run - bool private isRecordingProfitable; // Only record if scenario is profitable - - // Constructor removed - set trackPositions in run() instead - - // Store actions for current scenario - struct ActionRecord { - uint256 step; - string actionType; - address actor; - uint256 amount; - uint256 timestamp; - uint256 blockNumber; - int24 tickBefore; - int24 tickAfter; - } - - ActionRecord[] currentActions; - - function run() public override { - // Get run ID from environment or generate default - runId = vm.envOr("RUN_ID", string("LOCAL")); - - // Force position tracking to be enabled for CSV generation - vm.setEnv("TRACK_POSITIONS", "true"); - - console.log("=== RECORDED Fuzzing Analysis ==="); - console.log(string.concat("Run ID: ", runId)); - console.log("Recording enabled for profitable scenario replay"); - - super.run(); - } - - // Override to add recording for profitable scenarios - function _runImprovedScenario(uint256 seed) internal override returns (uint256 finalBalance, bool reachedDiscovery) { - // Clear previous scenario - delete currentActions; - isRecordingProfitable = false; - - // Run the base scenario - (finalBalance, reachedDiscovery) = super._runImprovedScenario(seed); - - // Export summary if profitable - uint256 initialBalance = 50 ether; - if (finalBalance > initialBalance && reachedDiscovery) { - _exportSummary(seed, initialBalance, finalBalance, reachedDiscovery); - } - - return (finalBalance, reachedDiscovery); - } - - // Override trade execution to record actions - function _executeBuy(address buyer, uint256 amount) internal override { - if (amount == 0 || weth.balanceOf(buyer) < amount) return; - - (, int24 tickBefore,,,,,) = pool.slot0(); - - // Execute trade - super._executeBuy(buyer, amount); - - (, int24 tickAfter,,,,,) = pool.slot0(); - - // Only record action summary (not full state) - _recordAction("BUY", buyer, amount, tickBefore, tickAfter); - } - - function _executeSell(address seller, uint256 amount) internal override { - if (amount == 0 || harberg.balanceOf(seller) < amount) return; - - (, int24 tickBefore,,,,,) = pool.slot0(); - - // Execute trade - super._executeSell(seller, amount); - - (, int24 tickAfter,,,,,) = pool.slot0(); - - // Only record action summary (not full state) - _recordAction("SELL", seller, amount, tickBefore, tickAfter); - } - - function _executeRecenter() internal { - (, int24 tickBefore,,,,,) = pool.slot0(); - - vm.warp(block.timestamp + 1 hours); - vm.prank(feeDestination); - try lm.recenter{gas: 50_000_000}() {} catch {} - - (, int24 tickAfter,,,,,) = pool.slot0(); - - // Only record action summary - _recordAction("RECENTER", feeDestination, 0, tickBefore, tickAfter); - } - - function _recordState(string memory label, address actor, uint256 amount) internal { - // Only record states if we're exporting a profitable scenario - if (!isRecordingProfitable || !enableRecording) return; - - (uint160 sqrtPriceX96, int24 currentTick,,,,,) = pool.slot0(); - - recorder.recordPreState( - weth.balanceOf(trader), - harberg.balanceOf(trader), - currentTick, - uint256(sqrtPriceX96), - 0, // VWAP placeholder - harberg.outstandingSupply() - ); - } - - function _recordAction( - string memory actionType, - address actor, - uint256 amount, - int24 tickBefore, - int24 tickAfter - ) internal { - currentActions.push(ActionRecord({ - step: currentActions.length, - actionType: actionType, - actor: actor, - amount: amount, - timestamp: block.timestamp, - blockNumber: block.number, - tickBefore: tickBefore, - tickAfter: tickAfter - })); - } - - function _exportSummary( - uint256 seed, - uint256 initialBalance, - uint256 finalBalance, - bool reachedDiscovery - ) internal { - console.log("\n[RECORDING] Exporting profitable scenario..."); - console.log(string.concat(" Run ID: ", runId)); - console.log(string.concat(" Seed: ", vm.toString(seed))); - - string memory summary = _generateActionSummary( - seed, - initialBalance, - finalBalance, - reachedDiscovery - ); - string memory summaryFilename = string.concat( - "scenario_summary_seed", vm.toString(seed), ".txt" - ); - vm.writeFile(summaryFilename, summary); - console.log(string.concat(" Summary exported to: ", summaryFilename)); - - if (currentActions.length > 0) { - string memory replayScript = _generateReplayScript(seed); - string memory scriptFilename = string.concat( - "replay_script_seed", vm.toString(seed), ".sol" - ); - vm.writeFile(scriptFilename, replayScript); - console.log(string.concat(" Replay script exported to: ", scriptFilename)); - } - } - - function _generateReplayScript(uint256 seed) internal view returns (string memory) { - string memory script = string.concat( - "// Replay script for profitable scenario\n", - "// Seed: ", vm.toString(seed), "\n", - "// Optimizer: ", optimizerClass, "\n", - "// Discovery reached: YES\n\n", - "function replayScenario() public {\n" - ); - - for (uint256 i = 0; i < currentActions.length; i++) { - ActionRecord memory action = currentActions[i]; - - script = string.concat(script, " // Step ", vm.toString(i), "\n"); - - if (keccak256(bytes(action.actionType)) == keccak256("BUY")) { - script = string.concat( - script, - " _executeBuy(", - action.actor == trader ? "trader" : "system", - ", ", vm.toString(action.amount), ");\n" - ); - } else if (keccak256(bytes(action.actionType)) == keccak256("SELL")) { - script = string.concat( - script, - " _executeSell(", - action.actor == trader ? "trader" : "system", - ", ", vm.toString(action.amount), ");\n" - ); - } else if (keccak256(bytes(action.actionType)) == keccak256("RECENTER")) { - script = string.concat( - script, - " vm.warp(", vm.toString(action.timestamp), ");\n", - " vm.prank(feeDestination);\n", - " lm.recenter();\n" - ); - } - - script = string.concat( - script, - " // Tick moved from ", vm.toString(action.tickBefore), - " to ", vm.toString(action.tickAfter), "\n\n" - ); - } - - script = string.concat(script, "}\n"); - return script; - } - - function _generateActionSummary( - uint256 seed, - uint256 initialBalance, - uint256 finalBalance, - bool reachedDiscovery - ) internal view returns (string memory) { - uint256 profit = finalBalance - initialBalance; - uint256 profitPct = (profit * 100) / initialBalance; - - string memory summary = string.concat( - "=== PROFITABLE SCENARIO SUMMARY ===\n", - "Run ID: ", runId, "\n", - "Seed: ", vm.toString(seed), "\n", - "Optimizer: ", optimizerClass, "\n", - "Discovery Reached: ", reachedDiscovery ? "YES" : "NO", "\n\n", - "Financial Results:\n", - " Initial Balance: ", vm.toString(initialBalance / 1e18), " ETH\n", - " Final Balance: ", vm.toString(finalBalance / 1e18), " ETH\n", - " Profit: ", vm.toString(profit / 1e18), " ETH (", vm.toString(profitPct), "%)\n\n", - "Action Sequence:\n" - ); - - for (uint256 i = 0; i < currentActions.length; i++) { - ActionRecord memory action = currentActions[i]; - summary = string.concat( - summary, - " ", vm.toString(i + 1), ". ", - action.actionType, " - ", - action.actor == trader ? "Trader" : "System", - action.amount > 0 ? string.concat(" - Amount: ", vm.toString(action.amount / 1e18), " ETH") : "", - " - Tick: ", vm.toString(action.tickBefore), " -> ", vm.toString(action.tickAfter), - "\n" - ); - } - - summary = string.concat( - summary, - "\nThis scenario can be replayed using the generated replay script.\n" - ); - - return summary; - } -} \ No newline at end of file diff --git a/onchain/analysis/StreamlinedFuzzing.s.sol b/onchain/analysis/StreamlinedFuzzing.s.sol new file mode 100644 index 0000000..109ad41 --- /dev/null +++ b/onchain/analysis/StreamlinedFuzzing.s.sol @@ -0,0 +1,346 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import "forge-std/Script.sol"; +import "forge-std/console.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")); + + console.log("=== Streamlined Fuzzing Analysis ==="); + console.log("Optimizer:", optimizerClass); + console.log("Runs:", numRuns); + console.log("Trades per run:", tradesPerRun); + + // 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))); + console.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 + uint256 traderFund = 50 ether + (uint256(keccak256(abi.encodePacked(runIndex, "trader"))) % 150 ether); + + vm.deal(trader, traderFund * 2); + vm.prank(trader); + weth.deposit{value: traderFund}(); + + // Initial state + _recordState("INIT", 0); + + // Debug: Check initial liquidity manager state + console.log("LM ETH balance:", address(lm).balance); + console.log("Pool address:", address(pool)); + + // Execute trades + for (uint256 i = 0; i < tradesPerRun; i++) { + // Check for recenter opportunity every 5 trades + if (i > 0 && i % 5 == 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 + _recordState("FINAL", 0); + } + + console.log("\n=== Analysis Complete ==="); + console.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) { + console.log("Initial recenter successful, isUp:", isUp); + } catch Error(string memory reason) { + console.log("Initial recenter failed:", reason); + } catch { + console.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) { + console.log("Buy returned 0, requested:", amount); + } + _recordState("BUY", actualAmount); + } catch Error(string memory reason) { + console.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 { + uint256 blocksSinceRecenter = block.number - lastRecenterBlock; + if (blocksSinceRecenter > 100) { + 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 = 1 ether + (uint256(keccak256(abi.encodePacked(runIndex, tradeIndex))) % 10 ether); + return isBuy ? baseAmount : baseAmount * 1000; + } + + 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); + } +} \ No newline at end of file diff --git a/onchain/analysis/anchor-width-calculations.md b/onchain/analysis/anchor-width-calculations.md deleted file mode 100644 index 0e58fe5..0000000 --- a/onchain/analysis/anchor-width-calculations.md +++ /dev/null @@ -1,84 +0,0 @@ -# AnchorWidth Price Range Calculations - -## Understanding the Formula - -From the code: -```solidity -int24 anchorSpacing = TICK_SPACING + (34 * int24(params.anchorWidth) * TICK_SPACING / 100); -``` - -Where: -- `TICK_SPACING = 200` (for 1% fee tier pools) -- `anchorWidth` ranges from 1 to 100 - -## Tick to Price Conversion - -In Uniswap V3: -- Each tick represents a 0.01% (1 basis point) price change -- Price at tick i = 1.0001^i -- So a tick difference of 100 = ~1.01% price change -- A tick difference of 10,000 = ~2.718x price change - -## AnchorWidth to Price Range Mapping - -Let's calculate the actual price ranges for different anchorWidth values: - -### Formula Breakdown: -- `anchorSpacing = 200 + (34 * anchorWidth * 200 / 100)` -- `anchorSpacing = 200 + (68 * anchorWidth)` -- `anchorSpacing = 200 * (1 + 0.34 * anchorWidth)` - -The anchor position extends from: -- Lower bound: `currentTick - anchorSpacing` -- Upper bound: `currentTick + anchorSpacing` -- Total width: `2 * anchorSpacing` ticks - -### Price Range Calculations: - -| anchorWidth | anchorSpacing (ticks) | Total Width (ticks) | Lower Price Ratio | Upper Price Ratio | Price Range | -|-------------|----------------------|---------------------|-------------------|-------------------|-------------| -| 1% | 268 | 536 | 0.974 | 1.027 | ±2.7% | -| 5% | 540 | 1080 | 0.947 | 1.056 | ±5.5% | -| 10% | 880 | 1760 | 0.916 | 1.092 | ±9.0% | -| 20% | 1560 | 3120 | 0.855 | 1.170 | ±16% | -| 30% | 2240 | 4480 | 0.800 | 1.251 | ±25% | -| 40% | 2920 | 5840 | 0.748 | 1.336 | ±33% | -| 50% | 3600 | 7200 | 0.700 | 1.429 | ±42% | -| 60% | 4280 | 8560 | 0.654 | 1.528 | ±52% | -| 70% | 4960 | 9920 | 0.612 | 1.635 | ±63% | -| 80% | 5640 | 11280 | 0.572 | 1.749 | ±74% | -| 90% | 6320 | 12640 | 0.535 | 1.871 | ±87% | -| 100% | 7000 | 14000 | 0.500 | 2.000 | ±100% | - -## Key Insights: - -1. **Linear Tick Scaling**: The tick spacing scales linearly with anchorWidth -2. **Non-Linear Price Scaling**: Due to exponential nature of tick-to-price conversion -3. **Asymmetric Percentages**: The percentage move down is smaller than up (e.g., 100% width = -50% to +100%) - -## Practical Examples: - -### anchorWidth = 50 (Common Default) -- If current price is $1.00: - - Lower bound: $0.70 (-30%) - - Upper bound: $1.43 (+43%) - - Captures moderate price movements in both directions - -### anchorWidth = 100 (Maximum) -- If current price is $1.00: - - Lower bound: $0.50 (-50%) - - Upper bound: $2.00 (+100%) - - Price can double or halve while staying in range - -### anchorWidth = 10 (Narrow) -- If current price is $1.00: - - Lower bound: $0.92 (-8%) - - Upper bound: $1.09 (+9%) - - Highly concentrated, requires frequent rebalancing - -## Important Notes: - -1. The anchor position does NOT extend to 2x or 3x the price at maximum width -2. At anchorWidth = 100, the upper bound is exactly 2x the current price -3. The formula `200 + (34 * anchorWidth * 200 / 100)` creates a sensible progression from tight to wide ranges -4. The minimum spacing (anchorWidth = 0) would be 200 ticks (±1% range), but minimum allowed is 1 \ No newline at end of file diff --git a/onchain/analysis/clean-csvs.sh b/onchain/analysis/clean-csvs.sh new file mode 100755 index 0000000..cd3e0bd --- /dev/null +++ b/onchain/analysis/clean-csvs.sh @@ -0,0 +1,16 @@ +#!/bin/bash + +# Clean up fuzzing CSV files from the analysis directory + +echo "Cleaning up fuzzing CSV files..." + +# Count files before deletion +COUNT=$(ls -1 analysis/fuzz-*.csv 2>/dev/null | wc -l) + +if [ "$COUNT" -gt 0 ]; then + # Remove all fuzzing CSV files from analysis folder + rm -f analysis/fuzz-*.csv + echo "Removed $COUNT CSV files from analysis/ directory" +else + echo "No CSV files to clean" +fi \ No newline at end of file diff --git a/onchain/analysis/helpers/SwapExecutor.sol b/onchain/analysis/helpers/SwapExecutor.sol index fd469c7..1a8dd51 100644 --- a/onchain/analysis/helpers/SwapExecutor.sol +++ b/onchain/analysis/helpers/SwapExecutor.sol @@ -28,15 +28,15 @@ contract SwapExecutor { liquidityManager = _liquidityManager; } - function executeBuy(uint256 amount, address recipient) external { + function executeBuy(uint256 amount, address recipient) external returns (uint256) { // Calculate maximum safe buy amount based on liquidity uint256 maxBuyAmount = LiquidityBoundaryHelper.calculateBuyLimit(pool, liquidityManager, token0isWeth); // Cap the amount to the safe limit uint256 safeAmount = amount > maxBuyAmount ? maxBuyAmount : amount; - // Skip if no liquidity available - if (safeAmount == 0) return; + // Skip if amount is zero + if (safeAmount == 0) return 0; // For buying HARB with WETH, we're swapping in the direction that increases HARB price // zeroForOne = true if WETH is token0, false if WETH is token1 @@ -59,17 +59,19 @@ contract SwapExecutor { sqrtPriceLimitX96, "" ); + + return safeAmount; } - function executeSell(uint256 amount, address recipient) external { + function executeSell(uint256 amount, address recipient) external returns (uint256) { // Calculate maximum safe sell amount based on liquidity uint256 maxSellAmount = LiquidityBoundaryHelper.calculateSellLimit(pool, liquidityManager, token0isWeth); // Cap the amount to the safe limit uint256 safeAmount = amount > maxSellAmount ? maxSellAmount : amount; - // Skip if no liquidity available - if (safeAmount == 0) return; + // Skip if amount is zero + if (safeAmount == 0) return 0; // For selling HARB for WETH, we're swapping in the direction that decreases HARB price // zeroForOne = false if WETH is token0, true if WETH is token1 @@ -92,6 +94,8 @@ contract SwapExecutor { sqrtPriceLimitX96, "" ); + + return safeAmount; } // Callback required for Uniswap V3 swaps diff --git a/onchain/analysis/replay-scenario.sh b/onchain/analysis/replay-scenario.sh deleted file mode 100755 index 22cfc7d..0000000 --- a/onchain/analysis/replay-scenario.sh +++ /dev/null @@ -1,211 +0,0 @@ -#!/bin/bash - -# Colors for output -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -BLUE='\033[0;34m' -NC='\033[0m' # No Color -BOLD='\033[1m' - -# Check arguments -if [ $# -lt 1 ]; then - echo "Usage: $0 [seed_number]" - echo "" - echo "Examples:" - echo " $0 241218-A7K9 # Replay all scenarios from run" - echo " $0 241218-A7K9 1 # Replay specific seed from run" - echo "" - echo "To find RUN_IDs, check fuzzing_results_recorded_* directories" - exit 1 -fi - -RUN_ID=$1 -SEED=$2 - -echo -e "${GREEN}=== Scenario Replay Tool ===${NC}" -echo -e "Run ID: ${BOLD}${RUN_ID}${NC}" - -# Find the results directory -RESULTS_DIR=$(find . -type d -name "*_*" 2>/dev/null | grep -E "fuzzing_results_recorded.*" | xargs -I {} sh -c 'ls -1 {}/scenario_'${RUN_ID}'_*.json 2>/dev/null && echo {}' | tail -1) - -if [ -z "$RESULTS_DIR" ]; then - echo -e "${RED}Error: No results found for Run ID ${RUN_ID}${NC}" - echo "" - echo "Available Run IDs:" - find . -type f -name "scenario_*_seed*.json" 2>/dev/null | sed 's/.*scenario_\(.*\)_seed.*/\1/' | sort -u - exit 1 -fi - -echo "Found results in: $RESULTS_DIR" -echo "" - -# If no seed specified, show available scenarios -if [ -z "$SEED" ]; then - echo -e "${YELLOW}Available scenarios for ${RUN_ID}:${NC}" - for summary in $RESULTS_DIR/summary_${RUN_ID}_seed*.txt; do - if [ -f "$summary" ]; then - SEED_NUM=$(echo $summary | sed "s/.*seed\(.*\)\.txt/\1/") - echo "" - echo -e "${BOLD}Seed $SEED_NUM:${NC}" - grep -E "Profit:|Discovery Reached:" "$summary" | head -2 - fi - done - echo "" - echo "To replay a specific scenario, run:" - echo " $0 ${RUN_ID} " - exit 0 -fi - -# Check if specific scenario files exist -SCENARIO_FILE="$RESULTS_DIR/scenario_${RUN_ID}_seed${SEED}.json" -REPLAY_FILE="$RESULTS_DIR/replay_${RUN_ID}_seed${SEED}.sol" -SUMMARY_FILE="$RESULTS_DIR/summary_${RUN_ID}_seed${SEED}.txt" - -if [ ! -f "$SCENARIO_FILE" ]; then - echo -e "${RED}Error: Scenario file not found for seed ${SEED}${NC}" - echo "Looking for: $SCENARIO_FILE" - exit 1 -fi - -echo -e "${GREEN}=== Scenario Details ===${NC}" -if [ -f "$SUMMARY_FILE" ]; then - cat "$SUMMARY_FILE" | head -20 - echo "" -fi - -# Create replay test file -# Replace hyphens with underscores for valid Solidity identifier -CONTRACT_SAFE_ID=$(echo $RUN_ID | tr '-' '_') -REPLAY_TEST="test/Replay_${RUN_ID}_Seed${SEED}.t.sol" - -echo -e "${YELLOW}Creating replay test file...${NC}" -cat > $REPLAY_TEST << 'EOF' -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.19; - -import "forge-std/Test.sol"; -import {TestEnvironment} from "./helpers/TestBase.sol"; -import {IUniswapV3Pool} from "@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.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 "../analysis/helpers/SwapExecutor.sol"; -import "../test/mocks/BullMarketOptimizer.sol"; - -contract Replay_CONTRACT_ID_Seed_SEED is Test { - TestEnvironment testEnv; - IUniswapV3Pool pool; - IWETH9 weth; - Kraiken kraiken; - Stake stake; - LiquidityManager lm; - bool token0isWeth; - - address trader = makeAddr("trader"); - address whale = makeAddr("whale"); - address feeDestination = makeAddr("fees"); - - function setUp() public { - // Setup from recorded scenario - testEnv = new TestEnvironment(feeDestination); - BullMarketOptimizer optimizer = new BullMarketOptimizer(); - - (,pool, weth, kraiken, stake, lm,, token0isWeth) = - testEnv.setupEnvironmentWithOptimizer(SEED_PARITY, feeDestination, address(optimizer)); - - vm.deal(address(lm), 200 ether); - - // Fund traders based on seed - uint256 traderFund = 50 ether + (uint256(keccak256(abi.encodePacked(uint256(SEED_NUM), "trader"))) % 150 ether); - uint256 whaleFund = 200 ether + (uint256(keccak256(abi.encodePacked(uint256(SEED_NUM), "whale"))) % 300 ether); - - vm.deal(trader, traderFund * 2); - vm.prank(trader); - weth.deposit{value: traderFund}(); - - vm.deal(whale, whaleFund * 2); - vm.prank(whale); - weth.deposit{value: whaleFund}(); - - vm.prank(feeDestination); - lm.recenter(); - } - - function test_replay_CONTRACT_ID_seed_SEED() public { - console.log("=== Replaying Scenario RUN_ID Seed SEED ==="); - - uint256 initialBalance = weth.balanceOf(trader); - - // INSERT_REPLAY_ACTIONS - - uint256 finalBalance = weth.balanceOf(trader); - - if (finalBalance > initialBalance) { - uint256 profit = finalBalance - initialBalance; - uint256 profitPct = (profit * 100) / initialBalance; - console.log(string.concat("[INVARIANT VIOLATED] Profit: ", vm.toString(profit / 1e18), " ETH (", vm.toString(profitPct), "%)")); - revert("Trader profited - invariant violated"); - } else { - console.log("[OK] No profit"); - } - } - - function _executeBuy(address buyer, uint256 amount) internal { - if (weth.balanceOf(buyer) < amount) return; - SwapExecutor executor = new SwapExecutor(pool, weth, kraiken, token0isWeth, lm); - vm.prank(buyer); - weth.transfer(address(executor), amount); - try executor.executeBuy(amount, buyer) {} catch {} - } - - function _executeSell(address seller, uint256 amount) internal { - if (kraiken.balanceOf(seller) < amount) { - amount = kraiken.balanceOf(seller); - if (amount == 0) return; - } - SwapExecutor executor = new SwapExecutor(pool, weth, kraiken, token0isWeth, lm); - vm.prank(seller); - kraiken.transfer(address(executor), amount); - try executor.executeSell(amount, seller) {} catch {} - } -} -EOF - -# Replace placeholders -sed -i "s/CONTRACT_ID/${CONTRACT_SAFE_ID}/g" $REPLAY_TEST -sed -i "s/RUN_ID/${RUN_ID}/g" $REPLAY_TEST -sed -i "s/SEED_NUM/${SEED}/g" $REPLAY_TEST -sed -i "s/SEED_PARITY/$([ $((SEED % 2)) -eq 0 ] && echo "true" || echo "false")/g" $REPLAY_TEST -sed -i "s/_SEED/_${SEED}/g" $REPLAY_TEST - -# Insert replay actions from the sol file -if [ -f "$REPLAY_FILE" ]; then - # Extract the function body from replay script - ACTIONS=$(sed -n '/function replayScenario/,/^}/p' "$REPLAY_FILE" | sed '1d;$d' | sed 's/^/ /') - - # Use a temporary file for the replacement - awk -v actions="$ACTIONS" '/INSERT_REPLAY_ACTIONS/ {print actions; next} {print}' $REPLAY_TEST > ${REPLAY_TEST}.tmp - mv ${REPLAY_TEST}.tmp $REPLAY_TEST -fi - -echo -e "${GREEN}Replay test created: $REPLAY_TEST${NC}" -echo "" - -# Run the replay test -echo -e "${YELLOW}Running replay test...${NC}" -echo "" - -forge test --match-contract Replay_${CONTRACT_SAFE_ID}_Seed_${SEED} -vv - -echo "" -echo -e "${GREEN}=== Replay Complete ===${NC}" -echo "" -echo "To debug further:" -echo " 1. Edit the test file: $REPLAY_TEST" -echo " 2. Add console.log statements or assertions" -echo " 3. Run with more verbosity: forge test --match-contract Replay_${CONTRACT_SAFE_ID}_Seed_${SEED} -vvvv" -echo "" -echo "To visualize positions:" -echo " ./analysis/view-scenarios.sh $RESULTS_DIR" \ No newline at end of file diff --git a/onchain/analysis/requirements.txt b/onchain/analysis/requirements.txt deleted file mode 100644 index 95f51d5..0000000 --- a/onchain/analysis/requirements.txt +++ /dev/null @@ -1,4 +0,0 @@ -pandas>=1.3.0 -matplotlib>=3.4.0 -seaborn>=0.11.0 -numpy>=1.21.0 \ No newline at end of file diff --git a/onchain/analysis/run-fuzzing.sh b/onchain/analysis/run-fuzzing.sh index c03ff9d..f13ece8 100755 --- a/onchain/analysis/run-fuzzing.sh +++ b/onchain/analysis/run-fuzzing.sh @@ -1,10 +1,11 @@ #!/bin/bash -# Usage: ./run-improved-fuzzing.sh [optimizer] [runs=N] [staking=on|off] [buybias=N] [trades=N] [stakingbias=N] +# Usage: ./run-fuzzing.sh [optimizer] [runs=N] [staking=on|off] [buybias=N] [trades=N] [stakingbias=N] [debugCSV] # Examples: -# ./run-improved-fuzzing.sh BullMarketOptimizer runs=50 -# ./run-improved-fuzzing.sh WhaleOptimizer runs=20 staking=off -# ./run-improved-fuzzing.sh BullMarketOptimizer runs=200 staking=on buybias=100 trades=30 stakingbias=95 +# ./run-fuzzing.sh BullMarketOptimizer runs=50 +# ./run-fuzzing.sh WhaleOptimizer runs=20 staking=off +# ./run-fuzzing.sh BullMarketOptimizer runs=200 staking=on buybias=100 trades=30 stakingbias=95 +# ./run-fuzzing.sh BullMarketOptimizer debugCSV # Opens HTML visualizer after generating CSVs # Colors for output RED='\033[0;31m' @@ -16,11 +17,23 @@ BOLD='\033[1m' # Configuration OPTIMIZER=${1:-BullMarketOptimizer} -RUNS=${2:-runs=20} -STAKING=${3:-staking=on} -BUYBIAS=${4:-buybias=50} -TRADES=${5:-trades=15} -STAKINGBIAS=${6:-stakingbias=80} + +# Check if second parameter is debugCSV +DEBUG_CSV=false +if [[ "$2" == "debugCSV" ]]; then + DEBUG_CSV=true + RUNS="runs=3" + STAKING="staking=on" + BUYBIAS="buybias=50" + TRADES="trades=5" + STAKINGBIAS="stakingbias=80" +else + RUNS=${2:-runs=20} + STAKING=${3:-staking=on} + BUYBIAS=${4:-buybias=50} + TRADES=${5:-trades=15} + STAKINGBIAS=${6:-stakingbias=80} +fi # Parse runs parameter if [[ $RUNS == runs=* ]]; then @@ -56,8 +69,7 @@ if [[ $STAKINGBIAS == stakingbias=* ]]; then STAKINGBIAS_VALUE=${STAKINGBIAS#stakingbias=} fi -TIMESTAMP=$(date +%Y%m%d_%H%M%S) -OUTPUT_DIR="fuzzing_results_${OPTIMIZER}_${TIMESTAMP}" +# Ensure we're in the onchain directory (script should be run from there) echo -e "${GREEN}=== Fuzzing Analysis ===${NC}" echo "Optimizer: $OPTIMIZER" @@ -68,7 +80,6 @@ if [ "$STAKING_ENABLED" = "true" ]; then echo "Staking bias: $STAKINGBIAS_VALUE%" fi echo "Buy bias: $BUYBIAS_VALUE%" -echo "Output directory: $OUTPUT_DIR" echo "" # Validate optimizer @@ -83,80 +94,100 @@ case $OPTIMIZER in ;; esac -# Create output directory -mkdir -p $OUTPUT_DIR - -# Run the fuzzing analysis +# Run the streamlined fuzzing analysis echo -e "${YELLOW}Starting fuzzing analysis...${NC}" echo "" +# Record existing CSV files in analysis folder before running +EXISTING_CSVS=$(ls -1 analysis/fuzz-????-???.csv 2>/dev/null | sort) + FUZZING_RUNS=$RUNS_VALUE \ OPTIMIZER_CLASS=$OPTIMIZER \ ENABLE_STAKING=$STAKING_ENABLED \ BUY_BIAS=$BUYBIAS_VALUE \ TRADES_PER_RUN=$TRADES_VALUE \ STAKING_BIAS=$STAKINGBIAS_VALUE \ -TRACK_POSITIONS=true \ -forge script analysis/ImprovedFuzzingAnalysis.s.sol:ImprovedFuzzingAnalysis --gas-limit 200000000 -vv 2>&1 | tee $OUTPUT_DIR/fuzzing.log +forge script analysis/StreamlinedFuzzing.s.sol:StreamlinedFuzzing --skip-simulation --gas-estimate-multiplier 300 -vv 2>&1 -# Extract key metrics +# Analysis complete echo "" echo -e "${GREEN}=== ANALYSIS COMPLETE ===${NC}" -# Show summary from log -tail -20 $OUTPUT_DIR/fuzzing.log | grep -E "Total scenarios|Profitable|Discovery|Stakes|Snatches" || true - -# Check for position CSVs -POSITION_CSV_COUNT=$(ls -1 improved_positions_*.csv 2>/dev/null | wc -l) - -if [ $POSITION_CSV_COUNT -gt 0 ]; then - # Move position CSVs to output directory - mv improved_positions_*.csv $OUTPUT_DIR/ - echo "" - echo -e "${GREEN}Position CSVs saved to: $OUTPUT_DIR/${NC}" - - # Calculate max staking percentage - echo "" - echo -e "${YELLOW}=== Maximum Staking Level ===${NC}" - for f in $OUTPUT_DIR/improved_positions_*.csv; do - tail -1 "$f" | cut -d',' -f13 - done | sort -n | tail -1 | awk '{ - pct = $1/1e16 - printf "%.2f%% of authorized stake (%.2f%% of KRAIKEN supply)\n", pct, pct*0.2 - }' -fi - -# Check for snatching activity -echo "" -echo -e "${YELLOW}=== Snatching Activity ===${NC}" -SNATCH_COUNT=$(grep -c "SNATCHED" $OUTPUT_DIR/fuzzing.log 2>/dev/null || true) -if [ -z "$SNATCH_COUNT" ]; then - SNATCH_COUNT="0" -fi -if [ "$SNATCH_COUNT" -gt "0" ]; then - echo -e "${GREEN}Snatching observed! Found $SNATCH_COUNT snatching events${NC}" - grep "SNATCHED" $OUTPUT_DIR/fuzzing.log | head -5 +# Find newly generated CSV files in analysis folder +NEW_CSVS=$(ls -1 analysis/fuzz-????-???.csv 2>/dev/null | sort) +# Get the scenario code from the first new file +if [ "$EXISTING_CSVS" != "$NEW_CSVS" ]; then + SCENARIO_CODE=$(echo "$NEW_CSVS" | grep -v -F "$EXISTING_CSVS" | head -1 | sed 's/fuzz-\(....\).*/\1/') else - echo "No snatching observed in this run" + SCENARIO_CODE="UNKN" fi +# Count generated CSV files for this run +CSV_COUNT=$(ls -1 analysis/fuzz-${SCENARIO_CODE}-*.csv 2>/dev/null | wc -l) +echo "Generated $CSV_COUNT CSV files with scenario ID: ${SCENARIO_CODE}" + # Check for profitable scenarios -PROFITABLE_COUNT=$(grep -c "PROFITABLE!" $OUTPUT_DIR/fuzzing.log 2>/dev/null || true) -if [ -z "$PROFITABLE_COUNT" ]; then - PROFITABLE_COUNT="0" -fi -if [ "$PROFITABLE_COUNT" -gt "0" ]; then - echo "" - echo -e "${GREEN}=== PROFITABLE SCENARIOS FOUND ===${NC}" - echo "Found $PROFITABLE_COUNT profitable scenarios" - grep "PROFITABLE!" $OUTPUT_DIR/fuzzing.log | head -5 +echo "" +echo -e "${YELLOW}=== Checking for Profitable Scenarios ===${NC}" +PROFITABLE_COUNT=0 +for csv in analysis/fuzz-${SCENARIO_CODE}-*.csv; do + if [ -f "$csv" ]; then + # Get INIT and FINAL rows + INIT_ETH=$(grep "^INIT," "$csv" | cut -d',' -f13) + FINAL_ETH=$(grep "^FINAL," "$csv" | cut -d',' -f13) + + if [ ! -z "$INIT_ETH" ] && [ ! -z "$FINAL_ETH" ]; then + if [ "$FINAL_ETH" -gt "$INIT_ETH" ]; then + PROFIT_PCT=$(echo "scale=1; ($FINAL_ETH - $INIT_ETH) * 100 / $INIT_ETH" | bc -l 2>/dev/null || echo "0") + echo -e "${GREEN} $csv: PROFITABLE (+${PROFIT_PCT}%)${NC}" + ((PROFITABLE_COUNT++)) + fi + fi + fi +done + +if [ $PROFITABLE_COUNT -eq 0 ]; then + echo " No profitable scenarios found" +else + echo -e "${GREEN}Found $PROFITABLE_COUNT profitable scenarios${NC}" fi echo "" -echo -e "${GREEN}Full results saved to: $OUTPUT_DIR/${NC}" -echo "" -echo "To view detailed logs:" -echo " cat $OUTPUT_DIR/fuzzing.log" -echo "" -echo "To visualize position movements (if CSVs generated):" -echo " ./analysis/view-scenarios.sh $OUTPUT_DIR" \ No newline at end of file +echo -e "${GREEN}CSV files generated with scenario ID: ${SCENARIO_CODE}${NC}" + +# Launch HTML visualizer if debugCSV mode +if [ "$DEBUG_CSV" = true ]; then + echo "" + echo -e "${YELLOW}Launching HTML visualizer...${NC}" + + # Check if Python3 is available + if command -v python3 &> /dev/null; then + echo "Starting local server from analysis folder at http://localhost:8000" + echo "Open http://localhost:8000/run-visualizer.html to view results" + # Start server from analysis directory so CSV files are directly accessible + cd analysis + python3 -m http.server 8000 --bind 127.0.0.1 & + SERVER_PID=$! + + # Try to open browser (cross-platform) + sleep 1 + if command -v open &> /dev/null; then + # macOS + open "http://localhost:8000/run-visualizer.html" + elif command -v xdg-open &> /dev/null; then + # Linux + xdg-open "http://localhost:8000/run-visualizer.html" + elif command -v wslview &> /dev/null; then + # WSL + wslview "http://localhost:8000/run-visualizer.html" + fi + + echo "" + echo "Press Ctrl+C to stop the server" + wait $SERVER_PID + else + echo "Python3 not found. To view results:" + echo " cd analysis && python3 -m http.server 8000" + echo " Then open http://localhost:8000/run-visualizer.html in your browser" + fi +fi \ No newline at end of file diff --git a/onchain/analysis/run-improved-fuzzing.sh b/onchain/analysis/run-improved-fuzzing.sh deleted file mode 100755 index 6570803..0000000 --- a/onchain/analysis/run-improved-fuzzing.sh +++ /dev/null @@ -1,162 +0,0 @@ -#!/bin/bash - -# Usage: ./run-improved-fuzzing.sh [optimizer] [runs=N] [staking=on|off] [buybias=N] [trades=N] [stakingbias=N] -# Examples: -# ./run-improved-fuzzing.sh BullMarketOptimizer runs=50 -# ./run-improved-fuzzing.sh WhaleOptimizer runs=20 staking=off -# ./run-improved-fuzzing.sh BullMarketOptimizer runs=200 staking=on buybias=100 trades=30 stakingbias=95 - -# Colors for output -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -BLUE='\033[0;34m' -NC='\033[0m' # No Color -BOLD='\033[1m' - -# Configuration -OPTIMIZER=${1:-BullMarketOptimizer} -RUNS=${2:-runs=20} -STAKING=${3:-staking=on} -BUYBIAS=${4:-buybias=50} -TRADES=${5:-trades=15} -STAKINGBIAS=${6:-stakingbias=80} - -# Parse runs parameter -if [[ $RUNS == runs=* ]]; then - RUNS_VALUE=${RUNS#runs=} -else - RUNS_VALUE=$RUNS -fi - -# Parse staking parameter -STAKING_ENABLED="true" -if [[ $STAKING == staking=* ]]; then - STAKING_VALUE=${STAKING#staking=} - if [[ $STAKING_VALUE == "off" ]] || [[ $STAKING_VALUE == "false" ]] || [[ $STAKING_VALUE == "0" ]]; then - STAKING_ENABLED="false" - fi -fi - -# Parse buy bias parameter -BUYBIAS_VALUE="50" -if [[ $BUYBIAS == buybias=* ]]; then - BUYBIAS_VALUE=${BUYBIAS#buybias=} -fi - -# Parse trades parameter -TRADES_VALUE="15" -if [[ $TRADES == trades=* ]]; then - TRADES_VALUE=${TRADES#trades=} -fi - -# Parse staking bias parameter -STAKINGBIAS_VALUE="80" -if [[ $STAKINGBIAS == stakingbias=* ]]; then - STAKINGBIAS_VALUE=${STAKINGBIAS#stakingbias=} -fi - -TIMESTAMP=$(date +%Y%m%d_%H%M%S) -OUTPUT_DIR="fuzzing_results_improved_${OPTIMIZER}_${TIMESTAMP}" - -echo -e "${GREEN}=== Improved Fuzzing Analysis ===${NC}" -echo "Optimizer: $OPTIMIZER" -echo "Total runs: $RUNS_VALUE" -echo "Trades per run: $TRADES_VALUE" -echo "Staking: $([ "$STAKING_ENABLED" = "true" ] && echo "enabled" || echo "disabled")" -if [ "$STAKING_ENABLED" = "true" ]; then - echo "Staking bias: $STAKINGBIAS_VALUE%" -fi -echo "Buy bias: $BUYBIAS_VALUE%" -echo "Output directory: $OUTPUT_DIR" -echo "" - -# Validate optimizer -case $OPTIMIZER in - BullMarketOptimizer|BearMarketOptimizer|NeutralMarketOptimizer|WhaleOptimizer|ExtremeOptimizer|MaliciousOptimizer) - echo "Optimizer validation passed" - ;; - *) - echo -e "${RED}Error: Invalid optimizer class${NC}" - echo "Valid options: BullMarketOptimizer, BearMarketOptimizer, NeutralMarketOptimizer, WhaleOptimizer, ExtremeOptimizer, MaliciousOptimizer" - exit 1 - ;; -esac - -# Create output directory -mkdir -p $OUTPUT_DIR - -# Run the improved fuzzing with buy bias -echo -e "${YELLOW}Starting improved fuzzing analysis with buy bias...${NC}" -echo "" - -FUZZING_RUNS=$RUNS_VALUE \ -OPTIMIZER_CLASS=$OPTIMIZER \ -ENABLE_STAKING=$STAKING_ENABLED \ -BUY_BIAS=$BUYBIAS_VALUE \ -TRADES_PER_RUN=$TRADES_VALUE \ -STAKING_BIAS=$STAKINGBIAS_VALUE \ -TRACK_POSITIONS=true \ -forge script analysis/ImprovedFuzzingAnalysis.s.sol:ImprovedFuzzingAnalysis --gas-limit 100000000 -vv 2>&1 | tee $OUTPUT_DIR/fuzzing.log - -# Extract key metrics -echo "" -echo -e "${GREEN}=== ANALYSIS COMPLETE ===${NC}" - -# Show summary from log -tail -20 $OUTPUT_DIR/fuzzing.log | grep -E "Total scenarios|Profitable|Discovery|Stakes|Snatches" || true - -# Check for position CSVs -POSITION_CSV_COUNT=$(ls -1 improved_positions_*.csv 2>/dev/null | wc -l) - -if [ $POSITION_CSV_COUNT -gt 0 ]; then - # Move position CSVs to output directory - mv improved_positions_*.csv $OUTPUT_DIR/ - echo "" - echo -e "${GREEN}Position CSVs saved to: $OUTPUT_DIR/${NC}" - - # Calculate max staking percentage - echo "" - echo -e "${YELLOW}=== Maximum Staking Level ===${NC}" - for f in $OUTPUT_DIR/improved_positions_*.csv; do - tail -1 "$f" | cut -d',' -f13 - done | sort -n | tail -1 | awk '{ - pct = $1/1e16 - printf "%.2f%% of authorized stake (%.2f%% of KRAIKEN supply)\n", pct, pct*0.2 - }' -fi - -# Check for snatching activity -echo "" -echo -e "${YELLOW}=== Snatching Activity ===${NC}" -SNATCH_COUNT=$(grep -c "SNATCHED" $OUTPUT_DIR/fuzzing.log 2>/dev/null || true) -if [ -z "$SNATCH_COUNT" ]; then - SNATCH_COUNT="0" -fi -if [ "$SNATCH_COUNT" -gt "0" ]; then - echo -e "${GREEN}Snatching observed! Found $SNATCH_COUNT snatching events${NC}" - grep "SNATCHED" $OUTPUT_DIR/fuzzing.log | head -5 -else - echo "No snatching observed in this run" -fi - -# Check for profitable scenarios -PROFITABLE_COUNT=$(grep -c "PROFITABLE!" $OUTPUT_DIR/fuzzing.log 2>/dev/null || true) -if [ -z "$PROFITABLE_COUNT" ]; then - PROFITABLE_COUNT="0" -fi -if [ "$PROFITABLE_COUNT" -gt "0" ]; then - echo "" - echo -e "${GREEN}=== PROFITABLE SCENARIOS FOUND ===${NC}" - echo "Found $PROFITABLE_COUNT profitable scenarios" - grep "PROFITABLE!" $OUTPUT_DIR/fuzzing.log | head -5 -fi - -echo "" -echo -e "${GREEN}Full results saved to: $OUTPUT_DIR/${NC}" -echo "" -echo "To view detailed logs:" -echo " cat $OUTPUT_DIR/fuzzing.log" -echo "" -echo "To visualize position movements (if CSVs generated):" -echo " ./analysis/view-scenarios.sh $OUTPUT_DIR" \ No newline at end of file diff --git a/onchain/analysis/run-recorded-fuzzing.sh b/onchain/analysis/run-recorded-fuzzing.sh deleted file mode 100755 index 64c09cd..0000000 --- a/onchain/analysis/run-recorded-fuzzing.sh +++ /dev/null @@ -1,256 +0,0 @@ -#!/bin/bash - -# Usage: ./run-recorded-fuzzing.sh [optimizer] [runs=N] [trades=N] [staking=on|off] [buybias=N] -# Examples: -# ./run-recorded-fuzzing.sh BullMarketOptimizer runs=50 -# ./run-recorded-fuzzing.sh WhaleOptimizer runs=20 trades=30 staking=off -# ./run-recorded-fuzzing.sh ExtremeOptimizer runs=100 trades=default staking=on buybias=80 - -# Colors for output -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -BLUE='\033[0;34m' -NC='\033[0m' # No Color -BOLD='\033[1m' - -# Configuration -OPTIMIZER=${1:-BullMarketOptimizer} -RUNS=${2:-runs=20} -TRADES=${3:-trades=default} -STAKING=${4:-staking=on} -BUYBIAS=${5:-buybias=50} - -# Parse runs parameter -if [[ $RUNS == runs=* ]]; then - RUNS_VALUE=${RUNS#runs=} -else - RUNS_VALUE=$RUNS - RUNS="runs=$RUNS" -fi - -# Parse trades parameter (for future use) -TRADES_VALUE="" -if [[ $TRADES == trades=* ]]; then - TRADES_VALUE=${TRADES#trades=} -fi - -# Parse staking parameter -STAKING_ENABLED="true" -if [[ $STAKING == staking=* ]]; then - STAKING_VALUE=${STAKING#staking=} - if [[ $STAKING_VALUE == "off" ]] || [[ $STAKING_VALUE == "false" ]] || [[ $STAKING_VALUE == "0" ]]; then - STAKING_ENABLED="false" - fi -elif [[ $STAKING == "nostaking" ]]; then - STAKING_ENABLED="false" -fi - -# Parse buy bias parameter -BUYBIAS_VALUE="50" -if [[ $BUYBIAS == buybias=* ]]; then - BUYBIAS_VALUE=${BUYBIAS#buybias=} -fi - -# Generate unique run ID (6 chars from timestamp + random) -RUN_ID=$(date +%y%m%d)-$(head /dev/urandom | tr -dc A-Z0-9 | head -c 4) -TIMESTAMP=$(date +%Y%m%d_%H%M%S) -OUTPUT_DIR="fuzzing_results_recorded_${OPTIMIZER}_${TIMESTAMP}" - -echo -e "${GREEN}=== Recorded Fuzzing Campaign ===${NC}" -echo -e "Run ID: ${BOLD}${RUN_ID}${NC}" -echo "Optimizer: $OPTIMIZER" -echo "Total runs: $RUNS_VALUE" -echo "Staking: $([ "$STAKING_ENABLED" = "true" ] && echo "enabled" || echo "disabled")" -echo "Buy bias: $BUYBIAS_VALUE%" -echo "Output directory: $OUTPUT_DIR" -echo "" - -# Validate optimizer -case $OPTIMIZER in - BullMarketOptimizer|BearMarketOptimizer|NeutralMarketOptimizer|WhaleOptimizer|ExtremeOptimizer|MaliciousOptimizer) - echo "Optimizer validation passed" - ;; - *) - echo -e "${RED}Error: Invalid optimizer class${NC}" - echo "Valid options: BullMarketOptimizer, BearMarketOptimizer, NeutralMarketOptimizer, WhaleOptimizer, ExtremeOptimizer, MaliciousOptimizer" - exit 1 - ;; -esac - -# Create output directory -mkdir -p $OUTPUT_DIR - -# Run the recorded fuzzing -echo -e "${YELLOW}Starting recorded fuzzing analysis...${NC}" -FUZZING_RUNS=$RUNS_VALUE \ -OPTIMIZER_CLASS=$OPTIMIZER \ -ENABLE_STAKING=$STAKING_ENABLED \ -BUY_BIAS=$BUYBIAS_VALUE \ -RUN_ID=$RUN_ID \ -forge script analysis/RecordedFuzzingAnalysis.s.sol:RecordedFuzzingAnalysis --gas-limit 1000000000 -vv 2>&1 | tee $OUTPUT_DIR/fuzzing.log - -# Check for generated scenario files (check for summaries, they're always generated) -SCENARIO_COUNT=$(ls -1 scenario_summary_seed*.txt 2>/dev/null | wc -l) - -if [ $SCENARIO_COUNT -gt 0 ]; then - echo "" - echo -e "${GREEN}=== INVARIANT VIOLATIONS FOUND! ===${NC}" - echo -e "${BOLD}Found $SCENARIO_COUNT profitable scenarios${NC}" - echo "" - - # Move recording files to output directory with run ID - for file in recorded_scenario_seed*.json; do - if [ -f "$file" ]; then - SEED=$(echo $file | sed 's/recorded_scenario_seed\(.*\)\.json/\1/') - NEW_NAME="scenario_${RUN_ID}_seed${SEED}.json" - mv "$file" "$OUTPUT_DIR/$NEW_NAME" - echo -e " Scenario JSON: ${BLUE}$OUTPUT_DIR/$NEW_NAME${NC}" - fi - done - - for file in replay_script_seed*.sol; do - if [ -f "$file" ]; then - SEED=$(echo $file | sed 's/replay_script_seed\(.*\)\.sol/\1/') - NEW_NAME="replay_${RUN_ID}_seed${SEED}.sol" - mv "$file" "$OUTPUT_DIR/$NEW_NAME" - echo -e " Replay script: ${BLUE}$OUTPUT_DIR/$NEW_NAME${NC}" - fi - done - - for file in scenario_summary_seed*.txt; do - if [ -f "$file" ]; then - SEED=$(echo $file | sed 's/scenario_summary_seed\(.*\)\.txt/\1/') - NEW_NAME="summary_${RUN_ID}_seed${SEED}.txt" - mv "$file" "$OUTPUT_DIR/$NEW_NAME" - echo -e " Summary: ${BLUE}$OUTPUT_DIR/$NEW_NAME${NC}" - - # Display summary preview - echo "" - echo -e "${YELLOW}--- Preview of $NEW_NAME ---${NC}" - head -n 15 "$OUTPUT_DIR/$NEW_NAME" - echo "..." - fi - done - - # Move position CSVs if they exist - 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 - cat > $OUTPUT_DIR/index.txt << EOF -Recorded Fuzzing Results -======================== -Run ID: $RUN_ID -Date: $(date) -Optimizer: $OPTIMIZER -Total Runs: $RUNS_VALUE -Profitable Scenarios: $SCENARIO_COUNT - -Files: ------- -EOF - - for file in $OUTPUT_DIR/*; do - echo "- $(basename $file)" >> $OUTPUT_DIR/index.txt - done - - echo "" - echo -e "${GREEN}=== NEXT STEPS ===${NC}" - echo "1. Review summaries:" - echo " cat $OUTPUT_DIR/summary_${RUN_ID}_seed*.txt" - echo "" - echo "2. Replay a specific scenario:" - echo " ./analysis/replay-scenario.sh ${RUN_ID} " - echo "" - echo "3. Visualize positions (if CSV tracking enabled):" - echo " ./analysis/view-scenarios.sh $OUTPUT_DIR" - echo "" - echo "4. Share with team:" - echo -e " ${BOLD}Reference ID: ${RUN_ID}${NC}" - echo " \"Found exploit ${RUN_ID} with ${SCENARIO_COUNT} profitable scenarios\"" - -else - echo "" - echo -e "${YELLOW}=== No Profitable Scenarios Found ===${NC}" - echo "This could mean the protocol is secure under these conditions." - echo "" - echo "Try adjusting parameters:" - echo " - Increase runs: ./analysis/run-recorded-fuzzing.sh $OPTIMIZER runs=100" - echo " - Try different optimizer: ./analysis/run-recorded-fuzzing.sh WhaleOptimizer" - echo " - Use extreme optimizer: ./analysis/run-recorded-fuzzing.sh ExtremeOptimizer" - echo " - Disable staking: ./analysis/run-recorded-fuzzing.sh $OPTIMIZER runs=$RUNS_VALUE trades=default staking=off" -fi - -echo "" -echo -e "${GREEN}Results saved to: $OUTPUT_DIR/${NC}" -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 in the analysis directory - TEMP_LINK="analysis/profitable_scenario.csv" - if [ -f "$TEMP_LINK" ] || [ -L "$TEMP_LINK" ]; then - rm -f "$TEMP_LINK" - fi - # Use absolute path for the symlink - ln -s "$(realpath $FIRST_CSV)" "$TEMP_LINK" - echo "Created symlink: $TEMP_LINK -> $FIRST_CSV" - - # 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 diff --git a/onchain/analysis/run-visualizer.html b/onchain/analysis/run-visualizer.html new file mode 100644 index 0000000..62eeac4 --- /dev/null +++ b/onchain/analysis/run-visualizer.html @@ -0,0 +1,916 @@ + + + + + + Kraiken Fuzzing Run Visualizer + + + + +
+

🐙 Kraiken Fuzzing Run Visualizer

+

Analyze individual trades from fuzzing runs

+
+ +
+ 📊 Anti-Arbitrage Three-Position Strategy (Uniswap V3 1% Pool)
+
+ Position Strategy:
+ Floor: Deep liquidity position - holds ETH when ETH is cheap (below current price)
+ Anchor: Shallow liquidity around current price for fast price discovery
+ Discovery: Edge liquidity position - holds KRAIKEN when ETH is expensive (above current price)
+
+ Price Multiples: Shows ETH price relative to current (1x):
+ • < 1x = ETH is cheaper than current price (positions below current hold ETH)
+ • 1x = Current ETH price (red dashed line)
+ • > 1x = ETH is more expensive than current price (positions above current hold KRAIKEN)
+
+ Usage: Select a CSV file from the dropdown and use Previous/Next buttons to navigate through trades +
+ +
+
+ + + + +
+ +
+ + +
+ + + + \ No newline at end of file diff --git a/onchain/analysis/scenario-visualizer.html b/onchain/analysis/scenario-visualizer.html deleted file mode 100644 index 24a3ed4..0000000 --- a/onchain/analysis/scenario-visualizer.html +++ /dev/null @@ -1,1204 +0,0 @@ - - - - - - Kraiken Liquidity Position Simulator - - - - -

Kraiken Liquidity Position Simulator

-
- 📊 Anti-Arbitrage Three-Position Strategy (Uniswap V3 1% Pool)
-
- Position Strategy:
- Floor: Deep liquidity position - holds ETH when ETH is cheap (below current price)
- Anchor: Shallow liquidity around current price for fast price discovery
- Discovery: Edge liquidity position - holds KRAIKEN when ETH is expensive (above current price)
-
- Price Multiples: Shows ETH price relative to current (1x):
- • < 1x = ETH is cheaper than current price (positions below current hold ETH)
- • 1x = Current ETH price (red dashed line)
- • > 1x = ETH is more expensive than current price (positions above current hold KRAIKEN)
-
- Note: The x-axis automatically adjusts based on token ordering in the pool
-
- Navigation: Use the Previous/Next buttons or URL parameter ?row=N to view specific CSV rows -
-
Loading profitable scenario data...
- - -
- - - - \ No newline at end of file diff --git a/onchain/analysis/staking-based-anchor-width.md b/onchain/analysis/staking-based-anchor-width.md deleted file mode 100644 index d7bbfb4..0000000 --- a/onchain/analysis/staking-based-anchor-width.md +++ /dev/null @@ -1,148 +0,0 @@ -# Staking-Based AnchorWidth Recommendations - -## Understanding Staking as a Sentiment Signal - -The Harberger tax staking mechanism creates a prediction market where: -- **Tax Rate** = Cost to hold a position (self-assessed valuation) -- **Percentage Staked** = Overall confidence in protocol -- **Average Tax Rate** = Market's price volatility expectation - -## Staking Metrics → Market Conditions Mapping - -### High Staking Percentage (>70%) -**Interpretation**: Strong bullish sentiment, holders confident in price appreciation -- Market expects upward price movement -- Stakers willing to lock capital despite opportunity cost -- Lower expected volatility (confident holders) - -**anchorWidth Recommendation**: **30-50%** -- Moderate width to capture expected upward movement -- Avoid excessive rebalancing during steady climb -- Maintain efficiency without being too narrow - -### Low Staking Percentage (<30%) -**Interpretation**: Bearish/uncertain sentiment, holders want liquidity -- Market expects downward pressure or high volatility -- Stakers unwilling to commit capital -- Higher expected volatility (nervous market) - -**anchorWidth Recommendation**: **60-80%** -- Wide range to handle volatility without constant rebalancing -- Defensive positioning during uncertainty -- Prioritize capital preservation over fee optimization - -### Medium Staking Percentage (30-70%) -**Interpretation**: Neutral market, mixed sentiment -- Balanced buyer/seller pressure -- Normal market conditions -- Moderate volatility expectations - -**anchorWidth Recommendation**: **40-60%** -- Balanced approach for normal conditions -- Reasonable fee capture with manageable rebalancing -- Standard operating parameters - -## Average Tax Rate → Volatility Expectations - -### High Average Tax Rate (>50% of max) -**Interpretation**: Market expects significant price movement -- Stakers setting high taxes = expect to be "snatched" soon -- Indicates expected volatility or trend change -- Short-term holding mentality - -**anchorWidth Recommendation**: **Increase by 20-30%** -- Wider anchor to handle expected volatility -- Reduce rebalancing frequency during turbulent period -- Example: Base 40% → Adjust to 60% - -### Low Average Tax Rate (<20% of max) -**Interpretation**: Market expects stability -- Stakers comfortable with low taxes = expect to hold long-term -- Low volatility expectations -- Long-term holding mentality - -**anchorWidth Recommendation**: **Decrease by 10-20%** -- Narrower anchor to maximize fee collection -- Take advantage of expected stability -- Example: Base 40% → Adjust to 30% - -## Proposed On-Chain Formula - -```solidity -function calculateAnchorWidth( - uint256 percentageStaked, // 0 to 1e18 - uint256 avgTaxRate // 0 to 1e18 (normalized) -) public pure returns (uint24) { - // Base width starts at 40% - uint24 baseWidth = 40; - - // Staking adjustment: -20% to +20% based on staking percentage - // High staking (bullish) → narrower width - // Low staking (bearish) → wider width - int24 stakingAdjustment = int24(20) - int24(uint24(percentageStaked * 40 / 1e18)); - - // Tax rate adjustment: -10% to +30% based on average tax - // High tax (volatile) → wider width - // Low tax (stable) → narrower width - int24 taxAdjustment = int24(uint24(avgTaxRate * 30 / 1e18)) - 10; - - // Combined width - int24 totalWidth = int24(baseWidth) + stakingAdjustment + taxAdjustment; - - // Clamp between 10 and 80 - if (totalWidth < 10) return 10; - if (totalWidth > 80) return 80; - - return uint24(totalWidth); -} -``` - -## Staking Signal Interpretations - -### Scenario 1: "Confident Bull Market" -- High staking (80%), Low tax rate (20%) -- Interpretation: Strong holders, expect steady appreciation -- anchorWidth: ~25-35% -- Rationale: Tight range for fee optimization in trending market - -### Scenario 2: "Fearful Bear Market" -- Low staking (20%), High tax rate (70%) -- Interpretation: Nervous market, expect volatility/decline -- anchorWidth: ~70-80% -- Rationale: Wide defensive positioning - -### Scenario 3: "Speculative Frenzy" -- High staking (70%), High tax rate (80%) -- Interpretation: Aggressive speculation, expect big moves -- anchorWidth: ~50-60% -- Rationale: Balance between capturing moves and managing volatility - -### Scenario 4: "Boring Stability" -- Medium staking (50%), Low tax rate (10%) -- Interpretation: Stable, range-bound market -- anchorWidth: ~30-40% -- Rationale: Optimize for fee collection in stable conditions - -## Key Advantages of Staking-Based Approach - -1. **On-Chain Native**: Uses only data available to smart contracts -2. **Forward-Looking**: Tax rates reflect expectations, not just history -3. **Self-Adjusting**: Market participants' actions directly influence parameters -4. **Sybil-Resistant**: Costly to manipulate due to tax payments -5. **Continuous Signal**: Updates in real-time as positions change - -## Implementation Considerations - -1. **Smoothing**: Average metrics over time window to prevent manipulation -2. **Bounds**: Always enforce min/max limits (10-80% recommended) -3. **Hysteresis**: Add small threshold before adjusting to reduce thrashing -4. **Gas Optimization**: Only recalculate when scraping/repositioning - -## Summary Recommendations - -For the on-chain Optimizer contract: -- Use `percentageStaked` as primary bull/bear indicator -- Use `averageTaxRate` as volatility expectation proxy -- Combine both signals for sophisticated width adjustment -- Default to 40% width when signals are neutral -- Never exceed 80% or go below 10% for safety \ No newline at end of file diff --git a/onchain/analysis/verify_anchor_width_logic.md b/onchain/analysis/verify_anchor_width_logic.md deleted file mode 100644 index b6d820c..0000000 --- a/onchain/analysis/verify_anchor_width_logic.md +++ /dev/null @@ -1,88 +0,0 @@ -# Anchor Width Calculation Verification - -## Formula -``` -anchorWidth = base + stakingAdjustment + taxAdjustment -where: - base = 40 - stakingAdjustment = 20 - (percentageStaked * 40 / 1e18) - taxAdjustment = (averageTaxRate * 40 / 1e18) - 10 - final = clamp(anchorWidth, 10, 80) -``` - -## Test Case Verification - -### 1. Bull Market (80% staked, 10% tax) -- Base: 40 -- Staking adj: 20 - (0.8 * 40) = 20 - 32 = -12 -- Tax adj: (0.1 * 40) - 10 = 4 - 10 = -6 -- Total: 40 + (-12) + (-6) = **22** ✓ - -### 2. Bear Market (20% staked, 70% tax) -- Base: 40 -- Staking adj: 20 - (0.2 * 40) = 20 - 8 = 12 -- Tax adj: (0.7 * 40) - 10 = 28 - 10 = 18 -- Total: 40 + 12 + 18 = **70** ✓ - -### 3. Neutral Market (50% staked, 30% tax) -- Base: 40 -- Staking adj: 20 - (0.5 * 40) = 20 - 20 = 0 -- Tax adj: (0.3 * 40) - 10 = 12 - 10 = 2 -- Total: 40 + 0 + 2 = **42** ✓ - -### 4. Speculative Frenzy (70% staked, 80% tax) -- Base: 40 -- Staking adj: 20 - (0.7 * 40) = 20 - 28 = -8 -- Tax adj: (0.8 * 40) - 10 = 32 - 10 = 22 -- Total: 40 + (-8) + 22 = **54** ✓ - -### 5. Stable Market (50% staked, 5% tax) -- Base: 40 -- Staking adj: 20 - (0.5 * 40) = 20 - 20 = 0 -- Tax adj: (0.05 * 40) - 10 = 2 - 10 = -8 -- Total: 40 + 0 + (-8) = **32** ✓ - -### 6. Zero Inputs (0% staked, 0% tax) -- Base: 40 -- Staking adj: 20 - (0 * 40) = 20 - 0 = 20 -- Tax adj: (0 * 40) - 10 = 0 - 10 = -10 -- Total: 40 + 20 + (-10) = **50** ✓ - -### 7. Max Inputs (100% staked, 100% tax) -- Base: 40 -- Staking adj: 20 - (1.0 * 40) = 20 - 40 = -20 -- Tax adj: (1.0 * 40) - 10 = 40 - 10 = 30 -- Total: 40 + (-20) + 30 = **50** ✓ - -### 8. Test Minimum Clamping (95% staked, 0% tax) -- Base: 40 -- Staking adj: 20 - (0.95 * 40) = 20 - 38 = -18 -- Tax adj: (0 * 40) - 10 = 0 - 10 = -10 -- Total: 40 + (-18) + (-10) = **12** (not clamped, above 10) ✓ - -### 9. Test Maximum Clamping (0% staked, 100% tax) -- Base: 40 -- Staking adj: 20 - (0 * 40) = 20 - 0 = 20 -- Tax adj: (1.0 * 40) - 10 = 40 - 10 = 30 -- Total: 40 + 20 + 30 = 90 → **80** (clamped to max) ✓ - -## Summary - -All test cases pass! The implementation correctly: - -1. **Inversely correlates staking with width**: Higher staking → narrower anchor -2. **Directly correlates tax with width**: Higher tax → wider anchor -3. **Maintains reasonable bounds**: 10-80% range -4. **Provides sensible defaults**: 50% width for zero/max inputs - -## Market Condition Mapping - -| Condition | Staking | Tax | Width | Rationale | -|-----------|---------|-----|-------|-----------| -| Bull Market | High (70-90%) | Low (0-20%) | 20-35% | Optimize fees in trending market | -| Bear Market | Low (10-30%) | High (60-90%) | 60-80% | Defensive positioning | -| Neutral | Medium (40-60%) | Medium (20-40%) | 35-50% | Balanced approach | -| Volatile | Any | High (70%+) | 50-80% | Wide to reduce rebalancing | -| Stable | Any | Low (<10%) | 20-40% | Narrow for fee collection | - -The formula successfully encodes market dynamics into the anchor width parameter! \ No newline at end of file diff --git a/onchain/analysis/view-scenarios.sh b/onchain/analysis/view-scenarios.sh deleted file mode 100755 index 9f869e4..0000000 --- a/onchain/analysis/view-scenarios.sh +++ /dev/null @@ -1,83 +0,0 @@ -#!/bin/bash - -# Scenario Visualizer Launcher -# Starts a local HTTP server and opens the visualization page - -echo "🚀 Starting Scenario Visualizer..." -echo "==================================" - -# Check if CSV file exists -if [ ! -f "profitable_scenario.csv" ]; then - echo "⚠️ No profitable_scenario.csv found in analysis folder" - echo "Run analysis first: forge script analysis/SimpleAnalysis.s.sol --ffi" - echo "" - read -p "Continue anyway? (y/n): " -n 1 -r - echo "" - if [[ ! $REPLY =~ ^[Yy]$ ]]; then - exit 1 - fi -fi - -# Function to detect available Python -detect_python() { - if command -v python3 &> /dev/null; then - echo "python3" - elif command -v python &> /dev/null; then - echo "python" - else - echo "" - fi -} - -# Function to start server -start_server() { - local python_cmd=$1 - local port=$2 - - echo "📡 Starting HTTP server on port $port..." - echo "🌐 Opening http://localhost:$port/scenario-visualizer.html" - echo "" - echo "Press Ctrl+C to stop the server" - echo "==================================" - - # Start server in background first - $python_cmd -m http.server $port & - SERVER_PID=$! - - # Give server a moment to start - sleep 1 - - # Try to open browser (works on most systems) - if command -v xdg-open &> /dev/null; then - xdg-open "http://localhost:$port/scenario-visualizer.html" 2>/dev/null & - elif command -v open &> /dev/null; then - open "http://localhost:$port/scenario-visualizer.html" 2>/dev/null & - else - echo "🔗 Manual: Open http://localhost:$port/scenario-visualizer.html in your browser" - fi - - # Wait for the server process - wait $SERVER_PID -} - -# Main execution -cd "$(dirname "$0")" - -# Check for Python -PYTHON_CMD=$(detect_python) -if [ -z "$PYTHON_CMD" ]; then - echo "❌ Python not found. Please install Python 3 or use alternative:" - echo " npx http-server . --port 8000" - echo " php -S localhost:8000" - exit 1 -fi - -# Check if port is available -PORT=8000 -if lsof -Pi :$PORT -sTCP:LISTEN -t >/dev/null 2>&1; then - echo "⚠️ Port $PORT is already in use. Trying port $((PORT + 1))..." - PORT=$((PORT + 1)) -fi - -# Start the server -start_server "$PYTHON_CMD" "$PORT" \ No newline at end of file diff --git a/onchain/improved_profitable_3601.csv b/onchain/improved_profitable_3601.csv deleted file mode 100644 index 40b1ac4..0000000 --- a/onchain/improved_profitable_3601.csv +++ /dev/null @@ -1,3 +0,0 @@ -Scenario,Seed,Initial Balance,Final Balance,Profit,Profit %,Discovery Reached -BullMarketOptimizer,1,152861730151649342497,165534767451273053750,12673037299623711253,8,true -BullMarketOptimizer,2,53215106468573532381,63779335616103461539,10564229147529929158,19,true diff --git a/onchain/improved_profitable_84051.csv b/onchain/improved_profitable_84051.csv deleted file mode 100644 index 85db56d..0000000 --- a/onchain/improved_profitable_84051.csv +++ /dev/null @@ -1,5 +0,0 @@ -Scenario,Seed,Initial Balance,Final Balance,Profit,Profit %,Discovery Reached -BullMarketOptimizer,3,86868696383720927358,91094180959940828354,4225484576219900996,4,true -BullMarketOptimizer,6,77383282103079540723,78863793676376493002,1480511573296952279,1,true -BullMarketOptimizer,9,52269169078465922293,60595359746230433174,8326190667764510881,15,false -BullMarketOptimizer,11,63309771874363779531,63524232370110625118,214460495746845587,0,true