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
3 changes: 1 addition & 2 deletions contracts/events/src/bounty.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ use soroban_sdk::{Address, BytesN, Env};

use crate::admin;
use crate::errors::Error;
use crate::event_ops::MAX_APPLICANTS_PER_EVENT;
use crate::events as evt;
use crate::idempotency::{self, tag};
use crate::profile_client;
Expand Down Expand Up @@ -33,7 +32,7 @@ pub fn apply(
applicant.require_auth();
idempotency::require_unseen(env, &applicant, &op_id)?;

storage::append_applicant(env, bounty_id, &applicant, MAX_APPLICANTS_PER_EVENT)?;
storage::append_applicant(env, bounty_id, &applicant)?;

let profile = profile_client::client(env);
let bootstrap_op = idempotency::derive_child(env, &op_id, tag::BOOTSTRAP);
Expand Down
7 changes: 5 additions & 2 deletions contracts/events/src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,12 +50,15 @@ pub enum Error {
BelowMinimumContribution = 57,
InvalidContributionAmount = 58,

// Per-event participant caps were removed (participant sets are
// unbounded; entries are per-participant and self-funded). 59 and 61 are
// kept for ABI stability and now only signal u32 counter overflow.
TooManyApplicants = 59,

OpAlreadySeen = 60,

// Also returned by append_submission's cap check: the hackathon submission
// cap reuses this rather than adding a near-duplicate "TooManySubmissions".
// Also returned by append_submission's overflow guard — reused rather
// than adding a near-duplicate "TooManySubmissions".
TooManyContributors = 61,

CancellationNotStarted = 62,
Expand Down
61 changes: 49 additions & 12 deletions contracts/events/src/event_ops.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,13 @@ const PENDING_MANAGER_TTL_LEDGERS: u32 = 17_280;
// before winners are selected). A per-event override needs a migration.
pub const PRIZE_CLAIM_WINDOW_SECS: u64 = 90 * 24 * 60 * 60;

pub const MAX_APPLICANTS_PER_EVENT: u32 = 5_000;
pub const MAX_CONTRIBUTORS_PER_EVENT: u32 = 5_000;
pub const MAX_SUBMISSIONS_PER_EVENT: u32 = 5_000;
// Participant sets (applicants, contributors, submissions) are unbounded:
// each entry is its own persistent ledger entry paid for by the participant's
// own transaction, and no state-changing path iterates the full set in one
// transaction (refunds are cranked in batches, winner selection takes an
// explicit bounded list). Full-list reads page through VIEW_PAGE_LIMIT
// entries per call so simulation stays inside per-tx read-entry limits.
pub const VIEW_PAGE_LIMIT: u32 = 100;
pub const MAX_CONTENT_URI_LEN: u32 = 256;

pub const MAX_REFUNDS_PER_BATCH: u32 = 25;
Expand Down Expand Up @@ -298,7 +302,7 @@ pub fn add_funds(
if is_non_owner {
let prior = prior_contribution;
if prior == 0 {
storage::append_contributor(env, event_id, &from, MAX_CONTRIBUTORS_PER_EVENT)?;
storage::append_contributor(env, event_id, &from)?;
}
}

Expand Down Expand Up @@ -557,11 +561,11 @@ pub fn submit(
}
}

// Reserve the slot before writing — Hackathon events have
// needs_application == false, so any address can call submit() with no
// prior gate. Without this cap, an attacker spamming fresh addresses
// grows persistent storage / rent burden without bound.
storage::append_submission(env, event_id, &applicant, MAX_SUBMISSIONS_PER_EVENT)?;
// Count the submission before writing. There is no cap: each submission
// is its own ledger entry whose write and rent are paid by the
// submitter's transaction, so spam addresses fund their own storage and
// cannot lock real participants out of a full event.
storage::append_submission(env, event_id, &applicant)?;

let submitted_at = existing
.as_ref()
Expand Down Expand Up @@ -906,12 +910,25 @@ pub fn get_submission(env: &Env, event_id: u64, applicant: Address) -> Result<Su
storage::get_submission(env, event_id, &applicant).ok_or(Error::SubmissionNotFound)
}

// Full-list getters return the first VIEW_PAGE_LIMIT entries; use the
// _page variants (or the per-index getters / the off-chain indexer) to
// read beyond that.
pub fn get_applicants(env: &Env, event_id: u64) -> Result<Vec<Address>, Error> {
get_applicants_page(env, event_id, 0, VIEW_PAGE_LIMIT)
}

pub fn get_applicants_page(
env: &Env,
event_id: u64,
start: u32,
limit: u32,
) -> Result<Vec<Address>, Error> {
storage::get_event(env, event_id).ok_or(Error::EventNotFound)?;
Ok(storage::applicants_snapshot(
env,
event_id,
MAX_APPLICANTS_PER_EVENT,
start,
limit.min(VIEW_PAGE_LIMIT),
))
}

Expand All @@ -926,11 +943,21 @@ pub fn get_applicant_at(env: &Env, event_id: u64, idx: u32) -> Result<Option<Add
}

