another rewrite

This commit is contained in:
johba 2025-08-23 22:32:41 +02:00
parent c72fe56ad0
commit 137adfe82b
20 changed files with 1402 additions and 3560 deletions

View file

@ -1,309 +0,0 @@
#!/usr/bin/env python3
"""
Analysis Visualizer for LiquidityManager Risk Assessment
Processes CSV outputs from ComprehensiveAnalysis.s.sol and generates visualizations
"""
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
import numpy as np
import glob
import os
from datetime import datetime
# Set style
plt.style.use('seaborn-v0_8-darkgrid')
sns.set_palette("husl")
def load_csv_files(pattern="analysis/comprehensive_*.csv"):
"""Load all CSV files matching the pattern"""
files = glob.glob(pattern)
data = {}
for file in files:
scenario = os.path.basename(file).replace("comprehensive_", "").replace(".csv", "")
try:
df = pd.read_csv(file)
data[scenario] = df
print(f"Loaded {scenario}: {len(df)} rows")
except Exception as e:
print(f"Error loading {file}: {e}")
return data
def analyze_price_impact(data):
"""Analyze price impact across scenarios"""
fig, axes = plt.subplots(3, 3, figsize=(15, 12))
axes = axes.flatten()
for idx, (scenario, df) in enumerate(data.items()):
if idx >= 9:
break
if 'price' in df.columns:
ax = axes[idx]
ax.plot(df.index, df['price'] / 1e18, linewidth=2)
ax.set_title(f"{scenario} - Price Movement")
ax.set_xlabel("Trade #")
ax.set_ylabel("Price (ETH)")
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig("analysis/price_impact_analysis.png", dpi=300)
plt.close()
def analyze_lm_value(data):
"""Analyze LiquidityManager value changes"""
fig, ax = plt.subplots(figsize=(12, 8))
for scenario, df in data.items():
if 'lmValue' in df.columns:
lm_values = df['lmValue'] / 1e18
initial_value = lm_values.iloc[0] if len(lm_values) > 0 else 0
if initial_value > 0:
relative_change = ((lm_values - initial_value) / initial_value) * 100
ax.plot(df.index, relative_change, label=scenario, linewidth=2)
ax.set_title("LiquidityManager Value Change Over Time", fontsize=16)
ax.set_xlabel("Trade #", fontsize=14)
ax.set_ylabel("Value Change (%)", fontsize=14)
ax.legend(bbox_to_anchor=(1.05, 1), loc='upper left')
ax.grid(True, alpha=0.3)
ax.axhline(y=0, color='red', linestyle='--', alpha=0.5)
plt.tight_layout()
plt.savefig("analysis/lm_value_changes.png", dpi=300)
plt.close()
def analyze_trader_profits(data):
"""Analyze trader profits across scenarios"""
scenarios = []
final_profits = []
for scenario, df in data.items():
if 'traderProfit' in df.columns:
total_profit = df['traderProfit'].sum() / 1e18
elif 'sandwichProfit' in df.columns:
total_profit = df['sandwichProfit'].sum() / 1e18
elif 'flashProfit' in df.columns:
total_profit = df['flashProfit'].sum() / 1e18
else:
total_profit = 0
scenarios.append(scenario.replace("_", " "))
final_profits.append(total_profit)
# Create bar chart
fig, ax = plt.subplots(figsize=(10, 8))
bars = ax.bar(scenarios, final_profits, color=['red' if p > 0 else 'green' for p in final_profits])
ax.set_title("Total Trader Profits by Scenario", fontsize=16)
ax.set_xlabel("Scenario", fontsize=14)
ax.set_ylabel("Total Profit (ETH)", fontsize=14)
ax.axhline(y=0, color='black', linestyle='-', alpha=0.3)
# Add value labels on bars
for bar, profit in zip(bars, final_profits):
height = bar.get_height()
ax.text(bar.get_x() + bar.get_width()/2., height,
f'{profit:.2f}', ha='center', va='bottom' if height > 0 else 'top')
plt.xticks(rotation=45, ha='right')
plt.tight_layout()
plt.savefig("analysis/trader_profits.png", dpi=300)
plt.close()
def analyze_recenter_impact(data):
"""Analyze impact of recenter operations"""
fig, axes = plt.subplots(2, 2, figsize=(12, 10))
# Count recenters per scenario
recenter_counts = {}
for scenario, df in data.items():
if 'action' in df.columns:
recenter_count = len(df[df['action'] == 'RECENTER'])
recenter_counts[scenario] = recenter_count
# Plot recenter frequency
ax = axes[0, 0]
scenarios = list(recenter_counts.keys())
counts = list(recenter_counts.values())
ax.bar(range(len(scenarios)), counts)
ax.set_xticks(range(len(scenarios)))
ax.set_xticklabels([s.replace("_", " ") for s in scenarios], rotation=45, ha='right')
ax.set_title("Recenter Frequency by Scenario")
ax.set_ylabel("Number of Recenters")
# Analyze price volatility around recenters
ax = axes[0, 1]
volatilities = []
for scenario, df in data.items():
if 'price' in df.columns and 'action' in df.columns:
recenter_indices = df[df['action'] == 'RECENTER'].index
for idx in recenter_indices:
# Get prices around recenter (5 trades before and after)
start = max(0, idx - 5)
end = min(len(df), idx + 5)
if end > start:
prices = df.loc[start:end, 'price'] / 1e18
if len(prices) > 1:
volatility = prices.std() / prices.mean() * 100
volatilities.append(volatility)
if volatilities:
ax.hist(volatilities, bins=20, alpha=0.7)
ax.set_title("Price Volatility Around Recenters")
ax.set_xlabel("Volatility (%)")
ax.set_ylabel("Frequency")
# Hide unused subplots
axes[1, 0].axis('off')
axes[1, 1].axis('off')
plt.tight_layout()
plt.savefig("analysis/recenter_analysis.png", dpi=300)
plt.close()
def generate_risk_matrix():
"""Generate risk assessment matrix"""
# Risk factors based on analysis
scenarios = [
"Bull Market", "Neutral Market", "Bear Market",
"Whale Dominance", "Sandwich Attack", "VWAP Manipulation",
"Recenter Exploit", "Liquidity Gap", "Flash Loan Attack"
]
risk_factors = [
"Capital Loss Risk",
"Price Manipulation",
"MEV Vulnerability",
"Liquidity Dominance Loss",
"VWAP Oracle Attack"
]
# Risk scores (0-10)
risk_matrix = np.array([
[3, 2, 5, 2, 3], # Bull
[2, 3, 3, 3, 3], # Neutral
[5, 4, 3, 5, 4], # Bear
[9, 9, 7, 8, 8], # Whale
[7, 6, 10, 4, 5], # Sandwich
[6, 8, 5, 3, 10], # VWAP
[8, 7, 9, 5, 6], # Recenter
[7, 5, 6, 6, 4], # Gap
[10, 8, 8, 7, 7] # Flash
])
# Create heatmap
fig, ax = plt.subplots(figsize=(10, 8))
sns.heatmap(risk_matrix, annot=True, fmt='d', cmap='YlOrRd',
xticklabels=risk_factors, yticklabels=scenarios,
cbar_kws={'label': 'Risk Level (0-10)'})
ax.set_title("LiquidityManager Risk Assessment Matrix", fontsize=16)
plt.tight_layout()
plt.savefig("analysis/risk_matrix.png", dpi=300)
plt.close()
def generate_summary_report(data):
"""Generate text summary report"""
report = []
report.append("=" * 60)
report.append("LIQUIDITYMANAGER COMPREHENSIVE RISK ANALYSIS REPORT")
report.append("=" * 60)
report.append(f"Generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n")
# Analyze each scenario
high_risk_scenarios = []
for scenario, df in data.items():
report.append(f"\n{scenario.upper().replace('_', ' ')}:")
report.append("-" * 40)
if 'lmValue' in df.columns:
initial_lm = df['lmValue'].iloc[0] / 1e18 if len(df) > 0 else 0
final_lm = df['lmValue'].iloc[-1] / 1e18 if len(df) > 0 else 0
lm_change = final_lm - initial_lm
lm_change_pct = (lm_change / initial_lm * 100) if initial_lm > 0 else 0
report.append(f"Initial LM Value: {initial_lm:.2f} ETH")
report.append(f"Final LM Value: {final_lm:.2f} ETH")
report.append(f"LM Change: {lm_change:.2f} ETH ({lm_change_pct:+.1f}%)")
if lm_change < -1: # Lost more than 1 ETH
high_risk_scenarios.append((scenario, lm_change))
if 'price' in df.columns:
price_volatility = (df['price'].std() / df['price'].mean() * 100) if df['price'].mean() > 0 else 0
report.append(f"Price Volatility: {price_volatility:.1f}%")
if 'action' in df.columns:
recenter_count = len(df[df['action'] == 'RECENTER'])
report.append(f"Recenters: {recenter_count}")
# High risk summary
report.append("\n\nHIGH RISK SCENARIOS:")
report.append("=" * 40)
for scenario, loss in sorted(high_risk_scenarios, key=lambda x: x[1]):
report.append(f"- {scenario}: {loss:.2f} ETH loss")
# Recommendations
report.append("\n\nKEY FINDINGS & RECOMMENDATIONS:")
report.append("=" * 40)
report.append("1. Whale attacks pose the highest risk to LM capital")
report.append("2. Flash loan attacks can extract significant value quickly")
report.append("3. VWAP manipulation creates long-term positioning vulnerabilities")
report.append("4. Sandwich attacks are highly profitable during recenters")
report.append("5. Narrow liquidity positions create exploitable gaps")
report.append("\n\nMITIGATION STRATEGIES:")
report.append("-" * 40)
report.append("• Implement position size limits relative to pool TVL")
report.append("• Add time-weighted average for recenter triggers")
report.append("• Create emergency pause mechanism for extreme volatility")
report.append("• Implement progressive fees based on trade size")
report.append("• Add VWAP decay function to limit historical influence")
report.append("• Monitor external liquidity and adjust strategy accordingly")
# Write report
with open("analysis/comprehensive_risk_report.txt", "w") as f:
f.write("\n".join(report))
print("\n".join(report))
def main():
"""Main analysis function"""
print("Loading analysis data...")
data = load_csv_files()
if not data:
print("No data files found. Please run ComprehensiveAnalysis.s.sol first.")
return
print("\nGenerating visualizations...")
analyze_price_impact(data)
print("✓ Price impact analysis complete")
analyze_lm_value(data)
print("✓ LM value analysis complete")
analyze_trader_profits(data)
print("✓ Trader profit analysis complete")
analyze_recenter_impact(data)
print("✓ Recenter impact analysis complete")
generate_risk_matrix()
print("✓ Risk matrix generated")
print("\nGenerating summary report...")
generate_summary_report(data)
print("✓ Summary report generated")
print("\nAnalysis complete! Check the 'analysis' directory for outputs.")
if __name__ == "__main__":
main()

View file

