Skip to content

test: add currency-precision helper boundary tests (#2092) - #1

Open
YazarAyobami wants to merge 175 commits into
mainfrom
test/issue-2092-currency-precision-tests
Open

test: add currency-precision helper boundary tests (#2092)#1
YazarAyobami wants to merge 175 commits into
mainfrom
test/issue-2092-currency-precision-tests

Conversation

@YazarAyobami

Copy link
Copy Markdown
Owner

Closes QuickLendX#2092

Summary

Adds a dedicated test module that locks in three behaviour buckets for payments::require_matching_currency_precision — the defence-in-depth guard that runs alongside every currency-amount entrypoint in the QuickLendX contract.

The existing tests in payments.rs cover the happy-path SAC case plus a handful of negative paths. Issue QuickLendX#2092 asks for exhaustive boundary coverage organised into three buckets, which is what this PR delivers.

What changed

File Change
quicklendx-contracts/src/test_currency_precision.rs New file (342 lines). 9 enabled tests + 1 #[ignore]-gated test.
quicklendx-contracts/src/lib.rs Adds #[cfg(test)] mod test_currency_precision; with the standard issue-tagged comment block used by QuickLendX#2083, QuickLendX#2089, etc.

Behaviour buckets covered

1. Matching (must succeed)

Test decimals() reported Expected
test_precision_matches_when_decimals_is_zero 0 Ok(())
test_precision_matches_when_decimals_is_eighteen 18 (upper bound, inclusive) Ok(())
test_precision_matches_sac_default_decimals 7 (SAC default) Ok(())

2. Over-precision (must be rejected with InvalidCurrency)

Test decimals() reported Expected
test_precision_rejects_decimals_just_above_max 19 (just-over) Err(InvalidCurrency)
test_precision_rejects_decimals_twenty 20 Err(InvalidCurrency)
test_precision_rejects_decimals_max_u32 u32::MAX (saturation) Err(InvalidCurrency)

3. Malformed (must be rejected with InvalidCurrency)

Test Address shape Expected
test_precision_rejects_unregistered_address fresh Address::generate (no contract) Err(InvalidCurrency)
test_precision_rejects_wrong_return_type contract whose decimals() returns Symbol instead of u32 Err(InvalidCurrency) (#[ignore]’d — see note below)

Cross-cutting

  • test_precision_overprecision_rejection_is_amount_independent — over-precision rejection must be independent of amount. Tested with both 1 and i128::MAX.
  • test_precision_amount_and_currency_errors_are_distinctInvalidAmount (negative / zero amount) and InvalidCurrency (token contract issues) must never be confused with each other.

Mock token contracts

Six minimal #[contract] + #[contractimpl] mocks live alongside the tests. Each exposes only the surface needed to drive one specific code path (typically just decimals()); none expose transfer / balance / allowance, which is fine because the helper under test never calls those. The pattern matches PropTestContract in test_volume_tier_props.rs.

About the #[ignore]’d test

test_precision_rejects_wrong_return_type is gated behind #[ignore] because Soroban 25.x aborts cross-contract type-mismatch calls at the host level rather than returning a transport-level Err that try_invoke_contract::<u32, _> can swallow. This mirrors the existing #[ignore] on test_create_escrow_unregistered_token_address_does_not_succeed in payments.rs. A // TODO(#2092) comment on the test explains how to re-enable it once the SDK stops aborting (current pin: 25.1.1 in Cargo.toml).

CI

The new module uses plain #[cfg(test)] with no feature gate, satisfying the issue’s acceptance criterion that “tests run on every CI matrix entry”. No --features fuzz-tests or --features legacy-tests flags required.

Local validation

  • cargo is not available in this codespace, so cargo build --tests / cargo test could not be run locally. CI is the source of truth for compilation + test execution.
  • Two code-review passes were completed before push; both approved the structure, boundary coverage, and #[ignore] rationale.

Test count

  • 10 total test functions (9 enabled, 1 #[ignore]).
  • All enabled tests assert on a single, explicit Err(QuickLendXError::InvalidCurrency) / Err(QuickLendXError::InvalidAmount) / Ok(()) — no fuzzy proptest heuristics.
  • All inputs are deterministic (no random fuzzing), matching the project convention for non-fuzz boundary tests.

Risk + rollback

  • Pure test addition; no production code is changed.
  • The #[ignore]’d test is not consumed by cargo test runs.
  • Revert by deleting quicklendx-contracts/src/test_currency_precision.rs and the 6-line mod test_currency_precision; block in lib.rs.

devfoma and others added 30 commits June 26, 2026 20:10
Documents the two independent invoice locking mechanisms (admin freeze
with no auto-release, escrow hold with no time-based expiry) and the
default path's grace-period-based eligibility window.

Closes QuickLendX#2102
Reject settlements that reference a stale investment snapshot. This is a defence-in-depth fix to ensure that the investment state the business is settling against matches the exact on-chain state, preventing TOCTOU issues.

Closes QuickLendX#100
Registers the previously-orphaned test_ratings_snapshot module (it was
never declared in lib.rs, so its lifecycle test was silently dead code
and never ran in CI) and adds two cases:

- test_ratings_snapshot_is_deterministic_for_unchanged_state: calling
  ratings_snapshot repeatedly against unchanged invoice state returns
  byte-for-byte identical snapshots, locking in same-input -> same-output.
- test_ratings_snapshot_fails_for_nonexistent_invoice: the explicit sad
  path, asserting InvoiceNotFound instead of a panic or empty snapshot.

Closes QuickLendX#1874
Defence-in-depth: verify that every invoice and bid amount is
compatible with the token contract's declared decimal precision
before any state is written.

The new helper in payments.rs uses try_invoke_contract to call
the token's decimals() entry-point.  It rejects:
- zero or negative amounts (InvalidAmount)
- currency addresses that do not expose a decimals() entry-point
  (InvalidCurrency)
- tokens reporting more than 18 decimals (InvalidCurrency)

The guard is wired into store_invoice, upload_invoice,
store_invoices_batch, and place_bid.  A full test suite in
payments_tests covers the happy path, non-token currencies,
and zero/negative amounts.

Threat model: Without this check, a caller who supplies a malformed
or non-standard token address as currency could pass a mis-scaled
amount that silently round-trips through the contract's integer
arithmetic, bypassing the expected precision and enabling subtle
financial manipulation (drained escrow, incorrect fees).

Closes QuickLendX#2091
…points

Implements issue QuickLendX#1902 — adds InvestorFreezeReason enum (AdminAction,
KYCExpired, ComplianceViolation, SuspiciousActivity, LegalHold) with
InvestorFreezeInfo struct, admin-gated freeze/unfreeze entry points,
and frozen checks in place_bid/withdraw_bid/accept_bid/validate_investor_investment.

Also adds missing set_frozen/is_frozen convenience methods to
InvoiceStorage that were previously called but not defined.
Mirrors the existing business_default_history pattern: a persistent,
per-investor counter that increments each time an invoice funded by
that investor transitions to Defaulted, exposed via a new
get_investor_default_history view function.

- storage.rs: new StorageKeys::investor_default_history(investor) key
  (inv_def_h), recorded in test_snapshots/storage_keys.txt with a
  stability test to prevent silent renames orphaning on-chain data.
- defaults.rs: handle_default now bumps the investor counter alongside
  the business one, guarded by the same atomic transition guard so it
  can't double-count on retries.
- lib.rs: new get_investor_default_history(investor) -> u32 view
  entrypoint, documented alongside get_business_default_history.
- docs/DEFAULT_ACCOUNTING.md: documents both default-history counters
  and how to read them.

Tests: extends test_default_after_grace_period to assert the counter
increments on default, adds
test_investor_default_history_isolated_per_investor to verify counters
are tracked independently per investor, and adds a storage-key
stability test for the new inv_def_h key.

Closes QuickLendX#1863
Add `bindings` Makefile target that runs `stellar contract bindings typescript` to generate type-safe TypeScript client code from the compiled WASM contract.

The `build` target now depends on `bindings`, making regeneration automatic on every build. The `wasm` target rebuilds WASM only (no bindings) for fast TDD iteration via `make test`.

Closes QuickLendX#2182
Closes QuickLendX#1915

Every admin/operator-facing entrypoint with concrete soroban contract
invoke CLI examples, organized by functional area:

1. Initialization & Protocol Config
2. Admin Management (one-step and two-step handover)
3. Pause, Maintenance & Incident Mode
4. Emergency Withdrawal (timelocked)
5. Currency Whitelist (single, batch, replace, clear)
6. Protocol Limits (min amount, grace, TTL, bid config)
7. Invoice Operations (verify, freeze, status, expiration)
8. Bid Operations (cleanup, ranking, history)
9. Escrow Operations (refund, extend, early release)
10. Dispute Resolution (review, resolve, timeline)
11. Fees & Revenue (set fee, revenue split, distribute)
12. KYC / Verification (business/investor verify/reject/revoke)
13. Default Handling (mark, scan, overdue)
14. Backup & Restore (create, validate, restore, retention)
15. Upgrade Management (schedule, execute, cancel)
16. Vesting (create, release, summary)
17. Maintenance & Indexing (rebuild, prune, repair)
18. Health & Monitoring (health, metrics, analytics)
19. Audit Trail (query, verify chain integrity)
20. Diagnostics
21. Common Workflows (new deployment, incident response, pre-upgrade, daily health check)

Cross-linked from both README.md and docs/README.md.
- Add symmetric invoice_batch_cancel entrypoint allowing businesses to cancel up to MAX_BATCH_INVOICES (10) in a single transaction with single auth.
- Enforce contract pause status, confused-deputy prevention, business verification/KYC gating, batch size bounds (1..=10), pre-flight invoice existence, ownership, and non-frozen status validation for atomic rollback.
- Add comprehensive unit test module test_invoice_batch_cancel covering single/multiple/max batch cancellations, invalid sizes, KYC gating, nonexistent/unauthorized/frozen item atomicity.
- Update documentation in docs/contracts/invoice.md and docs/BATCH_OPS.md.

Closes QuickLendX#1881
Baskarayelu and others added 30 commits July 27, 2026 01:09
…-currency-precision

fix: add require_matching_currency_precision helper (QuickLendX#2091)
…dispute arbiter registry, backfill/migration guard

Closes QuickLendX#1820, QuickLendX#1821, QuickLendX#1840, QuickLendX#1847.

QuickLendX#1820 — Per-invoice `early_payment_discount_bps: Option<u32>` on Invoice and
InvoiceInput, bounded 0–5000 bps (same ceiling as `late_payment_penalty_bps`).
Threaded through `store_invoice` / `upload_invoice` / `store_invoices_batch`
and `Invoice::new`. Reuses `InvalidFeeBasisPoints` for over-limit rejection
(no spare enum slot under the 50-error-variant cap).

QuickLendX#1821 — Boundary tests covering Same-day (T-0), T-1, T-30 plus the bps-value
edges (0, 5000, 5001, u32::MAX, None). Runs on every CI matrix entry
(no feature gate).

QuickLendX#1840 — Splits dispute-adjudication authority from admin authority:
new `arbiter.rs` module + entry points `register_arbiter` / `unregister_arbiter`
/ `is_arbiter` / `list_arbiters`. `require_dispute_arbiter` gate fires in
`resolve_dispute` and `resolve_dispute_structured` only (not in the review
transition, matching the issues explicit "resolve" wording). Typed
`NotArbiter` error. Threat model: a single compromised admin key must NOT
silently authorise every dispute resolution on the platform.

QuickLendX#1847 — `require_no_pending_backfill` guard in `schedule_upgrade`. Backfill
flag (`PENDING_BACKFILL_KEY`) is set at the start of `restore_from_backup`
and cleared unconditionally on completion (success OR error). Under Sorobans
atomic-tx model, transaction failure rolls back the flag write too, so no
leaked-flag window. Threat model: without this guard, a WASM upgrade landing
between backup `clear_all` and per-invoice `store_invoice` would leave the
new contract code reading half-restored state with no signal that the view
was partial.

Error-slot housekeeping: renamed two slots to make room — `InvalidFreezeReason`
→ `NotArbiter` (had a duplicate symbol-mapping arm as a latent bug) and
`BackupVersionUnsupported` → `BackfillInProgress` (preserves the slot for the
new guard while keeping the rename semantically consistent with the backfill
lifecycle). Also closed three pre-existing missing arms in the From<QuickLendXError>
for Symbol impl (DuplicateBid, InvalidLedgerSequence, BackfillInProgress).

no_std discipline preserved (soroban_sdk-only). Threat model documented per
guard.
- Catalogs all risk-related parameters with min/max/default values
- Linked from README.md and docs/README.md

Note: pre-existing build failures in contract code (missing set_frozen/
is_frozen functions, incomplete error pattern matches) are unrelated to
this doc-only change and were not introduced here.
Closes QuickLendX#1960

Lock down the InvoiceFrozen event schema and the freeze_appeal_channel
value so that accidental renames, deletions, or silent type changes are
caught by the test suite.

New tests in test_freeze_appeal_channel.rs:
- Event data-map schema lock (exactly 5 field names, all present/non-null)
- Administrative variant channel + label coverage
- FreezeInfo XDR serialisation round-trip for all 9 reason variants
- Channel consistency across freeze/unfreeze/refreeze cycles
- Negative: freeze_investor must not emit InvoiceFrozen
- Proptest: channel invariant, label snake_case, FreezeInfo roundtrip
Adds a property-style test for saturating timestamp behavior on timed
periods and a deterministic test confirming the AllTime window preserves
the full history range.

Note: main currently fails to compile (36 errors in
quicklendx-contracts, unrelated to this change) — see issue comment
for details. Verified via 'git stash' that main is broken independent
of this diff.
…peated-nonces

feat: reject repeated nonces in settlement payments (Closes QuickLendX#2177)
…uard-2070

test: add regression coverage for report-period guard boundary
…e-appeal-1960

test: add serialisation stability tests for freeze_appeal_channel field
…m-case-1957

test: add quorum boundary edge case tests for governance proposals
…21-1840-1847

feat: dispute arbiter registry, backfill/migration guard, per-invoice early payment discount (closes QuickLendX#1820, QuickLendX#1821, QuickLendX#1840, QuickLendX#1847)
* test: add insurance-claim guard past limit test

Add test for timestamp > due_date boundary condition to complete
coverage of insurance-claim guard (within, at, past limit).

Closes QuickLendX#2101

* test: add over quorum test for multisig-signature helper

Add test_verify_op_over_quorum to verify that verify_op succeeds
when more signatures than the threshold are provided. This completes
the test coverage for all quorum boundary cases:
- Below quorum: test_verify_op_insufficient_signatures
- At quorum: test_verify_op_success
- Over quorum: test_verify_op_over_quorum (new)

Closes QuickLendX#2110

* test: add escrow-time-limit guard tests

Closes QuickLendX#2098

Add boundary tests for the escrow-time-limit guard in verification::validate_bid_placement:
- bid_succeeds_within_time_limit: happy path when timestamp < due_date
- bid_blocked_at_time_limit: sad path when timestamp == due_date (>= boundary)
- bid_blocked_past_time_limit: sad path when timestamp > due_date

The guard prevents bidding on invoices that have already reached their due_date.
… and tag docs (QuickLendX#2364)

* feat: add expired-escrow guard, category validation, bid-match helper, and tag docs

Implements four defence-in-depth fixes:

1. Reject bid acceptance on expired invoices (escrow.rs)
   - Blocks escrow creation when invoice due_date has passed
   - Negative test: accept_bid_blocked_when_invoice_is_expired

2. Add docs/QLX_INVOICE_TAGS.md
   - Documents tag normalization, validation rules, threat model, and storage layout

3. Add require_valid_invoice_category helper (verification.rs)
   - Rejects InvoiceCategory::Other as reserved with InvalidTag error
   - Negative test: require_valid_invoice_category_rejects_other_as_reserved

4. Add verify_bid_match helper (bid.rs)
   - Validates bid-invoice compatibility (status, expiry, amount, ownership)
   - Negative tests for each precondition violation

Closes #...

* fix: repair 48 pre-existing compilation errors across 9 files

- Remove duplicate MIN_TRANSFER const in payments.rs
- Remove duplicate set_protocol_limits_full (keep 9-param version)
- Add missing imports: FreezeInfo (storage.rs), require_business_active (lib.rs),
  ToXdr (idempotency.rs)
- Add missing QuickLendXError variants: DuplicateBid, BatchSizeTooLarge
- Remove duplicate NoPendingTreasuryRotation match arm in error symbol mapping
- Add missing treasury rotation event emitters in events.rs
- Add is_frozen/set_frozen to InvoiceStorage wrapping invoice_lock
- Add Eq derive to InvestorTier; fix 11-char symbol INV_FRZ_RSN -> INV_FRZ
- Fix Invoice::new call missing origination_fee_bps arg
- Fix set_protocol_limits_authed/set_protocol_limits calls missing
  min_investor_tier arg (6 call sites)
- Fix borrow-after-move of new_address in fees.rs initiate_treasury_rotation

* fix: resolve all compilation errors after merge with main

Fix 98 errors from merging main into chore/defence-in-depth-four-fixes:
- Remove duplicate functions conflicting with main (events, profits, settlement)
- Remove duplicate type definitions (InvestorFreezeReason, imports)
- Add missing error variants (PendingGovernanceProposal, UnstableCursor,
  SettlementCurrencyNotAllowed, UpgradePending, PerInvestorPositionCapExceeded,
  BidBelowTierMinimum, InvalidTransactionHash, BatchSizeExceeded)
- Add missing PAUSE_REASON_KEY, DataKey::PerInvestorPositionCap
- Fix Invoice::new argument count (add early_payment_discount_bps)
- Fix set_protocol_limits call argument counts
- Fix idempotency.rs imports, verification.rs hex validator
- Fix contract function name length (reset_bid_grace_to_default)
- Fix StorageKeys::set_frozen/is_frozen (bool return values)
- Rename test call to match new signature

* chore: remove temporary error output file
* test: add insurance-claim guard past limit test

Add test for timestamp > due_date boundary condition to complete
coverage of insurance-claim guard (within, at, past limit).

Closes QuickLendX#2101

* test: add over quorum test for multisig-signature helper

Add test_verify_op_over_quorum to verify that verify_op succeeds
when more signatures than the threshold are provided. This completes
the test coverage for all quorum boundary cases:
- Below quorum: test_verify_op_insufficient_signatures
- At quorum: test_verify_op_success
- Over quorum: test_verify_op_over_quorum (new)

Closes QuickLendX#2110
Add test for timestamp > due_date boundary condition to complete
coverage of insurance-claim guard (within, at, past limit).

Closes QuickLendX#2101
* docs: add QLX_MULTISIG_CONFIG.md documenting multisig setup and rotation

* docs: add QLX_ESCROW_TIME_LIMITS.md documenting escrow time limits and unclaimed escrow handling
* feat: add invoice lock time limit guard

Implement require_lock_within_time_limit(l) guard to reject actions on
expired locks (older than 30 days). This provides defense-in-depth
against indefinite invoice freezing by compromised admin credentials.

- Add InvoiceLockExpired error variant (1009)
- Add LOCK_TIME_LIMIT_SECONDS constant (30 days)
- Implement require_lock_within_time_limit() in InvoiceStorage
- Integrate guard at all freeze check locations (settlement, lib, contract)
- Add negative tests for expired and fresh lock scenarios
- Fix duplicate MIN_TRANSFER constant in payments.rs

Closes QuickLendX#2103

* fix: shorten InvoiceLockExpired error symbol to 9 chars

Soroban symbols have a max length of 9 characters. Changed INV_LK_EXP
to INV_LK_XPD to comply with this limit.
* docs: add QLX_MULTISIG_CONFIG.md documenting multisig setup and rotation

* docs: add QLX_DISPUTE_TIME_LIMITS.md documenting dispute time limits and grace period
Add a dedicated test module that locks in three behaviour buckets for
`payments::require_matching_currency_precision`:

- **Matching** (must succeed): token contract reports decimals=0,
  decimals=18 (boundary inclusive), and the SAC default decimals=7.
- **Over-precision** (must be rejected with InvalidCurrency):
  decimals=19 (just-over), decimals=20, and decimals=u32::MAX (saturation).
- **Malformed** (must be rejected with InvalidCurrency): unregistered
  address with no contract behind it, plus a wrong-return-type case that
  is gated behind a `#[ignore]` because Soroban 25.x aborts cross-contract
  type-mismatch calls at the host level (TODO note explains how to
  re-enable once the SDK stops doing that).

Two cross-cutting tests confirm that the `InvalidAmount` vs
`InvalidCurrency` error buckets are never confused with each other,
and that over-precision rejection is amount-independent (both `1`
and `i128::MAX` are rejected).

The new module uses plain `#[cfg(test)]` with no feature gate so it
runs on every CI matrix entry, satisfying the issue's acceptance
criteria.

Closes QuickLendX#2092
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add tests for the currency-precision helper