LayerZero V2 Solana OFT Adapter Account
The Solana OFT, Endpoint, and ULN Programs are currently in Mainnet Beta!
OFT Adapter allows an existing token to expand to any supported chain as a native token with a unified global supply, inheriting all the features of the OFT Standard. This works as an intermediary contract that handles sending and receiving tokens that have already been deployed.
For example, when transferring an SPL Token from the source chain (Chain A), the token will lock in the OFT Adapter, triggering a new token to mint on the selected destination chain (Chain B) via the paired OFT Contract.
When you want to unlock the SPL token in the source chain's OFT Adapter, you will call send on the OFT Contract (Chain B), triggering the minted OFT token to be burnt, and sending a message via the protocol to unlock the same amount of token from the Adapter and transfer to the receiving address (Chain A).
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
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.
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 its custody. 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, returning them to circulation under specific conditions, typically when tokens are transferred back to the original blockchain from another chain.
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.
import {
Connection,
Keypair,
PublicKey,
Transaction,
sendAndConfirmTransaction,
SystemProgram,
getExplorerLink,
} from '@solana/web3.js';
import {
AuthorityType,
TOKEN_PROGRAM_ID,
createInitializeMintInstruction,
createSetAuthorityInstruction,
getMintLen,
} from '@solana/spl-token';
import {OftTools, OFT_SEED} from '@layerzerolabs/lz-solana-sdk-v2';
// Connect to the Solana cluster (testnet in this case)
const connection = new Connection(clusterApiUrl('testnet'));
const user = getKeypairFromEnvironment('SECRET_KEY');
// Create a new keypair for your SPL Token
const mintKp = Keypair.generate();
// Number of shared decimals for the token (recommended value is 6)
const LOCAL_DECIMALS = 6;
const SHARED_DECIMALS = 6;
// Your OFT_PROGRAM_ID
const OFT_PROGRAM_ID = new PublicKey('YOUR_OFT_PROGRAM_ID');
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]);
// The keypair for your escrow account
const lockBox = Keypair.generate();
// Set the mint authority to the OFT Config and initialize the OFT
const [oftConfig] = PublicKey.findProgramAddressSync(
[Buffer.from(OFT_SEED), mintKp.publicKey.toBuffer()],
oft.PROGRAM_ID,
);
transaction = new Transaction().add(
await OftTools.createInitAdapterOftIx(
user.publicKey,
user.publicKey,
mintKp.publicKey,
user.publicKey,
lockBox.publicKey,
6,
TOKEN_PROGRAM_ID,
),
);
// Send the transaction to initialize the OFT
const signature = await sendAndConfirmTransaction(connection, transaction, [user, lockBox]);
const link = getExplorerLink('tx', signature, 'testnet');
console.log(`✅ OFT Adapter Initialization Complete! View the transaction here: ${link}`);
You can now continue following the rest of the OFT setup guide in Solana OFT Program starting from Setting Peers. Return to this page when ready to call send, as OFT Adapter has specific considerations when quoting and sending tokens cross-chain.
Setting Configurations
Use all the same instructions found in the Solana OFT Program section to configure your Adapter OFT, but remember to derive your OFT Config Account using your lockbox keypair rather than the token mint keypair:
// 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,
);
Calling send
As mentioned in the OFT Program guide, when using OFT Adapter you will need to provide additional parameters for quoteWithUln
and createSendWithUlnIx
:
Parameter | Type | Description |
---|---|---|
tokenEscrow | PublicKey | The token escrow account for the OFT Adapter implementation. Required for quoting an OFT Adapter transfer. |
payInZRO | bool | Whether to pay the Endpoint in SOL or the LayerZero gas token. Defaults to false (i.e., pay in SOL). |
composeMsg | bytes | The compose message encoded. |
Below an example script has been provided showing how these new parameters should be used:
import {
Connection,
Keypair,
PublicKey,
Transaction,
sendAndConfirmTransaction,
SystemProgram,
getExplorerLink,
} 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';
import {addressToBytes32} from '@layerzerolabs/lz-v2-utilities';
// Connect to the Solana cluster (devnet in this case)
const connection = new Connection(clusterApiUrl('testnet'));
const user = getKeypairFromEnvironment('SECRET_KEY');
const mintKp = Keypair.generate();
const lockBox = Keypair.generate();
// Number of decimals for the token (recommended value is 6)
const OFT_DECIMALS = 6;
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]);
transaction = new Transaction().add(
await OftTools.createInitAdapterOftIx(
user.publicKey,
user.publicKey,
mintKp.publicKey,
user.publicKey,
lockBox.publicKey,
6,
TOKEN_PROGRAM_ID,
),
);
// Send the transaction to initialize the OFT
await sendAndConfirmTransaction(connection, transaction, [user, lockBox]);
// 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;
const amountToSend = 100n;
const receiver = addressToBytes32('0x0000000000000000000000000000000000000001');
const fee = await OftTools.quoteWithUln(
connection,
user.publicKey,
mintKp.publicKey,
peer.dstEid,
amountToSend,
amountToSend,
Options.newOptions().addExecutorLzReceiveOption(65000, 0).toBytes(),
receiver,
);
const sendTransaction = new Transaction().add(
await OftTools.createSendWithUlnIx(
connection,
user.publicKey, //
mintKp.publicKey,
associatedTokenAccount,
peer.dstEid,
amountToSend,
amountToSend,
Options.newOptions().addExecutorLzReceiveOption(65000, 0).toBytes(), // extra options
receiver,
fee.nativeFee,
lockBox.publicKey,
),
);
const sendSignature = await sendAndConfirmTransaction(connection, sendTransaction, [user]);
const link = getExplorerLink('tx', sendSignature, 'testnet');
console.log(
`✅ You sent ${amountToSend} for dstEid ${peer.dstEid}! View the transaction here: ${link}`,
);