How Vaults works
ImplementedThe full path from a source account changing a position to a vault holding the same asset.
#The one diagram that matters
Two providers answer two different questions, and only one of them is allowed anywhere near your money.
flowchart TB
subgraph identity["SourceIdentityProvider — ADVISORY, off-chain"]
I1["who is this?<br/>handle · name · avatar"]
end
subgraph positions["SourcePositionProvider — AUTHORITATIVE, on-chain"]
P1["what do they hold?<br/>balances · swaps · activity"]
end
I1 -->|"decides ONE thing:<br/>which address to observe,<br/>once, at curation"| REG[SourceRegistry<br/>onchain, versioned]
P1 -->|"decides everything<br/>the vault actually does"| SYNC["MirrorVault.sync()<br/>reads balanceOf itself"]
REG --> SYNCIf the identity provider vanished permanently, every vault would keep mirroring correctly. Vaults would lose display names and avatars. That is the entire blast radius, and it is enforced structurally rather than by convention: the module defining these interfaces imports nothing, so a position provider has nowhere to obtain an API response from.
#The sync path
A keeper calls sync(token) on a vault. Everything after that is the contract's decision, not the keeper's.
flowchart TB
K["Keeper calls sync(token)"] --> A{"Asset approved<br/>in the registry?"}
A -- no --> S1["Skip: AssetNotApproved<br/>recorded as visible divergence"]
A -- yes --> B{"Source balance<br/>changed since<br/>the accepted snapshot?"}
B -- no --> S2["Skip: NoChange"]
B -- yes --> C["Size the trade as a proportion<br/>of the source's PRIOR portfolio"]
C --> D["Apply caps: concentration, cash<br/>buffer, NAV cap, liquidity floor,<br/>price impact, slippage"]
D --> E{"Anything left<br/>above the dust<br/>threshold?"}
E -- no --> S3["Skip: reason recorded"]
E -- yes --> F["Swap via an allowlisted adapter<br/>recipient forced to this vault"]
F --> G["Accept the new snapshot<br/>emit Synced"]Each skip is recorded onchain with its reason, so a vault that declines to copy a trade says so rather than looking identical to one that had nothing to do.
#Sizing
A vault does not copy a trade's absolute size. It copies its proportion.
If a source moves 10% of its eligible portfolio into an asset, the vault aims to move 10% of its own NAV into that asset. The denominator is the source's portfolio before the trade — including the value of the asset it just spent. Reading the source's live base-asset balance after the trade would undercount the denominator and oversize every mirrored trade; an early version of this system did exactly that and oversized by about 11%.
Only approved assets count toward that denominator. That matters: it means nobody can shrink a vault's trades by airdropping its source a nominally valuable token.
The full derivation, with every cap in order, is in Mirror math.
#What bounds the trade
Six ceilings apply, and the smallest one wins:
| Bound | Stops |
|---|---|
| Position concentration | One asset becoming the whole vault |
| Cash buffer | The vault spending the reserve that keeps in-kind exit cheap |
| Vault NAV cap | The vault growing past the size its parameters were reviewed for |
| Liquidity floor | Trading into a pool that cannot absorb the size |
| Price impact ceiling | Executing at a price an independent oracle disagrees with |
| Slippage floor | Accepting materially less than quoted |
The registry stores hard constants above these. No role can raise a vault's parameters past them — not even the role that sets the values beneath them.
#Divergence is a first-class fact
A mirror is not a clone, and the interface never implies it is. Where the vault has not copied something, that difference is shown with its reason:
- In sync — the vault matches the snapshot it has accepted.
- Pending — the source has moved and the vault has not caught up yet.
- Awaiting keeper — an actionable change nobody has called
syncfor. - Skipped — the vault evaluated the change and declined, for a recorded reason.
See Synchronisation lifecycle.
#Valuation fails closed
If a held position cannot be priced, the vault's NAV cannot be computed, and totalAssets() reverts rather than returning a number that quietly omits the unpriceable position.
The consequence is deliberate: deposits are refused while that holds, because minting shares against a wrong NAV dilutes everyone already in, permanently. Refusing to trade costs an afternoon. In-kind redemption never calls the price path, so it keeps working throughout.
#What this depends on
Mirroring is autonomous, but it is not unconditional. It depends on a keeper actually calling sync, on curated asset metadata being correct, on price adapters being honest, and on the base asset behaving. Those dependencies are named individually in Known limitations.