// SPDX-License-Identifier: MIT pragma solidity ^0.8.19; import { Kraiken } from "../../src/Kraiken.sol"; import { ThreePositionStrategy } from "../../src/abstracts/ThreePositionStrategy.sol"; import "../../src/interfaces/IWETH9.sol"; import { LiquidityBoundaryHelper } from "./LiquidityBoundaryHelper.sol"; import { LiquidityAmounts } from "@aperture/uni-v3-lib/LiquidityAmounts.sol"; import { SqrtPriceMath } from "@aperture/uni-v3-lib/SqrtPriceMath.sol"; import { TickMath } from "@aperture/uni-v3-lib/TickMath.sol"; import "@uniswap-v3-core/interfaces/IUniswapV3Pool.sol"; import "forge-std/Test.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 = 15_000; // 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) { return LiquidityBoundaryHelper.calculateBuyLimit(pool, liquidityManager, token0isWeth); } 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) { return LiquidityBoundaryHelper.calculateSellLimit(pool, liquidityManager, token0isWeth); } 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 } }