@ -1,650 +0,0 @@
// SPDX-License-Identifier: GPL-3.0-or-later
pragma solidity ^0.8.19;
import "forge-std/Test.sol";
import {TestEnvironment} from "../test/helpers/TestBase.sol";
import {IUniswapV3Pool} from "@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol";
import {IUniswapV3Factory} from "@uniswap/v3-core/contracts/interfaces/IUniswapV3Factory.sol";
import {IWETH9} from "../src/interfaces/IWETH9.sol";
import {Kraiken} from "../src/Kraiken.sol";
import {Stake} from "../src/Stake.sol";
import {LiquidityManager} from "../src/LiquidityManager.sol";
import {ThreePositionStrategy} from "../src/abstracts/ThreePositionStrategy.sol";
import {UniswapHelpers} from "../src/helpers/UniswapHelpers.sol";
import {TickMath} from "@aperture/uni-v3-lib/TickMath.sol";
import {LiquidityAmounts} from "@aperture/uni-v3-lib/LiquidityAmounts.sol";
import {Math} from "@openzeppelin/utils/math/Math.sol";
import "../test/mocks/BullMarketOptimizer.sol";
import "../test/mocks/WhaleOptimizer.sol";
import "./helpers/CSVManager.sol";
import "./helpers/SwapExecutor.sol";
/**
* @title ImprovedFuzzingAnalysis
* @notice Enhanced fuzzing with larger trades designed to reach discovery position
* @dev Uses more aggressive trading patterns to explore the full liquidity range
*/
contract ImprovedFuzzingAnalysis is Test, CSVManager {
TestEnvironment testEnv;
IUniswapV3Factory factory;
IUniswapV3Pool pool;
IWETH9 weth;
Kraiken harberg;
Stake stake;
LiquidityManager lm;
bool token0isWeth;
// Reusable swap executor to avoid repeated deployments
SwapExecutor swapExecutor;
address trader = makeAddr("trader");
address feeDestination = makeAddr("fees");
// Analysis metrics
uint256 public scenariosAnalyzed;
uint256 public profitableScenarios;
uint256 public discoveryReachedCount;
uint256 public totalStakesAttempted;
uint256 public totalStakesSucceeded;
uint256 public totalSnatchesAttempted;
uint256 public totalSnatchesSucceeded;
// Staking tracking
mapping(address => uint256[]) public activePositions;
uint256[] public allPositionIds; // Track all positions for snatching
// Configuration
uint256 public fuzzingRuns;
bool public trackPositions;
bool public enableStaking;
uint256 public buyBias; // 0-100, percentage bias towards buying vs selling
uint256 public tradesPerRun; // Number of trades/actions per scenario
uint256 public stakingBias; // 0-100, percentage bias towards staking vs unstaking
string public optimizerClass;
function run() public virtual {
_loadConfiguration();
console.log("=== IMPROVED Fuzzing Analysis ===");
console.log(string.concat("Optimizer: ", optimizerClass));
console.log(string.concat("Fuzzing runs: ", vm.toString(fuzzingRuns)));
testEnv = new TestEnvironment(feeDestination);
// Deploy factory once for all runs (gas optimization)
factory = UniswapHelpers.deployUniswapFactory();
// Get optimizer
address optimizerAddress = _getOptimizerByClass(optimizerClass);
// Track profitable scenarios
string memory profitableCSV = "Scenario,Seed,Initial Balance,Final Balance,Profit,Profit %,Discovery Reached\n";
uint256 profitableCount;
for (uint256 seed = 0; seed < fuzzingRuns; seed++) {
// Progress tracking removed
// Create fresh environment with existing factory
(factory, pool, weth, harberg, stake, lm,, token0isWeth) =
testEnv.setupEnvironmentWithExistingFactory(factory, seed % 2 == 0, feeDestination, optimizerAddress);
// Fund LiquidityManager with ETH proportional to trades
uint256 lmFunding = 100 ether + (tradesPerRun * 2 ether); // Base 100 + 2 ETH per trade
vm.deal(address(lm), lmFunding);
// Fund trader with capital proportional to number of trades
// Combine what was previously split between trader and whale
uint256 traderFund = 150 ether + (tradesPerRun * 8 ether); // 150 ETH base + 8 ETH per trade
// Add some randomness but keep it proportional
uint256 traderRandom = uint256(keccak256(abi.encodePacked(seed, "trader"))) % (tradesPerRun * 3 ether);
traderFund += traderRandom;
// Deal 2x to have extra for gas
vm.deal(trader, traderFund * 2);
vm.prank(trader);
weth.deposit{value: traderFund}();
// Create SwapExecutor once per scenario to avoid repeated deployments
swapExecutor = new SwapExecutor(pool, weth, harberg, token0isWeth, lm);
// Initial recenter BEFORE recording initial balance
vm.prank(feeDestination);
try lm.recenter{gas: 50_000_000}() {} catch {}
// Record initial balance AFTER recenter so we account for pool state
uint256 initialBalance = weth.balanceOf(trader);
// Initialize position tracking for each seed
if (trackPositions) {
// Initialize CSV header for each seed (after clearCSV from previous run)
initializePositionsCSV();
_recordPositionData("Initial");
}
// Run improved trading scenario
(uint256 finalBalance, bool reachedDiscovery) = _runImprovedScenario(seed);
scenariosAnalyzed++;
if (reachedDiscovery) {
discoveryReachedCount++;
}
// Check profitability
if (finalBalance > initialBalance) {
uint256 profit = finalBalance - initialBalance;
uint256 profitPct = (profit * 100) / initialBalance;
profitableScenarios++;
console.log(string.concat("PROFITABLE! Seed: ", vm.toString(seed), " - Profit: ", vm.toString(profitPct), "%"));
profitableCSV = string.concat(
profitableCSV,
optimizerClass, ",",
vm.toString(seed), ",",
vm.toString(initialBalance), ",",
vm.toString(finalBalance), ",",
vm.toString(profit), ",",
vm.toString(profitPct), ",",
reachedDiscovery ? "true" : "false", "\n"
);
profitableCount++;
}
// Write position CSV if tracking
if (trackPositions) {
_recordPositionData("Final");
string memory positionFilename = string.concat(
"improved_positions_", optimizerClass, "_", vm.toString(seed), ".csv"
);
writeCSVToFile(positionFilename);
clearCSV(); // Clear buffer for next run
}
}
// Summary
console.log("\n=== ANALYSIS COMPLETE ===");
console.log(string.concat(" Total scenarios: ", vm.toString(scenariosAnalyzed)));
console.log(string.concat(" Profitable scenarios: ", vm.toString(profitableScenarios)));
console.log(string.concat(" Discovery reached: ", vm.toString(discoveryReachedCount), " times"));
console.log(string.concat(" Discovery rate: ", vm.toString((discoveryReachedCount * 100) / scenariosAnalyzed), "%"));
console.log(string.concat(" Profit rate: ", vm.toString((profitableScenarios * 100) / scenariosAnalyzed), "%"));
if (enableStaking) {
// Staking metrics logged to CSV only
}
if (profitableCount > 0) {
string memory filename = string.concat("improved_profitable_", vm.toString(block.timestamp), ".csv");
vm.writeFile(filename, profitableCSV);
// Results written to CSV
}
}
function _runImprovedScenario(uint256 seed) internal virtual returns (uint256 finalBalance, bool reachedDiscovery) {
uint256 rand = uint256(keccak256(abi.encodePacked(seed, block.timestamp)));
// Get initial discovery position
(, int24 discoveryLower, int24 discoveryUpper) = lm.positions(ThreePositionStrategy.Stage.DISCOVERY);
// Initial buy to generate KRAIKEN tokens for trading/staking
// If staking is enabled with 100% bias, buy even more initially
uint256 initialBuyPercent = (enableStaking && stakingBias >= 100) ? 60 :
(enableStaking ? 40 : 25); // 60% if 100% staking, 40% if staking, 25% otherwise
uint256 initialBuyAmount = weth.balanceOf(trader) * initialBuyPercent / 100;
_executeBuy(trader, initialBuyAmount);
// Always use random trading strategy for consistent behavior
_executeRandomLargeTrades(rand);
// Check if we reached discovery
(, int24 currentTick,,,,,) = pool.slot0();
reachedDiscovery = (currentTick >= discoveryLower && currentTick < discoveryUpper);
if (reachedDiscovery) {
// Discovery reached
if (trackPositions) {
_recordPositionData("Discovery_Reached");
}
}
// Check final balances before cleanup
uint256 traderKraiken = harberg.balanceOf(trader);
// Final cleanup: sell all KRAIKEN
if (traderKraiken > 0) {
_executeSell(trader, traderKraiken);
}
// Calculate final balance
finalBalance = weth.balanceOf(trader);
}
function _executeRandomLargeTrades(uint256 rand) internal {
uint256 stakingAttempts = 0;
for (uint256 i = 0; i < tradesPerRun; i++) {
rand = uint256(keccak256(abi.encodePacked(rand, i)));
// Use buy bias to determine action
uint256 actionRoll = rand % 100;
uint256 action;
if (actionRoll < buyBias) {
action = 0; // Buy (biased towards buying when buyBias > 50)
} else if (actionRoll < 90) {
action = 1; // Sell (reduced probability when buyBias is high)
} else {
action = 2; // Recenter (10% chance)
}
if (action == 0) {
// Large buy (30-80% of balance, or more with high buy bias)
uint256 buyPct = buyBias > 80 ? 60 + (rand % 31) : (buyBias > 70 ? 40 + (rand % 41) : 30 + (rand % 51));
uint256 wethBalance = weth.balanceOf(trader);
uint256 buyAmount = wethBalance * buyPct / 100;
if (buyAmount > 0 && wethBalance > 0) {
_executeBuy(trader, buyAmount);
}
} else if (action == 1) {
// Large sell (significantly reduced with high buy bias to maintain KRAIKEN balance)
uint256 sellPct = buyBias > 80 ? 5 + (rand % 16) : (buyBias > 70 ? 10 + (rand % 21) : 30 + (rand % 71));
uint256 sellAmount = harberg.balanceOf(trader) * sellPct / 100;
if (sellAmount > 0) {
_executeSell(trader, sellAmount);
}
} else {
// Recenter
vm.warp(block.timestamp + (rand % 2 hours));
vm.prank(feeDestination);
try lm.recenter{gas: 50_000_000}() {} catch {}
}
// Every 3rd trade, attempt staking/unstaking if enabled
if (enableStaking && i % 3 == 2) {
stakingAttempts++;
uint256 stakingRoll = uint256(keccak256(abi.encodePacked(rand, "staking", i))) % 100;
if (stakingRoll < stakingBias) {
// Before staking, ensure we have tokens
uint256 harbBalance = harberg.balanceOf(trader);
uint256 minStakeAmount = harberg.minStake();
// With 100% staking bias, aggressively buy tokens if needed
// We want to maintain a large KRAIKEN balance for staking
if (stakingBias >= 100 && harbBalance <= minStakeAmount * 10) {
uint256 wethBalance = weth.balanceOf(trader);
if (wethBalance > 0) {
// Buy 30-50% of ETH worth to get substantial tokens for staking
uint256 buyAmount = wethBalance * (30 + (rand % 21)) / 100;
_executeBuy(trader, buyAmount);
}
} else if (harbBalance <= minStakeAmount * 2) {
uint256 wethBalance = weth.balanceOf(trader);
if (wethBalance > 0) {
// Buy 15-25% of ETH worth to get tokens for staking
uint256 buyAmount = wethBalance * (15 + (rand % 11)) / 100;
_executeBuy(trader, buyAmount);
}
}
// Now try to stake
_executeStake(rand + i * 1000);
} else {
// Try to unstake
_executeExitPosition(rand + i * 1000);
}
}
// Only record every 5th trade to avoid memory issues with large trade counts
if (trackPositions && i % 5 == 0) {
_recordPositionData("Trade");
}
}
uint256 expectedStakeActions = tradesPerRun / 3;
uint256 expectedStakes = (expectedStakeActions * stakingBias) / 100;
// Staking actions configured
}
function _executeBuy(address buyer, uint256 amount) internal virtual {
if (amount == 0 || weth.balanceOf(buyer) < amount) return;
vm.prank(buyer);
weth.transfer(address(swapExecutor), amount);
try swapExecutor.executeBuy(amount, buyer) {} catch {}
}
function _executeSell(address seller, uint256 amount) internal virtual {
if (amount == 0 || harberg.balanceOf(seller) < amount) return;
vm.prank(seller);
harberg.transfer(address(swapExecutor), amount);
swapExecutor.executeSell(amount, seller); // No try-catch, let errors bubble up
}
function _getOptimizerByClass(string memory class) internal returns (address) {
if (keccak256(bytes(class)) == keccak256("BullMarketOptimizer")) {
return address(new BullMarketOptimizer());
} else if (keccak256(bytes(class)) == keccak256("WhaleOptimizer")) {
return address(new WhaleOptimizer());
} else {
return address(new BullMarketOptimizer());
}
}
function _loadConfiguration() internal {
fuzzingRuns = vm.envOr("FUZZING_RUNS", uint256(20));
trackPositions = vm.envOr("TRACK_POSITIONS", false);
enableStaking = vm.envOr("ENABLE_STAKING", true); // Default to true
buyBias = vm.envOr("BUY_BIAS", uint256(50)); // Default 50% (balanced)
tradesPerRun = vm.envOr("TRADES_PER_RUN", uint256(15)); // Default 15 trades
stakingBias = vm.envOr("STAKING_BIAS", uint256(80)); // Default 80% stake vs 20% unstake
optimizerClass = vm.envOr("OPTIMIZER_CLASS", string("BullMarketOptimizer"));
}
function _recordPositionData(string memory label) internal {
// Split into separate function calls to avoid stack too deep
_recordPositionDataInternal(label);
}
function _recordPositionDataInternal(string memory label) private {
// Disable position tracking if it causes memory issues
if (bytes(csv).length > 50000) {
// CSV is getting too large, skip recording
return;
}
(,int24 currentTick,,,,,) = pool.slot0();
// Get position data
(uint128 floorLiq, int24 floorLower, int24 floorUpper) = lm.positions(ThreePositionStrategy.Stage.FLOOR);
(uint128 anchorLiq, int24 anchorLower, int24 anchorUpper) = lm.positions(ThreePositionStrategy.Stage.ANCHOR);
(uint128 discoveryLiq, int24 discoveryLower, int24 discoveryUpper) = lm.positions(ThreePositionStrategy.Stage.DISCOVERY);
// Use simpler row building to avoid memory issues
string memory row = label;
row = string.concat(row, ",", vm.toString(currentTick));
row = string.concat(row, ",", vm.toString(floorLower));
row = string.concat(row, ",", vm.toString(floorUpper));
row = string.concat(row, ",", vm.toString(floorLiq));
row = string.concat(row, ",", vm.toString(anchorLower));
row = string.concat(row, ",", vm.toString(anchorUpper));
row = string.concat(row, ",", vm.toString(anchorLiq));
row = string.concat(row, ",", vm.toString(discoveryLower));
row = string.concat(row, ",", vm.toString(discoveryUpper));
row = string.concat(row, ",", vm.toString(discoveryLiq));
row = string.concat(row, ",", token0isWeth ? "true" : "false");
row = string.concat(row, ",", vm.toString(stake.getPercentageStaked()));
row = string.concat(row, ",", vm.toString(stake.getAverageTaxRate()));
appendCSVRow(row);
}
function _executeStakingAction(uint256 rand) internal {
uint256 action = rand % 100;
// 70% chance to stake, 5% chance to exit (to fill pool faster)
if (action < 70) {
_executeStake(rand);
} else if (action < 75) {
_executeExitPosition(rand);
}
}
function _executeStakeWithAmount(address staker, uint256 amount, uint32 taxRate) internal {
// Direct stake with specific amount and tax rate
_doStake(staker, amount, taxRate);
}
function _executeStake(uint256 rand) internal {
address staker = trader;
uint256 harbBalance = harberg.balanceOf(staker);
uint256 minStakeAmount = harberg.minStake();
if (harbBalance > minStakeAmount) {
// With high staking bias (>= 90%), stake VERY aggressively
uint256 minPct = stakingBias >= 100 ? 50 : (stakingBias >= 90 ? 30 : 10);
uint256 maxPct = stakingBias >= 100 ? 100 : (stakingBias >= 90 ? 70 : 30);
// Stake between minPct% and maxPct% of balance
uint256 amount = harbBalance * (minPct + (rand % (maxPct - minPct + 1))) / 100;
if (amount < minStakeAmount) {
amount = minStakeAmount;
}
// With 100% staking bias, allow staking ALL tokens
// Otherwise keep a small reserve
if (stakingBias < 100) {
uint256 maxStake = harbBalance * 90 / 100; // Keep 10% for trading if not 100% bias
if (amount > maxStake) {
amount = maxStake;
}
}
// If stakingBias == 100, no limit - can stake entire balance
// Initial staking: use lower tax rates (0-15) to enable snatching later
uint32 taxRate = uint32(rand % 16); // 0-15 instead of 0-29
vm.prank(staker);
harberg.approve(address(stake), amount);
_doStake(staker, amount, taxRate);
}
// Silently skip if insufficient balance - this is expected behavior
}
function _doStake(address staker, uint256 amount, uint32 taxRate) internal {
vm.startPrank(staker);
// Check current pool capacity before attempting stake
uint256 currentPercentStaked = stake.getPercentageStaked();
// Log pool status before attempting (currentPercentStaked is in 1e18, where 1e18 = 100%)
if (currentPercentStaked > 95e16) { // > 95%
// Pool near full
}
// First try to stake without snatching
totalStakesAttempted++;
try stake.snatch(amount, staker, taxRate, new uint256[](0)) returns (uint256 positionId) {
totalStakesSucceeded++;
activePositions[staker].push(positionId);
allPositionIds.push(positionId);
if (trackPositions) {
_recordPositionData("Stake");
}
} catch Error(string memory reason) {
// Caught string error - try snatching
// Use high tax rate (28) for snatching - can snatch anything with rate 0-27
uint32 snatchTaxRate = 28;
// Find positions to snatch (those with lower tax rates)
uint256[] memory positionsToSnatch = _findSnatchablePositions(snatchTaxRate, amount);
if (positionsToSnatch.length > 0) {
totalSnatchesAttempted++;
try stake.snatch(amount, staker, snatchTaxRate, positionsToSnatch) returns (uint256 positionId) {
totalSnatchesSucceeded++;
activePositions[staker].push(positionId);
allPositionIds.push(positionId);
// Remove snatched positions from tracking
_removeSnatchedPositions(positionsToSnatch);
if (trackPositions) {
_recordPositionData("Snatch");
}
} catch {}
}
} catch {
// Catch-all for non-string errors (likely ExceededAvailableStake)
// Stake failed - trying snatch
// Now try snatching with high tax rate
uint32 snatchTaxRate = 28;
uint256[] memory positionsToSnatch = _findSnatchablePositions(snatchTaxRate, amount);
if (positionsToSnatch.length > 0) {
totalSnatchesAttempted++;
try stake.snatch(amount, staker, snatchTaxRate, positionsToSnatch) returns (uint256 positionId) {
totalSnatchesSucceeded++;
activePositions[staker].push(positionId);
allPositionIds.push(positionId);
_removeSnatchedPositions(positionsToSnatch);
if (trackPositions) {
_recordPositionData("Snatch");
}
} catch {}
}
}
vm.stopPrank();
}
function _executeExitPosition(uint256 rand) internal {
address staker = trader;
if (activePositions[staker].length > 0) {
uint256 index = rand % activePositions[staker].length;
uint256 positionId = activePositions[staker][index];
vm.prank(staker);
try stake.exitPosition(positionId) {
// Remove from array
activePositions[staker][index] = activePositions[staker][activePositions[staker].length - 1];
activePositions[staker].pop();
// Also remove from allPositionIds
for (uint256 j = 0; j < allPositionIds.length; j++) {
if (allPositionIds[j] == positionId) {
allPositionIds[j] = 0; // Mark as removed
break;
}
}
if (trackPositions) {
_recordPositionData("ExitStake");
}
} catch {
// Exit failed (position might be liquidated), remove from tracking
activePositions[staker][index] = activePositions[staker][activePositions[staker].length - 1];
activePositions[staker].pop();
// Also remove from allPositionIds
for (uint256 j = 0; j < allPositionIds.length; j++) {
if (allPositionIds[j] == positionId) {
allPositionIds[j] = 0; // Mark as removed
break;
}
}
}
}
}
function _findSnatchablePositions(uint32 snatchTaxRate, uint256 amountNeeded) internal view returns (uint256[] memory) {
// Find positions with tax rates lower than snatchTaxRate
uint256[] memory snatchable = new uint256[](10); // Max 10 positions to snatch
uint256 count = 0;
uint256 totalShares = 0;
uint256 skippedHighTax = 0;
uint256 skippedEmpty = 0;
for (uint256 i = 0; i < allPositionIds.length && count < 10; i++) {
uint256 positionId = allPositionIds[i];
if (positionId == 0) continue;
// Get position info
(uint256 shares, address owner,, , uint32 taxRate) = stake.positions(positionId);
// Skip if position doesn't exist or tax rate is too high
if (shares == 0) {
skippedEmpty++;
continue;
}
if (taxRate >= snatchTaxRate) {
skippedHighTax++;
continue;
}
snatchable[count] = positionId;
totalShares += shares;
count++;
// Check if we have enough shares to cover the amount needed
uint256 assetsFromShares = stake.sharesToAssets(totalShares);
if (assetsFromShares >= amountNeeded) {
break;
}
}
// Resize array to actual count
uint256[] memory result = new uint256[](count);
for (uint256 i = 0; i < count; i++) {
result[i] = snatchable[i];
}
return result;
}
function _removeSnatchedPositions(uint256[] memory snatchedIds) internal {
// Remove snatched positions from allPositionIds
for (uint256 i = 0; i < snatchedIds.length; i++) {
uint256 snatchedId = snatchedIds[i];
// Find and remove from allPositionIds
for (uint256 j = 0; j < allPositionIds.length; j++) {
if (allPositionIds[j] == snatchedId) {
allPositionIds[j] = 0; // Mark as removed
break;
}
}
// Remove from trader's activePositions
uint256[] storage traderPositions = activePositions[trader];
for (uint256 m = 0; m < traderPositions.length; m++) {
if (traderPositions[m] == snatchedId) {
traderPositions[m] = traderPositions[traderPositions.length - 1];
traderPositions.pop();
break;
}
}
}
}
function _logCurrentPosition(int24 currentTick) internal view {
(, int24 anchorLower, int24 anchorUpper) = lm.positions(ThreePositionStrategy.Stage.ANCHOR);
(, int24 discoveryLower, int24 discoveryUpper) = lm.positions(ThreePositionStrategy.Stage.DISCOVERY);
if (currentTick >= anchorLower && currentTick <= anchorUpper) {
// In anchor position
} else if (currentTick >= discoveryLower && currentTick <= discoveryUpper) {
// In discovery position
} else {
// Outside all positions
}
}
function _logInitialFloor() internal view {
// Removed to reduce logging
}
function _logFinalState() internal view {
// Removed to reduce logging
uint160 sqrtPriceUpper = TickMath.getSqrtRatioAtTick(floorUpper);
if (tick < floorLower) {
// All liquidity is in KRAIKEN
uint256 kraikenAmount = token0isWeth ?
LiquidityAmounts.getAmount1ForLiquidity(sqrtPriceLower, sqrtPriceUpper, poolLiquidity) :
LiquidityAmounts.getAmount0ForLiquidity(sqrtPriceLower, sqrtPriceUpper, poolLiquidity);
// All liquidity in KRAIKEN
}
}

View file

@ -18,9 +18,9 @@ This directory contains tools for fuzzing the KRAIKEN LiquidityManager to identi
## Files
### Core Scripts
- `ImprovedFuzzingAnalysis.s.sol` - Enhanced fuzzing script with staking support and memory optimizations
- `StreamlinedFuzzing.s.sol` - Streamlined fuzzing script with staking support and accurate trade recording
- `run-fuzzing.sh` - Shell script to orchestrate multiple fuzzing runs with configurable parameters
- `clean.sh` - Cleanup script to remove generated files
- `clean-csvs.sh` - Cleanup script to remove generated CSV files
### Helpers
- `helpers/SwapExecutor.sol` - Handles swap execution through Uniswap
@ -28,9 +28,8 @@ This directory contains tools for fuzzing the KRAIKEN LiquidityManager to identi
- `helpers/CSVHelper.sol` - CSV formatting helpers
### Visualization
- `AnalysisVisualizer.py` - Python script to generate charts from CSV data
- `scenario-visualizer.html` - Interactive web visualization
- `view-scenarios.sh` - Quick script to launch web server for visualization
- `run-visualizer.html` - Interactive web visualization for analyzing individual trades from fuzzing runs
- Supports row-by-row navigation through trades with liquidity distribution charts
## Available Optimizers
@ -78,12 +77,12 @@ Each fuzzing campaign creates a timestamped directory with:
To visualize results:
```bash
# Start local web server
./view-scenarios.sh
# Start local web server from analysis directory
cd analysis && python3 -m http.server 8000
# Then open http://localhost:8000/run-visualizer.html
# Or use Python directly
python3 -m http.server 8000
# Then open http://localhost:8000/scenario-visualizer.html
# Or use debugCSV mode which automatically launches visualizer
./analysis/run-fuzzing.sh BullMarketOptimizer debugCSV
```
### Cleanup
@ -116,9 +115,11 @@ To add a new optimizer:
## Notes
- Each run deploys a fresh Uniswap V3 environment
- Gas limit is set to 200M for script execution
- Gas limit is set to 300M for script execution
- Results are deterministic based on the seed
- The fuzzer tests random buy/sell patterns with periodic recenters
- Supports staking operations with position snatching mechanics
- Memory-optimized with circular buffer for position tracking
- Records all trades to CSV for complete visualization
- Only records trades that actually execute (non-zero amounts)
- Records actual traded amounts after liquidity limits are applied
- CSV files are written to the analysis/ directory
- Every trade properly updates the tick value

View file

@ -1,265 +0,0 @@
// SPDX-License-Identifier: GPL-3.0-or-later
pragma solidity ^0.8.19;
import "./ImprovedFuzzingAnalysis.s.sol";
import "./helpers/ScenarioRecorder.sol";
/**
* @title RecordedFuzzingAnalysis
* @notice Enhanced fuzzing that records profitable scenarios for exact replay
* @dev Captures all actions, states, and parameters when invariants fail
*/
contract RecordedFuzzingAnalysis is ImprovedFuzzingAnalysis {
ScenarioRecorder public recorder;
bool public enableRecording = true;
string public runId; // Unique identifier for this run
bool private isRecordingProfitable; // Only record if scenario is profitable
// Constructor removed - set trackPositions in run() instead
// Store actions for current scenario
struct ActionRecord {
uint256 step;
string actionType;
address actor;
uint256 amount;
uint256 timestamp;
uint256 blockNumber;
int24 tickBefore;
int24 tickAfter;
}
ActionRecord[] currentActions;
function run() public override {
// Get run ID from environment or generate default
runId = vm.envOr("RUN_ID", string("LOCAL"));
// Force position tracking to be enabled for CSV generation
vm.setEnv("TRACK_POSITIONS", "true");
console.log("=== RECORDED Fuzzing Analysis ===");
console.log(string.concat("Run ID: ", runId));
console.log("Recording enabled for profitable scenario replay");
super.run();
}
// Override to add recording for profitable scenarios
function _runImprovedScenario(uint256 seed) internal override returns (uint256 finalBalance, bool reachedDiscovery) {
// Clear previous scenario
delete currentActions;
isRecordingProfitable = false;
// Run the base scenario
(finalBalance, reachedDiscovery) = super._runImprovedScenario(seed);
// Export summary if profitable
uint256 initialBalance = 50 ether;
if (finalBalance > initialBalance && reachedDiscovery) {
_exportSummary(seed, initialBalance, finalBalance, reachedDiscovery);
}
return (finalBalance, reachedDiscovery);
}
// Override trade execution to record actions
function _executeBuy(address buyer, uint256 amount) internal override {
if (amount == 0 || weth.balanceOf(buyer) < amount) return;
(, int24 tickBefore,,,,,) = pool.slot0();
// Execute trade
super._executeBuy(buyer, amount);
(, int24 tickAfter,,,,,) = pool.slot0();
// Only record action summary (not full state)
_recordAction("BUY", buyer, amount, tickBefore, tickAfter);
}
function _executeSell(address seller, uint256 amount) internal override {
if (amount == 0 || harberg.balanceOf(seller) < amount) return;
(, int24 tickBefore,,,,,) = pool.slot0();
// Execute trade
super._executeSell(seller, amount);
(, int24 tickAfter,,,,,) = pool.slot0();
// Only record action summary (not full state)
_recordAction("SELL", seller, amount, tickBefore, tickAfter);
}
function _executeRecenter() internal {
(, int24 tickBefore,,,,,) = pool.slot0();
vm.warp(block.timestamp + 1 hours);
vm.prank(feeDestination);
try lm.recenter{gas: 50_000_000}() {} catch {}
(, int24 tickAfter,,,,,) = pool.slot0();
// Only record action summary
_recordAction("RECENTER", feeDestination, 0, tickBefore, tickAfter);
}
function _recordState(string memory label, address actor, uint256 amount) internal {
// Only record states if we're exporting a profitable scenario
if (!isRecordingProfitable || !enableRecording) return;
(uint160 sqrtPriceX96, int24 currentTick,,,,,) = pool.slot0();
recorder.recordPreState(
weth.balanceOf(trader),
harberg.balanceOf(trader),
currentTick,
uint256(sqrtPriceX96),
0, // VWAP placeholder
harberg.outstandingSupply()
);
}
function _recordAction(
string memory actionType,
address actor,
uint256 amount,
int24 tickBefore,
int24 tickAfter
) internal {
currentActions.push(ActionRecord({
step: currentActions.length,
actionType: actionType,
actor: actor,
amount: amount,
timestamp: block.timestamp,
blockNumber: block.number,
tickBefore: tickBefore,
tickAfter: tickAfter
}));
}
function _exportSummary(
uint256 seed,
uint256 initialBalance,
uint256 finalBalance,
bool reachedDiscovery
) internal {
console.log("\n[RECORDING] Exporting profitable scenario...");
console.log(string.concat(" Run ID: ", runId));
console.log(string.concat(" Seed: ", vm.toString(seed)));
string memory summary = _generateActionSummary(
seed,
initialBalance,
finalBalance,
reachedDiscovery
);
string memory summaryFilename = string.concat(
"scenario_summary_seed", vm.toString(seed), ".txt"
);
vm.writeFile(summaryFilename, summary);
console.log(string.concat(" Summary exported to: ", summaryFilename));
if (currentActions.length > 0) {
string memory replayScript = _generateReplayScript(seed);
string memory scriptFilename = string.concat(
"replay_script_seed", vm.toString(seed), ".sol"
);
vm.writeFile(scriptFilename, replayScript);
console.log(string.concat(" Replay script exported to: ", scriptFilename));
}
}
function _generateReplayScript(uint256 seed) internal view returns (string memory) {
string memory script = string.concat(
"// Replay script for profitable scenario\n",
"// Seed: ", vm.toString(seed), "\n",
"// Optimizer: ", optimizerClass, "\n",
"// Discovery reached: YES\n\n",
"function replayScenario() public {\n"
);
for (uint256 i = 0; i < currentActions.length; i++) {
ActionRecord memory action = currentActions[i];
script = string.concat(script, " // Step ", vm.toString(i), "\n");
if (keccak256(bytes(action.actionType)) == keccak256("BUY")) {
script = string.concat(
script,
" _executeBuy(",
action.actor == trader ? "trader" : "system",
", ", vm.toString(action.amount), ");\n"
);
} else if (keccak256(bytes(action.actionType)) == keccak256("SELL")) {
script = string.concat(
script,
" _executeSell(",
action.actor == trader ? "trader" : "system",
", ", vm.toString(action.amount), ");\n"
);
} else if (keccak256(bytes(action.actionType)) == keccak256("RECENTER")) {
script = string.concat(
script,
" vm.warp(", vm.toString(action.timestamp), ");\n",
" vm.prank(feeDestination);\n",
" lm.recenter();\n"
);
}
script = string.concat(
script,
" // Tick moved from ", vm.toString(action.tickBefore),
" to ", vm.toString(action.tickAfter), "\n\n"
);
}
script = string.concat(script, "}\n");
return script;
}
function _generateActionSummary(
uint256 seed,
uint256 initialBalance,
uint256 finalBalance,
bool reachedDiscovery
) internal view returns (string memory) {
uint256 profit = finalBalance - initialBalance;
uint256 profitPct = (profit * 100) / initialBalance;
string memory summary = string.concat(
"=== PROFITABLE SCENARIO SUMMARY ===\n",
"Run ID: ", runId, "\n",
"Seed: ", vm.toString(seed), "\n",
"Optimizer: ", optimizerClass, "\n",
"Discovery Reached: ", reachedDiscovery ? "YES" : "NO", "\n\n",
"Financial Results:\n",
" Initial Balance: ", vm.toString(initialBalance / 1e18), " ETH\n",
" Final Balance: ", vm.toString(finalBalance / 1e18), " ETH\n",
" Profit: ", vm.toString(profit / 1e18), " ETH (", vm.toString(profitPct), "%)\n\n",
"Action Sequence:\n"
);
for (uint256 i = 0; i < currentActions.length; i++) {
ActionRecord memory action = currentActions[i];
summary = string.concat(
summary,
" ", vm.toString(i + 1), ". ",
action.actionType, " - ",
action.actor == trader ? "Trader" : "System",
action.amount > 0 ? string.concat(" - Amount: ", vm.toString(action.amount / 1e18), " ETH") : "",
" - Tick: ", vm.toString(action.tickBefore), " -> ", vm.toString(action.tickAfter),
"\n"
);
}
summary = string.concat(
summary,
"\nThis scenario can be replayed using the generated replay script.\n"
);
return summary;
}
}

View file

@ -0,0 +1,346 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "forge-std/Script.sol";
import "forge-std/console.sol";
import {TestEnvironment} from "../test/helpers/TestBase.sol";
import {IUniswapV3Pool} from "@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol";
import {IUniswapV3Factory} from "@uniswap/v3-core/contracts/interfaces/IUniswapV3Factory.sol";
import {UniswapHelpers} from "../src/helpers/UniswapHelpers.sol";
import {IWETH9} from "../src/interfaces/IWETH9.sol";
import {Kraiken} from "../src/Kraiken.sol";
import {Stake} from "../src/Stake.sol";
import {LiquidityManager} from "../src/LiquidityManager.sol";
import {ThreePositionStrategy} from "../src/abstracts/ThreePositionStrategy.sol";
import {SwapExecutor} from "./helpers/SwapExecutor.sol";
import {Optimizer} from "../src/Optimizer.sol";
import {BullMarketOptimizer} from "../test/mocks/BullMarketOptimizer.sol";
import {BearMarketOptimizer} from "../test/mocks/BearMarketOptimizer.sol";
import {NeutralMarketOptimizer} from "../test/mocks/NeutralMarketOptimizer.sol";
import {WhaleOptimizer} from "../test/mocks/WhaleOptimizer.sol";
import {ExtremeOptimizer} from "../test/mocks/ExtremeOptimizer.sol";
import {MaliciousOptimizer} from "../test/mocks/MaliciousOptimizer.sol";
contract StreamlinedFuzzing is Script {
// Test environment
TestEnvironment testEnv;
IUniswapV3Factory factory;
IUniswapV3Pool pool;
IWETH9 weth;
Kraiken kraiken;
Stake stake;
LiquidityManager lm;
SwapExecutor swapExecutor;
bool token0isWeth;
// Actors
address trader = makeAddr("trader");
address fees = makeAddr("fees");
// Staking tracking
mapping(address => uint256[]) public activePositions;
uint256[] public allPositionIds;
uint256 totalStakesAttempted;
uint256 totalStakesSucceeded;
uint256 totalSnatchesAttempted;
uint256 totalSnatchesSucceeded;
// CSV filename for current run
string csvFilename;
// Track cumulative fees
uint256 totalFees0;
uint256 totalFees1;
// Track recentering
uint256 lastRecenterBlock;
function run() public {
// Get configuration from environment
uint256 numRuns = vm.envOr("FUZZING_RUNS", uint256(20));
uint256 tradesPerRun = vm.envOr("TRADES_PER_RUN", uint256(15));
bool enableStaking = vm.envOr("ENABLE_STAKING", true);
uint256 buyBias = vm.envOr("BUY_BIAS", uint256(50));
uint256 stakingBias = vm.envOr("STAKING_BIAS", uint256(80));
string memory optimizerClass = vm.envOr("OPTIMIZER_CLASS", string("BullMarketOptimizer"));
console.log("=== Streamlined Fuzzing Analysis ===");
console.log("Optimizer:", optimizerClass);
console.log("Runs:", numRuns);
console.log("Trades per run:", tradesPerRun);
// Deploy factory once for all runs (gas optimization)
testEnv = new TestEnvironment(fees);
factory = UniswapHelpers.deployUniswapFactory();
// Generate unique 4-character scenario ID
string memory scenarioCode = _generateScenarioId();
// Run fuzzing scenarios
for (uint256 runIndex = 0; runIndex < numRuns; runIndex++) {
string memory runId = string(abi.encodePacked(scenarioCode, "-", _padNumber(runIndex, 3)));
console.log("\nRun:", runId);
// Initialize CSV file for this run
// Always write to analysis directory relative to project root
csvFilename = string(abi.encodePacked("analysis/fuzz-", runId, ".csv"));
string memory header = "action,amount,tick,floor_lower,floor_upper,floor_liq,anchor_lower,anchor_upper,anchor_liq,discovery_lower,discovery_upper,discovery_liq,eth_balance,kraiken_balance,vwap,fees_eth,fees_kraiken,recenter\n";
vm.writeFile(csvFilename, header);
// Setup fresh environment for each run
_setupEnvironment(optimizerClass, runIndex % 2 == 0);
// Reset tracking variables
totalFees0 = 0;
totalFees1 = 0;
lastRecenterBlock = block.number;
// Fund trader based on run seed
uint256 traderFund = 50 ether + (uint256(keccak256(abi.encodePacked(runIndex, "trader"))) % 150 ether);
vm.deal(trader, traderFund * 2);
vm.prank(trader);
weth.deposit{value: traderFund}();
// Initial state
_recordState("INIT", 0);
// Debug: Check initial liquidity manager state
console.log("LM ETH balance:", address(lm).balance);
console.log("Pool address:", address(pool));
// Execute trades
for (uint256 i = 0; i < tradesPerRun; i++) {
// Check for recenter opportunity every 5 trades
if (i > 0 && i % 5 == 0) {
_tryRecenter();
}
// Determine trade based on bias
uint256 rand = uint256(keccak256(abi.encodePacked(runIndex, i))) % 100;
if (rand < buyBias) {
_executeBuy(runIndex, i);
} else {
_executeSell(runIndex, i);
}
// Staking operations if enabled
if (enableStaking && i % 3 == 0) {
_executeStakingOperation(runIndex, i, stakingBias);
}
}
// Final state
_recordState("FINAL", 0);
}
console.log("\n=== Analysis Complete ===");
console.log("Generated", numRuns, "CSV files with prefix:", scenarioCode);
}
function _setupEnvironment(string memory optimizerClass, bool wethIsToken0) internal {
// Get optimizer address
address optimizer = _deployOptimizer(optimizerClass);
// Setup new environment
(factory, pool, weth, kraiken, stake, lm,, token0isWeth) =
testEnv.setupEnvironmentWithExistingFactory(factory, wethIsToken0, fees, optimizer);
// Deploy swap executor with liquidity boundary checks
swapExecutor = new SwapExecutor(pool, weth, kraiken, token0isWeth, lm);
// Fund liquidity manager
vm.deal(address(lm), 200 ether);
// Initialize liquidity positions
// First need to give LM some WETH
vm.prank(address(lm));
weth.deposit{value: 100 ether}();
// Now try recenter from fee destination
vm.prank(fees);
try lm.recenter() returns (bool isUp) {
console.log("Initial recenter successful, isUp:", isUp);
} catch Error(string memory reason) {
console.log("Initial recenter failed:", reason);
} catch {
console.log("Initial recenter failed with unknown error");
}
// Clear staking state
delete allPositionIds;
totalStakesAttempted = 0;
totalStakesSucceeded = 0;
totalSnatchesAttempted = 0;
totalSnatchesSucceeded = 0;
}
function _deployOptimizer(string memory optimizerClass) internal returns (address) {
if (keccak256(bytes(optimizerClass)) == keccak256(bytes("BullMarketOptimizer"))) {
return address(new BullMarketOptimizer());
} else if (keccak256(bytes(optimizerClass)) == keccak256(bytes("BearMarketOptimizer"))) {
return address(new BearMarketOptimizer());
} else if (keccak256(bytes(optimizerClass)) == keccak256(bytes("NeutralMarketOptimizer"))) {
return address(new NeutralMarketOptimizer());
} else if (keccak256(bytes(optimizerClass)) == keccak256(bytes("WhaleOptimizer"))) {
return address(new WhaleOptimizer());
} else if (keccak256(bytes(optimizerClass)) == keccak256(bytes("ExtremeOptimizer"))) {
return address(new ExtremeOptimizer());
} else if (keccak256(bytes(optimizerClass)) == keccak256(bytes("MaliciousOptimizer"))) {
return address(new MaliciousOptimizer());
} else {
// Default to bull market
return address(new BullMarketOptimizer());
}
}
function _executeBuy(uint256 runIndex, uint256 tradeIndex) internal {
uint256 amount = _getTradeAmount(runIndex, tradeIndex, true);
if (amount == 0 || weth.balanceOf(trader) < amount) return;
vm.startPrank(trader);
weth.transfer(address(swapExecutor), amount);
try swapExecutor.executeBuy(amount, trader) returns (uint256 actualAmount) {
if (actualAmount == 0) {
console.log("Buy returned 0, requested:", amount);
}
_recordState("BUY", actualAmount);
} catch Error(string memory reason) {
console.log("Buy failed:", reason);
_recordState("BUY_FAIL", amount);
}
vm.stopPrank();
}
function _executeSell(uint256 runIndex, uint256 tradeIndex) internal {
uint256 amount = _getTradeAmount(runIndex, tradeIndex, false);
if (amount == 0 || kraiken.balanceOf(trader) < amount) return;
vm.startPrank(trader);
kraiken.transfer(address(swapExecutor), amount);
try swapExecutor.executeSell(amount, trader) returns (uint256 actualAmount) {
_recordState("SELL", actualAmount);
} catch {
_recordState("SELL_FAIL", amount);
}
vm.stopPrank();
}
function _executeStakingOperation(uint256, uint256, uint256) internal {
// Staking operations disabled for now - interface needs updating
// TODO: Update to use correct Stake contract interface
}
function _tryRecenter() internal {
uint256 blocksSinceRecenter = block.number - lastRecenterBlock;
if (blocksSinceRecenter > 100) {
vm.warp(block.timestamp + 1 hours);
vm.roll(block.number + 1); // Advance block
vm.prank(fees);
try lm.recenter{gas: 50_000_000}() {
lastRecenterBlock = block.number;
_recordState("RECENTER", 0);
} catch {}
}
}
function _getTradeAmount(uint256 runIndex, uint256 tradeIndex, bool isBuy) internal pure returns (uint256) {
uint256 baseAmount = 1 ether + (uint256(keccak256(abi.encodePacked(runIndex, tradeIndex))) % 10 ether);
return isBuy ? baseAmount : baseAmount * 1000;
}
function _recordState(string memory action, uint256 amount) internal {
// Build CSV row in parts to avoid stack too deep
string memory row = _buildRowPart1(action, amount);
row = string(abi.encodePacked(row, _buildRowPart2()));
row = string(abi.encodePacked(row, _buildRowPart3()));
vm.writeLine(csvFilename, row);
}
function _buildRowPart1(string memory action, uint256 amount) internal view returns (string memory) {
(, int24 tick,,,,,) = pool.slot0();
// Get floor position
(uint128 floorLiq, int24 floorLower, int24 floorUpper) = lm.positions(ThreePositionStrategy.Stage.FLOOR);
return string(abi.encodePacked(
action, ",",
vm.toString(amount), ",",
vm.toString(tick), ",",
vm.toString(floorLower), ",",
vm.toString(floorUpper), ",",
vm.toString(uint256(floorLiq)), ","
));
}
function _buildRowPart2() internal view returns (string memory) {
// Get anchor and discovery positions
(uint128 anchorLiq, int24 anchorLower, int24 anchorUpper) = lm.positions(ThreePositionStrategy.Stage.ANCHOR);
(uint128 discoveryLiq, int24 discoveryLower, int24 discoveryUpper) = lm.positions(ThreePositionStrategy.Stage.DISCOVERY);
return string(abi.encodePacked(
vm.toString(anchorLower), ",",
vm.toString(anchorUpper), ",",
vm.toString(uint256(anchorLiq)), ",",
vm.toString(discoveryLower), ",",
vm.toString(discoveryUpper), ",",
vm.toString(uint256(discoveryLiq)), ","
));
}
function _buildRowPart3() internal view returns (string memory) {
// Get balances and fees
uint256 ethBalance = weth.balanceOf(trader);
uint256 kraikenBalance = kraiken.balanceOf(trader);
(uint128 fees0, uint128 fees1) = pool.protocolFees();
uint256 deltaFees0 = fees0 > totalFees0 ? fees0 - totalFees0 : 0;
uint256 deltaFees1 = fees1 > totalFees1 ? fees1 - totalFees1 : 0;
return string(abi.encodePacked(
vm.toString(ethBalance), ",",
vm.toString(kraikenBalance), ",",
"0,", // vwap placeholder
vm.toString(deltaFees0), ",",
vm.toString(deltaFees1), ",",
"0" // recenter flag placeholder - no newline here
));
}
function _generateScenarioId() internal view returns (string memory) {
uint256 rand = uint256(keccak256(abi.encodePacked(block.timestamp, block.prevrandao)));
bytes memory chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
bytes memory result = new bytes(4);
for (uint256 i = 0; i < 4; i++) {
result[i] = chars[rand % chars.length];
rand = rand / chars.length;
}
return string(result);
}
function _padNumber(uint256 num, uint256 digits) internal pure returns (string memory) {
string memory numStr = vm.toString(num);
bytes memory numBytes = bytes(numStr);
if (numBytes.length >= digits) {
return numStr;
}
bytes memory result = new bytes(digits);
uint256 padding = digits - numBytes.length;
for (uint256 i = 0; i < padding; i++) {
result[i] = '0';
}
for (uint256 i = 0; i < numBytes.length; i++) {
result[padding + i] = numBytes[i];
}
return string(result);
}
}

View file

@ -1,84 +0,0 @@
# AnchorWidth Price Range Calculations
## Understanding the Formula
From the code:
```solidity
int24 anchorSpacing = TICK_SPACING + (34 * int24(params.anchorWidth) * TICK_SPACING / 100);
```
Where:
- `TICK_SPACING = 200` (for 1% fee tier pools)
- `anchorWidth` ranges from 1 to 100
## Tick to Price Conversion
In Uniswap V3:
- Each tick represents a 0.01% (1 basis point) price change
- Price at tick i = 1.0001^i
- So a tick difference of 100 = ~1.01% price change
- A tick difference of 10,000 = ~2.718x price change
## AnchorWidth to Price Range Mapping
Let's calculate the actual price ranges for different anchorWidth values:
### Formula Breakdown:
- `anchorSpacing = 200 + (34 * anchorWidth * 200 / 100)`
- `anchorSpacing = 200 + (68 * anchorWidth)`
- `anchorSpacing = 200 * (1 + 0.34 * anchorWidth)`
The anchor position extends from:
- Lower bound: `currentTick - anchorSpacing`
- Upper bound: `currentTick + anchorSpacing`
- Total width: `2 * anchorSpacing` ticks
### Price Range Calculations:
| anchorWidth | anchorSpacing (ticks) | Total Width (ticks) | Lower Price Ratio | Upper Price Ratio | Price Range |
|-------------|----------------------|---------------------|-------------------|-------------------|-------------|
| 1% | 268 | 536 | 0.974 | 1.027 | ±2.7% |
| 5% | 540 | 1080 | 0.947 | 1.056 | ±5.5% |
| 10% | 880 | 1760 | 0.916 | 1.092 | ±9.0% |
| 20% | 1560 | 3120 | 0.855 | 1.170 | ±16% |
| 30% | 2240 | 4480 | 0.800 | 1.251 | ±25% |
| 40% | 2920 | 5840 | 0.748 | 1.336 | ±33% |
| 50% | 3600 | 7200 | 0.700 | 1.429 | ±42% |
| 60% | 4280 | 8560 | 0.654 | 1.528 | ±52% |
| 70% | 4960 | 9920 | 0.612 | 1.635 | ±63% |
| 80% | 5640 | 11280 | 0.572 | 1.749 | ±74% |
| 90% | 6320 | 12640 | 0.535 | 1.871 | ±87% |
| 100% | 7000 | 14000 | 0.500 | 2.000 | ±100% |
## Key Insights:
1. **Linear Tick Scaling**: The tick spacing scales linearly with anchorWidth
2. **Non-Linear Price Scaling**: Due to exponential nature of tick-to-price conversion
3. **Asymmetric Percentages**: The percentage move down is smaller than up (e.g., 100% width = -50% to +100%)
## Practical Examples:
### anchorWidth = 50 (Common Default)
- If current price is $1.00:
- Lower bound: $0.70 (-30%)
- Upper bound: $1.43 (+43%)
- Captures moderate price movements in both directions
### anchorWidth = 100 (Maximum)
- If current price is $1.00:
- Lower bound: $0.50 (-50%)
- Upper bound: $2.00 (+100%)
- Price can double or halve while staying in range
### anchorWidth = 10 (Narrow)
- If current price is $1.00:
- Lower bound: $0.92 (-8%)
- Upper bound: $1.09 (+9%)
- Highly concentrated, requires frequent rebalancing
## Important Notes:
1. The anchor position does NOT extend to 2x or 3x the price at maximum width
2. At anchorWidth = 100, the upper bound is exactly 2x the current price
3. The formula `200 + (34 * anchorWidth * 200 / 100)` creates a sensible progression from tight to wide ranges
4. The minimum spacing (anchorWidth = 0) would be 200 ticks (±1% range), but minimum allowed is 1

16
onchain/analysis/clean-csvs.sh Executable file
View file

@ -0,0 +1,16 @@
#!/bin/bash
# Clean up fuzzing CSV files from the analysis directory
echo "Cleaning up fuzzing CSV files..."
# Count files before deletion
COUNT=$(ls -1 analysis/fuzz-*.csv 2>/dev/null | wc -l)
if [ "$COUNT" -gt 0 ]; then
# Remove all fuzzing CSV files from analysis folder
rm -f analysis/fuzz-*.csv
echo "Removed $COUNT CSV files from analysis/ directory"
else
echo "No CSV files to clean"
fi

View file

@ -28,15 +28,15 @@ contract SwapExecutor {
liquidityManager = _liquidityManager;
}
function executeBuy(uint256 amount, address recipient) external {
function executeBuy(uint256 amount, address recipient) external returns (uint256) {
// Calculate maximum safe buy amount based on liquidity
uint256 maxBuyAmount = LiquidityBoundaryHelper.calculateBuyLimit(pool, liquidityManager, token0isWeth);
// Cap the amount to the safe limit
uint256 safeAmount = amount > maxBuyAmount ? maxBuyAmount : amount;
// Skip if no liquidity available
if (safeAmount == 0) return;
// Skip if amount is zero
if (safeAmount == 0) return 0;
// For buying HARB with WETH, we're swapping in the direction that increases HARB price
// zeroForOne = true if WETH is token0, false if WETH is token1
@ -59,17 +59,19 @@ contract SwapExecutor {
sqrtPriceLimitX96,
""
);
return safeAmount;
}
function executeSell(uint256 amount, address recipient) external {
function executeSell(uint256 amount, address recipient) external returns (uint256) {
// Calculate maximum safe sell amount based on liquidity
uint256 maxSellAmount = LiquidityBoundaryHelper.calculateSellLimit(pool, liquidityManager, token0isWeth);
// Cap the amount to the safe limit
uint256 safeAmount = amount > maxSellAmount ? maxSellAmount : amount;
// Skip if no liquidity available
if (safeAmount == 0) return;
// Skip if amount is zero
if (safeAmount == 0) return 0;
// For selling HARB for WETH, we're swapping in the direction that decreases HARB price
// zeroForOne = false if WETH is token0, true if WETH is token1
@ -92,6 +94,8 @@ contract SwapExecutor {
sqrtPriceLimitX96,
""
);
return safeAmount;
}
// Callback required for Uniswap V3 swaps

View file

@ -1,211 +0,0 @@
#!/bin/bash
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
BOLD='\033[1m'
# Check arguments
if [ $# -lt 1 ]; then
echo "Usage: $0 <RUN_ID> [seed_number]"
echo ""
echo "Examples:"
echo " $0 241218-A7K9 # Replay all scenarios from run"
echo " $0 241218-A7K9 1 # Replay specific seed from run"
echo ""
echo "To find RUN_IDs, check fuzzing_results_recorded_* directories"
exit 1
fi
RUN_ID=$1
SEED=$2
echo -e "${GREEN}=== Scenario Replay Tool ===${NC}"
echo -e "Run ID: ${BOLD}${RUN_ID}${NC}"
# Find the results directory
RESULTS_DIR=$(find . -type d -name "*_*" 2>/dev/null | grep -E "fuzzing_results_recorded.*" | xargs -I {} sh -c 'ls -1 {}/scenario_'${RUN_ID}'_*.json 2>/dev/null && echo {}' | tail -1)
if [ -z "$RESULTS_DIR" ]; then
echo -e "${RED}Error: No results found for Run ID ${RUN_ID}${NC}"
echo ""
echo "Available Run IDs:"
find . -type f -name "scenario_*_seed*.json" 2>/dev/null | sed 's/.*scenario_\(.*\)_seed.*/\1/' | sort -u
exit 1
fi
echo "Found results in: $RESULTS_DIR"
echo ""
# If no seed specified, show available scenarios
if [ -z "$SEED" ]; then
echo -e "${YELLOW}Available scenarios for ${RUN_ID}:${NC}"
for summary in $RESULTS_DIR/summary_${RUN_ID}_seed*.txt; do
if [ -f "$summary" ]; then
SEED_NUM=$(echo $summary | sed "s/.*seed\(.*\)\.txt/\1/")
echo ""
echo -e "${BOLD}Seed $SEED_NUM:${NC}"
grep -E "Profit:|Discovery Reached:" "$summary" | head -2
fi
done
echo ""
echo "To replay a specific scenario, run:"
echo " $0 ${RUN_ID} <seed_number>"
exit 0
fi
# Check if specific scenario files exist
SCENARIO_FILE="$RESULTS_DIR/scenario_${RUN_ID}_seed${SEED}.json"
REPLAY_FILE="$RESULTS_DIR/replay_${RUN_ID}_seed${SEED}.sol"
SUMMARY_FILE="$RESULTS_DIR/summary_${RUN_ID}_seed${SEED}.txt"
if [ ! -f "$SCENARIO_FILE" ]; then
echo -e "${RED}Error: Scenario file not found for seed ${SEED}${NC}"
echo "Looking for: $SCENARIO_FILE"
exit 1
fi
echo -e "${GREEN}=== Scenario Details ===${NC}"
if [ -f "$SUMMARY_FILE" ]; then
cat "$SUMMARY_FILE" | head -20
echo ""
fi
# Create replay test file
# Replace hyphens with underscores for valid Solidity identifier
CONTRACT_SAFE_ID=$(echo $RUN_ID | tr '-' '_')
REPLAY_TEST="test/Replay_${RUN_ID}_Seed${SEED}.t.sol"
echo -e "${YELLOW}Creating replay test file...${NC}"
cat > $REPLAY_TEST << 'EOF'
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "forge-std/Test.sol";
import {TestEnvironment} from "./helpers/TestBase.sol";
import {IUniswapV3Pool} from "@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol";
import {IWETH9} from "../src/interfaces/IWETH9.sol";
import {Kraiken} from "../src/Kraiken.sol";
import {Stake} from "../src/Stake.sol";
import {LiquidityManager} from "../src/LiquidityManager.sol";
import "../analysis/helpers/SwapExecutor.sol";
import "../test/mocks/BullMarketOptimizer.sol";
contract Replay_CONTRACT_ID_Seed_SEED is Test {
TestEnvironment testEnv;
IUniswapV3Pool pool;
IWETH9 weth;
Kraiken kraiken;
Stake stake;
LiquidityManager lm;
bool token0isWeth;
address trader = makeAddr("trader");
address whale = makeAddr("whale");
address feeDestination = makeAddr("fees");
function setUp() public {
// Setup from recorded scenario
testEnv = new TestEnvironment(feeDestination);
BullMarketOptimizer optimizer = new BullMarketOptimizer();
(,pool, weth, kraiken, stake, lm,, token0isWeth) =
testEnv.setupEnvironmentWithOptimizer(SEED_PARITY, feeDestination, address(optimizer));
vm.deal(address(lm), 200 ether);
// Fund traders based on seed
uint256 traderFund = 50 ether + (uint256(keccak256(abi.encodePacked(uint256(SEED_NUM), "trader"))) % 150 ether);
uint256 whaleFund = 200 ether + (uint256(keccak256(abi.encodePacked(uint256(SEED_NUM), "whale"))) % 300 ether);
vm.deal(trader, traderFund * 2);
vm.prank(trader);
weth.deposit{value: traderFund}();
vm.deal(whale, whaleFund * 2);
vm.prank(whale);
weth.deposit{value: whaleFund}();
vm.prank(feeDestination);
lm.recenter();
}
function test_replay_CONTRACT_ID_seed_SEED() public {
console.log("=== Replaying Scenario RUN_ID Seed SEED ===");
uint256 initialBalance = weth.balanceOf(trader);
// INSERT_REPLAY_ACTIONS
uint256 finalBalance = weth.balanceOf(trader);
if (finalBalance > initialBalance) {
uint256 profit = finalBalance - initialBalance;
uint256 profitPct = (profit * 100) / initialBalance;
console.log(string.concat("[INVARIANT VIOLATED] Profit: ", vm.toString(profit / 1e18), " ETH (", vm.toString(profitPct), "%)"));
revert("Trader profited - invariant violated");
} else {
console.log("[OK] No profit");
}
}
function _executeBuy(address buyer, uint256 amount) internal {
if (weth.balanceOf(buyer) < amount) return;
SwapExecutor executor = new SwapExecutor(pool, weth, kraiken, token0isWeth, lm);
vm.prank(buyer);
weth.transfer(address(executor), amount);
try executor.executeBuy(amount, buyer) {} catch {}
}
function _executeSell(address seller, uint256 amount) internal {
if (kraiken.balanceOf(seller) < amount) {
amount = kraiken.balanceOf(seller);
if (amount == 0) return;
}
SwapExecutor executor = new SwapExecutor(pool, weth, kraiken, token0isWeth, lm);
vm.prank(seller);
kraiken.transfer(address(executor), amount);
try executor.executeSell(amount, seller) {} catch {}
}
}
EOF
# Replace placeholders
sed -i "s/CONTRACT_ID/${CONTRACT_SAFE_ID}/g" $REPLAY_TEST
sed -i "s/RUN_ID/${RUN_ID}/g" $REPLAY_TEST
sed -i "s/SEED_NUM/${SEED}/g" $REPLAY_TEST
sed -i "s/SEED_PARITY/$([ $((SEED % 2)) -eq 0 ] && echo "true" || echo "false")/g" $REPLAY_TEST
sed -i "s/_SEED/_${SEED}/g" $REPLAY_TEST
# Insert replay actions from the sol file
if [ -f "$REPLAY_FILE" ]; then
# Extract the function body from replay script
ACTIONS=$(sed -n '/function replayScenario/,/^}/p' "$REPLAY_FILE" | sed '1d;$d' | sed 's/^/ /')
# Use a temporary file for the replacement
awk -v actions="$ACTIONS" '/INSERT_REPLAY_ACTIONS/ {print actions; next} {print}' $REPLAY_TEST > ${REPLAY_TEST}.tmp
mv ${REPLAY_TEST}.tmp $REPLAY_TEST
fi
echo -e "${GREEN}Replay test created: $REPLAY_TEST${NC}"
echo ""
# Run the replay test
echo -e "${YELLOW}Running replay test...${NC}"
echo ""
forge test --match-contract Replay_${CONTRACT_SAFE_ID}_Seed_${SEED} -vv
echo ""
echo -e "${GREEN}=== Replay Complete ===${NC}"
echo ""
echo "To debug further:"
echo " 1. Edit the test file: $REPLAY_TEST"
echo " 2. Add console.log statements or assertions"
echo " 3. Run with more verbosity: forge test --match-contract Replay_${CONTRACT_SAFE_ID}_Seed_${SEED} -vvvv"
echo ""
echo "To visualize positions:"
echo " ./analysis/view-scenarios.sh $RESULTS_DIR"

View file

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

View file

@ -1,10 +1,11 @@
#!/bin/bash
# Usage: ./run-improved-fuzzing.sh [optimizer] [runs=N] [staking=on|off] [buybias=N] [trades=N] [stakingbias=N]
# Usage: ./run-fuzzing.sh [optimizer] [runs=N] [staking=on|off] [buybias=N] [trades=N] [stakingbias=N] [debugCSV]
# Examples:
# ./run-improved-fuzzing.sh BullMarketOptimizer runs=50
# ./run-improved-fuzzing.sh WhaleOptimizer runs=20 staking=off
# ./run-improved-fuzzing.sh BullMarketOptimizer runs=200 staking=on buybias=100 trades=30 stakingbias=95
# ./run-fuzzing.sh BullMarketOptimizer runs=50
# ./run-fuzzing.sh WhaleOptimizer runs=20 staking=off
# ./run-fuzzing.sh BullMarketOptimizer runs=200 staking=on buybias=100 trades=30 stakingbias=95
# ./run-fuzzing.sh BullMarketOptimizer debugCSV # Opens HTML visualizer after generating CSVs
# Colors for output
RED='\033[0;31m'
@ -16,11 +17,23 @@ BOLD='\033[1m'
# Configuration
OPTIMIZER=${1:-BullMarketOptimizer}
# Check if second parameter is debugCSV
DEBUG_CSV=false
if [[ "$2" == "debugCSV" ]]; then
DEBUG_CSV=true
RUNS="runs=3"
STAKING="staking=on"
BUYBIAS="buybias=50"
TRADES="trades=5"
STAKINGBIAS="stakingbias=80"
else
RUNS=${2:-runs=20}
STAKING=${3:-staking=on}
BUYBIAS=${4:-buybias=50}
TRADES=${5:-trades=15}
STAKINGBIAS=${6:-stakingbias=80}
fi
# Parse runs parameter
if [[ $RUNS == runs=* ]]; then
@ -56,8 +69,7 @@ if [[ $STAKINGBIAS == stakingbias=* ]]; then
STAKINGBIAS_VALUE=${STAKINGBIAS#stakingbias=}
fi
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
OUTPUT_DIR="fuzzing_results_${OPTIMIZER}_${TIMESTAMP}"
# Ensure we're in the onchain directory (script should be run from there)
echo -e "${GREEN}=== Fuzzing Analysis ===${NC}"
echo "Optimizer: $OPTIMIZER"
@ -68,7 +80,6 @@ if [ "$STAKING_ENABLED" = "true" ]; then
echo "Staking bias: $STAKINGBIAS_VALUE%"
fi
echo "Buy bias: $BUYBIAS_VALUE%"
echo "Output directory: $OUTPUT_DIR"
echo ""
# Validate optimizer
@ -83,80 +94,100 @@ case $OPTIMIZER in
;;
esac
# Create output directory
mkdir -p $OUTPUT_DIR
# Run the fuzzing analysis
# Run the streamlined fuzzing analysis
echo -e "${YELLOW}Starting fuzzing analysis...${NC}"
echo ""
# Record existing CSV files in analysis folder before running
EXISTING_CSVS=$(ls -1 analysis/fuzz-????-???.csv 2>/dev/null | sort)
FUZZING_RUNS=$RUNS_VALUE \
OPTIMIZER_CLASS=$OPTIMIZER \
ENABLE_STAKING=$STAKING_ENABLED \
BUY_BIAS=$BUYBIAS_VALUE \
TRADES_PER_RUN=$TRADES_VALUE \
STAKING_BIAS=$STAKINGBIAS_VALUE \
TRACK_POSITIONS=true \
forge script analysis/ImprovedFuzzingAnalysis.s.sol:ImprovedFuzzingAnalysis --gas-limit 200000000 -vv 2>&1 | tee $OUTPUT_DIR/fuzzing.log
forge script analysis/StreamlinedFuzzing.s.sol:StreamlinedFuzzing --skip-simulation --gas-estimate-multiplier 300 -vv 2>&1
# Extract key metrics
# Analysis complete
echo ""
echo -e "${GREEN}=== ANALYSIS COMPLETE ===${NC}"
# Show summary from log
tail -20 $OUTPUT_DIR/fuzzing.log | grep -E "Total scenarios|Profitable|Discovery|Stakes|Snatches" || true
# Check for position CSVs
POSITION_CSV_COUNT=$(ls -1 improved_positions_*.csv 2>/dev/null | wc -l)
if [ $POSITION_CSV_COUNT -gt 0 ]; then
# Move position CSVs to output directory
mv improved_positions_*.csv $OUTPUT_DIR/
echo ""
echo -e "${GREEN}Position CSVs saved to: $OUTPUT_DIR/${NC}"
# Calculate max staking percentage
echo ""
echo -e "${YELLOW}=== Maximum Staking Level ===${NC}"
for f in $OUTPUT_DIR/improved_positions_*.csv; do
tail -1 "$f" | cut -d',' -f13
done | sort -n | tail -1 | awk '{
pct = $1/1e16
printf "%.2f%% of authorized stake (%.2f%% of KRAIKEN supply)\n", pct, pct*0.2
}'
fi
# Check for snatching activity
echo ""
echo -e "${YELLOW}=== Snatching Activity ===${NC}"
SNATCH_COUNT=$(grep -c "SNATCHED" $OUTPUT_DIR/fuzzing.log 2>/dev/null || true)
if [ -z "$SNATCH_COUNT" ]; then
SNATCH_COUNT="0"
fi
if [ "$SNATCH_COUNT" -gt "0" ]; then
echo -e "${GREEN}Snatching observed! Found $SNATCH_COUNT snatching events${NC}"
grep "SNATCHED" $OUTPUT_DIR/fuzzing.log | head -5
# Find newly generated CSV files in analysis folder
NEW_CSVS=$(ls -1 analysis/fuzz-????-???.csv 2>/dev/null | sort)
# Get the scenario code from the first new file
if [ "$EXISTING_CSVS" != "$NEW_CSVS" ]; then
SCENARIO_CODE=$(echo "$NEW_CSVS" | grep -v -F "$EXISTING_CSVS" | head -1 | sed 's/fuzz-\(....\).*/\1/')
else
echo "No snatching observed in this run"
SCENARIO_CODE="UNKN"
fi
# Count generated CSV files for this run
CSV_COUNT=$(ls -1 analysis/fuzz-${SCENARIO_CODE}-*.csv 2>/dev/null | wc -l)
echo "Generated $CSV_COUNT CSV files with scenario ID: ${SCENARIO_CODE}"
# Check for profitable scenarios
PROFITABLE_COUNT=$(grep -c "PROFITABLE!" $OUTPUT_DIR/fuzzing.log 2>/dev/null || true)
if [ -z "$PROFITABLE_COUNT" ]; then
PROFITABLE_COUNT="0"
fi
if [ "$PROFITABLE_COUNT" -gt "0" ]; then
echo ""
echo -e "${GREEN}=== PROFITABLE SCENARIOS FOUND ===${NC}"
echo "Found $PROFITABLE_COUNT profitable scenarios"
grep "PROFITABLE!" $OUTPUT_DIR/fuzzing.log | head -5
echo -e "${YELLOW}=== Checking for Profitable Scenarios ===${NC}"
PROFITABLE_COUNT=0
for csv in analysis/fuzz-${SCENARIO_CODE}-*.csv; do
if [ -f "$csv" ]; then
# Get INIT and FINAL rows
INIT_ETH=$(grep "^INIT," "$csv" | cut -d',' -f13)
FINAL_ETH=$(grep "^FINAL," "$csv" | cut -d',' -f13)
if [ ! -z "$INIT_ETH" ] && [ ! -z "$FINAL_ETH" ]; then
if [ "$FINAL_ETH" -gt "$INIT_ETH" ]; then
PROFIT_PCT=$(echo "scale=1; ($FINAL_ETH - $INIT_ETH) * 100 / $INIT_ETH" | bc -l 2>/dev/null || echo "0")
echo -e "${GREEN} $csv: PROFITABLE (+${PROFIT_PCT}%)${NC}"
((PROFITABLE_COUNT++))
fi
fi
fi
done
if [ $PROFITABLE_COUNT -eq 0 ]; then
echo " No profitable scenarios found"
else
echo -e "${GREEN}Found $PROFITABLE_COUNT profitable scenarios${NC}"
fi
echo ""
echo -e "${GREEN}Full results saved to: $OUTPUT_DIR/${NC}"
echo -e "${GREEN}CSV files generated with scenario ID: ${SCENARIO_CODE}${NC}"
# Launch HTML visualizer if debugCSV mode
if [ "$DEBUG_CSV" = true ]; then
echo ""
echo "To view detailed logs:"
echo " cat $OUTPUT_DIR/fuzzing.log"
echo -e "${YELLOW}Launching HTML visualizer...${NC}"
# Check if Python3 is available
if command -v python3 &> /dev/null; then
echo "Starting local server from analysis folder at http://localhost:8000"
echo "Open http://localhost:8000/run-visualizer.html to view results"
# Start server from analysis directory so CSV files are directly accessible
cd analysis
python3 -m http.server 8000 --bind 127.0.0.1 &
SERVER_PID=$!
# Try to open browser (cross-platform)
sleep 1
if command -v open &> /dev/null; then
# macOS
open "http://localhost:8000/run-visualizer.html"
elif command -v xdg-open &> /dev/null; then
# Linux
xdg-open "http://localhost:8000/run-visualizer.html"
elif command -v wslview &> /dev/null; then
# WSL
wslview "http://localhost:8000/run-visualizer.html"
fi
echo ""
echo "To visualize position movements (if CSVs generated):"
echo " ./analysis/view-scenarios.sh $OUTPUT_DIR"
echo "Press Ctrl+C to stop the server"
wait $SERVER_PID
else
echo "Python3 not found. To view results:"
echo " cd analysis && python3 -m http.server 8000"
echo " Then open http://localhost:8000/run-visualizer.html in your browser"
fi
fi

View file

@ -1,162 +0,0 @@
#!/bin/bash
# Usage: ./run-improved-fuzzing.sh [optimizer] [runs=N] [staking=on|off] [buybias=N] [trades=N] [stakingbias=N]
# Examples:
# ./run-improved-fuzzing.sh BullMarketOptimizer runs=50
# ./run-improved-fuzzing.sh WhaleOptimizer runs=20 staking=off
# ./run-improved-fuzzing.sh BullMarketOptimizer runs=200 staking=on buybias=100 trades=30 stakingbias=95
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
BOLD='\033[1m'
# Configuration
OPTIMIZER=${1:-BullMarketOptimizer}
RUNS=${2:-runs=20}
STAKING=${3:-staking=on}
BUYBIAS=${4:-buybias=50}
TRADES=${5:-trades=15}
STAKINGBIAS=${6:-stakingbias=80}
# Parse runs parameter
if [[ $RUNS == runs=* ]]; then
RUNS_VALUE=${RUNS#runs=}
else
RUNS_VALUE=$RUNS
fi
# Parse staking parameter
STAKING_ENABLED="true"
if [[ $STAKING == staking=* ]]; then
STAKING_VALUE=${STAKING#staking=}
if [[ $STAKING_VALUE == "off" ]] || [[ $STAKING_VALUE == "false" ]] || [[ $STAKING_VALUE == "0" ]]; then
STAKING_ENABLED="false"
fi
fi
# Parse buy bias parameter
BUYBIAS_VALUE="50"
if [[ $BUYBIAS == buybias=* ]]; then
BUYBIAS_VALUE=${BUYBIAS#buybias=}
fi
# Parse trades parameter
TRADES_VALUE="15"
if [[ $TRADES == trades=* ]]; then
TRADES_VALUE=${TRADES#trades=}
fi
# Parse staking bias parameter
STAKINGBIAS_VALUE="80"
if [[ $STAKINGBIAS == stakingbias=* ]]; then
STAKINGBIAS_VALUE=${STAKINGBIAS#stakingbias=}
fi
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
OUTPUT_DIR="fuzzing_results_improved_${OPTIMIZER}_${TIMESTAMP}"
echo -e "${GREEN}=== Improved Fuzzing Analysis ===${NC}"
echo "Optimizer: $OPTIMIZER"
echo "Total runs: $RUNS_VALUE"
echo "Trades per run: $TRADES_VALUE"
echo "Staking: $([ "$STAKING_ENABLED" = "true" ] && echo "enabled" || echo "disabled")"
if [ "$STAKING_ENABLED" = "true" ]; then
echo "Staking bias: $STAKINGBIAS_VALUE%"
fi
echo "Buy bias: $BUYBIAS_VALUE%"
echo "Output directory: $OUTPUT_DIR"
echo ""
# Validate optimizer
case $OPTIMIZER in
BullMarketOptimizer|BearMarketOptimizer|NeutralMarketOptimizer|WhaleOptimizer|ExtremeOptimizer|MaliciousOptimizer)
echo "Optimizer validation passed"
;;
*)
echo -e "${RED}Error: Invalid optimizer class${NC}"
echo "Valid options: BullMarketOptimizer, BearMarketOptimizer, NeutralMarketOptimizer, WhaleOptimizer, ExtremeOptimizer, MaliciousOptimizer"
exit 1
;;
esac
# Create output directory
mkdir -p $OUTPUT_DIR
# Run the improved fuzzing with buy bias
echo -e "${YELLOW}Starting improved fuzzing analysis with buy bias...${NC}"
echo ""
FUZZING_RUNS=$RUNS_VALUE \
OPTIMIZER_CLASS=$OPTIMIZER \
ENABLE_STAKING=$STAKING_ENABLED \
BUY_BIAS=$BUYBIAS_VALUE \
TRADES_PER_RUN=$TRADES_VALUE \
STAKING_BIAS=$STAKINGBIAS_VALUE \
TRACK_POSITIONS=true \
forge script analysis/ImprovedFuzzingAnalysis.s.sol:ImprovedFuzzingAnalysis --gas-limit 100000000 -vv 2>&1 | tee $OUTPUT_DIR/fuzzing.log
# Extract key metrics
echo ""
echo -e "${GREEN}=== ANALYSIS COMPLETE ===${NC}"
# Show summary from log
tail -20 $OUTPUT_DIR/fuzzing.log | grep -E "Total scenarios|Profitable|Discovery|Stakes|Snatches" || true
# Check for position CSVs
POSITION_CSV_COUNT=$(ls -1 improved_positions_*.csv 2>/dev/null | wc -l)
if [ $POSITION_CSV_COUNT -gt 0 ]; then
# Move position CSVs to output directory
mv improved_positions_*.csv $OUTPUT_DIR/
echo ""
echo -e "${GREEN}Position CSVs saved to: $OUTPUT_DIR/${NC}"
# Calculate max staking percentage
echo ""
echo -e "${YELLOW}=== Maximum Staking Level ===${NC}"
for f in $OUTPUT_DIR/improved_positions_*.csv; do
tail -1 "$f" | cut -d',' -f13
done | sort -n | tail -1 | awk '{
pct = $1/1e16
printf "%.2f%% of authorized stake (%.2f%% of KRAIKEN supply)\n", pct, pct*0.2
}'
fi
# Check for snatching activity
echo ""
echo -e "${YELLOW}=== Snatching Activity ===${NC}"
SNATCH_COUNT=$(grep -c "SNATCHED" $OUTPUT_DIR/fuzzing.log 2>/dev/null || true)
if [ -z "$SNATCH_COUNT" ]; then
SNATCH_COUNT="0"
fi
if [ "$SNATCH_COUNT" -gt "0" ]; then
echo -e "${GREEN}Snatching observed! Found $SNATCH_COUNT snatching events${NC}"
grep "SNATCHED" $OUTPUT_DIR/fuzzing.log | head -5
else
echo "No snatching observed in this run"
fi
# Check for profitable scenarios
PROFITABLE_COUNT=$(grep -c "PROFITABLE!" $OUTPUT_DIR/fuzzing.log 2>/dev/null || true)
if [ -z "$PROFITABLE_COUNT" ]; then
PROFITABLE_COUNT="0"
fi
if [ "$PROFITABLE_COUNT" -gt "0" ]; then
echo ""
echo -e "${GREEN}=== PROFITABLE SCENARIOS FOUND ===${NC}"
echo "Found $PROFITABLE_COUNT profitable scenarios"
grep "PROFITABLE!" $OUTPUT_DIR/fuzzing.log | head -5
fi
echo ""
echo -e "${GREEN}Full results saved to: $OUTPUT_DIR/${NC}"
echo ""
echo "To view detailed logs:"
echo " cat $OUTPUT_DIR/fuzzing.log"
echo ""
echo "To visualize position movements (if CSVs generated):"
echo " ./analysis/view-scenarios.sh $OUTPUT_DIR"

View file

@ -1,256 +0,0 @@
#!/bin/bash
# Usage: ./run-recorded-fuzzing.sh [optimizer] [runs=N] [trades=N] [staking=on|off] [buybias=N]
# Examples:
# ./run-recorded-fuzzing.sh BullMarketOptimizer runs=50
# ./run-recorded-fuzzing.sh WhaleOptimizer runs=20 trades=30 staking=off
# ./run-recorded-fuzzing.sh ExtremeOptimizer runs=100 trades=default staking=on buybias=80
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
BOLD='\033[1m'
# Configuration
OPTIMIZER=${1:-BullMarketOptimizer}
RUNS=${2:-runs=20}
TRADES=${3:-trades=default}
STAKING=${4:-staking=on}
BUYBIAS=${5:-buybias=50}
# Parse runs parameter
if [[ $RUNS == runs=* ]]; then
RUNS_VALUE=${RUNS#runs=}
else
RUNS_VALUE=$RUNS
RUNS="runs=$RUNS"
fi
# Parse trades parameter (for future use)
TRADES_VALUE=""
if [[ $TRADES == trades=* ]]; then
TRADES_VALUE=${TRADES#trades=}
fi
# Parse staking parameter
STAKING_ENABLED="true"
if [[ $STAKING == staking=* ]]; then
STAKING_VALUE=${STAKING#staking=}
if [[ $STAKING_VALUE == "off" ]] || [[ $STAKING_VALUE == "false" ]] || [[ $STAKING_VALUE == "0" ]]; then
STAKING_ENABLED="false"
fi
elif [[ $STAKING == "nostaking" ]]; then
STAKING_ENABLED="false"
fi
# Parse buy bias parameter
BUYBIAS_VALUE="50"
if [[ $BUYBIAS == buybias=* ]]; then
BUYBIAS_VALUE=${BUYBIAS#buybias=}
fi
# Generate unique run ID (6 chars from timestamp + random)
RUN_ID=$(date +%y%m%d)-$(head /dev/urandom | tr -dc A-Z0-9 | head -c 4)
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
OUTPUT_DIR="fuzzing_results_recorded_${OPTIMIZER}_${TIMESTAMP}"
echo -e "${GREEN}=== Recorded Fuzzing Campaign ===${NC}"
echo -e "Run ID: ${BOLD}${RUN_ID}${NC}"
echo "Optimizer: $OPTIMIZER"
echo "Total runs: $RUNS_VALUE"
echo "Staking: $([ "$STAKING_ENABLED" = "true" ] && echo "enabled" || echo "disabled")"
echo "Buy bias: $BUYBIAS_VALUE%"
echo "Output directory: $OUTPUT_DIR"
echo ""
# Validate optimizer
case $OPTIMIZER in
BullMarketOptimizer|BearMarketOptimizer|NeutralMarketOptimizer|WhaleOptimizer|ExtremeOptimizer|MaliciousOptimizer)
echo "Optimizer validation passed"
;;
*)
echo -e "${RED}Error: Invalid optimizer class${NC}"
echo "Valid options: BullMarketOptimizer, BearMarketOptimizer, NeutralMarketOptimizer, WhaleOptimizer, ExtremeOptimizer, MaliciousOptimizer"
exit 1
;;
esac
# Create output directory
mkdir -p $OUTPUT_DIR
# Run the recorded fuzzing
echo -e "${YELLOW}Starting recorded fuzzing analysis...${NC}"
FUZZING_RUNS=$RUNS_VALUE \
OPTIMIZER_CLASS=$OPTIMIZER \
ENABLE_STAKING=$STAKING_ENABLED \
BUY_BIAS=$BUYBIAS_VALUE \
RUN_ID=$RUN_ID \
forge script analysis/RecordedFuzzingAnalysis.s.sol:RecordedFuzzingAnalysis --gas-limit 1000000000 -vv 2>&1 | tee $OUTPUT_DIR/fuzzing.log
# Check for generated scenario files (check for summaries, they're always generated)
SCENARIO_COUNT=$(ls -1 scenario_summary_seed*.txt 2>/dev/null | wc -l)
if [ $SCENARIO_COUNT -gt 0 ]; then
echo ""
echo -e "${GREEN}=== INVARIANT VIOLATIONS FOUND! ===${NC}"
echo -e "${BOLD}Found $SCENARIO_COUNT profitable scenarios${NC}"
echo ""
# Move recording files to output directory with run ID
for file in recorded_scenario_seed*.json; do
if [ -f "$file" ]; then
SEED=$(echo $file | sed 's/recorded_scenario_seed\(.*\)\.json/\1/')
NEW_NAME="scenario_${RUN_ID}_seed${SEED}.json"
mv "$file" "$OUTPUT_DIR/$NEW_NAME"
echo -e " Scenario JSON: ${BLUE}$OUTPUT_DIR/$NEW_NAME${NC}"
fi
done
for file in replay_script_seed*.sol; do
if [ -f "$file" ]; then
SEED=$(echo $file | sed 's/replay_script_seed\(.*\)\.sol/\1/')
NEW_NAME="replay_${RUN_ID}_seed${SEED}.sol"
mv "$file" "$OUTPUT_DIR/$NEW_NAME"
echo -e " Replay script: ${BLUE}$OUTPUT_DIR/$NEW_NAME${NC}"
fi
done
for file in scenario_summary_seed*.txt; do
if [ -f "$file" ]; then
SEED=$(echo $file | sed 's/scenario_summary_seed\(.*\)\.txt/\1/')
NEW_NAME="summary_${RUN_ID}_seed${SEED}.txt"
mv "$file" "$OUTPUT_DIR/$NEW_NAME"
echo -e " Summary: ${BLUE}$OUTPUT_DIR/$NEW_NAME${NC}"
# Display summary preview
echo ""
echo -e "${YELLOW}--- Preview of $NEW_NAME ---${NC}"
head -n 15 "$OUTPUT_DIR/$NEW_NAME"
echo "..."
fi
done
# Move position CSVs if they exist
if ls improved_positions_*.csv 1> /dev/null 2>&1; then
mv improved_positions_*.csv $OUTPUT_DIR/
echo -e "\n Position CSVs for visualization moved to: ${BLUE}$OUTPUT_DIR/${NC}"
fi
# Create index file
cat > $OUTPUT_DIR/index.txt << EOF
Recorded Fuzzing Results
========================
Run ID: $RUN_ID
Date: $(date)
Optimizer: $OPTIMIZER
Total Runs: $RUNS_VALUE
Profitable Scenarios: $SCENARIO_COUNT
Files:
------
EOF
for file in $OUTPUT_DIR/*; do
echo "- $(basename $file)" >> $OUTPUT_DIR/index.txt
done
echo ""
echo -e "${GREEN}=== NEXT STEPS ===${NC}"
echo "1. Review summaries:"
echo " cat $OUTPUT_DIR/summary_${RUN_ID}_seed*.txt"
echo ""
echo "2. Replay a specific scenario:"
echo " ./analysis/replay-scenario.sh ${RUN_ID} <seed_number>"
echo ""
echo "3. Visualize positions (if CSV tracking enabled):"
echo " ./analysis/view-scenarios.sh $OUTPUT_DIR"
echo ""
echo "4. Share with team:"
echo -e " ${BOLD}Reference ID: ${RUN_ID}${NC}"
echo " \"Found exploit ${RUN_ID} with ${SCENARIO_COUNT} profitable scenarios\""
else
echo ""
echo -e "${YELLOW}=== No Profitable Scenarios Found ===${NC}"
echo "This could mean the protocol is secure under these conditions."
echo ""
echo "Try adjusting parameters:"
echo " - Increase runs: ./analysis/run-recorded-fuzzing.sh $OPTIMIZER runs=100"
echo " - Try different optimizer: ./analysis/run-recorded-fuzzing.sh WhaleOptimizer"
echo " - Use extreme optimizer: ./analysis/run-recorded-fuzzing.sh ExtremeOptimizer"
echo " - Disable staking: ./analysis/run-recorded-fuzzing.sh $OPTIMIZER runs=$RUNS_VALUE trades=default staking=off"
fi
echo ""
echo -e "${GREEN}Results saved to: $OUTPUT_DIR/${NC}"
echo -e "Run ID: ${BOLD}${RUN_ID}${NC}"
# Launch visualizer if position CSVs were generated
POSITION_CSV_COUNT=$(ls -1 $OUTPUT_DIR/improved_positions_*.csv 2>/dev/null | wc -l)
if [ $POSITION_CSV_COUNT -gt 0 ] && [ $SCENARIO_COUNT -gt 0 ]; then
echo ""
echo -e "${GREEN}=== Launching Scenario Visualizer ===${NC}"
# Get the first position CSV for visualization
FIRST_CSV=$(ls -1 $OUTPUT_DIR/improved_positions_*.csv 2>/dev/null | head -1)
echo "CSV file: $FIRST_CSV"
# Create a temporary symlink to the CSV for the viewer in the analysis directory
TEMP_LINK="analysis/profitable_scenario.csv"
if [ -f "$TEMP_LINK" ] || [ -L "$TEMP_LINK" ]; then
rm -f "$TEMP_LINK"
fi
# Use absolute path for the symlink
ln -s "$(realpath $FIRST_CSV)" "$TEMP_LINK"
echo "Created symlink: $TEMP_LINK -> $FIRST_CSV"
# Check if server is already running on common ports
SERVER_RUNNING=false
EXISTING_PORT=""
for PORT in 8000 8001 8002; do
if lsof -Pi :$PORT -sTCP:LISTEN -t >/dev/null 2>&1; then
# Check if it's a python http server in our analysis directory
if lsof -Pi :$PORT -sTCP:LISTEN 2>/dev/null | grep -q "python.*http.server"; then
SERVER_RUNNING=true
EXISTING_PORT=$PORT
break
fi
fi
done
if [ "$SERVER_RUNNING" = true ]; then
echo -e "${YELLOW}Server already running on port $EXISTING_PORT${NC}"
echo -e "${GREEN}Open browser to: http://localhost:$EXISTING_PORT/scenario-visualizer.html${NC}"
echo ""
echo -e "${YELLOW}Press Enter to exit (server will keep running)...${NC}"
read -r
else
# Start the viewer in background
./analysis/view-scenarios.sh &
VIEWER_PID=$!
# Give the server time to start
sleep 2
echo ""
echo -e "${GREEN}Browser should open to: http://localhost:8000/scenario-visualizer.html${NC}"
echo ""
echo -e "${YELLOW}Press Enter to stop the viewer and exit...${NC}"
read -r
# Kill the viewer process
if [ -n "$VIEWER_PID" ]; then
pkill -P $VIEWER_PID 2>/dev/null || true
kill $VIEWER_PID 2>/dev/null || true
fi
echo -e "${GREEN}Viewer stopped.${NC}"
fi
# Clean up the symlink
rm -f "$TEMP_LINK"
fi

View file

@ -0,0 +1,916 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Kraiken Fuzzing Run Visualizer</title>
<script src="https://cdn.plot.ly/plotly-latest.min.js"></script>
<style>
body {
font-family: Arial, sans-serif;
background-color: #f5f5f5;
margin: 0;
padding: 20px;
}
.header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 20px;
border-radius: 8px;
margin-bottom: 20px;
text-align: center;
}
.controls {
background: white;
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.file-selector {
display: flex;
gap: 15px;
align-items: center;
flex-wrap: wrap;
}
select {
padding: 10px;
border: 2px solid #ddd;
border-radius: 4px;
font-size: 14px;
min-width: 200px;
cursor: pointer;
}
select:hover {
border-color: #667eea;
}
button {
padding: 10px 20px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
font-weight: 600;
}
button:hover {
opacity: 0.9;
}
.info-panel {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 15px;
margin-top: 15px;
}
.info-item {
background: #f8f9fa;
padding: 10px;
border-radius: 4px;
border-left: 4px solid #667eea;
}
.info-label {
font-size: 12px;
color: #666;
margin-bottom: 5px;
}
.info-value {
font-size: 16px;
font-weight: bold;
color: #333;
}
.scenario-container {
margin-bottom: 40px;
background: white;
border-radius: 8px;
padding: 20px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.scenario-header {
margin: 0 0 20px 0;
padding: 10px;
background-color: #f8f9fa;
border-radius: 4px;
border-left: 4px solid #007bff;
}
.charts-container {
display: flex;
gap: 20px;
margin-bottom: 20px;
}
.chart-wrapper {
flex: 1;
min-width: 0;
}
.chart-title {
text-align: center;
margin-bottom: 10px;
font-weight: bold;
color: #333;
}
.chart-div {
width: 100%;
height: 500px;
border: 1px solid #ddd;
border-radius: 4px;
}
.summary-panel {
background-color: #f8f9fa;
border-radius: 4px;
padding: 15px;
margin-top: 20px;
}
.summary-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 15px;
margin-top: 10px;
}
.summary-item {
background: white;
padding: 10px;
border-radius: 4px;
border-left: 4px solid #007bff;
}
.summary-item.floor {
border-left-color: #1f77b4;
}
.summary-item.anchor {
border-left-color: #ff7f0e;
}
.summary-item.discovery {
border-left-color: #2ca02c;
}
.navigation {
margin: 20px 0;
text-align: center;
}
.navigation button {
margin: 0 10px;
padding: 10px 20px;
font-size: 16px;
}
.row-info {
margin: 20px 0;
padding: 15px;
background: #f8f9fa;
border-radius: 4px;
text-align: center;
}
.instructions {
background-color: #e3f2fd;
border-radius: 4px;
padding: 15px;
margin-bottom: 20px;
border-left: 4px solid #2196f3;
}
.error {
background-color: #ffebee;
border-radius: 4px;
padding: 15px;
margin: 20px 0;
border-left: 4px solid #f44336;
color: #c62828;
}
@media (max-width: 768px) {
.charts-container {
flex-direction: column;
}
}
</style>
</head>
<body>
<div class="header">
<h1>🐙 Kraiken Fuzzing Run Visualizer</h1>
<p>Analyze individual trades from fuzzing runs</p>
</div>
<div class="instructions">
<strong>📊 Anti-Arbitrage Three-Position Strategy (Uniswap V3 1% Pool)</strong><br>
<br>
<strong>Position Strategy:</strong><br>
<em>Floor</em>: Deep liquidity position - holds ETH when ETH is cheap (below current price)<br>
<em>Anchor</em>: Shallow liquidity around current price for fast price discovery<br>
<em>Discovery</em>: Edge liquidity position - holds KRAIKEN when ETH is expensive (above current price)<br>
<br>
<strong>Price Multiples:</strong> Shows ETH price relative to current (1x):<br>
&lt; 1x = ETH is cheaper than current price (positions below current hold ETH)<br>
• 1x = Current ETH price (red dashed line)<br>
&gt; 1x = ETH is more expensive than current price (positions above current hold KRAIKEN)<br>
<br>
<strong>Usage:</strong> Select a CSV file from the dropdown and use Previous/Next buttons to navigate through trades
</div>
<div class="controls">
<div class="file-selector">
<label for="csvSelect">Select CSV File:</label>
<select id="csvSelect">
<option value="">Loading files...</option>
</select>
<button onclick="refreshFileList()">Refresh List</button>
<button onclick="loadSelectedFile()">Load File</button>
</div>
<div class="info-panel" id="fileInfo" style="display: none;">
<div class="info-item">
<div class="info-label">File</div>
<div class="info-value" id="fileName">-</div>
</div>
<div class="info-item">
<div class="info-label">Total Rows</div>
<div class="info-value" id="totalRows">-</div>
</div>
<div class="info-item">
<div class="info-label">Scenario ID</div>
<div class="info-value" id="scenarioId">-</div>
</div>
<div class="info-item">
<div class="info-label">P&L</div>
<div class="info-value" id="pnl">-</div>
</div>
</div>
</div>
<div id="errorMessage" class="error" style="display: none;"></div>
<div id="simulations"></div>
<script>
// Position color scheme
const POSITION_COLORS = {
floor: '#1f77b4', // Dark Blue - Foundation/Stability
anchor: '#ff7f0e', // Orange - Current Price/Center
discovery: '#2ca02c' // Green - Growth/Expansion
};
// Position names for display
const POSITION_NAMES = {
floor: 'Floor',
anchor: 'Anchor',
discovery: 'Discovery'
};
// Global data storage
let currentData = null;
let currentFile = null;
// Token ordering configuration
const ethIsToken0 = false; // Default matches test setup
// Get row parameter from URL
function getRowParameter() {
const urlParams = new URLSearchParams(window.location.search);
const row = urlParams.get('row');
return row ? parseInt(row) - 2 : 0; // Convert CSV line number to array index
}
// Set row parameter in URL
function setRowParameter(lineNumber) {
const url = new URL(window.location);
url.searchParams.set('row', lineNumber);
window.history.pushState({}, '', url);
}
// Auto-load on page load
document.addEventListener('DOMContentLoaded', function() {
refreshFileList();
});
async function refreshFileList() {
const select = document.getElementById('csvSelect');
select.innerHTML = '<option value="">Loading files...</option>';
try {
// Fetch list of CSV files from the server
const response = await fetch('./');
const text = await response.text();
// Parse the directory listing to find CSV files
const parser = new DOMParser();
const doc = parser.parseFromString(text, 'text/html');
const links = doc.querySelectorAll('a');
const csvFiles = [];
links.forEach(link => {
const href = link.getAttribute('href');
if (href && href.endsWith('.csv')) {
// Filter for fuzz-XXXX-NNN.csv pattern
if (href.match(/fuzz-[A-Z0-9]{4}-\d{3}\.csv/)) {
csvFiles.push(href);
}
}
});
// Sort files by name
csvFiles.sort();
// Populate select
select.innerHTML = '<option value="">-- Select a file --</option>';
csvFiles.forEach(file => {
const option = document.createElement('option');
option.value = file;
option.textContent = file;
select.appendChild(option);
});
if (csvFiles.length === 0) {
select.innerHTML = '<option value="">No CSV files found</option>';
showError('No fuzzing CSV files found. Run ./run-fuzzing.sh to generate data.');
}
} catch (error) {
console.error('Error fetching file list:', error);
select.innerHTML = '<option value="">Error loading files</option>';
showError('Failed to load file list. Make sure the server is running from the analysis folder.');
}
}
async function loadSelectedFile() {
const select = document.getElementById('csvSelect');
const fileName = select.value;
if (!fileName) {
showError('Please select a CSV file');
return;
}
try {
const response = await fetch(`./${fileName}`);
if (!response.ok) {
throw new Error(`Failed to load ${fileName}`);
}
const csvText = await response.text();
currentFile = fileName;
loadCSVData(csvText, fileName);
hideError();
} catch (error) {
console.error('Error loading file:', error);
showError(`Failed to load ${fileName}. Make sure the file exists and the server has access.`);
}
}
function showError(message) {
const errorDiv = document.getElementById('errorMessage');
errorDiv.textContent = message;
errorDiv.style.display = 'block';
}
function hideError() {
document.getElementById('errorMessage').style.display = 'none';
}
function loadCSVData(csvText, fileName) {
const data = parseCSV(csvText);
currentData = data;
// Update file info
const fileInfo = document.getElementById('fileInfo');
fileInfo.style.display = 'block';
document.getElementById('fileName').textContent = fileName;
document.getElementById('totalRows').textContent = data.length;
// Extract scenario ID from filename
const scenarioMatch = fileName.match(/fuzz-([A-Z0-9]{4})-/);
if (scenarioMatch) {
document.getElementById('scenarioId').textContent = scenarioMatch[1];
}
// Calculate P&L
const initRow = data.find(row => (row.action === 'INIT' || row.precedingAction === 'INIT'));
const finalRow = data.find(row => (row.action === 'FINAL' || row.precedingAction === 'FINAL'));
if (initRow && finalRow) {
const initEth = parseFloat(initRow.eth_balance || initRow.ethBalance) || 0;
const finalEth = parseFloat(finalRow.eth_balance || finalRow.ethBalance) || 0;
if (initEth > 0) {
const pnl = ((finalEth - initEth) / initEth * 100).toFixed(2);
const pnlElement = document.getElementById('pnl');
pnlElement.textContent = `${pnl > 0 ? '+' : ''}${pnl}%`;
pnlElement.style.color = pnl > 0 ? '#4CAF50' : '#f44336';
}
}
simulateCSVData(data);
}
function parseCSV(csv) {
const lines = csv.trim().split('\n');
const headers = lines[0].split(',').map(h => h.trim());
const data = lines.slice(1).map(line => {
const values = line.split(',').map(v => v.trim());
const entry = {};
headers.forEach((header, index) => {
entry[header] = values[index];
});
return entry;
});
return data;
}
function simulateCSVData(data) {
// Clear previous simulations
document.getElementById('simulations').innerHTML = '';
// Get selected row from URL parameter
const selectedIndex = getRowParameter();
// Add current row info and navigation
const currentRowInfo = document.createElement('div');
currentRowInfo.className = 'row-info';
currentRowInfo.innerHTML = `
<strong>Currently viewing: Line ${selectedIndex + 2} - ${data[selectedIndex]?.action || data[selectedIndex]?.precedingAction || 'Unknown'}</strong><br>
<span style="color: #666; font-size: 14px;">Total rows: ${data.length} (Lines 2-${data.length + 1})</span>
`;
document.getElementById('simulations').appendChild(currentRowInfo);
// Add navigation buttons above
if (selectedIndex > 0 || selectedIndex < data.length - 1) {
const topNavDiv = document.createElement('div');
topNavDiv.className = 'navigation';
topNavDiv.innerHTML = `
${selectedIndex > 0 ? `
<button onclick="navigateRow(-1)">
← Previous (Line ${selectedIndex + 1})
</button>
` : ''}
${selectedIndex < data.length - 1 ? `
<button onclick="navigateRow(1)">
Next (Line ${selectedIndex + 3}) →
</button>
` : ''}
`;
document.getElementById('simulations').appendChild(topNavDiv);
}
// Process selected row
if (selectedIndex >= 0 && selectedIndex < data.length) {
const row = data[selectedIndex];
const previousRow = selectedIndex > 0 ? data[selectedIndex - 1] : null;
processRow(row, selectedIndex, previousRow);
// Add navigation buttons below
const bottomNavDiv = document.createElement('div');
bottomNavDiv.className = 'navigation';
bottomNavDiv.innerHTML = `
${selectedIndex > 0 ? `
<button onclick="navigateRow(-1)">
← Previous (Line ${selectedIndex + 1})
</button>
` : ''}
${selectedIndex < data.length - 1 ? `
<button onclick="navigateRow(1)">
Next (Line ${selectedIndex + 3}) →
</button>
` : ''}
`;
document.getElementById('simulations').appendChild(bottomNavDiv);
}
}
function navigateRow(direction) {
const currentIndex = getRowParameter();
const newIndex = currentIndex + direction;
if (currentData && newIndex >= 0 && newIndex < currentData.length) {
setRowParameter(newIndex + 2); // Convert to CSV line number
simulateCSVData(currentData);
}
}
function processRow(row, index, previousRow) {
// Map column names from actual CSV format
const precedingAction = row.action || row.precedingAction;
const currentTick = parseFloat(row.tick || row.currentTick);
const token0isWeth = row.token0isWeth === 'true' || row.token0isWeth === true || false;
const floorTickLower = parseFloat(row.floor_lower || row.floorTickLower);
const floorTickUpper = parseFloat(row.floor_upper || row.floorTickUpper);
const floorLiquidity = parseFloat(row.floor_liq || row.floorLiquidity || 0);
const anchorTickLower = parseFloat(row.anchor_lower || row.anchorTickLower);
const anchorTickUpper = parseFloat(row.anchor_upper || row.anchorTickUpper);
const anchorLiquidity = parseFloat(row.anchor_liq || row.anchorLiquidity || 0);
const discoveryTickLower = parseFloat(row.discovery_lower || row.discoveryTickLower);
const discoveryTickUpper = parseFloat(row.discovery_upper || row.discoveryTickUpper);
const discoveryLiquidity = parseFloat(row.discovery_liq || row.discoveryLiquidity || 0);
// Extract staking metrics if available
const percentageStaked = row.percentageStaked ? parseFloat(row.percentageStaked) : 0;
const avgTaxRate = row.avgTaxRate ? parseFloat(row.avgTaxRate) : 0;
// Calculate token amounts
const floorAmounts = getAmountsForLiquidity(floorLiquidity, floorTickLower, floorTickUpper, currentTick);
const anchorAmounts = getAmountsForLiquidity(anchorLiquidity, anchorTickLower, anchorTickUpper, currentTick);
const discoveryAmounts = getAmountsForLiquidity(discoveryLiquidity, discoveryTickLower, discoveryTickUpper, currentTick);
let floorEth, anchorEth, discoveryEth;
let floorKraiken, anchorKraiken, discoveryKraiken;
if (token0isWeth) {
floorEth = floorAmounts.amount0 / 1e18;
floorKraiken = floorAmounts.amount1 / 1e18;
anchorEth = anchorAmounts.amount0 / 1e18;
anchorKraiken = anchorAmounts.amount1 / 1e18;
discoveryEth = discoveryAmounts.amount0 / 1e18;
discoveryKraiken = discoveryAmounts.amount1 / 1e18;
} else {
floorEth = floorAmounts.amount1 / 1e18;
floorKraiken = floorAmounts.amount0 / 1e18;
anchorEth = anchorAmounts.amount1 / 1e18;
anchorKraiken = anchorAmounts.amount0 / 1e18;
discoveryEth = discoveryAmounts.amount1 / 1e18;
discoveryKraiken = discoveryAmounts.amount0 / 1e18;
}
// Create headline
const lineNumber = index + 2;
const headline = `Line ${lineNumber}: ${precedingAction}`;
simulateEnhanced(headline, currentTick,
floorTickLower, floorTickUpper, floorEth, floorKraiken, floorLiquidity,
anchorTickLower, anchorTickUpper, anchorEth, anchorKraiken, anchorLiquidity,
discoveryTickLower, discoveryTickUpper, discoveryEth, discoveryKraiken, discoveryLiquidity,
token0isWeth, index, precedingAction, percentageStaked, avgTaxRate);
}
// Uniswap V3 calculation functions
function tickToPrice(tick) {
return Math.pow(1.0001, tick);
}
function tickToSqrtPriceX96(tick) {
return Math.sqrt(Math.pow(1.0001, tick)) * (2 ** 96);
}
function getAmountsForLiquidity(liquidity, tickLower, tickUpper, currentTick) {
if (tickLower > tickUpper) {
[tickLower, tickUpper] = [tickUpper, tickLower];
}
const sqrtRatioAX96 = tickToSqrtPriceX96(tickLower);
const sqrtRatioBX96 = tickToSqrtPriceX96(tickUpper);
const sqrtRatioX96 = tickToSqrtPriceX96(currentTick);
let amount0 = 0;
let amount1 = 0;
const Q96 = 2 ** 96;
if (currentTick < tickLower) {
amount0 = liquidity * (sqrtRatioBX96 - sqrtRatioAX96) / Q96;
} else if (currentTick >= tickUpper) {
amount1 = liquidity * (sqrtRatioBX96 - sqrtRatioAX96) / Q96;
} else {
amount0 = liquidity * Q96 * (sqrtRatioBX96 - sqrtRatioX96) / sqrtRatioBX96 / sqrtRatioX96;
amount1 = liquidity * (sqrtRatioX96 - sqrtRatioAX96) / Q96;
}
return { amount0, amount1 };
}
function tickToPriceMultiple(tick, currentTick, token0isWeth) {
const price = tickToPrice(tick);
const currentPrice = tickToPrice(currentTick);
if (token0isWeth) {
return currentPrice / price;
} else {
return price / currentPrice;
}
}
function simulateEnhanced(precedingAction, currentTick,
floorTickLower, floorTickUpper, floorEth, floorKraiken, floorLiquidity,
anchorTickLower, anchorTickUpper, anchorEth, anchorKraiken, anchorLiquidity,
discoveryTickLower, discoveryTickUpper, discoveryEth, discoveryKraiken, discoveryLiquidity,
token0isWeth, index, originalAction, percentageStaked = 0, avgTaxRate = 0) {
const positions = {
floor: {
tickLower: floorTickLower,
tickUpper: floorTickUpper,
eth: floorEth,
kraiken: floorKraiken,
name: 'Floor',
liquidity: floorLiquidity
},
anchor: {
tickLower: anchorTickLower,
tickUpper: anchorTickUpper,
eth: anchorEth,
kraiken: anchorKraiken,
name: 'Anchor (Shallow Pool)',
liquidity: anchorLiquidity
},
discovery: {
tickLower: discoveryTickLower,
tickUpper: discoveryTickUpper,
eth: discoveryEth,
kraiken: discoveryKraiken,
name: 'Discovery',
liquidity: discoveryLiquidity
}
};
const totalLiquidity = Object.values(positions).reduce((sum, pos) => sum + pos.liquidity, 0);
// Create container
const scenarioContainer = document.createElement('div');
scenarioContainer.className = 'scenario-container';
// Create header
const header = document.createElement('div');
header.className = 'scenario-header';
header.innerHTML = `<h3>${precedingAction}</h3>`;
scenarioContainer.appendChild(header);
// Create charts container
const chartsContainer = document.createElement('div');
chartsContainer.className = 'charts-container';
// Create combined chart
const chartWrapper = document.createElement('div');
chartWrapper.className = 'chart-wrapper';
chartWrapper.style.width = '100%';
const chartTitle = document.createElement('div');
chartTitle.className = 'chart-title';
chartTitle.textContent = 'Total Liquidity Distribution (L × Tick Range)';
const combinedChart = document.createElement('div');
combinedChart.className = 'chart-div';
combinedChart.id = `combined-chart-${Date.now()}-${Math.random()}`;
chartWrapper.appendChild(chartTitle);
chartWrapper.appendChild(combinedChart);
chartsContainer.appendChild(chartWrapper);
scenarioContainer.appendChild(chartsContainer);
// Create summary panel
const summaryPanel = createSummaryPanel(positions, currentTick, token0isWeth, originalAction || precedingAction, index, percentageStaked, avgTaxRate);
scenarioContainer.appendChild(summaryPanel);
// Add to page
document.getElementById('simulations').appendChild(scenarioContainer);
// Create the combined chart
createCombinedChart(combinedChart, positions, currentTick, totalLiquidity, token0isWeth);
}
function createCombinedChart(chartDiv, positions, currentTick, totalLiquidity, token0isWeth) {
const positionKeys = ['floor', 'anchor', 'discovery'];
// Convert to price multiples
const priceMultiplePositions = {};
positionKeys.forEach(key => {
const pos = positions[key];
const lowerMultiple = tickToPriceMultiple(pos.tickLower, currentTick, token0isWeth);
const upperMultiple = tickToPriceMultiple(pos.tickUpper, currentTick, token0isWeth);
const centerMultiple = tickToPriceMultiple(pos.tickLower + (pos.tickUpper - pos.tickLower) / 2, currentTick, token0isWeth);
priceMultiplePositions[key] = {
lowerMultiple: lowerMultiple,
upperMultiple: upperMultiple,
centerMultiple: centerMultiple,
width: upperMultiple - lowerMultiple,
...pos
};
});
// Create liquidity traces
const liquidityTraces = [];
const MAX_REASONABLE_MULTIPLE = 50;
const MIN_REASONABLE_MULTIPLE = 0.02;
const allMultiples = positionKeys.flatMap(key => [priceMultiplePositions[key].lowerMultiple, priceMultiplePositions[key].upperMultiple]);
const cappedMultiples = allMultiples.map(m => Math.min(MAX_REASONABLE_MULTIPLE, Math.max(MIN_REASONABLE_MULTIPLE, m)));
const minMultiple = Math.min(...cappedMultiples);
const maxMultiple = Math.max(...cappedMultiples);
const multipleRange = maxMultiple - minMultiple;
const padding = multipleRange * 0.1;
const xAxisMin = Math.max(0.01, minMultiple - padding);
const xAxisMax = Math.min(100, maxMultiple + padding);
positionKeys.forEach(key => {
const pos = priceMultiplePositions[key];
const tickRange = pos.tickUpper - pos.tickLower;
const totalLiquidity = pos.liquidity * tickRange;
if (pos.liquidity === 0 || totalLiquidity === 0) {
return;
}
let displayLower = pos.lowerMultiple;
let displayUpper = pos.upperMultiple;
if (pos.upperMultiple < 0.01) {
displayLower = xAxisMin;
displayUpper = xAxisMin * 1.5;
} else if (pos.lowerMultiple > 50) {
displayLower = xAxisMax * 0.8;
displayUpper = xAxisMax;
} else {
displayLower = Math.max(xAxisMin, Math.min(xAxisMax, pos.lowerMultiple));
displayUpper = Math.max(xAxisMin, Math.min(xAxisMax, pos.upperMultiple));
}
let rangeText = `Range: ${pos.lowerMultiple.toFixed(3)}x - ${pos.upperMultiple.toFixed(3)}x`;
let extendsBeyond = false;
if (pos.lowerMultiple < xAxisMin || pos.upperMultiple > xAxisMax) {
rangeText += ' <b>(extends beyond chart)</b>';
extendsBeyond = true;
}
if (pos.upperMultiple > 100 || pos.lowerMultiple < 0.01) {
const lowerStr = pos.lowerMultiple < 0.01 ? pos.lowerMultiple.toExponential(2) : pos.lowerMultiple.toFixed(3) + 'x';
const upperStr = pos.upperMultiple > 100 ? pos.upperMultiple.toExponential(2) : pos.upperMultiple.toFixed(3) + 'x';
rangeText = `Range: ${lowerStr} - ${upperStr}`;
if (key === 'floor' && (pos.lowerMultiple < 0.01 || pos.upperMultiple > 100)) {
rangeText += ' <b>(VWAP Protection - ETH Scarcity)</b>';
} else {
rangeText += ' <b>(extreme range)</b>';
}
}
const trace = {
x: [displayLower, displayLower, displayUpper, displayUpper],
y: [0, totalLiquidity, totalLiquidity, 0],
fill: 'toself',
fillcolor: POSITION_COLORS[key],
opacity: extendsBeyond ? 0.5 : 0.7,
type: 'scatter',
mode: 'lines',
line: {
color: POSITION_COLORS[key],
width: 2,
dash: extendsBeyond ? 'dash' : 'solid'
},
name: pos.name,
text: `${pos.name}<br>Liquidity: ${pos.liquidity.toLocaleString(undefined, {minimumFractionDigits: 2, maximumFractionDigits: 2})}<br>Tick Range: ${tickRange.toLocaleString()}<br>Total (L×Ticks): ${totalLiquidity.toLocaleString(undefined, {minimumFractionDigits: 0, maximumFractionDigits: 0})}<br>ETH: ${pos.eth.toFixed(6)}<br>KRAIKEN: ${pos.kraiken.toLocaleString(undefined, {minimumFractionDigits: 2, maximumFractionDigits: 2})}<br>${rangeText}`,
hoverinfo: 'text',
showlegend: true
};
liquidityTraces.push(trace);
});
const data = liquidityTraces;
// Calculate y-axis range
const totalLiquidities = positionKeys.map(key => {
const pos = priceMultiplePositions[key];
const tickRange = pos.tickUpper - pos.tickLower;
return pos.liquidity * tickRange;
}).filter(l => l > 0);
const maxTotalLiquidity = totalLiquidities.length > 0 ? Math.max(...totalLiquidities) : 1;
const minTotalLiquidity = totalLiquidities.length > 0 ? Math.min(...totalLiquidities) : 0.1;
const yMin = Math.max(1e-10, minTotalLiquidity / 100);
const yMax = maxTotalLiquidity * 10;
// Add price line
const priceLineTrace = {
x: [1, 1],
y: [yMin, yMax],
mode: 'lines',
line: {
color: 'red',
width: 3,
dash: 'dash'
},
name: 'Current Price (1x)',
hoverinfo: 'name',
showlegend: true
};
data.push(priceLineTrace);
const layout = {
title: {
text: `Total Liquidity Distribution (L × Tick Range)`,
font: { size: 16 }
},
xaxis: {
title: 'Price Multiple (relative to current price)',
showgrid: true,
gridcolor: '#e0e0e0',
range: [Math.log10(xAxisMin), Math.log10(xAxisMax)],
tickformat: '.2f',
ticksuffix: 'x',
type: 'log'
},
yaxis: {
title: 'Total Liquidity (L × Ticks)',
type: 'log',
showgrid: true,
gridcolor: '#e0e0e0',
dtick: 1,
tickformat: '.0e',
range: [Math.log10(yMin), Math.log10(yMax)]
},
showlegend: true,
legend: {
x: 0.02,
y: 0.98,
bgcolor: 'rgba(255,255,255,0.8)',
bordercolor: '#ccc',
borderwidth: 1
},
plot_bgcolor: 'white',
paper_bgcolor: 'white',
margin: { l: 60, r: 60, t: 60, b: 50 },
hovermode: 'closest'
};
Plotly.newPlot(chartDiv, data, layout, {responsive: true});
}
function createSummaryPanel(positions, currentTick, token0isWeth, precedingAction, index, percentageStaked = 0, avgTaxRate = 0) {
const panel = document.createElement('div');
panel.className = 'summary-panel';
const title = document.createElement('h4');
title.textContent = 'Position Summary';
title.style.margin = '0 0 15px 0';
panel.appendChild(title);
const grid = document.createElement('div');
grid.className = 'summary-grid';
// Calculate totals
const totalEth = Object.values(positions).reduce((sum, pos) => sum + pos.eth, 0);
const totalKraiken = Object.values(positions).reduce((sum, pos) => sum + pos.kraiken, 0);
const totalUniV3Liquidity = Object.values(positions).reduce((sum, pos) => sum + pos.liquidity, 0);
// Add total summary
const totalItem = document.createElement('div');
totalItem.className = 'summary-item';
totalItem.innerHTML = `
<strong>Total Portfolio</strong><br>
ETH: ${totalEth.toLocaleString(undefined, {minimumFractionDigits: 6, maximumFractionDigits: 6})}<br>
KRAIKEN: ${totalKraiken.toLocaleString(undefined, {minimumFractionDigits: 2, maximumFractionDigits: 2})}<br>
Uniswap V3 Liquidity: ${totalUniV3Liquidity.toExponential(2)}
`;
grid.appendChild(totalItem);
// Add position summaries
Object.entries(positions).forEach(([key, pos]) => {
const item = document.createElement('div');
item.className = `summary-item ${key}`;
const liquidityPercent = totalUniV3Liquidity > 0 ? (pos.liquidity / totalUniV3Liquidity * 100).toFixed(1) : '0.0';
item.innerHTML = `
<strong>${pos.name} Position</strong><br>
ETH: ${pos.eth.toLocaleString(undefined, {minimumFractionDigits: 6, maximumFractionDigits: 6})}<br>
KRAIKEN: ${pos.kraiken.toLocaleString(undefined, {minimumFractionDigits: 2, maximumFractionDigits: 2})}<br>
Liquidity: ${pos.liquidity.toExponential(2)} (${liquidityPercent}%)<br>
Ticks: [${pos.tickLower.toLocaleString()}, ${pos.tickUpper.toLocaleString()}]
`;
grid.appendChild(item);
});
// Add current price info
const priceItem = document.createElement('div');
priceItem.className = 'summary-item';
const currentPrice = tickToPrice(currentTick);
let ethPriceInKraiken, kraikenPriceInEth;
if (token0isWeth) {
ethPriceInKraiken = currentPrice;
kraikenPriceInEth = 1 / currentPrice;
} else {
kraikenPriceInEth = currentPrice;
ethPriceInKraiken = 1 / currentPrice;
}
priceItem.innerHTML = `
<strong>Current Price</strong><br>
Tick: ${currentTick.toLocaleString()}<br>
1 ETH = ${ethPriceInKraiken.toLocaleString(undefined, {minimumFractionDigits: 2, maximumFractionDigits: 2})} KRAIKEN<br>
1 KRAIKEN = ${kraikenPriceInEth.toExponential(3)} ETH
`;
grid.appendChild(priceItem);
// Add staking metrics if available
if (percentageStaked > 0 || avgTaxRate > 0) {
const stakingItem = document.createElement('div');
stakingItem.className = 'summary-item';
const stakingPercent = (percentageStaked / 1e16).toFixed(2);
const taxRatePercent = (avgTaxRate / 1e16).toFixed(2);
stakingItem.innerHTML = `
<strong>Staking Metrics</strong><br>
Staked: ${stakingPercent}%<br>
Avg Tax Rate: ${taxRatePercent}%
`;
grid.appendChild(stakingItem);
}
panel.appendChild(grid);
return panel;
}
</script>
</body>
</html>

File diff suppressed because it is too large Load diff

View file

@ -1,148 +0,0 @@
# Staking-Based AnchorWidth Recommendations
## Understanding Staking as a Sentiment Signal
The Harberger tax staking mechanism creates a prediction market where:
- **Tax Rate** = Cost to hold a position (self-assessed valuation)
- **Percentage Staked** = Overall confidence in protocol
- **Average Tax Rate** = Market's price volatility expectation
## Staking Metrics → Market Conditions Mapping
### High Staking Percentage (>70%)
**Interpretation**: Strong bullish sentiment, holders confident in price appreciation
- Market expects upward price movement
- Stakers willing to lock capital despite opportunity cost
- Lower expected volatility (confident holders)
**anchorWidth Recommendation**: **30-50%**
- Moderate width to capture expected upward movement
- Avoid excessive rebalancing during steady climb
- Maintain efficiency without being too narrow
### Low Staking Percentage (<30%)
**Interpretation**: Bearish/uncertain sentiment, holders want liquidity
- Market expects downward pressure or high volatility
- Stakers unwilling to commit capital
- Higher expected volatility (nervous market)
**anchorWidth Recommendation**: **60-80%**
- Wide range to handle volatility without constant rebalancing
- Defensive positioning during uncertainty
- Prioritize capital preservation over fee optimization
### Medium Staking Percentage (30-70%)
**Interpretation**: Neutral market, mixed sentiment
- Balanced buyer/seller pressure
- Normal market conditions
- Moderate volatility expectations
**anchorWidth Recommendation**: **40-60%**
- Balanced approach for normal conditions
- Reasonable fee capture with manageable rebalancing
- Standard operating parameters
## Average Tax Rate → Volatility Expectations
### High Average Tax Rate (>50% of max)
**Interpretation**: Market expects significant price movement
- Stakers setting high taxes = expect to be "snatched" soon
- Indicates expected volatility or trend change
- Short-term holding mentality
**anchorWidth Recommendation**: **Increase by 20-30%**
- Wider anchor to handle expected volatility
- Reduce rebalancing frequency during turbulent period
- Example: Base 40% → Adjust to 60%
### Low Average Tax Rate (<20% of max)
**Interpretation**: Market expects stability
- Stakers comfortable with low taxes = expect to hold long-term
- Low volatility expectations
- Long-term holding mentality
**anchorWidth Recommendation**: **Decrease by 10-20%**
- Narrower anchor to maximize fee collection
- Take advantage of expected stability
- Example: Base 40% → Adjust to 30%
## Proposed On-Chain Formula
```solidity
function calculateAnchorWidth(
uint256 percentageStaked, // 0 to 1e18
uint256 avgTaxRate // 0 to 1e18 (normalized)
) public pure returns (uint24) {
// Base width starts at 40%
uint24 baseWidth = 40;
// Staking adjustment: -20% to +20% based on staking percentage
// High staking (bullish) → narrower width
// Low staking (bearish) → wider width
int24 stakingAdjustment = int24(20) - int24(uint24(percentageStaked * 40 / 1e18));
// Tax rate adjustment: -10% to +30% based on average tax
// High tax (volatile) → wider width
// Low tax (stable) → narrower width
int24 taxAdjustment = int24(uint24(avgTaxRate * 30 / 1e18)) - 10;
// Combined width
int24 totalWidth = int24(baseWidth) + stakingAdjustment + taxAdjustment;
// Clamp between 10 and 80
if (totalWidth < 10) return 10;
if (totalWidth > 80) return 80;
return uint24(totalWidth);
}
```
## Staking Signal Interpretations
### Scenario 1: "Confident Bull Market"
- High staking (80%), Low tax rate (20%)
- Interpretation: Strong holders, expect steady appreciation
- anchorWidth: ~25-35%
- Rationale: Tight range for fee optimization in trending market
### Scenario 2: "Fearful Bear Market"
- Low staking (20%), High tax rate (70%)
- Interpretation: Nervous market, expect volatility/decline
- anchorWidth: ~70-80%
- Rationale: Wide defensive positioning
### Scenario 3: "Speculative Frenzy"
- High staking (70%), High tax rate (80%)
- Interpretation: Aggressive speculation, expect big moves
- anchorWidth: ~50-60%
- Rationale: Balance between capturing moves and managing volatility
### Scenario 4: "Boring Stability"
- Medium staking (50%), Low tax rate (10%)
- Interpretation: Stable, range-bound market
- anchorWidth: ~30-40%
- Rationale: Optimize for fee collection in stable conditions
## Key Advantages of Staking-Based Approach
1. **On-Chain Native**: Uses only data available to smart contracts
2. **Forward-Looking**: Tax rates reflect expectations, not just history
3. **Self-Adjusting**: Market participants' actions directly influence parameters
4. **Sybil-Resistant**: Costly to manipulate due to tax payments
5. **Continuous Signal**: Updates in real-time as positions change
## Implementation Considerations
1. **Smoothing**: Average metrics over time window to prevent manipulation
2. **Bounds**: Always enforce min/max limits (10-80% recommended)
3. **Hysteresis**: Add small threshold before adjusting to reduce thrashing
4. **Gas Optimization**: Only recalculate when scraping/repositioning
## Summary Recommendations
For the on-chain Optimizer contract:
- Use `percentageStaked` as primary bull/bear indicator
- Use `averageTaxRate` as volatility expectation proxy
- Combine both signals for sophisticated width adjustment
- Default to 40% width when signals are neutral
- Never exceed 80% or go below 10% for safety

View file

@ -1,88 +0,0 @@
# Anchor Width Calculation Verification
## Formula
```
anchorWidth = base + stakingAdjustment + taxAdjustment
where:
base = 40
stakingAdjustment = 20 - (percentageStaked * 40 / 1e18)
taxAdjustment = (averageTaxRate * 40 / 1e18) - 10
final = clamp(anchorWidth, 10, 80)
```
## Test Case Verification
### 1. Bull Market (80% staked, 10% tax)
- Base: 40
- Staking adj: 20 - (0.8 * 40) = 20 - 32 = -12
- Tax adj: (0.1 * 40) - 10 = 4 - 10 = -6
- Total: 40 + (-12) + (-6) = **22**
### 2. Bear Market (20% staked, 70% tax)
- Base: 40
- Staking adj: 20 - (0.2 * 40) = 20 - 8 = 12
- Tax adj: (0.7 * 40) - 10 = 28 - 10 = 18
- Total: 40 + 12 + 18 = **70**
### 3. Neutral Market (50% staked, 30% tax)
- Base: 40
- Staking adj: 20 - (0.5 * 40) = 20 - 20 = 0
- Tax adj: (0.3 * 40) - 10 = 12 - 10 = 2
- Total: 40 + 0 + 2 = **42**
### 4. Speculative Frenzy (70% staked, 80% tax)
- Base: 40
- Staking adj: 20 - (0.7 * 40) = 20 - 28 = -8
- Tax adj: (0.8 * 40) - 10 = 32 - 10 = 22
- Total: 40 + (-8) + 22 = **54**
### 5. Stable Market (50% staked, 5% tax)
- Base: 40
- Staking adj: 20 - (0.5 * 40) = 20 - 20 = 0
- Tax adj: (0.05 * 40) - 10 = 2 - 10 = -8
- Total: 40 + 0 + (-8) = **32**
### 6. Zero Inputs (0% staked, 0% tax)
- Base: 40
- Staking adj: 20 - (0 * 40) = 20 - 0 = 20
- Tax adj: (0 * 40) - 10 = 0 - 10 = -10
- Total: 40 + 20 + (-10) = **50**
### 7. Max Inputs (100% staked, 100% tax)
- Base: 40
- Staking adj: 20 - (1.0 * 40) = 20 - 40 = -20
- Tax adj: (1.0 * 40) - 10 = 40 - 10 = 30
- Total: 40 + (-20) + 30 = **50**
### 8. Test Minimum Clamping (95% staked, 0% tax)
- Base: 40
- Staking adj: 20 - (0.95 * 40) = 20 - 38 = -18
- Tax adj: (0 * 40) - 10 = 0 - 10 = -10
- Total: 40 + (-18) + (-10) = **12** (not clamped, above 10) ✓
### 9. Test Maximum Clamping (0% staked, 100% tax)
- Base: 40
- Staking adj: 20 - (0 * 40) = 20 - 0 = 20
- Tax adj: (1.0 * 40) - 10 = 40 - 10 = 30
- Total: 40 + 20 + 30 = 90 → **80** (clamped to max) ✓
## Summary
All test cases pass! The implementation correctly:
1. **Inversely correlates staking with width**: Higher staking → narrower anchor
2. **Directly correlates tax with width**: Higher tax → wider anchor
3. **Maintains reasonable bounds**: 10-80% range
4. **Provides sensible defaults**: 50% width for zero/max inputs
## Market Condition Mapping
| Condition | Staking | Tax | Width | Rationale |
|-----------|---------|-----|-------|-----------|
| Bull Market | High (70-90%) | Low (0-20%) | 20-35% | Optimize fees in trending market |
| Bear Market | Low (10-30%) | High (60-90%) | 60-80% | Defensive positioning |
| Neutral | Medium (40-60%) | Medium (20-40%) | 35-50% | Balanced approach |
| Volatile | Any | High (70%+) | 50-80% | Wide to reduce rebalancing |
| Stable | Any | Low (<10%) | 20-40% | Narrow for fee collection |
The formula successfully encodes market dynamics into the anchor width parameter!

View file

@ -1,83 +0,0 @@
#!/bin/bash
# Scenario Visualizer Launcher
# Starts a local HTTP server and opens the visualization page
echo "🚀 Starting Scenario Visualizer..."
echo "=================================="
# Check if CSV file exists
if [ ! -f "profitable_scenario.csv" ]; then
echo "⚠️ No profitable_scenario.csv found in analysis folder"
echo "Run analysis first: forge script analysis/SimpleAnalysis.s.sol --ffi"
echo ""
read -p "Continue anyway? (y/n): " -n 1 -r
echo ""
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
exit 1
fi
fi
# Function to detect available Python
detect_python() {
if command -v python3 &> /dev/null; then
echo "python3"
elif command -v python &> /dev/null; then
echo "python"
else
echo ""
fi
}
# Function to start server
start_server() {
local python_cmd=$1
local port=$2
echo "📡 Starting HTTP server on port $port..."
echo "🌐 Opening http://localhost:$port/scenario-visualizer.html"
echo ""
echo "Press Ctrl+C to stop the server"
echo "=================================="
# Start server in background first
$python_cmd -m http.server $port &
SERVER_PID=$!
# Give server a moment to start
sleep 1
# Try to open browser (works on most systems)
if command -v xdg-open &> /dev/null; then
xdg-open "http://localhost:$port/scenario-visualizer.html" 2>/dev/null &
elif command -v open &> /dev/null; then
open "http://localhost:$port/scenario-visualizer.html" 2>/dev/null &
else
echo "🔗 Manual: Open http://localhost:$port/scenario-visualizer.html in your browser"
fi
# Wait for the server process
wait $SERVER_PID
}
# Main execution
cd "$(dirname "$0")"
# Check for Python
PYTHON_CMD=$(detect_python)
if [ -z "$PYTHON_CMD" ]; then
echo "❌ Python not found. Please install Python 3 or use alternative:"
echo " npx http-server . --port 8000"
echo " php -S localhost:8000"
exit 1
fi
# Check if port is available
PORT=8000
if lsof -Pi :$PORT -sTCP:LISTEN -t >/dev/null 2>&1; then
echo "⚠️ Port $PORT is already in use. Trying port $((PORT + 1))..."
PORT=$((PORT + 1))
fi
# Start the server
start_server "$PYTHON_CMD" "$PORT"

View file

@ -1,3 +0,0 @@
Scenario,Seed,Initial Balance,Final Balance,Profit,Profit %,Discovery Reached
BullMarketOptimizer,1,152861730151649342497,165534767451273053750,12673037299623711253,8,true
BullMarketOptimizer,2,53215106468573532381,63779335616103461539,10564229147529929158,19,true
1 Scenario Seed Initial Balance Final Balance Profit Profit % Discovery Reached
2 BullMarketOptimizer 1 152861730151649342497 165534767451273053750 12673037299623711253 8 true
3 BullMarketOptimizer 2 53215106468573532381 63779335616103461539 10564229147529929158 19 true

View file

@ -1,5 +0,0 @@
Scenario,Seed,Initial Balance,Final Balance,Profit,Profit %,Discovery Reached
BullMarketOptimizer,3,86868696383720927358,91094180959940828354,4225484576219900996,4,true
BullMarketOptimizer,6,77383282103079540723,78863793676376493002,1480511573296952279,1,true
BullMarketOptimizer,9,52269169078465922293,60595359746230433174,8326190667764510881,15,false
BullMarketOptimizer,11,63309771874363779531,63524232370110625118,214460495746845587,0,true
1 Scenario Seed Initial Balance Final Balance Profit Profit % Discovery Reached
2 BullMarketOptimizer 3 86868696383720927358 91094180959940828354 4225484576219900996 4 true
3 BullMarketOptimizer 6 77383282103079540723 78863793676376493002 1480511573296952279 1 true
4 BullMarketOptimizer 9 52269169078465922293 60595359746230433174 8326190667764510881 15 false
5 BullMarketOptimizer 11 63309771874363779531 63524232370110625118 214460495746845587 0 true