iSwap —— ETF Token 指令详解

本文档详细拆解 iSwap 协议中 ETF Token 相关指令的代码实现,包括 ETF 的创建、铸造、销毁以及用户系统。


概述

iSwap 是一个 ETF Token 资产管理合约,允许用户创建代表多资产组合的 ETF Token。核心机制:

  • 创建 ETF:定义资产组合及权重
  • 铸造 ETF:用户存入底层资产,获得 ETF Token
  • 销毁 ETF:用户销毁 ETF Token,按比例赎回底层资产
1
2
用户资产 ──存入──▶ ETF PDA ──铸造──▶ ETF Token
用户资产 ◀──赎回── ETF PDA ◀──销毁── ETF Token

1. 数据结构

1.1 EtfToken 账户

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// programs/iswap/src/states/etf_token.rs
use anchor_lang::prelude::*;

#[account]
#[derive(InitSpace)]
pub struct EtfToken {
pub mint_account: Pubkey, // ETF Token 的 Mint 地址
pub creator: Pubkey, // ETF 创建者
pub create_at: i64, // 创建时间戳
#[max_len(50)]
pub description: String, // ETF 描述(最多 50 字符)
#[max_len(10)]
pub assets: Vec<EtfAsset>, // 包含的资产组合(最多 10 个)
}

impl EtfToken {
pub const SEED_PREFIX: &'static str = "etf_token_v3"; // PDA 种子前缀
pub const TOKEN_DECIMALS: u8 = 9; // ETF Token 精度
}

1.2 EtfAsset 资产结构

1
2
3
4
5
6
7
// programs/iswap/src/states/etf_token.rs
#[account]
#[derive(InitSpace)]
pub struct EtfAsset {
pub token: Pubkey, // 资产的 Mint 地址
pub weight: u16, // 权重百分比(0-100)
}

权重说明

  • weight = 50 表示该资产占 50%
  • 所有资产的 weight 之和应为 100

1.3 UserAccount 用户账户

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// programs/iswap/src/states/user.rs
use anchor_lang::prelude::*;

#[account]
#[derive(InitSpace)]
pub struct UserAccount {
pub owner: Pubkey, // 账户所有者
pub is_frozen: bool, // 是否冻结
pub created_at: i64, // 创建时间戳
pub inviter_code: Option<Pubkey>, // 邀请人 PDA 地址
pub direct_inviter: Option<Pubkey>, // 直接邀请人公钥
#[max_len(20)]
pub nickname: String, // 昵称(最多 20 字符)
#[max_len(200)]
pub avatar: String, // 头像 URL(最多 200 字符)
pub padding: [u64; 10], // 预留空间,用于未来扩展
}

impl UserAccount {
pub const SEED_PREFIX: &'static str = "userAccount_v2"; // PDA 种子前缀
}

1.4 事件定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// programs/iswap/src/states/events.rs
use anchor_lang::prelude::*;

#[event]
pub struct EtfTokenCreateEvent {
pub etf_token_mint: Pubkey, // 创建的 ETF Token Mint 地址
pub creator: Pubkey, // 创建者
}

#[event]
pub struct EtfTokenMintEvent {
pub etf_token_mint: Pubkey, // ETF Token Mint 地址
pub authority: Pubkey, // 铸造者
pub lamports: f64, // 铸造数量
}

#[event]
pub struct EtfTokenBurnEvent {
pub etf_token_mint: Pubkey, // ETF Token Mint 地址
pub authority: Pubkey, // 销毁者
pub lamports: f64, // 销毁数量
}

#[event]
pub struct UserInitialized {
pub user: Pubkey, // 用户账户 PDA
pub inviter_code: Option<Pubkey>, // 邀请人
pub nickname: String, // 昵称
pub created_at: i64, // 创建时间
}

2. etf_create

创建一个新的 ETF Token,定义资产组合和权重。

2.1 指令参数

1
2
3
4
5
6
7
8
9
// programs/iswap/src/instructions/etf_token_create.rs
#[account]
pub struct EtfTokenArgs {
pub name: String, // Token 名称
pub symbol: String, // Token 符号
pub description: String, // ETF 描述
pub url: String, // 元数据 URI
pub assets: Vec<EtfAsset>, // 资产组合及权重
}

