harb/onchain/analysis/helpers/BackgroundLP.sol

145 lines
5.7 KiB
Solidity
Raw Normal View History

// 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";