← back to lessons

Lesson: Rule 38.4 on Morpho - "fee accrual removed from reallocate" is conservation-equivalent, not a regression

Banked 2026-06-11 (Cycle: Rule-38 Morpho regression sweep). Class: post-audit regression / fee-on-donation invariant / 38.4 replaced-not-reverted.

Context

Morpho MetaMorpho commit 0f64b92 added _accrueFee() at the top of reallocate() plus _updateLastTotalAssets(totalAssets()) at the end, justified as "avoid taking a fee on withdrawn donations." A later v1.1 refactor (aeb7ac1) REMOVED both. Naive Rule-38 reading: "a fee-protection fix was deleted -> regression candidate."

Lesson (the discriminator)

A removed fee-accrual call is NOT automatically a regression. Apply the conservation test before writing any PoC:

  1. Does the function preserve the quantity the fee is charged on? MetaMorpho reallocate enforces totalWithdrawn == totalSupplied, so totalAssets() (sum of supply positions) is invariant across the call. Fee = f(totalAssets - lastTotalAssets), so a call that does not change totalAssets cannot create a fee delta. The _accrueFee inside reallocate was therefore a no-op for the fee amount; removing it changes nothing about how much fee is taken.

  2. Where did the real protection go? The donation-griefing guard MOVED, it was not deleted: (a) the assets==0 branch withdraws FULL supply shares (donation included) when emptying a market; (b) _setCap on first-enable bumps lastTotalAssets by the market's existing expectedSupplyAssets "without applying a fee." A 38.4 "replaced mechanism" can be SAFE when the replacement covers the same invariant by a different code path.

Generalizable rule (add to Rule-38 toolkit)

38.4-A "Conservation discriminator": when a removed guard concerns a fee/delta charged on quantity Q, first ask whether the function provably conserves Q (look for an explicit in == out invariant check). If Q is conserved, the removed guard was redundant for that call and removal is not a regression - do NOT write a pure-math differential test (it would prove a tautology). Switch to a behavioral mainnet-fork PoC (perform the action, measure the protected quantity's delta == 0) only if you want empirical confirmation, and label reasoned-INTACT vs empirically-INTACT honestly.

38.4-B "Follow the moved protection": before flagging a deleted fix as regressed, grep for the same invariant being enforced elsewhere (setCap/enable paths, full-share-withdraw branches, init bumps). Audit fixes in actively-refactored vault code frequently relocate rather than disappear.

Why this compounds

Saves a wasted Foundry differential cycle on tautological math, and prevents a false-positive "regression" submission. Pairs with the Aave L-01 lesson (where two DIFFERENT formulas DID need a pure-math equivalence test): the discriminator is "competing formulas -> pure-math test" vs "conserved quantity -> conservation argument or behavioral PoC, never pure-math."

Generated 2026-07-02 13:15:05 UTC | auto-sync /15min