← back to lessons

Concrete Earn V2 - Cycle 214 keeper lessons (async ERC4626 withdrawal-queue vault)

Target: Concrete Earn V2 (Cantina $250K). ERC4626 multi-strategy aggregator + epoch-batched async withdrawal queue. HEAD a9ace61 (2025-11-12). Verdict: WALK CLEAN (6/6 PoCs defended).

Primitive: epoch-batched async-withdrawal-queue ERC4626

When you meet a vault where withdraw/redeem do NOT pay out immediately but escrow shares into a per-epoch queue that a keeper settles later (Concrete, ERC7540-style, Lagoon-style), the deposit-side inflation checklist is necessary but not sufficient. Add the queue checklist below.

Deposit-side (same as any ERC4626)

Queue-side checklist (NEW - the high-yield surface here)

  1. Single epoch-uniform settlement price. Every requester in an epoch must settle at ONE price computed at processing time. If different requesters in the same epoch can settle at different prices, there is a selection/ordering edge. Concrete locks epochPricePerSharePlusOne[epoch] once in processEpoch and every per-user claim uses it => PASS.
  2. Cancel only while the epoch is OPEN (before the price is knowable). If a user can cancel AFTER the settlement price is computed/observable, they hold a free option: claim when favorable, cancel and re-enter (or redeem via the standard path) when not. Concrete's cancelRequest(epochID) requires epochID >= latestEpochID (epoch still active); once closeEpoch advances the id, user cancel reverts. => PASS. Breaking this leg is the #1 thing to hunt on these vaults.
  3. Obligated assets are fenced. Settled-but-unclaimed assets (pastEpochsUnclaimedAssets) must be excluded from the standard withdraw float AND from any allocation-to-strategy path, or a later withdrawer / the allocator can drain assets owed to queue claimants. Concrete: _lockedAssets() = pastEpochsUnclaimedAssets, subtracted from withdrawable float and re-asserted in allocate. => PASS.
  4. Solvency-before-settlement guard. processEpoch must require availableAssets >= pastEpochsUnclaimedAssets + assetsNeeded before committing the epoch. Concrete has it. NOTE: a value-conservation PoC that reverts on THIS guard is a positive signal, not a dead test - reshape the scenario to stay solvent and re-test conservation (we hit this on H7).
  5. Rounding sign on settlement. Aggregate assetsNeeded Floor + per-user claim Floor => sum of per-user <= aggregate, so dust strands in the vault (protocol-favorable). Confirm the per-user and aggregate use the SAME price and SAME rounding direction. Concrete: both Floor, same epochPrice => PASS.

Role-fence first (saved most of the time)

doc/Architecture.md fenced 7 trusted roles. processEpoch/closeEpoch/moveRequestToNextEpoch/ claimUsersBatch/allocate/manager-overload cancel+claim are all keeper-gated => OUT of scope as attacker vectors. The only untrusted-reachable value surface is deposit/mint/withdraw/redeem + user cancelRequest(epochID) + user claimWithdrawal(epochIDs). Filter to those FIRST, then spend all empirical effort there.

PoC build notes (for next Concrete-family cycle)

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