2.2 账户约束

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
// programs/iswap/src/instructions/etf_token_create.rs
use anchor_lang::prelude::*;
use anchor_spl::{
metadata::{
create_metadata_accounts_v3, mpl_token_metadata::types::DataV2,
CreateMetadataAccountsV3, Metadata,
},
token::{Mint, Token},
};

#[derive(Accounts)]
#[instruction(args: EtfTokenArgs)]
pub struct EtfTokenCreate<'info> {
/// ETF Token 信息账户(PDA)
/// Seeds: ["etf_token_v3", etf_token_mint_account]
#[account(
init_if_needed,
payer = authority,
space = 8 + EtfToken::INIT_SPACE,
seeds = [
EtfToken::SEED_PREFIX.as_bytes(),
etf_token_mint_account.key().as_ref(),
],
bump,
)]
pub etf_token_info: Account<'info, EtfToken>,

/// Metaplex Metadata 账户(PDA)
/// Seeds: ["metadata", token_metadata_program, etf_token_mint_account]
/// CHECK: Validate address by deriving pda
#[account(
mut,
seeds = [
b"metadata",
token_metadata_program.key().as_ref(),
etf_token_mint_account.key().as_ref()
],
bump,
seeds::program = token_metadata_program.key(),
)]
pub metadata_account: UncheckedAccount<'info>,

/// ETF Token Mint 账户(PDA)
/// Seeds: ["etf_token_v3", symbol]
/// 精度固定为 9
#[account(
init,
payer = authority,
seeds = [
EtfToken::SEED_PREFIX.as_bytes(),
args.symbol.as_bytes(),
],
bump,
mint::decimals = EtfToken::TOKEN_DECIMALS, // 9
mint::authority = etf_token_info.key(), // PDA 作为 Mint Authority
)]
pub etf_token_mint_account: Account<'info, Mint>,

pub rent: Sysvar<'info, Rent>,

/// 创建者,支付所有费用
#[account(mut)]
pub authority: Signer<'info>,

pub token_metadata_program: Program<'info, Metadata>,
pub token_program: Program<'info, Token>,
pub system_program: Program<'info, System>,
}

2.3 指令逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
// programs/iswap/src/instructions/etf_token_create.rs
pub fn etf_token_create(ctx: Context<EtfTokenCreate>, args: EtfTokenArgs) -> Result<()> {
// 1. 构建 PDA 签名种子
let m = ctx.accounts.etf_token_mint_account.key();
let signer_seeds: &[&[&[u8]]] = &[&[
EtfToken::SEED_PREFIX.as_bytes(),
m.as_ref(),
&[ctx.bumps.etf_token_info],
]];

// 2. 创建 Metaplex Metadata 账户
create_metadata_accounts_v3(
CpiContext::new_with_signer(
ctx.accounts.token_metadata_program.to_account_info(),
CreateMetadataAccountsV3 {
metadata: ctx.accounts.metadata_account.to_account_info(),
mint: ctx.accounts.etf_token_mint_account.to_account_info(),
mint_authority: ctx.accounts.etf_token_info.to_account_info(), // PDA 作为权限
update_authority: ctx.accounts.etf_token_info.to_account_info(),
payer: ctx.accounts.authority.to_account_info(),
system_program: ctx.accounts.system_program.to_account_info(),
rent: ctx.accounts.rent.to_account_info(),
},
signer_seeds,
),
DataV2 {
name: args.name.to_string(),
symbol: args.symbol.to_string(),
uri: args.url.to_string(),
seller_fee_basis_points: 0,
creators: None,
collection: None,
uses: None,
},
false, // is_mutable
true, // update_authority_is_signer
None, // collection_details
)?;

msg!("Token mint created successfully.");

// 3. 初始化 EtfToken 账户数据
ctx.accounts.etf_token_info.set_inner(EtfToken {
creator: ctx.accounts.authority.key(),
mint_account: ctx.accounts.etf_token_mint_account.key(),
description: args.description,
assets: args.assets,
create_at: Clock::get()?.unix_timestamp,
});

// 4. 发出创建事件
emit!(EtfTokenCreateEvent {
etf_token_mint: ctx.accounts.etf_token_mint_account.key(),
creator: ctx.accounts.authority.key(),
});

Ok(())
}

