From 62cbd7f0c78fd5aeea0a4ffedb02fbd53dc1baa0 Mon Sep 17 00:00:00 2001 From: nekwasar Date: Mon, 31 Aug 2026 11:50:26 +0200 Subject: [PATCH 1/2] fix(reputation-registry): saturating increment to prevent u32 overflow Bare `current + 1` panics on overflow in Soroban, aborting the whole transaction. Use saturating_add so streaks/completions/defaults cap at u32::MAX instead of panicking. Closes #284 --- packages/reputation-registry/src/storage.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/reputation-registry/src/storage.rs b/packages/reputation-registry/src/storage.rs index 56cf182..c5bff85 100644 --- a/packages/reputation-registry/src/storage.rs +++ b/packages/reputation-registry/src/storage.rs @@ -21,7 +21,7 @@ pub fn get_streak(env: &Env, member: &Address, circle_id: &Address) -> u32 { /// Increment the streak count pub fn increment_streak(env: &Env, member: &Address, circle_id: &Address) { let current = get_streak(env, member, circle_id); - env.storage().persistent().set(&DataKey::Streak(member.clone(), circle_id.clone()), &(current + 1)); + env.storage().persistent().set(&DataKey::Streak(member.clone(), circle_id.clone()), ¤t.saturating_add(1)); } /// Get number of completed circles @@ -33,7 +33,7 @@ pub fn get_completions(env: &Env, member: &Address) -> u32 { /// Increment completions pub fn increment_completions(env: &Env, member: &Address) { let current = get_completions(env, member); - env.storage().persistent().set(&DataKey::Completions(member.clone()), &(current + 1)); + env.storage().persistent().set(&DataKey::Completions(member.clone()), ¤t.saturating_add(1)); } /// Get number of defaults @@ -45,7 +45,7 @@ pub fn get_defaults(env: &Env, member: &Address) -> u32 { /// Increment defaults pub fn increment_defaults(env: &Env, member: &Address) { let current = get_defaults(env, member); - env.storage().persistent().set(&DataKey::Defaults(member.clone()), &(current + 1)); + env.storage().persistent().set(&DataKey::Defaults(member.clone()), ¤t.saturating_add(1)); } /// Add an activity record From ae0ae1a61a327c07b56565aab4116145949ac5c8 Mon Sep 17 00:00:00 2001 From: nekwasar Date: Mon, 31 Aug 2026 11:50:27 +0200 Subject: [PATCH 2/2] fix(vrf): uniform Fisher-Yates shuffle to avoid position collisions The naive `vrf_val % n` modulo approach could push duplicate positions, biasing payout order and leaving some members unshuffled. Replace with a Fisher-Yates swap per VRF evaluation, producing a uniform permutation. Closes #285 --- packages/common/src/vrf.rs | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/packages/common/src/vrf.rs b/packages/common/src/vrf.rs index ecb3817..34eb1d9 100644 --- a/packages/common/src/vrf.rs +++ b/packages/common/src/vrf.rs @@ -215,11 +215,23 @@ pub fn verify_vrf( /// # Errors /// * `VrfError::NotInitialized` if `init_vrf` has not been called pub fn shuffle_positions(env: &Env, n: u32) -> Result, VrfError> { + // Build the identity permutation [0, 1, ..., n-1], then apply Fisher-Yates + // using VRF evaluations so every position is swapped exactly once. This + // produces a uniform random permutation with no duplicates or collisions + // (the naive modulus approach could push the same position twice). let mut shuffled = Vec::new(env); for i in 0..n { + shuffled.push_back(i); + } + let mut i = n; + while i > 1 { + i = i.saturating_sub(1); let vrf_val = evaluate_vrf(env, i)?; - let pos = vrf_val % n; - shuffled.push_back(pos); + let j = vrf_val % (i + 1); + let a = shuffled.get(i).unwrap_or(0u32); + let b = shuffled.get(j).unwrap_or(0u32); + shuffled.set(i, b); + shuffled.set(j, a); } Ok(shuffled) }