Skip to content

About

Compliance-gated RWA vault for Stellar/Soroban. OpenZeppelin-based, white-label: configure roles, allowlist and token at deploy.

Resources

Security policy

Stars

1 star

Watchers

0 watching

Forks

Latest commit

 

History

1,049 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

strata-vault-kit

White-label tokenized vault on Stellar for assets whose value is attested rather than traded. Shares are a claim on an off-chain asset, so no price exists at the moment you act: entry and exit are requests. What you put in goes into escrow, the next accepted attestation prices it, and you claim the result.

Each operator deploys, configures and brands an independent instance.

Two rules the contracts enforce:

  1. Only approved addresses can enter — an allowlist maintained post-KYC.
  2. A priced claim always pays — neither a pause, a delisting nor a stale valuation can block an already-priced, funded cash claim. A compliance freeze holds it until the freeze is lifted.

A request can be cancelled until the vault can read its price. Once an epoch is sealed, a valid report accepted after the seal closes cancellation, even while the notice still runs; the epoch is priced once the notice ends. If that report expires, or the NAV or the vault is paused, cancellation opens again. New deposits are refused while the vault is paused, while the valuation is stale or the NAV is paused, while an issuer clawback leaves the vault holding less than its pending deposits, and once a wind-down is proposed.

Documentation

  • Product and architecture — what the protocol does, why it is asynchronous, and how the pieces fit together.
  • Kit specification — invariants, deploy-time parameters, and complete role matrix.
  • Testing — which tests cover storage and TTL, auth, ledger time, events and calls between contracts.
  • Threat model — what can go wrong with the oracle, the roles and the redemption paths, what stops it, what is left, and the test for each control, with each threat placed under STRIDE.
  • STRIDE threat model — the threat model in the format of Stellar's STRIDE template: a data flow diagram, numbered flows, issues for each category and their remediations.
  • Security scan — what the self-service security tools found in the contracts, how each finding was triaged, and the plan for what stays open.
  • Monitoring plan — what an operator watches on chain, where each signal comes from, and what to do when it fires.
  • Grant differences — where the built kit differs from the SCF grant proposal, what is not built yet, and the limits an adopter should know.

Status

Pre-release, testnet only. Nothing here is audited or production-ready.

Deployed addresses

Testnet only. The deploy script rewrites the table below on every testnet deploy from environments.toml → [staging.contracts], which is the record.

Contract Address
async_vault CDJHQ2R5XUJGP6WUKCW4VBEVGWFAZ5TZLEONBITKITNXFDLWNNK7WVYO
nav_oracle CCZ4X25GLMYRQ33PM4ZYDMGKAJJKPUG2ABUZ4NIGUSA7W4RUTBOEEDYI
share_token CDOZT5VNIKSLQSFELF4PHVZRZA3GCJYJEHVDZTUB7O2QMBHBKKOYORQI
identity_verifier CCGHNGPI66WGVSLLALQVCIRIOZKX73QW23UDZLZ3LMXLGEG7SLVUOULD
compliance CCQI5NFRWBOO77LYSFY54D6PSU4TVWD5A3CFX2A5AWLEURVR35XISMKG
asset CCOHKZLQ5EAD2A3RLHYHH7W4K2TNLNELXUVLWZIZALH45WXHH5H3QV7E

Testnet flow verification

One testnet transaction per investor-facing flow, executed through the live dApp against the contract IDs above.

Flow Entrypoint Transaction
Connect wallet — n/a — no on-chain transaction
Deposit (subscribe) request_deposit 81e9c1a9…e590
Claim shares claim_deposit fe59e77e…e029
Redeem (request) request_redeem d6c02ee0…7524
Withdraw (claim cash) claim_redeem 27a81778…db64

Redemption queue

The notice-period queue on testnet, against the contract IDs above, run by the harness scenario redemption-queue.test.ts on 2026-11-02. Repeat it on a seeded deployment:

HARNESS_NETWORK=testnet npm run seed                            # once: holders hold shares
HARNESS_NETWORK=testnet npm run test:queue -w scripts/harness   # prints `tx <label> <hash>`

