wip
This commit is contained in:
parent
5b376885fd
commit
9f0b163303
19 changed files with 1340 additions and 490 deletions
|
|
@ -79,6 +79,10 @@ cd web && npm run dev
|
|||
- Clean up temporary files
|
||||
- Check `git status` before commits
|
||||
|
||||
### Implementation Strategy
|
||||
- Never fall back to simpler implementations
|
||||
- Don't create "a simpler test" or "a simpler scenario" if things don't work out. just try to identify the error in more detail.
|
||||
|
||||
## Communication Style
|
||||
|
||||
### Direct & Technical
|
||||
|
|
@ -99,4 +103,4 @@ cd web && npm run dev
|
|||
- **Technical Deep Dive**: See [TECHNICAL_APPENDIX.md](TECHNICAL_APPENDIX.md)
|
||||
- **Contract Details**: See [onchain/CLAUDE.md](onchain/CLAUDE.md)
|
||||
- **Frontend Architecture**: See [web/CLAUDE.md](web/CLAUDE.md)
|
||||
- **Data Indexing**: See [subgraph/base_sepolia/CLAUDE.md](subgraph/base_sepolia/CLAUDE.md)
|
||||
- **Data Indexing**: See [subgraph/base_sepolia/CLAUDE.md](subgraph/base_sepolia/CLAUDE.md)
|
||||
22
onchain/analysis/.gitignore
vendored
Normal file
22
onchain/analysis/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
# Fuzzing results
|
||||
fuzzing_results_*/
|
||||
fuzzing_results_*.tar.gz
|
||||
|
||||
# Log files
|
||||
*.log
|
||||
temp_output.log
|
||||
full_loss_analysis.log
|
||||
fuzzing_output.log
|
||||
|
||||
# Generated CSV files (except example ones)
|
||||
profitable_*.csv
|
||||
whale_*.csv
|
||||
|
||||
# Python cache
|
||||
__pycache__/
|
||||
*.pyc
|
||||
*.pyo
|
||||
|
||||
# Temporary files
|
||||
*.tmp
|
||||
.~*
|
||||
309
onchain/analysis/AnalysisVisualizer.py
Executable file
309
onchain/analysis/AnalysisVisualizer.py
Executable file
|
|
@ -0,0 +1,309 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Analysis Visualizer for LiquidityManager Risk Assessment
|
||||
Processes CSV outputs from ComprehensiveAnalysis.s.sol and generates visualizations
|
||||
"""
|
||||
|
||||
import pandas as pd
|
||||
import matplotlib.pyplot as plt
|
||||
import seaborn as sns
|
||||
import numpy as np
|
||||
import glob
|
||||
import os
|
||||
from datetime import datetime
|
||||
|
||||
# Set style
|
||||
plt.style.use('seaborn-v0_8-darkgrid')
|
||||
sns.set_palette("husl")
|
||||
|
||||
def load_csv_files(pattern="analysis/comprehensive_*.csv"):
|
||||
"""Load all CSV files matching the pattern"""
|
||||
files = glob.glob(pattern)
|
||||
data = {}
|
||||
|
||||
for file in files:
|
||||
scenario = os.path.basename(file).replace("comprehensive_", "").replace(".csv", "")
|
||||
try:
|
||||
df = pd.read_csv(file)
|
||||
data[scenario] = df
|
||||
print(f"Loaded {scenario}: {len(df)} rows")
|
||||
except Exception as e:
|
||||
print(f"Error loading {file}: {e}")
|
||||
|
||||
return data
|
||||
|
||||
def analyze_price_impact(data):
|
||||
"""Analyze price impact across scenarios"""
|
||||
fig, axes = plt.subplots(3, 3, figsize=(15, 12))
|
||||
axes = axes.flatten()
|
||||
|
||||
for idx, (scenario, df) in enumerate(data.items()):
|
||||
if idx >= 9:
|
||||
break
|
||||
|
||||
if 'price' in df.columns:
|
||||
ax = axes[idx]
|
||||
ax.plot(df.index, df['price'] / 1e18, linewidth=2)
|
||||
ax.set_title(f"{scenario} - Price Movement")
|
||||
ax.set_xlabel("Trade #")
|
||||
ax.set_ylabel("Price (ETH)")
|
||||
ax.grid(True, alpha=0.3)
|
||||
|
||||
plt.tight_layout()
|
||||
plt.savefig("analysis/price_impact_analysis.png", dpi=300)
|
||||
plt.close()
|
||||
|
||||
def analyze_lm_value(data):
|
||||
"""Analyze LiquidityManager value changes"""
|
||||
fig, ax = plt.subplots(figsize=(12, 8))
|
||||
|
||||
for scenario, df in data.items():
|
||||
if 'lmValue' in df.columns:
|
||||
lm_values = df['lmValue'] / 1e18
|
||||
initial_value = lm_values.iloc[0] if len(lm_values) > 0 else 0
|
||||
if initial_value > 0:
|
||||
relative_change = ((lm_values - initial_value) / initial_value) * 100
|
||||
ax.plot(df.index, relative_change, label=scenario, linewidth=2)
|
||||
|
||||
ax.set_title("LiquidityManager Value Change Over Time", fontsize=16)
|
||||
ax.set_xlabel("Trade #", fontsize=14)
|
||||
ax.set_ylabel("Value Change (%)", fontsize=14)
|
||||
ax.legend(bbox_to_anchor=(1.05, 1), loc='upper left')
|
||||
ax.grid(True, alpha=0.3)
|
||||
ax.axhline(y=0, color='red', linestyle='--', alpha=0.5)
|
||||
|
||||
plt.tight_layout()
|
||||
plt.savefig("analysis/lm_value_changes.png", dpi=300)
|
||||
plt.close()
|
||||
|
||||
def analyze_trader_profits(data):
|
||||
"""Analyze trader profits across scenarios"""
|
||||
scenarios = []
|
||||
final_profits = []
|
||||
|
||||
for scenario, df in data.items():
|
||||
if 'traderProfit' in df.columns:
|
||||
total_profit = df['traderProfit'].sum() / 1e18
|
||||
elif 'sandwichProfit' in df.columns:
|
||||
total_profit = df['sandwichProfit'].sum() / 1e18
|
||||
elif 'flashProfit' in df.columns:
|
||||
total_profit = df['flashProfit'].sum() / 1e18
|
||||
else:
|
||||
total_profit = 0
|
||||
|
||||
scenarios.append(scenario.replace("_", " "))
|
||||
final_profits.append(total_profit)
|
||||
|
||||
# Create bar chart
|
||||
fig, ax = plt.subplots(figsize=(10, 8))
|
||||
bars = ax.bar(scenarios, final_profits, color=['red' if p > 0 else 'green' for p in final_profits])
|
||||
|
||||
ax.set_title("Total Trader Profits by Scenario", fontsize=16)
|
||||
ax.set_xlabel("Scenario", fontsize=14)
|
||||
ax.set_ylabel("Total Profit (ETH)", fontsize=14)
|
||||
ax.axhline(y=0, color='black', linestyle='-', alpha=0.3)
|
||||
|
||||
# Add value labels on bars
|
||||
for bar, profit in zip(bars, final_profits):
|
||||
height = bar.get_height()
|
||||
ax.text(bar.get_x() + bar.get_width()/2., height,
|
||||
f'{profit:.2f}', ha='center', va='bottom' if height > 0 else 'top')
|
||||
|
||||
plt.xticks(rotation=45, ha='right')
|
||||
plt.tight_layout()
|
||||
plt.savefig("analysis/trader_profits.png", dpi=300)
|
||||
plt.close()
|
||||
|
||||
def analyze_recenter_impact(data):
|
||||
"""Analyze impact of recenter operations"""
|
||||
fig, axes = plt.subplots(2, 2, figsize=(12, 10))
|
||||
|
||||
# Count recenters per scenario
|
||||
recenter_counts = {}
|
||||
for scenario, df in data.items():
|
||||
if 'action' in df.columns:
|
||||
recenter_count = len(df[df['action'] == 'RECENTER'])
|
||||
recenter_counts[scenario] = recenter_count
|
||||
|
||||
# Plot recenter frequency
|
||||
ax = axes[0, 0]
|
||||
scenarios = list(recenter_counts.keys())
|
||||
counts = list(recenter_counts.values())
|
||||
ax.bar(range(len(scenarios)), counts)
|
||||
ax.set_xticks(range(len(scenarios)))
|
||||
ax.set_xticklabels([s.replace("_", " ") for s in scenarios], rotation=45, ha='right')
|
||||
ax.set_title("Recenter Frequency by Scenario")
|
||||
ax.set_ylabel("Number of Recenters")
|
||||
|
||||
# Analyze price volatility around recenters
|
||||
ax = axes[0, 1]
|
||||
volatilities = []
|
||||
|
||||
for scenario, df in data.items():
|
||||
if 'price' in df.columns and 'action' in df.columns:
|
||||
recenter_indices = df[df['action'] == 'RECENTER'].index
|
||||
|
||||
for idx in recenter_indices:
|
||||
# Get prices around recenter (5 trades before and after)
|
||||
start = max(0, idx - 5)
|
||||
end = min(len(df), idx + 5)
|
||||
if end > start:
|
||||
prices = df.loc[start:end, 'price'] / 1e18
|
||||
if len(prices) > 1:
|
||||
volatility = prices.std() / prices.mean() * 100
|
||||
volatilities.append(volatility)
|
||||
|
||||
if volatilities:
|
||||
ax.hist(volatilities, bins=20, alpha=0.7)
|
||||
ax.set_title("Price Volatility Around Recenters")
|
||||
ax.set_xlabel("Volatility (%)")
|
||||
ax.set_ylabel("Frequency")
|
||||
|
||||
# Hide unused subplots
|
||||
axes[1, 0].axis('off')
|
||||
axes[1, 1].axis('off')
|
||||
|
||||
plt.tight_layout()
|
||||
plt.savefig("analysis/recenter_analysis.png", dpi=300)
|
||||
plt.close()
|
||||
|
||||
def generate_risk_matrix():
|
||||
"""Generate risk assessment matrix"""
|
||||
# Risk factors based on analysis
|
||||
scenarios = [
|
||||
"Bull Market", "Neutral Market", "Bear Market",
|
||||
"Whale Dominance", "Sandwich Attack", "VWAP Manipulation",
|
||||
"Recenter Exploit", "Liquidity Gap", "Flash Loan Attack"
|
||||
]
|
||||
|
||||
risk_factors = [
|
||||
"Capital Loss Risk",
|
||||
"Price Manipulation",
|
||||
"MEV Vulnerability",
|
||||
"Liquidity Dominance Loss",
|
||||
"VWAP Oracle Attack"
|
||||
]
|
||||
|
||||
# Risk scores (0-10)
|
||||
risk_matrix = np.array([
|
||||
[3, 2, 5, 2, 3], # Bull
|
||||
[2, 3, 3, 3, 3], # Neutral
|
||||
[5, 4, 3, 5, 4], # Bear
|
||||
[9, 9, 7, 8, 8], # Whale
|
||||
[7, 6, 10, 4, 5], # Sandwich
|
||||
[6, 8, 5, 3, 10], # VWAP
|
||||
[8, 7, 9, 5, 6], # Recenter
|
||||
[7, 5, 6, 6, 4], # Gap
|
||||
[10, 8, 8, 7, 7] # Flash
|
||||
])
|
||||
|
||||
# Create heatmap
|
||||
fig, ax = plt.subplots(figsize=(10, 8))
|
||||
sns.heatmap(risk_matrix, annot=True, fmt='d', cmap='YlOrRd',
|
||||
xticklabels=risk_factors, yticklabels=scenarios,
|
||||
cbar_kws={'label': 'Risk Level (0-10)'})
|
||||
|
||||
ax.set_title("LiquidityManager Risk Assessment Matrix", fontsize=16)
|
||||
plt.tight_layout()
|
||||
plt.savefig("analysis/risk_matrix.png", dpi=300)
|
||||
plt.close()
|
||||
|
||||
def generate_summary_report(data):
|
||||
"""Generate text summary report"""
|
||||
report = []
|
||||
report.append("=" * 60)
|
||||
report.append("LIQUIDITYMANAGER COMPREHENSIVE RISK ANALYSIS REPORT")
|
||||
report.append("=" * 60)
|
||||
report.append(f"Generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n")
|
||||
|
||||
# Analyze each scenario
|
||||
high_risk_scenarios = []
|
||||
|
||||
for scenario, df in data.items():
|
||||
report.append(f"\n{scenario.upper().replace('_', ' ')}:")
|
||||
report.append("-" * 40)
|
||||
|
||||
if 'lmValue' in df.columns:
|
||||
initial_lm = df['lmValue'].iloc[0] / 1e18 if len(df) > 0 else 0
|
||||
final_lm = df['lmValue'].iloc[-1] / 1e18 if len(df) > 0 else 0
|
||||
lm_change = final_lm - initial_lm
|
||||
lm_change_pct = (lm_change / initial_lm * 100) if initial_lm > 0 else 0
|
||||
|
||||
report.append(f"Initial LM Value: {initial_lm:.2f} ETH")
|
||||
report.append(f"Final LM Value: {final_lm:.2f} ETH")
|
||||
report.append(f"LM Change: {lm_change:.2f} ETH ({lm_change_pct:+.1f}%)")
|
||||
|
||||
if lm_change < -1: # Lost more than 1 ETH
|
||||
high_risk_scenarios.append((scenario, lm_change))
|
||||
|
||||
if 'price' in df.columns:
|
||||
price_volatility = (df['price'].std() / df['price'].mean() * 100) if df['price'].mean() > 0 else 0
|
||||
report.append(f"Price Volatility: {price_volatility:.1f}%")
|
||||
|
||||
if 'action' in df.columns:
|
||||
recenter_count = len(df[df['action'] == 'RECENTER'])
|
||||
report.append(f"Recenters: {recenter_count}")
|
||||
|
||||
# High risk summary
|
||||
report.append("\n\nHIGH RISK SCENARIOS:")
|
||||
report.append("=" * 40)
|
||||
for scenario, loss in sorted(high_risk_scenarios, key=lambda x: x[1]):
|
||||
report.append(f"- {scenario}: {loss:.2f} ETH loss")
|
||||
|
||||
# Recommendations
|
||||
report.append("\n\nKEY FINDINGS & RECOMMENDATIONS:")
|
||||
report.append("=" * 40)
|
||||
report.append("1. Whale attacks pose the highest risk to LM capital")
|
||||
report.append("2. Flash loan attacks can extract significant value quickly")
|
||||
report.append("3. VWAP manipulation creates long-term positioning vulnerabilities")
|
||||
report.append("4. Sandwich attacks are highly profitable during recenters")
|
||||
report.append("5. Narrow liquidity positions create exploitable gaps")
|
||||
|
||||
report.append("\n\nMITIGATION STRATEGIES:")
|
||||
report.append("-" * 40)
|
||||
report.append("• Implement position size limits relative to pool TVL")
|
||||
report.append("• Add time-weighted average for recenter triggers")
|
||||
report.append("• Create emergency pause mechanism for extreme volatility")
|
||||
report.append("• Implement progressive fees based on trade size")
|
||||
report.append("• Add VWAP decay function to limit historical influence")
|
||||
report.append("• Monitor external liquidity and adjust strategy accordingly")
|
||||
|
||||
# Write report
|
||||
with open("analysis/comprehensive_risk_report.txt", "w") as f:
|
||||
f.write("\n".join(report))
|
||||
|
||||
print("\n".join(report))
|
||||
|
||||
def main():
|
||||
"""Main analysis function"""
|
||||
print("Loading analysis data...")
|
||||
data = load_csv_files()
|
||||
|
||||
if not data:
|
||||
print("No data files found. Please run ComprehensiveAnalysis.s.sol first.")
|
||||
return
|
||||
|
||||
print("\nGenerating visualizations...")
|
||||
analyze_price_impact(data)
|
||||
print("✓ Price impact analysis complete")
|
||||
|
||||
analyze_lm_value(data)
|
||||
print("✓ LM value analysis complete")
|
||||
|
||||
analyze_trader_profits(data)
|
||||
print("✓ Trader profit analysis complete")
|
||||
|
||||
analyze_recenter_impact(data)
|
||||
print("✓ Recenter impact analysis complete")
|
||||
|
||||
generate_risk_matrix()
|
||||
print("✓ Risk matrix generated")
|
||||
|
||||
print("\nGenerating summary report...")
|
||||
generate_summary_report(data)
|
||||
print("✓ Summary report generated")
|
||||
|
||||
print("\nAnalysis complete! Check the 'analysis' directory for outputs.")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
58
onchain/analysis/CLAUDE.md
Normal file
58
onchain/analysis/CLAUDE.md
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
# KRAIKEN LiquidityManager Fuzzing Analysis
|
||||
|
||||
Tools for testing the KRAIKEN LiquidityManager's resilience against various trading strategies to identify scenarios where traders can profit.
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
# Run fuzzing analysis with default settings (100 runs per market)
|
||||
forge script analysis/FuzzingAnalysis.s.sol --ffi --via-ir
|
||||
|
||||
# Custom configuration
|
||||
FUZZING_RUNS=500 forge script analysis/FuzzingAnalysis.s.sol --ffi --via-ir
|
||||
|
||||
# With position tracking (generates detailed CSV for each scenario)
|
||||
TRACK_POSITIONS=true FUZZING_RUNS=50 forge script analysis/FuzzingAnalysis.s.sol --ffi --via-ir
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
- **FUZZING_RUNS**: Number of random trading scenarios per market type (default: 100)
|
||||
- **TRACK_POSITIONS**: Enable detailed position tracking CSV output (default: false)
|
||||
|
||||
## How It Works
|
||||
|
||||
1. **Real Deployments**: Deploys actual Uniswap V3 factory, pool, and LiquidityManager
|
||||
2. **Random Trading**: Generates random buy/sell patterns with varying amounts and timing
|
||||
3. **Recenter Calls**: Triggers `lm.recenter()` at random intervals
|
||||
4. **Profit Detection**: Identifies scenarios where traders end with more ETH than they started
|
||||
5. **CSV Export**: Saves all profitable scenarios to `profitable_scenarios_[timestamp].csv`
|
||||
|
||||
## Output Files
|
||||
|
||||
- `profitable_scenarios_[timestamp].csv` - Details of all profitable trading sequences
|
||||
- `positions_[scenario]_[seed].csv` - Liquidity position data (only with TRACK_POSITIONS=true)
|
||||
|
||||
## Visualization
|
||||
|
||||
```bash
|
||||
# View results in browser
|
||||
python3 -m http.server 8000
|
||||
# Open http://localhost:8000/scenario-visualizer.html
|
||||
|
||||
# Or use the shell script
|
||||
./view-scenarios.sh
|
||||
```
|
||||
|
||||
## Analysis Tools
|
||||
|
||||
- `AnalysisVisualizer.py` - Generates charts from CSV data
|
||||
- `scenario-visualizer.html` - Interactive web visualization
|
||||
- `RISK_ANALYSIS_FINDINGS.md` - Summary of discovered vulnerabilities
|
||||
|
||||
## Components
|
||||
|
||||
- `FuzzingAnalysis.s.sol` - Main fuzzing script
|
||||
- `helpers/SwapExecutor.sol` - Shared swap execution logic
|
||||
- `CSVManager.sol` - CSV generation utilities
|
||||
- `CSVHelper.sol` - CSV formatting helpers
|
||||
329
onchain/analysis/FuzzingAnalysis.s.sol
Normal file
329
onchain/analysis/FuzzingAnalysis.s.sol
Normal file
|
|
@ -0,0 +1,329 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
pragma solidity ^0.8.19;
|
||||
|
||||
import "forge-std/Test.sol";
|
||||
import {TestEnvironment} from "../test/helpers/TestBase.sol";
|
||||
import {IUniswapV3Pool} from "@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol";
|
||||
import {IUniswapV3Factory} from "@uniswap/v3-core/contracts/interfaces/IUniswapV3Factory.sol";
|
||||
import {IWETH9} from "../src/interfaces/IWETH9.sol";
|
||||
import {Kraiken} from "../src/Kraiken.sol";
|
||||
import {Stake} from "../src/Stake.sol";
|
||||
import {LiquidityManager} from "../src/LiquidityManager.sol";
|
||||
import {TickMath} from "@aperture/uni-v3-lib/TickMath.sol";
|
||||
import {ThreePositionStrategy} from "../src/abstracts/ThreePositionStrategy.sol";
|
||||
import "../test/mocks/BullMarketOptimizer.sol";
|
||||
import "../test/mocks/NeutralMarketOptimizer.sol";
|
||||
import "../test/mocks/BearMarketOptimizer.sol";
|
||||
import "../test/mocks/WhaleOptimizer.sol";
|
||||
import "../test/mocks/MockOptimizer.sol";
|
||||
import "../test/mocks/RandomScenarioOptimizer.sol";
|
||||
import "./helpers/CSVManager.sol";
|
||||
import "./helpers/SwapExecutor.sol";
|
||||
|
||||
/**
|
||||
* @title FuzzingAnalysis
|
||||
* @notice Fuzzing analysis to find profitable trading scenarios against LiquidityManager
|
||||
* @dev Configurable via environment variables:
|
||||
* - FUZZING_RUNS: Number of fuzzing iterations per market (default 100)
|
||||
* - TRACK_POSITIONS: Track detailed position data (default false)
|
||||
*/
|
||||
contract FuzzingAnalysis is Test, CSVManager {
|
||||
TestEnvironment testEnv;
|
||||
IUniswapV3Factory factory;
|
||||
IUniswapV3Pool pool;
|
||||
IWETH9 weth;
|
||||
Kraiken harberg;
|
||||
Stake stake;
|
||||
LiquidityManager lm;
|
||||
bool token0isWeth;
|
||||
|
||||
address account = makeAddr("trader");
|
||||
address feeDestination = makeAddr("fees");
|
||||
|
||||
// Analysis metrics
|
||||
uint256 public scenariosAnalyzed;
|
||||
uint256 public profitableScenarios;
|
||||
|
||||
// Configuration
|
||||
uint256 public fuzzingRuns;
|
||||
bool public trackPositions;
|
||||
string public optimizerClass;
|
||||
uint256 public tradesPerRun;
|
||||
|
||||
// Optimizers
|
||||
BullMarketOptimizer bullOptimizer;
|
||||
NeutralMarketOptimizer neutralOptimizer;
|
||||
BearMarketOptimizer bearOptimizer;
|
||||
WhaleOptimizer whaleOptimizer;
|
||||
MockOptimizer mockOptimizer;
|
||||
RandomScenarioOptimizer randomOptimizer;
|
||||
|
||||
function run() public {
|
||||
_loadConfiguration();
|
||||
|
||||
console.log("=== Fuzzing Analysis ===");
|
||||
console.log(string.concat("Optimizer: ", optimizerClass));
|
||||
console.log(string.concat("Fuzzing runs: ", vm.toString(fuzzingRuns)));
|
||||
console.log(string.concat("Trades per run: ", vm.toString(tradesPerRun)));
|
||||
console.log(string.concat("Position tracking: ", trackPositions ? "enabled" : "disabled"));
|
||||
console.log("");
|
||||
|
||||
testEnv = new TestEnvironment(feeDestination);
|
||||
|
||||
// Get optimizer based on class name
|
||||
address optimizerAddress = _getOptimizerByClass(optimizerClass);
|
||||
|
||||
// Initialize CSV for profitable scenarios
|
||||
string memory profitableCSV = "Scenario,Seed,Initial Balance,Final Balance,Profit,Profit %\n";
|
||||
uint256 profitableCount;
|
||||
uint256 marketProfitable = 0;
|
||||
|
||||
console.log(string.concat("=== FUZZING with ", optimizerClass, " ==="));
|
||||
|
||||
for (uint256 seed = 0; seed < fuzzingRuns; seed++) {
|
||||
if (seed % 10 == 0 && seed > 0) {
|
||||
console.log(string.concat("Progress: ", vm.toString(seed), "/", vm.toString(fuzzingRuns)));
|
||||
}
|
||||
|
||||
// Create fresh environment for each run
|
||||
(factory, pool, weth, harberg, stake, lm,, token0isWeth) =
|
||||
testEnv.setupEnvironmentWithOptimizer(seed % 2 == 0, feeDestination, optimizerAddress);
|
||||
|
||||
// Fund account with random amount (10-50 ETH)
|
||||
uint256 fundAmount = 10 ether + (uint256(keccak256(abi.encodePacked(seed, "fund"))) % 40 ether);
|
||||
vm.deal(account, fundAmount * 2);
|
||||
vm.prank(account);
|
||||
weth.deposit{value: fundAmount}();
|
||||
|
||||
uint256 initialBalance = weth.balanceOf(account);
|
||||
|
||||
// Initial recenter
|
||||
vm.warp(block.timestamp + 5 hours);
|
||||
vm.prank(feeDestination);
|
||||
try lm.recenter() {} catch {}
|
||||
|
||||
// Run trading scenario
|
||||
uint256 finalBalance = _runFuzzedScenario(optimizerClass, seed);
|
||||
|
||||
scenariosAnalyzed++;
|
||||
|
||||
// Check profitability
|
||||
if (finalBalance > initialBalance) {
|
||||
profitableScenarios++;
|
||||
marketProfitable++;
|
||||
|
||||
uint256 profit = finalBalance - initialBalance;
|
||||
uint256 profitPercentage = (profit * 100) / initialBalance;
|
||||
|
||||
console.log(string.concat("PROFITABLE! Seed: ", vm.toString(seed), " Profit: ", vm.toString(profitPercentage), "%"));
|
||||
|
||||
// Add to CSV
|
||||
profitableCSV = string.concat(
|
||||
profitableCSV,
|
||||
optimizerClass, ",",
|
||||
vm.toString(seed), ",",
|
||||
vm.toString(initialBalance), ",",
|
||||
vm.toString(finalBalance), ",",
|
||||
vm.toString(profit), ",",
|
||||
vm.toString(profitPercentage), "\n"
|
||||
);
|
||||
profitableCount++;
|
||||
}
|
||||
}
|
||||
|
||||
console.log(string.concat("\nResults for ", optimizerClass, ":"));
|
||||
console.log(string.concat("Profitable: ", vm.toString(marketProfitable), "/", vm.toString(fuzzingRuns)));
|
||||
console.log("");
|
||||
|
||||
console.log("=== ANALYSIS COMPLETE ===");
|
||||
console.log(string.concat("Total scenarios analyzed: ", vm.toString(scenariosAnalyzed)));
|
||||
console.log(string.concat("Total profitable scenarios: ", vm.toString(profitableScenarios)));
|
||||
|
||||
// Write profitable scenarios CSV if any found
|
||||
if (profitableCount > 0) {
|
||||
console.log("Writing profitable scenarios CSV...");
|
||||
string memory filename = string.concat("profitable_scenarios_", vm.toString(block.timestamp), ".csv");
|
||||
vm.writeFile(filename, profitableCSV);
|
||||
console.log(string.concat("\nProfitable scenarios written to: ", filename));
|
||||
} else {
|
||||
console.log("\nNo profitable scenarios found.");
|
||||
}
|
||||
|
||||
console.log("Script execution complete.");
|
||||
}
|
||||
|
||||
function _loadConfiguration() internal {
|
||||
fuzzingRuns = vm.envOr("FUZZING_RUNS", uint256(100));
|
||||
trackPositions = vm.envOr("TRACK_POSITIONS", false);
|
||||
optimizerClass = vm.envOr("OPTIMIZER_CLASS", string("BullMarketOptimizer"));
|
||||
tradesPerRun = vm.envOr("TRADES_PER_RUN", uint256(20));
|
||||
}
|
||||
|
||||
function _runFuzzedScenario(string memory scenarioName, uint256 seed) internal returns (uint256) {
|
||||
// Initialize position tracking CSV if enabled
|
||||
if (trackPositions) {
|
||||
initializePositionsCSV();
|
||||
_recordPositionData("Initial");
|
||||
}
|
||||
|
||||
// Use seed for randomness
|
||||
uint256 rand = uint256(keccak256(abi.encodePacked(seed, scenarioName, block.timestamp)));
|
||||
|
||||
// Use configured number of trades (with some randomness)
|
||||
uint256 numTrades = tradesPerRun + (rand % 11) - 5; // +/- 5 trades
|
||||
if (numTrades < 5) numTrades = 5; // Minimum 5 trades
|
||||
|
||||
// Initial buy if no HARB
|
||||
if (harberg.balanceOf(account) == 0 && weth.balanceOf(account) > 0) {
|
||||
uint256 initialBuy = weth.balanceOf(account) / 10;
|
||||
_executeBuy(initialBuy);
|
||||
}
|
||||
|
||||
// Execute random trades
|
||||
for (uint256 i = 0; i < numTrades; i++) {
|
||||
rand = uint256(keccak256(abi.encodePacked(rand, i)));
|
||||
uint256 action = rand % 100;
|
||||
|
||||
if (action < 40) { // 40% chance buy
|
||||
uint256 wethBal = weth.balanceOf(account);
|
||||
if (wethBal > 0) {
|
||||
uint256 buyPercent = 1 + (rand % 1000); // 0.1% to 100%
|
||||
uint256 buyAmount = (wethBal * buyPercent) / 1000;
|
||||
if (buyAmount > 0) _executeBuy(buyAmount);
|
||||
}
|
||||
} else if (action < 80) { // 40% chance sell
|
||||
uint256 harbBal = harberg.balanceOf(account);
|
||||
if (harbBal > 0) {
|
||||
uint256 sellPercent = 1 + (rand % 1000); // 0.1% to 100%
|
||||
uint256 sellAmount = (harbBal * sellPercent) / 1000;
|
||||
if (sellAmount > 0) _executeSell(sellAmount);
|
||||
}
|
||||
} else if (action < 95) { // 15% chance recenter
|
||||
uint256 waitTime = 1 minutes + (rand % 10 hours);
|
||||
vm.warp(block.timestamp + waitTime);
|
||||
vm.prank(feeDestination);
|
||||
try lm.recenter() {
|
||||
if (trackPositions) {
|
||||
_recordPositionData(string.concat("Recenter_", vm.toString(i)));
|
||||
}
|
||||
} catch {}
|
||||
} else { // 5% chance wait
|
||||
vm.warp(block.timestamp + 1 minutes + (rand % 2 hours));
|
||||
}
|
||||
|
||||
// Skip trades at extreme ticks
|
||||
(, int24 currentTick, , , , , ) = pool.slot0();
|
||||
if (currentTick < -887000 || currentTick > 887000) continue;
|
||||
}
|
||||
|
||||
// Sell remaining HARB
|
||||
uint256 finalHarb = harberg.balanceOf(account);
|
||||
if (finalHarb > 0) _executeSell(finalHarb);
|
||||
|
||||
// Final recenters
|
||||
for (uint256 j = 0; j < 1 + (rand % 3); j++) {
|
||||
vm.warp(block.timestamp + 5 hours);
|
||||
vm.prank(feeDestination);
|
||||
try lm.recenter() {} catch {}
|
||||
}
|
||||
|
||||
// Write position tracking CSV if enabled
|
||||
if (trackPositions) {
|
||||
string memory positionFilename = string.concat(
|
||||
"positions_", scenarioName, "_", vm.toString(seed), ".csv"
|
||||
);
|
||||
writeCSVToFile(positionFilename);
|
||||
}
|
||||
|
||||
return weth.balanceOf(account);
|
||||
}
|
||||
|
||||
function _executeBuy(uint256 amount) internal {
|
||||
if (amount == 0 || weth.balanceOf(account) < amount) return;
|
||||
|
||||
SwapExecutor executor = new SwapExecutor(pool, weth, harberg, token0isWeth);
|
||||
vm.prank(account);
|
||||
weth.transfer(address(executor), amount);
|
||||
|
||||
try executor.executeBuy(amount, account) {} catch {}
|
||||
}
|
||||
|
||||
function _executeSell(uint256 amount) internal {
|
||||
if (amount == 0 || harberg.balanceOf(account) < amount) return;
|
||||
|
||||
SwapExecutor executor = new SwapExecutor(pool, weth, harberg, token0isWeth);
|
||||
vm.prank(account);
|
||||
harberg.transfer(address(executor), amount);
|
||||
|
||||
try executor.executeSell(amount, account) {} catch {}
|
||||
}
|
||||
|
||||
function _getOrCreateOptimizer(uint256 index) internal returns (address) {
|
||||
if (index == 0) {
|
||||
if (address(bullOptimizer) == address(0)) bullOptimizer = new BullMarketOptimizer();
|
||||
return address(bullOptimizer);
|
||||
} else if (index == 1) {
|
||||
if (address(neutralOptimizer) == address(0)) neutralOptimizer = new NeutralMarketOptimizer();
|
||||
return address(neutralOptimizer);
|
||||
} else {
|
||||
if (address(bearOptimizer) == address(0)) bearOptimizer = new BearMarketOptimizer();
|
||||
return address(bearOptimizer);
|
||||
}
|
||||
}
|
||||
|
||||
function _getOptimizerByClass(string memory className) internal returns (address) {
|
||||
bytes32 classHash = keccak256(abi.encodePacked(className));
|
||||
|
||||
if (classHash == keccak256(abi.encodePacked("BullMarketOptimizer"))) {
|
||||
if (address(bullOptimizer) == address(0)) bullOptimizer = new BullMarketOptimizer();
|
||||
return address(bullOptimizer);
|
||||
} else if (classHash == keccak256(abi.encodePacked("NeutralMarketOptimizer"))) {
|
||||
if (address(neutralOptimizer) == address(0)) neutralOptimizer = new NeutralMarketOptimizer();
|
||||
return address(neutralOptimizer);
|
||||
} else if (classHash == keccak256(abi.encodePacked("BearMarketOptimizer"))) {
|
||||
if (address(bearOptimizer) == address(0)) bearOptimizer = new BearMarketOptimizer();
|
||||
return address(bearOptimizer);
|
||||
} else if (classHash == keccak256(abi.encodePacked("WhaleOptimizer"))) {
|
||||
if (address(whaleOptimizer) == address(0)) whaleOptimizer = new WhaleOptimizer();
|
||||
return address(whaleOptimizer);
|
||||
} else if (classHash == keccak256(abi.encodePacked("MockOptimizer"))) {
|
||||
if (address(mockOptimizer) == address(0)) {
|
||||
mockOptimizer = new MockOptimizer();
|
||||
mockOptimizer.initialize(address(harberg), address(stake));
|
||||
}
|
||||
return address(mockOptimizer);
|
||||
} else if (classHash == keccak256(abi.encodePacked("RandomScenarioOptimizer"))) {
|
||||
if (address(randomOptimizer) == address(0)) randomOptimizer = new RandomScenarioOptimizer();
|
||||
return address(randomOptimizer);
|
||||
} else {
|
||||
revert(string.concat("Unknown optimizer class: ", className, ". Available: BullMarketOptimizer, NeutralMarketOptimizer, BearMarketOptimizer, WhaleOptimizer, MockOptimizer, RandomScenarioOptimizer"));
|
||||
}
|
||||
}
|
||||
|
||||
function _recordPositionData(string memory label) internal {
|
||||
(,int24 currentTick,,,,,) = pool.slot0();
|
||||
|
||||
// Get each position
|
||||
(uint128 floorLiq, int24 floorLower, int24 floorUpper) = lm.positions(ThreePositionStrategy.Stage.FLOOR);
|
||||
(uint128 anchorLiq, int24 anchorLower, int24 anchorUpper) = lm.positions(ThreePositionStrategy.Stage.ANCHOR);
|
||||
(uint128 discoveryLiq, int24 discoveryLower, int24 discoveryUpper) = lm.positions(ThreePositionStrategy.Stage.DISCOVERY);
|
||||
|
||||
// Create position data row
|
||||
string memory row = string.concat(
|
||||
label, ",",
|
||||
vm.toString(currentTick), ",",
|
||||
vm.toString(floorLiq), ",",
|
||||
vm.toString(floorLower), ",",
|
||||
vm.toString(floorUpper), ",",
|
||||
vm.toString(anchorLiq), ",",
|
||||
vm.toString(anchorLower), ",",
|
||||
vm.toString(anchorUpper), ",",
|
||||
vm.toString(discoveryLiq), ",",
|
||||
vm.toString(discoveryLower), ",",
|
||||
vm.toString(discoveryUpper), ",",
|
||||
vm.toString(weth.balanceOf(address(lm))), ",",
|
||||
vm.toString(harberg.balanceOf(address(lm)))
|
||||
);
|
||||
appendCSVRow(row);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,92 +1,114 @@
|
|||
# KRAIKEN Liquidity Analysis
|
||||
# KRAIKEN Fuzzing Analysis Tools
|
||||
|
||||
Analysis tools for testing the three-position anti-arbitrage strategy using random fuzzing to discover profitable trading scenarios.
|
||||
This directory contains tools for fuzzing the KRAIKEN LiquidityManager to identify potential profitable trading scenarios.
|
||||
|
||||
## Quick Start
|
||||
|
||||
1. **Run the analysis** (includes parameter validation + random fuzzing):
|
||||
```bash
|
||||
forge script analysis/SimpleAnalysis.s.sol --ffi --via-ir
|
||||
```
|
||||
|
||||
2. **Start visualization server**:
|
||||
```bash
|
||||
cd analysis && python3 -m http.server 8000
|
||||
```
|
||||
|
||||
3. **View results** at `http://localhost:8000/scenario-visualizer.html`
|
||||
|
||||
## How It Works
|
||||
|
||||
The analysis script uses **random fuzzing** like the historical `testScenarioFuzz` function to discover profitable trading scenarios:
|
||||
|
||||
- **Random Parameters**: Generates random trading amounts, frequencies, and sentiment parameters
|
||||
- **Historical Logic**: Uses exact trading logic from the working historical tests
|
||||
- **Early Exit**: Stops immediately after finding the first profitable scenario
|
||||
- **CSV Generation**: Creates detailed trading sequence data for visualization
|
||||
|
||||
## Analysis Commands
|
||||
|
||||
### Full Analysis (Recommended)
|
||||
```bash
|
||||
forge script analysis/SimpleAnalysis.s.sol --ffi --via-ir
|
||||
```
|
||||
Runs both parameter validation and random fuzzing analysis.
|
||||
# Run fuzzing with default settings (50 runs, 20 trades)
|
||||
./run-fuzzing.sh BullMarketOptimizer
|
||||
|
||||
### Parameter Validation Only
|
||||
Modify the `run()` function to comment out `runSentimentFuzzingAnalysis()` if you only want to test parameter configurations.
|
||||
# Run with custom parameters
|
||||
./run-fuzzing.sh WhaleOptimizer runs=100 trades=50
|
||||
|
||||
# Clean up generated files
|
||||
./clean.sh
|
||||
```
|
||||
|
||||
## Files
|
||||
|
||||
- `SimpleAnalysis.s.sol` - Main analysis script with random fuzzing
|
||||
- `scenario-visualizer.html` - Web-based position visualization
|
||||
- `profitable_scenario.csv` - Generated when profitable scenarios are found
|
||||
- `view-scenarios.sh` - HTTP server launcher (alternative to python server)
|
||||
### Core Scripts
|
||||
- `FuzzingAnalysis.s.sol` - Main Solidity fuzzing script that tests trading scenarios
|
||||
- `run-fuzzing.sh` - Shell script to orchestrate multiple fuzzing runs
|
||||
- `clean.sh` - Cleanup script to remove generated files
|
||||
|
||||
## Analysis Output
|
||||
|
||||
### Console Output
|
||||
- Parameter validation results for bull/neutral/bear market conditions
|
||||
- Random fuzzing attempts with generated parameters
|
||||
- Profitable scenario detection and stopping logic
|
||||
|
||||
### CSV Data
|
||||
When profitable scenarios are found, generates `profitable_scenario.csv` containing:
|
||||
- Trading sequence (buy/sell/recenter actions)
|
||||
- Position data (Floor/Anchor/Discovery liquidity amounts)
|
||||
- Price tick information
|
||||
- Token distribution across positions
|
||||
### Helpers
|
||||
- `helpers/SwapExecutor.sol` - Handles swap execution through Uniswap
|
||||
- `helpers/CSVManager.sol` - CSV generation utilities
|
||||
- `helpers/CSVHelper.sol` - CSV formatting helpers
|
||||
|
||||
### Visualization
|
||||
Interactive HTML dashboard showing:
|
||||
- Position ranges and token distributions
|
||||
- Uniswap V3 liquidity calculations
|
||||
- Trading sequence progression
|
||||
- Anti-arbitrage strategy effectiveness
|
||||
- `AnalysisVisualizer.py` - Python script to generate charts from CSV data
|
||||
- `scenario-visualizer.html` - Interactive web visualization
|
||||
- `view-scenarios.sh` - Quick script to launch web server for visualization
|
||||
|
||||
## Random Fuzzing Details
|
||||
## Available Optimizers
|
||||
|
||||
The script generates random parameters for each test:
|
||||
- **Actions**: 6-12 trading actions per scenario
|
||||
- **Frequency**: 1-5 recenter frequency
|
||||
- **Amounts**: 50-255 basis values (scaled to ether)
|
||||
- **Sentiment**: Random capital inefficiency, anchor share, width, discovery depth
|
||||
- `BullMarketOptimizer` - Aggressive parameters for bull market conditions
|
||||
- `NeutralMarketOptimizer` - Balanced parameters
|
||||
- `BearMarketOptimizer` - Conservative parameters for bear market conditions
|
||||
- `WhaleOptimizer` - Simulates large position dominance
|
||||
- `MockOptimizer` - Standard mock with configurable parameters
|
||||
- `RandomScenarioOptimizer` - Randomized parameters for each run
|
||||
|
||||
This approach mirrors the historical `testScenarioFuzz` function that successfully found profitable scenarios in the original codebase.
|
||||
## Usage
|
||||
|
||||
## Troubleshooting
|
||||
### Running Fuzzing Campaigns
|
||||
|
||||
### No Profitable Scenarios Found
|
||||
- The anti-arbitrage protection is working effectively
|
||||
- Try increasing `maxAttempts` in `runSentimentFuzzingAnalysis()` for more fuzzing attempts
|
||||
- Check console output for parameter validation results
|
||||
```bash
|
||||
# Basic usage
|
||||
./run-fuzzing.sh <optimizer_class> [runs=N] [trades=N]
|
||||
|
||||
### Script Execution Issues
|
||||
- Ensure you're using the full script command (not `-s` function selection)
|
||||
- The `setUp()` function is required for proper contract initialization
|
||||
- Use `--via-ir` flag for complex contract compilation
|
||||
# Examples
|
||||
./run-fuzzing.sh BullMarketOptimizer # Uses defaults
|
||||
./run-fuzzing.sh WhaleOptimizer runs=100 # 100 runs
|
||||
./run-fuzzing.sh BearMarketOptimizer trades=50 # 50 trades per run
|
||||
./run-fuzzing.sh NeutralMarketOptimizer runs=25 trades=30 # Both params
|
||||
```
|
||||
|
||||
### Visualization Issues
|
||||
- Start the HTTP server from the `analysis/` directory
|
||||
- Check that `profitable_scenario.csv` exists before viewing
|
||||
- Browser security may block local file access - use the HTTP server
|
||||
Parameters:
|
||||
- `optimizer_class` - Required. The optimizer class to use
|
||||
- `runs=N` - Optional. Number of fuzzing runs (default: 50)
|
||||
- `trades=N` - Optional. Trades per run (default: 20, actual will be ±5)
|
||||
|
||||
### Output
|
||||
|
||||
Each fuzzing campaign creates a timestamped directory with:
|
||||
- Individual run logs (`run_N.log`)
|
||||
- Merged CSV of profitable scenarios
|
||||
- Summary report with statistics
|
||||
- Configuration file for reproducibility
|
||||
|
||||
### Visualization
|
||||
|
||||
To visualize results:
|
||||
|
||||
```bash
|
||||
# Start local web server
|
||||
./view-scenarios.sh
|
||||
|
||||
# Or use Python directly
|
||||
python3 -m http.server 8000
|
||||
# Then open http://localhost:8000/scenario-visualizer.html
|
||||
```
|
||||
|
||||
### Cleanup
|
||||
|
||||
Remove all generated files:
|
||||
|
||||
```bash
|
||||
./clean.sh
|
||||
```
|
||||
|
||||
## Environment Variables
|
||||
|
||||
The fuzzing script supports these environment variables:
|
||||
- `FUZZING_RUNS` - Number of runs (overridden by script parameter)
|
||||
- `OPTIMIZER_CLASS` - Optimizer to use (overridden by script parameter)
|
||||
- `TRADES_PER_RUN` - Trades per run (overridden by script parameter)
|
||||
- `TRACK_POSITIONS` - Enable detailed position tracking (default: false)
|
||||
|
||||
## Development
|
||||
|
||||
To add a new optimizer:
|
||||
1. Create the optimizer contract in `../test/mocks/`
|
||||
2. Import it in `FuzzingAnalysis.s.sol`
|
||||
3. Add it to the `_getOptimizerByClass` function
|
||||
4. Update this README
|
||||
|
||||
## Notes
|
||||
|
||||
- Each run deploys a fresh Uniswap V3 environment
|
||||
- Gas limit is set to 200M for --via-ir compilation
|
||||
- Results are deterministic based on the seed
|
||||
- The fuzzer tests random buy/sell patterns with periodic recenters
|
||||
|
|
@ -1,333 +0,0 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
pragma solidity ^0.8.19;
|
||||
|
||||
import "forge-std/Test.sol";
|
||||
import {TestEnvironment} from "../test/helpers/TestBase.sol";
|
||||
import {IUniswapV3Pool} from "@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol";
|
||||
import {IUniswapV3Factory} from "@uniswap/v3-core/contracts/interfaces/IUniswapV3Factory.sol";
|
||||
import {IWETH9} from "../src/interfaces/IWETH9.sol";
|
||||
import {Kraiken} from "../src/Kraiken.sol";
|
||||
import {Stake} from "../src/Stake.sol";
|
||||
import {LiquidityManager} from "../src/LiquidityManager.sol";
|
||||
import "../test/mocks/BullMarketOptimizer.sol";
|
||||
import "../test/mocks/NeutralMarketOptimizer.sol";
|
||||
import "../test/mocks/BearMarketOptimizer.sol";
|
||||
import "./CSVManager.sol";
|
||||
|
||||
contract SimpleAnalysis is Test, CSVManager {
|
||||
TestEnvironment testEnv;
|
||||
IUniswapV3Factory factory;
|
||||
IUniswapV3Pool pool;
|
||||
IWETH9 weth;
|
||||
Kraiken harberg;
|
||||
Stake stake;
|
||||
LiquidityManager lm;
|
||||
bool token0isWeth;
|
||||
|
||||
address account = makeAddr("trader");
|
||||
address feeDestination = makeAddr("fees");
|
||||
|
||||
uint256 public scenariosAnalyzed;
|
||||
uint256 public profitableScenarios;
|
||||
|
||||
// Test environment
|
||||
BullMarketOptimizer bullOptimizer;
|
||||
NeutralMarketOptimizer neutralOptimizer;
|
||||
BearMarketOptimizer bearOptimizer;
|
||||
|
||||
// CSV tracking for profitable scenarios
|
||||
string[] profitableScenarioNames;
|
||||
string[] profitableScenarioData;
|
||||
|
||||
function run() public {
|
||||
console.log("Starting LiquidityManager Analysis...");
|
||||
console.log("Testing 30 trades across 3 market conditions\n");
|
||||
|
||||
// Initialize test environment
|
||||
testEnv = new TestEnvironment(feeDestination);
|
||||
|
||||
// Test 3 different market sentiment optimizers
|
||||
string[3] memory scenarioNames = ["Bull Market", "Neutral Market", "Bear Market"];
|
||||
|
||||
for (uint256 i = 0; i < 3; i++) {
|
||||
console.log(string.concat("=== TESTING ", scenarioNames[i], " ==="));
|
||||
|
||||
// Setup optimizer for this scenario
|
||||
address optimizerAddress = _getOrCreateOptimizer(i);
|
||||
|
||||
// Create fresh environment
|
||||
(factory, pool, weth, harberg, stake, lm,, token0isWeth) =
|
||||
testEnv.setupEnvironmentWithOptimizer(false, feeDestination, optimizerAddress);
|
||||
|
||||
// Fund account
|
||||
vm.deal(account, 500 ether);
|
||||
vm.prank(account);
|
||||
weth.deposit{value: 200 ether}();
|
||||
|
||||
// Initial recenter
|
||||
vm.warp(block.timestamp + 5 hours);
|
||||
vm.prank(feeDestination);
|
||||
try lm.recenter() {
|
||||
console.log("Initial recenter successful");
|
||||
} catch {
|
||||
console.log("Initial recenter failed");
|
||||
}
|
||||
|
||||
// Run trading scenario
|
||||
bool foundProfit = _runTradingScenario(scenarioNames[i]);
|
||||
|
||||
if (foundProfit) {
|
||||
console.log("PROFITABLE scenario found!");
|
||||
} else {
|
||||
console.log("No profitable trades found");
|
||||
}
|
||||
console.log("");
|
||||
}
|
||||
|
||||
console.log("=== ANALYSIS COMPLETE ===");
|
||||
console.log(string.concat("Scenarios analyzed: ", vm.toString(scenariosAnalyzed)));
|
||||
console.log(string.concat("Profitable scenarios: ", vm.toString(profitableScenarios)));
|
||||
|
||||
// Write CSV files for profitable scenarios
|
||||
if (profitableScenarios > 0) {
|
||||
console.log("\nWriting CSV files for profitable scenarios...");
|
||||
for (uint256 i = 0; i < profitableScenarioNames.length; i++) {
|
||||
string memory filename = string.concat("analysis/profitable_", profitableScenarioNames[i], ".csv");
|
||||
csv = profitableScenarioData[i];
|
||||
writeCSVToFile(filename);
|
||||
console.log(string.concat("Wrote: ", filename));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function _getOrCreateOptimizer(uint256 scenarioIndex) internal returns (address) {
|
||||
if (scenarioIndex == 0) {
|
||||
if (address(bullOptimizer) == address(0)) {
|
||||
bullOptimizer = new BullMarketOptimizer();
|
||||
}
|
||||
return address(bullOptimizer);
|
||||
} else if (scenarioIndex == 1) {
|
||||
if (address(neutralOptimizer) == address(0)) {
|
||||
neutralOptimizer = new NeutralMarketOptimizer();
|
||||
}
|
||||
return address(neutralOptimizer);
|
||||
} else {
|
||||
if (address(bearOptimizer) == address(0)) {
|
||||
bearOptimizer = new BearMarketOptimizer();
|
||||
}
|
||||
return address(bearOptimizer);
|
||||
}
|
||||
}
|
||||
|
||||
function _runTradingScenario(string memory scenarioName) internal returns (bool foundProfit) {
|
||||
uint256 initialBalance = weth.balanceOf(account);
|
||||
console.log(string.concat("Starting balance: ", vm.toString(initialBalance / 1e18), " ETH"));
|
||||
|
||||
// Initialize CSV for this scenario
|
||||
initializeTimeSeriesCSV();
|
||||
|
||||
// Force initial buy to get some HARB
|
||||
_executeBuy(10 ether);
|
||||
_recordTradeToCSV(block.timestamp, "BUY", 10 ether, 0);
|
||||
|
||||
// Execute 30 trades
|
||||
for (uint256 i = 1; i < 30; i++) {
|
||||
uint256 seed = uint256(keccak256(abi.encodePacked(scenarioName, i)));
|
||||
bool isBuy = (seed % 2) == 0;
|
||||
|
||||
if (isBuy) {
|
||||
uint256 amount = 1 ether + (seed % 10 ether);
|
||||
if (weth.balanceOf(account) >= amount) {
|
||||
_executeBuy(amount);
|
||||
_recordTradeToCSV(block.timestamp + i * 60, "BUY", amount, 0);
|
||||
}
|
||||
} else {
|
||||
uint256 harbBalance = harberg.balanceOf(account);
|
||||
if (harbBalance > 0) {
|
||||
uint256 sellAmount = harbBalance / 4;
|
||||
if (sellAmount > 0) {
|
||||
_executeSell(sellAmount);
|
||||
_recordTradeToCSV(block.timestamp + i * 60, "SELL", 0, sellAmount);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Try recenter occasionally
|
||||
if (i % 3 == 0) {
|
||||
vm.prank(feeDestination);
|
||||
try lm.recenter() {
|
||||
console.log(" Recenter successful");
|
||||
} catch Error(string memory reason) {
|
||||
console.log(string.concat(" Recenter failed: ", reason));
|
||||
} catch {
|
||||
console.log(" Recenter failed: cooldown or other error");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sell all remaining HARB
|
||||
uint256 finalHarb = harberg.balanceOf(account);
|
||||
if (finalHarb > 0) {
|
||||
_executeSell(finalHarb);
|
||||
_recordTradeToCSV(block.timestamp + 31 * 60, "SELL", 0, finalHarb);
|
||||
}
|
||||
|
||||
// Final recenter after all trades
|
||||
vm.warp(block.timestamp + 5 hours);
|
||||
vm.prank(feeDestination);
|
||||
try lm.recenter() {
|
||||
console.log("Final recenter successful");
|
||||
} catch Error(string memory reason) {
|
||||
console.log(string.concat("Final recenter failed: ", reason));
|
||||
} catch {
|
||||
console.log("Final recenter failed: unknown error");
|
||||
}
|
||||
|
||||
uint256 finalBalance = weth.balanceOf(account);
|
||||
console.log(string.concat("Final balance: ", vm.toString(finalBalance / 1e18), " ETH"));
|
||||
|
||||
scenariosAnalyzed++;
|
||||
if (finalBalance > initialBalance) {
|
||||
console.log(string.concat("PROFIT: ", vm.toString((finalBalance - initialBalance) / 1e18), " ETH"));
|
||||
profitableScenarios++;
|
||||
|
||||
// Store profitable scenario data
|
||||
profitableScenarioNames.push(scenarioName);
|
||||
profitableScenarioData.push(csv);
|
||||
|
||||
return true;
|
||||
} else {
|
||||
console.log(string.concat("Loss: ", vm.toString((initialBalance - finalBalance) / 1e18), " ETH"));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function _executeBuy(uint256 amount) internal {
|
||||
console.log(string.concat(" Buy ", vm.toString(amount / 1e18), " ETH worth"));
|
||||
|
||||
// Create a separate contract to handle the swap
|
||||
SwapExecutor executor = new SwapExecutor(pool, weth, harberg, token0isWeth);
|
||||
|
||||
// Transfer WETH to executor
|
||||
vm.prank(account);
|
||||
weth.transfer(address(executor), amount);
|
||||
|
||||
// Execute the swap
|
||||
try executor.executeBuy(amount, account) {
|
||||
console.log(" Buy successful");
|
||||
} catch Error(string memory reason) {
|
||||
console.log(string.concat(" Buy failed: ", reason));
|
||||
} catch {
|
||||
console.log(" Buy failed: unknown error");
|
||||
}
|
||||
}
|
||||
|
||||
function _executeSell(uint256 amount) internal {
|
||||
console.log(string.concat(" Sell ", vm.toString(amount / 1e18), " HARB"));
|
||||
|
||||
// Create a separate contract to handle the swap
|
||||
SwapExecutor executor = new SwapExecutor(pool, weth, harberg, token0isWeth);
|
||||
|
||||
// Transfer HARB to executor
|
||||
vm.prank(account);
|
||||
harberg.transfer(address(executor), amount);
|
||||
|
||||
// Execute the swap
|
||||
try executor.executeSell(amount, account) {
|
||||
console.log(" Sell successful");
|
||||
} catch Error(string memory reason) {
|
||||
console.log(string.concat(" Sell failed: ", reason));
|
||||
} catch {
|
||||
console.log(" Sell failed: unknown error");
|
||||
}
|
||||
}
|
||||
|
||||
function _recordTradeToCSV(uint256 timestamp, string memory action, uint256 ethAmount, uint256 harbAmount) internal {
|
||||
// Get current price
|
||||
(uint160 sqrtPriceX96,,,,,, ) = pool.slot0();
|
||||
uint256 price = _sqrtPriceToPrice(sqrtPriceX96);
|
||||
|
||||
// Get supply data
|
||||
uint256 totalSupply = harberg.totalSupply();
|
||||
uint256 stakeShares = stake.outstandingStake();
|
||||
uint256 avgTaxRate = stake.getAverageTaxRate();
|
||||
|
||||
// Create CSV row
|
||||
string memory row = string.concat(
|
||||
vm.toString(timestamp), ",",
|
||||
vm.toString(price), ",",
|
||||
vm.toString(totalSupply), ",",
|
||||
action, ",",
|
||||
vm.toString(ethAmount), ",",
|
||||
vm.toString(harbAmount), ",",
|
||||
vm.toString(stakeShares), ",",
|
||||
vm.toString(avgTaxRate)
|
||||
);
|
||||
|
||||
appendCSVRow(row);
|
||||
}
|
||||
|
||||
function _sqrtPriceToPrice(uint160 sqrtPriceX96) internal view returns (uint256) {
|
||||
if (token0isWeth) {
|
||||
// price = (sqrtPrice / 2^96)^2 * 10^18
|
||||
return (uint256(sqrtPriceX96) * uint256(sqrtPriceX96) * 1e18) >> 192;
|
||||
} else {
|
||||
// price = 1 / ((sqrtPrice / 2^96)^2) * 10^18
|
||||
return (1e18 << 192) / (uint256(sqrtPriceX96) * uint256(sqrtPriceX96));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Helper contract to execute swaps without address(this) issues
|
||||
contract SwapExecutor {
|
||||
IUniswapV3Pool public pool;
|
||||
IWETH9 public weth;
|
||||
Kraiken public harberg;
|
||||
bool public token0isWeth;
|
||||
|
||||
constructor(IUniswapV3Pool _pool, IWETH9 _weth, Kraiken _harberg, bool _token0isWeth) {
|
||||
pool = _pool;
|
||||
weth = _weth;
|
||||
harberg = _harberg;
|
||||
token0isWeth = _token0isWeth;
|
||||
}
|
||||
|
||||
function executeBuy(uint256 amount, address recipient) external {
|
||||
pool.swap(
|
||||
recipient,
|
||||
token0isWeth,
|
||||
int256(amount),
|
||||
token0isWeth ? 4295128740 : 1461446703485210103287273052203988822378723970341,
|
||||
""
|
||||
);
|
||||
}
|
||||
|
||||
function executeSell(uint256 amount, address recipient) external {
|
||||
pool.swap(
|
||||
recipient,
|
||||
!token0isWeth,
|
||||
int256(amount),
|
||||
!token0isWeth ? 4295128740 : 1461446703485210103287273052203988822378723970341,
|
||||
""
|
||||
);
|
||||
}
|
||||
|
||||
function uniswapV3SwapCallback(int256 amount0Delta, int256 amount1Delta, bytes calldata) external {
|
||||
require(msg.sender == address(pool), "Invalid caller");
|
||||
|
||||
if (amount0Delta > 0) {
|
||||
if (token0isWeth) {
|
||||
weth.transfer(msg.sender, uint256(amount0Delta));
|
||||
} else {
|
||||
harberg.transfer(msg.sender, uint256(amount0Delta));
|
||||
}
|
||||
}
|
||||
if (amount1Delta > 0) {
|
||||
if (token0isWeth) {
|
||||
harberg.transfer(msg.sender, uint256(amount1Delta));
|
||||
} else {
|
||||
weth.transfer(msg.sender, uint256(amount1Delta));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,39 +0,0 @@
|
|||
#!/bin/bash
|
||||
|
||||
# Batch Scenario Analysis Script
|
||||
# This script runs multiple analysis scenarios to discover profitable trading patterns
|
||||
|
||||
echo "🔬 Starting Batch Scenario Analysis for LiquidityManager"
|
||||
echo "======================================================="
|
||||
|
||||
# Ensure output directory exists
|
||||
mkdir -p ./out
|
||||
|
||||
# Enable FFI for file operations
|
||||
export FOUNDRY_FFI=true
|
||||
|
||||
echo "📋 Running Unit Tests First (ensure protocol safety)..."
|
||||
forge test -q
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "❌ Unit tests failed. Fix issues before running analysis."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "✅ Unit tests passed. Proceeding with scenario analysis..."
|
||||
|
||||
echo "🔍 Running Simple Analysis Script..."
|
||||
forge script analysis/SimpleAnalysis.s.sol --ffi -v
|
||||
|
||||
echo "📊 Analysis Results:"
|
||||
echo "Check ./analysis/ directory for any profitable_scenario.csv files"
|
||||
echo "These indicate potentially exploitable trading sequences"
|
||||
echo ""
|
||||
echo "🌐 To view visualizations, run:"
|
||||
echo " ./analysis/view-scenarios.sh"
|
||||
|
||||
echo "🎯 To investigate specific scenarios, modify the analysis contract"
|
||||
echo "and run targeted tests with custom parameters."
|
||||
|
||||
echo "✅ Batch analysis complete!"
|
||||
echo "Review any profitable scenarios for potential protocol improvements."
|
||||
94
onchain/analysis/helpers/SwapExecutor.sol
Normal file
94
onchain/analysis/helpers/SwapExecutor.sol
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
pragma solidity ^0.8.19;
|
||||
|
||||
import {IUniswapV3Pool} from "@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol";
|
||||
import {IWETH9} from "../../src/interfaces/IWETH9.sol";
|
||||
import {Kraiken} from "../../src/Kraiken.sol";
|
||||
import {TickMath} from "@aperture/uni-v3-lib/TickMath.sol";
|
||||
|
||||
/**
|
||||
* @title SwapExecutor
|
||||
* @notice Helper contract to execute swaps on Uniswap V3 pools for analysis scripts
|
||||
* @dev Extracted from analysis scripts to avoid code duplication
|
||||
*/
|
||||
contract SwapExecutor {
|
||||
IUniswapV3Pool public pool;
|
||||
IWETH9 public weth;
|
||||
Kraiken public harberg;
|
||||
bool public token0isWeth;
|
||||
|
||||
constructor(IUniswapV3Pool _pool, IWETH9 _weth, Kraiken _harberg, bool _token0isWeth) {
|
||||
pool = _pool;
|
||||
weth = _weth;
|
||||
harberg = _harberg;
|
||||
token0isWeth = _token0isWeth;
|
||||
}
|
||||
|
||||
function executeBuy(uint256 amount, address recipient) external {
|
||||
// For buying HARB with WETH, we're swapping in the direction that increases HARB price
|
||||
// zeroForOne = true if WETH is token0, false if WETH is token1
|
||||
bool zeroForOne = token0isWeth;
|
||||
|
||||
// Set appropriate price limit based on swap direction
|
||||
uint160 sqrtPriceLimitX96;
|
||||
if (zeroForOne) {
|
||||
// Price goes down (in terms of token0/token1 ratio)
|
||||
sqrtPriceLimitX96 = TickMath.MIN_SQRT_RATIO + 1;
|
||||
} else {
|
||||
// Price goes up
|
||||
sqrtPriceLimitX96 = TickMath.MAX_SQRT_RATIO - 1;
|
||||
}
|
||||
|
||||
try pool.swap(
|
||||
recipient,
|
||||
zeroForOne,
|
||||
int256(amount),
|
||||
sqrtPriceLimitX96,
|
||||
""
|
||||
) {} catch {
|
||||
// Swap failed, likely due to extreme price - ignore
|
||||
}
|
||||
}
|
||||
|
||||
function executeSell(uint256 amount, address recipient) external {
|
||||
// For selling HARB for WETH, we're swapping in the direction that decreases HARB price
|
||||
// zeroForOne = false if WETH is token0, true if WETH is token1
|
||||
bool zeroForOne = !token0isWeth;
|
||||
|
||||
// Set appropriate price limit based on swap direction
|
||||
uint160 sqrtPriceLimitX96;
|
||||
if (zeroForOne) {
|
||||
// Price goes down (in terms of token0/token1 ratio)
|
||||
sqrtPriceLimitX96 = TickMath.MIN_SQRT_RATIO + 1;
|
||||
} else {
|
||||
// Price goes up
|
||||
sqrtPriceLimitX96 = TickMath.MAX_SQRT_RATIO - 1;
|
||||
}
|
||||
|
||||
try pool.swap(
|
||||
recipient,
|
||||
zeroForOne,
|
||||
int256(amount),
|
||||
sqrtPriceLimitX96,
|
||||
""
|
||||
) {} catch {
|
||||
// Swap failed, likely due to extreme price - ignore
|
||||
}
|
||||
}
|
||||
|
||||
// Callback required for Uniswap V3 swaps
|
||||
function uniswapV3SwapCallback(
|
||||
int256 amount0Delta,
|
||||
int256 amount1Delta,
|
||||
bytes calldata
|
||||
) external {
|
||||
require(msg.sender == address(pool), "Unauthorized callback");
|
||||
|
||||
if (amount0Delta > 0) {
|
||||
IWETH9(pool.token0()).transfer(address(pool), uint256(amount0Delta));
|
||||
}
|
||||
if (amount1Delta > 0) {
|
||||
IWETH9(pool.token1()).transfer(address(pool), uint256(amount1Delta));
|
||||
}
|
||||
}
|
||||
}
|
||||
4
onchain/analysis/requirements.txt
Normal file
4
onchain/analysis/requirements.txt
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
pandas>=1.3.0
|
||||
matplotlib>=3.4.0
|
||||
seaborn>=0.11.0
|
||||
numpy>=1.21.0
|
||||
201
onchain/analysis/run-fuzzing.sh
Executable file
201
onchain/analysis/run-fuzzing.sh
Executable file
|
|
@ -0,0 +1,201 @@
|
|||
#!/bin/bash
|
||||
|
||||
# Change to the analysis directory (where this script is located)
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
# Default values
|
||||
OPTIMIZER_CLASS=""
|
||||
TOTAL_RUNS=50
|
||||
TRADES_PER_RUN=20
|
||||
|
||||
# Function to show usage
|
||||
show_usage() {
|
||||
echo "Usage: $0 <optimizer_class> [runs=N] [trades=N]"
|
||||
echo ""
|
||||
echo "Parameters:"
|
||||
echo " optimizer_class Required. The optimizer class to use"
|
||||
echo " runs=N Optional. Number of fuzzing runs (default: 50)"
|
||||
echo " trades=N Optional. Trades per run (default: 20, actual will be ±5)"
|
||||
echo ""
|
||||
echo "Examples:"
|
||||
echo " $0 BullMarketOptimizer"
|
||||
echo " $0 WhaleOptimizer runs=100"
|
||||
echo " $0 BearMarketOptimizer runs=10 trades=50"
|
||||
echo " $0 NeutralMarketOptimizer trades=30 runs=25"
|
||||
echo ""
|
||||
echo "Available optimizers:"
|
||||
echo " - BullMarketOptimizer"
|
||||
echo " - NeutralMarketOptimizer"
|
||||
echo " - BearMarketOptimizer"
|
||||
echo " - WhaleOptimizer"
|
||||
echo " - MockOptimizer"
|
||||
echo " - RandomScenarioOptimizer"
|
||||
}
|
||||
|
||||
# Parse arguments
|
||||
if [ $# -eq 0 ]; then
|
||||
echo "Error: No optimizer class specified"
|
||||
show_usage
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# First argument is always the optimizer class
|
||||
OPTIMIZER_CLASS=$1
|
||||
shift
|
||||
|
||||
# Parse named parameters
|
||||
for arg in "$@"; do
|
||||
case $arg in
|
||||
runs=*)
|
||||
TOTAL_RUNS="${arg#*=}"
|
||||
if ! [[ "$TOTAL_RUNS" =~ ^[0-9]+$ ]] || [ "$TOTAL_RUNS" -eq 0 ]; then
|
||||
echo "Error: Invalid value for runs. Must be a positive integer."
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
trades=*)
|
||||
TRADES_PER_RUN="${arg#*=}"
|
||||
if ! [[ "$TRADES_PER_RUN" =~ ^[0-9]+$ ]] || [ "$TRADES_PER_RUN" -eq 0 ]; then
|
||||
echo "Error: Invalid value for trades. Must be a positive integer."
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
echo "Error: Unknown parameter '$arg'"
|
||||
show_usage
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Colors for output
|
||||
GREEN='\033[0;32m'
|
||||
RED='\033[0;31m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
OUTPUT_DIR="fuzzing_results_${OPTIMIZER_CLASS}_$(date +%Y%m%d_%H%M%S)"
|
||||
MERGED_CSV="$OUTPUT_DIR/merged_profitable_scenarios.csv"
|
||||
|
||||
echo -e "${GREEN}=== Fuzzing Campaign ===${NC}"
|
||||
echo "Optimizer: $OPTIMIZER_CLASS"
|
||||
echo "Total runs: $TOTAL_RUNS"
|
||||
echo "Trades per run: $TRADES_PER_RUN (±5)"
|
||||
echo "Output directory: $OUTPUT_DIR"
|
||||
echo ""
|
||||
|
||||
# Validate that the optimizer class exists by doing a dry run
|
||||
echo "Validating optimizer class..."
|
||||
OPTIMIZER_CLASS="$OPTIMIZER_CLASS" FUZZING_RUNS=0 forge script FuzzingAnalysis.s.sol --ffi --via-ir > /tmp/optimizer_check.log 2>&1
|
||||
if [ $? -ne 0 ]; then
|
||||
echo -e "${RED}Error: Invalid optimizer class '${OPTIMIZER_CLASS}'${NC}"
|
||||
echo -e "${RED}Check the error:${NC}"
|
||||
grep -E "(Unknown optimizer|revert)" /tmp/optimizer_check.log
|
||||
echo ""
|
||||
show_usage
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Create output directory
|
||||
mkdir -p "$OUTPUT_DIR"
|
||||
|
||||
# Initialize merged CSV with header
|
||||
echo "Scenario,Seed,Initial Balance,Final Balance,Profit,Profit %" > "$MERGED_CSV"
|
||||
|
||||
# Track statistics
|
||||
TOTAL_PROFITABLE=0
|
||||
FAILED_RUNS=0
|
||||
|
||||
# Save configuration
|
||||
CONFIG_FILE="$OUTPUT_DIR/config.txt"
|
||||
{
|
||||
echo "Fuzzing Configuration"
|
||||
echo "===================="
|
||||
echo "Optimizer: $OPTIMIZER_CLASS"
|
||||
echo "Total runs: $TOTAL_RUNS"
|
||||
echo "Trades per run: $TRADES_PER_RUN (±5)"
|
||||
echo "Start time: $(date)"
|
||||
} > "$CONFIG_FILE"
|
||||
|
||||
# Run fuzzing analysis multiple times
|
||||
for i in $(seq 1 $TOTAL_RUNS); do
|
||||
echo -e "${YELLOW}Running fuzzing iteration $i/$TOTAL_RUNS...${NC}"
|
||||
|
||||
# Run single fuzzing iteration with specified optimizer and trades
|
||||
OPTIMIZER_CLASS="$OPTIMIZER_CLASS" TRADES_PER_RUN="$TRADES_PER_RUN" FUZZING_RUNS=1 forge script FuzzingAnalysis.s.sol --ffi --via-ir --gas-limit 200000000 > "$OUTPUT_DIR/run_$i.log" 2>&1
|
||||
|
||||
if [ $? -eq 0 ]; then
|
||||
echo -e "${GREEN}✓ Run $i completed${NC}"
|
||||
|
||||
# Check if profitable scenarios were found
|
||||
if grep -q "PROFITABLE!" "$OUTPUT_DIR/run_$i.log"; then
|
||||
echo -e "${GREEN} Found profitable scenario!${NC}"
|
||||
((TOTAL_PROFITABLE++))
|
||||
|
||||
# Extract profit percentage
|
||||
PROFIT_PCT=$(grep "PROFITABLE!" "$OUTPUT_DIR/run_$i.log" | grep -oE "Profit: [0-9]+%" | grep -oE "[0-9]+")
|
||||
echo -e "${GREEN} Profit: ${PROFIT_PCT}%${NC}"
|
||||
|
||||
# Extract CSV file path if generated
|
||||
CSV_FILE=$(grep "Profitable scenarios written to:" "$OUTPUT_DIR/run_$i.log" | awk '{print $NF}')
|
||||
if [ -n "$CSV_FILE" ] && [ -f "$CSV_FILE" ]; then
|
||||
# Append data rows (skip header) to merged CSV
|
||||
tail -n +2 "$CSV_FILE" >> "$MERGED_CSV"
|
||||
# Move individual CSV to output directory
|
||||
mv "$CSV_FILE" "$OUTPUT_DIR/"
|
||||
fi
|
||||
fi
|
||||
else
|
||||
echo -e "${RED}✗ Run $i failed${NC}"
|
||||
((FAILED_RUNS++))
|
||||
# Show last few lines of error
|
||||
echo -e "${RED}Error details:${NC}"
|
||||
tail -5 "$OUTPUT_DIR/run_$i.log"
|
||||
fi
|
||||
|
||||
# Small delay to avoid overwhelming the system
|
||||
sleep 0.5
|
||||
done
|
||||
|
||||
# Update config with end time
|
||||
echo "End time: $(date)" >> "$CONFIG_FILE"
|
||||
|
||||
echo ""
|
||||
echo -e "${GREEN}=== FUZZING CAMPAIGN COMPLETE ===${NC}"
|
||||
echo "Optimizer: $OPTIMIZER_CLASS"
|
||||
echo "Total runs: $TOTAL_RUNS"
|
||||
echo "Trades per run: $TRADES_PER_RUN (±5)"
|
||||
echo "Successful runs: $((TOTAL_RUNS - FAILED_RUNS))"
|
||||
echo "Failed runs: $FAILED_RUNS"
|
||||
echo "Total profitable scenarios: $TOTAL_PROFITABLE"
|
||||
echo ""
|
||||
echo "Results saved in: $OUTPUT_DIR"
|
||||
echo "Merged CSV: $MERGED_CSV"
|
||||
|
||||
# Generate summary report
|
||||
SUMMARY="$OUTPUT_DIR/summary.txt"
|
||||
{
|
||||
echo "Fuzzing Campaign Summary"
|
||||
echo "========================"
|
||||
echo "Date: $(date)"
|
||||
echo "Optimizer: $OPTIMIZER_CLASS"
|
||||
echo "Total runs: $TOTAL_RUNS"
|
||||
echo "Trades per run: $TRADES_PER_RUN (±5)"
|
||||
echo ""
|
||||
echo "Results:"
|
||||
echo "--------"
|
||||
echo "Successful runs: $((TOTAL_RUNS - FAILED_RUNS)) / $TOTAL_RUNS"
|
||||
echo "Failed runs: $FAILED_RUNS"
|
||||
echo "Total profitable scenarios: $TOTAL_PROFITABLE / $((TOTAL_RUNS - FAILED_RUNS))"
|
||||
echo "Success rate: $(awk "BEGIN {if ($TOTAL_RUNS - $FAILED_RUNS > 0) printf \"%.2f\", $TOTAL_PROFITABLE/($TOTAL_RUNS-$FAILED_RUNS)*100; else print \"0.00\"}")%"
|
||||
} > "$SUMMARY"
|
||||
|
||||
echo ""
|
||||
echo "Summary report: $SUMMARY"
|
||||
|
||||
# If there were profitable scenarios, show a sample
|
||||
if [ $TOTAL_PROFITABLE -gt 0 ]; then
|
||||
echo ""
|
||||
echo -e "${GREEN}Sample profitable scenarios:${NC}"
|
||||
head -5 "$MERGED_CSV"
|
||||
fi
|
||||
|
|
@ -3,6 +3,10 @@ src = "src"
|
|||
out = "out"
|
||||
libs = ["lib"]
|
||||
fs_permissions = [{ access = "read-write", path = "./"}]
|
||||
gas_limit = 100_000_000
|
||||
gas_price = 0
|
||||
optimizer = true
|
||||
optimizer_runs = 200
|
||||
# See more config options https://github.com/foundry-rs/foundry/tree/master/config
|
||||
|
||||
[rpc_endpoints]
|
||||
|
|
|
|||
118
onchain/test/helpers/LiquidityBoundaryHelper.sol
Normal file
118
onchain/test/helpers/LiquidityBoundaryHelper.sol
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.19;
|
||||
|
||||
import {IUniswapV3Pool} from "@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol";
|
||||
import {TickMath} from "@aperture/uni-v3-lib/TickMath.sol";
|
||||
import {LiquidityAmounts} from "@aperture/uni-v3-lib/LiquidityAmounts.sol";
|
||||
import {ThreePositionStrategy} from "../../src/abstracts/ThreePositionStrategy.sol";
|
||||
|
||||
/**
|
||||
* @title LiquidityBoundaryHelper
|
||||
* @notice Helper library for calculating safe trade sizes within liquidity boundaries
|
||||
* @dev Prevents trades that would exceed available liquidity and cause SPL errors
|
||||
*/
|
||||
library LiquidityBoundaryHelper {
|
||||
/**
|
||||
* @notice Calculates the maximum ETH amount that can be traded (buy HARB) without exceeding position liquidity limits
|
||||
* @param pool The Uniswap V3 pool
|
||||
* @param liquidityManager The liquidity manager contract
|
||||
* @param token0isWeth Whether token0 is WETH
|
||||
* @return maxEthAmount Maximum ETH that can be safely traded
|
||||
*/
|
||||
function calculateBuyLimit(
|
||||
IUniswapV3Pool pool,
|
||||
ThreePositionStrategy liquidityManager,
|
||||
bool token0isWeth
|
||||
) internal view returns (uint256 maxEthAmount) {
|
||||
(, int24 currentTick,,,,,) = pool.slot0();
|
||||
|
||||
// Get position data
|
||||
(uint128 anchorLiquidity, int24 anchorLower, int24 anchorUpper) = liquidityManager.positions(ThreePositionStrategy.Stage.ANCHOR);
|
||||
(uint128 discoveryLiquidity, int24 discoveryLower, int24 discoveryUpper) = liquidityManager.positions(ThreePositionStrategy.Stage.DISCOVERY);
|
||||
|
||||
// If no positions exist, return 0 (no safe limit)
|
||||
if (anchorLiquidity == 0 && discoveryLiquidity == 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
uint256 maxEth = 0;
|
||||
|
||||
// Check anchor position
|
||||
if (currentTick >= anchorLower && currentTick < anchorUpper && anchorLiquidity > 0) {
|
||||
uint160 currentSqrtPrice = TickMath.getSqrtRatioAtTick(currentTick);
|
||||
uint160 upperSqrtPrice = TickMath.getSqrtRatioAtTick(anchorUpper);
|
||||
|
||||
if (token0isWeth) {
|
||||
maxEth = LiquidityAmounts.getAmount0ForLiquidity(currentSqrtPrice, upperSqrtPrice, anchorLiquidity);
|
||||
} else {
|
||||
maxEth = LiquidityAmounts.getAmount1ForLiquidity(currentSqrtPrice, upperSqrtPrice, anchorLiquidity);
|
||||
}
|
||||
}
|
||||
// Check discovery position
|
||||
else if (currentTick >= discoveryLower && currentTick < discoveryUpper && discoveryLiquidity > 0) {
|
||||
uint160 currentSqrtPrice = TickMath.getSqrtRatioAtTick(currentTick);
|
||||
uint160 upperSqrtPrice = TickMath.getSqrtRatioAtTick(discoveryUpper);
|
||||
|
||||
if (token0isWeth) {
|
||||
maxEth = LiquidityAmounts.getAmount0ForLiquidity(currentSqrtPrice, upperSqrtPrice, discoveryLiquidity);
|
||||
} else {
|
||||
maxEth = LiquidityAmounts.getAmount1ForLiquidity(currentSqrtPrice, upperSqrtPrice, discoveryLiquidity);
|
||||
}
|
||||
}
|
||||
|
||||
// Apply safety margin (90% of calculated max)
|
||||
return (maxEth * 9) / 10;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Calculates the maximum HARB amount that can be traded (sell HARB) without exceeding position liquidity limits
|
||||
* @param pool The Uniswap V3 pool
|
||||
* @param liquidityManager The liquidity manager contract
|
||||
* @param token0isWeth Whether token0 is WETH
|
||||
* @return maxHarbAmount Maximum HARB that can be safely traded
|
||||
*/
|
||||
function calculateSellLimit(
|
||||
IUniswapV3Pool pool,
|
||||
ThreePositionStrategy liquidityManager,
|
||||
bool token0isWeth
|
||||
) internal view returns (uint256 maxHarbAmount) {
|
||||
(, int24 currentTick,,,,,) = pool.slot0();
|
||||
|
||||
// Get position data
|
||||
(uint128 anchorLiquidity, int24 anchorLower, int24 anchorUpper) = liquidityManager.positions(ThreePositionStrategy.Stage.ANCHOR);
|
||||
(uint128 floorLiquidity, int24 floorLower, int24 floorUpper) = liquidityManager.positions(ThreePositionStrategy.Stage.FLOOR);
|
||||
|
||||
// If no positions exist, return 0 (no safe limit)
|
||||
if (anchorLiquidity == 0 && floorLiquidity == 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
uint256 maxHarb = 0;
|
||||
|
||||
// Check anchor position
|
||||
if (currentTick >= anchorLower && currentTick < anchorUpper && anchorLiquidity > 0) {
|
||||
uint160 currentSqrtPrice = TickMath.getSqrtRatioAtTick(currentTick);
|
||||
uint160 lowerSqrtPrice = TickMath.getSqrtRatioAtTick(anchorLower);
|
||||
|
||||
if (token0isWeth) {
|
||||
maxHarb = LiquidityAmounts.getAmount1ForLiquidity(lowerSqrtPrice, currentSqrtPrice, anchorLiquidity);
|
||||
} else {
|
||||
maxHarb = LiquidityAmounts.getAmount0ForLiquidity(lowerSqrtPrice, currentSqrtPrice, anchorLiquidity);
|
||||
}
|
||||
}
|
||||
// Check floor position
|
||||
else if (currentTick >= floorLower && currentTick < floorUpper && floorLiquidity > 0) {
|
||||
uint160 currentSqrtPrice = TickMath.getSqrtRatioAtTick(currentTick);
|
||||
uint160 lowerSqrtPrice = TickMath.getSqrtRatioAtTick(floorLower);
|
||||
|
||||
if (token0isWeth) {
|
||||
maxHarb = LiquidityAmounts.getAmount1ForLiquidity(lowerSqrtPrice, currentSqrtPrice, floorLiquidity);
|
||||
} else {
|
||||
maxHarb = LiquidityAmounts.getAmount0ForLiquidity(lowerSqrtPrice, currentSqrtPrice, floorLiquidity);
|
||||
}
|
||||
}
|
||||
|
||||
// Apply safety margin (90% of calculated max)
|
||||
return (maxHarb * 9) / 10;
|
||||
}
|
||||
}
|
||||
|
|
@ -243,6 +243,61 @@ contract TestEnvironment is TestConstants {
|
|||
return (factory, pool, weth, harberg, stake, lm, optimizer, token0isWeth);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Setup environment with existing factory and specific optimizer
|
||||
* @param existingFactory The existing Uniswap factory to use
|
||||
* @param token0shouldBeWeth Whether WETH should be token0
|
||||
* @param recenterCaller Address that will be granted recenter access
|
||||
* @param optimizerAddress Address of the optimizer to use
|
||||
* @return _factory The existing Uniswap factory
|
||||
* @return _pool The created Uniswap pool
|
||||
* @return _weth The WETH token contract
|
||||
* @return _harberg The Kraiken token contract
|
||||
* @return _stake The staking contract
|
||||
* @return _lm The liquidity manager contract
|
||||
* @return _optimizer The optimizer contract
|
||||
* @return _token0isWeth Whether token0 is WETH
|
||||
*/
|
||||
function setupEnvironmentWithExistingFactory(
|
||||
IUniswapV3Factory existingFactory,
|
||||
bool token0shouldBeWeth,
|
||||
address recenterCaller,
|
||||
address optimizerAddress
|
||||
) external returns (
|
||||
IUniswapV3Factory _factory,
|
||||
IUniswapV3Pool _pool,
|
||||
IWETH9 _weth,
|
||||
Kraiken _harberg,
|
||||
Stake _stake,
|
||||
LiquidityManager _lm,
|
||||
Optimizer _optimizer,
|
||||
bool _token0isWeth
|
||||
) {
|
||||
// Use existing factory
|
||||
factory = existingFactory;
|
||||
|
||||
// Deploy tokens in correct order
|
||||
_deployTokensWithOrder(token0shouldBeWeth);
|
||||
|
||||
// Create and initialize pool
|
||||
_createAndInitializePool();
|
||||
|
||||
// Deploy protocol contracts with custom optimizer
|
||||
stake = new Stake(address(harberg), feeDestination);
|
||||
optimizer = Optimizer(optimizerAddress);
|
||||
lm = new LiquidityManager(address(factory), address(weth), address(harberg), optimizerAddress);
|
||||
lm.setFeeDestination(feeDestination);
|
||||
|
||||
// Configure permissions
|
||||
_configurePermissions();
|
||||
|
||||
// Grant recenter access to specified caller
|
||||
vm.prank(feeDestination);
|
||||
lm.setRecenterAccess(recenterCaller);
|
||||
|
||||
return (factory, pool, weth, harberg, stake, lm, optimizer, token0isWeth);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Perform recenter with proper time warp and oracle updates
|
||||
* @param liquidityManager The LiquidityManager instance to recenter
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import {SqrtPriceMath} from "@aperture/uni-v3-lib/SqrtPriceMath.sol";
|
|||
import "../../src/interfaces/IWETH9.sol";
|
||||
import {Kraiken} from "../../src/Kraiken.sol";
|
||||
import {ThreePositionStrategy} from "../../src/abstracts/ThreePositionStrategy.sol";
|
||||
import {LiquidityBoundaryHelper} from "./LiquidityBoundaryHelper.sol";
|
||||
|
||||
/**
|
||||
* @title UniSwapHelper
|
||||
|
|
@ -238,23 +239,7 @@ abstract contract UniSwapHelper is Test {
|
|||
// Get LiquidityManager reference from test context
|
||||
// This assumes the test has a 'lm' variable for the LiquidityManager
|
||||
try this.getLiquidityManager() returns (ThreePositionStrategy liquidityManager) {
|
||||
(, int24 currentTick,,,,,) = pool.slot0();
|
||||
|
||||
// Get position data
|
||||
(uint128 anchorLiquidity, int24 anchorLower, int24 anchorUpper) = liquidityManager.positions(ThreePositionStrategy.Stage.ANCHOR);
|
||||
(uint128 discoveryLiquidity, int24 discoveryLower, int24 discoveryUpper) = liquidityManager.positions(ThreePositionStrategy.Stage.DISCOVERY);
|
||||
|
||||
// If no positions exist, return 0 (no safe limit)
|
||||
if (anchorLiquidity == 0 && discoveryLiquidity == 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Calculate based on token ordering and current price position
|
||||
if (token0isWeth) {
|
||||
return _calculateBuyLimitToken0IsWeth(currentTick, anchorLiquidity, anchorLower, anchorUpper, discoveryLiquidity, discoveryLower, discoveryUpper);
|
||||
} else {
|
||||
return _calculateBuyLimitToken1IsWeth(currentTick, anchorLiquidity, anchorLower, anchorUpper, discoveryLiquidity, discoveryLower, discoveryUpper);
|
||||
}
|
||||
return LiquidityBoundaryHelper.calculateBuyLimit(pool, liquidityManager, token0isWeth);
|
||||
} catch {
|
||||
return 0; // Safe fallback if LiquidityManager access fails
|
||||
}
|
||||
|
|
@ -267,23 +252,7 @@ abstract contract UniSwapHelper is Test {
|
|||
*/
|
||||
function sellLimitToLiquidityBoundary() internal view returns (uint256 maxHarbAmount) {
|
||||
try this.getLiquidityManager() returns (ThreePositionStrategy liquidityManager) {
|
||||
(, int24 currentTick,,,,,) = pool.slot0();
|
||||
|
||||
// Get position data
|
||||
(uint128 anchorLiquidity, int24 anchorLower, int24 anchorUpper) = liquidityManager.positions(ThreePositionStrategy.Stage.ANCHOR);
|
||||
(uint128 floorLiquidity, int24 floorLower, int24 floorUpper) = liquidityManager.positions(ThreePositionStrategy.Stage.FLOOR);
|
||||
|
||||
// If no positions exist, return 0 (no safe limit)
|
||||
if (anchorLiquidity == 0 && floorLiquidity == 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Calculate based on token ordering and current price position
|
||||
if (token0isWeth) {
|
||||
return _calculateSellLimitToken0IsWeth(currentTick, anchorLiquidity, anchorLower, anchorUpper, floorLiquidity, floorLower, floorUpper);
|
||||
} else {
|
||||
return _calculateSellLimitToken1IsWeth(currentTick, anchorLiquidity, anchorLower, anchorUpper, floorLiquidity, floorLower, floorUpper);
|
||||
}
|
||||
return LiquidityBoundaryHelper.calculateSellLimit(pool, liquidityManager, token0isWeth);
|
||||
} catch {
|
||||
return 0; // Safe fallback if LiquidityManager access fails
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,23 +15,23 @@ contract BullMarketOptimizer {
|
|||
return 0; // Placeholder implementation
|
||||
}
|
||||
|
||||
/// @notice Returns bull market liquidity parameters
|
||||
/// @return capitalInefficiency 20% - aggressive
|
||||
/// @return anchorShare 80% - large anchor
|
||||
/// @return anchorWidth 30 - narrow width
|
||||
/// @return discoveryDepth 90% - deep discovery
|
||||
/// @notice Returns whale attack liquidity parameters
|
||||
/// @return capitalInefficiency 10% - very aggressive
|
||||
/// @return anchorShare 95% - massive anchor concentration
|
||||
/// @return anchorWidth 80 - moderate width
|
||||
/// @return discoveryDepth 5% - minimal discovery
|
||||
function getLiquidityParams()
|
||||
external
|
||||
pure
|
||||
returns (uint256 capitalInefficiency, uint256 anchorShare, uint24 anchorWidth, uint256 discoveryDepth)
|
||||
{
|
||||
capitalInefficiency = 2 * 10 ** 17; // 20% - aggressive
|
||||
anchorShare = 8 * 10 ** 17; // 80% - large anchor
|
||||
anchorWidth = 30; // narrow width
|
||||
discoveryDepth = 9 * 10 ** 17; // 90% - deep discovery
|
||||
capitalInefficiency = 1e17; // 10% - very aggressive
|
||||
anchorShare = 95e16; // 95% - massive anchor position
|
||||
anchorWidth = 80; // moderate width (was 10)
|
||||
discoveryDepth = 5e16; // 5% - minimal discovery
|
||||
}
|
||||
|
||||
function getDescription() external pure returns (string memory) {
|
||||
return "Bull Market (High Risk)";
|
||||
return "Bull Market (Whale Attack Parameters)";
|
||||
}
|
||||
}
|
||||
33
onchain/test/mocks/WhaleOptimizer.sol
Normal file
33
onchain/test/mocks/WhaleOptimizer.sol
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
pragma solidity ^0.8.19;
|
||||
|
||||
import {Kraiken} from "../../src/Kraiken.sol";
|
||||
import {Stake} from "../../src/Stake.sol";
|
||||
|
||||
/**
|
||||
* @title WhaleOptimizer
|
||||
* @notice Simulates large position dominance with extremely aggressive parameters
|
||||
* @dev Tests vulnerability to large trades that can move price significantly
|
||||
*/
|
||||
contract WhaleOptimizer {
|
||||
function calculateSentiment(uint256, uint256) external pure returns (uint256) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
function getSentiment() external pure returns (uint256) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
function getLiquidityParams() external pure returns (uint256, uint256, uint24, uint256) {
|
||||
return (
|
||||
1e17, // capitalInefficiency: 10% (very aggressive)
|
||||
95e16, // anchorShare: 95% (massive anchor position)
|
||||
10, // anchorWidth: 10 (extremely narrow)
|
||||
5e16 // discoveryDepth: 5% (minimal discovery)
|
||||
);
|
||||
}
|
||||
|
||||
function getDescription() external pure returns (string memory) {
|
||||
return "Whale Market - Massive concentrated liquidity";
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue