Skip to content

Fix valuation to scale by asset decimals and Pyth exponent - #177

Merged
mikemaccana merged 1 commit into
mainfrom
claude/vigilant-hypatia-fxmdss
Oct 1, 2026
Merged

mikemaccana merged 1 commit into
mainfrom
claude/vigilant-hypatia-fxmdss

Conversation

@mikemaccana

Copy link
Copy Markdown
Collaborator

Summary

The managed fund's valuation logic incorrectly assumed all assets had 6 decimals and Pyth feeds had an exponent of -8. This fix makes valuation decimal-aware by reading each asset's decimals from its AssetConfig and each Pyth feed's exponent from the price account, then scaling appropriately during conversions between asset and USDC amounts.

Key Changes

  • Oracle valuation: Updated asset_value_in_usdc() and usdc_to_asset_amount() to accept and use the asset's decimals and Pyth feed's exponent. Introduced mul_pow10_div() helper to handle scaling by powers of 10 (positive or negative).

  • Pyth feed reading: Added PYTH_EXPONENT_OFFSET constant (offset 89) to read the 4-byte exponent field from Pyth PriceUpdateV2 accounts. Updated all test helpers (write_price_feed, set_price_feed_posted_at) to accept and write the exponent.

  • AssetConfig storage: Added decimals field to AssetConfig to record each asset's mint decimals at registration time, enabling consistent valuation across the fund's lifetime.

  • Fund state: Added usdc_decimals field to Fund for consistency (USDC is always 6 decimals in practice, but stored for clarity).

  • Rebalance refactoring: Simplified the rebalance instruction to accept only the sell and buy asset indices; the program now computes the trade amounts from oracle prices and target weights using the corrected valuation. Changed the signer from manager to caller (anyone can rebalance). Removed explicit mint and config accounts from the instruction signature; they are now passed via remaining_accounts.

  • Router rate semantics: Updated mock router's AssetRate.usdc_per_token documentation and test constants to clarify it represents USDC minor units per whole token (e.g., 250_000_000 for $250), not per base unit.

  • Test coverage: Added test_valuation_scales_by_decimals_and_exponent to verify correct scaling for an 8-decimal asset with a -5 exponent Pyth feed. Extended rebalance tests to cover drift detection, threshold validation, and edge cases.

  • Threshold bounds: Added MIN_REBALANCE_THRESHOLD_BPS and MAX_REBALANCE_THRESHOLD_BPS constants; initialize_fund now validates the rebalance threshold is in range.

Implementation Details

  • Valuation formula: asset_value_usdc = amount_asset × price_pyth × 10^(exponent - asset_decimals + usdc_decimals) / 10^8
  • The conversion avoids overflow by using 128-bit intermediate values and careful ordering of operations.
  • All three implementations (Quasar, Anchor, Anchor-v1) are updated consistently.
  • Frontend components updated to display asset decimals and handle decimal-aware formatting.

https://claude.ai/code/session_01Jw5mz4PJSs6GbyMY1zNeEr

Valuation assumed every asset had USDC's six decimals and every Pyth feed an
exponent of -8: value = amount * price / 10^8, with nothing checking either.
An eight-decimal asset was valued 100 times too high, so a later depositor
bought almost no shares; Pyth's US equity feeds use exponent -5, a further
factor of 1,000. load_price now reads the exponent, AssetConfig records the
mint's decimals and Fund the USDC mint's, and deposit and rebalance value
and size swaps by 10^(usdc_decimals + exponent - asset_decimals). The mock
router's usdc_per_token is now USDC minor units per whole token.

rebalance(sell_amount, usdc_to_invest) let the manager choose both legs, so
a manager could trade a balanced fund back and forth and bleed it through
slippage to whoever filled the trades. rebalance(sell_index, buy_index) is
now permissionless and sizes its own trade: it values every asset, requires
the asset sold to be above its target by a threshold fixed at creation
(100..=2,000 bps, no setter) and the asset bought to be below its own, and
trades the smaller gap, spending only the sale's proceeds. A fund at its
targets has no trade to make.

Anchor v2, Anchor v1 and Quasar change together, with tests for churn, the
threshold, the buy side, retired assets, donations and mixed decimals, two
new Kani harnesses, and the web apps' IDL, client and rebalance panel.

From quicknode/solana-book review feedback on the Managed Fund chapter.

Claude-Session: https://claude.ai/code/session_01Jw5mz4PJSs6GbyMY1zNeEr
@mikemaccana
mikemaccana merged commit a8d4bf6 into main Oct 1, 2026
30 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant