This commit is contained in:
johba 2025-08-09 18:03:31 +02:00
parent 5b376885fd
commit 9f0b163303
19 changed files with 1340 additions and 490 deletions

22
onchain/analysis/.gitignore vendored Normal file
View file

@ -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
.~*

View file

@ -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()

View file

@ -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

View file

@ -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);
}
}

View file

@ -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 <optimizer_class> [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
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

View file

@ -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));
}
}
}
}

View file

@ -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."

View file

@ -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));
}
}
}

View file

@ -0,0 +1,4 @@
pandas>=1.3.0
matplotlib>=3.4.0
seaborn>=0.11.0
numpy>=1.21.0

201
onchain/analysis/run-fuzzing.sh Executable file
View file

@ -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 <optimizer_class> [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

View file

@ -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]

View file

@ -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;
}
}

View file

@ -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

View file

@ -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
}

View file

@ -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)";
}
}

View file

@ -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";
}
}