Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 47 additions & 12 deletions contracts/contracts/streampay-stream/src/events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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),
);
}
3 changes: 3 additions & 0 deletions contracts/contracts/streampay-stream/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(())
}

Expand All @@ -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(())
}

Expand Down Expand Up @@ -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(())
}

Expand Down
119 changes: 118 additions & 1 deletion contracts/contracts/streampay-stream/src/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
);
}
Loading