Full Report
In the first two posts they found two vulnerabilities that were already patched in LayerZero. This time, they go through a vulnerability in a different section of code. When calling an external smart contract, you can specify the amount of gas for the call. When doing this, you can pass in 63/64 of the gas in the call. Why? Because the current smart contract also needs some gas to finish executing! If all the gas was actually passed, there's a chance that the current one would run out of gas. Even with the 63/64 out of gas rule, it's possible to get the 1/64 to be eaten up, which is what this bug will exploit. LZ has two cross-chain token implementations: ONFT(ERC721) and OFT (ERC20). In the ONFT contract, is implements the specification correctly, including the onERC721Received() function. There is no gas limit on this external call, which means we can eat up a lot of gas. In the _blockingLzRecieve() function, it will try to store the reason for the revert. However, if we use 63/64 from the other contract (which we can from the onERC721Received() entrypoint) then we will be able to force this to revert which not enough gas. To freeze an ONFT, a user can bridge a low-value NFT then have a malicious contract there. To unfreeze it, the NFT owner can call forceResumeReceive() to remove the payload from the endpoint. Then, it can be resubmitted for 1.5M gas. When disclosed to LZ on Immunefi, it was pointed out that all NFT/ONFT payouts were limited to the low range of 1K-10K compared to the 25K-250K range for a high impact normally. The easy way to fix this is to override _safeMint() to pass the remaining gas for the transfer iteration and not 63/64th o f the gas. The small payout for this bug and the previous 2 bugs being dups really disincentivized further research. To make it worse, it seems that many of the bugs are not fixed within the core contracts, making it very difficult to test issues. On top of this, crosschain things are just difficult to test in general. The series was an interesting perspective into LayerZero bug bounty program and DoS bugs in weird spots. To me, some of the design decisions, like force removing a bad TX, are interesting for limiting the actual damage on the blockchain. LZ seems to generally have good defense in depth and design features. Besides this, it seems like LZ has some sketchy fixes and practices for bug bounty. From these readings, I personally wouldn't tackle their bug bounty program for the claimed $15M as it's A) very secure with good design and B) complicated to test with the fixes being all over the place and C) a tad sketch on payouts.
Analysis Summary
# Vulnerability: ONFT Denial of Service via Gas Exhaustion in Cross-Chain Receive
## CVE Details
- CVE ID: Not specified in the text.
- CVSS Score: Not specified in the text.
- CWE: CWE-776 (Improper Restriction of Operations within the Bounds of a Memory Buffer) or CWE-400 (Uncontrolled Resource Consumption) due to gas manipulation leading to DoS/freezing.
## Affected Systems
- Products: LayerZero ONFT (ERC721 Cross-Chain Token Implementation).
- Versions: Unspecified, but present in the deployed code prior to the reported fix.
- Configurations: Occurs when using LayerZero's NonBlockingLzApp mode where external calls are made with a 63/64 gas limit, specifically during the `onERC721Received()` callback execution path in the receiving contract.
## Vulnerability Description
The LayerZero receiving contract, when operating in NonBlockingLzApp mode, relies on the `_lzReceive()` entry point which calls `_blockingLzReceive()`. This function allocates only 63/64ths of the total allotted gas (determined by the Relayer and adapter parameters) to the local `_nonblockingLzReceive()` (or application callback, like `onERC721Received()` for ONFTs).
The flaw exists because if the external call (e.g., inside `onERC721Received()`, which lacks an explicit gas limit) consumes nearly all of this 63/64 portion of gas, insufficient gas (the remaining 1/64th of the original delivery gas limit) will be left for the LayerZero endpoint logic to execute `storeFailedMessage()`. Storing a failed payload requires a non-trivial amount of gas (estimated at $\sim 25\text{K}$). If the gas remaining after the application execution falls below this required amount, the endpoint reverts, and the payload message becomes blocked/frozen at the LayerZero Endpoint, effectively causing a Denial of Service (DoS) or freezing the bridging channel for that specific transaction/asset.
This DoS can be triggered by bridging a low-value NFT and having a malicious contract waiting on the destination chain that intentionally wastes the supplied gas during token receipt. Unfreezing the queue later via `forceResumeReceive()` requires a significant gas resubmission ($\sim 1.5\text{M}$ gas).
## Exploitation
- Status: Implied successful exploit leading to a bounty reward, but the text does not confirm exploitation in the wild before disclosure. PoC likely exists internally to the researchers.
- Complexity: Medium to High. Requires understanding of LayerZero's non-blocking flow, gas nuances (EIP-150 63/64th rule), and coordination across two chains (source bridging and destination malicious contract).
- Attack Vector: Network (Requires interaction with the cross-chain message passing system).
## Impact
- Confidentiality: No direct impact.
- Integrity: High. Can lead to the temporary freezing/blocking of the specific cross-chain token transfer channel until a high-cost manual unfreeze transaction (`forceResumeReceive`) is executed.
- Availability: High. The bridging queue for the affected chain/application pair can be halted (DoS).
## Remediation
### Patches
- The specific code changes implemented by LayerZero post-disclosure are **unknown/not verified** by the researchers.
- Recommended Action: Override `_safeMint()` to correctly pass only the remaining necessary gas for the transfer iteration instead of the full 63/64 of the gas bank during the receiving fallback.
- Broader Fix Recommendation: The `NonBlockingLzApp` soft-spot should be fixed universally to ensure $\sim 25\text{K}$ gas is always reserved for storing a failed payload exception.
### Workarounds
- Users relying on NonBlockingLzApp schemes should be aware that application callbacks can cause transaction failure high up the call stack if gas is fully consumed, leading to message blocking.
- Unfreezing a blocked message requires submitting a transaction with significantly increased gas to cover the cost of `forceResumeReceive()` and re-processing the stored payload.
## Detection
- Detection: Monitoring for LayerZero cross-chain messages that successfully execute `_lzReceive()` (from the Relayer's perspective) but subsequently fail to process or become stuck, requiring a high-gas `forceResumeReceive()`.
- Indicators of Compromise: Observing abnormal gas usage spiked during the application callback phase (`onERC721Received` execution) that leads to a transaction revert higher up the LZ endpoint logic.
## References
- Vendor Advisories: None publicly cited as verified fixes were elusive to the researchers.
- Relevant Links:
- [https://www.trust-security.xyz/post/learning-by-breaking-a-layerzero-case-study-part-three](https://www.trust-security.xyz/post/learning-by-breaking-a-layerzero-case-study-part-three) (Main article source)