Skip to content
Draft
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/two-cups-prove.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@layerzerolabs/oft-solana-example": patch
---

for sends to Solana - conditional value based on ATA existence
21 changes: 13 additions & 8 deletions examples/oft-solana/layerzero.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { generateConnectionsConfig } from '@layerzerolabs/metadata-tools'
import { OAppEnforcedOption, OmniPointHardhat } from '@layerzerolabs/toolbox-hardhat'

import { getOftStoreAddress } from './tasks/solana'
// import { SPL_TOKEN_ACCOUNT_RENT_VALUE } from './tasks/solana/utils'

// Note: Do not use address for EVM OmniPointHardhat contracts. Contracts are loaded using hardhat-deploy.
// If you do use an address, ensure artifacts exists.
Expand All @@ -26,26 +27,30 @@ const EVM_ENFORCED_OPTIONS: OAppEnforcedOption[] = [
},
]

const CU_LIMIT = 200000 // This represents the CU limit for executing the `lz_receive` function on Solana.
const SPL_TOKEN_ACCOUNT_RENT_VALUE = 2039280 // This figure represents lamports (https://solana.com/docs/references/terminology#lamport) on Solana. Read below for more details.
const SOLANA_CU_LIMIT = 200_000 // This represents the CU limit for executing the `lz_receive` function on Solana.

/*
* Elaboration on `value` when sending OFTs to Solana:
* When sending OFTs to Solana, SOL is needed for rent (https://solana.com/docs/core/accounts#rent) to initialize the recipient's token account.
* The `2039280` lamports value is the exact rent value needed for SPL token accounts (0.00203928 SOL).
* For Token2022 token accounts, 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.
* Inbound to Solana enforced options:
* - Use gas equal to the CU limit needed for lz_receive.
* - Keep value=0 here; supply per-tx msg.value only when the recipient’s ATA
* must be created (~2,039,280 lamports for SPL; Token-2022 may require more).
* In this repo, per-tx value is set in ./tasks/evm/sendEvm.ts.
* Details: https://docs.layerzero.network/v2/developers/solana/oft/account#setting-enforced-options-inbound-to-solana
*/

const SOLANA_ENFORCED_OPTIONS: OAppEnforcedOption[] = [
{
msgType: 1,
optionType: ExecutorOptionType.LZ_RECEIVE,
gas: CU_LIMIT,
value: SPL_TOKEN_ACCOUNT_RENT_VALUE,
gas: SOLANA_CU_LIMIT,
// value: SPL_TOKEN_ACCOUNT_RENT_VALUE, // If you enable this, all sends regardless of whether the recipient already has the Associated Token Account will include the rent value, which might be wasteful.
value: 0, // value will be set where quote/send is called. In this example, it is set in ./tasks/common/sendOFT.ts
},
]

