Skip to content
14 changes: 13 additions & 1 deletion contracts/events/src/event_ops.rs
Original file line number Diff line number Diff line change
Expand Up @@ -539,6 +539,11 @@ pub fn submit(
if !matches!(event.status, EventStatus::Active) {
return Err(Error::EventNotActive);
}
if storage::winner_count(env, event_id) > 0
|| storage::get_prize_claim_expiry(env, event_id).is_some()
{
return Err(Error::WinnersAlreadySelected);
}
if matches!(event.pillar, Pillar::Crowdfunding) {
return Err(Error::InvalidPillar);
}
Expand Down Expand Up @@ -567,15 +572,17 @@ pub fn submit(
// cannot lock real participants out of a full event.
storage::append_submission(env, event_id, &applicant)?;

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

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

Expand Down Expand Up @@ -605,6 +612,11 @@ pub fn withdraw_submission(
if !matches!(event.status, EventStatus::Active) {
return Err(Error::EventNotActive);
}
if storage::winner_count(env, event_id) > 0
|| storage::get_prize_claim_expiry(env, event_id).is_some()
{
return Err(Error::WinnersAlreadySelected);
}

applicant.require_auth();
idempotency::require_unseen(env, &applicant, &op_id)?;
Expand Down
33 changes: 31 additions & 2 deletions contracts/events/src/tests/hackathon_pillar.rs
Original file line number Diff line number Diff line change
Expand Up @@ -199,15 +199,44 @@ fn resubmit_keeps_original_timestamp_and_updates_uri() {
let uri_a = String::from_str(&ctx.env, "ipfs://Qm.../v1.json");
let op_a = BytesN::random(&ctx.env);
ctx.events.submit(&id, &ctx.applicant, &uri_a, &op_a);
let first_time = ctx.events.get_submission(&id, &ctx.applicant).submitted_at;
let first = ctx.events.get_submission(&id, &ctx.applicant);
assert_eq!(first.submitted_at, ctx.env.ledger().timestamp());
assert_eq!(first.updated_at, ctx.env.ledger().timestamp());

let uri_b = String::from_str(&ctx.env, "ipfs://Qm.../v2.json");
let op_b = BytesN::random(&ctx.env);
ctx.events.submit(&id, &ctx.applicant, &uri_b, &op_b);

let second = ctx.events.get_submission(&id, &ctx.applicant);
assert_eq!(second.content_uri, uri_b);
assert_eq!(second.submitted_at, first_time);
assert_eq!(second.submitted_at, first.submitted_at);
assert_eq!(second.updated_at, ctx.env.ledger().timestamp());
}

#[test]
fn submit_and_withdraw_after_selection_rejected() {
let ctx = setup();
let id = create_hackathon(&ctx);

let uri = String::from_str(&ctx.env, "ipfs://Qm.../v1.json");
let op = BytesN::random(&ctx.env);
ctx.events.submit(&id, &ctx.applicant, &uri, &op);

let winners = soroban_sdk::vec![
&ctx.env,
WinnerSpec {
recipient: ctx.applicant.clone(),
position: 1,
reputation_bump: 0,
},
];
ctx.events.select_winners(&id, &winners, &BytesN::random(&ctx.env));

let res = ctx.events.try_submit(&id, &ctx.applicant, &uri, &BytesN::random(&ctx.env));
assert_eq!(res, Err(Ok(Error::WinnersAlreadySelected)));

let res_w = ctx.events.try_withdraw_submission(&id, &ctx.applicant, &BytesN::random(&ctx.env));
assert_eq!(res_w, Err(Ok(Error::WinnersAlreadySelected)));
}

#[test]
Expand Down
1 change: 1 addition & 0 deletions contracts/events/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ pub struct Submission {
pub applicant: Address,
pub content_uri: String,
pub submitted_at: u64,
pub updated_at: u64,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

High Severity severity

Contract Lifecycle & State: Persistent state decode breakage in Submission

Submission is a #[contracttype] persisted in env.storage().persistent(); adding the new updated_at field changes its serialized layout, so any pre-upgrade stored Submission values may fail to deserialize after upgrade, potentially trapping on reads (e.g., storage::get_submission) and breaking submit/withdraw flows for existing applicants.

Add an explicit storage migration/versioning strategy for existing Submission entries (e.g., store updated_at separately, or make it Option<u64> and treat None as submitted_at, or migrate all stored submissions during an upgrade/init path) before deploying this change to an existing network.


Fix with MCP
Almanax found a vulnerability. Can you take a look and fix it?
Finding ID: 92a825b0-4b35-4916-82d3-ba8e39854fbc
Actions
  • Reply /almanax ask <question> to ask a follow-up question.
  • Reply /almanax dismiss [<reason>] and it won't appear again in future scans.
  • Reply /almanax resolve [<reason>] to mark the finding as resolved.
  • Reply /almanax severity <level> [<reason>] to override the severity.

}

// ============================================================
Expand Down