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)";
}
}