← back to lessons

Lesson: settle LST/LRT self-rate donation-inflatability with a FORK DONATION TEST, never with a single balanceOf read

Banked 2026-06-11 (SEAMS lane, self-rate variant follow-up to Morpho ezETH/WETH WALK)

The lesson

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.

Concrete miss-and-correct (pufETH)

How to test cheaply (the canonical snippet)

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.

Economics gate (donation cost-vs-extract, sunk-cost rule)

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.

Consumer-side scope gate (decisive STAGE vs WALK)

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.

What WALKED this cycle

Next-target pointer (24h queue)

  1. Hunt a LOW-TVL ERC4626 LST/LRT vault priced raw via convertToAssets in a PERMISSIONED lender (governance-listed). That is where the donation economics flip profitable.
  2. Dead-feed DoS: ZeroLend rsETH source 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.
  3. EulerRouter resolvedVaults sweep across all governed Euler mainnet routers for any low-TVL LST vault resolved via convertToAssets.
Generated 2026-07-02 13:15:05 UTC | auto-sync /15min