You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Solana程序中基于PDA实现SPL Token跨钱包转账遇阻求助

Solana SPL Token转账失败与PDA问题排查

问题背景

有EVM开发经验,在Solana开发程序,需求是:钱包B与程序交互时,程序根据B支付的SOL数量,将钱包A的SPL Token转账给B。已给钱包A的Token账户授权程序ID,但转账失败;创建PDA后账户生成,但sent字段始终为false,怀疑PDA使用不当。

关联账户地址:HgtVDBozNRq4hbAhhsGBa4SJ7ttLoSWK7XLsp9w7MQ1x(devnet)

原代码

use anchor_lang::prelude::*;
use anchor_spl::token::{self, Token, TokenAccount, Transfer as SplTransfer};

declare_id!("73r6t8WnAeQCdksfFr7aJWfM2yZ1HeK6ZfnNXDeWhAGD");

#[program]
mod pda_test {
    use super::*;
    pub fn create_pda(ctx: Context<PdaTest>, _pda_key: String) -> Result<()> {
        let pda_account = &mut ctx.accounts.pda_account;
        pda_account.owner = *ctx.accounts.signer.key;
        pda_account.sent = false;
        Ok(())
    }
    pub fn transfer(ctx: Context<TransferTest>, _amount: u64) -> Result<()> {

        let token_program = &ctx.accounts.token_program;
        let pda_account = &mut ctx.accounts.pda_account;
        let owner_token_account = &ctx.accounts.owner_token_account;
        let buyer_token_account = &ctx.accounts.buyer_token_account;

        let sol_format = 10u64.pow(9);
        let token_format = 10u64.pow(6);
        let amount = ((_amount * 500) / sol_format) * token_format;
        token::transfer(CpiContext::new(token_program.to_account_info(), SplTransfer {
            from: owner_token_account.to_account_info(),
            to: buyer_token_account.to_account_info(),
            authority: pda_account.to_account_info(),
        }), amount)?;

        pda_account.sent = true;

        Ok(())
    }
}

#[derive(Accounts)]
#[instruction(_pda_key: String)]
pub struct PdaTest<'info> {
    #[account(
        init,
        seeds = [_pda_key.as_bytes().as_ref(), signer.key().as_ref()],
        bump,
        payer = signer,
        space = 10240
    )]
    pda_account: Account<'info, PdaAccount>,
    #[account(mut)]
    signer: Signer<'info>,
    system_program: Program<'info, System>,
}

#[derive(Accounts)]
pub struct TransferTest<'info> {
    #[account(mut)]
    buyer: Signer<'info>,
    #[account(mut)]
    pda_account: Account<'info, PdaAccount>,
    #[account(mut)]
    owner_token_account: Account<'info, TokenAccount>,
    #[account(mut)]
    buyer_token_account: Account<'info, TokenAccount>,
    token_program: Program<'info, Token>,
    system_program: Program<'info, System>,
}

#[account]
pub struct PdaAccount {
    pub owner: Pubkey,
    pub sent: bool,
}

核心问题与解决方案

1. 授权对象错误

你给钱包A的Token账户授权的是程序ID,但转账时用的是PDA作为authority,Token程序会验证authority是否被授权,因此转账失败。

  • 修正:将钱包A的Token账户授权给PDA地址,而非程序ID。

2. PDA权限验证缺失

transfer指令中未验证PDA的合法性,也未提供PDA的签名证明,导致Token程序拒绝转账请求,sent字段无法被更新为true。

  • 修正步骤:
    • 在TransferTest结构体中添加PDA的seeds验证和bump参数
    • 调用token::transfer时使用CpiContext::new_with_signer,传入PDA的seeds生成签名

3. 修改后的代码示例

修正TransferTest结构体

#[derive(Accounts)]
#[instruction(_pda_key: String, _bump: u8)]
pub struct TransferTest<'info> {
    #[account(mut)]
    buyer: Signer<'info>,
    #[account(
        mut,
        seeds = [_pda_key.as_bytes().as_ref(), pda_account.owner.as_ref()],
        bump = _bump
    )]
    pda_account: Account<'info, PdaAccount>,
    #[account(mut)]
    owner_token_account: Account<'info, TokenAccount>,
    #[account(mut)]
    buyer_token_account: Account<'info, TokenAccount>,
    token_program: Program<'info, Token>,
    system_program: Program<'info, System>,
}

修正transfer函数

pub fn transfer(ctx: Context<TransferTest>, _pda_key: String, _bump: u8, _amount: u64) -> Result<()> {
    let token_program = &ctx.accounts.token_program;
    let pda_account = &mut ctx.accounts.pda_account;
    let owner_token_account = &ctx.accounts.owner_token_account;
    let buyer_token_account = &ctx.accounts.buyer_token_account;

    // 安全计算token数量,避免整数溢出
    let sol_format = 10u64.pow(9);
    let token_format = 10u64.pow(6);
    let amount = _amount.checked_mul(500)
        .ok_or(ErrorCode::MathOverflow)?
        .checked_div(sol_format)
        .ok_or(ErrorCode::MathOverflow)?
        .checked_mul(token_format)
        .ok_or(ErrorCode::MathOverflow)?;

    // 生成PDA签名所需的seeds
    let seeds = &[
        _pda_key.as_bytes().as_ref(),
        pda_account.owner.as_ref(),
        &[_bump]
    ];
    let signer_seeds = &[&seeds[..]];

    // 带签名调用Token转账
    token::transfer(
        CpiContext::new_with_signer(
            token_program.to_account_info(),
            SplTransfer {
                from: owner_token_account.to_account_info(),
                to: buyer_token_account.to_account_info(),
                authority: pda_account.to_account_info(),
            },
            signer_seeds
        ),
        amount
    )?;

    pda_account.sent = true;

    Ok(())
}

添加自定义错误码

在程序顶部添加错误枚举:

#[error_code]
pub enum ErrorCode {
    #[msg("Integer overflow occurred")]
    MathOverflow,
}

4. 后续操作步骤

  1. 重新部署修改后的程序
  2. 用Pubkey::find_program_address获取PDA地址和bump值(seeds为_pda_key和钱包A的公钥)
  3. 调用Token的approve指令,将钱包A的Token账户授权给该PDA地址
  4. 调用transfer指令时传入_pda_key、_bump和支付的SOL数量(lamports)

内容的提问来源于stack exchange,提问作者BeycanDeveloper

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.26 07:17:11