A minimal reinsurance-style vault on Stellar / Soroban. Liquidity providers deposit USDC and receive bvUSDC shares that track the vault's net asset value, redeemable for USDC on-chain. Built on the OpenZeppelin Soroban vault, with a React + Freighter dApp and a multisig-governed KYC allowlist.
Live demo (Stellar testnet): https://stellar-vault-demo-dapp-app-alpha.vercel.app
Note
Learning-grade proof of concept running on Stellar testnet. Shares stay 1:1 with the underlying (no yield accrual) and the owner cannot withdraw LP funds. It exists to explore Soroban vaults, native multisig governance, and a backend-free signature-collection flow — not for production use.
- ERC-4626-style vault — deposit USDC to mint
bvUSDC; withdraw or redeem back to USDC (1:1, no yield). The vault is the share token (implements SEP-41). - KYC allowlist —
deposit,mintandtransferrequire the counterparties to be allowlisted; exits (withdraw/redeem) stay open, so a de-listed holder can always leave. - Native multisig governance — the vault owner is a 2-of-3 Stellar account.
allow/disalloware authorized by quorum with no multisig code in the contract: it callsowner.require_auth()and the protocol resolves the account's signers and thresholds. - Backend-free signing — signatures are gathered by passing admin links (a
#tx=hash-fragment transport) between signers, then broadcast once the threshold is met. No server, no key custody. - Live dashboard — TVL, on-chain LP count, share price, your position, and gated Deposit / Withdraw. The
/adminconsole is visible only to governance signers.
flowchart LR
LP([Liquidity provider])
Gov([2-of-3 multisig owner])
subgraph chain[Stellar testnet]
Vault["Vault contract (bvUSDC shares)"]
end
LP -- deposit USDC --> Vault
Vault -- mint bvUSDC --> LP
Gov -- allow / disallow --> Vault
Vault -. "owner.require_auth()" .-> Gov
Entry is gated: a governance signer must allow an address before it can deposit. Authority is enforced on-chain — the /admin console only collects the quorum's signatures; the network verifies them against the owner account's thresholds.
- Contracts: Rust / Soroban (
soroban-sdk), OpenZeppelinstellar-tokens/stellar-access/stellar-macros. - Frontend: Vite + React + TypeScript, Stellar Wallets Kit / Freighter, generated contract clients (Scaffold Stellar), Vitest for the pure-logic modules.
- Network: Stellar testnet.
contracts/vault # the vault: deposit/withdraw/mint/redeem, KYC allowlist, LP counter
contracts/counter # throwaway learning contract
app/ # Vite + React frontend (dashboard + /admin signing console)
app/src/lib/ # pure logic: admin-link codec, threshold math, signature merge (unit-tested)
app-lib/ # scaffold runtime + generated TS contract clients
environments.toml # scaffold deploy config (network, accounts, contracts)
Important
Prerequisites: Rust with the wasm32v1-none target, Stellar CLI ≥ 25.2, and Node 20+.
stellar contract build # build contracts to WASM
cargo test -p vault # run the vault unit tests
cp app/.env.example app/.env # point the frontend at testnet
npm install && npm run dev # http://localhost:5173npm run dev runs stellar scaffold watch --build-clients (compile → deploy → regenerate TS clients) alongside Vite. To run the frontend against the already-deployed contracts without redeploying, use cd app && npx vite.
Warning
Build the contracts with stellar contract build, not cargo build. The OpenZeppelin crates enable an experimental soroban-sdk feature that only works through the CLI wrapper.
- Open the live demo (or your local instance) and connect Freighter, set to Testnet.
- Enable USDC — a one-time Stellar trustline so the account can hold USDC.
- Mint test USDC from Circle's faucet (select Stellar).
- Get allowlisted — deposits are KYC-gated. A governance signer must
allowyour address from the/adminconsole; until then the deposit form shows a KYC notice (withdraw stays open). - Deposit USDC to mint
bvUSDC, or withdraw to redeem.
Visible only when the connected wallet is a governance signer. Compose an allow / disallow op, then collect signatures across the 2-of-3:
- Compose (op + target) — you land in the sign view holding the first signature.
- Sign, then share the generated link with the next signer; they open it and sign too.
- Once the medium threshold is met, Submit — it only broadcasts the already-signed transaction; the wallet is not prompted again.
Caution
These are throwaway testnet keys, published on purpose so anyone can try the multisig flow. They hold no value and control nothing real. Never reuse them on mainnet or send anything of value to them.
The owner is a 2-of-3 multisig. Import these two governance signers into Freighter (Testnet) — together they meet the 2-of-3 threshold:
| Signer | Secret seed |
|---|---|
gov (owner + signer 1) |
SDAMOWWJSMUXRWJHHFZOPL4USLPCEBJO3SCQ6L3PTF4INVMQUPZPH3L2 |
gov2 (signer 2) |
SDZBPTPPCQUL3KHM7EMMPSOO2O4FKQOVDU5G34524KBOUU7FJ6G2FIC3 |
Then run the full round-trip solo:
- Fund your own wallet with friendbot and get test USDC (steps above).
- In
/admin, connected as gov, composeallow(<your address>)and sign; share the link, switch to gov2, sign, then submit. - Switch back to your wallet — now allowlisted — and deposit USDC to mint
bvUSDC.
| Component | Address |
|---|---|
| Vault (owner = gov multisig) | CAHC7K5NWSN3VFKBWU7VJT7H3E4VF55ODD6ZMRB32MCII64G3YAMI5QA |
| Gov multisig (2-of-3, compliance admin) | GDVL4VKURSZ7R66IWAORMUNHYHHDQ5Y65TMPWIGV6WDVKTRZZLQBYNXQ |
| USDC issuer (Circle testnet) | GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5 |
| USDC SAC (vault's underlying) | CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA |
Tip
USDC is a classic asset (needs a trustline to hold); bvUSDC is a Soroban token (no trustline, and not shown in Horizon — read balances from the contract). A deposit is a single transaction with nested authorization, so there is no separate approve step.
See AGENTS.md for build/run details and the Stellar gotchas learned the hard way.