-
Notifications
You must be signed in to change notification settings - Fork 156
DexOracle: indirect AMPL/USDC 24h TWAP provider #311
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
ahnaguib
wants to merge
3
commits into
master
Choose a base branch
from
dex-oracle-2026
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,302 @@ | ||
| // SPDX-License-Identifier: GPL-3.0-or-later | ||
| pragma solidity 0.8.4; | ||
|
|
||
| import {Ownable} from "./_external/Ownable.sol"; | ||
| import {SafeMath} from "./_external/SafeMath.sol"; | ||
| import {IUniswapV2Pair} from "./_external/IUniswapV2Pair.sol"; | ||
| import {UniswapV2OracleLibrary} from "./_external/UniswapV2OracleLibrary.sol"; | ||
|
|
||
| interface IERC20Decimals { | ||
| function decimals() external view returns (uint8); | ||
| } | ||
|
|
||
| interface IMedianOracle { | ||
| function pushReport(uint256 payload) external; | ||
|
|
||
| function purgeReports() external; | ||
| } | ||
|
|
||
| /** | ||
| * @title DexOracle | ||
| * | ||
| * @notice Computes a 24h time-weighted average price (TWAP) for an asset pair | ||
| * that has no direct UniswapV2 market by chaining two underlying | ||
| * markets, and reports the result to a MedianOracle instance. | ||
| * | ||
| * For AMPL/USDC the price is bridged through WETH: | ||
| * | ||
| * AMPL/USDC = (AMPL/WETH) * (WETH/USDC) | ||
| * | ||
| * - leg1 prices the source asset (AMPL) in the bridge asset (WETH) | ||
| * - leg2 prices the bridge asset (WETH) in the quote asset (USDC) | ||
| * | ||
| * UniswapV2 maintains per-pair price accumulators as UQ112x112 fixed | ||
| * point numbers denominated in raw (smallest-unit) reserves. Following | ||
| * the canonical fixed-window oracle, each leg's cumulative is bridged | ||
| * into an OUTPUT_DECIMALS (18) fixed point decimal price-seconds value | ||
| * and stored at `update()`; the TWAP is the difference of two such | ||
| * snapshots divided by the elapsed time: | ||
| * | ||
| * cumulative_18 = (cumulativeUQ112x112 * decimalsFactor) >> 112 | ||
| * decimalsFactor = 10**(OUTPUT_DECIMALS + baseDecimals - quoteDecimals) | ||
| * legPrice_18 = (cumulative_18_now - cumulative_18_last) / timeElapsed | ||
| * | ||
| * Intended 24h rebase cadence: | ||
| * - `update()` is called right after rebase (appended to the | ||
| * Orchestrator's transaction list) to open a fresh | ||
| * measurement window. Restricted to the Orchestrator | ||
| * and the owner. | ||
| * - `pushReport()` is called ~2h before the next rebase to report the | ||
| * TWAP. The MedianOracle's report-delay (security) | ||
| * window then ages the report before it is consumed at | ||
| * the following rebase, so no timing gate is enforced | ||
| * here. | ||
| */ | ||
| contract DexOracle is Ownable { | ||
| using SafeMath for uint256; | ||
|
|
||
| /// @notice Decimals of the reported price; matches MedianOracle.DECIMALS. | ||
| uint256 public constant OUTPUT_DECIMALS = 18; | ||
|
|
||
| /// @notice MedianOracle this contract reports to as a registered provider. | ||
| IMedianOracle public medianOracle; | ||
|
|
||
| /// @notice First leg market: prices the source asset in the bridge asset. | ||
| IUniswapV2Pair public immutable pairLeg1; | ||
| /// @notice Second leg market: prices the bridge asset in the quote asset. | ||
| IUniswapV2Pair public immutable pairLeg2; | ||
|
|
||
| /// @dev When true the leg reads price1 (token1 priced in token0), | ||
| /// otherwise price0 (token0 priced in token1). | ||
| bool public immutable leg1UseToken1Price; | ||
| bool public immutable leg2UseToken1Price; | ||
|
|
||
| /// @dev 10**(OUTPUT_DECIMALS + baseDecimals - quoteDecimals) per leg, used | ||
| /// to convert a raw UQ112x112 reserve ratio into a decimal price. | ||
| uint256 public immutable decimalsFactorLeg1; | ||
| uint256 public immutable decimalsFactorLeg2; | ||
|
|
||
| /// @notice Decimal (OUTPUT_DECIMALS) price-seconds cumulatives captured at | ||
| /// the last `update()`. | ||
| uint256 public priceLeg1CumulativeLast; | ||
| uint256 public priceLeg2CumulativeLast; | ||
| /// @notice Timestamp (mod 2**32) of the last `update()`. Zero until the | ||
| /// first `update()`, which marks the oracle as uninitialized. | ||
| uint32 public blockTimestampLast; | ||
|
|
||
| /// @notice The Orchestrator, which may call `update()` (it runs right after | ||
| /// each rebase). The owner may also call `update()`; no other caller | ||
| /// is authorized. | ||
| address public orchestrator; | ||
|
|
||
| event LogPriceUpdate( | ||
| uint256 priceLeg1Cumulative, | ||
| uint256 priceLeg2Cumulative, | ||
| uint32 timestamp | ||
| ); | ||
| event LogReportPushed(uint256 price, uint32 timeElapsed); | ||
| // Emitted once at construction. The two bridge-side tokens (leg1's quote | ||
| // and leg2's base) are expected to represent the same value; `matched` is | ||
| // true when they are the exact same address. They may legitimately differ | ||
| // (e.g. two equivalent wrapped representations), so this is informational | ||
| // only and never reverts. | ||
| event LogBridgeTokens(address leg1QuoteToken, address leg2BaseToken, bool matched); | ||
|
|
||
| /** | ||
| * @param medianOracle_ MedianOracle instance to report to. | ||
| * @param orchestrator_ Orchestrator allowed to call `update()` (alongside | ||
| * the owner). | ||
| * @param pairLeg1_ UniswapV2 pair for the first (source/bridge) leg. | ||
| * @param leg1UseToken1Price_ True to read price1 on leg1, false for price0. | ||
| * @param pairLeg2_ UniswapV2 pair for the second (bridge/quote) leg. | ||
| * @param leg2UseToken1Price_ True to read price1 on leg2, false for price0. | ||
| */ | ||
| constructor( | ||
| address medianOracle_, | ||
| address orchestrator_, | ||
| address pairLeg1_, | ||
| bool leg1UseToken1Price_, | ||
| address pairLeg2_, | ||
| bool leg2UseToken1Price_ | ||
| ) { | ||
| Ownable.initialize(msg.sender); | ||
|
|
||
| medianOracle = IMedianOracle(medianOracle_); | ||
| orchestrator = orchestrator_; | ||
|
|
||
| pairLeg1 = IUniswapV2Pair(pairLeg1_); | ||
| pairLeg2 = IUniswapV2Pair(pairLeg2_); | ||
| leg1UseToken1Price = leg1UseToken1Price_; | ||
| leg2UseToken1Price = leg2UseToken1Price_; | ||
|
|
||
| (address leg1Base, address leg1Quote) = _baseQuote(pairLeg1_, leg1UseToken1Price_); | ||
| (address leg2Base, address leg2Quote) = _baseQuote(pairLeg2_, leg2UseToken1Price_); | ||
| decimalsFactorLeg1 = _decimalsFactor(leg1Base, leg1Quote); | ||
| decimalsFactorLeg2 = _decimalsFactor(leg2Base, leg2Quote); | ||
|
|
||
| // The bridge token is leg1's quote (asset AMPL is priced in) and leg2's | ||
| // base (asset priced in USDC). Logged for off-chain inspection; the two | ||
| // may legitimately be distinct equivalent tokens, so this never reverts. | ||
| emit LogBridgeTokens(leg1Quote, leg2Base, leg1Quote == leg2Base); | ||
|
|
||
| // blockTimestampLast is left at 0 to mark the oracle as uninitialized. | ||
| } | ||
|
|
||
| /** | ||
| * @notice Opens a fresh measurement window by snapshotting the current | ||
| * price cumulatives. Intended to be appended to the Orchestrator's | ||
| * transaction list so it runs immediately after each rebase. | ||
| * @dev Restricted to the Orchestrator and the owner, who are trusted to | ||
| * open the window on schedule; both may call at any time. | ||
| */ | ||
| function update() external { | ||
| require(msg.sender == orchestrator || isOwner(), "DexOracle: UNAUTHORIZED"); | ||
|
|
||
| ( | ||
| uint256 leg1Cumulative, | ||
| uint256 leg2Cumulative, | ||
| uint32 blockTimestamp | ||
| ) = _currentCumulatives(); | ||
|
|
||
| priceLeg1CumulativeLast = leg1Cumulative; | ||
| priceLeg2CumulativeLast = leg2Cumulative; | ||
| blockTimestampLast = blockTimestamp; | ||
|
|
||
| emit LogPriceUpdate(leg1Cumulative, leg2Cumulative, blockTimestamp); | ||
| } | ||
|
|
||
| /** | ||
| * @notice Computes the chained TWAP over the current measurement window and | ||
| * reports it to the MedianOracle. Intended to be called ~2h before | ||
| * the next rebase, leaving the report to age past the MedianOracle | ||
| * report-delay window before it is consumed. | ||
| * @return price The reported AMPL/USDC price as an OUTPUT_DECIMALS number. | ||
| */ | ||
| function pushReport() external returns (uint256 price) { | ||
| uint32 timeElapsed; | ||
| (price, timeElapsed) = _computePrice(); | ||
|
|
||
| medianOracle.pushReport(price); | ||
| emit LogReportPushed(price, timeElapsed); | ||
| } | ||
|
|
||
| /** | ||
| * @notice Purges this provider's outstanding reports on the MedianOracle, | ||
| * e.g. to retract a report pushed in error. | ||
| */ | ||
| function purgeReports() external onlyOwner { | ||
| medianOracle.purgeReports(); | ||
| } | ||
|
|
||
| /** | ||
| * @notice Reads the chained TWAP over the current measurement window | ||
| * without reporting it. Unlike `update()` this never gates on the | ||
| * period, so the live average can always be inspected. | ||
| * @return price The AMPL/USDC price as an OUTPUT_DECIMALS number. | ||
| */ | ||
| function consult() external view returns (uint256 price) { | ||
| (price, ) = _computePrice(); | ||
| } | ||
|
|
||
| /** | ||
| * @notice Sets the MedianOracle this contract reports to. | ||
| * @param medianOracle_ The new MedianOracle address. | ||
| */ | ||
| function setMedianOracle(address medianOracle_) external onlyOwner { | ||
| medianOracle = IMedianOracle(medianOracle_); | ||
| } | ||
|
|
||
| /** | ||
| * @notice Sets the Orchestrator address allowed to call `update()`. | ||
| * @param orchestrator_ The Orchestrator address (zero to allow only the | ||
| * owner to call `update()`). | ||
| */ | ||
| function setOrchestrator(address orchestrator_) external onlyOwner { | ||
| orchestrator = orchestrator_; | ||
| } | ||
|
|
||
| /** | ||
| * @dev Computes the chained TWAP since the last `update()`. Requires the | ||
| * oracle to be initialized and at least one second of measurement (to | ||
| * avoid division by zero), but never gates on the full period. | ||
| * @return price The chained price as an OUTPUT_DECIMALS number. | ||
| * @return timeElapsed The length of the measurement window in seconds. | ||
| */ | ||
| function _computePrice() private view returns (uint256 price, uint32 timeElapsed) { | ||
| require(blockTimestampLast > 0, "DexOracle: UPDATE_NEVER_CALLED"); | ||
|
|
||
| ( | ||
| uint256 leg1Cumulative, | ||
| uint256 leg2Cumulative, | ||
| uint32 blockTimestamp | ||
| ) = _currentCumulatives(); | ||
| unchecked { | ||
| // Wraparound is desired; both timestamps are taken mod 2**32. | ||
| timeElapsed = blockTimestamp - blockTimestampLast; | ||
| } | ||
| require(timeElapsed > 0, "DexOracle: NO_TIME_ELAPSED"); | ||
|
|
||
| // The decimal cumulatives grow monotonically, so the windowed averages | ||
| // are plain differences divided by the elapsed time. | ||
| uint256 priceLeg1 = (leg1Cumulative - priceLeg1CumulativeLast) / timeElapsed; | ||
| uint256 priceLeg2 = (leg2Cumulative - priceLeg2CumulativeLast) / timeElapsed; | ||
| price = priceLeg1.mul(priceLeg2).div(10**OUTPUT_DECIMALS); | ||
| } | ||
|
|
||
| /** | ||
| * @dev Reads the current price cumulatives for both legs, selecting the | ||
| * configured direction and bridging each from a raw UQ112x112 reserve | ||
| * ratio into an OUTPUT_DECIMALS price-seconds cumulative. Both legs | ||
| * share the same block timestamp. | ||
| */ | ||
| function _currentCumulatives() | ||
| private | ||
| view | ||
| returns ( | ||
| uint256 leg1Cumulative, | ||
| uint256 leg2Cumulative, | ||
| uint32 blockTimestamp | ||
| ) | ||
| { | ||
| uint256 price0; | ||
| uint256 price1; | ||
|
|
||
| (price0, price1, blockTimestamp) = UniswapV2OracleLibrary.currentCumulativePrices( | ||
| address(pairLeg1) | ||
| ); | ||
| leg1Cumulative = (leg1UseToken1Price ? price1 : price0).mul(decimalsFactorLeg1) >> 112; | ||
|
|
||
| (price0, price1, ) = UniswapV2OracleLibrary.currentCumulativePrices(address(pairLeg2)); | ||
| leg2Cumulative = (leg2UseToken1Price ? price1 : price0).mul(decimalsFactorLeg2) >> 112; | ||
| } | ||
|
|
||
| /** | ||
| * @dev Resolves the (base, quote) tokens a leg prices, given the read | ||
| * direction. price0 prices token0 in token1; price1 prices token1 in | ||
| * token0. | ||
| */ | ||
| function _baseQuote(address pair, bool useToken1Price) | ||
| private | ||
| view | ||
| returns (address base, address quote) | ||
| { | ||
| if (useToken1Price) { | ||
| base = IUniswapV2Pair(pair).token1(); | ||
| quote = IUniswapV2Pair(pair).token0(); | ||
| } else { | ||
| base = IUniswapV2Pair(pair).token0(); | ||
| quote = IUniswapV2Pair(pair).token1(); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * @dev Computes 10**(OUTPUT_DECIMALS + baseDecimals - quoteDecimals), the | ||
| * factor that converts a leg's raw UQ112x112 reserve ratio into an | ||
| * OUTPUT_DECIMALS decimal price. | ||
| */ | ||
| function _decimalsFactor(address base, address quote) private view returns (uint256) { | ||
| uint256 baseDecimals = uint256(IERC20Decimals(base).decimals()); | ||
| uint256 quoteDecimals = uint256(IERC20Decimals(quote).decimals()); | ||
| return 10**(OUTPUT_DECIMALS.add(baseDecimals).sub(quoteDecimals)); | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| // SPDX-License-Identifier: GPL-3.0-or-later | ||
| pragma solidity 0.8.4; | ||
|
|
||
| /** | ||
| * @title IUniswapV2Pair | ||
| * @dev Minimal interface for the subset of UniswapV2Pair used by the oracle. | ||
| * See https://github.com/Uniswap/v2-core for the full interface. | ||
| */ | ||
| interface IUniswapV2Pair { | ||
| function token0() external view returns (address); | ||
|
|
||
| function token1() external view returns (address); | ||
|
|
||
| function getReserves() | ||
| external | ||
| view | ||
| returns ( | ||
| uint112 reserve0, | ||
| uint112 reserve1, | ||
| uint32 blockTimestampLast | ||
| ); | ||
|
|
||
| function price0CumulativeLast() external view returns (uint256); | ||
|
|
||
| function price1CumulativeLast() external view returns (uint256); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,58 @@ | ||
| // SPDX-License-Identifier: GPL-3.0-or-later | ||
| pragma solidity 0.8.4; | ||
|
|
||
| import {IUniswapV2Pair} from "./IUniswapV2Pair.sol"; | ||
|
|
||
| /** | ||
| * @title UniswapV2OracleLibrary | ||
| * @dev Helper methods for oracles that consume the UniswapV2 price | ||
| * accumulators. Ported to 0.8.x; the `unchecked` blocks preserve the | ||
| * modulo-2**N wraparound that the UniswapV2Pair accumulators rely on. | ||
| */ | ||
| library UniswapV2OracleLibrary { | ||
| // Returns the current block timestamp within the range of uint32, | ||
| // i.e. [0, 2**32 - 1]. Matches the truncation used by UniswapV2Pair. | ||
| function currentBlockTimestamp() internal view returns (uint32) { | ||
| return uint32(block.timestamp % 2**32); | ||
| } | ||
|
|
||
| // Produces the cumulative prices using counterfactuals to save gas and | ||
| // avoid a call to sync. The returned cumulatives are UQ112x112 fixed point | ||
| // numbers denominated in raw (smallest-unit) reserves. | ||
| function currentCumulativePrices(address pair) | ||
| internal | ||
| view | ||
| returns ( | ||
| uint256 price0Cumulative, | ||
| uint256 price1Cumulative, | ||
| uint32 blockTimestamp | ||
| ) | ||
| { | ||
| blockTimestamp = currentBlockTimestamp(); | ||
| price0Cumulative = IUniswapV2Pair(pair).price0CumulativeLast(); | ||
| price1Cumulative = IUniswapV2Pair(pair).price1CumulativeLast(); | ||
|
|
||
| // If time has elapsed since the last update on the pair, mock the | ||
| // accumulated price values to bring them current. | ||
| (uint112 reserve0, uint112 reserve1, uint32 blockTimestampLast) = IUniswapV2Pair(pair) | ||
| .getReserves(); | ||
| if (blockTimestampLast != blockTimestamp) { | ||
| unchecked { | ||
| // Subtraction overflow is desired. | ||
| uint32 timeElapsed = blockTimestamp - blockTimestampLast; | ||
| // Addition overflow is desired (matches UniswapV2Pair). | ||
| // counterfactual | ||
| price0Cumulative += uint256(_fraction(reserve1, reserve0)) * timeElapsed; | ||
| // counterfactual | ||
| price1Cumulative += uint256(_fraction(reserve0, reserve1)) * timeElapsed; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // Encodes (numerator / denominator) as a UQ112x112 fixed point number, | ||
| // mirroring UniswapV2's FixedPoint.fraction. | ||
| function _fraction(uint112 numerator, uint112 denominator) private pure returns (uint224) { | ||
| require(denominator > 0, "UniswapV2OracleLibrary: DIVISION_BY_ZERO"); | ||
| return uint224((uint256(numerator) << 112) / denominator); | ||
| } | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't see why this operation should be unchecked...
But it seems like the subtractions in the computatio of priceLeg1 and priceLeg2 should be?