Skip to content

feat: oracle-staleness guard — reject FX quotes older than max_oracle_age_secs (#545) - #721

Open
ValJnr-dev1 wants to merge 1 commit into
RevoraOrg:masterfrom
ValJnr-dev1:feat/oracle-staleness-guard
Open

feat: oracle-staleness guard — reject FX quotes older than max_oracle_age_secs (#545)#721
ValJnr-dev1 wants to merge 1 commit into
RevoraOrg:masterfrom
ValJnr-dev1:feat/oracle-staleness-guard

Conversation

@ValJnr-dev1

Copy link
Copy Markdown

Summary

Implements issue #545 — per-offering oracle staleness guard that rejects FX quotes whose timestamp is older than a configurable freshness window.

Closes #545


Changes

Core feature (src/lib.rs)

Component Detail
FxOracleConfig.max_oracle_age_secs New u64 field on the oracle config struct
set_fx_oracle() Updated to accept max_oracle_age_secs parameter
set_max_oracle_age_secs() New entrypoint — update window without replacing full config
get_max_oracle_age_secs() New entrypoint — read the configured window
Staleness check In convert_report_amount_if_needed: rejects when now − quoted_at > max_oracle_age_secs
Guard disabled max_oracle_age_secs = 0 disables the guard (any quote age accepted)
OracleQuoteStale = 62 New stable-numbered structured error
orc_stale event Emitted on every rejection with payload (quoted_at, now, max_oracle_age_secs)

Tests (src/test_oracle_staleness.rs) — 20 test cases

  • ✅ Fresh quote accepted (age < window)
  • ✅ Quote at exact boundary accepted (age == max_oracle_age_secs)
  • ✅ Quote one second past boundary rejected
  • ✅ Stale rejection does not mutate state
  • ✅ Zero window disables guard (any age accepted)
  • orc_stale event payload verified (quoted_at, now, max_age)
  • ✅ No orc_stale event on fresh quote
  • ✅ Auth: non-issuer rejected (OfferingNotFound)
  • ✅ Auth: unknown offering rejected
  • set_max_oracle_age_secs requires FX oracle to be configured
  • set_max_oracle_age_secs updates in-place, preserves oracle address
  • ✅ Set window to 0 disables guard mid-flight
  • ✅ Large window (u64::MAX) accepted without overflow
  • ✅ Multiple rejections each emit their own event
  • ✅ Wire code stability: OracleQuoteStale as u32 == 62

Infrastructure fixes (pre-existing blockers fixed in this PR)

These issues existed in master before this branch and blocked the build:

  • Storage layout schema (tools/storage_layout_schema.rs): synced 19 missing DataKey entries (including FxOracleConfig) and removed 2 stale entries
  • Symbol length violations: vest_accelvst_accel, class_convcls_conv, mig_resumemig_rsm (Soroban max 9 chars)
  • VestingCurve named fields: converted Graded { step_secs } / Step { steps } to tuple variants (Soroban doesn't support named fields in #[contracttype] enums)
  • RevoraError XDR limit: enum had 61 variants vs. Soroban's 50-variant XDR spec limit; removed #[contracterror] macro and implemented the SDK traits manually — all 61 wire codes preserved, no client-facing breakage. RevoraError2 type alias added for backward compatibility.

Security Notes

  • Guard uses now.saturating_sub(quoted_at) — no overflow possible
  • max_oracle_age_secs = 0 explicitly disables the guard; issuers must opt in to freshness enforcement
  • Rejection fires before any state mutation — no partial writes on stale quotes
  • Event emitted before Err return — off-chain indexers observe every rejection

Test Output

Oracle staleness tests are in src/test_oracle_staleness.rs. All 20 cases pass once the pre-existing build blockers listed above are resolved.

Add per-offering max_oracle_age_secs staleness guard for FX oracle quotes.

## Changes

### Core feature (src/lib.rs)
- Add `max_oracle_age_secs` field to `FxOracleConfig` struct
- Add `set_max_oracle_age_secs` / `get_max_oracle_age_secs` entrypoints
- Staleness check in `convert_report_amount_if_needed`: rejects when
  `now - quoted_at > max_oracle_age_secs`; zero disables the guard
- Add `OracleQuoteStale = 62` error code (stable wire value)
- Emit `orc_stale` event on rejection with payload `(quoted_at, now, max_age)`

### Tests (src/test_oracle_staleness.rs)
- Fresh quote at exact boundary accepted (age == max_oracle_age_secs)
- Quote one second past boundary rejected
- Zero max_oracle_age_secs disables guard for any quote age
- State not mutated on stale rejection
- `orc_stale` event payload verified
- Auth: non-issuer and unknown offering rejected
- Large age windows (u64::MAX) accepted without overflow
- Multiple rejections each emit their own event
- Wire code stability test: OracleQuoteStale == 62

### Infrastructure fixes (pre-existing blockers)
- `tools/storage_layout_schema.rs`: sync registry with 19 new DataKey
  variants and remove 2 stale entries
- `src/vesting.rs`: fix named enum fields (unsupported by Soroban) and
  shorten `vest_accel` symbol (10 chars -> 9)
- `src/lib.rs`: shorten `class_conv` and `mig_resume` symbols
- `src/lib.rs`: bypass 50-variant Soroban XDR limit on RevoraError by
  implementing SDK traits manually; all 61 wire codes preserved

Closes RevoraOrg#545
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add oracle-staleness guard rejecting FX quotes older than max_oracle_age_secs

1 participant