Soroban smart contracts for LiquiFact, the invoice liquidity network on Stellar. This repository currently contains the escrow contract that holds investor funds for tokenized invoices until settlement.
- Rust 1.70+ (stable)
- Soroban CLI (optional for deployment)
For local development and CI, Rust is enough.
cargo build
cargo testCompatible without redeploy when you only:
- Add new
DataKeyvariants and/or new#[contracttype]structs stored under new keys. - Read new keys with
.get(...).unwrap_or(default)so missing keys behave as “unset” on old deployments.
Requires new deployment or explicit migration when you:
- Change layout or meaning of an existing stored type (e.g. new required field on
InvoiceEscrowwithout a migration that rewritesDataKey::Escrow). - Rename or change the XDR shape of an existing
DataKeyvariant used in production.
Compatibility test plan (short):
- Deploy version N; exercise
init,fund,settle. - Deploy version N+1 with only new optional keys; repeat flows; assert old instances still readable.
- If
InvoiceEscrowchanges, add a migration test or document mandatory redeploy.
migrate today validates from_version against stored DataKey::Version and errors if no path is implemented.
| Command | Description |
|---|---|
cargo build |
Build the workspace |
cargo test |
Run unit tests |
cargo fmt |
Format code |
cargo fmt -- --check |
Check formatting |
Who may deploy production: only addresses and keys owned by LiquiFact governance (multisig / custody). Treat contract admin and deployer secrets as highly sensitive.
| Variable | Purpose |
|---|---|
STELLAR_NETWORK |
e.g. TESTNET / PUBLIC / custom Horizon passphrase |
SOROBAN_RPC_URL |
Soroban RPC endpoint |
SOURCE_SECRET |
Funding / deployer Stellar secret key (S ...) |
LIQUIFACT_ADMIN_ADDRESS |
Initial admin intended to control holds and funding target |
Exact CLI flags change between Soroban releases; always cross-check Stellar Soroban docs for your installed stellar / soroban CLI version.
rustup target add wasm32v1-none
cargo build --target wasm32v1-none --release
# Lint the escrow crate (mirrors CI)
cargo clippy -p escrow -- -D warnings
# Lint the entire workspace
cargo clippy --all-targets -- -D warnings
# Artifact (typical):
# target/wasm32v1-none/release/liquifact_escrow.wasminit: Create an invoice escrow.get_escrow: Read the current escrow state.fund: Record funding, track each investor's principal contribution, and mark the escrow funded once the target is reached.settle: Mark a funded escrow as settled.get_investor_count: Return the number of distinct investors recorded for the escrow.get_investor_contribution: Return the principal amount recorded for one investor.max_investors: Return the supported investor cap for one escrow.
The escrow stores a per-investor contribution map inside the contract instance. That map is intentionally bounded.
- Supported investor cardinality:
128distinct investors per escrow - Product assumption: invoices that need more than
128backers should be split across multiple escrows or a higher-level allocation flow - Security goal: prevent denial-of-storage attacks that keep inserting new investor keys until a single contract-data entry becomes too large or too expensive to update
The regression tests in escrow/src/test.rs enforce these assumptions:
- The
129thdistinct investor is rejected. - Re-funding an existing investor at the cap is still allowed.
- At
128investors, the serialized investor map and escrow entry must stay below documented byte thresholds. - The final insertion at the cap must stay within a bounded write footprint.
These limits are designed to keep the contract well below Soroban's contract-data entry limits and to catch future schema changes that would bloat per-investor storage.
- Funding amounts must be positive.
- Distinct investor growth is capped per escrow.
- Funding totals and investor balances use checked addition to avoid overflow.
- Storage-growth tests act as regression guards against accidental state bloat.
Run these before opening a PR:
cargo fmt --all -- --check
cargo build
cargo testEscrow tests are organized by feature area under escrow/src/test/:
init.rscovers initialization, invoice-id validation, getters, and init-shaped baselinesfunding.rscovers funding, contribution accounting, snapshots, and tier selectionsettlement.rscovers settlement, withdrawal, investor claims, maturity boundaries, and dust sweepadmin.rscovers admin-governed state changes, legal hold, migration guards, and collateral metadataintegration.rscovers external token-wrapper assumptions and metadata-only integration checksproperties.rscontains proptest-based invariants
Shared helpers remain in escrow/src/test.rs. Each test creates its own fresh
Env and local setup so feature modules do not rely on hidden cross-test state.
Core design decisions are captured in docs/adr/:
| ADR | Decision |
|---|---|
| ADR-001 | Escrow state model (status 0–3, forward-only transitions) |
| ADR-002 | Authorization boundaries per role (admin, SME, investor, treasury) |
| ADR-003 | Two-phase settlement flow and funding-close snapshot |
| ADR-004 | Legal / compliance hold mechanism |
| ADR-005 | Optional tiered yield and per-investor commitment locks |
| ADR-006 | Treasury dust sweep and SEP-41 token safety wrapper |
See docs/ESCROW_TOKEN_INTEGRATION_CHECKLIST.md for the supported token assumptions, explicit unsupported token warnings, and the integration-layer responsibilities required when this escrow contract interacts with external token contracts.
- Auth: state-changing entrypoints use
require_auth()for the appropriate role (admin, SME, investor, treasury for dust sweep). - Legal hold: is governance-controlled; misuse risk is mitigated by using a multisig
adminand operational policy. - Collateral record: is not proof of encumbrance until a future version explicitly enforces token transfers.
- Token integration: external token transfers and token safety validation must live in the integration layer; this contract stores only numeric amount state and collateral metadata.
- Overflow:
funduseschecked_addonfunded_amount. - Dust sweep: gated on terminal escrow status, per-call cap ([
MAX_DUST_SWEEP_AMOUNT]), actual balance, legal hold, and treasury auth; only the configured SEP-41 token is transferred, with post-transfer balance equality checks inexternal_calls. Wrong-asset or oversized balances still require operational discipline — the hook is not a general-purpose withdrawal for live liabilities. - Tiered yield / claim locks: first-deposit discipline (
fundvsfund_with_commitment) prevents changing an investor’s tier after their initial leg; claim timestamps are ledger-based. - Funding snapshot: single-write immutability avoids shifting pro-rata denominators after close.
- Registry ref: stored for discoverability only; it must not be used as an authority without verifying behavior of the registry contract off-chain or in a dedicated integration.
DataKeykeepsClonebecause key wrappers are reused for storage get/set paths.InvoiceEscrowandSmeCollateralCommitmentintentionally do not deriveClone; this prevents accidental full-state duplication in hot paths.InvoiceEscrowandSmeCollateralCommitmentderivePartialEqfor deterministic state assertions in tests andDebugfor failure diagnostics.initpublishesEscrowInitializedfrom stored state instead of cloning the in-memory escrow snapshot, reducing avoidable copy overhead.
MIT