Cross-Module Asset-Denomination Mismatch / Unit-Domain Confusion

  • Name: cross-module asset-denomination mismatch / unit-domain confusion
  • Category: exploit pattern / accounting-interface failure / dimensional-analysis failure / vault-strategy integration bug
  • Summary: This exploit family appears when two protocol modules pass the same numeric amount across an interface but do not mean the same asset unit by it. One side may mean underlying tokens while the other side means receipt shares, LP tokens, vault shares, or another priced claim on the underlying. If the downstream module executes the larger-valued interpretation and the upstream module settles the smaller-valued interpretation, the difference can accumulate as hidden surplus, phantom yield, or under-burned liability. The system then rebooks that mismatch as legitimate value and lets an attacker withdraw, borrow, or compound against accounting fiction.

Three seam-authority promotions reviewers should not collapse

  1. A seam-crossing amount exists at all

    • A bank passes amount, a vault calls withdraw(amount), or an adapter hands a raw integer to a downstream receipt-token module.
    • At this stage the safest truthful statement is only some quantity is crossing a module seam. That does not yet prove the caller and callee mean the same economic object.
  2. That raw amount is accepted as canonical cross-module value truth

    • The dangerous promotion is when the protocol acts as though this integer already means the same thing on both sides of the boundary: assets, shares, LP tokens, wrapper claims, or another balance-sheet object.
    • This is the semantic-identity question: the protocol believes one number still refers to one economic quantity after the seam, even though one side may mean underlying and the other side may mean receipt shares.
  3. That accepted interpretation still deserves live accounting, redemption, or collateral authority

    • Even after the system has picked one interpretation, a healthy design should still ask whether that seam-authored state deserves to update wantLockedTotal, PPS, collateral value, redeemable principal, or borrow power immediately.
    • This is the cash-out question: why did this seam interpretation get to move real money or canonical accounting right now?

Healthy reviews keep those promotions separate instead of flattening them into one vague amount looked fine story.

  • What it is:
    • A protocol has at least two layers: for example bank and strategy, vault and wrapper, market and adapter, or controller and accounting helper.
    • Both layers pass an integer amount across an interface.
    • The upstream layer thinks amount is denominated in one asset domain, usually the underlying token.
    • The downstream layer interprets the same integer as a different asset domain, such as a yield-bearing receipt token or share token with a changing exchange rate.
    • Because the receipt asset is not 1:1 with the underlying, executing the downstream action moves more or less economic value than the upstream layer intended.
    • If the protocol then only partially reconciles that difference, the leftover mismatch becomes latent accounting drift that later gets recognized as yield, principal, collateral, or redeemable value.
  • Typical preconditions:
    • The protocol uses a multi-layer design where different modules own different parts of the asset life cycle.
    • At least one layer operates on a receipt token, vault share, LP token, or wrapper whose exchange rate against the underlying is not fixed at 1.
    • The interface between layers does not encode unit semantics explicitly.
    • Internal totals are tracked incrementally rather than derived directly from live positions each time.
    • Mismatch handling clips or tolerates shortfalls instead of reverting and forcing reconciliation.
  • Common vulnerable surfaces:
    • yield-vault bank/strategy pairs
    • lending or vault adapters that pass underlying-denominated requests into receipt-share redemption paths
    • wrapper adapters that use underlying-denominated interfaces against share-denominated positions
    • accounting helpers that record principal in one unit but settle withdrawal or redemption in another
    • auto-compounding systems whose local leftover balances get swept back into principal or yield accounting
    • integrations where a module assumes a receipt token is always worth exactly one unit of underlying
  • Failure sequence:
    1. The attacker identifies a boundary where module A means underlying amount and module B means share amount or another differently valued claim.
    2. They send a large deposit or withdrawal through that boundary.
    3. Module B executes a higher- or lower-valued operation than module A intended because the same integer now refers to a different unit domain.
    4. The resulting surplus, deficit, or leftover sits inside an intermediate contract or bookkeeping variable.
    5. The protocol reclassifies that mismatch as legitimate principal, yield, or redeemable balance.
    6. The attacker loops the mismatch or cashes out once the phantom value is large enough.
  • Variants:
    • Underlying-versus-receipt mismatch — a bank or vault passes underlying units to a strategy that interprets them as receipt-share units.
    • Wrapper-rate confusion — one module assumes a wrapper token is always 1:1 while another settles at the live exchange rate.
    • Withdrawal-side over-redemption — an upstream module asks for X underlying, but the downstream module burns X shares or LP tokens and therefore realizes more than X underlying.
    • Leftover-yield inflation — surplus produced by the mismatch remains in an intermediate contract and gets swept into principal or yield accounting on the next cycle.
    • Silent-shortfall masking — instead of reverting on mismatch, the protocol clips to current balance and lets accounting drift persist.
  • Detection heuristics:
    • Annotate every cross-module amount parameter with units on paper. If reviewers cannot say whether it is underlying, share, LP, receipt, or scaled units, the interface is not review-ready.
    • Test integrations where a receipt token’s exchange rate has moved materially away from 1.
    • Recompute strategy value from live positions and compare it to incrementally maintained totals after every deposit and withdrawal.
    • Search for defensive code that silently changes _wantAmt or equivalent variables to current balance instead of reverting; those clips often hide unit mismatches.
    • Check whether local leftovers in strategy contracts are ever re-swept into accounting as if they were organic yield.
    • During review, diff function names and comments at both sides of the boundary: withdrawUnderlying, withdraw, sharesToAmount, wantLockedTotal, and similarly generic names often expose where semantics drifted while the type stayed uint256.
  • Defenses:
    • Encode unit semantics in interfaces and naming, not just comments.
    • Prefer explicit verbs such as withdrawUnderlying() versus withdrawShares() instead of one polymorphic withdraw(uint256 amount) surface.
    • Convert explicitly between underlying and receipt-token units at the module boundary.
    • Derive solvency-critical totals from live balances and live exchange rates whenever possible.
    • Revert on accounting mismatches instead of clipping them away.
    • Add integration tests where receipt-token exchange rates are well above or below 1.
    • Add round-trip invariants that deposit then withdraw through every module seam and assert no unexplained intermediate surplus remains.
    • Treat strategy-local leftovers as suspicious until reconciled, not as earned yield by default.
  • High-signal incidents:
    • leftover-yield inflation drain (2021-05-16) — the clean canonical case: BvaultsBank asked for BUSD-denominated withdrawals while the Alpaca strategy interpreted the same integer as ibBUSD, leaving surplus BUSD in the strategy and compounding that leftover into fake wantLockedTotal yield until the vault was drained.

What sound design looks like

  • Healthy designs keep three statements separate: a seam-crossing amount exists, both modules have agreed on one canonical economic meaning for it, and that agreed meaning is allowed to update live solvency or redemption state. The whole point of the interface is to make each promotion explicit rather than implicit.
  • Each module owns one explicit unit domain and says so in the interface: assets, shares, LP, scaledDebt, or similarly unambiguous naming. If a downstream component works in receipt shares while the caller works in underlying, the boundary should expose both concepts explicitly instead of pretending one generic amount can safely carry both meanings.
  • Conversions should happen once, at the seam, with the live exchange rate or valuation source made explicit. Healthy systems do not let one module silently reinterpret a raw integer in a different denomination after the fact.
  • Solvency-critical accounting should be derived from marked-to-market positions or independently recomputed reconciliation views, not only from incrementally updated local counters that can preserve old unit mistakes indefinitely.
  • Residual balances at intermediate adapters or strategy contracts should stay quarantined until reconciled. A leftover created by an interface seam is evidence of accounting disagreement, not presumptive earned yield.
  • Withdrawal and redemption paths should assert economic postconditions in the caller’s unit domain: assets requested, shares burned, and assets actually received should all line up within explicit rounding bounds.
  • Adapter upgrades, wrapper swaps, and strategy migrations should be treated as balance-sheet migrations. Any change in receipt-token semantics or exchange-rate behavior should trigger interface review, backfill reconciliation, and invariant tests before production use.

Failure-prevention checklist

  • Review three separate promotions on every solvency-critical seam: amount exists, amount still means the same economic object after the seam, and that accepted meaning is allowed to update live accounting or payout authority. Do not let one approval silently stand in for the next.
  • Ban unit-ambiguous interfaces on solvency-critical seams. Prefer withdrawAssets(uint256 assets) and redeemShares(uint256 shares) over one generic withdraw(uint256 amount) entrypoint.
  • Put unit semantics in code, not only docs: variable names, NatSpec, events, struct fields, and helper functions should all distinguish assetAmount from shareAmount.
  • Convert exactly once at the boundary and log both sides of the conversion. If module A asks for assets and module B acts on shares, the protocol should compute shares = previewWithdraw(assets) explicitly rather than reusing the same integer.
  • Recompute live position value regularly and compare it to local accounting totals. Any unexplained residual or drift should block sweeping, compounding, or reclassification into yield until reconciled.
  • Revert on shortfall, leftover, or denomination disagreement at the seam instead of clipping to current balance and letting drift persist.
  • Keep intermediate leftovers in a dedicated reconciliation bucket rather than rolling them back into principal, PPS, utilization, or reward accounting by default.
  • Test with exchange rates materially above and below 1, after yield accrual, and across partial withdrawals. Bugs that are invisible at a fresh 1:1 bootstrap state often become catastrophic after the wrapper has appreciated.
  • Add round-trip invariants across every module seam: deposit then withdraw, mint then redeem, borrow then repay, and harvest then unwind should not create unexplained residual value in adapters or strategies.
  • Treat wrapper integrations as external dependencies with their own trust boundary. If a receipt token can change semantics, decimals, or redemption behavior, integration tests and emergency controls should assume that boundary can fail.
  • During migrations or upgrades, reconcile old-unit and new-unit totals before re-enabling compounding or user withdrawals.

