Skip to main content
Version: Endpoint V2

LayerZero V2 Solana OFT Native Account

caution

The Solana OFT, Endpoint, and ULN Programs are currently in Mainnet Beta!


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 LayerZero 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.

OFT Example OFT Example

Using this design pattern, LayerZero can extend any fungible token to interoperate with other chains. The Solana equivalent of this standard is the OFT Program.

tip

You should be familiar with the Solana Program Library and the Token 2022 program before continuing.

Installation

To start using the LayerZero OFT Program, you can install the OFT package to an existing project:

npm install @layerzerolabs/lz-solana-sdk-v2
yarn add @layerzerolabs/lz-solana-sdk-v2

Deploying a Solana OFT

The OFT Program interacts with the Solana Token Program to allow new or existing Fungible Tokens on Solana to transfer balances between different chains. It achieves this using Program Derived Addresses (PDAs) that are derived from the address of the OFT Program's Mint Accounts.

If you’re not familiar with Solana’s Token Program, Mint Accounts are responsible for storing the global information of a Token derived from a specific program, and Token Accounts store the relationship between a wallet and that specific Mint Account.

caution

If you have not used Solana previously, you should follow this guide and only use the Solana Token Program.

While the OFT Program is compatible with the Token2022 Program, new Solana developers should first get a strong understanding of Solana development before attempting to use any custom extensions outside the scope of this guide.

OFT Account Model

Before creating a new OFT, you should first understand the Solana Account Model for the OFT Standard on Solana.

The Solana OFT uses 5 main accounts:

Account NameExecutableDescription
OFT ProgramtrueThe OFT Program itself, the executable, stateless code which controls how OFTs interact with the LayerZero Endpoint and the SPL Token.
SPL Token ProgramtrueThe SPL Token Program itself, the executable, stateless code which controls the token interface for Solana.
SPL Mint AccountfalseEffectively a global counter for a specific token, stores data such as total supply, decimal precision, mint authority, and freeze authority.
OFT Config AccountfalseStores data about each OFT such as the underlying SPL Token Mint, the SPL Token Program, Endpoint Program, admin, and extensions which hold either an escrow account for lockbox (OFT Adapter), or the Mint Authority for the SPL Token (OFT).
Associated Token AccountfalseA program-derived account consisting of the wallet address and the SPL Token Mint.

All of your specific configurations will be controlled by the OFT Config Account, a Program Derived Address (PDA) created based on whether you initialized an OFT or OFT Adapter (continue reading to determine which makes sense to deploy):

OFT

An OFT Config initialized using the createInitNativeOftIx can be derived by:

// Find the OFT Config PDA using your SPL Token Mint keypair
const [oftConfig] = PublicKey.findProgramAddressSync(
[Buffer.from(OFT_SEED), mintKp.publicKey.toBuffer()],
oft.PROGRAM_ID,
);

createInitNativeOftIx allows you to convert your newly minted SPL Token to the OFT Standard by transferring its mint authority to the OFT Config. This OFT Config then assumes control over the token mint’s functionalities, specifically to:

  • _debit (Burning Tokens): The OFT Config can burn tokens when a user transfers out of Solana to ensure that the supply on the original chain is accurately reflected.

  • _credit (Minting Tokens): Conversely, the OFT Config can mint when tokens are being transferred into Solana, allowing the OFT Config to increase the supply to match incoming transfers from other chains.

caution

Once the SPL Token Mint Authority has been transferred, it cannot be removed from the OFT Config without upgrading the OFT Program.

OFT Adapter

An OFT Config initialized using the createInitAdapterOftIx can be derived by:

// Find the OFT Config PDA using your escrow account lockbox keypair
const [oftConfig] = PublicKey.findProgramAddressSync(
[Buffer.from(OFT_SEED), lockboxKp.publicKey.toBuffer()],
oft.PROGRAM_ID,
);

The createInitAdapterOftIx creates an OFT Config that acts as a "lockbox", enabling existing Solana Token Mints that cannot transfer the mint authority to add cross-chain functionality.

