ERC4626SharePriceAssertion
Inherits: ERC4626BaseAssertion
Title: ERC4626SharePriceAssertion
Author: Phylax Systems
Asserts that the vault’s endpoint share price (totalAssets / totalSupply) does not decrease beyond a configurable tolerance. Invariants covered:
- Non-dilutive entry/exit: absent explicit fee accrual or loss recognition, deposit/mint/withdraw/redeem must not reduce assets-per-share for remaining holders.
- Rounding favors incumbents: the share price must not move against the vault (i.e., existing holders) during ordinary user operations.
Compares only the relevant pre/post endpoints. Healthy vault operations often update assets and shares at different internal call boundaries, so intermediate snapshots are not part of this property. Increases are permitted. Empty-supply endpoints have no holder share price and are outside this check.
Constants
sharePriceToleranceBps
Maximum acceptable share-price decrease in basis points.
uint256 public immutable sharePriceToleranceBps
Functions
constructor
constructor(uint256 _toleranceBps) ;
_registerSharePriceTriggers
Register the default trigger set: the exhaustive tx-wide envelope + per-call checks.
The tx-wide envelope (assertSharePriceEnvelope) uses the all-fork-points
assetsMatchSharePrice scan, which re-reads totalAssets()/totalSupply() at
~1+2*callFrames fork points. This is fine for vaults with a cheap totalAssets().
Vaults with an expensive computed totalAssets() (e.g. MetaMorpho, which loops the
Morpho Blue supply queue) should instead use _registerBoundedSharePriceTriggers():
the exhaustive scan can exhaust the assertion gas limit on unrelated high-call-count
transactions that merely touch the vault and revert (PrecompileOOG) — a false invalidation.
function _registerSharePriceTriggers() internal view;
_registerBoundedSharePriceTriggers
Register a gas-bounded trigger set: a pre/post tx-wide envelope + per-call checks.
The tx-wide check (assertSharePriceEnvelopeBounded) compares share price only between
the pre-tx and post-tx forks — 2 fork points, O(1) in call-frame count — so it never
triggers the all-forks scan that OOGs on expensive-totalAssets() vaults. It is
two-sided: it catches both dilution (share price down) and unexpected inflation (share
price up, e.g. a donation / direct-transfer manipulation) reached through ANY entrypoint,
including non-ERC-4626 selectors the per-call check never observes.
function _registerBoundedSharePriceTriggers() internal view;
_registerPerCallSharePriceTriggers
Register only the per-call share-price checks (deposit/mint/withdraw/redeem).
Each fires once per matching ERC-4626 operation and uses the cheap 2-fork
assetsMatchSharePriceAt around that call.
function _registerPerCallSharePriceTriggers() internal view;
assertSharePriceEnvelope
Verifies the share price did not decrease beyond tolerance across the entire transaction.
Compares PreTx and PostTx only. Intermediate call snapshots can contain healthy temporary asset/share ordering differences and are deliberately ignored.
function assertSharePriceEnvelope() external;
assertSharePriceEnvelopeBounded
Tx-wide share-price envelope bounded to the pre-tx vs post-tx comparison.
Two-sided within tolerance — catches both dilution (decrease) and unexpected inflation
(increase, e.g. a donation / direct-transfer share-price manipulation), regardless of the
entrypoint used. Evaluated at exactly two fork points via assetsMatchSharePriceAt, so it
stays within the assertion gas budget even for vaults with an expensive computed
totalAssets(). Unlike assertSharePriceEnvelope, it does not inspect intermediate
call-boundary forks; the per-call checks cover settlement during standard ERC-4626 ops.
function assertSharePriceEnvelopeBounded() external;
assertPerCallSharePrice
Verifies each individual deposit/mint/withdraw/redeem call does not decrease the share price beyond tolerance.
Uses the triggering call’s endpoint snapshots and permits share-price increases.
function assertPerCallSharePrice() external;