Roshi is a Solana-native vault protocol for strategist-managed portfolios. It combines share-based accounting, trusted NAV reporting, vault-scoped access control, authorized strategy execution, queued withdrawals, and performance-fee accounting.
Roshi is experimental software and has not been audited. It is provided as-is, without warranties or liability. Do not use it with production funds unless you have performed your own review, testing, and risk assessment.
- On-chain program instructions for vault initialization, deposits, redemptions, queued withdrawal settlement, NAV reporting, fee collection, supported asset configuration, pause/access controls, role rotation, and authorized strategist CPI execution.
- Shared interface types and checked integer math used by the program, tests, and client helpers.
- Thin Rust client builders for Roshi instructions.
- LiteSVM integration tests covering the main protocol flows.
- A coverage-guided invariant fuzzer (crucible: LibAFL + LiteSVM) for the core accounting loop.
crates/interface: reusable protocol types, instruction args, and math.crates/roshi: on-chain Solana program.crates/client: instruction-building helpers.crates/tests: LiteSVM integration test harness.fuzz: crucible invariant-fuzzing harness — a standalone workspace, not a member (see Fuzzing).vendor/crucible: the fuzzer engine, vendored as a git submodule.
just build
just check
just test-sbfUseful direct checks:
cargo fmt -- --check
cargo check
cargo check -p roshi --no-default-features
cargo test
cargo build-sbf --manifest-path crates/roshi/Cargo.tomljust build produces target/deploy/roshi.so. The integration tests use that
SBF artifact when present.
Generate the Codama IDL:
cargo run -p roshi-interface --example generate_codama_idlThe generator writes target/idl/roshi.codama.json by default. Pass a path as
the final argument to choose a different output file.
fuzz/ is a crucible
invariant-fuzzing harness that uses LibAFL and LiteSVM. It sends roshi-client
instructions to the real program. sBPF edge coverage on the LiteSVM execution
guides the fuzzer, so the program needs no instrumentation. After each mutated
action sequence, invariant_core evaluates the invariants below.
Post-sequence invariants
- Base tokens remain conserved. The program mints and burns shares only.
- Each registered non-base asset stays conserved in its own units. Its atoms
never enter the base sum; deposits credit
total_assetsin priced base terms. - Neither
performance_fee_bpsnorwithdrawal_buffer_bpsexceedsMAX_BPS. high_watermarknever decreases, so the same gains cannot incur performance fees twice.requested_withdrawal_sharesmatches shares on unstruck live tickets.pending_withdrawal_assetsmatches assets owed by all live tickets.
After every accepted report_nav, net total_assets plus fees_payable plus
pending_withdrawal_assets equals gross NAV. A mismatch exposes an error in the
fee or liability arithmetic.
What the harness exercises
- The core loop runs deposits, redeems, NAV reports, and withdrawal settlement.
manage,manage_batch,swap, andatomic_redeemrun arbitrary CPIs that the program authorizes in advance.authorize_actioncreates the authorization.validate_authorized_cpivalidates it, and sub-accountinvoke_signedexecutes the CPI.manageandmanage_batchscan every writable custody account for the sub-account before a CPI and re-check it after. No route can leave a sibling custody with a delegate or close authority for a later drain.swapmust stay within its realized input and output bounds.atomic_redeemmust stay within the share entitlement. Its unwind must land in custody before the program burns the shares.
- A tampered
managecannot move custody funds to an unpinned destination. - After
revoke_action, amanagecall for the same action moves nothing. - The vault starts in private mode over a real access merkle tree. Members
submit proofs with their deposits, so the core loop passes through the ACL by
default.
set_vault_accesstoggles the mode; private mode rejects a non-whitelisted outsider. - Actions for non-base deposits use a mock Pyth feed. At a clean first deposit, they accept a fresh price. They reject a stale price, an over-wide confidence interval, or a disabled asset without moving tokens.
- Other actions deposit and swap a registered bare Token-2022 asset.
- A separate action proves that
initialize_assetrejects a transfer-fee Token-2022 mint without creating its Asset PDA. - Rotation actions prove that each previous signer loses its former instruction. They cover the program authority, vault admin, strategist, swap authority, NAV authority, and withdrawal authority.
- Configuration actions call
update_vault_configorset_pause_flags. Throughupdate_vault_config, one action replaces the full profile for economic controls. - Separate sub-accounts hold deposit and withdrawal custody.
report_navcounts both, butprocess_withdrawalscan pay only from withdrawal custody. - Fee actions preserve exact accounting.
collect_feesandwrite_down_feesreject amounts abovefees_payablewithout changing the vault or token balances. - A deposit followed immediately by
atomic_redeemat flat NAV never returns more base than the user deposited.
A minimized seed corpus is committed in fuzz/corpus. The fuzz,
fuzz-stateful, and fuzz-cov recipes pass it with --corpus-in.
The vendor/crucible git submodule records a specific revision of the engine
fork. This fork pins litesvm 0.12 and solana-pubkey 4.x. The harness uses
solana-instruction 3.4 for Instruction and solana-pubkey 4.2 for Pubkey.
Both versions satisfy roshi-client's 3.3 and 4.1 constraints, so the types
unify.
One-time setup:
git submodule update --init vendor/crucible
cargo install --path vendor/crucible/crates/crucible-fuzz-cliRun (each recipe rebuilds roshi.so first):
just fuzz # stateless: full mutated sequence per iteration
just fuzz-stateful # stateful: single action over a live state pool (faster)
just fuzz-cov # LCOV + HTML coverage report (needs genhtml)Crash triage and regression replay:
just fuzz-crashes # list recorded crashes
just fuzz-show <crash-file-or-path> # inspect one recorded crash
just fuzz-replay <input-path> # replay a raw crash or regression input
just fuzz-tmin <crash-filename> # minimize one crash in place
just fuzz-tmin-all # minimize all recorded crashes
just fuzz-regressions # replay committed regression inputsWhen a crash is worth keeping, minimize the input and fix the bug. Then commit
the minimized input under fuzz/regressions/invariant_core/. After the fix,
just fuzz-regressions must not reproduce the failure.
Apache-2.0. See LICENSE.