The vault
Share accounting, donations, exits and the on-chain price average of QuantumVault.
QuantumVault holds every shielded token and all the state of every account. It has no owner, no upgrade path and no
initializer: the token, the market, the EntryPoint and the fee limit are immutable constructor arguments.
Shares
Accounts hold shares. The tokens backing all accounts are totalBacking (B); the shares are totalShares (S).
convertToShares(assets) = assets * (S + VIRTUAL_SHARES) / (B + 1)
convertToAssets(shares) = shares * (B + 1) / (S + VIRTUAL_SHARES)VIRTUAL_SHARES = 1e6 are shares that belong to nobody. They fix the opening exchange rate, so the first shield
cannot be priced out by a donation made in front of it.
| Function | Effect on B | Effect on S |
|---|---|---|
shield(account, amount) | + amount | + shares minted to the account |
donate(amount) | + amount | none |
absorb() | + any token balance above the books | none |
| transfer between accounts | none | none (shares move) |
| exit | − tokens paid out | − shares burned |
| network fee | none | − shares burned |
Donations and fees raise B/S: every account's balance in the token goes up at once, with no loop over accounts and no claim step.
Donations
Two functions add tokens for everyone without minting shares:
donate(amount)pulls tokens from the caller.absorb()books any tokens that reached the vault by a plain transfer. It is permissionless.
The books and the token balance always satisfy balance ≥ totalBacking + pendingExitTotal, and absorb closes any
gap in the holders' favour.
Exits
An operation with exitShares burns those shares and removes their tokens from the backing. What happens next depends
on exitTarget:
- No target: the tokens are transferred to
recipient. - A target: the vault approves the target for exactly the exit's amount and calls
onShieldExit(amount, recipient, data). The target pulls what it uses. Whatever it leaves, read from the remaining allowance, goes torecipient, and the approval is cleared.
Exited tokens stay counted in pendingExitTotal until they have physically left the vault, so nothing a target does
in the meantime can make them look like unowned tokens. shield, donate, absorb, transact and exit settlement share one transient
reentrancy lock.
Through transact, a failing target reverts the whole call. Through ERC-4337, the books are updated during validation
and the exit is escrowed in pendingExits[id], then settled in execute4337; there a failing target is caught and the
tokens go to the recipient. If execution itself ran out of gas, anyone can call claimExit(id) to release the tokens to
the recipient.
The price average
The vault keeps tokensPerEthEma: an exponential moving average of the market price, in tokens per ETH scaled by
1e18. It exists to price the network fee and to give the harvester a reference that a single block cannot move.
poke() is permissionless and is called by the router and the harvester on every trade:
- The spot price is read from the live venue: the Pons curve before graduation, the Uniswap v4 pool after.
- The first update in a block folds the previous update's price into the average:
ema = (7·ema + close) / 8. closeis clamped to withinMAX_PRICE_STEP_BPS(10%) of the average.
A price that lasts a single block enters the average at most once, clamped, and only when a later block updates it. Moving the average takes sustained prices across blocks.
The guardian
One role exists. The guardian can call setEntriesPaused, which makes shield revert, and can hand the role on or
renounce it.
It cannot touch anything already inside. Transfers, sales, exits and key changes have no pause, no allowlist and no admin path: nobody can be locked in.