Skip to content

Commit ba9762f

Browse files
authored
DEVREL-1012 fix squads vault detection (#1871)
1 parent 6c95cd3 commit ba9762f

4 files changed

Lines changed: 104 additions & 27 deletions

File tree

.changeset/small-phones-play.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@layerzerolabs/oft-solana-example": patch
3+
---
4+
5+
update debug script

.changeset/small-trees-sleep.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@layerzerolabs/devtools-solana": patch
3+
---
4+
5+
update assertValidSolanaAdmin

examples/oft-solana/tasks/solana/debug.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -147,8 +147,9 @@ task('lz:oft:solana:debug', 'Manages OFTStore and OAppRegistry information')
147147

148148
const tokenMetadata = await getSolanaTokenMetadata(umi, publicKey(oftStoreInfo.tokenMint), tokenProgramType)
149149

150-
const adminIsSquadsV4Vault = await isSquadsV4Vault(oftStoreInfo.admin)
151-
const delegateIsSquadsV4Vault = await isSquadsV4Vault(oAppRegistryInfo?.delegate?.toBase58())
150+
// Note: isSquadsV4Vault only works on mainnet
151+
const adminIsSquadsV4Vault = await isSquadsV4Vault(eid as number, oftStoreInfo.admin)
152+
const delegateIsSquadsV4Vault = await isSquadsV4Vault(eid as number, oAppRegistryInfo?.delegate?.toBase58())
152153

153154
const printOftStore = async () => {
154155
DebugLogger.header('OFT Store Information')

packages/devtools-solana/src/common/addresses.ts

Lines changed: 91 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,47 +1,56 @@
1-
import { DebugLogger, KnownErrors } from '@layerzerolabs/io-devtools'
1+
import { DebugLogger, KnownErrors, createModuleLogger } from '@layerzerolabs/io-devtools'
2+
import { EndpointId } from '@layerzerolabs/lz-definitions'
23
import { Connection, PublicKey, SystemProgram } from '@solana/web3.js'
3-
import { PROGRAM_ID as SQUADS_PROGRAM_ID } from '@sqds/multisig'
4+
import { PROGRAM_ID as SQUADS_V4_PROGRAM_ID } from '@sqds/multisig'
5+
6+
// Squads V3 program ID - legacy, not ideal but we allow it
7+
const SQUADS_V3_PROGRAM_ID = new PublicKey('SMPLecH534NA9acpos4G6x7uf3LWbCAwZQE9e8ZekMu')
8+
9+
const createLogger = () => createModuleLogger('Solana addresses')
410

511
/**
612
* 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.
713
*/
814
export function isOnCurveAddress(address: string): boolean {
15+
const logger = createLogger()
916
try {
10-
return PublicKey.isOnCurve(new PublicKey(address).toBytes())
17+
const result = PublicKey.isOnCurve(new PublicKey(address).toBytes())
18+
logger.debug(`[isOnCurveAddress] address=${address} onCurve=${result}`)
19+
return result
1120
} catch {
21+
logger.debug(`[isOnCurveAddress] address=${address} invalid public key`)
1222
return false
1323
}
1424
}
1525

1626
/**
17-
* Note: This was created before the existence of the Squads isVault endpoint was known.
18-
* Returns true if the provided address could be a Squads vault PDA.
27+
* Checks if an address is a Squads V4 vault using the Squads API.
28+
* Only works on Solana mainnet (30168). Returns null for testnet (40168).
29+
* Throws for any other EID.
1930
*/
20-
export async function isPossibleSquadsVault(connection: Connection, address: string): Promise<boolean> {
21-
try {
22-
const pubkey = new PublicKey(address)
23-
const accountInfo = await connection.getAccountInfo(pubkey)
24-
const isOnCurve = isOnCurveAddress(address)
25-
if (isOnCurve) {
26-
// a Squads Vault address is always off-curve
27-
return false
28-
}
31+
export async function isSquadsV4Vault(eid: EndpointId, address: string): Promise<boolean | null> {
32+
const logger = createLogger()
2933

30-
return accountInfo != null && accountInfo.owner.equals(SystemProgram.programId)
31-
} catch (error) {
32-
return false
34+
if (eid === EndpointId.SOLANA_V2_TESTNET) {
35+
logger.debug(`[isSquadsV4Vault] eid=${eid} is testnet, returning null (API only works on mainnet)`)
36+
return null
37+
}
38+
39+
if (eid !== EndpointId.SOLANA_V2_MAINNET) {
40+
throw new Error(`[isSquadsV4Vault] unsupported eid=${eid}, only Solana mainnet (30168) is supported`)
3341
}
34-
}
3542

36-
export async function isSquadsV4Vault(address: string): Promise<boolean> {
3743
// https://docs.squads.so/main/development/api/vault-check
3844
// 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.
3945
const response = await fetch(
4046
`https://4fnetmviidiqkjzenwxe66vgoa0soerr.lambda-url.us-east-1.on.aws/isSquad/${address}`
4147
)
4248
const data = await response.json()
49+
logger.debug(
50+
`[isSquadsV4Vault] address=${address} isSquad=${data?.isSquad} version=${data?.version} status=${response.status}`
51+
)
4352
if (data.isSquad && data.version != 'v4') {
44-
console.warn(`${address} is a Squads Vault but not v4`)
53+
logger.warn(`${address} is a Squads Vault but not v4`)
4554
return false
4655
} else if (data.isSquad && data.version === 'v4') {
4756
return true
@@ -50,25 +59,82 @@ export async function isSquadsV4Vault(address: string): Promise<boolean> {
5059
}
5160
}
5261

62+
/**
63+
* Validates that an address is acceptable as a Solana admin (owner/delegate).
64+
*
65+
* Throws if:
66+
* - Address is off-curve AND account exists AND owned by Squads V4 Program (multisig account, not vault)
67+
* - Address is off-curve AND account exists AND owned by unrecognized program
68+
*
69+
* Does NOT throw (valid) if:
70+
* - Address is on-curve (regular Solana address)
71+
* - Address is off-curve AND account does not exist (possibly unfunded Squads Vault)
72+
* - Address is off-curve AND account exists AND owned by System Program (funded Squads Vault)
73+
* - Address is off-curve AND account exists AND owned by Squads V3 Program (legacy, allowed with warning)
74+
*/
5375
export async function assertValidSolanaAdmin(connection: Connection, address: string): Promise<void> {
76+
const logger = createLogger()
5477
const pubkey = new PublicKey(address)
5578

5679
try {
80+
logger.debug(`[assertValidSolanaAdmin] start address=${address}`)
5781
const accountInfo = await connection.getAccountInfo(pubkey)
82+
const isOnCurve = isOnCurveAddress(address)
83+
84+
// On-curve (regular address) = always valid
85+
if (isOnCurve) {
86+
logger.debug(`[assertValidSolanaAdmin] address=${address} valid: on-curve (regular address)`)
87+
return
88+
}
89+
90+
// From here: off-curve
91+
const accountExists = accountInfo != null
92+
93+
// Off-curve + no account = possibly unfunded Squads Vault
94+
if (!accountExists) {
95+
logger.debug(
96+
`[assertValidSolanaAdmin] address=${address} valid: off-curve, account does not exist (possibly unfunded Squads Vault)`
97+
)
98+
return
99+
}
58100

59-
if (accountInfo != null && accountInfo.owner.equals(SQUADS_PROGRAM_ID)) {
101+
// From here: off-curve + account exists
102+
const owner = accountInfo.owner
103+
104+
// Owned by Squads V4 Program = multisig account (invalid, should use vault address)
105+
if (owner.equals(SQUADS_V4_PROGRAM_ID)) {
106+
logger.debug(
107+
`[assertValidSolanaAdmin] address=${address} invalid: off-curve, account exists, owned by Squads V4 Program (multisig account)`
108+
)
60109
DebugLogger.printErrorAndFixSuggestion(KnownErrors.SOLANA_OWNER_OR_DELEGATE_CANNOT_BE_MULTISIG_ACCOUNT)
61110
throw new Error(
62111
`Invalid owner/delegate address ${address}. This is a Squads multisig account. Use the vault address instead.`
63112
)
64113
}
65114

66-
if (!isOnCurveAddress(address) && !(await isPossibleSquadsVault(connection, address))) {
67-
DebugLogger.printErrorAndFixSuggestion(KnownErrors.SOLANA_INVALID_OWNER_OR_DELEGATE)
68-
throw new Error(
69-
`Invalid owner/delegate address ${address}. Must be a valid on-curve address or a Squads Vault PDA.`
115+
// Owned by System Program = funded Squads Vault (valid)
116+
if (owner.equals(SystemProgram.programId)) {
117+
logger.debug(
118+
`[assertValidSolanaAdmin] address=${address} valid: off-curve, account exists, owned by System Program (funded Squads Vault)`
70119
)
120+
return
71121
}
122+
123+
// Owned by Squads V3 Program = legacy, not ideal but allowed
124+
if (owner.equals(SQUADS_V3_PROGRAM_ID)) {
125+
logger.warn(
126+
`[assertValidSolanaAdmin] address=${address} valid: off-curve, account exists, owned by Squads V3 Program (legacy multisig - consider migrating to V4)`
127+
)
128+
return
129+
}
130+
131+
// Owned by unrecognized program = invalid
132+
logger.debug(
133+
`[assertValidSolanaAdmin] address=${address} invalid: off-curve, account exists, owned by unrecognized program ${owner.toBase58()}`
134+
)
135+
throw new Error(
136+
`Invalid owner/delegate address ${address}. Account is owned by unrecognized program ${owner.toBase58()}.`
137+
)
72138
} catch (error) {
73139
if (error instanceof Error) {
74140
throw error

0 commit comments

Comments
 (0)