Template project for a cross-chain token (OFT) powered by the LayerZero protocol. This example primarily involves Solana and EVM. There are also additional instructions for wiring to Aptos.
- Prerequisite Knowledge
- Requirements
- Scaffold this example
- Helper Tasks
- Setup
- Build
- Deploy
- Enable Messaging
- Sending OFT
- Next Steps
- Production Deployment Checklist
- Appendix
- Rust
v1.75.0 - Anchor
v0.29 - Solana CLI
v1.17.31 - Docker
28.3.0 - Node.js
>=18.16.0 pnpm(recommended) - or another package manager of your choice (npm, yarn)forge(optional) ->=0.2.0for testing, and if not using Hardhat for compilation
Create your local copy of this example:
LZ_ENABLE_SOLANA_OFT_EXAMPLE=1 pnpm dlx create-lz-oapp@latestSpecify the directory, select OFT (Solana) and proceed with the installation.
Note that create-lz-oapp will also automatically run the dependencies install step for you.
Throughout this walkthrough, helper tasks will be used. For the full list of available helper tasks, refer to the LayerZero Hardhat Helper Tasks section. All commands can be run at the project root.
0.29 and solana version 1.17.31 specifically to compile the build artifacts. Using higher Anchor and Solana versions can introduce unexpected issues during compilation. After compiling the correct build artifacts, you can change the Solana version to higher versions.
Docker
Docker is required to build using anchor. We highly recommend that you use the most up-to-date Docker version to avoid any issues with anchor builds.
Install Rust
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -yInstall Solana 1.17.31
sh -c "$(curl -sSfL https://release.anza.xyz/v1.17.31/install)"Install Anchor 0.29
cargo install --git https://github.com/coral-xyz/anchor --tag v0.29.0 anchor-cli --locked-
Copy
.env.exampleinto a new.env -
Solana Deployer:
- To set up your Solana deployer, you have 3 options:
- Use the keypair at the default path of
~/.config/solana/id.json. For this, no action is needed. - In the
.env, setSOLANA_PRIVATE_KEY- this can be either in base58 string format (i.e. when imported from a wallet) or the Uint8 Array in string format (all in one line, e.g.[1,1,...1]). - In the
.env, setSOLANA_KEYPAIR_PATH- the location to the keypair file that you want to use.
- Use the keypair at the default path of
- Fund your Solana deployer address
- Run:
solana airdrop 5 -u devnet - We recommend that you request 5 devnet SOL, which should be sufficient for this walkthrough. For the example here, we will deploy to Solana Devnet.
- If you hit rate limits with the above
airdropcommand, you can also use the official Solana faucet.
- Run:
- To set up your Solana deployer, you have 3 options:
-
Solana RPC
- Also set the
RPC_URL_SOLANA_TESTNETvalue. Note that while the naming used here isTESTNET, it refers to the Solana Devnet. We useTESTNETto keep it consistent with the existing EVM testnets.
- Also set the
-
EVM Deployer:
-
Set up your EVM deployer address/account via the
.env -
You can specify either
MNEMONICorPRIVATE_KEY:MNEMONIC="test test test test test test test test test test test junk" or... PRIVATE_KEY="0xabc...def" -
Fund your EVM deployer address with the native tokens of the chains you want to deploy to. This example by default will deploy to the following EVM testnet: Arbitrum Sepolia.
-
Create the OFT programId keypair by running:
anchor keys sync -p oftView the program ID's based on the generated keypairs:
anchor keys list
You will see an output such as:
endpoint: H3SKp4cL5rpzJDntDa2umKE9AHkGiyss1W8BNDndhHWp
oft: DLZdefiak8Ur82eWp3Fii59RiCRZn3SjNCmweCdhf1DDCopy the oft program ID value for use in the build step later.
Ensure you have Docker running before running the build command.
anchor build -v -e OFT_ID=<OFT_PROGRAM_ID>Where <OFT_PROGRAM_ID> is replaced with your OFT Program ID copied from the previous step.
Preview Rent Costs for the Solana OFT
ℹ️ The majority of the SOL required to deploy your program will be for rent (specifically, for the minimum balance of SOL required for rent-exemption), which is calculated based on the amount of bytes the program or account uses. Programs typically require more rent than PDAs as more bytes are required to store the program's executable code.
In our case, the OFT Program's rent accounts for roughly 99% of the SOL needed during deployment, while the other accounts' rent, OFT Store, Mint, Mint Authority Multisig and Escrow make up for only a fraction of the SOL needed.
You can preview how much SOL would be needed for the program account. Note that the total SOL required would to be slightly higher than just this, to account for the other accounts that need to be created.
solana rent $(wc -c < target/verifiable/oft.so)You should see an output such as
Rent-exempt minimum: 3.87415872 SOLℹ️ LayerZero's default deployment path for Solana OFTs require you to deploy your own OFT program as this means you own the Upgrade Authority and don't rely on LayerZero to manage that authority for you. Read this to understand more on why this is important.
To deploy a Solana OFT, you need to both deploy an OFT Program and also create the OFT Store, alongside the other configuration steps that are handled by the provided tasks. To understand the relationship between the OFT Program and the OFT Store, read the section 'The OFT Program' on the LayerZero docs.
While for building, we must use Solana v1.17.31, for deploying, we will be using v1.18.26 as it provides an improved program deployment experience (i.e. ability to attach priority fees and also exact-sized on-chain program length which prevents needing to provide 2x the rent as in v1.17.31).
First, we switch to Solana v1.18.26 (remember to switch back to v1.17.31 later)
sh -c "$(curl -sSfL https://release.anza.xyz/v1.18.26/install)"The deploy command will run with a priority fee. Read the section on 'Deploying Solana programs with a priority fee' to learn more.
solana program deploy --program-id target/deploy/oft-keypair.json target/verifiable/oft.so -u devnet --with-compute-unit-price <COMPUTE_UNIT_PRICE_IN_MICRO_LAMPORTS>ℹ️ the -u flag specifies the RPC URL that should be used. The options are mainnet-beta, devnet, testnet, localhost, which also have their respective shorthands: -um, -ud, -ut, -ul
1.17.31 and Anchor version 0.29.0
sh -c "$(curl -sSfL https://release.anza.xyz/v1.17.31/install)"pnpm hardhat lz:oft:solana:create --eid 40168 --program-id <PROGRAM_ID> --only-oft-store true --amount 100000000000The above command will create a Solana OFT which will have only the OFT Store as the Mint Authority and will also mint 100 OFT (given the default 9 decimals on Solana, this would be 100_000_000_000 in raw amount).
For an elaboration on the command params for this command to create an Solana OFT, refer to the section Create Solana OFT
pnpm hardhat lz:deploy # follow the promptsRun the following command to initialize the SendConfig and ReceiveConfig Accounts. This step is unique to pathways that involve Solana.
npx hardhat lz:oft:solana:init-config --oapp-config layerzero.config.tsThe OFT standard builds on top of the OApp standard, which enables generic message-passing between chains. After deploying the OFT on the respective chains, you enable messaging by running the wiring task.
ℹ️ This example uses the Simple Config Generator, which is recommended over manual configuration.
Run the wiring task:
pnpm hardhat lz:oapp:wire --oapp-config layerzero.config.tsSend From 1 OFT from Solana Devnet to Arbitrum Sepolia
npx hardhat lz:oft:send --src-eid 40168 --dst-eid 40231 --to <EVM_ADDRESS> --amount 1ℹ️
40168and40231are the Endpoint IDs of Solana Devnet and Arbitrum Sepolia respectively. View the list of chains and their Endpoint IDs on the Deployed Endpoints page.
Send 1 OFT From Arbitrum Sepolia to Solana Devnet
npx hardhat lz:oft:send --src-eid 40231 --dst-eid 40168 --to <SOLANA_ADDRESS> --amount 1Upon a successful send, the script will provide you with the link to the message on LayerZero Scan.
Once the message is delivered, you will be able to click on the destination transaction hash to verify that the OFT was sent.
Congratulations, you have now sent an OFT between Solana and Arbitrum!
If you run into any issues, refer to Troubleshooting.
After successfully deploying your OFT, consider the following steps:
- Review the Choosing between OFT, OFT Adapter and OFT Mint-and-Burn-Adapter section
- Review the Production Deployment Checklist before going to mainnet
- Learn about Security Stack
- Understand Message Execution Options
- Wiring Solana to Aptos - for Wiring Solana to Aptos please refer to the instructions in docs/wiring-to-aptos.md.
This section explains the three different options available for creating OFTs on Solana and when to use each one.
Do you have an existing Solana token (SPL or Token2022)?
│
┌───────────────────────────┴───────────────────────────┐
│ │
NO YES
│ │
✅ Use OFT (Preferred) Can you transfer the
• Creates a new token Mint Authority to OFT
• Uses burn and mint mechanism Store or new SPL Multisig?
│
┌────────────┴────────────┐
│ │
YES NO/WON'T
│ │
✅ Use OFT MABA (Mint-And-Burn Adapter) ⚠️ Use OFT Adapter (Last Resort)
• Uses existing token • Uses existing token
• Transfers Mint Authority • Keeps existing Mint Authority
to OFT Store/Multisig • Uses lock and unlock mechanism
• Uses burn and mint mechanism
- Mechanism: Burn and mint
- Token: Create new as part of the create task
- Note: Preferred option when you don't have an existing token
pnpm hardhat lz:oft:solana:create --eid 40168 --program-id <PROGRAM_ID> --only-oft-store true --amount 100000000000- Mechanism: Lock and unlock
- Token: Use existing
- Note:
⚠️ Last resort option when you can't or won't transfer Mint Authority of existing token
pnpm hardhat lz:oft-adapter:solana:create --eid 40168 --program-id <PROGRAM_ID> --mint <TOKEN_MINT> --token-program <TOKEN_PROGRAM_ID>- Mechanism: Burn and mint
- Token: Use existing
- Note:
⚠️ Requires transferring Mint Authority to OFT Store or new SPL Multisig. Cannot use if Mint Authority has been renounced.
pnpm hardhat lz:oft:solana:create --eid 40168 --program-id <PROGRAM_ID> --mint <TOKEN_MINT> --token-program <TOKEN_PROGRAM_ID>lz_receive to work. If you used --additional-minters, transfer to the newly created multisig address. Otherwise, set it to the OFT Store address.
Before deploying, ensure the following:
- (required) you are not using
MyOFTMock, which has a publicmintfunction- In
layerzero.config.ts, ensure you are not usingMyOFTMockas thecontractNamefor any of the contract objects.
- In
- (recommended) you have profiled the gas usage of
lzReceiveon your destination chains
pnpm testTo add additional chains to your OFT deployment:
- If EVM, add the new chain configuration to your
hardhat.config.ts - Deploy the OFT contract on the new chain
- Update your
layerzero.config.tsto include the new chain - Run
init-configfor the new pathway (if it involves Solana) - Run the wiring task
For production deployments, consider using multisig wallets:
- Solana: Use Squads multisig with the
--multisig-keyflag - EVM chains: Use Safe or similar multisig solutions
If your Solana OFT's delegate/owner is a Squads multisig, you can simply append the --multisig-key flag to the end of tasks such as the wire task:
pnpm hardhat lz:oapp:wire --oapp-config layerzero.config.ts --multisig-key <SQUADS_MULTISIG_ACCOUNT>If you are not happy with the deployer being a mint authority, you can create and set a new mint authority by running:
pnpm hardhat lz:oft:solana:setauthority --eid <SOLANA_EID> --mint <TOKEN_MINT> --program-id <PROGRAM_ID> --escrow <ESCROW> --additional-minters <MINTERS_CSV>The OFTStore is automatically added as a mint authority to the newly created mint authority, and does not need to be
included in the --additional-minters list.
This example includes various helper tasks. For a complete list, run:
npx hardhat --helplz:oft:solana:create
-
--eid(EndpointId)
Solana mainnet (30168) or testnet (40168) -
--program-id(string)
The OFT Program ID
-
--amount(number)
The initial supply to mint on Solana
Default: undefined -
--local-decimals(number)
Token local decimals
Default: 9 -
--shared-decimals(number)
OFT shared decimals
Default: 6 -
--name(string)
Token Name
Default: "MockOFT" -
--symbol(string)
Token Symbol
Default: "MOFT" -
--uri(string)
URI for token metadata
Default: "" -
--seller-fee-basis-points(number)
Seller fee basis points
Default: 0 -
--token-metadata-is-mutable(boolean)
Whether token metadata is mutable
Default: true -
--additional-minters(CSV string)
Comma-separated list of additional minters
Default: undefined -
--only-oft-store(boolean)
If you plan to have only the OFTStore and no additional minters. This is not reversible, and will result in losing the ability to mint new tokens by everything but the OFTStore.
Default: false -
--freeze-authority(string)
The Freeze Authority address (only supported in onlyOftStore mode)
Default: ""
The following parameters are only used for Mint-And-Burn Adapter (MABA) mode:
-
--mint(string)
The Token mint public key (used for MABA only)
Default: undefined -
--token-program(string)
The Token Program public key (used for MABA only)
Default: TOKEN_PROGRAM_ID
ℹ️ For OFT, the SPL token's Mint Authority is set to the Mint Authority Multisig, which always has the OFT Store as a signer. The multisig is fixed to needing 1 of N signatures.
ℹ️ You have the option to specify additional signers through the --additional-minters flag. If you choose not to, you must pass in --only-oft-store true, which means only the OFT Store will be a signer for the Mint Authority Multisig.
--only-oft-store, you will not be able to add in other signers/minters or update the Mint Authority, and the Freeze Authority will be immediately renounced. The token Mint Authority will be fixed Mint Authority Multisig address while the Freeze Authority will be set to None.
--additional-minters flag to add a CSV of additional minter addresses to the Mint Authority Multisig. If you do not want to, you must specify --only-oft-store true.
In layerzero.config.ts, the solanaContract.address is auto-populated with the oftStore address from the deployment file, which has the default path of deployments/solana-<mainnet/testnet>.
const solanaContract: OmniPointHardhat = {
eid: EndpointId.SOLANA_V2_TESTNET,
address: getOftStoreAddress(EndpointId.SOLANA_V2_TESTNET),
};address is specified only for the solana contract object. Do not specify addresses for the EVM chain contract objects. Under the hood, we use hardhat-deploy to retrieve the contract addresses of the deployed EVM chain contracts. You will run into an error if you specify address for an EVM chain contract object.
This is only relevant for OFT. If you opted to include the --amount flag in the create step, that means you already have minted some Solana OFT and you can skip this section.
ℹ️ This is only possible if you specified your deployer address as part of the --additional-minters flag when creating the Solana OFT. If you had chosen --only-oft-store true, you will not be able to mint your OFT on Solana.
First, you need to create the Associated Token Account for your address.
spl-token create-account <TOKEN_MINT>Then, you can mint. Note that the spl-token CLI expects the human-readable token amount and not the raw integer value for the <AMOUNT> param. This means, to mint 1 OFT, you would specify 1 as the amount. The spl-token handles the multiplication by 10^decimals for you.
spl-token mint <TOKEN_MINT> <AMOUNT> --multisig-signer ~/.config/solana/id.json --owner <MINT_AUTHORITY>ℹ️ ~/.config/solana/id.json assumes that you will use the keypair in the default location. To verify if this path applies to you, run solana config get and not the keypair path value.
ℹ️ You can get the <MINT_AUTHORITY> address from deployments/solana-testnet/OFT.json.
Refer to Verify the OFT Program.
Refer to the Solana Troubleshooting page on the LayerZero Docs to see how to solve common error when deploying Solana OFTs.