pub fn get_winners(env: &Env, event_id: u64) -> Result<Vec<Winner>, Error> {
get_winners_page(env, event_id, 0, VIEW_PAGE_LIMIT)
}

pub fn get_winners_page(
env: &Env,
event_id: u64,
start: u32,
limit: u32,
) -> Result<Vec<Winner>, Error> {
storage::get_event(env, event_id).ok_or(Error::EventNotFound)?;
Ok(storage::winners_snapshot(
env,
event_id,
MAX_WINNERS_PER_SELECT.saturating_mul(20),
start,
limit.min(VIEW_PAGE_LIMIT),
))
}

Expand All @@ -945,11 +972,21 @@ pub fn get_winner_at(env: &Env, event_id: u64, idx: u32) -> Result<Option<Winner
}

pub fn get_contributors(env: &Env, event_id: u64) -> Result<Vec<Address>, Error> {
get_contributors_page(env, event_id, 0, VIEW_PAGE_LIMIT)
}

pub fn get_contributors_page(
env: &Env,
event_id: u64,
start: u32,
limit: u32,
) -> Result<Vec<Address>, Error> {
storage::get_event(env, event_id).ok_or(Error::EventNotFound)?;
Ok(storage::contributors_snapshot(
env,
event_id,
MAX_CONTRIBUTORS_PER_EVENT,
start,
limit.min(VIEW_PAGE_LIMIT),
))
}

Expand Down
29 changes: 29 additions & 0 deletions contracts/events/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -274,10 +274,21 @@ impl EventsContract {
event_ops::get_submission(&env, event_id, applicant)
}

// Full-list getters return the first page (VIEW_PAGE_LIMIT entries);
// page through the _page variants or the per-index getters for more.
pub fn get_applicants(env: Env, event_id: u64) -> Result<Vec<Address>, Error> {
event_ops::get_applicants(&env, event_id)
}

pub fn get_applicants_page(
env: Env,
event_id: u64,
start: u32,
limit: u32,
) -> Result<Vec<Address>, Error> {
event_ops::get_applicants_page(&env, event_id, start, limit)
}

pub fn get_applicant_count(env: Env, event_id: u64) -> Result<u32, Error> {
event_ops::get_applicant_count(&env, event_id)
}
Expand All @@ -290,6 +301,15 @@ impl EventsContract {
event_ops::get_winners(&env, event_id)
}

pub fn get_winners_page(
env: Env,
event_id: u64,
start: u32,
limit: u32,
) -> Result<Vec<Winner>, Error> {
event_ops::get_winners_page(&env, event_id, start, limit)
}

pub fn get_winner_count(env: Env, event_id: u64) -> Result<u32, Error> {
event_ops::get_winner_count(&env, event_id)
}
Expand All @@ -302,6 +322,15 @@ impl EventsContract {
event_ops::get_contributors(&env, event_id)
}

pub fn get_contributors_page(
env: Env,
event_id: u64,
start: u32,
limit: u32,
) -> Result<Vec<Address>, Error> {
event_ops::get_contributors_page(&env, event_id, start, limit)
}

pub fn get_contributor_count(env: Env, event_id: u64) -> Result<u32, Error> {
event_ops::get_contributor_count(&env, event_id)
}
Expand Down
56 changes: 26 additions & 30 deletions contracts/events/src/storage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -338,20 +338,20 @@ pub fn applicant_slot(env: &Env, id: u64, addr: &Address) -> u32 {
slot.unwrap_or(0)
}

