test: add currency-precision helper boundary tests (#2092) - #1
Open
YazarAyobami wants to merge 175 commits into
Open
test: add currency-precision helper boundary tests (#2092)#1YazarAyobami wants to merge 175 commits into
YazarAyobami wants to merge 175 commits into
Conversation
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
…proposal references
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
…-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.
docs: add business KYC tiers documentation
…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)
…min merge, -X theirs)
…dmin merge, -X theirs)
* 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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.rscover 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
quicklendx-contracts/src/test_currency_precision.rs#[ignore]-gated test.quicklendx-contracts/src/lib.rs#[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)
decimals()reportedtest_precision_matches_when_decimals_is_zero0Ok(())test_precision_matches_when_decimals_is_eighteen18(upper bound, inclusive)Ok(())test_precision_matches_sac_default_decimals7(SAC default)Ok(())2. Over-precision (must be rejected with
InvalidCurrency)decimals()reportedtest_precision_rejects_decimals_just_above_max19(just-over)Err(InvalidCurrency)test_precision_rejects_decimals_twenty20Err(InvalidCurrency)test_precision_rejects_decimals_max_u32u32::MAX(saturation)Err(InvalidCurrency)3. Malformed (must be rejected with
InvalidCurrency)test_precision_rejects_unregistered_addressAddress::generate(no contract)Err(InvalidCurrency)test_precision_rejects_wrong_return_typedecimals()returnsSymbolinstead ofu32Err(InvalidCurrency)(#[ignore]’d — see note below)Cross-cutting
test_precision_overprecision_rejection_is_amount_independent— over-precision rejection must be independent ofamount. Tested with both1andi128::MAX.test_precision_amount_and_currency_errors_are_distinct—InvalidAmount(negative / zero amount) andInvalidCurrency(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 justdecimals()); none exposetransfer/balance/allowance, which is fine because the helper under test never calls those. The pattern matchesPropTestContractintest_volume_tier_props.rs.About the
#[ignore]’d testtest_precision_rejects_wrong_return_typeis gated behind#[ignore]because Soroban 25.x aborts cross-contract type-mismatch calls at the host level rather than returning a transport-levelErrthattry_invoke_contract::<u32, _>can swallow. This mirrors the existing#[ignore]ontest_create_escrow_unregistered_token_address_does_not_succeedinpayments.rs. A// TODO(#2092)comment on the test explains how to re-enable it once the SDK stops aborting (current pin: 25.1.1 inCargo.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-testsor--features legacy-testsflags required.Local validation
cargois not available in this codespace, socargo build --tests/cargo testcould not be run locally. CI is the source of truth for compilation + test execution.#[ignore]rationale.Test count
#[ignore]).Err(QuickLendXError::InvalidCurrency)/Err(QuickLendXError::InvalidAmount)/Ok(())— no fuzzy proptest heuristics.Risk + rollback
#[ignore]’d test is not consumed bycargo testruns.quicklendx-contracts/src/test_currency_precision.rsand the 6-linemod test_currency_precision;block inlib.rs.