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 {