LayerZero V2 OFT Quickstart
The Omnichain Fungible Token (OFT) Standard allows fungible tokens to be transferred across multiple blockchains without asset wrapping or middlechains.
This standard works by burning tokens on the source chain whenever an omnichain transfer is initiated, sending a message via the protocol and delivering a function call to the destination contract to mint the same number of tokens burned, creating a unified supply across all networks LayerZero supports.
Using this design pattern, LayerZero can extend any fungible token to interoperate with other chains. The most widely used of these standards is OFT.sol
, an extension of the OApp Contract Standard and the ERC20 Token Standard.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import { IOFT, OFTCore } from "./OFTCore.sol";
/**
* @title OFT Contract
* @dev OFT is an ERC-20 token that extends the functionality of the OFTCore contract.
*/
abstract contract OFT is OFTCore, ERC20 {
/**
* @dev Constructor for the OFT contract.
* @param _name The name of the OFT.
* @param _symbol The symbol of the OFT.
* @param _lzEndpoint The LayerZero endpoint address.
* @param _delegate The delegate capable of making OApp configurations inside of the endpoint.
*/
constructor(
string memory _name,
string memory _symbol,
address _lzEndpoint,
address _delegate
) ERC20(_name, _symbol) OFTCore(decimals(), _lzEndpoint, _delegate) {}
/**
* @notice Retrieves interfaceID and the version of the OFT.
* @return interfaceId The interface ID.
* @return version The version.
*
* @dev interfaceId: This specific interface ID is '0x02e49c2c'.
* @dev version: Indicates a cross-chain compatible msg encoding with other OFTs.
* @dev If a new feature is added to the OFT cross-chain msg encoding, the version will be incremented.
* ie. localOFT version(x,1) CAN send messages to remoteOFT version(x,1)
*/
function oftVersion() external pure virtual returns (bytes4 interfaceId, uint64 version) {
return (type(IOFT).interfaceId, 1);
}
/**
* @dev Retrieves the address of the underlying ERC20 implementation.
* @return The address of the OFT token.
*
* @dev In the case of OFT, address(this) and erc20 are the same contract.
*/
function token() external view returns (address) {
return address(this);
}
/**
* @notice Indicates whether the OFT contract requires approval of the 'token()' to send.
* @return requiresApproval Needs approval of the underlying token implementation.
*
* @dev In the case of OFT where the contract IS the token, approval is NOT required.
*/
function approvalRequired() external pure virtual returns (bool) {
return false;
}
/**
* @dev Burns tokens from the sender's specified balance.
* @param _amountLD The amount of tokens to send in local decimals.
* @param _minAmountLD The minimum amount to send in local decimals.
* @param _dstEid The destination chain ID.
* @return amountSentLD The amount sent in local decimals.
* @return amountReceivedLD The amount received in local decimals on the remote.
*/
function _debit(
uint256 _amountLD,
uint256 _minAmountLD,
uint32 _dstEid
) internal virtual override returns (uint256 amountSentLD, uint256 amountReceivedLD) {
(amountSentLD, amountReceivedLD) = _debitView(_amountLD, _minAmountLD, _dstEid);
// @dev In NON-default OFT, amountSentLD could be 100, with a 10% fee, the amountReceivedLD amount is 90,
// therefore amountSentLD CAN differ from amountReceivedLD.
// @dev Default OFT burns on src.
_burn(msg.sender, amountSentLD);
}
/**
* @dev Credits tokens to the specified address.
* @param _to The address to credit the tokens to.
* @param _amountLD The amount of tokens to credit in local decimals.
* @dev _srcEid The source chain ID.
* @return amountReceivedLD The amount of tokens ACTUALLY received in local decimals.
*/
function _credit(
address _to,
uint256 _amountLD,
uint32 /*_srcEid*/
) internal virtual override returns (uint256 amountReceivedLD) {
// @dev Default OFT mints on dst.
_mint(_to, _amountLD);
// @dev In the case of NON-default OFT, the _amountLD MIGHT not be == amountReceivedLD.
return _amountLD;
}
}
If you prefer reading the contract code, see the OFT contract in the Protocol Github.
Looking to use an already deployed ERC20 token as an OFT? Use the OFT Adapter extension.
Installation
To start using LayerZero contracts, you can install the OApp npm package to an existing project:
npm install @layerzerolabs/lz-evm-oapp-v2
LayerZero contracts work with both OpenZeppelin V5 and V4 contracts. Specify your desired version in your project's package.json:
"resolutions": {
"@openzeppelin/contracts": "^5.0.1",
}
LayerZero also provides create-lz-oapp, an npx package that allows developers to create any omnichain application in <4 minutes! Get started by running the following from your command line:
npx create-lz-oapp@latest
Constructing an OFT Contract
In this example, you'll create an OFT that is an ERC20 token named Silver (SLVR) for a hypothetical blockchain game.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.22;
import "@layerzerolabs/lz-evm-oapp-v2/contracts/oft/OFT.sol";
import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol";
contract SLVRToken is OFT {
constructor(
string memory _name, // token name
string memory _symbol, // token symbol
address _layerZeroEndpoint, // local endpoint address
address _owner // token owner used as a delegate in LayerZero Endpoint
) OFT(_name, _symbol, _layerZeroEndpoint, _owner) Ownable(_owner) {
// your contract logic here
_mint(msg.sender, 100 ether); // mints 100 tokens to the deployer
}
}
The SLVRToken
inherits from OFT
for the base implementation, while allowing for custom contract logic to be added. For example, you can mint 100
tokens to the address that deploys the contract.
By default, the OFT follows ERC20 convention and uses a value of 18
for decimals. To use a different value, you will need to override the decimals()
function in your contract.
This standard has already implemented OApp
related functions like _lzSend
and _lzReceive
. Instead, you will override and use _debit
and _credit
when writing your own custom OFT
logic.
This contract contains everything necessary to launch an omnichain ERC20 and can be deployed immediately. It also can be highly customized if you wish to add extra functionality. Let's dive in!
Token Supply Cap
When transferring tokens between EVM and non-EVM chains, it's crucial to consider how each chain stores token balances.
While EVM chains support uint256
for token balances, many non-EVM environments use uint64
. Because of this, the default OFT Standard has a max token supply (2^64 - 1)/(10^6)
, or 18,446,744,073,709.551615
. This ensures that token transfers won't fail due to a loss of precision or unexpected balance conversions.
If your token's supply needs to exceed this limit, you'll need to override the shared decimals value.
Optional: Overriding sharedDecimals
By default, an OFT has 6 sharedDecimals
, which is optimal for most ERC20 use cases that use 18
decimals.
// @dev Sets an implicit cap on the amount of tokens, over uint64.max() will need some sort of outbound cap / totalSupply cap
// Lowest common decimal denominator between chains.
// Defaults to 6 decimal places to provide up to 18,446,744,073,709.551615 units (max uint64).
// For tokens exceeding this totalSupply(), they will need to override the sharedDecimals function with something smaller.
// ie. 4 sharedDecimals would be 1,844,674,407,370,955.1615
function sharedDecimals() public view virtual returns (uint8) {
return 6;
}
Internal functions compare the shared decimal (SD) value with the token decimals (LD) to ensure that balances don't overflow between chains:
// @dev ensure no overflows when converting from amountLD to amountSD
// amountLDMax = amountSDMax * ld2sdRate
// (2^256 - 1) = (2^64 - 1) * ld2sdRate
// (2^256 - 1) = (2^64 - 1) * (10 ^ (_localDecimals - sharedDecimals()))
// (2^256 - 1) = (2^64 - 1) * (10 ^ MAX_LD_SD_DECIMAL_DIFF)
// (2^256 - 1) = (2^64 - 1) * (10 ^ 57.931~)
// ld - sd <= 57;
uint internal immutable ld2sdRate;
To modify this default, simply override the sharedDecimals
function to return another value.
Adding Send Logic
When calling the send
function, _debit
is invoked, triggering the ERC20 token on the source chain to be burned.
function send(
SendParam calldata _sendParam,
MessagingFee calldata _fee,
address _refundAddress
) external payable virtual returns (MessagingReceipt memory msgReceipt, OFTReceipt memory oftReceipt) {
// @dev Applies the token transfers regarding this send() operation.
// - amountSentLD is the amount in local decimals that was ACTUALLY sent/debited from the sender.
// - amountReceivedLD is the amount in local decimals that will be received/credited to the recipient on the remote OFT instance.
(uint256 amountSentLD, uint256 amountReceivedLD) = _debit(
_sendParam.amountLD,
_sendParam.minAmountLD,
_sendParam.dstEid
);
// ...
}
You can override the _debit
function with any additional logic you want to execute before the message is sent via the protocol, for example, taking custom fees.
All of the previous functions use the _debitView
function to handle how many tokens should be debited on the source chain, versus credited on the destination.
This function can be overriden, allowing your OFT to implement custom fees by changing the amountSentLD
and amountReceivedLD
amounts:
// @dev allows the quote functions to mock sending the actual values that would be sent in a send()
function _debitView(
uint256 _amountLD,
uint256 _minAmountLD,
uint32 /*_dstEid*/
) internal view virtual returns (uint256 amountSentLD, uint256 amountReceivedLD) {
// @dev Remove the dust so nothing is lost on the conversion between chains with different decimals for the token.
amountSentLD = _removeDust(_amountLD);
// @dev The amount to send is the same as amount received in the default implementation.
amountReceivedLD = amountSentLD;
// @dev Check for slippage.
if (amountReceivedLD < _minAmountLD) {
revert SlippageExceeded(amountReceivedLD, _minAmountLD);
}
}
Adding Receive Logic
Similar to send
, you can add custom logic when receiving an ERC20 token transfer on the destination chain by overriding the _credit
function.
function _credit(
address _to,
uint256 _amountToCreditLD,
uint32 /*_srcEid*/
) internal virtual override returns (uint256 amountReceivedLD) {
_mint(_to, _amountToCreditLD);
return _amountToCreditLD;
}
Be careful with the ordering of additional logic. Once a token is minted, the transaction is typically irreversible, so special care should be taken to understand how custom logic impacts end users.
Setting Delegates
In an OFT, a delegate can be assigned to implement custom configurations on behalf of the contract owner. This delegate gains the ability to handle various critical tasks such as setting configurations and MessageLibs, and skipping or clearing payloads, for the OFT.
By default, the contract owner is set as the delegate. The setDelegate
function allows for changing this, but we recommend you always keep contract owner as delegate.
function setDelegate(address _delegate) public onlyOwner {
endpoint.setDelegate(_delegate);
}
For instructions on how to implement custom configurations after setting your delegate, refer to the OApp Configuration.
Security and Governance
Given the impact associated with deployment, configuration, and debugging functions, OFT owners may want to add additional security measures in place to call core contract functions instead of onlyOwner
, such as:
Governance Controls: Implementing a governance mechanism where decisions to clear messages are voted upon by stakeholders.
Multisig Deployment: Deploying with a multisig wallet, preventing arbitrary actions by any one team member.
Timelocks: Using a timelock to delay the execution of the clear function, giving stakeholders time to react if the function is called inappropriately.
Deployment & Usage
You can now deploy your contracts and get one step closer to moving fungible tokens between chains.
Setting Trusted Peers
Once you've finished configuring your OFT, you can open the messaging channel and connect your OFT deployment to different chains by calling setPeer
.
The function takes 2 arguments: _eid
, the endpoint ID for the destination chain the other OFT contract lives on, and _peer
, the destination OFT contract address in bytes32
format.
// @dev must-have configurations for standard OApps
function setPeer(uint32 _eid, bytes32 _peer) public virtual onlyOwner {
peers[_eid] = _peer; // Array of peer addresses by destination.
emit PeerSet(_eid, _peer); // Event emitted each time a peer is set.
}
setPeer
opens your OFT to start receiving messages from the messaging channel, meaning you should configure any application settings you intend on changing prior to calling setPeer
.
OFTs need setPeer
to be called correctly on both contracts to send messages. The peer address uses bytes32
for handling non-EVM destination chains.
If the peer has been set to an incorrect destination address, your messages will not be delivered and handled properly. If not resolved, users can burn source funds without a corresponding mint on destination. You can confirm the peer address is the expected destination OFT address by using the isPeer
function.
The LayerZero Endpoint will use this peer as the destination address for the cross-chain message:
// @dev the endpoint send method called by _lzSend
endpoint.send{ value: messageValue }(
MessagingParams(_dstEid, _getPeerOrRevert(_dstEid), _message, _options, _fee.lzTokenFee > 0),
_refundAddress
);
To see if an address is the trusted peer you expect for a destination, you can read the peers
mapping directly:
/**
* @dev Internal function to check if peer is considered 'trusted' by the OApp.
* @param _eid The endpoint ID to check.
* @param _peer The peer to check.
* @return Whether the peer passed is considered 'trusted' by the OApp.
*
* @dev Enables OAppPreCrimeSimulator to check whether a potential Inbound Packet is from a trusted source.
*/
function isPeer(uint32 _eid, bytes32 _peer) public view virtual override returns (bool) {
return peers[_eid] == _peer;
}
This can be useful for confirming whether setPeer
has been called correctly and as expected.
Message Execution Options
_options
are a generated bytes array with specific instructions for the DVNs and Executor to use when handling the authentication and execution of received messages.
You can find how to generate all the available _options
in Message Execution Options, but for this tutorial we'll focus on how options work with OFT.
ExecutorLzReceiveOption
: instructions for how much gas the Executor should use when callinglzReceive
on the destination Endpoint.
For example, usually to send a vanilla OFT to a destination chain you will need 60000
wei in native gas on destination. The options will look like the following:
_options = 0x0003010011010000000000000000000000000000ea60;
ExecutorLzReceiveOption
specifies a quote paid in advance on the source chain by the msg.sender
for the equivalent amount of native gas to be used on the destination chain. If the actual cost to execute the message is less than what was set in _options
, there is no default way to refund the sender the difference. Application developers need to thoroughly profile and test gas amounts to ensure consumed gas amounts are correct and not excessive.
Setting Enforced Options
Once you determine ideal message _options
, you will want to make sure users adhere to it. In the case of OFT, you mostly want to make sure the gas is enough for transferring the ERC20 token, plus any additional logic.
A typical OFT's lzReceive
call will use 60000
gas on most EVM chains, so you can enforce this option to require callers to pay a 60000
gas limit in the source chain transaction to prevent out of gas issues:
_options = 0x0003010011010000000000000000000000000000ea60;
You can use the create-lz-oapp
npx package to set enforcedOptions
in a human readable format by defining your settings in your layerzero.config.ts
.
The setEnforcedOptions
function allows the contract owner to specify mandatory execution options, making sure that the application behaves as expected when users interact with it.
// inherited from EnforcedOptions.sol inside OApp
function setEnforcedOptions(
EnforcedOptionParam[] calldata _enforcedOptions
) external virtual onlyOwner {
for (uint i = 0; i < _enforcedOptions.length; i++) {
uint16 optionsType = uint16(bytes2(_enforcedOptions[i].options[0:2]));
if (optionsType != TYPE_3) revert InvalidOptions(); // not supported for options type 1 and 2
enforcedOptions[_enforcedOptions[i].eid][_enforcedOptions[i].msgType] = _enforcedOptions[i].options;
}
emit SetEnforcedOption(_enforcedOptions);
}
To use setEnforcedOptions
, we only need to pass one parameter:
EnforcedOptionParam[]
: a struct specifying the execution options per message type and destination chain.struct EnforcedOptionParam {
uint32 eid; // destination endpoint id
uint16 msgType; // the message type
bytes options; // the execution option bytes array
}The OFT Standard only has handling for 2 message types:
// @dev execution types to handle different enforcedOptions
uint16 internal constant SEND = 1; // a standard token transfer via send()
uint16 internal constant SEND_AND_CALL = 2; // a composed token transfer via send()Pass these values in when specifying the
msgType
for your_options
.
For best practice, generate this array off-chain and pass it as a parameter when configuring your OFT:
// Generate the EnforcedOptionParam[] array
let enforcedOptions = [
{
eid: /*your destination endpoint id here*/,
msgType: 1,
options: /*your options here. Our one would be 0x00030100110100000000000000000000000000030d40;*/
},
{
eid: /*your destination endpoint id here*/,
msgType: 2,
options: /*your options here*/
}
// ... add more destinations parameters as needed
];
// Call the setEnforcedOptions function
await contract.setEnforcedOptions(enforcedOptions);
When setting enforcedOptions
, try not to unintentionally pass a duplicate _options
argument to extraOptions
. Passing identical _options
in both enforcedOptions
and extraOptions
will cause the protocol to charge the caller twice on the source chain, because LayerZero interprets duplicate _options
as two separate requests for gas.
Setting Extra Options
Any _options
passed in the send
call itself should be considered _extraOptions
.
_extraOptions
can specify additional handling within the same message type. These _options
will then be combined with enforcedOption
if set.
If not needed in your application, you should pass an empty bytes array 0x
.
if (enforced.length > 0) {
// combine extra options with enforced options
// remove the first 2 bytes (TYPE_3) of extra options
// should pack executor options last in enforced options (assuming most extra options are executor options only)
// to save gas on grouping by worker id in message library
uint16 extraOptionsType = uint16(bytes2(_extraOptions[0:2]));
uint16 enforcedOptionsType = (uint16(uint8(enforced[0])) << 8) + uint8(enforced[1]);
if (extraOptionsType != enforcedOptionsType) revert InvalidOptions();
options = bytes.concat(enforced, _extraOptions[2:]);
} else {
// no enforced options, use extra options directly
options = _extraOptions;
}
As outlined above, decide on whether you need an application wide option via enforcedOptions
or a call specific option using extraOptions
. Be specific in what _options
you use for both parameters, as your transactions will reflect the exact settings you implement.
Estimating Gas Fees
Now let's get an estimate of how much gas a transfer will cost to be sent and received.
To do this we can call the quoteSend
function to return an estimate from the Endpoint contract to use as a recommended msg.value
.
Arguments of the estimate function:
SendParam
: what parameters should be used for the send call?/**
* @dev Struct representing token parameters for the OFT send() operation.
*/
struct SendParam {
uint32 dstEid; // Destination endpoint ID.
bytes32 to; // Recipient address.
uint256 amountLD; // Amount to send in local decimals.
uint256 minAmountLD; // Minimum amount to send in local decimals.
bytes extraOptions; // Additional options supplied by the caller to be used in the LayerZero message.
bytes composeMsg; // The composed message for the send() operation.
bytes oftCmd; // The OFT command to be executed, unused in default OFT implementations.
}
Here is a link to further explain Extra Message Options that would be used besides enforcedOptions
.
_payInLzToken
: what token will be used to pay for the transaction?struct MessagingFee {
uint nativeFee; // gas amount in native gas token
uint lzTokenFee; // gas amount in ZRO token
}
/**
* @notice Provides a quote for the send() operation.
* @param _sendParam The parameters for the send() operation.
* @param _payInLzToken Flag indicating whether the caller is paying in the LZ token.
* @return msgFee The calculated LayerZero messaging fee from the send() operation.
*
* @dev MessagingFee: LayerZero msg fee
* - nativeFee: The native fee.
* - lzTokenFee: The lzToken fee.
*/
function quoteSend(
SendParam calldata _sendParam,
bool _payInLzToken
) external view virtual returns (MessagingFee memory msgFee) {
// @dev mock the amount to receive, this is the same operation used in the send().
// The quote is as similar as possible to the actual send() operation.
(, uint256 amountReceivedLD) = _debitView(_sendParam.amountLD, _sendParam.minAmountLD, _sendParam.dstEid);
// @dev Builds the options and OFT message to quote in the endpoint.
(bytes memory message, bytes memory options) = _buildMsgAndOptions(_sendParam, amountReceivedLD);
// @dev Calculates the LayerZero fee for the send() operation.
return _quote(_sendParam.dstEid, message, options, _payInLzToken);
}
Calling send
Since the send
logic has already been defined, we'll instead view how the function should be called.
// @dev executes a cross-chain OFT swap via layerZero Endpoint
function send(
SendParam calldata _sendParam,
MessagingFee calldata _fee,
address _refundAddress
) external payable virtual returns (MessagingReceipt memory msgReceipt, OFTReceipt memory oftReceipt) {
// @dev Applies the token transfers regarding this send() operation.
// - amountSentLD is the amount in local decimals that was ACTUALLY sent/debited from the sender.
// - amountReceivedLD is the amount in local decimals that will be received/credited to the recipient on the remote OFT instance.
(uint256 amountSentLD, uint256 amountReceivedLD) = _debit(
_sendParam.amountLD,
_sendParam.minAmountLD,
_sendParam.dstEid
);
// @dev Builds the options and OFT message to quote in the endpoint.
(bytes memory message, bytes memory options) = _buildMsgAndOptions(_sendParam, amountReceivedLD);
// @dev Sends the message to the LayerZero endpoint and returns the LayerZero msg receipt.
msgReceipt = _lzSend(_sendParam.dstEid, message, options, _fee, _refundAddress);
// @dev Formulate the OFT receipt.
oftReceipt = OFTReceipt(amountSentLD, amountReceivedLD);
emit OFTSent(msgReceipt.guid, _sendParam.dstEid, msg.sender, amountSentLD, amountReceivedLD);
}
To do this, we only need to pass send
a few inputs:
SendParam
: what parameters should be used for the send call?struct SendParam {
uint32 dstEid; // Destination endpoint ID.
bytes32 to; // Recipient address.
uint256 amountLD; // Amount to send in local decimals.
uint256 minAmountLD; // Minimum amount to send in local decimals.
bytes extraOptions; // Additional options supplied by the caller to be used in the LayerZero message.
bytes composeMsg; // The composed message for the send() operation.
bytes oftCmd; // The OFT command to be executed, unused in default OFT implementations.
}
extraOptions
: should Extra Message Options be used besides enforcedOptions
?
_fee
: what token will be used to pay for the transaction?struct MessagingFee {
uint nativeFee; // gas amount in native gas token
uint lzTokenFee; // gas amount in ZRO token
}_refundAddress
: If the transaction fails on the source chain, where should funds be refunded?
Optional: _composedMsg
When sending an OFT, you can also include an optional _composedMsg
parameter in the transaction to execute additional logic on the destination chain as part of the token transfer.
// @dev executes an omnichain OFT swap via layerZero Endpoint
if (_composeMsg.length > 0) {
// @dev Remote chains will want to know the composed function caller.
// ALSO, the presence of a composeFrom msg.sender inside of the bytes array indicates the payload should
// be composed. ie. this allows users to compose with an empty payload, vs it must be length > 0
_composeMsg = abi.encodePacked(OFTMsgCodec.addressToBytes32(msg.sender), _composeMsg);
}
msgReceipt = _sendInternal(
_send,
combineOptions(_send.dstEid, SEND_AND_CALL, _extraOptions),
_msgFee, // message fee
_refundAddress, // refund address for failed source tx
_composeMsg // composed message
);
On the destination chain, the _lzReceive
function will first process the token transfer, crediting the recipient's account with the specified amount, and then check if _message.isComposed()
.
if (_message.isComposed()) {
bytes memory composeMsg = OFTComposeMsgCodec.encode(
_origin.nonce, // nonce of the origin transaction
_origin.srcEid, // source endpoint id of the transaction
amountLDReceive, // the token amount in local decimals to credit
_message.composeMsg() // the composed message
);
// @dev Stores the lzCompose payload that will be executed in a separate tx.
// standardizes functionality for delivering/executing arbitrary contract invocation on some non evm chains.
// @dev Composed toAddress is the same as the receiver of the oft/tokens
endpoint.deliverComposedMessage(toAddress, _guid, composeMsg);
}
If the message is composed, the contract retrieves and re-encodes the additional composed message information, then delivers the message to the endpoint, which will execute the additional logic as a separate transaction.
Optional: _oftCmd
_oftCmd is a reserved field in the OFT interface that allows customed OFT implementation.
_lzReceive
tokens
A successful send
call will be delivered to the destination chain, invoking the provided _lzReceive
method during execution:
function _lzReceive(
Origin calldata _origin,
bytes32 _guid,
bytes calldata _message,
address /*_executor*/,
bytes calldata /*_extraData*/
) internal virtual override {
// @dev sendTo is always a bytes32 as the remote chain initiating the call doesnt know remote chain address size
address toAddress = _message.sendTo().bytes32ToAddress();
uint256 amountToCreditLD = _toLD(_message.amountSD());
uint256 amountReceivedLD = _credit(toAddress, amountToCreditLD, _origin.srcEid);
if (_message.isComposed()) {
bytes memory composeMsg = OFTComposeMsgCodec.encode(
_origin.nonce,
_origin.srcEid,
amountReceivedLD,
_message.composeMsg()
);
// @dev Stores the lzCompose payload that will be executed in a separate tx.
// standardizes functionality for executing arbitrary contract invocation on some non-evm chains.
// @dev Composed toAddress is the same as the receiver of the oft/tokens
// TODO need to document the index / understand how to use it properly
endpoint.sendCompose(toAddress, _guid, 0, composeMsg);
}
emit OFTReceived(_guid, toAddress, amountToCreditLD, amountReceivedLD);
}
_credit
:
When receiving the message on your destination contract, _credit
is invoked, triggering the final steps to mint an ERC20 token on the destination to the specified address.
function _credit(
address _to,
uint256 _amountToCreditLD,
uint32 /*_srcEid*/
) internal virtual override returns (uint256 amountReceivedLD) {
_mint(_to, _amountToCreditLD);
return _amountToCreditLD;
}