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
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:
The source instance burned the user's tokens
A cross-chain message was sent
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
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;
Source: OverlayerWrapCore.sol
Each deployment had independent storage.
Consider a hub and a fresh remote:
Hub instance
totalBridgedOut = 0
Remote instance
totalBridgedOut = 0
After a user bridged 100 tokens from the hub:
Hub instance
totalBridgedOut = 100
Remote instance
totalBridgedOut = 0
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;
}
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
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;
}
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
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
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
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
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
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
Every retry reached:
0 - amountReceivedLD
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
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
The test result was:
1 passing
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_
);
and:
return _credit(
to_,
amountLD_,
srcEid_
);
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
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
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:
Deploy one hub instance
Deploy one fresh non-hub instance
Debit tokens on the hub
Deliver the first inbound credit to the remote
Confirm that the credit succeeds
Confirm that no arithmetic underflow occurs
Verify source and destination balances
Test a failed delivery and successful retry
Test a return transfer from the remote to the hub
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
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
The engineering invariant is simple:
Cross-chain accounting must never treat independent local counters as one shared global ledger.
Top comments (0)