Overview
This audit covered LayerZero Network’s Omnichain Fungible Token (OFT) Arbitrum native contracts, which integrate the OFT directly into the existing Arbitrum native bridge. This integration allows users to move between L3s using either the Arbitrum native bridge or the native OFT bridge interchangeably, without facing any race conditions or issues of splitting liquidity.
Our security assessment was a full review of the two smart contracts, spanning a total of 2 days.
During our audit we have identified several minor severity vulnerabilities and code optimisations.
Finally, all of our reported issues were fixed by the development team and consequently validated by us.
We can confidently say that the overall security and code quality have increased after completion of our audit.
Scope
The analyzed resources are located on:
https://github.com/LayerZero-Labs/orbit-adapter/blob/master/src/L2/OrbitERC20OFTAdapter.sol
Commit: b20f7820794e6e0e79254ed2617cdf2a0090f0d7
The issues described in this report were fixed in the following commit:
https://github.com/LayerZero-Labs/orbit-adapter/tree/87f8f99f7a776df8a176512e10f26c2df156b309
Summary
Weaknesses
This section contains the list of discovered weaknesses.
LZ02-7 | VARIABLE BRIDGE CAN BE SET TO IMMUTABLE
Severity:
Status:
Fixed
Path:
src/L2/OrbitERC20OFTAdapter.sol:L13
Description:
The address variable bridge in contract OrbitERC20OFTAdapter is only set once during the constructor, and there is no setter function to modify its value. Therefore, we can declare bridge as an immutable variable to save gas.
IBridge private bridge;
Remediation:
Consider changing the bridge variable to immutable.
LZ02-10 | SOME NATIVE TOKENS CAN BE LOST WHEN SENDING TOKENS FROM L3
Severity:
Status:
Fixed
Path:
src/L3/NativeOFTAdapterMsgValueTransfer.sol:L150-L153, src/L3/NativeOFTAdapterMsgValueTransfer.sol:L119-L128
Description:
In the contract NativeOFTAdapterMsgValueTransfer, the function _payNative() is overridden to adapt to the new implementation of the function send(), which allows users to mint OFT tokens using the msg.value when the msgSenderBalance < _sendParam.amountLD. The nativeFee is then modified to msg.value - mintAmount to account for the mint amount.
However, when the msgSenderBalance > _sendParam.amount, there is no verification between the msg.value and the feeWithExtraAmount.nativeFee, other than the requirement that msg.value must be greater than feeWithExtraAmount.nativeFee in the _payNative() function.
This lack of verification will lead to a scenario where:
- msg.value > feeWithExtraAmount.nativeFee AND
- msgSenderBalance > _sendParam.amount
This will result in the amount
msg.value - feeWithExtraAmount.nativeFeeof native tokens being lost in the contract.
function _payNative(uint256 _nativeFee) internal override returns (uint256 nativeFee) {
if (msg.value < _nativeFee) revert NotEnoughNative(msg.value);
return _nativeFee;
}
if (msgSenderBalance < _sendParam.amountLD) {
require(msgSenderBalance + msg.value >= _sendParam.amountLD, "NativeOFTAdapterMsgValueTransfer: Insufficient msg.value");
// user can cover difference with additional msg.value ie. wrapping
uint mintAmount = _sendParam.amountLD - msgSenderBalance;
_mint(address(msg.sender), mintAmount);
// update the messageFee to take out mintAmount
feeWithExtraAmount.nativeFee = msg.value - mintAmount;
}
Consider after line 128 adding a else block to verify whether msg.value == feeWithExtraAmount.nativeFee
if (msgSenderBalance < _sendParam.amountLD) {
require(msgSenderBalance + msg.value >= _sendParam.amountLD, "NativeOFTAdapterMsgValueTransfer: Insufficient msg.value");
// user can cover difference with additional msg.value ie. wrapping
uint mintAmount = _sendParam.amountLD - msgSenderBalance;
_mint(address(msg.sender), mintAmount);
// update the messageFee to take out mintAmount
feeWithExtraAmount.nativeFee = msg.value - mintAmount;
}
+ else {
+ require(msg.value == feeWithExtraAmount.nativeFee);
+ }
LZ02-9 | USE CUSTOM ERRORS
Severity:
Status:
Fixed
Path:
src/L3/NativeOFTAdapterMsgValueTransfer.sol
Description:
The NativeOFTAdapterMsgValueTransfer.sol contract's various errors are currently handled using native Solidity revert statements and custom errors, with some exceptions.
For example:
require(success, "NativeOFTAdapterMsgValueTransfer: failed to unwrap");
src/L3/NativeOFTAdapterMsgValueTransfer.sol:L85
require(success, "NativeOFTAdapterMsgValueTransfer: failed to unwrap");
src/L3/NativeOFTAdapterMsgValueTransfer.sol:L97
require(balanceOf(msg.sender) >= _amount, "NativeOFTAdapterMsgValueTransfer: Insufficient balance.");
src/L3/NativeOFTAdapterMsgValueTransfer.sol:L100
require(success, "NativeOFTAdapterMsgValueTransfer: failed to unwrap");
src/L3/NativeOFTAdapterMsgValueTransfer.sol:L120
require(msgSenderBalance + msg.value >= _sendParam.amountLD, "NativeOFTAdapterMsgValueTransfer: Insufficient msg.value");
Remediation:
Replace require statements with Custom Errors for a more streamlined and user-friendly experience. Furthermore, custom errors are much clearer as they allow for parameter values, making debugging much easier.
LZ02-11 | CONSIDER BURNING OFT TOKENS INSTEAD OF TRANSFERRING THEM WHEN SENDING TOKENS CROSS-CHAIN
Severity:
Status:
Fixed
Path:
src/L3/NativeOFTAdapterMsgValueTransfer.sol:L64
Description:
In the function NativeOFTAdapterMsgValueTransfer._debit(), the OFT tokens are transferred to the contract itself. Since the contract has no mechanism to manage these tokens, they are effectively rendered "dead".
_transfer(msg.sender, address(this), amountSentLD);
Remediation:
It is recommended to burn the OFT tokens from the sender instead of transferring them to the OFT contract.