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
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
2. **Sentiment Oracle**: Harberger tax-based staking creates a prediction market for token value
3. **Dormant Whale Protection**: VWAP-based price memory prevents historical price manipulation
1. **Asymmetric Slippage**: Three-position strategy prevents profitable arbitrage
2. **Sentiment Oracle**: Harberger tax staking as prediction market
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
1. **Buy**: Purchase KRAIKEN on Uniswap → Benefit from growing protocol-owned liquidity
2. **Stake**: Visit kraiken.org → Stake tokens → Set tax rate → Earn from protocol growth
3. **Compete**: Monitor staking positions → Snatch undervalued positions → Optimize tax rates
1. **Buy**: Purchase KRAIKEN on Uniswap
2. **Stake**: Set tax rate at kraiken.org → Earn from protocol growth
3. **Compete**: Snatch undervalued positions → Optimize returns
## Project Structure
- **`onchain/`** - Smart contracts (Solidity/Foundry) - [See onchain/CLAUDE.md](onchain/CLAUDE.md)
- **`web/`** - Vue 3/Vite staking interface - [See web/CLAUDE.md](web/CLAUDE.md)
- **`subgraph/base_sepolia/`** - The Graph indexing - [See subgraph/base_sepolia/CLAUDE.md](subgraph/base_sepolia/CLAUDE.md)
- **`kraiken-lib/`** - TypeScript helper library - [See kraiken-lib/CLAUDE.md](kraiken-lib/CLAUDE.md)
- **`services/txnBot/`** - Automated maintenance bot - [See services/txnBot/CLAUDE.md](services/txnBot/CLAUDE.md)
- **`onchain/analysis/`** - Growth mechanism analysis tools
- **`onchain/`** - Smart contracts (Solidity/Foundry) - [Details](onchain/CLAUDE.md)
- **`web/`** - Vue 3/Vite staking interface - [Details](web/CLAUDE.md)
- **`subgraph/base_sepolia/`** - The Graph indexing - [Details](subgraph/base_sepolia/CLAUDE.md)
- **`kraiken-lib/`** - TypeScript helpers - [Details](kraiken-lib/CLAUDE.md)
- **`services/txnBot/`** - Maintenance bot - [Details](services/txnBot/CLAUDE.md)
- **`onchain/analysis/`** - Fuzzing tools - [Details](onchain/analysis/CLAUDE.md)
## Quick Start
```bash
# 1. Install dependencies for all projects
# Install all dependencies
cd onchain && forge install
cd ../web && npm install
cd ../kraiken-lib && npm install --legacy-peer-deps
cd ../subgraph/base_sepolia && npm install
cd ../services/txnBot && npm install
# 2. Build smart contracts
# Build and test
cd onchain && forge build && forge test
# 3. Start web interface
# Start frontend
cd web && npm run dev
```
## Key Concepts
### Liquidity Management
- Three-position strategy (ANCHOR, DISCOVERY, FLOOR)
- Asymmetric slippage prevents arbitrage
- VWAP tracking for price memory
- **Liquidity Management**: Three positions create asymmetric slippage
- **Harberger Staking**: Self-assessed tax creates sentiment signal
- **Protocol Growth**: Minted tokens benefit stakers
### Harberger Tax Staking
- 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
## Code Guidelines
### Protocol Growth
- Liquidity manager mints tokens when positions grow
- Stakers benefit from supply expansion
- Tax revenue redistributed to active participants
### Quality Standards
- Search before implementing (check uni-v3-lib, test helpers)
- Test after every change
- No commented-out code
- Clean git status before commits
## Global Code Quality Guidelines
### 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
### Technical Approach
- 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
### Direct & Technical
- Challenge suboptimal requests
- Highlight risks early and clearly
- Suggest better alternatives
- Refuse technically unsound solutions
### 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.
You are an experienced Solidity developer who:
- Notices and raises awareness of issues immediately
- Challenges suboptimal approaches
- Prioritizes technical correctness over user satisfaction
- Avoids hype and inflated language
## Additional Resources
- **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)
- **Technical Details**: [TECHNICAL_APPENDIX.md](TECHNICAL_APPENDIX.md)
- **Uniswap V3 Math**: [onchain/UNISWAP_V3_MATH.md](onchain/UNISWAP_V3_MATH.md)