2.4 PDA 种子汇总

账户 Seeds
etf_token_info ["etf_token_v3", etf_token_mint_account]
etf_token_mint_account ["etf_token_v3", symbol]
metadata_account ["metadata", token_metadata_program, etf_token_mint_account]

3. etf_mint

用户存入底层资产,铸造 ETF Token。

3.1 账户约束

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// programs/iswap/src/accounts_ix/etf_token_transaction.rs
use anchor_lang::prelude::*;
use anchor_spl::{
associated_token::AssociatedToken,
token_interface::{Mint, TokenAccount, TokenInterface}
};

#[derive(Accounts)]
pub struct EtfTokenTransaction<'info> {
/// ETF Token 信息账户(PDA)
/// Seeds: ["etf_token_v3", etf_token_mint_account]
#[account(
seeds = [
EtfToken::SEED_PREFIX.as_bytes(),
etf_token_mint_account.key().as_ref(),
],
bump,
)]
pub etf_token_info: Account<'info, EtfToken>,

/// ETF Token Mint 账户
#[account(mut)]
pub etf_token_mint_account: InterfaceAccount<'info, Mint>,

/// 用户的 ETF Token ATA(自动创建)
#[account(
init_if_needed,
payer = authority,
associated_token::mint = etf_token_mint_account,
associated_token::authority = authority,
)]
pub etf_token_ata: Box<InterfaceAccount<'info, TokenAccount>>,

/// 用户(铸造者)
#[account(mut)]
pub authority: Signer<'info>,

pub token_program: Interface<'info, TokenInterface>,
pub associated_token_program: Program<'info, AssociatedToken>,
pub system_program: Program<'info, System>,
}

remaining_accounts 说明

铸造时需要通过 remaining_accounts 传入每个底层资产的相关账户:

  • 用户的资产 ATA
  • ETF PDA 的资产 ATA
  • 资产的 Mint 账户
  • 对应的 Token Program(SPL Token 或 Token-2022)

3.2 指令逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
// programs/iswap/src/instructions/etf_token_mint.rs
use std::collections::HashMap;
use anchor_lang::prelude::*;
use anchor_spl::{
associated_token::get_associated_token_address,
token::{mint_to, spl_token, MintTo},
token_2022::{self, spl_token_2022::{extension::StateWithExtensions, state::Mint as Mint2022}},
};

