Back to Blog 
Why
Smart Contract SecurityEVMDeFiCross-Chain Bridgesmsg.value
When Every Check Passes but a Protocol Is Underbacked
20 min
A smart contract can approve every operation in a batch and still lose track of the value that makes those operations legitimate.
The contradiction is only apparent. Each local check may ask a reasonable question: is this requested amount no greater than
msg.value? Is this individual transfer authorized? Does this call specify a value that fits its own parameters? But reserve backing is not a local property. It is a conservation property across the complete transaction.If one payment is credited twice, or one transaction-level value is forwarded twice, then two true comparisons can produce one false accounting result.
This article studies that narrow failure family. Its anchor is a pair of public findings from the YadaCoin cross-chain bridge review. It then compares five manually validated public findings, one realized exploit, and one averted incident. Two additional findings are used only as adjacent variants. The purpose is not to rank protocols or auditors, and it is not to suggest that every batch is dangerous. It is to give founders and developers a precise model for a recurring design error:
Transaction-scoped value must be allocated and consumed at transaction scope. It cannot safely be treated as independently available to every locally valid operation.
Readers who want the engagement context can start with the YadaCoin bridge case study. This article takes one accounting lesson from that work and tests it against a wider public record.
Methodology: a mechanism-led, bounded comparison
The dataset was built from mechanisms, not protocol categories or keyword resemblance. A record entered the core comparison only if its public source described one of these behaviors:
- the same transaction payment was credited by multiple operations;
- the same transaction payment was forwarded or released by multiple operations;
- explicit element values were locally accepted but not cumulatively bounded by the caller's payment; or
- protocol reserves could leave without equivalent caller input or authenticated claim destruction.
The core evidence consists of YadaCoin findings F-2026-0003 and F-2026-0004, plus public findings for Nested Finance, LiFi, MIMO, THORChain Router, and Drips. Opyn and MISO provide historical exact matches. Connext and Nibiru are separated as adjacent conservation variants because neither is a repeated-
msg.value case.This is a purposive corpus, not a representative sample of smart contracts, bridges, or audit findings. No frequency or prevalence claim follows from it.
Throughout the article, four kinds of statements are kept distinct:
- Source fact means the cited public source reports the behavior, label, or status.
- Zealynx classification means we grouped records by shared mechanism. It is not a new severity rating.
- Inference means a design conclusion derived across records.
- Limitation means the evidence does not establish production exposure, exploitation, remediation in a particular deployment, or a monetary impact.
Contest records also require care. A repository can simultaneously show a submitter's risk label, a judge downgrade, sponsor response, and selection for a final report. Those fields are preserved rather than flattened into a single cross-project score.
The invariant: total effect cannot exceed authentic backing
The core property can be written without reference to any one protocol:
1For every successful transaction:23cumulative trusted value credited or released4 <=5cumulative authentic value received or valid claims destroyed
For a native-token wrapping batch, this becomes:
1sum(native-backed representation minted)2 <=3native value allocated to wrapping4 <=5msg.value
For a forwarding batch:
1sum(native value forwarded for the caller)2 <=3msg.value allocated to those forwards
For redemption:
1reserve value released2 <=3value represented by authentic claims burned
These are transaction-level statements. A check such as
requestedAmount <= msg.value proves only that one amount is no larger than the transaction input. It does not prove that the sum of this amount and every earlier amount remains covered.This distinction is closely related to protocol solvency, but it is useful even when a contract is not normally described as a reserve system. An auction can create purchasing credit, a router can forward native currency, and a proxy can execute value-bearing calls. In each case, the contract must conserve an authenticated value source across all effects attributed to it.
Why msg.value is not a spending balance
In Solidity,
msg.value is the amount of wei sent with the current message. Solidity's documentation also states that delegatecall preserves msg.sender and msg.value from the calling context.[3]The practical consequence is simple but easy to obscure through composition: reading
msg.value does not consume it.Consider a payable function receiving 10 ETH and processing two entries:
1for (uint256 i; i < entries.length; ++i) {2 require(entries[i].amount <= msg.value, "insufficient value");3 _credit(entries[i].account, entries[i].amount);4}
If both entries request 10 ETH, both checks pass.
msg.value remains 10 ETH during both iterations. The loop can create 20 ETH of accounting credit from 10 ETH of input.The same issue can be less visible with
delegatecall:1for (uint256 i; i < calls.length; ++i) {2 (bool ok,) = address(this).delegatecall(calls[i]);3 require(ok);4}
If the delegated function treats
msg.value as payment for its own operation, every delegated execution can see the original value. The delegatecall glossary entry explains the wider execution-context behavior. For this research question, the key point is that a batch helper has not created several independent payments merely because it created several logical operations.[3]A safe design needs an explicit allocation model, for example:
1uint256 consumed;2for (uint256 i; i < entries.length; ++i) {3 consumed += entries[i].amount;4 require(consumed <= msg.value, "aggregate value exceeded");5 _credit(entries[i].account, entries[i].amount);6}
An exact equality check may be more appropriate if overpayment is forbidden. A remaining-budget variable can be clearer when operations have several native-value branches. The implementation choice varies. The invariant does not.
The YadaCoin anchor: repeated credit and unmatched release
The public YadaCoin record provides two complementary expressions of the problem.
F-2026-0004: one payment, multiple native wrap credits
F-2026-0004 is titled "Transaction-level msg.value reused across multiple native wrap permits enables minting unbacked wrapped tokens." The public record reports High severity and fixed status.[1]
The bridge checked
msg.value >= recipient.amount for a native wrap. That comparison ran for each recipient and permit, while a local transfer counter reset at the start of each permit. Because msg.value remained constant, two permits could each request the full transaction payment and each pass its local checks. The finding's example supplies 100 units once and processes two 100-unit wrap permits, producing approximately 200 units of wrapped representation from 100 units of native input.[1]The individual predicates were true:
1permit 1 amount <= msg.value2permit 2 amount <= msg.value
The needed predicate was different:
1permit 1 amount + permit 2 amount <= msg.value
This is the article's canonical repeated transaction input credit case. The effect is not merely that a number was counted twice. The extra accounting claim was a redeemable wrapped representation. According to the finding, excess wrapped tokens could be redeemed against native value supplied by other users.[1]
The public status tells us that the finding was recorded as fixed. It does not, by itself, establish which later deployment contains the correction or imply that exploitation occurred.
F-2026-0003: reserves released without matching caller value
F-2026-0003 is titled "Missing msg.value validation in transferOnly remainder path enables zero-cost drain of entire bridge native balance." The public record reports Critical severity and fixed status.[2]
Here the failure was not repeated minting. A native-token remainder branch calculated value from a declared permit amount and could transfer from the bridge's own balance without proving that the caller supplied equivalent native value. The finding describes a transfer-only operation with no recipients and zero
msg.value, where the remainder path could transfer bridge-held native tokens.[2]This is an unmatched reserve release variant. It belongs beside F-2026-0004 because both findings ask the same conservation question from opposite directions:
- F-2026-0004 could create too many claims from one authentic input.
- F-2026-0003 could release authentic reserves without matching input.
One inflates the liability side. The other reduces the asset side. Both break the relationship between authenticated transaction value and protocol effect.
Comparison matrix: the same scope error in different systems
The table below reports source labels exactly enough to retain their judging context. "Classification" is Zealynx's mechanism grouping, not a severity reassessment.
| Record | How the value mismatch appears | Source-reported context | Zealynx classification | Material difference or condition |
|---|---|---|---|---|
| YadaCoin F-2026-0004 | Multiple wrap permits each consult the same msg.value and create wrapped credit | High, fixed [1] | Exact repeated-input-credit anchor | Bridge wrapping and redeemable representation |
| Nested #226 | Multiple ETH deposit orders use the same msg.value; the finding reports accounted deposits exceeding supplied ETH | 2 (Med Risk), disagree with severity [4] | Exact repeated-input-credit match | Investment and swap orders, not bridge permits |
| LiFi #86 | A swap loop forwards unchanged outer msg.value on each iteration | 2 (Med Risk), disagree with severity, sponsor acknowledged [5] | Exact repeated-value-release match | External swap forwarding rather than minting |
| MIMO #153 | Repeated delegatecalls retain the original msg.value, enabling repeated forwarding from a funded proxy | 2 (Med Risk), disagree with severity, old-submission-method [6] | Exact delegatecall-batch variant | Also depends on execution permissions and proxy-held ETH |
| THORChain Router #44 | Batch elements repeatedly reach a native-value path using the full outer msg.value | 2 (Med Risk), downgraded by judge, selected for report, M-02 [7] | Exact repeated-reserve-release match | An attacker-controlled aggregation target is an additional condition |
| Drips #153 | Explicit call values are not summed against the outer payment | QA (Quality Assurance), downgraded by judge, Q-12 [8] | Exact missing-aggregate-budget match | No repeated read is required; this was judged QA |
| Opyn, 2020 | A loop reused one ETH payment to exercise multiple options | Realized exploit described by first-hand and independent technical sources [9][10] | Historical exact mechanism match | Options exercise rather than bridge wrapping |
| MISO, 2021 | Batched delegatecalls reused one payment across auction commitments; refund logic exposed held ETH | Responsibly disclosed and mitigated before completed theft [9][10][11] | Historical exact mechanism match | Auction commitments and refunds |
The matrix supports a bounded dataset observation: across these selected records, the repeated effect falls into three practical forms.
-
Credit the same input repeatedly. YadaCoin F-2026-0004 and Nested create multiple claims or deposits from one payment.[1][4]Opyn and MISO show the same effect through option exercises and auction commitments.[9][10]
-
Forward or release the same input repeatedly. LiFi, MIMO, and the THORChain Router finding connect later batch operations to value already held by the contract.[5][6][7]
-
Accept an unbounded explicit batch. Drips does not need each element to reread
msg.value. The same invariant fails because the sum of element values is not checked against transaction payment.[8]
This is not evidence that the records had equal impact or equal exploitability. Their public labels and preconditions differ substantially. It is evidence that the same accounting-scope error can survive several implementation styles.
Historical exact match 1: Opyn
The Opyn ETH Put exploit is the strongest realized precedent in the corpus. Trail of Bits describes the underlying result in Opyn and MISO as reuse of the same
msg.value multiple times, with Opyn using msg.value inside a loop in a payable function.[9]Samczsun's first-hand retrospective explains the payment asymmetry. Token payments performed a separate
transferFrom on each iteration, while ETH processing checked whether msg.value was sufficient. That allowed one ETH payment to be reused while exercising multiple options.[10]Zealynx classifies this as an exact mechanism match to YadaCoin F-2026-0004. The protocols and downstream assets differ, but the causal primitive is the same:
1one transaction payment2+ several locally sufficient checks3= aggregate credit greater than aggregate input
The comparison does not mean the YadaCoin finding caused an incident, remained in production, or would necessarily have followed Opyn's exploit path. It means a realized historical exploit validates the mechanism as more than a theoretical accounting curiosity.
Working auditors in your corner, all year
Zealynx Insiders: weekly live sessions, 1:1 advisory, pair-auditing, and Krait runs on your code, from the firm behind 45 audits. Founders get a two-day audit session on the $500/year plan.
No spam. Unsubscribe anytime.
Historical exact match 2: MISO
MISO demonstrates how composition can hide the same primitive.
Samczsun reports that the batching helper executed calls with
delegatecall, preserving msg.value. Multiple commitEth calls could therefore reuse one payment across auction commitments. Refund behavior connected those false commitments to ETH already held by the auction.[10]Mudit Gupta's incident-response account gives a concrete version of the mismatch: one ETH supplied to the batch could appear as one ETH to each of one hundred
commitEth calls, with the resulting accounting allowing a refund from contract-held value.[11]Trail of Bits groups MISO and Opyn by the same underlying result, while distinguishing explicit use in an Opyn loop from implicit reuse through MISO's delegatecalls.[9]
MISO is not a completed theft in this evidence record. It was responsibly disclosed and mitigated.[10][11] That distinction matters. An averted incident can establish a credible mechanism and reachable composition path without establishing realized loss.
For developers, the larger lesson is that a function can be sound under ordinary direct-call assumptions and unsafe when placed behind a batch executor. Composability changes the accounting scope. A payable subroutine that assumes "this call's
msg.value belongs to this operation" needs that assumption re-evaluated when several logical operations share one EVM call context.A practical taxonomy of transaction-level backing failures
The corpus supports five useful categories. The first three form the exact batched-value core. The fourth is the second YadaCoin anchor. The fifth extends the conservation model without claiming an exact
msg.value match.1. Repeated transaction input credit
The system creates several economic credits from one transaction input.
Examples include wrapped-token issuance and deposit accounting.[1][4]
The same category also covers option exercise and auction commitments.[9][10]
A typical code smell is a payable operation that checks the full
msg.value but has no shared consumed-value state:1require(msg.value >= amount);2credit(amount);
That fragment may be locally correct only if the operation is guaranteed to occur once per payment.
2. Repeated transaction input forwarding
Several operations forward value while each behaves as though the full transaction payment remains available.
LiFi's reported swap loop, MIMO's delegatecall batch, and the THORChain Router finding fit this category.[5][6][7] Later operations may be paid from a contract's pre-existing balance rather than from new caller value.
A pre-funded contract makes the bug observable as a reserve drain. An unfunded contract might instead revert. The absence of held ETH during a unit test therefore does not prove the accounting is sound.
3. Missing aggregate batch budget
Each element declares an explicit value, but the outer function does not enforce:
1sum(element.value) <= msg.value
Drips is the clearest example in the selected corpus.[8] This category matters because searching only for repeated reads of
msg.value misses it. The core error is not a particular syntax pattern. It is failure to bind aggregate effects to aggregate input.4. Unmatched reserve release
Protocol-held value leaves without caller payment, authenticated burn, or another valid claim-destruction event.
YadaCoin F-2026-0003 is the anchor.[2] The transfer can be mechanically valid and still be economically unauthorized because the declared operation amount is mistaken for provided value.
5. Adjacent conservation variants
Some systems violate the same broader inequality without batching or reusing
msg.value. Two public findings help define this boundary.The Connext finding reports that
_executePortalTransfer requested an underlying-token withdrawal but ignored the amount actually returned, then accounted as if the full requested amount had been received. Its repository labels are 2 (Med Risk), sponsor confirmed, and resolved.[12] Zealynx classifies it as a nominal-versus-actual backing input analogue, not an exact transaction-value-reuse match.The Nibiru FunToken finding reports that conversion back to ERC-20 released reserve tokens while burning only a post-fee amount of the bank-coin representation. Its labels are
2 (Med Risk), selected for report, sponsor confirmed, and M-02.[13] Zealynx classifies it as a reserve-release-versus-claim-destruction analogue.These adjacent records sharpen the invariant. Authentic backing is not necessarily
msg.value. It can be an amount actually received from an external pool or a claim actually destroyed during redemption. What matters is that the protocol measures the real backing event, not a requested, declared, or nominal amount.Design implications for founders and protocol teams
Treat the transaction as an accounting boundary
If several logical operations share one payable entry point, define who owns each unit of native value. Do not let subroutines infer independent ownership from a shared
msg.value.A useful design review question is:
If this operation is called twice through a loop, multicall, callback, or delegatecall batch, what prevents the second operation from claiming the first operation's payment?
Make allocation explicit
Prefer an explicit transaction budget over repeated ambient reads. Compute the required total before effects when practical, or maintain one monotonic
consumed counter that spans every relevant branch and nested operation.Be precise about equality:
required == msg.valuerejects accidental and ambiguous overpayment.required <= msg.valuerequires a deliberate refund or surplus policy.- Multiple asset types require separate budgets, not one mixed counter.
Separate caller-funded flow from reserves
Contracts often need to hold native value for refunds, escrow, redemptions, or operations in progress. That balance must not silently subsidize the current caller.
Code and tests should distinguish:
1opening reserve balance2current caller input3value allocated to operations4value returned to caller5closing reserve balance
A balance check after execution can catch errors that all precondition checks miss.
Review composition, not only functions
Opyn and MISO show two routes to the same outcome: direct repetition in a loop and inherited context through
delegatecall.[9][10] A function's safety therefore depends on every execution route that can invoke it.When adding a batch helper, router, proxy executor, or multicall feature, re-evaluate payable functions under the new call graph. The helper is not just a convenience layer. It changes the unit over which value must be conserved.
Assert postconditions
Input checks are necessary, but conservation is often easier to state as a postcondition:
1new credits - destroyed credits2 <=3authentic input - authorized output
For a wrapping transaction, assert that the relevant reserve increase covers the wrapped-supply increase, accounting for defined fees and pre-existing balances. For forwarding, assert that reserve balances did not fund caller-attributed sends. For redemption, assert that released reserves correspond to authentic claims burned.
Concrete developer checks
The following checks are deliberately implementation-oriented rather than audit-specific.
- Map every value source to its scope. Mark native value as transaction-scoped, token transfers as the actual balance delta received, and burns as the actual claim reduction.
- Calculate aggregate native requirements. Sum all recipient, remainder, fee, call, and wrap amounts that the caller is expected to fund.
- Use one transaction-level consumption counter. Do not reset it per permit, recipient, or branch if those operations share one payment.
- Avoid ambient payment assumptions in subroutines. Pass an allocated amount into internal logic instead of letting every operation interpret the full
msg.value. - Define surplus behavior. Reject excess value or refund it explicitly. Do not let accidental overpayment merge silently with reserves.
- Reject unsupported native value. Non-native paths should normally reject unexpected
msg.valueso funds cannot enter an undefined accounting state. - Test duplicate maximum entries. If one entry equal to
msg.valuesucceeds, two such entries in one batch must not both receive full credit. - Test zero payment against a funded contract. Pre-fund the contract, call value-bearing batch paths with
msg.value == 0, and verify reserves cannot subsidize them. - Test delegatecall composition. Invoke payable subroutines twice through every batching helper and confirm aggregate effects remain bounded.
- Check balance deltas, not only return values. Record caller, contract, reserve, and representation balances before and after the complete transaction.
- Fuzz batch length and ordering. Mix wrap, transfer, remainder, refund, and no-op entries. Accounting should remain correct regardless of order.
- Write a conservation property. For every successful generated transaction, assert that aggregate trusted credits or releases do not exceed authentic inputs or destroyed claims.
- Test the empty and singleton cases too. Fixes designed for multi-entry batches can introduce errors in empty arrays, single entries, or branches with no native operation.
- Document trusted pre-funding. If the design intentionally lets a batch spend protocol-held ETH, make the authority, limit, and accounting source explicit rather than relying on contract balance availability.
The most revealing test is often very small: fund the contract, provide one unit of caller value, repeat a one-unit operation twice, and inspect whether two units of effect occurred.
What this research does not establish
This comparison has several important limits.
First, it is curated around one mechanism. It cannot tell us how common transaction-level backing failures are across the industry.
Second, public contest labels are not a uniform measurement system. Nested, LiFi, and MIMO records include disagreement-with-severity labels.[4][5][6]
The THORChain Router record was downgraded by a judge and selected for the report, while Drips was judged QA.[7][8] Similar code shapes do not erase different preconditions, impacts, judging standards, or protocol contexts.
Third, inclusion means that the public source reports the mechanism. This article does not independently reproduce every proof of concept. It does not claim that disclosed findings remained in production or were exploited.
Fourth, "fixed," "resolved," "sponsor confirmed," and "sponsor acknowledged" are source-reported statuses or labels. They are not independent verification of every deployment.
Fifth, Connext and Nibiru are adjacent variants only. They test the broader conservation taxonomy, but neither supports a claim about repeated transaction-scoped
msg.value.[12][13]Finally, the article intentionally excludes impact dollar figures and several bridge incidents whose outcomes may sound similar but whose causal mechanisms differ or remain under-validated for this comparison. A shared outcome such as underbacking is not enough to establish a shared bug family.
The durable rule: consume value exactly once
The common failure in this corpus is not "loops are unsafe" or "delegatecall is unsafe." Both are too broad to be useful.
The more precise rule is:
When multiple operations share one authenticated value source, their aggregate effect must share one accounting budget.
YadaCoin F-2026-0004 shows repeated credit across bridge permits.[1]
Nested and Opyn show the same scope error in deposits and options.[4][9] MISO shows how delegatecall can preserve the context that makes reuse possible.[10]
LiFi, MIMO, and THORChain Router show repeated forwarding or reserve release.[5][6][7]
Drips shows that explicit per-call values still need an aggregate bound.[8]
YadaCoin F-2026-0003 supplies the opposite-side warning: reserves can also leave when a declared amount is mistaken for caller-funded value.[2]
The local operation is not the right unit of truth. The complete transaction is. If a contract receives value once, it must allocate that value once, consume it once, and prove that all resulting credits and releases remain covered when the transaction ends.
Sources
[1] https://zealynx.io/audits/yada-coin/cross-chain-bridge-q1-2026/findings/F-2026-0004 - Zealynx, YadaCoin F-2026-0004
[2] https://zealynx.io/audits/yada-coin/cross-chain-bridge-q1-2026/findings/F-2026-0003 - Zealynx, YadaCoin F-2026-0003
[3] https://docs.soliditylang.org/en/latest/units-and-global-variables.html - Solidity documentation, Units and Globally Available Variables
[4] https://github.com/code-423n4/2021-11-nested-findings/issues/226 - Code4rena Nested Finance finding #226
[5] https://github.com/code-423n4/2022-03-lifinance-findings/issues/86 - Code4rena LiFi finding #86
[6] https://github.com/code-423n4/2022-08-mimo-findings/issues/153 - Code4rena MIMO finding #153
[7] https://github.com/code-423n4/2024-06-thorchain-findings/issues/44 - Code4rena THORChain finding #44
[8] https://github.com/code-423n4/2023-01-drips-findings/issues/153 - Code4rena Drips finding #153
[9] https://blog.trailofbits.com/2021/12/16/detecting-miso-and-opyns-msg-value-reuse-vulnerability-with-slither - Trail of Bits, Detecting MISO and Opyn msg.value reuse
[10] https://samczsun.com/two-rights-might-make-a-wrong - Samczsun, Two Rights Might Make A Wrong
[11] https://mudit.blog/miso-war-room - Mudit Gupta, A peek inside the MISO war room
[12] https://github.com/code-423n4/2022-06-connext-findings/issues/181 - Code4rena Connext finding #181
[13] https://github.com/code-423n4/2024-11-nibiru-findings/issues/48 - Code4rena Nibiru finding #48
Working auditors in your corner, all year
Zealynx Insiders: weekly live sessions, 1:1 advisory, pair-auditing, and Krait runs on your code, from the firm behind 45 audits. Founders get a two-day audit session on the $500/year plan.
No spam. Unsubscribe anytime.
