feat: Require distinct oracle sources for quorum - #1433
Open
OG-wura wants to merge 2 commits into
Open
Conversation
|
@OG-wura Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits. You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀 |
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.
Close #1393
Require distinct oracle sources for quorum
Summary
The three-oracle median resolver (
OracleResolutionManager::resolve_with_median)reached "quorum" when the number of
includedquotes reachedMedianOracleConfig::min_sources. Each of the three slots (Pyth, Reflector, Band)was fetched from a distinct contract address configured in
MedianOracleConfig.Nothing, however, enforced that the three addresses were actually different on-chain
contracts.
If a configuration pointed two slots at the same contract, that single oracle was
counted as two (or three) independent sources. Such a configuration could:
min_sources(quorum) with effectively one source, andthan once.
This PR requires distinct oracle sources for quorum in two complementary layers,
so a single on-chain oracle can never satisfy or skew quorum.
Changes
1. Config-time validation (input validation / least privilege)
contracts/predictify-hybrid/src/resolution.rsOracleResolutionManager::validate_distinct_sources(&MedianOracleConfig) -> Result<(), Error>returns
Error::InvalidOracleConfigwhen any two ofpyth_address,reflector_address, andband_addressare equal.OracleResolutionManager::set_median_confignow calls it before persisting andreturns
Result<(), Error>. A duplicated-source configuration is rejected andnever written to storage, so a bad/quorum-vacating config cannot be established in
the first place.
2. Resolution-time guard (defense in depth / abuse resistance)
contracts/predictify-hybrid/src/resolution.rsOracleResolutionManager::dedupe_duplicate_sources(env, config, quotes)walks the fixed fetch order (Pyth → Reflector → Band) and keeps only the first
occurrence of each distinct contract address
included; any later quote from thesame address is flagged
included = false. Implemented with fixed-size arrays (noheap allocation) so it is
no_std/ WASM friendly.resolve_with_medianapplies the dedupe immediately after fetching and beforethe baseline-median and quorum steps, then rebinds
raw_quotesto the dedupedvector. As a result every downstream step (baseline median, outlier detection,
included_countvsmin_sources, confidence-weighted median) observes at most onequote per distinct source.
source can no longer satisfy
min_sourcesor appear twice in the median; if thesurviving distinct sources are fewer than
min_sources, resolution still failsdeterministically with
Error::OracleNoConsensus.This second layer also covers configurations persisted before the config-time check
existed (e.g. an already-deployed contract that had stored a duplicated config), so the
invariant holds even for legacy/malformed state.
Documentation
types.rs:MedianOracleConfignow documents the distinct-source invariant on thesignature of
min_sources.resolution.rs: doc comments onset_median_config,resolve_with_median(algorithm step list + a dedicated "Security note"),
validate_distinct_sources,and
dedupe_duplicate_sourcesdescribe why quorum requires distinct sources and howthe two enforcement layers interact.
Acceptance-criteria mapping
min_sources == 1still allowed for distinct configs.set_median_configstill stores via the same key after passing the new validation gate; failed validation performs no storage write (no partial state). Resolver preserves all existing guards (timeout,MarketClosed,MarketResolved, falldown).included = falsequotes and are handled exactly as before;dedupe_duplicate_sourcesis a pure post-fetch filter, so it is safe under retries and adds no new side effects. WASM is single-threaded; no shared-state race is introduced.resolve_with_median/MarketResolutionManagerbehavior unchanged for distinct configs.OracleResolutionManager::set_median_configchanges its return type from()toResult<(), Error>; it has no external caller today (only the contract-internal median feature), and the change is fail-closed (returns an error rather than persisting bad config) — no silent behavior change for valid inputs.Error::InvalidOracleConfig; resolution with insufficient distinct sources surfacesError::OracleNoConsensus, identical to the pre-existing under-quorum path, so the failure mode stays observable to operators. The existingOracleConsensusReachedEvent/oracle_median_quotesevents continue to report the (now deduped) quote vector.Security & failure-mode handling
"independent" oracle slots into one contract, gaining 2–3× weight in the median and
single-handedly satisfying quorum. This PR removes that lever at both the config and
resolution layers.
dedupe drops duplicates before any consensus math. There is no path where a duplicated
source can contribute to quorum after this change.
under-counts the effective number of sources and, if fewer than
min_sourcesdistinctsources survive, returns
Error::OracleNoConsensusrather than proceeding on a biasedmedian.
(
InvalidOracleConfig,OracleNoConsensus) — no new error surface, no EVM-equivalentencoding churn.
Tests
Run:
cargo test -p predictify-hybrid --lib median_resolution_testsAll 37 tests in
resolution::median_resolution_testspass, including these new ones:test_validate_distinct_sources_accepts_distinct— distinct addresses accepted.test_validate_distinct_sources_rejects_duplicates— rejects Pyth==Reflector,Reflector==Band, Band==Pyth, and all-equal configs.
test_set_median_config_persists_distinct_config— distinct config stored & reloaded.test_set_median_config_rejects_duplicate_sources_and_does_not_persist— duplicateconfig returns
InvalidOracleConfigand leaves storage untouched.test_dedupe_duplicate_sources_keeps_first_distinct_occurrence— Reflector/Bandcollision drops the Band quote.
test_dedupe_duplicate_sources_distinct_config_unchanged— distinct config leaves allquotes included.
test_dedupe_duplicate_sources_pyth_reflector_collision_keeps_pyth— Pyth keeps itsvote, Reflector (duplicate) is dropped.
Run the full package suite with:
cargo test -p predictify-hybridWASM size
bash scripts/check_wasm_size.shbuilds the contract for thewasm32v1-nonetarget andreports the (unoptimized) size. The new median code is not referenced by any live contract
entrypoint, so it is eliminated by release-mode dead-code stripping and does not add to the
deployed WASM. For a fully optimized size measurement the environment needs
stellar contract optimize(not installed here); the script falls back to the unoptimized binary inthat case.