Merge pull request 'fix: Backtesting #5: Position tracking + P&L metrics (#319)' (#354) from fix/issue-319 into master
This commit is contained in:
commit
33c5244e53
13 changed files with 744 additions and 269 deletions
|
|
@ -1,15 +1,16 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
pragma solidity ^0.8.19;
|
||||
|
||||
import { BacktestKraiken } from "./BacktestKraiken.sol";
|
||||
|
||||
import { EventReplayer } from "./EventReplayer.sol";
|
||||
import { KrAIkenDeployer, KrAIkenSystem } from "./KrAIkenDeployer.sol";
|
||||
import { MockToken } from "./MockToken.sol";
|
||||
import { ShadowPool, ShadowPoolDeployer } from "./ShadowPoolDeployer.sol";
|
||||
import { StrategyExecutor } from "./StrategyExecutor.sol";
|
||||
import { IERC20 } from "@openzeppelin/token/ERC20/IERC20.sol";
|
||||
import { Script } from "forge-std/Script.sol";
|
||||
import { console2 } from "forge-std/console2.sol";
|
||||
import { IERC20 } from "@openzeppelin/token/ERC20/IERC20.sol";
|
||||
import { MockToken } from "./MockToken.sol";
|
||||
import { BacktestKraiken } from "./BacktestKraiken.sol";
|
||||
import { ShadowPool, ShadowPoolDeployer } from "./ShadowPoolDeployer.sol";
|
||||
import { EventReplayer } from "./EventReplayer.sol";
|
||||
import { KrAIkenSystem, KrAIkenDeployer } from "./KrAIkenDeployer.sol";
|
||||
import { StrategyExecutor } from "./StrategyExecutor.sol";
|
||||
|
||||
/**
|
||||
* @title BacktestRunner
|
||||
|
|
@ -77,14 +78,14 @@ contract BacktestRunner is Script {
|
|||
// 1. Explicit env override.
|
||||
try vm.envUint("INITIAL_SQRT_PRICE_X96") returns (uint256 val) {
|
||||
if (val != 0) return uint160(val);
|
||||
} catch {}
|
||||
} catch { }
|
||||
|
||||
// 2. First Swap event in the events cache.
|
||||
try vm.envString("BACKTEST_EVENTS_FILE") returns (string memory eventsFile) {
|
||||
try this._parseSqrtPriceFromFile(eventsFile) returns (uint160 val) {
|
||||
if (val != 0) return val;
|
||||
} catch {}
|
||||
} catch {}
|
||||
} catch { }
|
||||
} catch { }
|
||||
|
||||
// 3. Fallback default.
|
||||
return DEFAULT_SQRT_PRICE_X96;
|
||||
|
|
@ -101,12 +102,12 @@ contract BacktestRunner is Script {
|
|||
uint256 recenterInterval = DEFAULT_RECENTER_INTERVAL;
|
||||
try vm.envUint("RECENTER_INTERVAL") returns (uint256 v) {
|
||||
if (v > 0) recenterInterval = v;
|
||||
} catch {}
|
||||
} catch { }
|
||||
|
||||
uint256 initialCapital = KrAIkenDeployer.DEFAULT_INITIAL_CAPITAL;
|
||||
try vm.envUint("INITIAL_CAPITAL_WETH") returns (uint256 v) {
|
||||
if (v > 0) initialCapital = v;
|
||||
} catch {}
|
||||
} catch { }
|
||||
|
||||
vm.startBroadcast();
|
||||
|
||||
|
|
@ -139,15 +140,15 @@ contract BacktestRunner is Script {
|
|||
// 4. Set feeDestination = sender.
|
||||
// 5. Mint initialCapital mock WETH to LM.
|
||||
// ------------------------------------------------------------------
|
||||
KrAIkenSystem memory sys =
|
||||
KrAIkenDeployer.deploy(address(sp.factory), address(mockWeth), address(krk), sender, initialCapital);
|
||||
KrAIkenSystem memory sys = KrAIkenDeployer.deploy(address(sp.factory), address(mockWeth), address(krk), sender, initialCapital);
|
||||
|
||||
// Deploy StrategyExecutor and grant it recenter access on the LM.
|
||||
// recenterAccess bypasses TWAP stability check and cooldown — correct
|
||||
// for simulation where vm.warp drives time, not a real oracle.
|
||||
// sender == feeDestination, so the onlyFeeDestination guard is satisfied.
|
||||
bool token0isWeth = sp.token0 == address(mockWeth);
|
||||
StrategyExecutor executor =
|
||||
new StrategyExecutor(sys.lm, IERC20(address(mockWeth)), IERC20(address(krk)), sender, recenterInterval);
|
||||
new StrategyExecutor(sys.lm, IERC20(address(mockWeth)), IERC20(address(krk)), sender, recenterInterval, sp.pool, token0isWeth);
|
||||
sys.lm.setRecenterAccess(address(executor));
|
||||
|
||||
vm.stopBroadcast();
|
||||
|
|
@ -185,9 +186,10 @@ contract BacktestRunner is Script {
|
|||
console2.log("Optimizer: ", address(sys.optimizer));
|
||||
console2.log("LiquidityMgr: ", address(sys.lm));
|
||||
console2.log("StrategyExec: ", address(executor));
|
||||
console2.log("PositionTracker:", address(executor.tracker()));
|
||||
console2.log("Recenter intv: ", recenterInterval, " blocks");
|
||||
console2.log("Initial capital:", initialCapital, " (mock WETH wei)");
|
||||
console2.log("token0isWeth: ", sp.token0 == address(mockWeth));
|
||||
console2.log("token0isWeth: ", token0isWeth);
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Event replay (runs as local simulation — no broadcast required).
|
||||
|
|
@ -199,7 +201,7 @@ contract BacktestRunner is Script {
|
|||
try vm.envString("BACKTEST_EVENTS_FILE") returns (string memory eventsFile) {
|
||||
if (bytes(eventsFile).length > 0) {
|
||||
// Reset file position — _resolveSqrtPrice() may have consumed line 1.
|
||||
try vm.closeFile(eventsFile) {} catch {}
|
||||
try vm.closeFile(eventsFile) { } catch { }
|
||||
|
||||
// Pre-count events so replay() can show "[N/total]" progress lines.
|
||||
uint256 totalEvents = 0;
|
||||
|
|
@ -221,6 +223,6 @@ contract BacktestRunner is Script {
|
|||
// Print final KrAIken strategy summary.
|
||||
executor.logSummary();
|
||||
}
|
||||
} catch {}
|
||||
} catch { }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,14 +1,15 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
pragma solidity ^0.8.19;
|
||||
|
||||
import { IUniswapV3MintCallback } from "@uniswap-v3-core/interfaces/callback/IUniswapV3MintCallback.sol";
|
||||
import { IUniswapV3SwapCallback } from "@uniswap-v3-core/interfaces/callback/IUniswapV3SwapCallback.sol";
|
||||
import { IUniswapV3Pool } from "@uniswap-v3-core/interfaces/IUniswapV3Pool.sol";
|
||||
import { TickMath } from "@aperture/uni-v3-lib/TickMath.sol";
|
||||
import { Vm } from "forge-std/Vm.sol";
|
||||
import { console2 } from "forge-std/console2.sol";
|
||||
import { MockToken } from "./MockToken.sol";
|
||||
import { StrategyExecutor } from "./StrategyExecutor.sol";
|
||||
import { TickMath } from "@aperture/uni-v3-lib/TickMath.sol";
|
||||
import { IUniswapV3Pool } from "@uniswap-v3-core/interfaces/IUniswapV3Pool.sol";
|
||||
import { IUniswapV3MintCallback } from "@uniswap-v3-core/interfaces/callback/IUniswapV3MintCallback.sol";
|
||||
import { IUniswapV3SwapCallback } from "@uniswap-v3-core/interfaces/callback/IUniswapV3SwapCallback.sol";
|
||||
|
||||
import { Vm } from "forge-std/Vm.sol";
|
||||
import { console2 } from "forge-std/console2.sol";
|
||||
|
||||
/**
|
||||
* @title EventReplayer
|
||||
|
|
@ -107,13 +108,7 @@ contract EventReplayer is IUniswapV3MintCallback, IUniswapV3SwapCallback {
|
|||
_replayWithStrategy(eventsFile, totalEvents, strategyExecutor);
|
||||
}
|
||||
|
||||
function _replayWithStrategy(
|
||||
string memory eventsFile,
|
||||
uint256 totalEvents,
|
||||
StrategyExecutor strategyExecutor
|
||||
)
|
||||
internal
|
||||
{
|
||||
function _replayWithStrategy(string memory eventsFile, uint256 totalEvents, StrategyExecutor strategyExecutor) internal {
|
||||
uint256 idx = 0;
|
||||
|
||||
// Track the last Swap event's expected state for drift measurement.
|
||||
|
|
@ -178,9 +173,8 @@ contract EventReplayer is IUniswapV3MintCallback, IUniswapV3SwapCallback {
|
|||
if (absDrift > maxDrift) maxDrift = absDrift;
|
||||
// Log sqrtPrice deviation when it exceeds ~0.01% (filters rounding noise).
|
||||
if (finalSqrtPrice != lastExpectedSqrtPrice) {
|
||||
uint256 priceDelta = finalSqrtPrice > lastExpectedSqrtPrice
|
||||
? uint256(finalSqrtPrice - lastExpectedSqrtPrice)
|
||||
: uint256(lastExpectedSqrtPrice - finalSqrtPrice);
|
||||
uint256 priceDelta =
|
||||
finalSqrtPrice > lastExpectedSqrtPrice ? uint256(finalSqrtPrice - lastExpectedSqrtPrice) : uint256(lastExpectedSqrtPrice - finalSqrtPrice);
|
||||
if (lastExpectedSqrtPrice > 0 && priceDelta * 10_000 > uint256(lastExpectedSqrtPrice)) {
|
||||
console2.log(" final sqrtPrice divergence:", priceDelta);
|
||||
}
|
||||
|
|
@ -392,14 +386,7 @@ contract EventReplayer is IUniswapV3MintCallback, IUniswapV3SwapCallback {
|
|||
/**
|
||||
* @notice Emit a progress line and accumulate drift statistics for one checkpoint.
|
||||
*/
|
||||
function _logCheckpoint(
|
||||
uint256 idx,
|
||||
uint256 totalEvents,
|
||||
int24 expectedTick,
|
||||
uint160 expectedSqrtPrice
|
||||
)
|
||||
internal
|
||||
{
|
||||
function _logCheckpoint(uint256 idx, uint256 totalEvents, int24 expectedTick, uint160 expectedSqrtPrice) internal {
|
||||
(uint160 currentSqrtPrice, int24 currentTick,,,,,) = pool.slot0();
|
||||
int256 diff = int256(currentTick) - int256(expectedTick);
|
||||
uint256 absDrift = diff >= 0 ? uint256(diff) : uint256(-diff);
|
||||
|
|
@ -425,9 +412,8 @@ contract EventReplayer is IUniswapV3MintCallback, IUniswapV3SwapCallback {
|
|||
|
||||
// Log sqrtPrice deviation when it exceeds ~0.01% (filters rounding noise).
|
||||
if (currentSqrtPrice != expectedSqrtPrice) {
|
||||
uint256 priceDelta = currentSqrtPrice > expectedSqrtPrice
|
||||
? uint256(currentSqrtPrice - expectedSqrtPrice)
|
||||
: uint256(expectedSqrtPrice - currentSqrtPrice);
|
||||
uint256 priceDelta =
|
||||
currentSqrtPrice > expectedSqrtPrice ? uint256(currentSqrtPrice - expectedSqrtPrice) : uint256(expectedSqrtPrice - currentSqrtPrice);
|
||||
if (expectedSqrtPrice > 0 && priceDelta * 10_000 > uint256(expectedSqrtPrice)) {
|
||||
console2.log(" sqrtPrice divergence:", priceDelta);
|
||||
}
|
||||
|
|
|
|||
33
onchain/script/backtesting/FormatLib.sol
Normal file
33
onchain/script/backtesting/FormatLib.sol
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
pragma solidity ^0.8.19;
|
||||
|
||||
/**
|
||||
* @title FormatLib
|
||||
* @notice Shared integer-to-string formatting helpers for backtesting log output.
|
||||
* Extracted to avoid copy-pasting the same logic across PositionTracker
|
||||
* and StrategyExecutor.
|
||||
*/
|
||||
library FormatLib {
|
||||
/// @notice Format an unsigned integer as a decimal string.
|
||||
function str(uint256 v) internal pure returns (string memory) {
|
||||
if (v == 0) return "0";
|
||||
uint256 tmp = v;
|
||||
uint256 len;
|
||||
while (tmp != 0) {
|
||||
len++;
|
||||
tmp /= 10;
|
||||
}
|
||||
bytes memory buf = new bytes(len);
|
||||
while (v != 0) {
|
||||
buf[--len] = bytes1(uint8(48 + v % 10));
|
||||
v /= 10;
|
||||
}
|
||||
return string(buf);
|
||||
}
|
||||
|
||||
/// @notice Format a signed integer as a decimal string (prefixed with '-' if negative).
|
||||
function istr(int256 v) internal pure returns (string memory) {
|
||||
if (v >= 0) return str(uint256(v));
|
||||
return string.concat("-", str(uint256(-v)));
|
||||
}
|
||||
}
|
||||
|
|
@ -1,10 +1,10 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
pragma solidity ^0.8.19;
|
||||
|
||||
import { MockToken } from "./MockToken.sol";
|
||||
import { BacktestKraiken } from "./BacktestKraiken.sol";
|
||||
import { OptimizerV3Push3 } from "../../src/OptimizerV3Push3.sol";
|
||||
import { LiquidityManager } from "../../src/LiquidityManager.sol";
|
||||
import { OptimizerV3Push3 } from "../../src/OptimizerV3Push3.sol";
|
||||
import { BacktestKraiken } from "./BacktestKraiken.sol";
|
||||
import { MockToken } from "./MockToken.sol";
|
||||
|
||||
/**
|
||||
* @notice Deployment result for the KrAIken system on the shadow pool.
|
||||
|
|
@ -40,15 +40,7 @@ library KrAIkenDeployer {
|
|||
/**
|
||||
* @notice Deploy the KrAIken system with default initial capital (10 ETH equivalent).
|
||||
*/
|
||||
function deploy(
|
||||
address shadowFactory,
|
||||
address mockWeth,
|
||||
address krkToken,
|
||||
address feeDestination
|
||||
)
|
||||
internal
|
||||
returns (KrAIkenSystem memory sys)
|
||||
{
|
||||
function deploy(address shadowFactory, address mockWeth, address krkToken, address feeDestination) internal returns (KrAIkenSystem memory sys) {
|
||||
return deploy(shadowFactory, mockWeth, krkToken, feeDestination, DEFAULT_INITIAL_CAPITAL);
|
||||
}
|
||||
|
||||
|
|
|
|||
496
onchain/script/backtesting/PositionTracker.sol
Normal file
496
onchain/script/backtesting/PositionTracker.sol
Normal file
|
|
@ -0,0 +1,496 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
pragma solidity ^0.8.19;
|
||||
|
||||
import { FormatLib } from "./FormatLib.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 { console2 } from "forge-std/console2.sol";
|
||||
|
||||
/**
|
||||
* @title PositionTracker
|
||||
* @notice Tracks KrAIken's three-position lifecycle and computes P&L metrics
|
||||
* for backtesting analysis.
|
||||
*
|
||||
* For each position lifecycle (open → close on next recenter), records:
|
||||
* - Tick range (tickLower, tickUpper) and liquidity
|
||||
* - Entry/exit block and timestamp
|
||||
* - Token amounts at entry vs exit (via Uniswap V3 LiquidityAmounts math)
|
||||
* - Fees earned — attributed only to positions whose tick range contained the
|
||||
* active tick at the time of close (the only positions that could have been
|
||||
* accruing fees). Falls back to raw-liquidity weighting if no position is
|
||||
* in range at close time (rare edge case).
|
||||
* - Impermanent loss vs holding the initial token amounts
|
||||
* - Net P&L = fees value + IL (IL is negative when LP underperforms HODL)
|
||||
*
|
||||
* Aggregate metrics:
|
||||
* - Total fees (token0 + token1 raw, and token0-equivalent)
|
||||
* - Cumulative IL and net P&L (token0 units)
|
||||
* - Rebalance count
|
||||
* - Time in range: % of notified blocks where Anchor tick range contains current tick
|
||||
* - Capital efficiency accumulators (feesToken0 / liqBlocks) for offline calculation
|
||||
*
|
||||
* All output lines carry a [TRACKER][TYPE] prefix for downstream parseability.
|
||||
* Cumulative P&L is logged every CUMULATIVE_LOG_INTERVAL blocks.
|
||||
*
|
||||
* Not a Script — no vm access. Uses console2 for output only.
|
||||
*
|
||||
* WARNING — backtesting use only: `recordRecenter` has no access control. Do not
|
||||
* reuse this contract in production contexts where untrusted callers exist.
|
||||
*/
|
||||
contract PositionTracker {
|
||||
using Math for uint256;
|
||||
using FormatLib for uint256;
|
||||
using FormatLib for int256;
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Types
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @notice Snapshot of all three KrAIken positions at a point in time.
|
||||
* @dev Stage ordinals: 0 = FLOOR, 1 = ANCHOR, 2 = DISCOVERY.
|
||||
*/
|
||||
struct PositionSnapshot {
|
||||
uint128 floorLiq;
|
||||
int24 floorLo;
|
||||
int24 floorHi;
|
||||
uint128 anchorLiq;
|
||||
int24 anchorLo;
|
||||
int24 anchorHi;
|
||||
uint128 discLiq;
|
||||
int24 discLo;
|
||||
int24 discHi;
|
||||
}
|
||||
|
||||
struct OpenPosition {
|
||||
int24 tickLower;
|
||||
int24 tickUpper;
|
||||
uint128 liquidity;
|
||||
uint256 entryBlock;
|
||||
uint256 entryTimestamp;
|
||||
uint256 entryAmount0;
|
||||
uint256 entryAmount1;
|
||||
bool active;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Constants
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
uint256 internal constant Q96 = 2 ** 96;
|
||||
|
||||
/// @notice Blocks between cumulative P&L log lines.
|
||||
uint256 public constant CUMULATIVE_LOG_INTERVAL = 500;
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Immutables
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
IUniswapV3Pool public immutable pool;
|
||||
|
||||
/// @notice True when pool token0 is WETH; affects fees0/fees1 mapping.
|
||||
bool public immutable token0isWeth;
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Open position state (indexed 0=FLOOR, 1=ANCHOR, 2=DISCOVERY)
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
OpenPosition[3] public openPositions;
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Cumulative aggregate metrics
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
uint256 public totalFees0;
|
||||
uint256 public totalFees1;
|
||||
uint256 public rebalanceCount;
|
||||
|
||||
/// @notice Cumulative IL across all closed positions in token0 units.
|
||||
/// Negative means LP underperformed HODL.
|
||||
int256 public totalILToken0;
|
||||
|
||||
/// @notice Cumulative net P&L = IL + fees value (token0 units).
|
||||
int256 public totalNetPnLToken0;
|
||||
|
||||
// Time-in-range (Anchor position).
|
||||
uint256 public blocksChecked;
|
||||
uint256 public blocksAnchorInRange;
|
||||
uint256 public lastNotifiedBlock;
|
||||
|
||||
// Capital efficiency accumulators.
|
||||
/// @notice Cumulative fees expressed in token0 units.
|
||||
uint256 public totalFeesToken0;
|
||||
/// @notice Sum of (liquidity × blocksOpen) for all closed positions.
|
||||
uint256 public totalLiquidityBlocks;
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Internal
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
uint256 internal _lastCumulativeLogBlock;
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Constructor
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
constructor(IUniswapV3Pool _pool, bool _token0isWeth) {
|
||||
pool = _pool;
|
||||
token0isWeth = _token0isWeth;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Integration API (called by StrategyExecutor)
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @notice Notify the tracker that a new block has been observed.
|
||||
* Updates Anchor time-in-range and emits cumulative P&L lines at
|
||||
* every CUMULATIVE_LOG_INTERVAL blocks.
|
||||
* @param blockNum Block number as advanced by EventReplayer.
|
||||
*/
|
||||
function notifyBlock(uint256 blockNum) external {
|
||||
if (blockNum == lastNotifiedBlock) return;
|
||||
lastNotifiedBlock = blockNum;
|
||||
|
||||
// Track Anchor time-in-range.
|
||||
OpenPosition storage anchor = openPositions[1]; // ANCHOR = 1
|
||||
if (anchor.active) {
|
||||
(, int24 tick,,,,,) = pool.slot0();
|
||||
blocksChecked++;
|
||||
if (tick >= anchor.tickLower && tick < anchor.tickUpper) {
|
||||
blocksAnchorInRange++;
|
||||
}
|
||||
}
|
||||
|
||||
// Log cumulative P&L at regular intervals.
|
||||
if (_lastCumulativeLogBlock == 0) {
|
||||
_lastCumulativeLogBlock = blockNum;
|
||||
} else if (blockNum - _lastCumulativeLogBlock >= CUMULATIVE_LOG_INTERVAL) {
|
||||
_logCumulative(blockNum);
|
||||
_lastCumulativeLogBlock = blockNum;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Record a successful recenter: close old positions, open new ones.
|
||||
*
|
||||
* Fee attribution: fees are distributed only to positions whose tick range
|
||||
* contained the active tick at the moment of close — the only positions that
|
||||
* could have been accruing fees in Uniswap V3. When no position is in range
|
||||
* at close time (rare), attribution falls back to raw liquidity weighting and
|
||||
* a warning is logged.
|
||||
*
|
||||
* @dev No access control — this contract is backtesting-only tooling.
|
||||
* @param oldPos Pre-recenter snapshot (positions being burned).
|
||||
* @param newPos Post-recenter snapshot (positions being minted).
|
||||
* @param feesWeth Total WETH fees collected across all positions this recenter.
|
||||
* @param feesKrk Total KRK fees collected across all positions this recenter.
|
||||
* @param blockNum Block number of the recenter.
|
||||
* @param timestamp Block timestamp of the recenter.
|
||||
*/
|
||||
function recordRecenter(
|
||||
PositionSnapshot calldata oldPos,
|
||||
PositionSnapshot calldata newPos,
|
||||
uint256 feesWeth,
|
||||
uint256 feesKrk,
|
||||
uint256 blockNum,
|
||||
uint256 timestamp
|
||||
)
|
||||
external
|
||||
{
|
||||
(uint160 sqrtPriceX96, int24 currentTick,,,,,) = pool.slot0();
|
||||
|
||||
// Map WETH/KRK fees to pool token0/token1 based on pool ordering.
|
||||
uint256 fees0 = token0isWeth ? feesWeth : feesKrk;
|
||||
uint256 fees1 = token0isWeth ? feesKrk : feesWeth;
|
||||
|
||||
totalFees0 += fees0;
|
||||
totalFees1 += fees1;
|
||||
totalFeesToken0 += _valueInToken0(fees0, fees1, sqrtPriceX96);
|
||||
rebalanceCount++;
|
||||
|
||||
// Uniswap V3 only accrues fees to a position when the active tick is
|
||||
// inside its range. Weight fee attribution by in-range liquidity only.
|
||||
uint256 fFloorWeight = _inRangeLiq(oldPos.floorLiq, oldPos.floorLo, oldPos.floorHi, currentTick);
|
||||
uint256 fAnchorWeight = _inRangeLiq(oldPos.anchorLiq, oldPos.anchorLo, oldPos.anchorHi, currentTick);
|
||||
uint256 fDiscWeight = _inRangeLiq(oldPos.discLiq, oldPos.discLo, oldPos.discHi, currentTick);
|
||||
uint256 totalFeeWeight = fFloorWeight + fAnchorWeight + fDiscWeight;
|
||||
|
||||
// Fallback to raw-liquidity weights when no position covers the current tick.
|
||||
if (totalFeeWeight == 0) {
|
||||
console2.log("[TRACKER][WARN] no position in range at close: fee attribution falling back to raw liquidity");
|
||||
fFloorWeight = oldPos.floorLiq;
|
||||
fAnchorWeight = oldPos.anchorLiq;
|
||||
fDiscWeight = oldPos.discLiq;
|
||||
totalFeeWeight = uint256(oldPos.floorLiq) + uint256(oldPos.anchorLiq) + uint256(oldPos.discLiq);
|
||||
}
|
||||
|
||||
// Close old positions, passing per-position fee weights.
|
||||
_closePosition(0, oldPos.floorLiq, oldPos.floorLo, oldPos.floorHi, fees0, fees1, fFloorWeight, totalFeeWeight, sqrtPriceX96, blockNum);
|
||||
_closePosition(1, oldPos.anchorLiq, oldPos.anchorLo, oldPos.anchorHi, fees0, fees1, fAnchorWeight, totalFeeWeight, sqrtPriceX96, blockNum);
|
||||
_closePosition(2, oldPos.discLiq, oldPos.discLo, oldPos.discHi, fees0, fees1, fDiscWeight, totalFeeWeight, sqrtPriceX96, blockNum);
|
||||
|
||||
// Open new positions.
|
||||
_openPosition(0, newPos.floorLiq, newPos.floorLo, newPos.floorHi, sqrtPriceX96, blockNum, timestamp);
|
||||
_openPosition(1, newPos.anchorLiq, newPos.anchorLo, newPos.anchorHi, sqrtPriceX96, blockNum, timestamp);
|
||||
_openPosition(2, newPos.discLiq, newPos.discLo, newPos.discHi, sqrtPriceX96, blockNum, timestamp);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Log the final aggregate summary. Call once at the end of replay.
|
||||
* @param blockNum Final block number (for context in the summary line).
|
||||
*/
|
||||
function logFinalSummary(uint256 blockNum) external view {
|
||||
// Compute incremental IL from still-open positions without mutating state.
|
||||
(uint160 sqrtPriceX96,,,,,,) = pool.slot0();
|
||||
int256 finalIL = totalILToken0;
|
||||
int256 finalNetPnL = totalNetPnLToken0;
|
||||
for (uint8 i = 0; i < 3; i++) {
|
||||
OpenPosition storage pos = openPositions[i];
|
||||
if (!pos.active) continue;
|
||||
(uint256 exitAmt0, uint256 exitAmt1) = _positionAmounts(pos.liquidity, pos.tickLower, pos.tickUpper, sqrtPriceX96);
|
||||
int256 il = _computeIL(pos.entryAmount0, pos.entryAmount1, exitAmt0, exitAmt1, sqrtPriceX96);
|
||||
finalIL += il;
|
||||
finalNetPnL += il; // no fees for unclosed positions
|
||||
}
|
||||
|
||||
uint256 timeInRangeBps = blocksChecked > 0 ? (blocksAnchorInRange * 10_000) / blocksChecked : 0;
|
||||
|
||||
console2.log("[TRACKER][SUMMARY] === Final P&L Summary ===");
|
||||
console2.log(string.concat("[TRACKER][SUMMARY] finalBlock=", blockNum.str()));
|
||||
console2.log(string.concat("[TRACKER][SUMMARY] rebalances=", rebalanceCount.str()));
|
||||
console2.log(
|
||||
string.concat("[TRACKER][SUMMARY] totalFees0=", totalFees0.str(), " totalFees1=", totalFees1.str(), " totalFeesToken0=", totalFeesToken0.str())
|
||||
);
|
||||
console2.log(string.concat("[TRACKER][SUMMARY] totalIL=", finalIL.istr(), " netPnL=", finalNetPnL.istr(), " (token0 units)"));
|
||||
console2.log(
|
||||
string.concat(
|
||||
"[TRACKER][SUMMARY] timeInRange=", timeInRangeBps.str(), " bps blocksAnchorInRange=", blocksAnchorInRange.str(), "/", blocksChecked.str()
|
||||
)
|
||||
);
|
||||
console2.log(string.concat("[TRACKER][SUMMARY] capitalEff: feesToken0=", totalFeesToken0.str(), " liqBlocks=", totalLiquidityBlocks.str()));
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Internal: position lifecycle
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
function _openPosition(
|
||||
uint8 stageIdx,
|
||||
uint128 liquidity,
|
||||
int24 tickLower,
|
||||
int24 tickUpper,
|
||||
uint160 sqrtPriceX96,
|
||||
uint256 blockNum,
|
||||
uint256 timestamp
|
||||
)
|
||||
internal
|
||||
{
|
||||
if (liquidity == 0) return;
|
||||
(uint256 amt0, uint256 amt1) = _positionAmounts(liquidity, tickLower, tickUpper, sqrtPriceX96);
|
||||
openPositions[stageIdx] = OpenPosition({
|
||||
tickLower: tickLower,
|
||||
tickUpper: tickUpper,
|
||||
liquidity: liquidity,
|
||||
entryBlock: blockNum,
|
||||
entryTimestamp: timestamp,
|
||||
entryAmount0: amt0,
|
||||
entryAmount1: amt1,
|
||||
active: true
|
||||
});
|
||||
console2.log(
|
||||
string.concat(
|
||||
"[TRACKER][OPEN] stage=",
|
||||
_stageName(stageIdx),
|
||||
" block=",
|
||||
blockNum.str(),
|
||||
" ts=",
|
||||
timestamp.str(),
|
||||
" tick=[",
|
||||
int256(tickLower).istr(),
|
||||
",",
|
||||
int256(tickUpper).istr(),
|
||||
"] liq=",
|
||||
uint256(liquidity).str(),
|
||||
" amt0=",
|
||||
amt0.str(),
|
||||
" amt1=",
|
||||
amt1.str()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function _closePosition(
|
||||
uint8 stageIdx,
|
||||
uint128 liquidity,
|
||||
int24 tickLower,
|
||||
int24 tickUpper,
|
||||
uint256 fees0Total,
|
||||
uint256 fees1Total,
|
||||
uint256 posWeight, // fee weight for this position (0 = out-of-range)
|
||||
uint256 totalWeight, // sum of fee weights across all positions
|
||||
uint160 sqrtPriceX96,
|
||||
uint256 blockNum
|
||||
)
|
||||
internal
|
||||
{
|
||||
if (liquidity == 0) return;
|
||||
|
||||
OpenPosition storage pos = openPositions[stageIdx];
|
||||
if (!pos.active) {
|
||||
// First recenter: no prior open record exists (LM deployed with positions
|
||||
// already placed before tracking began). Log a warning so the gap is visible.
|
||||
console2.log(
|
||||
string.concat("[TRACKER][WARN] stage=", _stageName(stageIdx), " close skipped at block=", blockNum.str(), " (no open record: first recenter)")
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Guard: the incoming snapshot must match the stored open record.
|
||||
// A mismatch would mean IL is computed across mismatched tick ranges.
|
||||
require(tickLower == pos.tickLower && tickUpper == pos.tickUpper, "PositionTracker: tick range mismatch");
|
||||
|
||||
(uint256 exitAmt0, uint256 exitAmt1) = _positionAmounts(liquidity, tickLower, tickUpper, sqrtPriceX96);
|
||||
|
||||
// Attribute fees proportional to this position's in-range weight.
|
||||
uint256 posLiq = uint256(liquidity);
|
||||
uint256 myFees0 = totalWeight > 0 ? fees0Total.mulDiv(posWeight, totalWeight) : 0;
|
||||
uint256 myFees1 = totalWeight > 0 ? fees1Total.mulDiv(posWeight, totalWeight) : 0;
|
||||
|
||||
// IL = LP exit value (ex-fees) − HODL value at exit price (both in token0 units).
|
||||
int256 il = _computeIL(pos.entryAmount0, pos.entryAmount1, exitAmt0, exitAmt1, sqrtPriceX96);
|
||||
int256 feesToken0 = int256(_valueInToken0(myFees0, myFees1, sqrtPriceX96));
|
||||
int256 netPnL = il + feesToken0;
|
||||
|
||||
totalILToken0 += il;
|
||||
totalNetPnLToken0 += netPnL;
|
||||
|
||||
uint256 blocksOpen = blockNum > pos.entryBlock ? blockNum - pos.entryBlock : 0;
|
||||
totalLiquidityBlocks += posLiq * blocksOpen;
|
||||
|
||||
console2.log(
|
||||
string.concat(
|
||||
"[TRACKER][CLOSE] stage=",
|
||||
_stageName(stageIdx),
|
||||
" block=",
|
||||
blockNum.str(),
|
||||
" entryBlock=",
|
||||
pos.entryBlock.str(),
|
||||
" tick=[",
|
||||
int256(tickLower).istr(),
|
||||
",",
|
||||
int256(tickUpper).istr(),
|
||||
"] liq=",
|
||||
posLiq.str()
|
||||
)
|
||||
);
|
||||
console2.log(
|
||||
string.concat(
|
||||
"[TRACKER][CLOSE] entryAmt0=",
|
||||
pos.entryAmount0.str(),
|
||||
" entryAmt1=",
|
||||
pos.entryAmount1.str(),
|
||||
" exitAmt0=",
|
||||
exitAmt0.str(),
|
||||
" exitAmt1=",
|
||||
exitAmt1.str(),
|
||||
" fees0=",
|
||||
myFees0.str(),
|
||||
" fees1=",
|
||||
myFees1.str()
|
||||
)
|
||||
);
|
||||
console2.log(string.concat("[TRACKER][CLOSE] IL=", il.istr(), " netPnL=", netPnL.istr(), " (token0 units)"));
|
||||
|
||||
delete openPositions[stageIdx];
|
||||
}
|
||||
|
||||
function _logCumulative(uint256 blockNum) internal view {
|
||||
uint256 timeInRangeBps = blocksChecked > 0 ? (blocksAnchorInRange * 10_000) / blocksChecked : 0;
|
||||
console2.log(
|
||||
string.concat(
|
||||
"[TRACKER][CUMULATIVE] block=",
|
||||
blockNum.str(),
|
||||
" rebalances=",
|
||||
rebalanceCount.str(),
|
||||
" totalFees0=",
|
||||
totalFees0.str(),
|
||||
" totalFees1=",
|
||||
totalFees1.str(),
|
||||
" IL=",
|
||||
totalILToken0.istr(),
|
||||
" netPnL=",
|
||||
totalNetPnLToken0.istr(),
|
||||
" timeInRange=",
|
||||
timeInRangeBps.str(),
|
||||
" bps"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Internal: Uniswap V3 math
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @notice Return `liquidity` if currentTick is inside [tickLo, tickHi), else 0.
|
||||
* Used to attribute fees only to positions that were potentially in range.
|
||||
*/
|
||||
function _inRangeLiq(uint128 liquidity, int24 tickLo, int24 tickHi, int24 currentTick) internal pure returns (uint256) {
|
||||
if (liquidity == 0) return 0;
|
||||
return (currentTick >= tickLo && currentTick < tickHi) ? uint256(liquidity) : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Compute (amount0, amount1) for a liquidity position at the given sqrt price.
|
||||
*/
|
||||
function _positionAmounts(
|
||||
uint128 liquidity,
|
||||
int24 tickLower,
|
||||
int24 tickUpper,
|
||||
uint160 sqrtPriceX96
|
||||
)
|
||||
internal
|
||||
pure
|
||||
returns (uint256 amount0, uint256 amount1)
|
||||
{
|
||||
uint160 sqrtRatioLow = TickMath.getSqrtRatioAtTick(tickLower);
|
||||
uint160 sqrtRatioHigh = TickMath.getSqrtRatioAtTick(tickUpper);
|
||||
(amount0, amount1) = LiquidityAmounts.getAmountsForLiquidity(sqrtPriceX96, sqrtRatioLow, sqrtRatioHigh, liquidity);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Impermanent loss for a position close (token0 units).
|
||||
* IL = lpExitValue − hodlValue at the exit price.
|
||||
* Negative when LP underperforms HODL (the typical case).
|
||||
*/
|
||||
function _computeIL(uint256 entryAmt0, uint256 entryAmt1, uint256 exitAmt0, uint256 exitAmt1, uint160 sqrtPriceX96) internal pure returns (int256) {
|
||||
uint256 lpVal = _valueInToken0(exitAmt0, exitAmt1, sqrtPriceX96);
|
||||
uint256 hodlVal = _valueInToken0(entryAmt0, entryAmt1, sqrtPriceX96);
|
||||
return int256(lpVal) - int256(hodlVal);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Convert (amount0, amount1) to token0-equivalent units at the given sqrt price.
|
||||
* @dev value = amount0 + amount1 / price
|
||||
* = amount0 + amount1 × Q96² / sqrtPriceX96²
|
||||
* = amount0 + mulDiv(mulDiv(amount1, Q96, sqrtPrice), Q96, sqrtPrice)
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Formatting helper
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
function _stageName(uint8 idx) internal pure returns (string memory) {
|
||||
if (idx == 0) return "FLOOR";
|
||||
if (idx == 1) return "ANCHOR";
|
||||
return "DISC";
|
||||
}
|
||||
}
|
||||
|
|
@ -1,9 +1,9 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
pragma solidity ^0.8.19;
|
||||
|
||||
import { UniswapHelpers } from "../../src/helpers/UniswapHelpers.sol";
|
||||
import { IUniswapV3Factory } from "@uniswap-v3-core/interfaces/IUniswapV3Factory.sol";
|
||||
import { IUniswapV3Pool } from "@uniswap-v3-core/interfaces/IUniswapV3Pool.sol";
|
||||
import { UniswapHelpers } from "../../src/helpers/UniswapHelpers.sol";
|
||||
|
||||
struct ShadowPool {
|
||||
IUniswapV3Factory factory;
|
||||
|
|
@ -27,14 +27,7 @@ library ShadowPoolDeployer {
|
|||
* @param sqrtPriceX96 Initial sqrt price (Q64.96).
|
||||
* @return sp ShadowPool struct with factory, pool, token0, token1 addresses.
|
||||
*/
|
||||
function deploy(
|
||||
address tokenA,
|
||||
address tokenB,
|
||||
uint160 sqrtPriceX96
|
||||
)
|
||||
internal
|
||||
returns (ShadowPool memory sp)
|
||||
{
|
||||
function deploy(address tokenA, address tokenB, uint160 sqrtPriceX96) internal returns (ShadowPool memory sp) {
|
||||
sp.factory = UniswapHelpers.deployUniswapFactory();
|
||||
|
||||
address poolAddr = sp.factory.createPool(tokenA, tokenB, SHADOW_FEE);
|
||||
|
|
|
|||
|
|
@ -1,10 +1,13 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
pragma solidity ^0.8.19;
|
||||
|
||||
import { console2 } from "forge-std/console2.sol";
|
||||
import { IERC20 } from "@openzeppelin/token/ERC20/IERC20.sol";
|
||||
import { LiquidityManager } from "../../src/LiquidityManager.sol";
|
||||
import { ThreePositionStrategy } from "../../src/abstracts/ThreePositionStrategy.sol";
|
||||
import { FormatLib } from "./FormatLib.sol";
|
||||
import { PositionTracker } from "./PositionTracker.sol";
|
||||
import { IERC20 } from "@openzeppelin/token/ERC20/IERC20.sol";
|
||||
import { IUniswapV3Pool } from "@uniswap-v3-core/interfaces/IUniswapV3Pool.sol";
|
||||
import { console2 } from "forge-std/console2.sol";
|
||||
|
||||
/**
|
||||
* @title StrategyExecutor
|
||||
|
|
@ -17,6 +20,10 @@ import { ThreePositionStrategy } from "../../src/abstracts/ThreePositionStrategy
|
|||
* - Fees collected (WETH + KRK) as the delta of feeDestination's balances
|
||||
* - Revert reason on failure (logged and skipped — replay never halts)
|
||||
*
|
||||
* Position tracking and P&L metrics are delegated to PositionTracker, which is
|
||||
* notified on every block (for time-in-range) and on each successful recenter
|
||||
* (for position lifecycle and fee/IL accounting).
|
||||
*
|
||||
* Access model: StrategyExecutor must be set as recenterAccess on the LM so that
|
||||
* the cooldown and TWAP price-stability checks are bypassed in the simulation
|
||||
* (vm.warp advances simulated time, not real oracle state).
|
||||
|
|
@ -27,6 +34,9 @@ import { ThreePositionStrategy } from "../../src/abstracts/ThreePositionStrategy
|
|||
* amounts based on KrAIken's active tick ranges.
|
||||
*/
|
||||
contract StrategyExecutor {
|
||||
using FormatLib for uint256;
|
||||
using FormatLib for int256;
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Configuration (immutable)
|
||||
// -------------------------------------------------------------------------
|
||||
|
|
@ -37,11 +47,17 @@ contract StrategyExecutor {
|
|||
address public immutable feeDestination;
|
||||
/// @notice Minimum block gap between recenter attempts.
|
||||
uint256 public immutable recenterInterval;
|
||||
/// @notice Position tracker — records lifecycle, fees, and P&L for each position.
|
||||
PositionTracker public immutable tracker;
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Runtime state
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/// @notice Block of the last recenter attempt (successful or not).
|
||||
/// Initialised to block.number at construction so the first recenter
|
||||
/// attempt is deferred by recenterInterval blocks rather than firing
|
||||
/// immediately on the first observed historical block.
|
||||
uint256 public lastRecenterBlock;
|
||||
uint256 public totalRecenters;
|
||||
uint256 public failedRecenters;
|
||||
|
|
@ -55,13 +71,19 @@ contract StrategyExecutor {
|
|||
IERC20 _wethToken,
|
||||
IERC20 _krkToken,
|
||||
address _feeDestination,
|
||||
uint256 _recenterInterval
|
||||
uint256 _recenterInterval,
|
||||
IUniswapV3Pool _pool,
|
||||
bool _token0isWeth
|
||||
) {
|
||||
lm = _lm;
|
||||
wethToken = _wethToken;
|
||||
krkToken = _krkToken;
|
||||
feeDestination = _feeDestination;
|
||||
recenterInterval = _recenterInterval;
|
||||
tracker = new PositionTracker(_pool, _token0isWeth);
|
||||
// Defer the first recenter attempt by recenterInterval blocks so we don't
|
||||
// try to recenter before any meaningful price movement has occurred.
|
||||
lastRecenterBlock = block.number;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
|
|
@ -70,9 +92,13 @@ contract StrategyExecutor {
|
|||
|
||||
/**
|
||||
* @notice Attempt a recenter if enough blocks have elapsed since the last one.
|
||||
* Always notifies the tracker of the current block for time-in-range accounting.
|
||||
* @param blockNum Current block number (as advanced by EventReplayer via vm.roll).
|
||||
*/
|
||||
function maybeRecenter(uint256 blockNum) external {
|
||||
// Always notify the tracker so time-in-range is counted for every observed block.
|
||||
tracker.notifyBlock(blockNum);
|
||||
|
||||
if (blockNum - lastRecenterBlock < recenterInterval) return;
|
||||
|
||||
// Snapshot pre-recenter positions.
|
||||
|
|
@ -103,9 +129,7 @@ contract StrategyExecutor {
|
|||
}
|
||||
|
||||
if (!success) {
|
||||
console2.log(
|
||||
string.concat("[recenter SKIP @ block ", _str(blockNum), "] reason: ", failReason)
|
||||
);
|
||||
console2.log(string.concat("[recenter SKIP @ block ", blockNum.str(), "] reason: ", failReason));
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -117,14 +141,56 @@ contract StrategyExecutor {
|
|||
uint256 feesWeth = wethToken.balanceOf(feeDestination) - wethPre;
|
||||
uint256 feesKrk = krkToken.balanceOf(feeDestination) - krkPre;
|
||||
|
||||
// Record recenter in position tracker.
|
||||
tracker.recordRecenter(
|
||||
PositionTracker.PositionSnapshot({
|
||||
floorLiq: fLiqPre,
|
||||
floorLo: fLoPre,
|
||||
floorHi: fHiPre,
|
||||
anchorLiq: aLiqPre,
|
||||
anchorLo: aLoPre,
|
||||
anchorHi: aHiPre,
|
||||
discLiq: dLiqPre,
|
||||
discLo: dLoPre,
|
||||
discHi: dHiPre
|
||||
}),
|
||||
PositionTracker.PositionSnapshot({
|
||||
floorLiq: fLiqPost,
|
||||
floorLo: fLoPost,
|
||||
floorHi: fHiPost,
|
||||
anchorLiq: aLiqPost,
|
||||
anchorLo: aLoPost,
|
||||
anchorHi: aHiPost,
|
||||
discLiq: dLiqPost,
|
||||
discLo: dLoPost,
|
||||
discHi: dHiPost
|
||||
}),
|
||||
feesWeth,
|
||||
feesKrk,
|
||||
blockNum,
|
||||
block.timestamp
|
||||
);
|
||||
|
||||
_logRecenter(
|
||||
blockNum,
|
||||
fLiqPre, fLoPre, fHiPre,
|
||||
aLiqPre, aLoPre, aHiPre,
|
||||
dLiqPre, dLoPre, dHiPre,
|
||||
fLiqPost, fLoPost, fHiPost,
|
||||
aLiqPost, aLoPost, aHiPost,
|
||||
dLiqPost, dLoPost, dHiPost,
|
||||
fLiqPre,
|
||||
fLoPre,
|
||||
fHiPre,
|
||||
aLiqPre,
|
||||
aLoPre,
|
||||
aHiPre,
|
||||
dLiqPre,
|
||||
dLoPre,
|
||||
dHiPre,
|
||||
fLiqPost,
|
||||
fLoPost,
|
||||
fHiPost,
|
||||
aLiqPost,
|
||||
aLoPost,
|
||||
aHiPost,
|
||||
dLiqPost,
|
||||
dLoPost,
|
||||
dHiPost,
|
||||
feesWeth,
|
||||
feesKrk
|
||||
);
|
||||
|
|
@ -146,36 +212,14 @@ contract StrategyExecutor {
|
|||
console2.log("Total recenters: ", totalRecenters);
|
||||
console2.log("Failed recenters: ", failedRecenters);
|
||||
console2.log("Recenter interval:", recenterInterval, "blocks");
|
||||
console2.log(
|
||||
string.concat(
|
||||
"Final Floor: tick [",
|
||||
_istr(fLo),
|
||||
", ",
|
||||
_istr(fHi),
|
||||
"] liq=",
|
||||
_str(uint256(fLiq))
|
||||
)
|
||||
);
|
||||
console2.log(
|
||||
string.concat(
|
||||
"Final Anchor: tick [",
|
||||
_istr(aLo),
|
||||
", ",
|
||||
_istr(aHi),
|
||||
"] liq=",
|
||||
_str(uint256(aLiq))
|
||||
)
|
||||
);
|
||||
console2.log(
|
||||
string.concat(
|
||||
"Final Discovery: tick [",
|
||||
_istr(dLo),
|
||||
", ",
|
||||
_istr(dHi),
|
||||
"] liq=",
|
||||
_str(uint256(dLiq))
|
||||
)
|
||||
);
|
||||
console2.log(string.concat("Final Floor: tick [", int256(fLo).istr(), ", ", int256(fHi).istr(), "] liq=", uint256(fLiq).str()));
|
||||
console2.log(string.concat("Final Anchor: tick [", int256(aLo).istr(), ", ", int256(aHi).istr(), "] liq=", uint256(aLiq).str()));
|
||||
console2.log(string.concat("Final Discovery: tick [", int256(dLo).istr(), ", ", int256(dHi).istr(), "] liq=", uint256(dLiq).str()));
|
||||
|
||||
// Use lastNotifiedBlock from the tracker as the authoritative final block —
|
||||
// it reflects the last block actually processed by the replay, which may be
|
||||
// up to recenterInterval blocks later than lastRecenterBlock.
|
||||
tracker.logFinalSummary(tracker.lastNotifiedBlock());
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
|
|
@ -184,108 +228,37 @@ contract StrategyExecutor {
|
|||
|
||||
function _logRecenter(
|
||||
uint256 blockNum,
|
||||
uint128 fLiqPre, int24 fLoPre, int24 fHiPre,
|
||||
uint128 aLiqPre, int24 aLoPre, int24 aHiPre,
|
||||
uint128 dLiqPre, int24 dLoPre, int24 dHiPre,
|
||||
uint128 fLiqPost, int24 fLoPost, int24 fHiPost,
|
||||
uint128 aLiqPost, int24 aLoPost, int24 aHiPost,
|
||||
uint128 dLiqPost, int24 dLoPost, int24 dHiPost,
|
||||
uint128 fLiqPre,
|
||||
int24 fLoPre,
|
||||
int24 fHiPre,
|
||||
uint128 aLiqPre,
|
||||
int24 aLoPre,
|
||||
int24 aHiPre,
|
||||
uint128 dLiqPre,
|
||||
int24 dLoPre,
|
||||
int24 dHiPre,
|
||||
uint128 fLiqPost,
|
||||
int24 fLoPost,
|
||||
int24 fHiPost,
|
||||
uint128 aLiqPost,
|
||||
int24 aLoPost,
|
||||
int24 aHiPost,
|
||||
uint128 dLiqPost,
|
||||
int24 dLoPost,
|
||||
int24 dHiPost,
|
||||
uint256 feesWeth,
|
||||
uint256 feesKrk
|
||||
)
|
||||
internal
|
||||
view
|
||||
{
|
||||
console2.log(
|
||||
string.concat("=== Recenter #", _str(totalRecenters), " @ block ", _str(blockNum), " ===")
|
||||
);
|
||||
console2.log(
|
||||
string.concat(
|
||||
" Floor pre: tick [",
|
||||
_istr(fLoPre),
|
||||
", ",
|
||||
_istr(fHiPre),
|
||||
"] liq=",
|
||||
_str(uint256(fLiqPre))
|
||||
)
|
||||
);
|
||||
console2.log(
|
||||
string.concat(
|
||||
" Anchor pre: tick [",
|
||||
_istr(aLoPre),
|
||||
", ",
|
||||
_istr(aHiPre),
|
||||
"] liq=",
|
||||
_str(uint256(aLiqPre))
|
||||
)
|
||||
);
|
||||
console2.log(
|
||||
string.concat(
|
||||
" Disc pre: tick [",
|
||||
_istr(dLoPre),
|
||||
", ",
|
||||
_istr(dHiPre),
|
||||
"] liq=",
|
||||
_str(uint256(dLiqPre))
|
||||
)
|
||||
);
|
||||
console2.log(
|
||||
string.concat(
|
||||
" Floor post: tick [",
|
||||
_istr(fLoPost),
|
||||
", ",
|
||||
_istr(fHiPost),
|
||||
"] liq=",
|
||||
_str(uint256(fLiqPost))
|
||||
)
|
||||
);
|
||||
console2.log(
|
||||
string.concat(
|
||||
" Anchor post: tick [",
|
||||
_istr(aLoPost),
|
||||
", ",
|
||||
_istr(aHiPost),
|
||||
"] liq=",
|
||||
_str(uint256(aLiqPost))
|
||||
)
|
||||
);
|
||||
console2.log(
|
||||
string.concat(
|
||||
" Disc post: tick [",
|
||||
_istr(dLoPost),
|
||||
", ",
|
||||
_istr(dHiPost),
|
||||
"] liq=",
|
||||
_str(uint256(dLiqPost))
|
||||
)
|
||||
);
|
||||
console2.log(
|
||||
string.concat(" Fees WETH: ", _str(feesWeth), " Fees KRK: ", _str(feesKrk))
|
||||
);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Formatting helpers (no vm dependency required)
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
function _str(uint256 v) internal pure returns (string memory) {
|
||||
if (v == 0) return "0";
|
||||
uint256 tmp = v;
|
||||
uint256 len;
|
||||
while (tmp != 0) {
|
||||
len++;
|
||||
tmp /= 10;
|
||||
}
|
||||
bytes memory buf = new bytes(len);
|
||||
while (v != 0) {
|
||||
buf[--len] = bytes1(uint8(48 + v % 10));
|
||||
v /= 10;
|
||||
}
|
||||
return string(buf);
|
||||
}
|
||||
|
||||
function _istr(int256 v) internal pure returns (string memory) {
|
||||
if (v >= 0) return _str(uint256(v));
|
||||
return string.concat("-", _str(uint256(-v)));
|
||||
console2.log(string.concat("=== Recenter #", totalRecenters.str(), " @ block ", blockNum.str(), " ==="));
|
||||
console2.log(string.concat(" Floor pre: tick [", int256(fLoPre).istr(), ", ", int256(fHiPre).istr(), "] liq=", uint256(fLiqPre).str()));
|
||||
console2.log(string.concat(" Anchor pre: tick [", int256(aLoPre).istr(), ", ", int256(aHiPre).istr(), "] liq=", uint256(aLiqPre).str()));
|
||||
console2.log(string.concat(" Disc pre: tick [", int256(dLoPre).istr(), ", ", int256(dHiPre).istr(), "] liq=", uint256(dLiqPre).str()));
|
||||
console2.log(string.concat(" Floor post: tick [", int256(fLoPost).istr(), ", ", int256(fHiPost).istr(), "] liq=", uint256(fLiqPost).str()));
|
||||
console2.log(string.concat(" Anchor post: tick [", int256(aLoPost).istr(), ", ", int256(aHiPost).istr(), "] liq=", uint256(aLiqPost).str()));
|
||||
console2.log(string.concat(" Disc post: tick [", int256(dLoPost).istr(), ", ", int256(dHiPost).istr(), "] liq=", uint256(dLiqPost).str()));
|
||||
console2.log(string.concat(" Fees WETH: ", feesWeth.str(), " Fees KRK: ", feesKrk.str()));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,131 +17,135 @@ contract OptimizerV3Push3 {
|
|||
* @param averageTaxRate Normalized average tax rate from Stake contract (0 to 1e18).
|
||||
* @return bull True if bull config, false if bear.
|
||||
*/
|
||||
function isBullMarket(
|
||||
uint256 percentageStaked,
|
||||
uint256 averageTaxRate
|
||||
) public pure returns (bool bull) {
|
||||
function isBullMarket(uint256 percentageStaked, uint256 averageTaxRate) public pure returns (bool bull) {
|
||||
require(percentageStaked <= 1e18, "Invalid percentage staked");
|
||||
require(averageTaxRate <= 1e18, "Invalid tax rate");
|
||||
uint256 taxrate = uint256(averageTaxRate);
|
||||
uint256 staked = uint256(((percentageStaked * 100) / 1000000000000000000));
|
||||
uint256 staked = uint256(((percentageStaked * 100) / 1_000_000_000_000_000_000));
|
||||
bool b33;
|
||||
if ((staked > 91)) {
|
||||
uint256 deltas = uint256((100 - staked));
|
||||
uint256 r28;
|
||||
if ((taxrate <= 206185567010309)) {
|
||||
if ((taxrate <= 206_185_567_010_309)) {
|
||||
r28 = uint256(0);
|
||||
} else {
|
||||
uint256 r27;
|
||||
if ((taxrate <= 412371134020618)) {
|
||||
if ((taxrate <= 412_371_134_020_618)) {
|
||||
r27 = uint256(1);
|
||||
} else {
|
||||
uint256 r26;
|
||||
if ((taxrate <= 618556701030927)) {
|
||||
if ((taxrate <= 618_556_701_030_927)) {
|
||||
r26 = uint256(2);
|
||||
} else {
|
||||
uint256 r25;
|
||||
if ((taxrate <= 1030927835051546)) {
|
||||
if ((taxrate <= 1_030_927_835_051_546)) {
|
||||
r25 = uint256(3);
|
||||
} else {
|
||||
uint256 r24;
|
||||
if ((taxrate <= 1546391752577319)) {
|
||||
if ((taxrate <= 1_546_391_752_577_319)) {
|
||||
r24 = uint256(4);
|
||||
} else {
|
||||
uint256 r23;
|
||||
if ((taxrate <= 2164948453608247)) {
|
||||
if ((taxrate <= 2_164_948_453_608_247)) {
|
||||
r23 = uint256(5);
|
||||
} else {
|
||||
uint256 r22;
|
||||
if ((taxrate <= 2783505154639175)) {
|
||||
if ((taxrate <= 2_783_505_154_639_175)) {
|
||||
r22 = uint256(6);
|
||||
} else {
|
||||
uint256 r21;
|
||||
if ((taxrate <= 3608247422680412)) {
|
||||
if ((taxrate <= 3_608_247_422_680_412)) {
|
||||
r21 = uint256(7);
|
||||
} else {
|
||||
uint256 r20;
|
||||
if ((taxrate <= 4639175257731958)) {
|
||||
if ((taxrate <= 4_639_175_257_731_958)) {
|
||||
r20 = uint256(8);
|
||||
} else {
|
||||
uint256 r19;
|
||||
if ((taxrate <= 5670103092783505)) {
|
||||
if ((taxrate <= 5_670_103_092_783_505)) {
|
||||
r19 = uint256(9);
|
||||
} else {
|
||||
uint256 r18;
|
||||
if ((taxrate <= 7216494845360824)) {
|
||||
if ((taxrate <= 7_216_494_845_360_824)) {
|
||||
r18 = uint256(10);
|
||||
} else {
|
||||
uint256 r17;
|
||||
if ((taxrate <= 9278350515463917)) {
|
||||
if ((taxrate <= 9_278_350_515_463_917)) {
|
||||
r17 = uint256(11);
|
||||
} else {
|
||||
uint256 r16;
|
||||
if ((taxrate <= 11855670103092783)) {
|
||||
if ((taxrate <= 11_855_670_103_092_783)) {
|
||||
r16 = uint256(12);
|
||||
} else {
|
||||
uint256 r15;
|
||||
if ((taxrate <= 15979381443298969)) {
|
||||
if ((taxrate <= 15_979_381_443_298_969)) {
|
||||
r15 = uint256(13);
|
||||
} else {
|
||||
uint256 r14;
|
||||
if ((taxrate <= 22164948453608247)) {
|
||||
if ((taxrate <= 22_164_948_453_608_247)) {
|
||||
r14 = uint256(14);
|
||||
} else {
|
||||
uint256 r13;
|
||||
if ((taxrate <= 29381443298969072)) {
|
||||
if ((taxrate <= 29_381_443_298_969_072)) {
|
||||
r13 = uint256(15);
|
||||
} else {
|
||||
uint256 r12;
|
||||
if ((taxrate <= 38144329896907216)) {
|
||||
if ((taxrate <= 38_144_329_896_907_216)) {
|
||||
r12 = uint256(16);
|
||||
} else {
|
||||
uint256 r11;
|
||||
if ((taxrate <= 49484536082474226)) {
|
||||
if ((taxrate <= 49_484_536_082_474_226)) {
|
||||
r11 = uint256(17);
|
||||
} else {
|
||||
uint256 r10;
|
||||
if ((taxrate <= 63917525773195876)) {
|
||||
if ((taxrate <= 63_917_525_773_195_876)) {
|
||||
r10 = uint256(18);
|
||||
} else {
|
||||
uint256 r9;
|
||||
if ((taxrate <= 83505154639175257)) {
|
||||
if ((taxrate <= 83_505_154_639_175_257)) {
|
||||
r9 = uint256(19);
|
||||
} else {
|
||||
uint256 r8;
|
||||
if ((taxrate <= 109278350515463917)) {
|
||||
if ((taxrate <= 109_278_350_515_463_917)) {
|
||||
r8 = uint256(20);
|
||||
} else {
|
||||
uint256 r7;
|
||||
if ((taxrate <= 144329896907216494)) {
|
||||
if ((taxrate <= 144_329_896_907_216_494)) {
|
||||
r7 = uint256(21);
|
||||
} else {
|
||||
uint256 r6;
|
||||
if ((taxrate <= 185567010309278350)) {
|
||||
if ((taxrate <= 185_567_010_309_278_350)) {
|
||||
r6 = uint256(22);
|
||||
} else {
|
||||
uint256 r5;
|
||||
if ((taxrate <= 237113402061855670)) {
|
||||
if ((taxrate <= 237_113_402_061_855_670)) {
|
||||
r5 = uint256(23);
|
||||
} else {
|
||||
uint256 r4;
|
||||
if ((taxrate <= 309278350515463917)) {
|
||||
if ((taxrate <= 309_278_350_515_463_917)) {
|
||||
r4 = uint256(24);
|
||||
} else {
|
||||
uint256 r3;
|
||||
if ((taxrate <= 402061855670103092)) {
|
||||
if ((taxrate <= 402_061_855_670_103_092)) {
|
||||
r3 = uint256(25);
|
||||
} else {
|
||||
uint256 r2;
|
||||
if ((taxrate <= 520618556701030927)) {
|
||||
if ((taxrate <= 520_618_556_701_030_927)) {
|
||||
r2 = uint256(26);
|
||||
} else {
|
||||
uint256 r1;
|
||||
if ((taxrate <= 680412371134020618)) {
|
||||
if (
|
||||
(taxrate <= 680_412_371_134_020_618)
|
||||
) {
|
||||
r1 = uint256(27);
|
||||
} else {
|
||||
uint256 r0;
|
||||
if ((taxrate <= 886597938144329896)) {
|
||||
if (
|
||||
(
|
||||
taxrate
|
||||
<= 886_597_938_144_329_896
|
||||
)
|
||||
) {
|
||||
r0 = uint256(28);
|
||||
} else {
|
||||
r0 = uint256(29);
|
||||
|
|
|
|||
|
|
@ -272,12 +272,24 @@ abstract contract ThreePositionStrategy is UniswapMath, VWAPTracker {
|
|||
isScarcity = true;
|
||||
if (token0isWeth) {
|
||||
floorTarget = scarcityTick;
|
||||
if (mirrorTick > floorTarget) { floorTarget = mirrorTick; isScarcity = false; }
|
||||
if (clampTick > floorTarget) { floorTarget = clampTick; isScarcity = false; }
|
||||
if (mirrorTick > floorTarget) {
|
||||
floorTarget = mirrorTick;
|
||||
isScarcity = false;
|
||||
}
|
||||
if (clampTick > floorTarget) {
|
||||
floorTarget = clampTick;
|
||||
isScarcity = false;
|
||||
}
|
||||
} else {
|
||||
floorTarget = scarcityTick;
|
||||
if (mirrorTick < floorTarget) { floorTarget = mirrorTick; isScarcity = false; }
|
||||
if (clampTick < floorTarget) { floorTarget = clampTick; isScarcity = false; }
|
||||
if (mirrorTick < floorTarget) {
|
||||
floorTarget = mirrorTick;
|
||||
isScarcity = false;
|
||||
}
|
||||
if (clampTick < floorTarget) {
|
||||
floorTarget = clampTick;
|
||||
isScarcity = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -64,9 +64,7 @@ contract Dummy {
|
|||
|
||||
/// @notice Harness that exposes LiquidityManager's internal abstract functions for coverage
|
||||
contract LiquidityManagerHarness is LiquidityManager {
|
||||
constructor(address _factory, address _WETH9, address _kraiken, address _optimizer)
|
||||
LiquidityManager(_factory, _WETH9, _kraiken, _optimizer)
|
||||
{ }
|
||||
constructor(address _factory, address _WETH9, address _kraiken, address _optimizer) LiquidityManager(_factory, _WETH9, _kraiken, _optimizer) { }
|
||||
|
||||
function exposed_getKraikenToken() external view returns (address) {
|
||||
return _getKraikenToken();
|
||||
|
|
@ -1080,8 +1078,7 @@ contract LiquidityManagerTest is UniSwapHelper {
|
|||
* to cover _getKraikenToken() and _getWethToken() (lines 270-271, 275-276)
|
||||
*/
|
||||
function testHarnessAbstractFunctions() public {
|
||||
LiquidityManagerHarness harness =
|
||||
new LiquidityManagerHarness(address(factory), address(weth), address(harberg), address(optimizer));
|
||||
LiquidityManagerHarness harness = new LiquidityManagerHarness(address(factory), address(weth), address(harberg), address(optimizer));
|
||||
|
||||
assertEq(harness.exposed_getKraikenToken(), address(harberg), "_getKraikenToken should return kraiken");
|
||||
assertEq(harness.exposed_getWethToken(), address(weth), "_getWethToken should return weth");
|
||||
|
|
|
|||
|
|
@ -5,9 +5,10 @@ import "../src/Optimizer.sol";
|
|||
|
||||
import "./mocks/MockKraiken.sol";
|
||||
import "./mocks/MockStake.sol";
|
||||
|
||||
import { ERC1967Proxy } from "@openzeppelin/proxy/ERC1967/ERC1967Proxy.sol";
|
||||
import "forge-std/Test.sol";
|
||||
import "forge-std/console.sol";
|
||||
import { ERC1967Proxy } from "@openzeppelin/proxy/ERC1967/ERC1967Proxy.sol";
|
||||
|
||||
/// @dev Harness to expose internal _calculateAnchorWidth for direct coverage of the totalWidth < 10 path
|
||||
contract OptimizerHarness is Optimizer {
|
||||
|
|
@ -290,10 +291,7 @@ contract OptimizerTest is Test {
|
|||
*/
|
||||
function testUUPSUpgrade() public {
|
||||
Optimizer impl1 = new Optimizer();
|
||||
ERC1967Proxy proxy = new ERC1967Proxy(
|
||||
address(impl1),
|
||||
abi.encodeWithSelector(Optimizer.initialize.selector, address(mockKraiken), address(mockStake))
|
||||
);
|
||||
ERC1967Proxy proxy = new ERC1967Proxy(address(impl1), abi.encodeWithSelector(Optimizer.initialize.selector, address(mockKraiken), address(mockStake)));
|
||||
Optimizer proxyOptimizer = Optimizer(address(proxy));
|
||||
|
||||
// Deployer (this contract) is admin — upgrade should succeed
|
||||
|
|
@ -331,10 +329,7 @@ contract OptimizerTest is Test {
|
|||
*/
|
||||
function testUnauthorizedUpgradeReverts() public {
|
||||
Optimizer impl1 = new Optimizer();
|
||||
ERC1967Proxy proxy = new ERC1967Proxy(
|
||||
address(impl1),
|
||||
abi.encodeWithSelector(Optimizer.initialize.selector, address(mockKraiken), address(mockStake))
|
||||
);
|
||||
ERC1967Proxy proxy = new ERC1967Proxy(address(impl1), abi.encodeWithSelector(Optimizer.initialize.selector, address(mockKraiken), address(mockStake)));
|
||||
Optimizer proxyOptimizer = Optimizer(address(proxy));
|
||||
|
||||
// Deploy impl2 BEFORE the prank so the prank applies only to upgradeTo
|
||||
|
|
|
|||
|
|
@ -95,7 +95,7 @@ contract OptimizerV3Push3Test is Test {
|
|||
|
||||
function testFuzzNeverReverts(uint256 percentageStaked, uint256 averageTaxRate) public view {
|
||||
percentageStaked = bound(percentageStaked, 0, 1e18);
|
||||
averageTaxRate = bound(averageTaxRate, 0, 1e18);
|
||||
averageTaxRate = bound(averageTaxRate, 0, 1e18);
|
||||
push3.isBullMarket(percentageStaked, averageTaxRate);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -474,9 +474,7 @@ contract VWAPTrackerTest is Test {
|
|||
uint256 cappedVWP = type(uint256).max / 2;
|
||||
uint256 expectedVolume = cappedVWP / extremePrice;
|
||||
|
||||
assertEq(
|
||||
vwapTracker.cumulativeVolumeWeightedPriceX96(), cappedVWP, "Single-tx overflow: cumulative VWAP should be capped"
|
||||
);
|
||||
assertEq(vwapTracker.cumulativeVolumeWeightedPriceX96(), cappedVWP, "Single-tx overflow: cumulative VWAP should be capped");
|
||||
assertEq(vwapTracker.cumulativeVolume(), expectedVolume, "Single-tx overflow: volume should be recalculated from cap");
|
||||
|
||||
// VWAP should equal the extreme price (capped numerator / recalculated denominator)
|
||||
|
|
@ -512,14 +510,8 @@ contract VWAPTrackerTest is Test {
|
|||
uint256 expectedCumulativeVolume = largeVolume / compressionFactor + newVolume;
|
||||
|
||||
assertEq(
|
||||
vwapTracker.cumulativeVolumeWeightedPriceX96(),
|
||||
expectedCumulativeVWAP,
|
||||
"Max compression: cumulative VWAP should be compressed by exactly 1000"
|
||||
);
|
||||
assertEq(
|
||||
vwapTracker.cumulativeVolume(),
|
||||
expectedCumulativeVolume,
|
||||
"Max compression: cumulative volume should be compressed by exactly 1000"
|
||||
vwapTracker.cumulativeVolumeWeightedPriceX96(), expectedCumulativeVWAP, "Max compression: cumulative VWAP should be compressed by exactly 1000"
|
||||
);
|
||||
assertEq(vwapTracker.cumulativeVolume(), expectedCumulativeVolume, "Max compression: cumulative volume should be compressed by exactly 1000");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue