OverlayerWrapCore inherits from LayerZero's OFT (v3.2.0) and overrides both _debit() and _credit() to track totalBridgedOut — a state variable used to compute the effective supply invariant on the hub chain.
The problem: both overrides execute unconditionally on every chain, including non-hub chains where totalBridgedOut is always 0.
Vulnerable Code — _credit() (OverlayerWrapCore.sol, L538–544):
function _credit(
address to_,
uint256 amountLD_,
uint32 srcEid_
) internal virtual override returns (uint256 amountReceivedLD) {
amountReceivedLD = super._credit(to_, amountLD_, srcEid_);
totalBridgedOut -= amountReceivedLD; // ← UNDERFLOW: totalBridgedOut is 0 on non-hub chains
}
Vulnerable Code — _debit() (OverlayerWrapCore.sol, L511–530):
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;
}
The protocol uses OverlayerWrapFactory to deploy the same OverlayerWrap contract on ALL chains — hub and non-hub alike. The constructor takes a hubChainId_ parameter to differentiate:
mint() and redeem() are restricted to the hub chain via onlyHubChain modifier_debit() and _credit() (LayerZero OFT hooks) run on ALL chains unconditionallyOn non-hub chains:
totalBridgedOut is initialized to 0 (Solidity default)mint() on non-hub chains (blocked by onlyHubChain)_credit() → totalBridgedOut(0) -= amountReceivedLD → arithmetic underflow revert (Solidity ^0.8.20)Hub Chain (Ethereum) Non-Hub Chain (Arbitrum)
───────────────────── ─────────────────────────
1. User calls send() to bridge
100 OVL tokens to Arbitrum
2. _debit() executes:
✓ Burns 100 tokens from user
✓ totalBridgedOut += 100
✓ LZ message dispatched
3. LZ EndpointV2 delivers message
4. _lzReceive() → _credit():
totalBridgedOut = 0
0 - 100 → REVERT (underflow)
5. Message stored in LZ retry queue
Retry → same revert → permanent failure
When _lzReceive() reverts on the destination chain:
lzReceive() again (retry) re-executes _credit() with the same parametersFrom @layerzerolabs/oft-evm v3.2.0 (OFT.sol):
// OFT._credit() — base implementation
function _credit(address _to, uint256 _amountLD, uint32) internal virtual override returns (uint256 amountReceivedLD) {
if (_to == address(0x0)) _to = address(0xdead);
_mint(_to, _amountLD);
return _amountLD;
}
The base _credit() simply mints tokens. The override in OverlayerWrapCore adds the totalBridgedOut -= amountReceivedLD line that causes the underflow.
From OFTCore._lzReceive():
uint256 amountReceivedLD = _credit(toAddress, _toLD(_message.amountSD()), _origin.srcEid);
This confirms _credit() is called directly by the LayerZero receive path.
Verify the _credit() override unconditionally decrements totalBridgedOut — Read OverlayerWrapCore.sol lines 538–544. Confirm totalBridgedOut -= amountReceivedLD executes with no if (block.chainid == hubChainId) guard:
Verify totalBridgedOut starts at 0 — Search the contract for the totalBridgedOut declaration. It is a plain uint256 state variable with no constructor initialization, defaulting to 0 on all chains:
Verify mint() is restricted to hub chain only — Read OverlayerWrap.sol. The mint() function calls _managerMint() which uses the onlyHubChain modifier, confirming no tokens can be minted on non-hub chains. This means _debit() can never run first on a non-hub chain to increment totalBridgedOut:
Verify the same contract is deployed on all chains — Read OverlayerWrapFactory.sol. The factory deploys OverlayerWrap on every chain using the same bytecode, with only the hubChainId_ constructor parameter differentiating hub from non-hub:
Verify Solidity ^0.8.20 with default overflow checks — Read OverlayerWrapCore.sol line 2: pragma solidity ^0.8.20;. This version has built-in arithmetic overflow/underflow checks enabled by default (no unchecked block wrapping the subtraction in _credit()):
Full PoC with Foundry test and detailed solution: See attached file H1-POC.md
See H1-POC.md for: