Skip to content

feat: add fallback oracle chain for FX (#547) - #1

Open
thelmaoffiong wants to merge 17 commits into
masterfrom
feat/oracle-fallback-chain
Open

feat: add fallback oracle chain for FX (#547)#1
thelmaoffiong wants to merge 17 commits into
masterfrom
feat/oracle-fallback-chain

Conversation

@thelmaoffiong

Copy link
Copy Markdown
Owner

Summary

Implements RevoraOrg#547: when the primary FX oracle returns a stale quote, the contract now walks an ordered oracle fallback chain and uses the first non-stale entry. If every oracle in the chain is stale the call returns AllOraclesStale without any state mutation.

Changes

New types

  • OracleEntry — a single chain entry: oracle address + currency pair symbols + max_age_secs
  • OracleChain — persisted ordered list of OracleEntry values (DataKey2::OracleChain(OfferingId))

New error codes (wire-stable)

  • OracleQuoteStale = 62 — single entry's quote is too old
  • AllOraclesStale = 63 — every entry in the chain is stale

New event

  • orc_used (EVENT_ORACLE_SOURCE_USED) — emitted when a chain entry wins, carrying (oracle, from, to, chain_index)

New entrypoints

  • set_oracle_chain(issuer, namespace, token, entries) — issuer-auth guarded; rejects if entries.len() > 10
  • get_oracle_chain(issuer, namespace, token) — read-only

Implementation

  • try_oracle_entry() private helper checks staleness; returns OracleQuoteStale if age exceeds max_age_secs (0 = disabled)
  • convert_report_amount_if_needed() wired: chain takes priority over legacy FxOracleConfig; short-circuits on first fresh quote; emits orc_used event; falls through to legacy path when no chain stored

Tests (11 in oracle_chain_tests module)

Test What it verifies
set_and_get_oracle_chain_round_trips Storage round-trip
no_chain_no_single_oracle_returns_payout_asset_mismatch No-oracle fallback error
chain_first_oracle_fresh_uses_first_entry Happy path, first entry wins
chain_first_stale_second_fresh_falls_through Fallback to second entry
all_oracles_stale_returns_all_oracles_stale_error All stale, no state change
chain_only_last_entry_fresh Walks entire chain to last entry
empty_chain_falls_back_to_single_oracle Empty chain uses legacy path
chain_takes_priority_over_legacy_single_oracle Chain wins over FxOracleConfig
set_oracle_chain_too_many_entries_returns_limit_reached Length guard (MAX=10)
set_oracle_chain_requires_issuer_auth Auth guard
chain_entry_zero_max_age_never_stale max_age_secs=0 disables staleness

Security notes

  • Only the current offering issuer may configure the chain
  • Chain evaluation is deterministic and short-circuit
  • AllOraclesStale is returned without writing any state
  • Maximum chain length of 10 bounds cross-contract call depth and gas

Build notes

  • cargo fmt --all -- --check passes
  • Build blocked by 774+ pre-existing errors on master; our changes introduce zero new root-cause errors

Closes RevoraOrg#547

Realbaji16 and others added 17 commits July 28, 2026 11:18
Add a shared storage-layout registry that emits docs/STORAGE_LAYOUT.json at build time and fails CI when enum keys drift from the checked-in schema.

Co-authored-by: Cursor <cursoragent@cursor.com>
Restore the tracked .vscode/settings.json file so the storage-layout branch only carries the intended RevoraOrg#617 changes.

Co-authored-by: Cursor <cursoragent@cursor.com>
Implements issue RevoraOrg#547: when the primary FX oracle is stale, the contract
now falls through an ordered chain of oracles before failing the transaction.

Changes:
- Add OracleEntry and OracleChain contracttype structs
- Add DataKey2::OracleChain(OfferingId) persistent storage key
- Add MAX_ORACLE_CHAIN_LEN = 10 safety cap
- Add RevoraError::OracleQuoteStale (62) and AllOraclesStale (63)
- Add EVENT_ORACLE_SOURCE_USED (orc_used) event constant
- Add set_oracle_chain() entrypoint (issuer-auth guarded)
- Add get_oracle_chain() read-only entrypoint
- Add try_oracle_entry() private helper for single-entry staleness check
- Wire oracle chain into convert_report_amount_if_needed():
  chain takes priority over legacy FxOracleConfig; chain iterates
  entries in order, short-circuits on first non-stale quote, emits
  oracle_source_used event with (oracle, from, to, chain_index)
- 11 new tests in oracle_chain_tests module covering:
  round-trip storage, no-chain fallback, first-fresh happy path,
  stale-fallback, all-stale error, only-last-fresh, empty chain,
  priority over legacy oracle, chain length limit, auth guard,
  zero max_age disables staleness check

Security: only the current offering issuer may set the oracle chain;
chain evaluation is deterministic and short-circuits at first fresh quote;
all oracles stale returns AllOraclesStale without any state mutation.

Closes RevoraOrg#547
* feat: paginate get_holder_statement

Add get_holder_statement_page with a stable PeriodEntry cursor so long holder ledgers can be read under gas limits without changing claim semantics.

Co-authored-by: Cursor <cursoragent@cursor.com>

* chore: restore tracked vscode settings

Restore the tracked .vscode/settings.json file so the holder-statement pagination branch only carries the intended RevoraOrg#599 changes.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Realbaji16 <bajiemeks16@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Add a registration API for per-key migration hooks that allow custom
transform functions (Identity, Rename, Custom) to be attached to specific
legacy storage keys. When the storage walker runs during a layout upgrade,
it applies each registered hook deterministically and emits audit events.

=== Changes ===

src/lib.rs:
- Add MigrationTransform enum (Identity, Rename, Custom)
- Add MigrationHook struct binding legacy key to transform
- Extend MigrationDataKey with hook count, index, and per-key storage
- Add EVENT_MIG_HOOK_APPLIED event symbol
- Implement register_migration_hook() (admin-gated)
- Implement clear_migration_hook() (admin-gated, idempotent)
- Implement get_registered_hooks() (read-only enumeration)
- Add apply_migration_hook() internal helper (dry-run aware)
- Update migrate_storage_walker() to iterate and apply hooks

src/test_storage_layout_version.rs:
- 16 new tests covering registration, clear, auth, dry-run, walker
  integration, replay protection, identity no-op, hook enumeration

Security:
- All hooks are deterministic and pure (enum variants, not closures)
- Admin auth required for register/clear (gates on stored DataKey::Admin)
- Dry-run mode emits plan events without mutating storage
- Replay protection preserved (cannot re-apply same migration)

Co-authored-by: thlpkee20-wq <thlpkee20@gmail.com>
…ml (RevoraOrg#640)

Co-authored-by: thlpkee20-wq <thlpkee20@gmail.com>
Co-authored-by: Developer <developer@teachlink.com>
Co-authored-by: thlpkee20-wq <thlpkee20@gmail.com>
* feat: add tax-bucket rollover past cost basis

* feat: gate register_offering on multi-issuer quorum

---------

Co-authored-by: Developer <developer@teachlink.com>
Co-authored-by: thlpkee20-wq <thlpkee20@gmail.com>
Co-authored-by: thlpkee20-wq <thlpkee20@gmail.com>
…rg#644)

* feat: block cross-class transfers by default (RevoraOrg#522)

Add class-restricted transfer rules preventing Class A to Class B
cross-flow to satisfy legal covenants requiring strict separation
between share classes.

Changes:
- Add ClassTransferBlocked error variant (wire value 58)
- Add EVENT_CLASS_XFER_BLOCK event (cls_block) on rejection
- Add get_primary_class helper to determine holder's share class
- Add missing DataKey2 variants (OfferingClasses, HolderShareClass,
  TransferRestrictions, HolderCategory, CategoryHolderCount)
- Add class transfer guard in both transfer_with_attestation paths
- Add zero-value guard in first transfer_with_attestation
- Add 20 comprehensive tests covering same-class, cross-class,
  backward compat, event emission, bypasses, custom classes, and
  multi-class holder edge cases

Closes RevoraOrg#522

* docs: add comprehensive PR description for class-restricted transfer rules (RevoraOrg#522)

---------

Co-authored-by: thlpkee20-wq <thlpkee20@gmail.com>
…flag (RevoraOrg#647)

Add emit_v2_compat admin flag that controls whether V2 indexed events are emitted alongside V3. Defaults to true for backward compatibility. Includes DataKey2::EmitV2Compat storage, is_emit_v2_compat() helper, emit_v2_and_v3() function, admin setter, tests, and deprecation docs.

Closes RevoraOrg#566

Co-authored-by: OpadijoIdris <opadijoidris@branda.com.ng>
Co-authored-by: thlpkee20-wq <thlpkee20@gmail.com>
* feat: add estimate_transfer eligibility query

* feat: enforce per-class supply cap independent of aggregate

---------

Co-authored-by: thlpkee20-wq <thlpkee20@gmail.com>
…aOrg#649)

Persist class_priority_index under (offering_id, class_id) and emit a
canonical class_pay_order event from close_period, ascending by
(priority_index, share_class.to_xdr() bytes) so ties tie-break
deterministically across reruns and across single-sig / dual-sig close.

* New DataKey2 variants `ClassPriority` and `ClassPayOrder`.
* New event symbols `EVENT_CLASS_PAY_ORDER` ("clspayo") and
  `EVENT_CLASS_PRIORITY_SET` ("clprio"); constant
  `DEFAULT_CLASS_PRIORITY = 0`.
* Public API: `set_class_priority`, `get_class_priority`,
  `get_class_pay_order`. `set_class_priority` requires issuer-quorum
  authorization matching `set_holder_share`'s governance model.
* `close_period` and `close_period_dual_sig` now record and emit the
  resolved `Vec<ShareClass>` after sealing, idempotent because the
  existing period-already-closed guard rejects repeats.
* `resolve_class_pay_order` performs an in-place bubble sort on
  `soroban_sdk::Vec` keyed by `(priority, xdr_bytes)`; n is bounded by
  the per-offering class count.

Tests: 11 new tests in `src/test_close_period.rs` cover happy path,
default-zero fallback, wrong-issuer, unknown-offering, unregistered
class, ascending-priority ordering, XDR tie-break correctness across
reruns, empty order for classless offerings, dual-sig consistency,
and the legacy-period empty-order migration fallback.

Docs: `docs/class-priority.md` documents storage, public API,
resolution semantics (with a worked example), security notes, event
payload reference, and migration / backwards-compatibility story.

Co-authored-by: thlpkee20-wq <thlpkee20@gmail.com>
* feat: log admin rotation history

* quick fix [ci skip]

* quick fix [ci skip]

* feat: two-phase admin rotation with delay

* quick fix [ci skip]

---------

Co-authored-by: thlpkee20-wq <thlpkee20@gmail.com>
Adds resolve_dispute(dispute_id, outcome, evidence_hash) entrypoint
that allows the global admin to resolve open IssuerDispute freezes.

- Add DisputeOutcome enum (Upheld, Rejected, PartiallyUpheld)
- Add DisputeEntry struct for on-chain dispute tracking
- Add resolve_dispute() with admin-only auth, atomic status update,
  freeze removal on Rejected outcome, and disp_res event emission
- Add get_dispute() read-only query
- Add set_dispute_window / get_dispute_window for window configuration
- Add dispute window enforcement in emergency_freeze_holder
- Add auto-creation of DisputeEntry on IssuerDispute freeze
- Add 17 comprehensive tests covering auth, outcomes, events, edge cases

Closes RevoraOrg#593

Co-authored-by: thlpkee20-wq <thlpkee20@gmail.com>
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 fallback-oracle chain so a secondary oracle serves when primary is stale

10 participants