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. 后续操作步骤
- 重新部署修改后的程序
- 用
Pubkey::find_program_address获取PDA地址和bump值(seeds为_pda_key和钱包A的公钥) - 调用Token的
approve指令,将钱包A的Token账户授权给该PDA地址 - 调用
transfer指令时传入_pda_key、_bump和支付的SOL数量(lamports)
内容的提问来源于stack exchange,提问作者BeycanDeveloper
相关产品推荐
相关产品推荐

