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