Skip to main content

Developer Guide

All USDT0 Network tokens (USDT0, XAUT0, and USAT) are Omnichain Fungible Tokens (OFT). The OFT standard allows tokens to move seamlessly across multiple blockchains using LayerZero's messaging protocol, ensuring a unified supply across all chains. USDT0 tokens leverage LayerZero's infrastructure to enable secure and efficient cross-chain transfers.

1. Architecture Overview​

The USDT0 implementation separates token functionality from cross-chain messaging. This split enables independent upgrades of token and messaging components while maintaining consistent token behavior across chains.

Core Components​

The implementation consists of three main components:

  1. OAdapterUpgradeable (on Ethereum):
    1. Implements LayerZero OFT functionality for Ethereum
    2. Handles both sending and receiving cross-chain messages
    3. Interfaces directly with the source token contract on Ethereum (TetherToken for USDT0, XAUT for XAUT0, or HadronToken for USAT)
    4. Locks/Unlocks tokens for cross-chain transfers
  2. OUpgradeable (on other chains):
    1. Implements LayerZero OFT functionality for other chains
    2. Handles both sending and receiving cross-chain messages
    3. Interfaces with the TetherTokenOFTExtension (or equivalent)
    4. Controls minting/burning for cross-chain transfers
  3. TetherTokenOFTExtension (on other chains):
    1. Offers mint/burn interface for the OFT

Component Interaction​

The system enables USDT0 tokens to be transferred across Ethereum, Chain A, and Chain B:

  1. Ethereum → Chain B:
    • USDT0 Adapter locks tokens on Ethereum
    • A LayerZero message triggers USDT0 OFT on Chain B to mint equivalent tokens
  2. Chain B → Chain A:
    • USDT0 OFT burns tokens on Chain B
    • A message triggers USDT0 OFT on Chain A to mint the equivalent
  3. Chain A → Ethereum:
    • USDT0 OFT burns tokens on Chain A
    • A message instructs the USDT0 Adapter to unlock tokens on Ethereum

The flow ensures consistent token supply across chains.

Component Interaction Diagram
Component Interaction Diagram

2. Interfaces Reference​

Token Interfaces​

On most chains, the USDT0 token implements the following standard interfaces:

  • ERC20
  • ERC20Permit (EIP-2612)
  • EIP-3009 (gasless transfers)
  • EIP-1271 signature support (smart contract wallets)

Support varies on a few chains, such as Polygon PoS, Tempo, and Hedera. For typed-data formats, signing examples, and the per-chain feature matrix, see Token features.

Key public functions for integration:

interface IERC20 {
function transfer(address to, uint256 amount) external returns (bool);
function transferFrom(address from, address to, uint256 amount) external returns (bool);
function approve(address spender, uint256 amount) external returns (bool);
function allowance(address owner, address spender) external view returns (uint256);
function balanceOf(address account) external view returns (uint256);
function totalSupply() external view returns (uint256);
}

interface IERC20Permit {
function permit(
address owner,
address spender,
uint256 value,
uint256 deadline,
uint8 v,
bytes32 r,
bytes32 s
) external;
}

// EIP-3009 functions for gasless transfers
interface IEIP3009 {
function transferWithAuthorization(
address from,
address to,
uint256 value,
uint256 validAfter,
uint256 validBefore,
bytes32 nonce,
bytes memory signature
) external;

function receiveWithAuthorization(
address from,
address to,
uint256 value,
uint256 validAfter,
uint256 validBefore,
bytes32 nonce,
bytes memory signature
) external;
}

OFT Interfaces​

The OFT implementation provides cross-chain transfer functionality through LayerZero:

interface IOFT {
struct SendParam {
uint32 dstEid; // Destination endpoint ID
bytes32 to; // Recipient address
uint256 amountLD; // Amount to send
uint256 minAmountLD; // Minimum amount to receive
bytes extraOptions; // Additional options
bytes composeMsg; // Optional composed message
bytes oftCmd; // OFT-specific command
}

struct MessagingFee {
uint256 nativeFee; // Fee in native gas
uint256 lzTokenFee; // Fee in LZ token
}

struct OFTReceipt {
uint256 amountSentLD; // Amount sent
uint256 amountReceivedLD; // Amount received
}

// Get expected received amount
function quoteOFT(SendParam calldata sendParam)
external view returns (
OFTLimit memory, // Min/max amounts
OFTFeeDetail[] memory, // Fee breakdown
OFTReceipt memory // Expected amounts
);

// Get messaging fee
function quoteSend(SendParam calldata sendParam, bool payInLzToken)
external view returns (MessagingFee memory);

// Execute the cross-chain transfer
function send(
SendParam calldata sendParam,
MessagingFee calldata fee,
address refundAddress
) external payable;
}