The treasury deploys the free reserve first, so the redemption that follows is larger than the cash left in the vault. All steps are in epoch 3.

Step Account Entrypoint Transaction
Deploy the free reserve to the custodian (5,499 USDC) treasury deploy_to_custodian 93f5fad4…3c59
Request a redemption (10 shares) holder-2 request_redeem 3c507d84…ee38
Cancel it before the epoch closes holder-2 cancel_redeem bd35fdcd…15c6
Request again (1,001 shares) holder-2 request_redeem 0b72ab37…4681
Seal the epoch manager close_epoch d1f52849…7ec3
Attest the NAV attester attest 14daff3d…3543
Price the epoch any account fulfill_epoch 2f3b4564…9902
Return the capital custodian fund 074cc4be…24fd
Claim the cash (1,001 USDC) holder-2 claim_redeem 8d169333…0eee

Once the epoch is priced the vault owes 1,001 USDC more against 1,000 USDC of liquid reserve, so uncovered() is above zero. The claim at that point is refused with ClaimNotCovered. It fails at simulation and leaves no transaction, so the evidence of the short reserve is the pricing transaction before it and the fund that clears it.

Vault figures

The live dApp shows Withdrawable and Committed in the strip at the top of the vault page, and Economic supply under "About this vault". Each one reads a view on async_vault.

Figure What it is View
Withdrawable The cash the vault holds now, less deposits that can still be cancelled. Priced redemptions are paid from it. liquid_reserve()
Committed The cash owed to priced redemptions that are not claimed yet. When it is more than Withdrawable, the gap shows as Uncovered. committed()
Economic supply The shares held outside the vault, plus the shares priced for deposits and not claimed yet. total_economic_supply()

Oracle tests

These tests show the oracle's price controls and how it keeps its storage alive (TTL). Run them with cargo test -p nav-oracle.

What the tests show File Tests
Band: a price below the minimum or above the maximum answer is refused contracts/nav-oracle/src/test/oracle.rs, contracts/nav-oracle/src/test/guards.rs attest_rejects_out_of_band, nav_in_band_refuses_a_value_outside_the_band
Deviation cap: one report cannot move the price past the upward or downward cap contracts/nav-oracle/src/test/oracle.rs, contracts/nav-oracle/src/test/guards.rs attest_enforces_cooldown_and_deviation, a_configured_downside_still_bounds_a_fall, a_full_downside_cap_still_bounds_a_rise, within_deviation_refuses_a_move_past_either_cap
Cooldown: a report inside the cooldown after the last one is refused contracts/nav-oracle/src/test/oracle.rs, contracts/nav-oracle/src/test/guards.rs attest_enforces_cooldown_and_deviation, two_reports_in_one_ledger_are_refused_at_the_shortest_cooldown, cooldown_elapsed_refuses_a_report_inside_the_cooldown
Freshness: a report never outlives the freshness window, a report that is already expired is refused, and once a price expires the oracle reports it stale and the vault refuses to price an epoch with it contracts/nav-oracle/src/test/oracle.rs, contracts/nav-oracle/src/test/guards.rs, contracts/async-vault/src/test/oracle_pricing.rs heartbeat_expiry_makes_it_stale, a_reporter_expiry_past_the_window_is_cut_to_the_window, expires_in_future_refuses_a_report_born_stale, fulfill_is_rejected_once_the_feed_goes_stale
TTL: the latest valuation keeps a full TTL while it is read contracts/nav-oracle/src/test/oracle.rs the_latest_valuation_keeps_a_full_ttl_while_it_is_read
TTL: setup, grant, hand-over and change calls extend the instance, and every role grant writes the role with its full TTL contracts/nav-oracle/src/test/governance.rs setup_grants_hand_overs_and_changes_restore_the_instance_ttl, the_constructor_writes_each_role_with_the_full_ttl, a_grant_during_setup_writes_the_role_with_the_full_ttl, a_grant_through_the_timelock_writes_the_role_with_the_full_ttl

