forked from QuickLendX/quicklendx-protocol
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathlib.rs
More file actions
5450 lines (4889 loc) · 212 KB
/
Copy pathlib.rs
File metadata and controls
5450 lines (4889 loc) · 212 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#![no_std]
#![allow(
dead_code,
unused_imports,
unused_variables,
unused_comparisons,
deprecated,
clippy::too_many_arguments,
clippy::doc_overindented_list_items,
clippy::absurd_extreme_comparisons,
clippy::needless_range_loop,
clippy::manual_checked_ops,
clippy::collapsible_match,
clippy::let_unit_value,
clippy::needless_borrow,
clippy::match_like_matches_macro,
clippy::needless_return,
clippy::disallowed_methods
)]
pub use crate::errors::QuickLendXError;
extern crate alloc;
#[cfg(all(test, feature = "legacy-tests"))]
mod scratch_events;
#[cfg(test)]
mod test_concurrent_default_overlap;
#[cfg(test)]
mod test_multisig;
#[cfg(all(test, feature = "legacy-tests"))]
mod test_default;
#[cfg(all(test, feature = "legacy-tests"))]
mod test_default_finality;
#[cfg(all(test, feature = "legacy-tests"))]
mod test_default_finality_matrix;
#[cfg(all(test, feature = "legacy-tests"))]
mod test_emergency_withdraw_props;
#[cfg(all(test, feature = "legacy-tests"))]
mod test_escrow;
#[cfg(all(test, feature = "legacy-tests"))]
mod test_escrow_uniqueness;
#[cfg(all(test, feature = "legacy-tests"))]
mod test_fees;
#[cfg(all(test, feature = "legacy-tests"))]
mod test_maintenance;
#[cfg(all(test, feature = "legacy-tests"))]
mod test_maintenance_write_matrix;
#[cfg(test)]
mod test_settlement_capacity_stress;
#[cfg(test)]
mod test_settlement_history_reconstruction;
// Issue #1920 — confirm require_regulatory_ok is truly a no-op by default.
#[cfg(test)]
mod test_regulatory_gate;
// Issue #1902 — investor freeze reason typed enum
#[cfg(test)]
mod test_investor_freeze_reason;
use crate::idempotency::{idempotency_exists, idempotency_key, store_idempotency};
use crate::verification::require_business_active;
use soroban_sdk::{contract, contractimpl, symbol_short, Address, BytesN, Env, Map, String, Vec};
pub mod address_summary;
pub mod admin;
pub mod analytics;
pub mod arbiter;
pub mod audit;
pub mod backpressure;
pub mod backup;
pub mod backup_v1;
#[cfg(any(test, feature = "testutils"))]
pub mod bench;
pub mod bid;
pub mod currency;
pub mod defaults;
pub mod diagnostics;
pub mod dispute;
pub mod dispute_timeline;
pub mod emergency;
pub mod errors;
pub mod escrow;
pub mod events;
pub mod fees;
pub mod freshness;
pub mod governance;
pub mod multisig;
pub mod health;
pub mod idempotency;
pub mod incident;
pub mod init;
pub mod invariants;
pub mod investment;
pub mod investment_queries;
pub mod invoice;
pub mod invoice_search;
pub mod maintenance;
pub mod monitor;
pub mod notifications;
pub mod operational_limits;
pub mod pagination;
pub mod panic_handler;
pub mod pause;
pub mod payments;
pub mod profits;
pub mod protocol_limits;
pub mod reentrancy;
pub mod regulatory;
pub mod settlement;
pub mod storage;
#[cfg(any(test, feature = "testutils"))]
pub mod test_utils;
#[cfg(all(test, feature = "legacy-tests"))]
mod test_accept_bid_instruction_budget;
#[cfg(all(test, feature = "legacy-tests"))]
mod test_accept_bid_race;
#[cfg(test)]
mod test_panic_handler;
#[cfg(test)]
mod test_due_date_guard;
#[cfg(test)]
mod test_lock_time_limit;
mod test_auto_resolution_boundary;
#[cfg(test)]
mod test_cancel_invoice_matrix;
#[cfg(test)]
mod test_governance;
#[cfg(all(test, feature = "legacy-tests"))]
mod test_admin;
#[cfg(all(test, feature = "legacy-tests"))]
mod test_admin_simple;
#[cfg(all(test, feature = "legacy-tests"))]
mod test_admin_standalone;
#[cfg(all(test, feature = "legacy-tests"))]
mod test_admin_two_step;
#[cfg(all(test, feature = "legacy-tests"))]
mod test_audit;
#[cfg(test)]
mod test_audit_config;
#[cfg(all(test, feature = "legacy-tests"))]
mod test_backup;
#[cfg(all(test, feature = "legacy-tests"))]
mod test_backup_restore_reindex;
#[cfg(all(test, feature = "legacy-tests"))]
mod test_backup_retention_enforcement;
#[cfg(all(test, feature = "legacy-tests"))]
mod test_backup_safety;
#[cfg(test)]
mod test_bid_cancel_accept_race;
#[cfg(all(test, feature = "legacy-tests"))]
mod test_bid_expiry_boundary;
#[cfg(test)]
mod test_bid_expiry_grace;
#[cfg(all(test, feature = "legacy-tests"))]
mod test_bid_ttl;
#[cfg(test)]
mod test_require_business_active;
#[cfg(test)]
mod test_require_valid_business_kyc_tier;
#[cfg(test)]
mod test_cancel_invoice_matrix;
#[cfg(all(test, feature = "legacy-tests"))]
mod test_cleanup_pagination;
#[cfg(test)]
mod test_config_bounds_matrix;
#[cfg(all(test, feature = "legacy-tests"))]
mod test_currency;
// Issue #2092 — currency-precision helper boundary tests; runs on every CI
// matrix entry (no feature gate). Covers matching (decimals=0, 7, 18),
// over-precision (decimals=19, 20, u32::MAX), and malformed cases
// (unregistered address, wrong return type).
#[cfg(test)]
mod test_currency_precision;
#[cfg(test)]
mod test_currency_batch;
#[cfg(all(test, feature = "legacy-tests"))]
mod test_currency_match_funding;
#[cfg(all(test, feature = "legacy-tests"))]
mod test_dispute;
#[cfg(test)]
mod test_dispute_refund_flow;
#[cfg(test)]
mod test_dispute_history_guard;
#[cfg(test)]
mod test_evidence_size_cap;
#[cfg(test)]
mod test_evidence_hash_format;
// Issue #1975 — evidence-kind guard matrix; no feature gate (runs on every CI
// matrix entry). Also the regression test for the fix landed alongside it:
// `create_dispute` was not calling `validate_dispute_evidence` /
// `validate_dispute_eligibility` at all.
#[cfg(test)]
mod test_evidence_kind_guard_matrix;
#[cfg(all(test, feature = "legacy-tests"))]
mod test_dispute_timeline_props;
#[cfg(test)]
mod test_due_date_guard;
#[cfg(test)]
mod test_lock_time_limit_guard;
#[cfg(test)]
// mod test_dispute_event_invariant;
#[cfg(test)]
mod test_dust_transfer;
// Issue #1840 — arbiter guard on dispute resolution (no feature gate: every
// CI matrix entry must exercise the negative test path).
#[cfg(test)]
mod test_dispute_arbiter;
// Issue #1847 — backfill guard against WASM upgrades (no feature gate).
#[cfg(test)]
mod test_backfill_guard;
// Issue #1820/#1821 — early_payment_discount_bps per-invoice config and
// boundary tests.
#[cfg(test)]
mod test_early_payment_discount_bps;
#[cfg(test)]
mod test_escrow_early_release;
#[cfg(all(test, feature = "legacy-tests"))]
mod test_escrow_event_completeness;
#[cfg(all(test, feature = "legacy-tests"))]
mod test_escrow_invariant_model;
#[cfg(all(test, feature = "legacy-tests"))]
mod test_escrow_refund_after_expiry;
#[cfg(all(test, feature = "legacy-tests"))]
mod test_expired_bids_cleanup;
#[cfg(test)]
mod test_freshness;
#[cfg(all(test, feature = "legacy-tests"))]
mod test_freshness_bounds;
#[cfg(test)]
mod test_investor_kyc;
#[cfg(test)]
mod test_payments;
#[cfg(test)]
mod test_queries;
#[cfg(test)]
mod test_rating_override;
#[cfg(all(test, feature = "legacy-tests"))]
mod test_self_call_rejection;
// Issue #1541 — lag at zero, lag at positive, lag during pause.
#[cfg(all(test, feature = "legacy-tests"))]
mod test_freshness_lag;
#[cfg(all(test, feature = "legacy-tests"))]
mod test_health_status;
#[cfg(all(test, feature = "legacy-tests"))]
mod test_init;
#[cfg(all(test, feature = "legacy-tests"))]
mod test_invariant_self_check;
#[cfg(all(test, feature = "legacy-tests"))]
mod test_investment_consistency;
#[cfg(test)]
mod test_operational_limits;
#[cfg(test)]
mod test_regulatory;
#[cfg(all(test, feature = "legacy-tests"))]
mod test_withdraw_bid_matrix;
// #[cfg(test)]
#[cfg(test)]
#[path = "test/test_investment_queries.rs"]
mod test_investment_queries;
// #[cfg(all(test, feature = "legacy-tests"))]
// mod test_overflow;
// #[cfg(all(test, feature = "legacy-tests"))]
// mod test_pause;
// #[cfg(all(test, feature = "legacy-tests"))]
// mod test_profit_fee;
// #[cfg(all(test, feature = "legacy-tests"))]
#[cfg(all(test, feature = "legacy-tests"))]
mod test_backpressure_shedding;
#[cfg(all(test, feature = "legacy-tests"))]
mod test_profit_fee;
#[cfg(all(test, feature = "legacy-tests"))]
mod test_protocol_health;
#[cfg(all(test, feature = "legacy-tests"))]
mod test_protocol_limits_boundary;
#[cfg(all(test, feature = "legacy-tests"))]
// mod test_refund;
// #[cfg(all(test, feature = "legacy-tests"))]
// mod test_storage;
#[cfg(all(test, feature = "legacy-tests"))]
mod test_reentrancy;
#[cfg(all(test, feature = "legacy-tests"))]
mod test_reentrancy_fault_injection;
#[cfg(test)]
mod test_settlement_accounting_identity;
// Issue #1908 — per-invoice settlement currency whitelist (defence-in-depth).
// Negative test: settlement blocked when whitelist does not match invoice currency.
#[cfg(test)]
mod test_settlement_currency_whitelist;
#[cfg(test)]
mod test_fuzz_settlement_currency_whitelist;
#[cfg(test)]
mod test_settle_during_dispute;
#[cfg(test)]
mod test_string_limits;
#[cfg(all(test, feature = "legacy-tests"))]
mod test_string_limits;
// #[cfg(all(test, feature = "legacy-tests"))]
// mod test_types;
#[cfg(all(test, feature = "legacy-tests"))]
mod test_analytics_consistency;
// Issue snapshot-tests — clean snapshot and snapshot with open dispute.
// No feature gate: runs on every CI matrix entry.
#[cfg(test)]
mod test_snapshot;
#[cfg(test)]
mod test_bid_capacity_stress;
// Issue #1891 — min-partial-fill amount boundary: at limit, one below, one above.
#[cfg(test)]
mod test_min_partial_fill_boundary;
// Issue #1858 — per-invoice per_investor_position_cap whale defence.
#[cfg(test)]
mod test_per_investor_position_cap;
#[cfg(all(test, feature = "fuzz-tests"))]
mod test_bid_compare_order_props;
#[cfg(all(test, feature = "legacy-tests"))]
mod test_bid_ranking;
// Issue #2083 — bid-match helper tests; runs on every CI matrix entry
// (no feature gate, since `legacy-tests` is OFF in CI). Covers
// `compare_bids`, `get_best_bid`, and `rank_bids`.
#[cfg(test)]
mod test_bid_match_helper;
// Issue #2089 — max-invoice-tags helper boundary tests; runs on every CI
// matrix entry (no feature gate). Locks in below/at/over-cap behaviour for
// `Invoice::add_tag`, the bulk ctor `Invoice::new`, and the pure validator
// `validate_invoice_tags`. Assertive names and deterministic inputs only.
#[cfg(test)]
mod test_max_invoice_tags_boundary;
#[cfg(test)]
mod test_require_valid_invoice_category;
#[cfg(test)]
mod test_verify_bid_match;
#[cfg(test)]
mod test_expired_escrow;
#[cfg(test)]
mod test_vesting;
#[cfg(test)]
mod test_vesting_summary;
// Issue #1551 — determinism tests for bid_ranking; no feature gate, runs on
// every CI matrix entry.
// #[cfg(test)]
// mod test_bid_ranking_determinism;
#[cfg(all(test, feature = "legacy-tests"))]
mod test_business_invoices_paged_ordering;
#[cfg(all(test, feature = "legacy-tests"))]
mod test_category_breakdown;
#[cfg(all(test, feature = "legacy-tests"))]
mod test_clock_rollover;
#[cfg(all(test, feature = "fuzz-tests"))]
mod test_compute_yield_props;
#[cfg(all(test, feature = "legacy-tests"))]
mod test_default_grace_boundary;
#[cfg(test)]
mod test_diagnostics;
#[cfg(all(test, feature = "legacy-tests"))]
mod test_events;
#[cfg(all(test, feature = "fuzz-tests"))]
mod test_fuzz_cancelled_noop;
#[cfg(all(test, feature = "legacy-tests", feature = "fuzz-tests"))]
mod test_fuzz_distribute_revenue;
#[cfg(test)]
mod test_fuzz_default_counter;
#[cfg(all(test, feature = "legacy-tests", feature = "fuzz-tests"))]
mod test_fuzz_invoice_metadata;
#[cfg(all(test, feature = "fuzz-tests"))]
mod test_fuzz_partial_payment;
#[cfg(all(test, feature = "fuzz-tests"))]
mod test_profits_props;
#[cfg(all(test, feature = "legacy-tests"))]
mod test_incident;
#[cfg(test)]
mod test_init_debug;
#[cfg(test)]
mod test_init_invariants;
#[cfg(test)]
mod test_input_matrix;
#[cfg(all(test, feature = "legacy-tests"))]
mod test_insurance_claim_payout;
#[cfg(test)]
mod test_insurance_optin_lifecycle;
#[cfg(test)]
mod test_invoice;
#[cfg(all(test, feature = "fuzz-tests"))]
mod test_insurance_premium_props;
#[cfg(all(test, feature = "legacy-tests"))]
mod test_investment_transitions;
// Issue #1949 — full InvestmentStatus transition matrix (CI-ungated).
#[cfg(test)]
mod test_investment_state_matrix;
#[cfg(all(test, feature = "legacy-tests"))]
mod test_investment_withdrawal;
#[cfg(all(test, feature = "legacy-tests"))]
mod test_invoice_metadata;
#[cfg(all(test, feature = "legacy-tests"))]
mod test_invoice_search_ranking;
#[cfg(all(test, feature = "legacy-tests"))]
mod test_line_item_consistency;
#[cfg(test)]
mod test_max_invoices_per_business;
#[cfg(all(test, feature = "legacy-tests"))]
mod test_notifications;
#[cfg(all(test, feature = "legacy-tests"))]
mod test_pause_reads_available;
#[cfg(all(test, feature = "legacy-tests"))]
mod test_pause_reason;
mod test_platform_metrics_reconciliation;
#[cfg(all(test, feature = "legacy-tests"))]
mod test_rebuild_indexes;
#[cfg(all(test, feature = "fuzz-tests"))]
mod test_seed;
#[cfg(all(test, feature = "legacy-tests", feature = "fuzz-tests"))]
mod test_treasury_split_overflow_props;
#[cfg(all(test, feature = "fuzz-tests"))]
mod test_twa_props;
#[cfg(all(test, feature = "fuzz-tests"))]
mod test_volume_tier_props;
// Issue #1482 — "cannot withdraw more than deposited" invariant: hard-coded sad
// path (always runs) + proptest property (requires fuzz-tests feature).
#[cfg(test)]
mod test_cannot_withdraw_more_than_deposited;
#[cfg(test)]
mod test_store_invoice_auth;
// Issue #1880 — batch-create boundary tests; no feature gate (runs on every CI matrix entry).
// Covers 0, 1, MAX_BATCH, MAX_BATCH+1, active-invoice cap, KYC gating, and atomicity.
#[cfg(test)]
mod test_store_invoices_batch;
// Issue #1881 — batch-cancel boundary tests; no feature gate (runs on every CI matrix entry).
// Covers 0, 1, MAX_BATCH, MAX_BATCH+1, KYC gating, frozen items, non-existent items, unauthorized items, and atomicity.
#[cfg(test)]
mod test_invoice_batch_cancel;
#[cfg(test)]
mod test_tier_boundary;
// Issue — symmetric pause/maintenance state-change tests (both directions); no
// feature gate so this runs on every CI matrix entry.
#[cfg(test)]
mod test_pause_toggle_symmetry;
#[cfg(test)]
mod test_verification_matrix;
pub mod types;
pub use types::*;
pub mod upgrade;
pub mod verification;
pub mod vesting;
use admin::require_not_self;
use admin::AdminStorage;
use defaults::{
handle_default as do_handle_default, mark_invoice_defaulted as do_mark_invoice_defaulted,
};
use escrow::{
accept_bid_and_fund as do_accept_bid_and_fund, refund_escrow_funds as do_refund_escrow_funds,
withdraw_investment as do_withdraw_investment,
};
use events::{
emit_bid_accepted, emit_bid_placed, emit_bid_withdrawn, emit_dispute_created,
emit_dispute_rejected, emit_dispute_resolved, emit_dispute_under_review, emit_escrow_created,
emit_escrow_released, emit_insurance_added, emit_insurance_premium_collected,
emit_investor_verified, emit_invoice_cancelled, emit_invoice_metadata_cleared,
emit_invoice_metadata_updated, emit_invoice_uploaded, emit_invoice_verified,
};
use investment::InvestmentStorage;
use invoice_search::InvoiceSearch;
use payments::{create_escrow, release_escrow, require_matching_currency_precision, EscrowStorage};
use profits::{calculate_profit as do_calculate_profit, PlatformFee};
use settlement::{
process_partial_payment as do_process_partial_payment, settle_invoice as do_settle_invoice,
};
use verification::{
calculate_investment_limit, calculate_investor_risk_score, compute_investor_tier,
determine_investor_tier, get_investor_verification as do_get_investor_verification,
normalize_tag, recompute_investor_tier, reject_business, reject_investor as do_reject_investor,
require_business_not_pending, require_investor_not_frozen, require_investor_not_pending,
revoke_investor_kyc as do_revoke_investor_kyc, submit_investor_kyc as do_submit_investor_kyc,
submit_kyc_application, validate_bid, validate_dispute_eligibility, validate_dispute_evidence,
validate_dispute_reason, validate_dispute_resolution, validate_investor_investment,
validate_invoice_metadata, verify_business,
verify_investor as do_verify_investor, verify_invoice_data, BusinessVerificationStatus,
BusinessVerificationStorage, InvestorRiskLevel, InvestorTier, InvestorVerification,
InvestorVerificationStorage,
};
use crate::storage::{BidStorage, InvoiceStorage};
/// Render a 1-5 rating score as a decimal `String` for audit-log serialization.
fn fmt_rating(env: &Env, value: u32) -> String {
let mut buf = [0u8; 10];
let len = audit::write_u64_to_buf(&mut buf, value as u64);
String::from_str(env, core::str::from_utf8(&buf[..len]).unwrap_or("0"))
}
#[contract]
pub struct QuickLendXContract;
/// Maximum number of records returned by paginated query endpoints.
pub const MAX_QUERY_LIMIT: u32 = pagination::MAX_QUERY_LIMIT;
/// @notice Validates and caps query limit to prevent resource abuse
/// @param limit The requested limit value
/// @return The capped limit value, never exceeding MAX_QUERY_LIMIT
/// @dev Returns 0 if limit is 0, enforcing empty result behavior
#[inline]
fn cap_query_limit(limit: u32) -> u32 {
pagination::cap_query_limit(limit)
}
/// @notice Validates query parameters for security and resource protection
/// @param offset The pagination offset
/// @param limit The requested result limit
/// @return Result indicating validation success or failure
/// @dev Prevents potential overflow and ensures reasonable query bounds
fn validate_query_params(offset: u32, limit: u32) -> Result<(), QuickLendXError> {
pagination::validate_query_params(offset, limit)
}
/// Defence-in-depth guard: reject any write when the target invoice is frozen.
///
/// An invoice can be frozen via `freeze_invoice` (admin action, compliance hold,
/// KYC revocation, etc.). While frozen, **all** state-mutating operations on that
/// invoice must be blocked — otherwise an attacker (or a compromised admin path)
/// could still drain escrow, alter metadata, or advance the lifecycle despite an
/// active hold.
///
/// Returns `Err(QuickLendXError::InvoiceFrozen)` when the invoice lock is active.
fn require_no_active_freeze(env: &Env, invoice_id: &BytesN<32>) -> Result<(), QuickLendXError> {
if InvoiceStorage::is_frozen(env, invoice_id) {
return Err(QuickLendXError::InvoiceFrozen);
}
Ok(())
}
/// Load an invoice and assert that the business caller has write access.
///
/// Consolidates the common preamble shared by `cancel_invoice`,
/// `update_invoice_metadata`, and `clear_invoice_metadata`:
/// - Protocol is not paused
/// - Invoice exists
/// - Invoice is not frozen
/// - Transaction is authorized by the invoice's business address
/// - Business is active (not deleted/frozen)
///
/// Returns the loaded `Invoice` so callers can proceed with their
/// specific logic without repeating these five guards.
fn require_invoice_writable_by_business(
env: &Env,
invoice_id: &BytesN<32>,
) -> Result<Invoice, QuickLendXError> {
pause::PauseControl::require_not_paused(env)?;
let invoice = InvoiceStorage::get_invoice(env, invoice_id)
.ok_or(QuickLendXError::InvoiceNotFound)?;
require_no_active_freeze(env, invoice_id)?;
invoice.business.require_auth();
require_business_active(env, &invoice.business)?;
Ok(invoice)
}
/// Write a `u32` as ASCII decimal into `buf`, return byte length.
#[inline]
fn u32_to_ascii_lib(mut value: u32, buf: &mut [u8; 10]) -> usize {
if value == 0 {
buf[0] = b'0';
return 1;
}
let mut tmp = [0u8; 10];
let mut len = 0usize;
while value > 0 {
tmp[len] = b'0' + (value % 10) as u8;
value /= 10;
len += 1;
}
for i in 0..len {
buf[i] = tmp[len - 1 - i];
}
len
}
/// Convert a `u32` to a soroban `String` using stack-allocated ASCII.
#[inline]
pub(crate) fn u32_to_string_lib(env: &Env, value: u32) -> String {
let mut buf = [0u8; 10];
let n = u32_to_ascii_lib(value, &mut buf);
let s = core::str::from_utf8(&buf[..n]).unwrap_or("0");
String::from_str(env, s)
}
/// Convert an `i64` to a soroban `String` using stack-allocated ASCII.
#[inline]
pub(crate) fn i64_to_string_lib(env: &Env, value: i64) -> String {
// "-9223372036854775808" = 20 chars
let mut buf = [0u8; 21];
let mut tmp = [0u8; 20];
let (negative, abs_val) = if value < 0 {
(true, (value as i128).unsigned_abs() as u64)
} else {
(false, value as u64)
};
let n = u64_to_ascii_20(abs_val, &mut tmp);
let start = if negative {
buf[0] = b'-';
buf[1..1 + n].copy_from_slice(&tmp[..n]);
1 + n
} else {
buf[..n].copy_from_slice(&tmp[..n]);
n
};
let s = core::str::from_utf8(&buf[..start]).unwrap_or("0");
String::from_str(env, s)
}
#[inline]
fn u64_to_ascii_20(mut value: u64, buf: &mut [u8; 20]) -> usize {
if value == 0 {
buf[0] = b'0';
return 1;
}
let mut tmp = [0u8; 20];
let mut len = 0usize;
while value > 0 {
tmp[len] = b'0' + (value % 10) as u8;
value /= 10;
len += 1;
}
for i in 0..len {
buf[i] = tmp[len - 1 - i];
}
len
}
fn early_release_approval_key(
invoice_id: &BytesN<32>,
approver: &Address,
) -> (soroban_sdk::Symbol, BytesN<32>, Address) {
(symbol_short!("er_appr"), invoice_id.clone(), approver.clone())
}
fn has_early_release_approval(env: &Env, invoice_id: &BytesN<32>, approver: &Address) -> bool {
env.storage()
.persistent()
.get(&early_release_approval_key(invoice_id, approver))
.unwrap_or(false)
}
#[contractimpl]
impl QuickLendXContract {
// ============================================================================
// Admin Management Functions
// ============================================================================
/// Initialize the protocol with all required configuration (one-time setup)
pub fn initialize(env: Env, params: init::InitializationParams) -> Result<(), QuickLendXError> {
init::ProtocolInitializer::initialize(&env, ¶ms)
}
/// Check if the protocol has been initialized
pub fn is_initialized(env: Env) -> bool {
init::ProtocolInitializer::is_initialized(&env)
}
/// Get the protocol/contract version
///
/// Returns the version written during initialization, or the current
/// PROTOCOL_VERSION constant if the contract has not been initialized yet.
///
/// # Returns
/// * `u32` - The protocol version number
///
/// # Version Format
/// Version is a simple integer increment (e.g., 1, 2, 3...)
/// Major versions indicate breaking changes that require migration.
pub fn get_version(env: Env) -> u32 {
init::ProtocolInitializer::get_version(&env)
}
/// Get current protocol limits
pub fn get_protocol_limits(env: Env) -> protocol_limits::ProtocolLimits {
protocol_limits::ProtocolLimitsContract::get_protocol_limits(env)
}
/// Admin-only: update the absolute minimum bid amount.
pub fn update_minimum_bid(
env: Env,
admin: Address,
amount: i128,
) -> Result<i128, QuickLendXError> {
protocol_limits::ProtocolLimitsContract::update_minimum_bid(env, admin, amount)
}
/// Admin-only: extends the TTL for all major persistent storage indexes.
pub fn extend_protocol_ttl(
env: Env,
admin: Address,
) -> Result<maintenance::ExtendReport, QuickLendXError> {
maintenance::MaintenanceControl::extend_protocol_ttl(&env, &admin)
}
/// Admin-gated protocol heartbeat. Authenticates `admin` as the stored protocol
/// admin, then runs every composed invariant check read-only.
pub fn invariant_self_check(
env: Env,
admin: Address,
) -> Result<invariants::InvariantReport, QuickLendXError> {
invariants::invariant_self_check(&env, &admin)
}
/// Initialize the admin address (deprecated: use initialize)
pub fn initialize_admin(env: Env, admin: Address) -> Result<(), QuickLendXError> {
AdminStorage::initialize(&env, &admin)
}
/// Transfer admin role to a new address
///
/// # Arguments
/// * `env` - The contract environment
/// * `new_admin` - The new admin address
///
/// # Returns
/// * `Ok(())` if transfer succeeds
/// * `Err(QuickLendXError::NotAdmin)` if caller is not current admin
///
/// # Security
/// - Requires authorization from current admin
pub fn transfer_admin(env: Env, new_admin: Address) -> Result<(), QuickLendXError> {
let admin = AdminStorage::get_admin(&env).ok_or(QuickLendXError::NotAdmin)?;
AdminStorage::transfer_admin(&env, &admin, &new_admin)
}
/// Initiate a two-step admin transfer.
pub fn initiate_admin_transfer(
env: Env,
admin: Address,
new_admin: Address,
) -> Result<(), QuickLendXError> {
AdminStorage::initiate_admin_transfer(&env, &admin, &new_admin)
}
/// Get the current admin address
///
/// # Returns
/// * `Some(Address)` if admin is set
/// * `None` if admin has not been initialized
pub fn get_current_admin(env: Env) -> Option<Address> {
AdminStorage::get_admin(&env)
}
/// Enable or disable two-step admin transfers.
pub fn set_two_step_enabled(
env: Env,
admin: Address,
enabled: bool,
) -> Result<(), QuickLendXError> {
AdminStorage::set_two_step_enabled(&env, &admin, enabled)
}
/// Set protocol configuration (admin only)
pub fn set_protocol_config(
env: Env,
admin: Address,
min_invoice_amount: i128,
max_due_date_days: u64,
grace_period_seconds: u64,
backfill_max_batch_size: u32,
) -> Result<(), QuickLendXError> {
init::ProtocolInitializer::set_protocol_config(
&env,
&admin,
min_invoice_amount,
max_due_date_days,
grace_period_seconds,
backfill_max_batch_size,
)
}
/// Set fee configuration (admin only)
pub fn set_fee_config(env: Env, admin: Address, fee_bps: u32) -> Result<(), QuickLendXError> {
init::ProtocolInitializer::set_fee_config(&env, &admin, fee_bps)
}
/// Dry-run preview for `set_protocol_config` and `set_fee_config` (admin-gated, read-only).
///
/// Returns a [`init::ProtocolConfigDiff`] showing projected before/after values and
/// validation metadata for the proposed `params`, **without mutating any contract state**.
///
/// # Security
/// - Requires admin authorization.
/// - No storage writes occur; safe for use in monitoring and governance tooling.
///
/// # Returns
/// * `Ok(ProtocolConfigDiff)` — before/after diff with `would_succeed` and `is_noop` flags.
/// * `Err(QuickLendXError::NotAdmin)` — caller is not the current admin.
/// * `Err(QuickLendXError::OperationNotAllowed)` — admin subsystem not initialized.
pub fn preview_protocol_config(
env: Env,
admin: Address,
params: init::ProtocolConfigParams,
) -> Result<init::ProtocolConfigDiff, QuickLendXError> {
init::ProtocolInitializer::preview_protocol_config(&env, &admin, params)
}
/// Set treasury address (admin only)
pub fn set_treasury(
env: Env,
admin: Address,
treasury: Address,
) -> Result<(), QuickLendXError> {
init::ProtocolInitializer::set_treasury(&env, &admin, &treasury)
}
/// Admin-only: cancel a pending treasury address rotation before it executes.
///
/// # Arguments
/// * `admin` - The address of the caller, must be the current admin.
pub fn cancel_treasury_rotation(env: Env, admin: Address) -> Result<(), QuickLendXError> {
admin::cancel_treasury_rotation(&env, &admin)
}
/// Get the pending treasury address and its execution timestamp, if any.
/// This is a view-only function for UI and testing purposes.
pub fn get_pending_treasury(env: Env) -> Option<(Address, u64)> {
storage::get_pending_treasury(&env)
}
/// Get current fee in basis points
pub fn get_fee_bps(env: Env) -> u32 {
init::ProtocolInitializer::get_fee_bps(&env)
}
/// Expose the current fee schedule
pub fn get_fee_schedule(env: Env) -> Vec<crate::fees::FeeStructure> {
crate::fees::FeeManager::get_fee_schedule(&env)
}
/// Get treasury address
pub fn get_treasury(env: Env) -> Option<Address> {
init::ProtocolInitializer::get_treasury(&env)
}
/// Get minimum invoice amount
pub fn get_min_invoice_amount(env: Env) -> i128 {
init::ProtocolInitializer::get_min_invoice_amount(&env)
}
/// Get maximum due date days
pub fn get_max_due_date_days(env: Env) -> u64 {
init::ProtocolInitializer::get_max_due_date_days(&env)
}
/// Get grace period in seconds
pub fn get_grace_period_seconds(env: Env) -> u64 {
init::ProtocolInitializer::get_grace_period_seconds(&env)
}
/// Get the corridor list (approved counterparty addresses for cross-invoice operations)
pub fn get_corridors(env: Env) -> Vec<Address> {
init::ProtocolInitializer::get_corridors(&env)
}
/// Admin-only: configure default bid TTL (days). Bounds: 1..=30.
pub fn set_bid_ttl_days(env: Env, days: u64) -> Result<u64, QuickLendXError> {
pause::PauseControl::require_not_paused(&env)?;
let admin = AdminStorage::get_admin(&env).ok_or(QuickLendXError::NotAdmin)?;
bid::BidStorage::set_bid_ttl_days(&env, &admin, days)
}
/// Get configured bid TTL in days (returns default 7 if not set)
pub fn get_bid_ttl_days(env: Env) -> u64 {
bid::BidStorage::get_bid_ttl_days(&env)
}
/// Get current bid TTL configuration snapshot
pub fn get_bid_ttl_config(env: Env) -> bid::BidTtlConfig {
bid::BidStorage::get_bid_ttl_config(&env)
}
/// Reset bid TTL to the compile-time default
pub fn reset_bid_ttl_to_default(env: Env) -> Result<u64, QuickLendXError> {
let admin = AdminStorage::get_admin(&env).ok_or(QuickLendXError::NotAdmin)?;
bid::BidStorage::reset_bid_ttl_to_default(&env, &admin)
}
/// Admin-only: configure the bid expiry grace period (seconds). Bounds: 0..=2_592_000 (30 days).
///
/// This is the additional buffer, on top of a bid's expiration timestamp,
/// that must elapse before the permissionless cleanup entrypoints
/// (`cleanup_expired_bids` / `cleanup_expired_bids_paged`) will transition
/// it from `Placed` to `Expired`. Defaults to `0`, matching the
/// pre-existing behaviour of cleaning up immediately at raw expiry.
pub fn set_bid_expiry_grace_seconds(env: Env, seconds: u64) -> Result<u64, QuickLendXError> {
pause::PauseControl::require_not_paused(&env)?;
let admin = AdminStorage::get_admin(&env).ok_or(QuickLendXError::NotAdmin)?;
bid::BidStorage::set_bid_expiry_grace_seconds(&env, &admin, seconds)
}
/// Get the configured bid expiry grace period in seconds (returns default 0 if not set)
pub fn get_bid_expiry_grace_seconds(env: Env) -> u64 {
bid::BidStorage::get_bid_expiry_grace_seconds(&env)
}
/// Get the current bid expiry grace-period configuration snapshot
pub fn get_bid_expiry_grace_config(env: Env) -> bid::BidExpiryGraceConfig {
bid::BidStorage::get_bid_expiry_grace_config(&env)
}
/// Reset the bid expiry grace period to the compile-time default (0)
pub fn reset_bid_grace_to_default(env: Env, admin: Address) -> Result<u64, QuickLendXError> {
admin.require_auth();
bid::BidStorage::reset_bid_expiry_grace_to_default(&env, &admin)
}
/// Get maximum active bids allowed per investor
pub fn get_max_active_bids_per_investor(env: Env) -> u32 {
bid::BidStorage::get_max_active_bids_per_investor(&env)
}
/// Set maximum active bids allowed per investor (admin only).
///
/// Pass `0` to disable the limit (any number of concurrent bids allowed).
pub fn set_max_active_bids_per_investor(env: Env, limit: u32) -> Result<u32, QuickLendXError> {
let admin = AdminStorage::get_admin(&env).ok_or(QuickLendXError::NotAdmin)?;
bid::BidStorage::set_max_active_bids_per_investor(&env, &admin, limit)
}
/// Reset the per-investor active-bid limit to the compile-time default (20).
///
/// Removes the stored override so [`get_bid_limit_config`] reports
/// `is_custom = false` and `is_disabled = false`. Useful for reverting a
/// previous [`set_max_active_bids_per_investor(0)`] call.
pub fn reset_investor_bid_limit(env: Env) -> Result<u32, QuickLendXError> {
let admin = AdminStorage::get_admin(&env).ok_or(QuickLendXError::NotAdmin)?;
bid::BidStorage::reset_max_active_bids_per_investor(&env, &admin)
}
/// Initiate emergency withdraw for stuck funds (admin only). Timelock applies before execute.
/// See docs/contracts/emergency-recovery.md. Last-resort only.
pub fn initiate_emergency_withdraw(
env: Env,
admin: Address,
token: Address,
amount: i128,
target_address: Address,
) -> Result<(), QuickLendXError> {
emergency::EmergencyWithdraw::initiate(&env, &admin, token, amount, target_address)
}
/// Execute emergency withdraw after timelock has elapsed (admin only).
/// Protected by payment reentrancy guard.
pub fn execute_emergency_withdraw(env: Env, admin: Address) -> Result<(), QuickLendXError> {
reentrancy::with_payment_guard(&env, || emergency::EmergencyWithdraw::execute(&env, &admin))
}
/// Get pending emergency withdrawal if any.
pub fn get_pending_emergency_withdraw(
env: Env,
) -> Option<emergency::PendingEmergencyWithdrawal> {
emergency::EmergencyWithdraw::get_pending(&env)
}
/// Check if the pending emergency withdrawal can be executed.
///
/// Returns true if the withdrawal exists, is not cancelled, timelock has elapsed,
/// has not expired, and does not exceed the same-token non-escrow surplus.
pub fn can_exec_emergency(env: Env) -> bool {
emergency::EmergencyWithdraw::can_execute(&env).unwrap_or(false)
}
/// Get time remaining until the emergency withdrawal can be executed.
///
/// Returns seconds until unlock (0 if already unlocked).
pub fn emg_time_until_unlock(env: Env) -> u64 {
emergency::EmergencyWithdraw::time_until_unlock(&env).unwrap_or(0)
}
/// Get time remaining until the emergency withdrawal expires.
///
/// Returns seconds until expiration (0 if already expired).
pub fn emg_time_until_expire(env: Env) -> u64 {
emergency::EmergencyWithdraw::time_until_expiration(&env).unwrap_or(0)
}
/// Add a token address to the currency whitelist (admin only).
pub fn add_currency(
env: Env,
admin: Address,
currency: Address,
) -> Result<(), QuickLendXError> {
pause::PauseControl::require_not_paused(&env)?;
currency::CurrencyWhitelist::add_currency(&env, &admin, ¤cy)
}
/// Remove a token address from the currency whitelist (admin only).
pub fn remove_currency(
env: Env,
admin: Address,
currency: Address,
) -> Result<(), QuickLendXError> {
pause::PauseControl::require_not_paused(&env)?;
currency::CurrencyWhitelist::remove_currency(&env, &admin, ¤cy)
}
/// Add multiple token addresses to the currency whitelist in one admin call.
///
/// Returns a per-item `Vec<bool>`: `true` = newly added, `false` = already present.
/// Empty input returns an empty result. Admin auth is required before any mutation.
pub fn add_currencies_batch(
env: Env,