Status DataClose notification

Overlayer Disclosed Report

Cross-Chain Transfers to Non-Hub Chains Permanently Bricked Due to `totalBridgedOut` Underflow in `_credit()`

Company
Created date
Mar 27 2026

Target

hidden

Vulnerability Details

Root Cause

OverlayerWrapCore inherits from LayerZero's OFT (v3.2.0) and overrides both _debit() and _credit() to track totalBridgedOut — a state variable used to compute the effective supply invariant on the hub chain.

The problem: both overrides execute unconditionally on every chain, including non-hub chains where totalBridgedOut is always 0.

Vulnerable Code — _credit() (OverlayerWrapCore.sol, L538–544):

function _credit(
    address to_,
    uint256 amountLD_,
    uint32 srcEid_
) internal virtual override returns (uint256 amountReceivedLD) {
    amountReceivedLD = super._credit(to_, amountLD_, srcEid_);
    totalBridgedOut -= amountReceivedLD; // ← UNDERFLOW: totalBridgedOut is 0 on non-hub chains
}

Vulnerable Code — _debit() (OverlayerWrapCore.sol, L511–530):

function _debit(
    address from_,
    uint256 amountLD_,
    uint256 minAmountLD_,
    uint32 dstEid_
)
    internal
    virtual
    override
    returns (uint256 amountSentLD, uint256 amountReceivedLD)
{
    (amountSentLD, amountReceivedLD) = super._debit(
        from_,
        amountLD_,
        minAmountLD_,
        dstEid_
    );
    totalBridgedOut += amountSentLD;
}

Why Non-Hub Chains Cannot Receive Tokens

The protocol uses OverlayerWrapFactory to deploy the same OverlayerWrap contract on ALL chains — hub and non-hub alike. The constructor takes a hubChainId_ parameter to differentiate:

  • mint() and redeem() are restricted to the hub chain via onlyHubChain modifier
  • But _debit() and _credit() (LayerZero OFT hooks) run on ALL chains unconditionally

On non-hub chains:

  1. totalBridgedOut is initialized to 0 (Solidity default)
  2. Users cannot mint() on non-hub chains (blocked by onlyHubChain)
  3. The only way to obtain tokens on a non-hub chain is to receive them via cross-chain transfer
  4. The first incoming cross-chain transfer calls _credit()totalBridgedOut(0) -= amountReceivedLDarithmetic underflow revert (Solidity ^0.8.20)

Execution Flow

Hub Chain (Ethereum)                         Non-Hub Chain (Arbitrum)
─────────────────────                        ─────────────────────────
1. User calls send() to bridge               
   100 OVL tokens to Arbitrum                
                                             
2. _debit() executes:                        
   ✓ Burns 100 tokens from user              
   ✓ totalBridgedOut += 100                  
   ✓ LZ message dispatched                  
                                             3. LZ EndpointV2 delivers message
                                             
                                             4. _lzReceive() → _credit():
                                                totalBridgedOut = 0
                                                0 - 100 → REVERT (underflow)
                                             
                                             5. Message stored in LZ retry queue
                                                Retry → same revert → permanent failure

LayerZero Behavior on Revert

When _lzReceive() reverts on the destination chain:

  • The message is stored in LayerZero EndpointV2's failed message queue
  • Calling lzReceive() again (retry) re-executes _credit() with the same parameters
  • Same underflow → same revert → permanently undeliverable
  • The tokens are already burned on the hub chain — they cannot be recovered

Verified Against LayerZero OFT v3.2.0 Source

From @layerzerolabs/oft-evm v3.2.0 (OFT.sol):

// OFT._credit() — base implementation
function _credit(address _to, uint256 _amountLD, uint32) internal virtual override returns (uint256 amountReceivedLD) {
    if (_to == address(0x0)) _to = address(0xdead);
    _mint(_to, _amountLD);
    return _amountLD;
}

The base _credit() simply mints tokens. The override in OverlayerWrapCore adds the totalBridgedOut -= amountReceivedLD line that causes the underflow.

From OFTCore._lzReceive():

uint256 amountReceivedLD = _credit(toAddress, _toLD(_message.amountSD()), _origin.srcEid);

This confirms _credit() is called directly by the LayerZero receive path.

Impact

  • Permanent loss of user funds: Tokens burned on the hub chain are unrecoverable — they are consumed by LayerZero's debit flow but never credited on the destination chain
  • Complete breakage of cross-chain functionality: Every single transfer to ANY non-hub chain fails. The OFT multi-chain deployment is rendered entirely non-functional
  • Protocol-level failure: The core value proposition of Overlayer — multi-chain stablecoin infrastructure — is broken. The protocol can only operate on a single chain
  • All users affected: Any user attempting to bridge tokens to a non-hub chain loses 100% of the bridged amount

Validation steps

Validation Steps

  1. Verify the _credit() override unconditionally decrements totalBridgedOut — Read OverlayerWrapCore.sol lines 538–544. Confirm totalBridgedOut -= amountReceivedLD executes with no if (block.chainid == hubChainId) guard:

    • https://github.com/Overlayerfi/contracts/blob/519c9e92fd9d80d11e35e9868130f6334b88d676/contracts/overlayer/OverlayerWrapCore.sol
  2. Verify totalBridgedOut starts at 0 — Search the contract for the totalBridgedOut declaration. It is a plain uint256 state variable with no constructor initialization, defaulting to 0 on all chains:

    • https://github.com/Overlayerfi/contracts/blob/519c9e92fd9d80d11e35e9868130f6334b88d676/contracts/overlayer/OverlayerWrapCore.sol
  3. Verify mint() is restricted to hub chain only — Read OverlayerWrap.sol. The mint() function calls _managerMint() which uses the onlyHubChain modifier, confirming no tokens can be minted on non-hub chains. This means _debit() can never run first on a non-hub chain to increment totalBridgedOut:

    • https://github.com/Overlayerfi/contracts/blob/519c9e92fd9d80d11e35e9868130f6334b88d676/contracts/overlayer/OverlayerWrap.sol
  4. Verify the same contract is deployed on all chains — Read OverlayerWrapFactory.sol. The factory deploys OverlayerWrap on every chain using the same bytecode, with only the hubChainId_ constructor parameter differentiating hub from non-hub:

    • https://github.com/Overlayerfi/contracts/blob/519c9e92fd9d80d11e35e9868130f6334b88d676/contracts/overlayer/OverlayerWrapFactory.sol
  5. Verify Solidity ^0.8.20 with default overflow checks — Read OverlayerWrapCore.sol line 2: pragma solidity ^0.8.20;. This version has built-in arithmetic overflow/underflow checks enabled by default (no unchecked block wrapping the subtraction in _credit()):

    • https://github.com/Overlayerfi/contracts/blob/519c9e92fd9d80d11e35e9868130f6334b88d676/contracts/overlayer/OverlayerWrapCore.sol

Full PoC with Foundry test and detailed solution: See attached file H1-POC.md


Supporting Files / PoC

See H1-POC.md for:

  • Foundry PoC demonstrating the underflow revert
  • Step-by-step attack scenario
  • Recommended fix with code

Attachments

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