// SPDX-License-Identifier: GPL-3.0-or-later pragma solidity ^0.8.19; import "@uniswap-v3-periphery/libraries/PositionKey.sol"; import "@uniswap-v3-core/interfaces/IUniswapV3Pool.sol"; import "@aperture/uni-v3-lib/PoolAddress.sol"; import "@aperture/uni-v3-lib/CallbackValidation.sol"; import "@openzeppelin/token/ERC20/IERC20.sol"; import "./interfaces/IWETH9.sol"; import {Kraiken} from "./Kraiken.sol"; import {Optimizer} from "./Optimizer.sol"; import "./abstracts/ThreePositionStrategy.sol"; import "./abstracts/PriceOracle.sol"; /** * @title LiquidityManager * @notice Manages liquidity provisioning on Uniswap V3 using the three-position anti-arbitrage strategy * @dev Inherits from modular contracts for better separation of concerns and testability * * Key features: * - Three-position anti-arbitrage strategy (ANCHOR, DISCOVERY, FLOOR) * - Dynamic parameter adjustment via Optimizer contract * - Asymmetric slippage profile prevents profitable arbitrage * - Exclusive minting rights for KRAIKEN token * * Price Validation: * - 5-minute TWAP with 50-tick tolerance * - Prevents oracle manipulation attacks */ contract LiquidityManager is ThreePositionStrategy, PriceOracle { /// @notice Uniswap V3 fee tier (1%) - 10,000 basis points uint24 internal constant FEE = uint24(10_000); /// @notice Immutable contract references address private immutable factory; IWETH9 private immutable weth; Kraiken private immutable kraiken; Optimizer private immutable optimizer; IUniswapV3Pool private immutable pool; bool private immutable token0isWeth; PoolKey private poolKey; /// @notice Access control and fee management address private recenterAccess; address public feeDestination; /// @notice Custom errors error ZeroAddressInSetter(); error AddressAlreadySet(); /// @notice Access control modifier modifier onlyFeeDestination() { require(msg.sender == address(feeDestination), "only callable by feeDestination"); _; } /// @notice Constructor initializes all contract references and pool configuration /// @param _factory The address of the Uniswap V3 factory /// @param _WETH9 The address of the WETH contract /// @param _kraiken The address of the Kraiken token contract /// @param _optimizer The address of the optimizer contract constructor(address _factory, address _WETH9, address _kraiken, address _optimizer) { factory = _factory; weth = IWETH9(_WETH9); poolKey = PoolAddress.getPoolKey(_WETH9, _kraiken, FEE); pool = IUniswapV3Pool(PoolAddress.computeAddress(factory, poolKey)); kraiken = Kraiken(_kraiken); token0isWeth = _WETH9 < _kraiken; optimizer = Optimizer(_optimizer); } /// @notice Callback function for Uniswap V3 mint operations /// @param amount0Owed Amount of token0 owed for the liquidity provision /// @param amount1Owed Amount of token1 owed for the liquidity provision function uniswapV3MintCallback(uint256 amount0Owed, uint256 amount1Owed, bytes calldata) external { CallbackValidation.verifyCallback(factory, poolKey); // Handle KRAIKEN minting uint256 kraikenPulled = token0isWeth ? amount1Owed : amount0Owed; kraiken.mint(kraikenPulled); // Handle WETH conversion uint256 ethOwed = token0isWeth ? amount0Owed : amount1Owed; if (weth.balanceOf(address(this)) < ethOwed) { weth.deposit{value: address(this).balance}(); } // Transfer tokens to pool if (amount0Owed > 0) IERC20(poolKey.token0).transfer(msg.sender, amount0Owed); if (amount1Owed > 0) IERC20(poolKey.token1).transfer(msg.sender, amount1Owed); } /// @notice Sets the fee destination address (can only be called once) /// @param feeDestination_ The address that will receive trading fees function setFeeDestination(address feeDestination_) external { if (address(0) == feeDestination_) revert ZeroAddressInSetter(); if (feeDestination != address(0)) revert AddressAlreadySet(); feeDestination = feeDestination_; } /// @notice Sets recenter access for testing/emergency purposes /// @param addr Address to grant recenter access function setRecenterAccess(address addr) external onlyFeeDestination { recenterAccess = addr; } /// @notice Revokes recenter access function revokeRecenterAccess() external onlyFeeDestination { recenterAccess = address(0); } /// @notice Adjusts liquidity positions in response to price movements /// @return isUp True if price moved up (relative to token ordering) function recenter() external returns (bool isUp) { (, int24 currentTick,,,,,) = pool.slot0(); // Validate access and price stability if (recenterAccess != address(0)) { require(msg.sender == recenterAccess, "access denied"); } else { require(_isPriceStable(currentTick), "price deviated from oracle"); } // Check if price movement is sufficient for recentering isUp = false; if (positions[Stage.ANCHOR].liquidity > 0) { int24 anchorTickLower = positions[Stage.ANCHOR].tickLower; int24 anchorTickUpper = positions[Stage.ANCHOR].tickUpper; int24 centerTick = anchorTickLower + (anchorTickUpper - anchorTickLower) / 2; bool isEnough; (isUp, isEnough) = _validatePriceMovement(currentTick, centerTick, TICK_SPACING, token0isWeth); require(isEnough, "amplitude not reached."); } // Remove all existing positions and collect fees _scrapePositions(); // Update total supply tracking if price moved up if (isUp) { kraiken.setPreviousTotalSupply(kraiken.totalSupply()); } // Get optimizer parameters and set new positions try optimizer.getLiquidityParams() returns ( uint256 capitalInefficiency, uint256 anchorShare, uint24 anchorWidth, uint256 discoveryDepth ) { // Clamp parameters to valid ranges PositionParams memory params = PositionParams({ capitalInefficiency: (capitalInefficiency > 10 ** 18) ? 10 ** 18 : capitalInefficiency, anchorShare: (anchorShare > 10 ** 18) ? 10 ** 18 : anchorShare, anchorWidth: (anchorWidth > 100) ? 100 : anchorWidth, discoveryDepth: (discoveryDepth > 10 ** 18) ? 10 ** 18 : discoveryDepth }); _setPositions(currentTick, params); } catch { // Fallback to default parameters if optimizer fails PositionParams memory defaultParams = PositionParams({ capitalInefficiency: 5 * 10 ** 17, // 50% anchorShare: 5 * 10 ** 17, // 50% anchorWidth: 50, // 50% discoveryDepth: 5 * 10 ** 17 // 50% }); _setPositions(currentTick, defaultParams); } } /// @notice Removes all positions and collects fees function _scrapePositions() internal { uint256 fee0 = 0; uint256 fee1 = 0; uint256 currentPrice; for (uint256 i = uint256(Stage.FLOOR); i <= uint256(Stage.DISCOVERY); i++) { TokenPosition storage position = positions[Stage(i)]; if (position.liquidity > 0) { // Burn liquidity and collect tokens + fees (uint256 amount0, uint256 amount1) = pool.burn( position.tickLower, position.tickUpper, position.liquidity ); (uint256 collected0, uint256 collected1) = pool.collect( address(this), position.tickLower, position.tickUpper, type(uint128).max, type(uint128).max ); // Calculate fees fee0 += collected0 - amount0; fee1 += collected1 - amount1; // Record price from anchor position for VWAP if (i == uint256(Stage.ANCHOR)) { int24 tick = position.tickLower + ((position.tickUpper - position.tickLower) / 2); currentPrice = _priceAtTick(token0isWeth ? -1 * tick : tick); } } } // Transfer fees and record volume for VWAP if (fee0 > 0) { if (token0isWeth) { IERC20(address(weth)).transfer(feeDestination, fee0); _recordVolumeAndPrice(currentPrice, fee0); } else { IERC20(address(kraiken)).transfer(feeDestination, fee0); } } if (fee1 > 0) { if (token0isWeth) { IERC20(address(kraiken)).transfer(feeDestination, fee1); } else { IERC20(address(weth)).transfer(feeDestination, fee1); _recordVolumeAndPrice(currentPrice, fee1); } } // Burn any remaining KRAIKEN tokens kraiken.burn(kraiken.balanceOf(address(this))); } /// @notice Allow contract to receive ETH receive() external payable {} // ======================================== // ABSTRACT FUNCTION IMPLEMENTATIONS // ======================================== /// @notice Implementation of abstract function from PriceOracle function _getPool() internal view override returns (IUniswapV3Pool) { return pool; } /// @notice Implementation of abstract function from ThreePositionStrategy function _getKraikenToken() internal view override returns (address) { return address(kraiken); } /// @notice Implementation of abstract function from ThreePositionStrategy function _getWethToken() internal view override returns (address) { return address(weth); } /// @notice Implementation of abstract function from ThreePositionStrategy function _isToken0Weth() internal view override returns (bool) { return token0isWeth; } /// @notice Implementation of abstract function from ThreePositionStrategy function _mintPosition(Stage stage, int24 tickLower, int24 tickUpper, uint128 liquidity) internal override { pool.mint(address(this), tickLower, tickUpper, liquidity, abi.encode(poolKey)); positions[stage] = TokenPosition({ liquidity: liquidity, tickLower: tickLower, tickUpper: tickUpper }); } /// @notice Implementation of abstract function from ThreePositionStrategy function _getEthBalance() internal view override returns (uint256) { return address(this).balance + weth.balanceOf(address(this)); } /// @notice Implementation of abstract function from ThreePositionStrategy function _getOutstandingSupply() internal view override returns (uint256) { return kraiken.outstandingSupply(); } }