harb/onchain/analysis/helpers/BackgroundLP.sol
openhands b7260b2eaf chore: analysis tooling, research artifacts, and code quality
- Analysis: parameter sweep scripts, adversarial testing, 2D frontier maps
- Research: KRAIKEN_RESEARCH_REPORT, SECURITY_REVIEW, STORAGE_LAYOUT
- FuzzingBase: consolidated fuzzing helper, BackgroundLP simulation
- Sweep results: CSV data for full 4D sweep (1050 combos), bull-bear,
  AS sweep, VWAP fix validation
- Code quality: .gitignore for fuzz CSVs, gas snapshot, updated docs
- Remove dead analysis helpers (CSVHelper, CSVManager, ScenarioRecorder)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-13 18:22:03 +00:00

144 lines
5.7 KiB
Solidity
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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