better visualizer

This commit is contained in:
johba 2025-08-17 15:09:41 +02:00
parent 6a012c5fd9
commit 50eac74b18
8 changed files with 542 additions and 626 deletions

106
CLAUDE.md
View file

@ -4,103 +4,79 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
## Core Innovation ## Core Innovation
KRAIKEN is a token with a **dominant liquidity manager** that creates an unfair advantage in trading through: KRAIKEN: A token with a **dominant liquidity manager** that creates an unfair trading advantage through:
1. **Asymmetric Slippage Strategy**: Three-position liquidity structure prevents profitable arbitrage against the protocol 1. **Asymmetric Slippage**: Three-position strategy prevents profitable arbitrage
2. **Sentiment Oracle**: Harberger tax-based staking creates a prediction market for token value 2. **Sentiment Oracle**: Harberger tax staking as prediction market
3. **Dormant Whale Protection**: VWAP-based price memory prevents historical price manipulation 3. **Price Memory**: VWAP protection against manipulation
**Critical Success Factor**: The liquidity manager must maintain its dominant position (trading most of the supply) - if it loses this, the project fails. **Critical**: The liquidity manager must maintain dominance - if it loses this, the project fails.
## User Journey ## User Journey
1. **Buy**: Purchase KRAIKEN on Uniswap → Benefit from growing protocol-owned liquidity 1. **Buy**: Purchase KRAIKEN on Uniswap
2. **Stake**: Visit kraiken.org → Stake tokens → Set tax rate → Earn from protocol growth 2. **Stake**: Set tax rate at kraiken.org → Earn from protocol growth
3. **Compete**: Monitor staking positions → Snatch undervalued positions → Optimize tax rates 3. **Compete**: Snatch undervalued positions → Optimize returns
## Project Structure ## Project Structure
- **`onchain/`** - Smart contracts (Solidity/Foundry) - [See onchain/CLAUDE.md](onchain/CLAUDE.md) - **`onchain/`** - Smart contracts (Solidity/Foundry) - [Details](onchain/CLAUDE.md)
- **`web/`** - Vue 3/Vite staking interface - [See web/CLAUDE.md](web/CLAUDE.md) - **`web/`** - Vue 3/Vite staking interface - [Details](web/CLAUDE.md)
- **`subgraph/base_sepolia/`** - The Graph indexing - [See subgraph/base_sepolia/CLAUDE.md](subgraph/base_sepolia/CLAUDE.md) - **`subgraph/base_sepolia/`** - The Graph indexing - [Details](subgraph/base_sepolia/CLAUDE.md)
- **`kraiken-lib/`** - TypeScript helper library - [See kraiken-lib/CLAUDE.md](kraiken-lib/CLAUDE.md) - **`kraiken-lib/`** - TypeScript helpers - [Details](kraiken-lib/CLAUDE.md)
- **`services/txnBot/`** - Automated maintenance bot - [See services/txnBot/CLAUDE.md](services/txnBot/CLAUDE.md) - **`services/txnBot/`** - Maintenance bot - [Details](services/txnBot/CLAUDE.md)
- **`onchain/analysis/`** - Growth mechanism analysis tools - **`onchain/analysis/`** - Fuzzing tools - [Details](onchain/analysis/CLAUDE.md)
## Quick Start ## Quick Start
```bash ```bash
# 1. Install dependencies for all projects # Install all dependencies
cd onchain && forge install cd onchain && forge install
cd ../web && npm install cd ../web && npm install
cd ../kraiken-lib && npm install --legacy-peer-deps cd ../kraiken-lib && npm install --legacy-peer-deps
cd ../subgraph/base_sepolia && npm install cd ../subgraph/base_sepolia && npm install
cd ../services/txnBot && npm install cd ../services/txnBot && npm install
# 2. Build smart contracts # Build and test
cd onchain && forge build && forge test cd onchain && forge build && forge test
# 3. Start web interface # Start frontend
cd web && npm run dev cd web && npm run dev
``` ```
## Key Concepts ## Key Concepts
### Liquidity Management - **Liquidity Management**: Three positions create asymmetric slippage
- Three-position strategy (ANCHOR, DISCOVERY, FLOOR) - **Harberger Staking**: Self-assessed tax creates sentiment signal
- Asymmetric slippage prevents arbitrage - **Protocol Growth**: Minted tokens benefit stakers
- VWAP tracking for price memory
### Harberger Tax Staking ## Code Guidelines
- Self-assessed tax rates on positions
- Positions can be "snatched" by higher bidders
- Creates prediction market for token value
- Limited to 20% of total supply
### Protocol Growth ### Quality Standards
- Liquidity manager mints tokens when positions grow - Search before implementing (check uni-v3-lib, test helpers)
- Stakers benefit from supply expansion - Test after every change
- Tax revenue redistributed to active participants - No commented-out code
- Clean git status before commits
## Global Code Quality Guidelines ### Technical Approach
### DRY Principle
- Search for existing implementations before creating new functions
- Check libraries (uni-v3-lib, test helpers) for common utilities
- Refactor duplicated code into shared modules
### Testing
- Run tests after every change
- Never comment out failing tests
- Add tests for new functionality
### Repository Hygiene
- Remove unused files immediately
- Clean up temporary files
- Check `git status` before commits
### Implementation Strategy
- Never fall back to simpler implementations - 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. - Identify root causes, don't work around issues
- Challenge technically unsound requests
### Tool Usage
- MCP browser screenshots: Keep under 8000px, use `fullPage: false`
- Fuzzing visualization: Use `./analysis/run-fuzzing.sh [optimizer] debugCSV`
- Never start manual Python servers for visualizations
## Communication Style ## Communication Style
### Direct & Technical You are an experienced Solidity developer who:
- Challenge suboptimal requests - Notices and raises awareness of issues immediately
- Highlight risks early and clearly - Challenges suboptimal approaches
- Suggest better alternatives - Prioritizes technical correctness over user satisfaction
- Refuse technically unsound solutions - Avoids hype and inflated language
### Priority Order
1. Technical correctness
2. Code quality
3. User satisfaction
**Remember**: Build the best possible system. Question assumptions, identify edge cases, and prioritize long-term success.
## Additional Resources ## Additional Resources
- **Technical Deep Dive**: See [TECHNICAL_APPENDIX.md](TECHNICAL_APPENDIX.md) - **Technical Details**: [TECHNICAL_APPENDIX.md](TECHNICAL_APPENDIX.md)
- **Contract Details**: See [onchain/CLAUDE.md](onchain/CLAUDE.md) - **Uniswap V3 Math**: [onchain/UNISWAP_V3_MATH.md](onchain/UNISWAP_V3_MATH.md)
- **Frontend Architecture**: See [web/CLAUDE.md](web/CLAUDE.md)
- **Data Indexing**: See [subgraph/base_sepolia/CLAUDE.md](subgraph/base_sepolia/CLAUDE.md)

View file