Audit prompts

  • Which promotion is happening at each step here: a seam-crossing amount exists, that amount still counts as one canonical economic object across the boundary, or that accepted interpretation is allowed to update live accounting or payout authority right now?
  • What exact unit does each amount parameter represent on each side of the module boundary, and where is that meaning enforced in code rather than assumed socially?
  • Can the downstream token appreciate, accrue yield, rebase, or otherwise drift away from 1:1 with the upstream module’s reference unit?
  • Where does the protocol convert between assets and shares, and does that conversion use an explicit live rate, preview function, or reserve snapshot?
  • If the caller requests X underlying, what exactly gets burned or redeemed downstream: X shares, previewWithdraw(X) shares, or whatever balance happens to be available?
  • Are there any min(balance, requestedAmount) style clips, silent shortfall masking, or fallback branches that would hide unit disagreement instead of surfacing it?
  • Can strategy-local leftovers, dust, or surplus balances be swept into principal or yield accounting without first proving where they came from?
  • Which totals are incrementally maintained, and can they drift from live wallet, vault, or wrapper balances after exchange-rate movement?
  • Does any collateral, PPS, utilization, or reward calculation trust local book values that were originally computed under a different unit interpretation?
  • If the receipt token’s exchange rate moves sharply, does a deposit-withdraw or withdraw-redeposit round trip create unexplained surplus, deficit, or debt drift?
  • During adapter changes or integrations, who explicitly owns dimensional analysis? If the answer is effectively everyone and no one, the seam is under-governed.

Lessons that should already be standard

  • An amount crossed the seam, both modules meant the same thing by it, and that meaning deserved to move money or canonical accounting are three different approvals.
  • uint256 amount is not a type system. If a protocol moves between assets and receipt shares, raw integers need explicit unit semantics or they will eventually be misread.
  • Generic verbs like deposit, withdraw, and harvest are dangerous at module seams unless the denomination is encoded in the API. Readability is a security control here, not cosmetic polish.
  • Leftover balances are not free alpha. When a seam unexpectedly produces surplus in an intermediate contract, the default assumption should be accounting error until proven otherwise.
  • Incremental bookkeeping across changing exchange-rate domains is brittle. Systems should continuously reconcile against live positions instead of trusting old counters to stay meaningful.
  • Wrapper and strategy integrations are not just routing glue; they are valuation boundaries. A quiet denomination mistake can be economically equivalent to minting unbacked claims.
  • Silent clipping is usually worse than a revert on solvency boundaries. It preserves user-facing liveness by converting a hard failure into hidden accounting drift that attackers can later monetize.
  • Dimensional analysis should be a standard review habit in smart-contract audits. Asking what unit is this number really in? catches a surprising fraction of severe integration bugs.
  • Every adapter or wrapper migration should be treated like an accounting migration. If unit semantics changed, prior assumptions about surplus, debt, and solvency may already be stale.

Where this pattern stops

  • unit-normalization failure is the sharper oracle-side cousin when the same dimensional-analysis mistake happens at a price-feed boundary rather than at an internal module seam.
  • balance-delta-provenance-failure is the sharper fit when the protocol’s real mistake is not unit disagreement, but treating balanceAfter - balanceBefore as proof that one specific deposit or reward happened.
  • donation-share-price manipulation is the sharper fit when the phantom value comes from an attacker moving real assets into the wrapper to distort exchange rate, rather than from the protocol misreading its own cross-module units.
  • shared-standard-entrypoint-invariant-drift is the sharper fit when a sibling issuance verb bypasses backing checks entirely, rather than two honest-looking modules disagreeing on what one amount means.

Internal linkages

Control surface