diff --git a/onchain/script/backtesting/BacktestRunner.s.sol b/onchain/script/backtesting/BacktestRunner.s.sol index facca20..0f9b954 100644 --- a/onchain/script/backtesting/BacktestRunner.s.sol +++ b/onchain/script/backtesting/BacktestRunner.s.sol @@ -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 { } } } diff --git a/onchain/script/backtesting/EventReplayer.sol b/onchain/script/backtesting/EventReplayer.sol index 2b46db0..16f2937 100644 --- a/onchain/script/backtesting/EventReplayer.sol +++ b/onchain/script/backtesting/EventReplayer.sol @@ -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); } diff --git a/onchain/script/backtesting/KrAIkenDeployer.sol b/onchain/script/backtesting/KrAIkenDeployer.sol index eec7c28..fb91233 100644 --- a/onchain/script/backtesting/KrAIkenDeployer.sol +++ b/onchain/script/backtesting/KrAIkenDeployer.sol @@ -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); } diff --git a/onchain/script/backtesting/PositionTracker.sol b/onchain/script/backtesting/PositionTracker.sol new file mode 100644 index 0000000..266230e --- /dev/null +++ b/onchain/script/backtesting/PositionTracker.sol @@ -0,0 +1,465 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8.19; + +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 (proportional to each position's share of total liquidity) + * - 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 numerator/denominator 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. + */ +contract PositionTracker { + using Math for uint256; + + // ------------------------------------------------------------------------- + // 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. + * @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,,,,,,) = 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++; + + uint256 totalOldLiq = uint256(oldPos.floorLiq) + uint256(oldPos.anchorLiq) + uint256(oldPos.discLiq); + + // Close old positions (skip stages where old liquidity is zero or no open record exists). + _closePosition(0, oldPos.floorLiq, oldPos.floorLo, oldPos.floorHi, fees0, fees1, totalOldLiq, sqrtPriceX96, blockNum); + _closePosition(1, oldPos.anchorLiq, oldPos.anchorLo, oldPos.anchorHi, fees0, fees1, totalOldLiq, sqrtPriceX96, blockNum); + _closePosition(2, oldPos.discLiq, oldPos.discLo, oldPos.discHi, fees0, fees1, totalOldLiq, 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=", _str(blockNum))); + console2.log(string.concat("[TRACKER][SUMMARY] rebalances=", _str(rebalanceCount))); + console2.log( + string.concat("[TRACKER][SUMMARY] totalFees0=", _str(totalFees0), " totalFees1=", _str(totalFees1), " totalFeesToken0=", _str(totalFeesToken0)) + ); + console2.log(string.concat("[TRACKER][SUMMARY] totalIL=", _istr(finalIL), " netPnL=", _istr(finalNetPnL), " (token0 units)")); + console2.log( + string.concat( + "[TRACKER][SUMMARY] timeInRange=", _str(timeInRangeBps), " bps blocksAnchorInRange=", _str(blocksAnchorInRange), "/", _str(blocksChecked) + ) + ); + console2.log(string.concat("[TRACKER][SUMMARY] capitalEff: feesToken0=", _str(totalFeesToken0), " liqBlocks=", _str(totalLiquidityBlocks))); + } + + // ------------------------------------------------------------------------- + // 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=", + _str(blockNum), + " ts=", + _str(timestamp), + " tick=[", + _istr(tickLower), + ",", + _istr(tickUpper), + "] liq=", + _str(uint256(liquidity)), + " amt0=", + _str(amt0), + " amt1=", + _str(amt1) + ) + ); + } + + function _closePosition( + uint8 stageIdx, + uint128 liquidity, + int24 tickLower, + int24 tickUpper, + uint256 fees0Total, + uint256 fees1Total, + uint256 totalOldLiq, + uint160 sqrtPriceX96, + uint256 blockNum + ) + internal + { + if (liquidity == 0) return; + OpenPosition storage pos = openPositions[stageIdx]; + if (!pos.active) return; // first ever close: no open record to match + + (uint256 exitAmt0, uint256 exitAmt1) = _positionAmounts(liquidity, tickLower, tickUpper, sqrtPriceX96); + + // Attribute fees proportional to this position's share of total liquidity. + uint256 posLiq = uint256(liquidity); + uint256 myFees0 = totalOldLiq > 0 ? fees0Total.mulDiv(posLiq, totalOldLiq) : 0; + uint256 myFees1 = totalOldLiq > 0 ? fees1Total.mulDiv(posLiq, totalOldLiq) : 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=", + _str(blockNum), + " entryBlock=", + _str(pos.entryBlock), + " tick=[", + _istr(tickLower), + ",", + _istr(tickUpper), + "] liq=", + _str(posLiq) + ) + ); + console2.log( + string.concat( + "[TRACKER][CLOSE] entryAmt0=", + _str(pos.entryAmount0), + " entryAmt1=", + _str(pos.entryAmount1), + " exitAmt0=", + _str(exitAmt0), + " exitAmt1=", + _str(exitAmt1), + " fees0=", + _str(myFees0), + " fees1=", + _str(myFees1) + ) + ); + console2.log(string.concat("[TRACKER][CLOSE] IL=", _istr(il), " netPnL=", _istr(netPnL), " (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=", + _str(blockNum), + " rebalances=", + _str(rebalanceCount), + " totalFees0=", + _str(totalFees0), + " totalFees1=", + _str(totalFees1), + " IL=", + _istr(totalILToken0), + " netPnL=", + _istr(totalNetPnLToken0), + " timeInRange=", + _str(timeInRangeBps), + " bps" + ) + ); + } + + // ------------------------------------------------------------------------- + // Internal: Uniswap V3 math + // ------------------------------------------------------------------------- + + /** + * @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 helpers (no vm dependency) + // ------------------------------------------------------------------------- + + function _stageName(uint8 idx) internal pure returns (string memory) { + if (idx == 0) return "FLOOR"; + if (idx == 1) return "ANCHOR"; + return "DISC"; + } + + 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))); + } +} diff --git a/onchain/script/backtesting/ShadowPoolDeployer.sol b/onchain/script/backtesting/ShadowPoolDeployer.sol index ba5a739..e39c831 100644 --- a/onchain/script/backtesting/ShadowPoolDeployer.sol +++ b/onchain/script/backtesting/ShadowPoolDeployer.sol @@ -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); diff --git a/onchain/script/backtesting/StrategyExecutor.sol b/onchain/script/backtesting/StrategyExecutor.sol index 969d77c..584a3a6 100644 --- a/onchain/script/backtesting/StrategyExecutor.sol +++ b/onchain/script/backtesting/StrategyExecutor.sol @@ -1,10 +1,12 @@ // 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 { 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 +19,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). @@ -37,6 +43,8 @@ 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 @@ -55,13 +63,16 @@ 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); } // ------------------------------------------------------------------------- @@ -70,9 +81,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 +118,7 @@ contract StrategyExecutor { } if (!success) { - console2.log( - string.concat("[recenter SKIP @ block ", _str(blockNum), "] reason: ", failReason) - ); + console2.log(string.concat("[recenter SKIP @ block ", _str(blockNum), "] reason: ", failReason)); return; } @@ -117,14 +130,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 +201,11 @@ 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 [", _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)))); + + tracker.logFinalSummary(lastRecenterBlock); } // ------------------------------------------------------------------------- @@ -184,84 +214,38 @@ 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)) - ); + 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))); } // ------------------------------------------------------------------------- diff --git a/onchain/src/OptimizerV3Push3.sol b/onchain/src/OptimizerV3Push3.sol index 1b8b382..2e5a8b2 100644 --- a/onchain/src/OptimizerV3Push3.sol +++ b/onchain/src/OptimizerV3Push3.sol @@ -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); diff --git a/onchain/src/abstracts/ThreePositionStrategy.sol b/onchain/src/abstracts/ThreePositionStrategy.sol index e84b57f..049d69b 100644 --- a/onchain/src/abstracts/ThreePositionStrategy.sol +++ b/onchain/src/abstracts/ThreePositionStrategy.sol @@ -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; + } } } } diff --git a/onchain/test/LiquidityManager.t.sol b/onchain/test/LiquidityManager.t.sol index 6a38392..a3907e8 100644 --- a/onchain/test/LiquidityManager.t.sol +++ b/onchain/test/LiquidityManager.t.sol @@ -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"); diff --git a/onchain/test/Optimizer.t.sol b/onchain/test/Optimizer.t.sol index dbdd241..d22eef6 100644 --- a/onchain/test/Optimizer.t.sol +++ b/onchain/test/Optimizer.t.sol @@ -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 diff --git a/onchain/test/OptimizerV3Push3.t.sol b/onchain/test/OptimizerV3Push3.t.sol index c912b00..83b958b 100644 --- a/onchain/test/OptimizerV3Push3.t.sol +++ b/onchain/test/OptimizerV3Push3.t.sol @@ -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); } } diff --git a/onchain/test/VWAPTracker.t.sol b/onchain/test/VWAPTracker.t.sol index e3da654..33fc02e 100644 --- a/onchain/test/VWAPTracker.t.sol +++ b/onchain/test/VWAPTracker.t.sol @@ -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"); } }