From cf6629ce7f45f914195d3180acbf9226490a5e4b Mon Sep 17 00:00:00 2001 From: nazreen <10964594+nazreen@users.noreply.github.com> Date: Wed, 10 Dec 2025 02:24:00 +0800 Subject: [PATCH 1/5] add logger --- .../devtools-solana/src/common/addresses.ts | 23 ++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/packages/devtools-solana/src/common/addresses.ts b/packages/devtools-solana/src/common/addresses.ts index f28eaf32e..6c08ba6d9 100644 --- a/packages/devtools-solana/src/common/addresses.ts +++ b/packages/devtools-solana/src/common/addresses.ts @@ -1,14 +1,20 @@ -import { DebugLogger, KnownErrors } from '@layerzerolabs/io-devtools' +import { DebugLogger, KnownErrors, createModuleLogger } from '@layerzerolabs/io-devtools' import { Connection, PublicKey, SystemProgram } from '@solana/web3.js' import { PROGRAM_ID as SQUADS_PROGRAM_ID } from '@sqds/multisig' +const createLogger = () => createModuleLogger('Solana addresses') + /** * Returns true if the provided address is a valid on-curve public key. This can mean the address is either a 'regular' Solana address or a Squads Vault PDA. */ export function isOnCurveAddress(address: string): boolean { + const logger = createLogger() try { - return PublicKey.isOnCurve(new PublicKey(address).toBytes()) + const result = PublicKey.isOnCurve(new PublicKey(address).toBytes()) + logger.debug(`[isOnCurveAddress] address=${address} onCurve=${result}`) + return result } catch { + logger.debug(`[isOnCurveAddress] address=${address} invalid public key`) return false } } @@ -18,10 +24,14 @@ export function isOnCurveAddress(address: string): boolean { * Returns true if the provided address could be a Squads vault PDA. */ export async function isPossibleSquadsVault(connection: Connection, address: string): Promise { + const logger = createLogger() try { const pubkey = new PublicKey(address) const accountInfo = await connection.getAccountInfo(pubkey) const isOnCurve = isOnCurveAddress(address) + logger.debug( + `[isPossibleSquadsVault] address=${address} onCurve=${isOnCurve} hasAccount=${accountInfo != null}` + ) if (isOnCurve) { // a Squads Vault address is always off-curve return false @@ -34,14 +44,18 @@ export async function isPossibleSquadsVault(connection: Connection, address: str } export async function isSquadsV4Vault(address: string): Promise { + const logger = createLogger() // https://docs.squads.so/main/development/api/vault-check // Note that this endpoint is rate-limited to 25 requests per minute. It's fine if run on end-dev side but if run on a backend, it should be cached. const response = await fetch( `https://4fnetmviidiqkjzenwxe66vgoa0soerr.lambda-url.us-east-1.on.aws/isSquad/${address}` ) const data = await response.json() + logger.debug( + `[isSquadsV4Vault] address=${address} isSquad=${data?.isSquad} version=${data?.version} status=${response.status}` + ) if (data.isSquad && data.version != 'v4') { - console.warn(`${address} is a Squads Vault but not v4`) + logger.warn(`${address} is a Squads Vault but not v4`) return false } else if (data.isSquad && data.version === 'v4') { return true @@ -51,9 +65,11 @@ export async function isSquadsV4Vault(address: string): Promise { } export async function assertValidSolanaAdmin(connection: Connection, address: string): Promise { + const logger = createLogger() const pubkey = new PublicKey(address) try { + logger.debug(`[assertValidSolanaAdmin] start address=${address}`) const accountInfo = await connection.getAccountInfo(pubkey) if (accountInfo != null && accountInfo.owner.equals(SQUADS_PROGRAM_ID)) { @@ -69,6 +85,7 @@ export async function assertValidSolanaAdmin(connection: Connection, address: st `Invalid owner/delegate address ${address}. Must be a valid on-curve address or a Squads Vault PDA.` ) } + logger.debug(`[assertValidSolanaAdmin] address=${address} valid`) } catch (error) { if (error instanceof Error) { throw error From cecb01abb7c2ab3211062c1ac6631ee733bba92e Mon Sep 17 00:00:00 2001 From: nazreen <10964594+nazreen@users.noreply.github.com> Date: Wed, 10 Dec 2025 02:47:09 +0800 Subject: [PATCH 2/5] fix --- .changeset/small-trees-sleep.md | 5 ++ .../devtools-solana/src/common/addresses.ts | 84 +++++++++++-------- 2 files changed, 52 insertions(+), 37 deletions(-) create mode 100644 .changeset/small-trees-sleep.md diff --git a/.changeset/small-trees-sleep.md b/.changeset/small-trees-sleep.md new file mode 100644 index 000000000..28f0e113e --- /dev/null +++ b/.changeset/small-trees-sleep.md @@ -0,0 +1,5 @@ +--- +"@layerzerolabs/devtools-solana": patch +--- + +update assertValidSolanaAdmin diff --git a/packages/devtools-solana/src/common/addresses.ts b/packages/devtools-solana/src/common/addresses.ts index 6c08ba6d9..5f1658110 100644 --- a/packages/devtools-solana/src/common/addresses.ts +++ b/packages/devtools-solana/src/common/addresses.ts @@ -19,30 +19,7 @@ export function isOnCurveAddress(address: string): boolean { } } -/** - * Note: This was created before the existence of the Squads isVault endpoint was known. - * Returns true if the provided address could be a Squads vault PDA. - */ -export async function isPossibleSquadsVault(connection: Connection, address: string): Promise { - const logger = createLogger() - try { - const pubkey = new PublicKey(address) - const accountInfo = await connection.getAccountInfo(pubkey) - const isOnCurve = isOnCurveAddress(address) - logger.debug( - `[isPossibleSquadsVault] address=${address} onCurve=${isOnCurve} hasAccount=${accountInfo != null}` - ) - if (isOnCurve) { - // a Squads Vault address is always off-curve - return false - } - - return accountInfo != null && accountInfo.owner.equals(SystemProgram.programId) - } catch (error) { - return false - } -} - +// this only works on mainnet export async function isSquadsV4Vault(address: string): Promise { const logger = createLogger() // https://docs.squads.so/main/development/api/vault-check @@ -64,6 +41,18 @@ export async function isSquadsV4Vault(address: string): Promise { } } +/** + * Validates that an address is acceptable as a Solana admin (owner/delegate). + * + * Throws if: + * - Address is off-curve AND account exists AND account is owned by Squads Program + * (This means it's a Squads Multisig account, not the vault address) + * + * Does NOT throw (valid) if: + * - Address is on-curve (regular Solana address) + * - Address is off-curve AND account exists AND account is owned by System Program (funded Squads Vault) + * - Address is off-curve AND account does not exist (possibly unfunded Squads Vault) + */ export async function assertValidSolanaAdmin(connection: Connection, address: string): Promise { const logger = createLogger() const pubkey = new PublicKey(address) @@ -71,21 +60,42 @@ export async function assertValidSolanaAdmin(connection: Connection, address: st try { logger.debug(`[assertValidSolanaAdmin] start address=${address}`) const accountInfo = await connection.getAccountInfo(pubkey) + const isOnCurve = isOnCurveAddress(address) - if (accountInfo != null && accountInfo.owner.equals(SQUADS_PROGRAM_ID)) { - DebugLogger.printErrorAndFixSuggestion(KnownErrors.SOLANA_OWNER_OR_DELEGATE_CANNOT_BE_MULTISIG_ACCOUNT) - throw new Error( - `Invalid owner/delegate address ${address}. This is a Squads multisig account. Use the vault address instead.` - ) - } - - if (!isOnCurveAddress(address) && !(await isPossibleSquadsVault(connection, address))) { - DebugLogger.printErrorAndFixSuggestion(KnownErrors.SOLANA_INVALID_OWNER_OR_DELEGATE) - throw new Error( - `Invalid owner/delegate address ${address}. Must be a valid on-curve address or a Squads Vault PDA.` - ) + // if onCurve (regular address), always valid + if (isOnCurve) { + logger.debug(`[assertValidSolanaAdmin] address=${address} valid: on-curve (regular address)`) + return + } else { + // if offCurve + const accountExists = accountInfo != null + if (accountExists) { + // here it could either be a funded Squads Vault account or a Squads Multisig account + const ownedBySquadsProgram = accountInfo?.owner.equals(SQUADS_PROGRAM_ID) + const ownedBySystemProgram = accountInfo?.owner.equals(SystemProgram.programId) + if (ownedBySquadsProgram) { + logger.debug( + `[assertValidSolanaAdmin] address=${address} invalid: off-curve, account exists, owned by Squads Program (multisig account)` + ) + DebugLogger.printErrorAndFixSuggestion( + KnownErrors.SOLANA_OWNER_OR_DELEGATE_CANNOT_BE_MULTISIG_ACCOUNT + ) + throw new Error( + `Invalid owner/delegate address ${address}. This is a Squads multisig account. Use the vault address instead.` + ) + } else if (ownedBySystemProgram) { + logger.debug( + `[assertValidSolanaAdmin] address=${address} valid: off-curve, account exists, owned by System Program (funded Squads Vault)` + ) + return + } + } else { + logger.debug( + `[assertValidSolanaAdmin] address=${address} valid: off-curve, account does not exist (possibly unfunded Squads Vault)` + ) + return + } } - logger.debug(`[assertValidSolanaAdmin] address=${address} valid`) } catch (error) { if (error instanceof Error) { throw error From 1dce800e0dc179d31cbb03d46eb6a950bd512f10 Mon Sep 17 00:00:00 2001 From: nazreen <10964594+nazreen@users.noreply.github.com> Date: Wed, 10 Dec 2025 02:53:47 +0800 Subject: [PATCH 3/5] update helper --- examples/oft-solana/tasks/solana/debug.ts | 5 +++-- .../devtools-solana/src/common/addresses.ts | 19 +++++++++++++++++-- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/examples/oft-solana/tasks/solana/debug.ts b/examples/oft-solana/tasks/solana/debug.ts index e928db21c..ba8e29ec2 100644 --- a/examples/oft-solana/tasks/solana/debug.ts +++ b/examples/oft-solana/tasks/solana/debug.ts @@ -147,8 +147,9 @@ task('lz:oft:solana:debug', 'Manages OFTStore and OAppRegistry information') const tokenMetadata = await getSolanaTokenMetadata(umi, publicKey(oftStoreInfo.tokenMint), tokenProgramType) - const adminIsSquadsV4Vault = await isSquadsV4Vault(oftStoreInfo.admin) - const delegateIsSquadsV4Vault = await isSquadsV4Vault(oAppRegistryInfo?.delegate?.toBase58()) + // Note: isSquadsV4Vault only works on mainnet + const adminIsSquadsV4Vault = await isSquadsV4Vault(eid as number, oftStoreInfo.admin) + const delegateIsSquadsV4Vault = await isSquadsV4Vault(eid as number, oAppRegistryInfo?.delegate?.toBase58()) const printOftStore = async () => { DebugLogger.header('OFT Store Information') diff --git a/packages/devtools-solana/src/common/addresses.ts b/packages/devtools-solana/src/common/addresses.ts index 5f1658110..8f2a36831 100644 --- a/packages/devtools-solana/src/common/addresses.ts +++ b/packages/devtools-solana/src/common/addresses.ts @@ -1,4 +1,5 @@ import { DebugLogger, KnownErrors, createModuleLogger } from '@layerzerolabs/io-devtools' +import { EndpointId } from '@layerzerolabs/lz-definitions' import { Connection, PublicKey, SystemProgram } from '@solana/web3.js' import { PROGRAM_ID as SQUADS_PROGRAM_ID } from '@sqds/multisig' @@ -19,9 +20,23 @@ export function isOnCurveAddress(address: string): boolean { } } -// this only works on mainnet -export async function isSquadsV4Vault(address: string): Promise { +/** + * Checks if an address is a Squads V4 vault using the Squads API. + * Only works on Solana mainnet (30168). Returns null for testnet (40168). + * Throws for any other EID. + */ +export async function isSquadsV4Vault(eid: EndpointId, address: string): Promise { const logger = createLogger() + + if (eid === EndpointId.SOLANA_V2_TESTNET) { + logger.debug(`[isSquadsV4Vault] eid=${eid} is testnet, returning null (API only works on mainnet)`) + return null + } + + if (eid !== EndpointId.SOLANA_V2_MAINNET) { + throw new Error(`[isSquadsV4Vault] unsupported eid=${eid}, only Solana mainnet (30168) is supported`) + } + // https://docs.squads.so/main/development/api/vault-check // Note that this endpoint is rate-limited to 25 requests per minute. It's fine if run on end-dev side but if run on a backend, it should be cached. const response = await fetch( From 44f32832b6e0585a225fe9d7bf0121d75303afc9 Mon Sep 17 00:00:00 2001 From: nazreen <10964594+nazreen@users.noreply.github.com> Date: Wed, 10 Dec 2025 02:58:29 +0800 Subject: [PATCH 4/5] flatten --- .../devtools-solana/src/common/addresses.ts | 92 ++++++++++++------- 1 file changed, 58 insertions(+), 34 deletions(-) diff --git a/packages/devtools-solana/src/common/addresses.ts b/packages/devtools-solana/src/common/addresses.ts index 8f2a36831..59c20bd4f 100644 --- a/packages/devtools-solana/src/common/addresses.ts +++ b/packages/devtools-solana/src/common/addresses.ts @@ -1,7 +1,10 @@ import { DebugLogger, KnownErrors, createModuleLogger } from '@layerzerolabs/io-devtools' import { EndpointId } from '@layerzerolabs/lz-definitions' import { Connection, PublicKey, SystemProgram } from '@solana/web3.js' -import { PROGRAM_ID as SQUADS_PROGRAM_ID } from '@sqds/multisig' +import { PROGRAM_ID as SQUADS_V4_PROGRAM_ID } from '@sqds/multisig' + +// Squads V3 program ID - legacy, not ideal but we allow it +const SQUADS_V3_PROGRAM_ID = new PublicKey('SMPLecH534NA9acpos4G6x7uf3LWbCAwZQE9e8ZekMu') const createLogger = () => createModuleLogger('Solana addresses') @@ -60,13 +63,14 @@ export async function isSquadsV4Vault(eid: EndpointId, address: string): Promise * Validates that an address is acceptable as a Solana admin (owner/delegate). * * Throws if: - * - Address is off-curve AND account exists AND account is owned by Squads Program - * (This means it's a Squads Multisig account, not the vault address) + * - Address is off-curve AND account exists AND owned by Squads V4 Program (multisig account, not vault) + * - Address is off-curve AND account exists AND owned by unrecognized program * * Does NOT throw (valid) if: * - Address is on-curve (regular Solana address) - * - Address is off-curve AND account exists AND account is owned by System Program (funded Squads Vault) * - Address is off-curve AND account does not exist (possibly unfunded Squads Vault) + * - Address is off-curve AND account exists AND owned by System Program (funded Squads Vault) + * - Address is off-curve AND account exists AND owned by Squads V3 Program (legacy, allowed with warning) */ export async function assertValidSolanaAdmin(connection: Connection, address: string): Promise { const logger = createLogger() @@ -77,40 +81,60 @@ export async function assertValidSolanaAdmin(connection: Connection, address: st const accountInfo = await connection.getAccountInfo(pubkey) const isOnCurve = isOnCurveAddress(address) - // if onCurve (regular address), always valid + // On-curve (regular address) = always valid if (isOnCurve) { logger.debug(`[assertValidSolanaAdmin] address=${address} valid: on-curve (regular address)`) return - } else { - // if offCurve - const accountExists = accountInfo != null - if (accountExists) { - // here it could either be a funded Squads Vault account or a Squads Multisig account - const ownedBySquadsProgram = accountInfo?.owner.equals(SQUADS_PROGRAM_ID) - const ownedBySystemProgram = accountInfo?.owner.equals(SystemProgram.programId) - if (ownedBySquadsProgram) { - logger.debug( - `[assertValidSolanaAdmin] address=${address} invalid: off-curve, account exists, owned by Squads Program (multisig account)` - ) - DebugLogger.printErrorAndFixSuggestion( - KnownErrors.SOLANA_OWNER_OR_DELEGATE_CANNOT_BE_MULTISIG_ACCOUNT - ) - throw new Error( - `Invalid owner/delegate address ${address}. This is a Squads multisig account. Use the vault address instead.` - ) - } else if (ownedBySystemProgram) { - logger.debug( - `[assertValidSolanaAdmin] address=${address} valid: off-curve, account exists, owned by System Program (funded Squads Vault)` - ) - return - } - } else { - logger.debug( - `[assertValidSolanaAdmin] address=${address} valid: off-curve, account does not exist (possibly unfunded Squads Vault)` - ) - return - } } + + // From here: off-curve + const accountExists = accountInfo != null + + // Off-curve + no account = possibly unfunded Squads Vault + if (!accountExists) { + logger.debug( + `[assertValidSolanaAdmin] address=${address} valid: off-curve, account does not exist (possibly unfunded Squads Vault)` + ) + return + } + + // From here: off-curve + account exists + const owner = accountInfo.owner + + // Owned by Squads V4 Program = multisig account (invalid, should use vault address) + if (owner.equals(SQUADS_V4_PROGRAM_ID)) { + logger.debug( + `[assertValidSolanaAdmin] address=${address} invalid: off-curve, account exists, owned by Squads V4 Program (multisig account)` + ) + DebugLogger.printErrorAndFixSuggestion(KnownErrors.SOLANA_OWNER_OR_DELEGATE_CANNOT_BE_MULTISIG_ACCOUNT) + throw new Error( + `Invalid owner/delegate address ${address}. This is a Squads multisig account. Use the vault address instead.` + ) + } + + // Owned by System Program = funded Squads Vault (valid) + if (owner.equals(SystemProgram.programId)) { + logger.debug( + `[assertValidSolanaAdmin] address=${address} valid: off-curve, account exists, owned by System Program (funded Squads Vault)` + ) + return + } + + // Owned by Squads V3 Program = legacy, not ideal but allowed + if (owner.equals(SQUADS_V3_PROGRAM_ID)) { + logger.warn( + `[assertValidSolanaAdmin] address=${address} valid: off-curve, account exists, owned by Squads V3 Program (legacy multisig - consider migrating to V4)` + ) + return + } + + // Owned by unrecognized program = invalid + logger.debug( + `[assertValidSolanaAdmin] address=${address} invalid: off-curve, account exists, owned by unrecognized program ${owner.toBase58()}` + ) + throw new Error( + `Invalid owner/delegate address ${address}. Account is owned by unrecognized program ${owner.toBase58()}.` + ) } catch (error) { if (error instanceof Error) { throw error From deccee6dddd0dde285d4540ce613b86746238f82 Mon Sep 17 00:00:00 2001 From: nazreen <10964594+nazreen@users.noreply.github.com> Date: Wed, 10 Dec 2025 03:03:37 +0800 Subject: [PATCH 5/5] changeset --- .changeset/small-phones-play.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/small-phones-play.md diff --git a/.changeset/small-phones-play.md b/.changeset/small-phones-play.md new file mode 100644 index 000000000..742c7cdb1 --- /dev/null +++ b/.changeset/small-phones-play.md @@ -0,0 +1,5 @@ +--- +"@layerzerolabs/oft-solana-example": patch +--- + +update debug script