diff --git a/contracts/events/src/admin.rs b/contracts/events/src/admin.rs
index 9511923..91b15fb 100644
--- a/contracts/events/src/admin.rs
+++ b/contracts/events/src/admin.rs
@@ -1,7 +1,3 @@
-// boundless-events: admin operations.
-//
-// Spec: boundless-platform-contract-prd.md Section 6.1.
-
use soroban_sdk::{panic_with_error, Address, BytesN, Env, String};
use crate::errors::Error;
@@ -9,35 +5,16 @@ use crate::events as evt;
use crate::storage;
use crate::types::{PendingAdmin, PendingUpgrade};
-// Two-step admin rotation TTL: 7 days at the mainnet 5-second ledger cadence.
-// 7 * 24 * 60 * 60 / 5 = 120_960 ledgers.
const PENDING_ADMIN_TTL_LEDGERS: u32 = 120_960;
-// Fee bps cap. 100% = 10_000 bps. L4 (2026-06 audit): tightened from 5_000
-// (50%) to 1_000 (10%). 10% covers the full envelope of real Boundless
-// pricing tiers; a config typo can no longer push the fee above operating
-// range. Per-event overrides still respect this cap.
pub(crate) const MAX_FEE_BPS: u32 = 1_000;
-// H6: timelocked upgrade windows.
-//
-// UPGRADE_TIMELOCK_LEDGERS earliest gap between propose and apply.
-// ~1 day so off-chain monitors have a window
-// to react before the new wasm lands.
-// PENDING_UPGRADE_TTL_LEDGERS hard expiry on the proposal; ~30 days.
-// Past this the admin must re-propose.
-// Testnet builds (`--features testnet`) zero the upgrade timelock for fast
-// iteration; the default build (mainnet + everything else) keeps the full
-// ~1-day timelock. Fail-safe: omitting the flag yields the secure value, never 0.
#[cfg(not(feature = "testnet"))]
const UPGRADE_TIMELOCK_LEDGERS: u32 = 17_280;
#[cfg(feature = "testnet")]
const UPGRADE_TIMELOCK_LEDGERS: u32 = 0;
const PENDING_UPGRADE_TTL_LEDGERS: u32 = 518_400;
-// Initial contract version. Written by __constructor and bumped on
-// apply_upgrade. Bump alongside any storage-layout or public-surface change
-// that warrants a migration entrypoint.
pub const INITIAL_VERSION: &str = "1.1.0";
// ============================================================
@@ -50,8 +27,6 @@ pub fn initialize(
fee_bps: u32,
profile_contract: Address,
) {
- // Refuse double-init by checking the admin key in instance storage (the
- // new home for admin/config per the 2026-06 audit).
if env.storage().instance().has(&crate::types::DataKey::Admin) {
panic_with_error!(env, Error::AlreadyInitialized);
}
@@ -140,12 +115,6 @@ pub fn set_fee_bps(env: &Env, new_bps: u32) -> Result<(), Error> {
pub fn set_fee_account(env: &Env, new_account: Address) -> Result<(), Error> {
require_admin(env)?;
- // M2 (2026-06 audit): we do not verify trustline existence at the
- // contract layer because Soroban's SAC interface cannot reliably
- // distinguish "no trustline" from "zero balance". Admin must verify
- // off-chain BEFORE calling this; the FeeAccountUpdated event below is
- // the signal off-chain monitors rely on to re-verify. See
- // docs/audit-2026-06-stellar-skill.md M2.
storage::set_fee_account(env, &new_account);
storage::touch_instance(env);
evt::FeeAccountUpdated {
@@ -190,22 +159,6 @@ pub fn unpause(env: &Env) -> Result<(), Error> {
// ============================================================
// UPGRADE (timelocked; H6)
-//
-// Three steps:
-// 1. propose_upgrade(wasm_hash, new_version) — admin-only; writes
-// PendingUpgrade with proposed_at = now, available_at = now + TIMELOCK,
-// expires_at = now + TTL. Off-chain monitors can see exactly which
-// version + wasm is queued before it lands.
-// 2. apply_upgrade() — admin-only; requires
-// now in [available_at, expires_at]; swaps the wasm hash and bumps
-// the on-chain version label.
-// 3. cancel_pending_upgrade() — admin-only; prunes a stale
-// or unwanted proposal so a fresh one can be queued.
-//
-// migrate(to_version) is a SEPARATE call that runs the one-shot data
-// migration matched to the just-applied version. Guard via MigratedToVersion.
-//
-// Spec: docs/audit-2026-06-stellar-skill.md H6.
// ============================================================
pub fn propose_upgrade(
env: &Env,
@@ -213,10 +166,6 @@ pub fn propose_upgrade(
new_version: String,
) -> Result<(), Error> {
require_admin(env)?;
- // Empty version is rejected; reuse InvalidPillar to stay inside the
- // soroban contracterror 50-variant cap (a dedicated InvalidVersion
- // would push us over). Off-chain monitors should treat InvalidPillar
- // on propose_upgrade as "bad version label."
if new_version.is_empty() {
return Err(Error::InvalidPillar);
}
@@ -262,7 +211,6 @@ pub fn apply_upgrade(env: &Env) -> Result<(), Error> {
new_version: pending.new_version.clone(),
}
.publish(env);
- // Keep the legacy Upgraded event for indexers built against the old shape.
evt::Upgraded {
new_wasm_hash: pending.wasm_hash,
}
@@ -286,29 +234,6 @@ pub fn cancel_pending_upgrade(env: &Env) -> Result<(), Error> {
// ============================================================
// MIGRATE (post-upgrade one-shot; H6)
-//
-// Called once per version after apply_upgrade swaps the wasm. The shape
-// is:
-//
-// 1. Read the current Version label (set by apply_upgrade) and the
-// previously-applied migration marker (MigratedToVersion). If the
-// marker already equals the current Version, reject as
-// MigrationAlreadyApplied — a second invocation is always a
-// misconfiguration.
-// 2. Dispatch on (prev, current) and run the migration body. Bodies
-// run cleanly inside the same tx as the marker write, so a failure
-// reverts both — there is no half-migrated state to recover from.
-// 3. Stamp MigratedToVersion = current and emit Migrated{}.
-//
-// Mainnet bootstrap: the first deploy lands the constructor with the
-// current storage layout, so no migration body is needed. The first real
-// migration body will land with the first storage-layout upgrade after
-// mainnet goes live. We keep an empty match arm for the no-op case so the
-// shape is stable and future contributors do not have to debate where
-// the dispatch goes.
-//
-// NB: Soroban String only supports equality + length, no `as_str()` /
-// pattern matching. The dispatch below uses `String::from_str` + equality.
// ============================================================
pub fn migrate(env: &Env) -> Result<(), Error> {
require_admin(env)?;
@@ -324,31 +249,8 @@ pub fn migrate(env: &Env) -> Result<(), Error> {
// ============================================================
// PER-(from -> to) MIGRATION DISPATCH
- //
- // Each future upgrade adds an `if` clause here with its migration body.
- // Touch only persistent / instance entries that the new layout changes;
- // anything the new code reads with backwards-compatible defaults can
- // be left alone.
- //
- // Pattern:
- //
- // if from_version == String::from_str(env, "0.2.0")
- // && current == String::from_str(env, "0.3.0")
- // {
- // migrate_0_2_0_to_0_3_0(env)?;
- // }
- //
- // The corresponding private fn lives below the match block. Keep it
- // small enough to read; if the migration is large, split it into named
- // helpers and call from inside the body.
// ============================================================
- // No-op for the 1.0.0 -> 1.1.0 credit-removal upgrade: the contracts hold
- // no events yet, so there are no EventRecord rows to rewrite. __constructor
- // populates storage in the current shape, so admin can call migrate() once
- // just to stamp the marker and unlock the audit trail (the Migrated event
- // signals off-chain runbooks that the post-upgrade cleanup ran).
-
storage::set_migrated_to_version(env, ¤t);
storage::touch_instance(env);
evt::Migrated {
@@ -404,8 +306,6 @@ pub fn require_admin(env: &Env) -> Result<(), Error> {
}
pub fn require_not_paused(env: &Env) -> Result<(), Error> {
- // Every operation path runs this first, so this is the single spot to
- // bump instance TTL on the hot path. Admin paths bump explicitly.
storage::touch_instance(env);
if storage::is_paused(env) {
return Err(Error::Paused);
diff --git a/contracts/events/src/bounty.rs b/contracts/events/src/bounty.rs
index 9d34340..98e392e 100644
--- a/contracts/events/src/bounty.rs
+++ b/contracts/events/src/bounty.rs
@@ -1,10 +1,3 @@
-// boundless-events: bounty-specific behavior.
-//
-// Spec: boundless-platform-contract-prd.md Sections 6.3, 7.
-//
-// Bounties use ReleaseKind::Single. Credits (apply cost / refunds) are handled
-// off-chain; the contract only records applicants and ensures their profile.
-
use soroban_sdk::{Address, BytesN, Env};
use crate::admin;
@@ -40,11 +33,8 @@ pub fn apply(
applicant.require_auth();
- // append_applicant returns Err on duplicate or cap exceeded.
storage::append_applicant(env, bounty_id, &applicant, MAX_APPLICANTS_PER_EVENT)?;
- // Cross-contract: ensure the applicant has a profile (idempotent). Credits
- // are charged off-chain now, so there is no on-chain spend here.
let profile = profile_client::client(env);
let bootstrap_op = idempotency::derive_child(env, &op_id, tag::BOOTSTRAP);
profile.bootstrap(&applicant, &bootstrap_op);
@@ -76,17 +66,12 @@ pub fn withdraw_application(
applicant.require_auth();
- // Reject withdrawal if the applicant already submitted.
if storage::get_submission(env, bounty_id, &applicant).is_some() {
return Err(Error::SubmissionAlreadyExists);
}
- // Membership check + swap-remove. Slot lookup is O(1), so this avoids
- // the prior O(n) linear scan even at the cap.
storage::remove_applicant(env, bounty_id, &applicant)?;
- // Credits (including any withdrawal refund) are handled off-chain.
-
evt::ApplicationWithdrawn {
event_id: bounty_id,
applicant,
diff --git a/contracts/events/src/crowdfunding.rs b/contracts/events/src/crowdfunding.rs
index d41632c..0ca78d1 100644
--- a/contracts/events/src/crowdfunding.rs
+++ b/contracts/events/src/crowdfunding.rs
@@ -1,25 +1,3 @@
-// boundless-events: crowdfunding-specific behavior.
-//
-// Spec: boundless-crowdfunding-prd.md.
-//
-// Crowdfunding is a builder-led, community-funded pillar with these rules:
-// - ReleaseKind::Multi(n>0): milestones drive release cadence, like grants.
-// - deadline required: defines the funding window. Submitting/contributing
-// after the deadline is rejected by the standard event-active checks.
-// - Owner is the project builder. There is exactly one recipient, also
-// the builder, registered as Winner at position 1 (100% of distribution).
-// - No upfront owner deposit: create_event SKIPS escrow::deposit_with_fee
-// for Pillar::Crowdfunding. Escrow starts at 0 and grows via add_funds
-// from community backers.
-// - winner_distribution MUST be a single entry at position 1 with 100%.
-// - claim_milestone uses dynamic math:
-// amount = remaining_escrow / (total_milestones - claimed_so_far)
-// so each release pays a fair share of whatever the campaign actually
-// raised. The first milestone takes 1/n of escrow, the next 1/(n-1) of
-// what's left, ..., the last takes the entire remainder.
-//
-// The contract enforces only the on-chain shape. Off-chain layers add admin
-// review, community voting, milestone validation, and pause semantics.
#![allow(dead_code)]
use soroban_sdk::{Address, Env};
@@ -28,19 +6,15 @@ use crate::errors::Error;
use crate::types::{EventRecord, ReleaseKind};
pub fn validate_create(_env: &Env, record: &EventRecord, _owner: &Address) -> Result<(), Error> {
- // Multi(n) required.
match record.release_kind {
ReleaseKind::Multi(n) if n > 0 => {}
_ => return Err(Error::InvalidReleaseKind),
}
- // Funding window required.
if record.deadline.is_none() {
return Err(Error::DeadlineRequired);
}
- // Distribution must be exactly one entry at position 1 with 100%. The
- // builder is the sole recipient; no co-recipient splits are supported.
if record.winner_distribution.len() != 1 {
return Err(Error::InvalidDistribution);
}
diff --git a/contracts/events/src/errors.rs b/contracts/events/src/errors.rs
index ca38b93..26e080e 100644
--- a/contracts/events/src/errors.rs
+++ b/contracts/events/src/errors.rs
@@ -1,14 +1,9 @@
-// boundless-events: error codes.
-//
-// Spec: boundless-platform-contract-prd.md Section 14.
-
use soroban_sdk::contracterror;
#[contracterror]
#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)]
#[repr(u32)]
pub enum Error {
- // Init
AlreadyInitialized = 1,
AdminCannotBeZero = 2,
FeeAccountCannotBeZero = 3,
@@ -16,17 +11,14 @@ pub enum Error {
InvalidFeeBps = 5,
NotInitialized = 6,
- // Auth
Unauthorized = 10,
NotAdmin = 11,
PendingAdminMismatch = 12,
PendingAdminExpired = 13,
- // Token
TokenNotSupported = 20,
FeeAccountMissingTrustline = 21,
- // Event lifecycle
EventNotFound = 30,
EventNotActive = 31,
InvalidPillar = 32,
@@ -38,14 +30,11 @@ pub enum Error {
DeadlineMustBeFuture = 38,
TitleTooLong = 39,
- // Participation
ApplicantAlreadyApplied = 40,
ApplicantNotApplied = 41,
SubmissionNotFound = 42,
SubmissionAlreadyExists = 43,
- // 44 (InsufficientCredits) retired with on-chain credits; left as a gap.
- // Winners
NoSubmissions = 50,
InvalidWinnerPosition = 51,
DuplicateWinnerPosition = 52,
@@ -55,20 +44,15 @@ pub enum Error {
InsufficientEscrow = 56,
WinnersAlreadySelected = 90,
- // Contributions
BelowMinimumContribution = 57,
InvalidContributionAmount = 58,
- // Capacity (per-event list caps; see MAX_*_PER_EVENT in event_ops)
TooManyApplicants = 59,
- // Idempotency
OpAlreadySeen = 60,
- // Capacity continued
TooManyContributors = 61,
- // Paged cancellation flow
CancellationNotStarted = 62,
CancellationAlreadyStarted = 63,
CancellationNotFinished = 64,
@@ -78,9 +62,7 @@ pub enum Error {
UpgradeProposalExpired = 68,
MigrationAlreadyApplied = 69,
- // Pause
Paused = 70,
- // Cross-contract
ProfileCallFailed = 80,
}
diff --git a/contracts/events/src/escrow.rs b/contracts/events/src/escrow.rs
index 4a7c23a..04e53b6 100644
--- a/contracts/events/src/escrow.rs
+++ b/contracts/events/src/escrow.rs
@@ -46,11 +46,6 @@ pub fn release(env: &Env, token_addr: &Address, recipient: &Address, amount: i12
client.transfer(&contract, recipient, &amount);
}
-/// Deposit exactly `amount` into escrow with NO platform fee taken here.
-///
-/// Used by pillars (crowdfunding) that charge the fee at release instead, so the
-/// funder pays exactly `amount` and a cancel refunds it in full. Returns the
-/// amount credited to escrow (== `amount`).
pub fn deposit_no_fee(env: &Env, token_addr: &Address, from: &Address, amount: i128) -> i128 {
let contract = env.current_contract_address();
let client = token::Client::new(env, token_addr);
@@ -58,13 +53,6 @@ pub fn deposit_no_fee(env: &Env, token_addr: &Address, from: &Address, amount: i
amount
}
-/// Release `amount` from escrow, taking the platform fee off the top: the
-/// recipient receives `amount - fee` and the fee account receives `fee`.
-///
-/// Used by pillars (crowdfunding) where the RECIPIENT bears the fee. The funder
-/// already deposited their full pledge via `deposit_no_fee`. The full `amount`
-/// leaves escrow (net to recipient + fee to platform), so callers decrement
-/// `remaining_escrow` by `amount`.
pub fn release_with_fee_at(
env: &Env,
token_addr: &Address,
diff --git a/contracts/events/src/event_ops.rs b/contracts/events/src/event_ops.rs
index b820751..74336d9 100644
--- a/contracts/events/src/event_ops.rs
+++ b/contracts/events/src/event_ops.rs
@@ -1,7 +1,3 @@
-// boundless-events: canonical event lifecycle operations.
-//
-// Spec: boundless-platform-contract-prd.md Sections 6.2, 6.4, 6.5, 7.
-
use soroban_sdk::{Address, BytesN, Env, String, Symbol, Vec};
use crate::admin::{self, MAX_FEE_BPS};
@@ -24,37 +20,16 @@ use crate::types::{
const MAX_TITLE_LEN: u32 = 120;
const MAX_WINNERS_PER_SELECT: u32 = 50;
-// Per-event list caps. Lifted from 100 to 5_000 once paged cancel landed:
-// start_cancel / process_cancel_batch / finalize_cancel split the refund
-// pass across multiple txs so the per-tx footprint never blows past
-// MAX_REFUNDS_PER_BATCH contributors.
-//
-// Spec: docs/audit-2026-06-stellar-skill.md, H3/H4 + paged-cancel follow-up.
pub const MAX_APPLICANTS_PER_EVENT: u32 = 5_000;
pub const MAX_CONTRIBUTORS_PER_EVENT: u32 = 5_000;
-// Max refunds per process_cancel_batch tx. Conservative; the actual ceiling
-// depends on the token's transfer cost. 25 is well below Soroban's ~100-entry
-// write footprint when each refund touches ContributorAmount + a token
-// transfer (3-4 ledger entries).
pub const MAX_REFUNDS_PER_BATCH: u32 = 25;
-// Open-contribution floor: 10 USDC at 7 decimals. The check is denominated in
-// stroops because every supported token on the whitelist uses Stellar's
-// canonical 7-decimal scale. If a future token adopts a different scale the
-// whitelist registration is the place to gate it; the contract floor stays
-// uniform.
-//
-// Spec: boundless-partner-contributions-prd.md Section 6.1.
-const MIN_CONTRIBUTION_STROOPS: i128 = 100_000_000_i128; // 10 * 10^7
+const MIN_CONTRIBUTION_STROOPS: i128 = 100_000_000_i128;
// ============================================================
// CREATE EVENT
// ============================================================
-/// The address authorized to manage an event (select winners, cancel). A
-/// per-event manager override takes precedence; otherwise management falls back
-/// to the event owner (legacy events created before manager support). This
-/// decouples the funding source (owner) from the operating identity (manager).
fn resolve_manager(env: &Env, event_id: u64, owner: &Address) -> Address {
storage::get_event_manager(env, event_id).unwrap_or_else(|| owner.clone())
}
@@ -65,20 +40,16 @@ pub fn create_event(env: &Env, params: CreateEventParams, op_id: BytesN<32>) ->
params.owner.require_auth();
- // Token whitelist.
token_whitelist::require_supported(env, ¶ms.token)?;
- // Title length.
if params.title.len() > MAX_TITLE_LEN {
return Err(Error::TitleTooLong);
}
- // Budget.
if params.total_budget <= 0 {
return Err(Error::InvalidBudget);
}
- // Distribution: at least one entry, percents sum to 100.
if params.winner_distribution.is_empty() {
return Err(Error::InvalidDistribution);
}
@@ -90,7 +61,6 @@ pub fn create_event(env: &Env, params: CreateEventParams, op_id: BytesN<32>) ->
return Err(Error::DistributionMismatch);
}
- // Deadline (if set) must be future.
if let Some(deadline) = params.deadline {
if deadline <= env.ledger().timestamp() {
return Err(Error::DeadlineMustBeFuture);
@@ -104,8 +74,6 @@ pub fn create_event(env: &Env, params: CreateEventParams, op_id: BytesN<32>) ->
}
let effective_bps = escrow::effective_fee_bps(env, params.fee_bps_override);
- // Crowdfunding flips total_budget into a funding goal; escrow starts at 0
- // and grows only via add_funds. Every other pillar deposits at create.
let is_crowdfunding = matches!(params.pillar, Pillar::Crowdfunding);
let initial_escrow: i128 = if is_crowdfunding {
0
@@ -146,18 +114,14 @@ pub fn create_event(env: &Env, params: CreateEventParams, op_id: BytesN<32>) ->
);
}
- // Assign id and persist.
let id = idempotency::next_event_id(env);
let record = EventRecord { id, ..provisional };
storage::set_event(env, id, &record);
- // Record the management authority override when the owner delegates it (so
- // an org can fund from any wallet but keep management on its own wallet).
if let Some(manager) = ¶ms.manager {
storage::set_event_manager(env, id, manager);
}
- // Crowdfunding: pre-seat the builder as the sole winner at position 1.
if is_crowdfunding {
storage::append_winner(
env,
@@ -187,9 +151,6 @@ pub fn create_event(env: &Env, params: CreateEventParams, op_id: BytesN<32>) ->
Ok(id)
}
-/// Re-assign (or set) the management authority for an event. Gated by the
-/// current manager (the override if present, else the owner), so an org can
-/// rotate its operating wallet but an outsider cannot hijack management.
pub fn set_manager(env: &Env, event_id: u64, new_manager: Address) -> Result<(), Error> {
admin::require_not_paused(env)?;
let event = storage::get_event(env, event_id).ok_or(Error::EventNotFound)?;
@@ -198,7 +159,6 @@ pub fn set_manager(env: &Env, event_id: u64, new_manager: Address) -> Result<(),
Ok(())
}
-/// The current management authority for an event (override if set, else owner).
pub fn get_manager(env: &Env, event_id: u64) -> Result
{
let event = storage::get_event(env, event_id).ok_or(Error::EventNotFound)?;
Ok(resolve_manager(env, event_id, &event.owner))
@@ -231,8 +191,6 @@ pub fn add_funds(
from.require_auth();
- // First-time contributor? Reserve the slot BEFORE moving tokens so a
- // cap-exceeded reverts the whole flow with no half-credited row.
if from != event.owner {
let prior = storage::get_contributor_amount(env, event_id, &from);
if prior == 0 {
@@ -240,15 +198,9 @@ pub fn add_funds(
}
}
- // Fee model is per-pillar. Crowdfunding backers pay EXACTLY their pledge:
- // the platform fee is borne by the builder and taken at claim_milestone, so
- // a cancelled campaign refunds backers in full. Every other pillar charges
- // the fee on top here (the funder is the program owner/sponsor).
let credited = if matches!(event.pillar, Pillar::Crowdfunding) {
escrow::deposit_no_fee(env, &event.token, &from, amount)
} else {
- // Rate snapshotted at publish so add_funds matches the program's quoted
- // rate even if the contract default changes mid-flight.
let effective_bps = escrow::effective_fee_bps(env, event.fee_bps_override);
escrow::deposit_with_fee_at(env, &event.token, &from, amount, effective_bps)
};
@@ -276,39 +228,6 @@ pub fn add_funds(
// ============================================================
// PAGED CANCEL
-//
-// Three-step flow to keep cancel inside Soroban's per-tx footprint budget:
-//
-// 1. start_cancel(id) — flip Active → Cancelling, snapshot the
-// refund math (non_owner_total, remaining,
-// count, branch). For events with 0
-// contributors, also handles the owner
-// refund inline.
-// 2. process_cancel_batch(id, n) — refund up to n contributors at the
-// cursor. Repeats until cursor == count.
-// 3. finalize_cancel(id) — require cursor exhausted; pay owner
-// residual on FullPartnerThenResidual;
-// flip Cancelling → Cancelled; clear the
-// state entry.
-//
-// Refund math (snapshotted at start_cancel; stable across batches because
-// Cancelling status blocks add_funds + other contributor mutations):
-//
-// non_owner_total = sum(ContributorAmount(event_id, *))
-// remaining = event.remaining_escrow
-//
-// OwnerOnly: non_owner_total == 0; owner gets remaining.
-// Settled inline at start_cancel.
-// FullPartnerThenResidual: remaining >= non_owner_total; each partner
-// gets full amount; owner residual paid at
-// finalize_cancel.
-// ProRataPartners: remaining < non_owner_total; partners get
-// floor(amt * remaining / non_owner_total).
-// Owner gets 0. Dust stays in contract.
-//
-// Spec: boundless-platform-contract-prd.md Section 6.2;
-// boundless-partner-contributions-prd.md Section 7;
-// docs/audit-2026-06-stellar-skill.md paged-cancel follow-up.
// ============================================================
pub fn start_cancel(env: &Env, event_id: u64, op_id: BytesN<32>) -> Result<(), Error> {
admin::require_not_paused(env)?;
@@ -322,16 +241,11 @@ pub fn start_cancel(env: &Env, event_id: u64, op_id: BytesN<32>) -> Result<(), E
return Err(Error::CancellationAlreadyStarted);
}
- // Management authority: the per-event manager override if set, else owner.
resolve_manager(env, event_id, &event.owner).require_auth();
let remaining = event.remaining_escrow;
let count = storage::contributor_count(env, event_id);
- // Sum non-owner contributions once. Bounded by MAX_CONTRIBUTORS_PER_EVENT
- // contributor_amount reads; for events with > MAX_REFUNDS_PER_BATCH
- // contributors the caller will need to start_cancel on smaller events
- // OR we accept this single read pass as the cost of snapshotting.
let mut non_owner_total: i128 = 0;
for idx in 0..count {
if let Some(c) = storage::contributor_at(env, event_id, idx) {
@@ -348,9 +262,6 @@ pub fn start_cancel(env: &Env, event_id: u64, op_id: BytesN<32>) -> Result<(), E
CancellationBranch::ProRataPartners
};
- // OwnerOnly shortcut: no partner refunds to page through; flip directly
- // to Cancelled and pay owner residual inline. Saves the caller a round
- // trip for the common "abandoned, no community contributions" case.
if matches!(branch, CancellationBranch::OwnerOnly) {
if remaining > 0 {
escrow::release(env, &event.token, &event.owner, remaining);
@@ -369,8 +280,6 @@ pub fn start_cancel(env: &Env, event_id: u64, op_id: BytesN<32>) -> Result<(), E
return Ok(());
}
- // Partner refund branches: persist the cursor + branch and flip to
- // Cancelling. process_cancel_batch + finalize_cancel finish the work.
let state = CancellationState {
non_owner_total,
remaining_at_start: remaining,
@@ -399,13 +308,11 @@ pub fn process_cancel_batch(
if !matches!(event.status, EventStatus::Cancelling) {
return Err(Error::CancellationNotStarted);
}
- // Management authority: the per-event manager override if set, else owner.
resolve_manager(env, event_id, &event.owner).require_auth();
let mut state =
storage::get_cancellation_state(env, event_id).ok_or(Error::CancellationNotStarted)?;
- // Anyone can call with a 0 batch_size if the cursor is at end; nothing to do.
let cap = if max_refunds > MAX_REFUNDS_PER_BATCH {
MAX_REFUNDS_PER_BATCH
} else {
@@ -430,7 +337,6 @@ pub fn process_cancel_batch(
CancellationBranch::ProRataPartners => {
amt.saturating_mul(state.remaining_at_start) / state.non_owner_total
}
- // OwnerOnly was settled inline at start_cancel.
CancellationBranch::OwnerOnly => 0,
};
@@ -462,7 +368,6 @@ pub fn finalize_cancel(env: &Env, event_id: u64, op_id: BytesN<32>) -> Result<()
if !matches!(event.status, EventStatus::Cancelling) {
return Err(Error::CancellationNotStarted);
}
- // Management authority: the per-event manager override if set, else owner.
resolve_manager(env, event_id, &event.owner).require_auth();
let state =
@@ -471,8 +376,6 @@ pub fn finalize_cancel(env: &Env, event_id: u64, op_id: BytesN<32>) -> Result<()
return Err(Error::CancellationNotFinished);
}
- // Owner residual paid only on FullPartnerThenResidual; ProRataPartners
- // intentionally pays the owner 0.
if matches!(state.branch, CancellationBranch::FullPartnerThenResidual) {
let owner_residual = state
.remaining_at_start
@@ -516,7 +419,6 @@ pub fn submit(
if !matches!(event.status, EventStatus::Active) {
return Err(Error::EventNotActive);
}
- // Crowdfunding has no submission concept: the builder is pre-seated.
if matches!(event.pillar, Pillar::Crowdfunding) {
return Err(Error::InvalidPillar);
}
@@ -530,8 +432,6 @@ pub fn submit(
let existing = storage::get_submission(env, event_id, &applicant);
- // Pillar-aware application gate (only enforced on first submission).
- // O(1) lookup via the slot index.
if existing.is_none() {
let needs_application = matches!(event.pillar, Pillar::Bounty | Pillar::Grant);
if needs_application && storage::applicant_slot(env, event_id, &applicant) == 0 {
@@ -622,12 +522,8 @@ pub fn select_winners(
return Err(Error::InvalidPillar);
}
- // Management authority: the per-event manager override if set, else owner.
resolve_manager(env, event_id, &event.owner).require_auth();
- // One-shot per event: detect a prior selection by an anchor row
- // (milestone == None). Crowdfunding's create_event seeds an anchor too,
- // but we returned above for crowdfunding.
let existing_count = storage::winner_count(env, event_id);
for idx in 0..existing_count {
if let Some(w) = storage::winner_at(env, event_id, idx) {
@@ -644,7 +540,6 @@ pub fn select_winners(
return Err(Error::InvalidWinnerPosition);
}
- // Validate each position exists in distribution and no duplicates.
let mut seen_positions: Vec = Vec::new(env);
for spec in winners.iter() {
let mut already = false;
@@ -669,15 +564,8 @@ pub fn select_winners(
match event.release_kind {
ReleaseKind::Single => {
- // M1: percent math is against the live escrow at select time
- // (snapshotted before the first refund), so partner top-ups via
- // add_funds flow into winner payouts rather than getting
- // trapped until cancel. Snapshot once so each winner gets the
- // intended share regardless of the order of releases inside
- // this same call.
let escrow_at_select = event.remaining_escrow;
- // First pass: compute per-winner amounts and verify total fits.
let mut total_owed: i128 = 0;
for spec in winners.iter() {
let percent = event
@@ -694,7 +582,6 @@ pub fn select_winners(
return Err(Error::InsufficientEscrow);
}
- // Second pass: release per winner with all four profile-side calls.
for (idx, spec) in winners.iter().enumerate() {
let sub_idx = idx as u8;
let percent = event
@@ -749,7 +636,6 @@ pub fn select_winners(
}
}
ReleaseKind::Multi(_) => {
- // Grants: record winners but defer payment to claim_milestone.
for spec in winners.iter() {
storage::append_winner(
env,
@@ -781,10 +667,6 @@ pub fn select_winners(
// ============================================================
// READS
-//
-// Aggregated reads (get_applicants, get_winners, get_contributors) cap at
-// MAX_*_PER_EVENT entries. Callers expecting larger lists should use the
-// paged accessors (*_count + *_at).
// ============================================================
pub fn get_event(env: &Env, event_id: u64) -> Result {
storage::get_event(env, event_id).ok_or(Error::EventNotFound)
@@ -860,6 +742,5 @@ pub fn get_contributor_amount(
Ok(storage::get_contributor_amount(env, event_id, &contributor))
}
-// Silence unused-import warnings until the stubbed ops land.
#[allow(dead_code)]
const _MARK_USED: (Option,) = (None,);
diff --git a/contracts/events/src/events.rs b/contracts/events/src/events.rs
index 91d253d..4c675dc 100644
--- a/contracts/events/src/events.rs
+++ b/contracts/events/src/events.rs
@@ -1,9 +1,3 @@
-// boundless-events: contract event emissions.
-//
-// Spec: boundless-platform-contract-prd.md Section 13.
-//
-// dead_code allowed: event structs are emitted via .publish() calls from
-// operation bodies that are stubbed in this pass.
#![allow(dead_code)]
use soroban_sdk::{contractevent, Address, BytesN, String};
@@ -18,8 +12,6 @@ pub struct EventCreated {
pub token: Address,
pub total_budget: i128,
pub content_uri: String,
- // L5 (2026-06 audit): title is part of the indexer payload so
- // listings can be rendered without a follow-up get_event read.
pub title: String,
}
diff --git a/contracts/events/src/grant.rs b/contracts/events/src/grant.rs
index d4f12f2..e33b893 100644
--- a/contracts/events/src/grant.rs
+++ b/contracts/events/src/grant.rs
@@ -1,12 +1,3 @@
-// boundless-events: grant-specific behavior + shared milestone-claim entry.
-//
-// Spec: boundless-platform-contract-prd.md Sections 6.4, 7;
-// boundless-crowdfunding-prd.md.
-//
-// Grants use ReleaseKind::Multi(n) and release per-milestone via
-// claim_milestone. Crowdfunding reuses the same entry point but takes a
-// different math path (dynamic, see comment in claim_milestone).
-
use soroban_sdk::{Address, BytesN, Env, Symbol};
use crate::admin;
@@ -28,35 +19,6 @@ pub fn validate_create(_env: &Env, record: &EventRecord, _owner: &Address) -> Re
// ============================================================
// CLAIM MILESTONE
// ============================================================
-//
-// Per-(recipient, milestone) idempotency via DataKey::MilestoneClaimed.
-//
-// Math depends on pillar:
-//
-// Grant (fixed):
-// amount = total_budget * distribution[position] / 100 / total_milestones
-//
-// Crowdfunding (dynamic):
-// amount = remaining_escrow / (total_milestones - already_claimed_count)
-//
-// The Crowdfunding path divides whatever escrow is actually present at
-// release time evenly across the remaining milestones. The last milestone
-// picks up any rounding remainder, so the total paid equals what was
-// raised exactly.
-//
-// Each call: token release + bootstrap (idempotent) + bump_reputation +
-// register_earnings. Marks the event Completed when the last milestone for
-// the last recipient drains remaining_escrow.
-//
-// Auth: event.owner for grants (organization-controlled); for crowdfunding
-// the owner is the builder, so the off-chain layer routes claim_milestone
-// through an admin-signed transaction where the admin signs on the builder's
-// behalf only after milestone validation. On-chain check is identical:
-// require_auth on event.owner. Operationally that means the builder's
-// abstracted-wallet key is used to sign, gated by admin approval upstream.
-//
-// Spec: boundless-platform-contract-prd.md Section 6.4, 8;
-// boundless-crowdfunding-prd.md.
pub fn claim_milestone(
env: &Env,
event_id: u64,
@@ -84,13 +46,6 @@ pub fn claim_milestone(
event.owner.require_auth();
- // M5 (2026-06 audit): crowdfunding claims require admin co-sign on top
- // of the builder's auth. Today the abstracted-wallet model keeps the
- // builder's key on the platform side, so this is operationally a
- // no-op; the on-chain check is the defense if the wallet model ever
- // changes to put the key in the builder's hands. Grants stay
- // single-auth because the event owner is the grant org, which is the
- // intended approver. See docs/audit-2026-06-stellar-skill.md M5.
if matches!(event.pillar, Pillar::Crowdfunding) {
let admin = storage::get_admin(env)?;
admin.require_auth();
@@ -100,9 +55,6 @@ pub fn claim_milestone(
return Err(Error::MilestoneAlreadyClaimed);
}
- // Locate the recipient's anchor row (milestone == None), and count any
- // prior per-milestone payouts so the fixed-split last-milestone sweep
- // can land exactly on total_share.
let count = storage::winner_count(env, event_id);
let mut winner_position: Option = None;
let mut already_claimed_for_recipient: u32 = 0;
@@ -127,8 +79,6 @@ pub fn claim_milestone(
let is_crowdfunding = matches!(event.pillar, Pillar::Crowdfunding);
let amount: i128 = if is_crowdfunding {
- // Dynamic split: divide what's left evenly across remaining milestones;
- // the final claim drains the remainder so no dust is stranded.
let claimed_count = storage::get_crowdfunding_milestones_claimed(env, event_id);
let remaining_milestones = total_milestones.saturating_sub(claimed_count);
if remaining_milestones == 0 {
@@ -143,9 +93,6 @@ pub fn claim_milestone(
event.remaining_escrow / (remaining_milestones as i128)
}
} else {
- // Fixed split with last-milestone sweep so the position share pays out
- // exactly. Per-recipient progress was tallied during the anchor scan
- // above.
let percent = event
.winner_distribution
.get(position)
@@ -166,10 +113,6 @@ pub fn claim_milestone(
return Err(Error::InsufficientEscrow);
}
- // Move money. Crowdfunding recipients (builders) bear the platform fee: it
- // is taken from each milestone payout here, because backers deposited their
- // full pledge fee-free. Other pillars already took the fee at deposit, so
- // they release the full amount. Either way the full `amount` leaves escrow.
if is_crowdfunding {
let bps = escrow::effective_fee_bps(env, event.fee_bps_override);
escrow::release_with_fee_at(env, &event.token, &recipient, amount, bps);
@@ -183,7 +126,6 @@ pub fn claim_milestone(
storage::set_crowdfunding_milestones_claimed(env, event_id, claimed.saturating_add(1));
}
- // Cross-contract profile mutations. Each call gets a unique child op_id.
let profile = profile_client::client(env);
let reason = Symbol::new(env, "milestone");
@@ -196,7 +138,6 @@ pub fn claim_milestone(
let earnings_op = idempotency::derive_child(env, &op_id, tag::REGISTER_EARNINGS);
profile.register_earnings(&recipient, &event.token, &amount, &earnings_op);
- // Append per-milestone Winner row for the audit trail.
storage::append_winner(
env,
event_id,
diff --git a/contracts/events/src/hackathon.rs b/contracts/events/src/hackathon.rs
index 6a45cf4..ca738d2 100644
--- a/contracts/events/src/hackathon.rs
+++ b/contracts/events/src/hackathon.rs
@@ -1,11 +1,3 @@
-// boundless-events: hackathon-specific behavior.
-//
-// Spec: boundless-platform-contract-prd.md Section 7.
-//
-// Hackathons use ReleaseKind::Single and require a submission deadline.
-// Open submission model: no per-applicant credit charge.
-//
-// Wired by create_event dispatch.
#![allow(dead_code)]
use soroban_sdk::{Address, Env};
diff --git a/contracts/events/src/idempotency.rs b/contracts/events/src/idempotency.rs
index 2e2c8aa..7c25305 100644
--- a/contracts/events/src/idempotency.rs
+++ b/contracts/events/src/idempotency.rs
@@ -1,8 +1,3 @@
-// boundless-events: idempotency helpers.
-//
-// Spec: boundless-platform-contract-prd.md Section 10.
-//
-// Wired by operation bodies as they land.
#![allow(dead_code)]
use soroban_sdk::{BytesN, Env};
@@ -21,10 +16,6 @@ pub fn mark_seen(env: &Env, op_id: &BytesN<32>) {
storage::mark_op_seen(env, op_id);
}
-// Deployment-epoch ID base: upper 32 bits encode the ledger sequence at deploy
-// time so that ids from different deployments never collide.
-//
-// Spec: boundless-platform-contract-prd.md Section 9.
pub fn id_base(env: &Env) -> u64 {
let seq = storage::get_deployment_seq(env);
(seq as u64) << 32
@@ -37,8 +28,6 @@ pub fn next_event_id(env: &Env) -> u64 {
id
}
-/// Tag constants for `derive_child`. One per cross-contract op kind so that
-/// the events contract and the profile contract never share an OpSeen marker.
pub mod tag {
pub const BOOTSTRAP: u8 = 0xB0;
pub const BUMP_REP: u8 = 0xD1;
@@ -46,21 +35,12 @@ pub mod tag {
pub const REGISTER_EARNINGS: u8 = 0xE1;
}
-/// Derive a child op_id from a parent so that cross-contract calls within a
-/// single events-side operation each have a unique idempotency marker.
-///
-/// XOR with a per-op tag in the first byte: cheap, deterministic, and the
-/// orchestrator's sha256-based parent op_ids make collisions effectively
-/// impossible.
pub fn derive_child(env: &Env, parent: &BytesN<32>, op_tag: u8) -> BytesN<32> {
let mut payload = parent.to_array();
payload[0] ^= op_tag;
BytesN::from_array(env, &payload)
}
-/// Same as `derive_child` but also XORs a sub-index into the second byte, so
-/// per-winner cross-contract calls within select_winners get unique op_ids
-/// even when the same op_tag is reused across winners.
pub fn derive_child_indexed(env: &Env, parent: &BytesN<32>, op_tag: u8, sub_idx: u8) -> BytesN<32> {
let mut payload = parent.to_array();
payload[0] ^= op_tag;
diff --git a/contracts/events/src/lib.rs b/contracts/events/src/lib.rs
index 959db86..fa3fcf8 100644
--- a/contracts/events/src/lib.rs
+++ b/contracts/events/src/lib.rs
@@ -1,10 +1,4 @@
// SPDX-License-Identifier: MIT
-//
-// boundless-events
-//
-// On-chain event records (Hackathon, Bounty, Grant) plus inlined escrow.
-// Companion: boundless-profile (credits + reputation).
-// Spec: boundless-platform-contract-prd.md
#![no_std]
use soroban_sdk::{contract, contractimpl, contractmeta, Address, BytesN, Env, String, Vec};
@@ -121,14 +115,10 @@ impl EventsContract {
token_whitelist::is_supported(&env, &token)
}
- /// Number of whitelisted tokens. Paged enumeration: read this, then
- /// supported_token_at(0..count) to recover the full whitelist from state
- /// (no dependence on ephemeral TokenRegistered/TokenDeregistered events).
pub fn supported_token_count(env: Env) -> u32 {
storage::supported_token_count(&env)
}
- /// Whitelisted token at `index`, or None when `index >= count`.
pub fn supported_token_at(env: Env, index: u32) -> Option {
storage::supported_token_at(&env, index)
}
@@ -339,7 +329,6 @@ impl EventsContract {
admin::get_migrated_to_version(&env)
}
- // Internal helper exposed for off-chain inspection; emits no event.
pub fn id_base(env: Env) -> u64 {
idempotency::id_base(&env)
}
diff --git a/contracts/events/src/profile_client.rs b/contracts/events/src/profile_client.rs
index e5c3f17..ab4b1d9 100644
--- a/contracts/events/src/profile_client.rs
+++ b/contracts/events/src/profile_client.rs
@@ -1,17 +1,5 @@
-// boundless-events: client for the boundless-profile contract.
-//
-// The events contract calls the profile contract for credit / reputation
-// mutations triggered by event-side flows. We declare the profile interface as
-// a trait and let Soroban generate a typed client. The actual implementation
-// lives in the separately-deployed profile contract; this is just the call
-// surface from the events side.
-//
-// Spec: boundless-platform-contract-prd.md Section 4 (cross-contract dance).
-
use soroban_sdk::{contractclient, Address, BytesN, Env, Symbol};
-// The trait body is consumed by the contractclient macro to generate
-// ProfileClient. The trait itself has no other callers.
#[allow(dead_code)]
#[contractclient(name = "ProfileClient")]
pub trait ProfileInterface {
@@ -21,8 +9,6 @@ pub trait ProfileInterface {
fn register_earnings(env: Env, user: Address, token: Address, amount: i128, op_id: BytesN<32>);
}
-/// Helper to build a typed ProfileClient pointing at the currently-configured
-/// profile contract address.
pub fn client<'a>(env: &Env) -> ProfileClient<'a> {
let addr = crate::storage::get_profile_contract(env);
ProfileClient::new(env, &addr)
diff --git a/contracts/events/src/storage.rs b/contracts/events/src/storage.rs
index 4aaa9ed..6cd8d4d 100644
--- a/contracts/events/src/storage.rs
+++ b/contracts/events/src/storage.rs
@@ -1,22 +1,3 @@
-// boundless-events: storage helpers.
-//
-// Storage layout (after the 2026-06 audit, H1-H4):
-//
-// instance() — admin + config + token whitelist + NextEventId.
-// persistent() — per-event Event records, per-event paged lists
-// (applicants, contributors, winners), and per-submission
-// entries. Each persistent read/write bumps TTL.
-// temporary() — OpSeen idempotency markers.
-//
-// Per-event lists are kept as count + indexed-entry pairs (e.g.
-// EventApplicantCount + EventApplicantAt(idx) + EventApplicantSlot(addr))
-// so a single growable Vec never overflows the 64KB ledger-entry cap and
-// presence checks stay O(1).
-//
-// Cap policy (event_ops::MAX_*_PER_EVENT) enforced at append time so that
-// cancel_event refund passes stay inside Soroban's per-tx footprint budget.
-// Paging cancel_event is a P1 follow-up before lifting the caps.
-
#![allow(dead_code)]
use soroban_sdk::{Address, BytesN, Env, Vec};
@@ -185,10 +166,6 @@ pub fn set_token_supported(env: &Env, token: &Address, supported: bool) {
.set(&DataKey::SupportedToken(token.clone()), &supported);
}
-// Enumerable whitelist index, kept in lockstep with the SupportedToken bool by
-// register/deregister. Mirrors the applicant index (count + at + slot, append
-// to tail, swap-with-last removal) but is global rather than per-event, so the
-// full whitelist can be read from state instead of replaying events.
pub fn supported_token_count(env: &Env) -> u32 {
env.storage()
.instance()
@@ -209,8 +186,6 @@ pub fn supported_token_slot(env: &Env, token: &Address) -> u32 {
.unwrap_or(0)
}
-/// Append a token to the enumerable index. Idempotent: a token already indexed
-/// is left untouched (so re-registering does not duplicate it).
pub fn append_supported_token(env: &Env, token: &Address) {
if supported_token_slot(env, token) != 0 {
return;
@@ -228,8 +203,6 @@ pub fn append_supported_token(env: &Env, token: &Address) {
.set(&DataKey::SupportedTokenCount, &slot);
}
-/// Remove a token from the enumerable index via swap-with-last. Idempotent: a
-/// token not in the index is a no-op.
pub fn remove_supported_token(env: &Env, token: &Address) {
let slot = supported_token_slot(env, token);
if slot == 0 {
@@ -239,7 +212,6 @@ pub fn remove_supported_token(env: &Env, token: &Address) {
let count = supported_token_count(env);
let last_idx = count.saturating_sub(1);
- // Move the last entry into the freed slot, unless we removed the tail.
if idx != last_idx {
if let Some(last) = supported_token_at(env, last_idx) {
env.storage()
@@ -299,7 +271,6 @@ pub fn set_event(env: &Env, id: u64, record: &EventRecord) {
touch_event_persistent(env, &key);
}
-// Per-event management authority override (side map; absent => owner manages).
pub fn get_event_manager(env: &Env, id: u64) -> Option {
let key = DataKey::EventManager(id);
let m: Option = env.storage().persistent().get(&key);
@@ -317,18 +288,6 @@ pub fn set_event_manager(env: &Env, id: u64, manager: &Address) {
// ============================================================
// APPLICANTS (paged, persistent)
-//
-// applicant_count(id) -> number of applicants in [0, count).
-// applicant_at(id, idx) -> address at slot idx (Some only when idx < count).
-// applicant_slot(id, addr) -> 1-based slot. 0 means absent. Stored 1-based
-// so a missing key (default 0) signals absence
-// without an Option round trip.
-// append_applicant(id, addr) -> appends to the tail. Returns the 1-based slot.
-// Fails with TooManyApplicants if cap is hit.
-// remove_applicant(id, addr) -> swap-with-last + decrement.
-// applicants_snapshot(id, max) -> Vec view capped at `max`; older callers
-// that read the whole list should migrate
-// to paged reads.
// ============================================================
pub fn applicant_count(env: &Env, id: u64) -> u32 {
let key = DataKey::EventApplicantCount(id);
@@ -389,7 +348,6 @@ pub fn remove_applicant(env: &Env, id: u64, addr: &Address) -> Result<(), Error>
let count = applicant_count(env, id);
let last_idx = count.checked_sub(1).ok_or(Error::EventNotFound)?;
- // If not the last entry, swap the last applicant into the freed slot.
if idx != last_idx {
let last_addr = applicant_at(env, id, last_idx).ok_or(Error::EventNotFound)?;
let at_key = DataKey::EventApplicantAt(id, idx);
@@ -456,13 +414,6 @@ pub fn remove_submission(env: &Env, id: u64, applicant: &Address) {
// ============================================================
// WINNERS (paged, persistent)
-//
-// winner_count(id) -> number of winner rows (anchors + per-milestone).
-// winner_at(id, idx) -> Winner at slot idx.
-// append_winner(id, w) -> append-only; select_winners and claim_milestone
-// never rewrite existing rows.
-// winners_snapshot(id,max) -> Vec view capped at `max`. Callers needing full
-// iteration should use the paged API.
// ============================================================
pub fn winner_count(env: &Env, id: u64) -> u32 {
let key = DataKey::EventWinnerCount(id);
@@ -553,7 +504,7 @@ pub fn contributor_slot(env: &Env, id: u64, addr: &Address) -> u32 {
pub fn append_contributor(env: &Env, id: u64, addr: &Address, cap: u32) -> Result {
if contributor_slot(env, id, addr) != 0 {
- return Ok(0); // already present; caller treats 0 as "no-op"
+ return Ok(0);
}
let cur = contributor_count(env, id);
if cur >= cap {
diff --git a/contracts/events/src/tests/admin.rs b/contracts/events/src/tests/admin.rs
index 20f2036..52f1b23 100644
--- a/contracts/events/src/tests/admin.rs
+++ b/contracts/events/src/tests/admin.rs
@@ -1,5 +1,3 @@
-// boundless-events: admin tests.
-
#![cfg(test)]
use soroban_sdk::{
@@ -10,7 +8,6 @@ use soroban_sdk::{
use super::common::setup;
use crate::errors::Error;
-// Mirror the constants from admin.rs to keep timelock arithmetic readable.
const UPGRADE_TIMELOCK_LEDGERS: u32 = 17_280;
const PENDING_UPGRADE_TTL_LEDGERS: u32 = 518_400;
@@ -40,7 +37,6 @@ fn pause_and_unpause_round_trip() {
fn id_base_encodes_deployment_sequence() {
let ctx = setup(250);
let base = ctx.client.id_base();
- // id_base should be (seq << 32); lower 32 bits zero.
assert_eq!(base & 0xFFFF_FFFF, 0);
}
@@ -83,7 +79,6 @@ fn propose_upgrade_rejects_empty_version() {
.err()
.expect("empty version rejected")
.unwrap();
- // Reuse of InvalidPillar documented in admin.rs.
assert_eq!(err, Error::InvalidPillar);
}
@@ -112,7 +107,6 @@ fn apply_upgrade_after_expiry_reverts() {
let start = ctx.env.ledger().sequence();
ctx.client.propose_upgrade(&new_hash, &new_version);
- // Past the expiry.
ctx.env.ledger().with_mut(|li| {
li.sequence_number = start + PENDING_UPGRADE_TTL_LEDGERS + 1;
});
@@ -136,7 +130,6 @@ fn cancel_pending_upgrade_clears_proposal() {
ctx.client.cancel_pending_upgrade();
assert_eq!(ctx.client.get_pending_upgrade(), None);
- // Version unchanged.
assert_eq!(ctx.client.version(), String::from_str(&ctx.env, "1.1.0"));
}
diff --git a/contracts/events/src/tests/bounty_pillar.rs b/contracts/events/src/tests/bounty_pillar.rs
index a537984..a29332e 100644
--- a/contracts/events/src/tests/bounty_pillar.rs
+++ b/contracts/events/src/tests/bounty_pillar.rs
@@ -1,13 +1,3 @@
-// boundless-events: bounty pillar tests.
-//
-// Covers Pillar::Bounty paths:
-// - validate_create (Single release only)
-// - apply_to_bounty (profile bootstrap via cross-contract; credits off-chain)
-// - withdraw_application (drops the applicant slot; credits off-chain)
-// - auth, idempotency, pause, deadline, and lifecycle guards
-//
-// Spec: boundless-platform-contract-prd.md Sections 6.3, 7.
-
#![cfg(test)]
use soroban_sdk::{
@@ -174,7 +164,6 @@ fn apply_bootstraps_profile_and_records_applicant() {
ctx.events
.apply_to_bounty(&bounty_id, &ctx.applicant, &op_id);
- // Apply bootstraps the applicant's profile (credits are off-chain now).
assert!(ctx.profile.get_profile(&ctx.applicant).is_some());
let applicants = ctx.events.get_applicants(&bounty_id);
@@ -350,7 +339,6 @@ fn withdraw_removes_the_applicant() {
ctx.events
.withdraw_application(&bounty_id, &ctx.applicant, &op_wd);
- // Credits (including any refund) are off-chain; on-chain just drops the slot.
let applicants = ctx.events.get_applicants(&bounty_id);
assert_eq!(applicants.len(), 0);
}
diff --git a/contracts/events/src/tests/cancel_refund.rs b/contracts/events/src/tests/cancel_refund.rs
index 9ce2e25..628b5a2 100644
--- a/contracts/events/src/tests/cancel_refund.rs
+++ b/contracts/events/src/tests/cancel_refund.rs
@@ -1,12 +1,3 @@
-// boundless-events: cancel + refund batch tests (#28).
-//
-// Covers start_cancel / process_cancel_batch / finalize_cancel:
-// - OwnerOnly branch settled inline.
-// - FullPartnerThenResidual: partners full + owner residual.
-// - ProRataPartners: remaining < non_owner_total.
-// - Pagination across multiple batches.
-// - Error variants: wrong state, replay, not finished.
-
#![cfg(test)]
use soroban_sdk::{
@@ -241,9 +232,6 @@ fn paged_cancel_processes_in_batches() {
#[test]
fn cancel_prorata_splits_remaining_across_partners_no_owner_residual() {
- // escrow = 2000 (owner 1000 + p1 500 + p2 500); select_winners pays
- // pos1 60% (1200) leaving remaining = 800. non_owner_total = 1000.
- // 800 < 1000 -> ProRata: each partner gets 500 * 800 / 1000 = 400; owner = 0.
let ctx = setup();
let mut dist = Map::new(&ctx.env);
dist.set(1, 60);
diff --git a/contracts/events/src/tests/common.rs b/contracts/events/src/tests/common.rs
index 8dfc709..653da2e 100644
--- a/contracts/events/src/tests/common.rs
+++ b/contracts/events/src/tests/common.rs
@@ -1,5 +1,3 @@
-// boundless-events: shared test setup.
-
#![cfg(test)]
use soroban_sdk::{
@@ -11,11 +9,6 @@ use crate::event_ops::MAX_REFUNDS_PER_BATCH;
use crate::types::EventStatus;
use crate::{EventsContract, EventsContractClient};
-/// Drive a paged cancel end-to-end for the given event. Used by tests so
-/// each call site does not have to thread start_cancel / process /
-/// finalize manually. The OwnerOnly branch settles inside start_cancel,
-/// so an event with no partner contributions skips the batch + finalize
-/// steps.
#[allow(dead_code)]
pub fn drive_cancel<'a>(env: &Env, client: &EventsContractClient<'a>, id: u64) {
let op_start = BytesN::random(env);
diff --git a/contracts/events/src/tests/contributions.rs b/contracts/events/src/tests/contributions.rs
index 2e79193..cb46fe8 100644
--- a/contracts/events/src/tests/contributions.rs
+++ b/contracts/events/src/tests/contributions.rs
@@ -1,29 +1,3 @@
-// boundless-events: partner contribution + refund tests.
-//
-// Covers the open `add_funds` path and the cancel-event refund matrix.
-//
-// Refund-policy reachability note: the contract has three cancel branches:
-//
-// 1. no contributors -> owner gets everything
-// 2. remaining >= non_owner_tot -> partners in full + owner residual
-// 3. remaining < non_owner_tot -> partners pro-rata, owner gets 0
-//
-// Branch 3 is defensive code. The current contract enforces a hard
-// invariant: total payouts <= total_budget (select_winners and
-// claim_milestone both cap per-recipient amounts via total_budget *
-// percent / 100). Therefore at cancel time:
-//
-// remaining = total_budget + non_owner_total - payouts
-// >= non_owner_total
-//
-// so branch 3 is unreachable today. We still test the boundary
-// remaining == non_owner_total to exercise the partner-pays-in-full path
-// when owner residual is zero. Future op kinds (admin slashing, fee
-// adjustments) could break the invariant; the branch is kept and covered
-// by code review until then.
-//
-// Spec: boundless-partner-contributions-prd.md Sections 6 + 7.
-
#![cfg(test)]
use soroban_sdk::{
@@ -39,10 +13,8 @@ use boundless_profile::{ProfileContract, ProfileContractClient};
const FEE_BPS: u32 = 250;
-// 10 USDC at 7 decimals = 100_000_000 stroops. Mirrors the contract constant.
const MIN_CONTRIB: i128 = 100_000_000_i128;
-// Hackathon-shaped budget: 1000 USDC.
const TOTAL_BUDGET: i128 = 1_000_0000000_i128;
struct Ctx<'a> {
@@ -162,7 +134,6 @@ fn anyone_can_top_up_an_active_event() {
let amt = ctx.events.get_contributor_amount(&id, &partner);
assert_eq!(amt, amount);
- // Fee account got create-time fee plus this contribution's fee.
let token = token::Client::new(&ctx.env, &ctx.token_addr);
let initial_owner_fee = TOTAL_BUDGET * FEE_BPS as i128 / 10_000_i128;
assert_eq!(token.balance(&ctx.fee_account), initial_owner_fee + fee);
@@ -181,7 +152,6 @@ fn below_minimum_contribution_reverts() {
let res = ctx.events.try_add_funds(&id, &partner, &too_small, &op);
assert!(res.is_err(), "below-minimum contribution must revert");
- // Exactly the minimum succeeds.
let op_ok = BytesN::random(&ctx.env);
ctx.events.add_funds(&id, &partner, &MIN_CONTRIB, &op_ok);
}
@@ -353,17 +323,6 @@ fn cancel_with_partner_pool_refunds_partners_then_owner_residual() {
#[test]
fn cancel_at_boundary_pays_partners_full_no_owner_residual() {
- // Drain enough escrow so remaining_escrow == non_owner_total at cancel.
- // After M1 (select_winners pays against remaining_escrow), we cannot
- // hit the boundary by paying out 100% of distribution — that drains
- // the partner pool too. Instead we fill only one position (50%) so
- // half the escrow is paid and half remains; with partner pool sized
- // to that remainder, the cancel falls on the equality boundary.
- //
- // TOTAL_BUDGET = 1000, partner = 1000 (split 500 / 500),
- // escrow_at_select = 2000, dist = 50% + 50% (positions 1 and 2),
- // pay only position 1: paid = 1000, remaining = 1000 = partner pool.
-
let ctx = setup();
let mut dist = Map::new(&ctx.env);
dist.set(1, 50);
@@ -397,8 +356,6 @@ fn cancel_at_boundary_pays_partners_full_no_owner_residual() {
let op2 = BytesN::random(&ctx.env);
ctx.events.add_funds(&id, &p2, &c2, &op2);
- // remaining = 1000 + 1000 = 2000. Pay only position 1 at 50% of escrow
- // = 1000. remaining_after = 1000 = non_owner_total. Boundary case A.
let winner_a = Address::generate(&ctx.env);
let winners = soroban_sdk::vec![
&ctx.env,
@@ -411,7 +368,6 @@ fn cancel_at_boundary_pays_partners_full_no_owner_residual() {
let op_select = BytesN::random(&ctx.env);
ctx.events.select_winners(&id, &winners, &op_select);
- // The event isn't Completed because remaining (1000) != 0.
let after_select = ctx.events.get_event(&id);
assert_eq!(after_select.status, EventStatus::Active);
assert_eq!(after_select.remaining_escrow, 1_000_0000000_i128);
@@ -450,7 +406,6 @@ fn cancel_with_owner_top_up_keeps_owner_residual_correct() {
let op_add = BytesN::random(&ctx.env);
ctx.events.add_funds(&id, &partner, &pc, &op_add);
- // remaining = 1000 + 200 + 300 = 1500. non_owner_total = 300.
let token = token::Client::new(&ctx.env, &ctx.token_addr);
let owner_before = token.balance(&ctx.owner);
let partner_before = token.balance(&partner);
@@ -458,7 +413,6 @@ fn cancel_with_owner_top_up_keeps_owner_residual_correct() {
drive_cancel(&ctx.env, &ctx.events, id);
assert_eq!(token.balance(&partner) - partner_before, pc);
- // Owner residual = 1500 - 300 = 1200 (their original budget + top-up).
assert_eq!(token.balance(&ctx.owner) - owner_before, 1_200_0000000_i128);
let event = ctx.events.get_event(&id);
@@ -475,8 +429,6 @@ fn add_funds_paged_storage_round_trip() {
let ctx = setup();
let id = create_hackathon(&ctx);
- // Three distinct partners; verify both the legacy snapshot read and
- // the paged accessors agree on the layout.
let partners: [Address; 3] = [
Address::generate(&ctx.env),
Address::generate(&ctx.env),
@@ -504,12 +456,6 @@ fn add_funds_paged_storage_round_trip() {
#[test]
fn paged_cancel_processes_in_batches() {
- // Seed 5 partners then drive the cancel paged-flow with a batch size
- // of 2. Confirms:
- // - start_cancel flips status to Cancelling and persists the cursor
- // - process_cancel_batch refunds the next N and advances the cursor
- // - finalize_cancel pays owner residual and flips to Cancelled
- // - all partners receive their original amount (branch A)
let ctx = setup();
let id = create_hackathon(&ctx);
@@ -539,14 +485,12 @@ fn paged_cancel_processes_in_batches() {
];
let owner_before = token.balance(&ctx.owner);
- // start_cancel: Cancelling, owner unpaid yet.
let op_start = BytesN::random(&ctx.env);
ctx.events.start_cancel(&id, &op_start);
let after_start = ctx.events.get_event(&id);
assert_eq!(after_start.status, EventStatus::Cancelling);
assert_eq!(token.balance(&ctx.owner) - owner_before, 0);
- // Process in batches of 2.
let batch = 2_u32;
let op_b1 = BytesN::random(&ctx.env);
let remaining = ctx.events.process_cancel_batch(&id, &batch, &op_b1);
@@ -556,7 +500,6 @@ fn paged_cancel_processes_in_batches() {
let remaining = ctx.events.process_cancel_batch(&id, &batch, &op_b2);
assert_eq!(remaining, 1);
- // Cannot finalize yet.
let op_too_early = BytesN::random(&ctx.env);
let r = ctx.events.try_finalize_cancel(&id, &op_too_early);
assert!(r.is_err(), "finalize before cursor end must revert");
@@ -572,18 +515,14 @@ fn paged_cancel_processes_in_batches() {
assert_eq!(event.status, EventStatus::Cancelled);
assert_eq!(event.remaining_escrow, 0);
- // Each partner received their original deposit.
for (i, p) in partners.iter().enumerate() {
assert_eq!(token.balance(p) - balances_before[i], per);
}
- // Owner residual = TOTAL_BUDGET (their original deposit; partners are paid in full).
assert_eq!(token.balance(&ctx.owner) - owner_before, TOTAL_BUDGET);
}
#[test]
fn paged_cancel_owner_only_settles_inside_start() {
- // No partner contributions: start_cancel settles inline + flips Cancelled
- // in one tx. process_cancel_batch / finalize_cancel must reject.
let ctx = setup();
let id = create_hackathon(&ctx);
@@ -597,7 +536,6 @@ fn paged_cancel_owner_only_settles_inside_start() {
assert_eq!(event.status, EventStatus::Cancelled);
assert_eq!(token.balance(&ctx.owner) - owner_before, TOTAL_BUDGET);
- // No CancellationState left to process or finalize.
let op_b = BytesN::random(&ctx.env);
let r = ctx.events.try_process_cancel_batch(&id, &10_u32, &op_b);
assert!(r.is_err());
diff --git a/contracts/events/src/tests/cross_contract.rs b/contracts/events/src/tests/cross_contract.rs
index 8f9b6d6..6ada2a5 100644
--- a/contracts/events/src/tests/cross_contract.rs
+++ b/contracts/events/src/tests/cross_contract.rs
@@ -1,11 +1,3 @@
-// boundless-events: cross-contract integration test.
-//
-// Deploys boundless-events + a real boundless-profile, wires them together,
-// and exercises cross-contract flows (select_winners, submit, cancel, grants).
-// Bounty apply / withdraw coverage lives in tests/bounty_pillar.rs.
-//
-// Spec: boundless-platform-contract-prd.md Section 4; boundless-credits-reputation-prd.md Section 10.1.
-
#![cfg(test)]
use soroban_sdk::{
@@ -17,10 +9,6 @@ use super::common::drive_cancel;
use crate::types::{CreateEventParams, EventStatus, Pillar, ReleaseKind, WinnerSpec};
use crate::{EventsContract, EventsContractClient};
-// boundless-profile lives in its own crate. For the integration test we use
-// the WASM artifact path that Soroban's testutils supports. Since we are in a
-// host-target test (not wasm32v1-none), we import the profile contract crate
-// directly and register it.
use boundless_profile::{ProfileContract, ProfileContractClient};
const FEE_BPS: u32 = 250;
@@ -37,16 +25,12 @@ struct Ctx<'a> {
fn setup<'a>() -> Ctx<'a> {
let env = Env::default();
- // Auth from non-root contract invocations is required (token transfers in
- // create_event, cross-contract calls into the profile contract).
env.mock_all_auths_allowing_non_root_auth();
- // Deploy profile contract.
let profile_admin = Address::generate(&env);
let profile_id = env.register(ProfileContract, (profile_admin.clone(),));
let profile = ProfileContractClient::new(&env, &profile_id);
- // Deploy events contract pointing at profile.
let events_admin = Address::generate(&env);
let fee_account = Address::generate(&env);
let events_id = env.register(
@@ -60,23 +44,18 @@ fn setup<'a>() -> Ctx<'a> {
);
let events = EventsContractClient::new(&env, &events_id);
- // Wire profile to recognize the events contract.
profile.set_events_contract(&events_id);
- // Mock USDC via Stellar Asset Contract.
let issuer = Address::generate(&env);
let sac = env.register_stellar_asset_contract_v2(issuer);
let token_addr = sac.address();
let token_admin = token::StellarAssetClient::new(&env, &token_addr);
- // Touch fee_account's trustline (mint 0).
token_admin.mint(&fee_account, &0);
- // Mint USDC to the bounty owner.
let owner = Address::generate(&env);
token_admin.mint(&owner, &1_000_000_0000000_i128);
- // Register USDC on events.
events.register_supported_token(&token_addr);
let applicant = Address::generate(&env);
@@ -128,12 +107,10 @@ fn select_winners_pays_recipient_and_bumps_profile() {
let ctx = setup();
let bounty_id = create_bounty(&ctx);
- // Applicant applies, getting bootstrapped.
let op_apply = BytesN::random(&ctx.env);
ctx.events
.apply_to_bounty(&bounty_id, &ctx.applicant, &op_apply);
- // Owner picks applicant as the sole winner of position 1 (100% of budget).
let winners = soroban_sdk::vec![
&ctx.env,
WinnerSpec {
@@ -145,26 +122,20 @@ fn select_winners_pays_recipient_and_bumps_profile() {
let op_select = BytesN::random(&ctx.env);
ctx.events.select_winners(&bounty_id, &winners, &op_select);
- // Token: winner received the full budget (no second-layer fee on release).
let token = token::Client::new(&ctx.env, &ctx.token_addr);
assert_eq!(token.balance(&ctx.applicant), TOTAL_BUDGET);
- // Fee account got the deposit-time fee.
assert_eq!(token.balance(&ctx.fee_account), FEE_AMOUNT);
- // Profile: the win bumps reputation (credits are off-chain now).
let profile = ctx.profile.get_profile(&ctx.applicant).unwrap();
assert_eq!(profile.reputation, 50);
- // Earnings registered against the event's token.
let earnings = ctx.profile.get_earnings(&ctx.applicant, &ctx.token_addr);
assert_eq!(earnings, TOTAL_BUDGET);
- // Event completed.
let event = ctx.events.get_event(&bounty_id);
assert_eq!(event.status, EventStatus::Completed);
assert_eq!(event.remaining_escrow, 0);
- // Winner record stored.
let winner_list = ctx.events.get_winners(&bounty_id);
assert_eq!(winner_list.len(), 1);
let recorded = winner_list.get(0).unwrap();
@@ -198,8 +169,6 @@ fn select_winners_requires_position_in_distribution() {
#[test]
fn select_winners_rejects_duplicate_position() {
let ctx = setup();
- // Use a distribution split 60/40 across positions 1 and 2 so the test
- // can supply a duplicate position 1 entry.
let owner = ctx.owner.clone();
let token_addr = ctx.token_addr.clone();
let mut dist = Map::new(&ctx.env);
@@ -258,7 +227,6 @@ fn select_winners_handles_multi_recipient_distribution() {
title: String::from_str(&ctx.env, "Multi Winner"),
deadline: Some(ctx.env.ledger().timestamp() + 86_400),
winner_distribution: dist,
- // free for this test
fee_bps_override: None,
manager: None,
};
@@ -350,7 +318,6 @@ fn cancel_already_cancelled_reverts() {
drive_cancel(&ctx.env, &ctx.events, bounty_id);
- // Second start_cancel on a Cancelled event must revert.
let op_again = BytesN::random(&ctx.env);
let res = ctx.events.try_start_cancel(&bounty_id, &op_again);
assert!(res.is_err(), "second cancel should revert");
@@ -359,7 +326,6 @@ fn cancel_already_cancelled_reverts() {
#[test]
fn cancel_after_select_winners_refunds_only_remaining() {
let ctx = setup();
- // 60/40 split so we can pay one winner and then cancel.
let mut dist = Map::new(&ctx.env);
dist.set(1, 60);
dist.set(2, 40);
@@ -379,7 +345,6 @@ fn cancel_after_select_winners_refunds_only_remaining() {
let op_create = BytesN::random(&ctx.env);
let bounty_id = ctx.events.create_event(¶ms, &op_create);
- // Pay one winner (60%).
let winner_a = Address::generate(&ctx.env);
let winners = soroban_sdk::vec![
&ctx.env,
@@ -392,7 +357,6 @@ fn cancel_after_select_winners_refunds_only_remaining() {
let op_select = BytesN::random(&ctx.env);
ctx.events.select_winners(&bounty_id, &winners, &op_select);
- // Now cancel — owner should get the remaining 40%.
let token = token::Client::new(&ctx.env, &ctx.token_addr);
let owner_before = token.balance(&ctx.owner);
@@ -436,7 +400,6 @@ fn select_grant_winner(ctx: &Ctx, grant_id: u64, recipient: &Address) {
WinnerSpec {
recipient: recipient.clone(),
position: 1,
- // ignored for Multi; payment-time bumps apply per milestone
reputation_bump: 0,
},
];
@@ -458,19 +421,15 @@ fn claim_milestone_pays_per_milestone_amount() {
ctx.events
.claim_milestone(&grant_id, &recipient, &0_u32, &5_u32, &op_claim);
- // Per-milestone amount: total_budget * 100% / 4 = TOTAL_BUDGET / 4
let per_milestone = TOTAL_BUDGET / 4;
assert_eq!(token.balance(&recipient) - recipient_before, per_milestone);
- // Profile: the milestone claim bumps reputation by 5 (credits off-chain).
let profile = ctx.profile.get_profile(&recipient).unwrap();
assert_eq!(profile.reputation, 5);
- // Earnings registered.
let earnings = ctx.profile.get_earnings(&recipient, &ctx.token_addr);
assert_eq!(earnings, per_milestone);
- // Event still Active, remaining_escrow decremented.
let event = ctx.events.get_event(&grant_id);
assert_eq!(event.status, EventStatus::Active);
assert_eq!(event.remaining_escrow, TOTAL_BUDGET - per_milestone);
@@ -487,14 +446,12 @@ fn claim_milestone_idempotent_per_recipient_and_milestone() {
ctx.events
.claim_milestone(&grant_id, &recipient, &0_u32, &5_u32, &op1);
- // Same milestone, different op_id — should still revert.
let op2 = BytesN::random(&ctx.env);
let res = ctx
.events
.try_claim_milestone(&grant_id, &recipient, &0_u32, &5_u32, &op2);
assert!(res.is_err(), "same milestone twice should revert");
- // Different milestone — should succeed.
let op3 = BytesN::random(&ctx.env);
ctx.events
.claim_milestone(&grant_id, &recipient, &1_u32, &5_u32, &op3);
@@ -507,7 +464,6 @@ fn claim_milestone_invalid_milestone_index_reverts() {
let grant_id = create_grant(&ctx, 4);
select_grant_winner(&ctx, grant_id, &recipient);
- // Milestone index 4 is out of range for a 4-milestone grant (valid: 0..=3).
let op = BytesN::random(&ctx.env);
let res = ctx
.events
@@ -531,9 +487,6 @@ fn claim_milestone_rejects_non_grant_events() {
let op_select = BytesN::random(&ctx.env);
ctx.events.select_winners(&bounty_id, &winners, &op_select);
- // The bounty is Completed now anyway, but claim_milestone should reject
- // even an Active Single-release event. Recreate a Single bounty without
- // winners selected:
let bounty_id2 = create_bounty(&ctx);
let op = BytesN::random(&ctx.env);
let res = ctx
@@ -701,20 +654,10 @@ fn withdraw_submission_without_submission_reverts() {
// ============================================================
// Per-event fee_bps_override
-//
-// Sales-side discount / comp / waiver lives on the event record, not the
-// global admin config. Tests below cover:
-// 1. create_event with Some(override) charges the override rate.
-// 2. add_funds reads the override from the event (not the live default).
-// 3. waiver (Some(0)) costs the owner exactly total_budget with no fee.
-// 4. an admin change to the global default does not retroactively re-price
-// in-flight events that snapshotted an override.
-// 5. publish rejects override > MAX_FEE_BPS.
// ============================================================
#[test]
fn create_event_charges_override_rate_when_provided() {
let ctx = setup();
- // Override to 1.5% (Hackathon launch rate) while the contract default is 2.5%.
let override_bps: u32 = 150;
let total_budget: i128 = 100_000_0000000_i128; // 100k USDC
let expected_fee = total_budget * (override_bps as i128) / 10_000;
@@ -739,15 +682,12 @@ fn create_event_charges_override_rate_when_provided() {
let op = BytesN::random(&ctx.env);
let id = ctx.events.create_event(¶ms, &op);
- // Owner paid total_budget + override_fee.
let owner_after = token.balance(&ctx.owner);
assert_eq!(owner_before - owner_after, total_budget + expected_fee);
- // Fee account received exactly the override fee.
let fee_after = token.balance(&ctx.fee_account);
assert_eq!(fee_after - fee_before, expected_fee);
- // Event record snapshotted the override.
let event = ctx.events.get_event(&id);
assert_eq!(event.fee_bps_override, Some(override_bps));
assert_eq!(event.remaining_escrow, total_budget);
@@ -756,7 +696,6 @@ fn create_event_charges_override_rate_when_provided() {
#[test]
fn add_funds_uses_event_override_not_global() {
let ctx = setup();
- // Publish at a 0.5% promo rate.
let override_bps: u32 = 50;
let total_budget: i128 = 10_000_0000000_i128;
@@ -776,13 +715,8 @@ fn add_funds_uses_event_override_not_global() {
let op_create = BytesN::random(&ctx.env);
let id = ctx.events.create_event(¶ms, &op_create);
- // Admin bumps global default upward; in-flight event must not re-price.
ctx.events.set_fee_bps(&500);
- // Partner contributes 1_000 USDC. The fee should be at 0.5% (override),
- // not 5% (new global). Mint 2_000 USDC so the partner balance comfortably
- // covers amount + fee in either branch (so the test fails on the math
- // assertion if the override is ignored, not on a balance error).
let partner = Address::generate(&ctx.env);
let token_admin = token::StellarAssetClient::new(&ctx.env, &ctx.token_addr);
token_admin.mint(&partner, &2_000_0000000_i128);
@@ -847,7 +781,6 @@ fn create_event_rejects_override_above_max_fee_bps() {
title: String::from_str(&ctx.env, "Bad rate"),
deadline: Some(ctx.env.ledger().timestamp() + 86_400),
winner_distribution: one_winner_distribution(&ctx.env),
- // 60% is above the MAX_FEE_BPS = 1000 cap (post L4 audit fix).
fee_bps_override: Some(6000),
manager: None,
};
@@ -883,18 +816,12 @@ fn create_event_omitted_override_falls_back_to_global_default() {
let fee_after = token.balance(&ctx.fee_account);
assert_eq!(fee_after - fee_before, expected_fee);
- // Event keeps None so future add_funds also pulls the live default.
let event = ctx.events.get_event(&id);
assert_eq!(event.fee_bps_override, None);
}
// ============================================================
// select_winners replay lock
-//
-// Calling select_winners twice would silently overwrite the anchor winner
-// records. The contract now rejects the second call so that downstream
-// claim_milestone reads and off-chain audits read a stable winner set.
-// Tested on Grant (Multi); the same code path serves Single releases.
// ============================================================
#[test]
fn select_winners_rejects_second_call_winners_already_selected() {
@@ -903,7 +830,6 @@ fn select_winners_rejects_second_call_winners_already_selected() {
let grant_id = create_grant(&ctx, 2);
select_grant_winner(&ctx, grant_id, &r1);
- // Second call (different recipient, fresh op_id) must be rejected.
let r2 = Address::generate(&ctx.env);
let winners = soroban_sdk::vec![
&ctx.env,
@@ -917,7 +843,6 @@ fn select_winners_rejects_second_call_winners_already_selected() {
let res = ctx.events.try_select_winners(&grant_id, &winners, &op);
assert!(res.is_err(), "second select_winners must revert");
- // First selection is untouched.
let recorded = ctx.events.get_winners(&grant_id);
assert_eq!(recorded.len(), 1);
assert_eq!(recorded.get(0).unwrap().recipient, r1);
@@ -925,27 +850,18 @@ fn select_winners_rejects_second_call_winners_already_selected() {
// ============================================================
// Grant last-milestone sweep
-//
-// Per-milestone math floors total_budget * percent / 100 / n_milestones. With
-// a budget that does not divide evenly across milestones the floored amount
-// strands a small residue in escrow. The last milestone for the recipient
-// now sweeps that residue so total paid equals their full position share.
// ============================================================
#[test]
fn grant_last_milestone_sweeps_rounding_residue() {
let ctx = setup();
let recipient = Address::generate(&ctx.env);
- // 100k / 3 milestones = 33,333.3333333 USDC each at 7 decimals.
- // Floored per-milestone: floor(100_000_0000000 / 3) = 33_333_3333333 stroops.
- // Residue: 100_000_0000000 - 3 * 33_333_3333333 = 1 stroop.
let grant_id = create_grant(&ctx, 3);
select_grant_winner(&ctx, grant_id, &recipient);
let token = token::Client::new(&ctx.env, &ctx.token_addr);
let before = token.balance(&recipient);
- // Claim m0 + m1 at floored rate.
let floored = TOTAL_BUDGET / 3;
ctx.events.claim_milestone(
&grant_id,
@@ -964,7 +880,6 @@ fn grant_last_milestone_sweeps_rounding_residue() {
let after_two = token.balance(&recipient);
assert_eq!(after_two - before, floored * 2);
- // Last claim: sweep so the recipient ends up with the full position share.
ctx.events.claim_milestone(
&grant_id,
&recipient,
@@ -979,7 +894,6 @@ fn grant_last_milestone_sweeps_rounding_residue() {
"last milestone must sweep residue: recipient receives full position share"
);
- // Event drained completely and marks Completed.
let event = ctx.events.get_event(&grant_id);
assert_eq!(event.remaining_escrow, 0);
assert_eq!(event.status, EventStatus::Completed);
@@ -991,10 +905,6 @@ fn grant_last_milestone_sweeps_rounding_residue() {
#[test]
fn select_winners_pays_against_remaining_escrow_including_top_ups() {
- // Owner deposits TOTAL_BUDGET. Partner tops up another 5_000 USDC.
- // Single winner at 100% should receive owner_budget + partner_top_up
- // (net of fees). Pre-audit this was capped at TOTAL_BUDGET and the
- // top-up would have stayed trapped until cancel.
let ctx = setup();
let bounty_id = create_bounty(&ctx);
@@ -1007,11 +917,9 @@ fn select_winners_pays_against_remaining_escrow_including_top_ups() {
let op_add = BytesN::random(&ctx.env);
ctx.events.add_funds(&bounty_id, &partner, &top_up, &op_add);
- // Confirm the event escrow grew.
let event_pre = ctx.events.get_event(&bounty_id);
assert_eq!(event_pre.remaining_escrow, TOTAL_BUDGET + top_up);
- // Single winner at 100%.
let winners = soroban_sdk::vec![
&ctx.env,
WinnerSpec {
@@ -1024,11 +932,8 @@ fn select_winners_pays_against_remaining_escrow_including_top_ups() {
ctx.events.select_winners(&bounty_id, &winners, &op_select);
let token = token::Client::new(&ctx.env, &ctx.token_addr);
- // Winner receives the full live escrow at select time, not the
- // original total_budget.
assert_eq!(token.balance(&ctx.applicant), TOTAL_BUDGET + top_up);
- // Event drained, status Completed, no orphaned partner top-up.
let event_post = ctx.events.get_event(&bounty_id);
assert_eq!(event_post.remaining_escrow, 0);
assert_eq!(event_post.status, EventStatus::Completed);
@@ -1059,11 +964,9 @@ fn create_bounty_with_manager(ctx: &Ctx, manager: &Address) -> u64 {
fn manager_defaults_to_owner_and_override_is_recorded() {
let ctx = setup();
- // No override: management falls back to the funder/owner (legacy behavior).
let default_id = create_bounty(&ctx);
assert_eq!(ctx.events.get_manager(&default_id), ctx.owner);
- // Override: management is the org wallet, distinct from the funder.
let manager = Address::generate(&ctx.env);
let managed_id = create_bounty_with_manager(&ctx, &manager);
assert_eq!(ctx.events.get_manager(&managed_id), manager);
@@ -1103,7 +1006,6 @@ fn manager_override_can_select_winners() {
let op_select = BytesN::random(&ctx.env);
ctx.events.select_winners(&bounty_id, &winners, &op_select);
- // Managed event settled via the manager authority.
let event = ctx.events.get_event(&bounty_id);
assert_eq!(event.status, EventStatus::Completed);
}
diff --git a/contracts/events/src/tests/crowdfunding.rs b/contracts/events/src/tests/crowdfunding.rs
index 1b79d4f..7c281a3 100644
--- a/contracts/events/src/tests/crowdfunding.rs
+++ b/contracts/events/src/tests/crowdfunding.rs
@@ -1,16 +1,3 @@
-// boundless-events: crowdfunding tests.
-//
-// Covers the Pillar::Crowdfunding paths:
-// - create_event with zero owner deposit, builder auto-registered as Winner
-// - validate_create rejects single-release / missing-deadline / wrong dist
-// - add_funds drives escrow from 0 up to whatever was raised
-// - claim_milestone dynamic math: amount = remaining / (total - claimed)
-// - last milestone drains remainder including rounding dust
-// - select_winners and submit are rejected for Crowdfunding
-// - cancel_event refunds all contributors (no owner deposit to refund)
-//
-// Spec: boundless-crowdfunding-prd.md.
-
#![cfg(test)]
use soroban_sdk::{
@@ -26,7 +13,6 @@ use boundless_profile::{ProfileContract, ProfileContractClient};
const FEE_BPS: u32 = 250;
-// Builder's stated funding goal (informational only on-chain).
const FUNDING_GOAL: i128 = 1_000_0000000_i128;
struct Ctx<'a> {
@@ -69,7 +55,6 @@ fn setup<'a>() -> Ctx<'a> {
let token_admin = token::StellarAssetClient::new(&env, &token_addr);
token_admin.mint(&fee_account, &0);
- // Builder pays nothing upfront, so no mint needed for them.
let builder = Address::generate(&env);
events.register_supported_token(&token_addr);
@@ -133,7 +118,6 @@ fn create_with_zero_owner_deposit_and_auto_registered_winner() {
let id = create_campaign(&ctx, 3);
- // No deposit pulled from builder. Builder balance unchanged.
assert_eq!(token.balance(&ctx.builder), builder_before);
let event = ctx.events.get_event(&id);
@@ -144,7 +128,6 @@ fn create_with_zero_owner_deposit_and_auto_registered_winner() {
"crowdfunding starts with empty escrow"
);
- // Builder auto-registered as Winner at position 1, milestone=None.
let winners = ctx.events.get_winners(&id);
assert_eq!(winners.len(), 1);
let w = winners.get(0).unwrap();
@@ -243,9 +226,6 @@ fn community_top_ups_raise_escrow_from_zero() {
#[test]
fn builder_top_up_does_not_appear_in_contributor_list() {
- // The builder is allowed to add funds (matching the owner-top-up rule).
- // Their entry shouldn't appear in ContributorList so cancel_event treats
- // them as residual, not partner.
let ctx = setup();
let id = create_campaign(&ctx, 3);
@@ -267,12 +247,6 @@ fn builder_top_up_does_not_appear_in_contributor_list() {
#[test]
fn claim_milestone_splits_evenly_and_charges_fee_at_release() {
- // 3 milestones, escrow raised = 900 USDC. The builder bears the 2.5% fee,
- // taken from each payout (backers deposited their full pledge fee-free):
- // m0 -> 900/3 = 300 gross; builder +292.5, fee_account +7.5; leaves 600.
- // m1 -> 600/2 = 300 gross; builder +292.5, fee_account +7.5; leaves 300.
- // m2 -> 300 gross (final); builder +292.5, fee_account +7.5; leaves 0.
- // Builder nets 877.5 (= 900 * 0.975); platform collects 22.5 (= 900 * .025).
let ctx = setup();
let id = create_campaign(&ctx, 3);
let backer = Address::generate(&ctx.env);
@@ -313,10 +287,6 @@ fn claim_milestone_splits_evenly_and_charges_fee_at_release() {
#[test]
fn claim_milestone_last_drains_dust_with_fee() {
- // 3 milestones, raised 100_000_001 stroops (just above MIN_CONTRIB and not
- // divisible by 3). The builder nets the raised amount minus the 2.5% fee;
- // the fee account collects the fee; together they drain escrow exactly so
- // no dust is stranded, even with per-milestone rounding.
let ctx = setup();
let id = create_campaign(&ctx, 3);
let backer = Address::generate(&ctx.env);
@@ -392,10 +362,6 @@ fn claim_milestone_with_empty_escrow_reverts() {
#[test]
fn backer_pays_exactly_pledge_and_creator_bears_fee() {
- // Regression for the fee-on-top bug: a backer holding EXACTLY their pledge
- // must be able to fund. The old model pulled pledge + fee from the backer
- // and reverted for a wallet that held only the pledge. Now the fee is borne
- // by the builder and taken at release, so deposit pulls exactly the pledge.
let ctx = setup();
let id = create_campaign(&ctx, 1);
@@ -409,7 +375,6 @@ fn backer_pays_exactly_pledge_and_creator_bears_fee() {
let op = BytesN::random(&ctx.env);
ctx.events.add_funds(&id, &backer, &pledge, &op);
- // Backer paid exactly their pledge; no fee taken at deposit.
assert_eq!(token.balance(&backer), 0, "backer pays exactly the pledge");
assert_eq!(
token.balance(&ctx.fee_account),
@@ -418,7 +383,6 @@ fn backer_pays_exactly_pledge_and_creator_bears_fee() {
);
assert_eq!(ctx.events.get_event(&id).remaining_escrow, pledge);
- // Single milestone: the builder claims and the fee is taken from the payout.
let claim = BytesN::random(&ctx.env);
ctx.events
.claim_milestone(&id, &ctx.builder, &0_u32, &0, &claim);
@@ -518,10 +482,6 @@ fn cancel_with_no_contributions_just_marks_cancelled() {
#[test]
fn cancel_after_partial_claim_pro_rates_remaining() {
- // Builder claimed m0 already, then campaign is cancelled. Remaining < non_owner_total.
- // 3 milestones, raised 900, claim m0 -> 300 to builder, remaining 600.
- // Cancel: non_owner_total still 900 (we don't subtract from contributor ledger
- // on claim), remaining = 600 < 900 -> case B pro-rata.
let ctx = setup();
let id = create_campaign(&ctx, 3);
@@ -540,9 +500,6 @@ fn cancel_after_partial_claim_pro_rates_remaining() {
drive_cancel(&ctx.env, &ctx.events, id);
- // remaining = 600, non_owner_total = 900.
- // p1 share = 300 * 600 / 900 = 200.
- // p2 share = 600 * 600 / 900 = 400.
assert_eq!(token.balance(&p1) - p1_before, 200_0000000_i128);
assert_eq!(token.balance(&p2) - p2_before, 400_0000000_i128);
@@ -557,10 +514,6 @@ fn cancel_after_partial_claim_pro_rates_remaining() {
#[test]
fn crowdfunding_claim_milestone_requires_admin_auth() {
- // Verify the admin's address is among the required auths for a
- // crowdfunding claim. mock_all_auths_allowing_non_root_auth makes the
- // call succeed, but env.auths() records the addresses whose auth was
- // demanded, which is the audit-relevant observation.
let ctx = setup();
let id = create_campaign(&ctx, 2);
let p = Address::generate(&ctx.env);
diff --git a/contracts/events/src/tests/escrow_fee_math.rs b/contracts/events/src/tests/escrow_fee_math.rs
index 502d76a..750c022 100644
--- a/contracts/events/src/tests/escrow_fee_math.rs
+++ b/contracts/events/src/tests/escrow_fee_math.rs
@@ -1,14 +1,3 @@
-// boundless-events: escrow fee + release math tests.
-//
-// Covers every function in escrow.rs (effective_fee_bps, compute_fee_at,
-// compute_fee, deposit_with_fee_at, deposit_with_fee, release) plus the
-// payout-split math in select_winners (Single) and claim_milestone (Multi /
-// Crowdfunding). Tests verify rounding, override bps, zero-fee edges,
-// auth rejection, and idempotency where relevant.
-//
-// Spec: boundless-platform-contract-prd.md Sections 6.2, 7, 8;
-// issue #31.
-
#![cfg(test)]
use soroban_sdk::{
@@ -252,7 +241,6 @@ fn override_at_max_bps_boundary_succeeds() {
#[test]
fn fee_rounds_down_non_divisible_amount() {
- // 1 stroop * 250 bps / 10_000 = 0.025 → truncates to 0.
let ctx = setup();
let token = token::Client::new(&ctx.env, &ctx.token_addr);
@@ -275,14 +263,12 @@ fn fee_rounds_down_non_divisible_amount() {
ctx.events.create_event(¶ms, &op);
let owner_after = token.balance(&ctx.owner);
- // fee = 1 * 250 / 10_000 = 0 (truncated)
assert_eq!(owner_before - owner_after, tiny_budget);
assert_eq!(token.balance(&ctx.fee_account), 0);
}
#[test]
fn fee_rounding_on_odd_amounts() {
- // 333 stroops at 250 bps: 333 * 250 / 10_000 = 8.325 → 8 stroops.
let ctx = setup();
let token = token::Client::new(&ctx.env, &ctx.token_addr);
@@ -482,20 +468,16 @@ fn three_way_33_33_34_split_rounding() {
let token = token::Client::new(&ctx.env, &ctx.token_addr);
let escrow = TOTAL_BUDGET;
- // 33% of 1000_0000000 = 330_0000000 (exact)
- // 34% of 1000_0000000 = 340_0000000 (exact)
assert_eq!(token.balance(&w1), escrow * 33 / 100);
assert_eq!(token.balance(&w2), escrow * 33 / 100);
assert_eq!(token.balance(&w3), escrow * 34 / 100);
let event = ctx.events.get_event(&id);
- // 330 + 330 + 340 = 1000. Exact, no dust.
assert_eq!(event.remaining_escrow, 0);
}
#[test]
fn partial_position_fill_leaves_residual_escrow() {
- // Fill only 1 of 2 positions → event stays Active with residual escrow.
let ctx = setup();
let mut dist = Map::new(&ctx.env);
@@ -585,21 +567,16 @@ fn grant_milestone_pays_floored_per_milestone() {
let token = token::Client::new(&ctx.env, &ctx.token_addr);
- // total_share = TOTAL_BUDGET * 100 / 100 = TOTAL_BUDGET
- // per_milestone_floored = TOTAL_BUDGET / 3 = 333_3333333 (floor)
let per_milestone = TOTAL_BUDGET / milestones as i128;
- // Milestone 0
let op_m0 = BytesN::random(&ctx.env);
ctx.events.claim_milestone(&id, &recipient, &0, &50, &op_m0);
assert_eq!(token.balance(&recipient), per_milestone);
- // Milestone 1
let op_m1 = BytesN::random(&ctx.env);
ctx.events.claim_milestone(&id, &recipient, &1, &50, &op_m1);
assert_eq!(token.balance(&recipient), per_milestone * 2);
- // Milestone 2 (last): sweep — pays total_share - already_paid
let op_m2 = BytesN::random(&ctx.env);
ctx.events.claim_milestone(&id, &recipient, &2, &50, &op_m2);
assert_eq!(
@@ -632,7 +609,6 @@ fn grant_milestone_double_claim_rejected() {
let op_m0 = BytesN::random(&ctx.env);
ctx.events.claim_milestone(&id, &recipient, &0, &50, &op_m0);
- // Same milestone again → MilestoneAlreadyClaimed
let op_m0_dup = BytesN::random(&ctx.env);
let res = ctx
.events
@@ -658,7 +634,6 @@ fn grant_milestone_out_of_range_rejected() {
let op_sel = BytesN::random(&ctx.env);
ctx.events.select_winners(&id, &winners, &op_sel);
- // Milestone 2 is out-of-range for Multi(2) (valid: 0, 1)
let op = BytesN::random(&ctx.env);
let res = ctx
.events
@@ -693,7 +668,6 @@ fn crowdfunding_dynamic_milestone_split() {
let op_create = BytesN::random(&ctx.env);
let id = ctx.events.create_event(¶ms, &op_create);
- // Fund from a backer — crowdfunding backers pay NO fee on deposit (deposit_no_fee).
let backer = Address::generate(&ctx.env);
let raised = 900_0000000_i128;
fund(&ctx, &backer, raised);
@@ -703,19 +677,14 @@ fn crowdfunding_dynamic_milestone_split() {
let token = token::Client::new(&ctx.env, &ctx.token_addr);
let owner_before = token.balance(&ctx.owner);
- // Crowdfunding claim_milestone uses release_with_fee_at: builder bears
- // the fee at release. Each milestone amount = remaining / remaining_milestones;
- // builder receives amount - fee.
let milestone_amount = raised / 3; // 300_0000000
let milestone_fee = milestone_amount * FEE_BPS as i128 / 10_000;
let net_per_milestone = milestone_amount - milestone_fee;
- // M0: amount = 900 / 3 = 300, net = 300 - fee
let op_m0 = BytesN::random(&ctx.env);
ctx.events.claim_milestone(&id, &ctx.owner, &0, &50, &op_m0);
assert_eq!(token.balance(&ctx.owner) - owner_before, net_per_milestone);
- // M1: remaining = 600 / 2 = 300, net = 300 - fee
let op_m1 = BytesN::random(&ctx.env);
ctx.events.claim_milestone(&id, &ctx.owner, &1, &50, &op_m1);
assert_eq!(
@@ -723,7 +692,6 @@ fn crowdfunding_dynamic_milestone_split() {
net_per_milestone * 2
);
- // M2: remaining = 300 / 1 = 300, net = 300 - fee
let op_m2 = BytesN::random(&ctx.env);
ctx.events.claim_milestone(&id, &ctx.owner, &2, &50, &op_m2);
assert_eq!(
@@ -737,11 +705,6 @@ fn crowdfunding_dynamic_milestone_split() {
#[test]
fn crowdfunding_dynamic_rounding_no_dust() {
- // Raise 1_000_0000001 stroops and split across 3 milestones.
- // 1_000_0000001 / 3 = 333_3333333 (floor), remaining = 666_6666668
- // 666_6666668 / 2 = 333_3333334 (floor), remaining = 333_3333334
- // last milestone takes entire remainder = 333_3333334
- // Total paid = 333_3333333 + 333_3333334 + 333_3333334 = 1_000_0000001 ✓
let ctx = setup();
let milestones = 3_u32;
@@ -953,7 +916,6 @@ fn select_winners_twice_reverts() {
let op1 = BytesN::random(&ctx.env);
ctx.events.select_winners(&id, &winners, &op1);
- // Second selection on the same event → WinnersAlreadySelected
let op2 = BytesN::random(&ctx.env);
let res = ctx.events.try_select_winners(&id, &winners, &op2);
assert!(
@@ -1003,10 +965,8 @@ fn mid_flight_global_bps_change_does_not_affect_override_event() {
let override_bps: u32 = 100;
let id = create_hackathon_with_override(&ctx, override_bps);
- // Admin changes the global fee bps
ctx.events.set_fee_bps(&500);
- // Partner add_funds should still use the event's override (100), not the new global (500)
let partner = Address::generate(&ctx.env);
let contrib = 500_0000000_i128;
let partner_fee = contrib * override_bps as i128 / 10_000;
diff --git a/contracts/events/src/tests/grant_pillar.rs b/contracts/events/src/tests/grant_pillar.rs
index f2adab5..40db2a0 100644
--- a/contracts/events/src/tests/grant_pillar.rs
+++ b/contracts/events/src/tests/grant_pillar.rs
@@ -1,9 +1,3 @@
-// boundless-events: grant pillar tests (#32).
-//
-// Covers validate_create (Multi release required) + claim_milestone
-// fixed-split math, last-milestone dust sweep, credit/rep side-effects,
-// and error variants.
-
#![cfg(test)]
use soroban_sdk::{
diff --git a/contracts/events/src/tests/hackathon_pillar.rs b/contracts/events/src/tests/hackathon_pillar.rs
index ab6d21a..4935cc5 100644
--- a/contracts/events/src/tests/hackathon_pillar.rs
+++ b/contracts/events/src/tests/hackathon_pillar.rs
@@ -1,24 +1,3 @@
-// boundless-events: hackathon pillar tests.
-//
-// Covers the Pillar::Hackathon paths end-to-end against a real
-// boundless-profile so the cross-contract reputation / earnings side-effects
-// of select_winners are exercised, not mocked:
-//
-// - create_event validation: Hackathon requires ReleaseKind::Single and a
-// future deadline; the full budget is escrowed (fee taken at deposit).
-// - submit: open submission model with no prior apply.
-// deadline gate, re-submit, idempotency, withdraw.
-// - select_winners distribution: single-recipient sweep and multi-position
-// split. Each split test asserts BOTH recipient and fee-account deltas
-// (CLAUDE.md hard rule), plus profile bumps and the stored winner rows.
-// - select_winners rejections: empty set, position not in distribution,
-// duplicate position, replay (WinnersAlreadySelected), missing event,
-// already-completed event, and owner-auth requirement.
-// - claim_milestone is rejected for a Single-release hackathon.
-//
-// Spec: boundless-platform-contract-prd.md Section 7. Template:
-// src/tests/crowdfunding.rs and src/tests/cross_contract.rs.
-
#![cfg(test)]
use soroban_sdk::{
@@ -33,7 +12,6 @@ use boundless_profile::{ProfileContract, ProfileContractClient};
const FEE_BPS: u32 = 250;
-// 10k USDC at 7 decimals.
const TOTAL_BUDGET: i128 = 10_000_0000000_i128;
const FEE_AMOUNT: i128 = (TOTAL_BUDGET * FEE_BPS as i128) / 10_000_i128;
@@ -50,8 +28,6 @@ struct Ctx<'a> {
fn setup<'a>() -> Ctx<'a> {
let env = Env::default();
- // Non-root auth needed for token transfers and the cross-contract calls
- // into the profile contract during select_winners.
env.mock_all_auths_allowing_non_root_auth();
let profile_admin = Address::generate(&env);
@@ -77,7 +53,6 @@ fn setup<'a>() -> Ctx<'a> {
let token_addr = sac.address();
let token_admin = token::StellarAssetClient::new(&env, &token_addr);
- // Touch fee_account's trustline (mint 0) and fund the owner.
token_admin.mint(&fee_account, &0);
let owner = Address::generate(&env);
token_admin.mint(&owner, &1_000_000_0000000_i128);
@@ -104,7 +79,6 @@ fn single_winner_dist(env: &Env) -> Map {
m
}
-// 50 / 30 / 20 across positions 1..=3.
fn three_way_dist(env: &Env) -> Map {
let mut m = Map::new(env);
m.set(1, 50);
@@ -156,7 +130,6 @@ fn create_deposits_full_budget_and_takes_fee_at_deposit() {
"hackathon escrows the full budget at create"
);
- // Owner paid budget + fee; fee account received the deposit-time fee.
assert_eq!(
owner_before - token.balance(&ctx.owner),
TOTAL_BUDGET + FEE_AMOUNT
@@ -209,7 +182,6 @@ fn create_rejects_missing_deadline() {
#[test]
fn create_rejects_past_deadline() {
let ctx = setup();
- // try_ variant so we observe the error instead of panicking on the host.
let params = CreateEventParams {
pillar: Pillar::Hackathon,
owner: ctx.owner.clone(),
@@ -218,7 +190,6 @@ fn create_rejects_past_deadline() {
release_kind: ReleaseKind::Single,
content_uri: String::from_str(&ctx.env, "uri"),
title: String::from_str(&ctx.env, "Hackathon"),
- // Equal-to-now is not in the future; create_event rejects it.
deadline: Some(ctx.env.ledger().timestamp()),
winner_distribution: single_winner_dist(&ctx.env),
fee_bps_override: None,
@@ -272,7 +243,6 @@ fn submit_after_deadline_reverts() {
let ctx = setup();
let id = create_hackathon(&ctx);
- // Jump the ledger past the 1-day submission deadline.
ctx.env.ledger().with_mut(|li| {
li.timestamp += 2 * 86_400;
});
@@ -336,12 +306,9 @@ fn select_winners_single_recipient_sweeps_escrow() {
let op = BytesN::random(&ctx.env);
ctx.events.select_winners(&id, &winners, &op);
- // Recipient delta: full budget. Fee account delta: 0 — the fee was taken
- // at deposit, never a second time on release.
assert_eq!(token.balance(&ctx.applicant) - winner_before, TOTAL_BUDGET);
assert_eq!(token.balance(&ctx.fee_account) - fee_before, 0);
- // Profile: fresh winner is bootstrapped then bumped for the win.
let p = ctx.profile.get_profile(&ctx.applicant).unwrap();
assert_eq!(p.reputation, 50);
assert_eq!(
@@ -349,7 +316,6 @@ fn select_winners_single_recipient_sweeps_escrow() {
TOTAL_BUDGET
);
- // Escrow drained -> Completed; winner row recorded.
let event = ctx.events.get_event(&id);
assert_eq!(event.status, EventStatus::Completed);
assert_eq!(event.remaining_escrow, 0);
@@ -402,14 +368,11 @@ fn select_winners_multi_position_splits_by_distribution() {
let amt_2 = TOTAL_BUDGET * 30 / 100;
let amt_3 = TOTAL_BUDGET * 20 / 100;
- // Recipient deltas across the split.
assert_eq!(token.balance(&first), amt_1);
assert_eq!(token.balance(&second), amt_2);
assert_eq!(token.balance(&third), amt_3);
- // Fee account delta across the split: unchanged (no release-time fee).
assert_eq!(token.balance(&ctx.fee_account) - fee_before, 0);
- // Profile bumps per winner.
let p1 = ctx.profile.get_profile(&first).unwrap();
let p2 = ctx.profile.get_profile(&second).unwrap();
let p3 = ctx.profile.get_profile(&third).unwrap();
@@ -417,7 +380,6 @@ fn select_winners_multi_position_splits_by_distribution() {
assert_eq!(p2.reputation, 40);
assert_eq!(p3.reputation, 20);
- // 50 + 30 + 20 == 100 -> escrow fully drained -> Completed.
let event = ctx.events.get_event(&id);
assert_eq!(event.status, EventStatus::Completed);
assert_eq!(event.remaining_escrow, 0);
@@ -486,8 +448,6 @@ fn select_winners_duplicate_position_reverts() {
fn select_winners_second_call_reverts_winners_already_selected() {
let ctx = setup();
let dl = Some(ctx.env.ledger().timestamp() + 86_400);
- // 50/30/20 so the first call pays only position 1 and leaves the event
- // Active, isolating WinnersAlreadySelected from EventNotActive.
let id = create_hackathon_with(&ctx, three_way_dist(&ctx.env), dl);
let first_winner = soroban_sdk::vec![
@@ -501,7 +461,6 @@ fn select_winners_second_call_reverts_winners_already_selected() {
let op1 = BytesN::random(&ctx.env);
ctx.events.select_winners(&id, &first_winner, &op1);
- // Event is still Active (60% escrow remains), but a prior anchor exists.
assert_eq!(ctx.events.get_event(&id).status, EventStatus::Active);
let second = Address::generate(&ctx.env);
@@ -590,9 +549,6 @@ fn select_winners_on_completed_event_reverts() {
#[test]
fn select_winners_demands_owner_auth() {
- // mock_all_auths_allowing_non_root_auth lets the call succeed, but
- // env.auths() records which addresses had to authorize — the audit-relevant
- // observation. select_winners requires the event owner.
let ctx = setup();
let id = create_hackathon(&ctx);
@@ -613,7 +569,6 @@ fn select_winners_demands_owner_auth() {
owner_required,
"select_winners must demand the event owner's auth"
);
- // Sanity: a random non-owner address was never asked to authorize.
assert!(
!auths.iter().any(|(addr, _)| *addr == ctx.events_admin),
"the events admin is not an authorizer of select_winners"
diff --git a/contracts/events/src/tests/mod.rs b/contracts/events/src/tests/mod.rs
index 3ae333b..0064ef6 100644
--- a/contracts/events/src/tests/mod.rs
+++ b/contracts/events/src/tests/mod.rs
@@ -1,9 +1,3 @@
-// boundless-events: test entry. Per-area test modules are mounted here.
-//
-// Tests are stubbed at the module level so that adding a new test file is a
-// one-line change. Full test coverage matrix is documented in the
-// boundless-platform-contract-prd.md Section 15.
-
#![cfg(test)]
mod admin;
diff --git a/contracts/events/src/tests/token_whitelist.rs b/contracts/events/src/tests/token_whitelist.rs
index 9f1ba1b..f8736ca 100644
--- a/contracts/events/src/tests/token_whitelist.rs
+++ b/contracts/events/src/tests/token_whitelist.rs
@@ -1,8 +1,3 @@
-// boundless-events: token whitelist tests (#27).
-//
-// Covers register_supported_token / deregister_supported_token /
-// is_supported_token + enforcement inside create_event.
-
#![cfg(test)]
use soroban_sdk::{
diff --git a/contracts/events/src/token_whitelist.rs b/contracts/events/src/token_whitelist.rs
index fd09708..9ea8a58 100644
--- a/contracts/events/src/token_whitelist.rs
+++ b/contracts/events/src/token_whitelist.rs
@@ -1,25 +1,3 @@
-// boundless-events: token whitelist management.
-//
-// Spec: boundless-platform-contract-prd.md Section 8.
-//
-// M2 (2026-06 audit): Trustline verification for the fee account is the
-// admin's off-chain responsibility (runbook in boundless-infra). On-chain
-// enforcement isn't reliable in Soroban today: Stellar Asset Contract's
-// `balance(addr)` returns 0 for "no trustline" and 0 for "trustline at
-// zero balance" indistinguishably, and a 0-amount `transfer` probe behaves
-// inconsistently across SAC variants. Until a non-standard token interface
-// extension lands, the chosen policy is:
-//
-// 1. Admin verifies trustline existence off-chain BEFORE calling
-// register (this module) or set_fee_account (admin module).
-// 2. On register/deregister we emit TokenRegistered/TokenDeregistered.
-// Off-chain monitors check trustline state and surface alarms if a
-// newly-registered token doesn't have a fee-account trustline.
-// 3. If trustline is missing, the first add_funds / deposit on the token
-// will revert inside SAC.transfer. The admin then rotates either the
-// fee account (via set_fee_account) or deregisters the token.
-//
-// See docs/audit-2026-06-stellar-skill.md M2.
#![allow(dead_code)]
use soroban_sdk::{Address, Env};
diff --git a/contracts/events/src/types.rs b/contracts/events/src/types.rs
index 99c2c6e..c936d5f 100644
--- a/contracts/events/src/types.rs
+++ b/contracts/events/src/types.rs
@@ -1,22 +1,7 @@
-// boundless-events: on-chain types.
-//
-// Spec: boundless-platform-contract-prd.md Sections 5.1 to 5.2.
-
use soroban_sdk::{contracttype, Address, BytesN, Map, String};
// ============================================================
// PILLAR
-//
-// Crowdfunding differs from the other three pillars:
-// - The owner is the project builder, not an organization.
-// - There is no upfront owner deposit; the escrow starts at 0 and grows
-// via add_funds from community backers.
-// - There is exactly one recipient (the builder, registered as Winner at
-// position 1 at create time).
-// - claim_milestone uses dynamic math: amount = remaining_escrow /
-// remaining_milestones, so each release pays a fair share of whatever
-// the campaign actually raised.
-// Spec: boundless-crowdfunding-prd.md.
// ============================================================
#[contracttype]
#[derive(Clone, Debug, Eq, PartialEq)]
@@ -29,12 +14,6 @@ pub enum Pillar {
// ============================================================
// STATUS
-//
-// Cancelling is the intermediate state once start_cancel has snapshotted
-// the contributor list. The event is frozen (no add_funds, submit,
-// claim_milestone, select_winners) until finalize_cancel flips it to
-// Cancelled. New variant added at the tail so prior on-chain variant
-// indices stay stable.
// ============================================================
#[contracttype]
#[derive(Clone, Debug, Eq, PartialEq)]
@@ -47,18 +26,6 @@ pub enum EventStatus {
// ============================================================
// CANCELLATION
-//
-// Paged cancel: see docs/audit-2026-06-stellar-skill.md H3+H4
-// follow-up. start_cancel snapshots the refund math once so that
-// concurrent contributor mutations (none possible while Cancelling, but
-// belt-and-suspenders) can't bias the per-batch payouts.
-//
-// Branch:
-// OwnerOnly - no partner contributions; owner gets all of remaining_at_start.
-// FullPartnerThenResidual - remaining >= non_owner_total; each partner gets full
-// amount, owner gets remaining - non_owner_total.
-// ProRataPartners - remaining < non_owner_total; partners get
-// floor(amt * remaining / non_owner_total); owner gets 0.
// ============================================================
#[contracttype]
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
@@ -126,9 +93,6 @@ pub struct CreateEventParams {
pub deadline: Option,
pub winner_distribution: Map,
pub fee_bps_override: Option,
- // Optional management authority. None => owner manages (legacy behavior).
- // When set, this address authorizes select_winners + cancel instead of the
- // owner, so funding source and management identity can differ.
pub manager: Option,
}
@@ -145,12 +109,6 @@ pub struct Submission {
// ============================================================
// CONTRIBUTION
-//
-// Partner / community top-up to an event's escrow. Owner deposits via
-// create_event are NOT recorded as Contribution entries; they live in
-// event.owner / event.total_budget. Contributions are recorded so that
-// cancel_event refunds partners ahead of the owner per the policy in
-// boundless-partner-contributions-prd.md.
// ============================================================
#[contracttype]
#[derive(Clone, Debug, Eq, PartialEq)]
@@ -175,10 +133,6 @@ pub struct Winner {
// ============================================================
// WINNER SELECTION SPEC
-//
-// Input to select_winners. The orchestrator computes reputation_bump off-chain
-// per the policy tables in boundless-credits-reputation-prd.md Section 9; the
-// contract just records.
// ============================================================
#[contracttype]
#[derive(Clone, Debug, Eq, PartialEq)]
@@ -194,7 +148,6 @@ pub struct WinnerSpec {
#[contracttype]
#[derive(Clone, Debug)]
pub enum DataKey {
- // Admin / config
Admin,
PendingAdmin,
FeeAccount,
@@ -203,86 +156,39 @@ pub enum DataKey {
DeploymentSeq,
ProfileContract,
- // Token whitelist
SupportedToken(Address),
- // Events
NextEventId,
Event(u64),
- // Per-event management authority override. When present, this address
- // (not event.owner) authorizes select_winners + cancel. Lets an org fund
- // from any wallet (owner) while keeping management bound to its canonical
- // wallet. Absent => management falls back to event.owner (legacy events).
EventManager(u64),
- // Per-event applicant list. Paged: ApplicantCount + ApplicantAt(idx).
- // Slot index (1-based) for O(1) membership / O(1) swap-remove. Slot of
- // 0 means absent. Caps at MAX_APPLICANTS_PER_EVENT to keep cancel-time
- // refund passes inside Soroban's per-tx footprint budget.
EventApplicantCount(u64),
EventApplicantAt(u64, u32),
EventApplicantSlot(u64, Address),
- // Per-event submission, keyed by (event_id, applicant). No iteration
- // surface — submissions are looked up by address only.
EventSubmission(u64, Address),
- // Per-event winner list. WinnerCount + WinnerAt(idx). No slot index
- // because select_winners enforces uniqueness on position, and the
- // claim_milestone path needs to enumerate anyway.
EventWinnerCount(u64),
EventWinnerAt(u64, u32),
- // Partner contributions to an event's escrow.
- // ContributorAmount(id, addr) -> i128 running total
- // ContributorCount(id) -> u32 number of distinct contributors
- // ContributorAt(id, idx) -> Address at slot idx
- // ContributorSlot(id, addr) -> u32 1-based slot index; 0 absent
- // Owner deposits do NOT appear here; they're tracked via event.owner /
- // event.total_budget.
ContributorAmount(u64, Address),
ContributorCount(u64),
ContributorAt(u64, u32),
ContributorSlot(u64, Address),
- // Grant milestone tracking: (event_id, recipient, milestone) -> bool
MilestoneClaimed(u64, Address, u32),
- // Crowdfunding: count of milestones already claimed against an event.
- // Used by the dynamic-payout math (amount = remaining_escrow /
- // (total_milestones - claimed_count)). Only written/read for
- // Pillar::Crowdfunding; absent entries default to 0.
CrowdfundingMilestonesClaimed(u64),
- // Paged cancellation cursor; present iff event.status == Cancelling.
CancellationState(u64),
- // H6: contract semver, timelocked upgrade slot, last migrated-to version.
- //
- // Version -> String. Set by __constructor; bumped by
- // apply_upgrade(). Exposed via version().
- // PendingUpgrade -> PendingUpgrade struct. Present between
- // propose_upgrade and apply_upgrade /
- // cancel_pending_upgrade.
- // MigratedToVersion -> String. Records the last Version that
- // successfully completed migrate(); guards
- // against double-running the same migration.
Version,
PendingUpgrade,
MigratedToVersion,
- // Idempotency
OpSeen(BytesN<32>),
- // Enumerable whitelist index (parallels the SupportedToken bool above).
- // Lets the full whitelist be read authoritatively from state via
- // supported_token_count + supported_token_at, instead of replaying the
- // ephemeral TokenRegistered / TokenDeregistered events (which the RPC only
- // retains for a short window). Appended here to keep existing keys stable.
- // SupportedTokenCount -> u32 number of whitelisted tokens
- // SupportedTokenAt(idx) -> Address at slot idx, idx in [0, count)
- // SupportedTokenSlot(addr)-> u32 1-based slot; 0 means absent
SupportedTokenCount,
SupportedTokenAt(u32),
SupportedTokenSlot(Address),
@@ -300,17 +206,6 @@ pub struct PendingAdmin {
// ============================================================
// PENDING UPGRADE (timelocked wasm rotation, H6)
-//
-// propose_upgrade writes a row carrying the proposed wasm hash and the
-// new_version label the contract will report after apply_upgrade. The
-// proposal is timelocked: apply_upgrade can only fire after
-// available_at_ledger and before expires_at_ledger.
-//
-// new_version is stored upfront so the apply step is purely mechanical;
-// off-chain monitoring can see exactly which version the proposal upgrades
-// to without inspecting the new wasm.
-//
-// Spec: docs/audit-2026-06-stellar-skill.md H6.
// ============================================================
#[contracttype]
#[derive(Clone, Debug, Eq, PartialEq)]
diff --git a/contracts/profile/src/admin.rs b/contracts/profile/src/admin.rs
index 2e43a97..59480cd 100644
--- a/contracts/profile/src/admin.rs
+++ b/contracts/profile/src/admin.rs
@@ -1,9 +1,3 @@
-// boundless-profile: admin operations.
-//
-// Two-step rotations for both admin and events_contract.
-//
-// Spec: boundless-credits-reputation-prd.md Sections 5.5, 6.
-
use soroban_sdk::{panic_with_error, Address, BytesN, Env, String};
use crate::errors::Error;
@@ -11,12 +5,8 @@ use crate::events as evt;
use crate::storage;
use crate::types::{PendingAdmin, PendingEventsContract, PendingUpgrade};
-const PENDING_TTL_LEDGERS: u32 = 120_960; // 7 days at ~5 sec per ledger.
+const PENDING_TTL_LEDGERS: u32 = 120_960;
-// H6: timelocked upgrade windows. Match the events contract for consistency.
-// Testnet builds (`--features testnet`) zero the upgrade timelock for fast
-// iteration; the default build (mainnet + everything else) keeps the full
-// ~1-day timelock. Fail-safe: omitting the flag yields the secure value, never 0.
#[cfg(not(feature = "testnet"))]
const UPGRADE_TIMELOCK_LEDGERS: u32 = 17_280;
#[cfg(feature = "testnet")]
@@ -25,15 +15,8 @@ const PENDING_UPGRADE_TTL_LEDGERS: u32 = 518_400;
pub const INITIAL_VERSION: &str = "1.1.0";
-// Events-contract rotation timelock: minimum delay between propose and
-// accept so off-chain monitoring has a window to react to a malicious
-// proposal. ~1 day at 5 sec per ledger.
-//
-// Spec: docs/audit-2026-06-stellar-skill.md, H5.
const EVENTS_CONTRACT_TIMELOCK_LEDGERS: u32 = 17_280;
-// Maximum window between propose and accept. After this the proposal must
-// be re-issued. Matches PENDING_TTL_LEDGERS for symmetry with admin rotation.
const PENDING_EVENTS_CONTRACT_TTL_LEDGERS: u32 = 120_960;
pub fn initialize(env: &Env, admin: Address) {
@@ -88,15 +71,6 @@ pub fn accept_admin(env: &Env) -> Result<(), Error> {
// ============================================================
// EVENTS CONTRACT BINDING
-//
-// First-set is single-step (deploy bootstrap; there's no live contract to
-// rotate from). Subsequent rotations require propose + accept with a
-// timelock window so off-chain monitors have time to react to a malicious
-// or mistaken proposal before it lands. Closes audit finding H5 (the prior
-// single-step rotation was the single soft point in the auth chain for
-// every credit/reputation/earnings mutation).
-//
-// Spec: docs/audit-2026-06-stellar-skill.md, H5.
// ============================================================
pub fn set_events_contract(env: &Env, new_addr: Address) -> Result<(), Error> {
require_admin(env)?;
@@ -138,14 +112,9 @@ pub fn accept_events_contract(env: &Env) -> Result<(), Error> {
storage::get_pending_events_contract(env).ok_or(Error::PendingEventsContractMismatch)?;
let now = env.ledger().sequence();
- // Late finalize: proposal expired. We do NOT clear here because the
- // Err return reverts every storage write in this tx anyway; the expired
- // entry stays put and admin must call cancel_pending_events_contract to
- // prune it before re-proposing.
if now > pending.expires_at_ledger {
return Err(Error::PendingEventsContractExpired);
}
- // Early finalize: still inside the timelock window.
let earliest = pending
.proposed_at_ledger
.saturating_add(EVENTS_CONTRACT_TIMELOCK_LEDGERS);
@@ -203,7 +172,6 @@ pub fn propose_upgrade(
) -> Result<(), Error> {
require_admin(env)?;
if new_version.is_empty() {
- // Reuse existing AdminCannotBeZero semantic for "empty input".
return Err(Error::AdminCannotBeZero);
}
let now = env.ledger().sequence();
@@ -271,12 +239,6 @@ pub fn cancel_pending_upgrade(env: &Env) -> Result<(), Error> {
// ============================================================
// MIGRATE (post-upgrade one-shot; H6)
-//
-// Mirror of the events contract's migrate(). See contracts/events/src/admin.rs
-// for the full pattern + dispatch-block convention. The profile contract has
-// a simpler storage layout, so most upgrades will not need a migration body
-// here; the empty pass-through still stamps MigratedToVersion so off-chain
-// runbooks see a Migrated event.
// ============================================================
pub fn migrate(env: &Env) -> Result<(), Error> {
require_admin(env)?;
@@ -292,24 +254,8 @@ pub fn migrate(env: &Env) -> Result<(), Error> {
// ============================================================
// PER-(from -> to) MIGRATION DISPATCH
- //
- // if from_version == String::from_str(env, "0.2.0")
- // && current == String::from_str(env, "0.3.0")
- // {
- // migrate_0_2_0_to_0_3_0(env)?;
- // }
- //
- // Soroban String only supports equality + length, so dispatch is via
- // `String::from_str` + `==`. Keep bodies inline unless the migration
- // grows past ~30 lines, then promote into a private fn below.
// ============================================================
- // No-op for the 1.0.0 -> 1.1.0 credit-removal upgrade: no Profile rows have
- // been bootstrapped yet, so there is nothing to rewrite for the dropped
- // `credits` field. __constructor populates storage in the current shape;
- // admin still calls migrate() once after the upgrade so the audit trail
- // records that the post-upgrade cleanup ran.
-
storage::set_migrated_to_version(env, ¤t);
storage::touch_instance(env);
evt::Migrated {
@@ -367,8 +313,6 @@ pub fn require_events_contract(env: &Env) -> Result<(), Error> {
}
pub fn require_not_paused(env: &Env) -> Result<(), Error> {
- // Every state-mutating operation runs this first; single touchpoint for
- // bumping instance TTL on the hot path. Admin paths bump explicitly.
storage::touch_instance(env);
if storage::is_paused(env) {
return Err(Error::Paused);
diff --git a/contracts/profile/src/bootstrap.rs b/contracts/profile/src/bootstrap.rs
index d75846d..2f669eb 100644
--- a/contracts/profile/src/bootstrap.rs
+++ b/contracts/profile/src/bootstrap.rs
@@ -1,8 +1,3 @@
-// boundless-profile: profile lifecycle (bootstrap).
-//
-// Credits were removed (2026-06) and are now an off-chain ledger. Bootstrap
-// only creates the per-user profile so reputation and earnings can attach.
-
use soroban_sdk::{Address, BytesN, Env};
use crate::admin;
@@ -12,8 +7,6 @@ use crate::idempotency;
use crate::storage;
use crate::types::Profile;
-/// Lazy bootstrap. Called by the events contract on a user's first touch.
-/// Idempotent: a second call when the profile already exists is a no-op.
pub fn bootstrap(env: &Env, user: Address, op_id: BytesN<32>) -> Result<(), Error> {
admin::require_events_contract(env)?;
admin::require_not_paused(env)?;
@@ -29,10 +22,6 @@ pub fn bootstrap(env: &Env, user: Address, op_id: BytesN<32>) -> Result<(), Erro
Ok(())
}
-/// Self-service bootstrap. A user creates their OWN profile by authorizing the
-/// call with their wallet (no admin key, no events-contract dependency). Used
-/// at onboarding so every user has a profile before they participate.
-/// Idempotent: a second call when the profile already exists is a no-op.
pub fn bootstrap_self(env: &Env, user: Address, op_id: BytesN<32>) -> Result<(), Error> {
user.require_auth();
admin::require_not_paused(env)?;
diff --git a/contracts/profile/src/earnings.rs b/contracts/profile/src/earnings.rs
index df0a692..8c5ca33 100644
--- a/contracts/profile/src/earnings.rs
+++ b/contracts/profile/src/earnings.rs
@@ -1,7 +1,3 @@
-// boundless-profile: per-token earnings registration.
-//
-// Spec: boundless-credits-reputation-prd.md Section 5.4.
-
use soroban_sdk::{Address, BytesN, Env};
use crate::admin;
diff --git a/contracts/profile/src/errors.rs b/contracts/profile/src/errors.rs
index e1f725c..12884d2 100644
--- a/contracts/profile/src/errors.rs
+++ b/contracts/profile/src/errors.rs
@@ -1,7 +1,3 @@
-// boundless-profile: error codes.
-//
-// Spec: boundless-credits-reputation-prd.md Section 8.
-
use soroban_sdk::contracterror;
#[contracterror]
@@ -22,14 +18,12 @@ pub enum Error {
PendingEventsContractTimelock = 17,
ProfileNotFound = 10,
- // 11 (InsufficientCredits) retired with on-chain credits; left as a gap.
InvalidAmount = 12,
ReasonRequired = 13,
OpAlreadySeen = 20,
Paused = 30,
- // H6: timelocked upgrade + migration
UpgradeNotProposed = 40,
UpgradeTimelockNotElapsed = 41,
UpgradeProposalExpired = 42,
diff --git a/contracts/profile/src/events.rs b/contracts/profile/src/events.rs
index 92d60fe..0678616 100644
--- a/contracts/profile/src/events.rs
+++ b/contracts/profile/src/events.rs
@@ -1,9 +1,3 @@
-// boundless-profile: contract event emissions.
-//
-// Spec: boundless-credits-reputation-prd.md Section 7.
-//
-// dead_code allowed: event structs are emitted via .publish() calls. Some
-// events are emitted only by ops still wiring up.
#![allow(dead_code)]
use soroban_sdk::{contractevent, Address, BytesN, String, Symbol};
diff --git a/contracts/profile/src/idempotency.rs b/contracts/profile/src/idempotency.rs
index 296e0e5..1d4fd26 100644
--- a/contracts/profile/src/idempotency.rs
+++ b/contracts/profile/src/idempotency.rs
@@ -1,5 +1,3 @@
-// boundless-profile: idempotency helpers.
-
use soroban_sdk::{BytesN, Env};
use crate::errors::Error;
diff --git a/contracts/profile/src/lib.rs b/contracts/profile/src/lib.rs
index 0aa2f6d..11b40df 100644
--- a/contracts/profile/src/lib.rs
+++ b/contracts/profile/src/lib.rs
@@ -1,12 +1,4 @@
// SPDX-License-Identifier: MIT
-//
-// boundless-profile
-//
-// Per-user reputation + per-token earnings. Mutated almost exclusively by the
-// events contract; admin can slash reputation directly with audited reasons.
-// Credits were removed (2026-06) and now live in an off-chain ledger.
-//
-// Spec: boundless-credits-reputation-prd.md
#![no_std]
use soroban_sdk::{contract, contractimpl, contractmeta, Address, BytesN, Env, String, Symbol};
@@ -108,9 +100,6 @@ impl ProfileContract {
bootstrap::bootstrap(&env, user, op_id)
}
- /// Self-service profile creation: the user authorizes their own bootstrap
- /// (no admin key, no events-contract dependency). Called at onboarding so
- /// every user has a profile before they participate.
pub fn bootstrap_self(env: Env, user: Address, op_id: BytesN<32>) -> Result<(), Error> {
bootstrap::bootstrap_self(&env, user, op_id)
}
diff --git a/contracts/profile/src/reputation.rs b/contracts/profile/src/reputation.rs
index 2075e05..d87ae6f 100644
--- a/contracts/profile/src/reputation.rs
+++ b/contracts/profile/src/reputation.rs
@@ -1,7 +1,3 @@
-// boundless-profile: reputation operations.
-//
-// Spec: boundless-credits-reputation-prd.md Section 5.3.
-
use soroban_sdk::{Address, BytesN, Env, String, Symbol};
use crate::admin;
diff --git a/contracts/profile/src/storage.rs b/contracts/profile/src/storage.rs
index 7cb8aef..17ceedb 100644
--- a/contracts/profile/src/storage.rs
+++ b/contracts/profile/src/storage.rs
@@ -1,18 +1,3 @@
-// boundless-profile: storage helpers.
-//
-// Storage layout (after the 2026-06 audit):
-//
-// instance() — admin + config (events binding, bootstrap default,
-// paused, deployment seq). Single bag, auto-extended
-// when we call touch_instance(env).
-// persistent() — Profile(user) + EarningsByToken(user, token). Each
-// read/write bumps TTL via touch_profile_persistent so
-// active users stay live indefinitely.
-// temporary() — OpSeen idempotency markers.
-//
-// Pre-audit this module placed everything in persistent. Same fix as the
-// events contract.
-
#![allow(dead_code)]
use soroban_sdk::{Address, BytesN, Env};
diff --git a/contracts/profile/src/tests/admin.rs b/contracts/profile/src/tests/admin.rs
index bcb2b0a..41c848e 100644
--- a/contracts/profile/src/tests/admin.rs
+++ b/contracts/profile/src/tests/admin.rs
@@ -1,5 +1,3 @@
-// boundless-profile: admin tests.
-
#![cfg(test)]
use soroban_sdk::{
@@ -10,7 +8,6 @@ use soroban_sdk::{
use super::common::setup;
use crate::errors::Error;
-// Mirror the constants from admin.rs so the timelock tests stay readable.
const EVENTS_CONTRACT_TIMELOCK_LEDGERS: u32 = 17_280;
const PENDING_EVENTS_CONTRACT_TTL_LEDGERS: u32 = 120_960;
@@ -83,7 +80,6 @@ fn propose_then_accept_after_timelock_swaps_events_contract() {
assert_eq!(pending.target, events_b);
assert_eq!(pending.proposed_at_ledger, start);
- // Advance past the timelock window.
ctx.env.ledger().with_mut(|li| {
li.sequence_number = start + EVENTS_CONTRACT_TIMELOCK_LEDGERS + 1;
});
@@ -108,7 +104,6 @@ fn accept_before_timelock_reverts() {
.expect("expected timelock to block")
.unwrap();
assert_eq!(err, Error::PendingEventsContractTimelock);
- // Events contract unchanged.
assert_eq!(ctx.client.get_events_contract(), Some(events_a));
}
@@ -122,7 +117,6 @@ fn accept_after_expiry_reverts_and_admin_must_cancel_to_prune() {
let start = ctx.env.ledger().sequence();
ctx.client.propose_events_contract(&events_b);
- // Advance past expiry.
ctx.env.ledger().with_mut(|li| {
li.sequence_number = start + PENDING_EVENTS_CONTRACT_TTL_LEDGERS + 1;
});
@@ -134,7 +128,6 @@ fn accept_after_expiry_reverts_and_admin_must_cancel_to_prune() {
.unwrap();
assert_eq!(err, Error::PendingEventsContractExpired);
- // The Err path reverts; the stale proposal stays put. Admin prunes it.
assert!(ctx.client.get_pending_events_contract().is_some());
assert_eq!(ctx.client.get_events_contract(), Some(events_a));
diff --git a/contracts/profile/src/tests/bootstrap.rs b/contracts/profile/src/tests/bootstrap.rs
index aec3264..5f12a37 100644
--- a/contracts/profile/src/tests/bootstrap.rs
+++ b/contracts/profile/src/tests/bootstrap.rs
@@ -1,10 +1,3 @@
-// boundless-profile: self-service bootstrap tests.
-//
-// `bootstrap_self` is the user-authorized profile-creation path used at
-// platform onboarding. Unlike `bootstrap` (events-contract-gated) it requires
-// NO admin key and NO events contract — the profile owner authorizes their own
-// creation. These tests pin that security property plus idempotency.
-
#![cfg(test)]
use soroban_sdk::{
@@ -30,10 +23,6 @@ fn bootstrap_self_creates_profile_for_caller() {
#[test]
fn bootstrap_self_demands_the_callers_own_auth_not_admin() {
- // The security property the design rests on: bootstrap_self requires the
- // USER's own authorization — no admin or other privileged key can create a
- // profile on someone's behalf. mock_all_auths lets the call through, but
- // env.auths() records whose auth the contract actually demanded.
let ctx = setup();
let user = Address::generate(&ctx.env);
let op_id = BytesN::random(&ctx.env);
@@ -59,7 +48,6 @@ fn bootstrap_self_is_idempotent_for_existing_profile() {
ctx.client.bootstrap_self(&user, &BytesN::random(&ctx.env));
let before = ctx.client.get_profile(&user).expect("profile created");
- // Second bootstrap (fresh op_id) when the profile already exists is a no-op.
ctx.client.bootstrap_self(&user, &BytesN::random(&ctx.env));
assert_eq!(ctx.client.get_profile(&user), Some(before));
diff --git a/contracts/profile/src/tests/common.rs b/contracts/profile/src/tests/common.rs
index 17480f6..8ef2c6f 100644
--- a/contracts/profile/src/tests/common.rs
+++ b/contracts/profile/src/tests/common.rs
@@ -1,5 +1,3 @@
-// boundless-profile: shared test setup.
-
#![cfg(test)]
use soroban_sdk::{testutils::Address as _, Address, Env};
diff --git a/contracts/profile/src/tests/earnings.rs b/contracts/profile/src/tests/earnings.rs
index 3393500..b413729 100644
--- a/contracts/profile/src/tests/earnings.rs
+++ b/contracts/profile/src/tests/earnings.rs
@@ -1,8 +1,3 @@
-// boundless-profile: earnings registration tests.
-//
-// Covers register_earnings() — guards, error variants, idempotency,
-// auth rejection, saturating arithmetic.
-
#![cfg(test)]
use soroban_sdk::{
@@ -25,10 +20,6 @@ fn token(env: &crate::Env) -> Address {
Address::generate(env)
}
-// ---------------------------------------------------------------------------
-// Happy path
-// ---------------------------------------------------------------------------
-
#[test]
fn register_earnings_succeeds() {
let ctx = setup();
@@ -61,10 +52,6 @@ fn register_earnings_accumulates() {
assert_eq!(ctx.client.get_earnings(&u, &t), 100);
}
-// ---------------------------------------------------------------------------
-// Edge cases: multiple tokens / users
-// ---------------------------------------------------------------------------
-
#[test]
fn register_earnings_multiple_tokens() {
let ctx = setup();
@@ -101,10 +88,6 @@ fn register_earnings_multiple_users() {
assert_eq!(ctx.client.get_earnings(&u2, &t), 200);
}
-// ---------------------------------------------------------------------------
-// Error: InvalidAmount (zero / negative)
-// ---------------------------------------------------------------------------
-
#[test]
fn register_earnings_rejects_zero() {
let ctx = setup();
@@ -139,10 +122,6 @@ fn register_earnings_rejects_negative() {
assert_eq!(err, Error::InvalidAmount);
}
-// ---------------------------------------------------------------------------
-// Error: EventsContractNotConfigured
-// ---------------------------------------------------------------------------
-
#[test]
fn register_earnings_reverts_no_events_contract() {
let ctx = setup();
@@ -158,10 +137,6 @@ fn register_earnings_reverts_no_events_contract() {
assert_eq!(err, Error::EventsContractNotConfigured);
}
-// ---------------------------------------------------------------------------
-// Error: Paused
-// ---------------------------------------------------------------------------
-
#[test]
fn register_earnings_reverts_when_paused() {
let ctx = setup();
@@ -180,10 +155,6 @@ fn register_earnings_reverts_when_paused() {
assert_eq!(err, Error::Paused);
}
-// ---------------------------------------------------------------------------
-// Idempotency: duplicate op_id
-// ---------------------------------------------------------------------------
-
#[test]
fn register_earnings_rejects_duplicate_op_id() {
let ctx = setup();
@@ -193,11 +164,9 @@ fn register_earnings_rejects_duplicate_op_id() {
let t = token(&ctx.env);
let op_id = BytesN::random(&ctx.env);
- // First call succeeds.
ctx.client.register_earnings(&u, &t, &100_i128, &op_id);
assert_eq!(ctx.client.get_earnings(&u, &t), 100);
- // Same op_id — idempotency guard.
let err = ctx
.client
.try_register_earnings(&u, &t, &200_i128, &op_id)
@@ -206,14 +175,9 @@ fn register_earnings_rejects_duplicate_op_id() {
.unwrap();
assert_eq!(err, Error::OpAlreadySeen);
- // Balance unchanged.
assert_eq!(ctx.client.get_earnings(&u, &t), 100);
}
-// ---------------------------------------------------------------------------
-// Edge: saturating arithmetic
-// ---------------------------------------------------------------------------
-
#[test]
fn register_earnings_saturating_add() {
let ctx = setup();
@@ -222,21 +186,15 @@ fn register_earnings_saturating_add() {
let u = user(&ctx.env);
let t = token(&ctx.env);
- // Push to i128::MAX - 1.
ctx.client
.register_earnings(&u, &t, &(i128::MAX - 1), &BytesN::random(&ctx.env));
assert_eq!(ctx.client.get_earnings(&u, &t), i128::MAX - 1);
- // Add 100 — should saturate at i128::MAX, not overflow.
ctx.client
.register_earnings(&u, &t, &100_i128, &BytesN::random(&ctx.env));
assert_eq!(ctx.client.get_earnings(&u, &t), i128::MAX);
}
-// ---------------------------------------------------------------------------
-// Auth rejection: caller is not the events contract.
-// ---------------------------------------------------------------------------
-
#[test]
fn register_earnings_auth_rejection() {
let env = soroban_sdk::Env::default();
@@ -244,7 +202,6 @@ fn register_earnings_auth_rejection() {
let contract_id = env.register(crate::ProfileContract, (admin.clone(),));
let client = crate::ProfileContractClient::new(&env, &contract_id);
- // Set events contract directly in storage to bypass admin auth.
env.as_contract(&contract_id, || {
crate::storage::set_events_contract(&env, &events_addr(&env));
});
@@ -252,8 +209,6 @@ fn register_earnings_auth_rejection() {
let u = user(&env);
let t = token(&env);
- // No mock_auths — the events address hasn't authorized this call,
- // so require_auth() inside register_earnings should fail.
let result = client.try_register_earnings(&u, &t, &100_i128, &BytesN::random(&env));
assert!(
result.is_err(),
diff --git a/contracts/profile/src/tests/mod.rs b/contracts/profile/src/tests/mod.rs
index 2125798..5dd4e45 100644
--- a/contracts/profile/src/tests/mod.rs
+++ b/contracts/profile/src/tests/mod.rs
@@ -1,5 +1,3 @@
-// boundless-profile: test entry.
-
#![cfg(test)]
mod admin;
diff --git a/contracts/profile/src/tests/reputation.rs b/contracts/profile/src/tests/reputation.rs
index 8274625..9ca44cf 100644
--- a/contracts/profile/src/tests/reputation.rs
+++ b/contracts/profile/src/tests/reputation.rs
@@ -1,11 +1,3 @@
-// boundless-profile: reputation tests.
-//
-// Covers reputation::bump, reputation::slash, reputation::admin_slash.
-// Every function: happy path + each reachable Error variant + edge cases
-// (saturating add/sub, zero delta) + auth-rejection + idempotency replay.
-//
-// Spec: boundless-credits-reputation-prd.md Section 5.3.
-
#![cfg(test)]
use soroban_sdk::{
@@ -20,17 +12,14 @@ use crate::errors::Error;
// Helpers
// ============================================================
-/// A fresh, unique idempotency key.
fn op_id(ctx: &TestCtx) -> BytesN<32> {
BytesN::random(&ctx.env)
}
-/// bump/slash reason (Symbol).
fn reason(ctx: &TestCtx) -> Symbol {
Symbol::new(&ctx.env, "win")
}
-/// Current reputation for a user that is expected to have a profile.
fn reputation_of(ctx: &TestCtx, user: &Address) -> u64 {
ctx.client
.get_profile(user)
@@ -38,12 +27,6 @@ fn reputation_of(ctx: &TestCtx, user: &Address) -> u64 {
.reputation
}
-/// setup() + wire an events contract + bootstrap one user so the
-/// events-gated reputation ops have a profile to mutate.
-///
-/// Returns the context plus the bootstrapped user. The events-contract
-/// address is mocked-authed by `setup`, so subsequent bump/slash calls
-/// satisfy `require_events_contract`.
fn setup_with_user<'a>() -> (TestCtx<'a>, Address) {
let ctx = setup();
let events = Address::generate(&ctx.env);
@@ -83,8 +66,6 @@ fn bump_accumulates_across_calls() {
#[test]
fn bump_accepts_u32_max_delta_without_overflow() {
- // delta is u32, reputation is u64. A single max-delta bump must widen
- // cleanly into u64 and never overflow/panic.
let (ctx, user) = setup_with_user();
ctx.client
@@ -101,7 +82,6 @@ fn bump_zero_delta_is_noop_but_marks_seen() {
ctx.client.bump_reputation(&user, &0, &reason(&ctx), &op);
assert_eq!(reputation_of(&ctx, &user), 0);
- // Replaying the same op_id is rejected even though the op was a no-op.
let err = ctx
.client
.try_bump_reputation(&user, &0, &reason(&ctx), &op)
@@ -113,8 +93,6 @@ fn bump_zero_delta_is_noop_but_marks_seen() {
#[test]
fn bump_reverts_when_events_contract_not_configured() {
- // No set_events_contract: the events-contract auth guard is the first
- // check and rejects before anything else.
let ctx = setup();
let user = Address::generate(&ctx.env);
@@ -147,7 +125,6 @@ fn bump_reverts_when_profile_not_found() {
let events = Address::generate(&ctx.env);
ctx.client.set_events_contract(&events);
- // A user that was never bootstrapped has no profile.
let ghost = Address::generate(&ctx.env);
let err = ctx
.client
@@ -173,15 +150,11 @@ fn bump_is_idempotent_on_replay() {
.expect("replay rejected")
.unwrap();
assert_eq!(err, Error::OpAlreadySeen);
- // Reputation unchanged: the replay did not double-apply.
assert_eq!(reputation_of(&ctx, &user), 5);
}
#[test]
fn bump_rejects_caller_without_events_contract_auth() {
- // Genuine auth rejection: the events contract is configured, but no
- // authorization is provided for the bump call, so events.require_auth()
- // fails and the host aborts the invocation.
let (ctx, user) = setup_with_user();
ctx.env.mock_auths(&[]);
@@ -209,8 +182,6 @@ fn slash_happy_path_decrements_reputation() {
#[test]
fn slash_saturates_at_zero() {
- // Slashing more than the current reputation floors at zero rather than
- // underflowing (saturating_sub).
let (ctx, user) = setup_with_user();
ctx.client
.bump_reputation(&user, &5, &reason(&ctx), &op_id(&ctx));
@@ -312,7 +283,6 @@ fn slash_rejects_caller_without_events_contract_auth() {
// admin_slash
// ============================================================
-/// admin_slash reason is a String (audited free text), not a Symbol.
fn admin_reason(ctx: &TestCtx) -> String {
String::from_str(&ctx.env, "fraud")
}
@@ -357,7 +327,6 @@ fn admin_slash_reverts_on_empty_reason() {
#[test]
fn admin_slash_reverts_when_paused() {
- // require_admin passes (mocked), then the pause guard fires.
let (ctx, user) = setup_with_user();
ctx.client.pause();
@@ -373,7 +342,6 @@ fn admin_slash_reverts_when_paused() {
#[test]
fn admin_slash_reverts_when_profile_not_found() {
let ctx = setup();
- // No events contract needed: admin_slash is admin-gated, not events-gated.
let ghost = Address::generate(&ctx.env);
let err = ctx
@@ -408,8 +376,6 @@ fn admin_slash_is_idempotent_on_replay() {
#[test]
fn admin_slash_rejects_non_admin_caller() {
- // Genuine auth rejection: no authorization provided, so admin.require_auth()
- // fails and the host aborts the invocation.
let (ctx, user) = setup_with_user();
ctx.env.mock_auths(&[]);
diff --git a/contracts/profile/src/types.rs b/contracts/profile/src/types.rs
index 6da7e2c..a0f4dc5 100644
--- a/contracts/profile/src/types.rs
+++ b/contracts/profile/src/types.rs
@@ -1,7 +1,3 @@
-// boundless-profile: types.
-//
-// Spec: boundless-credits-reputation-prd.md Section 4.
-
use soroban_sdk::{contracttype, Address, BytesN, String};
#[contracttype]
@@ -20,16 +16,6 @@ impl Profile {
}
}
-// M4 (2026-06 audit): dropped wins_count, submissions_count,
-// applications_count, milestones_completed. They were never incremented
-// anywhere in the contract.
-//
-// 2026-06: dropped `credits`. Credits are now an off-chain ledger
-// (boundless-nestjs); the profile contract holds only reputation + earnings.
-// Removing the field changes the persisted Profile layout: existing on-chain
-// profiles must be re-bootstrapped or migrated before this version is applied.
-// See the mainnet-deploy-runbook before upgrading.
-
#[contracttype]
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct PendingAdmin {
@@ -39,10 +25,6 @@ pub struct PendingAdmin {
// ============================================================
// PENDING EVENTS CONTRACT (two-step rotation w/ timelock)
-//
-// proposed_at_ledger gates the early-finalize window; accept can fire only
-// after proposed_at_ledger + EVENTS_CONTRACT_TIMELOCK_LEDGERS. expires_at_ledger
-// gates the late-finalize window; after expiry the proposal must be re-issued.
// ============================================================
#[contracttype]
#[derive(Clone, Debug, Eq, PartialEq)]
@@ -52,7 +34,6 @@ pub struct PendingEventsContract {
pub expires_at_ledger: u32,
}
-// H6: timelocked wasm rotation. Mirrors the events contract's PendingUpgrade.
#[contracttype]
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct PendingUpgrade {
@@ -76,7 +57,6 @@ pub enum DataKey {
Profile(Address),
EarningsByToken(Address, Address),
- // H6: contract semver, timelocked upgrade slot, last migrated-to version.
Version,
PendingUpgrade,
MigratedToVersion,