DEV Community

Daniel
Daniel

Posted on

How Overlayer's Destination-Local Counter Permanently Stranded First Inbound Transfers

Cross-chain accounting has a simple rule:

A value stored on one chain does not automatically exist on another.

Overlayer violated that rule with totalBridgedOut.

The counter was introduced to preserve backing calculations after wrapped tokens were burned during an outbound OFT transfer. The source contract increased the counter during _debit, allowing the protocol to continue counting supply that had left the local chain.

The destination contract later decreased totalBridgedOut during _credit.

That would work only if both operations modified the same ledger.

They did not.

Every OverlayerWrapCore deployment had its own local counter. A source-side debit increased the source instance's counter, while a destination-side credit decreased an unrelated counter stored on the destination instance.

A fresh non-hub destination started with:

totalBridgedOut = 0
Enter fullscreen mode Exit fullscreen mode

Its first inbound transfer therefore attempted to subtract the credited amount from zero. The destination transaction reverted with Panic(0x11), which undid the destination mint.

The source burn had already completed in a separate transaction on another chain.

The user was left with no accessible balance on either side, and repeated retries reached the same failing state.

The finding was validated as Critical in the Overlayer DualDefense Audit. The finding had 87 duplicate submissions, and my reward was $68.97.

The disclosed report is available on HackenProof.

Why totalBridgedOut existed

Overlayer used an OFT-style burn-and-mint bridge flow.

When wrapped tokens left one chain:

  1. The source instance burned the user's tokens

  2. A cross-chain message was sent

  3. The destination instance later minted the corresponding amount

The source burn immediately reduced local totalSupply.

That created a backing-accounting problem. The burned amount was no longer visible in local supply, but it still represented an outstanding economic obligation while the transfer was in progress or represented remotely.

The audited design accounted for that obligation through:

effective supply =
    totalSupply
    + totalBridgedOut
Enter fullscreen mode Exit fullscreen mode

The source bridge path increased totalBridgedOut after burning tokens.

The purpose was reasonable.

The implementation failed because it treated independent per-chain storage as though it were one globally shared counter.

The counter belonged to one contract instance

The relevant state was stored directly in OverlayerWrapCore:

uint256 public hubChainId;
uint256 public totalBridgedOut;
Enter fullscreen mode Exit fullscreen mode

Source: OverlayerWrapCore.sol

Each deployment had independent storage.

Consider a hub and a fresh remote:

Hub instance
totalBridgedOut = 0

Remote instance
totalBridgedOut = 0
Enter fullscreen mode Exit fullscreen mode

After a user bridged 100 tokens from the hub:

Hub instance
totalBridgedOut = 100

Remote instance
totalBridgedOut = 0
Enter fullscreen mode Exit fullscreen mode

The economic operation crossed chains.

The storage update did not.

The remote contract had no knowledge of the source-side increment unless the protocol explicitly communicated and recorded it.

_debit increased only the source-local counter

The outbound path overrode _debit:

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;
}
Enter fullscreen mode Exit fullscreen mode

Source: OverlayerWrapCore.sol

super._debit performed the ordinary OFT source logic, including the burn.

The resulting source state was:

User balance
Reduced

Local total supply
Reduced

Local totalBridgedOut
Increased
Enter fullscreen mode Exit fullscreen mode

Nothing in that transaction changed the destination contract's storage.

_credit decreased a different counter

The destination path overrode _credit:

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

    totalBridgedOut -=
        amountReceivedLD;
}
Enter fullscreen mode Exit fullscreen mode

Source: OverlayerWrapCore.sol

The destination first ran the normal credit logic.

It then subtracted the credited amount from its own totalBridgedOut.

On a fresh non-hub remote:

Destination totalBridgedOut
0

Inbound amount
100

Attempted subtraction
0 - 100
Enter fullscreen mode Exit fullscreen mode

Solidity's checked arithmetic reverted with Panic(0x11).

Because the subtraction happened inside the same destination transaction as super._credit, the mint was reverted as well.

The recipient received no destination balance.

Hub-only paths made the fresh-remote state important

