Skip to content
Closed
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
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion contracts/events/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "boundless-events"
version = "1.6.0"
version = "2.0.0"
edition = "2021"
publish = false

Expand Down
2 changes: 1 addition & 1 deletion contracts/events/src/admin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ const UPGRADE_TIMELOCK_LEDGERS: u32 = 0;
const UPGRADE_TIMELOCK_LEDGERS: u32 = 0;
const PENDING_UPGRADE_TTL_LEDGERS: u32 = 518_400;

pub const INITIAL_VERSION: &str = "1.7.0";
pub const INITIAL_VERSION: &str = "2.0.0";

// ============================================================
// INITIALIZATION
Expand Down
88 changes: 2 additions & 86 deletions contracts/events/src/bounty.rs
Original file line number Diff line number Diff line change
@@ -1,95 +1,11 @@
use soroban_sdk::{Address, BytesN, Env};
use soroban_sdk::{Address, Env};

use crate::admin;
use crate::errors::Error;
use crate::events as evt;
use crate::idempotency::{self, tag};
use crate::profile_client;
use crate::storage;
use crate::types::{EventRecord, EventStatus, Pillar, ReleaseKind};
use crate::types::{EventRecord, ReleaseKind};

pub fn validate_create(_env: &Env, record: &EventRecord, _owner: &Address) -> Result<(), Error> {
if !matches!(record.release_kind, ReleaseKind::Single) {
return Err(Error::InvalidReleaseKind);
}
Ok(())
}

// ============================================================
// APPLY
// ============================================================
pub fn apply(
env: &Env,
bounty_id: u64,
applicant: Address,
op_id: BytesN<32>,
) -> Result<(), Error> {
admin::require_not_paused(env)?;

let event = storage::get_event(env, bounty_id).ok_or(Error::EventNotFound)?;
require_active_bounty(&event)?;

applicant.require_auth();
idempotency::require_unseen(env, &applicant, &op_id)?;

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

let profile = profile_client::client(env);
let bootstrap_op = idempotency::derive_child(env, &op_id, tag::BOOTSTRAP);
profile.bootstrap(&applicant, &bootstrap_op);

evt::Applied {
event_id: bounty_id,
applicant: applicant.clone(),
}
.publish(env);

idempotency::mark_seen(env, &applicant, &op_id);
Ok(())
}

// ============================================================
// WITHDRAW APPLICATION
// ============================================================
pub fn withdraw_application(
env: &Env,
bounty_id: u64,
applicant: Address,
op_id: BytesN<32>,
) -> Result<(), Error> {
admin::require_not_paused(env)?;

let event = storage::get_event(env, bounty_id).ok_or(Error::EventNotFound)?;
require_active_bounty(&event)?;

applicant.require_auth();
idempotency::require_unseen(env, &applicant, &op_id)?;

if storage::has_any_submission(env, bounty_id, &applicant) {
return Err(Error::SubmissionAlreadyExists);
}

storage::remove_applicant(env, bounty_id, &applicant)?;

evt::ApplicationWithdrawn {
event_id: bounty_id,
applicant: applicant.clone(),
}
.publish(env);

idempotency::mark_seen(env, &applicant, &op_id);
Ok(())
}

// ============================================================
// HELPERS
// ============================================================
fn require_active_bounty(event: &EventRecord) -> Result<(), Error> {
if !matches!(event.pillar, Pillar::Bounty) {
return Err(Error::InvalidPillar);
}
if !matches!(event.status, EventStatus::Active) {
return Err(Error::EventNotActive);
}
Ok(())
}
20 changes: 8 additions & 12 deletions contracts/events/src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,10 @@ pub enum Error {
// off-chain/backend concern; the contract no longer gates on deadline).
TitleTooLong = 39,

ApplicantAlreadyApplied = 40,
ApplicantNotApplied = 41,
SubmissionNotFound = 42,
SubmissionAlreadyExists = 43,

// 40-43 retired in 2.0.0 with the on-chain participation records
// (ApplicantAlreadyApplied, ApplicantNotApplied, SubmissionNotFound,
// SubmissionAlreadyExists). Left as a gap rather than reused so an old
// client decoding a stored error code never reads the wrong case.
NoSubmissions = 50,
InvalidWinnerPosition = 51,
DuplicateWinnerPosition = 52,
Expand All @@ -50,15 +49,12 @@ 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,

// 59 retired in 2.0.0 with the applicant index (TooManyApplicants).
OpAlreadySeen = 60,

// Also returned by append_submission's overflow guard — reused rather
// than adding a near-duplicate "TooManySubmissions".
// Signals a u32 counter overflow on the contributor index. Per-event
// participant caps were removed (contributor sets are unbounded; entries
// are per-contributor and self-funded).
TooManyContributors = 61,

CancellationNotStarted = 62,
Expand Down
164 changes: 8 additions & 156 deletions contracts/events/src/event_ops.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use soroban_sdk::{Address, BytesN, Env, String, Symbol, Vec};
use soroban_sdk::{Address, BytesN, Env, Symbol, Vec};

use crate::admin::{self, MAX_FEE_BPS};
use crate::bounty;
Expand All @@ -14,7 +14,7 @@ use crate::storage;
use crate::token_whitelist;
use crate::types::{
CancellationBranch, CancellationState, CreateEventParams, EventRecord, EventStatus,
PendingManager, Pillar, PrizeAward, ReleaseKind, Submission, Winner, WinnerSpec,
PendingManager, Pillar, PrizeAward, ReleaseKind, Winner, WinnerSpec,
};

const MAX_TITLE_LEN: u32 = 120;
Expand All @@ -27,14 +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;

// 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.
// Contributor sets are unbounded: each entry is its own persistent ledger
// entry paid for by the contributor'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 @@ -537,112 +536,6 @@ pub fn finalize_cancel(env: &Env, event_id: u64, op_id: BytesN<32>) -> Result<()
Ok(())
}