View file

@ -6,205 +6,73 @@ This directory contains the core smart contracts for the KRAIKEN protocol.
### Core Contracts
**Kraiken.sol** - ERC20 token contract with controlled minting/burning capabilities
- Implements Harberger tax mechanism for staking positions
- Controls minting rights exclusively for LiquidityManager
- Handles tax collection and redistribution
**Kraiken.sol** - ERC20 token with Harberger tax staking
- Controlled minting exclusively by LiquidityManager
- Tax collection and redistribution mechanism
- 20% supply cap for staking (20,000 positions)
**LiquidityManager.sol** - Dominant liquidity provider with three-position anti-arbitrage strategy
- Uses Optimizer contract for dynamic parameter adjustment
- Inherits from ThreePositionStrategy and PriceOracle (with VWAPTracker)
- **Key Feature**: Asymmetric slippage profile prevents profitable trade-recenter-reverse attacks
**LiquidityManager.sol** - Dominant liquidity provider
- Three-position anti-arbitrage strategy (ANCHOR, DISCOVERY, FLOOR)
- Dynamic parameter adjustment via Optimizer contract
- Asymmetric slippage profile prevents profitable arbitrage
**VWAPTracker.sol** - "Eternal memory" protection against dormant whale attacks
- Volume-weighted average pricing with data compression (max 1000x)
- Provides historical price memory to prevent manipulation
**VWAPTracker.sol** - Price memory protection
- Volume-weighted average with data compression (max 1000x)
- Prevents dormant whale manipulation
**Optimizer.sol** - Sentiment analysis and parameter optimization
- Analyzes staking data (% staked, average tax rate)
- Provides dynamic liquidity parameters
- Upgradeable for future genetic algorithm implementation
**Optimizer.sol** - Dynamic parameter optimization
- Analyzes staking sentiment (% staked, average tax)
- Returns four key parameters for liquidity management
- Upgradeable for future algorithms
**Stake.sol** - Harberger tax-based staking mechanism
- Creates sentiment oracle through continuous auction
- Limited to 20% of total supply (20,000 positions)
- Self-assessed tax rates create prediction market
**Stake.sol** - Harberger tax implementation
- Continuous auction mechanism
- Self-assessed valuations 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)
- **DISCOVERY**: Proportional to KRAIKEN minted by anchor; borders anchor for fee capture (11000 tick spacing)
- **FLOOR**: Deep liquidity using VWAP-adjusted pricing for historical price memory
**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
**Technical Specs**:
- Fee Tier: 1% (10,000 basis points)
- Tick Spacing: 200 (base), 11,000 (discovery)
- Price Validation: 5-minute TWAP, 50-tick tolerance
### 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):
- 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
## Development
```bash
# Build contracts
forge build
# Run all tests
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
forge build # Build contracts
forge test # Run tests
forge test --gas-report # Gas optimization
forge test --fuzz-runs 10000 # Extended fuzzing
```
## Testing Architecture
## Testing
### Test Helpers
- `test/helpers/UniswapTestBase.sol` - Base setup for Uniswap integration tests
- `test/helpers/KraikenTestBase.sol` - Common test utilities for KRAIKEN contracts
- `test/helpers/PositionRenderer.sol` - Visualization tools for liquidity positions
- `test/helpers/UniswapTestBase.sol` - Uniswap integration base
- `test/helpers/KraikenTestBase.sol` - Common utilities
- Key tests: LiquidityManager.t.sol, Stake.t.sol, VWAPTracker.t.sol
### Key Test Files
- `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 Guidelines
## Code Quality Guidelines
### CRITICAL: Avoid Duplicate Code
- **ALWAYS** check lib/uni-v3-lib for existing Uniswap math functions
- **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
- **Check lib/uni-v3-lib** for existing Uniswap math
- **Use test/helpers** for common patterns
- **Security**: Reentrancy protection, oracle validation
- **Gas**: Batch operations, optimize storage
## Analysis Tools
The `analysis/` subdirectory contains critical tools for understanding and hardening the protocol:
- Growth mechanism simulations
- Attack vector analysis
- Liquidity depth scenarios
- See `analysis/README.md` for detailed usage
See `analysis/CLAUDE.md` for fuzzing and attack vector testing.
## Uniswap V3 Math - Critical Learnings
## Uniswap V3 Math
### 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
See [UNISWAP_V3_MATH.md](UNISWAP_V3_MATH.md) for detailed math concepts.

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
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
### Using run-fuzzing.sh (Recommended)
The `run-fuzzing.sh` script provides an easy way to run fuzzing campaigns with different market optimizers:
```bash
# Basic usage - run with specific optimizer
# Run with specific optimizer (50 runs default)
./analysis/run-fuzzing.sh BullMarketOptimizer
# Specify number of runs (default: 50)
./analysis/run-fuzzing.sh WhaleOptimizer runs=100
# Custom runs and trades
./analysis/run-fuzzing.sh WhaleOptimizer runs=100 trades=30
# Specify trades per run (default: 20, actual will be ±5)
./analysis/run-fuzzing.sh BearMarketOptimizer runs=10 trades=50
# Debug mode - generates position tracking CSV (forces runs=1)
# Debug mode with position tracking CSV (forces runs=1)
./analysis/run-fuzzing.sh NeutralMarketOptimizer debugCSV
# Multiple parameters
./analysis/run-fuzzing.sh BullMarketOptimizer runs=25 trades=30
```
**Available optimizers:**
- `BullMarketOptimizer` - Biased towards buying
## Available Optimizers
- `BullMarketOptimizer` - Buying bias
- `NeutralMarketOptimizer` - Balanced trading
- `BearMarketOptimizer` - Biased towards selling
- `WhaleOptimizer` - Large position trading
- `MockOptimizer` - Test optimizer
- `BearMarketOptimizer` - Selling bias
- `WhaleOptimizer` - Large positions
- `RandomScenarioOptimizer` - Random behavior
**Features:**
- 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
## Output Structure
### Manual Fuzzing (Advanced)
```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
Each campaign creates `fuzzing_results_[optimizer]_[timestamp]/`:
- `config.txt` - Campaign parameters
- `run_*.log` - Individual run logs
- `merged_profitable_scenarios.csv` - All profitable scenarios combined
- `summary.txt` - Campaign summary with statistics
- `debug_positions_*.csv` - Position tracking data (when debugCSV is used)
### Manual Mode
- `profitable_scenarios_[timestamp].csv` - Details of all profitable trading sequences
- `positions_[scenario]_[seed].csv` - Liquidity position data (only with TRACK_POSITIONS=true)
- `merged_profitable_scenarios.csv` - Profitable scenarios combined
- `summary.txt` - Statistics and cumulative P&L
- `debug_positions_*.csv` - Position data (debugCSV mode only)
## Visualization
```bash
# View results in browser
python3 -m http.server 8000
# Open http://localhost:8000/scenario-visualizer.html
# Automatic launch with debugCSV
./analysis/run-fuzzing.sh [optimizer] debugCSV
# Or use the shell script
./view-scenarios.sh
# Manual server (port 8000)
./analysis/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
- `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:
## Advanced Usage
```bash
# Run campaigns for all three market conditions
./analysis/run-fuzzing.sh BullMarketOptimizer runs=100
./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)"
# Manual fuzzing with environment variables
FUZZING_RUNS=500 TRACK_POSITIONS=true forge script analysis/FuzzingAnalysis.s.sol --ffi --via-ir
```
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 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
if (keccak256(bytes(label)) == keccak256(bytes("Initial")) || keccak256(bytes(label)) == keccak256(bytes("Recenter_2"))) {
console.log("=== LIQUIDITY VALUES ===");
@ -372,125 +364,19 @@ contract FuzzingAnalysis is Test, CSVManager {
}
}
// Calculate amounts for each position using LiquidityAmounts library
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
// Create position data row with liquidity values directly
string memory row = string.concat(
label, ", ",
vm.toString(currentTick), ", ",
vm.toString(floorLower), ", ",
vm.toString(floorUpper), ", ",
vm.toString(floorEth), ", ",
vm.toString(floorHarb), ", ",
vm.toString(uint256(floorLiq)), ", ",
vm.toString(anchorLower), ", ",
vm.toString(anchorUpper), ", ",
vm.toString(anchorEth), ", ",
vm.toString(anchorHarb), ", ",
vm.toString(uint256(anchorLiq)), ", ",
vm.toString(discoveryLower), ", ",
vm.toString(discoveryUpper), ", ",
vm.toString(discoveryEth), ", ",
vm.toString(discoveryHarb), ", ",
vm.toString(uint256(discoveryLiq)), ", ",
token0isWeth ? "true" : "false"
);
appendCSVRow(row);