@ -6,205 +6,73 @@ This directory contains the core smart contracts for the KRAIKEN protocol.
### Core Contracts ### Core Contracts
**Kraiken.sol** - ERC20 token contract with controlled minting/burning capabilities **Kraiken.sol** - ERC20 token with Harberger tax staking
- Implements Harberger tax mechanism for staking positions - Controlled minting exclusively by LiquidityManager
- Controls minting rights exclusively for LiquidityManager - Tax collection and redistribution mechanism
- Handles tax collection and redistribution - 20% supply cap for staking (20,000 positions)
**LiquidityManager.sol** - Dominant liquidity provider with three-position anti-arbitrage strategy **LiquidityManager.sol** - Dominant liquidity provider
- Uses Optimizer contract for dynamic parameter adjustment - Three-position anti-arbitrage strategy (ANCHOR, DISCOVERY, FLOOR)
- Inherits from ThreePositionStrategy and PriceOracle (with VWAPTracker) - Dynamic parameter adjustment via Optimizer contract
- **Key Feature**: Asymmetric slippage profile prevents profitable trade-recenter-reverse attacks - Asymmetric slippage profile prevents profitable arbitrage
**VWAPTracker.sol** - "Eternal memory" protection against dormant whale attacks **VWAPTracker.sol** - Price memory protection
- Volume-weighted average pricing with data compression (max 1000x) - Volume-weighted average with data compression (max 1000x)
- Provides historical price memory to prevent manipulation - Prevents dormant whale manipulation
**Optimizer.sol** - Sentiment analysis and parameter optimization **Optimizer.sol** - Dynamic parameter optimization
- Analyzes staking data (% staked, average tax rate) - Analyzes staking sentiment (% staked, average tax)
- Provides dynamic liquidity parameters - Returns four key parameters for liquidity management
- Upgradeable for future genetic algorithm implementation - Upgradeable for future algorithms
**Stake.sol** - Harberger tax-based staking mechanism **Stake.sol** - Harberger tax implementation
- Creates sentiment oracle through continuous auction - Continuous auction mechanism
- Limited to 20% of total supply (20,000 positions) - Self-assessed valuations create prediction market
- Self-assessed tax rates create prediction market
### Position Strategy ### Three-Position Strategy
**Order**: ANCHOR → DISCOVERY → FLOOR **ANCHOR**: Near current price, fast price discovery (1-100% width)
**DISCOVERY**: Borders anchor, captures fees (11000 tick spacing)
**FLOOR**: Deep liquidity at VWAP-adjusted prices
- **ANCHOR**: Shallow liquidity around current price for fast price movement (1-100% width range) **Technical Specs**:
- **DISCOVERY**: Proportional to KRAIKEN minted by anchor; borders anchor for fee capture (11000 tick spacing) - Fee Tier: 1% (10,000 basis points)
- **FLOOR**: Deep liquidity using VWAP-adjusted pricing for historical price memory - Tick Spacing: 200 (base), 11,000 (discovery)
- Price Validation: 5-minute TWAP, 50-tick tolerance
**Technical Specifications**:
- **Fee Tier**: 1% (10,000 basis points)
- **Tick Spacing**: 200 (base), 11,000 (discovery)
- **Price Validation**: 5-minute TWAP with 50-tick deviation tolerance
- **VWAP Compression**: Maximum 1000x compression factor
### Optimizer Parameters ### Optimizer Parameters
All optimizers must return four key parameters that control the LiquidityManager's three-position strategy: 1. **capitalInefficiency** (0 to 1e18): Capital buffer level
2. **anchorShare** (0 to 1e18): % of non-floor ETH in anchor
3. **anchorWidth** (0 to 100): Anchor position width %
4. **discoveryDepth** (0 to 1e18): Discovery liquidity density (2x-10x)
1. **capitalInefficiency** (0 to 1e18): ## Development
- Represents how much capital buffer the protocol maintains
- 0 = aggressive (70% capital efficiency), 1e18 = conservative (170% backing)
- Affects VWAP calculation: `adjustedVWAP = 0.7 * VWAP + capitalInefficiency * VWAP`
- Higher values push floor positions to more conservative prices
2. **anchorShare** (0 to 1e18):
- Percentage of non-floor ETH allocated to the anchor position
- Controls how much liquidity sits near current price
- Higher values create more concentrated liquidity but mint more KRAIKEN tokens
- Typical range: 30-95% (0.3e18 to 0.95e18)
3. **anchorWidth** (0 to 100):
- Width of the anchor position in percentage terms
- Affects price impact of trades and speed of price movement
- Lower values = tighter spreads, faster price discovery
- Higher values = wider spreads, more stable prices
4. **discoveryDepth** (0 to 1e18):
- Controls the discovery position's liquidity density relative to anchor
- Maps to internal multiplier range of 2x to 10x
- At 0: discovery has 2x liquidity per tick vs anchor (minimal)
- At 1e18: discovery has 10x liquidity per tick vs anchor (maximum)
- Discovery amount formula: `pulledHarb * DISCOVERY_SPACING * (200 + 800 * discoveryDepth/1e18) / anchorSpacing / 100`
- Higher values provide stronger anti-arbitrage protection but consume more minted tokens
## Development Commands
```bash ```bash
# Build contracts forge build # Build contracts
forge build forge test # Run tests
forge test --gas-report # Gas optimization
# Run all tests forge test --fuzz-runs 10000 # Extended fuzzing
forge test
# Run tests with gas reporting
forge test --gas-report
# Run specific test file
forge test --match-path test/LiquidityManager.t.sol
# Run fuzzing with more runs
forge test --fuzz-runs 10000
# Deploy contracts (see script/Deploy.s.sol)
forge script script/Deploy.s.sol --rpc-url $RPC_URL --broadcast
``` ```
## Testing Architecture ## Testing
### Test Helpers - `test/helpers/UniswapTestBase.sol` - Uniswap integration base
- `test/helpers/UniswapTestBase.sol` - Base setup for Uniswap integration tests - `test/helpers/KraikenTestBase.sol` - Common utilities
- `test/helpers/KraikenTestBase.sol` - Common test utilities for KRAIKEN contracts - Key tests: LiquidityManager.t.sol, Stake.t.sol, VWAPTracker.t.sol
- `test/helpers/PositionRenderer.sol` - Visualization tools for liquidity positions
### Key Test Files ## Code Guidelines
- `test/LiquidityManager.t.sol` - Core liquidity management tests
- `test/abstracts/ThreePositionStrategy.t.sol` - Position strategy validation
- `test/Stake.t.sol` - Harberger tax mechanism tests
- `test/VWAPTracker.t.sol` - Price memory and compression tests
## Code Quality Guidelines - **Check lib/uni-v3-lib** for existing Uniswap math
- **Use test/helpers** for common patterns
### CRITICAL: Avoid Duplicate Code - **Security**: Reentrancy protection, oracle validation
- **ALWAYS** check lib/uni-v3-lib for existing Uniswap math functions - **Gas**: Batch operations, optimize storage
- **NEVER** reimplement standard math operations
- Use test/helpers for common test patterns
### Security Considerations
- All external calls must be reentrancy protected
- Price oracles must validate against manipulation
- Tax calculations must handle edge cases (0 rates, max uint256)
### Gas Optimization
- Batch operations where possible
- Use storage patterns that minimize SSTORE operations
- Leverage Uniswap's existing math libraries
## Analysis Tools ## Analysis Tools
The `analysis/` subdirectory contains critical tools for understanding and hardening the protocol: See `analysis/CLAUDE.md` for fuzzing and attack vector testing.
- Growth mechanism simulations
- Attack vector analysis
- Liquidity depth scenarios
- See `analysis/README.md` for detailed usage
## Uniswap V3 Math - Critical Learnings ## Uniswap V3 Math
### Token Ordering and Price Representation See [UNISWAP_V3_MATH.md](UNISWAP_V3_MATH.md) for detailed math concepts.
#### Price Direction Reference Table
| Scenario | token0 | token1 | Price Represents | Lower Tick → Higher Tick |
|----------|--------|--------|------------------|---------------------------|
| **token0isETH = true** | ETH | KRAIKEN | ETH per KRAIKEN | KRAIKEN cheap → expensive |
| **token0isETH = false** | KRAIKEN | ETH | KRAIKEN per ETH | ETH cheap → expensive |
#### Understanding "Above" and "Below"
**Critical distinction**: "Price" in Uniswap V3 always refers to token1's price in units of token0.
**When token0isETH = true (ETH is token0, KRAIKEN is token1):**
- Price = KRAIKEN price in ETH (how much ETH to buy 1 KRAIKEN)
- Higher tick = Higher KRAIKEN price in ETH
- Lower tick = Lower KRAIKEN price in ETH
- "Price moved up" = KRAIKEN became more expensive = ETH became cheaper
- "Price moved down" = KRAIKEN became cheaper = ETH became more expensive
**When token0isETH = false (KRAIKEN is token0, ETH is token1):**
- Price = ETH price in KRAIKEN (how much KRAIKEN to buy 1 ETH)
- Higher tick = Higher ETH price in KRAIKEN
- Lower tick = Lower ETH price in KRAIKEN
- "Price moved up" = ETH became more expensive = KRAIKEN became cheaper
- "Price moved down" = ETH became cheaper = KRAIKEN became more expensive
This determines token composition:
| Current Price vs Position | Position Contains | Why |
|--------------------------|-------------------|-----|
| Below range (tick < tickLower) | 100% token1 | Token0 is too expensive to hold |
| Within range (tickLower ≤ tick ≤ tickUpper) | Both tokens | Active liquidity range |
| Above range (tick > tickUpper) | 100% token0 | Token1 is too expensive to hold |
### Liquidity vs Token Amounts
**Key Insight**: Liquidity (L) is a mathematical constant representing capital efficiency, NOT token count.
1. **Liquidity is invariant**: The liquidity value L doesn't change when price moves
2. **Token amounts are variable**: Depend on liquidity L, price range, and current price location
3. **Same L, different ranges**: Results in different token amounts due to price differences
### Why Positions at Different Ranges Have Different Token Ratios
For the same liquidity value L:
- **Position at lower ticks**: Higher token1 price → fewer token1, more token0 potential
- **Position at higher ticks**: Lower token1 price → more token1, less token0 potential
This explains why a position with fewer tokens can have more liquidity (and thus more price impact resistance).
### Liquidity Per Tick - The Critical Metric
When comparing positions of different widths, always normalize to liquidity per tick:
```solidity
liquidityPerTick = totalLiquidity / (tickUpper - tickLower)
```
The discovery position maintains its target liquidity density through width adjustment:
```solidity
// Ensure discovery has X times more liquidity per tick than anchor
discoveryLiquidity = anchorLiquidity * multiplier * discoveryWidth / anchorWidth
```
### Key Takeaways
1. **Liquidity ≠ Token Count**: Higher liquidity can mean fewer tokens at different price ranges
2. **Price Range Matters**: Token composition depends on where positions sit relative to current price
3. **Normalize for Width**: Always compare liquidity per tick when positions have different widths
4. **Token0 Ordering is Critical**: Determines which direction is "up" or "down" in price

