harb/onchain/script/backtesting/StrategyExecutor.sol
openhands 84203294af fix: Backtesting #4: Deploy KrAIken contracts + recenter execution loop (#318)
- Add BacktestKraiken.sol: extends MockToken with Kraiken-compatible interface
  (dual mint overloads — public mint(address,uint256) for EventReplayer and
  restricted mint(uint256) for LiquidityManager; peripheryContracts() stubs
  staking pool as address(0))

- Add KrAIkenDeployer.sol: library deploying OptimizerV3Push3 + LiquidityManager
  on the shadow pool, wiring BacktestKraiken permissions, setting fee destination,
  and funding LM with configurable initial mock-WETH capital (default 10 ETH)

- Add StrategyExecutor.sol: time-based recenter trigger (configurable block
  interval, default 100 blocks); logs block, pre/post positions (Floor/Anchor/
  Discovery tick ranges + liquidity), fees collected, and revert reason on skip;
  negligible-impact assumption documented as TODO(#319)

- Modify EventReplayer.sol: add overloaded replay() accepting an optional
  StrategyExecutor hook; maybeRecenter() called after each block advancement
  without halting replay on failure

- Modify BacktestRunner.s.sol: replace tokenA/B with MockWETH + BacktestKraiken,
  integrate KrAIkenDeployer + StrategyExecutor into broadcast block; configurable
  via RECENTER_INTERVAL and INITIAL_CAPITAL_WETH env vars; executor.logSummary()
  printed after replay

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-27 09:00:22 +00:00

291 lines
10 KiB
Solidity

// 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)));
}
}