harb/onchain/src/VWAPTracker.sol
openhands 0dd764b8b3 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>
2026-03-12 08:50:07 +00:00

108 lines
4.8 KiB
Solidity

// SPDX-License-Identifier: GPL-3.0-or-later
pragma solidity ^0.8.19;
import "@openzeppelin/utils/math/Math.sol";
/**
* @title VWAPTracker
* @notice Abstract contract for tracking Volume Weighted Average Price (VWAP) data
* @dev Provides VWAP calculation and storage functionality that can be inherited by other contracts.
* Price inputs are sourced from the current pool tick (pool.slot0()) at the time of each
* recenter, giving volume-weighted accuracy without per-swap gas overhead.
* The LiquidityManager feeds _recordVolumeAndPrice(currentPriceX96, ethFee) at each recenter.
*
* Key features:
* - Volume-weighted average with data compression (max 1000x compression)
* - Prevents dormant whale manipulation through historical price memory
* - Stores price² (squared price) in X96 format for VWAP calculation
* - Automatic overflow protection by compressing historic data when needed
* - Price source: current pool tick snapshot at recenter time (not TWAP, not anchor midpoint)
*/
abstract contract VWAPTracker {
using Math for uint256;
uint256 public cumulativeVolumeWeightedPriceX96;
uint256 public cumulativeVolume;
/**
* @notice Records volume and price data for VWAP calculation
* @param currentPriceX96 The current price in Q96 format (price * 2^96, from _priceAtTick)
* @param fee The fee amount used to calculate volume
* @dev Assumes fee represents 1% of volume, handles overflow by compressing historic data
*/
function _recordVolumeAndPrice(uint256 currentPriceX96, uint256 fee) internal {
// assuming FEE is 1%
uint256 volume = fee * 100;
uint256 volumeWeightedPriceX96 = currentPriceX96 * volume;
// ULTRA-RARE EDGE CASE: Check if the new data itself would overflow even before adding
// This can only happen with impossibly large transactions (>10,000 ETH + $billion token prices)
if (volumeWeightedPriceX96 > type(uint256).max / 2) {
// If a single transaction is this large, cap it to prevent system failure
// This preserves system functionality while limiting the impact of the extreme transaction
volumeWeightedPriceX96 = type(uint256).max / 2;
volume = volumeWeightedPriceX96 / currentPriceX96;
}
// Check for potential overflow. 10**70 is close to 2^256
if (cumulativeVolumeWeightedPriceX96 > 10 ** 70) {
// CRITICAL: Preserve historical significance for dormant whale protection
// Find the MINIMUM compression factor needed to prevent overflow
uint256 maxSafeValue = type(uint256).max / 10 ** 6; // Leave substantial room for future data
uint256 compressionFactor = (cumulativeVolumeWeightedPriceX96 / maxSafeValue) + 1;
// Cap maximum compression to preserve historical "eternal memory"
// Even in extreme cases, historical data should retain significant weight
if (compressionFactor > 1000) {
compressionFactor = 1000; // Maximum 1000x compression to preserve history
}
// Ensure minimum compression effectiveness
if (compressionFactor < 2) {
compressionFactor = 2; // At least 2x compression when triggered
}
// Compress both values by the same minimal factor
cumulativeVolumeWeightedPriceX96 = cumulativeVolumeWeightedPriceX96 / compressionFactor;
cumulativeVolume = cumulativeVolume / compressionFactor;
}
cumulativeVolumeWeightedPriceX96 += volumeWeightedPriceX96;
cumulativeVolume += volume;
}
/**
* @notice Calculates the current VWAP
* @return vwapX96 The volume weighted average price in X96 format
* @dev Returns 0 if no volume has been recorded
*/
function getVWAP() public view returns (uint256 vwapX96) {
if (cumulativeVolume > 0) {
vwapX96 = cumulativeVolumeWeightedPriceX96 / cumulativeVolume;
} else {
vwapX96 = 0;
}
}
/**
* @notice Calculates adjusted VWAP with capital inefficiency factor
* @param capitalInefficiency The capital inefficiency factor (scaled by 10^18)
* @return adjustedVwapX96 The adjusted VWAP in X96 format
* @dev Applies a 70% base weight plus capital inefficiency adjustment
*/
function getAdjustedVWAP(uint256 capitalInefficiency) public view returns (uint256 adjustedVwapX96) {
uint256 vwapX96 = getVWAP();
if (vwapX96 > 0) {
adjustedVwapX96 = (7 * vwapX96 / 10) + (vwapX96 * capitalInefficiency / 10 ** 18);
} else {
adjustedVwapX96 = 0;
}
}
/**
* @notice Resets VWAP tracking data
* @dev Can be called by inheriting contracts to reset tracking
*/
function _resetVWAP() internal {
cumulativeVolumeWeightedPriceX96 = 0;
cumulativeVolume = 0;
}
}