From 350a7df430402567f95fce826c76207cc5cbb893 Mon Sep 17 00:00:00 2001 From: THE-CHEMIST Date: Fri, 29 May 2026 08:37:18 +0000 Subject: [PATCH 1/3] feat: implemented Emergency Autonomous Network Kill Switch --- contracts/price-oracle/src/auth.rs | 26 ++++++++++++++ contracts/price-oracle/src/lib.rs | 56 ++++++++++++++++++++++++++++++ 2 files changed, 82 insertions(+) diff --git a/contracts/price-oracle/src/auth.rs b/contracts/price-oracle/src/auth.rs index fd23926..34267f0 100644 --- a/contracts/price-oracle/src/auth.rs +++ b/contracts/price-oracle/src/auth.rs @@ -14,6 +14,8 @@ pub enum DataKey { ActiveRelayers, CommunityCouncil, EmergencyFrozen, + /// Emergency halt flag — set by multi-sig governance to block all rate reads. + EmergencyHalt, /// Expiry timestamp (seconds) until which safety checks are bypassed. BypassSafetyChecks, /// Auto-incrementing counter for multi-sig action proposals. @@ -311,6 +313,30 @@ pub fn _require_not_frozen(env: &Env) { } } +// ───────────────────────────────────────────────────────────────────────────── +// Emergency Halt Helpers (multi-sig governance) +// ───────────────────────────────────────────────────────────────────────────── + +pub fn _is_halted(env: &Env) -> bool { + env.storage() + .instance() + .get::(&DataKey::EmergencyHalt) + .unwrap_or(false) +} + +pub fn _set_halted(env: &Env, status: bool) { + env.storage() + .instance() + .set(&DataKey::EmergencyHalt, &status); +} + +/// Panic if the emergency halt flag is active. +pub fn _require_not_halted(env: &Env) { + if _is_halted(env) { + panic!("Contract is emergency halted: rate reads are disabled"); + } +} + // ───────────────────────────────────────────────────────────────────────────── // Bypass Safety Checks Helpers // ───────────────────────────────────────────────────────────────────────────── diff --git a/contracts/price-oracle/src/lib.rs b/contracts/price-oracle/src/lib.rs index 297a355..ff972a0 100644 --- a/contracts/price-oracle/src/lib.rs +++ b/contracts/price-oracle/src/lib.rs @@ -267,6 +267,17 @@ pub trait StellarFlowTrait { /// Returns true if the contract is frozen, false otherwise. fn is_frozen(env: Env) -> bool; + /// Halt or resume all public rate read queries via multi-sig governance. + /// + /// Requires at least 2 of the registered governance admins to authorize. + /// When `status` is `true`, every public rate read (get_price, get_last_price, + /// get_prices, get_price_with_status, get_price_safe, get_twap, get_index_price) + /// will panic with `Error::EmergencyHalted` until the halt is lifted. + fn set_emergency_halt(env: Env, admin1: Address, admin2: Address, status: bool) -> Result<(), Error>; + + /// Return the current emergency halt state. + fn is_halted(env: Env) -> bool; + /// Enable a 1-hour grace period during which the circuit-breaker safety /// checks (flash-crash, price floor, and price bounds) are bypassed. /// @@ -349,6 +360,8 @@ pub enum Error { NoPreviousConfig = 23, /// Contract has not been initialized yet. NotInitialized = 24, + /// Contract is emergency halted — all rate read queries are blocked. + EmergencyHalted = 25, } #[contract] @@ -710,6 +723,9 @@ impl PriceOracle { env: Env, components: soroban_sdk::Vec, ) -> Result { + if crate::auth::_is_halted(&env) { + panic_with_error!(&env, Error::EmergencyHalted); + } if components.is_empty() { return Err(Error::AssetNotFound); } @@ -1001,6 +1017,9 @@ impl PriceOracle { /// /// Returns `Error::AssetNotFound` when the asset is missing or stale. pub fn get_price(env: Env, asset: Symbol, verified: bool) -> Result { + if crate::auth::_is_halted(&env) { + panic_with_error!(&env, Error::EmergencyHalted); + } let key = if verified { DataKey::VerifiedPrice(asset) } else { @@ -1022,6 +1041,9 @@ impl PriceOracle { /// Returns the last known price data and marks it stale when TTL has expired. /// Always reads from the `VerifiedPrice` bucket. pub fn get_price_with_status(env: Env, asset: Symbol) -> Result { + if crate::auth::_is_halted(&env) { + panic_with_error!(&env, Error::EmergencyHalted); + } match env .storage() .persistent() @@ -1041,6 +1063,9 @@ impl PriceOracle { /// Returns `None` instead of an error when the asset is not found. /// Always reads from the `VerifiedPrice` bucket. pub fn get_price_safe(env: Env, asset: Symbol) -> Option { + if crate::auth::_is_halted(&env) { + panic_with_error!(&env, Error::EmergencyHalted); + } env.storage() .persistent() .get::(&DataKey::VerifiedPrice(asset)) @@ -1051,6 +1076,9 @@ impl PriceOracle { /// Always reads from the `VerifiedPrice` bucket. /// Returns the price value as an i128, or an error if the asset is not found. pub fn get_last_price(env: Env, asset: Symbol) -> Result { + if crate::auth::_is_halted(&env) { + panic_with_error!(&env, Error::EmergencyHalted); + } let price_data = Self::get_price(env, asset, true)?; Ok(price_data.price) } @@ -1065,6 +1093,9 @@ impl PriceOracle { env: Env, assets: soroban_sdk::Vec, ) -> soroban_sdk::Vec> { + if crate::auth::_is_halted(&env) { + panic_with_error!(&env, Error::EmergencyHalted); + } let now = env.ledger().timestamp(); let mut result = soroban_sdk::Vec::new(&env); @@ -2604,6 +2635,28 @@ impl PriceOracle { crate::auth::_is_frozen(&env) } + /// Halt or resume all public rate read queries via multi-sig governance. + /// + /// Requires 2 distinct authorized admins. When `status` is `true`, every + /// public rate read panics with `Error::EmergencyHalted` until lifted. + pub fn set_emergency_halt(env: Env, admin1: Address, admin2: Address, status: bool) -> Result<(), Error> { + _require_not_destroyed(&env); + if admin1 == admin2 { + return Err(Error::MultiSigValidationFailed); + } + admin1.require_auth(); + admin2.require_auth(); + crate::auth::_require_authorized(&env, &admin1); + crate::auth::_require_authorized(&env, &admin2); + crate::auth::_set_halted(&env, status); + Ok(()) + } + + /// Return the current emergency halt state. + pub fn is_halted(env: Env) -> bool { + crate::auth::_is_halted(&env) + } + /// Get the price buffer for a specific asset. /// /// Returns all relayer submissions for the current ledger, @@ -2626,6 +2679,9 @@ impl PriceOracle { /// Get the Time-Weighted Average Price (TWAP) for a specific asset. pub fn get_twap(env: Env, asset: Symbol) -> Option { + if crate::auth::_is_halted(&env) { + panic_with_error!(&env, Error::EmergencyHalted); + } let key = DataKey::Twap(asset); let twap_buffer: soroban_sdk::Vec<(u64, i128)> = env.storage().persistent().get(&key)?; From 5853c897ff4c2793ed5168127007c42f7e7eb0f2 Mon Sep 17 00:00:00 2001 From: THE-CHEMIST Date: Fri, 29 May 2026 08:47:55 +0000 Subject: [PATCH 2/3] feat: implemented Graceful Withdrawal Unlock Cooldown Delay --- contracts/price-oracle/src/lib.rs | 88 +++++++++++++++++++++++++++- contracts/price-oracle/src/test.rs | 91 +++++++++++++++++++++++++++++ contracts/price-oracle/src/types.rs | 16 +++++ 3 files changed, 194 insertions(+), 1 deletion(-) diff --git a/contracts/price-oracle/src/lib.rs b/contracts/price-oracle/src/lib.rs index ff972a0..1d4563d 100644 --- a/contracts/price-oracle/src/lib.rs +++ b/contracts/price-oracle/src/lib.rs @@ -8,10 +8,14 @@ use soroban_sdk::{ use crate::types::{ AdminAction, AdminLogEntry, AssetInfo, AssetMeta, DataKey, PriceBounds, PriceBuffer, PriceBufferEntry, PriceData, PriceDataWithStatus, PriceEntryWithStatus, PriceUpdatePayload, - ProposedAction, RecentEvent, + ProposedAction, RecentEvent, UnbondingRequest, }; const ADMIN_TIMELOCK: u64 = 86_400; const MAX_CLEAR_ASSETS: u32 = 20; +/// Number of ledgers a relayer must wait after requesting a withdrawal before +/// stake can be released. Prevents instant unstaking after a malicious price +/// submission from draining security collateral. +const UNBONDING_FREEZE_LEDGERS: u32 = 10_000; /// A clean, gas-optimized interface for other Soroban contracts to fetch prices from StellarFlow. /// @@ -2784,6 +2788,88 @@ impl PriceOracle { pub fn get_bypass_safety_checks_expiry(env: Env) -> Option { crate::auth::_get_bypass_expiry(&env) } + + // ── Unbonding / Withdrawal (10,000-ledger freeze window) ───────────────── + + /// Request a stake withdrawal for a whitelisted relayer. + /// + /// Records the current ledger sequence and the requested `amount` under + /// `DataKey::UnbondRequest(relayer)`. The relayer's stake is frozen for + /// exactly `UNBONDING_FREEZE_LEDGERS` (10,000) ledgers from this point. + /// + /// Panics if the relayer already has a pending unbonding request. + pub fn request_withdrawal(env: Env, relayer: Address, amount: i128) -> Result<(), Error> { + _require_not_destroyed(&env); + _require_initialized(&env); + crate::auth::_require_not_frozen(&env); + relayer.require_auth(); + crate::auth::_require_provider(&env, &relayer); + + let key = DataKey::UnbondRequest(relayer.clone()); + if env.storage().persistent().has(&key) { + panic!("unbonding request already pending"); + } + + let req = UnbondingRequest { + request_ledger: env.ledger().sequence(), + amount, + }; + env.storage().persistent().set(&key, &req); + + env.events().publish( + (Symbol::new(&env, "unbond_requested"),), + (relayer, req.request_ledger, amount), + ); + + Ok(()) + } + + /// Release a relayer's stake after the 10,000-ledger unbonding window has passed. + /// + /// Verifies that at least `UNBONDING_FREEZE_LEDGERS` ledgers have elapsed + /// since the matching `request_withdrawal` call, then removes the pending + /// request. Token transfer logic should follow this call in the integrating + /// contract once the guard passes. + /// + /// Panics if no pending request exists or the freeze window has not expired. + pub fn release_stake(env: Env, relayer: Address) -> Result { + _require_not_destroyed(&env); + _require_initialized(&env); + crate::auth::_require_not_frozen(&env); + relayer.require_auth(); + + let key = DataKey::UnbondRequest(relayer.clone()); + let req: UnbondingRequest = env + .storage() + .persistent() + .get(&key) + .unwrap_or_else(|| panic!("no pending unbonding request")); + + let current_ledger = env.ledger().sequence(); + let elapsed = current_ledger.saturating_sub(req.request_ledger); + if elapsed < UNBONDING_FREEZE_LEDGERS { + panic!( + "unbonding freeze active: {} ledgers remaining", + UNBONDING_FREEZE_LEDGERS - elapsed + ); + } + + env.storage().persistent().remove(&key); + + env.events().publish( + (Symbol::new(&env, "stake_released"),), + (relayer, req.amount), + ); + + Ok(req.amount) + } + + /// Get the pending unbonding request for a relayer, if any. + pub fn get_unbonding_request(env: Env, relayer: Address) -> Option { + env.storage() + .persistent() + .get(&DataKey::UnbondRequest(relayer)) + } } mod asset_symbol; diff --git a/contracts/price-oracle/src/test.rs b/contracts/price-oracle/src/test.rs index 4741755..ccb784f 100644 --- a/contracts/price-oracle/src/test.rs +++ b/contracts/price-oracle/src/test.rs @@ -2774,3 +2774,94 @@ fn test_enable_bypass_requires_admin() { // non_admin is not in the admin list — should panic. client.enable_bypass_safety_checks(&non_admin); } + +// ── Unbonding freeze window tests ──────────────────────────────────────────── + +fn initialized_client_with_relayer( + env: &Env, +) -> (Address, PriceOracleClient<'static>, Address) { + let contract_id = env.register(PriceOracle, ()); + let client = PriceOracleClient::new(env, &contract_id); + let admin = Address::generate(env); + let relayer = Address::generate(env); + + client.initialize(&admin, &soroban_sdk::Vec::new(env)); + env.as_contract(&contract_id, || { + crate::auth::_add_provider(env, &relayer); + }); + + (contract_id, client, relayer) +} + +#[test] +fn test_request_withdrawal_stores_unbonding_request() { + let env = Env::default(); + env.mock_all_auths(); + env.ledger().set_sequence_number(100); + + let (_contract_id, client, relayer) = initialized_client_with_relayer(&env); + + client.request_withdrawal(&relayer, &5_000_i128); + + let req = client.get_unbonding_request(&relayer).expect("request should exist"); + assert_eq!(req.request_ledger, 100); + assert_eq!(req.amount, 5_000); +} + +#[test] +#[should_panic(expected = "unbonding freeze active")] +fn test_release_stake_blocked_before_freeze_window_expires() { + let env = Env::default(); + env.mock_all_auths(); + env.ledger().set_sequence_number(100); + + let (_contract_id, client, relayer) = initialized_client_with_relayer(&env); + client.request_withdrawal(&relayer, &5_000_i128); + + // Advance only 9,999 ledgers — one short of the required 10,000. + env.ledger().set_sequence_number(10_099); + client.release_stake(&relayer); +} + +#[test] +fn test_release_stake_succeeds_after_freeze_window_expires() { + let env = Env::default(); + env.mock_all_auths(); + env.ledger().set_sequence_number(100); + + let (_contract_id, client, relayer) = initialized_client_with_relayer(&env); + client.request_withdrawal(&relayer, &5_000_i128); + + // Advance exactly 10,000 ledgers. + env.ledger().set_sequence_number(10_100); + let released = client.release_stake(&relayer); + assert_eq!(released, 5_000); + + // Request should be cleared after release. + assert!(client.get_unbonding_request(&relayer).is_none()); +} + +#[test] +#[should_panic(expected = "unbonding request already pending")] +fn test_duplicate_withdrawal_request_panics() { + let env = Env::default(); + env.mock_all_auths(); + env.ledger().set_sequence_number(100); + + let (_contract_id, client, relayer) = initialized_client_with_relayer(&env); + client.request_withdrawal(&relayer, &1_000_i128); + // Second request before the first is released must panic. + client.request_withdrawal(&relayer, &2_000_i128); +} + +#[test] +#[should_panic(expected = "no pending unbonding request")] +fn test_release_stake_without_request_panics() { + let env = Env::default(); + env.mock_all_auths(); + env.ledger().set_sequence_number(100); + + let (_contract_id, client, relayer) = initialized_client_with_relayer(&env); + // No request was made — should panic. + client.release_stake(&relayer); +} diff --git a/contracts/price-oracle/src/types.rs b/contracts/price-oracle/src/types.rs index cdd4d73..5c9a0f2 100644 --- a/contracts/price-oracle/src/types.rs +++ b/contracts/price-oracle/src/types.rs @@ -73,6 +73,9 @@ pub enum DataKey { PrevPriceFloorEntry(Symbol), /// Minimum number of votes required for a governance action to reach quorum. MinQuorumThreshold, + /// Unbonding withdrawal request for a relayer: stores the ledger sequence at which + /// the request was made so the 10,000-ledger freeze window can be enforced. + UnbondRequest(Address), } /// Decimal metadata for an asset pair. @@ -307,3 +310,16 @@ pub struct AssetWeight { /// Weight in basis points (0–10000). All weights in a basket should sum to 10000. pub weight: u32, } + +/// Pending unbonding request for a relayer. +/// +/// Created by `request_withdrawal`; consumed (and deleted) by `release_stake` +/// once the 10,000-ledger freeze window has elapsed. +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct UnbondingRequest { + /// Ledger sequence number at which the withdrawal was requested. + pub request_ledger: u32, + /// Amount of stake the relayer wishes to withdraw. + pub amount: i128, +} From 497b674dced2c5ec7d5a979d4264b37d38b5b1ff Mon Sep 17 00:00:00 2001 From: THE-CHEMIST Date: Fri, 29 May 2026 09:05:07 +0000 Subject: [PATCH 3/3] feat: implemented Event Log Standard Compliance Auditing Repo Avatar --- contracts/price-oracle/src/event_topics.rs | 19 +- contracts/price-oracle/src/lib.rs | 326 +++++++++++++-------- 2 files changed, 217 insertions(+), 128 deletions(-) diff --git a/contracts/price-oracle/src/event_topics.rs b/contracts/price-oracle/src/event_topics.rs index c4b5a53..035fc8a 100644 --- a/contracts/price-oracle/src/event_topics.rs +++ b/contracts/price-oracle/src/event_topics.rs @@ -1,14 +1,11 @@ //! FE-216: Efficient event indexing via topic mapping. -//! The first topic in env.events().publish() is the asset Symbol -//! so indexers can filter by asset pair (e.g. NGN/XLM) without scanning all events. +//! Uses publish_event() with typed structs matching the core proxy interface. -use soroban_sdk::{Env, Symbol}; +use crate::PriceUpdatedEvent; +use soroban_sdk::Env; +use soroban_sdk::Symbol; -/// Publishes a price update event with the asset symbol as the first topic. -/// Indexers can filter on topic[0] == asset to find all events for that pair. -pub fn publish_price_event(env: &Env, asset: Symbol, price: i128, timestamp: u64) { - env.events().publish( - (asset,), // topic[0] = asset symbol for efficient filtering - (price, timestamp), // data payload - ); -} \ No newline at end of file +/// Publishes a price update event using the typed PriceUpdatedEvent struct. +pub fn publish_price_event(env: &Env, asset: Symbol, price: i128) { + env.events().publish_event(&PriceUpdatedEvent { asset, price }); +} diff --git a/contracts/price-oracle/src/lib.rs b/contracts/price-oracle/src/lib.rs index 1d4563d..4fae4eb 100644 --- a/contracts/price-oracle/src/lib.rs +++ b/contracts/price-oracle/src/lib.rs @@ -419,6 +419,127 @@ pub struct RescueTokensEvent { pub amount: i128, } +#[soroban_sdk::contractevent] +pub struct AdminChangedEvent { + pub admin: Address, +} + +#[soroban_sdk::contractevent] +pub struct PauseToggledEvent { + pub admin1: Address, + pub admin2: Address, + pub paused: bool, +} + +#[soroban_sdk::contractevent] +pub struct AdminRegisteredEvent { + pub admin1: Address, + pub admin2: Address, + pub new_admin: Address, +} + +#[soroban_sdk::contractevent] +pub struct AdminRemovedEvent { + pub admin1: Address, + pub admin2: Address, + pub removed_admin: Address, +} + +#[soroban_sdk::contractevent] +pub struct ContractDestroyedEvent { + pub admin1: Address, + pub admin2: Address, +} + +#[soroban_sdk::contractevent] +pub struct ContractUpgradedEvent { + pub executor: Address, +} + +#[soroban_sdk::contractevent] +pub struct ActionProposedEvent { + pub action_id: u64, + pub admin: Address, + pub action_type: u32, +} + +#[soroban_sdk::contractevent] +pub struct ActionVotedEvent { + pub action_id: u64, + pub voter: Address, + pub vote_count: u32, +} + +#[soroban_sdk::contractevent] +pub struct ActionExecutedEvent { + pub action_id: u64, + pub executor: Address, +} + +#[soroban_sdk::contractevent] +pub struct ActionCancelledEvent { + pub action_id: u64, + pub canceller: Address, +} + +#[soroban_sdk::contractevent] +pub struct CouncilSetEvent { + pub admin: Address, + pub council: Address, +} + +#[soroban_sdk::contractevent] +pub struct EmergencyFreezeEvent { + pub council: Address, +} + +#[soroban_sdk::contractevent] +pub struct UnbondRequestedEvent { + pub relayer: Address, + pub request_ledger: u32, + pub amount: i128, +} + +#[soroban_sdk::contractevent] +pub struct StakeReleasedEvent { + pub relayer: Address, + pub amount: i128, +} + +#[soroban_sdk::contractevent] +pub struct VoteDelegatedEvent { + pub owner: Address, + pub delegate: Address, +} + +#[soroban_sdk::contractevent] +pub struct VoteDelegateClearedEvent { + pub owner: Address, +} + +#[soroban_sdk::contractevent] +pub struct QuorumSetEvent { + pub admin: Address, + pub threshold: u32, +} + +#[soroban_sdk::contractevent] +pub struct AdminTransferInitiatedEvent { + pub current_admin: Address, + pub new_admin: Address, +} + +#[soroban_sdk::contractevent] +pub struct AdminTransferAcceptedEvent { + pub new_admin: Address, +} + +#[soroban_sdk::contractevent] +pub struct CommunityPriceUpdatedEvent { + pub asset: Symbol, + pub price: i128, +} + /// Returns the signed percentage change in basis points. /// /// Example: 1_000_000 -> 1_200_000 returns 2_000 (20.00%). @@ -702,15 +823,11 @@ impl PriceOracle { panic_with_error!(&env, Error::AlreadyInitialized); } - #[allow(deprecated)] - env.events() - .publish((Symbol::new(&env, "AdminChanged"),), admin.clone()); - - // Emit ContractInitialized event to log when the Oracle goes live - env.events().publish( - (Symbol::new(&env, "ContractInitialized"),), - (admin.clone(), String::from_str(&env, VERSION)), - ); + env.events().publish_event(&AdminChangedEvent { admin: admin.clone() }); + env.events().publish_event(&ContractInitialized { + admin: admin.clone(), + version: String::from_str(&env, VERSION), + }); //_log_admin_action(&env, &admin, AdminAction::Initialize, None); let admins = soroban_sdk::vec![&env, admin]; @@ -790,15 +907,11 @@ impl PriceOracle { panic_with_error!(&env, Error::AlreadyInitialized); } - #[allow(deprecated)] - env.events() - .publish((Symbol::new(&env, "AdminChanged"),), admin.clone()); - - // Emit ContractInitialized event to log when the Oracle goes live - env.events().publish( - (Symbol::new(&env, "ContractInitialized"),), - (admin.clone(), String::from_str(&env, VERSION)), - ); + env.events().publish_event(&AdminChangedEvent { admin: admin.clone() }); + env.events().publish_event(&ContractInitialized { + admin: admin.clone(), + version: String::from_str(&env, VERSION), + }); //_log_admin_action(&env, &admin, AdminAction::InitAdmin, None); let admins = soroban_sdk::vec![&env, admin]; @@ -845,7 +958,6 @@ impl PriceOracle { env.events().publish_event(&AssetAddedEvent { symbol: asset.clone(), }); - log_event(&env, Symbol::new(&env, "asset_added"), asset, 0); Ok(()) } @@ -943,6 +1055,10 @@ impl PriceOracle { env.storage() .instance() .set(&DataKey::PendingAdminTimestamp, &now); + env.events().publish_event(&AdminTransferInitiatedEvent { + current_admin, + new_admin, + }); } /// Finalizes the admin transfer after the timelock expires. @@ -984,6 +1100,7 @@ impl PriceOracle { env.storage() .instance() .remove(&DataKey::PendingAdminTimestamp); + env.events().publish_event(&AdminTransferAcceptedEvent { new_admin }); } /// Permanently renounce ownership of the contract. @@ -1262,7 +1379,6 @@ impl PriceOracle { asset: asset.clone(), price: val, }); - log_event(&env, Symbol::new(&env, "price_updated"), asset, val); return Ok(()); } } @@ -1284,19 +1400,7 @@ impl PriceOracle { env.events().publish_event(&AssetAddedEvent { symbol: asset.clone(), }); - log_event( - &env, - Symbol::new(&env, "asset_added"), - asset.clone(), - normalized, - ); } else { - log_event( - &env, - Symbol::new(&env, "price_updated"), - asset.clone(), - normalized, - ); env.events().publish_event(&PriceUpdatedEvent { asset: asset.clone(), price: normalized, @@ -1376,12 +1480,10 @@ impl PriceOracle { .persistent() .set(&DataKey::CommunityPrice(asset.clone()), &price_data); - log_event( - &env, - Symbol::new(&env, "community_price"), + env.events().publish_event(&CommunityPriceUpdatedEvent { asset, - normalized, - ); + price: normalized, + }); Ok(()) } @@ -1634,12 +1736,6 @@ impl PriceOracle { asset: asset.clone(), price: normalized, }); - log_event( - &env, - Symbol::new(&env, "price_updated"), - asset.clone(), - normalized, - ); // Notify all subscribed contracts of the price update let payload = PriceUpdatePayload { @@ -1930,10 +2026,11 @@ impl PriceOracle { crate::auth::_set_paused(&env, new_paused); // Emit event - env.events().publish( - (Symbol::new(&env, "pause_toggled"),), - (admin1.clone(), admin2.clone(), new_paused), - ); + env.events().publish_event(&PauseToggledEvent { + admin1: admin1.clone(), + admin2: admin2.clone(), + paused: new_paused, + }); Ok(new_paused) } @@ -1984,10 +2081,11 @@ impl PriceOracle { crate::auth::_add_authorized(&env, &new_admin); // Emit event - env.events().publish( - (Symbol::new(&env, "admin_registered"),), - (admin1.clone(), admin2.clone(), new_admin.clone()), - ); + env.events().publish_event(&AdminRegisteredEvent { + admin1: admin1.clone(), + admin2: admin2.clone(), + new_admin: new_admin.clone(), + }); Ok(()) } @@ -2043,10 +2141,11 @@ impl PriceOracle { crate::auth::_remove_authorized(&env, &admin_to_remove); // Emit event - env.events().publish( - (Symbol::new(&env, "admin_removed"),), - (admin1.clone(), admin2.clone(), admin_to_remove.clone()), - ); + env.events().publish_event(&AdminRemovedEvent { + admin1: admin1.clone(), + admin2: admin2.clone(), + removed_admin: admin_to_remove.clone(), + }); Ok(()) } @@ -2100,10 +2199,10 @@ impl PriceOracle { // Set the destroyed flag so the contract is permanently unusable env.storage().instance().set(&DataKey::Destroyed, &true); - env.events().publish( - (Symbol::new(&env, "contract_destroyed"),), - (admin1.clone(), admin2.clone()), - ); + env.events().publish_event(&ContractDestroyedEvent { + admin1: admin1.clone(), + admin2: admin2.clone(), + }); Ok(()) } @@ -2146,10 +2245,7 @@ impl PriceOracle { .persistent() .set(&DataKey::MinQuorumThreshold, &threshold); - env.events().publish( - (Symbol::new(&env, "quorum_set"),), - (admin, threshold), - ); + env.events().publish_event(&QuorumSetEvent { admin, threshold }); Ok(()) } @@ -2215,10 +2311,11 @@ impl PriceOracle { _log_admin_action(&env, &admin, AdminAction::ProposeAction, Some(details)); // Emit event - env.events().publish( - (Symbol::new(&env, "action_proposed"),), - (action_id, admin, action_type), - ); + env.events().publish_event(&ActionProposedEvent { + action_id, + admin, + action_type, + }); Ok(action_id) } @@ -2271,10 +2368,11 @@ impl PriceOracle { ); // Emit event - env.events().publish( - (Symbol::new(&env, "action_voted"),), - (action_id, voter, vote_count), - ); + env.events().publish_event(&ActionVotedEvent { + action_id, + voter, + vote_count, + }); Ok(vote_count) } @@ -2293,8 +2391,7 @@ impl PriceOracle { } crate::auth::_set_vote_delegate(&env, &owner, &delegate); - env.events() - .publish((Symbol::new(&env, "vote_delegated"),), (owner, delegate)); + env.events().publish_event(&VoteDelegatedEvent { owner, delegate }); Ok(()) } @@ -2305,8 +2402,7 @@ impl PriceOracle { owner.require_auth(); crate::auth::_remove_vote_delegate(&env, &owner); - env.events() - .publish((Symbol::new(&env, "vote_delegate_cleared"),), (owner,)); + env.events().publish_event(&VoteDelegateClearedEvent { owner }); Ok(()) } @@ -2381,10 +2477,11 @@ impl PriceOracle { AdminAction::TogglePause, Some(format!("Executed: pause={}", new_paused)), ); - env.events().publish( - (Symbol::new(&env, "pause_toggled"),), - (executor.clone(), new_paused), - ); + env.events().publish_event(&PauseToggledEvent { + admin1: executor.clone(), + admin2: executor.clone(), + paused: new_paused, + }); } AdminAction::RegisterAdmin => { if let Some(ref new_admin) = proposed.target { @@ -2396,10 +2493,11 @@ impl PriceOracle { AdminAction::RegisterAdmin, Some(format!("Registered: {}", new_admin)), ); - env.events().publish( - (Symbol::new(&env, "admin_registered"),), - (executor.clone(), new_admin.clone()), - ); + env.events().publish_event(&AdminRegisteredEvent { + admin1: executor.clone(), + admin2: executor.clone(), + new_admin: new_admin.clone(), + }); } else { return Err(Error::InvalidActionType); } @@ -2418,10 +2516,11 @@ impl PriceOracle { AdminAction::RemoveAdmin, Some(format!("Removed: {}", admin_to_remove)), ); - env.events().publish( - (Symbol::new(&env, "admin_removed"),), - (executor.clone(), admin_to_remove.clone()), - ); + env.events().publish_event(&AdminRemovedEvent { + admin1: executor.clone(), + admin2: executor.clone(), + removed_admin: admin_to_remove.clone(), + }); } else { return Err(Error::InvalidActionType); } @@ -2458,10 +2557,10 @@ impl PriceOracle { proposed.executed = true; _log_admin_action(&env, &executor, AdminAction::SelfDestruct, None); - env.events().publish( - (Symbol::new(&env, "contract_destroyed"),), - (executor.clone(),), - ); + env.events().publish_event(&ContractDestroyedEvent { + admin1: executor.clone(), + admin2: executor.clone(), + }); } AdminAction::Upgrade => { // Parse wasm hash from data (expected as hex string) @@ -2474,10 +2573,9 @@ impl PriceOracle { AdminAction::Upgrade, Some(format!("Data: {}", proposed.data.to_string())), ); - env.events().publish( - (Symbol::new(&env, "contract_upgraded"),), - (executor.clone(),), - ); + env.events().publish_event(&ContractUpgradedEvent { + executor: executor.clone(), + }); } _ => return Err(Error::InvalidActionType), } @@ -2486,10 +2584,7 @@ impl PriceOracle { crate::auth::_set_proposed_action(&env, action_id, &proposed); // Emit execution event - env.events().publish( - (Symbol::new(&env, "action_executed"),), - (action_id, executor), - ); + env.events().publish_event(&ActionExecutedEvent { action_id, executor }); Ok(()) } @@ -2563,10 +2658,7 @@ impl PriceOracle { ); // Emit event - env.events().publish( - (Symbol::new(&env, "action_cancelled"),), - (action_id, canceller), - ); + env.events().publish_event(&ActionCancelledEvent { action_id, canceller }); Ok(()) } @@ -2588,10 +2680,10 @@ impl PriceOracle { ); crate::auth::_set_council(&env, &council); - env.events().publish( - (Symbol::new(&env, "council_set"),), - (admin.clone(), council.clone()), - ); + env.events().publish_event(&CouncilSetEvent { + admin: admin.clone(), + council: council.clone(), + }); } /// Get the current Community Council address. @@ -2626,8 +2718,7 @@ impl PriceOracle { crate::auth::_set_frozen(&env, true); // Emit event - env.events() - .publish((Symbol::new(&env, "emergency_freeze"),), (council.clone(),)); + env.events().publish_event(&EmergencyFreezeEvent { council: council.clone() }); Ok(()) } @@ -2816,10 +2907,11 @@ impl PriceOracle { }; env.storage().persistent().set(&key, &req); - env.events().publish( - (Symbol::new(&env, "unbond_requested"),), - (relayer, req.request_ledger, amount), - ); + env.events().publish_event(&UnbondRequestedEvent { + relayer, + request_ledger: req.request_ledger, + amount, + }); Ok(()) } @@ -2856,10 +2948,10 @@ impl PriceOracle { env.storage().persistent().remove(&key); - env.events().publish( - (Symbol::new(&env, "stake_released"),), - (relayer, req.amount), - ); + env.events().publish_event(&StakeReleasedEvent { + relayer, + amount: req.amount, + }); Ok(req.amount) }