573 lines
26 KiB
Solidity
573 lines
26 KiB
Solidity
// SPDX-License-Identifier: MIT
|
|
pragma solidity ^0.8.19;
|
|
|
|
import "forge-std/Test.sol";
|
|
import "@uniswap-v3-core/interfaces/IUniswapV3Pool.sol";
|
|
import {TickMath} from "@aperture/uni-v3-lib/TickMath.sol";
|
|
import {LiquidityAmounts} from "@aperture/uni-v3-lib/LiquidityAmounts.sol";
|
|
import {SqrtPriceMath} from "@aperture/uni-v3-lib/SqrtPriceMath.sol";
|
|
import "../../src/interfaces/IWETH9.sol";
|
|
import {Kraiken} from "../../src/Kraiken.sol";
|
|
import {ThreePositionStrategy} from "../../src/abstracts/ThreePositionStrategy.sol";
|
|
|
|
/**
|
|
* @title UniSwapHelper
|
|
* @dev Helper contract for Uniswap V3 testing, providing reusable swap logic.
|
|
*/
|
|
abstract contract UniSwapHelper is Test {
|
|
address account = makeAddr("alice");
|
|
IUniswapV3Pool public pool;
|
|
IWETH9 public weth;
|
|
Kraiken public harberg;
|
|
bool public token0isWeth;
|
|
|
|
/**
|
|
* @dev Performs a swap in the Uniswap V3 pool.
|
|
* @param amount The amount to swap.
|
|
* @param isBuy True if buying WETH, false if selling.
|
|
*/
|
|
function performSwap(uint256 amount, bool isBuy) public {
|
|
uint160 limit;
|
|
// Determine the swap direction
|
|
bool zeroForOne = isBuy ? token0isWeth : !token0isWeth;
|
|
|
|
if (isBuy) {
|
|
vm.prank(account);
|
|
weth.transfer(address(this), amount);
|
|
} else {
|
|
vm.prank(account);
|
|
harberg.approve(address(this), amount);
|
|
}
|
|
|
|
// Set the sqrtPriceLimitX96 based on the swap direction
|
|
// Get current price to set appropriate limits
|
|
(uint160 currentSqrtPrice,,,,,,) = pool.slot0();
|
|
|
|
if (zeroForOne) {
|
|
// Swapping token0 for token1 - price goes down
|
|
// sqrtPriceLimitX96 must be less than current price but greater than MIN_SQRT_RATIO
|
|
uint160 minAllowedLimit = TickMath.MIN_SQRT_RATIO + 1;
|
|
// Safety check: ensure we have enough room to set a valid limit
|
|
if (currentSqrtPrice <= minAllowedLimit + 1) {
|
|
// Emergency fallback: current price is at or very close to minimum
|
|
// We can't safely set a limit, so use the minimum possible
|
|
limit = minAllowedLimit;
|
|
} else {
|
|
// Use aggressive limit close to MIN_SQRT_RATIO to allow full price movement
|
|
limit = minAllowedLimit;
|
|
}
|
|
} else {
|
|
// Swapping token1 for token0 - price goes up
|
|
// sqrtPriceLimitX96 must be greater than current price but less than MAX_SQRT_RATIO
|
|
uint160 maxAllowedLimit = TickMath.MAX_SQRT_RATIO - 1;
|
|
// Safety check: ensure we have enough room to set a valid limit
|
|
if (currentSqrtPrice >= maxAllowedLimit - 1) {
|
|
// Emergency fallback: current price is at or very close to maximum
|
|
// We can't safely set a limit, so use the maximum possible
|
|
limit = maxAllowedLimit;
|
|
} else {
|
|
// Use aggressive limit close to MAX_SQRT_RATIO to allow full price movement
|
|
limit = maxAllowedLimit;
|
|
}
|
|
}
|
|
|
|
pool.swap(account, zeroForOne, int256(amount), limit, abi.encode(account, int256(amount), isBuy));
|
|
}
|
|
|
|
/**
|
|
* @notice Performs a swap with aggressive price limits for extreme price normalization
|
|
* @param amount The amount to swap
|
|
* @param isBuy True if buying HARB, false if selling HARB
|
|
*/
|
|
function performSwapWithAggressiveLimits(uint256 amount, bool isBuy) internal {
|
|
uint160 limit;
|
|
// Determine the swap direction
|
|
bool zeroForOne = isBuy ? token0isWeth : !token0isWeth;
|
|
|
|
if (isBuy) {
|
|
vm.prank(account);
|
|
weth.transfer(address(this), amount);
|
|
} else {
|
|
vm.prank(account);
|
|
harberg.approve(address(this), amount);
|
|
}
|
|
|
|
// Set aggressive price limits that allow price to move to liquidity ranges
|
|
if (zeroForOne) {
|
|
// Swapping token0 for token1 - price goes down
|
|
// Use very aggressive limit close to MIN_SQRT_RATIO
|
|
limit = TickMath.MIN_SQRT_RATIO + 1;
|
|
} else {
|
|
// Swapping token1 for token0 - price goes up
|
|
// Use very aggressive limit close to MAX_SQRT_RATIO
|
|
limit = TickMath.MAX_SQRT_RATIO - 1;
|
|
}
|
|
|
|
pool.swap(account, zeroForOne, int256(amount), limit, abi.encode(account, int256(amount), isBuy));
|
|
}
|
|
|
|
/**
|
|
* @dev The Uniswap V3 swap callback.
|
|
*/
|
|
function uniswapV3SwapCallback(int256 amount0Delta, int256 amount1Delta, bytes calldata _data) external {
|
|
// Handle the case where no swap occurred (both deltas are 0)
|
|
if (amount0Delta == 0 && amount1Delta == 0) {
|
|
return;
|
|
}
|
|
|
|
require(amount0Delta > 0 || amount1Delta > 0);
|
|
|
|
(address seller,, bool isBuy) = abi.decode(_data, (address, uint256, bool));
|
|
|
|
(, uint256 amountToPay) =
|
|
amount0Delta > 0 ? (!token0isWeth, uint256(amount0Delta)) : (token0isWeth, uint256(amount1Delta));
|
|
if (isBuy) {
|
|
weth.transfer(msg.sender, amountToPay);
|
|
} else {
|
|
require(harberg.transferFrom(seller, msg.sender, amountToPay), "Transfer failed");
|
|
}
|
|
}
|
|
|
|
/// @notice Callback function that Uniswap V3 calls for liquidity actions requiring minting or burning of tokens.
|
|
/// @param amount0Owed The amount of token0 owed for the liquidity provision.
|
|
/// @param amount1Owed The amount of token1 owed for the liquidity provision.
|
|
/// @dev This function mints Kraiken tokens as needed and handles WETH deposits for ETH conversions during liquidity interactions.
|
|
function uniswapV3MintCallback(uint256 amount0Owed, uint256 amount1Owed, bytes calldata) external {
|
|
// CallbackValidation.verifyCallback(factory, poolKey);
|
|
// take care of harb
|
|
uint256 harbPulled = token0isWeth ? amount1Owed : amount0Owed;
|
|
if (harbPulled > 0) {
|
|
harberg.mint(harbPulled);
|
|
harberg.transfer(msg.sender, harbPulled);
|
|
}
|
|
|
|
// pack ETH
|
|
uint256 ethOwed = token0isWeth ? amount0Owed : amount1Owed;
|
|
if (weth.balanceOf(address(this)) < ethOwed) {
|
|
weth.deposit{value: address(this).balance}();
|
|
}
|
|
if (ethOwed > 0) {
|
|
weth.transfer(msg.sender, amount1Owed);
|
|
}
|
|
}
|
|
|
|
// ========================================
|
|
// EXTREME PRICE HANDLING
|
|
// ========================================
|
|
|
|
// Safety margin to prevent tick boundary violations (conservative approach)
|
|
int24 constant TICK_BOUNDARY_SAFETY_MARGIN = 15000;
|
|
|
|
// Price normalization constants
|
|
uint256 constant NORMALIZATION_HARB_PERCENTAGE = 100; // 1% of HARB balance
|
|
uint256 constant NORMALIZATION_ETH_AMOUNT = 0.01 ether; // Fixed ETH amount for normalization
|
|
uint256 constant MAX_NORMALIZATION_ATTEMPTS = 3; // Prevent infinite loops
|
|
uint256 constant PRICE_LIMIT_BUFFER = 1000; // Buffer from sqrt price limits
|
|
|
|
/**
|
|
* @notice Handles extreme price conditions by executing normalizing trades
|
|
* @dev This function should be called before any recenter operation to ensure
|
|
* the price is within safe boundaries for liquidity position creation
|
|
*/
|
|
function handleExtremePrice() internal {
|
|
uint256 attempts = 0;
|
|
|
|
while (attempts < MAX_NORMALIZATION_ATTEMPTS) {
|
|
(, int24 currentTick,,,,,) = pool.slot0();
|
|
|
|
if (currentTick >= TickMath.MAX_TICK - TICK_BOUNDARY_SAFETY_MARGIN) {
|
|
_executeNormalizingTrade(true); // Move price down
|
|
attempts++;
|
|
} else if (currentTick <= TickMath.MIN_TICK + TICK_BOUNDARY_SAFETY_MARGIN) {
|
|
_executeNormalizingTrade(false); // Move price up
|
|
attempts++;
|
|
} else {
|
|
// Price is now safe, exit loop
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* @notice Executes a small trade to move price away from tick boundaries
|
|
* @param moveDown True to move price down (sell HARB), false to move price up (buy HARB)
|
|
*/
|
|
function _executeNormalizingTrade(bool moveDown) internal {
|
|
if (moveDown) {
|
|
// Need to move price DOWN (reduce HARB price)
|
|
// This means: sell HARB for ETH (increase HARB supply in pool)
|
|
uint256 harbBalance = harberg.balanceOf(account);
|
|
if (harbBalance > 0) {
|
|
// Use 1% of account's HARB balance (conservative approach like original)
|
|
uint256 harbToSell = harbBalance / NORMALIZATION_HARB_PERCENTAGE;
|
|
if (harbToSell == 0) harbToSell = 1;
|
|
|
|
vm.prank(account);
|
|
harberg.transfer(address(this), harbToSell);
|
|
harberg.approve(address(pool), harbToSell);
|
|
|
|
// Sell HARB for ETH with aggressive price limits for normalization
|
|
performSwapWithAggressiveLimits(harbToSell, false);
|
|
}
|
|
} else {
|
|
// Need to move price UP (increase HARB price)
|
|
// This means: buy HARB with ETH (reduce HARB supply in pool)
|
|
uint256 ethBalance = weth.balanceOf(account);
|
|
if (ethBalance > 0) {
|
|
// Use small amount for normalization (like original)
|
|
uint256 ethToBuy = NORMALIZATION_ETH_AMOUNT;
|
|
if (ethToBuy > ethBalance) ethToBuy = ethBalance;
|
|
|
|
// Buy HARB with ETH with aggressive price limits for normalization
|
|
performSwapWithAggressiveLimits(ethToBuy, true);
|
|
}
|
|
}
|
|
}
|
|
|
|
// ========================================
|
|
// LIQUIDITY-AWARE TRADE SIZE CALCULATION
|
|
// ========================================
|
|
|
|
/**
|
|
* @notice Calculates the maximum ETH amount that can be traded (buy HARB) without exceeding position liquidity limits
|
|
* @dev When currentTick is in anchor range, calculates trade size to make anchor and discovery positions "full" of ETH
|
|
* @return maxEthAmount Maximum ETH that can be safely traded, 0 if no positions exist or already at limit
|
|
*/
|
|
function buyLimitToLiquidityBoundary() internal view returns (uint256 maxEthAmount) {
|
|
// Get LiquidityManager reference from test context
|
|
// This assumes the test has a 'lm' variable for the LiquidityManager
|
|
try this.getLiquidityManager() returns (ThreePositionStrategy liquidityManager) {
|
|
(, 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;
|
|
}
|
|
|
|
// Calculate based on token ordering and current price position
|
|
if (token0isWeth) {
|
|
return _calculateBuyLimitToken0IsWeth(currentTick, anchorLiquidity, anchorLower, anchorUpper, discoveryLiquidity, discoveryLower, discoveryUpper);
|
|
} else {
|
|
return _calculateBuyLimitToken1IsWeth(currentTick, anchorLiquidity, anchorLower, anchorUpper, discoveryLiquidity, discoveryLower, discoveryUpper);
|
|
}
|
|
} catch {
|
|
return 0; // Safe fallback if LiquidityManager access fails
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @notice Calculates the maximum HARB amount that can be traded (sell HARB) without exceeding position liquidity limits
|
|
* @dev When currentTick is in anchor range, calculates trade size to make anchor and floor positions "full" of HARB
|
|
* @return maxHarbAmount Maximum HARB that can be safely traded, 0 if no positions exist or already at limit
|
|
*/
|
|
function sellLimitToLiquidityBoundary() internal view returns (uint256 maxHarbAmount) {
|
|
try this.getLiquidityManager() returns (ThreePositionStrategy liquidityManager) {
|
|
(, 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;
|
|
}
|
|
|
|
// Calculate based on token ordering and current price position
|
|
if (token0isWeth) {
|
|
return _calculateSellLimitToken0IsWeth(currentTick, anchorLiquidity, anchorLower, anchorUpper, floorLiquidity, floorLower, floorUpper);
|
|
} else {
|
|
return _calculateSellLimitToken1IsWeth(currentTick, anchorLiquidity, anchorLower, anchorUpper, floorLiquidity, floorLower, floorUpper);
|
|
}
|
|
} catch {
|
|
return 0; // Safe fallback if LiquidityManager access fails
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @notice Helper function to get LiquidityManager reference from test context
|
|
* @dev This function should be overridden in the test contract to return the actual LiquidityManager instance
|
|
* @return liquidityManager The LiquidityManager contract instance
|
|
*/
|
|
function getLiquidityManager() external view virtual returns (ThreePositionStrategy liquidityManager) {
|
|
revert("getLiquidityManager must be implemented in test contract");
|
|
}
|
|
|
|
/**
|
|
* @notice Raw buy operation without liquidity limit checking
|
|
* @param amountEth Amount of ETH to spend buying HARB
|
|
*/
|
|
function buyRaw(uint256 amountEth) internal {
|
|
performSwap(amountEth, true);
|
|
// Note: No checkLiquidity call - this is the "raw" version
|
|
}
|
|
|
|
/**
|
|
* @notice Raw sell operation without liquidity limit checking
|
|
* @param amountHarb Amount of HARB to sell for ETH
|
|
*/
|
|
function sellRaw(uint256 amountHarb) internal {
|
|
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);
|
|
}
|
|
}
|