Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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/overview#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
39 changes: 34 additions & 5 deletions examples/oft-solana/tasks/common/sendOFT.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import { publicKey } from '@metaplex-foundation/umi'
import { task, types } from 'hardhat/config'
import { HardhatRuntimeEnvironment } from 'hardhat/types'

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

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

import { SendResult } from './types'
import { DebugLogger, KnownOutputs, KnownWarnings, getBlockExplorerLink } from './utils'
Expand All @@ -22,10 +25,14 @@ interface MasterArgs {
composeMsg?: string
/** EVM: 20-byte hex; Solana: base58 PDA of the store */
oftAddress?: string
/** EVM: 20-byte hex; Solana: base58 PDA of the store (currently only relevant for sends to Solana) */
dstOftAddress?: string
/** Solana only: override the OFT program ID (base58) */
oftProgramId?: string
tokenProgram?: string
computeUnitPriceScaleFactor?: number
/** Solana only (so far): minimum value needed successful lzReceive on the destination chain */
minimumLzReceiveValue?: number
addressLookupTables?: string
}

Expand Down Expand Up @@ -53,6 +60,12 @@ task('lz:oft:send', 'Sends OFT tokens cross‐chain from any supported chain')
undefined,
types.string
)
.addOptionalParam(
'dstOftAddress',
'Override the destination local deployment OFT address (20-byte hex for EVM, base58 PDA for Solana)',
undefined,
types.string
)
.addOptionalParam('oftProgramId', 'Solana only: override the OFT program ID (base58)', undefined, types.string)
.addOptionalParam('tokenProgram', 'Solana Token Program pubkey', undefined, types.string)
.addOptionalParam('computeUnitPriceScaleFactor', 'Solana compute unit price scale factor', 4, types.float)
Expand All @@ -63,7 +76,8 @@ task('lz:oft:send', 'Sends OFT tokens cross‐chain from any supported chain')
types.string
)
.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 @@ -73,16 +87,31 @@ 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) {
let minimumLzReceiveValue = 0
// If sending to Solana, compute minimum value needed for ATA creation
if (dstChainType === ChainType.SOLANA) {
const solanaDeployment = getSolanaDeployment(args.dstEid)
const { connection, umi } = await deriveConnection(args.dstEid)
// determines the absolute minimum value needed for an OFT send to Solana (based on ATA creation status)
minimumLzReceiveValue = await getMinimumValueForSendToSolana({
recipient: publicKey(args.to),
mint: publicKey(args.dstOftAddress || solanaDeployment.mint),
umi,
connection,
})
args.minimumLzReceiveValue = minimumLzReceiveValue
}

// 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,
addressLookupTables: args.addressLookupTables ? args.addressLookupTables.split(',') : [],
} 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
30 changes: 30 additions & 0 deletions examples/oft-solana/tasks/common/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,36 @@ export function decodeLzReceiveOptions(hex: string): string {
}
}

export function calculateGasAndValueShortfall(
enforcedOptions: string,
extraOptions?: string,
minimumValue?: number | bigint
): { gasShortfall: bigint; valueShortfall: bigint } {
// There's no Typescript function for combining options, so we'll decode both enforcedOptions and extraOptions to get their values
const enforcedOptionsValue = Options.fromOptions(enforcedOptions).decodeExecutorLzReceiveOption()?.value ?? 0n
const extraOptionsGas = extraOptions
? Options.fromOptions(extraOptions).decodeExecutorLzReceiveOption()?.gas ?? 0n
: 0n
const extraOptionsValue = extraOptions
? Options.fromOptions(extraOptions).decodeExecutorLzReceiveOption()?.value ?? 0n
: 0n
const totalOptionsValue = enforcedOptionsValue + extraOptionsValue

if (minimumValue === undefined || minimumValue === null) {
return { gasShortfall: extraOptionsGas, valueShortfall: 0n }
}

const minimum = BigInt(minimumValue)
if (minimum > totalOptionsValue) {
const shortfall = minimum - totalOptionsValue
console.info(
`minimum lzReceive value needed is greater than the total options value, adding extraOptions to cover the difference: ${minimum} (minimum) - ${totalOptionsValue} (total) = ${shortfall} (shortfall)`
)
return { gasShortfall: extraOptionsGas, valueShortfall: shortfall }
}
return { gasShortfall: extraOptionsGas, valueShortfall: 0n }
}

