diff --git a/onchain/analysis/anchor-width-calculations.md b/onchain/analysis/anchor-width-calculations.md new file mode 100644 index 0000000..0e58fe5 --- /dev/null +++ b/onchain/analysis/anchor-width-calculations.md @@ -0,0 +1,84 @@ +# AnchorWidth Price Range Calculations + +## Understanding the Formula + +From the code: +```solidity +int24 anchorSpacing = TICK_SPACING + (34 * int24(params.anchorWidth) * TICK_SPACING / 100); +``` + +Where: +- `TICK_SPACING = 200` (for 1% fee tier pools) +- `anchorWidth` ranges from 1 to 100 + +## Tick to Price Conversion + +In Uniswap V3: +- Each tick represents a 0.01% (1 basis point) price change +- Price at tick i = 1.0001^i +- So a tick difference of 100 = ~1.01% price change +- A tick difference of 10,000 = ~2.718x price change + +## AnchorWidth to Price Range Mapping + +Let's calculate the actual price ranges for different anchorWidth values: + +### Formula Breakdown: +- `anchorSpacing = 200 + (34 * anchorWidth * 200 / 100)` +- `anchorSpacing = 200 + (68 * anchorWidth)` +- `anchorSpacing = 200 * (1 + 0.34 * anchorWidth)` + +The anchor position extends from: +- Lower bound: `currentTick - anchorSpacing` +- Upper bound: `currentTick + anchorSpacing` +- Total width: `2 * anchorSpacing` ticks + +### Price Range Calculations: + +| anchorWidth | anchorSpacing (ticks) | Total Width (ticks) | Lower Price Ratio | Upper Price Ratio | Price Range | +|-------------|----------------------|---------------------|-------------------|-------------------|-------------| +| 1% | 268 | 536 | 0.974 | 1.027 | ±2.7% | +| 5% | 540 | 1080 | 0.947 | 1.056 | ±5.5% | +| 10% | 880 | 1760 | 0.916 | 1.092 | ±9.0% | +| 20% | 1560 | 3120 | 0.855 | 1.170 | ±16% | +| 30% | 2240 | 4480 | 0.800 | 1.251 | ±25% | +| 40% | 2920 | 5840 | 0.748 | 1.336 | ±33% | +| 50% | 3600 | 7200 | 0.700 | 1.429 | ±42% | +| 60% | 4280 | 8560 | 0.654 | 1.528 | ±52% | +| 70% | 4960 | 9920 | 0.612 | 1.635 | ±63% | +| 80% | 5640 | 11280 | 0.572 | 1.749 | ±74% | +| 90% | 6320 | 12640 | 0.535 | 1.871 | ±87% | +| 100% | 7000 | 14000 | 0.500 | 2.000 | ±100% | + +## Key Insights: + +1. **Linear Tick Scaling**: The tick spacing scales linearly with anchorWidth +2. **Non-Linear Price Scaling**: Due to exponential nature of tick-to-price conversion +3. **Asymmetric Percentages**: The percentage move down is smaller than up (e.g., 100% width = -50% to +100%) + +## Practical Examples: + +### anchorWidth = 50 (Common Default) +- If current price is $1.00: + - Lower bound: $0.70 (-30%) + - Upper bound: $1.43 (+43%) + - Captures moderate price movements in both directions + +### anchorWidth = 100 (Maximum) +- If current price is $1.00: + - Lower bound: $0.50 (-50%) + - Upper bound: $2.00 (+100%) + - Price can double or halve while staying in range + +### anchorWidth = 10 (Narrow) +- If current price is $1.00: + - Lower bound: $0.92 (-8%) + - Upper bound: $1.09 (+9%) + - Highly concentrated, requires frequent rebalancing + +## Important Notes: + +1. The anchor position does NOT extend to 2x or 3x the price at maximum width +2. At anchorWidth = 100, the upper bound is exactly 2x the current price +3. The formula `200 + (34 * anchorWidth * 200 / 100)` creates a sensible progression from tight to wide ranges +4. The minimum spacing (anchorWidth = 0) would be 200 ticks (±1% range), but minimum allowed is 1 \ No newline at end of file diff --git a/onchain/analysis/staking-based-anchor-width.md b/onchain/analysis/staking-based-anchor-width.md new file mode 100644 index 0000000..d7bbfb4 --- /dev/null +++ b/onchain/analysis/staking-based-anchor-width.md @@ -0,0 +1,148 @@ +# Staking-Based AnchorWidth Recommendations + +## Understanding Staking as a Sentiment Signal + +The Harberger tax staking mechanism creates a prediction market where: +- **Tax Rate** = Cost to hold a position (self-assessed valuation) +- **Percentage Staked** = Overall confidence in protocol +- **Average Tax Rate** = Market's price volatility expectation + +## Staking Metrics → Market Conditions Mapping + +### High Staking Percentage (>70%) +**Interpretation**: Strong bullish sentiment, holders confident in price appreciation +- Market expects upward price movement +- Stakers willing to lock capital despite opportunity cost +- Lower expected volatility (confident holders) + +**anchorWidth Recommendation**: **30-50%** +- Moderate width to capture expected upward movement +- Avoid excessive rebalancing during steady climb +- Maintain efficiency without being too narrow + +### Low Staking Percentage (<30%) +**Interpretation**: Bearish/uncertain sentiment, holders want liquidity +- Market expects downward pressure or high volatility +- Stakers unwilling to commit capital +- Higher expected volatility (nervous market) + +**anchorWidth Recommendation**: **60-80%** +- Wide range to handle volatility without constant rebalancing +- Defensive positioning during uncertainty +- Prioritize capital preservation over fee optimization + +### Medium Staking Percentage (30-70%) +**Interpretation**: Neutral market, mixed sentiment +- Balanced buyer/seller pressure +- Normal market conditions +- Moderate volatility expectations + +**anchorWidth Recommendation**: **40-60%** +- Balanced approach for normal conditions +- Reasonable fee capture with manageable rebalancing +- Standard operating parameters + +## Average Tax Rate → Volatility Expectations + +### High Average Tax Rate (>50% of max) +**Interpretation**: Market expects significant price movement +- Stakers setting high taxes = expect to be "snatched" soon +- Indicates expected volatility or trend change +- Short-term holding mentality + +**anchorWidth Recommendation**: **Increase by 20-30%** +- Wider anchor to handle expected volatility +- Reduce rebalancing frequency during turbulent period +- Example: Base 40% → Adjust to 60% + +### Low Average Tax Rate (<20% of max) +**Interpretation**: Market expects stability +- Stakers comfortable with low taxes = expect to hold long-term +- Low volatility expectations +- Long-term holding mentality + +**anchorWidth Recommendation**: **Decrease by 10-20%** +- Narrower anchor to maximize fee collection +- Take advantage of expected stability +- Example: Base 40% → Adjust to 30% + +## Proposed On-Chain Formula + +```solidity +function calculateAnchorWidth( + uint256 percentageStaked, // 0 to 1e18 + uint256 avgTaxRate // 0 to 1e18 (normalized) +) public pure returns (uint24) { + // Base width starts at 40% + uint24 baseWidth = 40; + + // Staking adjustment: -20% to +20% based on staking percentage + // High staking (bullish) → narrower width + // Low staking (bearish) → wider width + int24 stakingAdjustment = int24(20) - int24(uint24(percentageStaked * 40 / 1e18)); + + // Tax rate adjustment: -10% to +30% based on average tax + // High tax (volatile) → wider width + // Low tax (stable) → narrower width + int24 taxAdjustment = int24(uint24(avgTaxRate * 30 / 1e18)) - 10; + + // Combined width + int24 totalWidth = int24(baseWidth) + stakingAdjustment + taxAdjustment; + + // Clamp between 10 and 80 + if (totalWidth < 10) return 10; + if (totalWidth > 80) return 80; + + return uint24(totalWidth); +} +``` + +## Staking Signal Interpretations + +### Scenario 1: "Confident Bull Market" +- High staking (80%), Low tax rate (20%) +- Interpretation: Strong holders, expect steady appreciation +- anchorWidth: ~25-35% +- Rationale: Tight range for fee optimization in trending market + +### Scenario 2: "Fearful Bear Market" +- Low staking (20%), High tax rate (70%) +- Interpretation: Nervous market, expect volatility/decline +- anchorWidth: ~70-80% +- Rationale: Wide defensive positioning + +### Scenario 3: "Speculative Frenzy" +- High staking (70%), High tax rate (80%) +- Interpretation: Aggressive speculation, expect big moves +- anchorWidth: ~50-60% +- Rationale: Balance between capturing moves and managing volatility + +### Scenario 4: "Boring Stability" +- Medium staking (50%), Low tax rate (10%) +- Interpretation: Stable, range-bound market +- anchorWidth: ~30-40% +- Rationale: Optimize for fee collection in stable conditions + +## Key Advantages of Staking-Based Approach + +1. **On-Chain Native**: Uses only data available to smart contracts +2. **Forward-Looking**: Tax rates reflect expectations, not just history +3. **Self-Adjusting**: Market participants' actions directly influence parameters +4. **Sybil-Resistant**: Costly to manipulate due to tax payments +5. **Continuous Signal**: Updates in real-time as positions change + +## Implementation Considerations + +1. **Smoothing**: Average metrics over time window to prevent manipulation +2. **Bounds**: Always enforce min/max limits (10-80% recommended) +3. **Hysteresis**: Add small threshold before adjusting to reduce thrashing +4. **Gas Optimization**: Only recalculate when scraping/repositioning + +## Summary Recommendations + +For the on-chain Optimizer contract: +- Use `percentageStaked` as primary bull/bear indicator +- Use `averageTaxRate` as volatility expectation proxy +- Combine both signals for sophisticated width adjustment +- Default to 40% width when signals are neutral +- Never exceed 80% or go below 10% for safety \ No newline at end of file diff --git a/onchain/analysis/verify_anchor_width_logic.md b/onchain/analysis/verify_anchor_width_logic.md new file mode 100644 index 0000000..b6d820c --- /dev/null +++ b/onchain/analysis/verify_anchor_width_logic.md @@ -0,0 +1,88 @@ +# Anchor Width Calculation Verification + +## Formula +``` +anchorWidth = base + stakingAdjustment + taxAdjustment +where: + base = 40 + stakingAdjustment = 20 - (percentageStaked * 40 / 1e18) + taxAdjustment = (averageTaxRate * 40 / 1e18) - 10 + final = clamp(anchorWidth, 10, 80) +``` + +## Test Case Verification + +### 1. Bull Market (80% staked, 10% tax) +- Base: 40 +- Staking adj: 20 - (0.8 * 40) = 20 - 32 = -12 +- Tax adj: (0.1 * 40) - 10 = 4 - 10 = -6 +- Total: 40 + (-12) + (-6) = **22** ✓ + +### 2. Bear Market (20% staked, 70% tax) +- Base: 40 +- Staking adj: 20 - (0.2 * 40) = 20 - 8 = 12 +- Tax adj: (0.7 * 40) - 10 = 28 - 10 = 18 +- Total: 40 + 12 + 18 = **70** ✓ + +### 3. Neutral Market (50% staked, 30% tax) +- Base: 40 +- Staking adj: 20 - (0.5 * 40) = 20 - 20 = 0 +- Tax adj: (0.3 * 40) - 10 = 12 - 10 = 2 +- Total: 40 + 0 + 2 = **42** ✓ + +### 4. Speculative Frenzy (70% staked, 80% tax) +- Base: 40 +- Staking adj: 20 - (0.7 * 40) = 20 - 28 = -8 +- Tax adj: (0.8 * 40) - 10 = 32 - 10 = 22 +- Total: 40 + (-8) + 22 = **54** ✓ + +### 5. Stable Market (50% staked, 5% tax) +- Base: 40 +- Staking adj: 20 - (0.5 * 40) = 20 - 20 = 0 +- Tax adj: (0.05 * 40) - 10 = 2 - 10 = -8 +- Total: 40 + 0 + (-8) = **32** ✓ + +### 6. Zero Inputs (0% staked, 0% tax) +- Base: 40 +- Staking adj: 20 - (0 * 40) = 20 - 0 = 20 +- Tax adj: (0 * 40) - 10 = 0 - 10 = -10 +- Total: 40 + 20 + (-10) = **50** ✓ + +### 7. Max Inputs (100% staked, 100% tax) +- Base: 40 +- Staking adj: 20 - (1.0 * 40) = 20 - 40 = -20 +- Tax adj: (1.0 * 40) - 10 = 40 - 10 = 30 +- Total: 40 + (-20) + 30 = **50** ✓ + +### 8. Test Minimum Clamping (95% staked, 0% tax) +- Base: 40 +- Staking adj: 20 - (0.95 * 40) = 20 - 38 = -18 +- Tax adj: (0 * 40) - 10 = 0 - 10 = -10 +- Total: 40 + (-18) + (-10) = **12** (not clamped, above 10) ✓ + +### 9. Test Maximum Clamping (0% staked, 100% tax) +- Base: 40 +- Staking adj: 20 - (0 * 40) = 20 - 0 = 20 +- Tax adj: (1.0 * 40) - 10 = 40 - 10 = 30 +- Total: 40 + 20 + 30 = 90 → **80** (clamped to max) ✓ + +## Summary + +All test cases pass! The implementation correctly: + +1. **Inversely correlates staking with width**: Higher staking → narrower anchor +2. **Directly correlates tax with width**: Higher tax → wider anchor +3. **Maintains reasonable bounds**: 10-80% range +4. **Provides sensible defaults**: 50% width for zero/max inputs + +## Market Condition Mapping + +| Condition | Staking | Tax | Width | Rationale | +|-----------|---------|-----|-------|-----------| +| Bull Market | High (70-90%) | Low (0-20%) | 20-35% | Optimize fees in trending market | +| Bear Market | Low (10-30%) | High (60-90%) | 60-80% | Defensive positioning | +| Neutral | Medium (40-60%) | Medium (20-40%) | 35-50% | Balanced approach | +| Volatile | Any | High (70%+) | 50-80% | Wide to reduce rebalancing | +| Stable | Any | Low (<10%) | 20-40% | Narrow for fee collection | + +The formula successfully encodes market dynamics into the anchor width parameter! \ No newline at end of file diff --git a/onchain/src/Optimizer.sol b/onchain/src/Optimizer.sol index c0884f0..cac5630 100644 --- a/onchain/src/Optimizer.sol +++ b/onchain/src/Optimizer.sol @@ -20,6 +20,17 @@ import {Initializable} from "@openzeppelin/proxy/utils/Initializable.sol"; * 3. anchorWidth (0 to 100): Anchor position width % * 4. discoveryDepth (0 to 1e18): Discovery liquidity density (2x-10x) * - Upgradeable for future algorithm improvements + * + * AnchorWidth Price Ranges: + * The anchor position's price range depends on anchorWidth value: + * - anchorWidth = 10: ±9% range (0.92x to 1.09x current price) + * - anchorWidth = 40: ±33% range (0.75x to 1.34x current price) + * - anchorWidth = 50: ±42% range (0.70x to 1.43x current price) + * - anchorWidth = 80: ±74% range (0.57x to 1.75x current price) + * - anchorWidth = 100: -50% to +100% range (0.50x to 2.00x current price) + * + * The formula: anchorSpacing = TICK_SPACING + (34 * anchorWidth * TICK_SPACING / 100) + * creates a non-linear price range due to Uniswap V3's tick-based system */ contract Optimizer is Initializable, UUPSUpgradeable { Kraiken private harberg; @@ -99,12 +110,86 @@ contract Optimizer is Initializable, UUPSUpgradeable { sentiment = calculateSentiment(averageTaxRate, percentageStaked); } + /** + * @notice Calculates the optimal anchor width based on staking metrics. + * @param percentageStaked The percentage of tokens staked (0 to 1e18) + * @param averageTaxRate The average tax rate across all stakers (0 to 1e18) + * @return anchorWidth The calculated anchor width (10 to 80) + * + * @dev This function implements a staking-based approach to determine anchor width: + * + * Base Strategy: + * - Start with base width of 40% (balanced default) + * + * Staking Adjustment (-20% to +20%): + * - High staking (>70%) indicates bullish confidence → narrow anchor for fee optimization + * - Low staking (<30%) indicates bearish/uncertainty → wide anchor for safety + * - Inverse relationship: higher staking = lower width adjustment + * + * Tax Rate Adjustment (-10% to +30%): + * - High tax rates signal expected volatility → wider anchor to reduce rebalancing + * - Low tax rates signal expected stability → narrower anchor for fee collection + * - Direct relationship: higher tax = higher width adjustment + * + * The Harberger tax mechanism acts as a decentralized prediction market where: + * - Tax rates reflect holders' expectations of being "snatched" (volatility) + * - Staking percentage reflects overall market confidence + * + * Final width is clamped between 10 (minimum safe) and 80 (maximum effective) + */ + function _calculateAnchorWidth(uint256 percentageStaked, uint256 averageTaxRate) + internal + pure + returns (uint24) + { + // Base width: 40% is our neutral starting point + int256 baseWidth = 40; + + // Staking adjustment: -20% to +20% based on staking percentage + // Formula: 20 - (percentageStaked * 40 / 1e18) + // High staking (1e18) → -20 adjustment → narrower width + // Low staking (0) → +20 adjustment → wider width + int256 stakingAdjustment = 20 - int256(percentageStaked * 40 / 1e18); + + // Tax rate adjustment: -10% to +30% based on average tax rate + // Formula: (averageTaxRate * 40 / 1e18) - 10 + // High tax (1e18) → +30 adjustment → wider width for volatility + // Low tax (0) → -10 adjustment → narrower width for stability + int256 taxAdjustment = int256(averageTaxRate * 40 / 1e18) - 10; + + // Combine all adjustments + int256 totalWidth = baseWidth + stakingAdjustment + taxAdjustment; + + // Clamp to safe bounds (10 to 80) + // Below 10%: rebalancing costs exceed benefits + // Above 80%: capital efficiency degrades significantly + if (totalWidth < 10) { + return 10; + } + if (totalWidth > 80) { + return 80; + } + + return uint24(uint256(totalWidth)); + } + /** * @notice Returns liquidity parameters for the liquidity manager. * @return capitalInefficiency Calculated as (1e18 - sentiment). Capital buffer level (0-1e18) * @return anchorShare Set equal to the sentiment. % of non-floor ETH in anchor (0-1e18) - * @return anchorWidth Here set to a constant 100. Anchor position width % (1-100) + * @return anchorWidth Dynamically adjusted based on staking metrics. Anchor position width % (1-100) * @return discoveryDepth Set equal to the sentiment. + * + * @dev AnchorWidth Strategy: + * The anchorWidth parameter controls the price range of the anchor liquidity position. + * - anchorWidth = 50: Price range from 0.70x to 1.43x current price + * - anchorWidth = 100: Price range from 0.50x to 2.00x current price + * + * We use staking metrics as a decentralized prediction market: + * - High staking % → Bullish sentiment → Narrower width (30-50%) for fee optimization + * - Low staking % → Bearish/uncertain → Wider width (60-80%) for defensive positioning + * - High avg tax rate → Expects volatility → Wider anchor to reduce rebalancing + * - Low avg tax rate → Expects stability → Narrower anchor for fee collection */ function getLiquidityParams() external @@ -116,8 +201,10 @@ contract Optimizer is Initializable, UUPSUpgradeable { uint256 sentiment = calculateSentiment(averageTaxRate, percentageStaked); capitalInefficiency = 1e18 - sentiment; anchorShare = sentiment; - // Here we simply set anchorWidth to 100; adjust this formula if needed. - anchorWidth = 100; + + // Calculate dynamic anchorWidth based on staking metrics + anchorWidth = _calculateAnchorWidth(percentageStaked, averageTaxRate); + discoveryDepth = sentiment; } } diff --git a/onchain/test/Optimizer.t.sol b/onchain/test/Optimizer.t.sol new file mode 100644 index 0000000..41110ab --- /dev/null +++ b/onchain/test/Optimizer.t.sol @@ -0,0 +1,244 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8.19; + +import "forge-std/Test.sol"; +import "forge-std/console.sol"; +import "../src/Optimizer.sol"; +import "./mocks/MockStake.sol"; +import "./mocks/MockKraiken.sol"; + +contract OptimizerTest is Test { + Optimizer optimizer; + MockStake mockStake; + MockKraiken mockKraiken; + + function setUp() public { + // Deploy mocks + mockKraiken = new MockKraiken(); + mockStake = new MockStake(); + + // Deploy Optimizer implementation + Optimizer implementation = new Optimizer(); + + // Deploy proxy and initialize + bytes memory initData = abi.encodeWithSelector( + Optimizer.initialize.selector, + address(mockKraiken), + address(mockStake) + ); + + // For simplicity, we'll test the implementation directly + // In production, you'd use a proper proxy setup + optimizer = implementation; + optimizer.initialize(address(mockKraiken), address(mockStake)); + } + + /** + * @notice Test that anchorWidth adjusts correctly for bull market conditions + * @dev High staking, low tax → narrow anchor (30-35%) + */ + function testBullMarketAnchorWidth() public { + // Set bull market conditions: high staking (80%), low tax (10%) + mockStake.setPercentageStaked(0.8e18); + mockStake.setAverageTaxRate(0.1e18); + + (,, uint24 anchorWidth,) = optimizer.getLiquidityParams(); + + console.log("Bull Market - Anchor Width:", anchorWidth); + + // Expected: base(40) + staking_adj(20 - 32 = -12) + tax_adj(4 - 10 = -6) = 22 + assertEq(anchorWidth, 22, "Bull market should have narrow anchor width"); + assertTrue(anchorWidth >= 20 && anchorWidth <= 35, "Bull market width should be 20-35%"); + } + + /** + * @notice Test that anchorWidth adjusts correctly for bear market conditions + * @dev Low staking, high tax → wide anchor (60-80%) + */ + function testBearMarketAnchorWidth() public { + // Set bear market conditions: low staking (20%), high tax (70%) + mockStake.setPercentageStaked(0.2e18); + mockStake.setAverageTaxRate(0.7e18); + + (,, uint24 anchorWidth,) = optimizer.getLiquidityParams(); + + console.log("Bear Market - Anchor Width:", anchorWidth); + + // Expected: base(40) + staking_adj(20 - 8 = 12) + tax_adj(28 - 10 = 18) = 70 + assertEq(anchorWidth, 70, "Bear market should have wide anchor width"); + assertTrue(anchorWidth >= 60 && anchorWidth <= 80, "Bear market width should be 60-80%"); + } + + /** + * @notice Test neutral market conditions + * @dev Medium staking, medium tax → balanced anchor (35-50%) + */ + function testNeutralMarketAnchorWidth() public { + // Set neutral conditions: medium staking (50%), medium tax (30%) + mockStake.setPercentageStaked(0.5e18); + mockStake.setAverageTaxRate(0.3e18); + + (,, uint24 anchorWidth,) = optimizer.getLiquidityParams(); + + console.log("Neutral Market - Anchor Width:", anchorWidth); + + // Expected: base(40) + staking_adj(20 - 20 = 0) + tax_adj(12 - 10 = 2) = 42 + assertEq(anchorWidth, 42, "Neutral market should have balanced anchor width"); + assertTrue(anchorWidth >= 35 && anchorWidth <= 50, "Neutral width should be 35-50%"); + } + + /** + * @notice Test high volatility scenario + * @dev High staking with high tax (speculative frenzy) → moderate-wide anchor + */ + function testHighVolatilityAnchorWidth() public { + // High staking (70%) but also high tax (80%) - speculative market + mockStake.setPercentageStaked(0.7e18); + mockStake.setAverageTaxRate(0.8e18); + + (,, uint24 anchorWidth,) = optimizer.getLiquidityParams(); + + console.log("High Volatility - Anchor Width:", anchorWidth); + + // Expected: base(40) + staking_adj(20 - 28 = -8) + tax_adj(32 - 10 = 22) = 54 + assertEq(anchorWidth, 54, "High volatility should have moderate-wide anchor"); + assertTrue(anchorWidth >= 50 && anchorWidth <= 60, "Volatile width should be 50-60%"); + } + + /** + * @notice Test stable market conditions + * @dev Medium staking with very low tax → narrow anchor for fee optimization + */ + function testStableMarketAnchorWidth() public { + // Medium staking (50%), very low tax (5%) - stable conditions + mockStake.setPercentageStaked(0.5e18); + mockStake.setAverageTaxRate(0.05e18); + + (,, uint24 anchorWidth,) = optimizer.getLiquidityParams(); + + console.log("Stable Market - Anchor Width:", anchorWidth); + + // Expected: base(40) + staking_adj(20 - 20 = 0) + tax_adj(2 - 10 = -8) = 32 + assertEq(anchorWidth, 32, "Stable market should have narrower anchor"); + assertTrue(anchorWidth >= 30 && anchorWidth <= 40, "Stable width should be 30-40%"); + } + + /** + * @notice Test minimum bound enforcement + * @dev Extreme conditions that would result in width < 10 should clamp to 10 + */ + function testMinimumWidthBound() public { + // Extreme bull: very high staking (95%), zero tax + mockStake.setPercentageStaked(0.95e18); + mockStake.setAverageTaxRate(0); + + (,, uint24 anchorWidth,) = optimizer.getLiquidityParams(); + + console.log("Minimum Bound Test - Anchor Width:", anchorWidth); + + // Expected: base(40) + staking_adj(20 - 38 = -18) + tax_adj(0 - 10 = -10) = 12 + // But should be at least 10 + assertEq(anchorWidth, 12, "Should not go below calculated value if above 10"); + assertTrue(anchorWidth >= 10, "Width should never be less than 10"); + } + + /** + * @notice Test maximum bound enforcement + * @dev Extreme conditions that would result in width > 80 should clamp to 80 + */ + function testMaximumWidthBound() public { + // Extreme bear: zero staking, maximum tax + mockStake.setPercentageStaked(0); + mockStake.setAverageTaxRate(1e18); + + (,, uint24 anchorWidth,) = optimizer.getLiquidityParams(); + + console.log("Maximum Bound Test - Anchor Width:", anchorWidth); + + // Expected: base(40) + staking_adj(20 - 0 = 20) + tax_adj(40 - 10 = 30) = 90 + // But should be clamped to 80 + assertEq(anchorWidth, 80, "Should clamp to maximum of 80"); + assertTrue(anchorWidth <= 80, "Width should never exceed 80"); + } + + /** + * @notice Test edge case with exactly minimum staking and tax + */ + function testEdgeCaseMinimumInputs() public { + mockStake.setPercentageStaked(0); + mockStake.setAverageTaxRate(0); + + (,, uint24 anchorWidth,) = optimizer.getLiquidityParams(); + + console.log("Minimum Inputs - Anchor Width:", anchorWidth); + + // Expected: base(40) + staking_adj(20 - 0 = 20) + tax_adj(0 - 10 = -10) = 50 + assertEq(anchorWidth, 50, "Zero inputs should give moderate width"); + } + + /** + * @notice Test edge case with exactly maximum staking and tax + */ + function testEdgeCaseMaximumInputs() public { + mockStake.setPercentageStaked(1e18); + mockStake.setAverageTaxRate(1e18); + + (,, uint24 anchorWidth,) = optimizer.getLiquidityParams(); + + console.log("Maximum Inputs - Anchor Width:", anchorWidth); + + // Expected: base(40) + staking_adj(20 - 40 = -20) + tax_adj(40 - 10 = 30) = 50 + assertEq(anchorWidth, 50, "Maximum inputs should balance out to moderate width"); + } + + /** + * @notice Fuzz test to ensure anchorWidth always stays within bounds + */ + function testFuzzAnchorWidthBounds(uint256 percentageStaked, uint256 averageTaxRate) public { + // Bound inputs to valid ranges + percentageStaked = bound(percentageStaked, 0, 1e18); + averageTaxRate = bound(averageTaxRate, 0, 1e18); + + mockStake.setPercentageStaked(percentageStaked); + mockStake.setAverageTaxRate(averageTaxRate); + + (,, uint24 anchorWidth,) = optimizer.getLiquidityParams(); + + // Assert bounds are always respected + assertTrue(anchorWidth >= 10, "Width should never be less than 10"); + assertTrue(anchorWidth <= 80, "Width should never exceed 80"); + + // Log some interesting cases + if (anchorWidth == 10 || anchorWidth == 80) { + console.log("Bound hit - Staking:", percentageStaked / 1e16, "%, Tax:", averageTaxRate / 1e16, "%, Width:", anchorWidth); + } + } + + /** + * @notice Test that other liquidity params are still calculated correctly + */ + function testOtherLiquidityParams() public { + mockStake.setPercentageStaked(0.6e18); + mockStake.setAverageTaxRate(0.4e18); + + (uint256 capitalInefficiency, uint256 anchorShare, uint24 anchorWidth, uint256 discoveryDepth) = + optimizer.getLiquidityParams(); + + uint256 sentiment = optimizer.getSentiment(); + + console.log("Sentiment:", sentiment / 1e16, "%"); + console.log("Capital Inefficiency:", capitalInefficiency / 1e16, "%"); + console.log("Anchor Share:", anchorShare / 1e16, "%"); + console.log("Anchor Width:", anchorWidth, "%"); + console.log("Discovery Depth:", discoveryDepth / 1e16, "%"); + + // Verify relationships + assertEq(capitalInefficiency, 1e18 - sentiment, "Capital inefficiency should be 1 - sentiment"); + assertEq(anchorShare, sentiment, "Anchor share should equal sentiment"); + assertEq(discoveryDepth, sentiment, "Discovery depth should equal sentiment"); + + // Verify anchor width is calculated independently + // Expected: base(40) + staking_adj(20 - 24 = -4) + tax_adj(16 - 10 = 6) = 42 + assertEq(anchorWidth, 42, "Anchor width should be independently calculated"); + } +} \ No newline at end of file diff --git a/onchain/test/mocks/MockKraiken.sol b/onchain/test/mocks/MockKraiken.sol new file mode 100644 index 0000000..d30d974 --- /dev/null +++ b/onchain/test/mocks/MockKraiken.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8.19; + +/** + * @title MockKraiken + * @notice Minimal mock of Kraiken token for testing Optimizer + */ +contract MockKraiken { + uint8 public constant decimals = 18; + + function totalSupply() external pure returns (uint256) { + return 1000000 * 10**18; // 1M tokens + } +} \ No newline at end of file diff --git a/onchain/test/mocks/MockStake.sol b/onchain/test/mocks/MockStake.sol new file mode 100644 index 0000000..4055f28 --- /dev/null +++ b/onchain/test/mocks/MockStake.sol @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8.19; + +/** + * @title MockStake + * @notice Mock implementation of Stake contract for testing Optimizer + * @dev Allows setting percentageStaked and averageTaxRate for testing different scenarios + */ +contract MockStake { + uint256 private _percentageStaked; + uint256 private _averageTaxRate; + + /** + * @notice Set the percentage staked for testing + * @param percentage Value between 0 and 1e18 + */ + function setPercentageStaked(uint256 percentage) external { + require(percentage <= 1e18, "Percentage too high"); + _percentageStaked = percentage; + } + + /** + * @notice Set the average tax rate for testing + * @param rate Value between 0 and 1e18 + */ + function setAverageTaxRate(uint256 rate) external { + require(rate <= 1e18, "Rate too high"); + _averageTaxRate = rate; + } + + /** + * @notice Returns the mocked percentage staked + * @return percentageStaked A number between 0 and 1e18 + */ + function getPercentageStaked() external view returns (uint256) { + return _percentageStaked; + } + + /** + * @notice Returns the mocked average tax rate + * @return averageTaxRate A number between 0 and 1e18 + */ + function getAverageTaxRate() external view returns (uint256) { + return _averageTaxRate; + } +} \ No newline at end of file