From 090ddcb1205b1e8daec21bdd45e8a86f25cb0f8c Mon Sep 17 00:00:00 2001 From: root Date: Fri, 21 Aug 2026 19:11:20 +0000 Subject: [PATCH] fix(escrow): deduplicate sponsor contributions to prevent MAX_SPONSORS exhaustion A single address could call contribute() repeatedly, each time consuming a new slot against MAX_SPONSORS (20) and permanently locking out other would-be co-funders. The cap was intended to bound distinct sponsors, not contribution calls. Now contribute() checks whether the sponsor already has an existing Contribution entry for this issue_id. If so, it adds to that entry in place rather than appending a new indexed slot. Only genuinely new sponsors consume a slot against MAX_SPONSORS. Closes #139 --- contracts/escrow/src/lib.rs | 91 +++++++++++++++++++++++-------------- 1 file changed, 56 insertions(+), 35 deletions(-) diff --git a/contracts/escrow/src/lib.rs b/contracts/escrow/src/lib.rs index 7d96937..71780a4 100644 --- a/contracts/escrow/src/lib.rs +++ b/contracts/escrow/src/lib.rs @@ -133,51 +133,72 @@ impl EscrowContract { /// funder intended. Rejects `EscrowNotFound`, `AlreadyPaid`, /// `AlreadyRefunded`, and `TooManySponsors` once `MAX_SPONSORS` /// contributions have already been recorded. - pub fn contribute( - env: Env, - issue_id: u64, - sponsor: Address, - amount: i128, - ) -> Result<(), Error> { - sponsor.require_auth(); - - if amount <= 0 { - return Err(Error::InvalidAmount); - } - - let key = DataKey::Escrow(issue_id); - let mut escrow: Escrow = env - .storage() - .persistent() - .get(&key) - .ok_or(Error::EscrowNotFound)?; - - match escrow.status { - EscrowStatus::Paid => return Err(Error::AlreadyPaid), - EscrowStatus::Refunded => return Err(Error::AlreadyRefunded), - EscrowStatus::Funded => {} - } - - if escrow.contributor_count >= MAX_SPONSORS { - return Err(Error::TooManySponsors); + pub fn contribute( + env: Env, + issue_id: u64, + sponsor: Address, + amount: i128, + ) -> Result<(), Error> { + sponsor.require_auth(); + + if amount <= 0 { + return Err(Error::InvalidAmount); + } + + let key = DataKey::Escrow(issue_id); + let mut escrow: Escrow = env + .storage() + .persistent() + .get(&key) + .ok_or(Error::EscrowNotFound)?; + + match escrow.status { + EscrowStatus::Paid => return Err(Error::AlreadyPaid), + EscrowStatus::Refunded => return Err(Error::AlreadyRefunded), + EscrowStatus::Funded => {} + } + + // Check if sponsor already contributed to this issue_id. + // If so, update their existing contribution instead of consuming a new slot. + // This prevents a single address from exhausting MAX_SPONSORS via repeated calls. + let mut existing_index: Option = None; + for i in 0..escrow.contributor_count { + let ck = DataKey::Contribution(issue_id, i); + if let Some(c) = env.storage().persistent().get::<_, Contribution>(&ck) { + if c.sponsor == sponsor { + existing_index = Some(i); + break; + } + } } let token_client = token::Client::new(&env, &escrow.token); token_client.transfer(&sponsor, env.current_contract_address(), &amount); - let contribution_key = DataKey::Contribution(issue_id, escrow.contributor_count); - env.storage() - .persistent() - .set(&contribution_key, &Contribution { sponsor, amount }); - extend_ttl(&env, &contribution_key); + if let Some(idx) = existing_index { + let ck = DataKey::Contribution(issue_id, idx); + let mut c: Contribution = env.storage().persistent().get(&ck).unwrap(); + c.amount += amount; + env.storage().persistent().set(&ck, &c); + extend_ttl(&env, &ck); + } else { + if escrow.contributor_count >= MAX_SPONSORS { + return Err(Error::TooManySponsors); + } + let contribution_key = DataKey::Contribution(issue_id, escrow.contributor_count); + env.storage() + .persistent() + .set(&contribution_key, &Contribution { sponsor, amount }); + extend_ttl(&env, &contribution_key); + escrow.contributor_count += 1; + } escrow.amount += amount; - escrow.contributor_count += 1; env.storage().persistent().set(&key, &escrow); extend_ttl(&env, &key); - Ok(()) - } + Ok(()) + } /// Releases escrowed funds to one or more recipients. `recipients` is a /// list of (address, basis_points) pairs that must sum to exactly