Skip to content
Open
7 changes: 6 additions & 1 deletion leaderboard/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use soroban_sdk::{
};

const MAX_TOP_PLAYERS: u32 = 50;
const MAX_PAGE_SIZE: u32 = 20;
const TTL_BUMP: u32 = 3_153_600;
const TTL_HIGH: u32 = 6_307_200;

Expand Down Expand Up @@ -443,7 +444,11 @@ impl LeaderboardContract {
return vec![&env];
}

let end = (offset + page_size).min(count);
// Read only a bounded range from the write-time ordered index. The
// saturating addition also keeps an untrusted offset from overflowing
// before it is clamped to the current player count.
let page_size = page_size.min(MAX_PAGE_SIZE);
let end = offset.saturating_add(page_size).min(count);
let mut result = Vec::new(&env);
for i in offset..end {
if let Some(entry) = env.storage().persistent().get(&DataKey::TopPlayerAt(i)) {
Expand Down
30 changes: 30 additions & 0 deletions leaderboard/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,36 @@ fn test_top_players_capped_at_50() {
assert_eq!(client.get_top_player_count(), 50);
}

#[test]
fn test_pagination_reads_the_persistent_ordered_index() {
let (env, client, _admin, market, _referral) = setup();
let points = [10_u64, 50, 30, 40, 20];

for points in points {
let user = Address::generate(&env);
client.add_pts(&market, &user, &points, &true);
}

// The page is returned directly from slots 1 and 2 of the write-time
// ordered index, rather than rebuilding the complete ranking on read.
let page = client.get_top_players(&1_u32, &2_u32);
assert_eq!(page.len(), 2);
assert_eq!(page.get(0).unwrap().points, 40);
assert_eq!(page.get(1).unwrap().points, 30);
}

#[test]
fn test_pagination_caps_page_size_without_overflowing_offset() {
let (env, client, _admin, market, _referral) = setup();
let user = Address::generate(&env);
client.add_pts(&market, &user, &100_u64, &true);

// A caller cannot turn one view request into an unbounded storage read,
// and a maximal offset remains a safe empty page.
assert_eq!(client.get_top_players(&0_u32, &u32::MAX).len(), 1);
assert_eq!(client.get_top_players(&u32::MAX, &u32::MAX).len(), 0);
}

#[test]
fn test_pagination_offset_beyond_count() {
let (env, client, _admin, market, _referral) = setup();
Expand Down