Skip to content

Commit ac9be2d

Browse files
committed
fix: Winner backward compat + O(1) claim_prize lookup
Two findings from Almanax review: 1. Winner deserialization on upgrade (High): reputation_bump was added as a non-optional u32, which would brick reads of old on-chain Winner entries after upgrade. Changed to Option<u32> — old entries deserialize as None (treated as 0), new entries store Some(x). 2. Linear winner scan in claim_prize (Medium): claim_prize scanned all winners to find the anchor row, approaching budget limits at higher winner counts. Added DataKey::WinnerIndex stored at selection time for O(1) lookup in claim_prize. - Winner.reputation_bump: u32 → Option<u32> - DataKey::WinnerIndex variant + storage get/set helpers - select_winners stores index + Some() reputation_bump - claim_prize uses index lookup, falls back to PrizeAlreadyClaimed if paid_at.is_some() (canonical guard) - claim_milestone uses .unwrap_or(0) on read, Some() on write
1 parent cfce78f commit ac9be2d

4 files changed

Lines changed: 48 additions & 26 deletions

File tree

contracts/events/src/event_ops.rs

Lines changed: 27 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,7 @@ pub fn create_event(env: &Env, params: CreateEventParams, op_id: BytesN<32>) ->
152152
amount: 0,
153153
milestone: None,
154154
paid_at: None,
155-
reputation_bump: 0,
155+
reputation_bump: None,
156156
},
157157
);
158158
}
@@ -632,13 +632,14 @@ pub fn select_winners(
632632
return Err(Error::InsufficientEscrow);
633633
}
634634

635-
for spec in winners.iter() {
635+
for (i, spec) in winners.iter().enumerate() {
636636
let percent = event
637637
.winner_distribution
638638
.get(spec.position)
639639
.ok_or(Error::InvalidDistribution)? as i128;
640640
let amount = escrow_at_select.saturating_mul(percent) / 100_i128;
641641

642+
let anchor_idx = existing_count + (i as u32);
642643
storage::append_winner(
643644
env,
644645
event_id,
@@ -648,9 +649,16 @@ pub fn select_winners(
648649
amount,
649650
milestone: None,
650651
paid_at: None,
651-
reputation_bump: spec.reputation_bump,
652+
reputation_bump: Some(spec.reputation_bump),
652653
},
653654
);
655+
storage::set_winner_index(
656+
env,
657+
event_id,
658+
&spec.recipient,
659+
spec.position,
660+
anchor_idx,
661+
);
654662
}
655663
}
656664
ReleaseKind::Multi(_) => {
@@ -664,7 +672,7 @@ pub fn select_winners(
664672
amount: 0,
665673
milestone: None,
666674
paid_at: None,
667-
reputation_bump: spec.reputation_bump,
675+
reputation_bump: Some(spec.reputation_bump),
668676
},
669677
);
670678
}
@@ -723,25 +731,21 @@ pub fn claim_prize(
723731
return Err(Error::PrizeAlreadyClaimed);
724732
}
725733

726-
// Locate the anchor row (milestone == None) matching this recipient and
727-
// position. Captures the index for in-place update, the pre-computed
728-
// amount, and the manager-approved reputation_bump.
729-
let count = storage::winner_count(env, event_id);
730-
let mut winner_info: Option<(u32, i128, u32)> = None;
731-
for idx in 0..count {
732-
let w = match storage::winner_at(env, event_id, idx) {
733-
Some(w) => w,
734-
None => continue,
735-
};
736-
if w.recipient != recipient || w.position != position {
737-
continue;
738-
}
739-
if w.milestone.is_none() && w.paid_at.is_none() {
740-
winner_info = Some((idx, w.amount, w.reputation_bump));
741-
break;
742-
}
734+
// Look up the anchor index stored at selection time (O(1) instead of
735+
// a linear scan). Returns NoSubmissions if no winner matches or the
736+
// prize has already been claimed (canonical guard: paid_at).
737+
let anchor_idx = storage::get_winner_index(env, event_id, &recipient, position)
738+
.ok_or(Error::NoSubmissions)?;
739+
let w = storage::winner_at(env, event_id, anchor_idx)
740+
.ok_or(Error::NoSubmissions)?;
741+
if w.recipient != recipient || w.position != position || w.milestone.is_some() {
742+
return Err(Error::NoSubmissions);
743+
}
744+
if w.paid_at.is_some() {
745+
return Err(Error::PrizeAlreadyClaimed);
743746
}
744-
let (anchor_idx, amount, reputation_bump) = winner_info.ok_or(Error::NoSubmissions)?;
747+
let amount = w.amount;
748+
let reputation_bump = w.reputation_bump.unwrap_or(0);
745749

746750
if amount <= 0 {
747751
return Err(Error::InvalidDistribution);
@@ -782,7 +786,7 @@ pub fn claim_prize(
782786
amount,
783787
milestone: None,
784788
paid_at: Some(env.ledger().timestamp()),
785-
reputation_bump,
789+
reputation_bump: Some(reputation_bump),
786790
},
787791
);
788792

contracts/events/src/grant.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ pub fn claim_milestone(
7070
match w.milestone {
7171
None => {
7272
winner_position = Some(w.position);
73-
reputation_bump = w.reputation_bump;
73+
reputation_bump = w.reputation_bump.unwrap_or(0);
7474
}
7575
Some(_) => {
7676
already_claimed_for_recipient = already_claimed_for_recipient.saturating_add(1);
@@ -150,7 +150,7 @@ pub fn claim_milestone(
150150
amount,
151151
milestone: Some(milestone),
152152
paid_at: Some(env.ledger().timestamp()),
153-
reputation_bump,
153+
reputation_bump: Some(reputation_bump),
154154
},
155155
);
156156

contracts/events/src/storage.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -451,6 +451,21 @@ pub fn append_winner(env: &Env, id: u64, w: &Winner) {
451451
touch_event_persistent(env, &count_key);
452452
}
453453

454+
pub fn set_winner_index(env: &Env, id: u64, recipient: &Address, position: u32, idx: u32) {
455+
let key = DataKey::WinnerIndex(id, recipient.clone(), position);
456+
env.storage().persistent().set(&key, &idx);
457+
touch_event_persistent(env, &key);
458+
}
459+
460+
pub fn get_winner_index(env: &Env, id: u64, recipient: &Address, position: u32) -> Option<u32> {
461+
let key = DataKey::WinnerIndex(id, recipient.clone(), position);
462+
let idx: Option<u32> = env.storage().persistent().get(&key);
463+
if idx.is_some() {
464+
touch_event_persistent(env, &key);
465+
}
466+
idx
467+
}
468+
454469
pub fn winners_snapshot(env: &Env, id: u64, max: u32) -> Vec<Winner> {
455470
let count = winner_count(env, id);
456471
let upper = if count < max { count } else { max };

contracts/events/src/types.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ pub struct Winner {
129129
pub amount: i128,
130130
pub milestone: Option<u32>,
131131
pub paid_at: Option<u64>,
132-
pub reputation_bump: u32,
132+
pub reputation_bump: Option<u32>,
133133
}
134134

135135
// ============================================================
@@ -199,6 +199,9 @@ pub enum DataKey {
199199

200200
// Added last so earlier variant discriminants remain stable on upgrade.
201201
PrizeClaimed(u64, Address, u32),
202+
203+
// Winner anchor index for O(1) claim_prize lookup (added 2026-07).
204+
WinnerIndex(u64, Address, u32),
202205
}
203206

204207
// ============================================================

0 commit comments

Comments
 (0)