another rewrite
This commit is contained in:
parent
c72fe56ad0
commit
137adfe82b
20 changed files with 1402 additions and 3560 deletions
|
|
@ -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()
|
|
||||||
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -18,9 +18,9 @@ This directory contains tools for fuzzing the KRAIKEN LiquidityManager to identi
|
||||||
## Files
|
## Files
|
||||||
|
|
||||||
### Core Scripts
|
### 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
|
- `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
|
||||||
- `helpers/SwapExecutor.sol` - Handles swap execution through Uniswap
|
- `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
|
- `helpers/CSVHelper.sol` - CSV formatting helpers
|
||||||
|
|
||||||
### Visualization
|
### Visualization
|
||||||
- `AnalysisVisualizer.py` - Python script to generate charts from CSV data
|
- `run-visualizer.html` - Interactive web visualization for analyzing individual trades from fuzzing runs
|
||||||
- `scenario-visualizer.html` - Interactive web visualization
|
- Supports row-by-row navigation through trades with liquidity distribution charts
|
||||||
- `view-scenarios.sh` - Quick script to launch web server for visualization
|
|
||||||
|
|
||||||
## Available Optimizers
|
## Available Optimizers
|
||||||
|
|
||||||
|
|
@ -78,12 +77,12 @@ Each fuzzing campaign creates a timestamped directory with:
|
||||||
To visualize results:
|
To visualize results:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Start local web server
|
# Start local web server from analysis directory
|
||||||
./view-scenarios.sh
|
cd analysis && python3 -m http.server 8000
|
||||||
|
# Then open http://localhost:8000/run-visualizer.html
|
||||||
|
|
||||||
# Or use Python directly
|
# Or use debugCSV mode which automatically launches visualizer
|
||||||
python3 -m http.server 8000
|
./analysis/run-fuzzing.sh BullMarketOptimizer debugCSV
|
||||||
# Then open http://localhost:8000/scenario-visualizer.html
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Cleanup
|
### Cleanup
|
||||||
|
|
@ -116,9 +115,11 @@ To add a new optimizer:
|
||||||
## Notes
|
## Notes
|
||||||
|
|
||||||
- Each run deploys a fresh Uniswap V3 environment
|
- 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
|
- Results are deterministic based on the seed
|
||||||
- The fuzzer tests random buy/sell patterns with periodic recenters
|
- The fuzzer tests random buy/sell patterns with periodic recenters
|
||||||
- Supports staking operations with position snatching mechanics
|
- Supports staking operations with position snatching mechanics
|
||||||
- Memory-optimized with circular buffer for position tracking
|
- Only records trades that actually execute (non-zero amounts)
|
||||||
- Records all trades to CSV for complete visualization
|
- Records actual traded amounts after liquidity limits are applied
|
||||||
|
- CSV files are written to the analysis/ directory
|
||||||
|
- Every trade properly updates the tick value
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
346
onchain/analysis/StreamlinedFuzzing.s.sol
Normal file
346
onchain/analysis/StreamlinedFuzzing.s.sol
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
16
onchain/analysis/clean-csvs.sh
Executable 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
|
||||||
|
|
@ -28,15 +28,15 @@ contract SwapExecutor {
|
||||||
liquidityManager = _liquidityManager;
|
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
|
// Calculate maximum safe buy amount based on liquidity
|
||||||
uint256 maxBuyAmount = LiquidityBoundaryHelper.calculateBuyLimit(pool, liquidityManager, token0isWeth);
|
uint256 maxBuyAmount = LiquidityBoundaryHelper.calculateBuyLimit(pool, liquidityManager, token0isWeth);
|
||||||
|
|
||||||
// Cap the amount to the safe limit
|
// Cap the amount to the safe limit
|
||||||
uint256 safeAmount = amount > maxBuyAmount ? maxBuyAmount : amount;
|
uint256 safeAmount = amount > maxBuyAmount ? maxBuyAmount : amount;
|
||||||
|
|
||||||
// Skip if no liquidity available
|
// Skip if amount is zero
|
||||||
if (safeAmount == 0) return;
|
if (safeAmount == 0) return 0;
|
||||||
|
|
||||||
// For buying HARB with WETH, we're swapping in the direction that increases HARB price
|
// 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
|
// zeroForOne = true if WETH is token0, false if WETH is token1
|
||||||
|
|
@ -59,17 +59,19 @@ contract SwapExecutor {
|
||||||
sqrtPriceLimitX96,
|
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
|
// Calculate maximum safe sell amount based on liquidity
|
||||||
uint256 maxSellAmount = LiquidityBoundaryHelper.calculateSellLimit(pool, liquidityManager, token0isWeth);
|
uint256 maxSellAmount = LiquidityBoundaryHelper.calculateSellLimit(pool, liquidityManager, token0isWeth);
|
||||||
|
|
||||||
// Cap the amount to the safe limit
|
// Cap the amount to the safe limit
|
||||||
uint256 safeAmount = amount > maxSellAmount ? maxSellAmount : amount;
|
uint256 safeAmount = amount > maxSellAmount ? maxSellAmount : amount;
|
||||||
|
|
||||||
// Skip if no liquidity available
|
// Skip if amount is zero
|
||||||
if (safeAmount == 0) return;
|
if (safeAmount == 0) return 0;
|
||||||
|
|
||||||
// For selling HARB for WETH, we're swapping in the direction that decreases HARB price
|
// 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
|
// zeroForOne = false if WETH is token0, true if WETH is token1
|
||||||
|
|
@ -92,6 +94,8 @@ contract SwapExecutor {
|
||||||
sqrtPriceLimitX96,
|
sqrtPriceLimitX96,
|
||||||
""
|
""
|
||||||
);
|
);
|
||||||
|
|
||||||
|
return safeAmount;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Callback required for Uniswap V3 swaps
|
// Callback required for Uniswap V3 swaps
|
||||||
|
|
|
||||||
|
|
@ -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"
|
|
||||||
|
|
@ -1,4 +0,0 @@
|
||||||
pandas>=1.3.0
|
|
||||||
matplotlib>=3.4.0
|
|
||||||
seaborn>=0.11.0
|
|
||||||
numpy>=1.21.0
|
|
||||||
|
|
@ -1,10 +1,11 @@
|
||||||
#!/bin/bash
|
#!/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:
|
# Examples:
|
||||||
# ./run-improved-fuzzing.sh BullMarketOptimizer runs=50
|
# ./run-fuzzing.sh BullMarketOptimizer runs=50
|
||||||
# ./run-improved-fuzzing.sh WhaleOptimizer runs=20 staking=off
|
# ./run-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=200 staking=on buybias=100 trades=30 stakingbias=95
|
||||||
|
# ./run-fuzzing.sh BullMarketOptimizer debugCSV # Opens HTML visualizer after generating CSVs
|
||||||
|
|
||||||
# Colors for output
|
# Colors for output
|
||||||
RED='\033[0;31m'
|
RED='\033[0;31m'
|
||||||
|
|
@ -16,11 +17,23 @@ BOLD='\033[1m'
|
||||||
|
|
||||||
# Configuration
|
# Configuration
|
||||||
OPTIMIZER=${1:-BullMarketOptimizer}
|
OPTIMIZER=${1:-BullMarketOptimizer}
|
||||||
RUNS=${2:-runs=20}
|
|
||||||
STAKING=${3:-staking=on}
|
# Check if second parameter is debugCSV
|
||||||
BUYBIAS=${4:-buybias=50}
|
DEBUG_CSV=false
|
||||||
TRADES=${5:-trades=15}
|
if [[ "$2" == "debugCSV" ]]; then
|
||||||
STAKINGBIAS=${6:-stakingbias=80}
|
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
|
# Parse runs parameter
|
||||||
if [[ $RUNS == runs=* ]]; then
|
if [[ $RUNS == runs=* ]]; then
|
||||||
|
|
@ -56,8 +69,7 @@ if [[ $STAKINGBIAS == stakingbias=* ]]; then
|
||||||
STAKINGBIAS_VALUE=${STAKINGBIAS#stakingbias=}
|
STAKINGBIAS_VALUE=${STAKINGBIAS#stakingbias=}
|
||||||
fi
|
fi
|
||||||
|
|
||||||
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
|
# Ensure we're in the onchain directory (script should be run from there)
|
||||||
OUTPUT_DIR="fuzzing_results_${OPTIMIZER}_${TIMESTAMP}"
|
|
||||||
|
|
||||||
echo -e "${GREEN}=== Fuzzing Analysis ===${NC}"
|
echo -e "${GREEN}=== Fuzzing Analysis ===${NC}"
|
||||||
echo "Optimizer: $OPTIMIZER"
|
echo "Optimizer: $OPTIMIZER"
|
||||||
|
|
@ -68,7 +80,6 @@ if [ "$STAKING_ENABLED" = "true" ]; then
|
||||||
echo "Staking bias: $STAKINGBIAS_VALUE%"
|
echo "Staking bias: $STAKINGBIAS_VALUE%"
|
||||||
fi
|
fi
|
||||||
echo "Buy bias: $BUYBIAS_VALUE%"
|
echo "Buy bias: $BUYBIAS_VALUE%"
|
||||||
echo "Output directory: $OUTPUT_DIR"
|
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
# Validate optimizer
|
# Validate optimizer
|
||||||
|
|
@ -83,80 +94,100 @@ case $OPTIMIZER in
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
# Create output directory
|
# Run the streamlined fuzzing analysis
|
||||||
mkdir -p $OUTPUT_DIR
|
|
||||||
|
|
||||||
# Run the fuzzing analysis
|
|
||||||
echo -e "${YELLOW}Starting fuzzing analysis...${NC}"
|
echo -e "${YELLOW}Starting fuzzing analysis...${NC}"
|
||||||
echo ""
|
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 \
|
FUZZING_RUNS=$RUNS_VALUE \
|
||||||
OPTIMIZER_CLASS=$OPTIMIZER \
|
OPTIMIZER_CLASS=$OPTIMIZER \
|
||||||
ENABLE_STAKING=$STAKING_ENABLED \
|
ENABLE_STAKING=$STAKING_ENABLED \
|
||||||
BUY_BIAS=$BUYBIAS_VALUE \
|
BUY_BIAS=$BUYBIAS_VALUE \
|
||||||
TRADES_PER_RUN=$TRADES_VALUE \
|
TRADES_PER_RUN=$TRADES_VALUE \
|
||||||
STAKING_BIAS=$STAKINGBIAS_VALUE \
|
STAKING_BIAS=$STAKINGBIAS_VALUE \
|
||||||
TRACK_POSITIONS=true \
|
forge script analysis/StreamlinedFuzzing.s.sol:StreamlinedFuzzing --skip-simulation --gas-estimate-multiplier 300 -vv 2>&1
|
||||||
forge script analysis/ImprovedFuzzingAnalysis.s.sol:ImprovedFuzzingAnalysis --gas-limit 200000000 -vv 2>&1 | tee $OUTPUT_DIR/fuzzing.log
|
|
||||||
|
|
||||||
# Extract key metrics
|
# Analysis complete
|
||||||
echo ""
|
echo ""
|
||||||
echo -e "${GREEN}=== ANALYSIS COMPLETE ===${NC}"
|
echo -e "${GREEN}=== ANALYSIS COMPLETE ===${NC}"
|
||||||
|
|
||||||
# Show summary from log
|
# Find newly generated CSV files in analysis folder
|
||||||
tail -20 $OUTPUT_DIR/fuzzing.log | grep -E "Total scenarios|Profitable|Discovery|Stakes|Snatches" || true
|
NEW_CSVS=$(ls -1 analysis/fuzz-????-???.csv 2>/dev/null | sort)
|
||||||
|
# Get the scenario code from the first new file
|
||||||
# Check for position CSVs
|
if [ "$EXISTING_CSVS" != "$NEW_CSVS" ]; then
|
||||||
POSITION_CSV_COUNT=$(ls -1 improved_positions_*.csv 2>/dev/null | wc -l)
|
SCENARIO_CODE=$(echo "$NEW_CSVS" | grep -v -F "$EXISTING_CSVS" | head -1 | sed 's/fuzz-\(....\).*/\1/')
|
||||||
|
|
||||||
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
|
else
|
||||||
echo "No snatching observed in this run"
|
SCENARIO_CODE="UNKN"
|
||||||
fi
|
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
|
# Check for profitable scenarios
|
||||||
PROFITABLE_COUNT=$(grep -c "PROFITABLE!" $OUTPUT_DIR/fuzzing.log 2>/dev/null || true)
|
echo ""
|
||||||
if [ -z "$PROFITABLE_COUNT" ]; then
|
echo -e "${YELLOW}=== Checking for Profitable Scenarios ===${NC}"
|
||||||
PROFITABLE_COUNT="0"
|
PROFITABLE_COUNT=0
|
||||||
fi
|
for csv in analysis/fuzz-${SCENARIO_CODE}-*.csv; do
|
||||||
if [ "$PROFITABLE_COUNT" -gt "0" ]; then
|
if [ -f "$csv" ]; then
|
||||||
echo ""
|
# Get INIT and FINAL rows
|
||||||
echo -e "${GREEN}=== PROFITABLE SCENARIOS FOUND ===${NC}"
|
INIT_ETH=$(grep "^INIT," "$csv" | cut -d',' -f13)
|
||||||
echo "Found $PROFITABLE_COUNT profitable scenarios"
|
FINAL_ETH=$(grep "^FINAL," "$csv" | cut -d',' -f13)
|
||||||
grep "PROFITABLE!" $OUTPUT_DIR/fuzzing.log | head -5
|
|
||||||
|
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
|
fi
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo -e "${GREEN}Full results saved to: $OUTPUT_DIR/${NC}"
|
echo -e "${GREEN}CSV files generated with scenario ID: ${SCENARIO_CODE}${NC}"
|
||||||
echo ""
|
|
||||||
echo "To view detailed logs:"
|
# Launch HTML visualizer if debugCSV mode
|
||||||
echo " cat $OUTPUT_DIR/fuzzing.log"
|
if [ "$DEBUG_CSV" = true ]; then
|
||||||
echo ""
|
echo ""
|
||||||
echo "To visualize position movements (if CSVs generated):"
|
echo -e "${YELLOW}Launching HTML visualizer...${NC}"
|
||||||
echo " ./analysis/view-scenarios.sh $OUTPUT_DIR"
|
|
||||||
|
# Check if Python3 is available
|
||||||
|
if command -v python3 &> /dev/null; then
|
||||||
|
echo "Starting local server from analysis folder at http://localhost:8000"
|
||||||
|
echo "Open http://localhost:8000/run-visualizer.html to view results"
|
||||||
|
# Start server from analysis directory so CSV files are directly accessible
|
||||||
|
cd analysis
|
||||||
|
python3 -m http.server 8000 --bind 127.0.0.1 &
|
||||||
|
SERVER_PID=$!
|
||||||
|
|
||||||
|
# Try to open browser (cross-platform)
|
||||||
|
sleep 1
|
||||||
|
if command -v open &> /dev/null; then
|
||||||
|
# macOS
|
||||||
|
open "http://localhost:8000/run-visualizer.html"
|
||||||
|
elif command -v xdg-open &> /dev/null; then
|
||||||
|
# Linux
|
||||||
|
xdg-open "http://localhost:8000/run-visualizer.html"
|
||||||
|
elif command -v wslview &> /dev/null; then
|
||||||
|
# WSL
|
||||||
|
wslview "http://localhost:8000/run-visualizer.html"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Press Ctrl+C to stop the server"
|
||||||
|
wait $SERVER_PID
|
||||||
|
else
|
||||||
|
echo "Python3 not found. To view results:"
|
||||||
|
echo " cd analysis && python3 -m http.server 8000"
|
||||||
|
echo " Then open http://localhost:8000/run-visualizer.html in your browser"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
@ -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"
|
|
||||||
|
|
@ -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
|
|
||||||
916
onchain/analysis/run-visualizer.html
Normal file
916
onchain/analysis/run-visualizer.html
Normal 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>
|
||||||
|
• < 1x = ETH is cheaper than current price (positions below current hold ETH)<br>
|
||||||
|
• 1x = Current ETH price (red dashed line)<br>
|
||||||
|
• > 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
|
|
@ -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
|
|
||||||
|
|
@ -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!
|
|
||||||
|
|
@ -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"
|
|
||||||
|
|
@ -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,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
|
|
||||||
|
Loading…
Add table
Add a link
Reference in a new issue