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
3 changes: 3 additions & 0 deletions contracts/stream/src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,7 @@ pub enum Error {
ReentrancyForbidden = 16,
OperatorAlreadySet = 17,
NotInitialized = 18,
InvalidRecipient = 19,
BackdatedStream = 20,
StreamUnderfunded = 21,
}
11 changes: 8 additions & 3 deletions contracts/stream/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -708,12 +708,17 @@ impl DripStream {
.instance()
.get::<_, Address>(&DataKey::Operator)
{
if existing != operator {
return Err(Error::OperatorAlreadySet);
// Issue #417: If the new operator is the same, it's idempotent — return early.
// If different, replace atomically: revoke the old one, then set the new one.
// This allows key rotation in a single transaction without a no-operator gap.
if existing == operator {
return Ok(());
}
return Ok(());
// Revoke the old operator
events::operator_revoked(&env, &caller);
}

// Set the new operator
env.storage().instance().set(&DataKey::Operator, &operator);
events::operator_set(&env, &caller, &operator);
Ok(())
Expand Down
33 changes: 29 additions & 4 deletions contracts/stream/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1169,15 +1169,40 @@ fn set_operator_rejects_on_cancelled_stream() {
}

#[test]
fn set_operator_requires_revoke_before_replacement() {
fn set_operator_replaces_existing_operator_atomically() {
// Issue #417: set_operator should replace an existing operator in one call,
// allowing atomic key rotation without a no-operator gap.
let s = Setup::new(100, 3600, false);
let op1 = Address::generate(&s.env);
let op2 = Address::generate(&s.env);

// Set initial operator
s.client.set_operator(&s.sender, &op1);
assert_eq!(s.client.operator(), Some(op1.clone()));
let result = s.client.try_set_operator(&s.sender, &op2);
assert_eq!(result, Err(Ok(Error::OperatorAlreadySet)));
assert_eq!(s.client.operator(), Some(op1));

// Replace with a different operator - should succeed now
s.client.set_operator(&s.sender, &op2);
assert_eq!(s.client.operator(), Some(op2.clone()));

// Verify we can replace again
let op3 = Address::generate(&s.env);
s.client.set_operator(&s.sender, &op3);
assert_eq!(s.client.operator(), Some(op3.clone()));
}

#[test]
fn set_operator_is_idempotent_for_same_address() {
// Issue #417: Setting the same operator twice should be idempotent.
// The early return keeps this operation idempotent.
let s = Setup::new(100, 3600, false);
let operator = Address::generate(&s.env);

s.client.set_operator(&s.sender, &operator);
assert_eq!(s.client.operator(), Some(operator.clone()));

// Setting the same operator again should succeed without error
s.client.set_operator(&s.sender, &operator);
assert_eq!(s.client.operator(), Some(operator));
}

#[test]
Expand Down