The vault's test runs with cargo test -p async-vault fulfill_is_rejected_once_the_feed_goes_stale. Testing lists the TTL tests for every contract.

Admin panel

The operations console is at strata-vault-kit-app.vercel.app/admin. It reads the contracts the console and the transactions below ran against, not the set under Deployed addresses:

Contract Address
async_vault CD5LYYXGOJ7R23P6D7YUN6DUJQYMI23KEUEOIFKXOSWOX3KHKTXRITED
nav_oracle CBKVMUMIX7MXYHX5K34N5HYZOP6S2VGWF7MN27RIWBV3A3WD3X3L63CO
share_token CDOISQTQRCYOTH6S3DFQ6UKJ2TEXLNO7NFIGOZS4RZL63IRCVXXCGP5N
identity_verifier CCTCCY7AVFAIRGEWWJZT2UDMBHS3NGNKVUWLY43CHDNN75AQEV6P3EOY
compliance CDEH6FJBK7NDFLALO3X3GDXZUREYHUSNC7ODPM64RBG3BBXW2USMW7OO

One testnet transaction per privileged operation, each made through the console (not by script) and signed by the account that holds that authority, against those contracts. Every authority account has a single signer (weight 1) and the default thresholds, so each transaction carries one signature. Multisig is left to deployers. The transactions ran on 2026-11-02.

Authority Account Signers Threshold
Governance GAM3QHYPTTVFOHS2JPCRXL2X2JSH44P4NKKSPR75G47LOOXVR7OGX5AP 1 1
Manager GBFFWDAHPSD7H4BQQCC6FMPKW5S6KZL25WCYJRRNGXS7IB7X75N3VXR4 1 1
Treasury GA6UJXW73VJVKLU3ACZPZLPAVH5UTIIGIFGVOKQTF52AIPFXRTQ7U6CN 1 1
Guardian GDKSNPH4PBAYAHMBLWMESCS2AWRUGCXB3Z3AQKG5TKQF7A3GAROWZPPX 1 1
Attester GD4RCWJADWPH5UMT4QJ2GSVSTS4PNQ3L36OW2HEDRCNRWKZPFQV2NGHI 1 1
Compliance GBYDLIMNUC5RTDMQHZKIRHL5ALCF3PLJAPCMHOHEJ2C6GPUBCW3GSM4A 1 1
Authority Operation Entrypoint Transaction
Manager Close an epoch close_epoch b245b6b7…ebb4
Attester Attest the NAV attest 2a5b9065…d2b3
Treasury Deploy to the custodian deploy_to_custodian 42805dc6…f835
Guardian Pause the vault pause 0ca2701c…0267
Guardian Pause the NAV (raise the ripcord) raise_ripcord 1c2a59e6…6152
Governance Unpause the vault unpause 2163860b…0292
Governance Lower the ripcord set_ripcord 38fab33d…44f4
Governance Pause the share token pause a0585500…1391
Governance Unpause the share token unpause d16400cf…f07c
Compliance Allow an investor allow 5671fb19…ea35
Compliance Freeze an address set_address_frozen 8e6556db…48fb
Compliance Unfreeze an address set_address_frozen d4ee95a6…1469
Compliance Remove an investor (Disallow in the console) allow with false d8154ca3…ab36

Not recorded:

  • A timelocked change, proposed and applied (propose_upgrade, apply_upgrade): the vault's upgrade delay is seven days, its minimum, longer than this verification.
  • A wind-down proposal (propose_wind_down): the deployment's wind_down_delay is zero, so anyone could activate the wind-down in the next ledger, and activation cannot be undone.
  • legal_transfer, co-signed by governance: the contracts above predate it. Their console offers forced_transfer.

The share token is paused by governance, not the guardian: the guardian holds no role on the token.

Demo deployment

The Tranche 1 demo video was recorded on testnet against the team's E2E testing deployment, deployed from main at 45e0e23 on 2026-10-20 (ledger 5299871). It is a separate instance from the one listed under Deployed addresses. No contract source has changed on main since 45e0e23, so its contract code is the same as main.

