Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,21 @@ 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-10-01] - Managed Fund: decimal-aware valuation and permissionless rebalancing

### Fixed

- `finance/managed-fund` valued every asset as if it had six decimals and a
Pyth exponent of −8. It now reads each feed's exponent and records each
mint's decimals, in Anchor v2, Anchor v1 and Quasar.

### Changed

- `finance/managed-fund`'s `rebalance` is permissionless and computes its own
trade from oracle prices and target weights, once drift passes a threshold
fixed at fund creation, so no caller, the manager included, can churn the
fund. See the example's changelogs.

## [2026-09-28] - Token Fundraiser is renamed Fundraiser

Contributors to the fundraiser receive no token, only a refund if the target is
Expand Down
5 changes: 5 additions & 0 deletions finance/managed-fund/anchor-v1/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# Changelog

## 2026-10-01

- **Valuation scales by each asset's decimals and each feed's exponent.** The fund valued an asset as `amount × price / 10⁸`, which is right only when the asset has USDC's 6 decimals and its Pyth feed has exponent −8, and nothing checked either. An eight-decimal asset would have been valued 100 times too high, so a later depositor would have bought almost no shares; Pyth's US equity feeds use exponent −5, a further factor of 1,000. `load_price` now reads the exponent (offset 89) and returns an `OraclePrice`, `AssetConfig` records the mint's `decimals` and `Fund` the USDC mint's `usdc_decimals`, and `deposit` and `rebalance` value and size swaps with `asset_value_in_usdc` and `usdc_to_asset_amount`, which scale by `10^(usdc_decimals + exponent − asset_decimals)`. `PYTH_PRICE_PRECISION` is removed. The mock router's `usdc_per_token` is now USDC minor units per whole token, and its swaps scale by the asset mint's decimals. Tested by `test_valuation_scales_by_decimals_and_exponent`, which runs the story with an eight-decimal TSLAx on an exponent −5 feed.
- **Anyone may rebalance, and the program sizes the trade.** `rebalance(sell_amount, usdc_to_invest)`, manager-only, is now `rebalance(sell_index, buy_index)`, signed by any `caller`, with the same five remaining accounts per asset as `deposit`. It values every asset and requires the asset sold to sit above its target by at least the fund's threshold (new `DriftBelowThreshold`; a retired asset may always be sold) and the asset bought to sit below its target (new `NotUnderweight`). It trades the smaller of the two gaps and spends only what the sale brought in. Before, the manager chose both amounts, so they could trade a balanced fund back and forth and bleed it through slippage, to the benefit of whoever filled the trades. A fund at its targets now has no trade to make. `initialize_fund` takes a `rebalance_threshold_bps`, between `MIN_REBALANCE_THRESHOLD_BPS` (100) and `MAX_REBALANCE_THRESHOLD_BPS` (2,000) (new `RebalanceThresholdOutOfRange`), stored on `Fund` with no setter. Tested by `test_rebalance_refuses_fund_at_target`, `test_rebalance_refuses_drift_below_threshold`, `test_rebalance_cannot_churn`, `test_rebalance_refuses_buying_overweight_asset`, `test_rebalance_rejects_incomplete_assets`, `test_rebalance_sells_retired_asset`, `test_rebalance_ignores_donations` (replacing `test_rebalance_cannot_spend_donated_usdc`) and `test_initialize_rejects_threshold_out_of_range`. The Kani proofs gain `proof_rebalance_trade_never_overshoots` and `proof_sell_amount_never_worth_more_than_trade`. The web app's IDL, instruction builders and rebalance panel follow.

## 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.
Expand Down
7 changes: 4 additions & 3 deletions finance/managed-fund/anchor-v1/PRODUCT.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ 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 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.
- **Manager (primary):** a power user who creates and operates a fund — registers curator-approved assets, sets target weights, 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
Expand All @@ -35,14 +35,15 @@ A **multi-asset** fund built from single-asset vaults, transparently priced on-c
Program instructions the frontend can surface:

- **Registry / curator:** `initialize_registry`, `approve_asset` (whitelist of assets managers may use).
- **Manager:** `initialize_fund`, `add_asset`, `set_weight` (including set-to-zero to retire an asset), `rebalance`, `collect_fees`.
- **Manager:** `initialize_fund` (fee, slippage tolerance, and rebalance threshold, all fixed at creation), `add_asset`, `set_weight` (including set-to-zero to retire an asset), `collect_fees`.
- **Anyone:** `rebalance(sell_index, buy_index)`. The program sizes the trade from oracle prices and target weights, and refuses unless the asset sold is over its target by at least the fund's threshold and the asset bought is under its own (`DriftBelowThreshold`, `NotUnderweight`).
- **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 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`).
- 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 more than the recorded holdings (`InsufficientHoldings`), spends only what its sale brought in, and values assets from the recorded holdings, so a donation cannot force one.
- 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.
Expand Down
Loading
Loading