diff --git a/contracts/price-oracle/src/auth.rs b/contracts/price-oracle/src/auth.rs index 568fa99..0dca582 100644 --- a/contracts/price-oracle/src/auth.rs +++ b/contracts/price-oracle/src/auth.rs @@ -9,6 +9,7 @@ pub enum DataKey { Admin, Provider(Address), ProviderWeight(Address), + ProviderLastLedger(Address), IsPaused, ActiveRelayers, CommunityCouncil, @@ -134,13 +135,47 @@ pub fn _is_provider(env: &Env, addr: &Address) -> bool { .unwrap_or(false) } -/// Panics if the caller is not a whitelisted provider. +/// Panic if the caller is not a whitelisted provider. pub fn _require_provider(env: &Env, caller: &Address) { if !_is_provider(env, caller) { panic!("Unauthorised: caller is not a whitelisted provider"); } } +/// Prune inactive relayers from the whitelist. +/// +/// Iterates through the active relayers list and removes any provider that has not +/// submitted a price update within `max_inactive_ledgers` ledgers. +/// +/// # Arguments +/// * `env` - The contract environment +/// * `max_inactive_ledgers` - The maximum number of ledgers a provider can be inactive +/// +/// # Panics +/// Panics if the caller is not an authorized admin. +pub fn _prune_inactive_relayers(env: &Env, caller: &Address, max_inactive_ledgers: u32) -> u32 { + // Require admin authorization + _require_authorized(env, caller); + + let current_ledger = env.ledger().sequence(); + let relayers = _get_active_relayers(env); + let mut pruned_count: u32 = 0; + + for relayer in relayers.iter() { + let last_ledger = _get_provider_last_ledger(env, &relayer); + + // If provider has never submitted (last_ledger = 0) or has been inactive too long + let inactive_ledgers = current_ledger.saturating_sub(last_ledger); + + if last_ledger == 0 || inactive_ledgers > max_inactive_ledgers { + _remove_provider(env, &relayer); + pruned_count += 1; + } + } + + pruned_count +} + pub fn _set_provider_weight(env: &Env, provider: &Address, weight: u32) { env.storage() .instance() @@ -154,6 +189,22 @@ pub fn _get_provider_weight(env: &Env, provider: &Address) -> u32 { .unwrap_or(0) } +/// Set the last ledger sequence when a provider submitted a price update. +pub fn _set_provider_last_ledger(env: &Env, provider: &Address, ledger: u32) { + env.storage() + .instance() + .set(&DataKey::ProviderLastLedger(provider.clone()), &ledger); +} + +/// Get the last ledger sequence when a provider submitted a price update. +/// Returns 0 if the provider has never submitted a price. +pub fn _get_provider_last_ledger(env: &Env, provider: &Address) -> u32 { + env.storage() + .instance() + .get::(&DataKey::ProviderLastLedger(provider.clone())) + .unwrap_or(0) +} + /// Get the list of all active relayers (whitelisted providers). pub fn _get_active_relayers(env: &Env) -> Vec
{ env.storage() diff --git a/contracts/price-oracle/src/lib.rs b/contracts/price-oracle/src/lib.rs index d480363..1ff51fe 100644 --- a/contracts/price-oracle/src/lib.rs +++ b/contracts/price-oracle/src/lib.rs @@ -192,6 +192,19 @@ pub trait StellarFlowTrait { /// /// Returns true if the contract is frozen, false otherwise. fn is_frozen(env: Env) -> bool; + + /// Prune inactive relayers from the whitelist. + /// + /// This is a "Pull" mechanism (not automated) that allows admins to remove + /// providers that have not submitted a price update within `max_inactive_ledgers`. + /// + /// # Arguments + /// * `admin` - The admin address authorizing this action + /// * `max_inactive_ledgers` - The maximum number of ledgers a provider can be inactive + /// + /// # Returns + /// The number of providers that were pruned. + fn prune_inactive_relayers(env: Env, admin: Address, max_inactive_ledgers: u32) -> u32; } #[contractclient(name = "TokenContractClient")] @@ -1213,6 +1226,10 @@ impl PriceOracle { } } + // Track this provider's activity + let current_ledger = env.ledger().sequence(); + crate::auth::_set_provider_last_ledger(&env, &source, current_ledger); + // Add the normalized price entry to the buffer let entry = PriceBufferEntry { price: normalized, @@ -1723,6 +1740,25 @@ impl PriceOracle { pub fn get_price_update_subscribers(env: Env) -> soroban_sdk::Vec
{ callbacks::get_subscribers(&env) } + + /// Prune inactive relayers from the whitelist. + /// + /// This is a "Pull" mechanism (not automated) that allows admins to remove + /// providers that have not submitted a price update within `max_inactive_ledgers`. + /// + /// # Arguments + /// * `admin` - The admin address authorizing this action + /// * `max_inactive_ledgers` - The maximum number of ledgers a provider can be inactive + /// + /// # Returns + /// The number of providers that were pruned. + pub fn prune_inactive_relayers(env: Env, admin: Address, max_inactive_ledgers: u32) -> u32 { + _require_not_destroyed(&env); + crate::auth::_require_not_frozen(&env); + admin.require_auth(); + + crate::auth::_prune_inactive_relayers(&env, &admin, max_inactive_ledgers) + } } mod asset_symbol;