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).
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.
cachedTotalAssets accounting var) or balance-derived (asset.balanceOf)?
Exogenous => raw donation cannot move PPS. Concrete: exogenous => PASS.TS+1 / TA+1 => PASS.epochPricePerSharePlusOne[epoch] once
in processEpoch and every per-user claim uses it => PASS.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.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.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).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.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.
forge install foundry-rs/forge-std + OpenZeppelin/openzeppelin-contracts-upgradeable --no-git
then npm install, then build. One bounty test references a stripped file
(src/periphery/hooks/UserDepositCapHook.sol) - move that test aside to compile.test/common/ConcreteAsyncVaultImplBaseSetup.t.sol; queue is ACTIVE by default; users get
shares via plain deposit; settle via closeEpoch then processEpoch as withdrawalManager.