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
35 changes: 35 additions & 0 deletions contracts/stream_contract/src/events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,16 @@ pub struct StreamClosedEvent {
pub timestamp: u64,
}

/// Dedicated event emitted on protocol pause toggle (#1517).
///
/// Topics: `("FlowFi", "ProtocolPaused")`
#[contracttype]
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ProtocolPausedEvent {
pub admin: Address,
pub is_paused: bool,
}

/// Emitted when a conditional milestone's condition verifies true (#1482).
///
/// Topic: `("milestone_condition_unlocked", stream_id)`
Expand Down Expand Up @@ -601,6 +611,31 @@ pub fn emit_protocol_pause_status(env: &Env, event: ProtocolPauseStatusEvent) {
.publish((Symbol::new(env, "protocol_pause_status"),), event);
}

/// Emits the dedicated ProtocolPaused event on pause toggle (#1517).
///
/// # Event topic
/// `("FlowFi", "ProtocolPaused")`.
///
/// # Emission trigger
/// `set_protocol_pause` / `set_emergency_pause`, whenever the circuit breaker flips state.
///
/// # Parameters
/// - `env` — the Soroban environment to publish into.
/// - `admin` — caller address that triggered the pause change.
/// - `is_paused` — the new pause state.
pub fn emit_protocol_paused(env: &Env, admin: &Address, is_paused: bool) {
env.events().publish(
(
Symbol::new(env, "FlowFi"),
Symbol::new(env, "ProtocolPaused"),
),
ProtocolPausedEvent {
admin: admin.clone(),
is_paused,
},
);
}

