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:
commit
19df05843d
5 changed files with 611 additions and 16 deletions
94
onchain/script/backtesting/BacktestKraiken.sol
Normal file
94
onchain/script/backtesting/BacktestKraiken.sol
Normal 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));
|
||||
}
|
||||
}
|
||||
|
|
@ -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 {}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
100
onchain/script/backtesting/KrAIkenDeployer.sol
Normal file
100
onchain/script/backtesting/KrAIkenDeployer.sol
Normal 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 });
|
||||
}
|
||||
}
|
||||
291
onchain/script/backtesting/StrategyExecutor.sol
Normal file
291
onchain/script/backtesting/StrategyExecutor.sol
Normal 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)));
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue