diff --git a/.github/workflows/kani.yml b/.github/workflows/kani.yml index b01b7bd65..b0b442932 100644 --- a/.github/workflows/kani.yml +++ b/.github/workflows/kani.yml @@ -58,8 +58,8 @@ jobs: - order-book - lending - betting-market - - vault-strategy - - token-fundraiser + - managed-fund + - fundraiser - prop-amm - options steps: @@ -103,8 +103,8 @@ jobs: - order-book - lending - betting-market - - vault-strategy - - token-fundraiser + - managed-fund + - fundraiser - prop-amm - options steps: diff --git a/.github/workflows/typescript.yml b/.github/workflows/typescript.yml index 81369c340..2757d738d 100644 --- a/.github/workflows/typescript.yml +++ b/.github/workflows/typescript.yml @@ -34,12 +34,12 @@ jobs: # the program's IDL would have gone unnoticed. `verify` runs the client's # instruction encoding, account decoding and PDA derivation against the # committed IDL, offline, with no validator. - vault-strategy-app: - name: Vault strategy app + managed-fund-app: + name: Managed fund app runs-on: ubuntu-latest defaults: run: - working-directory: finance/vault-strategy/anchor/app + working-directory: finance/managed-fund/anchor/app steps: - uses: actions/checkout@v5 - uses: pnpm/action-setup@v4 diff --git a/CHANGELOG.md b/CHANGELOG.md index 85aab0688..ddd0f7262 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,51 @@ All notable changes to this repository are documented here. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). +## [2026-09-28] - Token Fundraiser is renamed Fundraiser + +Contributors to the fundraiser receive no token, only a refund if the target is +missed, so "Token Fundraiser" described something the program does not do. + +### Changed + +- `finance/token-fundraiser` is now `finance/fundraiser`, in Anchor v2, Anchor + v1 and Quasar, with its Kani proofs. The Quasar crate is `quasar-fundraiser` + and the proofs crate `fundraiser-kani-proofs`; the Anchor programs were + already named `fundraiser`. Accounts, instruction handlers and behavior are + unchanged. +- `README.md`, `llms.txt` and the example's READMEs say it was formerly Token + Fundraiser, and `finance/token-fundraiser/README.md` points old links to the + new location. + +## [2026-09-28] - Vault Strategy is renamed Managed Fund + +The example called Vault Strategy is a manager-run fund: investors deposit for +shares priced at net asset value, and a manager allocates the pool across a +basket of approved assets. Finance calls that a managed fund, the onchain +equivalent of a mutual fund. It is not an index fund, whose weights follow an +outside index with no manager choosing them. "Vault" also meant two things in +the example: the whole product, and the single-asset token accounts it owns. +Now it means only the token accounts. + +### Changed + +- `finance/vault-strategy` is now `finance/managed-fund`, in Anchor v2, Anchor + v1 and Quasar, with its Kani proofs and web app. The program crate is + `managed-fund` (library `managed_fund`; Quasar `quasar-managed-fund`), the + `Strategy` account is `Fund`, `initialize_strategy` is `initialize_fund`, the + `StrategyNotFullyAllocated` error is `FundNotFullyAllocated`, and the error + enum `VaultError` is `FundError`. The fund PDA's seed is `"fund"` instead of + `"strategy"`, so fund addresses change, and `initialize_fund` and `Fund` + have new Anchor discriminators. The program IDs and the programs' behavior + are unchanged, and every test passes under its new name. +- The web app's IDL, client and environment variables follow the rename + (`VITE_STRATEGY_INDEX` is `VITE_FUND_INDEX`, `VITE_VAULT_PROGRAM_ID` is + `VITE_FUND_PROGRAM_ID`). +- `README.md` and `llms.txt` list the example as Managed Fund and say it was + formerly Vault Strategy, and each of its READMEs says so too, so a search for + either name finds it. `finance/vault-strategy/README.md` stays behind as a + pointer to the new location for anyone following an old link. + ## [2026-09-25] - Anchor v1 is the current stable Anchor The README described Anchor v2 as Anchor's current major version and Anchor v1 diff --git a/Cargo.lock b/Cargo.lock index bb4ef795b..ac364fe35 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2552,6 +2552,24 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ca88d725a0a943b096803bd34e73a4437208b6077654cc4ecb2947a5f91618d" +[[package]] +name = "managed-fund" +version = "0.1.0" +dependencies = [ + "anchor-lang", + "anchor-spl", + "anchor-v2-testing", + "borsh 1.7.0", + "mock-swap-router", + "pinocchio 0.11.2", + "solana-account 3.4.0", + "solana-address 2.6.1", + "solana-clock 3.1.1", + "solana-kite", + "solana-sysvar 3.1.1", + "wincode 0.5.5", +] + [[package]] name = "memchr" version = "2.8.2" @@ -7463,24 +7481,6 @@ dependencies = [ "lazy_static", ] -[[package]] -name = "vault-strategy" -version = "0.1.0" -dependencies = [ - "anchor-lang", - "anchor-spl", - "anchor-v2-testing", - "borsh 1.7.0", - "mock-swap-router", - "pinocchio 0.11.2", - "solana-account 3.4.0", - "solana-address 2.6.1", - "solana-clock 3.1.1", - "solana-kite", - "solana-sysvar 3.1.1", - "wincode 0.5.5", -] - [[package]] name = "version_check" version = "0.9.5" diff --git a/Cargo.toml b/Cargo.toml index c1fd19028..e1b8135d3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -82,10 +82,10 @@ members = [ "finance/order-book/anchor/programs/order-book", "finance/perpetual-futures/anchor/programs/perpetual-futures", "finance/prop-amm/anchor/programs/prop-amm", - "finance/token-fundraiser/anchor/programs/fundraiser", + "finance/fundraiser/anchor/programs/fundraiser", "finance/token-swap/anchor/programs/token-swap", - "finance/vault-strategy/anchor/programs/vault-strategy", - "finance/vault-strategy/anchor/programs/mock-swap-router", + "finance/managed-fund/anchor/programs/managed-fund", + "finance/managed-fund/anchor/programs/mock-swap-router", # tokens "tokens/token-extensions/mint-close-authority/native/program", diff --git a/README.md b/README.md index f64e5631d..f6dd5c39c 100644 --- a/README.md +++ b/README.md @@ -68,11 +68,13 @@ A **proprietary AMM**: a market-making firm funds a venue with its own capital a [⚓ Anchor v1](./finance/prop-amm/anchor-v1) [⚓ Anchor v2](./finance/prop-amm/anchor) [💫 Quasar](./finance/prop-amm/quasar) -### Vault Strategy +### Managed Fund -A managed investment fund onchain, like an ETF or mutual fund. Investors deposit USDC for shares, a manager allocates the pool across a basket of assets (here, stocks like TSLAx and NVDAx), and each share's value tracks the fund's net asset value. The manager earns a management fee, and investors redeem a proportional slice of the underlying assets. +*Formerly **Vault Strategy**. Some platforms call this product a vault or a vault strategy; the example was renamed to use the finance term.* -[⚓ Anchor v1](./finance/vault-strategy/anchor-v1) [⚓ Anchor v2](./finance/vault-strategy/anchor) [💫 Quasar](./finance/vault-strategy/quasar) +A managed investment fund onchain, the equivalent of a mutual fund. Investors deposit USDC for shares, a manager allocates the pool across a basket of assets (here, stocks like TSLAx and NVDAx), and each share's value tracks the fund's net asset value. The manager earns a management fee, and investors redeem a proportional slice of the underlying assets. + +[⚓ Anchor v1](./finance/managed-fund/anchor-v1) [⚓ Anchor v2](./finance/managed-fund/anchor) [💫 Quasar](./finance/managed-fund/quasar) ### Betting Market @@ -95,11 +97,13 @@ A perpetual futures exchange: a venue for making leveraged bets on an asset's pr [⚓ Anchor v1](./finance/perpetual-futures/anchor-v1) [⚓ Anchor v2](./finance/perpetual-futures/anchor) [💫 Quasar](./finance/perpetual-futures/quasar) -### Token Fundraiser +### Fundraiser + +*Formerly **Token Fundraiser**. Contributors receive no token, only a refund if the target is missed, so the example was renamed.* Onchain crowdfunding, like Kickstarter or GoFundMe. A creator sets a target amount in a chosen token, and contributors deposit into the fundraiser's account until the goal is reached. -[⚓ Anchor v1](./finance/token-fundraiser/anchor-v1) [⚓ Anchor v2](./finance/token-fundraiser/anchor) [💫 Quasar](./finance/token-fundraiser/quasar) +[⚓ Anchor v1](./finance/fundraiser/anchor-v1) [⚓ Anchor v2](./finance/fundraiser/anchor) [💫 Quasar](./finance/fundraiser/quasar) ## Single concept examples diff --git a/finance/token-fundraiser/anchor-v1/Anchor.toml b/finance/fundraiser/anchor-v1/Anchor.toml similarity index 100% rename from finance/token-fundraiser/anchor-v1/Anchor.toml rename to finance/fundraiser/anchor-v1/Anchor.toml diff --git a/finance/token-fundraiser/anchor-v1/CHANGELOG.md b/finance/fundraiser/anchor-v1/CHANGELOG.md similarity index 67% rename from finance/token-fundraiser/anchor-v1/CHANGELOG.md rename to finance/fundraiser/anchor-v1/CHANGELOG.md index 12dbd4c8c..c18c05f0d 100644 --- a/finance/token-fundraiser/anchor-v1/CHANGELOG.md +++ b/finance/fundraiser/anchor-v1/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## 2026-09-28 + +- **Renamed from Token Fundraiser to Fundraiser.** The example moved from `finance/token-fundraiser` to `finance/fundraiser`: contributors receive no token, only a refund if the target is missed, so "Token" described something the program does not do. The program, its accounts, its instruction handlers and its tests are unchanged. + ## 2026-09-23 ### Added diff --git a/finance/token-fundraiser/anchor-v1/Cargo.toml b/finance/fundraiser/anchor-v1/Cargo.toml similarity index 100% rename from finance/token-fundraiser/anchor-v1/Cargo.toml rename to finance/fundraiser/anchor-v1/Cargo.toml diff --git a/finance/token-fundraiser/anchor-v1/README.md b/finance/fundraiser/anchor-v1/README.md similarity index 98% rename from finance/token-fundraiser/anchor-v1/README.md rename to finance/fundraiser/anchor-v1/README.md index 1c26d0ee4..77a19f769 100644 --- a/finance/token-fundraiser/anchor-v1/README.md +++ b/finance/fundraiser/anchor-v1/README.md @@ -1,4 +1,4 @@ -# Solana Token Fundraiser (Anchor) +# Solana Fundraiser (Anchor) > [!NOTE] > This is the **Anchor v1** copy of this example, on Anchor 1.2.0, the current @@ -8,6 +8,8 @@ Onchain crowdfunding on Solana: a program that collects tokens toward a target amount, like Kickstarter without a payment processor. A **maker** creates a fundraiser [account](https://solana.com/docs/terminology#account), specifies the [mint](https://solana.com/docs/terminology#token-mint) they want to receive, the target amount, and a duration in days. **Contributors** contribute while the window is open. If the target is reached, the maker claims the funds and each contributor closes their own record to take back its rent; if it is not reached by the deadline, contributors can refund, and once refunds are complete the maker can retire the fundraiser and open a new one. +This example was called **Token Fundraiser** (`finance/token-fundraiser`) until it was renamed: contributors receive no token, only a refund if the target is missed. + ## Architecture The fundraiser state account: diff --git a/finance/token-fundraiser/anchor-v1/programs/fundraiser/Cargo.toml b/finance/fundraiser/anchor-v1/programs/fundraiser/Cargo.toml similarity index 100% rename from finance/token-fundraiser/anchor-v1/programs/fundraiser/Cargo.toml rename to finance/fundraiser/anchor-v1/programs/fundraiser/Cargo.toml diff --git a/finance/token-fundraiser/anchor-v1/programs/fundraiser/Xargo.toml b/finance/fundraiser/anchor-v1/programs/fundraiser/Xargo.toml similarity index 100% rename from finance/token-fundraiser/anchor-v1/programs/fundraiser/Xargo.toml rename to finance/fundraiser/anchor-v1/programs/fundraiser/Xargo.toml diff --git a/finance/token-fundraiser/anchor-v1/programs/fundraiser/src/constants.rs b/finance/fundraiser/anchor-v1/programs/fundraiser/src/constants.rs similarity index 100% rename from finance/token-fundraiser/anchor-v1/programs/fundraiser/src/constants.rs rename to finance/fundraiser/anchor-v1/programs/fundraiser/src/constants.rs diff --git a/finance/token-fundraiser/anchor-v1/programs/fundraiser/src/error.rs b/finance/fundraiser/anchor-v1/programs/fundraiser/src/error.rs similarity index 100% rename from finance/token-fundraiser/anchor-v1/programs/fundraiser/src/error.rs rename to finance/fundraiser/anchor-v1/programs/fundraiser/src/error.rs diff --git a/finance/token-fundraiser/anchor-v1/programs/fundraiser/src/instructions/checker.rs b/finance/fundraiser/anchor-v1/programs/fundraiser/src/instructions/checker.rs similarity index 100% rename from finance/token-fundraiser/anchor-v1/programs/fundraiser/src/instructions/checker.rs rename to finance/fundraiser/anchor-v1/programs/fundraiser/src/instructions/checker.rs diff --git a/finance/token-fundraiser/anchor-v1/programs/fundraiser/src/instructions/close.rs b/finance/fundraiser/anchor-v1/programs/fundraiser/src/instructions/close.rs similarity index 100% rename from finance/token-fundraiser/anchor-v1/programs/fundraiser/src/instructions/close.rs rename to finance/fundraiser/anchor-v1/programs/fundraiser/src/instructions/close.rs diff --git a/finance/token-fundraiser/anchor-v1/programs/fundraiser/src/instructions/close_contributor.rs b/finance/fundraiser/anchor-v1/programs/fundraiser/src/instructions/close_contributor.rs similarity index 100% rename from finance/token-fundraiser/anchor-v1/programs/fundraiser/src/instructions/close_contributor.rs rename to finance/fundraiser/anchor-v1/programs/fundraiser/src/instructions/close_contributor.rs diff --git a/finance/token-fundraiser/anchor-v1/programs/fundraiser/src/instructions/contribute.rs b/finance/fundraiser/anchor-v1/programs/fundraiser/src/instructions/contribute.rs similarity index 100% rename from finance/token-fundraiser/anchor-v1/programs/fundraiser/src/instructions/contribute.rs rename to finance/fundraiser/anchor-v1/programs/fundraiser/src/instructions/contribute.rs diff --git a/finance/token-fundraiser/anchor-v1/programs/fundraiser/src/instructions/initialize_fundraiser.rs b/finance/fundraiser/anchor-v1/programs/fundraiser/src/instructions/initialize_fundraiser.rs similarity index 100% rename from finance/token-fundraiser/anchor-v1/programs/fundraiser/src/instructions/initialize_fundraiser.rs rename to finance/fundraiser/anchor-v1/programs/fundraiser/src/instructions/initialize_fundraiser.rs diff --git a/finance/token-fundraiser/anchor-v1/programs/fundraiser/src/instructions/mod.rs b/finance/fundraiser/anchor-v1/programs/fundraiser/src/instructions/mod.rs similarity index 100% rename from finance/token-fundraiser/anchor-v1/programs/fundraiser/src/instructions/mod.rs rename to finance/fundraiser/anchor-v1/programs/fundraiser/src/instructions/mod.rs diff --git a/finance/token-fundraiser/anchor-v1/programs/fundraiser/src/instructions/refund.rs b/finance/fundraiser/anchor-v1/programs/fundraiser/src/instructions/refund.rs similarity index 100% rename from finance/token-fundraiser/anchor-v1/programs/fundraiser/src/instructions/refund.rs rename to finance/fundraiser/anchor-v1/programs/fundraiser/src/instructions/refund.rs diff --git a/finance/token-fundraiser/anchor-v1/programs/fundraiser/src/lib.rs b/finance/fundraiser/anchor-v1/programs/fundraiser/src/lib.rs similarity index 100% rename from finance/token-fundraiser/anchor-v1/programs/fundraiser/src/lib.rs rename to finance/fundraiser/anchor-v1/programs/fundraiser/src/lib.rs diff --git a/finance/token-fundraiser/anchor-v1/programs/fundraiser/src/state/contributor.rs b/finance/fundraiser/anchor-v1/programs/fundraiser/src/state/contributor.rs similarity index 100% rename from finance/token-fundraiser/anchor-v1/programs/fundraiser/src/state/contributor.rs rename to finance/fundraiser/anchor-v1/programs/fundraiser/src/state/contributor.rs diff --git a/finance/token-fundraiser/anchor-v1/programs/fundraiser/src/state/fundraiser.rs b/finance/fundraiser/anchor-v1/programs/fundraiser/src/state/fundraiser.rs similarity index 100% rename from finance/token-fundraiser/anchor-v1/programs/fundraiser/src/state/fundraiser.rs rename to finance/fundraiser/anchor-v1/programs/fundraiser/src/state/fundraiser.rs diff --git a/finance/token-fundraiser/anchor-v1/programs/fundraiser/src/state/mod.rs b/finance/fundraiser/anchor-v1/programs/fundraiser/src/state/mod.rs similarity index 100% rename from finance/token-fundraiser/anchor-v1/programs/fundraiser/src/state/mod.rs rename to finance/fundraiser/anchor-v1/programs/fundraiser/src/state/mod.rs diff --git a/finance/token-fundraiser/anchor-v1/programs/fundraiser/tests/test_fundraiser.rs b/finance/fundraiser/anchor-v1/programs/fundraiser/tests/test_fundraiser.rs similarity index 100% rename from finance/token-fundraiser/anchor-v1/programs/fundraiser/tests/test_fundraiser.rs rename to finance/fundraiser/anchor-v1/programs/fundraiser/tests/test_fundraiser.rs diff --git a/finance/token-fundraiser/anchor/Anchor.toml b/finance/fundraiser/anchor/Anchor.toml similarity index 100% rename from finance/token-fundraiser/anchor/Anchor.toml rename to finance/fundraiser/anchor/Anchor.toml diff --git a/finance/token-fundraiser/anchor/CHANGELOG.md b/finance/fundraiser/anchor/CHANGELOG.md similarity index 66% rename from finance/token-fundraiser/anchor/CHANGELOG.md rename to finance/fundraiser/anchor/CHANGELOG.md index 459869746..6f0c1965a 100644 --- a/finance/token-fundraiser/anchor/CHANGELOG.md +++ b/finance/fundraiser/anchor/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## 2026-09-28 + +- **Renamed from Token Fundraiser to Fundraiser.** The example moved from `finance/token-fundraiser` to `finance/fundraiser`: contributors receive no token, only a refund if the target is missed, so "Token" described something the program does not do. The program, its accounts, its instruction handlers and its tests are unchanged. + ## 2026-09-14 ### Added diff --git a/finance/token-fundraiser/anchor/Cargo.toml b/finance/fundraiser/anchor/Cargo.toml similarity index 100% rename from finance/token-fundraiser/anchor/Cargo.toml rename to finance/fundraiser/anchor/Cargo.toml diff --git a/finance/token-fundraiser/anchor/README.md b/finance/fundraiser/anchor/README.md similarity index 98% rename from finance/token-fundraiser/anchor/README.md rename to finance/fundraiser/anchor/README.md index 91a81f7b6..439023ed0 100644 --- a/finance/token-fundraiser/anchor/README.md +++ b/finance/fundraiser/anchor/README.md @@ -1,4 +1,4 @@ -# Solana Token Fundraiser (Anchor) +# Solana Fundraiser (Anchor) > [!NOTE] > This is the **Anchor v2** copy of this example. Every `anchor` command on this page @@ -8,6 +8,8 @@ Onchain crowdfunding on Solana: a program that collects tokens toward a target amount, like Kickstarter without a payment processor. A **maker** creates a fundraiser [account](https://solana.com/docs/terminology#account), specifies the [mint](https://solana.com/docs/terminology#token-mint) they want to receive, the target amount, and a duration in days. **Contributors** contribute while the window is open. If the target is reached, the maker claims the funds and each contributor closes their own record to take back its rent; if it is not reached by the deadline, contributors can refund, and once refunds are complete the maker can retire the fundraiser and open a new one. +This example was called **Token Fundraiser** (`finance/token-fundraiser`) until it was renamed: contributors receive no token, only a refund if the target is missed. + ## Architecture The fundraiser state account: diff --git a/finance/token-fundraiser/anchor/programs/fundraiser/Cargo.toml b/finance/fundraiser/anchor/programs/fundraiser/Cargo.toml similarity index 100% rename from finance/token-fundraiser/anchor/programs/fundraiser/Cargo.toml rename to finance/fundraiser/anchor/programs/fundraiser/Cargo.toml diff --git a/finance/token-fundraiser/anchor/programs/fundraiser/Xargo.toml b/finance/fundraiser/anchor/programs/fundraiser/Xargo.toml similarity index 100% rename from finance/token-fundraiser/anchor/programs/fundraiser/Xargo.toml rename to finance/fundraiser/anchor/programs/fundraiser/Xargo.toml diff --git a/finance/token-fundraiser/anchor/programs/fundraiser/src/constants.rs b/finance/fundraiser/anchor/programs/fundraiser/src/constants.rs similarity index 100% rename from finance/token-fundraiser/anchor/programs/fundraiser/src/constants.rs rename to finance/fundraiser/anchor/programs/fundraiser/src/constants.rs diff --git a/finance/token-fundraiser/anchor/programs/fundraiser/src/error.rs b/finance/fundraiser/anchor/programs/fundraiser/src/error.rs similarity index 100% rename from finance/token-fundraiser/anchor/programs/fundraiser/src/error.rs rename to finance/fundraiser/anchor/programs/fundraiser/src/error.rs diff --git a/finance/token-fundraiser/anchor/programs/fundraiser/src/instructions/checker.rs b/finance/fundraiser/anchor/programs/fundraiser/src/instructions/checker.rs similarity index 100% rename from finance/token-fundraiser/anchor/programs/fundraiser/src/instructions/checker.rs rename to finance/fundraiser/anchor/programs/fundraiser/src/instructions/checker.rs diff --git a/finance/token-fundraiser/anchor/programs/fundraiser/src/instructions/close.rs b/finance/fundraiser/anchor/programs/fundraiser/src/instructions/close.rs similarity index 100% rename from finance/token-fundraiser/anchor/programs/fundraiser/src/instructions/close.rs rename to finance/fundraiser/anchor/programs/fundraiser/src/instructions/close.rs diff --git a/finance/token-fundraiser/anchor/programs/fundraiser/src/instructions/close_contributor.rs b/finance/fundraiser/anchor/programs/fundraiser/src/instructions/close_contributor.rs similarity index 100% rename from finance/token-fundraiser/anchor/programs/fundraiser/src/instructions/close_contributor.rs rename to finance/fundraiser/anchor/programs/fundraiser/src/instructions/close_contributor.rs diff --git a/finance/token-fundraiser/anchor/programs/fundraiser/src/instructions/contribute.rs b/finance/fundraiser/anchor/programs/fundraiser/src/instructions/contribute.rs similarity index 100% rename from finance/token-fundraiser/anchor/programs/fundraiser/src/instructions/contribute.rs rename to finance/fundraiser/anchor/programs/fundraiser/src/instructions/contribute.rs diff --git a/finance/token-fundraiser/anchor/programs/fundraiser/src/instructions/initialize_fundraiser.rs b/finance/fundraiser/anchor/programs/fundraiser/src/instructions/initialize_fundraiser.rs similarity index 100% rename from finance/token-fundraiser/anchor/programs/fundraiser/src/instructions/initialize_fundraiser.rs rename to finance/fundraiser/anchor/programs/fundraiser/src/instructions/initialize_fundraiser.rs diff --git a/finance/token-fundraiser/anchor/programs/fundraiser/src/instructions/mod.rs b/finance/fundraiser/anchor/programs/fundraiser/src/instructions/mod.rs similarity index 100% rename from finance/token-fundraiser/anchor/programs/fundraiser/src/instructions/mod.rs rename to finance/fundraiser/anchor/programs/fundraiser/src/instructions/mod.rs diff --git a/finance/token-fundraiser/anchor/programs/fundraiser/src/instructions/refund.rs b/finance/fundraiser/anchor/programs/fundraiser/src/instructions/refund.rs similarity index 100% rename from finance/token-fundraiser/anchor/programs/fundraiser/src/instructions/refund.rs rename to finance/fundraiser/anchor/programs/fundraiser/src/instructions/refund.rs diff --git a/finance/token-fundraiser/anchor/programs/fundraiser/src/lib.rs b/finance/fundraiser/anchor/programs/fundraiser/src/lib.rs similarity index 100% rename from finance/token-fundraiser/anchor/programs/fundraiser/src/lib.rs rename to finance/fundraiser/anchor/programs/fundraiser/src/lib.rs diff --git a/finance/token-fundraiser/anchor/programs/fundraiser/src/state/contributor.rs b/finance/fundraiser/anchor/programs/fundraiser/src/state/contributor.rs similarity index 100% rename from finance/token-fundraiser/anchor/programs/fundraiser/src/state/contributor.rs rename to finance/fundraiser/anchor/programs/fundraiser/src/state/contributor.rs diff --git a/finance/token-fundraiser/anchor/programs/fundraiser/src/state/fundraiser.rs b/finance/fundraiser/anchor/programs/fundraiser/src/state/fundraiser.rs similarity index 100% rename from finance/token-fundraiser/anchor/programs/fundraiser/src/state/fundraiser.rs rename to finance/fundraiser/anchor/programs/fundraiser/src/state/fundraiser.rs diff --git a/finance/token-fundraiser/anchor/programs/fundraiser/src/state/mod.rs b/finance/fundraiser/anchor/programs/fundraiser/src/state/mod.rs similarity index 100% rename from finance/token-fundraiser/anchor/programs/fundraiser/src/state/mod.rs rename to finance/fundraiser/anchor/programs/fundraiser/src/state/mod.rs diff --git a/finance/token-fundraiser/anchor/programs/fundraiser/tests/test_fundraiser.rs b/finance/fundraiser/anchor/programs/fundraiser/tests/test_fundraiser.rs similarity index 100% rename from finance/token-fundraiser/anchor/programs/fundraiser/tests/test_fundraiser.rs rename to finance/fundraiser/anchor/programs/fundraiser/tests/test_fundraiser.rs diff --git a/finance/vault-strategy/kani-proofs/Cargo.toml b/finance/fundraiser/kani-proofs/Cargo.toml similarity index 93% rename from finance/vault-strategy/kani-proofs/Cargo.toml rename to finance/fundraiser/kani-proofs/Cargo.toml index 029a5d6fd..397035e94 100644 --- a/finance/vault-strategy/kani-proofs/Cargo.toml +++ b/finance/fundraiser/kani-proofs/Cargo.toml @@ -6,7 +6,7 @@ [workspace] [package] -name = "vault-strategy-kani-proofs" +name = "fundraiser-kani-proofs" version = "0.1.0" edition = "2021" publish = false diff --git a/finance/token-fundraiser/kani-proofs/README.md b/finance/fundraiser/kani-proofs/README.md similarity index 93% rename from finance/token-fundraiser/kani-proofs/README.md rename to finance/fundraiser/kani-proofs/README.md index 48837ed54..57de49755 100644 --- a/finance/token-fundraiser/kani-proofs/README.md +++ b/finance/fundraiser/kani-proofs/README.md @@ -1,6 +1,6 @@ -# Token-fundraiser: Kani proofs +# Fundraiser: Kani proofs -Formal-verification harnesses for the token-fundraiser program, in the spirit of +Formal-verification harnesses for the fundraiser program, in the spirit of [`aeyakovenko/percolator`](https://github.com/aeyakovenko/percolator), which uses the [Kani](https://github.com/model-checking/kani) model checker to prove the mathematical correctness of a DeFi engine. diff --git a/finance/token-fundraiser/kani-proofs/src/lib.rs b/finance/fundraiser/kani-proofs/src/lib.rs similarity index 98% rename from finance/token-fundraiser/kani-proofs/src/lib.rs rename to finance/fundraiser/kani-proofs/src/lib.rs index 0323d44d9..7fbb5f5af 100644 --- a/finance/token-fundraiser/kani-proofs/src/lib.rs +++ b/finance/fundraiser/kani-proofs/src/lib.rs @@ -1,4 +1,4 @@ -//! Kani proof harnesses for the token-fundraiser program (`finance/token-fundraiser`). +//! Kani proof harnesses for the fundraiser program (`finance/fundraiser`). //! //! Inspired by aeyakovenko/percolator, which uses the Kani model checker to //! prove the mathematical correctness of a DeFi engine's pure numeric core. diff --git a/finance/token-fundraiser/quasar/CHANGELOG.md b/finance/fundraiser/quasar/CHANGELOG.md similarity index 85% rename from finance/token-fundraiser/quasar/CHANGELOG.md rename to finance/fundraiser/quasar/CHANGELOG.md index 2b8e2761e..bad0263b1 100644 --- a/finance/token-fundraiser/quasar/CHANGELOG.md +++ b/finance/fundraiser/quasar/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## [2026-09-28] + +### Changed + +- Renamed from Token Fundraiser to Fundraiser. The example moved from + `finance/token-fundraiser` to `finance/fundraiser`, and the crate is now + `quasar-fundraiser`. The program's behavior is unchanged. + ## [2026-09-14] ### Added diff --git a/finance/token-fundraiser/quasar/Cargo.toml b/finance/fundraiser/quasar/Cargo.toml similarity index 97% rename from finance/token-fundraiser/quasar/Cargo.toml rename to finance/fundraiser/quasar/Cargo.toml index 15f29e472..da8025739 100644 --- a/finance/token-fundraiser/quasar/Cargo.toml +++ b/finance/fundraiser/quasar/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "quasar-token-fundraiser" +name = "quasar-fundraiser" version = "0.1.0" edition = "2021" diff --git a/finance/vault-strategy/quasar/vault-strategy/Quasar.toml b/finance/fundraiser/quasar/Quasar.toml similarity index 79% rename from finance/vault-strategy/quasar/vault-strategy/Quasar.toml rename to finance/fundraiser/quasar/Quasar.toml index b432a9bbf..0b57b78f4 100644 --- a/finance/vault-strategy/quasar/vault-strategy/Quasar.toml +++ b/finance/fundraiser/quasar/Quasar.toml @@ -1,5 +1,5 @@ [project] -name = "quasar-vault-strategy" +name = "quasar-fundraiser" [testing] command = { program = "cargo", args = ["test"] } diff --git a/finance/token-fundraiser/quasar/README.md b/finance/fundraiser/quasar/README.md similarity index 95% rename from finance/token-fundraiser/quasar/README.md rename to finance/fundraiser/quasar/README.md index 631596bd1..5bcd81dfc 100644 --- a/finance/token-fundraiser/quasar/README.md +++ b/finance/fundraiser/quasar/README.md @@ -1,7 +1,9 @@ -# Solana Token Fundraiser (Quasar) +# Solana Fundraiser (Quasar) Onchain crowdfunding on Solana toward a target amount in a chosen token, written with [Quasar](https://quasar-lang.com/docs). A **maker** opens a fundraiser with a target amount and a deadline; **contributors** deposit tokens into a program-controlled vault. If the target is met the maker withdraws everything; if the deadline passes without the target being met, each contributor reclaims exactly what they put in. +This example was called **Token Fundraiser** (`finance/token-fundraiser`) until it was renamed: contributors receive no token, only a refund if the target is missed. + See also: the [repository catalog](../../../README.md) and the [Anchor variant](../anchor/) of the same program. ## Major concepts @@ -23,7 +25,7 @@ Errors are defined in `src/error.rs` as a `#[error_code]` enum starting at code ## Setup -From `finance/token-fundraiser/quasar/`: +From `finance/fundraiser/quasar/`: ```bash quasar build diff --git a/finance/token-fundraiser/quasar/src/error.rs b/finance/fundraiser/quasar/src/error.rs similarity index 100% rename from finance/token-fundraiser/quasar/src/error.rs rename to finance/fundraiser/quasar/src/error.rs diff --git a/finance/token-fundraiser/quasar/src/instructions/check_contributions.rs b/finance/fundraiser/quasar/src/instructions/check_contributions.rs similarity index 100% rename from finance/token-fundraiser/quasar/src/instructions/check_contributions.rs rename to finance/fundraiser/quasar/src/instructions/check_contributions.rs diff --git a/finance/token-fundraiser/quasar/src/instructions/close_contributor.rs b/finance/fundraiser/quasar/src/instructions/close_contributor.rs similarity index 100% rename from finance/token-fundraiser/quasar/src/instructions/close_contributor.rs rename to finance/fundraiser/quasar/src/instructions/close_contributor.rs diff --git a/finance/token-fundraiser/quasar/src/instructions/contribute.rs b/finance/fundraiser/quasar/src/instructions/contribute.rs similarity index 100% rename from finance/token-fundraiser/quasar/src/instructions/contribute.rs rename to finance/fundraiser/quasar/src/instructions/contribute.rs diff --git a/finance/token-fundraiser/quasar/src/instructions/initialize_fundraiser.rs b/finance/fundraiser/quasar/src/instructions/initialize_fundraiser.rs similarity index 100% rename from finance/token-fundraiser/quasar/src/instructions/initialize_fundraiser.rs rename to finance/fundraiser/quasar/src/instructions/initialize_fundraiser.rs diff --git a/finance/token-fundraiser/quasar/src/instructions/mod.rs b/finance/fundraiser/quasar/src/instructions/mod.rs similarity index 100% rename from finance/token-fundraiser/quasar/src/instructions/mod.rs rename to finance/fundraiser/quasar/src/instructions/mod.rs diff --git a/finance/token-fundraiser/quasar/src/instructions/refund.rs b/finance/fundraiser/quasar/src/instructions/refund.rs similarity index 100% rename from finance/token-fundraiser/quasar/src/instructions/refund.rs rename to finance/fundraiser/quasar/src/instructions/refund.rs diff --git a/finance/token-fundraiser/quasar/src/lib.rs b/finance/fundraiser/quasar/src/lib.rs similarity index 98% rename from finance/token-fundraiser/quasar/src/lib.rs rename to finance/fundraiser/quasar/src/lib.rs index e63f41879..0a18f1697 100644 --- a/finance/token-fundraiser/quasar/src/lib.rs +++ b/finance/fundraiser/quasar/src/lib.rs @@ -15,7 +15,7 @@ declare_id!("Eoiuq1dXvHxh6dLx3wh9gj8kSAUpga11krTrbfF5XYsC"); /// SPL token. Contributors deposit tokens into a vault. If the target is met, /// the maker withdraws everything. If not, contributors can reclaim their funds. #[program] -mod quasar_token_fundraiser { +mod quasar_fundraiser { use super::*; /// Create a new fundraiser with a target amount and duration. diff --git a/finance/token-fundraiser/quasar/src/state.rs b/finance/fundraiser/quasar/src/state.rs similarity index 100% rename from finance/token-fundraiser/quasar/src/state.rs rename to finance/fundraiser/quasar/src/state.rs diff --git a/finance/token-fundraiser/quasar/src/tests.rs b/finance/fundraiser/quasar/src/tests.rs similarity index 100% rename from finance/token-fundraiser/quasar/src/tests.rs rename to finance/fundraiser/quasar/src/tests.rs diff --git a/finance/vault-strategy/VIDEO_SCRIPT.md b/finance/managed-fund/VIDEO_SCRIPT.md similarity index 65% rename from finance/vault-strategy/VIDEO_SCRIPT.md rename to finance/managed-fund/VIDEO_SCRIPT.md index f1becc104..f6ff79e08 100644 --- a/finance/vault-strategy/VIDEO_SCRIPT.md +++ b/finance/managed-fund/VIDEO_SCRIPT.md @@ -1,6 +1,6 @@ -# Vault Strategy: a walkthrough +# Managed Fund: a walkthrough -A video script for the `vault-strategy` example. Target runtime is roughly nine minutes at a normal speaking pace. Narration lines are what the presenter says; the indented blocks are what is on screen as a running ledger of onchain state. +A video script for the `managed-fund` example. Target runtime is roughly nine minutes at a normal speaking pace. Narration lines are what the presenter says; the indented blocks are what is on screen as a running ledger of onchain state. Prices for TSLAx and NVDAx in this script are illustrative and match the rates the example's tests configure. They are not live quotes. USDC (US dollars), TSLAx (Tesla stock) and NVDAx (NVIDIA stock) are real assets; the swap behind the scenes is a deterministic test stand-in, which we will be honest about when we reach it. @@ -8,13 +8,13 @@ Prices for TSLAx and NVDAx in this script are illustrative and match the rates t NARRATION: -Let's build a vault strategy: the onchain equivalent of a mutual fund, or an actively managed ETF. You deposit cash with a manager, you receive shares, the manager invests across several assets and rebalances them over time, and your shares are priced at net asset value: the worth of everything the strategy holds, divided by the shares outstanding. The word net is a finance convention for value after subtracting what a fund owes; this strategy borrows nothing, so its net asset value is simply its holdings. For running the book, the manager earns a fee. +Let's build a managed fund: the onchain equivalent of a mutual fund, sometimes sold onchain as a vault strategy. You deposit cash with a manager, you receive shares, the manager invests across several assets and rebalances them over time, and your shares are priced at net asset value: the worth of everything the fund holds, divided by the shares outstanding. The word net is a finance convention for value after subtracting what a fund owes; this fund borrows nothing, so its net asset value is simply its holdings. For running the book, the manager earns a fee. -By the end you will have watched an asset get approved, a strategy get built, someone deposit, the manager invest and rebalance, a fee accrue, and someone redeem, and you will know which instruction handler does each one. The program controls every dollar the whole time: the manager invests the deposits but can never move them to herself, a limit we will pin down precisely. +By the end you will have watched an asset get approved, a fund get built, someone deposit, the manager invest and rebalance, a fee accrue, and someone redeem, and you will know which instruction handler does each one. The program controls every dollar the whole time: the manager invests the deposits but can never move them to herself, a limit we will pin down precisely. You have seen this shape on Solana, in protocols like Symmetry and Kamino. This is the teaching-sized version. -Two things genuinely change once the strategy is onchain: +Two things genuinely change once the fund is onchain: - The rules are the deployed bytecode. Maria cannot freeze redemptions, the fee is fixed at creation and capped in code at ten percent, and there is no admin lever to pull. - Entry and exit are permissionless and settle instantly. Anyone can deposit or redeem in a single transaction, priced live, with no minimum and no end-of-day cutoff. @@ -27,32 +27,32 @@ NARRATION: Custody is the whole game, so let us name the boxes before we move money. -First, the word vault, because it gets overloaded. By the common standard a vault holds a single asset: you put one kind of token in, you get shares out. A managed mix of several assets is not one vault; it lives in several vaults, one per asset, and is usually called a basket or a fund. Symmetry calls its multi-asset products baskets. We will keep it simple: a vault is one single-asset token account, and the strategy is the whole construct that owns them. So vault strategy reads literally, a strategy built from vaults. +First, the word vault, because it gets overloaded. By the common standard a vault holds a single asset: you put one kind of token in, you get shares out. A managed mix of several assets is not one vault; it lives in several vaults, one per asset, and is usually called a basket or a fund. Symmetry calls its multi-asset products baskets. We will keep it simple: a vault is one single-asset token account, and the fund is the whole construct that owns them. Some platforms call the whole product a vault, or a vault strategy; this example used to be called that too. Here the product is the fund, and the token accounts are its vaults. -The center of everything is the `Strategy` account, whose address is a PDA derived from the seeds `"strategy"` plus Maria's public key. A PDA is an address with no private key: it is found deliberately off the signing curve, so no key can sign for it and only the program can, by supplying the seeds. The strategy PDA is the authority over the USDC vault, every asset vault, and the share mint. Each vault is an associated token account owned by the strategy PDA and holds exactly one asset. The share mint's address is also a PDA, seeds `"share_mint"` plus the strategy address, so it is deterministic, one share mint per strategy, with the strategy PDA as its mint authority. +The center of everything is the `Fund` account, whose address is a PDA derived from the seeds `"fund"` plus Maria's public key. A PDA is an address with no private key: it is found deliberately off the signing curve, so no key can sign for it and only the program can, by supplying the seeds. The fund PDA is the authority over the USDC vault, every asset vault, and the share mint. Each vault is an associated token account owned by the fund PDA and holds exactly one asset. The share mint's address is also a PDA, seeds `"share_mint"` plus the fund address, so it is deterministic, one share mint per fund, with the fund PDA as its mint authority. -The asset set is not fixed. Each asset the strategy holds gets its own small account, an `AssetConfig`, whose address is a PDA seeded by the strategy and an index: zero, one, two, and so on. That indexing matters later: the assets are exactly the range zero up to the count, so any handler that values the whole strategy can re-derive every one and refuse to run if a single asset account is missing. +The asset set is not fixed. Each asset the fund holds gets its own small account, an `AssetConfig`, whose address is a PDA seeded by the fund and an index: zero, one, two, and so on. That indexing matters later: the assets are exactly the range zero up to the count, so any handler that values the whole fund can re-derive every one and refuse to run if a single asset account is missing. -One account sits outside any single strategy: a `Registry`, a curated whitelist of assets that strategies are allowed to hold. We will meet its keeper first. +One account sits outside any single fund: a `Registry`, a curated whitelist of assets that funds are allowed to hold. We will meet its keeper first. ON SCREEN: ``` Registry [off curve - PDA, seeds: "registry" + authority] owner = curator, not a manager -Strategy [off curve - PDA, seeds: "strategy" + manager] +Fund [off curve - PDA, seeds: "fund" + manager] authority over: vault_usdc, every asset vault, share_mint -share_mint [off curve - PDA, seeds: "share_mint" + strategy] authority = Strategy PDA -AssetConfig #i [off curve - PDA, seeds: "asset" + strategy + index] one per asset -vault_usdc / per-asset vaults [off curve - ATAs, one asset each] authority = Strategy PDA +share_mint [off curve - PDA, seeds: "share_mint" + fund] authority = Fund PDA +AssetConfig #i [off curve - PDA, seeds: "asset" + fund + index] one per asset +vault_usdc / per-asset vaults [off curve - ATAs, one asset each] authority = Fund PDA ``` ## Victor approves the assets NARRATION: -Meet Victor. Victor is not a fund manager; he runs the registry, the list of assets any strategy is allowed to hold. His motive is reputational: he is the gatekeeper who vets that an asset is real and has a trustworthy price feed. He calls `initialize_registry` once, then `whitelist_asset` for each approved token, and here is the important part, each whitelist entry binds the mint to its official Pyth price feed. +Meet Victor. Victor is not a fund manager; he runs the registry, the list of assets any fund is allowed to hold. His motive is reputational: he is the gatekeeper who vets that an asset is real and has a trustworthy price feed. He calls `initialize_registry` once, then `whitelist_asset` for each approved token, and here is the important part, each whitelist entry binds the mint to its official Pyth price feed. -Why a separate person at all? Because this is the line that stops fraud. If a manager could add any token to her own strategy, she could mint a worthless token herself, list it, and value it at whatever she liked. And even with a real token, if she could choose its price feed she could point at one she controls. Victor's registry removes both moves: a manager can only ever pick from assets Victor approved, and the price feed comes from Victor's entry, never from the manager. +Why a separate person at all? Because this is the line that stops fraud. If a manager could add any token to her own fund, she could mint a worthless token herself, list it, and value it at whatever she liked. And even with a real token, if she could choose its price feed she could point at one she controls. Victor's registry removes both moves: a manager can only ever pick from assets Victor approved, and the price feed comes from Victor's entry, never from the manager. ON SCREEN: @@ -68,18 +68,18 @@ TOKEN MOVEMENT: none - approvals only Fee generated: none ``` -## Maria opens the strategy +## Maria opens the fund NARRATION: -Maria is our portfolio manager, and she wants to run the basket and earn the fee. She calls `initialize_strategy`, binding her strategy to Victor's registry. She sets two numbers and no assets yet: a fee of one hundred basis points, which is one percent a year, and a maximum slippage of one hundred basis points, which we will use when she trades. Both are capped in code, the fee at ten percent and the slippage tolerance at ten percent, and both are fixed here at creation with no setter to change them later. +Maria is our portfolio manager, and she wants to run the basket and earn the fee. She calls `initialize_fund`, binding her fund to Victor's registry. She sets two numbers and no assets yet: a fee of one hundred basis points, which is one percent a year, and a maximum slippage of one hundred basis points, which we will use when she trades. Both are capped in code, the fee at ten percent and the slippage tolerance at ten percent, and both are fixed here at creation with no setter to change them later. The fee cap exists because the fee is paid by minting new shares to the manager; an uncapped fee would let a manager dilute depositors to nothing by configuration alone. ON SCREEN: ``` -ADDED - Strategy [off curve - PDA] +ADDED - Fund [off curve - PDA] manager: Maria registry: Victor's registry fee_bps: 100 max_slippage_bps: 100 total_shares: 0 asset_count: 0 total_weight_bps: 0 last_fee_accrual_timestamp: now @@ -101,15 +101,15 @@ Two honest notes. The weight is a target Maria maintains by hand with invest and ON SCREEN: ``` -ADDED - AssetConfig #0 [off curve - PDA, seeds: "asset" + strategy + 0] +ADDED - AssetConfig #0 [off curve - PDA, seeds: "asset" + fund + 0] mint: TSLAx price_feed: weight_bps: 4000 vault: vault_tsla ADDED - vault_tsla (empty) -ADDED - AssetConfig #1 [off curve - PDA, seeds: "asset" + strategy + 1] +ADDED - AssetConfig #1 [off curve - PDA, seeds: "asset" + fund + 1] mint: NVDAx price_feed: weight_bps: 6000 vault: vault_nvda ADDED - vault_nvda (empty) -UPDATED - Strategy asset_count: 0 -> 2 total_weight_bps: 0 -> 10000 +UPDATED - Fund asset_count: 0 -> 2 total_weight_bps: 0 -> 10000 TOKEN MOVEMENT: none - the vaults start empty Fee generated: none @@ -119,22 +119,22 @@ Fee generated: none NARRATION: -Alice wants exposure to both stocks without buying and rebalancing them herself, so she calls `deposit` with 900 USDC. `deposit` is permissionless: any user can call it. This is buying into the strategy. +Alice wants exposure to both stocks without buying and rebalancing them herself, so she calls `deposit` with 900 USDC. `deposit` is permissionless: any user can call it. This is buying into the fund. -The handler prices her shares against net asset value. It walks the complete asset set, index zero then index one, reading the holding the strategy has recorded for each vault and each Pyth price, and it will not proceed unless every asset's accounts are present, so nothing can be hidden from the valuation. The strategy is empty, so net asset value is zero, and the first deposit is defined as one to one. Alice gets 900 shares. Shares carry six decimals, so under the hood that is 900 million minor units, but think of it as 900 shares worth a dollar each. +The handler prices her shares against net asset value. It walks the complete asset set, index zero then index one, reading the holding the fund has recorded for each vault and each Pyth price, and it will not proceed unless every asset's accounts are present, so nothing can be hidden from the valuation. The fund is empty, so net asset value is zero, and the first deposit is defined as one to one. Alice gets 900 shares. Shares carry six decimals, so under the hood that is 900 million minor units, but think of it as 900 shares worth a dollar each. -Checks, effects, interactions: the handler raises `total_shares` first, then pulls her USDC into the USDC vault, then mints her the shares with the strategy PDA signing. +Checks, effects, interactions: the handler raises `total_shares` first, then pulls her USDC into the USDC vault, then mints her the shares with the fund PDA signing. ON SCREEN: ``` -UPDATED - Strategy total_shares: 0 -> 900,000,000 +UPDATED - Fund total_shares: 0 -> 900,000,000 UPDATED - vault_usdc 0 -> 900 USDC UPDATED - Alice share ATA 0 -> 900 shares TOKEN MOVEMENT: Alice USDC ATA -> vault_usdc 900 USDC (deposit) - share_mint -> Alice share ATA 900 shares (minted, Strategy PDA signs) + share_mint -> Alice share ATA 900 shares (minted, Fund PDA signs) Fee generated: none - deposits do not accrue fees ``` @@ -145,7 +145,7 @@ NARRATION: Now Maria earns her title. She calls `invest` twice, manager-only. It hands the swap to the registered router, which for this example is a deterministic mock: at a fixed rate it mints the asset into the matching vault and takes the USDC. First, 360 dollars into TSLAx at 250 dollars a share, so the TSLAx vault receives 1.44 TSLAx. Then 540 dollars into NVDAx at 180 dollars a share, so the NVDAx vault receives exactly 3 NVDAx. That is the 40/60 split, by hand. -The slippage guard is the part worth watching. Maria does not get to hand in a minimum, the program computes one. It reads the asset's Pyth price, works out how much the swap should return, and refuses anything more than her one percent tolerance below that. A bad or manipulated quote reverts instead of quietly draining the vault. The strategy PDA signs the swap, because the USDC leaves a vault only it controls. +The slippage guard is the part worth watching. Maria does not get to hand in a minimum, the program computes one. It reads the asset's Pyth price, works out how much the swap should return, and refuses anything more than her one percent tolerance below that. A bad or manipulated quote reverts instead of quietly draining the vault. The fund PDA signs the swap, because the USDC leaves a vault only it controls. ON SCREEN: @@ -179,7 +179,7 @@ ON SCREEN: ``` Net asset value before Bob: 0 + 1.44 x 250 + 3.0 x 200 = 360 + 600 = 960 USDC -UPDATED - Strategy total_shares: 900,000,000 -> 1,350,000,000 +UPDATED - Fund total_shares: 900,000,000 -> 1,350,000,000 UPDATED - vault_usdc 0 -> 480 USDC UPDATED - Bob share ATA 0 -> 450 shares @@ -194,9 +194,9 @@ Fee generated: none NARRATION: -NVIDIA's run pushed the holdings away from 40/60, so Maria calls `rebalance`. One handler, two swaps, both signed by the strategy PDA: it sells one asset for USDC, then spends that USDC on the other. Each leg carries the same oracle-computed floor as invest, so neither can be filled at a bad price. +NVIDIA's run pushed the holdings away from 40/60, so Maria calls `rebalance`. One handler, two swaps, both signed by the fund PDA: it sells one asset for USDC, then spends that USDC on the other. Each leg carries the same oracle-computed floor as invest, so neither can be filled at a bad price. -She sells 0.36 TSLAx, receiving 90 dollars, then buys 0.5 NVDAx with that same 90 dollars. The USDC vault nets to zero change across the two legs; the strategy just shifts weight from Tesla into NVIDIA. +She sells 0.36 TSLAx, receiving 90 dollars, then buys 0.5 NVDAx with that same 90 dollars. The USDC vault nets to zero change across the two legs; the fund just shifts weight from Tesla into NVIDIA. ON SCREEN: @@ -229,12 +229,12 @@ elapsed: 1 year (illustrative) fee_shares = total_shares x fee_bps x elapsed / (10,000 x seconds_per_year) = 1,350,000,000 x 100 x 1yr / (10,000 x 1yr) = 13,500,000 (13.5 shares) -UPDATED - Strategy total_shares: 1,350,000,000 -> 1,363,500,000 +UPDATED - Fund total_shares: 1,350,000,000 -> 1,363,500,000 last_fee_accrual_timestamp: updated UPDATED - Maria share ATA 0 -> 13.5 shares TOKEN MOVEMENT: - share_mint -> Maria share ATA 13.5 shares (minted, Strategy PDA signs) + share_mint -> Maria share ATA 13.5 shares (minted, Fund PDA signs) Fee generated: 13.5 shares to the manager; all other holders diluted ~1% ``` @@ -242,7 +242,7 @@ Fee generated: 13.5 shares to the manager; all other holders diluted ~1% NARRATION: -Alice calls `withdraw` and burns all 900 of her shares. Here is the part people miss: withdrawal is in kind and proportional. She does not get cash. She gets her exact fraction of every balance the strategy holds, across the USDC vault and both asset vaults. It is the same move an ETF makes when it redeems in kind, handing back the underlying holdings instead of cash. Just like deposit, the handler insists on seeing every asset, so her slice is computed against the whole strategy. +Alice calls `withdraw` and burns all 900 of her shares. Here is the part people miss: withdrawal is in kind and proportional. She does not get cash. She gets her exact fraction of every balance the fund holds, across the USDC vault and both asset vaults. It is the same move an ETF makes when it redeems in kind, handing back the underlying holdings instead of cash. Just like deposit, the handler insists on seeing every asset, so her slice is computed against the whole fund. Her fraction is 900 shares out of the 1,363.5 that now exist. The handler floors each amount in the protocol's favor, so any rounding dust stays with the remaining holders. @@ -255,7 +255,7 @@ amount_usdc = 480,000,000 x 900,000,000 / 1,363,500,000 = 316,831,683 (316.83 U amount_tsla = 1,080,000 x 900,000,000 / 1,363,500,000 = 712,871 (0.712871 TSLAx, floor) amount_nvda = 3,500,000 x 900,000,000 / 1,363,500,000 = 2,310,231 (2.310231 NVDAx, floor) -UPDATED - Strategy total_shares: 1,363,500,000 -> 463,500,000 +UPDATED - Fund total_shares: 1,363,500,000 -> 463,500,000 UPDATED - Alice share ATA 900 shares -> 0 (burned) TOKEN MOVEMENT: @@ -279,7 +279,7 @@ We promised to pin down what the manager can and cannot do, so here it is, now t - Her fee is fixed at creation, capped at ten percent, and paid only in newly minted shares. - No instruction anywhere sends a vault's tokens to the manager. She directs the assets; she cannot withdraw them. -What is left to trust, honestly, is the router and registry the strategy was pointed at. With an honest router the worst a careless manager can do is churn and pay market slippage, which hurts depositors but does not enrich her. She cannot abscond with the principal. +What is left to trust, honestly, is the router and registry the fund was pointed at. With an honest router the worst a careless manager can do is churn and pay market slippage, which hurts depositors but does not enrich her. She cannot abscond with the principal. ## Reconcile, and where everyone ended up @@ -287,13 +287,13 @@ NARRATION: Let us check the books. USDC into the USDC vault was 900 from Alice plus 480 from Bob, 1,380 total. The invests sent 900 to the router; rebalance was a wash. That leaves 480 in the USDC vault, and after Alice's withdrawal, 163.17 remains. Tokens in equal tokens out. -So: Alice came in with 900 dollars, rode NVIDIA up, paid her share of a one percent fee through dilution, and left with about 957 dollars of assets, in kind. The strategy passes returns through in both directions: had NVIDIA fallen instead of risen, the same arithmetic would have redeemed Alice for less than her 900 dollars. That market risk is hers, and the program neither cushions it nor hides it. Bob bought in fairly at the higher share price and still holds 450 shares worth roughly 478 dollars. Maria earned 13.5 shares, about 14 dollars, for running the book. Victor's only role was the guest list. The strategy held custody from the first deposit to the last withdrawal, the manager never touched the vaults with her own key, and the fee she could charge was capped in the bytecode. +So: Alice came in with 900 dollars, rode NVIDIA up, paid her share of a one percent fee through dilution, and left with about 957 dollars of assets, in kind. The fund passes returns through in both directions: had NVIDIA fallen instead of risen, the same arithmetic would have redeemed Alice for less than her 900 dollars. That market risk is hers, and the program neither cushions it nor hides it. Bob bought in fairly at the higher share price and still holds 450 shares worth roughly 478 dollars. Maria earned 13.5 shares, about 14 dollars, for running the book. Victor's only role was the guest list. The fund held custody from the first deposit to the last withdrawal, the manager never touched the vaults with her own key, and the fee she could charge was capped in the bytecode. ## Two honest footnotes NARRATION: -First, the swap router here is a deterministic test stand-in. It mints and burns at a fixed rate with no spread, and its rate matches the Pyth price, which keeps the math clean for teaching. A real deployment would call out to a live venue, and the strategy would only trust the one router address it registered at creation. That registration is checked on every invest and rebalance. +First, the swap router here is a deterministic test stand-in. It mints and burns at a fixed rate with no spread, and its rate matches the Pyth price, which keeps the math clean for teaching. A real deployment would call out to a live venue, and the fund would only trust the one router address it registered at creation. That registration is checked on every invest and rebalance. Second, two limits worth naming. The weights are a target Maria maintains, not an allocation the program enforces on each deposit; enforcing them in-program is a reasonable thing to add. And the basket is add-only in this version, you can grow it but not prune it, because the assets are addressed by a contiguous index and removing one would leave a gap. Both are clean extensions, not assumptions baked in. diff --git a/finance/vault-strategy/anchor-v1/.gitignore b/finance/managed-fund/anchor-v1/.gitignore similarity index 100% rename from finance/vault-strategy/anchor-v1/.gitignore rename to finance/managed-fund/anchor-v1/.gitignore diff --git a/finance/vault-strategy/anchor/Anchor.toml b/finance/managed-fund/anchor-v1/Anchor.toml similarity index 85% rename from finance/vault-strategy/anchor/Anchor.toml rename to finance/managed-fund/anchor-v1/Anchor.toml index f13e05b84..2ac67a144 100644 --- a/finance/vault-strategy/anchor/Anchor.toml +++ b/finance/managed-fund/anchor-v1/Anchor.toml @@ -8,7 +8,7 @@ resolution = true skip-lint = false [programs.localnet] -vault_strategy = "VLT5W7bqhRN4nCdRpXm8UfHRxZd9EuZGqiSAkGHQfGh" +managed_fund = "VLT5W7bqhRN4nCdRpXm8UfHRxZd9EuZGqiSAkGHQfGh" mock_swap_router = "SWPR8Rk3aq3DrDGLdaANq7xCMnXoUFUJWJJmCWxc8Jm" [provider] diff --git a/finance/vault-strategy/anchor-v1/CHANGELOG.md b/finance/managed-fund/anchor-v1/CHANGELOG.md similarity index 89% rename from finance/vault-strategy/anchor-v1/CHANGELOG.md rename to finance/managed-fund/anchor-v1/CHANGELOG.md index 9514752e0..a303322e1 100644 --- a/finance/vault-strategy/anchor-v1/CHANGELOG.md +++ b/finance/managed-fund/anchor-v1/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## 2026-09-28 + +- **Renamed from Vault Strategy to Managed Fund.** The example moved from `finance/vault-strategy` to `finance/managed-fund` and now uses the finance term for the product. The program crate is `managed-fund` (library `managed_fund`), the `Strategy` account is `Fund`, `initialize_strategy` is `initialize_fund`, the `StrategyNotFullyAllocated` error is `FundNotFullyAllocated`, and the fund PDA's seed is `"fund"` instead of `"strategy"`, so fund addresses change. The `initialize_fund` instruction and `Fund` account have new Anchor discriminators, and the web app's IDL and client follow. `vault` still means only a single-asset token account the fund owns. The program ID and the program's behavior are unchanged. + ## 2026-09-22 - **Donations are ignored.** The strategy records what it holds (`usdc_holdings` and `asset_holdings` on `Strategy`) and prices shares and pays withdrawals from those records, not from the vaults' token balances. `deposit`, `withdraw` and `rebalance` update them with what each transfer actually moved. Tokens transferred straight into a vault are outside the fund: they cannot inflate the share price, are never paid out, and `rebalance` can neither sell nor spend them (new `InsufficientHoldings` error). This closes the first-depositor inflation attack. Tested by `test_donation_does_not_inflate_share_price` and `test_rebalance_cannot_spend_donated_usdc`, and `test_full_lifecycle` checks the records against the vault balances after every step. diff --git a/finance/vault-strategy/anchor-v1/Cargo.toml b/finance/managed-fund/anchor-v1/Cargo.toml similarity index 100% rename from finance/vault-strategy/anchor-v1/Cargo.toml rename to finance/managed-fund/anchor-v1/Cargo.toml diff --git a/finance/vault-strategy/anchor/PRODUCT.md b/finance/managed-fund/anchor-v1/PRODUCT.md similarity index 59% rename from finance/vault-strategy/anchor/PRODUCT.md rename to finance/managed-fund/anchor-v1/PRODUCT.md index 0fa357ccb..66e553934 100644 --- a/finance/vault-strategy/anchor/PRODUCT.md +++ b/finance/managed-fund/anchor-v1/PRODUCT.md @@ -10,17 +10,17 @@ web The frontend serves two roles first; a third exists in the program and is secondary for now. -- **Depositor (primary):** a retail user who deposits USDC into a manager-run strategy, receives shares representing proportional ownership of the whole basket, watches their position's value track the portfolio, and withdraws their slice in kind when they choose. Highest-volume, lowest-expertise surface. -- **Manager (primary):** a power user who creates and operates a strategy — registers curator-approved assets, sets target weights, rebalances as prices drift, and collects the management fee. An operations surface where correctness and confirmation matter more than reach. +- **Depositor (primary):** a retail user who deposits USDC into a manager-run fund, receives shares representing proportional ownership of the whole basket, watches their position's value track the portfolio, and withdraws their slice in kind when they choose. Highest-volume, lowest-expertise surface. +- **Manager (primary):** a power user who creates and operates a fund — registers curator-approved assets, sets target weights, rebalances as prices drift, and collects the management fee. An operations surface where correctness and confirmation matter more than reach. - **Curator (secondary, not yet in scope):** governs the approved-asset registry that bounds what managers may hold. Low-frequency, high-trust. Recorded so future work does not treat the manager as the only privileged role. ## Product Purpose -An **educational demo dApp** shipped alongside the `vault-strategy` Solana program (part of a public Solana examples collection). Its job is to make the program's mechanics legible and touchable — show, through a real interface, how deposits are priced by NAV, how shares represent ownership, how managers configure weights and rebalance, and how in-kind withdrawal works. Success is a visitor understanding and exercising the full deposit → hold → withdraw and configure → allocate → rebalance loops against the deployed program. It teaches by letting people operate it, not by describing it. +An **educational demo dApp** shipped alongside the `managed-fund` Solana program (part of a public Solana examples collection). Its job is to make the program's mechanics legible and touchable — show, through a real interface, how deposits are priced by NAV, how shares represent ownership, how managers configure weights and rebalance, and how in-kind withdrawal works. Success is a visitor understanding and exercising the full deposit → hold → withdraw and configure → allocate → rebalance loops against the deployed program. It teaches by letting people operate it, not by describing it. ## Positioning -A **multi-asset** strategy built from single-asset vaults, transparently priced on-chain. Unlike an ERC-4626-style single-asset vault, one "strategy" owns one vault per asset plus a USDC vault, and every deposit is deployed across the basket at its target weights in the same transaction — there is no idle-cash mode. Deposit pricing, slippage floors, and fees are all derived on-chain from the Pyth oracle and the strategy's own parameters rather than trusted from a caller, which is the truth the interface must make visible: the numbers a user sees are the numbers the program enforces. +A **multi-asset** fund built from single-asset vaults, transparently priced on-chain. Unlike an ERC-4626-style single-asset vault, one "fund" owns one vault per asset plus a USDC vault, and every deposit is deployed across the basket at its target weights in the same transaction — there is no idle-cash mode. Deposit pricing, slippage floors, and fees are all derived on-chain from the Pyth oracle and the fund's own parameters rather than trusted from a caller, which is the truth the interface must make visible: the numbers a user sees are the numbers the program enforces. ## Operating Context @@ -35,14 +35,14 @@ A **multi-asset** strategy built from single-asset vaults, transparently priced Program instructions the frontend can surface: - **Registry / curator:** `initialize_registry`, `approve_asset` (whitelist of assets managers may use). -- **Manager:** `initialize_strategy`, `add_asset`, `set_weight` (including set-to-zero to retire an asset), `rebalance`, `collect_fees`. +- **Manager:** `initialize_fund`, `add_asset`, `set_weight` (including set-to-zero to retire an asset), `rebalance`, `collect_fees`. - **Depositor:** `deposit` (USDC → minted shares), `withdraw` (burn shares → in-kind slice of every vault). Rules the UI must respect and reflect: -- Deposits are accepted only when target weights sum to **exactly 10,000 bps**; a strategy is either still being configured or fully allocated and live (`StrategyNotFullyAllocated` otherwise). -- Shares: first deposit is 1:1 with USDC minor units; later deposits mint `deposit_usdc × total_shares / NAV`. Share mint is a PDA owned by the strategy PDA. -- NAV and withdrawals use the strategy's recorded holdings (`usdc_holdings`, `asset_holdings`), not vault token balances. Tokens sent straight to a vault are not part of the fund and should not be shown as fund value. A deposit too small to buy any of an asset is rejected (`DepositTooSmall`); a rebalance cannot sell or spend more than the recorded holdings (`InsufficientHoldings`). +- Deposits are accepted only when target weights sum to **exactly 10,000 bps**; a fund is either still being configured or fully allocated and live (`FundNotFullyAllocated` otherwise). +- Shares: first deposit is 1:1 with USDC minor units; later deposits mint `deposit_usdc × total_shares / NAV`. Share mint is a PDA owned by the fund PDA. +- NAV and withdrawals use the fund's recorded holdings (`usdc_holdings`, `asset_holdings`), not vault token balances. Tokens sent straight to a vault are not part of the fund and should not be shown as fund value. A deposit too small to buy any of an asset is rejected (`DepositTooSmall`); a rebalance cannot sell or spend more than the recorded holdings (`InsufficientHoldings`). - Management fee is charged by minting new shares to the manager (dilution), fixed at creation, capped at `MAX_FEE_BPS` = 1,000 bps (10%), no setter to raise it. `collect_fees` is permissionless. - Slippage floors are computed on-chain from the Pyth price and `max_slippage_bps` (capped at 1,000 bps); a manager-supplied minimum is not trusted. - `MAX_ASSETS` = 16. `deposit` re-derives the full `0..asset_count` PDA range and refuses to run if any asset account is missing (`IncompleteAssetAccounts`), so NAV can't be understated. @@ -52,12 +52,12 @@ Two program ports exist in the repo — an **Anchor** implementation and a **Qua ## Brand Commitments -Existing name: **Vault Strategy** (the on-chain construct is a "strategy" built from single-asset "vaults"; the README is deliberate about this distinction). No logo, palette, or visual identity is committed yet. Voice in existing docs is precise, plain, and educational — it defines terms and links canonical references rather than hyping. Future design should not contradict that register. +Existing name: **Managed Fund** (the on-chain construct is a "fund" built from single-asset "vaults"; the README is deliberate about this distinction). No logo, palette, or visual identity is committed yet. Voice in existing docs is precise, plain, and educational — it defines terms and links canonical references rather than hyping. Future design should not contradict that register. ## Evidence on Hand - `anchor/README.md` — thorough product and financial-concept documentation (NAV, shares, fees, weights, slippage, in-kind withdrawal, transaction-size limits). -- `VIDEO_SCRIPT.md` at the `vault-strategy` root — an existing narrative walkthrough of the product. +- `VIDEO_SCRIPT.md` at the `managed-fund` root — an existing narrative walkthrough of the product. - `anchor/CHANGELOG.md`, `quasar/CHANGELOG.md` — history. - Program source and tests under `anchor/programs/` and `quasar/`. - No real users, testimonials, deployment addresses, or production metrics exist; the demo runs on devnet with mock tokens. Future work must not fabricate mainnet deployment claims, real AUM, or user counts. @@ -66,7 +66,7 @@ Existing name: **Vault Strategy** (the on-chain construct is a "strategy" built 1. **The interface shows what the program enforces.** Every number a user sees (NAV, share price, slippage floor, fee) is derived on-chain; surface the real computed values, never a client-side approximation presented as truth. 2. **Teach by operating.** This is a demo whose purpose is comprehension — make each step of the deposit/withdraw and configure/rebalance loops legible as the user performs it. -3. **Respect the state machine.** A strategy is either being configured or live; deposits require exactly-10,000-bps allocation. The UI should make the current phase and what's required to advance it obvious rather than surfacing raw errors. +3. **Respect the state machine.** A fund is either being configured or live; deposits require exactly-10,000-bps allocation. The UI should make the current phase and what's required to advance it obvious rather than surfacing raw errors. 4. **Distinguish the roles.** Depositor and manager have different jobs, risks, and permissions; do not collapse them into one undifferentiated view. 5. **Devnet honesty.** Present the demo as a demo — mock tokens, devnet, a mock swap router — without dressing it as production. diff --git a/finance/vault-strategy/anchor-v1/README.md b/finance/managed-fund/anchor-v1/README.md similarity index 61% rename from finance/vault-strategy/anchor-v1/README.md rename to finance/managed-fund/anchor-v1/README.md index 40ff72500..bc2c5b065 100644 --- a/finance/vault-strategy/anchor-v1/README.md +++ b/finance/managed-fund/anchor-v1/README.md @@ -1,4 +1,4 @@ -# Solana Vault Strategy (Anchor) +# Solana Managed Fund (Anchor) > [!NOTE] > This is the **Anchor v1** copy of this example, on Anchor 1.2.0, the current @@ -6,17 +6,17 @@ > `avm install 1.2.0 && avm use 1.2.0`. The Anchor v2 version of this example is in > [`../anchor`](../anchor/). -A manager-run investment vault on Solana. Users deposit [USDC](https://www.investopedia.com/terms/u/usd-coin-usdc.asp) and receive shares representing proportional ownership of a portfolio of assets. The manager adds assets a curator has approved and sets their target weights; each deposit is deployed across those assets at its weights in the same transaction. The manager rebalances as prices drift, earns a fee, and depositors withdraw their proportional slice in kind when they choose. +A manager-run investment fund on Solana, the onchain equivalent of a mutual fund. Users deposit [USDC](https://www.investopedia.com/terms/u/usd-coin-usdc.asp) and receive shares representing proportional ownership of a portfolio of assets. The manager adds assets a curator has approved and sets their target weights; each deposit is deployed across those assets at its weights in the same transaction. The manager rebalances as prices drift, earns a fee, and depositors withdraw their proportional slice in kind when they choose. The example uses two stocks as the portfolio assets: **TSLAx** (Tesla) and **NVDAx** (NVIDIA) - [xStocks](https://backed.fi/xstocks) issued on Solana by Backed Finance. In tests these are mock [tokens](https://solana.com/docs/terminology#token). -A note on the word **vault**: by the common standard (ERC-4626) a vault holds a single asset. Here a vault is one single-asset [token account](https://solana.com/docs/terminology#token-account), and the whole multi-asset construct is the **strategy**, which owns one vault per asset plus a USDC vault. So "vault strategy" reads literally: a strategy built from vaults. +A note on the word **vault**: by the common standard (ERC-4626) a vault holds a single asset. Here a vault is one single-asset [token account](https://solana.com/docs/terminology#token-account), and the whole multi-asset construct is the **fund**, which owns one vault per asset plus a USDC vault. Some platforms call the whole product a vault, or a **vault strategy**. This example was called `vault-strategy` until it was renamed `managed-fund`, so a search for either name lands here. --- ## Programs -- **`vault-strategy`**: Registry and approved assets, strategy creation, asset registration, deposits, share minting, fee accrual, rebalancing, withdrawals +- **`managed-fund`**: Registry and approved assets, fund creation, asset registration, deposits, share minting, fee accrual, rebalancing, withdrawals - **`mock-swap-router`**: Test-only fake Jupiter. Stores exchange rates, mints/burns basket tokens for USDC. Replaced by real [Jupiter](https://jup.ag) in production. --- @@ -25,9 +25,9 @@ A note on the word **vault**: by the common standard (ERC-4626) a vault holds a ### Net Asset Value (NAV) -[NAV](https://www.investopedia.com/terms/n/nav.asp) is the total value of everything the strategy holds: its USDC plus each asset valued at its Pyth price. It prices new deposits fairly, so every depositor pays the same per-share price regardless of when they join. +[NAV](https://www.investopedia.com/terms/n/nav.asp) is the total value of everything the fund holds: its USDC plus each asset valued at its Pyth price. It prices new deposits fairly, so every depositor pays the same per-share price regardless of when they join. -The amounts come from the strategy's own records, `usdc_holdings` and `asset_holdings`, not from the vaults' token balances. Deposits, swaps and withdrawals update them with what each transfer actually moved, so they always equal what the fund owns. Anyone can transfer tokens straight into a vault, and those tokens (a donation) are outside the fund: they change a vault's balance and nothing the program reads. That is the defense against the first-depositor inflation attack, where a dust-sized first deposit followed by a donation would otherwise price one share above the next deposit and round it down to zero shares. Donated tokens are never paid out, and `rebalance` can neither sell nor spend them (`InsufficientHoldings`). +The amounts come from the fund's own records, `usdc_holdings` and `asset_holdings`, not from the vaults' token balances. Deposits, swaps and withdrawals update them with what each transfer actually moved, so they always equal what the fund owns. Anyone can transfer tokens straight into a vault, and those tokens (a donation) are outside the fund: they change a vault's balance and nothing the program reads. That is the defense against the first-depositor inflation attack, where a dust-sized first deposit followed by a donation would otherwise price one share above the next deposit and round it down to zero shares. Donated tokens are never paid out, and `rebalance` can neither sell nor spend them (`InsufficientHoldings`). Because the asset set is dynamic, `deposit` must value *every* asset. The assets live at PDAs indexed `0..asset_count`, and `deposit` re-derives that complete range from the accounts it is given, refusing to run if any asset is missing (`IncompleteAssetAccounts`). This makes it structurally impossible to omit an asset and understate NAV. @@ -37,12 +37,12 @@ Prices come from [Pyth Network](https://pyth.network/) `PriceUpdateV2` accounts. ### Shares -A [share](https://www.investopedia.com/terms/s/shares.asp) represents a fraction of the whole strategy. Hold 1% of shares and you own 1% of every vault. +A [share](https://www.investopedia.com/terms/s/shares.asp) represents a fraction of the whole fund. Hold 1% of shares and you own 1% of every vault. - **First deposit**: shares are issued 1:1 with USDC minor units (initial price of 1 USDC per share). - **Later deposits**: `shares_to_mint = deposit_usdc × total_shares / NAV`, with NAV valued from the recorded holdings. - **A deposit leg must buy something.** A deposit so small that one of its swaps spends USDC and returns none of the asset is refused (`DepositTooSmall`). Otherwise it would mint shares against no recorded value, and every later deposit would divide by a zero NAV. -- Shares are [SPL tokens](https://solana.com/docs/terminology#token); the share mint's address is a [PDA](https://solana.com/docs/terminology#program-derived-address-pda), so it is deterministic and the strategy PDA is its mint authority. +- Shares are [SPL tokens](https://solana.com/docs/terminology#token); the share mint's address is a [PDA](https://solana.com/docs/terminology#program-derived-address-pda), so it is deterministic and the fund PDA is its mint authority. ### Management Fee @@ -56,13 +56,13 @@ fee_shares = total_shares × fee_bps × elapsed_seconds / (10_000 × 31_536_000) ### Weights and Rebalancing -Each asset carries a target **weight** in basis points (e.g. 40% TSLAx, 60% NVDAx). A strategy accepts deposits only once its weights sum to exactly 10,000 (`add_asset` and `set_weight` keep the running sum at or below 10,000; `deposit` requires it to equal 10,000, else `StrategyNotFullyAllocated`). So a strategy is either still being configured or fully allocated and live, and `deposit` deploys each depositor's USDC straight into the basket at those weights, fully invested bar sub-cent rounding dust. There is no idle-cash mode. +Each asset carries a target **weight** in basis points (e.g. 40% TSLAx, 60% NVDAx). A fund accepts deposits only once its weights sum to exactly 10,000 (`add_asset` and `set_weight` keep the running sum at or below 10,000; `deposit` requires it to equal 10,000, else `FundNotFullyAllocated`). So a fund is either still being configured or fully allocated and live, and `deposit` deploys each depositor's USDC straight into the basket at those weights, fully invested bar sub-cent rounding dust. There is no idle-cash mode. [Rebalancing](https://www.investopedia.com/terms/r/rebalancing.asp) handles the drift that prices create after a deposit: `rebalance` sells an over-weight asset for USDC and buys an under-weight one in a single atomic instruction. `set_weight` changes a target after creation, including setting it to zero to **retire** an asset: deposits stop allocating to it, the manager sells its holdings out with `rebalance`, and the now-empty vault keeps its index so the contiguous `0..asset_count` range stays intact (the index is never reused). ### Slippage, bounded by the oracle -[Slippage](https://www.investopedia.com/terms/s/slippage.asp) is the gap between the expected and the realized amount of a swap. Rather than trust a manager-supplied minimum, `deposit` and `rebalance` compute the floor themselves from the Pyth price and the strategy's `max_slippage_bps`: a swap whose output falls more than that tolerance below the oracle-implied amount reverts. `max_slippage_bps` is set at creation and capped at `MAX_SLIPPAGE_BPS` (1,000 bps = 10%). +[Slippage](https://www.investopedia.com/terms/s/slippage.asp) is the gap between the expected and the realized amount of a swap. Rather than trust a manager-supplied minimum, `deposit` and `rebalance` compute the floor themselves from the Pyth price and the fund's `max_slippage_bps`: a swap whose output falls more than that tolerance below the oracle-implied amount reverts. `max_slippage_bps` is set at creation and capped at `MAX_SLIPPAGE_BPS` (1,000 bps = 10%). ### In-Kind Withdrawal @@ -75,9 +75,9 @@ An [in-kind distribution](https://www.investopedia.com/terms/i/in-kind.asp) retu ### Participants - **Victor**, the registry authority: curates which assets, and which official Pyth feed, are safe to hold. A protocol role, not a manager. -- **Maria**, the strategy manager: earns a 1% annual fee running a basket she has a thesis on. +- **Maria**, the fund manager: earns a 1% annual fee running a basket she has a thesis on. - **Alice**, the early depositor: wants diversified TSLAx and NVDAx exposure without managing positions. -- **Bob**, the later depositor: joins the same strategy after it has been running. +- **Bob**, the later depositor: joins the same fund after it has been running. `Maria` and `Victor` are stored as plain `Pubkey`s and may each be a [Squads](https://squads.so/) multisig; the program only checks the signature. @@ -85,17 +85,17 @@ An [in-kind distribution](https://www.investopedia.com/terms/i/in-kind.asp) retu `initialize_registry()` creates a `Registry` PDA (`["registry", victor]`) owned by Victor. The registry holds no list; it only names the curator. The approved set is the collection of `ApprovedAsset` accounts under it: `approve_asset(price_feed)` creates one `ApprovedAsset` PDA (`["approved_asset", registry, mint]`) per approved mint, binding it to its official Pyth feed, and an asset counts as approved exactly when that account exists. Only Victor can create them. This separation is the anti-fraud core: a manager can only ever add assets Victor approved, and the feed comes from the registry, so a manager cannot list a token they mint themselves or pair a real mint with a feed they control. -### Maria initializes the strategy +### Maria initializes the fund -`initialize_strategy(index=0, fee_bps=100, max_slippage_bps=100, swap_router)` creates the `Strategy` PDA (`["strategy", 0]`), the share mint, and the USDC vault, binding the strategy to Victor's registry. The strategy is addressed by a caller-chosen index (`"strategy" + 0`, `"strategy" + 1`, …) rather than the manager's key. No assets yet. +`initialize_fund(index=0, fee_bps=100, max_slippage_bps=100, swap_router)` creates the `Fund` PDA (`["fund", 0]`), the share mint, and the USDC vault, binding the fund to Victor's registry. The fund is addressed by a caller-chosen index (`"fund" + 0`, `"fund" + 1`, …) rather than the manager's key. No assets yet. ### Maria adds assets -`add_asset(weight_bps)`, once per asset, creates an `AssetConfig` at `["asset", strategy, index]` (index = current `asset_count`), copies the official feed from the ApprovedAsset account, and creates that asset's vault. TSLAx at index 0 (4000 bps), NVDAx at index 1 (6000 bps). Rejected if the mint is not approved (its ApprovedAsset account does not exist), if the weights would exceed 10,000 bps, or once `MAX_ASSETS` (16) is reached. Deposits stay closed until the weights sum to exactly 10,000. +`add_asset(weight_bps)`, once per asset, creates an `AssetConfig` at `["asset", fund, index]` (index = current `asset_count`), copies the official feed from the ApprovedAsset account, and creates that asset's vault. TSLAx at index 0 (4000 bps), NVDAx at index 1 (6000 bps). Rejected if the mint is not approved (its ApprovedAsset account does not exist), if the weights would exceed 10,000 bps, or once `MAX_ASSETS` (16) is reached. Deposits stay closed until the weights sum to exactly 10,000. ### Alice deposits, and her money is deployed at once -`deposit(usdc_amount, minimum_shares)`, with each asset's `[asset_config, vault, mint, rate, price_feed]` passed as remaining accounts, plus the router accounts. The handler requires the strategy to be fully allocated, values every asset's recorded holding for NAV (first deposit is 1:1), mints shares to Alice, then deploys her USDC across the basket at its target weights through the router, each leg under an oracle slippage floor. With the weights at 40/60, a 900 USDC deposit lands as 1.44 TSLAx and 3.0 NVDAx with no idle USDC. +`deposit(usdc_amount, minimum_shares)`, with each asset's `[asset_config, vault, mint, rate, price_feed]` passed as remaining accounts, plus the router accounts. The handler requires the fund to be fully allocated, values every asset's recorded holding for NAV (first deposit is 1:1), mints shares to Alice, then deploys her USDC across the basket at its target weights through the router, each leg under an oracle slippage floor. With the weights at 40/60, a 900 USDC deposit lands as 1.44 TSLAx and 3.0 NVDAx with no idle USDC. ### Bob deposits at the current share price @@ -123,13 +123,13 @@ A price move pushes the basket off target. `rebalance(sell_amount, usdc_to_inves ## Mock Swap Router vs Production -The `mock-swap-router` exists only for testing: it stores a `usdc_per_token` rate per asset, holds the basket mints' authority, and mints/burns to simulate swaps. Its single `router_config` account (`["router_config"]`) is both its state and its signer: it owns the USDC treasury, is the mint authority of every basket mint, and signs the router's token CPIs with its own seeds, the same way the strategy PDA does for the vaults. The `Strategy` stores the router program pubkey at creation, and `deposit` and `rebalance` require the router account to match it (`InvalidSwapRouter`). In production, replace the router CPIs with [Jupiter](https://jup.ag); the strategy PDA still signs. +The `mock-swap-router` exists only for testing: it stores a `usdc_per_token` rate per asset, holds the basket mints' authority, and mints/burns to simulate swaps. Its single `router_config` account (`["router_config"]`) is both its state and its signer: it owns the USDC treasury, is the mint authority of every basket mint, and signs the router's token CPIs with its own seeds, the same way the fund PDA does for the vaults. The `Fund` stores the router program pubkey at creation, and `deposit` and `rebalance` require the router account to match it (`InvalidSwapRouter`). In production, replace the router CPIs with [Jupiter](https://jup.ag); the fund PDA still signs. --- ## What restricts the manager -The strategy PDA holds all assets; no instruction moves a vault's tokens to the manager. The manager's powers are fenced: +The fund PDA holds all assets; no instruction moves a vault's tokens to the manager. The manager's powers are fenced: - **Assets** are limited to mints approved by the registry authority, with the price feed taken from the registry, not the manager. - **Swaps** go only through the one router registered at creation, and each leg's minimum output is computed from the oracle, not supplied by the manager. @@ -151,26 +151,26 @@ What remains to trust: the honesty of the registered router and registry. With a ```bash # Build each program on its own. Building the whole workspace at once unifies the -# vault's `cpi` feature into the router build and strips the router's entrypoint, +# fund's `cpi` feature into the router build and strips the router's entrypoint, # leaving a stub .so, so build per-manifest (as `anchor build` does): cargo build-sbf --manifest-path programs/mock-swap-router/Cargo.toml -cargo build-sbf --manifest-path programs/vault-strategy/Cargo.toml +cargo build-sbf --manifest-path programs/managed-fund/Cargo.toml # Run tests (LiteSVM, no local validator needed) -cargo test --manifest-path programs/vault-strategy/Cargo.toml +cargo test --manifest-path programs/managed-fund/Cargo.toml ``` -Tests live in `programs/vault-strategy/tests/vault_strategy.rs` and use [LiteSVM](https://github.com/LiteSVM/litesvm). Both `.so` files are loaded from `target/deploy/`, so build before testing. The suite covers the full lifecycle end to end (deposit with auto-deployment, a price move, rebalance back to target, a second depositor priced at the new NAV, a year's fee, in-kind withdrawal), retiring an asset with `set_weight` and reallocating to reopen deposits, and the rejection paths: unapproved asset, weight overflow, over-cap fee and slippage, oracle-bounded deposit slippage, an under-allocated strategy, non-manager `set_weight`, unregistered router, and incomplete asset accounts on deposit. `test_full_lifecycle` checks after every step that the recorded holdings equal the vaults' balances. `test_donation_does_not_inflate_share_price` runs the first-depositor attack (a one-minor-unit deposit, a 1,000 USDC transfer straight into the USDC vault, then a 1,000 USDC deposit with no `minimum_shares` floor) and checks the victim gets exactly the shares they would have got without the donation. `test_deposit_rejects_leg_that_buys_nothing` and `test_rebalance_cannot_spend_donated_usdc` pin the other two guards. +Tests live in `programs/managed-fund/tests/managed_fund.rs` and use [LiteSVM](https://github.com/LiteSVM/litesvm). Both `.so` files are loaded from `target/deploy/`, so build before testing. The suite covers the full lifecycle end to end (deposit with auto-deployment, a price move, rebalance back to target, a second depositor priced at the new NAV, a year's fee, in-kind withdrawal), retiring an asset with `set_weight` and reallocating to reopen deposits, and the rejection paths: unapproved asset, weight overflow, over-cap fee and slippage, oracle-bounded deposit slippage, an under-allocated fund, non-manager `set_weight`, unregistered router, and incomplete asset accounts on deposit. `test_full_lifecycle` checks after every step that the recorded holdings equal the vaults' balances. `test_donation_does_not_inflate_share_price` runs the first-depositor attack (a one-minor-unit deposit, a 1,000 USDC transfer straight into the USDC vault, then a 1,000 USDC deposit with no `minimum_shares` floor) and checks the victim gets exactly the shares they would have got without the donation. `test_deposit_rejects_leg_that_buys_nothing` and `test_rebalance_cannot_spend_donated_usdc` pin the other two guards. ## FAQ ### How do I build an onchain investment fund on Solana? -A manager creates a strategy with `initialize_strategy`, registers curator-approved assets with `add_asset` at target weights, and investors `deposit` USDC for shares. Each deposit is deployed across the basket in the same transaction, and `withdraw` redeems a proportional slice of every vault in kind. +A manager creates a fund with `initialize_fund`, registers curator-approved assets with `add_asset` at target weights, and investors `deposit` USDC for shares. Each deposit is deployed across the basket in the same transaction, and `withdraw` redeems a proportional slice of every vault in kind. ### How are share prices calculated? -Shares are priced at the strategy's net asset value: the total value of its recorded holdings at current prices divided by shares outstanding. A later depositor pays the current share price rather than diluting earlier ones. Tokens transferred straight into a vault are not part of the recorded holdings, so they cannot move the share price. +Shares are priced at the fund's net asset value: the total value of its recorded holdings at current prices divided by shares outstanding. A later depositor pays the current share price rather than diluting earlier ones. Tokens transferred straight into a vault are not part of the recorded holdings, so they cannot move the share price. ### How does the manager operate the fund? diff --git a/finance/vault-strategy/anchor/app/.env.example b/finance/managed-fund/anchor-v1/app/.env.example similarity index 73% rename from finance/vault-strategy/anchor/app/.env.example rename to finance/managed-fund/anchor-v1/app/.env.example index afef240a2..93b684403 100644 --- a/finance/vault-strategy/anchor/app/.env.example +++ b/finance/managed-fund/anchor-v1/app/.env.example @@ -5,15 +5,15 @@ VITE_RPC_URL=https://your-endpoint.solana-devnet.quiknode.pro/xxxxxxxx/ # Program IDs. Defaults below are the repo's localnet IDs (Anchor.toml / declare_id!). # A fresh devnet deploy will produce NEW ids — override them here after deploying. -VITE_VAULT_PROGRAM_ID=VLT5W7bqhRN4nCdRpXm8UfHRxZd9EuZGqiSAkGHQfGh +VITE_FUND_PROGRAM_ID=VLT5W7bqhRN4nCdRpXm8UfHRxZd9EuZGqiSAkGHQfGh VITE_ROUTER_PROGRAM_ID=SWPR8Rk3aq3DrDGLdaANq7xCMnXoUFUJWJJmCWxc8Jm -# The USDC mint the strategy was created with. The demo creates its own 6-decimal +# The USDC mint the fund was created with. The demo creates its own 6-decimal # mock USDC during seeding — paste that mint here. (No safe default exists.) VITE_USDC_MINT= -# Which strategy index to view (PDA seed "strategy" + index). Managers can create others. -VITE_STRATEGY_INDEX=0 +# Which fund index to view (PDA seed "fund" + index). Managers can create others. +VITE_FUND_INDEX=0 # Explorer cluster label: "devnet" | "mainnet-beta" | "custom". # "custom" uses VITE_RPC_URL as the explorer's customUrl. diff --git a/finance/vault-strategy/anchor-v1/app/.gitignore b/finance/managed-fund/anchor-v1/app/.gitignore similarity index 100% rename from finance/vault-strategy/anchor-v1/app/.gitignore rename to finance/managed-fund/anchor-v1/app/.gitignore diff --git a/finance/vault-strategy/anchor-v1/app/.npmrc b/finance/managed-fund/anchor-v1/app/.npmrc similarity index 100% rename from finance/vault-strategy/anchor-v1/app/.npmrc rename to finance/managed-fund/anchor-v1/app/.npmrc diff --git a/finance/vault-strategy/anchor-v1/app/README.md b/finance/managed-fund/anchor-v1/app/README.md similarity index 73% rename from finance/vault-strategy/anchor-v1/app/README.md rename to finance/managed-fund/anchor-v1/app/README.md index 2a01035fe..6ab21b8c0 100644 --- a/finance/vault-strategy/anchor-v1/app/README.md +++ b/finance/managed-fund/anchor-v1/app/README.md @@ -1,7 +1,7 @@ -# Vault Strategy — web app +# Managed Fund — web app -A small, data-forward frontend for the [`vault-strategy`](../README.md) Solana program: -an educational demo of a manager-run investment vault. Investors deposit USDC and hold +A small, data-forward frontend for the [`managed-fund`](../README.md) Solana program: +an educational demo of a manager-run investment fund. Investors deposit USDC and hold shares priced at net asset value; a manager allocates the pooled USDC across a basket and rebalances it. Every number on screen is a real on-chain account read. @@ -21,10 +21,10 @@ pnpm dev | Variable | Meaning | | --- | --- | | `VITE_RPC_URL` | Your Quicknode devnet RPC endpoint. | -| `VITE_VAULT_PROGRAM_ID` | vault-strategy program id. Defaults to the repo's localnet id; **override after a devnet deploy**. | +| `VITE_FUND_PROGRAM_ID` | managed-fund program id. Defaults to the repo's localnet id; **override after a devnet deploy**. | | `VITE_ROUTER_PROGRAM_ID` | mock-swap-router program id (same note). | -| `VITE_USDC_MINT` | The USDC mint the strategy was created with (the demo mints its own). | -| `VITE_STRATEGY_INDEX` | Which strategy PDA to view (`"strategy" + index`). Default `0`. | +| `VITE_USDC_MINT` | The USDC mint the fund was created with (the demo mints its own). | +| `VITE_FUND_INDEX` | Which fund PDA to view (`"fund" + index`). Default `0`. | | `VITE_CLUSTER` | `devnet` \| `mainnet-beta` \| `custom` — controls Explorer links. | Nothing is hardcoded: program ids come from config (defaulting to `Anchor.toml`), and the @@ -56,21 +56,21 @@ To bring the demo up you need `solana` + `anchor` (or `cargo build-sbf`) to: 1. Build & deploy both programs to devnet (you'll get **new** program ids — put them in `.env.local`). 2. Create a 6-decimal mock USDC + the basket mints; init the router, set rates, fund its treasury. 3. Create a registry, approve the basket assets (bound to Pyth `PriceUpdateV2` feeds), init a - strategy, and add the assets to 100% weight. + fund, and add the assets to 100% weight. A one-command deploy + seed script is a planned deliverable; until then, see the Rust tests -(`programs/vault-strategy/tests/vault_strategy.rs`) for the exact sequence — the TypeScript +(`programs/managed-fund/tests/managed_fund.rs`) for the exact sequence — the TypeScript client mirrors it call-for-call. ## Where the IDL comes from -`src/idl/vault_strategy.json` is generated from the program source with +`src/idl/managed_fund.json` is generated from the program source with `anchor idl build` (Anchor 1.2.0, the version CI installs) and committed. If you change the program, regenerate it: ```sh -cd ../ # finance/vault-strategy/anchor -anchor idl build --program-name vault_strategy --out app/src/idl/vault_strategy.json +cd ../ # finance/managed-fund/anchor +anchor idl build --program-name managed_fund --out app/src/idl/managed_fund.json ``` `pnpm verify` still asserts the client wiring offline (discriminators, encode/decode diff --git a/finance/vault-strategy/anchor-v1/app/index.html b/finance/managed-fund/anchor-v1/app/index.html similarity index 93% rename from finance/vault-strategy/anchor-v1/app/index.html rename to finance/managed-fund/anchor-v1/app/index.html index fe69edf04..2037955bb 100644 --- a/finance/vault-strategy/anchor-v1/app/index.html +++ b/finance/managed-fund/anchor-v1/app/index.html @@ -4,7 +4,7 @@ - Vault Strategy — devnet + Managed Fund — devnet - Vault Strategy — preview + Managed Fund — preview { b.writeBigUInt64LE(BigInt(n)); return b; }; -const strategy = PublicKey.findProgramAddressSync([Buffer.from("strategy"), u64le(0)], PROGRAM_ID)[0]; -const shareMint = PublicKey.findProgramAddressSync([Buffer.from("share_mint"), strategy.toBuffer()], PROGRAM_ID)[0]; +const fund = PublicKey.findProgramAddressSync([Buffer.from("fund"), u64le(0)], PROGRAM_ID)[0]; +const shareMint = PublicKey.findProgramAddressSync([Buffer.from("share_mint"), fund.toBuffer()], PROGRAM_ID)[0]; const asset0 = PublicKey.findProgramAddressSync( - [Buffer.from("asset"), strategy.toBuffer(), Buffer.from([0])], + [Buffer.from("asset"), fund.toBuffer(), Buffer.from([0])], PROGRAM_ID, )[0]; -console.log(` strategy ${strategy.toBase58()}`); +console.log(` fund ${fund.toBase58()}`); console.log(` share_mint ${shareMint.toBase58()}`); console.log(` asset[0] ${asset0.toBase58()}`); ok("PDA derivation"); diff --git a/finance/vault-strategy/anchor/app/src/App.tsx b/finance/managed-fund/anchor-v1/app/src/App.tsx similarity index 72% rename from finance/vault-strategy/anchor/app/src/App.tsx rename to finance/managed-fund/anchor-v1/app/src/App.tsx index 8aad9920c..3233d663c 100644 --- a/finance/vault-strategy/anchor/app/src/App.tsx +++ b/finance/managed-fund/anchor-v1/app/src/App.tsx @@ -1,8 +1,8 @@ import { useState } from "react"; import { Header } from "./components/Header"; -import { CreateStrategyForm, ManagerView } from "./components/ManagerView"; +import { CreateFundForm, ManagerView } from "./components/ManagerView"; import { ErrorPanel, Loading, NotFound } from "./components/states"; -import { useVault } from "./hooks/useVault"; +import { useFund } from "./hooks/useFund"; import { InvestorView } from "./views/InvestorView"; type Tab = "investor" | "manager"; @@ -28,34 +28,34 @@ function TabBar({ tab, onTab }: { tab: Tab; onTab: (t: Tab) => void }) { } export default function App() { - const vault = useVault(); + const fund = useFund(); const [tab, setTab] = useState("investor"); - const showManagerTab = Boolean(vault.view?.exists && vault.isManager); + const showManagerTab = Boolean(fund.view?.exists && fund.isManager); const activeTab: Tab = showManagerTab ? tab : "investor"; return (
-
+
{showManagerTab && }
- {vault.loading && } - {!vault.loading && vault.error && } - {!vault.loading && !vault.error && vault.view && ( + {fund.loading && } + {!fund.loading && fund.error && } + {!fund.loading && !fund.error && fund.view && (
- {vault.view.exists ? ( + {fund.view.exists ? ( activeTab === "manager" ? ( - + ) : ( - + ) ) : ( <> - - {vault.connected && ( + + {fund.connected && (
- +
)} diff --git a/finance/vault-strategy/anchor-v1/app/src/components/ActionTicket.tsx b/finance/managed-fund/anchor-v1/app/src/components/ActionTicket.tsx similarity index 97% rename from finance/vault-strategy/anchor-v1/app/src/components/ActionTicket.tsx rename to finance/managed-fund/anchor-v1/app/src/components/ActionTicket.tsx index edb4d790a..b4b22895f 100644 --- a/finance/vault-strategy/anchor-v1/app/src/components/ActionTicket.tsx +++ b/finance/managed-fund/anchor-v1/app/src/components/ActionTicket.tsx @@ -2,7 +2,7 @@ import { useState } from "react"; import { applyToleranceFloor, estimateRedeem, estimateSharesOut, parseAmount } from "../lib/amounts"; import { describeError } from "../lib/tx"; import { formatShares, formatUnits, formatUsdc, shortAddress } from "../solana/format"; -import type { Position, StrategyView } from "../solana/strategy"; +import type { FundView, Position } from "../solana/fund"; import { Button, Segmented, StatusLine, TextField, type TxStatus } from "./atoms"; const DEPOSIT_TOLERANCE_BPS = 100; // 1% — floor on shares out to protect the depositor @@ -18,7 +18,7 @@ function rawAmount(minor: bigint, decimals = 6): string { } export interface ActionTicketProps { - view: StrategyView; + view: FundView; connected: boolean; walletUsdc: bigint | null; position: Position | null; @@ -46,7 +46,7 @@ export function ActionTicket({ view, connected, walletUsdc, position, onDeposit, const depositBlock: string | null = !connected ? "Connect a wallet to deposit." : !view.fullyAllocated - ? "Strategy isn’t fully allocated — deposits are closed until target weights total 100%." + ? "Fund isn’t fully allocated — deposits are closed until target weights total 100%." : activeUnpriced ? "Oracle prices are stale or missing — a deposit would revert on-chain." : usdcMinor !== null && usdcMinor > 0n && walletUsdc !== null && usdcMinor > walletUsdc @@ -68,7 +68,7 @@ export function ActionTicket({ view, connected, walletUsdc, position, onDeposit, const redeemBlock: string | null = !connected ? "Connect a wallet to redeem." : shares === 0n - ? "You hold no shares in this strategy." + ? "You hold no shares in this fund." : sharesMinor !== null && sharesMinor > 0n && sharesMinor > shares ? "Amount exceeds your shares." : null; diff --git a/finance/vault-strategy/anchor/app/src/components/Allocation.tsx b/finance/managed-fund/anchor-v1/app/src/components/Allocation.tsx similarity index 97% rename from finance/vault-strategy/anchor/app/src/components/Allocation.tsx rename to finance/managed-fund/anchor-v1/app/src/components/Allocation.tsx index d5dce765f..cd13853cf 100644 --- a/finance/vault-strategy/anchor/app/src/components/Allocation.tsx +++ b/finance/managed-fund/anchor-v1/app/src/components/Allocation.tsx @@ -1,5 +1,5 @@ import { formatBps, formatRatioPct, formatUnits, formatUsdc } from "../solana/format"; -import type { StrategyView } from "../solana/strategy"; +import type { FundView } from "../solana/fund"; import { Addr } from "./atoms"; // A curated, desaturated "instrument" palette — matte, not neon — starting from the @@ -16,7 +16,7 @@ interface Segment { color: string; } -function buildSegments(view: StrategyView): Segment[] { +function buildSegments(view: FundView): Segment[] { const nav = Number(view.navMinor); const useActual = view.navComplete && view.navMinor > 0n; const segments: Segment[] = []; @@ -44,7 +44,7 @@ function priceUsd(price: bigint | null): string { return price === null ? "—" : `$${formatUnits(price, 8, 2)}`; } -export function AllocationPanel({ view }: { view: StrategyView }) { +export function AllocationPanel({ view }: { view: FundView }) { const s = view.account!; const segments = buildSegments(view); const basisLabel = view.navComplete && view.navMinor > 0n ? "live weights" : "target weights"; diff --git a/finance/vault-strategy/anchor/app/src/components/Header.tsx b/finance/managed-fund/anchor-v1/app/src/components/Header.tsx similarity index 84% rename from finance/vault-strategy/anchor/app/src/components/Header.tsx rename to finance/managed-fund/anchor-v1/app/src/components/Header.tsx index 43210cfa0..3b4cd3296 100644 --- a/finance/vault-strategy/anchor/app/src/components/Header.tsx +++ b/finance/managed-fund/anchor-v1/app/src/components/Header.tsx @@ -1,5 +1,5 @@ import { WalletMultiButton } from "@solana/wallet-adapter-react-ui"; -import { CLUSTER, STRATEGY_INDEX } from "../solana/config"; +import { CLUSTER, FUND_INDEX } from "../solana/config"; import { Chip } from "./atoms"; export function Header({ onRefresh }: { onRefresh: () => void }) { @@ -7,8 +7,8 @@ export function Header({ onRefresh }: { onRefresh: () => void }) {
- VAULT STRATEGY - / #{STRATEGY_INDEX.toString()} + VAULT FUND + / #{FUND_INDEX.toString()}
diff --git a/finance/vault-strategy/anchor/app/src/components/ManagerView.tsx b/finance/managed-fund/anchor-v1/app/src/components/ManagerView.tsx similarity index 88% rename from finance/vault-strategy/anchor/app/src/components/ManagerView.tsx rename to finance/managed-fund/anchor-v1/app/src/components/ManagerView.tsx index bbe6eae2f..54f630f3c 100644 --- a/finance/vault-strategy/anchor/app/src/components/ManagerView.tsx +++ b/finance/managed-fund/anchor-v1/app/src/components/ManagerView.tsx @@ -1,22 +1,22 @@ import { useConnection } from "@solana/wallet-adapter-react"; import { PublicKey } from "@solana/web3.js"; import { useEffect, useMemo, useState } from "react"; -import type { VaultState } from "../hooks/useVault"; +import type { FundState } from "../hooks/useFund"; import { parseAmount, parsePercentToBps, toAmountInput } from "../lib/amounts"; import { describeError } from "../lib/tx"; import { BPS_DENOMINATOR, + FUND_INDEX, MAX_ASSETS, MAX_FEE_BPS, MAX_SLIPPAGE_BPS, PYTH_PRICE_PRECISION, ROUTER_PROGRAM_ID, - STRATEGY_INDEX, USDC_MINT, } from "../solana/config"; import { formatBps, formatShares, formatUnits, formatUsdc, shortAddress } from "../solana/format"; +import type { FundView } from "../solana/fund"; import { approvedAssetPda } from "../solana/pdas"; -import type { StrategyView } from "../solana/strategy"; import { Button, Panel, Select, StatusLine, TextField, type TxStatus } from "./atoms"; const SECONDS_PER_YEAR = 31_536_000n; @@ -50,7 +50,7 @@ function tryPubkey(value: string): PublicKey | null { } } -const assetOptions = (view: StrategyView) => +const assetOptions = (view: FundView) => view.assets.map((a) => ({ value: String(a.index), label: `#${a.index} · ${shortAddress(a.mint)} · ${formatBps(a.weightBps, 0)}`, @@ -58,7 +58,7 @@ const assetOptions = (view: StrategyView) => // ---- Rebalance ------------------------------------------------------------- -function RebalancePanel({ view, onRebalance }: { view: StrategyView; onRebalance: VaultState["rebalance"] }) { +function RebalancePanel({ view, onRebalance }: { view: FundView; onRebalance: FundState["rebalance"] }) { const { status, busy, run } = useAction(); const [sell, setSell] = useState("0"); const [buy, setBuy] = useState(view.assets.length > 1 ? "1" : "0"); @@ -157,7 +157,7 @@ function RebalancePanel({ view, onRebalance }: { view: StrategyView; onRebalance // ---- Set weight ------------------------------------------------------------ -function SetWeightPanel({ view, onSetWeight }: { view: StrategyView; onSetWeight: VaultState["setWeight"] }) { +function SetWeightPanel({ view, onSetWeight }: { view: FundView; onSetWeight: FundState["setWeight"] }) { const { status, busy, run } = useAction(); const [idx, setIdx] = useState("0"); const [pct, setPct] = useState(""); @@ -210,7 +210,7 @@ function SetWeightPanel({ view, onSetWeight }: { view: StrategyView; onSetWeight // ---- Add asset ------------------------------------------------------------- -function AddAssetPanel({ view, onAddAsset }: { view: StrategyView; onAddAsset: VaultState["addAsset"] }) { +function AddAssetPanel({ view, onAddAsset }: { view: FundView; onAddAsset: FundState["addAsset"] }) { const { connection } = useConnection(); const { status, busy, run } = useAction(); const [mintStr, setMintStr] = useState(""); @@ -282,7 +282,7 @@ function AddAssetPanel({ view, onAddAsset }: { view: StrategyView; onAddAsset: V // ---- Collect fees ---------------------------------------------------------- -function CollectFeesPanel({ view, onCollect }: { view: StrategyView; onCollect: VaultState["collectFees"] }) { +function CollectFeesPanel({ view, onCollect }: { view: FundView; onCollect: FundState["collectFees"] }) { const { status, busy, run } = useAction(); const s = view.account!; const now = Math.floor(Date.now() / 1000); @@ -315,31 +315,31 @@ function CollectFeesPanel({ view, onCollect }: { view: StrategyView; onCollect: // ---- Manager view ---------------------------------------------------------- -export function ManagerView({ vault }: { vault: VaultState }) { - const view = vault.view!; +export function ManagerView({ fund }: { fund: FundState }) { + const view = fund.view!; return (
- Manager console — operating strategy #{view.index.toString()} + Manager console — operating fund #{view.index.toString()} fee {formatBps(view.account!.feeBps)} · fixed at creation, no setter
- - - - + + + +
); } -// ---- Create strategy (shown on the not-found path) ------------------------- +// ---- Create fund (shown on the not-found path) ------------------------- -export function CreateStrategyForm({ vault }: { vault: VaultState }) { +export function CreateFundForm({ fund }: { fund: FundState }) { const { status, busy, run } = useAction(); const [registry, setRegistry] = useState(""); const [usdc, setUsdc] = useState(USDC_MINT ? USDC_MINT.toBase58() : ""); @@ -353,8 +353,8 @@ export function CreateStrategyForm({ vault }: { vault: VaultState }) { const feeBps = parsePercentToBps(feePct); const slipBps = parsePercentToBps(slipPct); - const block = !vault.connected - ? "Connect a wallet to create the strategy." + const block = !fund.connected + ? "Connect a wallet to create the fund." : !registryPk ? "Enter the curator’s registry address." : !usdcPk @@ -370,10 +370,10 @@ export function CreateStrategyForm({ vault }: { vault: VaultState }) { return (
- +

- Bootstraps this strategy at index {STRATEGY_INDEX.toString()}. Requires a registry a curator has already - created and approved assets in. You become the manager. + Bootstraps this fund at index {FUND_INDEX.toString()}. Requires a registry a curator has already created and + approved assets in. You become the manager.

- {block && vault.connected &&

{block}

} + {block && fund.connected &&

{block}

} diff --git a/finance/vault-strategy/anchor-v1/app/src/components/atoms.tsx b/finance/managed-fund/anchor-v1/app/src/components/atoms.tsx similarity index 100% rename from finance/vault-strategy/anchor-v1/app/src/components/atoms.tsx rename to finance/managed-fund/anchor-v1/app/src/components/atoms.tsx diff --git a/finance/vault-strategy/anchor/app/src/components/states.tsx b/finance/managed-fund/anchor-v1/app/src/components/states.tsx similarity index 76% rename from finance/vault-strategy/anchor/app/src/components/states.tsx rename to finance/managed-fund/anchor-v1/app/src/components/states.tsx index 1dd8ddb23..599c011bc 100644 --- a/finance/vault-strategy/anchor/app/src/components/states.tsx +++ b/finance/managed-fund/anchor-v1/app/src/components/states.tsx @@ -1,5 +1,5 @@ -import { VAULT_PROGRAM_ID } from "../solana/config"; -import type { StrategyView } from "../solana/strategy"; +import { FUND_PROGRAM_ID } from "../solana/config"; +import type { FundView } from "../solana/fund"; import { Addr, Field } from "./atoms"; export function Loading() { @@ -26,25 +26,25 @@ export function ErrorPanel({ message }: { message: string }) { ); } -export function NotFound({ view }: { view: StrategyView }) { +export function NotFound({ view }: { view: FundView }) { return (
-
No strategy
+
No fund

- Nothing at strategy #{view.index.toString()} on this cluster + Nothing at fund #{view.index.toString()} on this cluster

- The program isn't deployed here, or no strategy exists at this index yet. Deploy the vault-strategy program to + The program isn't deployed here, or no fund exists at this index yet. Deploy the managed-fund program to devnet and seed it, then set the VITE_* variables to the new ids. See{" "} app/README.md.

- - + + - +
diff --git a/finance/vault-strategy/anchor/app/src/hooks/useVault.ts b/finance/managed-fund/anchor-v1/app/src/hooks/useFund.ts similarity index 84% rename from finance/vault-strategy/anchor/app/src/hooks/useVault.ts rename to finance/managed-fund/anchor-v1/app/src/hooks/useFund.ts index 25426f166..2c458348a 100644 --- a/finance/vault-strategy/anchor/app/src/hooks/useVault.ts +++ b/finance/managed-fund/anchor-v1/app/src/hooks/useFund.ts @@ -2,30 +2,30 @@ import { useAnchorWallet, useConnection, useWallet } from "@solana/wallet-adapte import type { PublicKey } from "@solana/web3.js"; import { useCallback, useEffect, useMemo, useState } from "react"; import { sendIxs } from "../lib/tx"; +import { type FundView, loadFundView, loadPosition, type Position } from "../solana/fund"; import { buildAddAssetIx, buildCollectFeesIx, buildDepositIx, - buildInitializeStrategyIx, + buildInitializeFundIx, buildRebalanceIx, buildSetWeightIx, buildWithdrawIxs, - type InitializeStrategyParams, + type InitializeFundParams, } from "../solana/instructions"; import { userAta } from "../solana/pdas"; import { getProgram } from "../solana/program"; import { readTokenAmount } from "../solana/pyth"; -import { loadPosition, loadStrategyView, type Position, type StrategyView } from "../solana/strategy"; interface Data { loading: boolean; error: string | null; - view: StrategyView | null; + view: FundView | null; position: Position | null; walletUsdc: bigint | null; } -export interface VaultState extends Data { +export interface FundState extends Data { connected: boolean; isManager: boolean; refresh: () => void; @@ -37,13 +37,13 @@ export interface VaultState extends Data { setWeight: (assetIndex: number, weightBps: number) => Promise; addAsset: (mint: PublicKey, weightBps: number) => Promise; collectFees: () => Promise; - createStrategy: (params: Omit) => Promise; + createFund: (params: Omit) => Promise; } -/** Loads the strategy, the connected wallet's position + USDC balance, and exposes both +/** Loads the fund, the connected wallet's position + USDC balance, and exposes both * depositor and manager actions. Every field is a live read; actions rebuild against a * fresh view before sending, then refresh. */ -export function useVault(): VaultState { +export function useFund(): FundState { const { connection } = useConnection(); const anchorWallet = useAnchorWallet(); const { publicKey } = useWallet(); @@ -63,7 +63,7 @@ export function useVault(): VaultState { setData((d) => ({ ...d, loading: true, error: null })); (async () => { try { - const view = await loadStrategyView(connection, program); + const view = await loadFundView(connection, program); let position: Position | null = null; let walletUsdc: bigint | null = null; if (view.exists && publicKey) { @@ -94,10 +94,10 @@ export function useVault(): VaultState { // All senders rebuild against a freshly-loaded view so account derivations (asset // count, mints, router) reflect the latest chain state, then refresh the UI. const withFreshView = useCallback( - async (send: (view: StrategyView, manager: PublicKey) => Promise) => { + async (send: (view: FundView, manager: PublicKey) => Promise) => { if (!anchorWallet) throw new Error("Connect a wallet first."); - const view = await loadStrategyView(connection, program); - if (!view.exists) throw new Error("Strategy not found on this cluster."); + const view = await loadFundView(connection, program); + if (!view.exists) throw new Error("Fund not found on this cluster."); const sig = await send(view, anchorWallet.publicKey); refresh(); return sig; @@ -152,10 +152,10 @@ export function useVault(): VaultState { [program, withFreshView], ); - const createStrategy = useCallback( - async (params: Omit) => { + const createFund = useCallback( + async (params: Omit) => { if (!anchorWallet) throw new Error("Connect a wallet first."); - const ix = await buildInitializeStrategyIx(program, { ...params, manager: anchorWallet.publicKey }); + const ix = await buildInitializeFundIx(program, { ...params, manager: anchorWallet.publicKey }); const sig = await sendIxs(program, [ix], 0); refresh(); return sig; @@ -181,6 +181,6 @@ export function useVault(): VaultState { setWeight, addAsset, collectFees, - createStrategy, + createFund, }; } diff --git a/finance/vault-strategy/anchor/app/src/idl/vaultStrategy.ts b/finance/managed-fund/anchor-v1/app/src/idl/managedFund.ts similarity index 90% rename from finance/vault-strategy/anchor/app/src/idl/vaultStrategy.ts rename to finance/managed-fund/anchor-v1/app/src/idl/managedFund.ts index 7af01610c..dd49b7f68 100644 --- a/finance/vault-strategy/anchor/app/src/idl/vaultStrategy.ts +++ b/finance/managed-fund/anchor-v1/app/src/idl/managedFund.ts @@ -1,18 +1,18 @@ import type { Idl } from "@coral-xyz/anchor"; -import idlJson from "./vault_strategy.json"; +import idlJson from "./managed_fund.json"; // The IDL, generated from the program source by `anchor idl build` (see the app // README for the regeneration command). `address` is overridden // at runtime by the configured program id (see src/solana/program.ts), so a fresh // devnet deploy under a new id works without editing this file. -export const VAULT_STRATEGY_IDL = idlJson as Idl; +export const MANAGED_FUND_IDL = idlJson as Idl; import type { BN } from "@coral-xyz/anchor"; // Typed shapes of the on-chain accounts, matching the `types` section of the IDL. // (Anchor decodes to these; we cast fetch results to them for ergonomics.) import type { PublicKey } from "@solana/web3.js"; -export interface StrategyAccount { +export interface FundAccount { index: BN; manager: PublicKey; registry: PublicKey; @@ -33,7 +33,7 @@ export interface StrategyAccount { } export interface AssetConfigAccount { - strategy: PublicKey; + fund: PublicKey; index: number; mint: PublicKey; priceFeed: PublicKey; diff --git a/finance/vault-strategy/anchor-v1/app/src/idl/vault_strategy.json b/finance/managed-fund/anchor-v1/app/src/idl/managed_fund.json similarity index 88% rename from finance/vault-strategy/anchor-v1/app/src/idl/vault_strategy.json rename to finance/managed-fund/anchor-v1/app/src/idl/managed_fund.json index 7f6a62b8b..16d4297cd 100644 --- a/finance/vault-strategy/anchor-v1/app/src/idl/vault_strategy.json +++ b/finance/managed-fund/anchor-v1/app/src/idl/managed_fund.json @@ -1,43 +1,43 @@ { "address": "VLT5W7bqhRN4nCdRpXm8UfHRxZd9EuZGqiSAkGHQfGh", "metadata": { - "name": "vault_strategy", + "name": "managed_fund", "version": "0.1.0", "spec": "0.1.0", - "description": "Manager-run investment vault with NAV-based shares" + "description": "Manager-run investment fund with NAV-based shares" }, "instructions": [ { "name": "add_asset", - "docs": ["Add a curator-approved asset to the strategy at the next index. Manager only."], + "docs": ["Add a curator-approved asset to the fund at the next index. Manager only."], "discriminator": [81, 53, 134, 142, 243, 73, 42, 179], "accounts": [ { "name": "manager", "writable": true, "signer": true, - "relations": ["strategy"] + "relations": ["fund"] }, { - "name": "strategy", + "name": "fund", "writable": true, "pda": { "seeds": [ { "kind": "const", - "value": [115, 116, 114, 97, 116, 101, 103, 121] + "value": [102, 117, 110, 100] }, { "kind": "account", - "path": "strategy.index", - "account": "Strategy" + "path": "fund.index", + "account": "Fund" } ] } }, { "name": "registry", - "relations": ["strategy"] + "relations": ["fund"] }, { "name": "asset_mint" @@ -71,13 +71,13 @@ }, { "name": "vault_asset", - "docs": ["Strategy-owned vault for this asset."], + "docs": ["Fund-owned vault for this asset."], "writable": true, "pda": { "seeds": [ { "kind": "account", - "path": "strategy" + "path": "fund" }, { "kind": "account", @@ -183,21 +183,21 @@ "accounts": [ { "name": "manager", - "relations": ["strategy"] + "relations": ["fund"] }, { - "name": "strategy", + "name": "fund", "writable": true, "pda": { "seeds": [ { "kind": "const", - "value": [115, 116, 114, 97, 116, 101, 103, 121] + "value": [102, 117, 110, 100] }, { "kind": "account", - "path": "strategy.index", - "account": "Strategy" + "path": "fund.index", + "account": "Fund" } ] } @@ -213,7 +213,7 @@ }, { "kind": "account", - "path": "strategy" + "path": "fund" } ] } @@ -275,18 +275,18 @@ "signer": true }, { - "name": "strategy", + "name": "fund", "writable": true, "pda": { "seeds": [ { "kind": "const", - "value": [115, 116, 114, 97, 116, 101, 103, 121] + "value": [102, 117, 110, 100] }, { "kind": "account", - "path": "strategy.index", - "account": "Strategy" + "path": "fund.index", + "account": "Fund" } ] } @@ -302,14 +302,14 @@ }, { "kind": "account", - "path": "strategy" + "path": "fund" } ] } }, { "name": "usdc_mint", - "relations": ["strategy"] + "relations": ["fund"] }, { "name": "depositor_usdc_account", @@ -372,7 +372,7 @@ "seeds": [ { "kind": "account", - "path": "strategy" + "path": "fund" }, { "kind": "account", @@ -464,12 +464,12 @@ "args": [] }, { - "name": "initialize_strategy", + "name": "initialize_fund", "docs": [ - "Open a strategy at a caller-chosen index, e.g. index 0 derives the PDA", - "from seeds `\"strategy\" + 0`. Manager pays and becomes the strategy's manager." + "Open a fund at a caller-chosen index, e.g. index 0 derives the PDA", + "from seeds `\"fund\" + 0`. Manager pays and becomes the fund's manager." ], - "discriminator": [208, 119, 144, 145, 178, 57, 105, 252], + "discriminator": [212, 42, 24, 245, 146, 141, 78, 198], "accounts": [ { "name": "manager", @@ -481,16 +481,16 @@ }, { "name": "registry", - "docs": ["Registry whose approved assets this strategy may hold."] + "docs": ["Registry whose approved assets this fund may hold."] }, { - "name": "strategy", + "name": "fund", "writable": true, "pda": { "seeds": [ { "kind": "const", - "value": [115, 116, 114, 97, 116, 101, 103, 121] + "value": [102, 117, 110, 100] }, { "kind": "arg", @@ -510,20 +510,20 @@ }, { "kind": "account", - "path": "strategy" + "path": "fund" } ] } }, { "name": "vault_usdc", - "docs": ["Vault's USDC token account - strategy PDA is the authority"], + "docs": ["Vault's USDC token account - fund PDA is the authority"], "writable": true, "pda": { "seeds": [ { "kind": "account", - "path": "strategy" + "path": "fund" }, { "kind": "account", @@ -581,28 +581,28 @@ { "name": "manager", "signer": true, - "relations": ["strategy"] + "relations": ["fund"] }, { - "name": "strategy", + "name": "fund", "writable": true, "pda": { "seeds": [ { "kind": "const", - "value": [115, 116, 114, 97, 116, 101, 103, 121] + "value": [102, 117, 110, 100] }, { "kind": "account", - "path": "strategy.index", - "account": "Strategy" + "path": "fund.index", + "account": "Fund" } ] } }, { "name": "usdc_mint", - "relations": ["strategy"] + "relations": ["fund"] }, { "name": "sell_mint", @@ -631,7 +631,7 @@ "seeds": [ { "kind": "account", - "path": "strategy" + "path": "fund" }, { "kind": "account", @@ -658,7 +658,7 @@ "seeds": [ { "kind": "account", - "path": "strategy" + "path": "fund" }, { "kind": "account", @@ -685,7 +685,7 @@ "seeds": [ { "kind": "account", - "path": "strategy" + "path": "fund" }, { "kind": "account", @@ -754,21 +754,21 @@ { "name": "manager", "signer": true, - "relations": ["strategy"] + "relations": ["fund"] }, { - "name": "strategy", + "name": "fund", "writable": true, "pda": { "seeds": [ { "kind": "const", - "value": [115, 116, 114, 97, 116, 101, 103, 121] + "value": [102, 117, 110, 100] }, { "kind": "account", - "path": "strategy.index", - "account": "Strategy" + "path": "fund.index", + "account": "Fund" } ] } @@ -795,18 +795,18 @@ "signer": true }, { - "name": "strategy", + "name": "fund", "writable": true, "pda": { "seeds": [ { "kind": "const", - "value": [115, 116, 114, 97, 116, 101, 103, 121] + "value": [102, 117, 110, 100] }, { "kind": "account", - "path": "strategy.index", - "account": "Strategy" + "path": "fund.index", + "account": "Fund" } ] } @@ -822,14 +822,14 @@ }, { "kind": "account", - "path": "strategy" + "path": "fund" } ] } }, { "name": "usdc_mint", - "relations": ["strategy"] + "relations": ["fund"] }, { "name": "user_share_account", @@ -892,7 +892,7 @@ "seeds": [ { "kind": "account", - "path": "strategy" + "path": "fund" }, { "kind": "account", @@ -950,8 +950,8 @@ "discriminator": [47, 174, 110, 246, 184, 182, 252, 218] }, { - "name": "Strategy", - "discriminator": [174, 110, 39, 119, 82, 106, 169, 102] + "name": "Fund", + "discriminator": [62, 128, 183, 208, 91, 31, 212, 209] } ], "errors": [ @@ -978,17 +978,17 @@ { "code": 6004, "name": "AssetNotFound", - "msg": "Asset mint is not part of this strategy" + "msg": "Asset mint is not part of this fund" }, { "code": 6005, "name": "TooManyAssets", - "msg": "Strategy already holds the maximum number of assets" + "msg": "Fund already holds the maximum number of assets" }, { "code": 6006, "name": "DuplicateAsset", - "msg": "Asset is already part of this strategy" + "msg": "Asset is already part of this fund" }, { "code": 6007, @@ -997,18 +997,18 @@ }, { "code": 6008, - "name": "StrategyNotFullyAllocated", - "msg": "Strategy weights must sum to 100% before it can accept deposits" + "name": "FundNotFullyAllocated", + "msg": "Fund weights must sum to 100% before it can accept deposits" }, { "code": 6009, "name": "IncompleteAssetAccounts", - "msg": "Wrong number of asset accounts supplied for the strategy's assets" + "msg": "Wrong number of asset accounts supplied for the fund's assets" }, { "code": 6010, "name": "InvalidAssetAccount", - "msg": "An asset account does not match the strategy's registered asset" + "msg": "An asset account does not match the fund's registered asset" }, { "code": 6011, @@ -1023,7 +1023,7 @@ { "code": 6013, "name": "InvalidRegistry", - "msg": "Registry does not match the strategy's registered registry" + "msg": "Registry does not match the fund's registered registry" }, { "code": 6014, @@ -1073,12 +1073,12 @@ { "code": 6023, "name": "InvalidUsdcMint", - "msg": "USDC mint does not match the strategy's registered USDC mint" + "msg": "USDC mint does not match the fund's registered USDC mint" }, { "code": 6024, "name": "InvalidSwapRouter", - "msg": "Swap router program does not match the strategy's registered swap router" + "msg": "Swap router program does not match the fund's registered swap router" }, { "code": 6025, @@ -1098,7 +1098,7 @@ { "code": 6028, "name": "InsufficientHoldings", - "msg": "Rebalance spends more than the strategy's recorded holdings" + "msg": "Rebalance spends more than the fund's recorded holdings" } ], "types": [ @@ -1137,7 +1137,7 @@ { "name": "AssetConfig", "docs": [ - "One basket asset. Its address is a PDA seeded by the strategy and the asset's", + "One basket asset. Its address is a PDA seeded by the fund and the asset's", "index, so the full set is the contiguous range 0..asset_count: any handler", "computing net asset value re-derives every index and refuses to proceed if an", "asset account is missing." @@ -1146,7 +1146,7 @@ "kind": "struct", "fields": [ { - "name": "strategy", + "name": "fund", "type": "pubkey" }, { @@ -1167,13 +1167,13 @@ }, { "name": "vault", - "docs": ["Strategy-owned associated token account holding this asset."], + "docs": ["Fund-owned associated token account holding this asset."], "type": "pubkey" }, { "name": "weight_bps", "docs": [ - "Target share of the strategy's value in basis points. deposit deploys at these", + "Target share of the fund's value in basis points. deposit deploys at these", "weights (the sum across assets must reach 10000 before deposits open), and the", "manager maintains them against price drift with rebalance." ], @@ -1219,7 +1219,7 @@ "itself is the collection of `ApprovedAsset` accounts derived from this registry,", "and an asset counts as approved exactly when its `ApprovedAsset` account exists.", "", - "The authority is deliberately not a strategy manager: the curator vets which", + "The authority is deliberately not a fund manager: the curator vets which", "real assets (and which official price feed) are safe, and a manager only", "chooses among them. This is what stops a manager from listing a token they", "mint themselves, or pairing a real mint with a feed they control." @@ -1239,10 +1239,10 @@ } }, { - "name": "Strategy", + "name": "Fund", "docs": [ - "One strategy (basket). Its address is a PDA seeded by a caller-chosen index,", - "e.g. seeds `\"strategy\" + 0`, so strategies are addressed by a simple counter", + "One fund (basket). Its address is a PDA seeded by a caller-chosen index,", + "e.g. seeds `\"fund\" + 0`, so funds are addressed by a simple counter", "rather than by the manager's key. The index is stored here so every handler", "can re-derive the PDA to sign for the vaults and share mint." ], @@ -1251,7 +1251,7 @@ "fields": [ { "name": "index", - "docs": ["Index used as the PDA seed, e.g. 0 for the first strategy."], + "docs": ["Index used as the PDA seed, e.g. 0 for the first fund."], "type": "u64" }, { diff --git a/finance/vault-strategy/anchor-v1/app/src/index.css b/finance/managed-fund/anchor-v1/app/src/index.css similarity index 100% rename from finance/vault-strategy/anchor-v1/app/src/index.css rename to finance/managed-fund/anchor-v1/app/src/index.css diff --git a/finance/vault-strategy/anchor-v1/app/src/lib/amounts.ts b/finance/managed-fund/anchor-v1/app/src/lib/amounts.ts similarity index 94% rename from finance/vault-strategy/anchor-v1/app/src/lib/amounts.ts rename to finance/managed-fund/anchor-v1/app/src/lib/amounts.ts index 876ca91af..2c29676f9 100644 --- a/finance/vault-strategy/anchor-v1/app/src/lib/amounts.ts +++ b/finance/managed-fund/anchor-v1/app/src/lib/amounts.ts @@ -1,4 +1,4 @@ -import type { StrategyView } from "../solana/strategy"; +import type { FundView } from "../solana/fund"; /** Parse a decimal string into minor units. Returns null if malformed or over-precise. */ export function parseAmount(input: string, decimals: number): bigint | null { @@ -31,7 +31,7 @@ export interface RedeemEstimate { } /** The in-kind slice a redemption pays out, matching withdraw's proportional math. */ -export function estimateRedeem(sharesMinor: bigint, view: StrategyView): RedeemEstimate { +export function estimateRedeem(sharesMinor: bigint, view: FundView): RedeemEstimate { if (view.totalShares === 0n || sharesMinor <= 0n) { return { usdcMinor: 0n, diff --git a/finance/vault-strategy/anchor/app/src/lib/tx.ts b/finance/managed-fund/anchor-v1/app/src/lib/tx.ts similarity index 95% rename from finance/vault-strategy/anchor/app/src/lib/tx.ts rename to finance/managed-fund/anchor-v1/app/src/lib/tx.ts index 446f17098..ad19f6e5e 100644 --- a/finance/vault-strategy/anchor/app/src/lib/tx.ts +++ b/finance/managed-fund/anchor-v1/app/src/lib/tx.ts @@ -1,6 +1,6 @@ import { AnchorError, type AnchorProvider } from "@coral-xyz/anchor"; import { ComputeBudgetProgram, Transaction, type TransactionInstruction } from "@solana/web3.js"; -import type { VaultProgram } from "../solana/program"; +import type { FundProgram } from "../solana/program"; /** * Build a legacy transaction from instructions (with a compute-unit bump — deposit and @@ -11,7 +11,7 @@ import type { VaultProgram } from "../solana/program"; * transaction with an Address Lookup Table; that's out of scope for the demo basket. */ export async function sendIxs( - program: VaultProgram, + program: FundProgram, ixs: TransactionInstruction[], computeUnits = 400_000, ): Promise { diff --git a/finance/vault-strategy/anchor-v1/app/src/main.tsx b/finance/managed-fund/anchor-v1/app/src/main.tsx similarity index 100% rename from finance/vault-strategy/anchor-v1/app/src/main.tsx rename to finance/managed-fund/anchor-v1/app/src/main.tsx diff --git a/finance/vault-strategy/anchor-v1/app/src/preview.tsx b/finance/managed-fund/anchor-v1/app/src/preview.tsx similarity index 91% rename from finance/vault-strategy/anchor-v1/app/src/preview.tsx rename to finance/managed-fund/anchor-v1/app/src/preview.tsx index 1c5e53f38..4e8ada07a 100644 --- a/finance/vault-strategy/anchor-v1/app/src/preview.tsx +++ b/finance/managed-fund/anchor-v1/app/src/preview.tsx @@ -3,9 +3,9 @@ import { PublicKey } from "@solana/web3.js"; import ReactDOM from "react-dom/client"; import "./index.css"; import { ManagerView } from "./components/ManagerView"; -import type { VaultState } from "./hooks/useVault"; -import type { StrategyAccount } from "./idl/vaultStrategy"; -import type { AssetView, StrategyView } from "./solana/strategy"; +import type { FundState } from "./hooks/useFund"; +import type { FundAccount } from "./idl/managedFund"; +import type { AssetView, FundView } from "./solana/fund"; import { InvestorView } from "./views/InvestorView"; // ──────────────────────────────────────────────────────────────────────────── @@ -38,7 +38,7 @@ const asset = ( actualWeight, }); -const account: StrategyAccount = { +const account: FundAccount = { index: new BN(0), manager: k(), registry: k(), @@ -56,10 +56,10 @@ const account: StrategyAccount = { bump: 255, }; -const view: StrategyView = { +const view: FundView = { exists: true, index: 0n, - strategy: k(), + fund: k(), shareMint: account.shareMint, usdcVault: k(), account, @@ -83,7 +83,7 @@ const act = async () => { return mockSig; }; -const vault: VaultState = { +const fund: FundState = { loading: false, error: null, view, @@ -104,7 +104,7 @@ const vault: VaultState = { setWeight: act, addAsset: act, collectFees: act, - createStrategy: act, + createFund: act, }; ReactDOM.createRoot(document.getElementById("root")!).render( @@ -112,7 +112,7 @@ ReactDOM.createRoot(document.getElementById("root")!).render(
- VAULT STRATEGY + VAULT FUND / #0
@@ -128,11 +128,11 @@ ReactDOM.createRoot(document.getElementById("root")!).render(
Investor tab
- +
Manager tab
- +