Ambient Payment-Context Reuse / msg.value Replay Across Batched Subcalls
- Name: ambient payment-context reuse /
msg.valuereplay across batched subcalls - Category: exploit pattern / payment provenance failure / ambient call-context reuse / native-asset accounting / batch-composition bug
- Summary: These exploits happen when a contract treats
msg.valueas proof that this specific logical sub-operation received fresh native-asset payment. That assumption is only safe when one payable action consumes one call frame. Once the same contract can execute multiple logical actions inside one outer call — via an internal loop, batching,delegatecall, or multicall — every subcall can inherit the same ambientmsg.value. If the code credits commitments, exercises options, mints assets, or computes refunds separately for each subcall, the same ETH or MATIC can be counted over and over. The dangerous leap is semantic, not arithmetic:msg.value > 0proves the outer transaction carried native assets, but it does not prove that each nested exercise, purchase, mint, or bid inside that transaction brought in fresh value of its own, or that any still-unconsumed remainder exists for later branches to spend or refund. - What it is:
- A protocol has a payable path whose accounting is keyed directly off
msg.value. - That payable path becomes reachable more than once inside one transaction through a batch helper,
delegatecallloop, multicall router, or explicit internal iteration. - Each logical subcall observes the same ambient payment context even though only one real native-asset transfer happened at the transaction boundary.
- The protocol treats each observation as a fresh payment and issues commitments, purchases, mints, or refunds accordingly.
- If partial-fill or oversubscription logic exists, the false repeated payment context can escalate from
free participationintodirect drain via repeated refunds.
- A protocol has a payable path whose accounting is keyed directly off
- Typical preconditions:
- The native-asset path uses
msg.valuedirectly instead of an internally consumedremainingValueledger or an explicit pre/post balance check. - The contract inherits or exposes a batching primitive such as
batch,multicall, or an internal loop over user-supplied actions. - The batch primitive preserves outer-call context for nested execution, as
delegatecalldoes. - The payable sub-operation can be run multiple times in one transaction without a one-shot guard.
- The economic logic contains either claim issuance on each subcall or refund logic that independently returns
msg.value - acceptedAmount.
- The native-asset path uses
- Common vulnerable surfaces:
- Dutch auctions and token-sale commitment functions that partially fill and refund the excess
- NFT mint or in-game purchase flows that price each action from
msg.value - options exercise or settlement loops where one ETH payment can be reused across multiple exercises
- generalized
batch()helpers mixed into contracts that were only reviewed as single-call entrypoints - native-asset wrappers that assume the payable path will never be invoked repeatedly inside one transaction
- Exploit sequence:
- The attacker identifies a payable function whose accounting or refund math depends directly on
msg.value. - They find a way to invoke that function multiple times inside one outer transaction, usually through
delegatecall-based batching or an internal loop. - They send one real native-asset payment with the outer call.
- Each logical subcall reuses the same ambient
msg.valueand is treated as if fresh payment arrived again. - The protocol repeatedly credits commitments, mints assets, or reaches a partial-fill branch that refunds excess value on each subcall.
- The attacker exits with free claims, inflated purchasing power, or more native assets refunded than were actually provided.
- The attacker identifies a payable function whose accounting or refund math depends directly on
- Variants:
- Direct loop replay — a function iterates through vault exercises, mints, or purchases and checks
msg.valueeach time instead of decrementing a consumed native-asset budget. Opyn is the clean preserved case. - Delegatecall batch replay — a helper like
batch(bytes[] calls, ...)usesdelegatecall(address(this), ...), so every nested payable function sees identicalmsg.senderandmsg.value. MISO is the canonical batch-helper variant. - Refund amplification — each subcall first accepts some capped amount and then refunds
msg.value - accepted, letting an attacker repeat the refund leg after the contract is effectively full. - Free-allocation participation — even without refunds, repeated
msg.valuereuse can let an attacker place bids or obtain allocations they did not really pay for, corrupting auction outcomes.
- Direct loop replay — a function iterates through vault exercises, mints, or purchases and checks
Three payment-context questions reviewers should not collapse
-
Did this transaction carry native assets at all?
- This is the weakest fact: the outer call arrived with nonzero
msg.valueor some other ambient native-payment context. - On its own, that only proves
some value entered this transaction frame somewhere.
- This is the weakest fact: the outer call arrived with nonzero
-
Did this exact logical action consume a uniquely assigned slice of that value?
- This is the harder accounting question: the protocol should be able to point to one action-local debit, receipt row, or monotonically shrinking
remainingValuebudget for this specific mint, bid, exercise, or purchase. - Opyn and MISO both preserve different ways this middle proof disappeared; the code kept reusing transaction-level payment as if it were fresh action-level payment.
- This is the harder accounting question: the protocol should be able to point to one action-local debit, receipt row, or monotonically shrinking
-
After that consumption, did any remaining budget still deserve later refund or entitlement authority?
- Even if one action genuinely consumed some portion of the transaction’s value, that does not mean later branches may still act as if the whole outer payment is unspent.
- The catastrophic versions of this family often cash out here, when a partial-fill or refund path quietly promotes
outer transaction carried valueintothis branch may still refund or allocate against the same full amount again.
- Detection heuristics:
- Search for any payable function that references
msg.value; then ask whether that function can be reached from multicall, batching,delegatecall, router callbacks, or explicit loops. - Compare native-asset and ERC-20 code paths. If ERC-20 logic uses
transferFromper operation but ETH logic only checksmsg.value, the native path deserves extra scrutiny. - Review partial-fill or hard-cap code carefully: a repeated refund branch is often more dangerous than the free-bid branch.
- Treat inherited batch helpers as part of the threat model even when the target function itself looks correct in isolation.
- Ask whether one transaction can cause more than one economic object — mint, bid, exercise, claim, or refund — to rely on the same outer-call payment context.
- Search for any payable function that references
- Defenses:
- Prefer ERC-20 style pull accounting or wrap native assets into WETH so every logical payment goes through an explicit transfer path.
- If native assets must be accepted, track a consumed payment budget explicitly rather than reading raw
msg.valueinside each sub-operation. - For batch helpers, forbid repeated invocation of payable accounting functions or require the caller to pre-split value and pass per-call amounts that are internally debited.
- Validate real balance growth or an explicit internal receipt variable instead of ambient call context when proving payment.
- Audit compositions, not just components:
safe payable functionplussafe batch helpercan still be an unsafe system. - Add adversarial tests that call the same payable function multiple times through the protocol’s batching surface with only one outer
msg.value.
What sound design looks like
- Healthy systems keep three payment facts separate on purpose:
this transaction carried native value,this exact logical action consumed this exact slice of that value, andthis later branch still has this much remaining budget to refund or spend. The design should never need one ambientmsg.valueobservation to stand in for all three. - The contract has an explicit answer to
which logical action consumed which native-asset consideration?That answer lives in storage or in transaction-local memory owned by the protocol, not in ambient EVM context. If three sub-operations run, the code can point to three debits against a finite internal value budget rather than rereading one outermsg.valuethree times. - Batch composition does not manufacture payment provenance. A healthy multicall or module executor either forbids value-sensitive function families entirely, pre-allocates per-subcall value and debits it monotonically, or normalizes native assets into WETH before any business logic that can repeat. The helper cannot pretend subcalls are isolated if it preserves one shared payment context.
- Refunds are derived from consumed consideration, not from
msg.valueobserved at a nested frame. Once one branch allocatesxwei to an auction commitment or option exercise, later branches can only refund what remains after that allocation, never replay the full outer-call value as if it were still unspent. - Native and token payment paths converge on the same accounting model. The protocol should be able to swap
ETH pathforWETH pathwithout changing entitlement logic; if native support requires special semantics about ambient context, the design is already carrying extra hidden trust assumptions. - Payable functions have explicit transaction-level composition rules. If an action is only safe once per transaction, the contract encodes that as a one-shot rule; if it is safe many times, the contract encodes how payment budget is partitioned across those repetitions.
Frontend won't do thatis not part of the model. - The system’s conservation invariant is machine-checkable: cumulative mints, bids, exercises, allocations, and refunds from one transaction cannot imply more native-asset consideration than actually crossed the transaction boundary. Good design makes that statement easy to test and hard to violate accidentally through future batching upgrades.
Failure-prevention checklist
- Treat
transaction carried native value,this action consumed x, andremaining budget is yas three separate accounting states. Do not let one rawmsg.valueread silently answer all three questions. - Treat native-asset payment as a consumable resource, not ambient context. If one transaction can perform many logical actions, keep an internal
remainingValueor per-action receipt ledger and decrement it exactly once per action. - Decide whether a payable function is semantically single-use per transaction. If yes, enforce it with a one-shot guard instead of assuming callers will not reach it again through batching or internal recursion.
- Review every batching surface together with every payable accounting surface:
multicall,batch, routers, module executors,delegatecall(address(this), ...), and internal loops that fan out over user-supplied actions. - For refund logic, compute refund eligibility from an internally consumed amount, not from raw
msg.valueobserved at each nested frame. Repeated partial-fill branches are often the real payout path. - Prefer explicit per-subcall amounts passed through calldata and checked against a debited budget over
if (msg.value >= price)style gates inside nested logic. - Unify native and ERC-20 accounting semantics where possible. If the ERC-20 path proves payment with
transferFromper action but the native path only checks ambient call context, the native path should be presumed higher risk until proven otherwise. - Keep balance-growth checks and payment-consumption checks aligned. A function should not both trust ambient
msg.valueand separately issue claims based on later state that never proves fresh native value entered. - Fuzz compositions, not only entrypoints: one outer payment with repeated inner payable calls, mixed success and revert branches inside batching, partial-fill refund paths near caps, and nested router/module execution.
- Expose operational kill switches for batch helpers that touch value-moving paths. Many protocols can pause the sale or vault but forget the helper that makes payment-context reuse reachable.
- Write an invariant in tests and reviews that the total claims, allocations, exercises, or refunds produced inside one transaction cannot imply more native-asset consideration than the transaction actually delivered.
Audit prompts
- Which payment-context question is each branch answering here:
the transaction carried value,this exact action consumed value, orthis later branch still has value left to refund or spend? Where are those states stored separately? - Which exact logical action is
msg.valuesupposed to pay for here, and what stops the same outer-call value from being observed by a second action in the same transaction? - Can this payable function be reached through any composition surface the original author may not have mentally modeled:
delegatecallbatchers, proxy module executors, router callbacks, loops over calldata, or inherited multicall utilities? - If several inner calls succeed and some fail, how is the remaining native-asset budget tracked across those branches? Does any revert-handling or partial-fill path effectively reset the budget?
- Does refund math read raw
msg.value, or does it readvalue allocated to this subcallfrom an internal ledger? If the answer is the former, can the refund leg be repeated? - Are there any helper contracts or mixins that preserve call context (
msg.sender,msg.value) while pretending to provide isolated subcalls? - Is the protocol implicitly trusting social rules like
users will only call this onceorfrontends do not expose multicallinstead of enforcing single-use semantics onchain? - If the native-asset path were rewritten to WETH-style explicit transfers, would any business logic change? If yes, that difference often highlights where ambient context is standing in for real payment proof.
- What invariant proves that cumulative allocations, collateral exercises, or refunds from one transaction are bounded by the transaction’s actual native-asset inflow?
- Can an attacker place the system near a cap or partial-fill boundary first, so the economically meaningful exploit becomes repeated refunds rather than repeated purchases?
- Are pause controls, monitoring, and incident playbooks aimed at the payable function itself, or at the composition route that actually makes repeated observation possible?
Lessons that should already be standard
-
Transaction carried value,this action consumed value, andlater branches still have budget leftare different claims. Healthy native-asset accounting keeps them disjoint instead of letting one ambientmsg.valueobservation answer all three. -
msg.valueproves transaction-level payment happened, not action-level payment provenance. Treating it as reusable evidence for each nested step is a category error. -
Native-asset support is not the same as ERC-20 support with nicer UX. Ambient call context has different compositional hazards and needs its own accounting discipline.
-
Batch helpers are part of solvency logic when they can reach payable code. A harmless-looking
delegatecallconvenience layer can become the exploit primitive that monetizes an otherwise local accounting assumption. -
Refund branches deserve the same paranoia as mint branches. Many
msg.valuereplay bugs become catastrophic only when repeated partial refunds turn semantic confusion into direct asset extraction. -
Works in isolationis not a security property for payable functions. If the protocol ships composition surfaces, the threat model must be defined at the composed-system level. -
If one real payment can satisfy more than one logical entitlement story, the design does not yet have payment provenance — it has ambient context dressed up as accounting.
-
Explicit internal journals beat ambient EVM context. The protocol should always be able to answer
how much native value remains unconsumed for this transaction path?without rereadingmsg.value. -
Frontend assumptions are not controls. If calling the same payable action twice in one transaction would break accounting, the contract has to make that impossible.
-
The safest native-asset pattern is often to immediately wrap into WETH and continue as explicit token accounting. Every place that resists that translation deserves extra review.
-
Where this family stops:
- Stay here when the same real native-asset payment is being counted multiple times across nested actions, subcalls, or loop iterations inside one outer transaction.
- Pivot to deposit-signal locked-value mismatch when the more important bug is that a bridge or relayer accepted one deposit-looking artifact as proof that custody happened at all. THORChain’s July 2021 router exploits are the sharp cousin here: they also involved misleading native-value context, but the durable bridge lesson is that source-side deposit truth was forged for downstream relayers rather than that one batch helper replayed payment across multiple logical exercises.
- Pivot to balance-delta-provenance-failure when the protocol is not reusing ambient
msg.value, but instead is misattributing a tokenbalanceAfter - balanceBeforedelta to the wrong deposit, reward, or callback source. - Pivot to reentrancy when repeated execution matters mainly because stale mutable state stayed live across an external callback, even if no native-asset payment context was being reused.
-
High-signal incidents:
- [[../../../incidents/accounting-issuance-claims/opyn-oeth-put-exercise-msg-value-reuse-double-exercise-collateral-drain-2020-08-04|Opyn oETH Put
exercise()msg.valuereuse / double-exercise collateral drain (2020-08-04)]] — the direct loop variant: one outer ETH payment satisfied multiple_exercise()checks across a vault list, so the samemsg.valueunlocked more than one USDC collateral payout. - [[../../../incidents/accounting-issuance-claims/sushiswap-miso-dutchauction-batchable-msg-value-refund-drain-near-miss-2021-08-17|SushiSwap MISO DutchAuction batchable
msg.valuerefund-drain near miss (2021-08-17)]] — thedelegatecallbatch variant:BoringBatchable.batch()preserved ambientmsg.valueacross repeatedcommitEth()subcalls, and DutchAuction’s partial-fill refund path turned one outer ETH payment into potentially many refunds once the auction approached its cap.
- [[../../../incidents/accounting-issuance-claims/opyn-oeth-put-exercise-msg-value-reuse-double-exercise-collateral-drain-2020-08-04|Opyn oETH Put
Internal linkages
- Positive-design companion: canonical-payer-identity-and-receipt-binding-discipline — useful when the sharper design question is what exact payer rail, one-shot receipt object, and action-local consumption journal should have replaced raw ambient
msg.valueas payment truth. - Adjacent pattern: balance-delta-provenance-failure — both families confuse proof that some value appeared with proof that this exact logical action delivered the value, but that family misattributes live token-balance deltas while this one misattributes ambient native-payment context.
- Adjacent pattern: deposit-signal locked-value mismatch — both preserve source-of-value mistakes, but deposit-signal failures usually involve one code path falsely emitting or recording a deposit, whereas this family double-counts one real payment across multiple logical actions in the same transaction.
- Adjacent pattern: reentrancy — the exploit often feels reentrancy-adjacent because control re-enters accounting through a helper, but the preserved invariant failure is
payment context was reused, not necessarilystate was mutated before an external call. - Best direct-loop exemplar: opyn-oeth-put-exercise-msg-value-reuse-double-exercise-collateral-drain-2020-08-04.
- Best
delegatecallbatch exemplar: sushiswap-miso-dutchauction-batchable-msg-value-refund-drain-near-miss-2021-08-17.
Control surface
-
The leverage sits in who is allowed to batch payable actions, whether the protocol proves fresh payment per logical action, and whether refund math can run independently for each reuse of the same outer-call context.
-
The recurring mistake is a three-step collapse:
this transaction sent ETH,this exact nested purchase or bid consumed some of that ETH, andthis later refund or allocation branch still has budget to act onare getting treated as one fact. -
The exploit becomes real where ambient transaction-level payment is promoted into action-level receipt truth and then promoted again into residual refund or entitlement authority.
-
Sources:
- https://medium.com/opyn/opyn-eth-put-exploit-post-mortem-1a009e3347a8
- https://peckshield.medium.com/opyn-hacks-root-cause-analysis-c65f3fe249db
- https://samczsun.com/two-rights-might-make-a-wrong/
- https://mudit.blog/miso-war-room/
- https://boringcrypto.medium.com/smart-contract-issue-in-pixel-inc-9ebfe1c8bc14
- https://github.com/opynfinance/Convexity-Protocol/blob/master/contracts/OptionsContract.sol#L805-L809
- 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
-
Last reviewed: 2026-07-31 UTC