NAV Lifecycle & Action Choreography
Which on-chain actions move value between NAV buckets, when the off-chain
FundNavFeed report has to land, and how the per-asset
navDenom / effNavDenom snapshots and PPS are
refreshed by updateNav(). For the PPS formula, deviation guard,
and staleness rules, see 04 · NAV Management.
adjust* helpers so reads stay coherent between snapshots.•
updateNav() overwrites navDenom and effNavDenom from scratch by calling NavAggregateModel.computeNav.• FundNavFeed is read only inside
computeNav — off-chain P&L enters NAV only at the next snapshot.
FundNavFeed.
A strategy move (allocate / deallocate / yield) is not visible to NAV
until both legs are reflected and updateNav() re-aggregates.
Per-call effect on each NAV bucket. ↑ increases the slot,
↓ decreases, ↔ moves between matching slots,
— untouched. In-flight mutations apply to navDenom / effNavDenom
via adjustNavDenom / adjustEffNavDenom in the same call; updateNav()
later overwrites those slots from scratch.
| Action (caller) | BALANCE_OF | navDenom | effNavDenom | pending | claimable | redeemShares |
|---|---|---|---|---|---|---|
AssetVault.deposit / mintuser, oracle conversion |
↑ | ↑ | ↑ | — | — | — |
AssetVault.requestRedeemuser, oracle at request time |
— | — | ↓ | ↑ | — | ↑ |
AsyncRequestManager.fulfillRedeemoperator |
↓ | ↓ | ↔ | ↓ | ↑ | — |
AssetVault.redeem / withdraw (claim)user, assets leave AssetVault |
— | ↓ | ↓ | — | ↓ | ↓ |
AssetVault.cancelRedeemoperator, return to FundVault |
↑ | ↑ | ↑ | ↓ | ↓ | ↓ |
FundVault.allocatecurator, ratio conversion (pre-pull) |
↓ | ↓ | ↓ | — | — | — |
FundVault.deallocatecurator, oracle conversion |
↑ | ↑ | ↑ | — | — | — |
VaultManager.adjustBalanceOfFundVault or AssetVault internal |
± | ± | ± | — | — | — |
VaultManager.syncBalanceOfOPERATOR_ROLE override |
= | ± | ± | — | — | — |
FundNavFeed.syncNavValueOPERATOR_ROLE off-chain report |
— | — | — | — | — | — |
VaultManager.updateNavOPERATOR_ROLE, snapshot refresh |
— | = | = | — | — | = |
± = signed (caller picks sign), = = overwritten with a computed value,
↔ = the slot ends roughly flat because the same call both adds and subtracts.
The FundNav slot in FundNavFeed is conceptually a 7th column — only
syncNavValue mutates it, and updateNav() reads it on the next snapshot.
FundVault.allocate moves capital from the FundVault's idle balance
(counted by BALANCE_OF) into a strategy. The strategy's value lives
off-chain until the operator reports it via FundNavFeed.syncNavValue.
Between Stage B and Stage C, an on-chain updateNav() would read a
truncated picture and PPS would collapse. Operational discipline: report the
strategy NAV in the same block as allocate, or atomically inside the
same operator transaction.
syncNavValue
reduce, gross NAV is overstated and an early updateNav() would
publish an inflated PPS — which the deviation guard
(deviationPps) may or may not catch depending on the magnitude.
Always reduce FundNav by the deallocated amount before the next snapshot.
Single-asset USDC vault. Genesis state: 1,000,000 USDC idle in FundVault,
zero off-chain position, 1,000,000 shares outstanding, PPS = 1.000.
Amounts shown in millions of USDC denom (1e18 scale). Live PPS = PPS that would result
if updateNav() were called at that exact moment; snapshot PPS = the value
currently stored in pricePerShare.
| Step | BALANCE_OF | FundNav | navDenom | effNavDenom | PPS (live) | PPS (snapshot) |
|---|---|---|---|---|---|---|
| 0. Genesis | 1.000M | 0 | 1.000M | 1.000M | 1.000 | 1.000 |
| 1. allocate(strategy, 0.5M) | 0.500M | 0 | 0.500M | 0.500M | 0.500 | 1.000 |
| 2. syncNavValue(USDC, "hyperliquid", 0.5M) | 0.500M | 0.500M | 1.000M | 1.000M | 1.000 | 1.000 |
| 3. updateNav() | 0.500M | 0.500M | 1.000M | 1.000M | 1.000 | 1.000 |
| 4. strategy earns 10k (off-chain) | 0.500M | 0.500M (stale) | 1.000M (stale) | 1.000M (stale) | 1.000 | 1.000 |
| 5. syncNavValue(USDC, "hyperliquid", 0.510M) | 0.500M | 0.510M | 1.010M | 1.010M | 1.010 | 1.000 |
| 6. updateNav() | 0.500M | 0.510M | 1.010M | 1.010M | 1.010 | 1.010 |
effNavDenom = 1.0e24 − 0 − 0 = 1.0e24
PPS = 1.0e24 × 1e18 / 1e24 = 1.0e18 ✓
PPS = 0.5e24 × 1e18 / 1e24 = 0.5e18 ← dip
pricePerShare doesn't change until step 3 (and again at step 6).
The "live" PPS column shows what a hypothetical immediate
updateNav() would produce. Conversion functions
(convertToShares, convertToAssets) use the
snapshot; only NAV reads via computeNav see the live
picture. Discipline: never call updateNav() at step 1 —
do step 2 first.
adjustBalanceOf(asset, delta) — bumps BALANCE_OF, navDenom, effNavDenom by delta.•
adjustPendingDenom(vault, delta) — bumps pendingDenom on requestRedeem / fulfillRedeem / cancelRedeem.•
adjustClaimableDenom(vault, delta) — bumps claimableDenom on fulfillRedeem / claim.•
adjustGlobalRedeemShares(delta) — bumps the global redeem-shares counter.• Saturating subtract on all denom-scaled counters: oracle drift between paired in/out events clamps to 0 instead of reverting.
updateNav())NavAggregateModel.computeNav(registeredAssets) — reads FundNavFeed, BALANCE_OF, vault state, ShareToken.totalSupply().• Overwrites navDenom[asset] and effNavDenom[asset] per asset from
result.assetTotalNavs[i] / assetEffNavDenoms[i].• Overwrites globalRedeemShares, pricePerShare,
lastNavUpdated.• Does not mutate BALANCE_OF, pendingDenom, or claimableDenom — those flow only through in-flight deltas.
• Reverts with
InvalidPricePerShare() if NavAggregateModel._isValidPps rejects the new PPS (deviation guard).
convertAssetToDenomByOracle
inside deposits, or NAV reader queries) don't lag user activity.
updateNav() is the moment the off-chain leg
(FundNavFeed) joins the on-chain picture and PPS is republished.
Anything that only moves value off-chain (strategy yield, allocate)
is invisible to PPS until syncNavValue + updateNav().
| Topic | Where |
|---|---|
PPS formula & _computePps edge cases (genesis, all-redeem freeze) |
04 · NAV Management — ① NAV Formula |
PPS deviation guard (deviationPps) and staleness (maxNavStaleness) |
04 · NAV Management — ③ Deviation Guard |
| Fee accrual effect on PPS (management + performance, high-water mark) | 08 · Fee Management |
| Allocate / deallocate flow at the action level (cap enforcement, ratio vs oracle) | 02 · Allocate & Deallocate |
| Async redeem lifecycle (requestRedeem → fulfillRedeem → redeem/withdraw) | 03 · Withdraw Flow |
| Recommended operator sequencing (harvest → syncNavValue → updateNav) | 06 · Common Operations |