Banked 2026-06-11 (SEAMS lane, self-rate variant follow-up to Morpho ezETH/WETH WALK)
When deciding whether a token's self-rate (convertToAssets / getRate / totalAssets) is donation-inflatable (brief case (a)), the cheap passive read can LIE. The decisive test is an empirical fork donation, not a balanceOf inspection.
0xD9A442856C234a39a81a089C06451EBAa4306a72) asset() = WETH, but WETH.balanceOf(pufETH) = 0 while totalAssets() = 27748e18. Passive read => "totalAssets is internal accounting => donation-resistant." THIS WAS WRONG.totalAssets +100 WETH (27748 -> 27848), convertToAssets(1e18) 1.076623 -> 1.080503 (+0.36%, 3603 millionths). pufETH's totalAssets() DOES count WETH.balanceOf(self). It IS donation-inflatable.balanceOf(asset) (plus staked/internal positions). balanceOf==0 != internal-accounting. Always run the donation fork test.uint256 r0 = vault.convertToAssets(1e18);
deal(vault.asset(), address(this), D);
IERC20(vault.asset()).transfer(address(vault), D); // donation, no shares minted
uint256 r1 = vault.convertToAssets(1e18);
// r1 > r0 => donation-inflatable (case a). r1 == r0 => internal/share-accounted (safe).
If deal fails on a rebasing token (stETH), use the asset() token; the asset-side donation is the relevant surface anyway.
A single non-recursive donation is unrecoverable (you get NO shares). To move the rate X%, donate X% of totalAssets. Net for an attacker holding C collateral tokens at LLTV L: - net = D * (L*C / totalAssets - 1). Profitable ONLY if C > totalAssets / L. - Translation: for a one-shot donation to pay off, you must ALREADY hold collateral worth more than the entire vault TVL. So donation-inflatability is dangerous mainly for LOW-TVL vaults priced raw via self-rate, OR via recursive stealth-donation on SMALL share bases (rounding compounding) - impractical on a 25M-share, TVL-$45M vault like pufETH. - ALWAYS pair the "mechanism confirmed" with the "economics impractical?" check before claiming severity.
The mechanism living in the token is NOT a finding by itself. You need a LIVE consumer that reads the self-rate RAW. Map the consumer's oracle source first:
- Aave-v3-fork (ZeroLend etc.): AaveOracle.getSourceOfAsset(token) then read adapter storage slot0/slot1. RedStone/Chainlink MARKET feed x ETH/USD = defended (NOT self-rate). Raw ETH/USD = ignores premium (under-prices, safe). A convertToAssets/getRate adapter = the dangerous case.
- Euler V2: EulerRouter.resolvedVaults(token) != address(0) means it IS priced via convertToAssets. Euler's docs EXPLICITLY designate this a per-vault GOVERNOR responsibility gated behind "verify convertToAssets can't be manipulated" -> permissionless/governor accepted-risk unless a specific governed vault wired a low-TVL donation-inflatable vault.
- Permissioned (governance-listed reserve) + self-rate + low TVL = stageable. Permissionless curator/governor choice = advisory only.
0xF4a3e183...3696. pufETH/swETH = raw Chainlink ETH/USD (under-priced, safe). No self-rate consumer. WALK.0x33B13F46...F1Be and pzETH source feed 0x0b6c5c37...870a currently REVERT (InvalidLastUpdateDetails). A reverting price source can brick borrow/withdraw/liquidate on that reserve. Separate brief - check if these reserves have live debt that cannot be liquidated.resolvedVaults sweep across all governed Euler mainnet routers for any low-TVL LST vault resolved via convertToAssets.