- 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>
144 lines
5.7 KiB
Solidity
144 lines
5.7 KiB
Solidity
// 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";
|