// ============================================================
// SUBMIT
// ============================================================
pub fn submit(
env: &Env,
event_id: u64,
applicant: Address,
slot: u32,
content_uri: String,
op_id: BytesN<32>,
) -> Result<(), Error> {
admin::require_not_paused(env)?;

let event = storage::get_event(env, event_id).ok_or(Error::EventNotFound)?;
if !matches!(event.status, EventStatus::Active) {
return Err(Error::EventNotActive);
}
if matches!(event.pillar, Pillar::Crowdfunding) {
return Err(Error::InvalidPillar);
}

applicant.require_auth();
idempotency::require_unseen(env, &applicant, &op_id)?;

// Reused rather than adding a new variant — stays inside the
// contracterror 50-variant cap (see BACKLOG.md L7 for precedent).
if content_uri.len() > MAX_CONTENT_URI_LEN {
return Err(Error::TitleTooLong);
}

let existing = storage::get_submission(env, event_id, &applicant, slot);

if !storage::has_any_submission(env, event_id, &applicant) {
let needs_application = matches!(event.pillar, Pillar::Bounty | Pillar::Grant);
if needs_application && storage::applicant_slot(env, event_id, &applicant) == 0 {
return Err(Error::ApplicantNotApplied);
}
}

// Count the slot before writing. There is no cap: each entry 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, slot)?;

let submitted_at = existing
.as_ref()
.map(|s| s.submitted_at)
.unwrap_or_else(|| env.ledger().timestamp());

let submission = Submission {
applicant: applicant.clone(),
content_uri: content_uri.clone(),
submitted_at,
};
storage::set_submission(env, event_id, &applicant, slot, &submission);

evt::Submitted {
event_id,
applicant: applicant.clone(),
slot,
content_uri,
}
.publish(env);

idempotency::mark_seen(env, &applicant, &op_id);
Ok(())
}

// ============================================================
// WITHDRAW SUBMISSION
// ============================================================
pub fn withdraw_submission(
env: &Env,
event_id: u64,
applicant: Address,
slot: u32,
op_id: BytesN<32>,
) -> Result<(), Error> {
admin::require_not_paused(env)?;

let event = storage::get_event(env, event_id).ok_or(Error::EventNotFound)?;
if !matches!(event.status, EventStatus::Active) {
return Err(Error::EventNotActive);
}

applicant.require_auth();
idempotency::require_unseen(env, &applicant, &op_id)?;

if storage::get_submission(env, event_id, &applicant, slot).is_none() {
return Err(Error::SubmissionNotFound);
}

storage::remove_submission(env, event_id, &applicant, slot);

evt::SubmissionWithdrawn {
event_id,
applicant: applicant.clone(),
slot,
}
.publish(env);

idempotency::mark_seen(env, &applicant, &op_id);
Ok(())
}

// ============================================================
// SELECT WINNERS
// ============================================================
Expand Down Expand Up @@ -951,47 +844,6 @@ pub fn get_event(env: &Env, event_id: u64) -> Result<EventRecord, Error> {
storage::get_event(env, event_id).ok_or(Error::EventNotFound)
}

pub fn get_submission(
env: &Env,
event_id: u64,
applicant: Address,
slot: u32,
) -> Result<Submission, Error> {
storage::get_submission(env, event_id, &applicant, slot).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,
start,
limit.min(VIEW_PAGE_LIMIT),
))
}

pub fn get_applicant_count(env: &Env, event_id: u64) -> Result<u32, Error> {
storage::get_event(env, event_id).ok_or(Error::EventNotFound)?;
Ok(storage::applicant_count(env, event_id))
}

pub fn get_applicant_at(env: &Env, event_id: u64, idx: u32) -> Result<Option<Address>, Error> {
storage::get_event(env, event_id).ok_or(Error::EventNotFound)?;
Ok(storage::applicant_at(env, event_id, idx))
}

pub fn get_winners(env: &Env, event_id: u64) -> Result<Vec<Winner>, Error> {
get_winners_page(env, event_id, 0, VIEW_PAGE_LIMIT)
}
Expand Down
27 changes: 0 additions & 27 deletions contracts/events/src/events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,33 +60,6 @@ pub struct OwnerResidualRefunded {
pub amount: i128,
}

#[contractevent]
pub struct Applied {
pub event_id: u64,
pub applicant: Address,
}

#[contractevent]
pub struct ApplicationWithdrawn {
pub event_id: u64,
pub applicant: Address,
}

#[contractevent]
pub struct Submitted {
pub event_id: u64,
pub applicant: Address,
pub slot: u32,
pub content_uri: String,
}

#[contractevent]
pub struct SubmissionWithdrawn {
pub event_id: u64,
pub applicant: Address,
pub slot: u32,
}

#[contractevent]
pub struct WinnersSelected {
pub event_id: u64,
Expand Down
Loading