From 901d753056cca823b903b27e044d893ed5f6b5f5 Mon Sep 17 00:00:00 2001 From: shogun444 Date: Mon, 29 Jun 2026 19:10:16 +0530 Subject: [PATCH] feat: admin audit events for set_paused, set_admin, set_token_allowed --- .../contracts/streampay-stream/src/events.rs | 59 +++++++-- .../contracts/streampay-stream/src/lib.rs | 3 + .../contracts/streampay-stream/src/test.rs | 119 +++++++++++++++++- 3 files changed, 168 insertions(+), 13 deletions(-) diff --git a/contracts/contracts/streampay-stream/src/events.rs b/contracts/contracts/streampay-stream/src/events.rs index a34e7f91..d5a05c0d 100644 --- a/contracts/contracts/streampay-stream/src/events.rs +++ b/contracts/contracts/streampay-stream/src/events.rs @@ -6,17 +6,20 @@ //! //! ## Event schema (for Horizon indexers and the transactional outbox) //! -//! | Event | topic[1] | Data tuple (in order) | -//! |-------------|---------------|----------------------------------------------------------------------------------------------------------| -//! | created | "created" | (stream_id: u64, sender: Address, recipient: Address, token: Address, total_amount: i128, timestamp: u64) | -//! | started | "started" | (stream_id: u64, start_time: u64, end_time: u64, timestamp: u64) | -//! | withdrawn | "withdrawn" | (stream_id: u64, recipient: Address, amount: i128, timestamp: u64) | -//! | settled | "settled" | (stream_id: u64, recipient: Address, total_amount: i128, timestamp: u64) | -//! | paused | "paused" | (stream_id: u64, sender: Address, pause_time: u64, timestamp: u64) | -//! | resumed | "resumed" | (stream_id: u64, sender: Address, end_time: u64, timestamp: u64) | -//! | cancelled | "cancelled" | (stream_id: u64, cancelled_by: Address, returned_amount: i128, released_amount: i128, timestamp: u64) | -//! | amended | "amended" | (stream_id: u64, amended_by: Address, new_rate_per_second: i128, new_end_time: u64, timestamp: u64) | -//! | admin_action| "admin_action"| (stream_id: u64, admin: Address, action: Symbol, timestamp: u64) | +//! | Event | topic[1] | Data tuple (in order) | +//! |-----------------|---------------------|----------------------------------------------------------------------------------------------------------| +//! | created | "created" | (stream_id: u64, sender: Address, recipient: Address, token: Address, total_amount: i128, timestamp: u64) | +//! | started | "started" | (stream_id: u64, start_time: u64, end_time: u64, timestamp: u64) | +//! | withdrawn | "withdrawn" | (stream_id: u64, recipient: Address, amount: i128, timestamp: u64) | +//! | settled | "settled" | (stream_id: u64, recipient: Address, total_amount: i128, timestamp: u64) | +//! | paused | "paused" | (stream_id: u64, sender: Address, pause_time: u64, timestamp: u64) | +//! | resumed | "resumed" | (stream_id: u64, sender: Address, end_time: u64, timestamp: u64) | +//! | cancelled | "cancelled" | (stream_id: u64, cancelled_by: Address, returned_amount: i128, released_amount: i128, timestamp: u64) | +//! | amended | "amended" | (stream_id: u64, amended_by: Address, new_rate_per_second: i128, new_end_time: u64, timestamp: u64) | +//! | admin_action | "admin_action" | (stream_id: u64, admin: Address, action: Symbol, timestamp: u64) | +//! | paused_set | "set_pause" | (admin: Address, paused: bool, timestamp: u64) | +//! | admin_changed | "set_admin" | (admin: Address, new_admin: Address, timestamp: u64) | +//! | token_allowed_set| "set_token" | (admin: Address, token: Address, allowed: bool, timestamp: u64) | //! //! All events are emitted AFTER successful state mutation and any token transfer. //! Failed calls (returning Err) emit no events. @@ -151,7 +154,39 @@ pub fn amended( /// indexers can track admin operations on behalf of senders. pub fn admin_action(env: &Env, stream_id: u64, admin: &Address, action: Symbol, timestamp: u64) { env.events().publish( - (symbol_short!("stream"), symbol_short!("admin_action")), + (symbol_short!("stream"), Symbol::new(env, "admin_action")), (stream_id, admin.clone(), action, timestamp), ); } + +/// Emitted when the admin toggles the global pause flag via +/// [`Contract::set_paused`]. +pub fn paused_set(env: &Env, admin: &Address, paused: bool, timestamp: u64) { + env.events().publish( + (symbol_short!("stream"), symbol_short!("set_pause")), + (admin.clone(), paused, timestamp), + ); +} + +/// Emitted when the admin role is transferred via [`Contract::set_admin`]. +pub fn admin_changed(env: &Env, admin: &Address, new_admin: &Address, timestamp: u64) { + env.events().publish( + (symbol_short!("stream"), symbol_short!("set_admin")), + (admin.clone(), new_admin.clone(), timestamp), + ); +} + +/// Emitted when a token's allowlist status is changed via +/// [`Contract::set_token_allowed`]. +pub fn token_allowed_set( + env: &Env, + admin: &Address, + token: &Address, + allowed: bool, + timestamp: u64, +) { + env.events().publish( + (symbol_short!("stream"), symbol_short!("set_token")), + (admin.clone(), token.clone(), allowed, timestamp), + ); +} diff --git a/contracts/contracts/streampay-stream/src/lib.rs b/contracts/contracts/streampay-stream/src/lib.rs index 264e24dc..ef7cdf69 100644 --- a/contracts/contracts/streampay-stream/src/lib.rs +++ b/contracts/contracts/streampay-stream/src/lib.rs @@ -243,6 +243,7 @@ impl Contract { pub fn set_paused(env: Env, admin: Address, paused: bool) -> Result<(), Error> { require_admin(&env, &admin)?; storage::set_paused(&env, paused); + events::paused_set(&env, &admin, paused, env.ledger().timestamp()); Ok(()) } @@ -256,6 +257,7 @@ impl Contract { pub fn set_admin(env: Env, admin: Address, new_admin: Address) -> Result<(), Error> { require_admin(&env, &admin)?; storage::set_admin(&env, &new_admin); + events::admin_changed(&env, &admin, &new_admin, env.ledger().timestamp()); Ok(()) } @@ -286,6 +288,7 @@ impl Contract { ) -> Result<(), Error> { require_admin(&env, &admin)?; storage::set_token_allowed(&env, &token, allowed); + events::token_allowed_set(&env, &admin, &token, allowed, env.ledger().timestamp()); Ok(()) } diff --git a/contracts/contracts/streampay-stream/src/test.rs b/contracts/contracts/streampay-stream/src/test.rs index f1eb42c2..c8fc143c 100644 --- a/contracts/contracts/streampay-stream/src/test.rs +++ b/contracts/contracts/streampay-stream/src/test.rs @@ -16,7 +16,7 @@ use super::*; use soroban_sdk::testutils::{Address as _, Events as _, Ledger as _}; -use soroban_sdk::{token::StellarAssetClient, Address, Env}; +use soroban_sdk::{symbol_short, token::StellarAssetClient, Address, Env}; /// All addresses and tokens needed by a single test. We use a /// fixed-size array on the stack (no `Vec`) because the contract @@ -1188,3 +1188,120 @@ fn pause_resume_preserves_vested_amount() { let resumed_vested = client.stream_balance(&id); assert_eq!(resumed_vested, 1000); } + +// ── Admin audit event tests ────────────────────────────────────────────────── + +#[test] +fn set_paused_emits_event() { + let data = setup_init(); + let client = contract_client(&data.env); + + client.initialize(&data.admin); + + // Clear any events from initialization + data.env.events().all(); + + client.set_paused(&data.admin, &true); + + let events = data.env.events().all(); + assert_eq!(events.len(), 1, "set_paused should emit exactly 1 event"); + + let (_, topics, data) = events.get(0).unwrap(); + assert_eq!(topics.len(), 2, "Event should have 2 topics"); + assert_eq!( + topics.get(0).unwrap(), + symbol_short!("stream"), + "topic[0] should be stream" + ); + assert_eq!( + topics.get(1).unwrap(), + symbol_short!("set_pause"), + "topic[1] should be set_paused" + ); + assert_eq!(data.len(), 3, "Data should have 3 fields (admin, paused, timestamp)"); +} + +#[test] +fn set_admin_emits_event() { + let data = setup_init(); + let client = contract_client(&data.env); + + client.initialize(&data.admin); + + let new_admin = Address::generate(&data.env); + + // Clear any events from initialization + data.env.events().all(); + + client.set_admin(&data.admin, &new_admin); + + let events = data.env.events().all(); + assert_eq!(events.len(), 1, "set_admin should emit exactly 1 event"); + + let (_, topics, data) = events.get(0).unwrap(); + assert_eq!(topics.len(), 2, "Event should have 2 topics"); + assert_eq!( + topics.get(0).unwrap(), + symbol_short!("stream"), + "topic[0] should be stream" + ); + assert_eq!( + topics.get(1).unwrap(), + symbol_short!("set_admin"), + "topic[1] should be set_admin" + ); + assert_eq!(data.len(), 3, "Data should have 3 fields (admin, new_admin, timestamp)"); +} + +#[test] +fn set_token_allowed_emits_event() { + let data = setup_init(); + let client = contract_client(&data.env); + + client.initialize(&data.admin); + + // Clear any events from initialization + data.env.events().all(); + + client.set_token_allowed(&data.admin, &data.tokens[0], &false); + + let events = data.env.events().all(); + assert_eq!(events.len(), 1, "set_token_allowed should emit exactly 1 event"); + + let (_, topics, data) = events.get(0).unwrap(); + assert_eq!(topics.len(), 2, "Event should have 2 topics"); + assert_eq!( + topics.get(0).unwrap(), + symbol_short!("stream"), + "topic[0] should be stream" + ); + assert_eq!( + topics.get(1).unwrap(), + symbol_short!("set_token"), + "topic[1] should be set_token" + ); + assert_eq!(data.len(), 4, "Data should have 4 fields (admin, token, allowed, timestamp)"); +} + +#[test] +fn set_paused_failure_emits_no_event() { + let data = setup_init(); + let client = contract_client(&data.env); + + client.initialize(&data.admin); + + let impostor = Address::generate(&data.env); + + // Clear any events from initialization + data.env.events().all(); + + // Call with wrong admin — should fail + let _ = client.try_set_paused(&impostor, &true); + + let events = data.env.events().all(); + assert!( + events.is_empty(), + "Failed set_paused should not emit events, got: {:?}", + events + ); +}