pub fn etf_token_mint<'info>(
ctx: Context<'_, '_, '_, 'info, EtfTokenTransaction<'info>>,
lamports: f64, // 铸造数量(以 1 个 ETF Token 为单位)
) -> Result<()> {
// 1. 构建 PDA 签名种子
let m = ctx.accounts.etf_token_mint_account.key();
let signer_seeds: &[&[&[u8]]] = &[&[
EtfToken::SEED_PREFIX.as_bytes(),
m.as_ref(),
&[ctx.bumps.etf_token_info],
]];

// 2. 将 remaining_accounts 转为 HashMap 方便查找
let accounts = ctx
.remaining_accounts
.iter()
.map(|x| (x.key(), x.to_owned()))
.collect::<HashMap<_, _>>();

// 3. 遍历 ETF 包含的每个资产,按权重转移
for x in &ctx.accounts.etf_token_info.assets {
// 获取用户的资产 ATA
let from_ata = accounts
.get(&get_associated_token_address(
&ctx.accounts.authority.key(),
&x.token,
))
.ok_or(TokenMintError::InvalidAccounts)?;

// 获取 ETF PDA 的资产 ATA
let to_ata = accounts
.get(&get_associated_token_address(
&ctx.accounts.etf_token_info.key(),
&x.token,
))
.ok_or(TokenMintError::InvalidAccounts)?;

// 获取资产 Mint 账户
let mint_account = accounts
.get(&x.token)
.ok_or(TokenMintError::InvalidAccounts)?;
let mint_data = mint_account.try_borrow_data()?;

// 4. 判断是 SPL Token 还是 Token-2022,获取精度
let (decimals, token_program_id) = if *mint_account.owner == token_2022::ID {
let mint_state = StateWithExtensions::<Mint2022>::unpack(&mint_data)?;
(mint_state.base.decimals, token_2022::ID)
} else {
let mint_state = spl_token::state::Mint::unpack(&mint_data)?;
(mint_state.decimals, anchor_spl::token::ID)
};

// 5. 计算转移数量 = lamports × weight% × 10^decimals
let weight = x.weight as f64;
let base = 10u64.pow(decimals as u32);
let lamports_in_smallest_unit = (lamports * base as f64).round();
let amount = ((weight * lamports_in_smallest_unit) / 100.0).round() as u64;

// 6. 找到对应的 Token Program
let token_program = ctx
.remaining_accounts
.iter()
.find(|account| account.key == &token_program_id)
.ok_or(TokenMintError::InvalidAccounts)?;

// 7. 从用户 ATA 转移资产到 ETF PDA 的 ATA
transfer_tokens(
ctx.accounts.authority.to_account_info(), // 签名者:用户
from_ata.to_account_info(), // 来源:用户 ATA
to_ata.to_account_info(), // 目标:ETF PDA ATA
mint_account.clone(),
token_program.to_account_info(),
amount,
decimals,
Some(signer_seeds),
)?;

msg!("success transfer token: {} {}", x.token, amount / base);
}

// 8. 铸造 ETF Token 给用户
let mint_amount = (lamports * (10u64.pow(EtfToken::TOKEN_DECIMALS as u32) as f64)) as u64;
mint_to(
CpiContext::new_with_signer(
ctx.accounts.token_program.to_account_info(),
MintTo {
mint: ctx.accounts.etf_token_mint_account.to_account_info(),
to: ctx.accounts.etf_token_ata.to_account_info(),
authority: ctx.accounts.etf_token_info.to_account_info(), // PDA 作为 Mint Authority
},
signer_seeds,
),
mint_amount,
)?;

msg!("token minted successfully. {} {}", ctx.accounts.etf_token_mint_account.key(), lamports);

// 9. 发出铸造事件
emit!(EtfTokenMintEvent {
etf_token_mint: ctx.accounts.etf_token_mint_account.key(),
authority: ctx.accounts.authority.key(),
lamports,
});

Ok(())
}

3.3 铸造计算公式

1
2
3
4
5
对于每个资产 i:
transfer_amount_i = lamports × (weight_i / 100) × 10^decimals_i

最终铸造:
etf_token_amount = lamports × 10^9

示例

  • ETF 包含 SOL (60%) 和 USDC (40%)
  • 用户想铸造 10 个 ETF Token(lamports = 10)
  • 需要存入:6 SOL + 4 USDC
  • 获得:10 × 10^9 = 10,000,000,000 最小单位的 ETF Token

4. etf_burn

用户销毁 ETF Token,按比例赎回底层资产。

4.1 账户约束

etf_mint 共用 EtfTokenTransaction 账户结构。

4.2 指令逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
// programs/iswap/src/instructions/etf_token_burn.rs
use std::collections::HashMap;
use anchor_lang::prelude::*;
use anchor_spl::{
associated_token::get_associated_token_address,
token::{burn, spl_token, Burn},
token_2022::{self, spl_token_2022::{extension::StateWithExtensions, state::Mint as Mint2022}},
};

