Soroban smart contracts for the InvoFi decentralised invoice financing protocol, built with Rust + Soroban SDK 22 on Stellar.
InvoFi is split across two repositories so the fast-moving app layer and the slow-moving, audit-bound contract layer stay decoupled:
| Repo | Contains | Why separate |
|---|---|---|
| invofi | Next.js frontend (apps/frontend), SDK, docs, scripts |
App-layer changes constantly, Node/npm CI, no audit dependency |
| invofi-contracts (this repo) | All Soroban Rust contracts — registry, financing, repayment, insurance, reputation, common | Stable, auditable, slow-moving history; Rust-only CI; the repo that goes through the SCF Audit Bank |
Smart-contract contributions happen here; frontend and SDK contributions happen in invofi. The frontend supports Freighter and LOBSTR wallets via an approved-wallet allowlist (approved-wallets.ts, Task 6A) — approving a third wallet is a one-line config change.
InvoFi's protocol state is spread across five auditable Soroban contracts (plus the SEP-41 position token), each with a narrow job:
| Contract | Crate | Responsibility |
|---|---|---|
| registry | registry/ |
Invoice lifecycle — register, cancel, status transitions, blacklist, disputes |
| financing | financing/ |
Offers — create, withdraw, accept (moves principal and mints the position token), reject |
| repayment | repayment/ |
Repayments (full/partial), mark overdue, reclaim/default |
| insurance | insurance/ |
Coverage reserve — stake/unstake, and payout on default capped at pool balance (Task 10) |
| reputation | reputation/ |
Originator credit history — repayment outcomes → public score (Task 11) |
Cross-contract calls are restricted: the registry only accepts status transitions from the registered financing/repayment contracts (implicit contract-invoker auth, per Stellar's Authorization docs), and financing only accepts repayment callbacks from the registered repayment contract. User auth never propagates across contract boundaries.
register_invoice() → create_offer() → accept_offer()
↓ ↓
[Pending] funds to business + POS minted to lender (Task 7)
↓ ↓
reject_offer() [Financed]
stays Pending ↓
repay_invoice() (partial or full)
↓
[Repaid] ← balance cleared
[Overdue] ← mark_overdue()
↓
reclaim_invoice() (after 7-day grace)
offer → [Defaulted]
| Function | Auth | Description |
|---|---|---|
__constructor(admin) |
Deployer (at deploy) | Sets admin atomically in the deploy operation — no initialize() to front-run (ADR-0005) |
register_invoice(id, originator, amount, currency, due_date) |
Originator | Register invoice; rejects dust (< 10 XLM) and past-due dates |
get_invoice(id) |
Anyone | Read invoice state |
cancel_invoice(id, originator) |
Originator | Cancel a Pending invoice |
set_financing_contract(admin, addr) / set_repayment_contract(admin, addr) |
Admin | Authorize the only cross-contract callers |
financing_marks_invoice_financed(id) |
financing | System transition: Pending → Financed |
repayment_marks_invoice_repaid(id, fully_repaid) |
repayment | System transition: Financed → Financed/Repaid |
mark_invoice_overdue(id) |
Anyone | Overdue once due_date passes |
raise_dispute / resolve_dispute |
Originator / Admin | Dispute lifecycle |
blacklist_address / unblacklist_address / is_blacklisted |
Admin | Address blocking |
set_rate / set_fee / transfer_admin / pause / unpause |
Admin | Admin controls |
| Function | Auth | Description |
|---|---|---|
__constructor(admin, registry, token) |
Deployer (at deploy) | Wire to registry + default settlement token (ADR-0005) |
create_offer(offer_id, invoice_id, lender, amount, currency, rate, duration) |
Lender | Submit an offer (validates amount/rate/duration bounds) |
withdraw_offer / reject_offer |
Lender / Originator | Withdraw or reject a Pending offer |
accept_offer(offer_id, originator) |
Originator | Pulls principal lender → business and mints the lender's position token (Task 7) |
register_currency(admin, currency, token) |
Admin | Add a settlement currency — one registry entry, no code branch per currency |
set_position_token(admin, token) |
Admin | Configure the SEP-41 position-token contract (ADR-0002) |
get_position_token() |
Anyone | Read the configured position token |
update_offer_status / update_offer_amount_repaid / update_lender_stats_repaid / update_stats_repaid |
repayment | Repayment callbacks (registered caller only) |
pause / unpause / transfer_admin |
Admin | Admin controls |
| Function | Auth | Description |
|---|---|---|
__constructor(admin, registry, financing, token) |
Deployer (at deploy) | Wire to registry + financing (ADR-0005) |
repay_invoice(invoice_id, offer_id, repayer, amount) |
Originator | Full or partial repayment (principal + yield) |
mark_overdue(invoice_id) |
Anyone | Flag a past-due Financed invoice |
reclaim_invoice(invoice_id, offer_id, lender) |
Lender | After the 7-day grace period → offer Defaulted |
calculate_total_due(offer_id) |
Anyone | Principal + accrued yield |
| Function | Auth | Description |
|---|---|---|
__constructor(admin, token) |
Deployer (at deploy) | Set admin + staking token (ADR-0005) |
stake(staker, amount) |
Staker | Deposit the staking token into the pool (approve + pull pattern) |
unstake(staker, amount) |
Staker | Withdraw; the pool pays back directly |
get_stake(staker) |
Anyone | Staker's balance |
get_pool_total() |
Anyone | Accounting total of staked funds |
get_stakers_count() |
Anyone | Number of active stakers |
get_contract_token_balance() |
Anyone | Actual on-chain balance — audit check that accounting matches |
pay_out(beneficiary, amount) |
Payout caller only (repayment) | Pay up to amount, capped at pool balance; returns amount actually paid (Task 10) |
get_payout_caller() |
Anyone | Read the configured payout caller |
set_staking_token / pause / unpause / transfer_admin |
Admin | Admin controls |
Yield-rate calculation remains intentionally out of scope — the pool is flat accounting with payout-on-default wired through
pay_out(Task 10). See ADR-0003 for the payout design.
On accept_offer, the financing contract mints a SEP-41 position token to the lender, 1:1
with the offer amount (one token per base unit of principal — see ADR-0002).
The token is a Stellar Asset Contract (POS, issued by the protocol deployer) whose admin is
the financing contract, so minting is authorized via implicit contract-invoker auth.
Position tokens are plain SEP-41 assets: any wallet can hold and transfer them (Task 8), and
they represent the lender's claim on the financed invoice until it is repaid. Because they are
Stellar assets, a holder must establish a POS trustline before mint/transfer can credit them —
the frontend's portfolio offers a one-click trustline helper.
| Function | Auth | Description |
|---|---|---|
__constructor(admin) |
Deployer (at deploy) | Sets admin atomically in the deploy operation (ADR-0005) |
set_recorder(admin, recorder) |
Admin | Set the repayment contract as the only writer |
record_outcome(originator, outcome) |
Recorder only | 0 = repaid, 1 = defaulted; updates outcome counts |
get_score(originator) |
Anyone | repayments − 2×defaults, floored at 0 (ADR-0004) |
get_record(originator) |
Anyone | Raw {repayments, defaults} counts — the source of truth |
Every state-mutating function publishes a Soroban contract event. Topics are
(event_name, subject_id) — indexers can filter by invoice or offer id without decoding payloads.
| Event | Emitted by | Data payload |
|---|---|---|
inv_reg |
register_invoice |
(originator, amount, due_date) |
inv_amt |
update_invoice_amount |
new_amount |
inv_sts |
status transitions (update / finance / repay) | InvoiceStatus |
off_new |
create_offer |
(invoice_id, lender, amount, interest_rate) |
off_acc |
accept_offer |
(invoice_id, lender, amount) |
off_rej |
reject_offer |
invoice_id |
off_wdr |
withdraw_offer |
lender |
off_def |
reclaim_invoice |
(invoice_id, lender) |
inv_rep |
repay_invoice |
(offer_id, amount, fully_repaid) |
inv_ovd |
mark_overdue |
due_date |
inv_cxl |
cancel_invoice |
originator |
inv_dsp |
raise_dispute |
originator |
inv_rsl |
resolve_dispute |
new_status |
pos_mint |
accept_offer (financing) |
(lender, amount) — position token minted |
pool_stk |
stake (insurance) |
amount |
pool_un |
unstake (insurance) |
amount |
pool_pay |
pay_out (insurance, Task 10) |
amount paid |
reputn |
record_outcome (reputation, Task 11) |
outcome |
| Constant | Value | Description |
|---|---|---|
GRACE_PERIOD_SECS |
604,800 | 7-day grace period before lender can reclaim |
MIN_OFFER_DURATION_SECS |
86,400 | Minimum offer duration (1 day) |
MAX_OFFER_DURATION_SECS |
31,536,000 | Maximum offer duration (1 year) |
MIN_INVOICE_AMOUNT |
10,000,000 | Minimum invoice amount in stroops (10 XLM / 10 USDC) |
# Build
cargo build --target wasm32v1-none --release
# Run tests (110 tests across registry / financing / repayment / insurance / reputation)
cargo test
# Check WASM size stays under 256 KB
bash scripts/check-size.sh
# Deploy to Testnet (requires stellar-cli)
bash scripts/deploy.shOr trigger the Deploy Contract GitHub Actions workflow for a one-click Testnet deploy.
- Five auditable contract crates with restricted cross-contract auth (Tasks 4–5)
- SEP-41 token movement —
accept_offer(lender → business),repay_invoice(principal + yield) - Position tokens (Task 7), transferable positions (Task 8), insurance stake/unstake (Task 9)
- Insurance payout on default (Task 10), reputation scoring (Task 11)
- Emergency pause / circuit breaker (Task 4A), full protocol event coverage (Task 13)
- Deployer-bound
__constructorinitialization (issue #75), CI: tests + clippy + Soroban Scout
- Mainnet deployment
- Independent security audit (SCF Audit Bank)
- Overdue-penalty interest
- Multisig admin governance
- Contract upgradeability with timelock
See CHANGELOG.md for version history.
See the compliance & regulatory posture in the main repo — KYC/SEP-12 roadmap, jurisdictions avoided at launch, and the securities-by-design analysis.
- @samjay8 — project maintainer and protocol owner
Thanks to everyone who has contributed to InvoFi — the list below is generated automatically from the GitHub API whenever code lands on master. No action needed on your part after a merged PR.
|
Samuel Ojetunde |
hexlaapp |
See CONTRIBUTING.md for build, test, and PR guidelines.
MIT © 2026 InvoFi Contributors