SushiSwap MISO DutchAuction Batchable msg.value Refund-Drain Near Miss (2021-08-17)
- Name: SushiSwap MISO DutchAuction batchable
msg.valuerefund-drain near miss - Date: 2021-08-17
- Protocol: SushiSwap MISO
- Chain: Ethereum
- Category: ambient payment-context reuse /
msg.valuereplay /delegatecallbatch-composition failure / refund-path drain / whitehat near miss - Loss: no realized theft, but samczsun estimated about
109,000 ETH— roughly$350Mat the time — was exposed in the live Dutch auction contract - Summary: SushiSwap’s MISO launchpad mixed two individually reasonable components into a dangerous system.
DutchAuction.commitEth()priced participation from rawmsg.value, calculated how much of that payment fit before the auction cap, and refunded the excess. Separately,BoringBatchable.batch()let callers execute multipledelegatecalls back into the same contract. Underdelegatecall, each nested call inherits the outer transaction’smsg.value. That meant one outer ETH payment could be reused across manycommitEth()subcalls. Early in the batch, the auction treated that ambient ETH as repeated new commitments; once the auction approached or exceeded its cap, later subcalls treated the same ambient ETH as fully refundable and transferred it back out. In the extreme case Sam described, sending1 ETHinto a batch with only1 ETHof capacity left could make the first subcall consume that last slot and every later subcall refund another1 ETH, turning one real payment into a multi-refund drain. Whitehats and Sushi contributors privately coordinated a response, the active Dutch auction sold out naturally, and the team finalized it before anyone exploited the bug onchain. - Trigger / entry point:
- The live vulnerable contract was the MISO Dutch auction at
0x4c4564a1FE775D97297F9e3Dc2e762e0Ed5Dda0e, which samczsun inspected directly on Etherscan. DutchAuctioninheritedBoringBatchable, makingbatch(bytes[] calls, bool revertOnFail)available on the auction contract itself.batch()usedaddress(this).delegatecall(calls[i])for each nested payload, preserving the outer call’smsg.senderandmsg.valueacross all subcalls.- The payable participation path
commitEth(address payable _beneficiary, bool ...)trusted rawmsg.valueas the ETH presented by that logical commitment.
- The live vulnerable contract was the MISO Dutch auction at
- Root cause:
- MISO implicitly treated
msg.valueas a fresh per-commit payment proof even when the same outer call frame could execute many commitments. BoringBatchable.batch()was not broken in isolation, andcommitEth()was not obviously broken in isolation. The exploit appeared only when the two were composed.- Inside
commitEth(), lines272-281computedethToTransfer = calculateCommitment(msg.value)andethToRefund = msg.value - ethToTransfer, then transferred the refund to the beneficiary. - Because
delegatecallpreserved the same ambientmsg.valuefor every batched subcall, eachcommitEth()invocation re-ran the same commitment-and-refund logic as if fresh ETH had arrived again. - The hard-cap / partial-fill logic made the bug much worse: once remaining auction capacity was small or zero, repeated subcalls converted reused
msg.valueinto repeated refunds instead of just free commitments.
- MISO implicitly treated
- Exploit path:
- The attacker prepared a
batch()call containing many encodedcommitEth()payloads targeting the same live Dutch auction. - They sent only one outer ETH payment with the
batch()transaction. BoringBatchable.batch()iterated through the payloads anddelegatecalled intocommitEth()for each one, preserving the samemsg.valueevery time.- On each subcall,
commitEth()computedethToTransfer = calculateCommitment(msg.value)from that reused ambient payment context. - If auction capacity still remained, some early subcalls were credited as if the attacker had paid again; once the auction was near full, later subcalls computed
ethToTransferas a small residual or0andethToRefundas most or all of the samemsg.value. - Each of those later subcalls independently executed
_beneficiary.transfer(ethToRefund), so the same outer ETH payment could generate many refunds. - Sam’s concrete illustration was the sharpest version: with only
1 ETHof auction capacity left, a batched call that reused1 ETHacross one hundredcommitEth()subcalls could let the first call fill the final1 ETHslot and the remaining ninety-nine calls each refund1 ETH, extracting99 ETHnet.
- The attacker prepared a
- What the attacker needed to be true:
- The target MISO auction needed to accept ETH through
commitEth()and expose the inheritedbatch()helper. batch()needed to preserve outer call context for nested calls, whichdelegatecalldoes by design.commitEth()needed to use rawmsg.valuerather than a consumed internal payment budget.- The auction needed either remaining capacity or a cap-refund branch so repeated subcalls could monetize the reused payment context.
- Enough ETH had to remain in the contract — from ongoing commitments or auction balances — for the refund leg to be cash-settled.
- The target MISO auction needed to accept ETH through
- Technical anatomy worth remembering:
- The key preserved lesson is not
delegatecall dangerousin the abstract. The important mistake was using ambient call-level payment context as if it were per-subcall economic truth. - The ERC-20 path was meaningfully safer here:
commitTokensFrom()called_safeTransferFrom(...)for each commitment, so each logical commitment required a fresh token transfer. The native-asset path reusedmsg.valueinstead. - The refund branch is what turned a free-bid bug into a treasury-drain bug. Without refunds, the issue could still corrupt auction outcomes; with refunds, it could directly extract ETH.
- This is a clean composition failure. Reviewers can audit a batch helper and a payable commitment function separately and still miss the emergent exploit path at the boundary.
- The incident also produced an operationally useful mitigation pattern for the still-live MISO batch auction: use an external validation hook to check that actual ETH balances matched total commitments, blocking fake batched commitments without changing auction state.
- The key preserved lesson is not
Three payment-provenance promotions this incident collapsed
- A payable ETH commitment-shaped action existed at all
- Through
BoringBatchable.batch(), MISO could still reachcommitEth()many times inside one outer transaction, and each nesteddelegatecallstill arrived carrying a valid-lookingmsg.senderplus the same ambientmsg.value. - At this stage the strongest true fact was only
a batched subcall reached the normal ETH commitment function while one outer payment context still existed, notthis logical commitment brought fresh ETH consideration of its own.
- Through
- That action was promoted into canonical truth that this exact logical bid had paid fresh ETH into the auction
- This was the decisive failure.
commitEth()treated rawmsg.valueas if it were subcall-local payment proof even though batching let many logical commitments reuse the same outer call frame. - MISO therefore collapsed
a nested call can still read one ambient payment contextandthis specific commitment independently contributed new ETH to auction accountinginto the same accounting truth, so repeated subcalls were credited as if repeated funding had arrived.
- This was the decisive failure.
- That accepted payment truth still deserved independent refund authority against live auction ETH
- Once the hard-cap logic saw little or no remaining capacity, later batched subcalls still computed their own
ethToRefund = msg.value - ethToTransferfrom the reused payment context and transferred ETH out separately. - The system therefore preserved almost no boundary between
one real payment already got consumed by an earlier logical commitmentandlater logical commitments may still cash out their own refund story, which is why the bug escalated from fake participation into a potential treasury drain.
- Once the hard-cap logic saw little or no remaining capacity, later batched subcalls still computed their own
- Representative contracts and code paths:
- Vulnerable live Dutch auction contract:
0x4c4564a1FE775D97297F9e3Dc2e762e0Ed5Dda0e BoringBatchable.batch()delegatecall loop:contracts/Utils/BoringBatchable.sollines35-43in commit44b51769b614826c50d8f5c55cb60094fa1c7badDutchAuction.commitEth()refund logic:contracts/Auctions/DutchAuction.sollines261-283in the same commitDutchAuction.calculateCommitment()cap logic:contracts/Auctions/DutchAuction.sollines362-367
- Vulnerable live Dutch auction contract:
- Response / mitigation:
- Samczsun verified the issue, escalated it through Georgios Konstantopoulos to Sushi contributors, and a small war room formed with Sushi and Immunefi participants.
- The team considered three options: do nothing, whitehat-rescue funds using the exploit, or buy out and finalize the live Dutch auction.
- They prepared a flash-loan-assisted rescue / finalization contract for the live Dutch auction in case they had to force the close themselves.
- Before the rescue had to be executed, the live Dutch auction sold out naturally; the team then finalized it immediately, moving funds out of the vulnerable auction contract and neutralizing the highest-risk exposure.
- A second live MISO batch auction shared the broader fake-commitment issue but lacked the Dutch auction’s refund path, so it posed outcome-integrity risk rather than direct treasury-drain risk. The team designed a points-list-based validation hook that checked ETH balances against commitments, then ultimately let that auction finish naturally because no direct funds-at-risk path remained.
- Key transactions and artifacts:
- No public theft transaction hashes are known because the issue was privately disclosed and mitigated before exploitation.
- The key public artifacts are the vulnerable live auction contract, Samczsun’s reconstruction, Mudit Gupta’s war-room writeup, and the archived MISO source code showing
batch()andcommitEth().
Pattern tags
- ambient-payment-context-reuse
delegatecallbatch payment replay / inheritedBoringBatchable.batch()preserved one outermsg.valueacross many nestedcommitEth()calls- Refund-amplification branch / once auction capacity approached zero, reused payment context turned into repeated ETH refunds rather than only fake commitments
- Component-composition invariant failure /
batch()andcommitEth()looked locally reasonable, but the composed system no longer had one-payment-per-commitment semantics
Why the exploit worked
- MISO let one outer transaction masquerade as many logical ETH commitments.
BoringBatchable.batch()executeddelegatecall(address(this), ...), so every nestedcommitEth()inherited the samemsg.valueandmsg.senderas the parent transaction instead of seeing a fresh payment boundary for each commitment. commitEth()used ambient call context as if it were subcall-local economic truth. The function computedethToTransfer = calculateCommitment(msg.value)andethToRefund = msg.value - ethToTransfer, which is only safe when one payable commitment consumes one call frame. Under batching, that same formula replayed the same outer ETH across every nested commitment.- The hard-cap refund path converted semantic confusion into direct extractable value. Early batched calls could still be read as fake commitments, but once remaining auction capacity shrank, later subcalls independently refunded most or all of the reused
msg.value. That is why the live Dutch auction carried treasury-drain risk rather than merely bad auction accounting. - The exploit existed at the system boundary, not inside one obviously broken function. Mudit’s war-room note and Sam’s writeup are useful precisely because they preserve the composed lesson: a helper that safely batches ordinary calls and a payable auction function that safely handles ordinary users can still become unsafe when the helper erases the payment provenance the auction relied on.
Detection and audit pivots
- Treat every inherited
batch,multicall, module executor, or router self-delegatecallhelper as part of the threat model for every payable function in the contract. If a nested call can still read outermsg.value, then any logic keyed directly offmsg.valuedeserves adversarial composition review. - Distinguish
commitment inflationfromrefund amplification. MISO is the reminder that the refund branch is often the economically decisive leg: once the contract can independently computemsg.value - acceptedAmountseveral times, the bug graduates from free participation to actual ETH extraction. - Compare native-asset and tokenized payment paths. If the ERC-20 path proves each commitment with
transferFrom()but the ETH path just re-readsmsg.value, the native path is carrying a weaker payment-provenance model even before batching is added. - Fuzz capacity-edge cases, not only fully open auctions. The most dangerous states are often
almost full,hard cap reached, orsmall residual capacitybecause that is where a reused payment context starts emitting refunds rather than only bogus commitments. - Ask the composition question bluntly: can one real incoming payment satisfy more than one logical entitlement or refund story in one transaction? If the answer is yes, or the contract cannot point to an internal consumed-value ledger, then the auction still lacks a real payment-provenance boundary.
Failure-prevention checklist
- Treat every inherited batch or multicall helper as part of the solvency surface for every payable function on the contract. If
delegatecall(address(this), ...)can reachcommitEth(), the review scope must include the composed system rather than the components in isolation. - Compute commitment and refund rights from an internally consumed payment budget, not from raw
msg.valuere-read inside each nested call. Once one subcall allocates some ETH, later subcalls should only be able to use what remains. - Forbid or specially gate value-sensitive functions under self-
delegatecallbatching. IfcommitEth()is only safe as a top-level entrypoint, encode that restriction onchain instead of assuming users or frontends will not batch it. - Keep native-asset and ERC-20 commitment semantics aligned. If
commitTokensFrom()proves each commitment with a fresh token transfer butcommitEth()only consults inherited call context, the ETH path should be presumed higher risk until normalized or wrapped. - Treat hard-cap and partial-fill refund branches as first-class loss surfaces. Repeated refund computation from one ambient payment is often more dangerous than repeated free participation.
- Maintain an independent balance-truth check or circuit breaker for live auctions. MISO’s war-room mitigation relied on comparing real ETH held to commitments, which is exactly the kind of invariant the contract should already know how to enforce.
Audit prompts
- Which payable functions can be reached through
batch,multicall, module execution, or self-delegatecall, even if the author mentally modeled them as one-shot entrypoints? - Does any nested path read raw
msg.valueand then independently computeacceptedAmountandrefundAmountwithout debiting a transaction-local budget first? - Can the same outer payment satisfy more than one commitment story, or worse, more than one refund story, in a single transaction?
- What happens near the hard cap or with only a tiny amount of auction capacity left? Is that edge state where the bug becomes cash extraction instead of merely free participation?
- If the ERC-20 path requires a fresh transfer per commitment but the ETH path does not, what exact justification keeps those two branches economically equivalent?
- If the protocol needed an emergency validation hook tomorrow, what invariant would it enforce: actual balance equals commitments, one commitment per caller, per-transaction payment budget consumption, or something weaker?
Lessons that should already be standard
Safe helperplussafe payable functioncan still be an unsafe contract once composition erases payment provenance.- Refund logic is custody logic. The moment a contract can independently compute and send
msg.value - acceptedAmount, replayed payment context can become a direct treasury-drain path. delegatecallpreserves more than control flow; it preserves authority context likemsg.senderandmsg.value, so any accounting keyed to those globals has to be reviewed compositionally.- Native-asset convenience paths usually carry more hidden semantics risk than token-transfer paths. If wrapping to WETH would make the design obviously safer, that is a useful warning, not mere implementation annoyance.
- Operational hooks that compare live balances to commitments are not just incident-response tricks; they expose the real invariant the system should have been enforcing all along.
Why this incident matters
- It is one of the clearest preserved examples of a
safe component + safe component = unsafe systemfailure in DeFi contract composition. - It deserves to sit beside more famous balance-delta and reentrancy bugs because the preserved invariant is equally reusable: one real payment was allowed to masquerade as many logical payments.
- It also cleanly separates
free participationfromcash drain. The hard-cap refund design is what escalated reusedmsg.valuefrom auction corruption into immediate ETH extraction.
Internal linkages
- Core recurring pattern: ambient-payment-context-reuse.
- Adjacent source-of-value pattern: balance-delta-provenance-failure.
- Positive-design companion: helper-path-invariant-reuse-and-fail-closed-discipline — useful when the sharper design question is why an inherited batching helper was allowed to preserve a weaker payment-provenance model than the auction’s canonical one-payment-per-commitment story.
- Adjacent bridge-side cousin: deposit-signal-locked-value-mismatch — different surface, but the same broader lesson that proving
some call context looked deposit-likeis weaker than proving real value arrived for the exact economic action being credited.
Control surface
-
The decisive leverage sat in the inherited
batch()helper, the ETH-specificcommitEth()payment and refund accounting, and the auction’s choice to refund excess ETH instead of rejecting post-cap commitments. -
The scary part was not just
msg.value can be reused. It was that the protocol turned each reuse into an independently cash-settled refund. -
Sources:
- https://samczsun.com/two-rights-might-make-a-wrong/
- https://mudit.blog/miso-war-room/
- https://www.coindesk.com/tech/2021/08/17/white-hats-just-defused-a-potential-350m-heist-on-sushiswap/
- https://github.com/sushiswap/miso/blob/44b51769b614826c50d8f5c55cb60094fa1c7bad/contracts/Utils/BoringBatchable.sol#L31-L43
- https://github.com/sushiswap/miso/blob/44b51769b614826c50d8f5c55cb60094fa1c7bad/contracts/Auctions/DutchAuction.sol#L261-L283
- https://github.com/sushiswap/miso/blob/44b51769b614826c50d8f5c55cb60094fa1c7bad/contracts/Auctions/DutchAuction.sol#L362-L367
-
Last reviewed: 2026-08-05 UTC