Skip to content
trancheyieldPublic

About

Structured credit on Solana: senior/junior tranches over a simulated lending pool, with an on-chain loss waterfall and operator-recorded loss events. Devnet demo.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

Wash App

Structured credit on Solana: a deposit into a lending pool is sliced into a senior tranche with a capped, predictable yield and a junior tranche that takes the residual yield and absorbs losses first. A separate loss-protection market will let anyone buy or sell cover against a loss event in a specific pool, settled automatically on chain.

The lending pool in this repository is a simulation: yield accrues at a configured rate on a scaled model clock, and loss events are recorded by the pool operator. Integration with external lending protocols (Kamino, MarginFi, …) is out of scope.

Status — v0.1.0: pool, tranches, yield and loss waterfall are live on devnet with a web page and an operator sheet. The protection market (v0.2.0) and the "what if" cabinet (v0.3.0) are next; their screens in the web app still run on mock data.

How the waterfall works

  • The pool holds one base token (demo mint WUSD, 6 decimals). Deposits mint tranche shares — sWUSD for senior, jWUSD for junior — at the tranche's current NAV (assets / supply, 1:1 while the tranche is empty).
  • Yield accrues on the whole pool at yield_rate. A performance fee goes to the operator's treasury, senior gets at most senior_rate on its assets, junior takes everything left (including rounding remainders).
  • A loss of L hits junior first; senior loses only L − junior once junior is wiped out. Every loss is written as an immutable LossEvent account (ts, model time, bps, amount, junior/senior split, assets before) — the loss history is read from chain, no server.
  • A deposit into senior is refused if junior would drop below min_junior_bps of pool assets (the junior floor); the first deposit into an empty pool must therefore be junior.
  • Invariant after every instruction: assets == senior_assets + junior_assets == vault.amount. Yield is minted into the vault, losses are burned from it.
  • Every user instruction starts with accrue. Time is a model clock: model_time += elapsed × time_scale. The demo pool runs at 43 200×, so one minute on chain is 30 model days — enough to watch NAV move during a demo.

All amounts are integers in micro-units (u64 on chain, bigint in TypeScript); products go through u128; everything is checked arithmetic.

Devnet

Site https://trancheyield.github.io/Wash-App/ — landing page (GitHub Pages, built from main)
Web app https://trancheyield.github.io/Wash-App/app/
Program 2Yq39tVgTH5e8be8YdssyhvM6339f2WG6QweNmxGpBbf (SBPFv0, upgradeable)
Pool 0 yield 8 %/yr · senior 5 %/yr · junior floor 20 % · fee 10 % · clock 43 200×
Faucet 1 000 WUSD per request from the pool page (cap per call in Config)

fixtures/params.json is the single source of the demo parameters for demo:init, the program tests and the design brief.

Layout

programs/washapp      Anchor 1.2 program: init_config, faucet, create_pool, accrue, deposit,
                      redeem, record_loss; pure waterfall math in src/math.rs
packages/chain        Codama-generated client on @solana/kit, PDAs, account readers,
                      instruction builders (each with a round-trip decode test)
packages/shared       TypeScript mirror of the waterfall math, micro-unit formatting, Zod schemas
apps/web              React + Vite SPA: pool page, deposit/redeem, loss sheet with operator
                      panel (live on devnet); protection and position screens (mock data, M2/M3)
apps/landing          static landing page served at the site root (no build)
tools/demo            devnet init and the scripted end-to-end scenario
fixtures/             evidence generated by the Rust tests and the scenario (see below)
scripts/              WSL build/deploy wrappers and the trace sweep run in CI

Requirements

  • Node ≥ 22, pnpm 9.15 (packageManager is pinned).
  • For the program: Rust 1.97.1 (rust-toolchain.toml), Agave / cargo-build-sbf 3.1.10, Anchor CLI 1.2.0 — only needed to emit the IDL. On Windows the on-chain toolchain runs in WSL.
  • A Solana RPC for devnet. The public endpoint rate-limits a deploy and the scripted scenario; a free Helius key is enough.

