将资金发送到错误地址可能导致永久损失。地址验证确保您和您的用户只向能够正确接收和访问资金的地址发送。如果您运营交易所,建议执行这些检查,并在发送前让用户验证地址。
验证方式取决于您发送的内容:
- SPL 代币具有一定的自我保护机制。Token Program 会拒绝账户与预期铸币地址不匹配的转账,因此误转代币的操作会失败,且不会造成资金损失。本页面大部分内容涵盖 SPL 代币的发送。
- 原生 SOL 没有此类保护机制。System Program 的转账可以成功进入任何账户,因此发送给错误接收方会永久锁定 SOL。请参阅 发送原生 SOL。
请参阅 Solana 支付的工作原理 了解核心支付概念。
了解 Solana 地址
Solana 账户有两种类型的地址:曲线上地址和曲线外地址。
曲线上地址
标准地址是来自 Ed25519 keypair 的公钥。这些地址:
- 具有可对交易进行签名的对应私钥
- 用作钱包地址
曲线外地址(PDA)
Program Derived Address 由程序 ID 和种子确定性派生而来。这些地址:
- 没有对应的私钥
- 只能由派生该地址的程序进行签名
由程序控制的钱包,通常称为智能钱包或智能账户,拥有离曲线地址。本指南全程使用"智能钱包"这一术语。
支付中的账户类型
使用地址从网络获取账户,检查其程序所有者和账户类型,以确定如何处理该地址。
了解一个地址是曲线上还是曲线外,并不能告诉您该账户的类型、哪个程序拥有它,或者该地址上是否存在账户。您必须从网络中获取该账户才能确定这些详细信息。
System Program 账户(钱包)
由 System Program 拥有的账户是标准钱包。要向钱包发送 SPL 代币,请派生并使用其 Associated Token Account (ATA)。
派生出 ATA 地址后,请检查该 token account 是否已存在于链上。如果 ATA 不存在,你可以在与转账相同的交易中加入一条指令,用于创建收款方的 token account。不过,这需要为新的 token account 支付 rent。由于收款方拥有该 ATA,发送方无法收回为 rent 支付的 SOL。
系统账户可以存在于曲线外。如果您的用户想将代币发送到离曲线地址,建议提示用户确认其是否打算将资金发送到智能钱包。
如果没有保护措施,补贴创建 ATA 的机制可能会被利用。恶意 用户可以请求一笔转账,让你出资创建其 ATA,然后关闭 该 ATA 以取回 rent SOL,并重复此操作。
Token Accounts
Token accounts 由 Token Program 或 Token-2022 Program 拥有,并持有代币余额。如果你收到的地址由某个 token program 拥有,那么在发送前,你应验证该账户是 token account(而不是 mint account),并且与预期的代币 mint account 相匹配。
Token Programs 会自动验证转账中的两个 token accounts 是否持有同一 mint 的代币。如果验证失败,交易会被拒绝,且不会损失任何资金。
Mint Accounts
Mint accounts 用于跟踪特定代币的供应量和元数据。Mint accounts 也由 Token Programs 拥有,但 不是 代币转账的有效接收方。尝试向 mint 地址发送代币会导致交易失败,但不会损失任何资金。
其他账户
由其他程序拥有的账户是有效的,但受相应拥有程序所设定的权限规则约束。建议提示用户确认其是否打算将资金发送到智能钱包。
发送原生 SOL
上述分类决定了 SPL 代币的去向。原生 SOL 的要求更为严格:唯一安全的接收方是 System Program 钱包(或一个尚未注资、位于曲线上的地址,该地址将成为 System Program 钱包)。
System Program 转账会将 lamport 添加到任意账户,包括铸币账户、token account、program account 和 PDA。lamport 只能由账户的所属程序转出,因此将 SOL 发送至错误的接收方可能导致资金永久丢失。
与 SPL 代币转账不同,当接收方为意外地址时,交易不会失败。
在发送原生 SOL 时,只有 IS_WALLET 的结果是可接受的。IS_TOKEN_ACCOUNT
则不可接受:token
account 持有 SPL 代币,发送至该地址的 SOL 将超出发送方的控制范围。
这是 SOL 丢失的常见方式:用户将代币的铸币地址(或 program account 地址)粘贴到 SOL 提款操作中。转账成功执行,但 SOL 无法找回。在签署 SOL 转账前,请务必对接收方进行分类验证。
验证流程
下图展示了验证地址的参考决策树:
获取账户
使用该地址从网络中获取账户详情。
账户不存在
如果该地址不存在账户,请检查地址是在曲线上还是曲线外:
-
曲线外(PDA):在创建 ATA 或发送 SPL 代币之前,要求收款方明确确认。在没有现有账户的情况下,您无法仅凭地址判断是哪个程序派生了此 PDA,也无法确认收款方的程序是否能访问其 ATA。
-
曲线上:这是一个尚未充值的有效钱包地址(公钥)。派生 ATA,检查其是否存在,然后向其发送代币。您必须制定策略,决定是否在 ATA 不存在时为其创建提供资金。
账户存在
如果账户存在,请检查哪个程序拥有它:
-
System Program:这是一个标准钱包。派生 ATA,检查其是否存在,然后向其发送代币。您必须制定策略,决定是否在 ATA 不存在时为其创建提供资金。
-
Token Program / Token-2022:验证该账户是 token account(而非 mint account),并且持有您打算发送的代币(mint)。如果有效,直接向此地址发送代币。如果是 mint account 或针对其他 mint 的 token account,则拒绝该地址。
-
其他程序:多签钱包等程序是 token account 的合法拥有者。要求用户确认其打算发送到智能钱包。
一些常见的智能钱包:
演示
以下示例仅展示地址验证逻辑,本代码仅供参考说明之用。
演示未展示如何派生 ATA 或构建发送代币的交易。请参阅 token account 和 token transfer 文档获取示例代码。
以下演示包含三种可能的结果:
| 结果 | 含义 | 操作 |
|---|---|---|
IS_WALLET | 有效的钱包地址 | 派生并发送至 associated token account |
IS_TOKEN_ACCOUNT | 有效的 token account | 直接向该地址发送代币 |
USER_VERIFICATION | 地址需要收款方确认 | 在发送 SPL 代币或为 ATA 充值之前,要求明确审核 |
REJECT | 无效地址 | 请勿发送 |
然后,它将每个结果映射到每种资产的可接受性:canReceiveNativeSol(仅限钱包)和 canReceiveSplToken(钱包或 token account)。USER_VERIFICATION 结果不会被自动接受;您的应用程序应在发送 SPL 代币之前让用户确认其意图。token account 返回 IS_TOKEN_ACCOUNT,因此可以接收 SPL 代币,但不能接收原生 SOL——正是这一区别防止了 SOL 被锁定。
/*** Validates an input address and classifies it as a wallet, token account,* address requiring recipient verification, or invalid.** @param inputAddress - The address to validate* @param rpc - Optional RPC client (defaults to mainnet)* @returns Classification result:* - IS_WALLET: Valid wallet address* - IS_TOKEN_ACCOUNT: Valid token account* - USER_VERIFICATION: Require explicit review before sending SPL tokens* - REJECT: Invalid address for transfers*/export async function validateAddress(inputAddress: Address,rpc: Rpc<GetAccountInfoApi> = defaultRpc): Promise<ValidationResult> {const account = await fetchJsonParsedAccount(rpc, inputAddress);// Log the account data for democonsole.log("\nAccount:", account);// Account doesn't exist onchainif (!account.exists) {// An unfunded PDA may belong to a smart wallet, but the address// alone cannot prove that its program can access a newly-created ATA.if (isOffCurveAddress(inputAddress)) {return {type: "USER_VERIFICATION",reason:"Unfunded PDA: confirm the recipient is using a smart wallet and controls this address"};}// On-curve = valid keypair address, treat as unfunded walletreturn { type: "IS_WALLET" };}// Account exists, check program ownerconst owner = account.programAddress;// A System Program account can be a standard wallet or an off-curve smart// wallet. Require verification before automatically sending to the// latter.if (owner === SYSTEM_PROGRAM) {if (isOffCurveAddress(inputAddress)) {return {type: "USER_VERIFICATION",reason:"Off-curve System Program account: confirm recipient is using a smart wallet"};}return { type: "IS_WALLET" };}// Token Program or Token-2022, check if token accountif (owner === TOKEN_PROGRAM || owner === TOKEN_2022_PROGRAM) {const accountType = (account.data as { parsedAccountMeta?: { type?: string } }).parsedAccountMeta?.type;if (accountType === "account") {return { type: "IS_TOKEN_ACCOUNT" };}// Reject if not a token account (mint account)return {type: "REJECT",reason: "Not a token account"};}// An account owned by another program can be a smart wallet.return {type: "USER_VERIFICATION",reason:"Program-owned account: confirm the recipient is using a smart wallet"};}/*** Native SOL is only safe to send to a wallet. Any other account locks it.*/function canReceiveNativeSol(result: ValidationResult): boolean {return result.type === "IS_WALLET";}/*** SPL tokens can go to a wallet (via its ATA) or directly to a token account.*/function canReceiveSplToken(result: ValidationResult): boolean {return result.type === "IS_WALLET" || result.type === "IS_TOKEN_ACCOUNT";}// =============================================================================// Examples// =============================================================================
Is this page helpful?