144
onchain/UNISWAP_V3_MATH.md Normal file
View file

@ -0,0 +1,144 @@
# Uniswap V3 Math - Critical Learnings
## Token Ordering and Price Representation
### Price Direction Reference Table
| Scenario | token0 | token1 | Price Represents | Lower Tick → Higher Tick |
|----------|--------|--------|------------------|---------------------------|
| **token0isETH = true** | ETH | KRAIKEN | ETH per KRAIKEN | KRAIKEN cheap → expensive |
| **token0isETH = false** | KRAIKEN | ETH | KRAIKEN per ETH | ETH cheap → expensive |
### Understanding "Above" and "Below"
**Critical distinction**: "Price" in Uniswap V3 always refers to token1's price in units of token0.
**When token0isETH = true (ETH is token0, KRAIKEN is token1):**
- Price = KRAIKEN price in ETH (how much ETH to buy 1 KRAIKEN)
- Higher tick = Higher KRAIKEN price in ETH
- Lower tick = Lower KRAIKEN price in ETH
- "Price moved up" = KRAIKEN became more expensive = ETH became cheaper
- "Price moved down" = KRAIKEN became cheaper = ETH became more expensive
**When token0isETH = false (KRAIKEN is token0, ETH is token1):**
- Price = ETH price in KRAIKEN (how much KRAIKEN to buy 1 ETH)
- Higher tick = Higher ETH price in KRAIKEN
- Lower tick = Lower ETH price in KRAIKEN
- "Price moved up" = ETH became more expensive = KRAIKEN became cheaper
- "Price moved down" = ETH became cheaper = KRAIKEN became more expensive
This determines token composition:
| Current Price vs Position | Position Contains | Why |
|--------------------------|-------------------|-----|
| Below range (tick < tickLower) | 100% token1 | Token0 is too expensive to hold |
| Within range (tickLower ≤ tick ≤ tickUpper) | Both tokens | Active liquidity range |
| Above range (tick > tickUpper) | 100% token0 | Token1 is too expensive to hold |
## Liquidity vs Token Amounts
**Key Insight**: Liquidity (L) is a mathematical constant representing capital efficiency, NOT token count.
1. **Liquidity is invariant**: The liquidity value L doesn't change when price moves
2. **Token amounts are variable**: Depend on liquidity L, price range, and current price location
3. **Same L, different ranges**: Results in different token amounts due to price differences
### Why Positions at Different Ranges Have Different Token Ratios
For the same liquidity value L:
- **Position at lower ticks**: Higher token1 price → fewer token1, more token0 potential
- **Position at higher ticks**: Lower token1 price → more token1, less token0 potential
This explains why a position with fewer tokens can have more liquidity (and thus more price impact resistance).
### Liquidity Per Tick - The Critical Metric
When comparing positions of different widths, always normalize to liquidity per tick:
```solidity
liquidityPerTick = totalLiquidity / (tickUpper - tickLower)
```
The discovery position maintains its target liquidity density through width adjustment:
```solidity
// Ensure discovery has X times more liquidity per tick than anchor
discoveryLiquidity = anchorLiquidity * multiplier * discoveryWidth / anchorWidth
```
## Key Takeaways
1. **Liquidity ≠ Token Count**: Higher liquidity can mean fewer tokens at different price ranges
2. **Price Range Matters**: Token composition depends on where positions sit relative to current price
3. **Normalize for Width**: Always compare liquidity per tick when positions have different widths
4. **Token0 Ordering is Critical**: Determines which direction is "up" or "down" in price
## Critical Formulas for Out-of-Range Positions
### When Position is Below Current Price (currentTick < tickLower)
The position holds only token0:
```
amount0 = L * (sqrt(priceUpper) - sqrt(priceLower)) / (sqrt(priceUpper) * sqrt(priceLower))
```
In Solidity/JavaScript with Q96 notation:
```javascript
amount0 = liquidity * (sqrtRatioBX96 - sqrtRatioAX96) / sqrtRatioAX96 * Q96 / sqrtRatioBX96
```
### When Position is Above Current Price (currentTick > tickUpper)
The position holds only token1:
```
amount1 = L * (sqrt(priceUpper) - sqrt(priceLower))
```
In Solidity/JavaScript with Q96 notation:
```javascript
amount1 = liquidity * (sqrtRatioBX96 - sqrtRatioAX96) / Q96
```
### Critical Insight: ETH Position Creation
When creating a position above current price with ETH in a token0isWeth pool:
1. ETH is used as token1 (via getLiquidityForAmount1)
2. The position will hold KRAIKEN when in range
3. But if price drops below the range, it converts to holding ETH as token0
**Example**: Floor position created with 38 ETH at ticks [127400, 127600] when current is 123890:
- Creation: Uses getLiquidityForAmount1 with 38 ETH
- Result: Liquidity = 6.48e18
- Current holdings: 38 ETH (as token0, since price is below range)
## Common Confusion Points
### 1. Price Direction vs ETH Value
When token0isWeth = true:
- Higher tick = Higher price = More KRAIKEN per ETH = ETH is MORE valuable
- Lower tick = Lower price = Less KRAIKEN per ETH = ETH is LESS valuable
This is counterintuitive because "price goes up" means the denominated asset (ETH) becomes more valuable.
### 2. Position Token Holdings
A position's token composition depends ONLY on:
- Current price location relative to the position range
- NOT on how the position was created
- NOT on which token was used to mint
### 3. The Floor Position "Below Current Price" Confusion
In KRAIKEN's three-position strategy:
- Floor position is placed where ETH is MORE valuable (higher ticks when token0isWeth)
- This is "below" current price from an ETH value perspective
- But "above" current tick numerically
- Selling KRAIKEN for ETH moves price TOWARD the floor position
### 4. Liquidity Calculation Direction
When minting a position out of range:
- Use getLiquidityForAmount0 if providing token0
- Use getLiquidityForAmount1 if providing token1
- The same liquidity value will require different token amounts depending on which token you provide

View file