In this setup, the OFT Config does not directly mint or burn tokens but instead controls their distribution through locking and unlocking mechanisms:

  • _debit (Locking Tokens): The OFT Config can lock tokens in the escrow account. This means that while the tokens remain on the blockchain, they are not available for use by the original owner or anyone else, effectively removing them from circulation without actually burning them.

  • _credit (Unlocking Tokens): Correspondingly, the OFT Config can unlock tokens from escrow, returning them to circulation under specific conditions, typically when tokens are transferred back to Solana from another chain.

If you have already deployed an SPL Token and do not want to or cannot transfer the mint authority, you should use the Solana OFT Adapter.

danger

There can only be one OFT Adapter used in an OFT deployment. Multiple OFT Adapters break omnichain unified liquidity by effectively creating token pools. If you create OFT Adapters on multiple chains, you have no way to guarantee finality for token transfers due to the fact that the source chain has no knowledge of the destination lockbox's supply (or lack of supply). This can create race conditions where if a sent amount exceeds the available supply on the destination chain, those sent tokens will be permanently lost.

info

The createInitAdapterOftIx instruction creates an OFT Config that acts as a "lockbox", enabling existing Solana Token Mints that cannot transfer the mint authority to use the OFT Standard.

Creating a New Native OFT

SOL is the 'native gas token' of Solana. All other tokens (fungible and non-fungible tokens (NFTs)), are called Solana Program Library Tokens.

You will use the Solana Token Program to create a new fungible token on Solana, before initializing that token with the LayerZero OFT Program. You should understand how to add your own Token Metadata before deploying your SPL Token as an OFT.

To interact with each of these programs, you will use @solana/web3.js and the @layerzerolabs/lz-solana-sdk-v2:

npm i @layerzerolabs/lz-solana-sdk-v2 @solana/web3.js @solana/spl-token @solana-developers/helpers esrun
// Import necessary functions and classes from Solana SDKs
import {
Connection,
Keypair,
PublicKey,
Transaction,
sendAndConfirmTransaction,
SystemProgram,
} from '@solana/web3.js';

import {
AuthorityType,
TOKEN_PROGRAM_ID,
createInitializeMintInstruction,
createSetAuthorityInstruction,
getMintLen,
} from '@solana/spl-token';

import {OftTools, oft, OFT_SEED} from '@layerzerolabs/lz-solana-sdk-v2';

// Connect to the Solana cluster (devnet in this case)
const connection = new Connection(clusterApiUrl('testnet'));

// Load the user's keypair from the environment variables
const user = getKeypairFromEnvironment('SECRET_KEY');
console.log(
`🔑 Loaded our keypair securely from an env file! Our public key is: ${user.publicKey.toBase58()}`,
);

// Create a new keypair for your Token Mint Account
const mintKp = Keypair.generate();

// Number of local and shared decimals for the token (recommended value is 6)
const OFT_DECIMALS = 6;

// Initialize a new SPL Token Mint transaction
const minimumBalanceForMint = await connection.getMinimumBalanceForRentExemption(getMintLen([]));
let transaction = new Transaction().add(
SystemProgram.createAccount({
fromPubkey: user.publicKey,
newAccountPubkey: mintKp.publicKey,
space: getMintLen([]),
lamports: minimumBalanceForMint,
programId: TOKEN_PROGRAM_ID,
}),
await createInitializeMintInstruction(
mintKp.publicKey, // mint public key
OFT_DECIMALS, // decimals
user.publicKey, // mint authority
null, // freeze authority (not used here)
TOKEN_PROGRAM_ID, // token program id
),
);

// Send the transaction to create the mint
await sendAndConfirmTransaction(connection, transaction, [user, mintKp]);

// Find the OFT Config PDA using your mint keypair
const [oftConfig] = PublicKey.findProgramAddressSync(
[Buffer.from(OFT_SEED), mintKp.publicKey.toBuffer()],
oft.PROGRAM_ID,
);

