Merge branch 'anchor'
This commit is contained in:
commit
e5d47b1890
7 changed files with 714 additions and 3 deletions
84
onchain/analysis/anchor-width-calculations.md
Normal file
84
onchain/analysis/anchor-width-calculations.md
Normal file
|
|
@ -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
|
||||
148
onchain/analysis/staking-based-anchor-width.md
Normal file
148
onchain/analysis/staking-based-anchor-width.md
Normal file
|
|
@ -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
|
||||
88
onchain/analysis/verify_anchor_width_logic.md
Normal file
88
onchain/analysis/verify_anchor_width_logic.md
Normal file
|
|
@ -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!
|
||||
|
|
@ -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 kraiken;
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
244
onchain/test/Optimizer.t.sol
Normal file
244
onchain/test/Optimizer.t.sol
Normal file
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
14
onchain/test/mocks/MockKraiken.sol
Normal file
14
onchain/test/mocks/MockKraiken.sol
Normal file
|
|
@ -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
|
||||
}
|
||||
}
|
||||
46
onchain/test/mocks/MockStake.sol
Normal file
46
onchain/test/mocks/MockStake.sol
Normal file
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue