From cde04e1d67a860ec939064bc752b61cd140d2cd0 Mon Sep 17 00:00:00 2001 From: Liam Phung Date: Fri, 16 Oct 2026 14:55:13 +0700 Subject: [PATCH] fix(checkpoint): cap bump_checkpoints_ttl_range span at MAX_PAGE_SIZE (#1203) --- contracts/checkpoint/src/lib.rs | 11 ++++++++- contracts/checkpoint/src/test.rs | 42 ++++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 1 deletion(-) diff --git a/contracts/checkpoint/src/lib.rs b/contracts/checkpoint/src/lib.rs index e18d00a3..7de46fa5 100644 --- a/contracts/checkpoint/src/lib.rs +++ b/contracts/checkpoint/src/lib.rs @@ -723,7 +723,8 @@ impl CalloraCheckpoint { /// # Errors /// * [`CheckpointError::NotInitialized`] -- contract not initialized. /// * [`CheckpointError::Unauthorized`] -- caller is not the current admin. - /// * [`CheckpointError::InvalidPageSize`] -- `start_id` is greater than `end_id`. + /// * [`CheckpointError::InvalidPageSize`] -- `start_id` is greater than `end_id`, + /// or the inclusive span `end_id - start_id + 1` exceeds [`MAX_PAGE_SIZE`]. pub fn bump_checkpoints_ttl_range( env: Env, caller: Address, @@ -736,6 +737,14 @@ impl CalloraCheckpoint { return Err(CheckpointError::InvalidPageSize); } + let span = end_id + .checked_sub(start_id) + .and_then(|diff| diff.checked_add(1)) + .ok_or(CheckpointError::InvalidPageSize)?; + if span > u64::from(MAX_PAGE_SIZE) { + return Err(CheckpointError::InvalidPageSize); + } + for id in start_id..=end_id { let key = StorageKey::Checkpoint(id); if env.storage().persistent().has(&key) { diff --git a/contracts/checkpoint/src/test.rs b/contracts/checkpoint/src/test.rs index 5c35c589..89c3efa1 100644 --- a/contracts/checkpoint/src/test.rs +++ b/contracts/checkpoint/src/test.rs @@ -527,6 +527,48 @@ fn test_bump_checkpoints_ttl_range_skips_missing_ids() { assert!(result.is_ok()); } +#[test] +fn test_bump_checkpoints_ttl_range_rejects_span_above_max_page_size() { + let (_env, admin, client) = setup(); + + // Span of MAX_PAGE_SIZE + 1 (1..=101 -> 101 IDs) must fail with InvalidPageSize. + let over_cap_end = u64::from(MAX_PAGE_SIZE) + 1; + let res = client.try_bump_checkpoints_ttl_range(&admin, &1u64, &over_cap_end); + assert_eq!(res, Err(Ok(CheckpointError::InvalidPageSize))); + + // Full u64 span (0..=u64::MAX) must also fail cleanly with InvalidPageSize. + let overflow_res = client.try_bump_checkpoints_ttl_range(&admin, &0u64, &u64::MAX); + assert_eq!(overflow_res, Err(Ok(CheckpointError::InvalidPageSize))); +} + +#[test] +fn test_bump_checkpoints_ttl_range_allows_exact_max_page_size_span() { + let (env, admin, client) = setup(); + let subject = Address::generate(&env); + let token = Address::generate(&env); + let meta = Symbol::new(&env, "exact_cap"); + + let id = client.create_checkpoint(&admin, &subject, &token, &500i128, &meta); + let key = StorageKey::Checkpoint(id); + + let seq = env.ledger().sequence(); + env.as_contract(&client.address, || { + env.storage() + .instance() + .extend_ttl(BUMP_AMOUNT, BUMP_AMOUNT); + }); + env.ledger() + .set_sequence_number(seq + BUMP_AMOUNT - LIFETIME_THRESHOLD + 1); + + // Span of exactly MAX_PAGE_SIZE (1..=100) is accepted and bumps existing IDs. + let exact_cap_end = u64::from(MAX_PAGE_SIZE); + let res = client.try_bump_checkpoints_ttl_range(&admin, &1u64, &exact_cap_end); + assert!(res.is_ok()); + + let ttl_after = env.as_contract(&client.address, || env.storage().persistent().get_ttl(&key)); + assert_eq!(ttl_after, BUMP_AMOUNT); +} + // =========================================================================== // Read-path TTL bump tests (buffer #26) // ===========================================================================