Merge pull request 'fix: Backtesting #4: Deploy KrAIken contracts + recenter execution loop (#318)' (#349) from fix/issue-318 into master

This commit is contained in:
johba 2026-02-27 10:26:12 +01:00
commit 19df05843d
5 changed files with 611 additions and 16 deletions

View file

@ -0,0 +1,94 @@
// SPDX-License-Identifier: GPL-3.0-or-later
pragma solidity ^0.8.19;
import { MockToken } from "./MockToken.sol";
/**
* @title BacktestKraiken
* @notice Kraiken-compatible ERC-20 for backtesting purposes only.
*
* Dual mint interface:
* - MockToken.mint(address to, uint256 amount) public, inherited from MockToken.
* Used by EventReplayer to seed historical pool positions without access checks.
* - mint(uint256 amount) restricted to liquidityManager.
* Mirrors Kraiken.mint(uint256) so LiquidityManager operates unchanged.
*
* burn(uint256), setPreviousTotalSupply(), outstandingSupply(), and peripheryContracts()
* mirror the real Kraiken interface that LiquidityManager depends on.
*
* Omissions vs real Kraiken (acceptable for backtesting):
* - No staking pool proportional minting (no Stake.sol deployed).
* - No ERC20Permit extension.
* - setLiquidityManager() has no deployer-only guard (script context only).
*/
contract BacktestKraiken is MockToken {
address public liquidityManager;
uint256 public previousTotalSupply;
constructor() MockToken("Backtesting KRK", "bKRK", 18) { }
modifier onlyLiquidityManager() {
require(msg.sender == liquidityManager, "only liquidity manager");
_;
}
// -------------------------------------------------------------------------
// Wiring
// -------------------------------------------------------------------------
/**
* @notice Wire the LiquidityManager. Can only be called once.
*/
function setLiquidityManager(address lm) external {
require(liquidityManager == address(0), "already set");
liquidityManager = lm;
}
// -------------------------------------------------------------------------
// LM-restricted supply management (mirrors Kraiken interface)
// -------------------------------------------------------------------------
/**
* @notice Mint `amount` tokens to the LiquidityManager.
* Overloads MockToken.mint(address,uint256) the two signatures
* have distinct ABI selectors and coexist without conflict.
*/
function mint(uint256 amount) external onlyLiquidityManager {
if (amount > 0) _mint(liquidityManager, amount);
if (previousTotalSupply == 0) previousTotalSupply = totalSupply();
}
/**
* @notice Burn `amount` tokens from the LiquidityManager's balance.
*/
function burn(uint256 amount) external onlyLiquidityManager {
if (amount > 0) _burn(liquidityManager, amount);
}
/**
* @notice Called by LM when isUp == true during recenter.
*/
function setPreviousTotalSupply(uint256 ts) external onlyLiquidityManager {
previousTotalSupply = ts;
}
// -------------------------------------------------------------------------
// View helpers (mirrors Kraiken interface)
// -------------------------------------------------------------------------
/**
* @notice Total supply minus the LM's own balance (trader-held supply).
*/
function outstandingSupply() external view returns (uint256) {
return totalSupply() - balanceOf(liquidityManager);
}
/**
* @notice Returns (liquidityManager, address(0)) no staking pool in backtests.
* LiquidityManager._getOutstandingSupply() calls this to exclude staked KRK;
* returning address(0) for stakingPool skips that subtraction safely.
*/
function peripheryContracts() external view returns (address, address) {
return (liquidityManager, address(0));
}
}

View file

@ -3,21 +3,42 @@ pragma solidity ^0.8.19;
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
* @notice Entry point for backtesting. Deploys a UniswapV3 shadow pool that mirrors the
* AERO/WETH 1% pool configuration, initialised at the price from the event cache,
* then replays all Swap/Mint/Burn events from the cache against the shadow pool.
* deploys the KrAIken protocol on top of the shadow pool, then replays all
* Swap/Mint/Burn events from the cache while triggering recenter() at configurable
* block intervals.
*
* Token setup:
* Mock WETH (MockToken) and BacktestKraiken are the shadow pool token pair, sorted by
* address. Both are freely mintable via MockToken.mint(address,uint256), so EventReplayer
* can seed historical LP positions without access-control friction. LiquidityManager
* additionally uses BacktestKraiken.mint(uint256) the Kraiken-compatible overload
* restricted to the LM address for its own liquidity minting.
*
* Negligible-impact assumption:
* KrAIken positions sit in the same shadow pool as replayed historical events.
* Events are replayed as-is without adjusting swap amounts for KrAIken's liquidity.
* TODO(#319): Add a feedback loop that accounts for KrAIken's active tick ranges
* when computing the actual swap amounts seen by each LP.
*
* Usage:
* forge script script/backtesting/BacktestRunner.s.sol \
* --rpc-url http://localhost:8545 --broadcast \
* [BACKTEST_EVENTS_FILE=/path/to/events.jsonl] \
* [INITIAL_SQRT_PRICE_X96=<uint160>]
* [INITIAL_SQRT_PRICE_X96=<uint160>] \
* [RECENTER_INTERVAL=100] \
* [INITIAL_CAPITAL_WETH=10000000000000000000]
*
* Price resolution order (first that succeeds wins):
* 1. Environment variable INITIAL_SQRT_PRICE_X96
@ -28,6 +49,9 @@ contract BacktestRunner is Script {
/// @dev 2^96 corresponds to tick 0 (price ratio 1:1).
uint160 internal constant DEFAULT_SQRT_PRICE_X96 = 79_228_162_514_264_337_593_543_950_336;
/// @dev Default number of blocks between recenter attempts (~3 min on Base at 2s/block).
uint256 internal constant DEFAULT_RECENTER_INTERVAL = 100;
// -------------------------------------------------------------------------
// Price resolution helpers
// -------------------------------------------------------------------------
@ -71,28 +95,73 @@ contract BacktestRunner is Script {
// -------------------------------------------------------------------------
function run() external {
// Resolve price before broadcast so the self-call is not recorded.
// Resolve configuration before broadcast so self-calls are not recorded.
uint160 sqrtPriceX96 = _resolveSqrtPrice();
uint256 recenterInterval = DEFAULT_RECENTER_INTERVAL;
try vm.envUint("RECENTER_INTERVAL") returns (uint256 v) {
if (v > 0) recenterInterval = v;
} catch {}
uint256 initialCapital = KrAIkenDeployer.DEFAULT_INITIAL_CAPITAL;
try vm.envUint("INITIAL_CAPITAL_WETH") returns (uint256 v) {
if (v > 0) initialCapital = v;
} catch {}
vm.startBroadcast();
// Deploy mock ERC20 tokens (18 decimals, matching AERO and WETH).
MockToken tokenA = new MockToken("Mock AERO", "mAERO", 18);
MockToken tokenB = new MockToken("Mock WETH", "mWETH", 18);
// Mint an initial supply to the broadcaster for future liquidity seeding.
address sender = msg.sender;
tokenA.mint(sender, 1_000_000 ether);
tokenB.mint(sender, 1_000_000 ether);
// Deploy factory + pool and initialise at the resolved price.
ShadowPool memory sp = ShadowPoolDeployer.deploy(address(tokenA), address(tokenB), sqrtPriceX96);
// ------------------------------------------------------------------
// Token deployment
//
// BacktestKraiken extends MockToken, so EventReplayer can mint it
// freely via MockToken.mint(address,uint256) for historical pool
// positions. LiquidityManager calls the restricted mint(uint256)
// overload (onlyLiquidityManager) wired by KrAIkenDeployer.deploy().
// ------------------------------------------------------------------
MockToken mockWeth = new MockToken("Mock WETH", "mWETH", 18);
BacktestKraiken krk = new BacktestKraiken();
// Seed the broadcaster with mock WETH for manual interactions.
mockWeth.mint(sender, 1_000_000 ether);
// Deploy factory + pool, sorted by address (token0 < token1).
ShadowPool memory sp = ShadowPoolDeployer.deploy(address(mockWeth), address(krk), sqrtPriceX96);
// ------------------------------------------------------------------
// KrAIken system deployment (follows DeployLocal.sol pattern)
//
// 1. Deploy OptimizerV3Push3 (no proxy only exposes isBullMarket(),
// causing LM to fall back to safe bear-mode defaults via try/catch).
// 2. Deploy LiquidityManager pointing at the shadow pool.
// 3. Wire BacktestKraiken.setLiquidityManager(lm).
// 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);
// 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.
StrategyExecutor executor =
new StrategyExecutor(sys.lm, IERC20(address(mockWeth)), IERC20(address(krk)), sender, recenterInterval);
sys.lm.setRecenterAccess(address(executor));
vm.stopBroadcast();
// Instantiate EventReplayer outside the broadcast block: it uses Foundry cheat codes
// (vm.readLine, vm.roll, vm.warp) that only work in the forge simulation context and
// must not be sent as real transactions to the RPC endpoint.
// ------------------------------------------------------------------
// EventReplayer is instantiated outside the broadcast block because
// it uses Foundry cheat codes (vm.readLine, vm.roll, vm.warp) that
// only work in the forge simulation context and must not be sent as
// real transactions.
//
// sp.token0 / sp.token1 are cast to MockToken BacktestKraiken
// inherits MockToken so the cast is valid; its mint(address,uint256)
// selector matches, enabling EventReplayer's mint callback.
// ------------------------------------------------------------------
EventReplayer replayer = new EventReplayer(sp.pool, MockToken(sp.token0), MockToken(sp.token1));
// Query pool state (view calls, no broadcast needed).
@ -111,11 +180,21 @@ contract BacktestRunner is Script {
console2.log("sqrtPriceX96: ", uint256(slot0SqrtPrice));
console2.log("Initial tick: ", int256(tick));
console2.log("Liquidity: ", uint256(liquidity));
console2.log("\n=== KrAIken System ===");
console2.log("BacktestKraiken:", address(krk));
console2.log("Optimizer: ", address(sys.optimizer));
console2.log("LiquidityMgr: ", address(sys.lm));
console2.log("StrategyExec: ", address(executor));
console2.log("Recenter intv: ", recenterInterval, " blocks");
console2.log("Initial capital:", initialCapital, " (mock WETH wei)");
console2.log("token0isWeth: ", sp.token0 == address(mockWeth));
// -----------------------------------------------------------------------
// Event replay (runs as local simulation no broadcast required).
// Each pool.mint / pool.swap / pool.burn call executes against the shadow
// pool; vm.roll + vm.warp advance block state to match historical timing.
// After each block advancement, StrategyExecutor.maybeRecenter() is called
// to trigger LiquidityManager.recenter() at the configured interval.
// -----------------------------------------------------------------------
try vm.envString("BACKTEST_EVENTS_FILE") returns (string memory eventsFile) {
if (bytes(eventsFile).length > 0) {
@ -137,7 +216,10 @@ contract BacktestRunner is Script {
console2.log("\n=== Starting Event Replay ===");
console2.log("Events file: ", eventsFile);
console2.log("Total events: ", totalEvents);
replayer.replay(eventsFile, totalEvents);
replayer.replay(eventsFile, totalEvents, executor);
// Print final KrAIken strategy summary.
executor.logSummary();
}
} catch {}
}

View file

@ -8,6 +8,7 @@ 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";
/**
* @title EventReplayer
@ -92,6 +93,27 @@ contract EventReplayer is IUniswapV3MintCallback, IUniswapV3SwapCallback {
* Pass 0 to omit the denominator from progress logs.
*/
function replay(string memory eventsFile, uint256 totalEvents) external {
_replayWithStrategy(eventsFile, totalEvents, StrategyExecutor(address(0)));
}
/**
* @notice Replay events and trigger KrAIken recenter logic after each block.
* @param eventsFile Path to the .jsonl events cache.
* @param totalEvents Total event count (for progress logs).
* @param strategyExecutor StrategyExecutor to call after every block advancement.
* Pass address(0) to disable strategy integration.
*/
function replay(string memory eventsFile, uint256 totalEvents, StrategyExecutor strategyExecutor) external {
_replayWithStrategy(eventsFile, totalEvents, strategyExecutor);
}
function _replayWithStrategy(
string memory eventsFile,
uint256 totalEvents,
StrategyExecutor strategyExecutor
)
internal
{
uint256 idx = 0;
// Track the last Swap event's expected state for drift measurement.
@ -113,6 +135,12 @@ contract EventReplayer is IUniswapV3MintCallback, IUniswapV3SwapCallback {
_advanceChain(blockNum);
// After advancing chain state, give the strategy a chance to recenter.
// The call is best-effort: any revert is caught inside StrategyExecutor.
if (address(strategyExecutor) != address(0)) {
strategyExecutor.maybeRecenter(blockNum);
}
if (_streq(eventName, "Swap")) {
(int24 expTick, uint160 expSqrtPrice) = _replaySwap(line);
// Update reference state only when the swap was not skipped.

View file

@ -0,0 +1,100 @@
// 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";
/**
* @notice Deployment result for the KrAIken system on the shadow pool.
*/
struct KrAIkenSystem {
BacktestKraiken kraiken;
/// @dev OptimizerV3Push3 is used as the optimizer address. It does not implement
/// getLiquidityParams(), so LiquidityManager's try/catch falls back to safe
/// bear-mode defaults on every recenter. This is intentional for backtesting.
OptimizerV3Push3 optimizer;
LiquidityManager lm;
}
/**
* @title KrAIkenDeployer
* @notice Library that deploys the KrAIken protocol contracts on top of a shadow pool
* for backtesting purposes.
*
* Deployment order follows DeployLocal.sol:
* 1. Deploy OptimizerV3Push3 (no proxy it only exposes isBullMarket(), so LM falls
* back to bear-mode defaults via try/catch on every recenter call).
* 2. Deploy LiquidityManager pointing at the shadow factory + mock WETH + BacktestKraiken.
* 3. Wire BacktestKraiken LM (setLiquidityManager).
* 4. Set fee destination on LM.
* 5. Fund LM with mock WETH (initial capital).
*
* @dev All functions are `internal` so they are inlined into BacktestRunner.s.sol,
* keeping msg.sender consistent with the broadcaster throughout deployment.
*/
library KrAIkenDeployer {
uint256 internal constant DEFAULT_INITIAL_CAPITAL = 10 ether;
/**
* @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)
{
return deploy(shadowFactory, mockWeth, krkToken, feeDestination, DEFAULT_INITIAL_CAPITAL);
}
/**
* @notice Deploy the KrAIken system with configurable initial capital.
*
* @param shadowFactory Factory that created the shadow pool.
* @param mockWeth Mock WETH token address (18-decimal ERC-20, freely mintable).
* @param krkToken BacktestKraiken token address (setLiquidityManager not yet called).
* @param feeDestination Address that will receive LP fees; also used as the caller
* for subsequent setRecenterAccess() calls.
* @param initialCapital Mock WETH minted to LM so recenter() has capital to deploy.
*/
function deploy(
address shadowFactory,
address mockWeth,
address krkToken,
address feeDestination,
uint256 initialCapital
)
internal
returns (KrAIkenSystem memory sys)
{
// 1. Deploy OptimizerV3Push3.
// LiquidityManager wraps getLiquidityParams() in a try/catch and falls back to
// safe bear-mode defaults when the call reverts. Since OptimizerV3Push3 only
// exposes isBullMarket(), every recenter uses bear defaults conservative and
// correct for a baseline backtest.
OptimizerV3Push3 optimizer = new OptimizerV3Push3();
// 2. Deploy LiquidityManager. It computes the pool address from factory + WETH +
// KRK + FEE (10 000). The shadow pool must have been created with the same
// factory and the same fee tier (ShadowPoolDeployer.SHADOW_FEE == 10 000).
LiquidityManager lm = new LiquidityManager(shadowFactory, mockWeth, krkToken, address(optimizer));
// 3. Wire BacktestKraiken LM so the restricted mint/burn functions work.
BacktestKraiken(krkToken).setLiquidityManager(address(lm));
// 4. Set fee destination (required before setRecenterAccess can be called).
lm.setFeeDestination(feeDestination);
// 5. Fund LM with mock WETH. recenter() uses _getEthBalance() which reads
// weth.balanceOf(address(this)). Pre-funding avoids calling weth.deposit()
// (which MockToken does not implement).
MockToken(mockWeth).mint(address(lm), initialCapital);
sys = KrAIkenSystem({ kraiken: BacktestKraiken(krkToken), optimizer: optimizer, lm: lm });
}
}

View file

@ -0,0 +1,291 @@
// 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";
/**
* @title StrategyExecutor
* @notice Drives the KrAIken recenter loop during event replay.
*
* Called by EventReplayer after every block advancement. Triggers
* LiquidityManager.recenter() once per `recenterInterval` blocks and logs:
* - Block number and timestamp
* - Pre/post positions (tickLower, tickUpper, liquidity) for Floor, Anchor, Discovery
* - Fees collected (WETH + KRK) as the delta of feeDestination's balances
* - Revert reason on failure (logged and skipped replay never halts)
*
* 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).
*
* TODO(#319): The negligible-impact assumption means we replay historical events
* as-is without accounting for KrAIken's own liquidity affecting swap outcomes.
* A future enhancement should add a feedback loop that adjusts replayed swap
* amounts based on KrAIken's active tick ranges.
*/
contract StrategyExecutor {
// -------------------------------------------------------------------------
// Configuration (immutable)
// -------------------------------------------------------------------------
LiquidityManager public immutable lm;
IERC20 public immutable wethToken;
IERC20 public immutable krkToken;
address public immutable feeDestination;
/// @notice Minimum block gap between recenter attempts.
uint256 public immutable recenterInterval;
// -------------------------------------------------------------------------
// Runtime state
// -------------------------------------------------------------------------
uint256 public lastRecenterBlock;
uint256 public totalRecenters;
uint256 public failedRecenters;
// -------------------------------------------------------------------------
// Constructor
// -------------------------------------------------------------------------
constructor(
LiquidityManager _lm,
IERC20 _wethToken,
IERC20 _krkToken,
address _feeDestination,
uint256 _recenterInterval
) {
lm = _lm;
wethToken = _wethToken;
krkToken = _krkToken;
feeDestination = _feeDestination;
recenterInterval = _recenterInterval;
}
// -------------------------------------------------------------------------
// Main entry point (called by EventReplayer after each block advancement)
// -------------------------------------------------------------------------
/**
* @notice Attempt a recenter if enough blocks have elapsed since the last one.
* @param blockNum Current block number (as advanced by EventReplayer via vm.roll).
*/
function maybeRecenter(uint256 blockNum) external {
if (blockNum - lastRecenterBlock < recenterInterval) return;
// Snapshot pre-recenter positions.
(uint128 fLiqPre, int24 fLoPre, int24 fHiPre) = lm.positions(ThreePositionStrategy.Stage.FLOOR);
(uint128 aLiqPre, int24 aLoPre, int24 aHiPre) = lm.positions(ThreePositionStrategy.Stage.ANCHOR);
(uint128 dLiqPre, int24 dLoPre, int24 dHiPre) = lm.positions(ThreePositionStrategy.Stage.DISCOVERY);
// Snapshot fee destination balances to compute fees collected during this recenter.
uint256 wethPre = wethToken.balanceOf(feeDestination);
uint256 krkPre = krkToken.balanceOf(feeDestination);
// Always advance lastRecenterBlock, even on failure, to avoid hammering a
// persistently failing condition on every subsequent block.
lastRecenterBlock = blockNum;
bool success;
string memory failReason;
try lm.recenter() {
success = true;
totalRecenters++;
} catch Error(string memory reason) {
failReason = reason;
failedRecenters++;
} catch {
failReason = "unknown revert";
failedRecenters++;
}
if (!success) {
console2.log(
string.concat("[recenter SKIP @ block ", _str(blockNum), "] reason: ", failReason)
);
return;
}
// Snapshot post-recenter positions.
(uint128 fLiqPost, int24 fLoPost, int24 fHiPost) = lm.positions(ThreePositionStrategy.Stage.FLOOR);
(uint128 aLiqPost, int24 aLoPost, int24 aHiPost) = lm.positions(ThreePositionStrategy.Stage.ANCHOR);
(uint128 dLiqPost, int24 dLoPost, int24 dHiPost) = lm.positions(ThreePositionStrategy.Stage.DISCOVERY);
uint256 feesWeth = wethToken.balanceOf(feeDestination) - wethPre;
uint256 feesKrk = krkToken.balanceOf(feeDestination) - krkPre;
_logRecenter(
blockNum,
fLiqPre, fLoPre, fHiPre,
aLiqPre, aLoPre, aHiPre,
dLiqPre, dLoPre, dHiPre,
fLiqPost, fLoPost, fHiPost,
aLiqPost, aLoPost, aHiPost,
dLiqPost, dLoPost, dHiPost,
feesWeth,
feesKrk
);
}
// -------------------------------------------------------------------------
// Summary
// -------------------------------------------------------------------------
/**
* @notice Print a summary of all recenter activity. Call at end of replay.
*/
function logSummary() external view {
(uint128 fLiq, int24 fLo, int24 fHi) = lm.positions(ThreePositionStrategy.Stage.FLOOR);
(uint128 aLiq, int24 aLo, int24 aHi) = lm.positions(ThreePositionStrategy.Stage.ANCHOR);
(uint128 dLiq, int24 dLo, int24 dHi) = lm.positions(ThreePositionStrategy.Stage.DISCOVERY);
console2.log("=== KrAIken Strategy Summary ===");
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))
)
);
}
// -------------------------------------------------------------------------
// Internal helpers
// -------------------------------------------------------------------------
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,
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)));
}
}