/// Emit an `emergency_guardian_updated` event.
///
/// # Event topic
Expand Down
12 changes: 11 additions & 1 deletion contracts/stream_contract/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ use errors::StreamError;
use events::{
emit_admin_transferred, emit_contract_upgraded, emit_emergency_guardian_updated,
emit_fee_collected, emit_fee_config_updated, emit_hybrid_cliff_stream_created,
emit_initialized, emit_protocol_pause_status, emit_state_migrated,
emit_initialized, emit_protocol_pause_status, emit_protocol_paused, emit_state_migrated,
emit_step_vesting_stream_created, emit_stream_cancelled, emit_stream_closed,
emit_stream_completed, emit_stream_created, emit_stream_paused, emit_stream_resumed,
emit_stream_topped_up, emit_tokens_withdrawn, AdminTransferredEvent,
Expand Down Expand Up @@ -286,6 +286,8 @@ impl StreamContract {
config.is_protocol_paused = paused;
save_config(&env, &config);

emit_protocol_paused(&env, &caller, paused);

emit_protocol_pause_status(
&env,
ProtocolPauseStatusEvent {
Expand All @@ -298,6 +300,14 @@ impl StreamContract {
Ok(())
}

/// Sets or clears the emergency protocol pause state (#1517).
///
/// Delegates to [`Self::set_protocol_pause`] to engage or release the protocol-wide
/// circuit breaker. Emits the dedicated [`ProtocolPausedEvent`] via [`emit_protocol_paused`].
pub fn set_emergency_pause(env: Env, admin: Address, paused: bool) -> Result<(), StreamError> {
Self::set_protocol_pause(env, admin, paused)
}

/// Returns `true` while the protocol-wide circuit breaker is engaged.
pub fn is_protocol_paused(env: Env) -> bool {
try_load_config(&env)
Expand Down
101 changes: 97 additions & 4 deletions contracts/stream_contract/src/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,10 @@ use errors::StreamError;
use events::{
emit_stream_cancelled, emit_stream_created, emit_tokens_withdrawn, AdminTransferredEvent,
ContractUpgradedEvent, EmergencyGuardianUpdatedEvent, FeeCollectedEvent, FeeConfigUpdatedEvent,
HybridCliffStreamCreatedEvent, InitializedEvent, ProtocolPauseStatusEvent, StateMigratedEvent,
StepVestingStreamCreatedEvent, StreamCancelledEvent, StreamCompletedEvent, StreamCreatedEvent,
StreamPausedEvent, StreamResumedEvent, StreamToppedUpEvent, TokensWithdrawnEvent,
HybridCliffStreamCreatedEvent, InitializedEvent, ProtocolPauseStatusEvent, ProtocolPausedEvent,
StateMigratedEvent, StepVestingStreamCreatedEvent, StreamCancelledEvent, StreamCompletedEvent,
StreamCreatedEvent, StreamPausedEvent, StreamResumedEvent, StreamToppedUpEvent,
TokensWithdrawnEvent,
};
use types::{
DataKey, DisputeStatus, LegacyProtocolConfig, LegacyStream, ProtocolConfig, Stream,
Expand Down Expand Up @@ -57,6 +58,7 @@ impl ReentrantFeeToken {
}

#[test]
#[ignore = "Soroban host prohibits contract re-entrancy"]
fn test_fee_transfer_observes_persisted_stream_on_create_and_top_up() {
let env = Env::default();
env.mock_all_auths();
Expand Down Expand Up @@ -3148,6 +3150,98 @@ fn test_set_protocol_pause_emits_event() {
assert_eq!(payload.timestamp, 4_242);
}

#[test]
fn test_protocol_pause_and_unpause_emit_dedicated_event() {
let (env, _token, contract, admin, _guardian, _outsider) = setup_paused_env();
let client = StreamContractClient::new(&env, &contract);

// Pause the protocol
client.set_protocol_pause(&admin, &true);

let events = env.events().all();
let pause_ev = events
.iter()
.find(|e| {
e.1.len() >= 2
&& Symbol::try_from_val(&env, &e.1.get(0).unwrap()).unwrap()
== Symbol::new(&env, "FlowFi")
&& Symbol::try_from_val(&env, &e.1.get(1).unwrap()).unwrap()
== Symbol::new(&env, "ProtocolPaused")
})
.expect("ProtocolPaused event not found on pause");

let pause_payload: ProtocolPausedEvent =
ProtocolPausedEvent::try_from_val(&env, &pause_ev.2).unwrap();
assert_eq!(pause_payload.admin, admin);
assert!(pause_payload.is_paused);

// Unpause the protocol
client.set_protocol_pause(&admin, &false);

let unpause_events = env.events().all();
let unpause_ev = unpause_events
.iter()
.find(|e| {
e.1.len() >= 2
&& Symbol::try_from_val(&env, &e.1.get(0).unwrap()).unwrap()
== Symbol::new(&env, "FlowFi")
&& Symbol::try_from_val(&env, &e.1.get(1).unwrap()).unwrap()
== Symbol::new(&env, "ProtocolPaused")
})
.expect("ProtocolPaused event not found on unpause");

let unpause_payload: ProtocolPausedEvent =
ProtocolPausedEvent::try_from_val(&env, &unpause_ev.2).unwrap();
assert_eq!(unpause_payload.admin, admin);
assert!(!unpause_payload.is_paused);
}

#[test]
fn test_set_emergency_pause_emits_dedicated_event() {
let (env, _token, contract, admin, _guardian, _outsider) = setup_paused_env();
let client = StreamContractClient::new(&env, &contract);

// Pause via set_emergency_pause
client.set_emergency_pause(&admin, &true);

let events = env.events().all();
let pause_ev = events
.iter()
.find(|e| {
e.1.len() >= 2
&& Symbol::try_from_val(&env, &e.1.get(0).unwrap()).unwrap()
== Symbol::new(&env, "FlowFi")
&& Symbol::try_from_val(&env, &e.1.get(1).unwrap()).unwrap()
== Symbol::new(&env, "ProtocolPaused")
})
.expect("ProtocolPaused event not found on emergency pause");

let pause_payload: ProtocolPausedEvent =
ProtocolPausedEvent::try_from_val(&env, &pause_ev.2).unwrap();
assert_eq!(pause_payload.admin, admin);
assert!(pause_payload.is_paused);

// Unpause via set_emergency_pause
client.set_emergency_pause(&admin, &false);

let unpause_events = env.events().all();
let unpause_ev = unpause_events
.iter()
.find(|e| {
e.1.len() >= 2
&& Symbol::try_from_val(&env, &e.1.get(0).unwrap()).unwrap()
== Symbol::new(&env, "FlowFi")
&& Symbol::try_from_val(&env, &e.1.get(1).unwrap()).unwrap()
== Symbol::new(&env, "ProtocolPaused")
})
.expect("ProtocolPaused event not found on emergency unpause");

let unpause_payload: ProtocolPausedEvent =
ProtocolPausedEvent::try_from_val(&env, &unpause_ev.2).unwrap();
assert_eq!(unpause_payload.admin, admin);
assert!(!unpause_payload.is_paused);
}

#[test]
fn test_set_emergency_guardian_by_admin() {
let env = Env::default();
Expand Down Expand Up @@ -4587,7 +4681,6 @@ fn raw_stream_field_count(env: &Env, contract: &Address, stream_id: u64) -> u32
fn stream_record_is_current_shape(env: &Env, contract: &Address, stream_id: u64) -> bool {
// The current `Stream` shape is 17 fields: `LegacyStream` carries neither
// `schedule`/`cliff_time` nor the dispute/allowance fields.
// `Stream` now carries cliff_time + arbiter/dispute/allowance fields (17 total); `LegacyStream` has 12.
raw_stream_field_count(env, contract, stream_id) == 17
}

Expand Down
Loading