// SPDX-License-Identifier: MIT pragma solidity ^0.8.19; import { IWETH9 } from "../../src/interfaces/IWETH9.sol"; import { IERC20 } from "@openzeppelin/token/ERC20/IERC20.sol"; import { IUniswapV3Pool } from "@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol"; import { IUniswapV3MintCallback } from "@uniswap/v3-core/contracts/interfaces/callback/IUniswapV3MintCallback.sol"; /// @title BackgroundLP /// @notice Simulates competing liquidity providers with stacked positions /// approximating a Gaussian curve centered on current tick. /// Rebalances on demand to track the market. contract BackgroundLP is IUniswapV3MintCallback { IUniswapV3Pool public pool; IWETH9 public weth; IERC20 public kraiken; bool public token0isWeth; int24 public tickSpacing; // Track active positions for cleanup struct Position { int24 tickLower; int24 tickUpper; uint128 liquidity; } Position[6] public positions; uint256 public numPositions; // Gaussian approximation: 5 stacked positions with increasing width // Widths in multiples of tickSpacing // Layer 0: ±10 spacings (narrowest, highest concentration) // Layer 1: ±20 spacings // Layer 2: ±40 spacings // Layer 3: ±80 spacings // Layer 4: ±160 spacings (widest, lowest concentration) uint24[5] public HALF_WIDTHS = [10, 20, 40, 80, 160]; constructor(IUniswapV3Pool _pool, IWETH9 _weth, IERC20 _kraiken, bool _token0isWeth) { pool = _pool; weth = _weth; kraiken = _kraiken; token0isWeth = _token0isWeth; tickSpacing = pool.tickSpacing(); } /// @notice Deploy stacked positions centered on current tick /// @param ethPerLayer ETH to allocate per layer (total = 5 × ethPerLayer) function rebalance(uint256 ethPerLayer) external { // 1. Remove old positions _burnAll(); // 2. Get current tick (, int24 currentTick,,,,,) = pool.slot0(); int24 centerTick = (currentTick / tickSpacing) * tickSpacing; // 3. Deploy 5 stacked layers for (uint256 i = 0; i < 5; i++) { int24 halfWidth = int24(uint24(HALF_WIDTHS[i])) * tickSpacing; int24 tickLower = centerTick - halfWidth; int24 tickUpper = centerTick + halfWidth; // Clamp to valid range if (tickLower < -887_200) tickLower = -887_200; if (tickUpper > 887_200) tickUpper = 887_200; // Calculate liquidity from ETH amount // Use half the ethPerLayer since we need both tokens for in-range positions uint128 liquidity = _getLiquidityForEth(tickLower, tickUpper, ethPerLayer); if (liquidity == 0) continue; // Mint position pool.mint(address(this), tickLower, tickUpper, liquidity, ""); positions[numPositions] = Position(tickLower, tickUpper, liquidity); numPositions++; } } /// @notice Burn all active positions function _burnAll() internal { for (uint256 i = 0; i < numPositions; i++) { Position memory pos = positions[i]; if (pos.liquidity > 0) { pool.burn(pos.tickLower, pos.tickUpper, pos.liquidity); // Collect tokens back pool.collect(address(this), pos.tickLower, pos.tickUpper, type(uint128).max, type(uint128).max); } } numPositions = 0; } /// @notice Calculate liquidity for a given ETH amount and tick range function _getLiquidityForEth(int24 tickLower, int24 tickUpper, uint256 ethAmount) internal view returns (uint128) { uint160 sqrtLower = TickMath.getSqrtRatioAtTick(tickLower); uint160 sqrtUpper = TickMath.getSqrtRatioAtTick(tickUpper); (, int24 currentTick,,,,,) = pool.slot0(); uint160 sqrtCurrent = TickMath.getSqrtRatioAtTick(currentTick); if (token0isWeth) { // WETH is token0 — for in-range, need amount0 if (sqrtCurrent <= sqrtLower) { // All token0 (WETH) return LiquidityAmounts.getLiquidityForAmount0(sqrtLower, sqrtUpper, ethAmount); } else if (sqrtCurrent < sqrtUpper) { // In range — use amount0 portion return LiquidityAmounts.getLiquidityForAmount0(sqrtCurrent, sqrtUpper, ethAmount / 2); } else { return 0; // All token1, no ETH needed } } else { // WETH is token1 — for in-range, need amount1 if (sqrtCurrent >= sqrtUpper) { // All token1 (WETH) return LiquidityAmounts.getLiquidityForAmount1(sqrtLower, sqrtUpper, ethAmount); } else if (sqrtCurrent > sqrtLower) { // In range — use amount1 portion return LiquidityAmounts.getLiquidityForAmount1(sqrtLower, sqrtCurrent, ethAmount / 2); } else { return 0; // All token0, no ETH needed } } } /// @notice Uniswap V3 mint callback — provide tokens function uniswapV3MintCallback(uint256 amount0Owed, uint256 amount1Owed, bytes calldata) external override { require(msg.sender == address(pool), "not pool"); if (amount0Owed > 0) { IERC20(token0isWeth ? address(weth) : address(kraiken)).transfer(msg.sender, amount0Owed); } if (amount1Owed > 0) { IERC20(token0isWeth ? address(kraiken) : address(weth)).transfer(msg.sender, amount1Owed); } } /// @notice Allow receiving ETH receive() external payable { } } // Import 0.8-compatible math from aperture's uni-v3-lib import { LiquidityAmounts } from "@aperture/uni-v3-lib/LiquidityAmounts.sol"; import { TickMath } from "@aperture/uni-v3-lib/TickMath.sol";