Status DataClose notification

Overlayer Disclosed Report

Cross-Chain Bridging DoS via totalBridgedOut Underflow on Spoke Chains

Company
Created date
Mar 27 2026

Target

hidden

Vulnerability Details

The _credit override in OverlayerWrapCore.sol causes an arithmetic underflow on spoke chains, completely blocking cross-chain token bridging via LayerZero OFT and permanently losing bridged tokens.

Root Cause

OverlayerWrapCore._credit() (line 543) unconditionally decrements totalBridgedOut:

function _credit(
    address to_, uint256 amountLD_, uint32 srcEid_
) internal virtual override returns (uint256 amountReceivedLD) {
    amountReceivedLD = super._credit(to_, amountLD_, srcEid_);
    totalBridgedOut -= amountReceivedLD;  // L543: UNDERFLOW ON SPOKE
}

The LayerZero OFT flow is:

  1. Hub chain: _debit() burns tokens + totalBridgedOut += amountSentLD
  2. Spoke chain: _credit() should mint tokens + totalBridgedOut -= amountReceivedLDREVERTS

On spoke chains, totalBridgedOut is initialized to 0 (Solidity default). The first operation is _credit (receiving tokens from hub). 0 - amountReceivedLD causes arithmetic underflow in Solidity 0.8.20 checked math, reverting the transaction.

The subtraction has no conditional guard — no if (block.chainid == hubChainId) or if (totalBridgedOut >= amountReceivedLD) check exists.

Evidence of multi-chain deployment

  • OverlayerWrapCore inherits OFT from LayerZero (line 10, 27)
  • hubChainId parameter is separate from block.chainid, indicating deployment on chains where block.chainid != hubChainId
  • onlyHubChain modifier restricts mint/redeem to hub but allows OFT operations on spoke
  • Hardhat config includes eth_sepolia and arbitrum_sepolia endpoints
  • NatDoc comments: _debit says "Tracks tokens leaving the hub chain", _credit says "Tracks tokens returning to the hub chain"

Affected contracts

  • contracts/overlayer/OverlayerWrapCore.sol_credit() lines 537-544, _debit() lines 510-528

How to reproduce

  1. OverlayerWrap is deployed on hub chain (e.g., Ethereum) and spoke chain (e.g., Arbitrum) with LayerZero OFT
  2. On spoke chain, totalBridgedOut = 0 (fresh deployment, no prior _debit)
  3. User calls send() on hub chain to bridge tokens to spoke
  4. Hub chain: _debit() burns tokens and increments totalBridgedOut — succeeds
  5. Spoke chain: LayerZero delivers message, calls _lzReceive()_credit()
  6. _credit() executes totalBridgedOut -= amountReceivedLD0 - amountarithmetic underflow → revert
  7. LayerZero message enters FAILED state — retry produces the same revert (deterministic)
  8. Tokens are burned on hub but never minted on spoke — permanent loss

Impact

  • Permanent loss of 100% of bridged tokens (burned on hub, never minted on spoke)
  • Cross-chain functionality completely broken — no tokens can ever arrive on any spoke chain
  • Affects every user who attempts to bridge, without exception
  • LayerZero message retry does not help (same deterministic revert)
  • Spoke chain is in a deadlock: can't _debit (no balance) and can't _credit (underflow)

Suggested fix

function _credit(
    address to_, uint256 amountLD_, uint32 srcEid_
) internal virtual override returns (uint256 amountReceivedLD) {
    amountReceivedLD = super._credit(to_, amountLD_, srcEid_);
    // Only decrement on hub chain, where _debit incremented
    if (block.chainid == hubChainId) {
        totalBridgedOut -= amountReceivedLD;
    }
}

Validation steps

Validation steps

  1. Clone the repository: git clone https://github.com/Overlayerfi/contracts at commit 519c9e92
  2. Open contracts/overlayer/OverlayerWrapCore.sol:
    • Line 543: confirm totalBridgedOut -= amountReceivedLD in _credit() with no conditional guard
    • Line 527: confirm totalBridgedOut += amountSentLD in _debit()
    • Confirm there is no if (block.chainid == hubChainId) or if (totalBridgedOut >= amountReceivedLD) check
  3. Confirm multi-chain intent:
    • Line 10, 27: OverlayerWrapCore inherits OFT (LayerZero)
    • hubChainId is a constructor parameter separate from block.chainid
    • onlyHubChain modifier exists — proves spoke deployments are expected
    • Hardhat config: eth_sepolia + arbitrum_sepolia endpoints configured
  4. Trace the flow manually:
    • Spoke chain fresh deployment → totalBridgedOut = 0
    • First _credit() call → 0 - amountReceivedLD → underflow in Solidity 0.8.20 → revert
    • Even 1 wei triggers the underflow
  5. Confirm LayerZero v2 behavior: if _lzReceive reverts, message enters FAILED state; retry produces the same revert (deterministic)
  6. Run the attached PoC: forge test --match-contract C02_TotalBridgedOutUnderflow -vvv — all tests pass confirming the underflow

See attached C02_TotalBridgedOutUnderflow.t.sol

Attachments

hidden
CommentsReport History
Comments on this report are hidden
Details
Statedisclosed
Severity
Critical
Bounty$69
Visibilitypartially
VulnerabilityBlockchain
Participants
hidden