OverlayerWrapCore._credit() unconditionally decrements totalBridgedOut on every chain where the contract is deployed. On non-hub chains, totalBridgedOut is initialized to 0 and can never be incremented first because _debit() requires holding tokens, and tokens can only arrive via _credit() (direct minting is blocked by onlyHubChain). This creates an unbreakable circular dependency: _credit always reverts with arithmetic underflow, permanently bricking inbound cross-chain transfers and causing irrecoverable fund loss.
File: contracts/overlayer/OverlayerWrapCore.sol, lines 537-544
function _credit(
address to_,
uint256 amountLD_,
uint32 srcEid_
) internal virtual override returns (uint256 amountReceivedLD) {
amountReceivedLD = super._credit(to_, amountLD_, srcEid_);
totalBridgedOut -= amountReceivedLD; // @audit underflows on non-hub chains
}
The symmetric _debit override (lines 510-528) increments totalBridgedOut, but requires the caller to hold tokens to burn. On a non-hub chain:
_credit is the only way tokens can enter (minting is blocked by onlyHubChain on _managerMint)_debit requires tokens to burn, which can only come from _credit_credit always reverts because totalBridgedOut == 0 and 0 - amount underflows in Solidity 0.8This is a structural deadlock with no resolution path.
Permanent, irrecoverable loss of 100% of bridged funds. When a user bridges OverlayerWrap tokens from the hub chain to any non-hub chain:
_debit() (irreversible)_credit() reverts on the destination due to arithmetic underflowlzReceive call atomically reverts — super._credit()'s _mint is rolled back along with every other state change in the transaction. No tokens leak or persist on the destination.totalBridgedOut, mint tokens on non-hub chains, or refund the source chainOn the hub chain, totalBridgedOut is permanently inflated by the lost amount, creating phantom supply in AaveHandler.supply() accounting (normalizedSupply = (totalSupply + totalBridgedOut) / DECIMALS_DIFF_AMOUNT).
Multi-chain deployment is confirmed:
hardhat.config.ts configures eth_sepolia + arbitrum_sepolia with distinct LayerZero endpoint IDshubChainId parameter, distinguishing hub from non-hubonlyHubChain modifier restricts mint/redeem — only meaningful if non-hub deployments existscripts/utils/deployAllSepolia.ts references pre-deployed OFT contractsSee test/CreditUnderflow.ts. Deploys OverlayerWrapMock with hubChainId = 1 on Hardhat chain 31337 (non-hub). Confirms _credit reverts with panic 0x11 (underflow) and mint is blocked by onlyHubChain, proving no path exists to bootstrap totalBridgedOut.
mv run.sh.txt run.sh
chmod +x ./run.sh
ALCHEMY_KEY=<your_key> ./run.sh
Requires the same env vars as the existing test suite (ALCHEMY_KEY, wallet keys). See project README.
Guard the totalBridgedOut accounting to only execute on the hub chain, where the variable has semantic meaning:
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;
}
}
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;
}
}
This preserves the hub-chain accounting invariant (totalSupply + totalBridgedOut tracks effective supply for collateral-backing calculations) while allowing _credit to function normally on non-hub chains.