Status DataClose notification

Overlayer Disclosed Report

Cross-chain receive reverts and permanently locks bridged user funds

Company
Created date
Apr 09 2026

Target

hidden

Vulnerability Details

OverlayerWrapCore tracks bridged supply in a local storage variable, totalBridgedOut, and mutates it in both OFT hooks:

  • _debit() increases totalBridgedOut
  • _credit() decreases totalBridgedOut

This accounting is incorrect for a multi-chain OFT deployment because _debit() executes on the source-chain deployment while _credit() executes on a different destination-chain deployment with its own independent storage.

As a result, a normal user bridge from the hub chain to a fresh or net-inflow satellite chain can revert on the destination side:

  1. A user mints or already holds OverlayerWrap on the hub chain.
  2. The user bridges tokens from the hub to a satellite chain.
  3. On the hub deployment, _debit() burns/sends the user’s tokens and increments the hub’s local totalBridgedOut.
  4. On the satellite deployment, _credit() is called to mint the bridged tokens.
  5. The satellite deployment starts with local totalBridgedOut == 0, because the source-side debit happened on a different contract instance.
  6. _credit() executes totalBridgedOut -= amountReceivedLD.
  7. Solidity 0.8 checked arithmetic reverts.
  8. The destination delivery fails even though the source-side debit already happened.

The user has already lost custody on the source chain, while the destination chain cannot deliver the bridged tokens until protocol intervention changes code or state.

Likelihood and impact:

  • no privileged role is required
  • it is triggered by a standard user bridge flow
  • it requires no large capital or unusual conditions
  • it can permanently lock end-user funds in a failed cross-chain transfer

Validation steps

Affected code

contracts/overlayer/OverlayerWrapCore.sol

Relevant logic:

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;
}

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

The issue is validated with a dedicated Hardhat unit test that models the realistic architecture:

  • one OverlayerWrapMock deployed as the hub instance
  • one separate OverlayerWrapMock deployed as the satellite instance
  • a real source-side testDebit(...) on the hub
  • a destination-side testCredit(...) on the fresh satellite

Run the attached PoC:

npx hardhat test test/OverlayerWrap.ts --grep "fresh satellite"

The test passes by proving all of the following:

  • the hub-side debit succeeds
  • the user’s hub balance is reduced
  • the hub’s totalBridgedOut increases
  • the fresh satellite deployment still has totalBridgedOut == 0
  • the inbound testCredit(...) on the satellite reverts
  • the satellite user balance remains 0
  • the source-side debit remains applied after the destination failure

Attachments

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