RWA-backed stablecoin: withdrawal-queue + keeper-bot + sanctions-wrapper + yield-adapter audit primitives
Date: 2026-06-13
Source: Cycle-229 dreUSD gap-fill (board-dry lesson-mine). Pre-armed primitives for the components the existing cycle-229 hypotheses do NOT cover. Banked from public scope metadata only (no source read - these are HYPOTHESES/lenses, not findings).
Transfers to: RWA-backed / cash-backed stablecoins with an async redemption queue (dreUSD, Ondo USDY/OUSG, M^0, Mountain USDM, Midas mTBILL/mBASIS, Hashnote USYC, Backed bTokens) and any ERC-4626 vault that pairs a withdrawal-NFT queue with a keeper/automation bot.
Why this file exists
Cycle-229 (dreUSD, Sherlock 1259) pre-staged 4 hypotheses: mint/redeem decimals (cycle212), ERC4626 inflation (cycle214/200), OFT supply-conservation (cycle234), rewards-sign (cycle206). Those cover the value-accounting core. They do NOT cover four other in-scope components that are classic RWA-stablecoin bug habitats:
dreWithdrawalNFT.sol (134) + IWithdrawalNFTQueue - tokenized async-redemption claim
dreWithdrawalKeeperBot.sol (87) + IAutomationCompatible + IWithdrawalKeeperManager - the off-chain crank
whitelist/SanctionsListWhitelistWrapper.sol (35) + ISanctionsList - OFAC/transfer gate
dreAaveAdapter.sol (105) + IAaveV3Pool - yield-on-backing deployment
This file banks the lens for each, so the moment operator unblocks the repo the hunt fires broad, not narrow.
Primitive A - Withdrawal-NFT queue (claim/redeem async)
The pattern: redeem burns/locks stable now, mints an NFT representing a claim on USDC later; keeper finalizes; holder calls claim. Bug habitat:
- A1 Price-lock timing (the big one). At what instant is the redeemed USDC amount fixed - at request (NFT mint), at keeper-finalize, or at claim? If the exchange rate / oracle can move between request and claim, a redeemer either front-runs a known rate increase (request right before, claim after) or the protocol eats the delta. Compare to cycle214 "epoch-uniform locked-price + open-only-cancel": the SAFE shape locks price at request and forbids post-lock value mutation. Check: can the NFT's payout be recomputed at claim using a current read? If yes -> rate-arbitrage seam.
- A2 NFT transfer + claim auth. NFT is transferable -> claim must pay
ownerOf(id) at claim-time, not the original requester. A captured requester field paid instead of current owner = stolen-claim. Conversely if it pays current owner but burns by a stale index -> double-claim. Also: does the sanctions/whitelist gate (Primitive C) apply to the NFT TRANSFER and to CLAIM, or only to the original mint? A sanctioned address can often receive a claim NFT by transfer and cash out if claim() doesn't re-check.
- A3 Queue accounting vs solvency. Sum of outstanding NFT claims must be reserved against backing. If new redemptions or yield-adapter withdrawals (Primitive D) can drain reserves that a queued NFT already has a claim on, late claimers get bricked (first-come drains the pool) = liquidation-DoS-class. Check the order: is queued liability subtracted from available-for-mint/withdraw?
- A4 Cancel/partial-fill rounding. If a request can be partially filled or cancelled, the refund path is a second value-mutation site - re-run cycle212 validate-then-mutate + rounding-direction on it.
Primitive B - Keeper / automation bot (Chainlink-Automation-shaped)
IAutomationCompatible => checkUpkeep (view, off-chain) + performUpkeep (state). Bug habitat:
- B1 performUpkeep auth. Chainlink Automation's
performUpkeep is callable by ANYONE by default (the registry is just the usual caller). If the bot trusts checkUpkeep's returned performData without re-validating on-chain, an attacker calls performUpkeep directly with crafted performData to finalize the WRONG queue entries, finalize at a stale rate, or skip the price-lock. The classic Automation bug = "performData is attacker-controlled; validate it on-chain in performUpkeep." Check every field of performData is re-derived/re-checked, not trusted.
- B2 Crank ordering / griefing. Can an attacker call performUpkeep to process the queue out-of-order, or repeatedly with empty work to grief gas / advance a cursor past unprocessed entries? Is there a bounded batch with a resumable cursor, and can the cursor be desynced?
- B3 Keeper-manager privilege.
IWithdrawalKeeperManager implies a manager role can set/replace the keeper. Single EOA keeper-manager that can redirect finalize-payouts or pause the queue = centralization + a keys-compromise blast radius (note for the report, usually informational unless it can steal).
- B4 checkUpkeep/performUpkeep state divergence. checkUpkeep is off-chain over a possibly-stale block; performUpkeep runs later. Any invariant assumed in checkUpkeep (rate, balance, queue head) must be RE-asserted in performUpkeep or the gap is exploitable (the same request/claim TOCTOU as A1).
Primitive C - Sanctions / whitelist wrapper
SanctionsListWhitelistWrapper over Chainalysis-style ISanctionsList.isSanctioned(addr). Bug habitat:
- C1 Enforcement-surface completeness. The gate must cover EVERY value-moving path: mint, transfer, redeem-request, NFT-transfer (A2), claim, OFT send/receive (cross-chain re-entry of a sanctioned addr), and rewards-claim. RWA stablecoins routinely gate
transfer but forget the OFT receive path or the NFT-claim path -> a sanctioned address re-enters via the unchecked door. Enumerate all 6+ doors, check each calls the wrapper.
- C2 Fail-open vs fail-closed. If
ISanctionsList reverts / returns garbage / is unset, does the wrapper block (fail-closed) or pass (fail-open)? An unset or swappable list address = governance can silently disable the gate. Cross-ref the cycle238 governance/timelock lens for who can set the list.
- C3 Wrapper bypass. Is the underlying token's raw
_transfer reachable WITHOUT the wrapper (e.g., the OFT adapter or Aave adapter holding/moving tokens internally), skipping the gate? "Pause-circumvention via direct underlying-dependency call" (CLAUDE.md novel-pattern list) applies directly.
Primitive D - Aave V3 yield adapter (yield on backing)
Backing USDC is deployed into Aave V3 for yield. Bug habitat:
- D1 Liquidity vs redemption race. If backing sits in Aave, an instant redeem / queue-finalize needs to withdraw from Aave first. If Aave utilization is high (withdraw reverts / partial), redemptions brick = liquidity-DoS. Is there a reserved buffer, and can an attacker spike Aave utilization to block redemptions (griefing) or force a discounted exit?
- D2 aToken accounting. aTokens rebase (balance grows). If
totalAssets() for the ERC-4626 vault (cycle214 lens) reads aToken balance directly, donation/rebase timing interacts with share price. Confirm totalAssets uses the rebasing balance consistently and isn't double-counting buffered USDC + aUSDC.
- D3 Adapter auth + reward token sweep. Who can call deposit/withdraw on the adapter? Aave liquidity-mining reward tokens accruing to the adapter - is there a sweep, and can it pull principal? Check
dreAaveAdapter withdraw can't be pointed at an attacker recipient.
- D4 Loss socialization. If Aave incurs a bad-debt/haircut, who eats it - the vault PPS (all holders) or the redemption queue (late claimers, A3)? Mismatch between who-bears-loss and who-already-locked-a-price = an exit-before-loss arbitrage.
Highest-EV firing order when source lands (combine with cycle-229's 4)
- A1 + B4 + B1 (price-lock TOCTOU across request -> keeper-performUpkeep -> claim, with attacker-controlled performData) - this triad is the single most likely real bug; it crosses three contracts and is exactly where async-queue stablecoins break.
- C1 enumerate all 6+ sanctions doors; the NFT-claim and OFT-receive doors are the usual misses.
- A3 + D1 queue-liability vs Aave-buffer solvency (late-claimer DoS).
- Then the cycle-229 four (decimals, inflation, OFT supply, rewards-sign).
Reflection
- WORKED (method): board-dry time should pre-arm the ONE gated fresh target's UNCOVERED surface, not grind saturated cores. cycle-229 had 4 value-accounting hypotheses; this adds the queue/keeper/sanctions/adapter axis -> hunt fires broad on unblock.
- TRANSFER: the A1/B1/B4 async-queue-TOCTOU triad and the C1 "enumerate every gate door" lens generalize to ALL async-redemption RWA stablecoins and to any ERC-4626 + withdrawal-NFT + Chainlink-Automation stack.
- Cross-refs: [cycle212 decimals/validate-then-mutate], [cycle214/200 ERC4626 inflation + epoch-locked-price], [cycle206 rewards-sign], [cycle234 OFT], [cycle238 governance/timelock], CLAUDE.md novel-pattern "pause-circumvention via direct underlying-dependency call".
- These are LENSES, not findings. No dreUSD source was read (repo still anon-404 as of 2026-06-13). Nothing claimed PASS/BREAK.