another fixup of fuzzer
This commit is contained in:
parent
c32f1b102b
commit
0de1cffea8
8 changed files with 375 additions and 700 deletions
29
onchain/AGENTS.md
Normal file
29
onchain/AGENTS.md
Normal 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.
|
||||
|
|
@ -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
|
||||
|
|
@ -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.
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue