Banked from spark-vaults-v2 SparkVault.sol (WALK CLEAN) - an sUSDS fork (ERC4626 + chi/VSR accumulator) with a NOVEL TAKER_ROLE that pulls liquidity to deploy in strategies. Reusable for any chi/index-accumulator vault or liquidity-pull vault fork.
When share price derives from a TIME-EXTRAPOLATED accumulator (nowChi() = _rpow(vsr, now-rho)*chi/RAY) rather than balance/supply, then:
- DONATION-immune: donating assets to the vault changes balanceOf but NOT chi -> share price unchanged (only lowers assetsOutstanding = user-favorable). The ERC4626 first-deposit/inflation attack is STRUCTURALLY IMPOSSIBLE (price = chi, not a balance/supply ratio). Same family as the share-accounted lesson, stronger.
- DRIP-ORDER-immune: a function that skips drip() (like take) creates NO conversion mismatch, because every view (totalAssets/convertToAssets/assetsOutstanding) recomputes at LIVE time via nowChi(). The stale STORED accumulator (chi) only matters if a SHARE<->ASSET path reads it instead of the live value.
- AUDIT CHECK: "does any share<->asset conversion read the STORED accumulator instead of the live nowChi()?" If the stale stored value feeds only an EVENT field (here drip's diff), it is inert. If it feeds a conversion, a function that skips drip() is a bug.
vsr >= RAY) is the single load-bearing guardrailchi monotonic-non-decreasing requires nChi = _rpow(vsr,dt)*chi/RAY >= chi, i.e. vsr >= RAY. Enforced by setVsrBounds: require(minVsr_ >= RAY) + setVsr: require(newVsr >= minVsr). This ONE check protects the WHOLE chain: if minVsr could drop below RAY, chi could DECREASE -> assetsOf drops -> users lose value retroactively, and the diff = totalSupply*nChi/RAY - totalSupply*chi/RAY subtraction could underflow. FLAG any future change that loosens minVsr >= RAY. (General: for any index/rate accumulator, find the single floor invariant that keeps it monotonic and treat it as load-bearing.)
take) is safe IFF the debt tracker is DECOUPLED from conversion mathThe novel pattern: a TAKER pulls liquidity (take -> _pushAsset, bounded by value <= balanceOf), tracked by assetsOutstanding() = totalAssets() - balanceOf(this) (clamped >= 0). This is SAFE because:
- take touches NO accounting state (chi/rho/totalSupply/balanceOf-shares) - it only moves real tokens. Accounting-INERT by design.
- assetsOutstanding is PURELY INFORMATIONAL (a display of the taker's debt) - it is NEVER an input to mint/redeem pricing. THE ANTI-PATTERN to hunt in other liquidity-pull vault forks: an assetsOutstanding-equivalent that FEEDS BACK into share pricing (then donate/take manipulates share value).
- Withdrawals are LIQUIDITY-AWARE: maxWithdraw/maxRedeem = min(user, balanceOf*RAY/nowChi); the withdraw path reverts if balanceOf < assets. So a taker draining liquidity causes clean reverts (trusted-taker illiquidity = accepted), not insolvency-at-wrong-price.
- The chi-accrued diff is emitted but NOT minted/backed -> the taker must FUND the yield (trusted-taker model). totalAssets grows via chi; backing comes from the taker's strategy returns. Accepted design (like a lending pool's utilization).
_burn: decrement balanceOf/totalSupply BEFORE safeTransfer out (CEI) - correct here._mint: safeTransferFrom (pull) precedes the share update, but it's the FIRST state change -> reentering during an asset hook == entering before _mint (chi-priced shares for full assets paid, no extraction; transient cap-overshoot only). Safe ONLY because the asset is governance-chosen (no hooks/FoT/rebasing). FLAG if the vault ever allows an arbitrary/hook asset.
Related: [[2026-06-11-metamorpho-totalassets-is-share-accounted-not-donation-inflatable]], [[2026-06-21-cycle267-snapshot-postcondition-safety-net-for-new-entry-paths]], [[2026-06-21-cycle267-lst-deposit-min-rate-market-defeats-depeg-and-curve-manip]].