James Bachini

Building LayerZero OFT Tokens In Solidity

layerzero oft token solidity

LayerZero OFT tokens are a new standard for cross-chain assets. OFT’s (Omnichain Fungible Tokens) are not actually tokens themselves, but rather a set of instructions that tell LayerZero how to transfer tokens between different blockchains. In this tutorial we will be building a LayerZero OFT ERC20 token using Solidity and Remix.

  1. How LayerZero OFT Tokens Work
  2. Example OFT Token Solidity Code
  3. Example OFT Proxy Solidity Code
  4. OFT Token Cross Chain Transfers

How LayerZero OFT Tokens Work

LayerZero’s OFT standard is a Solidity library and pattern for creating cross chain tokens. The developer will need to deploy a token contract on each chain that they want to interact with.

We then use the library function setTrustedRemoteAddress() to create a mapping of where each token is on each chain. Note that LayerZero chain IDs are different to Ethereum chain IDs*.

So what we end up with is each token contract on each chain has a list of chains and token contract addresses for the contract that has been deployed. On Ethereum mainnet or whichever chain is the primary devs will also mint some tokens. You can see in the contract below that there is a if (block.chainid == 5) statement which means we can deploy the same contract everywhere and it will only mint the tokens on Goerli.

At this point we have all the tokens on Goerli and the library provides another fuction sendFrom() which allows us to move tokens between chains.

When a holder uses sendFrom() on Goerli the tokens are burnt while providing minting authority for the same amount on the destination chain. LayerZero provides the cross-chain communications to enable this.

The holder on the secondary receiver chain can then use the tokens as normal or send them back or to another chain reversing the process of burning them locally and minting them on the next chain.


Example OFT Token Solidity Code

This LayerZero OFT is from my Solidity Snippets Github repo.

Full code: https://github.com/jamesbachini/Solidity-Snippets/blob/main/contracts/LayerZeroOFT.sol

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;

/*
    // https://layerzero.gitbook.io/docs/technical-reference/testnet/testnet-addresses
    Goerli          lzEndpointAddress = 0xbfD2135BFfbb0B5378b56643c2Df8a87552Bfa23
    chainId: 10121  deploymentAddress = 
    Optimism-Goerli lzEndpointAddress = 0xae92d5aD7583AD66E49A0c67BAd18F6ba52dDDc1
    chainId: 10132  deploymentAddress = 
*/
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "https://github.com/LayerZero-Labs/solidity-examples/blob/main/contracts/token/oft/OFTCore.sol";
import "https://github.com/LayerZero-Labs/solidity-examples/blob/main/contracts/token/oft/IOFT.sol";

contract LayerZeroOFT is OFTCore, ERC20, IOFT {
    address constant lzEndpointAddress = 0xbfD2135BFfbb0B5378b56643c2Df8a87552Bfa23;

    constructor() ERC20("LayerZeroOFT", "LZOFT") OFTCore(lzEndpointAddress) {
        if (block.chainid == 5) { // Only mint initial supply on Goerli
            _mint(msg.sender, 1_000_000 * 10 ** decimals());
        }
    }

    function supportsInterface(bytes4 interfaceId) public view virtual override(OFTCore, IERC165) returns (bool) {
        return interfaceId == type(IOFT).interfaceId || interfaceId == type(IERC20).interfaceId || super.supportsInterface(interfaceId);
    }

    function token() public view virtual override returns (address) {
        return address(this);
    }

    function circulatingSupply() public view virtual override returns (uint) {
        return totalSupply();
    }

    function _debitFrom(address _from, uint16, bytes memory, uint _amount) internal virtual override returns(uint) {
        address spender = _msgSender();
        if (_from != spender) _spendAllowance(_from, spender, _amount);
        _burn(_from, _amount);
        return _amount;
    }

    function _creditTo(uint16, address _toAddress, uint _amount) internal virtual override returns(uint) {
        _mint(_toAddress, _amount);
        return _amount;
    }
}

Example OFT Proxy Solidity Code

The OFT Proxy contract is used like a vault to create a cross-chain tokenised wrapper for an existing ERC20 token. This code is straight from the LayerZero examples at: https://github.com/LayerZero-Labs/solidity-examples/blob/main/contracts/token/oft/extension/ProxyOFT.sol

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "../OFTCore.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";

contract ProxyOFT is OFTCore {
    using SafeERC20 for IERC20;

    IERC20 internal immutable innerToken;

    constructor(address _lzEndpoint, address _token) OFTCore(_lzEndpoint) {
        innerToken = IERC20(_token);
    }

    function circulatingSupply() public view virtual override returns (uint) {
        unchecked {
            return innerToken.totalSupply() - innerToken.balanceOf(address(this));
        }
    }

    function token() public view virtual override returns (address) {
        return address(innerToken);
    }

    function _debitFrom(address _from, uint16, bytes memory, uint _amount) internal virtual override returns(uint) {
        require(_from == _msgSender(), "ProxyOFT: owner is not send caller");
        uint before = innerToken.balanceOf(address(this));
        innerToken.safeTransferFrom(_from, address(this), _amount);
        return innerToken.balanceOf(address(this)) - before;
    }

    function _creditTo(uint16, address _toAddress, uint _amount) internal virtual override returns(uint) {
        uint before = innerToken.balanceOf(_toAddress);
        innerToken.safeTransfer(_toAddress, _amount);
        return innerToken.balanceOf(_toAddress) - before;
    }
}

OFT Token Cross Chain Transfers

Ideally developers will want to deploy tokens from a clean address with no transaction history on any chains. This will mean the token contract addresses will be the same on each chain and make things easier to manage.

A deployment script can be written in Hardhat to fund the wallets, deploy the contracts and call setTrustedRemoteAddress() functions.

Once the token contracts are set up a simple user interface can be prepared to make it easy for users to move tokens between chains. Frontend devs can use Viem or Ethers to connect the sendFrom() function to a web3 interface.

The BTC.b cross-chain bitcoin token has an elegant, simple UI which can provide a good working example of what this could look like.

LayerZero OFT UI



Get The Blockchain Sector Newsletter, binge the YouTube channel and connect with me on Twitter

The Blockchain Sector newsletter goes out a few times a month when there is breaking news or interesting developments to discuss. All the content I produce is free, if you’d like to help please share this content on social media.

Thank you.

James Bachini

Disclaimer: Not a financial advisor, not financial advice. The content I create is to document my journey and for educational and entertainment purposes only. It is not under any circumstances investment advice. I am not an investment or trading professional and am learning myself while still making plenty of mistakes along the way. Any code published is experimental and not production ready to be used for financial transactions. Do your own research and do not play with funds you do not want to lose.


Posted

in

, , , ,

by