LayerZero logo

LayerZero OFT Arbitrum Native Contracts Security Review Report

June 2024

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

https://github.com/LayerZero-Labs/orbit-adapter/blob/master/src/L3/NativeOFTAdapterMsgValueTransfer.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

Total number of findings
4

Weaknesses

This section contains the list of discovered weaknesses.

LZ02-7 | VARIABLE BRIDGE CAN BE SET TO IMMUTABLE

Severity:

Low

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:

Low

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.nativeFee of 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:

Informational

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:

Informational

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.

Table of contents