Updated: 2025-07-25 Applies to: All four Soroban smart contracts (registry, invoice, escrow, pool)
This document describes the trust assumptions, authentication gates, and attack surfaces of the TrusTrove protocol. It is intended for security researchers, integrators, and contributors evaluating the protocol's risk profile.
| Role | Description | Trust Level |
|---|---|---|
| Admin | Deployer address that calls initialize() on each contract. Controls profile management, default triggers, and configuration. |
High — single point of failure |
| Issuer | A verified SME that creates invoices and requests funding. | Low — only self-authenticates |
| Buyer | A verified counterparty obligated to repay an invoice at maturity. | Low — only self-authenticates |
| LP | Liquidity provider that deposits USDC into a pool. | Low — only self-authenticates |
| Anonymous caller | Any Stellar account that can call permissionless functions. | None — no auth required |
┌─────────────────────────────────────────────────────┐
│ Contract Layer │
│ ┌──────────┐ ┌───────────┐ ┌─────────┐ ┌────┐ │
│ │ Registry │ │ Invoice │ │ Escrow │ │Pool│ │
│ └────┬─────┘ └─────┬─────┘ └────┬────┘ └─┬──┘ │
│ │ │ │ │ │
│ └──────────────┴──────────────┴──────────┘ │
│ Trust boundary │
│ (contracts trust each other's registered addr) │
└───────────────────────────────────────────────────────┘
▲ ▲
│ │
Verified via registry Self-authenticated
(issuer / buyer) (LP deposits / withdrawals)
Key trust assumption: All contracts trust the addresses stored during
initialize() — if any stored reference is wrong, the caller loses funds
irrevocably.
Every state-changing function enforces one of these auth patterns:
The caller authenticates only themselves. No admin or third party is involved.
| Function | Contract | Who Auths |
|---|---|---|
register_issuer |
Registry | The issuer address |
register_buyer |
Registry | The buyer address |
update_metadata |
Registry | The profile address |
create |
Invoice | The issuer |
list_for_financing |
Invoice | The issuer |
mark_shipped |
Invoice | The issuer |
confirm_delivery |
Invoice | The confirmer (issuer or buyer) |
repay |
Invoice | The buyer |
check_auth |
Invoice | The supplied address |
deposit |
Pool | The LP |
withdraw |
Pool | The LP |
Only the stored admin address may call this function.
| Function | Contract | Why |
|---|---|---|
revoke |
Registry | Prevents unauthorised removal of profiles |
verify_profile |
Registry | Controls verification status |
batch_register_issuers |
Registry | Batch operations gated by admin |
set_pool_contract |
Invoice | Wiring a new pool changes fund flows |
trigger_default |
Invoice | Declaring a default is a state change with fund consequences |
set_expiry_window |
Invoice | Configuration parameter |
expire_listing |
Invoice | Admin fallback path |
fund_invoice |
Pool | (currently) Capital allocation from pool |
set_max_utilization |
Pool | Risk parameter |
transfer_ownership |
All | Changes the admin key |
One contract authenticates as the caller of another contract's function.
| Function | Contract | Authenticated Caller | Why |
|---|---|---|---|
mark_funded |
Invoice | The pool contract | Prevents faking funding records |
receive_repayment |
Pool | The invoice contract | Prevents faking repayment events |
receive_repayment_with_refund |
Pool | The invoice contract | Same as above |
handle_default |
Pool | The invoice contract | Prevents premature default recovery |
lock |
Escrow | The pool contract | Only the pool can lock funds |
release_to_issuer |
Escrow | The pool contract | Only the pool can release to issuer |
release_to_pool |
Escrow | The pool contract | Only the pool can reclaim escrowed funds |
Two distinct parties must both sign.
| Function | Contract | Signers |
|---|---|---|
initialize |
All | The incoming admin (prevents assigning an uncontrolled address) |
transfer_ownership |
All | Current admin + new admin (prevents accidental transfers) |
Any account can call these.
| Function | Contract | Notes |
|---|---|---|
get_profile, is_verified, get_verification_status, get_admin |
Registry | Read-only views |
get, get_status, get_face_value, get_discount_bps, get_funding_asset, get_issuer, get_by_status, get_by_issuer, get_by_buyer, get_counts, get_expiry_window |
Invoice | Read-only views |
get_locked, get_history |
Escrow | Read-only views |
get_stats, get_lp_position, get_utilization_rate, get_usdc_asset |
Pool | Read-only views |
expire_listing |
Invoice | Also callable by issuer (Pattern A) |
Risk: The admin key controls profile revocations, defaults, pool funding, and ownership transfer. Compromise of this key has a high blast radius.
Mitigation:
transfer_ownershiprequires dual auth (current + new admin) — a stolen key cannot transfer ownership to the attacker without the new key also signing.- The escrow
handle_defaulthas an alternative caller (pool contract), so admin compromise does not grant unilateral fund recovery. - Planned: Multi-sig admin (see Roadmap section in README).
Risk: A malicious contract deployed at the same address as a legitimate
contract could fake require_auth() calls.
Mitigation:
- Contract addresses are hardcoded during
initialize()and cannot be changed without admin auth. - The invoice contract checks the pool address on every
mark_fundedcall, and the pool checks the invoice address onreceive_repaymentandhandle_default.
Risk: After the pool accrues yield, the share price rises above 1.0. A tiny deposit can round down to 0 shares while the USDC is still transferred into the pool, effectively donating the depositor's funds to existing LPs.
Mitigation: The deposit() function rejects any deposit that would mint
0 shares with PoolError::MinimumDeposit (#14). The check runs before the
token transfer, so no funds leave the depositor on the rejection path.
Risk: An attacker could try to call state-transition functions multiple times on the same invoice (e.g., double-funding).
Mitigation: Every state transition in the invoice contract checks the
current status before proceeding. list_for_financing only works on Created
invoices; mark_funded only on Listed; mark_shipped only on Funded;
confirm_delivery only on Active; repay only on Confirmed; and
trigger_default only on Funded/Active/Confirmed. Once the status
advances, the previous function cannot be called again.
Additionally, escrow's lock() function rejects re-locking an already-locked
invoice with EscrowError::AlreadyLocked.
Risk: Arithmetic errors in amount calculations could lead to incorrect fund allocations.
Mitigation:
- All contracts use Rust
u128for amounts. The release profile enablesoverflow-checks = true(see Cargo.toml). receive_repaymentvalidatesamount >= funded_amount.receive_repayment_with_refundvalidatesrefund <= amount - funded_amount.withdrawvalidatesshares <= lp_sharesandusdc_to_return <= available.handle_defaultin the pool usessaturating_subfor deposit reduction.- The invoice's
repayfunctions usesaturating_subfor discount calculations.
Risk: An attacker could front-run an expire_listing call to fund an
invoice that would otherwise expire.
Mitigation: expire_listing only succeeds when `current_time > listed_at
- expiry_window
. If the expiry window has passed, anyone can callexpire_listing, but a funder could still callfund_invoicein the same ledger (beforeexpire_listing`). This is an accepted race condition given the current permissionless funding model.
Risk: An admin could set max_utilization_bps above 10000 (100%) to
allow over-leveraging the pool.
Mitigation: set_max_utilization validates new_cap_bps <= 10000 and
rejects values above 10000.
Risk: An attacker could submit a very large batch of issuers to exhaust gas or storage.
Mitigation: batch_register_issuers limits entries to ≤ 50 and
panics with RegistryError::BatchSizeExceeded otherwise.
| Risk | Current State | Planned Mitigation |
|---|---|---|
| Single admin key | Admin is a single Stellar address | Multi-sig (3-of-5) before mainnet |
Admin-gated fund_invoice |
Pool funding requires admin auth | LP-governed allocation (see README) |
| No emergency pause | No circuit breaker exists | admin_pause() / admin_unpause() before mainnet |
| Centralised registry | Admin controls all registrations/revocations | On-chain governance (longer-term) |
| Single oracle for verification | Registry is the sole source of truth for identity | Multi-sig registry admin |
If the admin key is compromised or malicious:
| What They Can Do | What They Cannot Do |
|---|---|
| Revoke any profile | Transfer admin to a non-consenting address (dual auth) |
| Register fraudulent profiles | Directly withdraw LP funds |
| Trigger false defaults | Bypass the utilization cap (>10000 bps) |
| Set a new pool contract | Mint unbacked LP shares |
| Change pool utilization cap (≤10000 bps) | Release escrowed funds to themselves (only pool contract can) |
| Expire listings |
- Registry has no access to any funds.
- Escrow holds USDC but cannot release it without the pool contract.
- Pool manages accounting but does not hold USDC directly (it holds a balance from deposits, but escrow holds funded amounts).
- Invoice is a pure state machine with no fund custody.
Every persistent().set() is immediately followed by extend_ttl() with
live_until_ledger = 100 and seq = 2_000_000. Instance storage is also
extended on every state-changing call via extend_instance_ttl().
See STORAGE.md for the full storage key schema and TTL parameters.
The contracts depend only on soroban-sdk and each other. There are no
oracle integrations, bridge contracts, or off-chain relayers whose compromise
could affect on-chain state.
The following are explicitly outside this threat model:
- Stellar protocol-level attacks (consensus, validator compromise, etc.) — report to Stellar Development Foundation.
- Freighter / wallet vulnerabilities — report to the respective wallet team.
- Frontend phishing or social engineering — users are responsible for verifying transaction details before signing.
- Off-chain indexer downtime — the contracts function independently of the indexer; missed events do not affect fund safety.
- Testnet-only risks (Friendbot dust, ephemeral validators) — see LIMITATIONS.md.
Report vulnerabilities to security@trusttrove.xyz or via the Security Advisory tab. See SECURITY.md for our disclosure policy and response timeline.