2026-02-27 13:08:53 +00:00
|
|
|
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
|
|
pragma solidity ^0.8.19;
|
|
|
|
|
|
|
|
|
|
|
|
import { FormatLib } from "./FormatLib.sol";
|
|
|
|
|
|
import { MockToken } from "./MockToken.sol";
|
|
|
|
|
|
import { LiquidityAmounts } from "@aperture/uni-v3-lib/LiquidityAmounts.sol";
|
|
|
|
|
|
import { TickMath } from "@aperture/uni-v3-lib/TickMath.sol";
|
|
|
|
|
|
import { Math } from "@openzeppelin/utils/math/Math.sol";
|
|
|
|
|
|
import { IUniswapV3Pool } from "@uniswap-v3-core/interfaces/IUniswapV3Pool.sol";
|
|
|
|
|
|
import { IUniswapV3MintCallback } from "@uniswap-v3-core/interfaces/callback/IUniswapV3MintCallback.sol";
|
|
|
|
|
|
import { console2 } from "forge-std/console2.sol";
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* @title BaselineStrategies
|
|
|
|
|
|
* @notice Manages three comparison strategies run alongside KrAIken during event replay:
|
|
|
|
|
|
*
|
|
|
|
|
|
* HODL — Hold initial token amounts, no LP. Zero fees, zero IL by definition.
|
|
|
|
|
|
* Full-Range — Single position from aligned MIN_TICK to MAX_TICK. Set once, never rebalanced.
|
|
|
|
|
|
* Always 100% time-in-range.
|
|
|
|
|
|
* Fixed-Width — ±FIXED_WIDTH_TICKS around current price. Rebalanced at the same block-interval
|
|
|
|
|
|
* as KrAIken when the active tick leaves the range.
|
|
|
|
|
|
*
|
2026-02-27 13:43:49 +00:00
|
|
|
|
* Call order:
|
|
|
|
|
|
* 1. initialize(initialCapital) — during vm.startBroadcast()
|
|
|
|
|
|
* 2. maybeUpdate(blockNum) — called by EventReplayer on every block during replay
|
|
|
|
|
|
* 3. collectFinalFees() — after replay, before logFinalSummary / getResults
|
|
|
|
|
|
* 4. logFinalSummary() / getResults() — view, rely on collectFinalFees() having run
|
2026-02-27 13:08:53 +00:00
|
|
|
|
*
|
2026-02-27 13:43:49 +00:00
|
|
|
|
* Capital:
|
|
|
|
|
|
* All strategies receive the same initial capital split 50/50 by value.
|
|
|
|
|
|
* HODL holds the same token amounts that Full-Range LP actually deployed, so the
|
|
|
|
|
|
* HODL vs LP comparison is perfectly apples-to-apples.
|
|
|
|
|
|
*
|
|
|
|
|
|
* Fee accounting:
|
|
|
|
|
|
* Fixed-Width fees are accumulated at each rebalance's contemporaneous sqrtPriceX96 into
|
|
|
|
|
|
* fwTotalFeesToken0. Full-Range fees are collected once (in collectFinalFees) at the final
|
|
|
|
|
|
* price into frTotalFeesToken0. Both accumulators are used directly by getResults().
|
2026-02-27 13:08:53 +00:00
|
|
|
|
*
|
|
|
|
|
|
* WARNING — backtesting use only. No access control.
|
|
|
|
|
|
*/
|
|
|
|
|
|
contract BaselineStrategies is IUniswapV3MintCallback {
|
|
|
|
|
|
using Math for uint256;
|
|
|
|
|
|
using FormatLib for uint256;
|
|
|
|
|
|
using FormatLib for int256;
|
|
|
|
|
|
|
|
|
|
|
|
// -------------------------------------------------------------------------
|
|
|
|
|
|
// Types
|
|
|
|
|
|
// -------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
/// @notice Packed result for a single strategy, consumed by Reporter.
|
|
|
|
|
|
struct StrategyResult {
|
|
|
|
|
|
uint256 initialCapitalToken0;
|
|
|
|
|
|
uint256 finalValueToken0;
|
|
|
|
|
|
uint256 feesToken0;
|
|
|
|
|
|
uint256 rebalances;
|
|
|
|
|
|
int256 ilToken0;
|
|
|
|
|
|
int256 netPnLToken0;
|
|
|
|
|
|
uint256 blocksInRange;
|
|
|
|
|
|
uint256 totalBlocks;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// -------------------------------------------------------------------------
|
|
|
|
|
|
// Constants
|
|
|
|
|
|
// -------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
uint256 internal constant Q96 = 2 ** 96;
|
|
|
|
|
|
|
|
|
|
|
|
/// @notice Half-width of the fixed-width LP range in ticks (total width = 2×).
|
|
|
|
|
|
int24 public constant FIXED_WIDTH_TICKS = 2000;
|
|
|
|
|
|
|
|
|
|
|
|
// -------------------------------------------------------------------------
|
|
|
|
|
|
// Immutables
|
|
|
|
|
|
// -------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
IUniswapV3Pool public immutable pool;
|
|
|
|
|
|
MockToken public immutable token0;
|
|
|
|
|
|
MockToken public immutable token1;
|
|
|
|
|
|
bool public immutable token0isWeth;
|
|
|
|
|
|
uint256 public immutable recenterInterval;
|
|
|
|
|
|
int24 public immutable tickSpacing;
|
|
|
|
|
|
|
|
|
|
|
|
// -------------------------------------------------------------------------
|
|
|
|
|
|
// HODL state
|
|
|
|
|
|
// -------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
uint256 public hodlEntryToken0;
|
|
|
|
|
|
uint256 public hodlEntryToken1;
|
|
|
|
|
|
uint256 public hodlInitialValueToken0;
|
|
|
|
|
|
|
|
|
|
|
|
// -------------------------------------------------------------------------
|
|
|
|
|
|
// Full-Range LP state
|
|
|
|
|
|
// -------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
uint128 public fullRangeLiquidity;
|
|
|
|
|
|
int24 public frLo;
|
|
|
|
|
|
int24 public frHi;
|
2026-02-27 13:43:49 +00:00
|
|
|
|
/// @notice Token amounts deployed into the Full-Range position at entry.
|
2026-02-27 13:08:53 +00:00
|
|
|
|
uint256 public frEntryToken0;
|
|
|
|
|
|
uint256 public frEntryToken1;
|
|
|
|
|
|
uint256 public frInitialValueToken0;
|
2026-02-27 13:43:49 +00:00
|
|
|
|
/// @notice Raw fee totals (populated by collectFinalFees).
|
2026-02-27 13:08:53 +00:00
|
|
|
|
uint256 public frFees0;
|
|
|
|
|
|
uint256 public frFees1;
|
2026-02-27 13:43:49 +00:00
|
|
|
|
/// @notice Fees in token0-equivalent units at collection price (set by collectFinalFees).
|
|
|
|
|
|
uint256 public frTotalFeesToken0;
|
2026-02-27 13:08:53 +00:00
|
|
|
|
uint256 public frTotalBlocks;
|
|
|
|
|
|
uint256 public frBlocksInRange;
|
|
|
|
|
|
|
|
|
|
|
|
// -------------------------------------------------------------------------
|
|
|
|
|
|
// Fixed-Width LP state
|
|
|
|
|
|
// -------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
uint128 public fwLiquidity;
|
|
|
|
|
|
int24 public fwLo;
|
|
|
|
|
|
int24 public fwHi;
|
|
|
|
|
|
/// @notice Entry amounts for the CURRENT open period (reset on each rebalance).
|
|
|
|
|
|
uint256 public fwPeriodEntryToken0;
|
|
|
|
|
|
uint256 public fwPeriodEntryToken1;
|
|
|
|
|
|
uint256 public fwInitialValueToken0;
|
2026-02-27 13:43:49 +00:00
|
|
|
|
/// @notice Raw fee totals accumulated across all periods.
|
2026-02-27 13:08:53 +00:00
|
|
|
|
uint256 public fwFees0;
|
|
|
|
|
|
uint256 public fwFees1;
|
2026-02-27 13:43:49 +00:00
|
|
|
|
/// @notice Fees accumulated at contemporaneous prices across all closed periods,
|
|
|
|
|
|
/// plus the current period fees added by collectFinalFees().
|
2026-02-27 13:08:53 +00:00
|
|
|
|
uint256 public fwTotalFeesToken0;
|
|
|
|
|
|
uint256 public fwRebalances;
|
|
|
|
|
|
/// @notice Cumulative IL from closed periods (token0 units, negative = LP underperformed HODL).
|
|
|
|
|
|
int256 public fwCumulativeIL;
|
|
|
|
|
|
uint256 public fwTotalBlocks;
|
|
|
|
|
|
uint256 public fwBlocksInRange;
|
|
|
|
|
|
uint256 public fwLastCheckBlock;
|
|
|
|
|
|
|
|
|
|
|
|
// -------------------------------------------------------------------------
|
|
|
|
|
|
// Common state
|
|
|
|
|
|
// -------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
bool public initialized;
|
|
|
|
|
|
uint256 public lastUpdateBlock;
|
2026-02-27 13:43:49 +00:00
|
|
|
|
/// @notice First block number observed during replay (set on first maybeUpdate call).
|
|
|
|
|
|
uint256 public firstUpdateBlock;
|
2026-02-27 13:08:53 +00:00
|
|
|
|
|
|
|
|
|
|
// -------------------------------------------------------------------------
|
|
|
|
|
|
// Constructor
|
|
|
|
|
|
// -------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
constructor(IUniswapV3Pool _pool, MockToken _token0, MockToken _token1, bool _token0isWeth, uint256 _recenterInterval) {
|
|
|
|
|
|
pool = _pool;
|
|
|
|
|
|
token0 = _token0;
|
|
|
|
|
|
token1 = _token1;
|
|
|
|
|
|
token0isWeth = _token0isWeth;
|
|
|
|
|
|
recenterInterval = _recenterInterval;
|
|
|
|
|
|
tickSpacing = _pool.tickSpacing();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// -------------------------------------------------------------------------
|
|
|
|
|
|
// Initialization
|
|
|
|
|
|
// -------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* @notice Deploy all three baseline strategies with `initialCapital` token0 equivalent.
|
2026-02-27 13:43:49 +00:00
|
|
|
|
* Capital is split 50/50 by value: half in token0, the equivalent in token1.
|
|
|
|
|
|
* Full-Range LP is deployed first; HODL holds the same token amounts that
|
|
|
|
|
|
* Full-Range actually consumed so both strategies start with identical capital.
|
2026-02-27 13:08:53 +00:00
|
|
|
|
*/
|
|
|
|
|
|
function initialize(uint256 initialCapital) external {
|
|
|
|
|
|
require(!initialized, "BaselineStrategies: already initialized");
|
|
|
|
|
|
initialized = true;
|
|
|
|
|
|
|
|
|
|
|
|
(uint160 sqrtPriceX96, int24 currentTick,,,,,) = pool.slot0();
|
|
|
|
|
|
|
|
|
|
|
|
uint256 half0 = initialCapital / 2;
|
|
|
|
|
|
uint256 half1 = _valueInToken1(half0, sqrtPriceX96);
|
|
|
|
|
|
|
2026-02-27 13:43:49 +00:00
|
|
|
|
// ----- Full-Range LP (deployed first so HODL can mirror its amounts) -----
|
2026-02-27 13:08:53 +00:00
|
|
|
|
{
|
|
|
|
|
|
int24 lo = _fullRangeLo();
|
|
|
|
|
|
int24 hi = _fullRangeHi();
|
|
|
|
|
|
|
|
|
|
|
|
uint128 liq = LiquidityAmounts.getLiquidityForAmounts(
|
|
|
|
|
|
sqrtPriceX96, TickMath.getSqrtRatioAtTick(lo), TickMath.getSqrtRatioAtTick(hi), half0, half1
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
if (liq > 0) {
|
|
|
|
|
|
(uint256 used0, uint256 used1) = pool.mint(address(this), lo, hi, liq, "");
|
|
|
|
|
|
fullRangeLiquidity = liq;
|
|
|
|
|
|
frLo = lo;
|
|
|
|
|
|
frHi = hi;
|
|
|
|
|
|
frEntryToken0 = used0;
|
|
|
|
|
|
frEntryToken1 = used1;
|
|
|
|
|
|
frInitialValueToken0 = _valueInToken0(used0, used1, sqrtPriceX96);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-27 13:43:49 +00:00
|
|
|
|
// ----- HODL: hold the same amounts Full-Range actually deployed -----
|
|
|
|
|
|
// Falls back to half0/half1 if FullRange mint produced zero (edge case).
|
|
|
|
|
|
if (frEntryToken0 > 0 || frEntryToken1 > 0) {
|
|
|
|
|
|
hodlEntryToken0 = frEntryToken0;
|
|
|
|
|
|
hodlEntryToken1 = frEntryToken1;
|
|
|
|
|
|
hodlInitialValueToken0 = frInitialValueToken0;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
hodlEntryToken0 = half0;
|
|
|
|
|
|
hodlEntryToken1 = half1;
|
|
|
|
|
|
hodlInitialValueToken0 = _valueInToken0(half0, half1, sqrtPriceX96);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-27 13:08:53 +00:00
|
|
|
|
// ----- Fixed-Width LP -----
|
|
|
|
|
|
{
|
|
|
|
|
|
(int24 lo, int24 hi) = _computeFixedRange(currentTick);
|
|
|
|
|
|
|
|
|
|
|
|
uint128 liq = LiquidityAmounts.getLiquidityForAmounts(
|
|
|
|
|
|
sqrtPriceX96, TickMath.getSqrtRatioAtTick(lo), TickMath.getSqrtRatioAtTick(hi), half0, half1
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
if (liq > 0) {
|
|
|
|
|
|
(uint256 used0, uint256 used1) = pool.mint(address(this), lo, hi, liq, "");
|
|
|
|
|
|
fwLiquidity = liq;
|
|
|
|
|
|
fwLo = lo;
|
|
|
|
|
|
fwHi = hi;
|
|
|
|
|
|
fwPeriodEntryToken0 = used0;
|
|
|
|
|
|
fwPeriodEntryToken1 = used1;
|
|
|
|
|
|
fwInitialValueToken0 = _valueInToken0(used0, used1, sqrtPriceX96);
|
|
|
|
|
|
}
|
|
|
|
|
|
fwLastCheckBlock = block.number;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
console2.log("=== Baseline Strategies Initialized ===");
|
|
|
|
|
|
console2.log(
|
|
|
|
|
|
string.concat(
|
|
|
|
|
|
"HODL: entry=[",
|
|
|
|
|
|
hodlEntryToken0.str(),
|
|
|
|
|
|
",",
|
|
|
|
|
|
hodlEntryToken1.str(),
|
|
|
|
|
|
"] initialValue=",
|
|
|
|
|
|
hodlInitialValueToken0.str()
|
|
|
|
|
|
)
|
|
|
|
|
|
);
|
|
|
|
|
|
console2.log(
|
|
|
|
|
|
string.concat(
|
|
|
|
|
|
"FullRange: liq=",
|
|
|
|
|
|
uint256(fullRangeLiquidity).str(),
|
|
|
|
|
|
" ticks=[",
|
|
|
|
|
|
int256(frLo).istr(),
|
|
|
|
|
|
",",
|
|
|
|
|
|
int256(frHi).istr(),
|
|
|
|
|
|
"] entry=[",
|
|
|
|
|
|
frEntryToken0.str(),
|
|
|
|
|
|
",",
|
|
|
|
|
|
frEntryToken1.str(),
|
|
|
|
|
|
"]"
|
|
|
|
|
|
)
|
|
|
|
|
|
);
|
|
|
|
|
|
console2.log(
|
|
|
|
|
|
string.concat(
|
|
|
|
|
|
"FixedWidth: liq=",
|
|
|
|
|
|
uint256(fwLiquidity).str(),
|
|
|
|
|
|
" ticks=[",
|
|
|
|
|
|
int256(fwLo).istr(),
|
|
|
|
|
|
",",
|
|
|
|
|
|
int256(fwHi).istr(),
|
|
|
|
|
|
"] entry=[",
|
|
|
|
|
|
fwPeriodEntryToken0.str(),
|
|
|
|
|
|
",",
|
|
|
|
|
|
fwPeriodEntryToken1.str(),
|
|
|
|
|
|
"]"
|
|
|
|
|
|
)
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// -------------------------------------------------------------------------
|
|
|
|
|
|
// Per-block update (called by EventReplayer after each block advance)
|
|
|
|
|
|
// -------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* @notice Update time-in-range counters and trigger FixedWidth rebalancing when needed.
|
|
|
|
|
|
* @param blockNum Current block number as advanced by EventReplayer.
|
|
|
|
|
|
*/
|
|
|
|
|
|
function maybeUpdate(uint256 blockNum) external {
|
|
|
|
|
|
if (!initialized) return;
|
|
|
|
|
|
if (blockNum == lastUpdateBlock) return;
|
|
|
|
|
|
lastUpdateBlock = blockNum;
|
|
|
|
|
|
|
2026-02-27 13:43:49 +00:00
|
|
|
|
// Record the first block seen during replay for accurate period reporting.
|
|
|
|
|
|
if (firstUpdateBlock == 0) firstUpdateBlock = blockNum;
|
|
|
|
|
|
|
2026-02-27 13:08:53 +00:00
|
|
|
|
(, int24 currentTick,,,,,) = pool.slot0();
|
|
|
|
|
|
|
|
|
|
|
|
// Full-Range: always in range.
|
|
|
|
|
|
frTotalBlocks++;
|
|
|
|
|
|
frBlocksInRange++;
|
|
|
|
|
|
|
|
|
|
|
|
// Fixed-Width: track in-range blocks.
|
|
|
|
|
|
fwTotalBlocks++;
|
|
|
|
|
|
if (fwLiquidity > 0 && currentTick >= fwLo && currentTick < fwHi) {
|
|
|
|
|
|
fwBlocksInRange++;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Fixed-Width rebalance: check every recenterInterval blocks.
|
|
|
|
|
|
if (fwLiquidity > 0 && blockNum >= fwLastCheckBlock + recenterInterval) {
|
|
|
|
|
|
fwLastCheckBlock = blockNum;
|
|
|
|
|
|
if (currentTick < fwLo || currentTick >= fwHi) {
|
|
|
|
|
|
_rebalanceFixedWidth(currentTick);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// -------------------------------------------------------------------------
|
2026-02-27 13:43:49 +00:00
|
|
|
|
// Post-replay fee collection (MUST be called before logFinalSummary / getResults)
|
2026-02-27 13:08:53 +00:00
|
|
|
|
// -------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
2026-02-27 13:43:49 +00:00
|
|
|
|
* @notice Materialize accrued fees for the still-open Full-Range and Fixed-Width positions.
|
|
|
|
|
|
* Uses pool.burn(lo, hi, 0) to trigger fee accrual without removing liquidity,
|
|
|
|
|
|
* then pool.collect() to sweep fees to this contract.
|
|
|
|
|
|
*
|
|
|
|
|
|
* Call once after replay ends and before any summary/reporting functions.
|
|
|
|
|
|
* Idempotent if positions have zero pending fees.
|
|
|
|
|
|
*/
|
|
|
|
|
|
function collectFinalFees() external {
|
|
|
|
|
|
(uint160 sqrtPriceX96,,,,,,) = pool.slot0();
|
|
|
|
|
|
|
|
|
|
|
|
// ----- Full-Range: single collection at end-of-replay price -----
|
|
|
|
|
|
if (fullRangeLiquidity > 0) {
|
|
|
|
|
|
pool.burn(frLo, frHi, 0); // trigger fee accrual; no liquidity removed
|
|
|
|
|
|
(uint128 f0, uint128 f1) = pool.collect(address(this), frLo, frHi, type(uint128).max, type(uint128).max);
|
|
|
|
|
|
frFees0 += uint256(f0);
|
|
|
|
|
|
frFees1 += uint256(f1);
|
|
|
|
|
|
// Value all Full-Range fees at current price (only one collection, so no
|
|
|
|
|
|
// historical repricing concern for this strategy).
|
|
|
|
|
|
frTotalFeesToken0 = _valueInToken0(frFees0, frFees1, sqrtPriceX96);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ----- Fixed-Width: current open period fees (historical fees already in fwTotalFeesToken0) -----
|
|
|
|
|
|
if (fwLiquidity > 0) {
|
|
|
|
|
|
pool.burn(fwLo, fwHi, 0); // trigger fee accrual; no liquidity removed
|
|
|
|
|
|
(uint128 f0, uint128 f1) = pool.collect(address(this), fwLo, fwHi, type(uint128).max, type(uint128).max);
|
|
|
|
|
|
fwFees0 += uint256(f0);
|
|
|
|
|
|
fwFees1 += uint256(f1);
|
|
|
|
|
|
// Add current-period fees at current price to the historical-price accumulator.
|
|
|
|
|
|
fwTotalFeesToken0 += _valueInToken0(uint256(f0), uint256(f1), sqrtPriceX96);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// -------------------------------------------------------------------------
|
|
|
|
|
|
// Final summary (view — requires collectFinalFees() to have been called first)
|
|
|
|
|
|
// -------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* @notice Log final metrics for all three strategies. Call once after collectFinalFees().
|
2026-02-27 13:08:53 +00:00
|
|
|
|
*/
|
|
|
|
|
|
function logFinalSummary() external view {
|
|
|
|
|
|
(uint160 sqrtPriceX96,,,,,,) = pool.slot0();
|
|
|
|
|
|
|
|
|
|
|
|
// ----- HODL -----
|
|
|
|
|
|
uint256 hodlFinalValue = _valueInToken0(hodlEntryToken0, hodlEntryToken1, sqrtPriceX96);
|
|
|
|
|
|
int256 hodlNetPnL = int256(hodlFinalValue) - int256(hodlInitialValueToken0);
|
|
|
|
|
|
console2.log("[BASELINE][HODL][SUMMARY] === HODL ===");
|
|
|
|
|
|
console2.log(
|
|
|
|
|
|
string.concat(
|
|
|
|
|
|
"[BASELINE][HODL][SUMMARY] initialValue=",
|
|
|
|
|
|
hodlInitialValueToken0.str(),
|
|
|
|
|
|
" finalValue=",
|
|
|
|
|
|
hodlFinalValue.str(),
|
|
|
|
|
|
" netPnL=",
|
|
|
|
|
|
hodlNetPnL.istr()
|
|
|
|
|
|
)
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
// ----- Full-Range LP -----
|
|
|
|
|
|
{
|
|
|
|
|
|
(uint256 fr0, uint256 fr1) = _positionAmounts(fullRangeLiquidity, frLo, frHi, sqrtPriceX96);
|
|
|
|
|
|
uint256 frFinalPos = _valueInToken0(fr0, fr1, sqrtPriceX96);
|
2026-02-27 13:43:49 +00:00
|
|
|
|
// frTotalFeesToken0 populated by collectFinalFees().
|
2026-02-27 13:08:53 +00:00
|
|
|
|
uint256 frHodlVal = _valueInToken0(frEntryToken0, frEntryToken1, sqrtPriceX96);
|
|
|
|
|
|
int256 frIL = int256(frFinalPos) - int256(frHodlVal);
|
2026-02-27 13:43:49 +00:00
|
|
|
|
int256 frNetPnL = frIL + int256(frTotalFeesToken0);
|
|
|
|
|
|
uint256 frFinalValue = frFinalPos + frTotalFeesToken0;
|
2026-02-27 13:08:53 +00:00
|
|
|
|
uint256 frTIR = frTotalBlocks > 0 ? (frBlocksInRange * 10_000) / frTotalBlocks : 10_000;
|
|
|
|
|
|
console2.log("[BASELINE][FR][SUMMARY] === Full-Range LP ===");
|
|
|
|
|
|
console2.log(
|
|
|
|
|
|
string.concat(
|
|
|
|
|
|
"[BASELINE][FR][SUMMARY] initialValue=",
|
|
|
|
|
|
frInitialValueToken0.str(),
|
2026-02-27 13:43:49 +00:00
|
|
|
|
" finalValue=",
|
|
|
|
|
|
frFinalValue.str(),
|
2026-02-27 13:08:53 +00:00
|
|
|
|
" feesToken0=",
|
2026-02-27 13:43:49 +00:00
|
|
|
|
frTotalFeesToken0.str(),
|
2026-02-27 13:08:53 +00:00
|
|
|
|
" IL=",
|
|
|
|
|
|
frIL.istr(),
|
|
|
|
|
|
" netPnL=",
|
|
|
|
|
|
frNetPnL.istr(),
|
|
|
|
|
|
" timeInRange=",
|
|
|
|
|
|
frTIR.str(),
|
|
|
|
|
|
" bps"
|
|
|
|
|
|
)
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ----- Fixed-Width LP -----
|
|
|
|
|
|
{
|
|
|
|
|
|
(uint256 fw0, uint256 fw1) = _positionAmounts(fwLiquidity, fwLo, fwHi, sqrtPriceX96);
|
|
|
|
|
|
uint256 fwFinalPos = _valueInToken0(fw0, fw1, sqrtPriceX96);
|
2026-02-27 13:43:49 +00:00
|
|
|
|
// fwTotalFeesToken0: historical periods at contemporaneous prices + current period at current
|
|
|
|
|
|
// price (added by collectFinalFees()).
|
|
|
|
|
|
uint256 fwFinalValue = fwFinalPos + fwTotalFeesToken0;
|
2026-02-27 13:08:53 +00:00
|
|
|
|
// Current period IL (open position vs holding current period entry amounts).
|
|
|
|
|
|
uint256 fwPeriodHodlVal = _valueInToken0(fwPeriodEntryToken0, fwPeriodEntryToken1, sqrtPriceX96);
|
|
|
|
|
|
int256 fwPeriodIL = int256(fwFinalPos) - int256(fwPeriodHodlVal);
|
|
|
|
|
|
int256 fwTotalIL = fwCumulativeIL + fwPeriodIL;
|
2026-02-27 13:43:49 +00:00
|
|
|
|
int256 fwNetPnL = fwTotalIL + int256(fwTotalFeesToken0);
|
2026-02-27 13:08:53 +00:00
|
|
|
|
uint256 fwTIR = fwTotalBlocks > 0 ? (fwBlocksInRange * 10_000) / fwTotalBlocks : 0;
|
|
|
|
|
|
console2.log("[BASELINE][FW][SUMMARY] === Fixed-Width LP ===");
|
|
|
|
|
|
console2.log(
|
|
|
|
|
|
string.concat(
|
|
|
|
|
|
"[BASELINE][FW][SUMMARY] initialValue=",
|
|
|
|
|
|
fwInitialValueToken0.str(),
|
2026-02-27 13:43:49 +00:00
|
|
|
|
" finalValue=",
|
|
|
|
|
|
fwFinalValue.str(),
|
2026-02-27 13:08:53 +00:00
|
|
|
|
" feesToken0=",
|
2026-02-27 13:43:49 +00:00
|
|
|
|
fwTotalFeesToken0.str(),
|
2026-02-27 13:08:53 +00:00
|
|
|
|
" cumulativeIL=",
|
|
|
|
|
|
fwCumulativeIL.istr(),
|
|
|
|
|
|
" currentPeriodIL=",
|
|
|
|
|
|
fwPeriodIL.istr(),
|
|
|
|
|
|
" totalIL=",
|
|
|
|
|
|
fwTotalIL.istr(),
|
|
|
|
|
|
" netPnL=",
|
|
|
|
|
|
fwNetPnL.istr(),
|
|
|
|
|
|
" rebalances=",
|
|
|
|
|
|
fwRebalances.str(),
|
|
|
|
|
|
" timeInRange=",
|
|
|
|
|
|
fwTIR.str(),
|
|
|
|
|
|
" bps"
|
|
|
|
|
|
)
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* @notice Return final computed metrics for each strategy. Used by Reporter.
|
2026-02-27 13:43:49 +00:00
|
|
|
|
* @dev Requires collectFinalFees() to have been called first so fee accumulators are complete.
|
|
|
|
|
|
* finalValueToken0 = positionValue + feesToken0 (both non-negative, no underflow risk).
|
2026-02-27 13:08:53 +00:00
|
|
|
|
*/
|
|
|
|
|
|
function getResults()
|
|
|
|
|
|
external
|
|
|
|
|
|
view
|
|
|
|
|
|
returns (StrategyResult memory hodlResult, StrategyResult memory frResult, StrategyResult memory fwResult)
|
|
|
|
|
|
{
|
|
|
|
|
|
(uint160 sqrtPriceX96,,,,,,) = pool.slot0();
|
|
|
|
|
|
|
|
|
|
|
|
// ----- HODL -----
|
|
|
|
|
|
uint256 hodlFinalValue = _valueInToken0(hodlEntryToken0, hodlEntryToken1, sqrtPriceX96);
|
|
|
|
|
|
hodlResult = StrategyResult({
|
|
|
|
|
|
initialCapitalToken0: hodlInitialValueToken0,
|
|
|
|
|
|
finalValueToken0: hodlFinalValue,
|
|
|
|
|
|
feesToken0: 0,
|
|
|
|
|
|
rebalances: 0,
|
|
|
|
|
|
ilToken0: 0,
|
|
|
|
|
|
netPnLToken0: int256(hodlFinalValue) - int256(hodlInitialValueToken0),
|
|
|
|
|
|
blocksInRange: 0,
|
|
|
|
|
|
totalBlocks: 0
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// ----- Full-Range LP -----
|
|
|
|
|
|
{
|
|
|
|
|
|
(uint256 fr0, uint256 fr1) = _positionAmounts(fullRangeLiquidity, frLo, frHi, sqrtPriceX96);
|
|
|
|
|
|
uint256 frPosValue = _valueInToken0(fr0, fr1, sqrtPriceX96);
|
2026-02-27 13:43:49 +00:00
|
|
|
|
// finalValue = position value + fees (always non-negative).
|
|
|
|
|
|
uint256 frFinalValue = frPosValue + frTotalFeesToken0;
|
2026-02-27 13:08:53 +00:00
|
|
|
|
uint256 frHodlVal = _valueInToken0(frEntryToken0, frEntryToken1, sqrtPriceX96);
|
|
|
|
|
|
int256 frIL = int256(frPosValue) - int256(frHodlVal);
|
2026-02-27 13:43:49 +00:00
|
|
|
|
int256 frNetPnL = int256(frFinalValue) - int256(frInitialValueToken0);
|
2026-02-27 13:08:53 +00:00
|
|
|
|
frResult = StrategyResult({
|
|
|
|
|
|
initialCapitalToken0: frInitialValueToken0,
|
2026-02-27 13:43:49 +00:00
|
|
|
|
finalValueToken0: frFinalValue,
|
|
|
|
|
|
feesToken0: frTotalFeesToken0,
|
2026-02-27 13:08:53 +00:00
|
|
|
|
rebalances: 0,
|
|
|
|
|
|
ilToken0: frIL,
|
|
|
|
|
|
netPnLToken0: frNetPnL,
|
|
|
|
|
|
blocksInRange: frBlocksInRange,
|
|
|
|
|
|
totalBlocks: frTotalBlocks
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ----- Fixed-Width LP -----
|
|
|
|
|
|
{
|
|
|
|
|
|
(uint256 fw0, uint256 fw1) = _positionAmounts(fwLiquidity, fwLo, fwHi, sqrtPriceX96);
|
|
|
|
|
|
uint256 fwPosValue = _valueInToken0(fw0, fw1, sqrtPriceX96);
|
2026-02-27 13:43:49 +00:00
|
|
|
|
// finalValue = position value + all fees (always non-negative).
|
|
|
|
|
|
uint256 fwFinalValue = fwPosValue + fwTotalFeesToken0;
|
2026-02-27 13:08:53 +00:00
|
|
|
|
// Current period IL.
|
|
|
|
|
|
uint256 fwPeriodHodlVal = _valueInToken0(fwPeriodEntryToken0, fwPeriodEntryToken1, sqrtPriceX96);
|
|
|
|
|
|
int256 fwPeriodIL = int256(fwPosValue) - int256(fwPeriodHodlVal);
|
|
|
|
|
|
int256 fwTotalIL = fwCumulativeIL + fwPeriodIL;
|
2026-02-27 13:43:49 +00:00
|
|
|
|
int256 fwNetPnL = int256(fwFinalValue) - int256(fwInitialValueToken0);
|
2026-02-27 13:08:53 +00:00
|
|
|
|
fwResult = StrategyResult({
|
|
|
|
|
|
initialCapitalToken0: fwInitialValueToken0,
|
2026-02-27 13:43:49 +00:00
|
|
|
|
finalValueToken0: fwFinalValue,
|
|
|
|
|
|
feesToken0: fwTotalFeesToken0,
|
2026-02-27 13:08:53 +00:00
|
|
|
|
rebalances: fwRebalances,
|
|
|
|
|
|
ilToken0: fwTotalIL,
|
|
|
|
|
|
netPnLToken0: fwNetPnL,
|
|
|
|
|
|
blocksInRange: fwBlocksInRange,
|
|
|
|
|
|
totalBlocks: fwTotalBlocks
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// -------------------------------------------------------------------------
|
|
|
|
|
|
// Uniswap V3 mint callback
|
|
|
|
|
|
// -------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
/// @inheritdoc IUniswapV3MintCallback
|
|
|
|
|
|
function uniswapV3MintCallback(uint256 amount0Owed, uint256 amount1Owed, bytes calldata) external override {
|
|
|
|
|
|
require(msg.sender == address(pool), "BaselineStrategies: bad mint callback");
|
|
|
|
|
|
if (amount0Owed > 0) {
|
|
|
|
|
|
uint256 have = token0.balanceOf(address(this));
|
|
|
|
|
|
if (have < amount0Owed) token0.mint(address(this), amount0Owed - have);
|
|
|
|
|
|
token0.transfer(address(pool), amount0Owed);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (amount1Owed > 0) {
|
|
|
|
|
|
uint256 have = token1.balanceOf(address(this));
|
|
|
|
|
|
if (have < amount1Owed) token1.mint(address(this), amount1Owed - have);
|
|
|
|
|
|
token1.transfer(address(pool), amount1Owed);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// -------------------------------------------------------------------------
|
|
|
|
|
|
// Internal: FixedWidth rebalancing
|
|
|
|
|
|
// -------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
function _rebalanceFixedWidth(int24 currentTick) internal {
|
2026-02-27 13:43:49 +00:00
|
|
|
|
(uint160 sqrtPriceX96,,,,,,) = pool.slot0();
|
|
|
|
|
|
|
|
|
|
|
|
// Capture old range before any mutation — used in the log line below.
|
|
|
|
|
|
int24 oldLo = fwLo;
|
|
|
|
|
|
int24 oldHi = fwHi;
|
2026-02-27 13:08:53 +00:00
|
|
|
|
|
|
|
|
|
|
// Burn current position and collect principal + fees.
|
|
|
|
|
|
(uint256 p0, uint256 p1) = pool.burn(fwLo, fwHi, fwLiquidity);
|
|
|
|
|
|
(uint128 c0, uint128 c1) = pool.collect(address(this), fwLo, fwHi, type(uint128).max, type(uint128).max);
|
|
|
|
|
|
|
|
|
|
|
|
// fees = collected - principal
|
|
|
|
|
|
uint256 f0 = uint256(c0) > p0 ? uint256(c0) - p0 : 0;
|
|
|
|
|
|
uint256 f1 = uint256(c1) > p1 ? uint256(c1) - p1 : 0;
|
|
|
|
|
|
fwFees0 += f0;
|
|
|
|
|
|
fwFees1 += f1;
|
2026-02-27 13:43:49 +00:00
|
|
|
|
// Accumulate at contemporaneous price (more accurate than repricing at end).
|
2026-02-27 13:08:53 +00:00
|
|
|
|
fwTotalFeesToken0 += _valueInToken0(f0, f1, sqrtPriceX96);
|
|
|
|
|
|
|
|
|
|
|
|
// IL for this closed period: LP exit value vs holding entry amounts at exit price.
|
|
|
|
|
|
uint256 exitVal = _valueInToken0(p0, p1, sqrtPriceX96);
|
|
|
|
|
|
uint256 hodlVal = _valueInToken0(fwPeriodEntryToken0, fwPeriodEntryToken1, sqrtPriceX96);
|
|
|
|
|
|
fwCumulativeIL += int256(exitVal) - int256(hodlVal);
|
|
|
|
|
|
|
|
|
|
|
|
fwRebalances++;
|
|
|
|
|
|
fwLiquidity = 0;
|
|
|
|
|
|
|
|
|
|
|
|
// Deploy new position centered around current tick.
|
|
|
|
|
|
(int24 newLo, int24 newHi) = _computeFixedRange(currentTick);
|
|
|
|
|
|
uint256 avail0 = uint256(c0);
|
|
|
|
|
|
uint256 avail1 = uint256(c1);
|
|
|
|
|
|
|
|
|
|
|
|
uint128 newLiq = LiquidityAmounts.getLiquidityForAmounts(
|
|
|
|
|
|
sqrtPriceX96, TickMath.getSqrtRatioAtTick(newLo), TickMath.getSqrtRatioAtTick(newHi), avail0, avail1
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
if (newLiq > 0) {
|
|
|
|
|
|
(uint256 used0, uint256 used1) = pool.mint(address(this), newLo, newHi, newLiq, "");
|
|
|
|
|
|
fwLiquidity = newLiq;
|
|
|
|
|
|
fwLo = newLo;
|
|
|
|
|
|
fwHi = newHi;
|
|
|
|
|
|
fwPeriodEntryToken0 = used0;
|
|
|
|
|
|
fwPeriodEntryToken1 = used1;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-27 13:43:49 +00:00
|
|
|
|
// Log uses oldLo/oldHi captured before fwLo/fwHi were overwritten.
|
2026-02-27 13:08:53 +00:00
|
|
|
|
console2.log(
|
|
|
|
|
|
string.concat(
|
|
|
|
|
|
"[BASELINE][FW][REBALANCE] #",
|
|
|
|
|
|
fwRebalances.str(),
|
|
|
|
|
|
" tick=",
|
|
|
|
|
|
int256(currentTick).istr(),
|
|
|
|
|
|
" oldRange=[",
|
2026-02-27 13:43:49 +00:00
|
|
|
|
int256(oldLo).istr(),
|
2026-02-27 13:08:53 +00:00
|
|
|
|
",",
|
2026-02-27 13:43:49 +00:00
|
|
|
|
int256(oldHi).istr(),
|
2026-02-27 13:08:53 +00:00
|
|
|
|
"] newRange=[",
|
|
|
|
|
|
int256(newLo).istr(),
|
|
|
|
|
|
",",
|
|
|
|
|
|
int256(newHi).istr(),
|
|
|
|
|
|
"] fees0=",
|
|
|
|
|
|
f0.str(),
|
|
|
|
|
|
" fees1=",
|
|
|
|
|
|
f1.str()
|
|
|
|
|
|
)
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// -------------------------------------------------------------------------
|
|
|
|
|
|
// Internal: Uniswap V3 math helpers
|
|
|
|
|
|
// -------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
function _positionAmounts(
|
|
|
|
|
|
uint128 liquidity,
|
|
|
|
|
|
int24 tickLower,
|
|
|
|
|
|
int24 tickUpper,
|
|
|
|
|
|
uint160 sqrtPriceX96
|
|
|
|
|
|
)
|
|
|
|
|
|
internal
|
|
|
|
|
|
pure
|
|
|
|
|
|
returns (uint256 amount0, uint256 amount1)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (liquidity == 0) return (0, 0);
|
|
|
|
|
|
uint160 sqrtLow = TickMath.getSqrtRatioAtTick(tickLower);
|
|
|
|
|
|
uint160 sqrtHigh = TickMath.getSqrtRatioAtTick(tickUpper);
|
|
|
|
|
|
(amount0, amount1) = LiquidityAmounts.getAmountsForLiquidity(sqrtPriceX96, sqrtLow, sqrtHigh, liquidity);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* @notice Convert (amount0, amount1) to token0-equivalent units.
|
|
|
|
|
|
* value = amount0 + amount1 × Q96² / sqrtPriceX96²
|
|
|
|
|
|
*/
|
|
|
|
|
|
function _valueInToken0(uint256 amount0, uint256 amount1, uint160 sqrtPriceX96) internal pure returns (uint256) {
|
|
|
|
|
|
if (sqrtPriceX96 == 0 || amount1 == 0) return amount0;
|
|
|
|
|
|
uint256 amt1InT0 = Math.mulDiv(Math.mulDiv(amount1, Q96, uint256(sqrtPriceX96)), Q96, uint256(sqrtPriceX96));
|
|
|
|
|
|
return amount0 + amt1InT0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* @notice Convert a token0 amount to its token1 equivalent at the current price.
|
|
|
|
|
|
* amount1 = amount0 × sqrtPriceX96² / Q96²
|
|
|
|
|
|
*/
|
|
|
|
|
|
function _valueInToken1(uint256 amount0, uint160 sqrtPriceX96) internal pure returns (uint256) {
|
|
|
|
|
|
if (sqrtPriceX96 == 0 || amount0 == 0) return 0;
|
|
|
|
|
|
return Math.mulDiv(Math.mulDiv(amount0, uint256(sqrtPriceX96), Q96), uint256(sqrtPriceX96), Q96);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// -------------------------------------------------------------------------
|
|
|
|
|
|
// Internal: tick range helpers
|
|
|
|
|
|
// -------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
/// @notice Compute the full-range lower tick (smallest valid tick aligned to tickSpacing).
|
|
|
|
|
|
function _fullRangeLo() internal view returns (int24) {
|
|
|
|
|
|
// Solidity truncation toward zero = ceiling for negatives → smallest valid multiple >= MIN_TICK.
|
|
|
|
|
|
return (TickMath.MIN_TICK / tickSpacing) * tickSpacing;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// @notice Compute the full-range upper tick (largest valid tick aligned to tickSpacing).
|
|
|
|
|
|
function _fullRangeHi() internal view returns (int24) {
|
|
|
|
|
|
return (TickMath.MAX_TICK / tickSpacing) * tickSpacing;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* @notice Compute ±FIXED_WIDTH_TICKS range centered on `currentTick`, aligned to tickSpacing.
|
|
|
|
|
|
* lo is aligned DOWN (floor); hi is aligned UP (ceiling).
|
|
|
|
|
|
* Result is clamped to the full-range bounds.
|
|
|
|
|
|
*/
|
|
|
|
|
|
function _computeFixedRange(int24 currentTick) internal view returns (int24 lo, int24 hi) {
|
|
|
|
|
|
int24 rawLo = currentTick - FIXED_WIDTH_TICKS;
|
|
|
|
|
|
int24 rawHi = currentTick + FIXED_WIDTH_TICKS;
|
|
|
|
|
|
|
|
|
|
|
|
// Floor for lo (align down toward more-negative).
|
|
|
|
|
|
lo = (rawLo / tickSpacing) * tickSpacing;
|
|
|
|
|
|
if (rawLo < 0 && rawLo % tickSpacing != 0) lo -= tickSpacing;
|
|
|
|
|
|
|
|
|
|
|
|
// Ceiling for hi (align up toward more-positive).
|
|
|
|
|
|
hi = (rawHi / tickSpacing) * tickSpacing;
|
|
|
|
|
|
if (rawHi > 0 && rawHi % tickSpacing != 0) hi += tickSpacing;
|
|
|
|
|
|
|
|
|
|
|
|
// Clamp to valid pool range.
|
|
|
|
|
|
int24 minValid = _fullRangeLo();
|
|
|
|
|
|
int24 maxValid = _fullRangeHi();
|
|
|
|
|
|
if (lo < minValid) lo = minValid;
|
|
|
|
|
|
if (hi > maxValid) hi = maxValid;
|
|
|
|
|
|
if (lo >= hi) {
|
|
|
|
|
|
lo = minValid;
|
|
|
|
|
|
hi = maxValid;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|