// Create a new transaction to transfer your SPL Tokens Mint Authority
// and Initialize the OFT config
transaction = new Transaction().add(
createSetAuthorityInstruction(
mintKp.publicKey, // spl token mint public key
user.publicKey, // current mint authority
AuthorityType.MintTokens, // authority type
oftConfig, // new mint authority
[], // multisig owners (none in this case)
TOKEN_PROGRAM_ID, // token program id
),
await OftTools.createInitNativeOftIx(
user.publicKey, // payer
user.publicKey, // admin
mintKp.publicKey, // mint account
user.publicKey, // OFT Mint Authority
user.publicKey, // lzDelegate
OFT_DECIMALS, // OFT local and shared decimals
TOKEN_PROGRAM_ID, // token program to build from (spl-token or token2022)
),
);

// Send the transaction to initialize the OFT
const signature = await sendAndConfirmTransaction(connection, transaction, [user]);
const link = getExplorerLink('tx', signature, 'testnet');
console.log(`✅ OFT Initialization Complete! View the transaction here: ${link}`);

You should see:

✅ OFT Initialization Complete! View the transaction here: https://explorer.solana.com/address/HYeUCAqdsQBkqQNHRoBPov42QySDhwM7zAqiorToosbz?cluster=devnet

Make sure to keep track of your OFT Config Account, you will need it to continue configuring your OFT.

After deploying your OFT and minting any additional supply, you should set the OFT Mint Authority to null:

import {OftTools} from '@layerzerolabs/lz-solana-sdk-v2';
import {PublicKey} from '@solana/web3.js';

// Find the OFT Config PDA using your mint keypair
const [oftConfig] = PublicKey.findProgramAddressSync(
[Buffer.from(OFT_SEED), mintKp.publicKey.toBuffer()],
oft.PROGRAM_ID,
);

transaction = new Transaction().add(
await OftTools.createSetMintAuthorityIx(
user.publicKey,
oftConfig, // your oft config pda
null, // the oft program enforces that once the OFT mint authority it set to null, it cannot be reset
),
);

// Send the transaction to initialize the OFT
await sendAndConfirmTransaction(connection, transaction, [user]);

You should only mint additional tokens if this is your first canonical OFT supply (i.e., the first source of tokens across every chain). While OFT <-> OFT connections will handle additional token supplies without an issue, OFT <-> OFT Adapter connections will need to ensure that the total supply globally is equal to the lockbox supply in OFT Adapter.

caution

Once you set the OFT Mint Authority to null, you cannot change the OFT Mint Authority again. This enforces that tokens can only be minted when receiving a cross-chain transfer.

In certain edge cases, for example if a receiver's addres is blacklisted, there will be no way to recover the funds on Solana, without either upgrading the OFT Program itself, or recovering the funds on the source chain using an exposed mint method.

info

The SPL Token Mint Authority and OFT Mint Authority are separate accounts. When initializing your OFT, you transfer the SPL Token Mint Authority to your OFT Config PDA. The OFT Mint Authority controls the passthrough function mint_to, which allows the set mint authority of the OFT Config Account to create new tokens by calling the underlying SPL Token's mint method.

Adapting an Existing Token

The createInitAdapterOftIx creates an OFT Config that acts as a "lockbox", enabling existing Solana Token Mints that cannot transfer the mint authority to add cross-chain functionality.

See Solana OFT Adapter for more info on enabling cross-chain transfers for existing tokens.

After creating your oftConfig with the createInitAdapterOftIx instruction, return to this page to continue OFT setup.

Optional: Setting New Delegate

During OFT Initialization, you will set an address as a delegate. This address will have the ability to implement custom configurations such as setting DVNs, Executors, and also message debugging functions such as skipping inbound packets.

By default, the contract owner should be set as the delegate, but if you want to set a new address, you can use createSetDelegateIx.

Setting Trusted Peers

Now that you have deployed your Solana token, you will need to connect the OFT Instance to your other chains.

tip

This guide assumes you already have deployed other OFT Instances on your desired EVM or other non-EVM chains. If you have not deployed any other OFT contracts yet, see the OFT Quickstart in the EVM section.


While LayerZero provides default configuration settings for most pathways, you should only connect your OFT Instances on different chains after viewing your DVN and Executor Configuration Settings.

