DeFi 协议集成
DeFi(去中心化金融)是 Web3 最具革命性的应用场景之一。理解如何与 DeFi 协议交互,是从代币开发者成长为 DeFi 开发者的关键一步。
在 Solana 生态中,主流的 DeFi 协议包括 Raydium、Orca、Marinade、Jupiter 等。这些协议的核心都是对代币的各种操作——交换、质押、借贷。掌握了 SPL Token 的基础,你就拥有了理解这些协议的钥匙。
本章将通过简化的示例,展示与 AMM(自动做市商)和借贷协议交互的基本模式。真实的协议实现会更复杂,但核心原理是相通的。
与 AMM(自动做市商)交互
AMM 是去中心化交易所的核心机制。与传统的订单簿模式不同,AMM 使用数学公式来确定资产价格,允许用户随时进行交易而无需等待对手方。
最经典的 AMM 模型是恒定乘积做市商(Constant Product Market Maker),其核心公式是 x * y = k,其中 x 和 y 分别是池子中两种代币的数量,k 是一个常数。
以下示例展示如何与 Raydium 风格的 AMM 进行代币交换的基本结构:
rustuse anchor_lang::prelude::*; use anchor_spl::token::{Token, TokenAccount}; // 简化的 swap 接口 #[derive(Accounts)] pub struct SwapTokens<'info> { pub user: Signer<'info>, #[account(mut)] pub user_token_in: Account<'info, TokenAccount>, #[account(mut)] pub user_token_out: Account<'info, TokenAccount>, #[account(mut)] pub pool_token_in: Account<'info, TokenAccount>, #[account(mut)] pub pool_token_out: Account<'info, TokenAccount>, /// CHECK: AMM 程序 pub amm_program: AccountInfo<'info>, pub token_program: Program<'info, Token>, } pub fn calculate_swap_output( amount_in: u64, reserve_in: u64, reserve_out: u64, fee_numerator: u64, fee_denominator: u64, ) -> u64 { // 扣除手续费后的输入量 let amount_in_with_fee = (amount_in as u128) .checked_mul((fee_denominator - fee_numerator) as u128) .unwrap(); // 恒定乘积公式: x * y = k // amount_out = (amount_in_with_fee * reserve_out) / (reserve_in * fee_denominator + amount_in_with_fee) let numerator = amount_in_with_fee .checked_mul(reserve_out as u128) .unwrap(); let denominator = (reserve_in as u128) .checked_mul(fee_denominator as u128) .unwrap() .checked_add(amount_in_with_fee) .unwrap(); (numerator / denominator) as u64 }
与借贷协议交互
借贷协议是 DeFi 的另一个核心组件。它允许用户存入资产赚取利息,或者抵押资产借出其他资产。Solana 上的 Solend、MarginFi 等协议都遵循类似的设计模式。
借贷协议的核心概念包括:
- 存款池:用户存入的资产被汇集到一个池子中
- 利率模型:根据资金利用率动态调整借贷利率
- 抵押因子:决定用户可以借出多少资产的比例
- 清算机制:当抵押率低于安全阈值时,允许清算人接管仓位
以下示例展示如何实现一个简化的存款功能:
rust#[account] pub struct UserDeposit { pub user: Pubkey, pub mint: Pubkey, pub deposited_amount: u64, pub deposit_time: i64, pub bump: u8, } #[derive(Accounts)] pub struct Deposit<'info> { #[account(mut)] pub user: Signer<'info>, pub mint: Account<'info, Mint>, #[account( init_if_needed, payer = user, space = 8 + 32 + 32 + 8 + 8 + 1, seeds = [b"deposit", user.key().as_ref(), mint.key().as_ref()], bump, )] pub user_deposit: Account<'info, UserDeposit>, #[account( mut, token::mint = mint, token::authority = user, )] pub user_token_account: Account<'info, TokenAccount>, #[account( mut, seeds = [b"pool", mint.key().as_ref()], bump, )] pub pool_token_account: Account<'info, TokenAccount>, pub system_program: Program<'info, System>, pub token_program: Program<'info, Token>, } pub fn deposit(ctx: Context<Deposit>, amount: u64) -> Result<()> { let clock = Clock::get()?; // 转移代币到池子 transfer( CpiContext::new( ctx.accounts.token_program.to_account_info(), Transfer { from: ctx.accounts.user_token_account.to_account_info(), to: ctx.accounts.pool_token_account.to_account_info(), authority: ctx.accounts.user.to_account_info(), }, ), amount, )?; // 更新用户存款记录 let deposit = &mut ctx.accounts.user_deposit; deposit.user = ctx.accounts.user.key(); deposit.mint = ctx.accounts.mint.key(); deposit.deposited_amount = deposit.deposited_amount.checked_add(amount).unwrap(); deposit.deposit_time = clock.unix_timestamp; deposit.bump = ctx.bumps.user_deposit; msg!("用户 {} 存入 {} 代币", ctx.accounts.user.key(), amount); Ok(()) }
补充:安全最佳实践
在构建涉及代币操作的程序时,安全性是第一优先级。一个小小的漏洞可能导致巨额资金损失。以下是一些经过实践检验的安全最佳实践。
权限验证
永远不要假设调用者的身份。每个涉及权限的操作都应该有明确的验证。
rust// 不安全的做法 - 缺少权限验证 pub fn unsafe_mint(ctx: Context<UnsafeMint>, amount: u64) -> Result<()> { // 直接铸造,没有检查调用者是否有权限 mint_to(...)?; Ok(()) } // 安全的做法 - 使用 Anchor 约束进行验证 #[derive(Accounts)] pub struct SafeMint<'info> { #[account( mut, seeds = [b"config", mint.key().as_ref()], bump = config.bump, has_one = authority @ TokenError::InvalidAuthority, // 自动验证 )] pub config: Account<'info, TokenConfig>, pub authority: Signer<'info>, // 必须签名 // ... }
数值溢出保护
Rust 在 release 模式下默认不检查整数溢出,这可能导致严重的安全问题。
rust// 不安全 - 可能溢出 let total = amount1 + amount2; // 安全 - 使用 checked 方法 let total = amount1.checked_add(amount2) .ok_or(TokenError::Overflow)?; // 或使用 saturating 方法(溢出时返回最大值) let total = amount1.saturating_add(amount2);
重入攻击防护
虽然 Solana 的执行模型使得传统的重入攻击更难实施,但在进行 CPI 调用时仍需谨慎。
rust// 推荐的模式:先更新状态,再进行外部调用 pub fn withdraw(ctx: Context<Withdraw>, amount: u64) -> Result<()> { let user_deposit = &mut ctx.accounts.user_deposit; // 1. 先检查 require!(user_deposit.amount >= amount, TokenError::InsufficientFunds); // 2. 再更新状态 user_deposit.amount = user_deposit.amount.checked_sub(amount).unwrap(); // 3. 最后执行转账 transfer(...)?; Ok(()) }
账户验证清单
在编写每个指令时,对照以下清单检查你的账户验证:
- 所有权验证:账户的 owner 是否正确?
- PDA 验证:PDA 的 seeds 是否包含所有必要的信息?
- Mint 匹配:涉及的 Token Account 是否都对应同一个 Mint?
- 签名验证:所有需要授权的操作是否都有正确的签名者?
- 余额检查:转账前是否验证了足够的余额?
- 状态检查:账户是否处于正确的状态(如未被冻结)?