The OFT interface is consistent across all chains, whether using OAdapterUpgradeable on Ethereum or OUpgradeable on other chains. The only difference is that on Ethereum, users need to approve the OFT adapter to spend their tokens before calling send.

Example: Bridging from Ethereum to Arbitrum​

The following example shows how to bridge tokens from Ethereum to Arbitrum, including necessary approvals and parameter handling. This example uses USDT0, but the same code works for XAUT0 by changing the token and OFT addresses.

import { ethers } from 'ethers';

// USDT0 addresses
const USDT_ADDRESS = '0xdAC17F958D2ee523a2206206994597C13D831ec7';
const OFT_ADDRESS = '0x6C96dE32CEa08842dcc4058c14d3aaAD7Fa41dee';

// For XAUT0, use:
// const XAUT_ADDRESS = "0x68749665FF8D2d112Fa859AA293F07A622782F38";
// const OFT_ADDRESS = "0xb9c2321BB7D0Db468f570D10A424d1Cc8EFd696C";

const ARB_EID = 30110;
const MAX_UINT256 = ethers.constants.MaxUint256;

const OFT_ABI = [
'function quoteOFT(tuple(uint32,bytes32,uint256,uint256,bytes,bytes,bytes)) view returns (tuple(uint256,uint256), tuple(int256,string)[], tuple(uint256,uint256))',
'function quoteSend(tuple(uint32,bytes32,uint256,uint256,bytes,bytes,bytes), bool) view returns (tuple(uint256,uint256))',
'function send(tuple(uint32,bytes32,uint256,uint256,bytes,bytes,bytes), tuple(uint256,uint256), address) payable returns (tuple(bytes32,uint64,tuple(uint256,uint256)), tuple(uint256,uint256))',
];

async function sendT0ToArbitrum(
signer: ethers.Signer,
amount: string,
recipient: string,
refundAddress: string,
tokenAddress: string,
oftAddress: string,
decimals: number = 6
) {
const token = new ethers.Contract(tokenAddress, ['function approve(address,uint256)'], signer);
const oft = new ethers.Contract(oftAddress, OFT_ABI, signer);

const amountWei = ethers.utils.parseUnits(amount, decimals);

// Approve max amount
await token.approve(oftAddress, MAX_UINT256).then(({ wait }) => wait());

const sendParam = [ARB_EID, ethers.utils.hexZeroPad(recipient, 32), amountWei, 0, '0x', '0x', '0x'];

const [, , oftReceipt] = await oft.callStatic.quoteOFT(sendParam);
sendParam[3] = oftReceipt[1];
const msgFee = await oft.callStatic.quoteSend(sendParam, false);

const tx = await oft.send(sendParam, msgFee, refundAddress, { value: msgFee[0] });

return tx;
}

HyperCore Composer​

The HyperCore Composer enables transfers to HyperCore accounts from any connected chains.

important

Account Activation Requirement: The recipient account on HyperCore must be active, or the transfer amount must be at least 1 USDT0. If the account is inactive, 1 USDT0 will be deducted from the transfer amount to activate the account. With XAUT0 the account must be active.

The following example shows how to bridge tokens from Ethereum to HyperCore using the composeMsg parameter:

import { Options } from '@layerzerolabs/lz-v2-utilities';
import { ethers } from 'ethers';

// USDT0 addresses on Ethereum
const USDT_ADDRESS = '0xdAC17F958D2ee523a2206206994597C13D831ec7';
const OFT_ADDRESS = '0x6C96dE32CEa08842dcc4058c14d3aaAD7Fa41dee';

// For XAUT0, use:
// const XAUT_ADDRESS = "0x68749665FF8D2d112Fa859AA293F07A622782F38";
// const OFT_ADDRESS = "0xb9c2321BB7D0Db468f570D10A424d1Cc8EFd696C";

const HYPEREVM_EID = 30367;
const HYPERCORE_COMPOSER = '0x80123Ab57c9bc0C452d6c18F92A653a4ee2e7585';
const MAX_UINT256 = ethers.constants.MaxUint256;