Once you've finished configuring your OFT, you can connect your OFT deployment to different chains by using the createSetPeerIx.

The function takes 2 arguments: dstEid, the endpoint ID for the destination chain that the other OFT contract lives on, and peer, the destination OFT's contract address in bytes32 format.

Create an empty file called set-peer.ts. After loading keypairs, and your OFT Config, you will call createSetPeerIx for each pairwise connection.

See the current list of Deployed Endpoint IDs in the EVM section.

// Add the createSetPeer instruction import from the @layerzerolabs/solana-sdk
import {OftTools} from '@layerzerolabs/lz-solana-sdk-v2';

// Optional: Add the LayerZero utilities for converting addressToBytes32
import {addressToBytes32} from '@layerzerolabs/lz-v2-utilities';

// Before setting the peer, we need to convert the EVM peer addresses to bytes32.
// To do this, we left zero-pad the address until it fills a bytes32 value.

// Replace with your dstEid's and peerAddresses
// Before setting the peer, we need to convert the EVM peer addresses to bytes32
const peers = [
{dstEid: 30101, peerAddress: addressToBytes32('0x0000000000000000000000000000000000000001')},
{dstEid: 30102, peerAddress: addressToBytes32('0x0000000000000000000000000000000000000002')},
// ...
];

for (const peer of peers) {
const peerTransaction = new Transaction().add(
await OftTools.createSetPeerIx(
user.publicKey, // admin
oftConfig, // oft config account
peer.dstEid, // destination endpoint id
peer.peerAddress, // peer address
),
);

const peerSignature = await sendAndConfirmTransaction(connection, peerTransaction, [user]);
const link = getExplorerLink('tx', peerSignature, 'testnet');
console.log(
`✅ You set ${await getPeerAddress(connection, oftConfig, peer.dstEid)} for dstEid ${
peer.dstEid
}! View the transaction here: ${link}`,
);
}

Run the script using npx esrun set-peer.ts.

When this OFT receives messages, the LayerZero Endpoint will compare the OFT's set peer against the inbound message's sender, and reject the message if the sender's address does not match the peer.

caution

setPeer opens your OFT to start receiving messages from the address set, meaning you should configure any application settings you intend on changing prior to calling setPeer.


danger

OFTs need setPeer to be called correctly on both Chain A and Chain B to send and receive 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.


Message Execution Options

_options are a generated bytes array with specific instructions for the DVNs and Executor to when handling cross-chain messages.

You can find how to generate all the available _options in Message Execution Options, but for this tutorial you should focus primarily on using @layerzerolabs/lz-v2-utilities, specifically the Options class.

Setting Enforced Options Outbound to EVM

An enforced options sets a minimum amount of gas that will always be charged to users calling send, and delivered to a destination chain per message. In the case of OFT, you mostly want to make sure you set enough gas_limit for delivering the message and transferring the ERC20 token to the receiver, plus any additional logic.

A typical OFT's lzReceive call and mint 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 on destination:

Options.newOptions().addExecutorLzReceiveOption(gas_limit, msg.value);
// Add the createSetEnforcedOptionsIx to your imports
import {OftTools} from '@layerzerolabs/lz-solana-sdk-v2';

// Optional: Add the LayerZero utilities for building options
import {Options} from '@layerzerolabs/lz-v2-utilities';

// ... OFT mint initialization

// You can reuse your peers mapping to set options for each dstEid
const peers = [
{dstEid: 30101, peerAddress: addressToBytes32('0x0000000000000000000000000000000000000001')},
{dstEid: 30102, peerAddress: addressToBytes32('0x0000000000000000000000000000000000000002')},
// ...
];