Contract Address
async_vault CAGHFSATIJHNDXQA577APGG6GF2L7Q7KP6H2IUXVW6IT3CJ2OBE3QCEB
nav_oracle CDDRBLB3GHZGKHGFATZBKFSXC6XRVFLHNPVWHTIE67FVNDYHU2RRRNRO
share_token CCB23VF4CSI7QNWNDOAAAVOKOLWO5EEQFZIODWEXHS6OB3MMHGFZ3VA6
identity_verifier CDMV7DGGLGIWDWI4ERIBZ5YS2FZA3QVZPVUWCAWR5VBLVJMLFYCW6DML
compliance CCCA6XDCXBZWWTIDYGLTL5GEHROCZTGYAIRAFA2KBO2JAWOJFU3TMZCB
asset CAU7LDCUCLAWSJZS72B7VIPIE7FSN3T63D7MZMSSXTNLKYDYECVGH522

Test USDC: code USDC, issuer GCUYFZRTNQ6WIIN5XBEYWVENKNVCBPK5BRXK4PDBCOGCYA4BZIRGMVIN.

Development

Two ways to get a working toolchain.

With Nix (recommended)

nix develop              # pinned Rust + stellar-cli + wabt/twiggy/tlaplus + prek
stellar contract build   # build the contract wasms (not `cargo build` — OZ spec-shaking)
cargo test               # run the workspace unit tests

With direnv + nix-direnv, echo 'use flake' > .envrc && direnv allow loads the shell — and your editor's language server — automatically.

Without Nix

rustup show                                # installs the toolchain pinned in rust-toolchain.toml
rustup target add wasm32v1-none            # Soroban's wasm target
cargo install --locked stellar-cli@27.0.0
stellar contract build && cargo test

Formatting and secret-scanning run on commit via a prek hook — see .pre-commit-config.yaml and run prek install once. CI also scans every pull request's commits with the same gitleaks version.

Console and script tests

The tests and the type check for app/, app-lib/ and scripts/ run against the TypeScript clients generated from the contracts. Generate them once, and again after a contract's interface changes:

npm ci
stellar contract build && npm run build:clients
npm run test        # stops and names any client that is missing
npm run typecheck   # app, app-lib and the harness

Integration & On-Chain Tests

From a fresh clone to a vault set deployed on a local network. Run these inside nix develop, or with the toolchain from "Without Nix" plus Node 22 and Docker.

# A local Stellar network, RPC at http://localhost:8000/rpc
docker run --rm -d -p 8000:8000 --name stellar-local \
  docker.io/stellar/quickstart:v687-b1580.1-latest@sha256:7711899e9942c60316ebacf8ab901be15bed7a5a31705d847b55375ff3bc8356 \
  --local --enable core,rpc --protocol-version 30

npm ci                                         # JS dependencies, including the harness
stellar contract build                         # contract wasms
npm run build:clients                          # TypeScript clients from the wasms
HARNESS_NETWORK=local npm run deploy -w scripts/harness

HARNESS_NETWORK defaults to local; testnet deploys to testnet and rewrites the addresses table above and [staging.contracts] in environments.toml. Commit both files after a testnet deploy: the app's build reads those IDs, so until the change is committed it points at the previous contracts. The local deploy funds its accounts from the quickstart root key, so it needs no stellar keys identity.

The first deploy writes a secret key for each harness account to .env.keys.<network> at the repository root, and later deploys reuse them. Back that file up and never commit it. The harness README explains how to use a key you already hold for an account.

Output of a real run from a fresh clone. Contract IDs differ on every run.

> @strata-vault-kit/harness@0.1.0 deploy
> tsx deploy.ts