export async function getSolanaUlnConfigPDAs(
remote: EndpointId,
connection: Connection,
Expand Down
50 changes: 35 additions & 15 deletions examples/oft-solana/tasks/evm/sendEvm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,13 @@ import { makeBytes32 } from '@layerzerolabs/devtools'
import { createGetHreByEid } from '@layerzerolabs/devtools-evm-hardhat'
import { createLogger, promptToContinue } from '@layerzerolabs/io-devtools'
import { ChainType, endpointIdToChainType, endpointIdToNetwork } from '@layerzerolabs/lz-definitions'
import { Options } from '@layerzerolabs/lz-v2-utilities'

import layerzeroConfig from '../../layerzero.config'
import { SendResult } from '../common/types'
import { DebugLogger, KnownErrors, MSG_TYPE, isEmptyOptionsEvm } from '../common/utils'
import { DebugLogger, KnownErrors, MSG_TYPE, calculateGasAndValueShortfall, isEmptyOptionsEvm } from '../common/utils'
import { getLayerZeroScanLink } from '../solana'

const logger = createLogger()
export interface EvmArgs {
srcEid: number
Expand All @@ -22,9 +24,10 @@ export interface EvmArgs {
extraOptions?: string
composeMsg?: string
oftAddress?: string
minimumLzReceiveValue?: number
}
export async function sendEvm(
{ srcEid, dstEid, amount, to, minAmount, extraOptions, composeMsg, oftAddress }: EvmArgs,
{ srcEid, dstEid, amount, to, minAmount, extraOptions, composeMsg, oftAddress, minimumLzReceiveValue }: EvmArgs,
hre: HardhatRuntimeEnvironment
): Promise<SendResult> {
if (endpointIdToChainType(srcEid) !== ChainType.EVM) {
Expand All @@ -41,6 +44,7 @@ export async function sendEvm(
)
throw error
}

const signer = await srcEidHre.ethers.getNamedSigner('deployer')
// 1️⃣ resolve the OFT wrapper address
let wrapperAddress: string
Expand Down Expand Up @@ -74,22 +78,14 @@ export async function sendEvm(
// hex string → Uint8Array → zero-pad to 32 bytes
toBytes = makeBytes32(to)
}
// 6️⃣ build sendParam and dispatch
const sendParam = {
dstEid,
to: toBytes,
amountLD: amountUnits.toString(),
minAmountLD: minAmount ? parseUnits(minAmount, decimals).toString() : amountUnits.toString(),
extraOptions: extraOptions ? extraOptions.toString() : '0x',
composeMsg: composeMsg ? composeMsg.toString() : '0x',
oftCmd: '0x',
}

// Check whether there are extra options or enforced options. If not, warn the user.
let enforcedOptions = '0x'

// BOF: Check whether there are extra options or enforced options. If not, warn the user.
// Read on Message Options: https://docs.layerzero.network/v2/concepts/message-options
if (!extraOptions) {
if (!extraOptions || extraOptions === '0x') {
try {
const enforcedOptions = composeMsg
enforcedOptions = composeMsg
? await oft.enforcedOptions(dstEid, MSG_TYPE.SEND_AND_CALL)
: await oft.enforcedOptions(dstEid, MSG_TYPE.SEND)

Expand All @@ -105,6 +101,30 @@ export async function sendEvm(
logger.debug(`Failed to check enforced options: ${error}`)
}
}
// EOF: Check whether there are extra options or enforced options. If not, warn the user.

// BOF: evaluate whether options require additional value
const { gasShortfall, valueShortfall } = calculateGasAndValueShortfall(
enforcedOptions,
extraOptions,
minimumLzReceiveValue
)
if (valueShortfall > 0n) {
// if there's a value shortfall, we add the difference as extraOptions
extraOptions = Options.newOptions().addExecutorLzReceiveOption(gasShortfall, valueShortfall).toHex()
}
// EOF: evaluate whether options require additional value

// 6️⃣ build sendParam and dispatch
const sendParam = {
dstEid,
to: toBytes,
amountLD: amountUnits.toString(),
minAmountLD: minAmount ? parseUnits(minAmount, decimals).toString() : amountUnits.toString(),
extraOptions: extraOptions ? extraOptions.toString() : '0x',
composeMsg: composeMsg ? composeMsg.toString() : '0x',
oftCmd: '0x',
}

// 6️⃣ Quote (MessagingFee = { nativeFee, lzTokenFee })
logger.info('Quoting the native gas cost for the send transaction...')
Expand Down
Loading
Loading