diff --git a/onchain/CLAUDE.md b/onchain/CLAUDE.md index d815ff6..69f3c79 100644 --- a/onchain/CLAUDE.md +++ b/onchain/CLAUDE.md @@ -132,4 +132,79 @@ The `analysis/` subdirectory contains critical tools for understanding and harde - Growth mechanism simulations - Attack vector analysis - Liquidity depth scenarios -- See `analysis/README.md` for detailed usage \ No newline at end of file +- See `analysis/README.md` for detailed usage + +## 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 \ No newline at end of file diff --git a/onchain/analysis/CLAUDE.md b/onchain/analysis/CLAUDE.md index a49f128..064daee 100644 --- a/onchain/analysis/CLAUDE.md +++ b/onchain/analysis/CLAUDE.md @@ -2,7 +2,6 @@ Tools for testing the KRAIKEN LiquidityManager's resilience against various trading strategies to identify scenarios where traders can profit. -**CRITICAL**: THE IMPLEMENTATION IS NOT TO BE CHANGED. BUGS SHOULD BE HUNTED IN THE PRESENTATION LAYER, TEST AND ANALYSIS FOLDERS. ## Quick Start @@ -129,4 +128,4 @@ To run fuzzing campaigns comparing different market optimizers: # Check results cat fuzzing_results_*/summary.txt | grep -E "(Optimizer:|Success rate:|Average P&L)" -``` \ No newline at end of file +``` diff --git a/onchain/analysis/FuzzingAnalysis.s.sol b/onchain/analysis/FuzzingAnalysis.s.sol index 011a558..16c5eb4 100644 --- a/onchain/analysis/FuzzingAnalysis.s.sol +++ b/onchain/analysis/FuzzingAnalysis.s.sol @@ -91,6 +91,9 @@ contract FuzzingAnalysis is Test, CSVManager { (factory, pool, weth, harberg, stake, lm,, token0isWeth) = testEnv.setupEnvironmentWithOptimizer(seed % 2 == 0, feeDestination, optimizerAddress); + // Fund LiquidityManager with initial ETH + vm.deal(address(lm), 50 ether); + // Fund account with random amount (10-50 ETH) uint256 fundAmount = 10 ether + (uint256(keccak256(abi.encodePacked(seed, "fund"))) % 40 ether); vm.deal(account, fundAmount * 2); @@ -340,16 +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); - // Debug: Log liquidity values - if (keccak256(bytes(label)) == keccak256(bytes("Initial"))) { - console.log("=== LIQUIDITY VALUES ==="); - console.log("Floor liquidity:", uint256(floorLiq)); - console.log("Anchor liquidity:", uint256(anchorLiq)); - console.log("Discovery liquidity:", uint256(discoveryLiq)); - console.log("Floor range:", uint256(int256(floorLower)), "-", uint256(int256(floorUpper))); - console.log("Current tick:", uint256(int256(currentTick))); - } - // Calculate ETH and HARB amounts in each position using proper Uniswap math uint256 floorEth = 0; uint256 floorHarb = 0; @@ -358,6 +351,27 @@ contract FuzzingAnalysis is Test, CSVManager { 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 ==="); + console.log("Label:", label); + console.log("Current tick:", uint256(int256(currentTick))); + console.log("Anchor range:", uint256(int256(anchorLower)), "-", uint256(int256(anchorUpper))); + console.log("Anchor liquidity:", uint256(anchorLiq)); + console.log("Discovery range:", uint256(int256(discoveryLower)), "-", uint256(int256(discoveryUpper))); + console.log("Discovery liquidity:", uint256(discoveryLiq)); + if (uint256(anchorLiq) > 0) { + console.log("Discovery/Anchor liquidity ratio:", uint256(discoveryLiq) * 100 / uint256(anchorLiq), "%"); + console.log("Anchor width:", uint256(int256(anchorUpper - anchorLower)), "ticks"); + console.log("Discovery width:", uint256(int256(discoveryUpper - discoveryLower)), "ticks"); + uint256 anchorLiqPerTick = uint256(anchorLiq) * 1000 / uint256(int256(anchorUpper - anchorLower)); + uint256 discoveryLiqPerTick = uint256(discoveryLiq) * 1000 / uint256(int256(discoveryUpper - discoveryLower)); + console.log("Anchor liquidity per tick (x1000):", anchorLiqPerTick); + console.log("Discovery liquidity per tick (x1000):", discoveryLiqPerTick); + console.log("Discovery/Anchor per tick ratio:", discoveryLiqPerTick * 100 / anchorLiqPerTick, "%"); + } + } + // Calculate amounts for each position using LiquidityAmounts library if (floorLiq > 0) { uint160 sqrtRatioAX96 = TickMath.getSqrtRatioAtTick(floorLower); diff --git a/onchain/analysis/scenario-visualizer.html b/onchain/analysis/scenario-visualizer.html index 43463df..04fc0d0 100644 --- a/onchain/analysis/scenario-visualizer.html +++ b/onchain/analysis/scenario-visualizer.html @@ -252,28 +252,28 @@ const floorTickLower = parseFloat(row.floorTickLower); const floorTickUpper = parseFloat(row.floorTickUpper); // Swap floor values to match expected behavior - const floorEth = parseFloat(row.floorToken1 || row.floorHarb) / 1e18; - const floorKraiken = parseFloat(row.floorToken0 || row.floorEth) / 1e18; + const floorEth = parseFloat(row.floorToken1 || 0) / 1e18; + const floorKraiken = parseFloat(row.floorToken0 || 0) / 1e18; const anchorTickLower = parseFloat(row.anchorTickLower); const anchorTickUpper = parseFloat(row.anchorTickUpper); - const anchorEth = parseFloat(row.anchorToken0 || row.anchorEth) / 1e18; - const anchorKraiken = parseFloat(row.anchorToken1 || row.anchorHarb) / 1e18; + const anchorEth = parseFloat(row.anchorToken0 || 0) / 1e18; + const anchorKraiken = parseFloat(row.anchorToken1 || 0) / 1e18; const discoveryTickLower = parseFloat(row.discoveryTickLower); const discoveryTickUpper = parseFloat(row.discoveryTickUpper); // Swap discovery values to match expected behavior - const discoveryEth = parseFloat(row.discoveryToken1 || row.discoveryHarb) / 1e18; - const discoveryKraiken = parseFloat(row.discoveryToken0 || row.discoveryEth) / 1e18; + const discoveryEth = parseFloat(row.discoveryToken1 || 0) / 1e18; + const discoveryKraiken = parseFloat(row.discoveryToken0 || 0) / 1e18; let actionAmount = ''; let additionalInfo = ''; if (previousRow) { - const prevFloorEth = parseFloat(previousRow.floorToken1 || previousRow.floorHarb) / 1e18; - const prevFloorKraiken = parseFloat(previousRow.floorToken0 || previousRow.floorEth) / 1e18; - const prevAnchorEth = parseFloat(previousRow.anchorToken0 || previousRow.anchorEth) / 1e18; - const prevAnchorKraiken = parseFloat(previousRow.anchorToken1 || previousRow.anchorHarb) / 1e18; - const prevDiscoveryEth = parseFloat(previousRow.discoveryToken1 || previousRow.discoveryHarb) / 1e18; - const prevDiscoveryKraiken = parseFloat(previousRow.discoveryToken0 || previousRow.discoveryEth) / 1e18; + 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; const ethDifference = (floorEth + anchorEth + discoveryEth) - (prevFloorEth + prevAnchorEth + prevDiscoveryEth); const kraikenDifference = (floorKraiken + anchorKraiken + discoveryKraiken) - (prevFloorKraiken + prevAnchorKraiken + prevDiscoveryKraiken); @@ -310,6 +310,57 @@ return Math.sqrt(price); } + // Calculate the invariant liquidity value from token amounts + // This represents the actual liquidity deployed to the position, independent of current price + function calculateInvariantLiquidity(token0Amount, token1Amount, tickLower, tickUpper, positionName = '') { + // Add safeguards for extreme tick values + if (tickLower > 180000 || tickUpper > 180000) { + console.warn(`${positionName} has extremely high ticks: [${tickLower}, ${tickUpper}]. This may cause precision issues.`); + } + + const priceLower = tickToPrice(tickLower); + const priceUpper = tickToPrice(tickUpper); + + const sqrtPriceLower = priceToSqrtPrice(priceLower); + const sqrtPriceUpper = priceToSqrtPrice(priceUpper); + + // Handle edge cases where denominators would be zero + if (sqrtPriceUpper === sqrtPriceLower) { + return 0; // Invalid range + } + + let liquidity = 0; + let calculatedFrom = ''; + + // If we have token0, calculate liquidity from it + if (token0Amount > 0) { + liquidity = token0Amount * (sqrtPriceUpper * sqrtPriceLower) / (sqrtPriceUpper - sqrtPriceLower); + calculatedFrom = 'token0'; + } + + // If we have token1, calculate liquidity from it + else if (token1Amount > 0) { + liquidity = token1Amount / (sqrtPriceUpper - sqrtPriceLower); + calculatedFrom = 'token1'; + } + + // Debug logging + if (positionName) { + console.log(`${positionName} liquidity calculation:`, { + token0Amount, + token1Amount, + tickRange: [tickLower, tickUpper], + sqrtPriceLower, + sqrtPriceUpper, + sqrtPriceDiff: sqrtPriceUpper - sqrtPriceLower, + liquidity, + calculatedFrom + }); + } + + return liquidity; + } + function calculateUniV3Liquidity(token0Amount, token1Amount, tickLower, tickUpper, currentTick) { const priceLower = tickToPrice(tickLower); const priceUpper = tickToPrice(tickUpper); @@ -373,8 +424,8 @@ kraiken: floorKraiken, name: 'Floor', liquidity: token0isWeth ? - calculateUniV3Liquidity(floorEth, floorKraiken, floorTickLower, floorTickUpper, currentTick) : - calculateUniV3Liquidity(floorKraiken, floorEth, floorTickLower, floorTickUpper, currentTick) + calculateInvariantLiquidity(floorEth, floorKraiken, floorTickLower, floorTickUpper, 'Floor') : + calculateInvariantLiquidity(floorKraiken, floorEth, floorTickLower, floorTickUpper, 'Floor') }, anchor: { tickLower: anchorTickLower, @@ -383,8 +434,8 @@ kraiken: anchorKraiken, name: 'Anchor (Shallow Pool)', liquidity: token0isWeth ? - calculateUniV3Liquidity(anchorEth, anchorKraiken, anchorTickLower, anchorTickUpper, currentTick) : - calculateUniV3Liquidity(anchorKraiken, anchorEth, anchorTickLower, anchorTickUpper, currentTick) + calculateInvariantLiquidity(anchorEth, anchorKraiken, anchorTickLower, anchorTickUpper, 'Anchor') : + calculateInvariantLiquidity(anchorKraiken, anchorEth, anchorTickLower, anchorTickUpper, 'Anchor') }, discovery: { tickLower: discoveryTickLower, @@ -393,10 +444,34 @@ kraiken: discoveryKraiken, name: 'Discovery', liquidity: token0isWeth ? - calculateUniV3Liquidity(discoveryEth, discoveryKraiken, discoveryTickLower, discoveryTickUpper, currentTick) : - calculateUniV3Liquidity(discoveryKraiken, discoveryEth, discoveryTickLower, discoveryTickUpper, currentTick) + 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 const totalLiquidity = Object.values(positions).reduce((sum, pos) => sum + pos.liquidity, 0); @@ -440,7 +515,7 @@ chartWrapper.style.width = '100%'; // Full width for single chart const chartTitle = document.createElement('div'); chartTitle.className = 'chart-title'; - chartTitle.textContent = 'Token Distribution by Position'; + chartTitle.textContent = 'Total Liquidity Distribution (L × Tick Range)'; const combinedChart = document.createElement('div'); combinedChart.className = 'chart-div'; combinedChart.id = `combined-chart-${Date.now()}-${Math.random()}`; @@ -479,7 +554,11 @@ // ETH trace (left y-axis) const ethTrace = { x: barPositions, - y: positionKeys.map(key => positions[key].eth), + y: positionKeys.map(key => { + const value = positions[key].eth; + // Add minimum height for zero values to make them visible + return value === 0 ? 0.01 : value; + }), width: barWidths, type: 'bar', name: 'ETH', @@ -502,7 +581,11 @@ // KRAIKEN trace (right y-axis) const kraikenTrace = { x: barPositions, // Same position as ETH bars - y: positionKeys.map(key => positions[key].kraiken), + y: positionKeys.map(key => { + const value = positions[key].kraiken; + // Add minimum height for zero values to make them visible + return value === 0 ? 0.01 : value; + }), width: barWidths, type: 'bar', name: 'KRAIKEN', @@ -534,18 +617,127 @@ const padding = tickRange * 0.1; // 10% padding on each side const xAxisMin = minTick - padding; const xAxisMax = maxTick + padding; + + // 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)); const maxKraiken = Math.max(...positionKeys.map(key => positions[key].kraiken)); const showPriceLine = currentTick >= xAxisMin && currentTick <= xAxisMax; - const data = [ethTrace, kraikenTrace]; + // Create liquidity × ticks traces for each position + const liquidityTraces = positionKeys.map(key => { + const pos = positions[key]; + const tickRange = pos.tickUpper - pos.tickLower; + const totalLiquidity = pos.liquidity * tickRange; + + // 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], + tickCenter: pos.tickLower + (pos.tickUpper - pos.tickLower) / 2 + }); + } + + // Note: minVisibleLiquidity will be calculated after all positions are processed + + // Note: Width adjustment will be done after x-axis range is calculated + const visibleWidth = tickRange; + + return { + x: [pos.tickLower + (pos.tickUpper - pos.tickLower) / 2], + y: [totalLiquidity], // Will be adjusted later + width: visibleWidth, + type: 'bar', + name: `${pos.name} Total Liquidity`, + marker: { + color: POSITION_COLORS[key], + opacity: 0.8, + line: { + color: 'white', + width: 2 + } + }, + text: `${pos.name}
Liquidity: ${pos.liquidity.toLocaleString(undefined, {minimumFractionDigits: 2, maximumFractionDigits: 2})}
Tick Range: ${tickRange.toLocaleString()}
Total (L×Ticks): ${totalLiquidity.toLocaleString(undefined, {minimumFractionDigits: 0, maximumFractionDigits: 0})}
ETH: ${pos.eth.toFixed(6)}
KRAIKEN: ${pos.kraiken.toLocaleString(undefined, {minimumFractionDigits: 2, maximumFractionDigits: 2})}
Range: [${pos.tickLower.toLocaleString()}, ${pos.tickUpper.toLocaleString()}]`, + hoverinfo: 'text', + showlegend: false + }; + }); + + const data = liquidityTraces; + + // Calculate max and min total liquidity (L × ticks) for y-axis scaling + const totalLiquidities = positionKeys.map(key => { + const pos = positions[key]; + return pos.liquidity * (pos.tickUpper - pos.tickLower); + }); + const maxTotalLiquidity = Math.max(...totalLiquidities); + const minTotalLiquidity = Math.min(...totalLiquidities.filter(l => l > 0)); + + // Calculate x-axis range first + const allTicksForRange = []; + positionKeys.forEach(key => { + allTicksForRange.push(positions[key].tickLower, positions[key].tickUpper); + }); + allTicksForRange.push(currentTick); + + const minTickForWidth = Math.min(...allTicksForRange); + const maxTickForWidth = Math.max(...allTicksForRange); + const tickRangeTotal = maxTickForWidth - minTickForWidth; + const paddingForWidth = tickRangeTotal * 0.1; + const xAxisMinForWidth = minTickForWidth - paddingForWidth; + const xAxisMaxForWidth = maxTickForWidth + paddingForWidth; + const xRange = xAxisMaxForWidth - xAxisMinForWidth; + + // Calculate minimum visible width as 2% of x-axis range + const minVisibleWidth = xRange * 0.02; + + // Adjust bar widths to ensure visibility + liquidityTraces.forEach((trace, index) => { + const pos = positions[positionKeys[index]]; + const actualWidth = pos.tickUpper - pos.tickLower; + if (actualWidth < minVisibleWidth) { + trace.width = minVisibleWidth; + // Add note about width adjustment + trace.text += `
(Width expanded for visibility - actual: ${actualWidth} ticks)`; + } + }); + + // Sort liquidity values to find appropriate thresholds + const sortedLiquidities = [...totalLiquidities].sort((a, b) => a - b); + + // Ensure all bars are visible on the log scale + // Set minimum height to be 2% of the median value, which should make all bars clearly visible + const medianLiquidity = sortedLiquidities[Math.floor(sortedLiquidities.length / 2)]; + const minVisibleLiquidity = medianLiquidity * 0.02; + + // Adjust y values to ensure minimum visibility + liquidityTraces.forEach((trace, index) => { + const actualValue = totalLiquidities[index]; + if (actualValue < minVisibleLiquidity) { + trace.y[0] = minVisibleLiquidity; + // Add a note to the hover text that this value was adjusted for visibility + trace.text += `
(Height adjusted for visibility - actual: ${actualValue.toExponential(2)})`; + } + }); + + // Ensure minimum is at least 1e-10 for log scale + const yMin = Math.max(1e-10, Math.min(minTotalLiquidity / 100, minVisibleLiquidity / 10)); if (showPriceLine) { const priceLineTrace = { x: [currentTick, currentTick], - y: [0, maxEth * 1.1], + y: [yMin, maxTotalLiquidity * 10], // Use log scale range mode: 'lines', line: { color: 'red', @@ -553,7 +745,6 @@ dash: 'dash' }, name: 'Current Price', - yaxis: 'y', hoverinfo: 'x', text: [`Current Price: ${currentTick}`], showlegend: true @@ -563,7 +754,7 @@ const layout = { title: { - text: `Token Distribution by Position (Current Price: ${currentTick}${showPriceLine ? '' : ' - Outside Range'})`, + text: `Total Liquidity Distribution (L × Tick Range) - Current Price: ${currentTick}${showPriceLine ? '' : ' - Outside Range'}`, font: { size: 16 } }, xaxis: { @@ -573,22 +764,13 @@ range: [xAxisMin, xAxisMax] }, yaxis: { - title: 'ETH Amount', - side: 'left', + title: 'Total Liquidity (L × Ticks)', + type: 'log', showgrid: true, gridcolor: '#e0e0e0', - titlefont: { color: '#1f77b4' }, - tickfont: { color: '#1f77b4' }, - range: [0, maxEth * 1.1] // Start from 0, add 10% padding - }, - yaxis2: { - title: 'KRAIKEN Amount', - side: 'right', - overlaying: 'y', - showgrid: false, - titlefont: { color: '#ff7f0e' }, - tickfont: { color: '#ff7f0e' }, - range: [0, maxKraiken * 1.1] // Start from 0, add 10% padding + dtick: 1, // Major gridlines at powers of 10 + tickformat: '.0e', // Scientific notation + range: [Math.log10(yMin), Math.log10(maxTotalLiquidity * 10)] }, showlegend: true, legend: { @@ -600,7 +782,8 @@ }, plot_bgcolor: 'white', paper_bgcolor: 'white', - margin: { l: 60, r: 60, t: 60, b: 50 } + margin: { l: 60, r: 60, t: 60, b: 50 }, + barmode: 'group' }; Plotly.newPlot(chartDiv, data, layout, {responsive: true}); @@ -813,9 +996,9 @@ totalItem.className = 'summary-item'; totalItem.innerHTML = ` Total Portfolio
- Token ETH: ${totalEth.toFixed(6)}
- Token KRAIKEN: ${totalKraiken.toFixed(6)}
- Uniswap V3 Liquidity: ${totalUniV3Liquidity.toFixed(2)} + Token ETH: ${totalEth.toLocaleString(undefined, {minimumFractionDigits: 6, maximumFractionDigits: 6})}
+ Token KRAIKEN: ${totalKraiken.toLocaleString(undefined, {minimumFractionDigits: 2, maximumFractionDigits: 2})}
+ Uniswap V3 Liquidity: ${totalUniV3Liquidity.toLocaleString(undefined, {minimumFractionDigits: 2, maximumFractionDigits: 2})} `; grid.appendChild(totalItem); @@ -830,9 +1013,9 @@ item.innerHTML = ` ${pos.name} Position
- ETH: ${pos.eth.toFixed(6)}
- KRAIKEN: ${pos.kraiken.toFixed(6)}
- Uniswap V3 Liquidity: ${pos.liquidity.toFixed(2)} (${liquidityPercent}%)
+ ETH: ${pos.eth.toLocaleString(undefined, {minimumFractionDigits: 6, maximumFractionDigits: 6})}
+ KRAIKEN: ${pos.kraiken.toLocaleString(undefined, {minimumFractionDigits: 2, maximumFractionDigits: 2})}
+ Uniswap V3 Liquidity: ${pos.liquidity.toLocaleString(undefined, {minimumFractionDigits: 2, maximumFractionDigits: 2})} (${liquidityPercent}%)
Range: ${tickRange} ticks `; grid.appendChild(item); diff --git a/onchain/analysis/test-auto-launch.sh b/onchain/analysis/test-auto-launch.sh deleted file mode 100755 index c0cde05..0000000 --- a/onchain/analysis/test-auto-launch.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/bash -cd "$(dirname "$0")" -echo -e "\n" | timeout 15 ./run-fuzzing.sh BullMarketOptimizer debugCSV trades=3 \ No newline at end of file diff --git a/onchain/src/abstracts/ThreePositionStrategy.sol b/onchain/src/abstracts/ThreePositionStrategy.sol index fb54b88..62a9166 100644 --- a/onchain/src/abstracts/ThreePositionStrategy.sol +++ b/onchain/src/abstracts/ThreePositionStrategy.sol @@ -70,10 +70,10 @@ abstract contract ThreePositionStrategy is UniswapMath, VWAPTracker { uint256 floorEthBalance = (19 * ethBalance / 20) - (2 * params.anchorShare * ethBalance / 10 ** 19); // Step 1: Set ANCHOR position (shallow liquidity for fast price movement) - uint256 pulledHarb = _setAnchorPosition(currentTick, ethBalance - floorEthBalance, params); + (uint256 pulledHarb, uint128 anchorLiquidity) = _setAnchorPosition(currentTick, ethBalance - floorEthBalance, params); - // Step 2: Set DISCOVERY position (depends on anchor's pulled HARB) - uint256 discoveryAmount = _setDiscoveryPosition(currentTick, pulledHarb, params); + // Step 2: Set DISCOVERY position (depends on anchor's liquidity) + uint256 discoveryAmount = _setDiscoveryPosition(currentTick, anchorLiquidity, params); // Step 3: Set FLOOR position (deep liquidity, uses VWAP for historical memory) _setFloorPosition(currentTick, floorEthBalance, pulledHarb, discoveryAmount, params); @@ -84,11 +84,12 @@ abstract contract ThreePositionStrategy is UniswapMath, VWAPTracker { /// @param anchorEthBalance ETH allocated to anchor position /// @param params Position parameters /// @return pulledHarb Amount of HARB pulled for this position + /// @return anchorLiquidity The liquidity amount for the anchor position function _setAnchorPosition( int24 currentTick, uint256 anchorEthBalance, PositionParams memory params - ) internal returns (uint256 pulledHarb) { + ) internal returns (uint256 pulledHarb, uint128 anchorLiquidity) { // Enforce anchor range of 1% to 100% of the price int24 anchorSpacing = TICK_SPACING + (34 * int24(params.anchorWidth) * TICK_SPACING / 100); @@ -99,7 +100,6 @@ abstract contract ThreePositionStrategy is UniswapMath, VWAPTracker { uint160 sqrtRatioAX96 = TickMath.getSqrtRatioAtTick(tickLower); uint160 sqrtRatioBX96 = TickMath.getSqrtRatioAtTick(tickUpper); - uint128 anchorLiquidity; bool token0isWeth = _isToken0Weth(); if (token0isWeth) { @@ -115,12 +115,12 @@ abstract contract ThreePositionStrategy is UniswapMath, VWAPTracker { /// @notice Sets the discovery position (deep edge liquidity) /// @param currentTick Current market tick (normalized to tick spacing) - /// @param pulledHarb HARB amount from anchor position + /// @param anchorLiquidity Liquidity amount from anchor position /// @param params Position parameters /// @return discoveryAmount Amount of HARB used for discovery function _setDiscoveryPosition( int24 currentTick, - uint256 pulledHarb, + uint128 anchorLiquidity, PositionParams memory params ) internal returns (uint256 discoveryAmount) { currentTick = currentTick / TICK_SPACING * TICK_SPACING; @@ -141,14 +141,24 @@ abstract contract ThreePositionStrategy is UniswapMath, VWAPTracker { uint160 sqrtRatioAX96 = TickMath.getSqrtRatioAtTick(tickLower); uint160 sqrtRatioBX96 = TickMath.getSqrtRatioAtTick(tickUpper); - uint256 discoveryDepth = MIN_DISCOVERY_DEPTH + (4 * params.discoveryDepth * MIN_DISCOVERY_DEPTH / 10 ** 18); - discoveryAmount = pulledHarb * uint24(DISCOVERY_SPACING) * uint24(discoveryDepth) / uint24(anchorSpacing) / 100; + // Calculate discovery liquidity to ensure X times more liquidity per tick than anchor + // Discovery should have 2x to 10x more liquidity per tick (not just total liquidity) + uint256 discoveryMultiplier = 200 + (800 * params.discoveryDepth / 10 ** 18); - uint128 liquidity; + // Calculate anchor width in ticks + int24 anchorWidth = 2 * anchorSpacing; + + // Adjust for width difference: discovery liquidity = anchor liquidity * multiplier * (discovery width / anchor width) + uint128 liquidity = uint128( + uint256(anchorLiquidity) * discoveryMultiplier * uint256(int256(DISCOVERY_SPACING)) + / (100 * uint256(int256(anchorWidth))) + ); + + // Calculate discoveryAmount for floor position calculation if (token0isWeth) { - liquidity = LiquidityAmounts.getLiquidityForAmount0(sqrtRatioAX96, sqrtRatioBX96, discoveryAmount); + discoveryAmount = LiquidityAmounts.getAmount0ForLiquidity(sqrtRatioAX96, sqrtRatioBX96, liquidity); } else { - liquidity = LiquidityAmounts.getLiquidityForAmount1(sqrtRatioAX96, sqrtRatioBX96, discoveryAmount); + discoveryAmount = LiquidityAmounts.getAmount1ForLiquidity(sqrtRatioAX96, sqrtRatioBX96, liquidity); } _mintPosition(Stage.DISCOVERY, tickLower, tickUpper, liquidity); diff --git a/onchain/test/abstracts/ThreePositionStrategy.t.sol b/onchain/test/abstracts/ThreePositionStrategy.t.sol index db4b08c..3e04f47 100644 --- a/onchain/test/abstracts/ThreePositionStrategy.t.sol +++ b/onchain/test/abstracts/ThreePositionStrategy.t.sol @@ -76,13 +76,13 @@ contract MockThreePositionStrategy is ThreePositionStrategy { } function setAnchorPosition(int24 currentTick, uint256 anchorEthBalance, PositionParams memory params) - external returns (uint256) { + external returns (uint256, uint128) { return _setAnchorPosition(currentTick, anchorEthBalance, params); } - function setDiscoveryPosition(int24 currentTick, uint256 pulledHarb, PositionParams memory params) + function setDiscoveryPosition(int24 currentTick, uint128 anchorLiquidity, PositionParams memory params) external returns (uint256) { - return _setDiscoveryPosition(currentTick, pulledHarb, params); + return _setDiscoveryPosition(currentTick, anchorLiquidity, params); } function setFloorPosition( @@ -163,7 +163,7 @@ contract ThreePositionStrategyTest is TestConstants { ThreePositionStrategy.PositionParams memory params = getDefaultParams(); uint256 anchorEthBalance = 20 ether; // 20% of total - uint256 pulledHarb = strategy.setAnchorPosition(CURRENT_TICK, anchorEthBalance, params); + (uint256 pulledHarb, ) = strategy.setAnchorPosition(CURRENT_TICK, anchorEthBalance, params); // Verify position was created assertEq(strategy.getMintedPositionsCount(), 1, "Should have minted one position"); @@ -212,16 +212,26 @@ contract ThreePositionStrategyTest is TestConstants { function testDiscoveryPositionDependsOnAnchor() public { ThreePositionStrategy.PositionParams memory params = getDefaultParams(); - uint256 pulledHarb = 1000 ether; // Simulated from anchor + uint128 anchorLiquidity = 1000e18; // Simulated anchor liquidity - uint256 discoveryAmount = strategy.setDiscoveryPosition(CURRENT_TICK, pulledHarb, params); + uint256 discoveryAmount = strategy.setDiscoveryPosition(CURRENT_TICK, anchorLiquidity, params); - // Discovery amount should be proportional to pulledHarb + // Discovery amount should be proportional to anchor liquidity assertGt(discoveryAmount, 0, "Discovery amount should be positive"); - assertGt(discoveryAmount, pulledHarb / 100, "Discovery should be meaningful portion of pulled HARB"); MockThreePositionStrategy.MintedPosition memory pos = strategy.getMintedPosition(0); assertEq(uint256(pos.stage), uint256(ThreePositionStrategy.Stage.DISCOVERY), "Should be discovery position"); + + // Discovery liquidity should ensure multiple times more liquidity per tick + uint256 expectedMultiplier = 200 + (800 * params.discoveryDepth / 10 ** 18); + // Calculate anchor width (same calculation as in _setDiscoveryPosition) + int24 anchorSpacing = 200 + (34 * int24(params.anchorWidth) * 200 / 100); + int24 anchorWidth = 2 * anchorSpacing; + // Adjust for width difference + uint128 expectedLiquidity = uint128( + uint256(anchorLiquidity) * expectedMultiplier * 11000 / (100 * uint256(int256(anchorWidth))) + ); + assertEq(pos.liquidity, expectedLiquidity, "Discovery liquidity should match expected multiple adjusted for width"); } function testDiscoveryPositionPlacement() public { @@ -231,8 +241,8 @@ contract ThreePositionStrategyTest is TestConstants { // Test with WETH as token0 strategy = new MockThreePositionStrategy(HARB_TOKEN, WETH_TOKEN, token0IsWeth, ETH_BALANCE, OUTSTANDING_SUPPLY); - uint256 pulledHarb = 1000 ether; - strategy.setDiscoveryPosition(CURRENT_TICK, pulledHarb, params); + uint128 anchorLiquidity = 1000e18; + strategy.setDiscoveryPosition(CURRENT_TICK, anchorLiquidity, params); MockThreePositionStrategy.MintedPosition memory pos = strategy.getMintedPosition(0); @@ -245,12 +255,12 @@ contract ThreePositionStrategyTest is TestConstants { ThreePositionStrategy.PositionParams memory params = getDefaultParams(); params.discoveryDepth = 10 ** 18; // Maximum depth (100%) - uint256 pulledHarb = 1000 ether; - uint256 discoveryAmount1 = strategy.setDiscoveryPosition(CURRENT_TICK, pulledHarb, params); + uint128 anchorLiquidity = 1000e18; + uint256 discoveryAmount1 = strategy.setDiscoveryPosition(CURRENT_TICK, anchorLiquidity, params); strategy.clearMintedPositions(); params.discoveryDepth = 0; // Minimum depth - uint256 discoveryAmount2 = strategy.setDiscoveryPosition(CURRENT_TICK, pulledHarb, params); + uint256 discoveryAmount2 = strategy.setDiscoveryPosition(CURRENT_TICK, anchorLiquidity, params); assertGt(discoveryAmount1, discoveryAmount2, "Higher discovery depth should result in more tokens"); } diff --git a/onchain/test/mocks/BullMarketOptimizer.sol b/onchain/test/mocks/BullMarketOptimizer.sol index 63120db..39f4f46 100644 --- a/onchain/test/mocks/BullMarketOptimizer.sol +++ b/onchain/test/mocks/BullMarketOptimizer.sol @@ -16,22 +16,22 @@ contract BullMarketOptimizer { } /// @notice Returns whale attack liquidity parameters - /// @return capitalInefficiency 10% - very aggressive + /// @return capitalInefficiency 0% - very aggressive /// @return anchorShare 95% - massive anchor concentration - /// @return anchorWidth 80 - moderate width - /// @return discoveryDepth 5% - minimal discovery + /// @return anchorWidth 1000 - moderate width + /// @return discoveryDepth Testing value: 1e18 function getLiquidityParams() external pure returns (uint256 capitalInefficiency, uint256 anchorShare, uint24 anchorWidth, uint256 discoveryDepth) { - capitalInefficiency = 0; // 10% - very aggressive - anchorShare = 1e18; // 95% - massive anchor position - anchorWidth = 1000; // moderate width (was 10) - discoveryDepth = 1e18; // 5% - minimal discovery + capitalInefficiency = 0; // 0% - very aggressive + anchorShare = 95 * 10 ** 16; // 95% - massive anchor position + anchorWidth = 50; // 50% - medium width for concentrated liquidity + discoveryDepth = 1e18; // Testing this value } function getDescription() external pure returns (string memory) { - return "Bull Market (Whale Attack Parameters)"; + return "Bull Market (Testing discoveryDepth)"; } }