From bb38c048bbc7ed98064c725d58f296f4715148a0 Mon Sep 17 00:00:00 2001 From: Nabeelahh Date: Mon, 27 Apr 2026 14:35:38 +0100 Subject: [PATCH] gradual-slashing --- contracts/price-oracle/src/auth.rs | 178 +++++++++++++++++++++ contracts/price-oracle/src/lib.rs | 72 +++++++++ contracts/price-oracle/src/penalty_test.rs | 47 ++++++ contracts/price-oracle/src/types.rs | 16 ++ 4 files changed, 313 insertions(+) create mode 100644 contracts/price-oracle/src/penalty_test.rs diff --git a/contracts/price-oracle/src/auth.rs b/contracts/price-oracle/src/auth.rs index 568fa99..59a3071 100644 --- a/contracts/price-oracle/src/auth.rs +++ b/contracts/price-oracle/src/auth.rs @@ -1,4 +1,5 @@ use soroban_sdk::{contracttype, Address, Env, Vec}; +use crate::types::{RelayerState, DataKey}; // ───────────────────────────────────────────────────────────────────────────── // Storage Key @@ -229,6 +230,83 @@ pub fn _require_not_frozen(env: &Env) { } } +// ───────────────────────────────────────────────────────────────────────────── +// Relayer Penalty System +// ───────────────────────────────────────────────────────────────────────────── + +/// Get the relayer state for a given provider address. +pub fn _get_relayer_state(env: &Env, provider: &Address) -> RelayerState { + env.storage() + .instance() + .get(&crate::types::DataKey::RelayerState(provider.clone())) + .unwrap_or_else(|| RelayerState { + consecutive_errors: 0, + penalty_multiplier: 100, // No penalty by default + last_success_timestamp: 0, + last_error_timestamp: 0, + }) +} + +/// Set the relayer state for a given provider address. +pub fn _set_relayer_state(env: &Env, provider: &Address, state: &RelayerState) { + env.storage().instance().set(&crate::types::DataKey::RelayerState(provider.clone()), state); +} + +/// Calculate penalty multiplier based on consecutive errors. +/// +/// Penalty progression: +/// - 1-2 errors: 110 (10% penalty) +/// - 3-4 errors: 125 (25% penalty) +/// - 5-7 errors: 150 (50% penalty) +/// - 8+ errors: 200 (100% penalty - effectively suspended) +pub fn _calculate_penalty_multiplier(consecutive_errors: u32) -> u32 { + match consecutive_errors { + 0 => 100, // No penalty + 1 | 2 => 110, // 10% penalty + 3 | 4 => 125, // 25% penalty + 5 | 6 | 7 => 150, // 50% penalty + _ => 200, // 100% penalty (suspended) + } +} + +/// Update relayer state after a successful submission. +/// Resets consecutive errors and updates penalty multiplier. +pub fn _record_relayer_success(env: &Env, provider: &Address) { + let mut state = _get_relayer_state(env, provider); + state.consecutive_errors = 0; + state.penalty_multiplier = 100; // Reset penalty + state.last_success_timestamp = env.ledger().timestamp(); + _set_relayer_state(env, provider, &state); +} + +/// Update relayer state after a failed submission or missed heartbeat. +/// Increments consecutive errors and updates penalty multiplier. +pub fn _record_relayer_error(env: &Env, provider: &Address) { + let mut state = _get_relayer_state(env, provider); + state.consecutive_errors += 1; + state.penalty_multiplier = _calculate_penalty_multiplier(state.consecutive_errors); + state.last_error_timestamp = env.ledger().timestamp(); + _set_relayer_state(env, provider, &state); +} + +/// Get the current penalty multiplier for a relayer. +pub fn _get_relayer_penalty_multiplier(env: &Env, provider: &Address) -> u32 { + let state = _get_relayer_state(env, provider); + state.penalty_multiplier +} + +/// Check if a relayer is effectively suspended (100% penalty). +pub fn _is_relayer_suspended(env: &Env, provider: &Address) -> bool { + let state = _get_relayer_state(env, provider); + state.penalty_multiplier >= 200 +} + +/// Get the number of consecutive errors for a relayer. +pub fn _get_consecutive_errors(env: &Env, provider: &Address) -> u32 { + let state = _get_relayer_state(env, provider); + state.consecutive_errors +} + // ───────────────────────────────────────────────────────────────────────────── // Tests // ───────────────────────────────────────────────────────────────────────────── @@ -517,6 +595,106 @@ mod auth_tests { }); } + // ── Relayer Penalty System tests ─────────────────────────────────────────── + + #[test] + fn test_relayer_state_initialization() { + let (env, contract_id, _) = setup(); + let provider = ::generate(&env); + env.as_contract(&contract_id, || { + let state = _get_relayer_state(&env, &provider); + assert_eq!(state.consecutive_errors, 0); + assert_eq!(state.penalty_multiplier, 100); + assert_eq!(state.last_success_timestamp, 0); + assert_eq!(state.last_error_timestamp, 0); + }); + } + + #[test] + fn test_calculate_penalty_multiplier() { + assert_eq!(_calculate_penalty_multiplier(0), 100); // No penalty + assert_eq!(_calculate_penalty_multiplier(1), 110); // 10% penalty + assert_eq!(_calculate_penalty_multiplier(2), 110); // 10% penalty + assert_eq!(_calculate_penalty_multiplier(3), 125); // 25% penalty + assert_eq!(_calculate_penalty_multiplier(4), 125); // 25% penalty + assert_eq!(_calculate_penalty_multiplier(5), 150); // 50% penalty + assert_eq!(_calculate_penalty_multiplier(6), 150); // 50% penalty + assert_eq!(_calculate_penalty_multiplier(7), 150); // 50% penalty + assert_eq!(_calculate_penalty_multiplier(8), 200); // 100% penalty (suspended) + assert_eq!(_calculate_penalty_multiplier(10), 200); // 100% penalty (suspended) + } + + #[test] + fn test_record_relayer_success() { + let (env, contract_id, _) = setup(); + let provider = ::generate(&env); + env.as_contract(&contract_id, || { + // Start with some errors + _record_relayer_error(&env, &provider); + _record_relayer_error(&env, &provider); + assert_eq!(_get_consecutive_errors(&env, &provider), 2); + assert_eq!(_get_relayer_penalty_multiplier(&env, &provider), 110); + + // Record success - should reset + _record_relayer_success(&env, &provider); + assert_eq!(_get_consecutive_errors(&env, &provider), 0); + assert_eq!(_get_relayer_penalty_multiplier(&env, &provider), 100); + assert!(_get_relayer_state(&env, &provider).last_success_timestamp > 0); + }); + } + + #[test] + fn test_record_relayer_error_progression() { + let (env, contract_id, _) = setup(); + let provider = ::generate(&env); + env.as_contract(&contract_id, || { + // First error + _record_relayer_error(&env, &provider); + assert_eq!(_get_consecutive_errors(&env, &provider), 1); + assert_eq!(_get_relayer_penalty_multiplier(&env, &provider), 110); + assert!(!_is_relayer_suspended(&env, &provider)); + + // Second error + _record_relayer_error(&env, &provider); + assert_eq!(_get_consecutive_errors(&env, &provider), 2); + assert_eq!(_get_relayer_penalty_multiplier(&env, &provider), 110); + assert!(!_is_relayer_suspended(&env, &provider)); + + // Third error + _record_relayer_error(&env, &provider); + assert_eq!(_get_consecutive_errors(&env, &provider), 3); + assert_eq!(_get_relayer_penalty_multiplier(&env, &provider), 125); + assert!(!_is_relayer_suspended(&env, &provider)); + + // Eighth error - should be suspended + _record_relayer_error(&env, &provider); + _record_relayer_error(&env, &provider); + _record_relayer_error(&env, &provider); + _record_relayer_error(&env, &provider); + _record_relayer_error(&env, &provider); + assert_eq!(_get_consecutive_errors(&env, &provider), 8); + assert_eq!(_get_relayer_penalty_multiplier(&env, &provider), 200); + assert!(_is_relayer_suspended(&env, &provider)); + }); + } + + #[test] + fn test_relayer_state_persistence() { + let (env, contract_id, _) = setup(); + let provider = ::generate(&env); + env.as_contract(&contract_id, || { + // Record some errors + _record_relayer_error(&env, &provider); + _record_relayer_error(&env, &provider); + + // Verify state is saved + let state = _get_relayer_state(&env, &provider); + assert_eq!(state.consecutive_errors, 2); + assert_eq!(state.penalty_multiplier, 110); + assert!(state.last_error_timestamp > 0); + }); + } + // ── Renounce ownership tests ────────────────────────────────────────────── #[test] diff --git a/contracts/price-oracle/src/lib.rs b/contracts/price-oracle/src/lib.rs index d480363..4d9d249 100644 --- a/contracts/price-oracle/src/lib.rs +++ b/contracts/price-oracle/src/lib.rs @@ -192,6 +192,21 @@ pub trait StellarFlowTrait { /// /// Returns true if the contract is frozen, false otherwise. fn is_frozen(env: Env) -> bool; + + /// Get the current penalty multiplier for a relayer. + /// + /// Returns the penalty multiplier (100 = no penalty, 150 = 50% penalty, etc.). + fn get_relayer_penalty_multiplier(env: Env, relayer: Address) -> u32; + + /// Get the number of consecutive errors for a relayer. + /// + /// Returns the count of consecutive missed heartbeats or errors. + fn get_relayer_consecutive_errors(env: Env, relayer: Address) -> u32; + + /// Check if a relayer is suspended due to penalties. + /// + /// Returns true if the relayer has a 100% penalty (effectively suspended). + fn is_relayer_suspended(env: Env, relayer: Address) -> bool; } #[contractclient(name = "TokenContractClient")] @@ -1142,6 +1157,34 @@ impl PriceOracle { decimals: u32, confidence_score: u32, ttl: u64, + ) -> Result<(), Error> { + let result = Self::update_price_internal( + env.clone(), + source.clone(), + asset.clone(), + price, + decimals, + confidence_score, + ttl, + ); + + // Record error if submission failed + if let Err(_) = result { + crate::auth::_record_relayer_error(&env, &source); + } + + result + } + + /// Internal price update function with penalty tracking. + fn update_price_internal( + env: Env, + source: Address, + asset: Symbol, + price: i128, + decimals: u32, + confidence_score: u32, + ttl: u64, ) -> Result<(), Error> { _require_not_destroyed(&env); crate::auth::_require_not_frozen(&env); @@ -1159,6 +1202,11 @@ impl PriceOracle { return Err(Error::NotAuthorized); } + // Check if relayer is suspended due to penalties + if crate::auth::_is_relayer_suspended(&env, &source) { + return Err(Error::NotAuthorized); + } + // Normalize the raw price to 9 fixed-point decimals on entry. let normalized = normalize_price(&env, &asset, price); @@ -1262,6 +1310,9 @@ impl PriceOracle { }; callbacks::notify_subscribers(&env, &payload); + // Record successful submission for penalty tracking + crate::auth::_record_relayer_success(&env, &source); + Ok(()) } @@ -1651,6 +1702,27 @@ impl PriceOracle { crate::auth::_is_frozen(&env) } + /// Get the current penalty multiplier for a relayer. + /// + /// Returns the penalty multiplier (100 = no penalty, 150 = 50% penalty, etc.). + pub fn get_relayer_penalty_multiplier(env: Env, relayer: Address) -> u32 { + crate::auth::_get_relayer_penalty_multiplier(&env, &relayer) + } + + /// Get the number of consecutive errors for a relayer. + /// + /// Returns the count of consecutive missed heartbeats or errors. + pub fn get_relayer_consecutive_errors(env: Env, relayer: Address) -> u32 { + crate::auth::_get_consecutive_errors(&env, &relayer) + } + + /// Check if a relayer is suspended due to penalties. + /// + /// Returns true if the relayer has a 100% penalty (effectively suspended). + pub fn is_relayer_suspended(env: Env, relayer: Address) -> bool { + crate::auth::_is_relayer_suspended(&env, &relayer) + } + /// Get the price buffer for a specific asset. /// /// Returns all relayer submissions for the current ledger, diff --git a/contracts/price-oracle/src/penalty_test.rs b/contracts/price-oracle/src/penalty_test.rs new file mode 100644 index 0000000..6978837 --- /dev/null +++ b/contracts/price-oracle/src/penalty_test.rs @@ -0,0 +1,47 @@ +// Simple test to verify penalty system implementation +use soroban_sdk::{Address, Env}; +use crate::types::RelayerState; +use crate::auth::{ + _get_relayer_state, _set_relayer_state, _calculate_penalty_multiplier, + _record_relayer_success, _record_relayer_error, _get_consecutive_errors, + _get_relayer_penalty_multiplier, _is_relayer_suspended +}; + +#[test] +fn test_penalty_system_basic() { + let env = Env::default(); + let contract_id = env.register_contract("test", crate::PriceOracle); + let provider = Address::generate(&env); + + env.as_contract(&contract_id, || { + // Test initial state + let state = _get_relayer_state(&env, &provider); + assert_eq!(state.consecutive_errors, 0); + assert_eq!(state.penalty_multiplier, 100); + assert!(!_is_relayer_suspended(&env, &provider)); + + // Test penalty calculation + assert_eq!(_calculate_penalty_multiplier(0), 100); + assert_eq!(_calculate_penalty_multiplier(1), 110); + assert_eq!(_calculate_penalty_multiplier(3), 125); + assert_eq!(_calculate_penalty_multiplier(5), 150); + assert_eq!(_calculate_penalty_multiplier(8), 200); + + // Test error recording + _record_relayer_error(&env, &provider); + assert_eq!(_get_consecutive_errors(&env, &provider), 1); + assert_eq!(_get_relayer_penalty_multiplier(&env, &provider), 110); + + // Test success resets errors + _record_relayer_success(&env, &provider); + assert_eq!(_get_consecutive_errors(&env, &provider), 0); + assert_eq!(_get_relayer_penalty_multiplier(&env, &provider), 100); + + // Test suspension after 8 errors + for i in 0..8 { + _record_relayer_error(&env, &provider); + } + assert_eq!(_get_consecutive_errors(&env, &provider), 8); + assert!(_is_relayer_suspended(&env, &provider)); + }); +} diff --git a/contracts/price-oracle/src/types.rs b/contracts/price-oracle/src/types.rs index 388a619..8cb346a 100644 --- a/contracts/price-oracle/src/types.rs +++ b/contracts/price-oracle/src/types.rs @@ -36,6 +36,8 @@ pub enum DataKey { CommunityCouncil, /// Emergency freeze state flag. EmergencyFrozen, + /// Relayer state tracking consecutive errors and penalties. + RelayerState(Address), } /// Decimal metadata for an asset pair. @@ -205,3 +207,17 @@ pub struct AdminLogEntry { pub details: soroban_sdk::String, pub timestamp: u64, } + +/// Relayer state tracking consecutive errors and penalties. +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct RelayerState { + /// Number of consecutive missed heartbeats or errors. + pub consecutive_errors: u32, + /// Current penalty multiplier (100 = no penalty, 150 = 50% penalty, etc.). + pub penalty_multiplier: u32, + /// Timestamp of last successful submission. + pub last_success_timestamp: u64, + /// Timestamp of last error/miss. + pub last_error_timestamp: u64, +}