const OFT_ABI = [
'function quoteOFT(tuple(uint32,bytes32,uint256,uint256,bytes,bytes,bytes)) view returns (tuple(uint256,uint256), tuple(int256,string)[], tuple(uint256,uint256))',
'function quoteSend(tuple(uint32,bytes32,uint256,uint256,bytes,bytes,bytes), bool) view returns (tuple(uint256,uint256))',
'function send(tuple(uint32,bytes32,uint256,uint256,bytes,bytes,bytes), tuple(uint256,uint256), address) payable returns (tuple(bytes32,uint64,tuple(uint256,uint256)), tuple(uint256,uint256))',
];

async function sendT0ToHyperCore(
signer: ethers.Signer,
amount: string,
recipient: string,
refundAddress: string,
tokenAddress: string,
oftAddress: string,
decimals: number = 6
) {
const token = new ethers.Contract(tokenAddress, ['function approve(address,uint256)'], signer);
const oft = new ethers.Contract(oftAddress, OFT_ABI, signer);

const amountWei = ethers.utils.parseUnits(amount, decimals);

// Approve max amount
await token.approve(oftAddress, MAX_UINT256);

const abiCoder = new ethers.utils.AbiCoder();
const composeMsg = abiCoder.encode(['uint256 minMsgValue', 'address recipient'], ['0', recipient]);

const extraOptions = Options.newOptions();
extraOptions.addExecutorComposeOption(0, 60_000, 0);

const sendParam = [
HYPEREVM_EID,
ethers.utils.hexZeroPad(HYPERCORE_COMPOSER, 32),
amountWei,
0,
extraOptions.toHex(),
composeMsg, // composeMsg
'0x', // oftCmd
];

const [, , oftReceipt] = await oft.callStatic.quoteOFT(sendParam);
sendParam[3] = oftReceipt[1];
const msgFee = await oft.callStatic.quoteSend(sendParam, false);

const tx = await oft.send(sendParam, msgFee, refundAddress, { value: msgFee[0] });

return tx;
}

3. Security Configuration (DVNs)​

USDT0 tokens utilize a 3/3 DVN security configuration requiring verification from all three independent networks:

  • LayerZero DVN
  • USDT0 DVN (used for all USDT0 products)
  • Canary DVN (Canary Protocol)

All three DVNs must verify the payloadHash before a cross-chain message can be committed for execution. This 3/3 threshold means no single DVN compromise can affect message validity, providing stronger guarantees than a quorum-based setup.

For detailed information about LayerZero DVNs and security stacks, refer to: https://docs.layerzero.network/v2/home/modular-security/security-stack-dvns

4. Legacy Mesh (USDT0-Specific)​

The Legacy Mesh is a cross-chain liquidity network for USDT that connects legacy deployments on Ethereum, Arbitrum, Celo, Tron, and TON. It enables USDT transfers across chains without minting or burning. Instead, it uses a credit-based system where liquidity is locked and unlocked between smart contract pools on each chain.

At its core is the UsdtOFT contract, which implements LayerZero's IOFT interface for compatibility with standard OFT tooling. Unlike standard OFT contracts, UsdtOFT does not alter token supply—it moves USDT by crediting and debiting pool balances.

Key Mechanics​

  • Fee-based Transfers: A small fee (0.03% in basis points) is deducted from each transfer. The feeBps variable defines the rate.
  • Interface Support:
    • quoteOFT(): Returns transfer limits, expected amounts, and fees
    • quoteSend(): Returns LayerZero messaging fees
    • send(): Executes the cross-chain transfer and applies fees

This mechanism enables seamless interoperability across non-upgradeable USDT deployments.

warning

When an upgrade is processed, the entire Legacy Mesh infrastructure smart contracts are migrated as a whole. If your application integrates Legacy Mesh directly at the smart contract level, we recommend reaching out to integrations@usdt0.to. This ensures you have a direct communication channel and remain fully updated on the latest changes.

Learn more about the Legacy Mesh

5. IOTA (Dedicated Lockbox Route)​

USDT0 on IOTA is served by a dedicated route rather than the shared OFT Adapter. IOTA here means IOTA L1, the Move-based IOTA mainnet (LayerZero chain key iotal1), not IOTA EVM.

A separate OFT Adapter on Ethereum, the IOTA lockbox, holds USDT locked for IOTA. Its only peer is the USDT0 OFT on IOTA. On the IOTA side, every LayerZero endpoint other than Ethereum is blocked, and the USDT0 OFTs on other chains do not peer with IOTA.

warning

Only Ethereum to IOTA and IOTA to Ethereum transfers are supported. To move USDT0 from IOTA to any other chain, send it to Ethereum first, then use the main OFT Adapter.

