← back to lessons

Snapshot + recomputed-share postcondition = safe-by-construction new entry-paths (EtherFi PriorityWithdrawalQueue, 2026-06-21)

Pattern (positive case study / invariant)

A share-accounted withdrawal queue can add NEW user entry-paths (new token, new permit variant) safely WITHOUT re-auditing the accounting, IF every entry-path: 1. takes a balance snapshot BEFORE any external interaction: lpEthBefore = LP.balance; queueSharesBefore = shareToken.shares(address(this)); 2. funnels into ONE shared internal _queueWithdrawRequest(user, amount, amountWithFee); and 3. ends with a postcondition that asserts the EXACT expected delta recomputed from the SAME amount: - shareToken.shares(this) == queueSharesBefore + sharesForAmount(amount) (exact, not >=), AND - LP.balance == lpEthBefore (no surprise ETH movement).

Why it's safe-by-construction: the actual shares moved by an amount-denominated transfer are sharesForAmount(amount) (the share token converts amount->shares with its own rounding), and the postcondition expects sharesForAmount(amount) for the SAME amount -> they match by identity regardless of the amount<->share round-trip rounding. Any deviation (unsafe downcast truncation, reentrancy moving shares, the unwrap/transfer touching LP ETH, a fee-on-transfer token, double-credit) makes the == false and REVERTS. So a whole class of accounting bugs is converted into a revert, not a loss.

The concrete instance

EtherFi PriorityWithdrawalQueue added requestWithdrawWithWeETH/...AndPermit AFTER its 2026-03-05 Certora audit (which covered only the eETH path). The new path does weETH.unwrap(weEthAmount) (which = eETH.transfer(this, amountForShare(weEthAmount)), moving sharesForAmount(eEthAmount) shares) then the SAME _verifyRequestPostConditions(.., eEthAmount) that asserts +sharesForAmount(eEthAmount). Exact match. The uint96(weETH.unwrap(...)) unsafe downcast is also neutralized: a truncated eEthAmount makes the postcondition expect the wrong (small) delta while the queue actually received the large delta -> revert, no loss (needs ~79B ETH to trigger anyway).

How to use this when auditing post-audit drift

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