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
34 changes: 15 additions & 19 deletions contracts/campaign-escrow/src/storage.rs
Original file line number Diff line number Diff line change
@@ -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};
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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<Address> = 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,
Expand All @@ -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<Vec<Address>> = env
let count: Option<u32> = 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)
}
44 changes: 44 additions & 0 deletions contracts/campaign-escrow/src/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down