Accounting
MORE Vaults separate position logic from asset accounting. Each strategy facet owns its own storage and position-management code, while the invariant core is the single source of truth for the vault’s totalAssets
. This design keeps upgrades local to the facet that changes, yet guarantees that every share is always backed 1 : 1 by on‑chain‑verifiable value.
Deposit Tokens
Before a vault goes live the strategist whitelists a set of deposit tokens: the ERC-20s users are allowed to send when calling deposit(). The list can be contain a single token (e.g. USDC) or multiple tokens (ETH, stETH, USDC). Each token must have a reliable oracle listed in the Oracle Registry so the core can convert incoming amounts into the accounting asset during batch finalisation. If a user tries to deposit a token that is not whitelisted, the transaction reverts. Smaller sets reduce oracle risk, keep gas costs down, and simplify price charts.
Available Assets
A vault can end up holding many different tokens beyond its deposit list including LP shares, yield tokens, debt receipts, even bridged assets on another chain. These tokens encompass anything that may appear in availableAssets()
and therefore in totalAssets
.
For each available asset the strategist must ensure two things:
Valuation path – An oracle, TWAP, or deterministic formula can convert the asset to the accounting unit at any time.
Registry entry – The oracle contract is published in the Oracle Registry or the pricing method is included in facet accounting so auditors and dashboards can trace the number.
If either requirement is missing, the asset will be excluded from its valuation and impact total asset accounting and share price.
Per-Facet Tracking
Every facet declares a unique storage slot keyed by keccak256("facet.accounting." + facetId)
. Inside it stores whatever is needed to price its positions such as token balances, LP IDs, debt amounts, oracle addresses, etc.
Facets expose a single selector, facetAssets() → (uint256 assets)
, that returns their current valuation in the vault’s accounting asset. There are no cross‑facet calls.
Valuation must be deterministic at call time. If pricing depends on a DEX TWAP or Chainlink round, the facet fetches and converts it internally.
NAV Aggregation
core.totalAssets = Σ facetAssets() + liquidAssetsHeld
Every action on a vault updates the NAV calculation, with updates occurring at most once per block, enforced by lastSyncBlock
. In order to ensure the correct NAV for depositors and withdrawers, sync()
runs before deposit finalisation or redeem settlement so that all share price calculations see the latest NAV.
If a facet reverts during sync()
, the core marks it stale (facetStatus[facetId] = Inactive
) and emits FacetStale(facetId)
. Vault governance can then inspect, replace, or pause the offending module without halting withdrawals.
Valuation Sources
Source Type
When Used
Guard Rails
Spot balance
Simple ERC‑20 or native token positions
Priced via Pyth, Chainlink, Redstone, etc. oracle; revert if price feed is older than MAX_DELAY
.
DEX LP positions
Uniswap V3, Balancer, Curve
Use an in‑facet math lib to value assets at pool’s virtual price; sanity‑check against on‑chain oracles.
Lending protocol deposits
Aave v3, Compound v3
Read getUserAccountData
.
Yield‑bearing tokens
LRT, wstETH, PT-sUSDe, etc.
Pull exchangeRate()
directly on-chain from protocol and multiply by holding balance.
Facets MUST convert their asset values into the vault’s single accounting asset using at most one intermediate oracle call to prevent gas‑bomb loops.
Fee Accrual
Management and performance fees are applied inside sync()
in the VaultFacet before the NAV snapshot is finalised:
newAssets = rawAssets − mgmtFee − perfFee
totalAssets = newAssets
This keeps fee accounting transparent. Fees show up as an explicit delta rather than hidden dilution.
Last updated