pub fn append_applicant(env: &Env, id: u64, addr: &Address, cap: u32) -> Result<u32, Error> {
pub fn append_applicant(env: &Env, id: u64, addr: &Address) -> Result<u32, Error> {
if applicant_slot(env, id, addr) != 0 {
return Err(Error::ApplicantAlreadyApplied);
}
let cur = applicant_count(env, id);
if cur >= cap {
return Err(Error::TooManyApplicants);
}
// No product cap: each applicant is its own ledger entry paid for by the
// applicant's own transaction, so growth is O(1) per append. Only guard
// the u32 counter itself.
let slot = cur.checked_add(1).ok_or(Error::TooManyApplicants)?;
let at_key = DataKey::EventApplicantAt(id, cur);
env.storage().persistent().set(&at_key, addr);
touch_event_persistent(env, &at_key);

let slot_key = DataKey::EventApplicantSlot(id, addr.clone());
let slot = cur.saturating_add(1);
env.storage().persistent().set(&slot_key, &slot);
touch_event_persistent(env, &slot_key);

Expand Down Expand Up @@ -399,11 +399,11 @@ pub fn remove_applicant(env: &Env, id: u64, addr: &Address) -> Result<(), Error>
Ok(())
}

pub fn applicants_snapshot(env: &Env, id: u64, max: u32) -> Vec<Address> {
pub fn applicants_snapshot(env: &Env, id: u64, start: u32, limit: u32) -> Vec<Address> {
let count = applicant_count(env, id);
let upper = if count < max { count } else { max };
let end = start.saturating_add(limit).min(count);
let mut out: Vec<Address> = Vec::new(env);
for idx in 0..upper {
for idx in start..end {
if let Some(addr) = applicant_at(env, id, idx) {
out.push_back(addr);
}
Expand Down Expand Up @@ -460,23 +460,21 @@ pub fn submission_count(env: &Env, id: u64) -> u32 {
n.unwrap_or(0)
}

/// Reserve a submission slot against the per-event cap before writing the
/// entry (mirrors `append_contributor`/`append_applicant`). A no-op when the
/// applicant already has a submission — re-submission updates the existing
/// entry in place and must not recount against the cap.
/// Count a new submission before writing the entry (mirrors
/// `append_contributor`/`append_applicant`). A no-op when the applicant
/// already has a submission — re-submission updates the existing entry in
/// place and must not recount.
///
/// Returns `Error::TooManyContributors` on cap-exceed — reused rather than
/// a new variant since the errors enum is at the 50-case XDR cap.
pub fn append_submission(env: &Env, id: u64, addr: &Address, cap: u32) -> Result<(), Error> {
/// Returns `Error::TooManyContributors` only on u32 counter overflow —
/// reused rather than a new variant since the errors enum is at the
/// 50-case XDR cap.
pub fn append_submission(env: &Env, id: u64, addr: &Address) -> Result<(), Error> {
if get_submission(env, id, addr).is_some() {
return Ok(());
}
let cur = submission_count(env, id);
if cur >= cap {
return Err(Error::TooManyContributors);
}
let next = cur.checked_add(1).ok_or(Error::TooManyContributors)?;
let count_key = DataKey::EventSubmissionCount(id);
let next = cur.saturating_add(1);
env.storage().persistent().set(&count_key, &next);
touch_event_persistent(env, &count_key);
Ok(())
Expand Down Expand Up @@ -584,11 +582,11 @@ pub fn set_prize_claim_expiry(env: &Env, id: u64, expires_at: u64) {
touch_event_persistent(env, &key);
}

pub fn winners_snapshot(env: &Env, id: u64, max: u32) -> Vec<Winner> {
pub fn winners_snapshot(env: &Env, id: u64, start: u32, limit: u32) -> Vec<Winner> {
let count = winner_count(env, id);
let upper = if count < max { count } else { max };
let end = start.saturating_add(limit).min(count);
let mut out: Vec<Winner> = Vec::new(env);
for idx in 0..upper {
for idx in start..end {
if let Some(w) = winner_at(env, id, idx) {
out.push_back(w);
}
Expand Down Expand Up @@ -656,20 +654,18 @@ pub fn contributor_slot(env: &Env, id: u64, addr: &Address) -> u32 {
slot.unwrap_or(0)
}

pub fn append_contributor(env: &Env, id: u64, addr: &Address, cap: u32) -> Result<u32, Error> {
pub fn append_contributor(env: &Env, id: u64, addr: &Address) -> Result<u32, Error> {
if contributor_slot(env, id, addr) != 0 {
return Ok(0);
}
let cur = contributor_count(env, id);
if cur >= cap {
return Err(Error::TooManyContributors);
}
// No product cap (see append_applicant); guard only the u32 counter.
let slot = cur.checked_add(1).ok_or(Error::TooManyContributors)?;
let at_key = DataKey::ContributorAt(id, cur);
env.storage().persistent().set(&at_key, addr);
touch_event_persistent(env, &at_key);

let slot_key = DataKey::ContributorSlot(id, addr.clone());
let slot = cur.saturating_add(1);
env.storage().persistent().set(&slot_key, &slot);
touch_event_persistent(env, &slot_key);

Expand All @@ -679,11 +675,11 @@ pub fn append_contributor(env: &Env, id: u64, addr: &Address, cap: u32) -> Resul
Ok(slot)
}

pub fn contributors_snapshot(env: &Env, id: u64, max: u32) -> Vec<Address> {
pub fn contributors_snapshot(env: &Env, id: u64, start: u32, limit: u32) -> Vec<Address> {
let count = contributor_count(env, id);
let upper = if count < max { count } else { max };
let end = start.saturating_add(limit).min(count);
let mut out: Vec<Address> = Vec::new(env);
for idx in 0..upper {
for idx in start..end {
if let Some(addr) = contributor_at(env, id, idx) {
out.push_back(addr);
}
Expand Down
Loading
Loading