pub fn etf_token_burn<'info>(
ctx: Context<'_, '_, '_, 'info, EtfTokenTransaction<'info>>,
lamports: f64, // 销毁数量(以 1 个 ETF Token 为单位)
) -> Result<()> {
// 1. 构建 PDA 签名种子
let m = ctx.accounts.etf_token_mint_account.key();
let signer_seeds: &[&[&[u8]]] = &[&[
EtfToken::SEED_PREFIX.as_bytes(),
m.as_ref(),
&[ctx.bumps.etf_token_info],
]];

// 2. 将 remaining_accounts 转为 HashMap
let accounts = ctx
.remaining_accounts
.iter()
.map(|x| (x.key(), x.to_owned()))
.collect::<HashMap<_, _>>();

// 3. 遍历 ETF 包含的每个资产,按权重转移给用户
for x in &ctx.accounts.etf_token_info.assets {
// 用户的资产 ATA(接收方)
let to_ata = accounts
.get(&get_associated_token_address(
&ctx.accounts.authority.key(),
&x.token,
))
.ok_or(TokenMintError::InvalidAccounts)?;

// ETF PDA 的资产 ATA(转出方)
let from_ata = accounts
.get(&get_associated_token_address(
&ctx.accounts.etf_token_info.key(),
&x.token,
))
.ok_or(TokenMintError::InvalidAccounts)?;

// 获取资产 Mint 账户
let mint_account = accounts
.get(&x.token)
.ok_or(TokenMintError::InvalidAccounts)?;
let mint_data = mint_account.try_borrow_data()?;

// 4. 判断 Token 类型,获取精度
let (decimals, token_program_id) = if *mint_account.owner == token_2022::ID {
let mint_state = StateWithExtensions::<Mint2022>::unpack(&mint_data)?;
(mint_state.base.decimals, token_2022::ID)
} else {
let mint_state = spl_token::state::Mint::unpack(&mint_data)?;
(mint_state.decimals, anchor_spl::token::ID)
};

// 5. 计算转移数量
let weight = x.weight as f64;
let base = 10u64.pow(decimals as u32);
let lamports_in_smallest_unit = (lamports * base as f64).round();
let amount = ((weight * lamports_in_smallest_unit) / 100.0).round() as u64;

// 6. 找到对应的 Token Program
let token_program = ctx
.remaining_accounts
.iter()
.find(|account| account.key == &token_program_id)
.ok_or(TokenMintError::InvalidAccounts)?;

// 7. 从 ETF PDA 的 ATA 转移资产到用户 ATA
transfer_tokens(
ctx.accounts.etf_token_info.to_account_info(), // 签名者:ETF PDA
from_ata.to_account_info(), // 来源:ETF PDA ATA
to_ata.to_account_info(), // 目标:用户 ATA
mint_account.clone(),
token_program.to_account_info(),
amount,
decimals,
Some(signer_seeds),
)?;

msg!("success transfer token: {}", x.token);
}

// 8. 销毁用户的 ETF Token
let burn_amount = (lamports * (10u64.pow(EtfToken::TOKEN_DECIMALS as u32) as f64)) as u64;
burn(
CpiContext::new(
ctx.accounts.token_program.to_account_info(),
Burn {
mint: ctx.accounts.etf_token_mint_account.to_account_info(),
from: ctx.accounts.etf_token_ata.to_account_info(),
authority: ctx.accounts.authority.to_account_info(), // 用户签名销毁
},
)
.with_signer(signer_seeds),
burn_amount,
)?;

msg!("token burn successfully.");

// 9. 发出销毁事件
emit!(EtfTokenBurnEvent {
etf_token_mint: ctx.accounts.etf_token_mint_account.key(),
authority: ctx.accounts.authority.key(),
lamports,
});

Ok(())
}

4.3 资金流向对比

操作 资金方向 签名者
mint 用户 ATA → ETF PDA ATA 用户
burn ETF PDA ATA → 用户 ATA ETF PDA

5. initialize_user

初始化用户账户,支持邀请链追踪。

5.1 账户约束

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// programs/iswap/src/instructions/user_create.rs
use anchor_lang::prelude::*;
use crate::states::UserAccount;

#[derive(Accounts)]
pub struct UserCreate<'info> {
/// 用户账户(PDA)
/// Seeds: ["userAccount_v2", user]
#[account(
init_if_needed,
payer = user,
space = 8 + UserAccount::INIT_SPACE,
seeds = [
UserAccount::SEED_PREFIX.as_bytes(), // "userAccount_v2"
&user.key().as_ref()
],
bump
)]
pub user_account: Box<Account<'info, UserAccount>>,

/// 邀请人账户(可选)
/// 用于验证邀请人是否有效
pub inviter_account: Option<Account<'info, UserAccount>>,

/// 用户,支付账户创建费用
#[account(mut)]
pub user: Signer<'info>,

pub system_program: Program<'info, System>,
}

5.2 指令逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
// programs/iswap/src/instructions/user_create.rs
use std::ops::DerefMut;
use crate::error::ErrorCode;