// In this example you will set a static execution gas _option for every chain.
// In practice, you will likely want to profile each of your gas settings on the destination chain,
// and set a custom gas amount depending on the unique gas needs of each destination chain.
for (const peer of peers) {
const optionTransaction = new Transaction().add(
await OftTools.createSetEnforcedOptionsIx(
user.publicKey, // your admin address
oftConfig, // your OFT Config
peer.dstEid, // destination endpoint id for the options to apply to
Options.newOptions().addExecutorLzReceiveOption(65000, 0).toBytes(), // send options
Options.newOptions()
.addExecutorLzReceiveOption(65000, 0)
.addExecutorComposeOption(0, 50000, 0)
.toBytes(), // sendAndCall options
),
);

// Send the setEnforcedOptions transaction
const optionSignature = await sendAndConfirmTransaction(connection, optionTransaction, [user]);
const link = getExplorerLink('tx', optionSignature, 'testnet');
console.log(`✅ You set options for dstEid ${peer.dstEid}! View the transaction here: ${link}`);
}

You can see all the available options settings in Solana Execution Options.

caution

Your enforcedOptions will always be charged to a user when calling send. Any extraOptions passed in the send call will be charged on top of the enforced settings.

Passing identical _options in both enforcedOptions and extraOptions will charge the caller twice on the source chain, because LayerZero interprets duplicate _options as two separate requests for gas.

tip

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 Inbound to Solana

On your equivalent EVM implementation of the OFT, you must send at minimum 0.0015 SOL (1500000 lamports) in your lzReceiveOption when sending to Solana.

Options.newOptions().addExecutorLzReceiveOption(compute_units, lamports);
caution

Unlike EVM addresses, every Solana Account requires a minimum balance of the native gas token to exist rent free. To send tokens to Solana, you will need a minimum amount of lamports to execute and initialize the account within the transaction.

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.

info

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 quoteWithUln function to return an estimate from the Endpoint contract to use as a recommended amount of gas needed to call send.

// Add quoteWithUln to your imports
import {OftTools} from '@layerzerolabs/lz-solana-sdk-v2';

// Optional: Add the LayerZero utilities for converting addressToBytes32
import {addressToBytes32} from '@layerzerolabs/lz-v2-utilities';

// Define your receiver address (typically bytes20 for evm addresses)
const receiver = addressToBytes32('0x0000000000000000000000000000000000000001');

// As an example you can log the LayerZero quote to see if your params are correct
console.log(
await OftTools.quoteWithUln(
connection,
user.publicKey, // the payer's address
mintKp.publicKey, // your token mint account
30101, // the dstEid
100n, // the amount of tokens to send
100n, // the minimum amount of tokens to send (for slippage)
Options.newOptions().addExecutorLzReceiveOption(0, 0).toBytes(), // any extra execution options to add on top of enforced
receiver, // the receiver's address in bytes32
),
);

You will use this generated quote as the cost of the send transaction.

Besides the params listed above, there are also 3 optional params for quoteWithUln that may be relevant to your use case:

ParameterTypeDescription
tokenEscrowPublicKeyThe token escrow account for the OFT Adapter implementation. Required for quoting an OFT Adapter transfer.
payInZROboolWhether to pay the Endpoint in SOL or the LayerZero gas token. Defaults to false (i.e., pay in SOL).
composeMsgbytesThe compose message encoded.

See the OFT Adapter section for generating an OFT Adapter instance's quote and calling send.

Calling send

Since the send logic has already been defined, we'll instead view how the function should be called.

// Add the quoteWithUln and createSendWithUlnIx to your imports
import {OftTools, OFT_SEED} from '@layerzerolabs/lz-solana-sdk-v2';
import {Options, addressToBytes32} from '@layerzerolabs/lz-v2-utilities';

// Replace with your dstEid's and peerAddresses
const peer = {
dstEid: 30101,
peerAddress: addressToBytes32('0x0000000000000000000000000000000000000001'),
};
// ...

const associatedTokenAccount = (
await getOrCreateAssociatedTokenAccount(
connection,
user,
mintKp.publicKey,
user.publicKey,
false,
'confirmed',
)
).address;

// Define your receiver's address (typically bytes20 for evm addresses)
const receiver = addressToBytes32('0x0000000000000000000000000000000000000001');

// Define your amount to send in big number format
const amountToSend = 100n;