The IOTA lockbox has a peer for EID 30423 only, so quoteSend and send to any other destination revert with NoPeer. From IOTA, quoteSend aborts for any destination other than Ethereum (EID 30101).

Route Parameters​

ParameterValue
OFT Adapter (IOTA Lockbox)0xAEf027F94008430BF4Fc27FFABB49ea6F1dd3414
Locked assetUSDT 0xdAC17F958D2ee523a2206206994597C13D831ec7
Ethereum EID30101
IOTA EID30423
IOTA OFT package0xe6a11eb6a514b5510d731e5ed9d8e9294bcaad3b4696fa5d45406d11560b5902
IOTA OFT object0x7fdb961ed464e89deca0b3885b9cbc8f269171b958b3faf3a268afb98bb9addd
IOTA OApp object0x6bd5f804a6877aa042f97a2617124bcbd4374bf7d5c9649d4bceb6d591093216
USDT0 coin type on IOTA0x25afeacdd3b0e757ae40aa4b9852261003e1dffeeb37d2c4f2904bb809807ac9::usdt0::USDT0
Shared decimals6 (USDT0 on IOTA also has 6 local decimals)
OFT fee0 bps
DVNsLayerZero Labs, USDT0, Canary (3/3 required, same as the main route)
Ethereum lockbox ownerSame Safe multisig as the main OFT Adapter
IOTA adminNative IOTA multisig 0xfb11bd61a1f27d9004d62488b324ff53c925f8a1adfe2e5892db92996d949174. Holds the AdminCap, MigrationCap, the USDT0 DenyCap and the UpgradeCaps for the OFT and coin packages

USDT0 on IOTA is a Move coin, not an ERC-20. Balances are Coin<USDT0> objects owned by the wallet address. There is no approve step on IOTA.

Example: Ethereum to IOTA​

The Ethereum side is a standard OFT Adapter, so the flow matches the Ethereum to Arbitrum example. Approve the IOTA lockbox instead of the main adapter, and use the IOTA EID. IOTA addresses are already 32 bytes, so they can be passed as to without padding.

import { ethers } from 'ethers';

const USDT_ADDRESS = '0xdAC17F958D2ee523a2206206994597C13D831ec7';
const IOTA_LOCKBOX = '0xAEf027F94008430BF4Fc27FFABB49ea6F1dd3414';
const IOTA_EID = 30423;

const OFT_ABI = [
'function quoteOFT(tuple(uint32,bytes32,uint256,uint256,bytes,bytes,bytes)) view returns (tuple(uint256,uint256), tuple(int256,string)[], tuple(uint256,uint256))',
'function quoteSend(tuple(uint32,bytes32,uint256,uint256,bytes,bytes,bytes), bool) view returns (tuple(uint256,uint256))',
'function send(tuple(uint32,bytes32,uint256,uint256,bytes,bytes,bytes), tuple(uint256,uint256), address) payable returns (tuple(bytes32,uint64,tuple(uint256,uint256)), tuple(uint256,uint256))',
];

async function sendUsdtToIota(
signer: ethers.Signer,
amount: string, // in USDT, e.g. "100.5"
iotaRecipient: string, // 0x-prefixed 32-byte IOTA address
refundAddress: string
) {
const usdt = new ethers.Contract(USDT_ADDRESS, ['function approve(address,uint256)'], signer);
const lockbox = new ethers.Contract(IOTA_LOCKBOX, OFT_ABI, signer);

const amountLd = ethers.utils.parseUnits(amount, 6);

// The lockbox pulls USDT from the sender, so it needs an allowance.
await usdt.approve(IOTA_LOCKBOX, amountLd).then(({ wait }) => wait());

const sendParam = [IOTA_EID, ethers.utils.hexZeroPad(iotaRecipient, 32), amountLd, 0, '0x', '0x', '0x'];

const [, , oftReceipt] = await lockbox.callStatic.quoteOFT(sendParam);
sendParam[3] = oftReceipt[1]; // minAmountLd = amountReceivedLd
const msgFee = await lockbox.callStatic.quoteSend(sendParam, false);

return lockbox.send(sendParam, msgFee, refundAddress, { value: msgFee[0] });
}

Example: IOTA to Ethereum​

On IOTA, transfers are built as a programmable transaction block using the LayerZero IOTA SDKs. Install @iota/iota-sdk, @layerzerolabs/lz-iotal1-sdk-v2, @layerzerolabs/lz-iotal1-oft-sdk-v2 and @layerzerolabs/lz-v2-utilities.

