- 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>
108 lines
4.8 KiB
Solidity
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;
|
|
}
|
|
}
|