Status DataClose notification

Overlayer Disclosed Report

Integer underflow in OverlayerWrap cross-chain credit permanently locks bridged funds on fresh spoke deployments

Company
Created date
Mar 28 2026

Target

hidden

Vulnerability Details

OverlayerWrapCore._credit() decrements totalBridgedOut on every destination chain receipt, but totalBridgedOut is only increased by _debit() on the local source deployment. On a fresh spoke deployment, totalBridgedOut starts at 0, so the first inbound bridge from the hub executes 0 - amountReceivedLD and reverts under Solidity 0.8 checked arithmetic. The inbound LayerZero delivery fails, so the bridged amount is not credited on the spoke and remains stuck in the failed cross-chain message flow until the protocol fixes and replays it.

Reproduction steps:

  1. Deploy OverlayerWrap on a hub chain and a fresh spoke chain.
  2. Ensure the spoke deployment has never processed any local outbound bridge, so totalBridgedOut == 0.
  3. Mint OverlayerWrap on the hub for a normal user.
  4. Initiate a bridge from the hub to the spoke for any nonzero amount.
  5. On the hub, _debit() executes and increases the hub’s local totalBridgedOut.
  6. On the spoke, LayerZero delivery calls _credit().
  7. _credit() runs:
    • amountReceivedLD = super._credit(...)
    • totalBridgedOut -= amountReceivedLD
  8. Because the spoke’s local totalBridgedOut is 0, the subtraction underflows and the transaction reverts.
  9. Result: the bridge message cannot be delivered on the fresh spoke, and the user’s bridged funds are locked in the failed message path.

Relevant code path:

function _debit(...) internal virtual override returns (...) {
    (amountSentLD, amountReceivedLD) = super._debit(...);
    totalBridgedOut += amountSentLD;
}

function _credit(...) internal virtual override returns (uint256 amountReceivedLD) {
    amountReceivedLD = super._credit(to_, amountLD_, srcEid_);
    totalBridgedOut -= amountReceivedLD;
}

Root causes:

  • totalBridgedOut is local storage per deployment
  • _debit() increments it on the source chain
  • _credit() decrements it on the destination chain
  • for the first inbound transfer to a fresh spoke, the destination-side counter was never incremented first

Validation steps

  1. Deploy a fresh OverlayerWrapMock instance configured as a spoke chain, with hubChainId set to a value different from the local chain ID.
  2. Confirm the fresh spoke state by reading totalBridgedOut() and verifying it is 0.
  3. Simulate the first inbound OFT delivery by calling testCredit(receiver, amount, 0)on the fresh spoke before any local testDebit() has ever been executed on that same instance.
  4. Observe that the transaction reverts.
  5. Verify that after the revert:
    • totalBridgedOut() is still 0
    • balanceOf(receiver) is still 0 The revert happens because _credit() executes totalBridgedOut -= amountReceivedLD on the spoke, but that spoke-local variable was never incremented first.

Attachments

hidden
CommentsReport History
Comments on this report are hidden
Details
Statedisclosed
Severity
Critical
Bounty$69
Visibilitypartially
VulnerabilityDoS with (Unexpected) revert
Participants
hidden