Status DataClose notification

Overlayer Disclosed Report

First inbound bridge transfer to a fresh satellite locks bridged user funds due to incorrect `totalBridgedOut` accounting

Company
Created date
Mar 27 2026

Target

hidden

Vulnerability Details

Summary

  • Root cause: OverlayerWrapCore._credit() always executes totalBridgedOut -= amountReceivedLD even on a fresh non-hub deployment where totalBridgedOut starts at 0.
  • Exploit condition: a user performs the first inbound OFT credit into a fresh satellite deployment after their source-chain bridge burn succeeds.
  • Impact: the source-side burn completes, but the destination credit always reverts with arithmetic underflow, locking the bridged user funds within the current deployed logic because the satellite can never bootstrap a positive totalBridgedOut.

Weakness/VRT: Integer underflow / CWE-191 Severity: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:H (9.1)

  • Any network user can trigger it through the intended bridge path.
  • No special state is required beyond “fresh satellite deployment” and “first inbound transfer”.
  • The result is irreversible loss of asset availability for the bridged amount within the current logic, unless code is replaced or storage is manually repaired. Scope asset: https://github.com/Overlayerfi/contracts/tree/519c9e92fd9d80d11e35e9868130f6334b88d676

Affected:

  • Overlayerfi/contracts@519c9e92fd9d80d11e35e9868130f6334b88d676OverlayerWrapCore tracks OFT burns/credits with totalBridgedOut, but the destination-side decrement assumes prior outbound burns on the same deployment.
  • GitHub deeplink: https://github.com/Overlayerfi/contracts/blob/519c9e92fd9d80d11e35e9868130f6334b88d676/contracts/overlayer/OverlayerWrapCore.sol#L60-L63
  • GitHub deeplink: https://github.com/Overlayerfi/contracts/blob/519c9e92fd9d80d11e35e9868130f6334b88d676/contracts/overlayer/OverlayerWrapCore.sol#L510-L543
  • GitHub deeplink: https://github.com/Overlayerfi/contracts/blob/519c9e92fd9d80d11e35e9868130f6334b88d676/contracts/overlayer/OverlayerWrapCore.sol#L353-L359

Attack Scenario (Realistic)

  • Attacker type: any normal user bridging their own OverlayerWrap from the hub chain to a fresh satellite deployment.
  • Preconditions: the destination deployment has never performed an outbound bridge burn, so its local totalBridgedOut is still 0.
  • Exploit steps at a high level: mint on the hub chain, bridge out from the hub, then let LayerZero attempt the first destination credit.
  • Primary security outcome: the user’s source-side tokens are burned, but the destination chain never credits replacement tokens, permanently locking the bridged value.
  • Why attacker control is realistic: the only required input is a standard bridge transfer initiated by a normal user; no admin action, custom config, or dependency compromise is needed.
  • In-scope check (1 line): the bug is in contracts/overlayer/OverlayerWrapCore.sol, which is one of the explicitly scoped files for the scoped repo asset above.

Attacker-Victim Model

  • Attacker: any user who initiates a bridge transfer from the hub deployment.
  • Victim: the same user whose source-side tokens are burned and never re-issued on the destination.
  • Trust Source: the bridge accounting invariant that totalBridgedOut reflects prior burns on the same deployment.
  • Protected Resource: the bridged OverlayerWrap balance that should appear on the destination chain after the source burn.
  • Security/Business Outcome proved: permanent user-fund lock on the first inbound transfer to a fresh satellite deployment.

Steps to Reproduce

PoC walkthrough: the PoC deploys a hub-style OverlayerWrapMock and a fresh satellite-style OverlayerWrapMock, seeds the expected LayerZero endpoint address with inert code so the repo’s constructor path can run locally, mints tokens on the hub instance, and simulates a bridge burn via _debit(). It then triggers the first destination _credit() on the fresh satellite instance and shows that the credit reverts with arithmetic underflow while the hub-side burn already happened. Finally, it runs a control case on the same deployment where _debit() and _credit() succeed together.

0) Prereqs: from repos/contracts, dependencies installed (npm install --force already done in this workspace).

1) Quick verification (single command): bash pocs/ov-bridge-lock_first_inbound_bridge_credit_permanent_lock/run.sh

2) Expected output: BUGGY_SATELLITE_CREDIT_REVERT=1, BUGGY_SATELLITE_TOTAL_SUPPLY=0, and CONTROL_CREDIT_SUCCESS=1 appear in artifacts/run_poc.txt.

uploaded-image

3) Production-like reproduction notes: the PoC uses the repo’s own Hardhat build and contracts/test/OverlayerWrapMock.sol, and only stubs the LayerZero endpoint bytecode locally so the bridge accounting path can be exercised without external infrastructure.

Technical Details / Evidence

  • Code refs:
    • contracts/overlayer/OverlayerWrapCore.sol:60-63 stores totalBridgedOut as a plain counter initialized to zero on every deployment.
    • contracts/overlayer/OverlayerWrapCore.sol:510-527 increases totalBridgedOut only during _debit(), i.e. after a local source-chain burn.
    • contracts/overlayer/OverlayerWrapCore.sol:537-543 always subtracts amountReceivedLD during _credit(), which underflows on a fresh destination deployment before any local debit has occurred.
  • Why controls fail:
    • The intended control is “effective supply = local totalSupply + totalBridgedOut”, so a destination credit should only reverse prior burns tracked on that same deployment.
    • On a fresh satellite deployment, totalBridgedOut is zero and there is no guard such as min(totalBridgedOut, amountReceivedLD) or a per-chain source check before subtracting.
    • Because _debit() is the only code path that can increase totalBridgedOut, the first inbound credit to a fresh satellite always hits a Solidity 0.8 underflow and reverts.
    • The same file also restricts minting to the hub chain (_managerMint(... ) onlyHubChain(block.chainid)), so the satellite cannot self-bootstrap circulating supply and then create a local debit to repair the counter.

Clarifications

  • Why this is not intended behavior: totalBridgedOut is documented in-code as bridge accounting for OFT burns/credits, so a fresh destination rejecting its first inbound credit contradicts the intended bridge flow.
  • Why attacker path is realistic in default/likely deployment: any protocol rollout that deploys a new satellite chain before users bridge in has this exact starting state (totalBridgedOut == 0).
  • Minimum prerequisites and why they are realistic: a fresh satellite deployment plus one normal user bridge; those are normal launch conditions, not unusual admin misuse.
  • Why this meets the criticality bar: the first bridged amount is user-controlled, so the locked value is not capped to dust and can plausibly exceed the program’s TVL / user-deposit impact guidance on a real rollout.
  • Which artifact(s) prove compromised outcome: artifacts/run_poc.txt, artifacts/assertions.txt, and artifacts/control_comparison.txt.

PoC

  • Location: pocs/ov-bridge-lock_first_inbound_bridge_credit_permanent_lock/
  • How to run: bash pocs/ov-bridge-lock_first_inbound_bridge_credit_permanent_lock/run.sh

Artifacts & Attachments

pocs/
`-- ov-bridge-lock_first_inbound_bridge_credit_permanent_lock/
    |-- README.md                         # Setup, execution, expected output, and cleanup notes
    |-- run.sh                            # One-command PoC entrypoint that generates the replay artifacts
    |-- e2e/
    |   `-- bridge_lock_poc.js           # Hardhat script that reproduces the buggy bridge credit and control case
    `-- artifacts/
        |-- assertions.txt               # Pass/fail assertions mapped to the permanent-lock claim
        |-- control_comparison.txt       # Buggy first-credit behavior versus working debit-then-credit control
        |-- manifest.txt                 # File inventory with one-line purpose for every PoC file
        |-- replay.sh                    # Deterministic analyst replay helper
        `-- run_poc.txt                  # Full PoC console output

Impact (What security impact can an attacker achieve?)

  • Any user can permanently strand their own bridged value within the current deployed logic when they are the first inbound transfer into a fresh satellite deployment.
  • The loss happens on the intended production bridge path: the source-side burn succeeds before the destination-side credit reverts.
  • If a satellite launch receives user deposits before any repair, every first inbound bridge user on that route is exposed to irreversible fund lock.

Mitigation / Patch

  • Do not blindly decrement totalBridgedOut on every destination _credit().
  • Track bridged-out accounting per deployment and only subtract when there is prior local bridged-out inventory to reverse.
  • At minimum, guard _credit() against amountReceivedLD > totalBridgedOut and define a separate initialization path for fresh satellite deployments so the first inbound credit can succeed safely.

Validation steps

  1. In the prepared workspace, run: bash pocs/ov-bridge-lock_first_inbound_bridge_credit_permanent_lock/run.sh

  2. The PoC deploys:

    • a hub-style OverlayerWrapMock
    • a fresh satellite-style OverlayerWrapMock
    • inert bytecode at the expected LayerZero endpoint address so the repo’s own constructor path can run locally
  3. The script then:

    • mints tokens on the hub instance
    • simulates the source-chain bridge burn via _debit()
    • attempts the first destination _credit() on the fresh satellite instance
    • runs a control case where _debit() and _credit() happen on the same deployment
  4. Confirm the buggy path output contains:

    • BUGGY_SOURCE_BALANCE_AFTER_DEBIT=13000000000000000000
    • BUGGY_SOURCE_TOTAL_BRIDGED_OUT=7000000000000000000
    • BUGGY_SATELLITE_CREDIT_REVERT=1
    • BUGGY_SATELLITE_REVERT_LINE=... panic code 0x11 ...
    • BUGGY_SATELLITE_TOTAL_SUPPLY=0
  5. Confirm the control path output contains:

    • CONTROL_CREDIT_SUCCESS=1
    • CONTROL_TOTAL_BRIDGED_OUT=0
  6. The replay artifacts are written under:

    • pocs/ov-bridge-lock_first_inbound_bridge_credit_permanent_lock/artifacts/run_poc.txt
    • pocs/ov-bridge-lock_first_inbound_bridge_credit_permanent_lock/artifacts/assertions.txt
    • pocs/ov-bridge-lock_first_inbound_bridge_credit_permanent_lock/artifacts/control_comparison.txt
  7. Validation conclusion:

    • the source-side burn succeeds,
    • the first inbound satellite credit reverts,
    • destination supply stays at zero,
    • and the control case succeeds, which proves the first inbound bridge transfer to a fresh satellite locks bridged user funds within the current logic.

Attachments

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