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
6 changes: 6 additions & 0 deletions contracts/factory/src/deploy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,12 @@ pub fn deploy_stream(
stream_id: u64,
init_args: Vec<Val>,
) -> Result<Address, Error> {
// Validate WASM hash registry prior to instantiation (Issue #788).
// Verify that the WASM hash is registered and available before attempting deployment.
// This prevents deploying with a stale or invalid hash that would fail mid-transaction.
let _ = env.ledger().get_wasm(wasm_hash)
.map_err(|_| Error::InvalidWasmHash)?;

// Derive a deterministic salt from the stream ID so each stream gets a
// unique, reproducible contract address.
let salt = compute_salt(env, stream_id);
Expand Down
2 changes: 2 additions & 0 deletions contracts/factory/src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,4 +63,6 @@ pub enum Error {
InvalidGovernor = 31,
/// The salt used for deploying the stream contract has already been used.
SaltAlreadyUsed = 32,
/// `cancel_batch_streams` was called with duplicate stream addresses (Issue #787).
DuplicateStreamId = 33,
}
19 changes: 9 additions & 10 deletions contracts/factory/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -604,21 +604,20 @@ impl DripFactory {
return Err(Error::BatchTooLarge);
}

// Deduplicate addresses to prevent attempting multiple cancels on the same stream.
// Issue #416, #627: If a duplicated address is passed, the first cancel succeeds
// and sets FLAG_CANCELLED; the second cancel on the now-cancelled stream would
// return Error::StreamCancelled, which the non-try_ variant turns into a panic.
// Deduplicating the list ensures each unique stream is cancelled exactly once.
// Uses a Map-backed set (O(log n) lookup/insert) rather than an O(n²) nested loop scan.
// Validate that no duplicate stream IDs are present in the batch (Issue #787).
// Check for duplicates using a Map-backed set (O(log n) lookup/insert).
let mut seen = Map::new(&env);
let mut unique_addresses: Vec<Address> = Vec::new(&env);
for stream_addr in stream_addresses.iter() {
if !seen.contains_key(stream_addr.clone()) {
seen.set(stream_addr.clone(), ());
unique_addresses.push_back(stream_addr);
if seen.contains_key(stream_addr.clone()) {
return Err(Error::DuplicateStreamId);
}
seen.set(stream_addr.clone(), ());
}

// Process cancellations for all unique stream addresses.
// Issue #416, #627: The validation above ensures each address appears exactly once.
let unique_addresses = stream_addresses;

for stream_addr in unique_addresses.iter() {
let stream_client = drip_stream::DripStreamClient::new(&env, &stream_addr);
stream_client.cancel(&sender);
Expand Down
22 changes: 18 additions & 4 deletions contracts/stream/src/events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,29 +84,43 @@ pub fn withdrawn(
);
}

pub fn cancelled(env: &Env, sender: &Address, refund_amount: i128, withdrawn_so_far: i128) {
pub fn cancelled(
env: &Env,
sender: &Address,
refund_amount: i128,
withdrawn_so_far: i128,
remaining_deposit: i128,
) {
assert_non_negative_amount(env, refund_amount);
assert_non_negative_amount(env, withdrawn_so_far);
assert_non_negative_amount(env, remaining_deposit);

let sequence = next_sequence(env);
env.events().publish(
(symbol_short!("cancelled"), sender.clone(), sequence),
(refund_amount, withdrawn_so_far),
(refund_amount, withdrawn_so_far, remaining_deposit),
);
}

/// Recipient-initiated cancellation via `force_cancel`, after the sender left
/// the stream paused past the threshold. Distinct from `cancelled` (the
/// sender/operator-initiated path via `cancel`) so consumers of the event log
/// can tell the two apart without correlating who signed the transaction.
pub fn force_cancelled(env: &Env, sender: &Address, refund_amount: i128, withdrawn_so_far: i128) {
pub fn force_cancelled(
env: &Env,
sender: &Address,
refund_amount: i128,
withdrawn_so_far: i128,
remaining_deposit: i128,
) {
assert_non_negative_amount(env, refund_amount);
assert_non_negative_amount(env, withdrawn_so_far);
assert_non_negative_amount(env, remaining_deposit);

let sequence = next_sequence(env);
env.events().publish(
(symbol_short!("force_cxl"), sender.clone(), sequence),
(refund_amount, withdrawn_so_far),
(refund_amount, withdrawn_so_far, remaining_deposit),
);
}

Expand Down
4 changes: 2 additions & 2 deletions contracts/stream/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -432,7 +432,7 @@ impl DripStream {
tk.transfer(&contract_addr, &info.sender, &refund_to_sender);
}

events::cancelled(env, &info.sender, refund_to_sender, total_withdrawn);
events::cancelled(env, &info.sender, refund_to_sender, total_withdrawn, refund_to_sender);
Ok(())
}

Expand Down Expand Up @@ -1084,7 +1084,7 @@ impl DripStream {
tk.transfer(&contract_addr, &info.sender, &refund_to_sender);
}

events::force_cancelled(env, &info.sender, refund_to_sender, total_withdrawn);
events::force_cancelled(env, &info.sender, refund_to_sender, total_withdrawn, refund_to_sender);
Ok(())
}

Expand Down
15 changes: 10 additions & 5 deletions contracts/token-vault/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,8 @@ fn withdraw_payouts(
expected_balance = expected_balance
.checked_sub(amount)
.ok_or(Error::ArithmeticOverflow)?;
tk.transfer(&env.current_contract_address(), &to, &amount);
tk.transfer(&env.current_contract_address(), &to, &amount)
.map_err(|_| Error::WithdrawTransferFailed)?;
let new_balance = vault_balance(env)?;
if new_balance != expected_balance {
return Err(Error::WithdrawTransferFailed);
Expand Down Expand Up @@ -252,7 +253,8 @@ impl TokenVault {
}

let tk = token_client(&env)?;
tk.transfer(&from, &env.current_contract_address(), &amount);
tk.transfer(&from, &env.current_contract_address(), &amount)
.map_err(|_| Error::DepositTransferFailed)?;
let new_balance = tk.balance(&env.current_contract_address());
if new_balance > max {
return Err(Error::LimitExceeded);
Expand Down Expand Up @@ -330,7 +332,8 @@ impl TokenVault {
let contract_addr = env.current_contract_address();

for deposit in deposits.iter() {
tk.transfer(&deposit.from, &contract_addr, &deposit.amount);
tk.transfer(&deposit.from, &contract_addr, &deposit.amount)
.map_err(|_| Error::DepositTransferFailed)?;
let current_balance = tk.balance(&contract_addr);
events::deposited(&env, &deposit.from, deposit.amount, current_balance);
}
Expand Down Expand Up @@ -729,7 +732,8 @@ impl TokenVault {
}

let tk = token::Client::new(&env, &token);
tk.transfer(&from, &env.current_contract_address(), &amount);
tk.transfer(&from, &env.current_contract_address(), &amount)
.map_err(|_| Error::DepositTransferFailed)?;

let current_acc_balance = get_token_account_balance(&env, &token, &from);
let new_acc_balance = current_acc_balance
Expand Down Expand Up @@ -776,7 +780,8 @@ impl TokenVault {
return Err(Error::LimitExceeded);
}

tk.transfer(&env.current_contract_address(), &to, &amount);
tk.transfer(&env.current_contract_address(), &to, &amount)
.map_err(|_| Error::WithdrawTransferFailed)?;

if current_acc_balance >= amount {
set_token_account_balance(&env, &token, &caller, current_acc_balance - amount);
Expand Down