diff --git a/contracts/campaign-escrow/src/storage.rs b/contracts/campaign-escrow/src/storage.rs index 7efdf95..aec3e49 100644 --- a/contracts/campaign-escrow/src/storage.rs +++ b/contracts/campaign-escrow/src/storage.rs @@ -1,7 +1,7 @@ #![allow(dead_code)] use ads_bazaar_shared::CampaignId; -use soroban_sdk::{contracttype, Address, Env, String, Vec}; +use soroban_sdk::{contracttype, Address, Env, String}; use crate::error::Error; use crate::types::{Application, Campaign}; @@ -34,10 +34,10 @@ pub enum DataKey { /// Whether the contract is currently paused. See `require_not_paused` /// and `pause`/`unpause` in `lib.rs`. Paused, - /// Ordered list of creator addresses that have applied to a campaign. - /// Used by `update_campaign_metadata` to enforce that the brief is - /// locked once any creator has applied. - CampaignApplicants(CampaignId), + /// Count of creators that have applied to a campaign. Used by + /// `update_campaign_metadata` to enforce that the brief is locked once + /// any creator has applied. + ApplicantCount(CampaignId), } pub fn is_initialized(env: &Env) -> bool { @@ -173,18 +173,14 @@ pub fn set_paused(env: &Env, paused: bool) { env.storage().instance().set(&DataKey::Paused, &paused); } -/// Append `creator` to the applicants list for `campaign_id`. Called from +/// Increment the applicant count for `campaign_id`. Called from /// `apply_to_campaign` so `update_campaign_metadata` can lock the brief -/// once at least one creator has applied. -pub fn add_campaign_applicant(env: &Env, campaign_id: CampaignId, creator: &Address) { - let key = DataKey::CampaignApplicants(campaign_id); - let mut applicants: Vec
= env - .storage() - .persistent() - .get(&key) - .unwrap_or(Vec::new(env)); - applicants.push_back(creator.clone()); - env.storage().persistent().set(&key, &applicants); +/// once at least one creator has applied. O(1) regardless of how many +/// creators have applied, unlike an ever-growing list of applicants. +pub fn add_campaign_applicant(env: &Env, campaign_id: CampaignId, _creator: &Address) { + let key = DataKey::ApplicantCount(campaign_id); + let count: u32 = env.storage().persistent().get(&key).unwrap_or(0); + env.storage().persistent().set(&key, &(count + 1)); env.storage().persistent().extend_ttl( &key, PERSISTENT_LIFETIME_THRESHOLD, @@ -194,9 +190,9 @@ pub fn add_campaign_applicant(env: &Env, campaign_id: CampaignId, creator: &Addr /// Return whether any creator has applied to `campaign_id`. pub fn has_campaign_applicants(env: &Env, campaign_id: CampaignId) -> bool { - let applicants: Option> = env + let count: Option = env .storage() .persistent() - .get(&DataKey::CampaignApplicants(campaign_id)); - applicants.is_some_and(|v| !v.is_empty()) + .get(&DataKey::ApplicantCount(campaign_id)); + count.is_some_and(|c| c > 0) } diff --git a/contracts/campaign-escrow/src/test.rs b/contracts/campaign-escrow/src/test.rs index a510498..af5656f 100644 --- a/contracts/campaign-escrow/src/test.rs +++ b/contracts/campaign-escrow/src/test.rs @@ -1142,6 +1142,50 @@ mod test_update_metadata { ); assert_eq!(result, Err(Ok(Error::InvalidStatus))); } + + /// Applying to a campaign must stay O(1) in storage-write cost + /// regardless of how many creators already applied — the applicant + /// tracking is a counter, not a growing list. Apply with a large number + /// of prior applicants, then confirm the write cost of a later apply is + /// no larger than an early one, and that the lock-after-first-apply + /// behavior from `applications_exist_blocks_metadata_update` still holds. + #[test] + fn applying_with_many_prior_applicants_does_not_regress_write_cost() { + let (env, contract_id) = setup_env(); + let (client, _admin, _dispute, business, token) = bootstrap(&env, &contract_id, 50); + + // max_creators is a cap on approved creators, not applicants, so a + // low cap here doesn't limit how many creators can apply. + let id = create_funded_campaign(&env, &client, &business, &token, 10_000_000, 1); + + let first_creator = Address::generate(&env); + client.apply_to_campaign(&first_creator, &id, &String::from_str(&env, "pitch")); + let first_apply_write_bytes = env.cost_estimate().resources().write_bytes; + + // A large number of additional creators apply to the same campaign. + const N: u32 = 200; + for _ in 0..N { + let creator = Address::generate(&env); + client.apply_to_campaign(&creator, &id, &String::from_str(&env, "pitch")); + } + + let last_creator = Address::generate(&env); + client.apply_to_campaign(&last_creator, &id, &String::from_str(&env, "pitch")); + let last_apply_write_bytes = env.cost_estimate().resources().write_bytes; + + // The write cost of applying must not grow with the number of prior + // applicants — an ever-growing Vec would regress this. + assert_eq!(first_apply_write_bytes, last_apply_write_bytes); + + // The brief is still locked once any creator has applied, exactly + // as in `applications_exist_blocks_metadata_update`. + let result = client.try_update_campaign_metadata( + &id, + &business, + &String::from_str(&env, "ipfs://updated-brief"), + ); + assert_eq!(result, Err(Ok(Error::ApplicationsExist))); + } } mod test_resolve_dispute {