// Learn about Message Execution Options: https://docs.layerzero.network/v2/developers/solana/oft/account#message-execution-options
// Learn more about the Simple Config Generator - https://docs.layerzero.network/v2/developers/evm/technical-reference/simple-config
// Learn about DVNs - https://docs.layerzero.network/v2/concepts/modular-security/security-stack-dvns
export default async function () {
// note: pathways declared here are automatically bidirectional
// if you declare A,B there's no need to declare B,A
Expand Down
37 changes: 32 additions & 5 deletions examples/oft-solana/tasks/common/sendOFT.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@ import { task, types } from 'hardhat/config'
import { HardhatRuntimeEnvironment } from 'hardhat/types'

import { ChainType, endpointIdToChainType, endpointIdToNetwork } from '@layerzerolabs/lz-definitions'
import { Options } from '@layerzerolabs/lz-v2-utilities'

import { EvmArgs, sendEvm } from '../evm/sendEvm'
import { getSolanaDeployment } from '../solana'
import { SolanaArgs, sendSolana } from '../solana/sendSolana'
import { getConditionalValueForSendToSolana } from '../solana/utils'

import { SendResult } from './types'
import { DebugLogger, KnownOutputs, KnownWarnings, getBlockExplorerLink } from './utils'
Expand Down Expand Up @@ -55,7 +58,8 @@ task('lz:oft:send', 'Sends OFT tokens cross‐chain from any supported chain')
.addOptionalParam('tokenProgram', 'Solana Token Program pubkey', undefined, types.string)
.addOptionalParam('computeUnitPriceScaleFactor', 'Solana compute unit price scale factor', 4, types.float)
.setAction(async (args: MasterArgs, hre: HardhatRuntimeEnvironment) => {
const chainType = endpointIdToChainType(args.srcEid)
const srcChainType = endpointIdToChainType(args.srcEid)
const dstChainType = endpointIdToChainType(args.dstEid)
let result: SendResult

if (args.oftAddress || args.oftProgramId) {
Expand All @@ -65,13 +69,36 @@ task('lz:oft:send', 'Sends OFT tokens cross‐chain from any supported chain')
)
}

// route to the correct function based on the chain type
if (chainType === ChainType.EVM) {
// NOTE: the conditionalValue block below assumes that in layerzeroconfig.ts, in the SOLANA_ENFORCED_OPTIONS, you have set the value to 0
// Setting value both in the SOLANA_ENFORCED_OPTIONS and in the conditionalValue block below will result in redundant value being sent
let conditionalValue = 0
// If sending to Solana, compute conditional value for ATA creation
if (dstChainType === ChainType.SOLANA) {
const solanaDeployment = getSolanaDeployment(args.dstEid)
conditionalValue = await getConditionalValueForSendToSolana({
eid: args.dstEid,
recipient: args.to,
mint: solanaDeployment.mint,
})
}

// throw if user specified extraOptions and conditionalValue is non-zero
if (args.extraOptions && conditionalValue > 0) {
throw new Error('extraOptions and conditionalValue cannot be set at the same time')
// hint: do not pass in extraOptions via params
}
// if there's conditionalValue, we build the extraOptions to be passed in
if (conditionalValue > 0) {
args.extraOptions = Options.newOptions().addExecutorLzReceiveOption(0, conditionalValue).toHex()
}

// route to the correct send function based on the source chain type
if (srcChainType === ChainType.EVM) {
result = await sendEvm(args as EvmArgs, hre)
} else if (chainType === ChainType.SOLANA) {
} else if (srcChainType === ChainType.SOLANA) {
result = await sendSolana(args as SolanaArgs)
} else {
throw new Error(`The chain type ${chainType} is not implemented in sendOFT for this example`)
throw new Error(`The chain type ${srcChainType} is not implemented in sendOFT for this example`)
}

DebugLogger.printLayerZeroOutput(
Expand Down
83 changes: 81 additions & 2 deletions examples/oft-solana/tasks/solana/utils.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import { findAssociatedTokenPda, safeFetchMint, safeFetchToken } from '@metaplex-foundation/mpl-toolbox'
import { PublicKey, Umi, publicKey } from '@metaplex-foundation/umi'
import { TOKEN_2022_PROGRAM_ID, TOKEN_PROGRAM_ID } from '@solana/spl-token'
import { Connection } from '@solana/web3.js'
import { HardhatRuntimeEnvironment } from 'hardhat/types'

Expand All @@ -10,6 +13,11 @@ import {
TASK_LZ_OAPP_CONFIG_GET,
} from '@layerzerolabs/ua-devtools-evm-hardhat'

import { deriveConnection } from './index'

export const SPL_TOKEN_ACCOUNT_RENT_VALUE = 2_039_280 // This figure represents lamports (https://solana.com/docs/references/terminology#lamport) on Solana. Read below for more details.
export const TOKEN_2022_ACCOUNT_RENT_VALUE = 2_500_000 // NOTE: The actual value needed depends on which specific extensions you have enabled for your Token2022 token. You would need to determine this value based on your own Token2022 token.

export const findSolanaEndpointIdInGraph = async (
hre: HardhatRuntimeEnvironment,
oappConfig: string
Expand Down Expand Up @@ -72,8 +80,9 @@ export function parseDecimalToUnits(amount: string, decimals: number): bigint {
* that mentions the 429 retry.
*/
export function silenceSolana429(connection: Connection): void {
const origWrite = process.stderr.write.bind(process.stderr)
process.stderr.write = ((chunk: any, ...args: any[]) => {
type WriteFn = (chunk: string | Buffer, ...args: unknown[]) => boolean
const origWrite = process.stderr.write.bind(process.stderr) as WriteFn
process.stderr.write = ((chunk: string | Buffer, ...args: unknown[]) => {
const str = Buffer.isBuffer(chunk) ? chunk.toString('utf8') : chunk
if (typeof str === 'string' && str.includes('429 Too Many Requests')) {
// swallow it
Expand All @@ -83,3 +92,73 @@ export function silenceSolana429(connection: Connection): void {
return origWrite(chunk, ...args)
}) as typeof process.stderr.write
}

export enum SolanaTokenType {
SPL = 'spl',
TOKEN2022 = 'token2022',
}

/**
* Check if an Associated Token Account (ATA) exists for a given mint and owner.
* Returns the derived ATA and a boolean indicating existence.
*/
export async function checkAssociatedTokenAccountExists(args: {
umi?: Umi
eid: EndpointId
mint: PublicKey | string
owner: PublicKey | string
}): Promise<{ ata: string; ataExists: boolean; tokenType: SolanaTokenType | null }> {
const { umi: providedUmi, eid, mint, owner } = args
const umi = providedUmi ?? (await deriveConnection(eid, true)).umi

const mintPk = typeof mint === 'string' ? publicKey(mint) : mint
const ownerPk = typeof owner === 'string' ? publicKey(owner) : owner

const ata = findAssociatedTokenPda(umi, { mint: mintPk, owner: ownerPk })
const account = await safeFetchToken(umi, ata)
const mintAccount = await safeFetchMint(umi, mintPk)
// check header.owner to determine if the token is SPL or Token2022 using switch
let tokenType: SolanaTokenType | null = null

switch (mintAccount?.header.owner) {
case TOKEN_PROGRAM_ID.toBase58():
tokenType = SolanaTokenType.SPL
break
case TOKEN_2022_PROGRAM_ID.toBase58():
tokenType = SolanaTokenType.TOKEN2022
break
default:
throw new Error(`Unknown token type: ${account?.header.owner}`)
}

return { ata: ata[0], ataExists: !!account, tokenType }
}

/**
* Compute the per-transaction msg.value to attach when sending to Solana.
* Returns 0 if the recipient ATA already exists or if the mint is Token2022.
* Returns SPL_TOKEN_ACCOUNT_RENT_VALUE if the recipient ATA is missing and the mint is SPL.
*/
export async function getConditionalValueForSendToSolana(args: {
eid: EndpointId
recipient: string
mint: string | PublicKey
umi?: Umi
}): Promise<number> {
const { eid, recipient, mint, umi } = args
const { ataExists, tokenType } = await checkAssociatedTokenAccountExists({
eid,
owner: recipient,
mint,
umi,
})
if (!ataExists && tokenType === SolanaTokenType.SPL) {
return SPL_TOKEN_ACCOUNT_RENT_VALUE
} else if (!ataExists && tokenType === SolanaTokenType.TOKEN2022) {
console.warn(
'Ensure that the TOKEN_2022_ACCOUNT_RENT_VALUE has been updated according to your actual token account size'
)
return TOKEN_2022_ACCOUNT_RENT_VALUE
}
return 0
}