OFT V1.2 (Legacy) Walkthrough
OFT V1.2 (legacy) is NOT built on LayerZero V2. Rather it is an updated version of OFT for Endpoint V1.
Developers looking to build on LayerZero V2 should head to the new V2 Documentation.
Omnichain Fungible Tokens (OFTs) are a type of token built on top of LayerZero that can exist and be transferred across multiple blockchain networks while maintaining a single, unified total supply. This is a groundbreaking feature because, traditionally, tokens are native to a single blockchain. For example, an ERC-20 token exists solely on the Ethereum network. However, OFTs break this limitation by allowing the same token to move seamlessly between different blockchains.
Overview
Transferring OFT tokens across chains involves similar steps to the simple cross-chain interaction process. However, there are specific nuances related to handling fungible tokens and their associated transactions. Here's a high-level example of how a user might send OFT tokens from source to destination:
- The user calls a function in the OFT contract on source, specifying the token amount to send and the Ethereum destination address.
- The contract debits the tokens from the user's balance on source and sends a cross-chain message to the destination contract.
- Upon receiving the message, the destination contract credits the tokens to the specified address.
Deployment
The OFT contract needs to be deployed on both the source and the destination chains. This process is similar to the deployment process for our simple cross-chain transaction sample. However, this time we will mint 100 tokens to the address that deploys the contract in our constructor.
To initiate, set up a deployment script in your /scripts
folder, and add constructor arguments based on the OFTV2 constructor function below:
Solidity
import "@layerzerolabs/solidity-examples/contracts/token/oft/v2/OFTV2.sol";
contract OFT is OFTV2 {
constructor(
string memory _name,
string memory _symbol,
uint8 _sharedDecimals,
address _lzEndpoint
) OFTV2(_name, _symbol, _sharedDecimals, _lzEndpoint) {
// ... your contract logic here
}
}
JavaScript
// deploy.js
// Using Ethers v5
const hre = require('hardhat');
async function main() {
const YourOFT = await hre.ethers.getContractFactory('OFTV2');
const endpointAddress = '0xEndpointAddress'; // Replace with the given chain's endpoint address
// Deploy the contract with the specified constructor arguments
const oft = await YourOFT.deploy(
'MyFirstOFT', // OFT name
'MYOFT', // OFT symbol
8, // shared decimals for your OFT
endpointAddress, // chain endpoint address
);
// Wait for the deployment to finish
await oft.waitForDeployment();
console.log('Your OFT deployed to:', await oft.getAddress());
}
// We recommend this pattern to be able to use async/await everywhere
// and properly handle errors.
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});
Then run the scripts in your hardhat environment (or the environment of your choice):
Hardhat CLI
npx hardhat run scripts/deploy.js --network networkA
npx hardhat run scripts/deploy.js --network networkA
What should I set as shared decimals?
- Setting Decimals on Non-EVM Chains: If your token is deployed on both non-EVM and EVM chains, you should set the decimal count to the lowest value used on any chain. For example, if it's 6 on Aptos (non-EVM) and 18 on Ethereum (EVM), you should use 6 as the standard across all chains. This ensures consistency in token divisibility across different platforms.
- Setting Decimals on EVM Chains Only: If your token is only on EVM chains and all deployments use more than 8 decimals, you should standardize it to 8 decimals across these chains.
Set Trusted Remotes
When a message is sent from the source contract on one blockchain, the destination contract on another blockchain must be pre-configured to recognize and accept messages only from a specific, trusted source contract address. This setup ensures that when the destination contract receives a message, it can authenticate that it's indeed from the correct source. Trusted remotes can be set by calling the following function:
// This function sets the trusted path for the cross-chain communication
// The _remoteChainId refers to the LayerZero EndpointId
function setTrustedRemoteAddress(uint16 _remoteChainId, bytes calldata _remoteAddress) external onlyOwner {
// We pack the remote address with the local address to create a unique chain path
trustedRemoteLookup[_remoteChainId] = abi.encodePacked(_remoteAddress, address(this));
emit SetTrustedRemoteAddress(_remoteChainId, _remoteAddress);
}
// This function sets the trusted path for the cross-chain communication
// The _remoteChainId refers to the LayerZero EndpointId
function setTrustedRemoteAddress(uint16 _remoteChainId, bytes calldata _remoteAddress) external onlyOwner {
// We pack the remote address with the local address to create a unique chain path
trustedRemoteLookup[_remoteChainId] = abi.encodePacked(_remoteAddress, address(this));
emit SetTrustedRemoteAddress(_remoteChainId, _remoteAddress);
}
Once your trusted remotes are set your contracts are ready to send and receive cross-chain transactions!
To implement custom parameters that enable more nuanced configurability when wiring up your contracts, please see Wire Up Configration.
Conducting an OFT Token Transfer
To initiate a transfer:
From the Source Chain
- Define your
adapterParams
according to your specifications. - Call the
estimateSendFee
function, which will return anativeFee
gas quote for your token transfer.- Set the
nativeFee
as the msg.value for yoursendFrom
function.
- Set the
- Call the
sendFrom
function in your smart contract that triggers_debitFrom
, which handles the deduction of tokens from the user's balance and initiates the cross-chain request using LayerZero's messaging system.
On the Destination Chain: When the message is received, _creditTo
is invoked on the destination chain, crediting the specified amount of tokens to the target address.
Let's compile this token transfer process into a script that sends 10 MYOFT tokens from the source network to destination with the necessary input parameters for estimateSendFee
and sendFrom
properly labeled:
JavaScript
// crossChainOFTTransfer.js
// Using Ethers v5
const hre = require('hardhat');
async function performOFTTransfer() {
const ethers = hre.ethers;
// Connect to your OFT contract
const oftContractFactory = await ethers.getContractFactory('YourOFTContract');
const oftContract = oftContractFactory.attach('0xYourOFTContractAddress'); // Replace with your OFT contract's address
// Define the destination endpoint ID and recipient address
const dstEndpointId = '101'; // Destination LayerZero Endpoint ID (example: 101)
const toAddress = ethers.utils.zeroHexPad('0xRecipientAddress', 32); // Convert recipient's address to bytes32 format
// Specify the amount of tokens to be transferred
const transferAmount = ethers.parseUnits('10', 8); // Transferring 10 tokens with 8 decimals (shared decimals amount)
// Prepare adapter parameters for LayerZero
const adapterParams = ethers.utils.solidityPack(['uint16', 'uint256'], [1, 200000]); // Customizing LayerZero's adapter parameters
// Estimate the fees for the cross-chain transfer
const [nativeFee, zroFee] = await oftContract.estimateSendFee(
dstEndpointId,
toAddress,
transferAmount,
false,
adapterParams,
); // false indicates no ZRO payment will be made
console.log(`Estimated Fees: Native - ${nativeFee}, ZRO - ${zroFee}`);
// Execute the cross-chain transfer
const tx = await oftContract.sendFrom(
'0xYourAddress', // Your address as the sender
dstEndpointId,
toAddress,
transferAmount,
{
refundAddress: '0xYourRefundAddress', // Address for refunded gas to be sent
zroPaymentAddress: '0xZroPaymentAddress', // Address for ZRO token payment, if used
adapterParams: adapterParams,
},
{value: nativeFee},
);
await tx.wait();
console.log('Cross-chain OFT transfer completed', tx);
}
performOFTTransfer()
.then(() => console.log('OFT Transfer completed successfully'))
.catch((error) => console.error('Error in OFT transfer:', error));
Hardhat CLI
npx hardhat run scripts/crossChainOFTTransfer.js --network networkA
Examine the transaction on LayerZero Scan
Finally, let's see what's happening in our transaction. Take your transaction hash (printed in your console logs from the step before) and paste it into: https://testnet.layerzeroscan.com/.
You should see Status: Delivered, confirming your message has been delivered to its destination using LayerZero.
Congrats, you just executed your first Omnichain fungible token transfer! 🥳