// Generate the send quote using your sendParams
const fee = await OftTools.quoteWithUln(
connection, // your connection
user.publicKey, // payer address
mintKp.publicKey, // token mint address
peer.dstEid, // destination endpoint id
amountToSend, // amount of tokens to send
amountToSend, // minimum amount of tokens to send (for slippage)
Options.newOptions().addExecutorLzReceiveOption(0, 0).toBytes(), // extra options to send
receiver, // receiver address
);

// Create the send transaction
const sendTransaction = new Transaction().add(
await OftTools.createSendWithUlnIx(
connection, // your connection
user.publicKey, // payer address
mintKp.publicKey, // token mint address
associatedTokenAccount, // associated token address
peer.dstEid, // destination endpoint id
amountToSend, // amount of tokens to send
amountToSend, // minimum amount of tokens to send (for slippage)
Options.newOptions().addExecutorLzReceiveOption(0, 0).toBytes(), // extra options to send
receiver, // receiver address
fee.nativeFee, // native fee to pay (using quote)
),
);

// Send and confirm the send transaction
const sendSignature = await sendAndConfirmTransaction(connection, sendTransaction, [user]);
const link = getExplorerLink('tx', sendSignature, 'testnet');
console.log(
`✅ You sent ${amountToSend} to dstEid ${peer.dstEid}! View the transaction here: ${link}`,
);

Similar to the quote params, the send instruction also has multiple optional params for the createSendWithUlnIx:

ParameterTypeDescription
tokenEscrowPublicKeyThe token escrow account for the OFT Adapter implementation. Required for quoting an OFT Adapter transfer.
payInZROboolWhether to pay the Endpoint in SOL or the LayerZero gas token. Defaults to false (i.e., pay in SOL).
composeMsgbytesThe compose message encoded.

See the OFT Adapter section for generating an OFT Adapter instance's quote and calling send.

_lzReceive tokens

A successful send call will be delivered to the destination chain, invoking the provided _lzReceive method during execution on the destination chain, for example an EVM equivalent:

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;
}

Additional Information

Token Supply Cap

When transferring tokens across different blockchain VMs, each chain may have a different level of decimal precision for the smallest unit of a token.

While EVM chains support uint256 for token balances, Solana uses 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.

info

If your token's supply needs to exceed this limit, you'll need to override the shared decimals value.

Optional: Overriding sharedDecimals

This shared decimal precision is essentially the maximum number of decimal places that can be reliably represented and handled across different blockchain VMs when transferring tokens.

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
const OFT_DECIMALS = 6;

To modify this default, simply change the OFT_DECIMALS to another value during deployment.

caution

Shared decimals also control how token transfer precision is calculated.

Token Transfer Precision

The OFT Standard also handles differences in decimal precision before every cross-chain transfer by "cleaning" the amount from any decimal precision that cannot be represented in the shared system.

The OFT Standard defines these small token transfer amounts as "dust".

Example

ERC20 OFTs use a local decimal value of 18 (the norm for ERC20 tokens), and a shared decimal value of 6 (the norm for Solana tokens).

decimalConversionRate = 10^(localDecimals − sharedDecimals) = 10^(18−6) = 10^12

This means the conversion rate is 10^12, which indicates the smallest unit that can be transferred is 10^-12 in terms of the token's local decimals.

For example, if you send a value of 1234567890123456789 (a token amount with 18 decimals), the OFT Standard will:

  1. Divides by decimalConversionRate:
1234567890123456789 / 10^12 = 1234567.890123456789 = 1234567
tip

Remember that solidity performs integer arithmetic. This means when you divide two integers, the result is also an integer with the fractional part discarded.


  1. Multiplies by decimalConversionRate:
1234567 * 10^12 = 1234567000000000000

This process removes the last 12 digits from the original amount, effectively "cleaning" the amount from any "dust" that cannot be represented in a system with 6 decimal places.

Adding Send and Receive Logic

In Solana, the concept of function overrides as commonly understood in object-oriented languages like Solidity does not directly apply. Because of this, to change or add any custom business logic to the token, you will need to deploy your own variant of the OFT Program.

For more information, visit the OFT Program Library.