欢迎订阅专栏 :10分钟Solana-性能web3
在 Solana 生态中,跨合约调用 通常被称为 跨程序调用(CPI,Cross-Program Invocation) 。它允许一个 Solana 程序(智能合约)调用另一个程序的公开指令,从而实现可组合性------这是 Solana 和 DeFi 的核心特性之一。
🌐 为什么需要 CPI?
Solana 的程序是无状态 的(逻辑与数据分离),任何程序都无法直接修改另一个程序的数据。但通过 CPI,一个程序可以代表其自身或由其派生的 PDA,向另一个程序发起一个完整的指令调用,并传递必要的账户和参数。
CPI 让开发者能够:
- 重用已有的协议:例如,从你自己的程序中调用 SPL Token 程序进行转账。
- 组合多个操作:在一个交易中原子性地完成多个步骤(例如,在 DEX 中交换代币,并在同一个程序中更新用户状态)。
- 实现代理签名:利用 PDA 代表程序进行授权操作(如转移 NFT)。
⚙️ CPI 的两种模式
根据被调用程序返回结果的方式,CPI 分为两种:
| 类型 | 描述 | 使用场景 |
|---|---|---|
| 同步 CPI | 当前程序暂停执行,等待被调用程序执行完毕并返回结果后,再继续执行后续代码。 | 绝大多数情况,如转账、代币操作等。 |
| 异步 CPI | 当前程序继续执行 ,不等待被调用程序完成。目前 Solana 不支持异步 CPI。 | -- |
🧩 如何在程序内发起 CPI
在 Solana 中,发起 CPI 可以通过原生 Rust SDK 或使用 Anchor 框架 提供的封装。
1. 使用原生 solana_program SDK
原生方式直接使用 invoke 或 invoke_signed 函数。
rust
use solana_program::{
account_info::AccountInfo,
program::invoke,
system_instruction,
pubkey::Pubkey,
};
// 调用系统程序转账 SOL
pub fn transfer_sol(
from: &AccountInfo,
to: &AccountInfo,
amount: u64,
) -> ProgramResult {
let ix = system_instruction::transfer(from.key, to.key, amount);
invoke(&ix, &[from.clone(), to.clone()])
}
invoke:用于普通 CPI,账户签名由当前程序外的签名者(如用户)提供。invoke_signed:用于 PDA 签名,需要提供 PDA 的种子和bump。
rust
// 使用 PDA 作为签名者调用 SPL Token 程序的 mint_to 指令
let seeds = &[b"mint_authority".as_ref(), &[bump]];
invoke_signed(
&spl_token::instruction::mint_to(
token_program.key,
mint_account.key,
destination.key,
authority.key,
&[], // 无额外 signers
amount,
)?,
&[mint_account.clone(), destination.clone(), authority.clone()],
&[seeds],
)?;
2. 使用 Anchor 框架
Anchor 提供了更高级的封装,自动处理账户序列化和安全检查。
cpi模块:Anchor 为每个依赖的程序生成一个 CPI 模块,其中包含了该程序所有指令的辅助函数。- 调用方式 :在 Anchor 程序内部,通过
CpiContext和ctx.accounts发起调用。
示例:调用 SPL Token 程序 (需添加 anchor-spl 依赖)
rust
use anchor_spl::token::{Token, MintTo, mint_to};
#[program]
pub mod my_project {
use super::*;
pub fn mint_tokens(ctx: Context<MintTokens>, amount: u64) -> Result<()> {
let cpi_program = ctx.accounts.token_program.to_account_info();
let cpi_accounts = MintTo {
mint: ctx.accounts.mint.to_account_info(),
to: ctx.accounts.destination.to_account_info(),
authority: ctx.accounts.mint_authority.to_account_info(),
};
let seeds = &[b"mint_auth".as_ref(), &[ctx.bumps.mint_authority]];
let cpi_ctx = CpiContext::new_with_signer(cpi_program, cpi_accounts, &[seeds]);
mint_to(cpi_ctx, amount)?;
Ok(())
}
}
关键点:
CpiContext::new_with_signer用于需要 PDA 签名的场景。- Anchor 会自动验证
ctx.bumps是否有效。
🔐 安全性要点
CPI 是 Solana 程序中最容易引入漏洞的环节之一,务必注意以下几点:
-
目标程序地址验证 :确保你调用的是预期的程序(例如,检查其
key是否等于已知的可靠程序 ID)。否则可能被重定向到恶意程序。rustif ctx.accounts.token_program.key != &spl_token::ID { return Err(ProgramError::InvalidArgument); } -
账户所有权验证:在 CPI 之前,确保传入的账户是调用方程序拥有的,避免被攻击者伪造。
-
PDA 签名验证 :使用
invoke_signed或 Anchor 的CpiContext::new_with_signer时,务必传入正确的种子和 bump。不正确的种子可能导致程序非法使用他人的 PDA。 -
重入防护 :虽然 Solana 的 CPI 是同步的(当前程序会等待被调用程序返回),但仍需注意状态一致性。例如,确保在调用外部程序之前已完成所有内部状态更新,防止重入攻击(尽管 Solana 的账户模型天然降低了部分风险,但并非绝对)。
-
计算单元 (CU) 消耗:CPI 会增加交易的计算成本。过多的嵌套调用可能导致交易超出 CU 上限,提前做好预估。
📦 常用 CPI 场景与库
anchor-spl:与 SPL Token、Associated Token Account 等官方程序交互的标准库。pump-fun-sdk:与 pump.fun 协议交互的社区库。- 自定义程序:只要另一个程序公开了 IDL,你就能通过 Anchor 生成对应的 CPI 模块。
🧪 测试 CPI
在 Anchor 测试中,你可以通过 program 对象的 cpi 方法进行模拟调用,或者利用 solana-program-test 框架进行集成测试。通常,推荐使用 主网分叉(fork) 测试来验证 CPI 的行为与主网一致。
💎 总结
跨程序调用(CPI)是 Solana 可组合性的基石。它让开发者能够:
- 重用现有的协议逻辑
- 通过 PDA 实现安全授权
- 组合多个操作完成复杂业务
最佳实践:
- 使用 Anchor 提供的
CpiContext和cpi模块,减少手动序列化错误。 - 验证目标程序地址,防止重定向攻击。
- 仔细管理 PDA 签名,确保种子正确。
- 在测试网进行充分的 CPI 测试,涵盖正常路径和错误路径。
CPI 是一个相对进阶的话题,但一旦掌握,它将为你打开构建复杂 Solana 应用的大门。如果想了解如何与特定协议进行 CPI,或遇到具体的问题,欢迎继续提问。