harb/onchain/src/abstracts/PriceOracle.sol
johba a76d3937dd fix: bundled dust cleanup — onchain source quality (#1134)
- Fix misleading taxRate comment in AttackRunner.s.sol (index into TAX_RATES[], not raw rate)
- Clarify _validatePriceMovement NatSpec return doc in PriceOracle.sol
- Remove redundant double-cast uint256(uint256(...)) in OptimizerV3Push3Lib.sol
- Add Basescan URL source comments for SWAP_ROUTER and WETH addresses

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 09:51:46 +00:00

78 lines
4.1 KiB
Solidity

// SPDX-License-Identifier: GPL-3.0-or-later
pragma solidity ^0.8.19;
import "@openzeppelin/utils/math/SignedMath.sol";
import "@uniswap-v3-core/interfaces/IUniswapV3Pool.sol";
/**
* @title PriceOracle
* @notice Abstract contract providing price stability validation using Uniswap V3 TWAP oracle
* @dev Contains oracle-related functionality for validating price movements and stability
*/
abstract contract PriceOracle {
/// @notice Interval for price stability checks (30 seconds)
/// @dev Reduced from 300s (5 min) to 30s to align with MIN_RECENTER_INTERVAL (60s).
/// The old 300s window caused recenter to revert with "price deviated from oracle"
/// for ~285s after any significant trade (> ~1000-tick move), creating a window
/// where the LiquidityManager could not reposition and an adversary with parasitic
/// LP could extract value from passive holders (issue #517).
/// 30s still prevents same-block manipulation on Ethereum mainnet (~12s block time)
/// while ensuring TWAP converges well within the 60s cooldown period.
uint32 internal constant PRICE_STABILITY_INTERVAL = 30;
/// @notice Maximum allowed tick deviation from TWAP average
int24 internal constant MAX_TICK_DEVIATION = 50;
/// @notice The Uniswap V3 pool used for oracle data
function _getPool() internal view virtual returns (IUniswapV3Pool);
/// @notice Validates if the current price is stable compared to TWAP oracle
/// @param currentTick The current tick to validate
/// @return isStable True if price is within acceptable deviation from TWAP
function _isPriceStable(int24 currentTick) internal view returns (bool isStable) {
IUniswapV3Pool pool = _getPool();
uint32[] memory secondsAgo = new uint32[](2);
secondsAgo[0] = PRICE_STABILITY_INTERVAL; // 30 seconds ago
secondsAgo[1] = 0; // current block timestamp
int24 averageTick;
try pool.observe(secondsAgo) returns (int56[] memory tickCumulatives, uint160[] memory) {
int56 tickCumulativeDiff = tickCumulatives[1] - tickCumulatives[0];
averageTick = int24(tickCumulativeDiff / int56(int32(PRICE_STABILITY_INTERVAL)));
} catch {
// Fallback to longer timeframe if recent data unavailable
uint32 fallbackInterval = PRICE_STABILITY_INTERVAL * 200; // 6,000 seconds
secondsAgo[0] = fallbackInterval;
try pool.observe(secondsAgo) returns (int56[] memory fallbackCumulatives, uint160[] memory) {
int56 tickCumulativeDiff = fallbackCumulatives[1] - fallbackCumulatives[0];
averageTick = int24(tickCumulativeDiff / int56(int32(fallbackInterval)));
} catch {
// Pool has insufficient observation history for both intervals.
// Treat price as unstable (safe default) — prevents recenter() from
// reverting with an opaque Uniswap error on pools with very short history.
return false;
}
}
isStable = (currentTick >= averageTick - MAX_TICK_DEVIATION && currentTick <= averageTick + MAX_TICK_DEVIATION);
}
/// @notice Validates if price movement is sufficient for recentering
/// @param currentTick The current market tick
/// @param centerTick The center tick of the anchor position
/// @param tickSpacing The tick spacing for minimum amplitude calculation
/// @param token0isWeth Whether token0 is WETH (affects price direction logic)
/// @return isUp True if ETH price moved up (WETH appreciating vs KRK); accounts for token0isWeth ordering
/// @return isEnough True if movement amplitude is sufficient for recentering
function _validatePriceMovement(int24 currentTick, int24 centerTick, int24 tickSpacing, bool token0isWeth)
internal
pure
returns (bool isUp, bool isEnough)
{
uint256 minAmplitude = uint24(tickSpacing) * 2;
// Determine the correct comparison direction based on token0isWeth
isUp = token0isWeth ? currentTick < centerTick : currentTick > centerTick;
isEnough = SignedMath.abs(currentTick - centerTick) > minAmplitude;
}
}