Status DataClose notification

Overlayer Disclosed Report

Integer Underflow in OverlayerWrapCore._credit() Permanently Locks Bridged User Funds on Non-Hub Chains

Company
Created date
Mar 28 2026

Target

hidden

Vulnerability Details

Summary

OverlayerWrapCore._credit() (line 543) performs totalBridgedOut -= amountReceivedLD, which reverts on underflow in Solidity 0.8. On any non-hub deployment where totalBridgedOut starts at zero, every incoming cross-chain credit reverts. Tokens burned on the hub via _debit() can never be minted on the spoke, permanently locking bridged user funds under the current contract logic.

Affected Asset

  • Contract: OverlayerWrapCore.sol (line 543)
  • Commit: 519c9e92fd9d80d11e35e9868130f6334b88d676
  • Function: _credit()

Severity: CRITICAL

High Likelihood:

  • Triggers on the first cross-chain transfer to any non-hub deployment
  • No privileged roles required — any user calling OFT send() triggers it
  • A single valid dustless OFT transfer of trivial size is sufficient
  • Deterministic: no special conditions beyond normal cross-chain usage

High Impact:

  • Permanent lock of bridged user funds, irrecoverable under current contract logic
  • Tokens burned on source, never minted on destination
  • Affects 100% of any cross-chain transfer to a non-hub deployment (exceeds >1% user deposit threshold)
  • No admin setter for totalBridgedOut; contract is not upgradeable

Relationship to Prior Audit Findings

totalBridgedOut was introduced in commit f55efce / PR #70 to fix F-2026-15250 (High): OFT burns reduced hub totalSupply(), causing underflow in AaveHandler.supply(). The fix added totalBridgedOut tracking so totalSupply() + totalBridgedOut reflects effective supply.

This report identifies a regression in that fix: _credit() unconditionally decrements totalBridgedOut without checking if the subtraction is valid on the current chain. On a spoke where totalBridgedOut was never incremented, the subtraction always underflows.

I did not identify a public GitHub issue referencing this spoke-chain underflow.

Prior (F-2026-15250) This finding
Function AaveHandler.supply() OverlayerWrapCore._credit()
Chain Hub only Non-hub only
Impact DoS on supply() (High) Permanent lock of bridged funds (Critical)
Root cause totalSupply drops below accounting expectation _credit() unconditionally decrements totalBridgedOut without chain-awareness

Root Cause

// OverlayerWrapCore.sol line 537-544
function _credit(address to_, uint256 amountLD_, uint32 srcEid_)
    internal virtual override returns (uint256 amountReceivedLD) {
    amountReceivedLD = super._credit(to_, amountLD_, srcEid_);
    totalBridgedOut -= amountReceivedLD;  // UNDERFLOWS when totalBridgedOut == 0
}

totalBridgedOut starts at 0 on every deployment and is only incremented by _debit(). On a spoke:

  • _debit() requires tokens to burn — but spoke has none (minting blocked by onlyHubChain)
  • _credit() is the only token entry path — but it underflows because totalBridgedOut == 0
  • Permanent chicken-and-egg deadlock.

Why Recovery Is Impossible

  1. No admin setter for totalBridgedOut: Written only by _debit (+) and _credit (-).
  2. Not upgradeable: No proxy pattern or admin repair path.
  3. LZ retry does not help: Revert cause is deterministic — every retry fails identically. Administrative message-discard mechanisms do not restore burned source-chain tokens.
  4. onlyHubChain: Blocks mint/redeem on spoke — cross-chain credit is the sole entry path.

Applicability

The Hacken audit (12/03/2026) describes the protocol on Ethereum and Base with LayerZero OFT. The onlyHubChain modifier establishes a designated hub with non-hub deployments. The vulnerability manifests on any deployment where hubChainId != block.chainid — the current hub/spoke model, not a hypothetical configuration.

Attack Scenario

  1. Protocol deploys OverlayerWrap on Chain A (hub) and Chain B (non-hub).
  2. Alice mints 100 OW on hub.
  3. Alice bridges 50 OW to spoke via OFT send().
  4. Hub: _debit() burns 50 tokens.
  5. Spoke: _credit()totalBridgedOut -= 50UNDERFLOW REVERT.
  6. Alice's 50 OW burned on hub, never minted on spoke. Permanently locked.

Proof of Concept

The PoC exercises the same _debit()/_credit() hooks that OFT send()/lzReceive() uses, following the same methodology as the prior Hacken audit's PoC for F-2026-15250.

File: test/CreditUnderflow.ts | Run: npx hardhat test test/CreditUnderflow.ts --network hardhat

Results (5/5 pass — hub + spoke simulation):

Test 1: Hub baseline — debit/credit round-trip works correctly. Test 2: Spoke _credit() reverts with panic(0x11) — arithmetic underflow. Test 3: Spoke mint() reverts with OverlayerWrapCoreNotHubChainId. Test 4: Spoke deadlock — credit, mint, and debit all revert. No escape. Test 5: Full chain — Alice mints 100, bridges 50, spoke reverts. Alice has 50 accessible, 50 permanently locked (50% loss).

Suggested Fix

Only track totalBridgedOut on the hub chain:

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

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

