Status DataClose notification

Overlayer Disclosed Report

Permanent Loss of Bridged Funds Due to `totalBridgedOut` Underflow on Non-Hub Chains²

Company
Created date
Apr 08 2026

Target

hidden

Vulnerability Details

Summary

OverlayerWrapCore._credit() unconditionally decrements totalBridgedOut on every chain where the contract is deployed. On non-hub chains, totalBridgedOut is initialized to 0 and can never be incremented first because _debit() requires holding tokens, and tokens can only arrive via _credit() (direct minting is blocked by onlyHubChain). This creates an unbreakable circular dependency: _credit always reverts with arithmetic underflow, permanently bricking inbound cross-chain transfers and causing irrecoverable fund loss.

Root Cause

File: contracts/overlayer/OverlayerWrapCore.sol, lines 537-544

function _credit(
    address to_,
    uint256 amountLD_,
    uint32 srcEid_
) internal virtual override returns (uint256 amountReceivedLD) {
    amountReceivedLD = super._credit(to_, amountLD_, srcEid_);
    totalBridgedOut -= amountReceivedLD; // @audit underflows on non-hub chains
}

The symmetric _debit override (lines 510-528) increments totalBridgedOut, but requires the caller to hold tokens to burn. On a non-hub chain:

  1. _credit is the only way tokens can enter (minting is blocked by onlyHubChain on _managerMint)
  2. _debit requires tokens to burn, which can only come from _credit
  3. _credit always reverts because totalBridgedOut == 0 and 0 - amount underflows in Solidity 0.8

This is a structural deadlock with no resolution path.

Impact

Permanent, irrecoverable loss of 100% of bridged funds. When a user bridges OverlayerWrap tokens from the hub chain to any non-hub chain:

  1. Tokens are burned on the hub chain by _debit() (irreversible)
  2. _credit() reverts on the destination due to arithmetic underflow
  3. The entire lzReceive call atomically reverts — super._credit()'s _mint is rolled back along with every other state change in the transaction. No tokens leak or persist on the destination.
  4. LayerZero V2 stores the failed message for retry, but retries produce the identical underflow
  5. No admin function exists to set totalBridgedOut, mint tokens on non-hub chains, or refund the source chain
  6. The contract is non-upgradeable (no proxy pattern)

On the hub chain, totalBridgedOut is permanently inflated by the lost amount, creating phantom supply in AaveHandler.supply() accounting (normalizedSupply = (totalSupply + totalBridgedOut) / DECIMALS_DIFF_AMOUNT).

Multi-chain deployment is confirmed:

  • hardhat.config.ts configures eth_sepolia + arbitrum_sepolia with distinct LayerZero endpoint IDs
  • Constructor accepts hubChainId parameter, distinguishing hub from non-hub
  • onlyHubChain modifier restricts mint/redeem — only meaningful if non-hub deployments exist
  • No separate non-hub contract exists in the repository
  • scripts/utils/deployAllSepolia.ts references pre-deployed OFT contracts

Validation steps

Proof of Concept

See test/CreditUnderflow.ts. Deploys OverlayerWrapMock with hubChainId = 1 on Hardhat chain 31337 (non-hub). Confirms _credit reverts with panic 0x11 (underflow) and mint is blocked by onlyHubChain, proving no path exists to bootstrap totalBridgedOut.

mv run.sh.txt run.sh
chmod +x ./run.sh
ALCHEMY_KEY=<your_key> ./run.sh

Requires the same env vars as the existing test suite (ALCHEMY_KEY, wallet keys). See project README.

Suggested Fix

Guard the totalBridgedOut accounting to only execute on the hub chain, where the variable has semantic meaning:

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_);
    if (block.chainid == hubChainId) {
        totalBridgedOut += amountSentLD;
    }
}

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

This preserves the hub-chain accounting invariant (totalSupply + totalBridgedOut tracks effective supply for collateral-backing calculations) while allowing _credit to function normally on non-hub chains.

Attachments

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