End-to-end instruction on how to deploy a Solana OFT can be found in the README at https://github.com/LayerZero-Labs/devtools/tree/main/examples/oft-solana, which will be the README of your project when you setup using the LayerZero CLI.
Quickstart
Example
For the step-by-step instructions on how to build, deploy and wire a Solana OFT, view the Solana OFT example.Scaffold
Spin up a new OFT workspace (based on the example) in seconds:OFT (Solana) and proceed with the installation.
Follow the provided README instructions to make your first crosschain OFT transfer between Solana and an EVM chain.
The rest of this page contains additional information that you should read before deploying to mainnet.
Prerequisite Knowledge
Understanding the following will help you with the rest of this page:- Mint Authority and Freeze Authority
- Token Metadata
- Solana Account Model
- Solana Program Library and the Token-2022
The OFT Program
The OFT Program interacts with the Solana Token Program to allow new or existing Fungible Tokens on Solana to transfer balances between different chains.Solana now has two token programs. The original Token Program (commonly referred to as ‘SPL token’) and the newer Token-2022 program.
If using the same repo, you will need to rename the existing
deployments/solana-<CLUSTER_NAME>/OFT.json as it will be overwritten otherwise. You will also need to either rename the existing layerzero.config.ts or use a different config file for the subsequent OFTs.OFT Account Model
Before creating a new OFT, you should first understand the Solana Account Model which is used for the OFT Standard on Solana. The Solana OFT Standard uses 6 main accounts:| Account Name | Executable | Description |
|---|---|---|
| OFT Program | true | The OFT Program itself, the executable, stateless code which controls how OFTs interact with the LayerZero Endpoint and the SPL Token. |
| Mint Account | false | This is the Mint Account for the OFT’s SPL Token. Stores the key metadata for a specific token, such as total supply, decimal precision, mint authority, freeze authority and update authority. |
| Mint Authority Multisig | false | A 1 of N Multisig that serves as the Mint Authority for the SPL Token. The OFT Store is always required as a signer. It’s also possible to add additional signers. |
| Escrow | false | The Token Account for the corresponding Mint Account, owned by the OFT Store. For OFT Adapter deployments and also for storing fees, if fees are enabled. For both OFT and OFT Adapter, the Escrow address is part of the derivation for the OFT Store PDA. Escrow is a regular Token Account and not an Associated Token Account. |
| OFT Store | false | A PDA account that stores data about each OFT such as the underlying SPL Token Mint, the SPL Token Program, Endpoint Program, the OFT’s fee structure, and extensions. Is the owner for the Escrow account. The OFT Store is a signer for the Mint Authority multisig. |
| PeerConfig | false | A PDA account that stores configuration for each remote chain, including peer addresses, enforced options, rate limiters, and fee settings. This account is derived from the OFT Store and remote EID. |
The SPL Token Program handles all creation and management of SPL tokens on the Solana blockchain. An OFT’s deployment interacts with this program to create the Mint Account.
Message Execution Options
_options are a generated bytes array with specific instructions for the DVNs and Executor when handling crosschain messages.
Note that you must have at least either enforcedOptions set for your OApp or extraOptions passed in for a particular transaction. If both are absent, the transaction will fail. For sends from EVM chains, quoteSend() will revert. For sends from Solana, you will see a ZeroLzReceiveGasProvided error.
If you had set enforcedOptions, then you can pass an empty bytes array (0x if sending from EVM, Buffer.from('') if sending from Solana).
If you did not set enforcedOptions, then continue reading.
Setting Extra Options
Any_options passed in the send call itself is considered as _extraOptions.
_extraOptions can specify additional handling within the same message type. These _options will then be combined with enforcedOption if set.
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.
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.Setting Options Inbound to EVM chains
A typical OFT’slzReceive 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.
To pass in extraOptions for Solana to EVM (Sepolia, in our example) transactions, modify tasks/solana/sendOFT.ts
Refer to the sample code diff below:
Setting Options Inbound to Solana
When sending to Solana, amsg.value is only required if the recipient address does not already have the Associated Token Account (ATA) for your mint. There are two ways to provide this value:
- Enforced Options (app-level default): set
valueinenforcedOptionsfor the pathway. This guarantees the amount is always included, but it will waste lamports for recipients that already have an ATA. Use with caution in production. - Extra Options (per-transaction): set
msg.valueinextraOptionsonly when needed after checking whether the recipient’s ATA exists. This is the recommended approach to avoid unnecessary costs.
value to provide:
- SPL Token accounts: the rent-exempt amount is
2_039_280lamports (0.00203928 SOL). - Token-2022 accounts: the required value depends on the token account size, which varies by the enabled extensions. You can inspect the size of your token’s token account and calculate the rent amount needed.
enforcedOptions in layerzero.config.ts, the parameter is value. If building per-transaction options in TypeScript, it is the second parameter to addExecutorLzReceiveOption(gas_limit, msg_value).
See the next section for how to detect ATA existence and attach msg.value conditionally via extraOptions.
For Solana OFTs that use Token2022, you will need to increase
value to a higher amount, which depends on the token account size, which in turn depends on the extensions that you enable.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 when the recipient’s ATA does not already exist.
enforcedOptions. When attaching per-transaction value for ATA creation via extraOptions, set gas to 0 and only provide msg.value as needed. The protocol combines extraOptions with your enforced baseline at execution time.
Conditional msg.value for ATA creation
For sends to Solana, you can avoid overpaying rent by setting your enforced optionsvalue to 0 and supplying msg.value only when the recipient’s Associated Token Account (ATA) is missing. This pattern is useful when recipients may or may not have an ATA for your mint.
Steps:
- Set
enforcedOptionsvalue to 0 inlayerzero.config.tsfor pathways that deliver to Solana. - Before constructing
extraOptionsfor a specific send to Solana, check if the recipient’s ATA exists. - If ATA exists: set
msg.value = 0inaddExecutorLzReceiveOption. - If ATA does not exist: set
msg.valueto the rent-exempt minimum for the token account (e.g.,2_039_280lamports for SPL; Token-2022 may require more depending on enabled extensions).
options.toHex() (EVM) or options.toBytes() (Solana) when populating extraOptions/options in your send call. These values will be combined with any enforcedOptions configured at the app level. If your mint is Token-2022, compute the rent-exempt minimum from the token account size (varies by enabled extensions) and replace SPL_TOKEN_ACCOUNT_RENT_VALUE accordingly.
Precautions
One OFT Adapter per OFT deployment/mesh
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 pool’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.Token Transfer Precision
The OFT Standard also handles differences in decimal precision before every crosschain 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 of18 (the norm for ERC20 tokens), and a shared decimal value of 6 (the norm for Solana tokens).
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:
- Divides by
decimalConversionRate:
- Multiplies by
decimalConversionRate:
Choosing the right local decimals value
Be careful when selecting your Solana token’s local decimals. Although the default is9 (SPL standard), choosing a value that is too high can severely limit your maximum mintable supply because Solana balances are u64. For instance, setting 18 decimals (common on EVM) would cap your supply to roughly ~18 whole tokens on Solana. Prefer the smallest decimals that satisfy your UX and supply requirements (many projects use 6 or 9). This is independent from sharedDecimals (default 6), which governs crosschain precision and dust handling.
See the detailed guidance and max-supply table in Deciding the number of local decimals for your Solana OFT.
(Optional) Verify the OFT Program
To continue, you must first install solana-verify. You can learn about how program verification works in the official Solana program verification guide.The commands given below assume that you did not make any modifications to the Solana OFT program source code. If you did, you can refer to the instructions in solana-verify directly.
declare_id! is embedded in the bytecode, altering its hash. We solve this by having you supply the program ID as an environment variable during build time. This variable is then read by the program_id_from_env function in the OFT program’s lib.rs snippet.
Below is the relevant code snippet:
OFT_ID as an environment variable when running solana-verify, which is demonstrated in the following sections.
Compare locally
If you wish to, you can view the program hash of the locally built OFT program:Verify against a repository and submit verification data onchain
Run the following command to verify against the repo that contains the program source code:
You can also pass in --commit-hash <SHA> to pin the verification to a commit
The above instruction runs against the Solana Devnet as it uses the -ud flag. To run it against Solana Mainnet, replace -ud with -um.
Submit verification data when the Upgrade Authority is your local keypair
Upon successful verification, you will be prompted with the following:y to proceed with uploading of the program verification data onchain.
Submit verification data when the Upgrade Authority is a Multisig
The steps are similar to the above, except you will not be able to submit a valid verification PDA when prompted since the Upgrade Authority is not your local keypair. Instead, run the following, after having runverify-from-repo:
- Ensure your
solana-verifyversion is at minimum0.4.0to be able to useexport-pda-tx - You can also pass in
--commit-hash <SHA>to pin the verification to a commit
export-pda-tx will return a base58 string that represents the transaction data for uploading the verification PDA. Import this into Squads for approval and execution.
(mainnet only) Submit to the OtterSec API
This will provide your program with theVerified status on explorers. Note that currently the Verified status only exists on mainnet explorers.
Verify against the code in the git repo and submit for verification status:
You must run the above step using the same keypair as the program’s upgrade authority. Learn more about the solana-verify CLI from the official repo.
Renouncing the OFT Program’s Upgrade Authority
If you intend on renouncing the Upgrade Authority of your OFT program, we recommend that you go through with program verification first. After renouncing, it will no longer be possible to verify your program as it requires submitting the verification PDA using the Upgrade Authority address. A program that has been verified will remain verified if its onchain hash does not change, even if its Upgrade Authority has been renounced.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 supportuint256 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.
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.
OFT_DECIMALS to another value during deployment.
Troubleshooting
DeclaredProgramIdMismatch
Full error:AnchorError occurred. Error Code: DeclaredProgramIdMismatch. Error Number: 4100. Error Message: The declared program id does not match the actual program id.
Fixing this error requires upgrading the deployed program.
This error occurs when the program is built with a declare_id! value that does not match its onchain program ID. The program ID onchain is determined by the original program keypair used when deploying (created by solana-keygen new -o target/deploy/endpoint-keypair.json --force).
To debug, check the following:
the following section in Anchor.toml:
anchor keys list:
oft values match your OFT program’s onchain ID.
If they already do, and you are still encountering DeclaredProgramIdMismatch, this means that you ran the build command with the wrong program ID, causing the declared program ID onchain to mismatch.
To fix this, you can re-run the build command, ensuring you pass in the OFT_ID env var:
- the existing program starts off as being unaffected
- the updated program’s bytecode is uploaded to a buffer account (new account, hence SOL for rent is required) which acts as a temporary staging area
- the contents of the buffer account are then copied to the program data account
- the buffer account is closed, and its rent SOL is returned
To deploy to Solana Mainnet, replace
-u devnet with -u mainnet-beta.Retrying Failed Transactions
If a transaction fails, it may be due to network congestion or other temporary issues. You can retry the transaction by resubmitting it. Ensure that you have enough SOL in your account to cover the transaction fees.Recovering Failed Rent
For more troubleshooting help, refer to the Solana OFT README.
Building without Docker
Our default instructions ask you to build in verifiable mode:-v flag instructs anchor to build in verifiable mode. We highly recommend you to build in verifiable mode, so that you can carry out program verification.
Verifiable mode requires Docker. If you cannot build using Docker, then the alternative is to build in regular mode, which results in slight differences in commands for two steps: build and deploy.
For building:
target/verifiable/oft.so. In regular mode, the output defaults to target/deploy/oft.so.
For deploying:
Known Limitations
Max number of DVNs
Given Solana’s transaction size limit of 1232 bytes, the current max number of DVNs for a pathway involving Solana is 5.Token Extensions
While it is possible to create a Solana OFT using the Token2022 (Token Extensions) there is limited compatibility with token extensions. It is advised for you to conduct an end-to-end test if you require token extensions for your Solana OFT. For the Transfer Hook extension, regular Solana OFTs may work with this extension only when OFT Fees are set to 0. By default, all OFTs have fees set to 0. As long as you do not update OFT fees to a non-zero value, your Solana OFT will work with the Transfer Hook extension. OFT Adapters do not currently support the Transfer Hook extension. For every extension that you wish to enable, you should check for its support by the protocols that you wish to integrate with.Cross Program Invocation into the OFT Program (CPI Depth limitation)
Solana has the max CPI Depth of 4. A Solana OFT send instruction has the following CPI trace:OFT.send call, it would not be possible to have it be done in the same instruction. However, since Solana allows for multiple instructions per transaction, you can instead have it be grouped into the same transaction as the OFT.send instruction.
For example, if you have a project that involves staking OFTs crosschain, and when unstaking (let’s refer to this instruction as StakingProgram.unstake), you want to allow for the OFT to be sent (via OFT.send) to another chain in the same transaction, then you can do the following:
- prepare the
StakingProgram.unstakeinstruction - prepare the
OFT.sendinstruction - submit both instructions in one transaction