Copy .env.example to .env. SOLANA_RPC_URL and WASH_KEYS_DIR are read by the demo tools and the deploy script; VITE_* by the web app. The operator, deployer and program keypairs live in WASH_KEYS_DIR, outside the repository.

Commands

pnpm install
pnpm gate            # biome + tsc + vitest across the workspace
pnpm dev             # web on :5173
pnpm codama          # IDL → packages/chain/src/generated (after wsl-build.sh idl)
pnpm demo:init       # devnet: config, demo mint, treasury and pool 0 (idempotent)
pnpm demo:scenario   # devnet: fresh buyer and seller → deposits → cover sold and bought → 60 s →
                     # loss → settle → expire → redeem → withdraw

On-chain, from PowerShell into WSL:

wsl.exe -e bash <repo>/scripts/wsl-build.sh gate      # fmt-check, clippy, build-sbf, cargo test
wsl.exe -e bash <repo>/scripts/wsl-build.sh idl       # anchor build for the IDL only
wsl.exe -e bash <repo>/scripts/wsl-build.sh fixtures  # regenerate fixtures/*.json from the tests
wsl.exe -e bash <repo>/scripts/wsl-deploy.sh deploy   # devnet deploy or upgrade, then verify
wsl.exe -e bash <repo>/scripts/wsl-deploy.sh verify   # bytecode on chain == local .so, e_flags 0x0

The network artifact is built with cargo-build-sbf (SBPFv0 — Agave 3.1.10 on devnet does not execute v3 bytecode). anchor build is used only to emit the IDL and overwrites the .so with a v3 build, so build-sbf always follows it.

Deployment

Web app — GitHub Pages

The site is static: there is no server, every number on it is read from chain in the browser. .github/workflows/pages.yml runs on every push to main (or by hand from Actions → Pages → Run workflow) and publishes one artifact of two folders:

https://<owner>.github.io/<repo>/       apps/landing — static landing page, copied as is
https://<owner>.github.io/<repo>/app/   apps/web — the app, built with base `/<repo>/app/`

apps/landing has no build and no dependencies (index.html, styles.css, assets/); every figure on it comes from this README and fixtures/demo-run.json. Its og:image is an absolute URL to the project site — change it together with the domain.

One-time setup in the repository:

  1. Settings → Pages → Source: GitHub Actions (not "Deploy from a branch" — the branch root has no index.html).

  2. Optionally, Settings → Secrets and variables → Actions → Variables:

    Variable Unset means
    VITE_SOLANA_RPC_URL public https://api.devnet.solana.com
    VITE_WASH_DEFAULT_POOL pool 0
    VITE_SOLANA_CHAIN solana:devnet (wallet-standard chain id)

    These are variables, not secrets: every VITE_* value is readable in the published bundle. An RPC URL with a key is public once it is here. Helius Free (October 2026) cannot lock a key to a domain, so use a key from a separate account that serves nothing else, and test any provider's origin lock with curl -H "Origin: https://example.com" before relying on it. Re-run the workflow after changing a variable.

Notes:

  • Do not publish a local pnpm build: it reads the repo-root .env and would bake its RPC key into the bundle. CI has no .env.
  • The site path (/Wash-App) comes from actions/configure-pages and the app is built under <site path>/app/, so a custom domain needs no code change in the app. Set the domain in Settings → Pages only — a CNAME file in the repository is ignored when Pages deploys from a workflow.
  • Deep links: the workflow copies the app's index.html into every route of the default pool (/app/pool/<id>/…, /app/me, /app/operator), so a direct link or a reload answers 200. Any other path — another pool id, or a link from before the app moved under /app/ — renders the app through the site's root 404.html with status 404; the app moves an old link below /app/ before the router reads it.
  • Pages has no response headers (no CSP) and no place to hide a key. It covers the devnet demo; a mainnet deployment with real funds needs a host with headers (Vercel, Cloudflare Pages) or IPFS.

