From 0de1cffea8ed49dc3a87b8bdf8f4eb6e5fc80d68 Mon Sep 17 00:00:00 2001 From: Your Name Date: Tue, 16 Sep 2025 22:46:43 +0200 Subject: [PATCH] another fixup of fuzzer --- onchain/AGENTS.md | 29 ++ onchain/CLAUDE.md | 163 --------- onchain/GEMINI.md | 139 -------- onchain/analysis/StreamlinedFuzzing.s.sol | 88 +++-- onchain/src/Optimizer.sol | 15 +- onchain/test/Optimizer.t.sol | 78 +++-- .../test/helpers/LiquidityBoundaryHelper.sol | 308 +++++++++++++----- onchain/test/helpers/UniswapTestBase.sol | 255 --------------- 8 files changed, 375 insertions(+), 700 deletions(-) create mode 100644 onchain/AGENTS.md delete mode 100644 onchain/CLAUDE.md delete mode 100644 onchain/GEMINI.md diff --git a/onchain/AGENTS.md b/onchain/AGENTS.md new file mode 100644 index 0000000..53f8a79 --- /dev/null +++ b/onchain/AGENTS.md @@ -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. diff --git a/onchain/CLAUDE.md b/onchain/CLAUDE.md deleted file mode 100644 index 2983778..0000000 --- a/onchain/CLAUDE.md +++ /dev/null @@ -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 diff --git a/onchain/GEMINI.md b/onchain/GEMINI.md deleted file mode 100644 index b3b71cc..0000000 --- a/onchain/GEMINI.md +++ /dev/null @@ -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 - ``` -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 - ``` - -## 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. \ No newline at end of file diff --git a/onchain/analysis/StreamlinedFuzzing.s.sol b/onchain/analysis/StreamlinedFuzzing.s.sol index 84b933b..86b4c6b 100644 --- a/onchain/analysis/StreamlinedFuzzing.s.sol +++ b/onchain/analysis/StreamlinedFuzzing.s.sol @@ -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); } -} \ No newline at end of file +} diff --git a/onchain/src/Optimizer.sol b/onchain/src/Optimizer.sol index 1e1a094..5c78b11 100644 --- a/onchain/src/Optimizer.sol +++ b/onchain/src/Optimizer.sol @@ -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; diff --git a/onchain/test/Optimizer.t.sol b/onchain/test/Optimizer.t.sol index d538fe7..5966c4c 100644 --- a/onchain/test/Optimizer.t.sol +++ b/onchain/test/Optimizer.t.sol @@ -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"); diff --git a/onchain/test/helpers/LiquidityBoundaryHelper.sol b/onchain/test/helpers/LiquidityBoundaryHelper.sol index 1c39ec8..f019d08 100644 --- a/onchain/test/helpers/LiquidityBoundaryHelper.sol +++ b/onchain/test/helpers/LiquidityBoundaryHelper.sol @@ -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 + ); } -} \ No newline at end of file + + 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); + } +} diff --git a/onchain/test/helpers/UniswapTestBase.sol b/onchain/test/helpers/UniswapTestBase.sol index 66a8d01..e3cb5ec 100644 --- a/onchain/test/helpers/UniswapTestBase.sol +++ b/onchain/test/helpers/UniswapTestBase.sol @@ -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); - } }