Skip to main content
Version: Endpoint V2

OFT Alt

When deploying OApps, developers might encounter scenarios where the native gas token cannot be used to pay the LayerZero Endpoint to send a message.

These Endpoints have been deployed using the EndpointV2Alt.sol contract, so that they can use an alternative ERC20 token on the same chain to pay for cross-chain messages. Because these Endpoints do not use the native gas token, some changes must be made to your OApp contracts (including OFT).

For example, the OFTAlt.sol demonstrates this implementation fully, which you can reference when modifying your other OApp-based contracts:

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.22;

import { OFTAlt } from "../OFTAlt.sol";

contract MyOFT is OFTAlt {
constructor(
string memory _name,
string memory _symbol,
address _lzEndpoint,
address _delegate
) OFTAlt(_name, _symbol, _lzEndpoint, _delegate) {
// constructor logic ...
}
}

Contract Changes

At a high level, only a few changes to your OApp are needed to interact with the EndpointV2Alt.sol contract:

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.22;

import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol";

import { SafeERC20, IERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";

import { MessagingParams } from "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/ILayerZeroEndpointV2.sol";

import { MessagingFee, MessagingReceipt } from "../interfaces/IOFT.sol";
import { OFT } from "../OFT.sol";

contract OFTAlt is OFT {
using SafeERC20 for IERC20;

error LzAltTokenUnavailable();

constructor(
string memory _name,
string memory _symbol,
address _lzEndpoint,
address _delegate
) OFT(_name, _symbol, _lzEndpoint, _delegate) Ownable(_delegate) {}

/**
* @dev Internal function to interact with the LayerZero EndpointV2.send() for sending a message.
* @param _dstEid The destination endpoint ID.
* @param _message The message payload.
* @param _options Additional options for the message.
* @param _fee The calculated LayerZero fee for the message.
* - nativeFee: The native fee.
* - lzTokenFee: The lzToken fee.
* @param _refundAddress The address to receive any excess fee values sent to the endpoint.
* @return receipt The receipt for the sent message.
* - guid: The unique identifier for the sent message.
* - nonce: The nonce of the sent message.
* - fee: The LayerZero fee incurred for the message.
*/
function _lzSend(
uint32 _dstEid,
bytes memory _message,
bytes memory _options,
MessagingFee memory _fee,
address _refundAddress
) internal virtual override returns (MessagingReceipt memory receipt) {
// @dev Push corresponding fees to the endpoint, any excess is sent back to the _refundAddress from the endpoint.
_payNative(_fee.nativeFee);
if (_fee.lzTokenFee > 0) _payLzToken(_fee.lzTokenFee);

return
// solhint-disable-next-line check-send-result
endpoint.send(
MessagingParams(_dstEid, _getPeerOrRevert(_dstEid), _message, _options, _fee.lzTokenFee > 0),
_refundAddress
);
}

/**
* @dev Internal function to pay the alt token fee associated with the message.
* @param _nativeFee The alt token fee to be paid.
*
* @dev If the OApp needs to initiate MULTIPLE LayerZero messages in a single transaction,
* this will need to be overridden because alt token would contain multiple lzFees.
*/
function _payNative(uint256 _nativeFee) internal virtual override returns (uint256 nativeFee) {
address nativeErc20 = endpoint.nativeToken();
if (nativeErc20 == address(0)) revert LzAltTokenUnavailable();

// Pay Alt token fee by sending tokens to the endpoint.
IERC20(nativeErc20).safeTransferFrom(msg.sender, address(endpoint), _nativeFee);
}
}

Pass OpenZeppelin Ownable

constructor(
string memory _name,
string memory _symbol,
address _lzEndpoint,
address _delegate
) OFT(_name, _symbol, _lzEndpoint, _delegate) Ownable(_delegate) {}

Make sure to pass OpenZeppelin's Ownable modifier to the constructor. The access control has already been applied, but must be explicitly passed in the constructor to compile successfully.

Using SafeERC20

using SafeERC20 for IERC20;

You should include the SafeERC20 library for safely interacting with ERC20 tokens. This is crucial for ensuring that token transfers handle potential errors like reverts or exceptions.

Error Handling

error LzAltTokenUnavailable();

A custom error LzAltTokenUnavailable which is used to handle cases where the native ERC20 token for fee payment is not set in the EndpointV2Alt contract.

Override _payNative

/**
* @dev Internal function to pay the alt token fee associated with the message.
* @param _nativeFee The alt token fee to be paid.
*
* @dev If the OApp needs to initiate MULTIPLE LayerZero messages in a single transaction,
* this will need to be overridden because alt token would contain multiple lzFees.
*/
function _payNative(uint256 _nativeFee) internal virtual override returns (uint256 nativeFee) {
address nativeErc20 = endpoint.nativeToken();
if (nativeErc20 == address(0)) revert LzAltTokenUnavailable();

// Pay Alt token fee by sending tokens to the endpoint.
IERC20(nativeErc20).safeTransferFrom(msg.sender, address(endpoint), _nativeFee);
}

You should override the _payNative function to handle paying using an ERC20 token. This function checks if the ERC20 token address is set (nativeErc20), reverts if not, and performs a safeTransferFrom to transfer the fee from the sender to the endpoint.

This ensures that the contract can handle fees in the specified ERC20 token by the EndpointV2Alt.

Override _lzSend

/**
* @dev Internal function to interact with the LayerZero EndpointV2.send() for sending a message.
* @param _dstEid The destination endpoint ID.
* @param _message The message payload.
* @param _options Additional options for the message.
* @param _fee The calculated LayerZero fee for the message.
* - nativeFee: The native fee.
* - lzTokenFee: The lzToken fee.
* @param _refundAddress The address to receive any excess fee values sent to the endpoint.
* @return receipt The receipt for the sent message.
* - guid: The unique identifier for the sent message.
* - nonce: The nonce of the sent message.
* - fee: The LayerZero fee incurred for the message.
*/
function _lzSend(
uint32 _dstEid,
bytes memory _message,
bytes memory _options,
MessagingFee memory _fee,
address _refundAddress
) internal virtual override returns (MessagingReceipt memory receipt) {
// @dev Push corresponding fees to the endpoint, any excess is sent back to the _refundAddress from the endpoint.
_payNative(_fee.nativeFee);
if (_fee.lzTokenFee > 0) _payLzToken(_fee.lzTokenFee);

return
// solhint-disable-next-line check-send-result
endpoint.send(
MessagingParams(_dstEid, _getPeerOrRevert(_dstEid), _message, _options, _fee.lzTokenFee > 0),
_refundAddress
);
}

To apply the changes made in _payNative, you should also override _lzSend to handle the ERC20 token fee.

info

Because _lzSend now uses an ERC20 token as payment, you must now approve the OFT as a spender of your ERC20 token.