From 9f0b16330304e2562d04b4a347a4dffbb096791a Mon Sep 17 00:00:00 2001 From: johba Date: Sat, 9 Aug 2025 18:03:31 +0200 Subject: [PATCH] wip --- CLAUDE.md | 6 +- onchain/analysis/.gitignore | 22 ++ onchain/analysis/AnalysisVisualizer.py | 309 ++++++++++++++++ onchain/analysis/CLAUDE.md | 58 +++ onchain/analysis/FuzzingAnalysis.s.sol | 329 +++++++++++++++++ onchain/analysis/README.md | 168 +++++---- onchain/analysis/SimpleAnalysis.s.sol | 333 ------------------ onchain/analysis/examples/batch_analysis.sh | 39 -- onchain/analysis/{ => helpers}/CSVHelper.sol | 0 onchain/analysis/{ => helpers}/CSVManager.sol | 0 onchain/analysis/helpers/SwapExecutor.sol | 94 +++++ onchain/analysis/requirements.txt | 4 + onchain/analysis/run-fuzzing.sh | 201 +++++++++++ onchain/foundry.toml | 4 + .../test/helpers/LiquidityBoundaryHelper.sol | 118 +++++++ onchain/test/helpers/TestBase.sol | 55 +++ onchain/test/helpers/UniswapTestBase.sol | 37 +- onchain/test/mocks/BullMarketOptimizer.sol | 20 +- onchain/test/mocks/WhaleOptimizer.sol | 33 ++ 19 files changed, 1340 insertions(+), 490 deletions(-) create mode 100644 onchain/analysis/.gitignore create mode 100755 onchain/analysis/AnalysisVisualizer.py create mode 100644 onchain/analysis/CLAUDE.md create mode 100644 onchain/analysis/FuzzingAnalysis.s.sol delete mode 100644 onchain/analysis/SimpleAnalysis.s.sol delete mode 100755 onchain/analysis/examples/batch_analysis.sh rename onchain/analysis/{ => helpers}/CSVHelper.sol (100%) rename onchain/analysis/{ => helpers}/CSVManager.sol (100%) create mode 100644 onchain/analysis/helpers/SwapExecutor.sol create mode 100644 onchain/analysis/requirements.txt create mode 100755 onchain/analysis/run-fuzzing.sh create mode 100644 onchain/test/helpers/LiquidityBoundaryHelper.sol create mode 100644 onchain/test/mocks/WhaleOptimizer.sol diff --git a/CLAUDE.md b/CLAUDE.md index dac05f5..3083a51 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -79,6 +79,10 @@ cd web && npm run dev - Clean up temporary files - Check `git status` before commits +### Implementation Strategy +- Never fall back to simpler implementations +- Don't create "a simpler test" or "a simpler scenario" if things don't work out. just try to identify the error in more detail. + ## Communication Style ### Direct & Technical @@ -99,4 +103,4 @@ cd web && npm run dev - **Technical Deep Dive**: See [TECHNICAL_APPENDIX.md](TECHNICAL_APPENDIX.md) - **Contract Details**: See [onchain/CLAUDE.md](onchain/CLAUDE.md) - **Frontend Architecture**: See [web/CLAUDE.md](web/CLAUDE.md) -- **Data Indexing**: See [subgraph/base_sepolia/CLAUDE.md](subgraph/base_sepolia/CLAUDE.md) +- **Data Indexing**: See [subgraph/base_sepolia/CLAUDE.md](subgraph/base_sepolia/CLAUDE.md) \ No newline at end of file diff --git a/onchain/analysis/.gitignore b/onchain/analysis/.gitignore new file mode 100644 index 0000000..f7e681f --- /dev/null +++ b/onchain/analysis/.gitignore @@ -0,0 +1,22 @@ +# Fuzzing results +fuzzing_results_*/ +fuzzing_results_*.tar.gz + +# Log files +*.log +temp_output.log +full_loss_analysis.log +fuzzing_output.log + +# Generated CSV files (except example ones) +profitable_*.csv +whale_*.csv + +# Python cache +__pycache__/ +*.pyc +*.pyo + +# Temporary files +*.tmp +.~* \ No newline at end of file diff --git a/onchain/analysis/AnalysisVisualizer.py b/onchain/analysis/AnalysisVisualizer.py new file mode 100755 index 0000000..da853bd --- /dev/null +++ b/onchain/analysis/AnalysisVisualizer.py @@ -0,0 +1,309 @@ +#!/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/CLAUDE.md b/onchain/analysis/CLAUDE.md new file mode 100644 index 0000000..f98c40e --- /dev/null +++ b/onchain/analysis/CLAUDE.md @@ -0,0 +1,58 @@ +# KRAIKEN LiquidityManager Fuzzing Analysis + +Tools for testing the KRAIKEN LiquidityManager's resilience against various trading strategies to identify scenarios where traders can profit. + +## Quick Start + +```bash +# Run fuzzing analysis with default settings (100 runs per market) +forge script analysis/FuzzingAnalysis.s.sol --ffi --via-ir + +# Custom configuration +FUZZING_RUNS=500 forge script analysis/FuzzingAnalysis.s.sol --ffi --via-ir + +# With position tracking (generates detailed CSV for each scenario) +TRACK_POSITIONS=true FUZZING_RUNS=50 forge script analysis/FuzzingAnalysis.s.sol --ffi --via-ir +``` + +## Configuration + +- **FUZZING_RUNS**: Number of random trading scenarios per market type (default: 100) +- **TRACK_POSITIONS**: Enable detailed position tracking CSV output (default: false) + +## How It Works + +1. **Real Deployments**: Deploys actual Uniswap V3 factory, pool, and LiquidityManager +2. **Random Trading**: Generates random buy/sell patterns with varying amounts and timing +3. **Recenter Calls**: Triggers `lm.recenter()` at random intervals +4. **Profit Detection**: Identifies scenarios where traders end with more ETH than they started +5. **CSV Export**: Saves all profitable scenarios to `profitable_scenarios_[timestamp].csv` + +## Output Files + +- `profitable_scenarios_[timestamp].csv` - Details of all profitable trading sequences +- `positions_[scenario]_[seed].csv` - Liquidity position data (only with TRACK_POSITIONS=true) + +## Visualization + +```bash +# View results in browser +python3 -m http.server 8000 +# Open http://localhost:8000/scenario-visualizer.html + +# Or use the shell script +./view-scenarios.sh +``` + +## Analysis Tools + +- `AnalysisVisualizer.py` - Generates charts from CSV data +- `scenario-visualizer.html` - Interactive web visualization +- `RISK_ANALYSIS_FINDINGS.md` - Summary of discovered vulnerabilities + +## Components + +- `FuzzingAnalysis.s.sol` - Main fuzzing script +- `helpers/SwapExecutor.sol` - Shared swap execution logic +- `CSVManager.sol` - CSV generation utilities +- `CSVHelper.sol` - CSV formatting helpers \ No newline at end of file diff --git a/onchain/analysis/FuzzingAnalysis.s.sol b/onchain/analysis/FuzzingAnalysis.s.sol new file mode 100644 index 0000000..f11e1a9 --- /dev/null +++ b/onchain/analysis/FuzzingAnalysis.s.sol @@ -0,0 +1,329 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8.19; + +import "forge-std/Test.sol"; +import {TestEnvironment} from "../test/helpers/TestBase.sol"; +import {IUniswapV3Pool} from "@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol"; +import {IUniswapV3Factory} from "@uniswap/v3-core/contracts/interfaces/IUniswapV3Factory.sol"; +import {IWETH9} from "../src/interfaces/IWETH9.sol"; +import {Kraiken} from "../src/Kraiken.sol"; +import {Stake} from "../src/Stake.sol"; +import {LiquidityManager} from "../src/LiquidityManager.sol"; +import {TickMath} from "@aperture/uni-v3-lib/TickMath.sol"; +import {ThreePositionStrategy} from "../src/abstracts/ThreePositionStrategy.sol"; +import "../test/mocks/BullMarketOptimizer.sol"; +import "../test/mocks/NeutralMarketOptimizer.sol"; +import "../test/mocks/BearMarketOptimizer.sol"; +import "../test/mocks/WhaleOptimizer.sol"; +import "../test/mocks/MockOptimizer.sol"; +import "../test/mocks/RandomScenarioOptimizer.sol"; +import "./helpers/CSVManager.sol"; +import "./helpers/SwapExecutor.sol"; + +/** + * @title FuzzingAnalysis + * @notice Fuzzing analysis to find profitable trading scenarios against LiquidityManager + * @dev Configurable via environment variables: + * - FUZZING_RUNS: Number of fuzzing iterations per market (default 100) + * - TRACK_POSITIONS: Track detailed position data (default false) + */ +contract FuzzingAnalysis is Test, CSVManager { + TestEnvironment testEnv; + IUniswapV3Factory factory; + IUniswapV3Pool pool; + IWETH9 weth; + Kraiken harberg; + Stake stake; + LiquidityManager lm; + bool token0isWeth; + + address account = makeAddr("trader"); + address feeDestination = makeAddr("fees"); + + // Analysis metrics + uint256 public scenariosAnalyzed; + uint256 public profitableScenarios; + + // Configuration + uint256 public fuzzingRuns; + bool public trackPositions; + string public optimizerClass; + uint256 public tradesPerRun; + + // Optimizers + BullMarketOptimizer bullOptimizer; + NeutralMarketOptimizer neutralOptimizer; + BearMarketOptimizer bearOptimizer; + WhaleOptimizer whaleOptimizer; + MockOptimizer mockOptimizer; + RandomScenarioOptimizer randomOptimizer; + + function run() public { + _loadConfiguration(); + + console.log("=== Fuzzing Analysis ==="); + console.log(string.concat("Optimizer: ", optimizerClass)); + console.log(string.concat("Fuzzing runs: ", vm.toString(fuzzingRuns))); + console.log(string.concat("Trades per run: ", vm.toString(tradesPerRun))); + console.log(string.concat("Position tracking: ", trackPositions ? "enabled" : "disabled")); + console.log(""); + + testEnv = new TestEnvironment(feeDestination); + + // Get optimizer based on class name + address optimizerAddress = _getOptimizerByClass(optimizerClass); + + // Initialize CSV for profitable scenarios + string memory profitableCSV = "Scenario,Seed,Initial Balance,Final Balance,Profit,Profit %\n"; + uint256 profitableCount; + uint256 marketProfitable = 0; + + console.log(string.concat("=== FUZZING with ", optimizerClass, " ===")); + + for (uint256 seed = 0; seed < fuzzingRuns; seed++) { + if (seed % 10 == 0 && seed > 0) { + console.log(string.concat("Progress: ", vm.toString(seed), "/", vm.toString(fuzzingRuns))); + } + + // Create fresh environment for each run + (factory, pool, weth, harberg, stake, lm,, token0isWeth) = + testEnv.setupEnvironmentWithOptimizer(seed % 2 == 0, feeDestination, optimizerAddress); + + // Fund account with random amount (10-50 ETH) + uint256 fundAmount = 10 ether + (uint256(keccak256(abi.encodePacked(seed, "fund"))) % 40 ether); + vm.deal(account, fundAmount * 2); + vm.prank(account); + weth.deposit{value: fundAmount}(); + + uint256 initialBalance = weth.balanceOf(account); + + // Initial recenter + vm.warp(block.timestamp + 5 hours); + vm.prank(feeDestination); + try lm.recenter() {} catch {} + + // Run trading scenario + uint256 finalBalance = _runFuzzedScenario(optimizerClass, seed); + + scenariosAnalyzed++; + + // Check profitability + if (finalBalance > initialBalance) { + profitableScenarios++; + marketProfitable++; + + uint256 profit = finalBalance - initialBalance; + uint256 profitPercentage = (profit * 100) / initialBalance; + + console.log(string.concat("PROFITABLE! Seed: ", vm.toString(seed), " Profit: ", vm.toString(profitPercentage), "%")); + + // Add to CSV + profitableCSV = string.concat( + profitableCSV, + optimizerClass, ",", + vm.toString(seed), ",", + vm.toString(initialBalance), ",", + vm.toString(finalBalance), ",", + vm.toString(profit), ",", + vm.toString(profitPercentage), "\n" + ); + profitableCount++; + } + } + + console.log(string.concat("\nResults for ", optimizerClass, ":")); + console.log(string.concat("Profitable: ", vm.toString(marketProfitable), "/", vm.toString(fuzzingRuns))); + console.log(""); + + console.log("=== ANALYSIS COMPLETE ==="); + console.log(string.concat("Total scenarios analyzed: ", vm.toString(scenariosAnalyzed))); + console.log(string.concat("Total profitable scenarios: ", vm.toString(profitableScenarios))); + + // Write profitable scenarios CSV if any found + if (profitableCount > 0) { + console.log("Writing profitable scenarios CSV..."); + string memory filename = string.concat("profitable_scenarios_", vm.toString(block.timestamp), ".csv"); + vm.writeFile(filename, profitableCSV); + console.log(string.concat("\nProfitable scenarios written to: ", filename)); + } else { + console.log("\nNo profitable scenarios found."); + } + + console.log("Script execution complete."); + } + + function _loadConfiguration() internal { + fuzzingRuns = vm.envOr("FUZZING_RUNS", uint256(100)); + trackPositions = vm.envOr("TRACK_POSITIONS", false); + optimizerClass = vm.envOr("OPTIMIZER_CLASS", string("BullMarketOptimizer")); + tradesPerRun = vm.envOr("TRADES_PER_RUN", uint256(20)); + } + + function _runFuzzedScenario(string memory scenarioName, uint256 seed) internal returns (uint256) { + // Initialize position tracking CSV if enabled + if (trackPositions) { + initializePositionsCSV(); + _recordPositionData("Initial"); + } + + // Use seed for randomness + uint256 rand = uint256(keccak256(abi.encodePacked(seed, scenarioName, block.timestamp))); + + // Use configured number of trades (with some randomness) + uint256 numTrades = tradesPerRun + (rand % 11) - 5; // +/- 5 trades + if (numTrades < 5) numTrades = 5; // Minimum 5 trades + + // Initial buy if no HARB + if (harberg.balanceOf(account) == 0 && weth.balanceOf(account) > 0) { + uint256 initialBuy = weth.balanceOf(account) / 10; + _executeBuy(initialBuy); + } + + // Execute random trades + for (uint256 i = 0; i < numTrades; i++) { + rand = uint256(keccak256(abi.encodePacked(rand, i))); + uint256 action = rand % 100; + + if (action < 40) { // 40% chance buy + uint256 wethBal = weth.balanceOf(account); + if (wethBal > 0) { + uint256 buyPercent = 1 + (rand % 1000); // 0.1% to 100% + uint256 buyAmount = (wethBal * buyPercent) / 1000; + if (buyAmount > 0) _executeBuy(buyAmount); + } + } else if (action < 80) { // 40% chance sell + uint256 harbBal = harberg.balanceOf(account); + if (harbBal > 0) { + uint256 sellPercent = 1 + (rand % 1000); // 0.1% to 100% + uint256 sellAmount = (harbBal * sellPercent) / 1000; + if (sellAmount > 0) _executeSell(sellAmount); + } + } else if (action < 95) { // 15% chance recenter + uint256 waitTime = 1 minutes + (rand % 10 hours); + vm.warp(block.timestamp + waitTime); + vm.prank(feeDestination); + try lm.recenter() { + if (trackPositions) { + _recordPositionData(string.concat("Recenter_", vm.toString(i))); + } + } catch {} + } else { // 5% chance wait + vm.warp(block.timestamp + 1 minutes + (rand % 2 hours)); + } + + // Skip trades at extreme ticks + (, int24 currentTick, , , , , ) = pool.slot0(); + if (currentTick < -887000 || currentTick > 887000) continue; + } + + // Sell remaining HARB + uint256 finalHarb = harberg.balanceOf(account); + if (finalHarb > 0) _executeSell(finalHarb); + + // Final recenters + for (uint256 j = 0; j < 1 + (rand % 3); j++) { + vm.warp(block.timestamp + 5 hours); + vm.prank(feeDestination); + try lm.recenter() {} catch {} + } + + // Write position tracking CSV if enabled + if (trackPositions) { + string memory positionFilename = string.concat( + "positions_", scenarioName, "_", vm.toString(seed), ".csv" + ); + writeCSVToFile(positionFilename); + } + + return weth.balanceOf(account); + } + + function _executeBuy(uint256 amount) internal { + if (amount == 0 || weth.balanceOf(account) < amount) return; + + SwapExecutor executor = new SwapExecutor(pool, weth, harberg, token0isWeth); + vm.prank(account); + weth.transfer(address(executor), amount); + + try executor.executeBuy(amount, account) {} catch {} + } + + function _executeSell(uint256 amount) internal { + if (amount == 0 || harberg.balanceOf(account) < amount) return; + + SwapExecutor executor = new SwapExecutor(pool, weth, harberg, token0isWeth); + vm.prank(account); + harberg.transfer(address(executor), amount); + + try executor.executeSell(amount, account) {} catch {} + } + + function _getOrCreateOptimizer(uint256 index) internal returns (address) { + if (index == 0) { + if (address(bullOptimizer) == address(0)) bullOptimizer = new BullMarketOptimizer(); + return address(bullOptimizer); + } else if (index == 1) { + if (address(neutralOptimizer) == address(0)) neutralOptimizer = new NeutralMarketOptimizer(); + return address(neutralOptimizer); + } else { + if (address(bearOptimizer) == address(0)) bearOptimizer = new BearMarketOptimizer(); + return address(bearOptimizer); + } + } + + function _getOptimizerByClass(string memory className) internal returns (address) { + bytes32 classHash = keccak256(abi.encodePacked(className)); + + if (classHash == keccak256(abi.encodePacked("BullMarketOptimizer"))) { + if (address(bullOptimizer) == address(0)) bullOptimizer = new BullMarketOptimizer(); + return address(bullOptimizer); + } else if (classHash == keccak256(abi.encodePacked("NeutralMarketOptimizer"))) { + if (address(neutralOptimizer) == address(0)) neutralOptimizer = new NeutralMarketOptimizer(); + return address(neutralOptimizer); + } else if (classHash == keccak256(abi.encodePacked("BearMarketOptimizer"))) { + if (address(bearOptimizer) == address(0)) bearOptimizer = new BearMarketOptimizer(); + return address(bearOptimizer); + } else if (classHash == keccak256(abi.encodePacked("WhaleOptimizer"))) { + if (address(whaleOptimizer) == address(0)) whaleOptimizer = new WhaleOptimizer(); + return address(whaleOptimizer); + } else if (classHash == keccak256(abi.encodePacked("MockOptimizer"))) { + if (address(mockOptimizer) == address(0)) { + mockOptimizer = new MockOptimizer(); + mockOptimizer.initialize(address(harberg), address(stake)); + } + return address(mockOptimizer); + } else if (classHash == keccak256(abi.encodePacked("RandomScenarioOptimizer"))) { + if (address(randomOptimizer) == address(0)) randomOptimizer = new RandomScenarioOptimizer(); + return address(randomOptimizer); + } else { + revert(string.concat("Unknown optimizer class: ", className, ". Available: BullMarketOptimizer, NeutralMarketOptimizer, BearMarketOptimizer, WhaleOptimizer, MockOptimizer, RandomScenarioOptimizer")); + } + } + + function _recordPositionData(string memory label) internal { + (,int24 currentTick,,,,,) = pool.slot0(); + + // Get each position + (uint128 floorLiq, int24 floorLower, int24 floorUpper) = lm.positions(ThreePositionStrategy.Stage.FLOOR); + (uint128 anchorLiq, int24 anchorLower, int24 anchorUpper) = lm.positions(ThreePositionStrategy.Stage.ANCHOR); + (uint128 discoveryLiq, int24 discoveryLower, int24 discoveryUpper) = lm.positions(ThreePositionStrategy.Stage.DISCOVERY); + + // Create position data row + string memory row = string.concat( + label, ",", + vm.toString(currentTick), ",", + vm.toString(floorLiq), ",", + vm.toString(floorLower), ",", + vm.toString(floorUpper), ",", + vm.toString(anchorLiq), ",", + vm.toString(anchorLower), ",", + vm.toString(anchorUpper), ",", + vm.toString(discoveryLiq), ",", + vm.toString(discoveryLower), ",", + vm.toString(discoveryUpper), ",", + vm.toString(weth.balanceOf(address(lm))), ",", + vm.toString(harberg.balanceOf(address(lm))) + ); + appendCSVRow(row); + } +} \ No newline at end of file diff --git a/onchain/analysis/README.md b/onchain/analysis/README.md index 70e3df8..b6e943a 100644 --- a/onchain/analysis/README.md +++ b/onchain/analysis/README.md @@ -1,92 +1,114 @@ -# KRAIKEN Liquidity Analysis +# KRAIKEN Fuzzing Analysis Tools -Analysis tools for testing the three-position anti-arbitrage strategy using random fuzzing to discover profitable trading scenarios. +This directory contains tools for fuzzing the KRAIKEN LiquidityManager to identify potential profitable trading scenarios. ## Quick Start -1. **Run the analysis** (includes parameter validation + random fuzzing): - ```bash - forge script analysis/SimpleAnalysis.s.sol --ffi --via-ir - ``` - -2. **Start visualization server**: - ```bash - cd analysis && python3 -m http.server 8000 - ``` - -3. **View results** at `http://localhost:8000/scenario-visualizer.html` - -## How It Works - -The analysis script uses **random fuzzing** like the historical `testScenarioFuzz` function to discover profitable trading scenarios: - -- **Random Parameters**: Generates random trading amounts, frequencies, and sentiment parameters -- **Historical Logic**: Uses exact trading logic from the working historical tests -- **Early Exit**: Stops immediately after finding the first profitable scenario -- **CSV Generation**: Creates detailed trading sequence data for visualization - -## Analysis Commands - -### Full Analysis (Recommended) ```bash -forge script analysis/SimpleAnalysis.s.sol --ffi --via-ir -``` -Runs both parameter validation and random fuzzing analysis. +# Run fuzzing with default settings (50 runs, 20 trades) +./run-fuzzing.sh BullMarketOptimizer -### Parameter Validation Only -Modify the `run()` function to comment out `runSentimentFuzzingAnalysis()` if you only want to test parameter configurations. +# Run with custom parameters +./run-fuzzing.sh WhaleOptimizer runs=100 trades=50 + +# Clean up generated files +./clean.sh +``` ## Files -- `SimpleAnalysis.s.sol` - Main analysis script with random fuzzing -- `scenario-visualizer.html` - Web-based position visualization -- `profitable_scenario.csv` - Generated when profitable scenarios are found -- `view-scenarios.sh` - HTTP server launcher (alternative to python server) +### Core Scripts +- `FuzzingAnalysis.s.sol` - Main Solidity fuzzing script that tests trading scenarios +- `run-fuzzing.sh` - Shell script to orchestrate multiple fuzzing runs +- `clean.sh` - Cleanup script to remove generated files -## Analysis Output - -### Console Output -- Parameter validation results for bull/neutral/bear market conditions -- Random fuzzing attempts with generated parameters -- Profitable scenario detection and stopping logic - -### CSV Data -When profitable scenarios are found, generates `profitable_scenario.csv` containing: -- Trading sequence (buy/sell/recenter actions) -- Position data (Floor/Anchor/Discovery liquidity amounts) -- Price tick information -- Token distribution across positions +### Helpers +- `helpers/SwapExecutor.sol` - Handles swap execution through Uniswap +- `helpers/CSVManager.sol` - CSV generation utilities +- `helpers/CSVHelper.sol` - CSV formatting helpers ### Visualization -Interactive HTML dashboard showing: -- Position ranges and token distributions -- Uniswap V3 liquidity calculations -- Trading sequence progression -- Anti-arbitrage strategy effectiveness +- `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 -## Random Fuzzing Details +## Available Optimizers -The script generates random parameters for each test: -- **Actions**: 6-12 trading actions per scenario -- **Frequency**: 1-5 recenter frequency -- **Amounts**: 50-255 basis values (scaled to ether) -- **Sentiment**: Random capital inefficiency, anchor share, width, discovery depth +- `BullMarketOptimizer` - Aggressive parameters for bull market conditions +- `NeutralMarketOptimizer` - Balanced parameters +- `BearMarketOptimizer` - Conservative parameters for bear market conditions +- `WhaleOptimizer` - Simulates large position dominance +- `MockOptimizer` - Standard mock with configurable parameters +- `RandomScenarioOptimizer` - Randomized parameters for each run -This approach mirrors the historical `testScenarioFuzz` function that successfully found profitable scenarios in the original codebase. +## Usage -## Troubleshooting +### Running Fuzzing Campaigns -### No Profitable Scenarios Found -- The anti-arbitrage protection is working effectively -- Try increasing `maxAttempts` in `runSentimentFuzzingAnalysis()` for more fuzzing attempts -- Check console output for parameter validation results +```bash +# Basic usage +./run-fuzzing.sh [runs=N] [trades=N] -### Script Execution Issues -- Ensure you're using the full script command (not `-s` function selection) -- The `setUp()` function is required for proper contract initialization -- Use `--via-ir` flag for complex contract compilation +# Examples +./run-fuzzing.sh BullMarketOptimizer # Uses defaults +./run-fuzzing.sh WhaleOptimizer runs=100 # 100 runs +./run-fuzzing.sh BearMarketOptimizer trades=50 # 50 trades per run +./run-fuzzing.sh NeutralMarketOptimizer runs=25 trades=30 # Both params +``` -### Visualization Issues -- Start the HTTP server from the `analysis/` directory -- Check that `profitable_scenario.csv` exists before viewing -- Browser security may block local file access - use the HTTP server \ No newline at end of file +Parameters: +- `optimizer_class` - Required. The optimizer class to use +- `runs=N` - Optional. Number of fuzzing runs (default: 50) +- `trades=N` - Optional. Trades per run (default: 20, actual will be ±5) + +### Output + +Each fuzzing campaign creates a timestamped directory with: +- Individual run logs (`run_N.log`) +- Merged CSV of profitable scenarios +- Summary report with statistics +- Configuration file for reproducibility + +### Visualization + +To visualize results: + +```bash +# Start local web server +./view-scenarios.sh + +# Or use Python directly +python3 -m http.server 8000 +# Then open http://localhost:8000/scenario-visualizer.html +``` + +### Cleanup + +Remove all generated files: + +```bash +./clean.sh +``` + +## Environment Variables + +The fuzzing script supports these environment variables: +- `FUZZING_RUNS` - Number of runs (overridden by script parameter) +- `OPTIMIZER_CLASS` - Optimizer to use (overridden by script parameter) +- `TRADES_PER_RUN` - Trades per run (overridden by script parameter) +- `TRACK_POSITIONS` - Enable detailed position tracking (default: false) + +## Development + +To add a new optimizer: +1. Create the optimizer contract in `../test/mocks/` +2. Import it in `FuzzingAnalysis.s.sol` +3. Add it to the `_getOptimizerByClass` function +4. Update this README + +## Notes + +- Each run deploys a fresh Uniswap V3 environment +- Gas limit is set to 200M for --via-ir compilation +- Results are deterministic based on the seed +- The fuzzer tests random buy/sell patterns with periodic recenters \ No newline at end of file diff --git a/onchain/analysis/SimpleAnalysis.s.sol b/onchain/analysis/SimpleAnalysis.s.sol deleted file mode 100644 index 64d392a..0000000 --- a/onchain/analysis/SimpleAnalysis.s.sol +++ /dev/null @@ -1,333 +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 "../test/mocks/BullMarketOptimizer.sol"; -import "../test/mocks/NeutralMarketOptimizer.sol"; -import "../test/mocks/BearMarketOptimizer.sol"; -import "./CSVManager.sol"; - -contract SimpleAnalysis is Test, CSVManager { - TestEnvironment testEnv; - IUniswapV3Factory factory; - IUniswapV3Pool pool; - IWETH9 weth; - Kraiken harberg; - Stake stake; - LiquidityManager lm; - bool token0isWeth; - - address account = makeAddr("trader"); - address feeDestination = makeAddr("fees"); - - uint256 public scenariosAnalyzed; - uint256 public profitableScenarios; - - // Test environment - BullMarketOptimizer bullOptimizer; - NeutralMarketOptimizer neutralOptimizer; - BearMarketOptimizer bearOptimizer; - - // CSV tracking for profitable scenarios - string[] profitableScenarioNames; - string[] profitableScenarioData; - - function run() public { - console.log("Starting LiquidityManager Analysis..."); - console.log("Testing 30 trades across 3 market conditions\n"); - - // Initialize test environment - testEnv = new TestEnvironment(feeDestination); - - // Test 3 different market sentiment optimizers - string[3] memory scenarioNames = ["Bull Market", "Neutral Market", "Bear Market"]; - - for (uint256 i = 0; i < 3; i++) { - console.log(string.concat("=== TESTING ", scenarioNames[i], " ===")); - - // Setup optimizer for this scenario - address optimizerAddress = _getOrCreateOptimizer(i); - - // Create fresh environment - (factory, pool, weth, harberg, stake, lm,, token0isWeth) = - testEnv.setupEnvironmentWithOptimizer(false, feeDestination, optimizerAddress); - - // Fund account - vm.deal(account, 500 ether); - vm.prank(account); - weth.deposit{value: 200 ether}(); - - // Initial recenter - vm.warp(block.timestamp + 5 hours); - vm.prank(feeDestination); - try lm.recenter() { - console.log("Initial recenter successful"); - } catch { - console.log("Initial recenter failed"); - } - - // Run trading scenario - bool foundProfit = _runTradingScenario(scenarioNames[i]); - - if (foundProfit) { - console.log("PROFITABLE scenario found!"); - } else { - console.log("No profitable trades found"); - } - console.log(""); - } - - console.log("=== ANALYSIS COMPLETE ==="); - console.log(string.concat("Scenarios analyzed: ", vm.toString(scenariosAnalyzed))); - console.log(string.concat("Profitable scenarios: ", vm.toString(profitableScenarios))); - - // Write CSV files for profitable scenarios - if (profitableScenarios > 0) { - console.log("\nWriting CSV files for profitable scenarios..."); - for (uint256 i = 0; i < profitableScenarioNames.length; i++) { - string memory filename = string.concat("analysis/profitable_", profitableScenarioNames[i], ".csv"); - csv = profitableScenarioData[i]; - writeCSVToFile(filename); - console.log(string.concat("Wrote: ", filename)); - } - } - } - - function _getOrCreateOptimizer(uint256 scenarioIndex) internal returns (address) { - if (scenarioIndex == 0) { - if (address(bullOptimizer) == address(0)) { - bullOptimizer = new BullMarketOptimizer(); - } - return address(bullOptimizer); - } else if (scenarioIndex == 1) { - if (address(neutralOptimizer) == address(0)) { - neutralOptimizer = new NeutralMarketOptimizer(); - } - return address(neutralOptimizer); - } else { - if (address(bearOptimizer) == address(0)) { - bearOptimizer = new BearMarketOptimizer(); - } - return address(bearOptimizer); - } - } - - function _runTradingScenario(string memory scenarioName) internal returns (bool foundProfit) { - uint256 initialBalance = weth.balanceOf(account); - console.log(string.concat("Starting balance: ", vm.toString(initialBalance / 1e18), " ETH")); - - // Initialize CSV for this scenario - initializeTimeSeriesCSV(); - - // Force initial buy to get some HARB - _executeBuy(10 ether); - _recordTradeToCSV(block.timestamp, "BUY", 10 ether, 0); - - // Execute 30 trades - for (uint256 i = 1; i < 30; i++) { - uint256 seed = uint256(keccak256(abi.encodePacked(scenarioName, i))); - bool isBuy = (seed % 2) == 0; - - if (isBuy) { - uint256 amount = 1 ether + (seed % 10 ether); - if (weth.balanceOf(account) >= amount) { - _executeBuy(amount); - _recordTradeToCSV(block.timestamp + i * 60, "BUY", amount, 0); - } - } else { - uint256 harbBalance = harberg.balanceOf(account); - if (harbBalance > 0) { - uint256 sellAmount = harbBalance / 4; - if (sellAmount > 0) { - _executeSell(sellAmount); - _recordTradeToCSV(block.timestamp + i * 60, "SELL", 0, sellAmount); - } - } - } - - // Try recenter occasionally - if (i % 3 == 0) { - vm.prank(feeDestination); - try lm.recenter() { - console.log(" Recenter successful"); - } catch Error(string memory reason) { - console.log(string.concat(" Recenter failed: ", reason)); - } catch { - console.log(" Recenter failed: cooldown or other error"); - } - } - } - - // Sell all remaining HARB - uint256 finalHarb = harberg.balanceOf(account); - if (finalHarb > 0) { - _executeSell(finalHarb); - _recordTradeToCSV(block.timestamp + 31 * 60, "SELL", 0, finalHarb); - } - - // Final recenter after all trades - vm.warp(block.timestamp + 5 hours); - vm.prank(feeDestination); - try lm.recenter() { - console.log("Final recenter successful"); - } catch Error(string memory reason) { - console.log(string.concat("Final recenter failed: ", reason)); - } catch { - console.log("Final recenter failed: unknown error"); - } - - uint256 finalBalance = weth.balanceOf(account); - console.log(string.concat("Final balance: ", vm.toString(finalBalance / 1e18), " ETH")); - - scenariosAnalyzed++; - if (finalBalance > initialBalance) { - console.log(string.concat("PROFIT: ", vm.toString((finalBalance - initialBalance) / 1e18), " ETH")); - profitableScenarios++; - - // Store profitable scenario data - profitableScenarioNames.push(scenarioName); - profitableScenarioData.push(csv); - - return true; - } else { - console.log(string.concat("Loss: ", vm.toString((initialBalance - finalBalance) / 1e18), " ETH")); - return false; - } - } - - function _executeBuy(uint256 amount) internal { - console.log(string.concat(" Buy ", vm.toString(amount / 1e18), " ETH worth")); - - // Create a separate contract to handle the swap - SwapExecutor executor = new SwapExecutor(pool, weth, harberg, token0isWeth); - - // Transfer WETH to executor - vm.prank(account); - weth.transfer(address(executor), amount); - - // Execute the swap - try executor.executeBuy(amount, account) { - console.log(" Buy successful"); - } catch Error(string memory reason) { - console.log(string.concat(" Buy failed: ", reason)); - } catch { - console.log(" Buy failed: unknown error"); - } - } - - function _executeSell(uint256 amount) internal { - console.log(string.concat(" Sell ", vm.toString(amount / 1e18), " HARB")); - - // Create a separate contract to handle the swap - SwapExecutor executor = new SwapExecutor(pool, weth, harberg, token0isWeth); - - // Transfer HARB to executor - vm.prank(account); - harberg.transfer(address(executor), amount); - - // Execute the swap - try executor.executeSell(amount, account) { - console.log(" Sell successful"); - } catch Error(string memory reason) { - console.log(string.concat(" Sell failed: ", reason)); - } catch { - console.log(" Sell failed: unknown error"); - } - } - - function _recordTradeToCSV(uint256 timestamp, string memory action, uint256 ethAmount, uint256 harbAmount) internal { - // Get current price - (uint160 sqrtPriceX96,,,,,, ) = pool.slot0(); - uint256 price = _sqrtPriceToPrice(sqrtPriceX96); - - // Get supply data - uint256 totalSupply = harberg.totalSupply(); - uint256 stakeShares = stake.outstandingStake(); - uint256 avgTaxRate = stake.getAverageTaxRate(); - - // Create CSV row - string memory row = string.concat( - vm.toString(timestamp), ",", - vm.toString(price), ",", - vm.toString(totalSupply), ",", - action, ",", - vm.toString(ethAmount), ",", - vm.toString(harbAmount), ",", - vm.toString(stakeShares), ",", - vm.toString(avgTaxRate) - ); - - appendCSVRow(row); - } - - function _sqrtPriceToPrice(uint160 sqrtPriceX96) internal view returns (uint256) { - if (token0isWeth) { - // price = (sqrtPrice / 2^96)^2 * 10^18 - return (uint256(sqrtPriceX96) * uint256(sqrtPriceX96) * 1e18) >> 192; - } else { - // price = 1 / ((sqrtPrice / 2^96)^2) * 10^18 - return (1e18 << 192) / (uint256(sqrtPriceX96) * uint256(sqrtPriceX96)); - } - } -} - -// Helper contract to execute swaps without address(this) issues -contract SwapExecutor { - IUniswapV3Pool public pool; - IWETH9 public weth; - Kraiken public harberg; - bool public token0isWeth; - - constructor(IUniswapV3Pool _pool, IWETH9 _weth, Kraiken _harberg, bool _token0isWeth) { - pool = _pool; - weth = _weth; - harberg = _harberg; - token0isWeth = _token0isWeth; - } - - function executeBuy(uint256 amount, address recipient) external { - pool.swap( - recipient, - token0isWeth, - int256(amount), - token0isWeth ? 4295128740 : 1461446703485210103287273052203988822378723970341, - "" - ); - } - - function executeSell(uint256 amount, address recipient) external { - pool.swap( - recipient, - !token0isWeth, - int256(amount), - !token0isWeth ? 4295128740 : 1461446703485210103287273052203988822378723970341, - "" - ); - } - - function uniswapV3SwapCallback(int256 amount0Delta, int256 amount1Delta, bytes calldata) external { - require(msg.sender == address(pool), "Invalid caller"); - - if (amount0Delta > 0) { - if (token0isWeth) { - weth.transfer(msg.sender, uint256(amount0Delta)); - } else { - harberg.transfer(msg.sender, uint256(amount0Delta)); - } - } - if (amount1Delta > 0) { - if (token0isWeth) { - harberg.transfer(msg.sender, uint256(amount1Delta)); - } else { - weth.transfer(msg.sender, uint256(amount1Delta)); - } - } - } -} \ No newline at end of file diff --git a/onchain/analysis/examples/batch_analysis.sh b/onchain/analysis/examples/batch_analysis.sh deleted file mode 100755 index 0d872cc..0000000 --- a/onchain/analysis/examples/batch_analysis.sh +++ /dev/null @@ -1,39 +0,0 @@ -#!/bin/bash - -# Batch Scenario Analysis Script -# This script runs multiple analysis scenarios to discover profitable trading patterns - -echo "🔬 Starting Batch Scenario Analysis for LiquidityManager" -echo "=======================================================" - -# Ensure output directory exists -mkdir -p ./out - -# Enable FFI for file operations -export FOUNDRY_FFI=true - -echo "📋 Running Unit Tests First (ensure protocol safety)..." -forge test -q - -if [ $? -ne 0 ]; then - echo "❌ Unit tests failed. Fix issues before running analysis." - exit 1 -fi - -echo "✅ Unit tests passed. Proceeding with scenario analysis..." - -echo "🔍 Running Simple Analysis Script..." -forge script analysis/SimpleAnalysis.s.sol --ffi -v - -echo "📊 Analysis Results:" -echo "Check ./analysis/ directory for any profitable_scenario.csv files" -echo "These indicate potentially exploitable trading sequences" -echo "" -echo "🌐 To view visualizations, run:" -echo " ./analysis/view-scenarios.sh" - -echo "🎯 To investigate specific scenarios, modify the analysis contract" -echo "and run targeted tests with custom parameters." - -echo "✅ Batch analysis complete!" -echo "Review any profitable scenarios for potential protocol improvements." \ No newline at end of file diff --git a/onchain/analysis/CSVHelper.sol b/onchain/analysis/helpers/CSVHelper.sol similarity index 100% rename from onchain/analysis/CSVHelper.sol rename to onchain/analysis/helpers/CSVHelper.sol diff --git a/onchain/analysis/CSVManager.sol b/onchain/analysis/helpers/CSVManager.sol similarity index 100% rename from onchain/analysis/CSVManager.sol rename to onchain/analysis/helpers/CSVManager.sol diff --git a/onchain/analysis/helpers/SwapExecutor.sol b/onchain/analysis/helpers/SwapExecutor.sol new file mode 100644 index 0000000..f818821 --- /dev/null +++ b/onchain/analysis/helpers/SwapExecutor.sol @@ -0,0 +1,94 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8.19; + +import {IUniswapV3Pool} from "@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol"; +import {IWETH9} from "../../src/interfaces/IWETH9.sol"; +import {Kraiken} from "../../src/Kraiken.sol"; +import {TickMath} from "@aperture/uni-v3-lib/TickMath.sol"; + +/** + * @title SwapExecutor + * @notice Helper contract to execute swaps on Uniswap V3 pools for analysis scripts + * @dev Extracted from analysis scripts to avoid code duplication + */ +contract SwapExecutor { + IUniswapV3Pool public pool; + IWETH9 public weth; + Kraiken public harberg; + bool public token0isWeth; + + constructor(IUniswapV3Pool _pool, IWETH9 _weth, Kraiken _harberg, bool _token0isWeth) { + pool = _pool; + weth = _weth; + harberg = _harberg; + token0isWeth = _token0isWeth; + } + + function executeBuy(uint256 amount, address recipient) external { + // 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 + bool zeroForOne = token0isWeth; + + // Set appropriate price limit based on swap direction + uint160 sqrtPriceLimitX96; + if (zeroForOne) { + // Price goes down (in terms of token0/token1 ratio) + sqrtPriceLimitX96 = TickMath.MIN_SQRT_RATIO + 1; + } else { + // Price goes up + sqrtPriceLimitX96 = TickMath.MAX_SQRT_RATIO - 1; + } + + try pool.swap( + recipient, + zeroForOne, + int256(amount), + sqrtPriceLimitX96, + "" + ) {} catch { + // Swap failed, likely due to extreme price - ignore + } + } + + function executeSell(uint256 amount, address recipient) external { + // 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 + bool zeroForOne = !token0isWeth; + + // Set appropriate price limit based on swap direction + uint160 sqrtPriceLimitX96; + if (zeroForOne) { + // Price goes down (in terms of token0/token1 ratio) + sqrtPriceLimitX96 = TickMath.MIN_SQRT_RATIO + 1; + } else { + // Price goes up + sqrtPriceLimitX96 = TickMath.MAX_SQRT_RATIO - 1; + } + + try pool.swap( + recipient, + zeroForOne, + int256(amount), + sqrtPriceLimitX96, + "" + ) {} catch { + // Swap failed, likely due to extreme price - ignore + } + } + + // Callback required for Uniswap V3 swaps + function uniswapV3SwapCallback( + int256 amount0Delta, + int256 amount1Delta, + bytes calldata + ) external { + require(msg.sender == address(pool), "Unauthorized callback"); + + if (amount0Delta > 0) { + IWETH9(pool.token0()).transfer(address(pool), uint256(amount0Delta)); + } + if (amount1Delta > 0) { + IWETH9(pool.token1()).transfer(address(pool), uint256(amount1Delta)); + } + } +} \ No newline at end of file diff --git a/onchain/analysis/requirements.txt b/onchain/analysis/requirements.txt new file mode 100644 index 0000000..95f51d5 --- /dev/null +++ b/onchain/analysis/requirements.txt @@ -0,0 +1,4 @@ +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 new file mode 100755 index 0000000..e614756 --- /dev/null +++ b/onchain/analysis/run-fuzzing.sh @@ -0,0 +1,201 @@ +#!/bin/bash + +# Change to the analysis directory (where this script is located) +cd "$(dirname "$0")" + +# Default values +OPTIMIZER_CLASS="" +TOTAL_RUNS=50 +TRADES_PER_RUN=20 + +# Function to show usage +show_usage() { + echo "Usage: $0 [runs=N] [trades=N]" + echo "" + echo "Parameters:" + echo " optimizer_class Required. The optimizer class to use" + echo " runs=N Optional. Number of fuzzing runs (default: 50)" + echo " trades=N Optional. Trades per run (default: 20, actual will be ±5)" + echo "" + echo "Examples:" + echo " $0 BullMarketOptimizer" + echo " $0 WhaleOptimizer runs=100" + echo " $0 BearMarketOptimizer runs=10 trades=50" + echo " $0 NeutralMarketOptimizer trades=30 runs=25" + echo "" + echo "Available optimizers:" + echo " - BullMarketOptimizer" + echo " - NeutralMarketOptimizer" + echo " - BearMarketOptimizer" + echo " - WhaleOptimizer" + echo " - MockOptimizer" + echo " - RandomScenarioOptimizer" +} + +# Parse arguments +if [ $# -eq 0 ]; then + echo "Error: No optimizer class specified" + show_usage + exit 1 +fi + +# First argument is always the optimizer class +OPTIMIZER_CLASS=$1 +shift + +# Parse named parameters +for arg in "$@"; do + case $arg in + runs=*) + TOTAL_RUNS="${arg#*=}" + if ! [[ "$TOTAL_RUNS" =~ ^[0-9]+$ ]] || [ "$TOTAL_RUNS" -eq 0 ]; then + echo "Error: Invalid value for runs. Must be a positive integer." + exit 1 + fi + ;; + trades=*) + TRADES_PER_RUN="${arg#*=}" + if ! [[ "$TRADES_PER_RUN" =~ ^[0-9]+$ ]] || [ "$TRADES_PER_RUN" -eq 0 ]; then + echo "Error: Invalid value for trades. Must be a positive integer." + exit 1 + fi + ;; + *) + echo "Error: Unknown parameter '$arg'" + show_usage + exit 1 + ;; + esac +done + +# Colors for output +GREEN='\033[0;32m' +RED='\033[0;31m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +OUTPUT_DIR="fuzzing_results_${OPTIMIZER_CLASS}_$(date +%Y%m%d_%H%M%S)" +MERGED_CSV="$OUTPUT_DIR/merged_profitable_scenarios.csv" + +echo -e "${GREEN}=== Fuzzing Campaign ===${NC}" +echo "Optimizer: $OPTIMIZER_CLASS" +echo "Total runs: $TOTAL_RUNS" +echo "Trades per run: $TRADES_PER_RUN (±5)" +echo "Output directory: $OUTPUT_DIR" +echo "" + +# Validate that the optimizer class exists by doing a dry run +echo "Validating optimizer class..." +OPTIMIZER_CLASS="$OPTIMIZER_CLASS" FUZZING_RUNS=0 forge script FuzzingAnalysis.s.sol --ffi --via-ir > /tmp/optimizer_check.log 2>&1 +if [ $? -ne 0 ]; then + echo -e "${RED}Error: Invalid optimizer class '${OPTIMIZER_CLASS}'${NC}" + echo -e "${RED}Check the error:${NC}" + grep -E "(Unknown optimizer|revert)" /tmp/optimizer_check.log + echo "" + show_usage + exit 1 +fi + +# Create output directory +mkdir -p "$OUTPUT_DIR" + +# Initialize merged CSV with header +echo "Scenario,Seed,Initial Balance,Final Balance,Profit,Profit %" > "$MERGED_CSV" + +# Track statistics +TOTAL_PROFITABLE=0 +FAILED_RUNS=0 + +# Save configuration +CONFIG_FILE="$OUTPUT_DIR/config.txt" +{ + echo "Fuzzing Configuration" + echo "====================" + echo "Optimizer: $OPTIMIZER_CLASS" + echo "Total runs: $TOTAL_RUNS" + echo "Trades per run: $TRADES_PER_RUN (±5)" + echo "Start time: $(date)" +} > "$CONFIG_FILE" + +# Run fuzzing analysis multiple times +for i in $(seq 1 $TOTAL_RUNS); do + echo -e "${YELLOW}Running fuzzing iteration $i/$TOTAL_RUNS...${NC}" + + # Run single fuzzing iteration with specified optimizer and trades + OPTIMIZER_CLASS="$OPTIMIZER_CLASS" TRADES_PER_RUN="$TRADES_PER_RUN" FUZZING_RUNS=1 forge script FuzzingAnalysis.s.sol --ffi --via-ir --gas-limit 200000000 > "$OUTPUT_DIR/run_$i.log" 2>&1 + + if [ $? -eq 0 ]; then + echo -e "${GREEN}✓ Run $i completed${NC}" + + # Check if profitable scenarios were found + if grep -q "PROFITABLE!" "$OUTPUT_DIR/run_$i.log"; then + echo -e "${GREEN} Found profitable scenario!${NC}" + ((TOTAL_PROFITABLE++)) + + # Extract profit percentage + PROFIT_PCT=$(grep "PROFITABLE!" "$OUTPUT_DIR/run_$i.log" | grep -oE "Profit: [0-9]+%" | grep -oE "[0-9]+") + echo -e "${GREEN} Profit: ${PROFIT_PCT}%${NC}" + + # Extract CSV file path if generated + CSV_FILE=$(grep "Profitable scenarios written to:" "$OUTPUT_DIR/run_$i.log" | awk '{print $NF}') + if [ -n "$CSV_FILE" ] && [ -f "$CSV_FILE" ]; then + # Append data rows (skip header) to merged CSV + tail -n +2 "$CSV_FILE" >> "$MERGED_CSV" + # Move individual CSV to output directory + mv "$CSV_FILE" "$OUTPUT_DIR/" + fi + fi + else + echo -e "${RED}✗ Run $i failed${NC}" + ((FAILED_RUNS++)) + # Show last few lines of error + echo -e "${RED}Error details:${NC}" + tail -5 "$OUTPUT_DIR/run_$i.log" + fi + + # Small delay to avoid overwhelming the system + sleep 0.5 +done + +# Update config with end time +echo "End time: $(date)" >> "$CONFIG_FILE" + +echo "" +echo -e "${GREEN}=== FUZZING CAMPAIGN COMPLETE ===${NC}" +echo "Optimizer: $OPTIMIZER_CLASS" +echo "Total runs: $TOTAL_RUNS" +echo "Trades per run: $TRADES_PER_RUN (±5)" +echo "Successful runs: $((TOTAL_RUNS - FAILED_RUNS))" +echo "Failed runs: $FAILED_RUNS" +echo "Total profitable scenarios: $TOTAL_PROFITABLE" +echo "" +echo "Results saved in: $OUTPUT_DIR" +echo "Merged CSV: $MERGED_CSV" + +# Generate summary report +SUMMARY="$OUTPUT_DIR/summary.txt" +{ + echo "Fuzzing Campaign Summary" + echo "========================" + echo "Date: $(date)" + echo "Optimizer: $OPTIMIZER_CLASS" + echo "Total runs: $TOTAL_RUNS" + echo "Trades per run: $TRADES_PER_RUN (±5)" + echo "" + echo "Results:" + echo "--------" + echo "Successful runs: $((TOTAL_RUNS - FAILED_RUNS)) / $TOTAL_RUNS" + echo "Failed runs: $FAILED_RUNS" + echo "Total profitable scenarios: $TOTAL_PROFITABLE / $((TOTAL_RUNS - FAILED_RUNS))" + echo "Success rate: $(awk "BEGIN {if ($TOTAL_RUNS - $FAILED_RUNS > 0) printf \"%.2f\", $TOTAL_PROFITABLE/($TOTAL_RUNS-$FAILED_RUNS)*100; else print \"0.00\"}")%" +} > "$SUMMARY" + +echo "" +echo "Summary report: $SUMMARY" + +# If there were profitable scenarios, show a sample +if [ $TOTAL_PROFITABLE -gt 0 ]; then + echo "" + echo -e "${GREEN}Sample profitable scenarios:${NC}" + head -5 "$MERGED_CSV" +fi \ No newline at end of file diff --git a/onchain/foundry.toml b/onchain/foundry.toml index 8ca3ae7..f83b1aa 100644 --- a/onchain/foundry.toml +++ b/onchain/foundry.toml @@ -3,6 +3,10 @@ src = "src" out = "out" libs = ["lib"] fs_permissions = [{ access = "read-write", path = "./"}] +gas_limit = 100_000_000 +gas_price = 0 +optimizer = true +optimizer_runs = 200 # See more config options https://github.com/foundry-rs/foundry/tree/master/config [rpc_endpoints] diff --git a/onchain/test/helpers/LiquidityBoundaryHelper.sol b/onchain/test/helpers/LiquidityBoundaryHelper.sol new file mode 100644 index 0000000..1c39ec8 --- /dev/null +++ b/onchain/test/helpers/LiquidityBoundaryHelper.sol @@ -0,0 +1,118 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import {IUniswapV3Pool} from "@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol"; +import {TickMath} from "@aperture/uni-v3-lib/TickMath.sol"; +import {LiquidityAmounts} from "@aperture/uni-v3-lib/LiquidityAmounts.sol"; +import {ThreePositionStrategy} from "../../src/abstracts/ThreePositionStrategy.sol"; + +/** + * @title LiquidityBoundaryHelper + * @notice Helper library for calculating safe trade sizes within liquidity boundaries + * @dev Prevents trades that would exceed available liquidity and cause SPL errors + */ +library LiquidityBoundaryHelper { + /** + * @notice Calculates the maximum ETH amount that can be traded (buy HARB) without exceeding position liquidity limits + * @param pool The Uniswap V3 pool + * @param liquidityManager The liquidity manager contract + * @param token0isWeth Whether token0 is WETH + * @return maxEthAmount Maximum ETH that can be safely traded + */ + function calculateBuyLimit( + IUniswapV3Pool pool, + ThreePositionStrategy liquidityManager, + bool token0isWeth + ) internal view returns (uint256 maxEthAmount) { + (, int24 currentTick,,,,,) = pool.slot0(); + + // Get position data + (uint128 anchorLiquidity, int24 anchorLower, int24 anchorUpper) = liquidityManager.positions(ThreePositionStrategy.Stage.ANCHOR); + (uint128 discoveryLiquidity, int24 discoveryLower, int24 discoveryUpper) = liquidityManager.positions(ThreePositionStrategy.Stage.DISCOVERY); + + // If no positions exist, return 0 (no safe limit) + if (anchorLiquidity == 0 && discoveryLiquidity == 0) { + return 0; + } + + uint256 maxEth = 0; + + // Check anchor position + if (currentTick >= anchorLower && currentTick < anchorUpper && anchorLiquidity > 0) { + uint160 currentSqrtPrice = TickMath.getSqrtRatioAtTick(currentTick); + uint160 upperSqrtPrice = TickMath.getSqrtRatioAtTick(anchorUpper); + + if (token0isWeth) { + maxEth = LiquidityAmounts.getAmount0ForLiquidity(currentSqrtPrice, upperSqrtPrice, anchorLiquidity); + } else { + maxEth = LiquidityAmounts.getAmount1ForLiquidity(currentSqrtPrice, upperSqrtPrice, anchorLiquidity); + } + } + // Check discovery position + else if (currentTick >= discoveryLower && currentTick < discoveryUpper && discoveryLiquidity > 0) { + uint160 currentSqrtPrice = TickMath.getSqrtRatioAtTick(currentTick); + uint160 upperSqrtPrice = TickMath.getSqrtRatioAtTick(discoveryUpper); + + if (token0isWeth) { + maxEth = LiquidityAmounts.getAmount0ForLiquidity(currentSqrtPrice, upperSqrtPrice, discoveryLiquidity); + } else { + maxEth = LiquidityAmounts.getAmount1ForLiquidity(currentSqrtPrice, upperSqrtPrice, discoveryLiquidity); + } + } + + // Apply safety margin (90% of calculated max) + return (maxEth * 9) / 10; + } + + /** + * @notice Calculates the maximum HARB amount that can be traded (sell HARB) without exceeding position liquidity limits + * @param pool The Uniswap V3 pool + * @param liquidityManager The liquidity manager contract + * @param token0isWeth Whether token0 is WETH + * @return maxHarbAmount Maximum HARB that can be safely traded + */ + function calculateSellLimit( + IUniswapV3Pool pool, + ThreePositionStrategy liquidityManager, + bool token0isWeth + ) internal view returns (uint256 maxHarbAmount) { + (, int24 currentTick,,,,,) = pool.slot0(); + + // Get position data + (uint128 anchorLiquidity, int24 anchorLower, int24 anchorUpper) = liquidityManager.positions(ThreePositionStrategy.Stage.ANCHOR); + (uint128 floorLiquidity, int24 floorLower, int24 floorUpper) = liquidityManager.positions(ThreePositionStrategy.Stage.FLOOR); + + // If no positions exist, return 0 (no safe limit) + if (anchorLiquidity == 0 && floorLiquidity == 0) { + return 0; + } + + uint256 maxHarb = 0; + + // Check anchor position + if (currentTick >= anchorLower && currentTick < anchorUpper && anchorLiquidity > 0) { + uint160 currentSqrtPrice = TickMath.getSqrtRatioAtTick(currentTick); + uint160 lowerSqrtPrice = TickMath.getSqrtRatioAtTick(anchorLower); + + if (token0isWeth) { + maxHarb = LiquidityAmounts.getAmount1ForLiquidity(lowerSqrtPrice, currentSqrtPrice, anchorLiquidity); + } else { + maxHarb = LiquidityAmounts.getAmount0ForLiquidity(lowerSqrtPrice, currentSqrtPrice, anchorLiquidity); + } + } + // Check floor position + else if (currentTick >= floorLower && currentTick < floorUpper && floorLiquidity > 0) { + uint160 currentSqrtPrice = TickMath.getSqrtRatioAtTick(currentTick); + uint160 lowerSqrtPrice = TickMath.getSqrtRatioAtTick(floorLower); + + if (token0isWeth) { + maxHarb = LiquidityAmounts.getAmount1ForLiquidity(lowerSqrtPrice, currentSqrtPrice, floorLiquidity); + } else { + maxHarb = LiquidityAmounts.getAmount0ForLiquidity(lowerSqrtPrice, currentSqrtPrice, floorLiquidity); + } + } + + // Apply safety margin (90% of calculated max) + return (maxHarb * 9) / 10; + } +} \ No newline at end of file diff --git a/onchain/test/helpers/TestBase.sol b/onchain/test/helpers/TestBase.sol index 73eb2fc..ad25c42 100644 --- a/onchain/test/helpers/TestBase.sol +++ b/onchain/test/helpers/TestBase.sol @@ -243,6 +243,61 @@ contract TestEnvironment is TestConstants { return (factory, pool, weth, harberg, stake, lm, optimizer, token0isWeth); } + /** + * @notice Setup environment with existing factory and specific optimizer + * @param existingFactory The existing Uniswap factory to use + * @param token0shouldBeWeth Whether WETH should be token0 + * @param recenterCaller Address that will be granted recenter access + * @param optimizerAddress Address of the optimizer to use + * @return _factory The existing Uniswap factory + * @return _pool The created Uniswap pool + * @return _weth The WETH token contract + * @return _harberg The Kraiken token contract + * @return _stake The staking contract + * @return _lm The liquidity manager contract + * @return _optimizer The optimizer contract + * @return _token0isWeth Whether token0 is WETH + */ + function setupEnvironmentWithExistingFactory( + IUniswapV3Factory existingFactory, + bool token0shouldBeWeth, + address recenterCaller, + address optimizerAddress + ) external returns ( + IUniswapV3Factory _factory, + IUniswapV3Pool _pool, + IWETH9 _weth, + Kraiken _harberg, + Stake _stake, + LiquidityManager _lm, + Optimizer _optimizer, + bool _token0isWeth + ) { + // Use existing factory + factory = existingFactory; + + // Deploy tokens in correct order + _deployTokensWithOrder(token0shouldBeWeth); + + // Create and initialize pool + _createAndInitializePool(); + + // Deploy protocol contracts with custom optimizer + stake = new Stake(address(harberg), feeDestination); + optimizer = Optimizer(optimizerAddress); + lm = new LiquidityManager(address(factory), address(weth), address(harberg), optimizerAddress); + lm.setFeeDestination(feeDestination); + + // Configure permissions + _configurePermissions(); + + // Grant recenter access to specified caller + vm.prank(feeDestination); + lm.setRecenterAccess(recenterCaller); + + return (factory, pool, weth, harberg, stake, lm, optimizer, token0isWeth); + } + /** * @notice Perform recenter with proper time warp and oracle updates * @param liquidityManager The LiquidityManager instance to recenter diff --git a/onchain/test/helpers/UniswapTestBase.sol b/onchain/test/helpers/UniswapTestBase.sol index 7fcef6e..66a8d01 100644 --- a/onchain/test/helpers/UniswapTestBase.sol +++ b/onchain/test/helpers/UniswapTestBase.sol @@ -9,6 +9,7 @@ import {SqrtPriceMath} from "@aperture/uni-v3-lib/SqrtPriceMath.sol"; import "../../src/interfaces/IWETH9.sol"; import {Kraiken} from "../../src/Kraiken.sol"; import {ThreePositionStrategy} from "../../src/abstracts/ThreePositionStrategy.sol"; +import {LiquidityBoundaryHelper} from "./LiquidityBoundaryHelper.sol"; /** * @title UniSwapHelper @@ -238,23 +239,7 @@ abstract contract UniSwapHelper is Test { // Get LiquidityManager reference from test context // This assumes the test has a 'lm' variable for the LiquidityManager try this.getLiquidityManager() returns (ThreePositionStrategy liquidityManager) { - (, int24 currentTick,,,,,) = pool.slot0(); - - // Get position data - (uint128 anchorLiquidity, int24 anchorLower, int24 anchorUpper) = liquidityManager.positions(ThreePositionStrategy.Stage.ANCHOR); - (uint128 discoveryLiquidity, int24 discoveryLower, int24 discoveryUpper) = liquidityManager.positions(ThreePositionStrategy.Stage.DISCOVERY); - - // If no positions exist, return 0 (no safe limit) - if (anchorLiquidity == 0 && discoveryLiquidity == 0) { - return 0; - } - - // Calculate based on token ordering and current price position - if (token0isWeth) { - return _calculateBuyLimitToken0IsWeth(currentTick, anchorLiquidity, anchorLower, anchorUpper, discoveryLiquidity, discoveryLower, discoveryUpper); - } else { - return _calculateBuyLimitToken1IsWeth(currentTick, anchorLiquidity, anchorLower, anchorUpper, discoveryLiquidity, discoveryLower, discoveryUpper); - } + return LiquidityBoundaryHelper.calculateBuyLimit(pool, liquidityManager, token0isWeth); } catch { return 0; // Safe fallback if LiquidityManager access fails } @@ -267,23 +252,7 @@ abstract contract UniSwapHelper is Test { */ function sellLimitToLiquidityBoundary() internal view returns (uint256 maxHarbAmount) { try this.getLiquidityManager() returns (ThreePositionStrategy liquidityManager) { - (, int24 currentTick,,,,,) = pool.slot0(); - - // Get position data - (uint128 anchorLiquidity, int24 anchorLower, int24 anchorUpper) = liquidityManager.positions(ThreePositionStrategy.Stage.ANCHOR); - (uint128 floorLiquidity, int24 floorLower, int24 floorUpper) = liquidityManager.positions(ThreePositionStrategy.Stage.FLOOR); - - // If no positions exist, return 0 (no safe limit) - if (anchorLiquidity == 0 && floorLiquidity == 0) { - return 0; - } - - // Calculate based on token ordering and current price position - if (token0isWeth) { - return _calculateSellLimitToken0IsWeth(currentTick, anchorLiquidity, anchorLower, anchorUpper, floorLiquidity, floorLower, floorUpper); - } else { - return _calculateSellLimitToken1IsWeth(currentTick, anchorLiquidity, anchorLower, anchorUpper, floorLiquidity, floorLower, floorUpper); - } + return LiquidityBoundaryHelper.calculateSellLimit(pool, liquidityManager, token0isWeth); } catch { return 0; // Safe fallback if LiquidityManager access fails } diff --git a/onchain/test/mocks/BullMarketOptimizer.sol b/onchain/test/mocks/BullMarketOptimizer.sol index e948ccb..43429cf 100644 --- a/onchain/test/mocks/BullMarketOptimizer.sol +++ b/onchain/test/mocks/BullMarketOptimizer.sol @@ -15,23 +15,23 @@ contract BullMarketOptimizer { return 0; // Placeholder implementation } - /// @notice Returns bull market liquidity parameters - /// @return capitalInefficiency 20% - aggressive - /// @return anchorShare 80% - large anchor - /// @return anchorWidth 30 - narrow width - /// @return discoveryDepth 90% - deep discovery + /// @notice Returns whale attack liquidity parameters + /// @return capitalInefficiency 10% - very aggressive + /// @return anchorShare 95% - massive anchor concentration + /// @return anchorWidth 80 - moderate width + /// @return discoveryDepth 5% - minimal discovery function getLiquidityParams() external pure returns (uint256 capitalInefficiency, uint256 anchorShare, uint24 anchorWidth, uint256 discoveryDepth) { - capitalInefficiency = 2 * 10 ** 17; // 20% - aggressive - anchorShare = 8 * 10 ** 17; // 80% - large anchor - anchorWidth = 30; // narrow width - discoveryDepth = 9 * 10 ** 17; // 90% - deep discovery + capitalInefficiency = 1e17; // 10% - very aggressive + anchorShare = 95e16; // 95% - massive anchor position + anchorWidth = 80; // moderate width (was 10) + discoveryDepth = 5e16; // 5% - minimal discovery } function getDescription() external pure returns (string memory) { - return "Bull Market (High Risk)"; + return "Bull Market (Whale Attack Parameters)"; } } \ No newline at end of file diff --git a/onchain/test/mocks/WhaleOptimizer.sol b/onchain/test/mocks/WhaleOptimizer.sol new file mode 100644 index 0000000..e46594d --- /dev/null +++ b/onchain/test/mocks/WhaleOptimizer.sol @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8.19; + +import {Kraiken} from "../../src/Kraiken.sol"; +import {Stake} from "../../src/Stake.sol"; + +/** + * @title WhaleOptimizer + * @notice Simulates large position dominance with extremely aggressive parameters + * @dev Tests vulnerability to large trades that can move price significantly + */ +contract WhaleOptimizer { + function calculateSentiment(uint256, uint256) external pure returns (uint256) { + return 0; + } + + function getSentiment() external pure returns (uint256) { + return 0; + } + + function getLiquidityParams() external pure returns (uint256, uint256, uint24, uint256) { + return ( + 1e17, // capitalInefficiency: 10% (very aggressive) + 95e16, // anchorShare: 95% (massive anchor position) + 10, // anchorWidth: 10 (extremely narrow) + 5e16 // discoveryDepth: 5% (minimal discovery) + ); + } + + function getDescription() external pure returns (string memory) { + return "Whale Market - Massive concentrated liquidity"; + } +} \ No newline at end of file