Conversation
|
hey @holyfuchs - an FYI - there is this old branch with Oracle Aggregator related changes - https://github.com/onflow/FlowCreditMarket/tree/gio/oracle-integration |
6571b44 to
755c23d
Compare
- average price - spread - gradient missing historical price collection
add historical price collection add gradient calculation including tests
aebdbb4 to
0e4345a
Compare
0e4345a to
fd5e166
Compare
fd5e166 to
130c543
Compare
eddc7e2 to
82542ae
Compare
| let price = oracle.price(ofToken: self.ofToken) | ||
| if price == nil { | ||
| emit PriceNotAvailable() | ||
| return nil |
There was a problem hiding this comment.
One unavailable oracle would block the entire aggregator? Is this intended? Do we want the PriceNotAvailable to include the oracle type message for debugging purpose?
There was a problem hiding this comment.
A single oracle with a spread too high would also block the aggregator.
It could be updated to have a threshold of oracles to be available / within the spread but this can also be added later without much issues.
I will add the type message!
There was a problem hiding this comment.
Will get clarification in todays Q&A
| "baseTolerance must be <= 10000.0" | ||
| driftExpansionRate <= 10000.0: | ||
| "driftExpansionRate must be <= 10000.0" | ||
| minimumPriceHistory <= priceHistorySize: |
There was a problem hiding this comment.
maybe also require minimumPriceHistory > 0? otherwise it would restore the old behavior
There was a problem hiding this comment.
There might be instances where this is intended, will get clarification in todays Q&A.
| let count = prices.length | ||
|
|
||
| // Handle edge cases where trimming isn't possible | ||
| if count == 0 { return nil } | ||
| if count == 1 { return prices[0] } | ||
| if count == 2 { return (prices[0] + prices[1]) / 2.0 } |
There was a problem hiding this comment.
I think the point is to always trim the min and max. If there is not enough data to trim, then we should return nil, instead of a value as if it has been trimmed. But I haven't thought through the impact of this changes yet.
Also better to explain the behavior in the function comments.
| let count = prices.length | |
| // Handle edge cases where trimming isn't possible | |
| if count == 0 { return nil } | |
| if count == 1 { return prices[0] } | |
| if count == 2 { return (prices[0] + prices[1]) / 2.0 } | |
| // Handle edge cases where trimming isn't possible | |
| if prices.length <= 2 { return nil } |
There was a problem hiding this comment.
This breaks the aggregator for tokens with only two oracles.
Will get clarification in todays Q&A.
Closes: #132
Price Oracle Architecture
This document describes the price oracle design for the ALP.
How multiple sources are combined into a single trusted oracle interface, and how routing and aggregation are split across two contracts.
Overview
The protocol depends on a single trusted oracle that returns either a valid price or
nilwhen the price should not be used (e.g. liquidation or rebalancing should be skipped). The protocol does not validate prices; it only consumes the oracle’s result.Two contracts implement this design:
baseTolerance+driftExpansionRate(stability).DeFiActions.PriceOraclethat routes by token type. Each token has its own oracle; typically each oracle is an aggregator.Typical usage: create one aggregator per market (same token pair, multiple sources), then register each aggregator in a router under the corresponding token type. The protocol then uses the router as its single oracle.
Immutable Configuration
The Aggregator and Router are immutable by design to eliminate the risks associated with live production changes.
PriceOracleUpdatedevent, ensuring all shifts in logic or parameters are visible and expected.FlowPriceOracleAggregatorv1
One aggregated oracle per “market” (e.g. FLOW in USDC). Multiple underlying oracles, single unit of account, fixed tolerances.
PriceNotAvailable, return nil.maxSpread→ emitPriceNotWithinSpreadTolerance, return nil.baseTolerance + driftExpansionRate * deltaTMinutes; if any relative difference exceeds that → emitPriceNotWithinHistoryTolerance, return nil.(price, timestamp)is maintained. Updates are permissionless viatryAddPriceToHistory()(idempotent); A FlowCron job should be created to call this regularly.Additionally every call to price() will also attempt to store the price in the history.
Aggregate price (trimmed mean)
To avoid the complexity of a full median, the aggregator uses a trimmed mean: remove the single maximum and single minimum, then average the rest. This reduces the impact of a single outlier.
(sum - min - max) / (count - 2).Oracle spread (coherence)
A pessimistic relative spread is used: the distance between the most extreme oracle prices relative to the minimum price.
The price set is coherent only if:
Short-term stability (history tolerance)
The aggregator keeps an array of the last n aggregated prices (with timestamps), respecting
priceHistoryIntervalandmaxPriceHistoryAge.Stability is defined by two parameters:
For each historical point (i), the allowed relative difference between the current price and the history price grows with time:
where delta t_minutes is the time in minutes from the history entry to now. The actual relative difference is:
The current price is stable only if every such relative difference (from each valid history entry to the current price) is at or below the allowed tolerance for that entry. If any exceeds it, the aggregator emits
PriceNotWithinHistoryTolerance(relativeDiff, deltaTMinutes, maxAllowedRelativeDiff)and returns nil.Implementationally, entries older than
maxPriceHistoryAgeare ignored when evaluating stability.Parameter units:
maxSpread,baseTolerance, anddriftExpansionRateare dimensionless relative values (e.g.0.01= 1%,1.0= 100%). All are bounded by the contract to ≤ 10000.0.FlowPriceOracleRouterv1
Single oracle interface that routes by token type. Each token type maps to an oracle. This makes it easy to combine different aggregators without the need to supply different kinds of thresholds for individual token types.
price(ofToken)looks up the oracle for that token type; if none is registered, returnsnil. All oracles must share the sameunitOfAccount(enforced at router creation).price(ofToken)returnsnil.