Program — devnet

Run from PowerShell into WSL, with SOLANA_RPC_URL and WASH_KEYS_DIR set in .env:

wsl.exe -e bash <repo>/scripts/wsl-build.sh build-sbf   # SBPFv0 artifact in target/deploy
wsl.exe -e bash <repo>/scripts/wsl-deploy.sh deploy     # first deploy or upgrade, then verify
wsl.exe -e bash <repo>/scripts/wsl-deploy.sh status     # program account and deployer balance
pnpm demo:init                                          # Config, mint, treasury, pool 0, its cover market
  • The program keypair (washapp-keypair.json) fixes the address and must match declare_id!; the deployer keypair is the upgrade authority. Both live in WASH_KEYS_DIR; the deploy script creates the deployer if it is missing.
  • deploy refuses to start until the deployer can pay: a first deploy needs rent for ProgramData (.so × 1.5, room for upgrades) plus a buffer of the .so size; an upgrade needs only the buffer. The buffer is refunded after the write. The script prints the exact amounts.
  • deploy ends with verify: the bytecode on chain must equal target/deploy/washapp.so byte for byte and be SBPFv0.
  • An upgrade that keeps the instruction and account layout needs no site rebuild. If the IDL changed, regenerate the client (wsl-build.sh idl, then build-sbf, then pnpm codama) and push — Pages rebuilds from main.
  • A program under a different address is a rebuild, not a setting: the address sits in declare_id! (bytecode) and in the generated client (pnpm codama), which the site and the tools both use. Set the new keypair, update declare_id! and WASH_PROGRAM_ID in .env, then idl → build-sbf → pnpm codama → deploy, and push.

Tests and evidence

Program tests run the real .so under mollusk-svm in cargo test: unit tests on math.rs with proptest invariants, one test file per instruction, a compute-unit gate (every instruction < 200 000 CU; current peak 40 713 for record_loss), and 100 seeded random sequences of deposit/redeem/accrue/loss that check the vault invariant and the loss split byte for byte after every step.

fixtures/ holds what the Rust side generated so the TypeScript side can be checked against the program rather than against itself:

  • waterfall.json — 152 cases from math.rs; packages/shared must reproduce them exactly.
  • pda.json — 30 PDA derivations at the edges of every numeric seed.
  • accounts/m0.json — raw account bytes from the SVM before and after a 15 % loss; the readers decode them in vitest.
  • demo-run.json — the last demo:scenario run on devnet: per-step timings (signature → confirmed → change visible in RPC), the loss waterfall checked against the on-chain event, and the protection checks — premiums against the mirror, the buyer's balance after settle (+notional × loss), the expired contract's released reservation, the seller's result.

Guard tests fail cargo test when a fixture is stale, and vitest fails when a fixture breaks a budget (scenario ≤ 180 s, each step ≤ 10 s) or records a failed check. The last run: 97.4 s end to end over 16 transactions, slowest step 4.2 s; settle paid 150.00 on a 1,000 WUSD notional for a 15 % loss, to the unit.

CI (.github/workflows/ci.yml) runs the Rust gate on the SBPFv0 artifact, the Node gate, and a sweep for keys and machine-specific traces over the whole history.

Roadmap

  • v0.1.0 — pool, tranches, yield, loss waterfall; pool page, deposit/redeem, operator loss sheet; devnet scenario. This release.
  • v0.2.0 — protection market: sellers fund a cover pool, buyers pay a premium for cover against a loss above a trigger, settlement pays out from the cover pool when a loss is recorded.
  • v0.3.0 — position cabinet with a "what if the pool loses X %" forecast that must match the program to one micro-unit.

About

Structured credit on Solana: senior/junior tranches over a simulated lending pool, with an on-chain loss waterfall and operator-recorded loss events. Devnet demo.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages