fix: fix: Restore proper VWAP — gas-efficient volume-weighted pricing (revert TWAP) (#603)

- Replace pool.observe() TWAP price source with current pool tick (pool.slot0()) sampled once per recenter
- Remove _getTWAPOrFallback() and TWAPFallback event (added by PR #575)
- _scrapePositions now takes int24 currentTick instead of uint256 prevTimestamp; price computed via _priceAtTick before the burn loop
- Volume weighting (ethFee * 100) is unchanged — fees proxy swap volume over the recenter interval
- Direction fix from #566 (shouldRecordVWAP only on sell events) is preserved
- Remove test_twapReflectsAveragePriceNotJustLastSwap (tested reverted TWAP behaviour)
- ORACLE_CARDINALITY / increaseObservationCardinalityNext retained for _isPriceStable()
- All 188 tests pass

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
openhands 2026-03-12 08:50:07 +00:00
parent a075f8bd61
commit 0dd764b8b3
3 changed files with 12 additions and 129 deletions

View file

@ -19,7 +19,6 @@ import { LiquidityManager } from "../src/LiquidityManager.sol";
import { ThreePositionStrategy } from "../src/abstracts/ThreePositionStrategy.sol";
import { TestEnvironment } from "./helpers/TestBase.sol";
import { UniSwapHelper } from "./helpers/UniswapTestBase.sol";
import "@aperture/uni-v3-lib/TickMath.sol";
contract VWAPFloorProtectionTest is UniSwapHelper {
address constant RECENTER_CALLER = address(0x7777);
@ -213,81 +212,6 @@ contract VWAPFloorProtectionTest is UniSwapHelper {
}
}
// =========================================================================
// TWAP: price reflects average across interval, not just last swap
// =========================================================================
/**
* @notice TWAP oracle gives an average price over the recenter interval,
* not merely the last-swap anchor midpoint.
*
* Sequence:
* 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).
* elapsed = 300 s; pool.observe([300,0]) gives TWAP over the full interval.
*
* The TWAP covers 100 s at initial price + 100 s at P_mid + 100 s at P_high.
* The old anchor-midpoint approach would record only the initial anchor tick
* (placed during step 1 before any buys occurred).
* Therefore TWAP-based VWAP > initial-anchor-midpoint VWAP because it accounts
* for the price appreciation during the interval.
*/
function test_twapReflectsAveragePriceNotJustLastSwap() public {
// Note: in Foundry, `block.timestamp` within the test function always returns the
// value at test-function entry (1). vm.warp() takes effect for external calls, so
// we track elapsed time with a local variable.
// Step 1: initial recenter places positions at the pool's current price.
// 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();
assertEq(lm.cumulativeVolume(), 0, "no fees before first buy");
uint256 t = 1; // track warped time independently
// Step 2: advance 100 s, then buy (price rises; observation written for prior period).
t += 100;
vm.warp(t); // t = 101
buyRaw(25 ether);
// Step 3: advance another 100 s, buy again (price rises further).
t += 100;
vm.warp(t); // t = 201
buyRaw(25 ether);
// Capture the current (elevated) tick after two rounds of buying.
(, int24 elevatedTick,,,,,) = pool.slot0();
// For !token0isWeth: buying KRK increases the tick (KRK price in WETH rises).
assertGt(elevatedTick, initialTick, "price must have risen after buys");
// Step 4: advance 100 s then do the bootstrap recenter.
// cumulativeVolume == 0, so shouldRecordVWAP = true regardless of direction.
// elapsed = 300 s pool.observe([300, 0]) TWAP tick avg of three 100-s periods.
t += 100;
vm.warp(t); // t = 301
vm.prank(RECENTER_CALLER);
lm.recenter();
// 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");
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");
}
// =========================================================================
// getLiquidityManager override for UniSwapHelper boundary helpers
// =========================================================================