From fcd95fe201a55390c204a68ca2f069deed8b8e06 Mon Sep 17 00:00:00 2001 From: devUnixx Date: Wed, 29 Apr 2026 03:02:19 +0000 Subject: [PATCH] security(stream): document exact-deposit transfer, add approval audit test (#65) --- contracts/stream/src/lib.rs | 9 +++++++++ contracts/stream/src/test.rs | 25 +++++++++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/contracts/stream/src/lib.rs b/contracts/stream/src/lib.rs index 7de4d52..33c60bf 100644 --- a/contracts/stream/src/lib.rs +++ b/contracts/stream/src/lib.rs @@ -132,6 +132,15 @@ impl StreamContract { /// Create a salary stream with an optional cliff period (#123). /// /// `cliff_time` — ledger timestamp before which nothing is claimable (0 = no cliff). + /// + /// # Token approval / transfer security (#65) + /// + /// This function calls `token::transfer(employer → contract, deposit)` — it transfers + /// **exactly** the `deposit` amount and nothing more. No `approve` call is made by + /// the contract; the caller must have pre-approved at least `deposit` tokens to this + /// contract address before invoking `create_stream`. This design prevents + /// over-approval: the contract can never pull more than the caller explicitly + /// authorised for this single transaction. pub fn create_stream( env: Env, employer: Address, diff --git a/contracts/stream/src/test.rs b/contracts/stream/src/test.rs index e8b9f8a..2edcbf3 100644 --- a/contracts/stream/src/test.rs +++ b/contracts/stream/src/test.rs @@ -51,6 +51,31 @@ fn test_create_stream() { assert!(!s.locked); } +/// Issue #65: create_stream transfers exactly `deposit` tokens — no more, no less. +/// Verifies the exact-deposit approval model: employer balance decreases by exactly +/// `deposit` and the contract balance increases by exactly `deposit`. +#[test] +fn test_create_stream_transfers_exact_deposit() { + let (env, client) = setup(); + let admin = Address::generate(&env); + let employer = Address::generate(&env); + let employee = Address::generate(&env); + let token_id = setup_token(&env, &employer); + let token = paystream_token::TokenContractClient::new(&env, &token_id); + + let deposit: i128 = 5_000; + let employer_balance_before = token.balance(&employer); + + client.initialize(&admin); + client.set_min_deposit(&admin, &0, &100); + client.create_stream(&employer, &employee, &token_id, &deposit, &1, &0, &0, &0); + + // Employer lost exactly `deposit` tokens. + assert_eq!(token.balance(&employer), employer_balance_before - deposit); + // Contract holds exactly `deposit` tokens. + assert_eq!(token.balance(&env.current_contract_address()), deposit); +} + #[test] fn test_claimable_increases_with_time() { let (env, client) = setup();