OverlayerWrapCore tracks cross-chain net outflow with totalBridgedOut. In _credit, the contract always decrements this counter:totalBridgedOut -= amountReceivedLD;
On a destination chain receiving tokens, totalBridgedOut can be 0 before the first inbound credit.
That subtraction underflows and reverts with panic 0x11, causing the inbound credit path to fail.
Because source-side bridging burns/debits tokens before destination credit, this can produce a user-funds lock/liveness failure in cross-chain transfers.
The root cause is in this file contracts/overlayer/OverlayerWrapCore.sol, the function:_credit(address to_, uint256 amountLD_, uint32 srcEid_).
Scenario of a problem:
_credit() executes and attempts to subtract amountReceivedLD from local totalBridgedOuttotalBridgedOut < amountReceivedLD, like 0 on first net inbound, underflow occursIt seems like a Critical according to your Hackenproof classification for smart contracts because it causes permanent freezing of funds.
It causes:
I think we should use bounded or saturating decrement like:
if (amountReceivedLD >= totalBridgedOut) {
totalBridgedOut = 0;
} else {
totalBridgedOut -= amountReceivedLD;
}
We could also constrain totalBridgedOut accounting updates to the chain/context where this metric is intended to represent net outflow.
The PoC deploys:
OverlayerWrapMock (test wrapper that exposes internal _credit)setDelegate() because we need to satisfy the OFT constructor expectationsThen it calls exposed _credit while totalBridgedOut == 0, reproducing the revert.
We check out this commit to respect the scope:519c9e92fd9d80d11e35e9868130f6334b88d676
cp ~/Downloads/poc_credit_underflow.js contracts/test/poc_credit_underflow.js
npm i --force
npx hardhat run --config hardhat.audit.config.ts test/poc_credit_underflow.js
Save this poc as poc_credit_underflow.js
const { ethers } = require('hardhat');
async function main() {
const [admin] = await ethers.getSigners();
const Endpoint = await ethers.getContractFactory('EndpointSetterMock');
const endpoint = await Endpoint.deploy();
const Collateral = await ethers.getContractFactory('SixDecimalsUsd');
const collateral = await Collateral.deploy(1000, 'COLLATERAL', 'COLLATERAL');
const aCollateral = await Collateral.deploy(1000, 'aCOLLATERAL', 'aCOLLATERAL');
const OverlayerWrap = await ethers.getContractFactory('OverlayerWrapMock');
const overlayerWrap = await OverlayerWrap.deploy({
admin: admin.address,
lzEndpoint: await endpoint.getAddress(),
name: 'O',
symbol: 'O+',
collateral: { addr: await collateral.getAddress(), decimals: await collateral.decimals() },
aCollateral: { addr: await aCollateral.getAddress(), decimals: await aCollateral.decimals() },
maxMintPerBlock: ethers.MaxUint256,
maxRedeemPerBlock: ethers.MaxUint256,
minValmaxRedeemPerBlock: 1n,
hubChainId: 31337n
});
const amt = ethers.parseEther('1');
console.log('totalBridgedOut(before)=', (await overlayerWrap.totalBridgedOut()).toString());
try {
const tx = await overlayerWrap.testCredit(admin.address, amt, 0);
await tx.wait();
console.log('UNEXPECTED: credit succeeded');
} catch (e) {
console.log('credit reverted as expected');
console.log(String(e.message).slice(0, 300));
}
console.log('totalBridgedOut(after)=', (await overlayerWrap.totalBridgedOut()).toString());
}
main().catch((e) => {
console.error(e);
process.exit(1);
});
totalBridgedOut(before)= 0
credit reverted as expected
VM Exception while processing transaction: reverted with panic code 0x11 (Arithmetic operation overflowed outside of an unchecked block)
totalBridgedOut(after)= 0
Thank you for reading my submission!