The report also identified an important state constraint.

Normal mint and redeem manager paths were restricted to the configured hub chain.

A fresh non-hub deployment therefore had no ordinary local path that would pre-seed totalBridgedOut before receiving its first inbound OFT credit.

That made this state realistic:

Remote is non-hub
YES

Remote has prior outbound accounting
NO

Remote totalBridgedOut
0

First inbound credit
Reverts
Enter fullscreen mode Exit fullscreen mode

The issue did not require a privileged attacker to manufacture an unusual storage state.

It followed from the intended deployment model.

Why the source burn remained final

The source debit and destination credit were not one atomic operation.

The sequence was:

Source transaction
Burn tokens and send message

Destination transaction
Receive message and attempt credit
Enter fullscreen mode Exit fullscreen mode

A revert on the destination could undo the destination mint.

It could not roll back a source transaction that had already finalized on another chain.

After the failed delivery:

Source user balance
0

Destination user balance
0

Source totalBridgedOut
Contains the bridged amount

Destination totalBridgedOut
0
Enter fullscreen mode Exit fullscreen mode

The value remained represented only by source-side bookkeeping.

The user had no accessible token balance on either chain.

The failure sequence

The complete path was straightforward:

1. A user holds wrapped tokens on the hub

2. A fresh non-hub remote exists

3. The user bridges tokens from the hub to that remote

4. Source _debit burns the user's tokens

5. Source totalBridgedOut increases

6. The destination receives the bridge message

7. Destination super._credit attempts the mint

8. Destination totalBridgedOut subtraction underflows

9. The destination transaction reverts

10. The destination mint is undone

11. The source burn remains final
Enter fullscreen mode Exit fullscreen mode

No administrator, governance action, keeper compromise, or unusual token behavior was required.

Why retrying did not recover the transfer

A retry helps only when the destination failure condition changes.

Here, the destination remained at:

totalBridgedOut = 0
Enter fullscreen mode Exit fullscreen mode

Every retry reached:

0 - amountReceivedLD
Enter fullscreen mode Exit fullscreen mode

and reverted again with the same arithmetic panic.

The proof also checked the ordinary user-callable recovery paths demonstrated by the protocol.

The user could not redeem on the hub because the source balance had been burned.

The user had no destination balance to transfer or redeem because the credit had never persisted.

The remote was non-hub, so the normal hub-restricted mint and redeem paths were unavailable there.

Under the protocol logic tested in the report, the affected transfer remained permanently stranded.

What the proof demonstrated

The proof compared two states.

Same-instance control

The control called debit and credit against the same instance.

The counter was increased before it was decreased:

Before debit
0

After debit
100

After credit
0
Enter fullscreen mode Exit fullscreen mode

The credit succeeded.

This control showed that the arithmetic worked when both operations touched the same ledger.

It also demonstrated why a same-instance test was insufficient for a cross-chain invariant.

Fresh remote probe

The probe used separate source and destination instances.

The source debit succeeded and increased only the source counter.

The fresh destination remained at zero and reverted during the first inbound credit.

The measured result was:

Source debit
Succeeded

Destination credit
Reverted with Panic(0x11)

Retry
Reverted again

Source user balance
0

Destination user balance
0

Accessible user balance
0

Stranded amount
100 percent of the transfer

Locked share
10,000 basis points
Enter fullscreen mode Exit fullscreen mode

The test result was:

1 passing
Enter fullscreen mode Exit fullscreen mode

Why the harness did not create the bug

The proof used OverlayerWrapMock to expose the internal _debit and _credit functions:

return _debit(
    from_,
    amountLD_,
    minAmountLD_,
    dstEid_
);
Enter fullscreen mode Exit fullscreen mode

and:

return _credit(
    to_,
    amountLD_,
    srcEid_
);
Enter fullscreen mode Exit fullscreen mode

Source: OverlayerWrapMock.sol

The harness did not modify the counter logic.

It did not bypass the subtraction, alter arithmetic behavior, or fabricate shared and isolated storage.

It only made the production internal paths callable from the test.

Why the finding was Critical

