Module:
quicklendx-contracts/src/lib.rs— query endpoints Tests:quicklendx-contracts/src/test_queries.rs,quicklendx-contracts/src/test_investment_queries.rs,quicklendx-contracts/src/test_limit.rs
All query endpoints in the QuickLendX protocol are designed to handle missing
or non-existent records gracefully — returning None, an empty Vec, or a
typed Err rather than panicking or producing inconsistent results.
Additionally, all paginated endpoints enforce strict hard caps on query limits to prevent resource abuse and ensure predictable performance characteristics.
All paginated endpoints enforce a hard cap of MAX_QUERY_LIMIT = 100 records per query.
This limit cannot be bypassed by:
- Passing
limit > MAX_QUERY_LIMIT(automatically capped) - Using overflow attacks with large offset values (validated and rejected)
- Combining parameters to exceed resource bounds (comprehensive validation)
- Limit Capping:
limitparameter is automatically capped usinglimit.min(MAX_QUERY_LIMIT) - Overflow Protection: Offset values that could cause
offset + MAX_QUERY_LIMITto overflow are rejected - Empty Results: Invalid parameters return empty results rather than errors
- Zero Limit Handling:
limit=0returns empty results (not an error)
/// Maximum number of records returned by paginated query endpoints.
pub(crate) const MAX_QUERY_LIMIT: u32 = 100;
/// Validates and caps query limit to prevent resource abuse
#[inline]
fn cap_query_limit(limit: u32) -> u32 {
limit.min(MAX_QUERY_LIMIT)
}
/// Validates query parameters for security and resource protection
fn validate_query_params(offset: u32, limit: u32) -> Result<(), QuickLendXError> {
// Check for potential overflow in offset + limit calculation
if offset > u32::MAX - MAX_QUERY_LIMIT {
return Err(QuickLendXError::InvalidAmount);
}
Ok(())
}| Endpoint | Hard Cap Applied | Validation |
|---|---|---|
get_business_invoices_paged |
✅ MAX_QUERY_LIMIT | ✅ Overflow protection |
get_investor_investments_paged |
✅ MAX_QUERY_LIMIT | ✅ Overflow protection |
get_available_invoices_paged |
✅ MAX_QUERY_LIMIT | ✅ Overflow protection |
get_bid_history_paged |
✅ MAX_QUERY_LIMIT | ✅ Overflow protection |
get_investor_bids_paged |
✅ MAX_QUERY_LIMIT | ✅ Overflow protection |
get_whitelisted_currencies_paged |
✅ MAX_QUERY_LIMIT | ✅ Overflow protection |
get_payment_records |
✅ MAX_QUERY_LIMIT | ✅ Overflow protection |
get_business_invoices_paged applies status filtering first, then deterministically orders
results by (created_at ASC, invoice_id ASC) before pagination.
Security and consistency implications:
- Repeated calls with identical contract state return identical page boundaries.
- Status-filtered pagination remains deterministic and does not leak cross-status entries.
- Tie-breaking by
invoice_idavoids validator-dependent ordering when timestamps match.
| Endpoint | Missing record behaviour |
|---|---|
get_invoice(id) |
Returns Err(InvoiceNotFound) |
get_bid(id) |
Returns None |
get_investment(id) |
Returns Err(StorageKeyNotFound) |
get_invoice_investment(id) |
Returns Err(StorageKeyNotFound) |
get_bids_for_invoice(id) |
Returns empty Vec |
get_best_bid(id) |
Returns None |
get_ranked_bids(id) |
Returns empty Vec |
get_bids_by_status(id, status) |
Returns empty Vec |
get_bids_by_investor(id, investor) |
Returns empty Vec |
get_all_bids_by_investor(investor) |
Returns empty Vec |
get_business_invoices(business) |
Returns empty Vec |
get_investments_by_investor(investor) |
Returns empty Vec |
get_escrow_details(id) |
Returns Err(StorageKeyNotFound) |
get_bid_history_paged(id, ...) |
Returns empty Vec (capped) |
get_investor_bids_paged(investor, ...) |
Returns empty Vec (capped) |
cleanup_expired_bids(id) |
Returns 0 |
- Hard cap enforcement: No query endpoint can return more than
MAX_QUERY_LIMITrecords - Overflow protection: All pagination arithmetic uses overflow-safe operations
- No panics: No query endpoint panics on missing input — all storage lookups use
Optionreturns (getreturningNone) which are handled before unwrapping - Authorization-free: Query endpoints are read-only and require no authorization — they cannot mutate state
- Information isolation: Missing records never leak information about other records
- Resource bounds: Query execution time and memory usage are bounded by
MAX_QUERY_LIMIT
- Limit=0 scenarios: All endpoints return empty results
- Limit > MAX_QUERY_LIMIT: All endpoints cap to MAX_QUERY_LIMIT
- Large offset scenarios: Offsets beyond data return empty results
- Overflow protection: Dangerous offset values are safely handled
- Pagination consistency: Multi-page results maintain order and completeness
- Edge cases: Exactly MAX_QUERY_LIMIT items, extreme values
- Cross-endpoint validation: Consistent behavior across all paginated endpoints
- Parameter validation: Edge cases for offset/limit combinations
- Data consistency: No duplicates or missing items across pagination
cd quicklendx-contracts
cargo test test_queries
cargo test test_limitA self-contained, dependency-free pagination utility module that powers every query endpoint's offset/limit handling. It is decoupled from Soroban storage so its semantics can be unit-tested in isolation and is therefore runnable today even while the legacy contract library is mid-migration.
| Symbol | Purpose |
|---|---|
MAX_QUERY_LIMIT: u32 |
Hard cap = 100 records per response. |
cap_query_limit |
Clamp any limit to MAX_QUERY_LIMIT. |
validate_pagination_params |
Returns (safe_offset, effective_limit, has_more) for a total. |
calculate_safe_bounds |
Returns [start, end) slice bounds, always within collection. |
paginate_slice<T: Clone> |
Generic offset/limit slice over any &[T]; stable ordering. |
- Hard cap — No call returns more than
MAX_QUERY_LIMITitems. - Empty on overflow —
offset >= total_countalways returns empty. - Stable ordering — Input order is preserved; no sorting, no dedup.
- No unbounded loops — Every loop is bounded by
min(limit, MAX_QUERY_LIMIT, remaining). - No panics — All arithmetic uses
saturating_*; nounwrap()or indexing outside pre-computed safe bounds.
- Worst-case allocation per call ≤
MAX_QUERY_LIMIT * size_of::<T>()bytes. (offset, limit) = (u32::MAX, u32::MAX)for anytotal_countis a no-op (empty result, no panic).- Read-only; no authorization required.
- Independent of validator order — output is a function of
(items, offset, limit)only.
Located at src/test_queries.rs and src/test_investment_queries.rs, both
wired into src/lib.rs behind #[cfg(test)].
Coverage highlights:
limit=0returns empty (deterministic across collection sizes).offset >= total_countreturns empty (includingu32::MAX).limit > MAX_QUERY_LIMITis clamped toMAX_QUERY_LIMIT.- Stable ordering across repeated calls and consecutive pages.
- No duplicates across pages (
size ∈ {1, 3, 7, 25, MAX_QUERY_LIMIT}). - Cross-type coverage:
u64,[u8; 32],Stringnewtype, and a mockInvestmentstruct. - Proptest properties: clamping, order preservation, page coverage, and
no-panic-on-
u32::MAXextremes.
cd quicklendx-contracts
cargo test --verbose