Skip to content
Open
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/small-phones-play.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@layerzerolabs/oft-solana-example": patch
---

update debug script
5 changes: 5 additions & 0 deletions .changeset/small-trees-sleep.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@layerzerolabs/devtools-solana": patch
---

update assertValidSolanaAdmin
5 changes: 3 additions & 2 deletions examples/oft-solana/tasks/solana/debug.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down
116 changes: 91 additions & 25 deletions packages/devtools-solana/src/common/addresses.ts
Original file line number Diff line number Diff line change
@@ -1,47 +1,56 @@
import { DebugLogger, KnownErrors } from '@layerzerolabs/io-devtools'
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')

/**
* 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
}
}

/**
* 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.
* 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 isPossibleSquadsVault(connection: Connection, address: string): Promise<boolean> {
try {
const pubkey = new PublicKey(address)
const accountInfo = await connection.getAccountInfo(pubkey)
const isOnCurve = isOnCurveAddress(address)
if (isOnCurve) {
// a Squads Vault address is always off-curve
return false
}
export async function isSquadsV4Vault(eid: EndpointId, address: string): Promise<boolean | null> {
const logger = createLogger()

return accountInfo != null && accountInfo.owner.equals(SystemProgram.programId)
} catch (error) {
return false
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`)
}
}

export async function isSquadsV4Vault(address: string): Promise<boolean> {
// 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
Expand All @@ -50,25 +59,82 @@ export async function isSquadsV4Vault(address: string): Promise<boolean> {
}
}

/**
* Validates that an address is acceptable as a Solana admin (owner/delegate).
*
* Throws if:
* - 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 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<void> {
const logger = createLogger()
const pubkey = new PublicKey(address)

try {
logger.debug(`[assertValidSolanaAdmin] start address=${address}`)
const accountInfo = await connection.getAccountInfo(pubkey)
const isOnCurve = isOnCurveAddress(address)

// On-curve (regular address) = always valid
if (isOnCurve) {
logger.debug(`[assertValidSolanaAdmin] address=${address} valid: on-curve (regular address)`)
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
}

if (accountInfo != null && accountInfo.owner.equals(SQUADS_PROGRAM_ID)) {
// 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.`
)
}

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.`
// 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
Expand Down
Loading