harb/onchain/test/helpers/UniswapTestBase.sol

286 lines
12 KiB
Solidity
Raw Normal View History

// 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";
/**
2025-07-25 19:09:11 +02:00
* @title UniSwapHelper
* @dev Helper contract for Uniswap V3 testing, providing reusable swap logic.
*/
2025-07-25 19:09:11 +02:00
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.
*/
2025-07-06 10:08:59 +02:00
function performSwap(uint256 amount, bool isBuy) public {
2024-12-09 23:08:24 +01:00
uint160 limit;
// Determine the swap direction
bool zeroForOne = isBuy ? token0isWeth : !token0isWeth;
2024-12-09 23:08:24 +01:00
if (isBuy) {
vm.prank(account);
weth.transfer(address(this), amount);
} else {
vm.prank(account);
harberg.approve(address(this), amount);
}
2024-12-09 23:08:24 +01:00
// Set the sqrtPriceLimitX96 based on the swap direction
2025-07-06 10:08:59 +02:00
// Get current price to set appropriate limits
(uint160 currentSqrtPrice,,,,,,) = pool.slot0();
2025-07-08 10:33:10 +02:00
2024-12-09 23:08:24 +01:00
if (zeroForOne) {
2025-07-06 10:08:59 +02:00
// Swapping token0 for token1 - price goes down
2024-12-09 23:08:24 +01:00
// sqrtPriceLimitX96 must be less than current price but greater than MIN_SQRT_RATIO
2025-07-17 21:35:18 +02:00
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
2025-07-17 21:35:18 +02:00
limit = minAllowedLimit;
} else {
// Use aggressive limit close to MIN_SQRT_RATIO to allow full price movement
limit = minAllowedLimit;
2025-07-17 21:35:18 +02:00
}
2024-12-09 23:08:24 +01:00
} else {
2025-07-06 10:08:59 +02:00
// Swapping token1 for token0 - price goes up
2024-12-09 23:08:24 +01:00
// sqrtPriceLimitX96 must be greater than current price but less than MAX_SQRT_RATIO
2025-07-06 10:08:59 +02:00
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;
2025-07-06 10:08:59 +02:00
} else {
// Use aggressive limit close to MAX_SQRT_RATIO to allow full price movement
limit = maxAllowedLimit;
2025-07-06 10:08:59 +02:00
}
2024-12-09 23:08:24 +01:00
}
2025-07-08 10:33:10 +02:00
pool.swap(account, zeroForOne, int256(amount), limit, abi.encode(account, int256(amount), isBuy));
}
2025-07-17 21:35:18 +02:00
/**
* @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
2025-07-17 21:35:18 +02:00
// 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.
*/
2025-07-08 10:33:10 +02:00
function uniswapV3SwapCallback(int256 amount0Delta, int256 amount1Delta, bytes calldata _data) external {
2025-07-17 21:35:18 +02:00
// Handle the case where no swap occurred (both deltas are 0)
if (amount0Delta == 0 && amount1Delta == 0) {
return;
}
require(amount0Delta > 0 || amount1Delta > 0);
2025-07-08 10:33:10 +02:00
(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");
}
}
2024-12-09 23:08:24 +01:00
/// @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.
2024-12-09 23:08:24 +01:00
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);
}
2025-07-08 10:33:10 +02:00
2024-12-09 23:08:24 +01:00
// pack ETH
uint256 ethOwed = token0isWeth ? amount0Owed : amount1Owed;
if (weth.balanceOf(address(this)) < ethOwed) {
weth.deposit{ value: address(this).balance }();
2024-12-09 23:08:24 +01:00
}
if (ethOwed > 0) {
weth.transfer(msg.sender, amount1Owed);
}
}
2025-07-17 21:35:18 +02:00
// ========================================
// EXTREME PRICE HANDLING
// ========================================
// Safety margin to prevent tick boundary violations (conservative approach)
int24 constant TICK_BOUNDARY_SAFETY_MARGIN = 15_000;
2025-07-17 21:35:18 +02:00
// 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;
2025-07-17 21:35:18 +02:00
while (attempts < MAX_NORMALIZATION_ATTEMPTS) {
(, int24 currentTick,,,,,) = pool.slot0();
2025-07-17 21:35:18 +02:00
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;
2025-07-17 21:35:18 +02:00
vm.prank(account);
harberg.transfer(address(this), harbToSell);
harberg.approve(address(pool), harbToSell);
2025-07-17 21:35:18 +02:00
// Sell HARB for ETH with aggressive price limits for normalization
performSwapWithAggressiveLimits(harbToSell, false);
}
} else {
// Need to move price UP (increase HARB price)
2025-07-17 21:35:18 +02:00
// 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
2025-07-17 21:35:18 +02:00
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) {
2025-08-09 18:03:31 +02:00
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) {
2025-08-09 18:03:31 +02:00
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
}
}