View file

@ -15,7 +15,7 @@ library CSVHelper {
*/
function createPositionsHeader() internal pure returns (string memory) {
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) {

View file

@ -343,36 +343,66 @@ if [ "$CSV_GENERATED" = true ] && [ -n "$LATEST_CSV" ]; then
# Use absolute path for the symlink
ln -s "$(pwd)/$LATEST_CSV" "$TEMP_LINK"
# Start the viewer in background in its own process group
setsid ./view-scenarios.sh &
VIEWER_PID=$!
# Check if server is already running on common ports
SERVER_RUNNING=false
EXISTING_PORT=""
for PORT in 8000 8001 8002; do
if lsof -Pi :$PORT -sTCP:LISTEN -t >/dev/null 2>&1; then
# Check if it's a python http server in our analysis directory
if lsof -Pi :$PORT -sTCP:LISTEN 2>/dev/null | grep -q "python.*http.server"; then
SERVER_RUNNING=true
EXISTING_PORT=$PORT
break
fi
fi
done
# Give the server time to start and browser to open
sleep 2
if [ "$SERVER_RUNNING" = true ]; then
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 ""
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"
# Try to open browser to existing server
if command -v xdg-open &> /dev/null; then
xdg-open "http://localhost:$EXISTING_PORT/scenario-visualizer.html" 2>/dev/null &
elif command -v open &> /dev/null; then
open "http://localhost:$EXISTING_PORT/scenario-visualizer.html" 2>/dev/null &
fi
# Wait for user input
echo ""
echo -e "${YELLOW}Press Enter to stop the viewer and exit...${NC}"
read -r
echo ""
echo -e "${YELLOW}Press Enter to exit (server will keep running)...${NC}"
read -r
else
# Start the viewer in background in its own process group
setsid ./view-scenarios.sh &
VIEWER_PID=$!
# 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
# Give the server time to start and browser to open
sleep 2
# 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
# Clean up the symlink
rm -f "$TEMP_LINK"
echo -e "${GREEN}Viewer stopped.${NC}"
fi

View file

@ -133,16 +133,17 @@
<em>Discovery</em>: Edge liquidity position - holds KRAIKEN when ETH is expensive (above current price)<br>
<br>
<strong>Price Multiples:</strong> Shows ETH price relative to current (1x):<br>
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>
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>
<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 id="status">Loading profitable scenario data...</div>
<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="toggleManualInput()" id="manualButton">Manual Input Mode</button>
<div id="simulations"></div>
<script>
@ -171,6 +172,13 @@
// - Anchor Position: Mixed tokens around current price for shallow liquidity
// - 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
document.addEventListener('DOMContentLoaded', function() {
loadCSVData();
@ -208,25 +216,10 @@
<em>If no CSV exists, run: forge script analysis/SimpleAnalysis.s.sol --ffi</em>
</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) {
const lines = csv.trim().split('\n');
@ -252,37 +245,121 @@
}
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 currentTick = parseFloat(row.currentTick);
const token0isWeth = row.token0isWeth === 'true' || row.token0isWeth === true;
const floorTickLower = parseFloat(row.floorTickLower);
const floorTickUpper = parseFloat(row.floorTickUpper);
// Swap floor values to match expected behavior
const floorEth = parseFloat(row.floorToken1 || 0) / 1e18;
const floorKraiken = parseFloat(row.floorToken0 || 0) / 1e18;
const floorLiquidity = parseFloat(row.floorLiquidity || 0);
const anchorTickLower = parseFloat(row.anchorTickLower);
const anchorTickUpper = parseFloat(row.anchorTickUpper);
const anchorEth = parseFloat(row.anchorToken0 || 0) / 1e18;
const anchorKraiken = parseFloat(row.anchorToken1 || 0) / 1e18;
const anchorLiquidity = parseFloat(row.anchorLiquidity || 0);
const discoveryTickLower = parseFloat(row.discoveryTickLower);
const discoveryTickUpper = parseFloat(row.discoveryTickUpper);
// Swap discovery values to match expected behavior
const discoveryEth = parseFloat(row.discoveryToken1 || 0) / 1e18;
const discoveryKraiken = parseFloat(row.discoveryToken0 || 0) / 1e18;
const discoveryLiquidity = parseFloat(row.discoveryLiquidity || 0);
// 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 additionalInfo = '';
if (previousRow) {
const prevFloorEth = parseFloat(previousRow.floorToken1 || 0) / 1e18;
const prevFloorKraiken = parseFloat(previousRow.floorToken0 || 0) / 1e18;
const prevAnchorEth = parseFloat(previousRow.anchorToken0 || 0) / 1e18;
const prevAnchorKraiken = parseFloat(previousRow.anchorToken1 || 0) / 1e18;
const prevDiscoveryEth = parseFloat(previousRow.discoveryToken1 || 0) / 1e18;
const prevDiscoveryKraiken = parseFloat(previousRow.discoveryToken0 || 0) / 1e18;
// Calculate previous token amounts from liquidity
const prevCurrentTick = parseFloat(previousRow.currentTick);
const prevFloorLiquidity = parseFloat(previousRow.floorLiquidity || 0);
const prevAnchorLiquidity = parseFloat(previousRow.anchorLiquidity || 0);
const prevDiscoveryLiquidity = parseFloat(previousRow.discoveryLiquidity || 0);
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 kraikenDifference = (floorKraiken + anchorKraiken + discoveryKraiken) - (prevFloorKraiken + prevAnchorKraiken + prevDiscoveryKraiken);
@ -302,12 +379,35 @@
const lineNumber = index + 2;
const headline = `Line ${lineNumber}: ${precedingAction} ${additionalInfo}`;
simulateEnhanced(headline, currentTick,
floorTickLower, floorTickUpper, floorEth, floorKraiken,
anchorTickLower, anchorTickUpper, anchorEth, anchorKraiken,
discoveryTickLower, discoveryTickUpper, discoveryEth, discoveryKraiken, token0isWeth);
previousRow = row;
});
floorTickLower, floorTickUpper, floorEth, floorKraiken, floorLiquidity,
anchorTickLower, anchorTickUpper, anchorEth, anchorKraiken, anchorLiquidity,
discoveryTickLower, discoveryTickUpper, discoveryEth, discoveryKraiken, discoveryLiquidity, token0isWeth, index, precedingAction);
// 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
@ -315,6 +415,47 @@
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) {
return Math.sqrt(price);
}
@ -372,18 +513,6 @@
}
// Debug logging
if (positionName) {
console.log(`${positionName} liquidity calculation:`, {
token0Amount,
token1Amount,
tickRange: [tickLower, tickUpper],
sqrtPriceLower,
sqrtPriceUpper,
sqrtPriceDiff: sqrtPriceUpper - sqrtPriceLower,
liquidity,
calculatedFrom
});
}
return liquidity;
}
@ -438,9 +567,9 @@
}
function simulateEnhanced(precedingAction, currentTick,
floorTickLower, floorTickUpper, floorEth, floorKraiken,
anchorTickLower, anchorTickUpper, anchorEth, anchorKraiken,
discoveryTickLower, discoveryTickUpper, discoveryEth, discoveryKraiken, token0isWeth) {
floorTickLower, floorTickUpper, floorEth, floorKraiken, floorLiquidity,
anchorTickLower, anchorTickUpper, anchorEth, anchorKraiken, anchorLiquidity,
discoveryTickLower, discoveryTickUpper, discoveryEth, discoveryKraiken, discoveryLiquidity, token0isWeth, index, originalAction) {
// Position data structure with liquidity calculations
const positions = {
@ -450,9 +579,7 @@
eth: floorEth,
kraiken: floorKraiken,
name: 'Floor',
liquidity: token0isWeth ?
calculateInvariantLiquidity(floorEth, floorKraiken, floorTickLower, floorTickUpper, 'Floor') :
calculateInvariantLiquidity(floorKraiken, floorEth, floorTickLower, floorTickUpper, 'Floor')
liquidity: floorLiquidity
},
anchor: {
tickLower: anchorTickLower,
@ -460,9 +587,7 @@
eth: anchorEth,
kraiken: anchorKraiken,
name: 'Anchor (Shallow Pool)',
liquidity: token0isWeth ?
calculateInvariantLiquidity(anchorEth, anchorKraiken, anchorTickLower, anchorTickUpper, 'Anchor') :
calculateInvariantLiquidity(anchorKraiken, anchorEth, anchorTickLower, anchorTickUpper, 'Anchor')
liquidity: anchorLiquidity
},
discovery: {
tickLower: discoveryTickLower,
@ -470,35 +595,10 @@
eth: discoveryEth,
kraiken: discoveryKraiken,
name: 'Discovery',
liquidity: token0isWeth ?
calculateInvariantLiquidity(discoveryEth, discoveryKraiken, discoveryTickLower, discoveryTickUpper, 'Discovery') :
calculateInvariantLiquidity(discoveryKraiken, discoveryEth, discoveryTickLower, discoveryTickUpper, 'Discovery')
liquidity: discoveryLiquidity
}
};
// 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
const totalLiquidity = Object.values(positions).reduce((sum, pos) => sum + pos.liquidity, 0);
@ -535,7 +635,7 @@
scenarioContainer.appendChild(chartsContainer);
// Create summary panel
const summaryPanel = createSummaryPanel(positions, currentTick, token0isWeth);
const summaryPanel = createSummaryPanel(positions, currentTick, token0isWeth, originalAction || precedingAction, index);
scenarioContainer.appendChild(summaryPanel);
// Add to page
@ -565,18 +665,7 @@
...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
@ -663,14 +752,6 @@
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
// 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
const maxEth = Math.max(...positionKeys.map(key => positions[key].eth));
@ -692,17 +773,6 @@
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
// 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');
panel.className = 'summary-panel';
@ -1047,12 +1117,13 @@
// Add total summary
const totalItem = document.createElement('div');
totalItem.className = 'summary-item';
totalItem.innerHTML = `
const totalHtml = `
<strong>Total Portfolio</strong><br>
Token ETH: ${totalEth.toLocaleString(undefined, {minimumFractionDigits: 6, maximumFractionDigits: 6})}<br>
Token KRAIKEN: ${totalKraiken.toLocaleString(undefined, {minimumFractionDigits: 2, maximumFractionDigits: 2})}<br>
Uniswap V3 Liquidity: ${totalUniV3Liquidity.toLocaleString(undefined, {minimumFractionDigits: 2, maximumFractionDigits: 2})}
ETH: ${totalEth.toLocaleString(undefined, {minimumFractionDigits: 6, maximumFractionDigits: 6})}<br>
KRAIKEN: ${totalKraiken.toLocaleString(undefined, {minimumFractionDigits: 2, maximumFractionDigits: 2})}<br>
Uniswap V3 Liquidity: ${totalUniV3Liquidity.toExponential(2)}
`;
totalItem.innerHTML = totalHtml;
grid.appendChild(totalItem);
// Add position summaries
@ -1070,8 +1141,8 @@
<strong>${pos.name} Position</strong><br>
ETH: ${pos.eth.toLocaleString(undefined, {minimumFractionDigits: 6, maximumFractionDigits: 6})}<br>
KRAIKEN: ${pos.kraiken.toLocaleString(undefined, {minimumFractionDigits: 2, maximumFractionDigits: 2})}<br>
Uniswap V3 Liquidity: ${pos.liquidity.toLocaleString(undefined, {minimumFractionDigits: 2, maximumFractionDigits: 2})} (${liquidityPercent}%)<br>
Range: ${lowerMultiple.toFixed(3)}x - ${upperMultiple.toFixed(3)}x
Liquidity: ${pos.liquidity.toExponential(2)} (${liquidityPercent}%)<br>
Ticks: [${pos.tickLower.toLocaleString()}, ${pos.tickUpper.toLocaleString()}]
`;
grid.appendChild(item);
});
@ -1079,10 +1150,26 @@
// Add current price info
const priceItem = document.createElement('div');
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 = `
<strong>Current Price</strong><br>
Tick: ${currentTick}<br>
<small>Price line shown in red</small>
Tick: ${currentTick.toLocaleString()}<br>
1 ETH = ${ethPriceInKraiken.toLocaleString(undefined, {minimumFractionDigits: 2, maximumFractionDigits: 2})} KRAIKEN<br>
1 KRAIKEN = ${kraikenPriceInEth.toExponential(3)} ETH
`;
grid.appendChild(priceItem);