Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 13 additions & 2 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10796,7 +10796,7 @@ impl RevoraRevenueShare {
);

let n = holders.len();
let mut payouts: Vec<DistributionEntry> = Vec::new(env);
let mut payout_rows: std::vec::Vec<(u32, u32, Address, i128)> = std::vec::Vec::new();
let mut total: i128 = 0;

for i in 0..n {
Expand Down Expand Up @@ -10829,8 +10829,19 @@ impl RevoraRevenueShare {
Self::compute_share(env.clone(), period_revenue, bounded_bps, mode);

total = total.saturating_add(normalized_payout);
payout_rows.push((bounded_bps, share_bps, holder.clone(), normalized_payout));
}

payout_rows.sort_by(|a, b| match b.0.cmp(&a.0) {
core::cmp::Ordering::Equal => a.2.cmp(&b.2),
other => other,
});

let mut payouts: Vec<DistributionEntry> = Vec::new(env);
for (bounded_bps, share_bps, holder, normalized_payout) in payout_rows {
let _ = bounded_bps;
payouts.push_back(DistributionEntry {
holder: holder.clone(),
holder,
share_bps,
normalized_payout,
});
Expand Down
15 changes: 15 additions & 0 deletions src/proptest_helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,21 @@

use proptest::prelude::*;

/// Return a reproducible shuffled copy of a slice using a deterministic local PRNG.
/// Useful for regression tests that need to exercise many re-orderings of the same fixture.
pub fn shuffle_vec_with_seed<T: Clone>(values: &[T], seed: u64) -> std::vec::Vec<T> {
let mut shuffled = values.to_vec();
let mut state = seed.wrapping_mul(0x9E3779B97F4A7C15).wrapping_add(0x85EBCA6B);

for index in (1..shuffled.len()).rev() {
state = state.rotate_left(17).wrapping_mul(0x9E3779B97F4A7C15);
let j = (state as usize) % (index + 1);
shuffled.swap(index, j);
}

shuffled
}

// ── Primitive strategies ─────────────────────────────────────────────────────

/// Any valid basis-points value (0–10 000 inclusive).
Expand Down
89 changes: 89 additions & 0 deletions src/test_close_period.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
#![cfg(test)]
use super::*;
use crate::proptest_helpers::shuffle_vec_with_seed;
use proptest::prelude::*;
use soroban_sdk::{
testutils::{Address as _, Events as _, Ledger},
token, Address, Env,
Expand Down Expand Up @@ -84,6 +86,93 @@ fn setup_offering() -> (Env, RevoraRevenueShareClient<'static>, Address, Address
(env, client, issuer, token, payment_token)
}

proptest! {
#![proptest_config(ProptestConfig {
cases: 16,
..ProptestConfig::default()
})]

#[test]
fn close_period_preflight_is_deterministic_across_shuffled_holders(
seed in any::<u64>(),
) {
let env = Env::default();
env.mock_all_auths();
let contract_id = env.register_contract(None, RevoraRevenueShare);
let client = RevoraRevenueShareClient::new(&env, &contract_id);

let issuer = Address::generate(&env);
let token = Address::generate(&env);
let payment_token = env.register_stellar_asset_contract_v2(issuer.clone()).address();
let ns = symbol_short!("ns");

client.register_offering(
&issuer,
&Vec::from_array(&env, []),
&1u32,
&ns,
&token,
&10_000u32,
&payment_token,
&0i128,
&symbol_short!(""),
&0u32,
);

let holder_a = Address::generate(&env);
let holder_b = Address::generate(&env);
let holder_c = Address::generate(&env);
let holder_d = Address::generate(&env);
let holder_e = Address::generate(&env);

client.set_holder_share(&issuer, &ns, &token, &holder_a, &3_000u32);
client.set_holder_share(&issuer, &ns, &token, &holder_b, &2_500u32);
client.set_holder_share(&issuer, &ns, &token, &holder_c, &1_500u32);
client.set_holder_share(&issuer, &ns, &token, &holder_d, &1_000u32);
client.set_holder_share(&issuer, &ns, &token, &holder_e, &2_000u32);

mint(&env, &payment_token, &issuer, 10_000_000);
client.deposit_revenue(&issuer, &ns, &token, &payment_token, &10_000_000i128, &1u64);

let base_holders = std::vec![
holder_a.clone(),
holder_b.clone(),
holder_c.clone(),
holder_d.clone(),
holder_e.clone(),
holder_a.clone(),
];

let mut baseline: Option<PreflightCloseResult> = None;
for iteration in 0..512u64 {
let shuffled = shuffle_vec_with_seed(&base_holders, seed.wrapping_add(iteration));
let mut soroban_holders = Vec::new(&env);
for holder in shuffled.iter() {
soroban_holders.push_back(holder.clone());
}

let result = RevoraRevenueShare::preflight_close_period(
env.clone(),
OfferingId {
issuer: issuer.clone(),
namespace: ns.clone(),
token: token.clone(),
},
1u64,
soroban_holders,
)
.unwrap();

if let Some(ref expected) = baseline {
prop_assert_eq!(result.payouts, expected.payouts);
prop_assert_eq!(result.total_distributed, expected.total_distributed);
} else {
baseline = Some(result);
}
}
}
}

#[test]
fn close_period_happy_path() {
let (_env, client, issuer, token, _payment) = setup_offering();
Expand Down
Loading