Skip to content

feat(investment_vault): ERC-4626-style read views — share_price, preview_deposit, preview_withdraw, max_withdraw, max_deposit (#617) - #646

Merged
abayomicornelius merged 3 commits into
Heliobond:mainfrom
boluwacodes:feat/617-erc4626-views
Sep 26, 2026
Merged

abayomicornelius merged 3 commits into
Heliobond:mainfrom
boluwacodes:feat/617-erc4626-views

Conversation

@boluwacodes

@boluwacodes boluwacodes commented Sep 26, 2026 •

Copy link
Copy Markdown
Contributor

Problem

The vault only exposes convert_to_shares / convert_to_assets. Those ignore:

  • the insurance premium and management / volume-tier fees;
  • the graduated withdrawal limit;
  • deposit locks and the supply cap.

The frontend even calls a nonexistent share_price view, so clients re-implement contract logic off-chain and get it wrong.

New views

View Returns Accounts for
share_price() i128, USDC per whole share ×10^7 Live NAV. It is 10^7 (1:1) when empty, matching the first-deposit mint.
preview_deposit(usdc) shares Insurance premium (50 bps), then the management / volume-tier fee, then NAV. It raises the same errors as deposit: amount bounds, max-tx cap, supply cap, paused.
preview_withdraw(shares) (usdc_now, usdc_queued) NAV, the graduated utilization limit (#45) and the liquid-vs-FIFO-queue split, with withdraw's amount errors.
max_withdraw(account) USDC Balance value, the utilization tier limit and the max-tx cap. It is 0 while paused, during the deposit lock (#33), or below MIN_WITHDRAW.
max_deposit(account) USDC MAX_DEPOSIT, the max-tx cap and the remaining HBS supply headroom (#20). It is 0 while paused. Funding rounds only block share transfers (#38), so they don't affect deposits.

No storage writes

total_assets() writes CachedTotalAssets on every call. The views instead read NAV through a new read_total_assets() helper that has no side effects. total_assets() now wraps that helper and still caches, so its behaviour is unchanged.

Previews can't drift from the real calls

deposit and withdraw now use the same helpers as the views:

  • validate_deposit_amount;
  • deposit_breakdown, which returns premium, fee and investable amount;
  • withdraw_tier_limit.

The logic moved without changing, so the fee and limit calculations are defined in exactly one place.

Tests

  • preview_deposit equals deposit, with a management fee configured.
  • share_price is 1:1 when empty and tracks total_assets / total_supply.
  • max_withdraw is 0 during the deposit lock. After the lock, preview_withdraw and max_withdraw both equal what withdraw actually pays.
  • max_deposit and max_withdraw are 0 while paused.
  • A cost-estimate test emits gas_budget lines for share_price, preview_deposit and max_withdraw.

Docs and gas

  • INTERFACE.md has rows for all five views, and the total_assets() row now says it writes the cache.
  • gas-budgets.json has entries for the three measured views, set to deposit's budget as an upper bound because they do the same NAV reads without writes.

Commits

  • feat(investment_vault): ERC-4626-style read views share_price/preview_*/max_* (#617)
  • test(investment_vault): views equal their state-changing calls; gas budgets (#617)
  • docs(interface): document ERC-4626-style vault views (#617)

Verification status: not run

main does not compile, independently of this PR. investment_vault/src/storage.rs:37 has a garbled test attribute (#c[cfg)test]) and an unclosed assert_eq!( on line 48, so cargo build -p investment-vault fails before reaching this code. I left that out of this PR to keep its scope to #617; it should get its own fix. Once it is fixed, run:
Closes #617
Closes #618
Closes #619
Closes #620

stellar contract build
cargo test -p investment-vault test_617 -- --nocapture

scripts/check_interface_docs.py already reports undocumented functions on main: funding-round and volume-tier setters, and batch getters. None of the #617 views are in that list.

…_*/max_* (Heliobond#617)

Add pure views that account for everything the real calls do:
- share_price(): NAV per whole share, scaled by 10^7 (1:1 when empty).
- preview_deposit(usdc): shares after insurance premium + management /
  volume-tier fee, with deposit's validation and supply-cap checks.
- preview_withdraw(shares): (usdc_now, usdc_queued), applying the
  graduated utilization limit and the liquid-vs-queue split.
- max_withdraw(account): balance value capped by tier limit and max-tx
  cap; 0 while paused, deposit-locked, or below the minimum.
- max_deposit(account): MAX_DEPOSIT capped by max-tx and remaining HBS
  supply headroom; 0 while paused.

None of them write storage: NAV is read via a new read_total_assets()
helper (total_assets() now wraps it and still caches). deposit/withdraw
are refactored onto the same deposit_breakdown / validate_deposit_amount /
withdraw_tier_limit helpers so previews can't drift from the real calls.
…udgets (Heliobond#617)

- preview_deposit == deposit (with a management fee configured).
- share_price is 1:1 when empty and tracks total_assets / total_supply.
- max_withdraw is 0 during the deposit lock; afterwards preview_withdraw
  and max_withdraw equal what withdraw actually pays.
- max_deposit / max_withdraw are 0 while paused.
- Cost-estimate test emits gas_budget lines for share_price,
  preview_deposit and max_withdraw, with matching gas-budgets.json entries.
@drips-wave

drips-wave Bot commented Sep 26, 2026

Copy link
Copy Markdown

@boluwacodes Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

@abayomicornelius
abayomicornelius merged commit c79daec into Heliobond:main Sep 26, 2026
0 of 7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment