This document describes the on-chain data model and storage schema for the QuickLendX invoice factoring protocol. The schema is designed for the MVP flow: invoice upload → bids → accept → settlement.
- ID:
BytesN<32>- Unique identifier - Business:
Address- Business that uploaded the invoice - Amount:
i128- Total invoice amount - Currency:
Address- Currency token address - Due Date:
u64- Due date timestamp - Status:
InvoiceStatus- Current lifecycle status - Metadata:
InvoiceMetadata- Customer info, line items, etc. - Payments:
Vec<PaymentRecord>- Payment history - Ratings:
Vec<InvoiceRating>- Investor feedback
- ID:
BytesN<32>- Unique bid identifier - Invoice ID:
BytesN<32>- Invoice being bid on - Investor:
Address- Investor making the bid - Amount:
i128- Bid amount - Expected Return:
i128- Expected return amount - Status:
BidStatus- Current bid status - Expiration:
u64- Bid expiration timestamp
- ID:
BytesN<32>- Unique investment identifier - Invoice ID:
BytesN<32>- Invoice being invested in - Investor:
Address- Investor address - Amount:
i128- Investment amount - Status:
InvestmentStatus- Current investment status - Insurance:
Vec<InsuranceCoverage>- Insurance coverages
Pending- Awaiting verificationVerified- Available for biddingFunded- Has been fundedPaid- Settled successfullyDefaulted- Payment overdueCancelled- Cancelled by businessRefunded- Escrow funds returned to investor
Placed- Active bidWithdrawn- Withdrawn by investorAccepted- Accepted by businessExpired- Expired without acceptanceCancelled- Cancelled due to refund or withdrawal
Active- Currently funding invoiceWithdrawn- Withdrawn by investorCompleted- Invoice paid successfullyDefaulted- Invoice defaultedRefunded- Investment refunded to investor
Primary entity keys use the DataKey enum to namespace storage:
DataKey::Invoice(invoice_id)→InvoiceDataKey::Bid(bid_id)→BidDataKey::Investment(investment_id)→Investment
The Soroban host serializes the enum discriminant with the payload, guaranteeing that
Invoice(x), Bid(x), and Investment(x) never collide regardless of ID values.
fees→PlatformFeeConfig
inv_count→u64- Invoice counterbid_count→u64- Bid counterinvst_count→u64- Investment counter
inv_bus + business_address→Vec<BytesN<32>>- Invoices by businessinvst_stat + status→Vec<BytesN<32>>- Invoices by status
bids_inv + invoice_id→Vec<BytesN<32>>- Bids by invoicebids_invr + investor→Vec<BytesN<32>>- Bids by investorbids_stat + status→Vec<BytesN<32>>- Bids by status
invst_inv + invoice_id→Vec<BytesN<32>>- Investments by invoiceinvst_invstr + investor→Vec<BytesN<32>>- Investments by investorinvst_stat + status→Vec<BytesN<32>>- Investments by status
- All keys use unique symbols to prevent collisions
- Primary keys use entity IDs (BytesN<32>) for uniqueness
- Index keys combine symbols with entity-specific data
- Critical: Both persistent and instance storage entries must have their TTL extended to prevent archival
- Without TTL extension, funded invoices, bids, investments, and escrow records could be archived mid-lifecycle, causing permanent fund loss
- TTL threshold is
PERSISTENT_TTL_THRESHOLD(34,732,800 seconds ~402 days) - TTL threshold covers: max_due_date_days (365 days) + grace_period_seconds (7 days) + 30-day safety margin
- TTL extension is applied on every read/write of long-lived keys in all storage modules:
- InvoiceStorage (persistent storage): store, get, update, and all index operations (business, status, customer, tax_id, tag, category)
- BidStorage (persistent storage): store_bid, get_bid, update_bid, index writes/reads
- InvestmentStorage (persistent storage): store_investment, get_investment, update_investment, investor/active indexes
- EscrowStorage (persistent storage): store_escrow, get_escrow, update_escrow, get_escrow_by_invoice
- Storage keys are designed to be backward compatible
- New fields can be added to structs without breaking existing data
- Index keys use stable symbols that won't change
- Only authorized addresses can modify data
- Business can only modify their own invoices
- Investors can only modify their own bids/investments
- All monetary amounts use
i128to prevent overflow - Timestamps use
u64for Unix timestamps - Addresses use Soroban's
Addresstype for built-in validation
- Primary entity lookup: O(1)
- Index queries: O(n) where n is number of entities in index
- Status-based queries: Efficient for filtering active entities
- Entity updates: O(1) for primary storage
- Index updates: O(n) for index maintenance
- Batch operations: Optimized for common workflows
- Persistent storage used for long-term data
- Instance storage for frequently accessed config
- Indexes increase storage costs but improve query performance
- Invoice Upload: Store invoice, update business and status indexes
- Bid Placement: Store bid, update invoice, investor, and status indexes
- Bid Acceptance: Update bid status, create investment, update indexes
- Settlement: Update invoice and investment statuses, record payments
The schema is designed to support future features:
- Dispute resolution (already included in Invoice struct)
- Insurance claims (included in Investment struct)
- Analytics and reporting (separate analytics storage)
- Multi-currency support (currency field in Invoice)
- Partial payments (payments vector in Invoice)
The protocol includes comprehensive invariant tests to verify storage consistency:
cargo test --lib test_invariants-
Status Index Coherence
- Invoice status indexes match primary records
- Bid status indexes match primary records
- Investment status indexes match primary records
-
Index Update Consistency
- Status changes properly update indexes
- No orphaned records in indexes
-
Cross-Module Consistency
- Funded invoices have associated investors
- Accepted bids correspond to investments
- Counters increment correctly
-
Full Lifecycle Tests
- End-to-end workflow validation
- Multi-entity stress testing with status bucket verification
- Sum of all status bucket counts equals total entity count
- All entities in indexes exist in primary storage
- No duplicate entries in indexes
- Counters are monotonically increasing