deploying to local (http://localhost:8000/rpc)
account keys in .env.keys.local (13 new)
funded 13 accounts; 0 already existed
asset contract CCW7OKVOXBBBX3YK6O7F6TZPNIFG32OG4UY7R2K6NSWZUI4R75QCEPVT
returned the deposit asset of 0 accounts to its issuer
compliance CDPCD2BQQKVQBQFAFI4YBLN3TRX34J5NZUQQMUBGHPYNY64LCEM63DTL
identity_verifier CC4SRUIMKOBZSEO2GKGVQMDWPWCBJBBIMYYE7BBCUEZE6VMD3SFQDAMW
share_token CDC7SRTDZIMJSLC5AONUD3FIFYZGMFOHRAS7LLYV2ATJRT7RMTMQO2DG
nav_oracle CCUUBP3RGOIZZL4DUGRL4FBOSQJZKBPNQ7VUWIN6UOOYBJYYKGNT5E52
async_vault CB2KVM2WHLXDZRHGCYPTGE7PZM6OORXCNVBUMC3L7GY5KX4BA6NMADXE
commit <sha>
clients generated
vault holds share_token manager role
share_token bound to the vault, setup finished
vault is allowlisted
share_token bound to compliance
compliance bound to the vault, setup finished
nav_oracle bound to the vault, setup finished
vault notice set to 10 seconds
vault minimum epoch duration set to 86400 seconds
vault setup finished
health {"addresses_distinct":true,"allowlisted":true,"compliance_bound":true,"custodian_set":true,"decimals_match":true,"feed_answers":true,"feed_valid":false,"freshness_covers_notice":true,"oracle_bound":true,"setup_open":false,"sole_token_manager":true,"token_bound":true,"vault_unfrozen":true}
wrote deployed.local.json

The addresses land in scripts/harness/deployed.local.json; a later deploy keeps the previous file as deployed.local.<timestamp>.json. Then run the integration suites against that deployment:

npm run test:happy-path -w scripts/harness
npm run test:guards -w scripts/harness
CARGO_TARGET_DIR=target/short-wind-down stellar contract build --package async-vault --features short_wind_down
npm run test:wind-down -w scripts/harness

Deploy afresh before each run of the suites. The deploy sets the vault's notice to twice the oracle cooldown (10 seconds on the local network, 120 on testnet), and the suites wait out the notice before they price an epoch. It also sets a minimum epoch duration of one day on every network, so an outsider can eventually seal an epoch the manager leaves open; the guards suite checks the refusal before that minimum elapses, since waiting a real day is not practical for a test run. test:wind-down ignores deployed.local.json: it deploys its own throwaway vault set, built from the short_wind_down wasm above, so it can reach an active wind-down without the real seven-day minimum.

Each deploy first returns any deposit asset the harness accounts hold to its issuer, so every holder starts a new set at zero. Deploy again and the suites run again on the same network.

Keeping a deployment alive

Every contract extends its own storage when it is called, but a deployment nobody calls archives after 30 days. Keep the contracts callable with:

STELLAR_ACCOUNT=<identity or secret> npm run extend -w scripts/harness

It reads the testnet contracts from environments.toml and covers three kinds of entry: each contract instance, the Wasm it runs and each pending timelock change. It restores any of them that has archived, then extends each by 30 days. Any funded account pays, since extending needs no authority. Use --env <name> for another environment, or --config <record> for a deployment record.

It extends nothing else. Other entries, such as epochs, requests, balances and roles, still archive when nobody uses them. Nothing in them is lost: the next call that needs an archived entry restores it, and the account that sends that call pays the restore fee. For a holder's balance or request, that is usually the holder, on their next call. The TTL policy says what each contract extends on its own and how a restore works.

With --config, it uses the network the record names (local or testnet) unless --env names another environment, and it prints the network and RPC it uses before it sends anything. Put flags after --, or npm swallows them: npm run extend -w scripts/harness -- --config <record>.

Reference base

Built on our own stellar-vault-demo-dapp, a working testnet demo. This repo is the productized version of it: code moves across deliberately, with the roles, pause and configuration the demo never had.

About

Compliance-gated RWA vault for Stellar/Soroban. OpenZeppelin-based, white-label: configure roles, allowlist and token at deploy.

Resources

Security policy

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages