292 lines
10 KiB
Solidity
292 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)));
|
||
|
|
}
|
||
|
|
}
|