Scope Verification

  • In-scope: OverlayerWrapCore.sol at commit 519c9e92fd9d80d11e35e9868130f6334b88d676.
  • Regression: Separate from F-2026-15250 (hub AaveHandler.supply() DoS). This targets spoke _credit().
  • No public issue found: I did not identify a public GitHub issue referencing _credit() underflow. PR #70 and PR #46/#44 address related but distinct issues.
  • Not frontrunning: Deterministic.
  • PoC: 5/5 Hardhat tests pass.
  • Threshold: 100% of bridged amount locked. Bug exists in the audited code as written, not a hypothetical configuration.

Validation steps

Environment Setup

git clone https://github.com/Overlayerfi/contracts.git
cd contracts
git checkout 519c9e92fd9d80d11e35e9868130f6334b88d676
npm install --force

The PoC uses EndpointV2Mock from @layerzerolabs/oapp-evm and does not interact with any mainnet contracts. However, Ethereum mainnet forking must be enabled in hardhat.config.ts because the repo's Solidity compilation requires the Paris EVM target (set automatically when forking is active). Ensure forking is enabled with any valid Ethereum RPC:

hardhat: {
  forking: {
    url: "https://eth-mainnet.public.blastapi.io",  // or your own Alchemy/Infura URL
    enabled: true,
    blockNumber: 22917626,
  },
  allowUnlimitedContractSize: true,
  ...
}

Place the attached CreditUnderflow.ts file in test/.

Run PoC

npx hardhat test test/CreditUnderflow.ts --network hardhat

Expected Output (5/5 tests pass)

Test 1: Hub baseline — _credit works after _debit

Proves the hub chain path works correctly: debit increments totalBridgedOut, credit decrements it. Establishes that the bug is spoke-specific.

=== TEST 1: HUB BASELINE ===
Hub debit 20 -> totalBridgedOut=20 -> credit 20 -> totalBridgedOut=0
Hub chain works correctly (totalBridgedOut was incremented by prior debit).

Test 2: Spoke _credit ALWAYS reverts (core vulnerability)

Deploys a separate spoke instance with hubChainId != block.chainid. Proves _credit() reverts with arithmetic underflow (panic 0x11) because totalBridgedOut is 0.

=== TEST 2: SPOKE _credit REVERTS ===
Spoke hubChainId: 8453 (different from block.chainid: 31337)
totalBridgedOut = 0, credit amount = 20 OW
Result: REVERTED with panic(0x11) — arithmetic underflow
Spoke chain can NEVER receive tokens via _credit.

Test 3: Spoke cannot mint — onlyHubChain blocks it

Proves minting is impossible on spoke chains, confirming _credit is the ONLY token entry path.

=== TEST 3: SPOKE CANNOT MINT ===
mint() reverts with OverlayerWrapCoreNotHubChainId on spoke.
The ONLY way to get tokens on spoke is via _credit — which always reverts.

Test 4: Spoke permanent deadlock — all paths explicitly blocked

Proves the chicken-and-egg deadlock with hard assertions: _credit reverts (underflow), _debit reverts (no tokens to burn), mint reverts (onlyHubChain). No operation can break the cycle.

=== TEST 4: SPOKE PERMANENT DEADLOCK ===
Cannot _credit: underflow revert
Cannot mint: onlyHubChain blocks it
Cannot _debit: no tokens exist to burn
DEADLOCK: No admin setter for totalBridgedOut. Not upgradeable.

Test 5: Full attack chain — hub burn + spoke revert = permanent lock of bridged funds

End-to-end: Alice mints 100 OW on hub, bridges 50 to spoke. Hub burns 50 via _debit. Spoke _credit reverts. Alice permanently loses 50% of her tokens.

=== TEST 5: FULL ATTACK CHAIN ===
Step 1: Alice mints 100.0 OW on hub
Step 2: Alice bridges 50.0 OW from hub (burned via _debit)
Step 3: Spoke _credit REVERTS (totalBridgedOut=0, underflow)
Step 4: Alice's accessible funds:
  Hub balance:   50.0 OW
  Spoke balance: 0.0 OW
  Total:         50.0 OW
  Started with:  100.0 OW
  LOST:          50.0 OW (50%)

Files to Attach

  1. CreditUnderflow.ts — Complete runnable PoC (5 tests, hub + spoke instances). Place in test/ and run with the command above.

Test Architecture

The PoC deploys two OverlayerWrapMock instances on the same Hardhat network:

  • Hub: hubChainId = 31337 (matches block.chainid) — minting allowed
  • Spoke: hubChainId = 8453 (does NOT match block.chainid) — minting blocked by onlyHubChain

Both use EndpointV2Mock from @layerzerolabs/oapp-evm. The testDebit()/testCredit() functions on OverlayerWrapMock expose the internal _debit()/_credit() hooks. In the real LayerZero OFT flow, OFTCore.send() calls _debit() on the source chain to burn tokens and send a cross-chain message; the destination endpoint then calls OFTCore._lzReceive() which calls _credit() to mint tokens. The PoC exercises these exact hooks directly, matching the methodology used in the prior Hacken audit's PoC for F-2026-15250.

Attachments

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