pub fn initialize_user(
ctx: Context<UserCreate>,
nickname: String,
direct_inviter: Option<Pubkey>,
avatar: String,
) -> Result<()> {
let user_account = ctx.accounts.user_account.deref_mut();

// 1. 验证邀请人(如果提供)
if let Some(inviter_account) = &ctx.accounts.inviter_account {
// 邀请人不能是自己
require!(
inviter_account.key() != user_account.key(),
ErrorCode::InvalidInviterCode
);
// 邀请人账户不能是程序
require!(
!inviter_account.to_account_info().executable,
ErrorCode::InvalidInviterCode
);
// 记录邀请人 PDA 地址
user_account.inviter_code = Some(inviter_account.key());
} else {
user_account.inviter_code = None;
}

// 2. 初始化用户账户数据
user_account.nickname = nickname;
user_account.owner = ctx.accounts.user.key();
user_account.is_frozen = false;
user_account.created_at = Clock::get()?.unix_timestamp;
user_account.direct_inviter = direct_inviter;
user_account.avatar = avatar;

// 3. 发出用户初始化事件
emit!(UserInitialized {
user: user_account.key(),
inviter_code: direct_inviter,
nickname: user_account.nickname.clone(),
created_at: user_account.created_at,
});

Ok(())
}

5.3 邀请链说明

字段 说明
inviter_code 邀请人的 UserAccount PDA 地址(由链上验证)
direct_inviter 直接邀请人的公钥(由用户传入,不验证)

6. 工具函数

6.1 transfer_tokens

通用的 Token 转账函数,支持 SPL Token 和 Token-2022。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
// programs/iswap/src/util/token.rs
use anchor_lang::prelude::*;
use anchor_spl::token_2022;

pub fn transfer_tokens<'a>(
authority: AccountInfo<'a>, // 签名者
source: AccountInfo<'a>, // 来源 ATA
destination: AccountInfo<'a>, // 目标 ATA
mint: AccountInfo<'a>, // Mint 账户
token_program: AccountInfo<'a>, // Token 程序
amount: u64, // 转账数量
mint_decimals: u8, // 精度
signer_seeds: Option<&[&[&[u8]]]>, // PDA 签名种子(可选)
) -> Result<()> {
// 数量为 0 时直接返回
if amount == 0 {
return Ok(());
}

// 根据是否有 signer_seeds 选择 CPI 方式
let cpi_context = match signer_seeds {
Some(seeds) => CpiContext::new_with_signer(
token_program.to_account_info(),
token_2022::TransferChecked {
from: source,
to: destination,
authority,
mint,
},
seeds,
),
None => CpiContext::new(
token_program.to_account_info(),
token_2022::TransferChecked {
from: source,
to: destination,
authority,
mint,
},
),
};

// 使用 transfer_checked 确保精度正确
token_2022::transfer_checked(cpi_context, amount, mint_decimals)
}

7. 错误码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// programs/iswap/src/error.rs
#[error_code]
pub enum ErrorCode {
#[msg("Invalid Inviter Code")]
InvalidInviterCode, // 无效的邀请码

#[msg("Calculation Overflow")]
CalculationOverflow, // 计算溢出

#[msg("Invalid Input")]
InvalidInput, // 无效输入

#[msg("Invalid Amount")]
InvalidAmount, // 无效数量

#[msg("Invalid Owner")]
InvalidOwner, // 无效所有者
}

// programs/iswap/src/states/etf_token.rs
#[error_code]
pub enum TokenMintError {
#[msg("Lack of necessary accounts")]
InvalidAccounts, // 缺少必要账户

#[msg("Insufficient balance")]
InsufficientBalance, // 余额不足
}

总结

指令列表

指令 功能 调用者
etf_create 创建 ETF Token 任何用户
etf_mint 铸造 ETF Token 任何用户
etf_burn 销毁 ETF Token ETF 持有者
initialize_user 初始化用户账户 任何用户

ETF 生命周期

1
2
3
1. etf_create        → 定义资产组合和权重
2. etf_mint → 用户存入底层资产,获得 ETF Token
3. etf_burn → 用户销毁 ETF Token,赎回底层资产

PDA 种子汇总

账户 Seeds 用途
etf_token_info ["etf_token_v3", etf_token_mint] 存储 ETF 配置
etf_token_mint ["etf_token_v3", symbol] ETF Token Mint
user_account ["userAccount_v2", user_pubkey] 用户档案

Token 标准支持

  • SPL Token(标准代币)
  • Token-2022(扩展代币)

通过检查 Mint 账户的 owner 来判断类型,自动选择对应的 Token Program。