The steps are:

  1. Build a SendParam with the Ethereum EID and the recipient left-padded to 32 bytes.
  2. Call quoteOft to get the amount that will be received, and use it as minAmountLd.
  3. Call quoteSend to get the LayerZero messaging fee, paid in IOTA.
  4. Split the exact amount of USDT0 from the sender's coins, call send, and return the remainder to the sender.
import { IotaClient, getFullnodeUrl } from '@iota/iota-sdk/client';
import { Transaction } from '@iota/iota-sdk/transactions';
import { Ed25519Keypair } from '@iota/iota-sdk/keypairs/ed25519';
import { SDK } from '@layerzerolabs/lz-iotal1-sdk-v2';
import { OFT, type SendParam } from '@layerzerolabs/lz-iotal1-oft-sdk-v2';
import { Options } from '@layerzerolabs/lz-v2-utilities';
import { ethers } from 'ethers';

const IOTA_OFT_PACKAGE = '0xe6a11eb6a514b5510d731e5ed9d8e9294bcaad3b4696fa5d45406d11560b5902';
const IOTA_OFT_OBJECT = '0x7fdb961ed464e89deca0b3885b9cbc8f269171b958b3faf3a268afb98bb9addd';
const IOTA_OAPP_OBJECT = '0x6bd5f804a6877aa042f97a2617124bcbd4374bf7d5c9649d4bceb6d591093216';
const USDT0_COIN_TYPE = '0x25afeacdd3b0e757ae40aa4b9852261003e1dffeeb37d2c4f2904bb809807ac9::usdt0::USDT0';
const ETH_EID = 30101;

async function sendUsdt0ToEthereum(
keypair: Ed25519Keypair,
amount: string, // in USDT0, e.g. "100.5"
ethRecipient: string // 0x-prefixed 20-byte Ethereum address
) {
const client = new IotaClient({ url: getFullnodeUrl('mainnet') });
const sender = keypair.toIotaAddress();

const protocolSDK = new SDK({ client }); // defaults to IOTA L1 mainnet
const oft = new OFT(protocolSDK, IOTA_OFT_PACKAGE, IOTA_OFT_OBJECT, USDT0_COIN_TYPE, IOTA_OAPP_OBJECT);

const amountLd = ethers.utils.parseUnits(amount, 6).toBigInt();

const sendParam: SendParam = {
dstEid: ETH_EID,
to: ethers.utils.arrayify(ethers.utils.hexZeroPad(ethRecipient, 32)),
amountLd,
minAmountLd: 0n,
extraOptions: Options.newOptions().toBytes(), // empty options; enforced options apply
composeMsg: new Uint8Array(),
oftCmd: new Uint8Array(),
};

const { receipt } = await oft.quoteOft(sendParam);
sendParam.minAmountLd = receipt.amountReceivedLd;

const { nativeFee, zroFee } = await oft.quoteSend(sender, sendParam, false);

const tx = new Transaction();
const coin = await oft.splitCoinMoveCall(tx, sender, amountLd);
await oft.sendMoveCall(tx, sender, sendParam, coin, nativeFee, zroFee, sender);
tx.transferObjects([coin], tx.pure.address(sender));

return client.signAndExecuteTransaction({ transaction: tx, signer: keypair });
}

splitCoinMoveCall merges the sender's USDT0 coin objects as needed and splits off the exact amount. The send call consumes that amount, and the final transferObjects returns whatever is left in the split coin to the sender.

The example signs with a local keypair. In a browser, build the same Transaction and pass it to the connected IOTA wallet to sign and execute.

6. Deployment Addresses​

View all contract addresses for USDT0, XAUT0 and USAT across supported chains: All Deployments

7. Integration Support​

For integration assistance or questions:

8. Key Considerations​

Token Decimals​

  • USDT0: 6 decimals
  • XAUT0: 6 decimals (represents troy ounces of gold)
  • USAT: 6 decimals

Gas Considerations​

Cross-chain transfers require native gas on the source chain to pay for LayerZero messaging fees. Use quoteSend() to estimate required gas fees before executing transfers.

tip

Consider adding a buffer to the message fee returned by quoteSend() to account for potential gas price increases on the destination chain. Any excess fee will be refunded to the refundAddress specified in the send(). Make sure to correctly set the refundAddress to the desired recipient of excess fees (e.g. the sender).

Minimum Transfer Amounts​

Each USDT0 token may have minimum transfer amounts enforced at the contract level. Check quoteOFT() for transfer limits on specific routes.

Transfer Times​

Standard cross-chain transfers typically complete in 30 seconds to 3 minutes, depending on network conditions and the specific chain pair.