The proof demonstrated a complete loss of user access to the affected transfer amount:

Accessible balance on source
0

Accessible balance on destination
0

Retry recovery
Unavailable

Normal user recovery
Unavailable

Amount stranded
100 percent
Enter fullscreen mode Exit fullscreen mode

This was not a temporary delay or a display-only accounting discrepancy.

The source burn persisted, the destination mint could not persist, and the same delivery failed repeatedly.

The report therefore matched the program's permanent-lock impact category.

HackenProof validated the finding as Critical.

The deeper design mistake

The protocol tried to maintain a cross-chain supply invariant with an uncoordinated local variable.

The implementation effectively assumed:

source totalBridgedOut
=
destination totalBridgedOut
Enter fullscreen mode Exit fullscreen mode

No mechanism enforced that relationship.

A local counter is safe only when every increment and decrement describes events observed by the same contract instance.

That condition was not satisfied.

The increment represented an outbound burn on one chain.

The decrement represented an inbound mint on another.

They belonged to different ledgers.

Recommended remediation

The report's safest recommendation was to keep this accounting in the source or hub domain.

If totalBridgedOut exists to preserve hub backing after supply leaves the hub, then the hub should remain the authoritative ledger.

Hub-authoritative accounting

The hub counter increases when tokens leave the hub.

It decreases only when value returns to the hub through a message that proves the corresponding return.

Non-hub destinations do not decrement the hub's accounting variable locally.

Per-instance outbound accounting

Each deployment may track its own outbound burns.

An instance may decrease only an amount that was previously increased on that same instance.

An inbound credit must not assume that the destination recorded the source-side debit.

Explicit synchronized accounting

If the protocol truly needs a global value, the accounting transition must be transmitted and verified explicitly across domains.

A local storage variable cannot become a global counter merely because every deployment uses the same variable name.

Required regression tests

The fix should be tested with genuinely separate instances.

At minimum:

  1. Deploy one hub instance

  2. Deploy one fresh non-hub instance

  3. Debit tokens on the hub

  4. Deliver the first inbound credit to the remote

  5. Confirm that the credit succeeds

  6. Confirm that no arithmetic underflow occurs

  7. Verify source and destination balances

  8. Test a failed delivery and successful retry

  9. Test a return transfer from the remote to the hub

  10. Verify backing calculations before, during, and after the bridge cycle

The central invariant is:

A destination must never decrease bridge accounting that was not previously increased on that destination.

Broader audit lessons

Cross-chain state is not shared state

Identical contracts deployed across multiple chains still have independent storage.

The same variable name does not create one global ledger.

Fresh deployments deserve dedicated tests

The first inbound transfer, first outbound transfer, first retry, and first return transfer often expose assumptions hidden by accumulated state.

Same-instance tests can hide distributed failures

A debit and credit test against one contract instance may pass because both operations touch the same counter.

That does not validate a real cross-chain lifecycle.

Destination reverts do not restore source state

When a bridge burns or locks funds before asynchronous delivery, destination failure analysis must include recovery.

A destination revert is safe only when the protocol can restore or reclaim the source-side value.

Accounting scope must match storage scope

Local storage should represent local facts.

A global economic invariant requires one authoritative ledger or explicit synchronization.

Conclusion

Overlayer introduced totalBridgedOut to preserve effective supply after outbound OFT burns.

The source _debit increased the source instance's counter.

The destination _credit decreased a different counter stored on the destination instance.

On a fresh non-hub remote, that counter was zero.

The first inbound credit therefore attempted:

0 - bridged amount
Enter fullscreen mode Exit fullscreen mode

and reverted with Panic(0x11).

The destination mint was undone, but the source burn remained final because it had completed in a separate cross-chain transaction.

The proof demonstrated:

Source user balance
0

Destination user balance
0

Repeated retry
Still fails

Stranded amount
100 percent of the transfer

Ordinary user recovery
Unavailable
Enter fullscreen mode Exit fullscreen mode

The engineering invariant is simple:

Cross-chain accounting must never treat independent local counters as one shared global ledger.

Top comments (0)