fix: Address review findings from PR #575

- Fix unsafe int32 intermediate cast: int56(int32(elapsed)) → int56(uint56(elapsed))
  to prevent TWAP tick sign inversion for intervals above int32 max (~68 years)
- Remove redundant lastRecenterTimestamp state variable; capture prevTimestamp
  from existing lastRecenterTime instead (saves ~20k gas per recenter)
- Use pool.increaseObservationCardinalityNext(ORACLE_CARDINALITY) in constructor
  instead of recomputing the pool address; extract magic 100 to named constant
- Add TWAPFallback(uint32 elapsed) event emitted when pool.observe() reverts
  so monitoring can distinguish degraded operation from normal bootstrap
- Remove conditional bypass paths in test_twapReflectsAveragePriceNotJustLastSwap;
  assert vwapAfter > 0 and vwapAfter > initialPriceX96 unconditionally

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
openhands 2026-03-11 10:02:47 +00:00
parent 83e43386bc
commit f72f99aefa
2 changed files with 30 additions and 50 deletions

View file

@ -222,7 +222,7 @@ contract VWAPFloorProtectionTest is UniSwapHelper {
* not merely the last-swap anchor midpoint.
*
* Sequence:
* 1. First recenter positions set, no fees (lastRecenterTimestamp = t0).
* 1. First recenter positions set, no fees (lastRecenterTime = t0).
* 2. Warp 100 s buy KRK: price moves UP, observation written at t0+100.
* 3. Warp 100 s buy KRK again: price moves further UP, observation at t0+200.
* 4. Warp 100 s bootstrap recenter (cumulativeVolume==0 always records).
@ -240,8 +240,9 @@ contract VWAPFloorProtectionTest is UniSwapHelper {
// we track elapsed time with a local variable.
// Step 1: initial recenter places positions at the pool's current price.
// No fees yet; lastRecenterTimestamp is set to block.timestamp.
// No fees yet; lastRecenterTime is set to block.timestamp.
(, int24 initialTick,,,,,) = pool.slot0();
assertFalse(token0isWeth, "test assumes token0isWeth=false");
vm.prank(RECENTER_CALLER);
lm.recenter();
@ -261,10 +262,7 @@ contract VWAPFloorProtectionTest is UniSwapHelper {
// Capture the current (elevated) tick after two rounds of buying.
(, int24 elevatedTick,,,,,) = pool.slot0();
// The price must have risen sanity check for !token0isWeth ordering.
// For !token0isWeth: buying KRK increases the tick (KRK price in WETH rises).
assertFalse(token0isWeth, "test assumes token0isWeth=false");
assertGt(elevatedTick, initialTick, "price must have risen after buys");
// Step 4: advance 100 s then do the bootstrap recenter.
@ -272,44 +270,22 @@ contract VWAPFloorProtectionTest is UniSwapHelper {
// elapsed = 300 s pool.observe([300, 0]) TWAP tick avg of three 100-s periods.
t += 100;
vm.warp(t); // t = 301
uint256 vwapBefore = lm.getVWAP();
vm.prank(RECENTER_CALLER);
try lm.recenter() {
uint256 vwapAfter = lm.getVWAP();
lm.recenter();
// If fees were collected, VWAP was updated.
if (vwapAfter > 0 && vwapAfter != vwapBefore) {
// TWAP over the 300-s window reflects higher prices than the initial anchor tick.
// The initial anchor was placed at `initialTick` (before any buys).
// TWAP tick (initialTick·100 + midTick·100 + elevatedTick·100) / 300 > initialTick.
// Correspondingly, priceX96(TWAP) > priceX96(initialTick).
//
// Compute a reference: the price at the initial anchor tick.
// For !token0isWeth, _priceAtTick uses the tick directly (no negation).
// We approximate it via TickMath: sqrtRatio² >> 96.
uint160 sqrtAtInitial = uint160(uint256(TickMath.getSqrtRatioAtTick(initialTick)));
uint256 initialPriceX96 = uint256(sqrtAtInitial) * uint256(sqrtAtInitial) >> 96;
// TWAP over the 300-s window reflects higher prices than the initial anchor tick.
// TWAP tick (initialTick·100 + midTick·100 + elevatedTick·100) / 300 > initialTick.
// Correspondingly, priceX96(TWAP) > priceX96(initialTick).
//
// Compute a reference: the price at the initial anchor tick.
// For !token0isWeth, _priceAtTick uses the tick directly (no negation).
// We approximate it via TickMath: sqrtRatio² >> 96.
uint256 vwapAfter = lm.getVWAP();
assertGt(vwapAfter, 0, "VWAP must be bootstrapped after fees from two large buys");
assertGt(
vwapAfter,
initialPriceX96,
"TWAP VWAP must exceed initial-anchor-midpoint price"
);
} else if (lm.cumulativeVolume() == 0) {
// No ETH fees collected: ethFee == 0 so _recordVolumeAndPrice was skipped.
// This can happen when feeDestination receives all fees before recording.
// Accept the result as long as VWAP is still 0 (nothing recorded yet).
assertEq(vwapAfter, 0, "VWAP still zero when no ETH fees collected");
}
} catch (bytes memory reason) {
// Only "amplitude not reached" is an acceptable failure it means the second
// recenter couldn't detect sufficient price movement relative to the first one.
assertEq(
keccak256(reason),
keccak256(abi.encodeWithSignature("Error(string)", "amplitude not reached.")),
"unexpected revert in bootstrap recenter"
);
}
uint160 sqrtAtInitial = uint160(uint256(TickMath.getSqrtRatioAtTick(initialTick)));
uint256 initialPriceX96 = uint256(sqrtAtInitial) * uint256(sqrtAtInitial) >> 96;
assertGt(vwapAfter, initialPriceX96, "TWAP VWAP must exceed initial-anchor-midpoint price");
}
// =========================================================================