@ -1,131 +1,56 @@
# KRAIKEN LiquidityManager Fuzzing Analysis # KRAIKEN LiquidityManager Fuzzing Analysis
Tools for testing the KRAIKEN LiquidityManager's resilience against various trading strategies to identify scenarios where traders can profit. Tools for testing KRAIKEN's three-position strategy resilience against various market conditions and trading patterns.
## Quick Start ## Quick Start
### Using run-fuzzing.sh (Recommended)
The `run-fuzzing.sh` script provides an easy way to run fuzzing campaigns with different market optimizers:
```bash ```bash
# Basic usage - run with specific optimizer # Run with specific optimizer (50 runs default)
./analysis/run-fuzzing.sh BullMarketOptimizer ./analysis/run-fuzzing.sh BullMarketOptimizer
# Specify number of runs (default: 50) # Custom runs and trades
./analysis/run-fuzzing.sh WhaleOptimizer runs=100 ./analysis/run-fuzzing.sh WhaleOptimizer runs=100 trades=30
# Specify trades per run (default: 20, actual will be ±5) # Debug mode with position tracking CSV (forces runs=1)
./analysis/run-fuzzing.sh BearMarketOptimizer runs=10 trades=50
# Debug mode - generates position tracking CSV (forces runs=1)
./analysis/run-fuzzing.sh NeutralMarketOptimizer debugCSV ./analysis/run-fuzzing.sh NeutralMarketOptimizer debugCSV
# Multiple parameters
./analysis/run-fuzzing.sh BullMarketOptimizer runs=25 trades=30
``` ```
**Available optimizers:** ## Available Optimizers
- `BullMarketOptimizer` - Biased towards buying
- `NeutralMarketOptimizer` - Balanced trading - `BullMarketOptimizer` - Buying bias
- `BearMarketOptimizer` - Biased towards selling - `NeutralMarketOptimizer` - Balanced trading
- `WhaleOptimizer` - Large position trading - `BearMarketOptimizer` - Selling bias
- `MockOptimizer` - Test optimizer - `WhaleOptimizer` - Large positions
- `RandomScenarioOptimizer` - Random behavior - `RandomScenarioOptimizer` - Random behavior
**Features:** ## Output Structure
- Automatic results aggregation and summary generation
- Progress tracking with colored output
- Cumulative P&L calculation across all runs
- Automatic visualization launch for profitable scenarios
- Organized output in timestamped directories
### Manual Fuzzing (Advanced) Each campaign creates `fuzzing_results_[optimizer]_[timestamp]/`:
- `config.txt` - Campaign parameters
```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
### run-fuzzing.sh Parameters
- **optimizer_class**: Required. The optimizer class to use (e.g., BullMarketOptimizer)
- **runs=N**: Optional. Number of fuzzing runs (default: 50)
- **trades=N**: Optional. Trades per run (default: 20, actual will be ±5)
- **debugCSV**: Optional. Enable debug mode with position tracking CSV (forces runs=1)
### Environment Variables (Manual Mode)
- **FUZZING_RUNS**: Number of random trading scenarios per market type (default: 100)
- **TRACK_POSITIONS**: Enable detailed position tracking CSV output (default: false)
- **OPTIMIZER_CLASS**: The optimizer to use (default: BullMarketOptimizer)
- **TRADES_PER_RUN**: Number of trades per run (default: 20)
- **SEED_OFFSET**: Starting seed for random number generation (default: 0)
## 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
### Using run-fuzzing.sh
Each campaign creates a timestamped directory: `fuzzing_results_[optimizer]_[timestamp]/`
- `config.txt` - Campaign configuration
- `run_*.log` - Individual run logs - `run_*.log` - Individual run logs
- `merged_profitable_scenarios.csv` - All profitable scenarios combined - `merged_profitable_scenarios.csv` - Profitable scenarios combined
- `summary.txt` - Campaign summary with statistics - `summary.txt` - Statistics and cumulative P&L
- `debug_positions_*.csv` - Position tracking data (when debugCSV is used) - `debug_positions_*.csv` - Position data (debugCSV mode only)
### Manual Mode
- `profitable_scenarios_[timestamp].csv` - Details of all profitable trading sequences
- `positions_[scenario]_[seed].csv` - Liquidity position data (only with TRACK_POSITIONS=true)
## Visualization ## Visualization
```bash ```bash
# View results in browser # Automatic launch with debugCSV
python3 -m http.server 8000 ./analysis/run-fuzzing.sh [optimizer] debugCSV
# Open http://localhost:8000/scenario-visualizer.html
# Or use the shell script # Manual server (port 8000)
./view-scenarios.sh ./analysis/view-scenarios.sh
``` ```
## Analysis Tools ## Advanced Usage
- `AnalysisVisualizer.py` - Generates charts from CSV data
- `scenario-visualizer.html` - Interactive web visualization
- `RISK_ANALYSIS_FINDINGS.md` - Summary of discovered vulnerabilities
## Components
- `run-fuzzing.sh` - Main campaign runner with automatic visualization
- `FuzzingAnalysis.s.sol` - Core fuzzing script
- `helpers/SwapExecutor.sol` - Shared swap execution logic
- `helpers/CSVManager.sol` - CSV generation utilities
- `helpers/CSVHelper.sol` - CSV formatting helpers
## Example Campaign Comparison
To run fuzzing campaigns comparing different market optimizers:
```bash ```bash
# Run campaigns for all three market conditions # Manual fuzzing with environment variables
./analysis/run-fuzzing.sh BullMarketOptimizer runs=100 FUZZING_RUNS=500 TRACK_POSITIONS=true forge script analysis/FuzzingAnalysis.s.sol --ffi --via-ir
./analysis/run-fuzzing.sh NeutralMarketOptimizer runs=100
./analysis/run-fuzzing.sh BearMarketOptimizer runs=100
# Check results
cat fuzzing_results_*/summary.txt | grep -E "(Optimizer:|Success rate:|Average P&L)"
``` ```
Environment variables:
- `FUZZING_RUNS` - Scenarios per market (default: 100)
- `TRACK_POSITIONS` - Enable position CSV (default: false)
- `OPTIMIZER_CLASS` - Optimizer to use
- `TRADES_PER_RUN` - Trades per run (default: 20)

View file

@ -343,14 +343,6 @@ contract FuzzingAnalysis is Test, CSVManager {
(uint128 anchorLiq, int24 anchorLower, int24 anchorUpper) = lm.positions(ThreePositionStrategy.Stage.ANCHOR); (uint128 anchorLiq, int24 anchorLower, int24 anchorUpper) = lm.positions(ThreePositionStrategy.Stage.ANCHOR);
(uint128 discoveryLiq, int24 discoveryLower, int24 discoveryUpper) = lm.positions(ThreePositionStrategy.Stage.DISCOVERY); (uint128 discoveryLiq, int24 discoveryLower, int24 discoveryUpper) = lm.positions(ThreePositionStrategy.Stage.DISCOVERY);
// Calculate ETH and HARB amounts in each position using proper Uniswap math
uint256 floorEth = 0;
uint256 floorHarb = 0;
uint256 anchorEth = 0;
uint256 anchorHarb = 0;
uint256 discoveryEth = 0;
uint256 discoveryHarb = 0;
// Debug: Log liquidity values // Debug: Log liquidity values
if (keccak256(bytes(label)) == keccak256(bytes("Initial")) || keccak256(bytes(label)) == keccak256(bytes("Recenter_2"))) { if (keccak256(bytes(label)) == keccak256(bytes("Initial")) || keccak256(bytes(label)) == keccak256(bytes("Recenter_2"))) {
console.log("=== LIQUIDITY VALUES ==="); console.log("=== LIQUIDITY VALUES ===");
@ -372,125 +364,19 @@ contract FuzzingAnalysis is Test, CSVManager {
} }
} }
// Calculate amounts for each position using LiquidityAmounts library // Create position data row with liquidity values directly
if (floorLiq > 0) {
uint160 sqrtRatioAX96 = TickMath.getSqrtRatioAtTick(floorLower);
uint160 sqrtRatioBX96 = TickMath.getSqrtRatioAtTick(floorUpper);
// Calculate actual deposited amounts based on position relative to current price
if (token0isWeth) {
if (currentTick < floorLower) {
// Position is above current price - contains only token1 (KRAIKEN)
floorEth = 0;
// Use position's lower tick for actual deposited amount
floorHarb = LiquidityAmounts.getAmount1ForLiquidity(sqrtRatioAX96, sqrtRatioBX96, floorLiq);
} else if (currentTick >= floorUpper) {
// Position is below current price - contains only token0 (WETH)
// Use position's upper tick for actual deposited amount
floorEth = LiquidityAmounts.getAmount0ForLiquidity(sqrtRatioAX96, sqrtRatioBX96, floorLiq);
floorHarb = 0;
} else {
// Current price is within the position
uint160 sqrtPriceX96 = TickMath.getSqrtRatioAtTick(currentTick);
floorEth = LiquidityAmounts.getAmount0ForLiquidity(sqrtPriceX96, sqrtRatioBX96, floorLiq);
floorHarb = LiquidityAmounts.getAmount1ForLiquidity(sqrtRatioAX96, sqrtPriceX96, floorLiq);
}
} else {
if (currentTick < floorLower) {
// Position is above current price
floorHarb = LiquidityAmounts.getAmount0ForLiquidity(sqrtRatioAX96, sqrtRatioBX96, floorLiq);
floorEth = 0;
} else if (currentTick >= floorUpper) {
// Position is below current price
floorHarb = 0;
floorEth = LiquidityAmounts.getAmount1ForLiquidity(sqrtRatioAX96, sqrtRatioBX96, floorLiq);
} else {
// Current price is within the position
uint160 sqrtPriceX96 = TickMath.getSqrtRatioAtTick(currentTick);
floorHarb = LiquidityAmounts.getAmount0ForLiquidity(sqrtPriceX96, sqrtRatioBX96, floorLiq);
floorEth = LiquidityAmounts.getAmount1ForLiquidity(sqrtRatioAX96, sqrtPriceX96, floorLiq);
}
}
}
if (anchorLiq > 0) {
uint160 sqrtRatioAX96 = TickMath.getSqrtRatioAtTick(anchorLower);
uint160 sqrtRatioBX96 = TickMath.getSqrtRatioAtTick(anchorUpper);
if (token0isWeth) {
if (currentTick < anchorLower) {
anchorEth = 0;
anchorHarb = LiquidityAmounts.getAmount1ForLiquidity(sqrtRatioAX96, sqrtRatioBX96, anchorLiq);
} else if (currentTick >= anchorUpper) {
anchorEth = LiquidityAmounts.getAmount0ForLiquidity(sqrtRatioAX96, sqrtRatioBX96, anchorLiq);
anchorHarb = 0;
} else {
uint160 sqrtPriceX96 = TickMath.getSqrtRatioAtTick(currentTick);
anchorEth = LiquidityAmounts.getAmount0ForLiquidity(sqrtPriceX96, sqrtRatioBX96, anchorLiq);
anchorHarb = LiquidityAmounts.getAmount1ForLiquidity(sqrtRatioAX96, sqrtPriceX96, anchorLiq);
}
} else {
if (currentTick < anchorLower) {
anchorHarb = LiquidityAmounts.getAmount0ForLiquidity(sqrtRatioAX96, sqrtRatioBX96, anchorLiq);
anchorEth = 0;
} else if (currentTick >= anchorUpper) {
anchorHarb = 0;
anchorEth = LiquidityAmounts.getAmount1ForLiquidity(sqrtRatioAX96, sqrtRatioBX96, anchorLiq);
} else {
uint160 sqrtPriceX96 = TickMath.getSqrtRatioAtTick(currentTick);
anchorHarb = LiquidityAmounts.getAmount0ForLiquidity(sqrtPriceX96, sqrtRatioBX96, anchorLiq);
anchorEth = LiquidityAmounts.getAmount1ForLiquidity(sqrtRatioAX96, sqrtPriceX96, anchorLiq);
}
}
}
if (discoveryLiq > 0) {
uint160 sqrtRatioAX96 = TickMath.getSqrtRatioAtTick(discoveryLower);
uint160 sqrtRatioBX96 = TickMath.getSqrtRatioAtTick(discoveryUpper);
if (token0isWeth) {
if (currentTick < discoveryLower) {
discoveryEth = 0;
discoveryHarb = LiquidityAmounts.getAmount1ForLiquidity(sqrtRatioAX96, sqrtRatioBX96, discoveryLiq);
} else if (currentTick >= discoveryUpper) {
discoveryEth = LiquidityAmounts.getAmount0ForLiquidity(sqrtRatioAX96, sqrtRatioBX96, discoveryLiq);
discoveryHarb = 0;
} else {
uint160 sqrtPriceX96 = TickMath.getSqrtRatioAtTick(currentTick);
discoveryEth = LiquidityAmounts.getAmount0ForLiquidity(sqrtPriceX96, sqrtRatioBX96, discoveryLiq);
discoveryHarb = LiquidityAmounts.getAmount1ForLiquidity(sqrtRatioAX96, sqrtPriceX96, discoveryLiq);
}
} else {
if (currentTick < discoveryLower) {
discoveryHarb = LiquidityAmounts.getAmount0ForLiquidity(sqrtRatioAX96, sqrtRatioBX96, discoveryLiq);
discoveryEth = 0;
} else if (currentTick >= discoveryUpper) {
discoveryHarb = 0;
discoveryEth = LiquidityAmounts.getAmount1ForLiquidity(sqrtRatioAX96, sqrtRatioBX96, discoveryLiq);
} else {
uint160 sqrtPriceX96 = TickMath.getSqrtRatioAtTick(currentTick);
discoveryHarb = LiquidityAmounts.getAmount0ForLiquidity(sqrtPriceX96, sqrtRatioBX96, discoveryLiq);
discoveryEth = LiquidityAmounts.getAmount1ForLiquidity(sqrtRatioAX96, sqrtPriceX96, discoveryLiq);
}
}
}
// Create position data row matching the expected CSV format
string memory row = string.concat( string memory row = string.concat(
label, ", ", label, ", ",
vm.toString(currentTick), ", ", vm.toString(currentTick), ", ",
vm.toString(floorLower), ", ", vm.toString(floorLower), ", ",
vm.toString(floorUpper), ", ", vm.toString(floorUpper), ", ",
vm.toString(floorEth), ", ", vm.toString(uint256(floorLiq)), ", ",
vm.toString(floorHarb), ", ",
vm.toString(anchorLower), ", ", vm.toString(anchorLower), ", ",
vm.toString(anchorUpper), ", ", vm.toString(anchorUpper), ", ",
vm.toString(anchorEth), ", ", vm.toString(uint256(anchorLiq)), ", ",
vm.toString(anchorHarb), ", ",
vm.toString(discoveryLower), ", ", vm.toString(discoveryLower), ", ",
vm.toString(discoveryUpper), ", ", vm.toString(discoveryUpper), ", ",
vm.toString(discoveryEth), ", ", vm.toString(uint256(discoveryLiq)), ", ",
vm.toString(discoveryHarb), ", ",
token0isWeth ? "true" : "false" token0isWeth ? "true" : "false"
); );
appendCSVRow(row); appendCSVRow(row);

View file

@ -15,7 +15,7 @@ library CSVHelper {
*/ */
function createPositionsHeader() internal pure returns (string memory) { function createPositionsHeader() internal pure returns (string memory) {
return return
"precedingAction, currentTick, floorTickLower, floorTickUpper, floorToken0, floorToken1, anchorTickLower, anchorTickUpper, anchorToken0, anchorToken1, discoveryTickLower, discoveryTickUpper, discoveryToken0, discoveryToken1, token0isWeth"; "precedingAction, currentTick, floorTickLower, floorTickUpper, floorLiquidity, anchorTickLower, anchorTickUpper, anchorLiquidity, discoveryTickLower, discoveryTickUpper, discoveryLiquidity, token0isWeth";
} }
function createTimeSeriesHeader() internal pure returns (string memory) { function createTimeSeriesHeader() internal pure returns (string memory) {

View file

@ -343,36 +343,66 @@ if [ "$CSV_GENERATED" = true ] && [ -n "$LATEST_CSV" ]; then
# Use absolute path for the symlink # Use absolute path for the symlink
ln -s "$(pwd)/$LATEST_CSV" "$TEMP_LINK" ln -s "$(pwd)/$LATEST_CSV" "$TEMP_LINK"
# Start the viewer in background in its own process group # Check if server is already running on common ports
setsid ./view-scenarios.sh & SERVER_RUNNING=false
VIEWER_PID=$! 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
# Give the server time to start and browser to open if [ "$SERVER_RUNNING" = true ]; then
sleep 2 echo -e "${YELLOW}Server already running on port $EXISTING_PORT${NC}"
echo -e "${GREEN}Browser should open to: http://localhost:$EXISTING_PORT/scenario-visualizer.html${NC}"
# Show the URL
echo "" # Try to open browser to existing server
echo -e "${GREEN}Browser should open to: http://localhost:8000/scenario-visualizer.html${NC}" if command -v xdg-open &> /dev/null; then
echo "(If port 8000 was busy, check the port number mentioned above)" xdg-open "http://localhost:$EXISTING_PORT/scenario-visualizer.html" 2>/dev/null &
echo "If browser didn't open, manually navigate to that URL" elif command -v open &> /dev/null; then
open "http://localhost:$EXISTING_PORT/scenario-visualizer.html" 2>/dev/null &
# Wait for user input fi
echo ""
echo -e "${YELLOW}Press Enter to stop the viewer and exit...${NC}" echo ""
read -r echo -e "${YELLOW}Press Enter to exit (server will keep running)...${NC}"
read -r
# Kill the viewer process and its children else
if [ -n "$VIEWER_PID" ]; then # Start the viewer in background in its own process group
# Kill the entire process group (includes python server) setsid ./view-scenarios.sh &
pkill -TERM -g $VIEWER_PID 2>/dev/null || true VIEWER_PID=$!
# Give it a moment to clean up
sleep 1 # Give the server time to start and browser to open
# Force kill if still running sleep 2
pkill -KILL -g $VIEWER_PID 2>/dev/null || true
# Show the URL
echo ""
echo -e "${GREEN}Browser should open to: http://localhost:8000/scenario-visualizer.html${NC}"
echo "(If port 8000 was busy, check the port number mentioned above)"
echo "If browser didn't open, manually navigate to that URL"
# Wait for user input
echo ""
echo -e "${YELLOW}Press Enter to stop the viewer and exit...${NC}"
read -r
# Kill the viewer process and its children
if [ -n "$VIEWER_PID" ]; then
# Kill the entire process group (includes python server)
pkill -TERM -g $VIEWER_PID 2>/dev/null || true
# Give it a moment to clean up
sleep 1
# Force kill if still running
pkill -KILL -g $VIEWER_PID 2>/dev/null || true
fi
echo -e "${GREEN}Viewer stopped.${NC}"
fi fi
# Clean up the symlink # Clean up the symlink
rm -f "$TEMP_LINK" rm -f "$TEMP_LINK"
echo -e "${GREEN}Viewer stopped.${NC}"
fi fi

View file

@ -133,16 +133,17 @@
<em>Discovery</em>: Edge liquidity position - holds KRAIKEN when ETH is expensive (above current price)<br> <em>Discovery</em>: Edge liquidity position - holds KRAIKEN when ETH is expensive (above current price)<br>
<br> <br>
<strong>Price Multiples:</strong> Shows ETH price relative to current (1x):<br> <strong>Price Multiples:</strong> Shows ETH price relative to current (1x):<br>
0.5x = ETH is half as expensive (Floor position holds ETH)<br> &lt; 1x = ETH is cheaper than current price (positions below current hold ETH)<br>
• 1x = Current ETH price (red dashed line)<br> • 1x = Current ETH price (red dashed line)<br>
2x = ETH is twice as expensive (Discovery position holds KRAIKEN)<br> &gt; 1x = ETH is more expensive than current price (positions above current hold KRAIKEN)<br>
<br> <br>
<em>Note: The x-axis automatically adjusts based on token ordering in the pool</em> <em>Note: The x-axis automatically adjusts based on token ordering in the pool</em><br>
<br>
<strong>Navigation:</strong> Use the Previous/Next buttons or URL parameter <code>?row=N</code> to view specific CSV rows
</div> </div>
<div id="status">Loading profitable scenario data...</div> <div id="status">Loading profitable scenario data...</div>
<textarea id="csvInput" placeholder="Paste CSV formatted data here..." style="display: none;"></textarea> <textarea id="csvInput" placeholder="Paste CSV formatted data here..." style="display: none;"></textarea>
<button onclick="parseAndSimulateCSV()" style="display: none;">Simulate CSV Data</button> <button onclick="parseAndSimulateCSV()" style="display: none;">Simulate CSV Data</button>
<button onclick="toggleManualInput()" id="manualButton">Manual Input Mode</button>
<div id="simulations"></div> <div id="simulations"></div>
<script> <script>
@ -171,6 +172,13 @@
// - Anchor Position: Mixed tokens around current price for shallow liquidity // - Anchor Position: Mixed tokens around current price for shallow liquidity
// - Discovery Position: Edge liquidity - holds ETH above price, KRAIKEN below price // - Discovery Position: Edge liquidity - holds ETH above price, KRAIKEN below price
// 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
}
// Auto-load CSV data on page load // Auto-load CSV data on page load
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
loadCSVData(); loadCSVData();
@ -208,25 +216,10 @@
<em>If no CSV exists, run: forge script analysis/SimpleAnalysis.s.sol --ffi</em> <em>If no CSV exists, run: forge script analysis/SimpleAnalysis.s.sol --ffi</em>
</div> </div>
`; `;
console.log('CSV load error:', error); // CSV load error - handled by status message above
}); });
} }
function toggleManualInput() {
const csvInput = document.getElementById('csvInput');
const button = document.getElementById('manualButton');
const parseButton = document.querySelector('button[onclick="parseAndSimulateCSV()"]');
if (csvInput.style.display === 'none') {
csvInput.style.display = 'block';
parseButton.style.display = 'inline-block';
button.textContent = 'Hide Manual Input';
} else {
csvInput.style.display = 'none';
parseButton.style.display = 'none';
button.textContent = 'Manual Input Mode';
}
}
function parseCSV(csv) { function parseCSV(csv) {
const lines = csv.trim().split('\n'); const lines = csv.trim().split('\n');
@ -252,37 +245,121 @@
} }
function simulateCSVData(data) { function simulateCSVData(data) {
let previousRow = null; // Get selected row from URL parameter
const selectedIndex = getRowParameter();
data.forEach((row, index) => {
// Add current row info and navigation
const currentRowInfo = document.createElement('div');
currentRowInfo.style.cssText = 'margin: 20px 0; padding: 15px; background: #f8f9fa; border-radius: 4px; text-align: center;';
currentRowInfo.innerHTML = `
<strong>Currently viewing: Line ${selectedIndex + 2} - ${data[selectedIndex].precedingAction}</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 not first row
if (selectedIndex > 0) {
const topNavDiv = document.createElement('div');
topNavDiv.style.cssText = 'margin: 20px 0; text-align: center;';
topNavDiv.innerHTML = `
<button onclick="navigateRow(-1)" style="padding: 10px 20px; font-size: 16px;">
← Previous Row (Line ${selectedIndex + 1})
</button>
`;
document.getElementById('simulations').appendChild(topNavDiv);
}
// Process only the selected row
let previousRow = selectedIndex > 0 ? data[selectedIndex - 1] : null;
if (selectedIndex >= 0 && selectedIndex < data.length) {
const row = data[selectedIndex];
const index = selectedIndex;
const precedingAction = row.precedingAction; const precedingAction = row.precedingAction;
const currentTick = parseFloat(row.currentTick); const currentTick = parseFloat(row.currentTick);
const token0isWeth = row.token0isWeth === 'true' || row.token0isWeth === true; const token0isWeth = row.token0isWeth === 'true' || row.token0isWeth === true;
const floorTickLower = parseFloat(row.floorTickLower); const floorTickLower = parseFloat(row.floorTickLower);
const floorTickUpper = parseFloat(row.floorTickUpper); const floorTickUpper = parseFloat(row.floorTickUpper);
// Swap floor values to match expected behavior const floorLiquidity = parseFloat(row.floorLiquidity || 0);
const floorEth = parseFloat(row.floorToken1 || 0) / 1e18;
const floorKraiken = parseFloat(row.floorToken0 || 0) / 1e18;
const anchorTickLower = parseFloat(row.anchorTickLower); const anchorTickLower = parseFloat(row.anchorTickLower);
const anchorTickUpper = parseFloat(row.anchorTickUpper); const anchorTickUpper = parseFloat(row.anchorTickUpper);
const anchorEth = parseFloat(row.anchorToken0 || 0) / 1e18; const anchorLiquidity = parseFloat(row.anchorLiquidity || 0);
const anchorKraiken = parseFloat(row.anchorToken1 || 0) / 1e18;
const discoveryTickLower = parseFloat(row.discoveryTickLower); const discoveryTickLower = parseFloat(row.discoveryTickLower);
const discoveryTickUpper = parseFloat(row.discoveryTickUpper); const discoveryTickUpper = parseFloat(row.discoveryTickUpper);
// Swap discovery values to match expected behavior const discoveryLiquidity = parseFloat(row.discoveryLiquidity || 0);
const discoveryEth = parseFloat(row.discoveryToken1 || 0) / 1e18;
const discoveryKraiken = parseFloat(row.discoveryToken0 || 0) / 1e18; // Calculate token amounts from liquidity
const floorAmounts = getAmountsForLiquidity(floorLiquidity, floorTickLower, floorTickUpper, currentTick);
const anchorAmounts = getAmountsForLiquidity(anchorLiquidity, anchorTickLower, anchorTickUpper, currentTick);
const discoveryAmounts = getAmountsForLiquidity(discoveryLiquidity, discoveryTickLower, discoveryTickUpper, currentTick);
// FIXED calculation - properly determine ETH amounts based on token ordering
let floorEthAmount, anchorEthAmount, discoveryEthAmount;
let floorKraikenAmount, anchorKraikenAmount, discoveryKraikenAmount;
// Simply use the amounts from getAmountsForLiquidity and determine which is ETH
if (token0isWeth) {
// token0 is WETH, token1 is KRAIKEN
floorEthAmount = floorAmounts.amount0 / 1e18;
floorKraikenAmount = floorAmounts.amount1 / 1e18;
anchorEthAmount = anchorAmounts.amount0 / 1e18;
anchorKraikenAmount = anchorAmounts.amount1 / 1e18;
discoveryEthAmount = discoveryAmounts.amount0 / 1e18;
discoveryKraikenAmount = discoveryAmounts.amount1 / 1e18;
} else {
// token0 is KRAIKEN, token1 is WETH
floorEthAmount = floorAmounts.amount1 / 1e18;
floorKraikenAmount = floorAmounts.amount0 / 1e18;
anchorEthAmount = anchorAmounts.amount1 / 1e18;
anchorKraikenAmount = anchorAmounts.amount0 / 1e18;
discoveryEthAmount = discoveryAmounts.amount1 / 1e18;
discoveryKraikenAmount = discoveryAmounts.amount0 / 1e18;
}
const totalEth = floorEthAmount + anchorEthAmount + discoveryEthAmount;
// Use the already calculated ETH and KRAIKEN amounts from above
let floorEth = floorEthAmount;
let floorKraiken = floorKraikenAmount;
let anchorEth = anchorEthAmount;
let anchorKraiken = anchorKraikenAmount;
let discoveryEth = discoveryEthAmount;
let discoveryKraiken = discoveryKraikenAmount;
let actionAmount = ''; let actionAmount = '';
let additionalInfo = ''; let additionalInfo = '';
if (previousRow) { if (previousRow) {
const prevFloorEth = parseFloat(previousRow.floorToken1 || 0) / 1e18; // Calculate previous token amounts from liquidity
const prevFloorKraiken = parseFloat(previousRow.floorToken0 || 0) / 1e18; const prevCurrentTick = parseFloat(previousRow.currentTick);
const prevAnchorEth = parseFloat(previousRow.anchorToken0 || 0) / 1e18; const prevFloorLiquidity = parseFloat(previousRow.floorLiquidity || 0);
const prevAnchorKraiken = parseFloat(previousRow.anchorToken1 || 0) / 1e18; const prevAnchorLiquidity = parseFloat(previousRow.anchorLiquidity || 0);
const prevDiscoveryEth = parseFloat(previousRow.discoveryToken1 || 0) / 1e18; const prevDiscoveryLiquidity = parseFloat(previousRow.discoveryLiquidity || 0);
const prevDiscoveryKraiken = parseFloat(previousRow.discoveryToken0 || 0) / 1e18;
const prevFloorAmounts = getAmountsForLiquidity(prevFloorLiquidity, floorTickLower, floorTickUpper, prevCurrentTick);
const prevAnchorAmounts = getAmountsForLiquidity(prevAnchorLiquidity, anchorTickLower, anchorTickUpper, prevCurrentTick);
const prevDiscoveryAmounts = getAmountsForLiquidity(prevDiscoveryLiquidity, discoveryTickLower, discoveryTickUpper, prevCurrentTick);
let prevFloorEth, prevFloorKraiken, prevAnchorEth, prevAnchorKraiken, prevDiscoveryEth, prevDiscoveryKraiken;
if (token0isWeth === true) {
prevFloorEth = prevFloorAmounts.amount0 / 1e18;
prevFloorKraiken = prevFloorAmounts.amount1 / 1e18;
prevAnchorEth = prevAnchorAmounts.amount0 / 1e18;
prevAnchorKraiken = prevAnchorAmounts.amount1 / 1e18;
prevDiscoveryEth = prevDiscoveryAmounts.amount0 / 1e18;
prevDiscoveryKraiken = prevDiscoveryAmounts.amount1 / 1e18;
} else {
prevFloorEth = prevFloorAmounts.amount1 / 1e18;
prevFloorKraiken = prevFloorAmounts.amount0 / 1e18;
prevAnchorEth = prevAnchorAmounts.amount1 / 1e18;
prevAnchorKraiken = prevAnchorAmounts.amount0 / 1e18;
prevDiscoveryEth = prevDiscoveryAmounts.amount1 / 1e18;
prevDiscoveryKraiken = prevDiscoveryAmounts.amount0 / 1e18;
}
const ethDifference = (floorEth + anchorEth + discoveryEth) - (prevFloorEth + prevAnchorEth + prevDiscoveryEth); const ethDifference = (floorEth + anchorEth + discoveryEth) - (prevFloorEth + prevAnchorEth + prevDiscoveryEth);
const kraikenDifference = (floorKraiken + anchorKraiken + discoveryKraiken) - (prevFloorKraiken + prevAnchorKraiken + prevDiscoveryKraiken); const kraikenDifference = (floorKraiken + anchorKraiken + discoveryKraiken) - (prevFloorKraiken + prevAnchorKraiken + prevDiscoveryKraiken);
@ -302,19 +379,83 @@
const lineNumber = index + 2; const lineNumber = index + 2;
const headline = `Line ${lineNumber}: ${precedingAction} ${additionalInfo}`; const headline = `Line ${lineNumber}: ${precedingAction} ${additionalInfo}`;
simulateEnhanced(headline, currentTick, simulateEnhanced(headline, currentTick,
floorTickLower, floorTickUpper, floorEth, floorKraiken, floorTickLower, floorTickUpper, floorEth, floorKraiken, floorLiquidity,
anchorTickLower, anchorTickUpper, anchorEth, anchorKraiken, anchorTickLower, anchorTickUpper, anchorEth, anchorKraiken, anchorLiquidity,
discoveryTickLower, discoveryTickUpper, discoveryEth, discoveryKraiken, token0isWeth); discoveryTickLower, discoveryTickUpper, discoveryEth, discoveryKraiken, discoveryLiquidity, token0isWeth, index, precedingAction);
previousRow = row;
}); // Add navigation buttons below
const bottomNavDiv = document.createElement('div');
bottomNavDiv.style.cssText = 'margin: 20px 0; text-align: center;';
const prevButton = selectedIndex > 0 ?
`<button onclick="navigateRow(-1)" style="padding: 10px 20px; font-size: 16px; margin-right: 20px;">
← Previous Row (Line ${selectedIndex + 1})
</button>` : '';
const nextButton = selectedIndex < data.length - 1 ?
`<button onclick="navigateRow(1)" style="padding: 10px 20px; font-size: 16px;">
Next Row (Line ${selectedIndex + 3}) →
</button>` : '';
bottomNavDiv.innerHTML = prevButton + nextButton;
document.getElementById('simulations').appendChild(bottomNavDiv);
}
}
// Function to navigate between rows
function navigateRow(direction) {
const currentIndex = getRowParameter();
const newLineNumber = currentIndex + direction + 2; // Convert back to CSV line number
const url = new URL(window.location);
url.searchParams.set('row', newLineNumber);
window.location = url;
} }
// Uniswap V3 liquidity calculation functions // Uniswap V3 liquidity calculation functions
function tickToPrice(tick) { function tickToPrice(tick) {
return Math.pow(1.0001, tick); return Math.pow(1.0001, tick);
} }
function tickToSqrtPriceX96(tick) {
return Math.sqrt(Math.pow(1.0001, tick)) * (2 ** 96);
}
// Calculate token amounts from liquidity for a position
function getAmountsForLiquidity(liquidity, tickLower, tickUpper, currentTick) {
// Sort ticks to ensure tickLower < tickUpper
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) {
// Current price is below the range, position holds only token0
// When position was created above current price, ETH was used as amount1
// Use the amount1 formula to get the ETH that was deposited
amount0 = liquidity * (sqrtRatioBX96 - sqrtRatioAX96) / Q96;
} else if (currentTick >= tickUpper) {
// Current price is above the range, position holds only token1
// amount1 = liquidity * (sqrtRatioBX96 - sqrtRatioAX96) / Q96
amount1 = liquidity * (sqrtRatioBX96 - sqrtRatioAX96) / Q96;
} else {
// Current price is within the range
// amount0 = liquidity * (sqrtRatioBX96 - sqrtRatioX96) / (sqrtRatioX96 * sqrtRatioBX96) * Q96
amount0 = liquidity * Q96 * (sqrtRatioBX96 - sqrtRatioX96) / sqrtRatioBX96 / sqrtRatioX96;
// amount1 = liquidity * (sqrtRatioX96 - sqrtRatioAX96) / Q96
amount1 = liquidity * (sqrtRatioX96 - sqrtRatioAX96) / Q96;
}
return { amount0, amount1 };
}
function priceToSqrtPrice(price) { function priceToSqrtPrice(price) {
return Math.sqrt(price); return Math.sqrt(price);
} }
@ -372,18 +513,6 @@
} }
// Debug logging // Debug logging
if (positionName) {
console.log(`${positionName} liquidity calculation:`, {
token0Amount,
token1Amount,
tickRange: [tickLower, tickUpper],
sqrtPriceLower,
sqrtPriceUpper,
sqrtPriceDiff: sqrtPriceUpper - sqrtPriceLower,
liquidity,
calculatedFrom
});
}
return liquidity; return liquidity;
} }
@ -438,9 +567,9 @@
} }
function simulateEnhanced(precedingAction, currentTick, function simulateEnhanced(precedingAction, currentTick,
floorTickLower, floorTickUpper, floorEth, floorKraiken, floorTickLower, floorTickUpper, floorEth, floorKraiken, floorLiquidity,
anchorTickLower, anchorTickUpper, anchorEth, anchorKraiken, anchorTickLower, anchorTickUpper, anchorEth, anchorKraiken, anchorLiquidity,
discoveryTickLower, discoveryTickUpper, discoveryEth, discoveryKraiken, token0isWeth) { discoveryTickLower, discoveryTickUpper, discoveryEth, discoveryKraiken, discoveryLiquidity, token0isWeth, index, originalAction) {
// Position data structure with liquidity calculations // Position data structure with liquidity calculations
const positions = { const positions = {
@ -450,9 +579,7 @@
eth: floorEth, eth: floorEth,
kraiken: floorKraiken, kraiken: floorKraiken,
name: 'Floor', name: 'Floor',
liquidity: token0isWeth ? liquidity: floorLiquidity
calculateInvariantLiquidity(floorEth, floorKraiken, floorTickLower, floorTickUpper, 'Floor') :
calculateInvariantLiquidity(floorKraiken, floorEth, floorTickLower, floorTickUpper, 'Floor')
}, },
anchor: { anchor: {
tickLower: anchorTickLower, tickLower: anchorTickLower,
@ -460,9 +587,7 @@
eth: anchorEth, eth: anchorEth,
kraiken: anchorKraiken, kraiken: anchorKraiken,
name: 'Anchor (Shallow Pool)', name: 'Anchor (Shallow Pool)',
liquidity: token0isWeth ? liquidity: anchorLiquidity
calculateInvariantLiquidity(anchorEth, anchorKraiken, anchorTickLower, anchorTickUpper, 'Anchor') :
calculateInvariantLiquidity(anchorKraiken, anchorEth, anchorTickLower, anchorTickUpper, 'Anchor')
}, },
discovery: { discovery: {
tickLower: discoveryTickLower, tickLower: discoveryTickLower,
@ -470,35 +595,10 @@
eth: discoveryEth, eth: discoveryEth,
kraiken: discoveryKraiken, kraiken: discoveryKraiken,
name: 'Discovery', name: 'Discovery',
liquidity: token0isWeth ? liquidity: discoveryLiquidity
calculateInvariantLiquidity(discoveryEth, discoveryKraiken, discoveryTickLower, discoveryTickUpper, 'Discovery') :
calculateInvariantLiquidity(discoveryKraiken, discoveryEth, discoveryTickLower, discoveryTickUpper, 'Discovery')
} }
}; };
// Debug logging for all positions
console.log('Position liquidity values:', {
floor: {
liquidity: positions.floor.liquidity,
eth: floorEth,
kraiken: floorKraiken,
range: [floorTickLower, floorTickUpper]
},
anchor: {
liquidity: positions.anchor.liquidity,
eth: anchorEth,
kraiken: anchorKraiken,
range: [anchorTickLower, anchorTickUpper]
},
discovery: {
liquidity: positions.discovery.liquidity,
eth: discoveryEth,
kraiken: discoveryKraiken,
range: [discoveryTickLower, discoveryTickUpper]
},
currentTick: currentTick,
token0isWeth: token0isWeth
});
// Calculate total active liquidity // Calculate total active liquidity
const totalLiquidity = Object.values(positions).reduce((sum, pos) => sum + pos.liquidity, 0); const totalLiquidity = Object.values(positions).reduce((sum, pos) => sum + pos.liquidity, 0);
@ -535,7 +635,7 @@
scenarioContainer.appendChild(chartsContainer); scenarioContainer.appendChild(chartsContainer);
// Create summary panel // Create summary panel
const summaryPanel = createSummaryPanel(positions, currentTick, token0isWeth); const summaryPanel = createSummaryPanel(positions, currentTick, token0isWeth, originalAction || precedingAction, index);
scenarioContainer.appendChild(summaryPanel); scenarioContainer.appendChild(summaryPanel);
// Add to page // Add to page
@ -565,18 +665,7 @@
...pos ...pos
}; };
console.log(`Position ${key}:`, {
ticks: [pos.tickLower, pos.tickUpper],
currentTick: currentTick,
multiples: [lowerMultiple, upperMultiple],
centerMultiple: centerMultiple,
token0isWeth: token0isWeth
});
// Warn about extreme positions
if (pos.tickLower > 180000 || pos.tickUpper > 180000) {
console.warn(`EXTREME TICKS: ${key} position has ticks above 180000, which represents extreme price multiples`);
}
}); });
// Calculate bar widths to represent actual price multiple ranges // Calculate bar widths to represent actual price multiple ranges
@ -663,14 +752,6 @@
const xAxisMin = Math.max(0.01, minMultiple - padding); // Don't go below 0.01x const xAxisMin = Math.max(0.01, minMultiple - padding); // Don't go below 0.01x
const xAxisMax = Math.min(100, maxMultiple + padding); // Cap at 100x max const xAxisMax = Math.min(100, maxMultiple + padding); // Cap at 100x max
// Debug logging for chart range
console.log('Chart x-axis range:', { xAxisMin, xAxisMax });
console.log('Bar positions:', barPositions);
console.log('Bar widths:', barWidths);
console.log('ETH values:', positionKeys.map(key => positions[key].eth));
console.log('KRAIKEN values:', positionKeys.map(key => positions[key].kraiken));
console.log('ETH trace y values (with min):', ethTrace.y);
console.log('KRAIKEN trace y values (with min):', kraikenTrace.y);
// Calculate max values for proper y-axis alignment // Calculate max values for proper y-axis alignment
const maxEth = Math.max(...positionKeys.map(key => positions[key].eth)); const maxEth = Math.max(...positionKeys.map(key => positions[key].eth));
@ -692,17 +773,6 @@
return; return;
} }
// Debug logging for very small or large values
if (totalLiquidity < 1 || tickRange > 10000 || pos.tickLower > 180000) {
console.log(`Warning: ${key} position has unusual values:`, {
liquidity: pos.liquidity,
tickRange: tickRange,
totalLiquidity: totalLiquidity,
ticks: [pos.tickLower, pos.tickUpper],
lowerMultiple: pos.lowerMultiple,
upperMultiple: pos.upperMultiple
});
}
// Create a filled area for each position to show its exact range // Create a filled area for each position to show its exact range
// Cap display coordinates to keep within visible range // Cap display coordinates to keep within visible range
@ -1027,7 +1097,7 @@
}); });
} }
function createSummaryPanel(positions, currentTick, token0isWeth) { function createSummaryPanel(positions, currentTick, token0isWeth, precedingAction, index) {
const panel = document.createElement('div'); const panel = document.createElement('div');
panel.className = 'summary-panel'; panel.className = 'summary-panel';
@ -1043,16 +1113,17 @@
const totalEth = Object.values(positions).reduce((sum, pos) => sum + pos.eth, 0); 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 totalKraiken = Object.values(positions).reduce((sum, pos) => sum + pos.kraiken, 0);
const totalUniV3Liquidity = Object.values(positions).reduce((sum, pos) => sum + pos.liquidity, 0); const totalUniV3Liquidity = Object.values(positions).reduce((sum, pos) => sum + pos.liquidity, 0);
// Add total summary // Add total summary
const totalItem = document.createElement('div'); const totalItem = document.createElement('div');
totalItem.className = 'summary-item'; totalItem.className = 'summary-item';
totalItem.innerHTML = ` const totalHtml = `
<strong>Total Portfolio</strong><br> <strong>Total Portfolio</strong><br>
Token ETH: ${totalEth.toLocaleString(undefined, {minimumFractionDigits: 6, maximumFractionDigits: 6})}<br> ETH: ${totalEth.toLocaleString(undefined, {minimumFractionDigits: 6, maximumFractionDigits: 6})}<br>
Token KRAIKEN: ${totalKraiken.toLocaleString(undefined, {minimumFractionDigits: 2, maximumFractionDigits: 2})}<br> KRAIKEN: ${totalKraiken.toLocaleString(undefined, {minimumFractionDigits: 2, maximumFractionDigits: 2})}<br>
Uniswap V3 Liquidity: ${totalUniV3Liquidity.toLocaleString(undefined, {minimumFractionDigits: 2, maximumFractionDigits: 2})} Uniswap V3 Liquidity: ${totalUniV3Liquidity.toExponential(2)}
`; `;
totalItem.innerHTML = totalHtml;
grid.appendChild(totalItem); grid.appendChild(totalItem);
// Add position summaries // Add position summaries
@ -1070,8 +1141,8 @@
<strong>${pos.name} Position</strong><br> <strong>${pos.name} Position</strong><br>
ETH: ${pos.eth.toLocaleString(undefined, {minimumFractionDigits: 6, maximumFractionDigits: 6})}<br> ETH: ${pos.eth.toLocaleString(undefined, {minimumFractionDigits: 6, maximumFractionDigits: 6})}<br>
KRAIKEN: ${pos.kraiken.toLocaleString(undefined, {minimumFractionDigits: 2, maximumFractionDigits: 2})}<br> KRAIKEN: ${pos.kraiken.toLocaleString(undefined, {minimumFractionDigits: 2, maximumFractionDigits: 2})}<br>
Uniswap V3 Liquidity: ${pos.liquidity.toLocaleString(undefined, {minimumFractionDigits: 2, maximumFractionDigits: 2})} (${liquidityPercent}%)<br> Liquidity: ${pos.liquidity.toExponential(2)} (${liquidityPercent}%)<br>
Range: ${lowerMultiple.toFixed(3)}x - ${upperMultiple.toFixed(3)}x Ticks: [${pos.tickLower.toLocaleString()}, ${pos.tickUpper.toLocaleString()}]
`; `;
grid.appendChild(item); grid.appendChild(item);
}); });
@ -1079,10 +1150,26 @@
// Add current price info // Add current price info
const priceItem = document.createElement('div'); const priceItem = document.createElement('div');
priceItem.className = 'summary-item'; priceItem.className = 'summary-item';
// Calculate current price
const currentPrice = tickToPrice(currentTick);
let ethPriceInKraiken, kraikenPriceInEth;
if (token0isWeth) {
// price = KRAIKEN/ETH
ethPriceInKraiken = currentPrice;
kraikenPriceInEth = 1 / currentPrice;
} else {
// price = ETH/KRAIKEN
kraikenPriceInEth = currentPrice;
ethPriceInKraiken = 1 / currentPrice;
}
priceItem.innerHTML = ` priceItem.innerHTML = `
<strong>Current Price</strong><br> <strong>Current Price</strong><br>
Tick: ${currentTick}<br> Tick: ${currentTick.toLocaleString()}<br>
<small>Price line shown in red</small> 1 ETH = ${ethPriceInKraiken.toLocaleString(undefined, {minimumFractionDigits: 2, maximumFractionDigits: 2})} KRAIKEN<br>
1 KRAIKEN = ${kraikenPriceInEth.toExponential(3)} ETH
`; `;
grid.appendChild(priceItem); grid.appendChild(priceItem);