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:
parent
a075f8bd61
commit
0dd764b8b3
3 changed files with 12 additions and 129 deletions
|
|
@ -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
|
||||
// =========================================================================
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue