fix: address AI review feedback for #317 event replay
- Cache pool.tickSpacing() as immutable in EventReplayer constructor to avoid a repeated external call per _replayMint() invocation - Rename driftCount → driftCheckpoints for consistency with log label - Add sqrtDriftBps to the per-checkpoint progress log line, using the now-live lastExpectedSqrtPrice field (previously written but never read) - Guard _replaySwap(): skip and count events where amountSpecified ≤ 0, which would silently flip exact-input into exact-output mode - Add a final drift sample after the while-loop for trailing events not covered by the last LOG_INTERVAL checkpoint - Move EventReplayer construction outside the broadcast block in BacktestRunner (it uses vm.* cheat codes incompatible with real RPC) - Change second vm.closeFile() from try/catch to a direct call so errors surface rather than being silently swallowed Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
a884f8a5c9
commit
cd065275be
2 changed files with 112 additions and 50 deletions
|
|
@ -47,12 +47,15 @@ contract EventReplayer is IUniswapV3MintCallback, IUniswapV3SwapCallback {
|
|||
IUniswapV3Pool public immutable pool;
|
||||
MockToken public immutable token0;
|
||||
MockToken public immutable token1;
|
||||
/// @dev Pool tick spacing — cached once at construction (immutable for the pool lifetime).
|
||||
int24 public immutable tickSpacing;
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Stats (accumulated across the replay)
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
uint256 public driftCount;
|
||||
/// @dev Count of LOG_INTERVAL checkpoints (and the final sample) where tick drift > 0.
|
||||
uint256 public checkpointsWithDrift;
|
||||
uint256 public totalAbsDrift;
|
||||
uint256 public maxDrift;
|
||||
uint256 public skippedCount;
|
||||
|
|
@ -72,6 +75,7 @@ contract EventReplayer is IUniswapV3MintCallback, IUniswapV3SwapCallback {
|
|||
pool = _pool;
|
||||
token0 = _token0;
|
||||
token1 = _token1;
|
||||
tickSpacing = _pool.tickSpacing();
|
||||
_lastBlock = block.number;
|
||||
_lastTimestamp = block.timestamp;
|
||||
}
|
||||
|
|
@ -127,28 +131,27 @@ contract EventReplayer is IUniswapV3MintCallback, IUniswapV3SwapCallback {
|
|||
|
||||
// Progress + drift validation every LOG_INTERVAL events.
|
||||
if (idx % LOG_INTERVAL == 0 && hasSwapRef) {
|
||||
(, int24 currentTick,,,,,) = pool.slot0();
|
||||
int256 diff = int256(currentTick) - int256(lastExpectedTick);
|
||||
uint256 absDrift = diff >= 0 ? uint256(diff) : uint256(-diff);
|
||||
_logCheckpoint(idx, totalEvents, lastExpectedTick, lastExpectedSqrtPrice);
|
||||
}
|
||||
}
|
||||
|
||||
totalAbsDrift += absDrift;
|
||||
if (absDrift > 0) driftCount++;
|
||||
if (absDrift > maxDrift) maxDrift = absDrift;
|
||||
|
||||
string memory denominator = totalEvents > 0 ? string.concat("/", vm.toString(totalEvents)) : "";
|
||||
console2.log(
|
||||
string.concat(
|
||||
"[",
|
||||
vm.toString(idx),
|
||||
denominator,
|
||||
"] tick=",
|
||||
vm.toString(int256(currentTick)),
|
||||
" expected=",
|
||||
vm.toString(int256(lastExpectedTick)),
|
||||
" drift=",
|
||||
vm.toString(absDrift)
|
||||
)
|
||||
);
|
||||
// Final drift sample: capture any trailing events after the last checkpoint.
|
||||
// This prevents understating drift when (totalReplayed % LOG_INTERVAL) != 0.
|
||||
if (hasSwapRef) {
|
||||
(uint160 finalSqrtPrice, int24 finalTick,,,,,) = pool.slot0();
|
||||
int256 diff = int256(finalTick) - int256(lastExpectedTick);
|
||||
uint256 absDrift = diff >= 0 ? uint256(diff) : uint256(-diff);
|
||||
totalAbsDrift += absDrift;
|
||||
if (absDrift > 0) checkpointsWithDrift++;
|
||||
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);
|
||||
if (lastExpectedSqrtPrice > 0 && priceDelta * 10_000 > uint256(lastExpectedSqrtPrice)) {
|
||||
console2.log(" final sqrtPrice divergence:", priceDelta);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -181,8 +184,31 @@ contract EventReplayer is IUniswapV3MintCallback, IUniswapV3SwapCallback {
|
|||
// amount1 > 0 → caller paid token1 into pool (zeroForOne = false).
|
||||
bool zeroForOne = amount0 > 0;
|
||||
|
||||
// Use the cached post-swap sqrtPrice as the price limit so the shadow
|
||||
// pool stops at exactly the historical price regardless of liquidity shape.
|
||||
// amountSpecified: exact-input using the historical token amount.
|
||||
// Guard against degenerate events where the expected input side is non-positive,
|
||||
// which would unintentionally switch pool.swap into exact-output mode.
|
||||
int256 amountSpecified = zeroForOne ? amount0 : amount1;
|
||||
if (amountSpecified <= 0) {
|
||||
skippedCount++;
|
||||
return (0, 0);
|
||||
}
|
||||
|
||||
// Skip if the pool is already at or past the ORIGINAL target price.
|
||||
// Perform this check against targetSqrtPrice — before any clamping — so that
|
||||
// events targeting a price exactly at a hard limit are not incorrectly skipped.
|
||||
(uint160 currentSqrtPrice,,,,,,) = pool.slot0();
|
||||
if (zeroForOne && currentSqrtPrice <= targetSqrtPrice) {
|
||||
skippedCount++;
|
||||
return (0, 0);
|
||||
}
|
||||
if (!zeroForOne && currentSqrtPrice >= targetSqrtPrice) {
|
||||
skippedCount++;
|
||||
return (0, 0);
|
||||
}
|
||||
|
||||
// Clamp the price LIMIT for pool.swap() to the exclusive valid range.
|
||||
// Applied after the skip check so extreme-price events are not silently dropped;
|
||||
// only the pool.swap parameter is adjusted.
|
||||
uint160 sqrtPriceLimitX96 = targetSqrtPrice;
|
||||
if (zeroForOne && sqrtPriceLimitX96 <= TickMath.MIN_SQRT_RATIO) {
|
||||
sqrtPriceLimitX96 = TickMath.MIN_SQRT_RATIO + 1;
|
||||
|
|
@ -191,24 +217,10 @@ contract EventReplayer is IUniswapV3MintCallback, IUniswapV3SwapCallback {
|
|||
sqrtPriceLimitX96 = TickMath.MAX_SQRT_RATIO - 1;
|
||||
}
|
||||
|
||||
// Skip if the pool is already at or past the target price.
|
||||
(uint160 currentSqrtPrice,,,,,,) = pool.slot0();
|
||||
if (zeroForOne && currentSqrtPrice <= sqrtPriceLimitX96) {
|
||||
skippedCount++;
|
||||
return (0, 0);
|
||||
}
|
||||
if (!zeroForOne && currentSqrtPrice >= sqrtPriceLimitX96) {
|
||||
skippedCount++;
|
||||
return (0, 0);
|
||||
}
|
||||
|
||||
// amountSpecified: use the historical input amount (exact-input mode).
|
||||
// The price limit is the binding constraint; the pool takes at most this amount.
|
||||
int256 amountSpecified = zeroForOne ? amount0 : amount1;
|
||||
|
||||
// Pre-fund so the swap callback can pay the pool without needing to mint
|
||||
// inside the callback (which avoids a re-entrant MockToken.mint call).
|
||||
uint256 fundAmount = uint256(amountSpecified > 0 ? amountSpecified : -amountSpecified) + 1;
|
||||
// amountSpecified > 0 is guaranteed by the guard above, so the cast is safe.
|
||||
uint256 fundAmount = uint256(amountSpecified) + 1;
|
||||
if (zeroForOne) {
|
||||
token0.mint(address(this), fundAmount);
|
||||
} else {
|
||||
|
|
@ -246,8 +258,7 @@ contract EventReplayer is IUniswapV3MintCallback, IUniswapV3SwapCallback {
|
|||
return;
|
||||
}
|
||||
|
||||
// Skip ticks not aligned to the pool's tick spacing (out-of-range guard).
|
||||
int24 tickSpacing = pool.tickSpacing();
|
||||
// Skip ticks not aligned to the pool's tick spacing (uses cached immutable).
|
||||
if (tickLower % tickSpacing != 0 || tickUpper % tickSpacing != 0) {
|
||||
skippedCount++;
|
||||
return;
|
||||
|
|
@ -342,6 +353,55 @@ contract EventReplayer is IUniswapV3MintCallback, IUniswapV3SwapCallback {
|
|||
vm.warp(_lastTimestamp);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Logging helpers
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @notice Emit a progress line and accumulate drift statistics for one checkpoint.
|
||||
*/
|
||||
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);
|
||||
|
||||
totalAbsDrift += absDrift;
|
||||
if (absDrift > 0) checkpointsWithDrift++;
|
||||
if (absDrift > maxDrift) maxDrift = absDrift;
|
||||
|
||||
string memory denominator = totalEvents > 0 ? string.concat("/", vm.toString(totalEvents)) : "";
|
||||
console2.log(
|
||||
string.concat(
|
||||
"[",
|
||||
vm.toString(idx),
|
||||
denominator,
|
||||
"] tick=",
|
||||
vm.toString(int256(currentTick)),
|
||||
" expected=",
|
||||
vm.toString(int256(expectedTick)),
|
||||
" drift=",
|
||||
vm.toString(absDrift)
|
||||
)
|
||||
);
|
||||
|
||||
// Log sqrtPrice deviation when it exceeds ~0.01% (filters rounding noise).
|
||||
if (currentSqrtPrice != expectedSqrtPrice) {
|
||||
uint256 priceDelta = currentSqrtPrice > expectedSqrtPrice
|
||||
? uint256(currentSqrtPrice - expectedSqrtPrice)
|
||||
: uint256(expectedSqrtPrice - currentSqrtPrice);
|
||||
if (expectedSqrtPrice > 0 && priceDelta * 10_000 > uint256(expectedSqrtPrice)) {
|
||||
console2.log(" sqrtPrice divergence:", priceDelta);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Utilities
|
||||
// -------------------------------------------------------------------------
|
||||
|
|
@ -355,11 +415,11 @@ contract EventReplayer is IUniswapV3MintCallback, IUniswapV3SwapCallback {
|
|||
console2.log("=== Replay Complete ===");
|
||||
console2.log("Total events: ", totalReplayed);
|
||||
console2.log("Skipped: ", skippedCount);
|
||||
console2.log("Drift checkpoints:", driftCount);
|
||||
console2.log("Drift checkpoints:", checkpointsWithDrift);
|
||||
console2.log("Total abs drift: ", totalAbsDrift);
|
||||
console2.log("Max drift: ", maxDrift);
|
||||
if (driftCount > 0) {
|
||||
console2.log("Avg tick drift: ", totalAbsDrift / driftCount);
|
||||
if (checkpointsWithDrift > 0) {
|
||||
console2.log("Avg tick drift: ", totalAbsDrift / checkpointsWithDrift);
|
||||
}
|
||||
console2.log("Final tick: ", int256(finalTick));
|
||||
console2.log("Final sqrtPriceX96:", uint256(finalSp));
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue