another fixup of fuzzer

This commit is contained in:
Your Name 2025-09-16 22:46:43 +02:00
parent c32f1b102b
commit 0de1cffea8
8 changed files with 375 additions and 700 deletions

29
onchain/AGENTS.md Normal file
View file

@ -0,0 +1,29 @@
# Agent Brief: Kraiken Protocol
## System Snapshot
- Solidity/Foundry DeFi protocol built around the Kraiken ERC20 token, a staking auction, and a three-position Uniswap V3 liquidity manager.
- LiquidityManager.sol mints/burns supply to maintain ANCHOR (near price), DISCOVERY (offset discovery band), and FLOOR (VWAP-protected) positions with asymmetric slippage to resist arbitrage.
- VWAPTracker.sol stores squared price in X96 format, compresses history, and feeds adjusted VWAP data for FLOOR placement and ETH scarcity checks.
- Optimizer.sol (upgradeable) interprets staking sentiment to output capitalInefficiency, anchorShare, anchorWidth, and discoveryDepth parameters.
- Stake.sol runs the self-assessed tax staking system with snatching auctions, discrete tax brackets, and redistribution to tax recipients/UBI.
## Development Workflow
- Tooling: Foundry (`forge build`, `forge test`, `forge fmt`, `forge snapshot`), Anvil for local chain, Base Sepolia deployment script (`forge script ...BaseSepoliaDeploy`).
- Repo structure highlights: `src/` (core contracts), `test/helpers/` (Uniswap/Kraiken bases), `lib/uni-v3-lib` (math + JS setup), `script/` (deploy), `out/` (artifacts), config via `foundry.toml` & `remappings.txt`.
- Setup steps: clone repo, init/update submodules, install `lib/uni-v3-lib` dependencies (yarn), ensure Foundry installed.
## Strategy & Mechanics
- Outstanding supply excludes liquidity position balances; enforce 20% staking cap (~20k positions).
- Anchor width and discovery depth adjusted dynamically; anchorShare tunes ETH allocation, discoveryDepth controls liquidity density multiples (≈2x-10x), capitalInefficiency shifts VWAP floor valuation (70%-170%).
- `token0isWeth` flag flips meaning of amount0/amount1 to ETH/KRAIKEN respectively.
- Recenter logic keeps Uniswap positions aligned; ETH scarcity calculation uses `sqrt(vwapX96)` and fixed-point math.
## Testing & Analysis Suite
- Fuzzing scripts (`./analysis/run-fuzzing.sh`, `run-recorded-fuzzing.sh`) support configurable trade/stake biases, scenario recording (Run IDs), replay scripts, and summary outputs.
- Optimizer modes: Bull, Bear, Neutral, Whale, Extreme, Malicious for varied stress profiles.
- Recorded artifacts include JSON scenarios, replay Solidity scripts, CSV tick traces, and human-readable summaries for exploit triage.
## Guardrails & Conventions
- Respect access controls (`onlyLiquidityManager`, owner) and avoid editing implementation helpers like LiquidityProvider or ThreePositionStrategy.
- Debug tips: inspect position CSVs, verify token type assumptions, and monitor `EthScarcity` events during simulations.
- Staking positions tracked by `positionId`; tax rates drawn from discrete array within Stake.sol.

View file

@ -1,163 +0,0 @@
# Smart Contracts
Core KRAIKEN protocol contracts implementing the dominant liquidity manager strategy.
## Core Contracts
**Kraiken.sol** - ERC20 with self-assessed tax staking
- `outstandingSupply()` = totalSupply - liquidityManager balance
- Proportional staking pool growth/shrink on mint/burn
- 20% supply cap (20k positions max)
**LiquidityManager.sol** - Three-position strategy
- ANCHOR: Near price (1-100% width)
- DISCOVERY: Borders anchor (11k tick spacing)
- FLOOR: Deep liquidity at VWAP-adjusted price
- Asymmetric slippage prevents arbitrage
**VWAPTracker.sol** - Historical price memory
- **Stores price² (squared) in X96 format**
- Records anchor midpoint on scrape
- Max 1000x compression on overflow
- `getAdjustedVWAP()` applies capital inefficiency
**Optimizer.sol** - Dynamic parameters
- Reads staking sentiment (% staked, avg tax)
- Returns 4 params for position adjustment
- Upgradeable for new strategies
**Stake.sol** - Self-assessed tax system
- Self-assessed valuations
- Continuous auction mechanism
## Critical Implementation Details
### Token Calculations
When `token0isWeth = true`:
- Amount0 functions return **ETH** amounts
- Amount1 functions return **KRAIKEN** amounts
### Outstanding Supply
Excludes tokens used for liquidity positions:
```solidity
outstandingSupply -= pulledHarb; // Anchor KRAIKEN
outstandingSupply -= discoveryAmount; // Discovery KRAIKEN
```
### ETH Scarcity Check
```solidity
// VWAP is price² in X96, must take sqrt
uint256 sqrtVwapX96 = Math.sqrt(vwapX96) << 48;
uint256 requiredEth = outstandingSupply.mulDiv(sqrtVwapX96, 1 << 96);
```
## Optimizer Parameters
1. **capitalInefficiency** (0-1e18)
- 0% = KRAIKEN valued at 70% for reserves
- 100% = KRAIKEN valued at 170% for reserves
2. **anchorShare** (0-1e18)
- 0 anchorShare = 5% of ETH in anchor
- 1e18 anchorShare = 25% of ETH in anchor
3. **anchorWidth** (0-100)
- token width of the anchor position, for now we keep it an 50
4. **discoveryDepth** (0-1e18)
- 2x-10x liquidity multiplier vs anchor
## Fuzzing Analysis
### Fuzzing with Staking
Test strategy resilience with configurable trading and staking:
```bash
# Basic test with default parameters
./analysis/run-fuzzing.sh BullMarketOptimizer runs=20
# Advanced test with custom parameters
./analysis/run-fuzzing.sh BullMarketOptimizer runs=50 staking=on buybias=85 trades=60 stakingbias=95
```
**Parameters**:
- `runs=N`: Number of fuzzing scenarios (default: 20)
- `staking=on|off`: Enable/disable staking (default: on)
- `buybias=N`: 0-100% bias towards buying vs selling (default: 50)
- `trades=N`: Number of trades per scenario (default: 15, supports 100+ with optimizations)
- `stakingbias=N`: 0-100% bias towards staking vs unstaking (default: 80)
**How it works**:
- Uses random trading strategy with configurable biases
- Staking/unstaking happens automatically every 3rd trade
- Records position data for every trade for complete visualization
- Tracks staking metrics: attempts, successes, snatching events
### Advanced Recording & Replay System
**Find and Record Invariant Violations**:
```bash
# Run fuzzing with automatic scenario recording
./analysis/run-recorded-fuzzing.sh BullMarketOptimizer runs=50
# Output includes unique Run ID (e.g., 241218-A7K9)
# When profitable scenarios found, creates:
# - scenario_[RUN_ID]_seed[N].json (full recording)
# - replay_[RUN_ID]_seed[N].sol (replay script)
# - summary_[RUN_ID]_seed[N].txt (human summary)
```
**Replay Captured Scenarios**:
```bash
# List all scenarios from a run
./analysis/replay-scenario.sh 241218-A7K9
# Replay specific scenario
./analysis/replay-scenario.sh 241218-A7K9 1
# Creates test file and runs replay automatically
```
**Workflow for Debugging Invariant Violations**:
1. **Find violations**: Run recorded fuzzing until profitable scenario found
2. **Capture details**: System automatically records exact action sequence
3. **Share reference**: Use Run ID (e.g., "Found exploit 241218-A7K9")
4. **Replay & debug**: Deterministically reproduce the exact scenario
5. **Test fixes**: Verify fix prevents the recorded exploit
**Optimizers**:
- `BullMarketOptimizer`: Aggressive risk-taking (best for finding exploits)
- `BearMarketOptimizer`: Conservative positioning
- `NeutralMarketOptimizer`: Balanced approach
- `WhaleOptimizer`: Large capital movements
- `ExtremeOptimizer`: Cycles through parameter extremes
- `MaliciousOptimizer`: Intentionally adversarial parameters
**Output**: `fuzzing_results_[optimizer]_[timestamp]/`
- Unique Run ID for each campaign
- JSON recordings of profitable scenarios
- Replay scripts for exact reproduction
- Position CSVs showing tick movements
- Summary reports with profit calculations
## Development
```bash
forge build # Compile
forge test # Run tests
forge test -vvv # Debug mode
forge test --mc Test # Match contract
```
**Debugging Tips**:
- Check positions CSV for tick placement
- Verify token types in calculations
- Use EthScarcity events for diagnostics
## Key Files
- `test/helpers/UniswapTestBase.sol` - Pool setup
- `test/helpers/KraikenTestBase.sol` - Common utils
- `lib/uni-v3-lib/` - Uniswap V3 math
- [UNISWAP_V3_MATH.md](UNISWAP_V3_MATH.md) - Math reference
- IMPORTANT: do not modify implementation files like LiquidityProvider or ThreePositionStrategy

View file

@ -1,139 +0,0 @@
# Project Overview
This project is a Solidity-based decentralized finance (DeFi) application built using the Foundry framework. The core of the project revolves around the `Kraiken` token, a sophisticated liquidity management strategy, and a unique staking mechanism.
## Core Contracts
* **Kraiken.sol:** An ERC20 token with a self-assessed tax staking system. The supply is managed by a `LiquidityManager`, and a `stakingPool` receives a proportional share of all mints and burns. There's a 20% cap on the total supply that can be staked.
* **LiquidityManager.sol:** Implements a three-position liquidity strategy:
* **ANCHOR:** A narrow, near-price position.
* **DISCOVERY:** A wider position that borders the anchor.
* **FLOOR:** A deep liquidity position at a VWAP-adjusted price.
This strategy uses asymmetric slippage to prevent arbitrage.
* **VWAPTracker.sol:** Stores historical price data (as price squared in X96 format) to provide a time-weighted average price. It includes a `getAdjustedVWAP()` function that accounts for capital inefficiency.
* **Optimizer.sol:** A dynamic and upgradeable contract that reads staking sentiment (percentage of tokens staked, average tax rate) and returns four parameters to adjust the liquidity positions.
* **Stake.sol:** A self-assessed tax system where users can stake their `Kraiken` tokens. It features a continuous auction mechanism for staking positions.
## Architecture
The system is designed around a dominant liquidity manager strategy. The `LiquidityManager` controls the token supply and manages the three-tiered liquidity positions. The `Optimizer` dynamically adjusts the parameters of these positions based on market conditions and staking sentiment. The `Stake` contract provides a competitive environment for users to earn rewards, with a "snatching" mechanism that adds a game-theoretic layer to the staking strategy.
# Building and Running
## Dependencies
* [Foundry](https://getfoundry.sh/)
## Installation
1. Clone the repository:
```bash
git clone <repository-url>
```
2. Initialize and update submodules:
```bash
git submodule init
git submodule update
```
3. Install uni-v3-lib dependencies:
```bash
cd lib/uni-v3-lib
yarn
```
## Build
```bash
forge build
```
## Testing
```bash
forge test
```
For more verbose output:
```bash
forge test -vvv
```
To run tests matching a specific contract:
```bash
forge test --mc TestContractName
```
## Deployment
The project includes scripts for deploying to Base Sepolia.
```bash
source .env
forge script script/BaseSepoliaDeploy.sol:BaseSepoliaDeploy --slow --broadcast --verify --rpc-url ${BASE_SEPOLIA_RPC_URL}
```
# Fuzzing Analysis
The project includes a sophisticated fuzzing framework to test the resilience of the trading strategy under various market conditions.
## Running Fuzzing Tests
```bash
# Basic test with default parameters
./analysis/run-fuzzing.sh BullMarketOptimizer runs=20
# Advanced test with custom parameters
./analysis/run-fuzzing.sh BullMarketOptimizer runs=50 staking=on buybias=85 trades=60 stakingbias=95
```
### Fuzzing Parameters
* `runs=N`: Number of fuzzing scenarios (default: 20)
* `staking=on|off`: Enable/disable staking (default: on)
* `buybias=N`: 0-100% bias towards buying vs selling (default: 50)
* `trades=N`: Number of trades per scenario (default: 15)
* `stakingbias=N`: 0-100% bias towards staking vs unstaking (default: 80)
## Advanced Recording & Replay
The framework can record and replay scenarios that trigger invariant violations.
* **Record scenarios:**
```bash
./analysis/run-recorded-fuzzing.sh BullMarketOptimizer runs=50
```
* **Replay a scenario:**
```bash
./analysis/replay-scenario.sh <RUN_ID> <SEED_NUMBER>
```
## Optimizer Strategies
The fuzzing framework can be run with different optimizer strategies:
* `BullMarketOptimizer`: Aggressive risk-taking.
* `BearMarketOptimizer`: Conservative positioning.
* `NeutralMarketOptimizer`: Balanced approach.
* `WhaleOptimizer`: Large capital movements.
* `ExtremeOptimizer`: Cycles through parameter extremes.
* `MaliciousOptimizer`: Intentionally adversarial parameters.
# Development Conventions
* **Code Style:** The code follows standard Solidity style conventions.
* **Testing:** The project uses Foundry for testing. The `test` directory contains the test files.
* **Access Control:** The contracts use a combination of `onlyLiquidityManager` modifiers and owner checks to enforce access control.
* **Important:** Do not modify implementation files like `LiquidityProvider` or `ThreePositionStrategy`.
## Key Files
* `test/helpers/UniswapTestBase.sol`: Test helper for Uniswap pool setup.
* `test/helpers/KraikenTestBase.sol`: Common test utilities.
* `lib/uni-v3-lib/`: Uniswap V3 math library.
* `UNISWAP_V3_MATH.md`: Reference for the Uniswap V3 math.

View file

@ -2,7 +2,7 @@
pragma solidity ^0.8.19;
import "forge-std/Script.sol";
import "forge-std/console.sol";
import "forge-std/console2.sol";
import {TestEnvironment} from "../test/helpers/TestBase.sol";
import {IUniswapV3Pool} from "@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol";
import {IUniswapV3Factory} from "@uniswap/v3-core/contracts/interfaces/IUniswapV3Factory.sol";
@ -64,10 +64,8 @@ contract StreamlinedFuzzing is Script {
uint256 stakingBias = vm.envOr("STAKING_BIAS", uint256(80));
string memory optimizerClass = vm.envOr("OPTIMIZER_CLASS", string("BullMarketOptimizer"));
console.log("=== Streamlined Fuzzing Analysis ===");
console.log("Optimizer:", optimizerClass);
console.log("Runs:", numRuns);
console.log("Trades per run:", tradesPerRun);
console2.log("=== Streamlined Fuzzing Analysis ===");
console2.log("Optimizer:", optimizerClass);
// Deploy factory once for all runs (gas optimization)
testEnv = new TestEnvironment(fees);
@ -79,7 +77,7 @@ contract StreamlinedFuzzing is Script {
// Run fuzzing scenarios
for (uint256 runIndex = 0; runIndex < numRuns; runIndex++) {
string memory runId = string(abi.encodePacked(scenarioCode, "-", _padNumber(runIndex, 3)));
console.log("\nRun:", runId);
console2.log("\nRun:", runId);
// Initialize CSV file for this run
// Always write to analysis directory relative to project root
@ -95,9 +93,9 @@ contract StreamlinedFuzzing is Script {
totalFees1 = 0;
lastRecenterBlock = block.number;
// Fund trader based on run seed
uint256 traderFund = 50 ether + (uint256(keccak256(abi.encodePacked(runIndex, "trader"))) % 150 ether);
// Fund trader based on run seed - increased for longer campaigns
uint256 traderFund = 500 ether + (uint256(keccak256(abi.encodePacked(runIndex, "trader"))) % 500 ether);
vm.deal(trader, traderFund * 2);
vm.prank(trader);
weth.deposit{value: traderFund}();
@ -105,10 +103,6 @@ contract StreamlinedFuzzing is Script {
// Initial state
_recordState("INIT", 0);
// Debug: Check initial liquidity manager state
console.log("LM ETH balance:", address(lm).balance);
console.log("Pool address:", address(pool));
// Execute trades
for (uint256 i = 0; i < tradesPerRun; i++) {
// Check for recenter opportunity on average every 3 trades
@ -131,13 +125,14 @@ contract StreamlinedFuzzing is Script {
_executeStakingOperation(runIndex, i, stakingBias);
}
}
// Final state
_liquidateTraderHoldings();
_recordState("FINAL", 0);
}
console.log("\n=== Analysis Complete ===");
console.log("Generated", numRuns, "CSV files with prefix:", scenarioCode);
console2.log("\n=== Analysis Complete ===");
console2.log("Generated", numRuns, "CSV files with prefix:", scenarioCode);
}
function _setupEnvironment(string memory optimizerClass, bool wethIsToken0) internal {
@ -162,11 +157,11 @@ contract StreamlinedFuzzing is Script {
// Now try recenter from fee destination
vm.prank(fees);
try lm.recenter() returns (bool isUp) {
console.log("Initial recenter successful, isUp:", isUp);
console2.log("Initial recenter successful, isUp:", isUp);
} catch Error(string memory reason) {
console.log("Initial recenter failed:", reason);
console2.log("Initial recenter failed:", reason);
} catch {
console.log("Initial recenter failed with unknown error");
console2.log("Initial recenter failed with unknown error");
}
// Clear staking state
@ -204,11 +199,11 @@ contract StreamlinedFuzzing is Script {
weth.transfer(address(swapExecutor), amount);
try swapExecutor.executeBuy(amount, trader) returns (uint256 actualAmount) {
if (actualAmount == 0) {
console.log("Buy returned 0, requested:", amount);
console2.log("Buy returned 0, requested:", amount);
}
_recordState("BUY", actualAmount);
} catch Error(string memory reason) {
console.log("Buy failed:", reason);
console2.log("Buy failed:", reason);
_recordState("BUY_FAIL", amount);
}
vm.stopPrank();
@ -244,10 +239,53 @@ contract StreamlinedFuzzing is Script {
}
function _getTradeAmount(uint256 runIndex, uint256 tradeIndex, bool isBuy) internal pure returns (uint256) {
uint256 baseAmount = 1 ether + (uint256(keccak256(abi.encodePacked(runIndex, tradeIndex))) % 10 ether);
uint256 baseAmount = 10 ether + (uint256(keccak256(abi.encodePacked(runIndex, tradeIndex))) % 90 ether);
return isBuy ? baseAmount : baseAmount * 1000;
}
function _liquidateTraderHoldings() internal {
uint256 remaining = kraiken.balanceOf(trader);
uint256 attempts;
// Repeatedly sell down inventory, respecting liquidity limits in SwapExecutor
while (remaining > 0 && attempts < 10) {
uint256 prevRemaining = remaining;
vm.startPrank(trader);
kraiken.transfer(address(swapExecutor), remaining);
try swapExecutor.executeSell(remaining, trader) returns (uint256 actualAmount) {
if (actualAmount == 0) {
vm.stopPrank();
console2.log("Liquidity liquidation halted: sell returned 0");
break;
}
} catch Error(string memory reason) {
vm.stopPrank();
console2.log("Liquidity liquidation failed:", reason);
break;
} catch {
vm.stopPrank();
console2.log("Liquidity liquidation failed with unknown error");
break;
}
vm.stopPrank();
remaining = kraiken.balanceOf(trader);
if (remaining >= prevRemaining) {
console2.log("Liquidity liquidation made no progress; remaining KRAIKEN:", remaining);
break;
}
unchecked {
attempts++;
}
}
if (kraiken.balanceOf(trader) > 0) {
console2.log("Warning: trader still holds KRAIKEN after liquidation:", kraiken.balanceOf(trader));
}
}
function _recordState(string memory action, uint256 amount) internal {
// Build CSV row in parts to avoid stack too deep
string memory row = _buildRowPart1(action, amount);
@ -341,4 +379,4 @@ contract StreamlinedFuzzing is Script {
return string(result);
}
}
}

View file

@ -75,7 +75,10 @@ contract Optimizer is Initializable, UUPSUpgradeable {
pure
returns (uint256 sentimentValue)
{
// deltaS is the slack available below full staking
// Ensure percentageStaked doesn't exceed 100%
require(percentageStaked <= 1e18, "Invalid percentage staked");
// deltaS is the "slack" available below full staking
uint256 deltaS = 1e18 - percentageStaked;
if (percentageStaked > 92e16) {
@ -85,7 +88,9 @@ contract Optimizer is Initializable, UUPSUpgradeable {
sentimentValue = penalty / 2;
} else {
// For lower staked percentages, sentiment decreases roughly linearly.
uint256 baseSentiment = 1e18 - ((percentageStaked * 1e18) / (92e16));
// Ensure we don't underflow if percentageStaked approaches 92%
uint256 scaledStake = (percentageStaked * 1e18) / (92e16);
uint256 baseSentiment = scaledStake >= 1e18 ? 0 : 1e18 - scaledStake;
// Apply a penalty based on the average tax rate.
if (averageTaxRate <= 1e16) {
sentimentValue = baseSentiment;
@ -199,6 +204,12 @@ contract Optimizer is Initializable, UUPSUpgradeable {
uint256 percentageStaked = stake.getPercentageStaked();
uint256 averageTaxRate = stake.getAverageTaxRate();
uint256 sentiment = calculateSentiment(averageTaxRate, percentageStaked);
// Ensure sentiment doesn't exceed 1e18 to prevent underflow
// Cap sentiment at 1e18 if it somehow exceeds it
if (sentiment > 1e18) {
sentiment = 1e18;
}
capitalInefficiency = 1e18 - sentiment;
anchorShare = sentiment;

View file

@ -41,11 +41,9 @@ contract OptimizerTest is Test {
// 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%");
@ -59,11 +57,9 @@ contract OptimizerTest is Test {
// 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%");
@ -77,11 +73,9 @@ contract OptimizerTest is Test {
// 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%");
@ -95,11 +89,9 @@ contract OptimizerTest is Test {
// 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%");
@ -113,11 +105,9 @@ contract OptimizerTest is Test {
// 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%");
@ -131,11 +121,9 @@ contract OptimizerTest is Test {
// 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");
@ -150,11 +138,9 @@ contract OptimizerTest is Test {
// 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");
@ -167,11 +153,9 @@ contract OptimizerTest is Test {
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");
}
@ -182,15 +166,39 @@ contract OptimizerTest is Test {
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 Test edge case with high staking and high tax rate
* @dev This specific case previously caused an overflow
*/
function testHighStakingHighTaxEdgeCase() public {
// Set conditions that previously caused overflow
// ~94.6% staked, ~96.7% tax rate
mockStake.setPercentageStaked(946350908835331692);
mockStake.setAverageTaxRate(966925542613630263);
(uint256 capitalInefficiency, uint256 anchorShare, uint24 anchorWidth, uint256 discoveryDepth) = optimizer.getLiquidityParams();
// With very high staking (>92%) and high tax, sentiment reaches maximum (1e18)
// This results in zero capital inefficiency
assertEq(capitalInefficiency, 0, "Max sentiment should result in zero capital inefficiency");
// Anchor share should be at maximum
assertEq(anchorShare, 1e18, "Max sentiment should result in maximum anchor share");
// Anchor width should still be within bounds
assertTrue(anchorWidth >= 10 && anchorWidth <= 80, "Anchor width should be within bounds");
// Expected: base(40) + staking_adj(20 - 37 = -17) + tax_adj(38 - 10 = 28) = 51
assertEq(anchorWidth, 51, "Should calculate correct width for edge case");
}
/**
* @notice Fuzz test to ensure anchorWidth always stays within bounds
*/
@ -198,7 +206,7 @@ contract OptimizerTest is Test {
// Bound inputs to valid ranges
percentageStaked = bound(percentageStaked, 0, 1e18);
averageTaxRate = bound(averageTaxRate, 0, 1e18);
mockStake.setPercentageStaked(percentageStaked);
mockStake.setAverageTaxRate(averageTaxRate);
@ -208,11 +216,7 @@ contract OptimizerTest is Test {
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) {
// Commented out due to console.log compilation issue
// console.log("Bound hit - Staking:", percentageStaked / 1e16, "%, Tax:", averageTaxRate / 1e16, "%, Width:", anchorWidth);
}
// Edge cases (10 or 80) are valid and tested by assertions
}
/**
@ -227,12 +231,6 @@ contract OptimizerTest is Test {
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");

View file

@ -13,106 +13,262 @@ import {ThreePositionStrategy} from "../../src/abstracts/ThreePositionStrategy.s
*/
library LiquidityBoundaryHelper {
/**
* @notice Calculates the maximum ETH amount that can be traded (buy HARB) without exceeding position liquidity limits
* @param pool The Uniswap V3 pool
* @param liquidityManager The liquidity manager contract
* @param token0isWeth Whether token0 is WETH
* @return maxEthAmount Maximum ETH that can be safely traded
* @notice Calculates the ETH required to push price to the outer discovery bound
*/
function calculateBuyLimit(
IUniswapV3Pool pool,
ThreePositionStrategy liquidityManager,
bool token0isWeth
) internal view returns (uint256 maxEthAmount) {
) internal view returns (uint256) {
(, int24 currentTick,,,,,) = pool.slot0();
// Get position data
(uint128 anchorLiquidity, int24 anchorLower, int24 anchorUpper) = liquidityManager.positions(ThreePositionStrategy.Stage.ANCHOR);
(uint128 discoveryLiquidity, int24 discoveryLower, int24 discoveryUpper) = liquidityManager.positions(ThreePositionStrategy.Stage.DISCOVERY);
// If no positions exist, return 0 (no safe limit)
if (anchorLiquidity == 0 && discoveryLiquidity == 0) {
return 0;
}
uint256 maxEth = 0;
// Check anchor position
if (currentTick >= anchorLower && currentTick < anchorUpper && anchorLiquidity > 0) {
uint160 currentSqrtPrice = TickMath.getSqrtRatioAtTick(currentTick);
uint160 upperSqrtPrice = TickMath.getSqrtRatioAtTick(anchorUpper);
if (token0isWeth) {
maxEth = LiquidityAmounts.getAmount0ForLiquidity(currentSqrtPrice, upperSqrtPrice, anchorLiquidity);
} else {
maxEth = LiquidityAmounts.getAmount1ForLiquidity(currentSqrtPrice, upperSqrtPrice, anchorLiquidity);
}
if (token0isWeth) {
return _calculateBuyLimitToken0IsWeth(
currentTick,
anchorLiquidity,
anchorLower,
anchorUpper,
discoveryLiquidity,
discoveryLower,
discoveryUpper
);
}
// Check discovery position
else if (currentTick >= discoveryLower && currentTick < discoveryUpper && discoveryLiquidity > 0) {
uint160 currentSqrtPrice = TickMath.getSqrtRatioAtTick(currentTick);
uint160 upperSqrtPrice = TickMath.getSqrtRatioAtTick(discoveryUpper);
if (token0isWeth) {
maxEth = LiquidityAmounts.getAmount0ForLiquidity(currentSqrtPrice, upperSqrtPrice, discoveryLiquidity);
} else {
maxEth = LiquidityAmounts.getAmount1ForLiquidity(currentSqrtPrice, upperSqrtPrice, discoveryLiquidity);
}
}
// Apply safety margin (90% of calculated max)
return (maxEth * 9) / 10;
return _calculateBuyLimitToken1IsWeth(
currentTick,
anchorLiquidity,
anchorLower,
anchorUpper,
discoveryLiquidity,
discoveryLower,
discoveryUpper
);
}
/**
* @notice Calculates the maximum HARB amount that can be traded (sell HARB) without exceeding position liquidity limits
* @param pool The Uniswap V3 pool
* @param liquidityManager The liquidity manager contract
* @param token0isWeth Whether token0 is WETH
* @return maxHarbAmount Maximum HARB that can be safely traded
* @notice Calculates the HARB required to push price to the outer floor bound
*/
function calculateSellLimit(
IUniswapV3Pool pool,
ThreePositionStrategy liquidityManager,
bool token0isWeth
) internal view returns (uint256 maxHarbAmount) {
) internal view returns (uint256) {
(, int24 currentTick,,,,,) = pool.slot0();
// Get position data
(uint128 anchorLiquidity, int24 anchorLower, int24 anchorUpper) = liquidityManager.positions(ThreePositionStrategy.Stage.ANCHOR);
(uint128 floorLiquidity, int24 floorLower, int24 floorUpper) = liquidityManager.positions(ThreePositionStrategy.Stage.FLOOR);
// If no positions exist, return 0 (no safe limit)
if (anchorLiquidity == 0 && floorLiquidity == 0) {
return 0;
}
uint256 maxHarb = 0;
// Check anchor position
if (currentTick >= anchorLower && currentTick < anchorUpper && anchorLiquidity > 0) {
uint160 currentSqrtPrice = TickMath.getSqrtRatioAtTick(currentTick);
uint160 lowerSqrtPrice = TickMath.getSqrtRatioAtTick(anchorLower);
if (token0isWeth) {
maxHarb = LiquidityAmounts.getAmount1ForLiquidity(lowerSqrtPrice, currentSqrtPrice, anchorLiquidity);
} else {
maxHarb = LiquidityAmounts.getAmount0ForLiquidity(lowerSqrtPrice, currentSqrtPrice, anchorLiquidity);
}
if (token0isWeth) {
return _calculateSellLimitToken0IsWeth(
currentTick,
anchorLiquidity,
anchorLower,
anchorUpper,
floorLiquidity,
floorLower,
floorUpper
);
}
// Check floor position
else if (currentTick >= floorLower && currentTick < floorUpper && floorLiquidity > 0) {
uint160 currentSqrtPrice = TickMath.getSqrtRatioAtTick(currentTick);
uint160 lowerSqrtPrice = TickMath.getSqrtRatioAtTick(floorLower);
if (token0isWeth) {
maxHarb = LiquidityAmounts.getAmount1ForLiquidity(lowerSqrtPrice, currentSqrtPrice, floorLiquidity);
} else {
maxHarb = LiquidityAmounts.getAmount0ForLiquidity(lowerSqrtPrice, currentSqrtPrice, floorLiquidity);
}
}
// Apply safety margin (90% of calculated max)
return (maxHarb * 9) / 10;
return _calculateSellLimitToken1IsWeth(
currentTick,
anchorLiquidity,
anchorLower,
anchorUpper,
floorLiquidity,
floorLower,
floorUpper
);
}
}
function _calculateBuyLimitToken0IsWeth(
int24 currentTick,
uint128 anchorLiquidity,
int24 anchorLower,
int24 anchorUpper,
uint128 discoveryLiquidity,
int24 discoveryLower,
int24 discoveryUpper
) private pure returns (uint256) {
if (discoveryLiquidity == 0) {
return type(uint256).max;
}
int24 targetTick = discoveryUpper > anchorUpper ? discoveryUpper : anchorUpper;
if (currentTick >= targetTick) {
return 0;
}
uint256 totalEthNeeded = 0;
if (currentTick >= anchorLower && currentTick < anchorUpper && anchorLiquidity > 0) {
int24 anchorEndTick = targetTick < anchorUpper ? targetTick : anchorUpper;
totalEthNeeded += _calculateEthToMoveBetweenTicks(currentTick, anchorEndTick, anchorLiquidity);
}
if (targetTick > anchorUpper && discoveryLiquidity > 0) {
int24 discoveryStartTick = currentTick > discoveryLower ? currentTick : discoveryLower;
if (discoveryStartTick < discoveryUpper) {
totalEthNeeded += _calculateEthToMoveBetweenTicks(discoveryStartTick, targetTick, discoveryLiquidity);
}
}
return totalEthNeeded;
}
function _calculateBuyLimitToken1IsWeth(
int24 currentTick,
uint128 anchorLiquidity,
int24 anchorLower,
int24 anchorUpper,
uint128 discoveryLiquidity,
int24 discoveryLower,
int24 discoveryUpper
) private pure returns (uint256) {
if (discoveryLiquidity == 0) {
return type(uint256).max;
}
int24 targetTick = discoveryLower < anchorLower ? discoveryLower : anchorLower;
if (currentTick <= targetTick) {
return 0;
}
uint256 totalEthNeeded = 0;
if (currentTick <= anchorUpper && currentTick > anchorLower && anchorLiquidity > 0) {
int24 anchorEndTick = targetTick > anchorLower ? targetTick : anchorLower;
totalEthNeeded += _calculateEthToMoveBetweenTicksDown(currentTick, anchorEndTick, anchorLiquidity);
}
if (targetTick < anchorLower && discoveryLiquidity > 0) {
int24 discoveryStartTick = currentTick < discoveryUpper ? currentTick : discoveryUpper;
if (discoveryStartTick > discoveryLower) {
totalEthNeeded += _calculateEthToMoveBetweenTicksDown(discoveryStartTick, targetTick, discoveryLiquidity);
}
}
return totalEthNeeded;
}
function _calculateSellLimitToken0IsWeth(
int24 currentTick,
uint128 anchorLiquidity,
int24 anchorLower,
int24 anchorUpper,
uint128 floorLiquidity,
int24 floorLower,
int24 floorUpper
) private pure returns (uint256) {
if (floorLiquidity == 0) {
return type(uint256).max;
}
int24 targetTick = floorLower < anchorLower ? floorLower : anchorLower;
if (currentTick <= targetTick) {
return 0;
}
uint256 totalHarbNeeded = 0;
if (currentTick <= anchorUpper && currentTick > anchorLower && anchorLiquidity > 0) {
int24 anchorEndTick = targetTick > anchorLower ? targetTick : anchorLower;
totalHarbNeeded += _calculateHarbToMoveBetweenTicks(currentTick, anchorEndTick, anchorLiquidity);
}
if (targetTick < anchorLower && floorLiquidity > 0) {
int24 floorStartTick = currentTick < floorUpper ? currentTick : floorUpper;
if (floorStartTick > floorLower) {
totalHarbNeeded += _calculateHarbToMoveBetweenTicks(floorStartTick, targetTick, floorLiquidity);
}
}
return totalHarbNeeded;
}
function _calculateSellLimitToken1IsWeth(
int24 currentTick,
uint128 anchorLiquidity,
int24 anchorLower,
int24 anchorUpper,
uint128 floorLiquidity,
int24 floorLower,
int24 floorUpper
) private pure returns (uint256) {
if (floorLiquidity == 0) {
return type(uint256).max;
}
int24 targetTick = floorUpper > anchorUpper ? floorUpper : anchorUpper;
if (currentTick >= targetTick) {
return 0;
}
uint256 totalHarbNeeded = 0;
if (currentTick >= anchorLower && currentTick < anchorUpper && anchorLiquidity > 0) {
int24 anchorEndTick = targetTick < anchorUpper ? targetTick : anchorUpper;
totalHarbNeeded += _calculateHarbToMoveUpBetweenTicks(currentTick, anchorEndTick, anchorLiquidity);
}
if (targetTick > anchorUpper && floorLiquidity > 0) {
int24 floorStartTick = currentTick > floorLower ? currentTick : floorLower;
if (floorStartTick < floorUpper) {
totalHarbNeeded += _calculateHarbToMoveUpBetweenTicks(floorStartTick, targetTick, floorLiquidity);
}
}
return totalHarbNeeded;
}
function _calculateEthToMoveBetweenTicks(int24 fromTick, int24 toTick, uint128 liquidity) private pure returns (uint256) {
if (fromTick >= toTick || liquidity == 0) {
return 0;
}
uint160 sqrtPriceFromX96 = TickMath.getSqrtRatioAtTick(fromTick);
uint160 sqrtPriceToX96 = TickMath.getSqrtRatioAtTick(toTick);
return LiquidityAmounts.getAmount0ForLiquidity(sqrtPriceFromX96, sqrtPriceToX96, liquidity);
}
function _calculateEthToMoveBetweenTicksDown(int24 fromTick, int24 toTick, uint128 liquidity) private pure returns (uint256) {
if (fromTick <= toTick || liquidity == 0) {
return 0;
}
uint160 sqrtPriceFromX96 = TickMath.getSqrtRatioAtTick(fromTick);
uint160 sqrtPriceToX96 = TickMath.getSqrtRatioAtTick(toTick);
return LiquidityAmounts.getAmount1ForLiquidity(sqrtPriceToX96, sqrtPriceFromX96, liquidity);
}
function _calculateHarbToMoveBetweenTicks(int24 fromTick, int24 toTick, uint128 liquidity) private pure returns (uint256) {
if (fromTick <= toTick || liquidity == 0) {
return 0;
}
uint160 sqrtPriceFromX96 = TickMath.getSqrtRatioAtTick(fromTick);
uint160 sqrtPriceToX96 = TickMath.getSqrtRatioAtTick(toTick);
return LiquidityAmounts.getAmount1ForLiquidity(sqrtPriceToX96, sqrtPriceFromX96, liquidity);
}
function _calculateHarbToMoveUpBetweenTicks(int24 fromTick, int24 toTick, uint128 liquidity) private pure returns (uint256) {
if (fromTick >= toTick || liquidity == 0) {
return 0;
}
uint160 sqrtPriceFromX96 = TickMath.getSqrtRatioAtTick(fromTick);
uint160 sqrtPriceToX96 = TickMath.getSqrtRatioAtTick(toTick);
return LiquidityAmounts.getAmount0ForLiquidity(sqrtPriceFromX96, sqrtPriceToX96, liquidity);
}
}

View file

@ -284,259 +284,4 @@ abstract contract UniSwapHelper is Test {
performSwap(amountHarb, false);
// Note: No checkLiquidity call - this is the "raw" version
}
// ========================================
// INTERNAL LIQUIDITY CALCULATION HELPERS
// ========================================
function _calculateBuyLimitToken0IsWeth(
int24 currentTick,
uint128 anchorLiquidity, int24 anchorLower, int24 anchorUpper,
uint128 discoveryLiquidity, int24 discoveryLower, int24 discoveryUpper
) internal pure returns (uint256) {
// When token0 is WETH, buying HARB moves price up (towards higher ticks)
// We want to calculate how much ETH needed to move to the upper bound of discovery
if (discoveryLiquidity == 0) {
return type(uint256).max; // No discovery position, no limit
}
// Find the highest upper bound (outermost position boundary)
int24 targetTick = discoveryUpper > anchorUpper ? discoveryUpper : anchorUpper;
// If we're already at or above the target, return 0
if (currentTick >= targetTick) {
return 0;
}
// Calculate total ETH needed to move price from currentTick to targetTick
// This requires summing up ETH consumption across all positions
uint256 totalEthNeeded = 0;
// Calculate ETH needed from anchor position (if current tick is within its range)
if (currentTick >= anchorLower && currentTick < anchorUpper && anchorLiquidity > 0) {
int24 anchorEndTick = targetTick < anchorUpper ? targetTick : anchorUpper;
totalEthNeeded += _calculateEthToMoveBetweenTicks(currentTick, anchorEndTick, anchorLiquidity);
}
// Calculate ETH needed from discovery position (if applicable)
if (targetTick > anchorUpper && discoveryLiquidity > 0) {
int24 discoveryStartTick = currentTick > discoveryLower ? currentTick : discoveryLower;
if (discoveryStartTick < discoveryUpper) {
totalEthNeeded += _calculateEthToMoveBetweenTicks(discoveryStartTick, targetTick, discoveryLiquidity);
}
}
return totalEthNeeded;
}
function _calculateBuyLimitToken1IsWeth(
int24 currentTick,
uint128 anchorLiquidity, int24 anchorLower, int24 anchorUpper,
uint128 discoveryLiquidity, int24 discoveryLower, int24 discoveryUpper
) internal pure returns (uint256) {
// When token1 is WETH, buying HARB (token0) moves price down (towards lower ticks)
// We want to calculate how much ETH needed to move to the lower bound of discovery
if (discoveryLiquidity == 0) {
return type(uint256).max; // No discovery position, no limit
}
// Find the lowest lower bound (outermost position boundary)
int24 targetTick = discoveryLower < anchorLower ? discoveryLower : anchorLower;
// If we're already at or below the target, return 0
if (currentTick <= targetTick) {
return 0;
}
// Calculate total ETH needed to move price from currentTick down to targetTick
uint256 totalEthNeeded = 0;
// Calculate ETH needed from anchor position (if current tick is within its range)
if (currentTick <= anchorUpper && currentTick > anchorLower && anchorLiquidity > 0) {
int24 anchorEndTick = targetTick > anchorLower ? targetTick : anchorLower;
totalEthNeeded += _calculateEthToMoveBetweenTicksDown(currentTick, anchorEndTick, anchorLiquidity);
}
// Calculate ETH needed from discovery position (if applicable)
if (targetTick < anchorLower && discoveryLiquidity > 0) {
int24 discoveryStartTick = currentTick < discoveryUpper ? currentTick : discoveryUpper;
if (discoveryStartTick > discoveryLower) {
totalEthNeeded += _calculateEthToMoveBetweenTicksDown(discoveryStartTick, targetTick, discoveryLiquidity);
}
}
return totalEthNeeded;
}
function _calculateSellLimitToken0IsWeth(
int24 currentTick,
uint128 anchorLiquidity, int24 anchorLower, int24 anchorUpper,
uint128 floorLiquidity, int24 floorLower, int24 floorUpper
) internal pure returns (uint256) {
// When token0 is WETH, selling HARB moves price down (towards lower ticks)
// We want to calculate how much HARB needed to move to the lower bound of floor
if (floorLiquidity == 0) {
return type(uint256).max; // No floor position, no limit
}
// Find the lowest lower bound (outermost position boundary)
int24 targetTick = floorLower < anchorLower ? floorLower : anchorLower;
// If we're already at or below the target, return 0
if (currentTick <= targetTick) {
return 0;
}
// Calculate total HARB needed to move price from currentTick down to targetTick
uint256 totalHarbNeeded = 0;
// Calculate HARB needed from anchor position (if current tick is within its range)
if (currentTick <= anchorUpper && currentTick > anchorLower && anchorLiquidity > 0) {
int24 anchorEndTick = targetTick > anchorLower ? targetTick : anchorLower;
totalHarbNeeded += _calculateHarbToMoveBetweenTicks(currentTick, anchorEndTick, anchorLiquidity);
}
// Calculate HARB needed from floor position (if applicable)
if (targetTick < anchorLower && floorLiquidity > 0) {
int24 floorStartTick = currentTick < floorUpper ? currentTick : floorUpper;
if (floorStartTick > floorLower) {
totalHarbNeeded += _calculateHarbToMoveBetweenTicks(floorStartTick, targetTick, floorLiquidity);
}
}
return totalHarbNeeded;
}
function _calculateSellLimitToken1IsWeth(
int24 currentTick,
uint128 anchorLiquidity, int24 anchorLower, int24 anchorUpper,
uint128 floorLiquidity, int24 floorLower, int24 floorUpper
) internal pure returns (uint256) {
// When token1 is WETH, selling HARB (token0) moves price up (towards higher ticks)
// We want to calculate how much HARB needed to move to the upper bound of floor
if (floorLiquidity == 0) {
return type(uint256).max; // No floor position, no limit
}
// Find the highest upper bound (outermost position boundary)
int24 targetTick = floorUpper > anchorUpper ? floorUpper : anchorUpper;
// If we're already at or above the target, return 0
if (currentTick >= targetTick) {
return 0;
}
// Calculate total HARB needed to move price from currentTick up to targetTick
uint256 totalHarbNeeded = 0;
// Calculate HARB needed from anchor position (if current tick is within its range)
if (currentTick >= anchorLower && currentTick < anchorUpper && anchorLiquidity > 0) {
int24 anchorEndTick = targetTick < anchorUpper ? targetTick : anchorUpper;
totalHarbNeeded += _calculateHarbToMoveUpBetweenTicks(currentTick, anchorEndTick, anchorLiquidity);
}
// Calculate HARB needed from floor position (if applicable)
if (targetTick > anchorUpper && floorLiquidity > 0) {
int24 floorStartTick = currentTick > floorLower ? currentTick : floorLower;
if (floorStartTick < floorUpper) {
totalHarbNeeded += _calculateHarbToMoveUpBetweenTicks(floorStartTick, targetTick, floorLiquidity);
}
}
return totalHarbNeeded;
}
/**
* @notice Calculates ETH needed to move price between two ticks given liquidity
* @param fromTick Starting tick
* @param toTick Target tick (must be > fromTick)
* @param liquidity Amount of liquidity in this range
* @return ethAmount ETH needed for this price movement
*/
function _calculateEthToMoveBetweenTicks(int24 fromTick, int24 toTick, uint128 liquidity) internal pure returns (uint256 ethAmount) {
if (fromTick >= toTick || liquidity == 0) {
return 0;
}
// Get sqrt prices for the tick range
uint160 sqrtPriceFromX96 = TickMath.getSqrtRatioAtTick(fromTick);
uint160 sqrtPriceToX96 = TickMath.getSqrtRatioAtTick(toTick);
// For moving price up (buying token1 with token0), token0 is consumed
// Amount of token0 needed = liquidity * (1/sqrt(Pa) - 1/sqrt(Pb))
// Where Pa is lower price, Pb is higher price
return LiquidityAmounts.getAmount0ForLiquidity(sqrtPriceFromX96, sqrtPriceToX96, liquidity);
}
/**
* @notice Calculates ETH needed to move price down between two ticks given liquidity
* @param fromTick Starting tick (must be > toTick for downward movement)
* @param toTick Target tick
* @param liquidity Amount of liquidity in this range
* @return ethAmount ETH needed for this downward price movement
*/
function _calculateEthToMoveBetweenTicksDown(int24 fromTick, int24 toTick, uint128 liquidity) internal pure returns (uint256 ethAmount) {
if (fromTick <= toTick || liquidity == 0) {
return 0;
}
// Get sqrt prices for the tick range
uint160 sqrtPriceFromX96 = TickMath.getSqrtRatioAtTick(fromTick);
uint160 sqrtPriceToX96 = TickMath.getSqrtRatioAtTick(toTick);
// For moving price down (selling token0 for token1), when token1 is WETH
// We're actually buying token0 (HARB) with token1 (WETH)
// Amount of token1 needed = liquidity * (sqrt(Pa) - sqrt(Pb))
// Where Pa is higher price, Pb is lower price
return LiquidityAmounts.getAmount1ForLiquidity(sqrtPriceToX96, sqrtPriceFromX96, liquidity);
}
/**
* @notice Calculates HARB needed to move price between two ticks given liquidity
* @param fromTick Starting tick (must be > toTick for downward movement)
* @param toTick Target tick
* @param liquidity Amount of liquidity in this range
* @return harbAmount HARB needed for this price movement
*/
function _calculateHarbToMoveBetweenTicks(int24 fromTick, int24 toTick, uint128 liquidity) internal pure returns (uint256 harbAmount) {
if (fromTick <= toTick || liquidity == 0) {
return 0;
}
// Get sqrt prices for the tick range (note: fromTick > toTick for downward movement)
uint160 sqrtPriceFromX96 = TickMath.getSqrtRatioAtTick(fromTick);
uint160 sqrtPriceToX96 = TickMath.getSqrtRatioAtTick(toTick);
// For moving price down (selling token1 for token0), token1 is consumed
// Amount of token1 needed = liquidity * (sqrt(Pb) - sqrt(Pa))
// Where Pa is lower price, Pb is higher price
return LiquidityAmounts.getAmount1ForLiquidity(sqrtPriceToX96, sqrtPriceFromX96, liquidity);
}
/**
* @notice Calculates HARB needed to move price up between two ticks given liquidity
* @param fromTick Starting tick
* @param toTick Target tick (must be > fromTick for upward movement)
* @param liquidity Amount of liquidity in this range
* @return harbAmount HARB needed for this upward price movement
*/
function _calculateHarbToMoveUpBetweenTicks(int24 fromTick, int24 toTick, uint128 liquidity) internal pure returns (uint256 harbAmount) {
if (fromTick >= toTick || liquidity == 0) {
return 0;
}
// Get sqrt prices for the tick range
uint160 sqrtPriceFromX96 = TickMath.getSqrtRatioAtTick(fromTick);
uint160 sqrtPriceToX96 = TickMath.getSqrtRatioAtTick(toTick);
// For moving price up (selling token0 for token1), when token1 is WETH
// We're selling token0 (HARB) for token1 (WETH)
// Amount of token0 needed = liquidity * (1/sqrt(Pb) - 1/sqrt(Pa))
// Where Pa is lower price, Pb is higher price
return LiquidityAmounts.getAmount0ForLiquidity(sqrtPriceFromX96, sqrtPriceToX96, liquidity);
}
}