记录一次Rust借用的生命周期问题

Unstake的结构体声明如下:

1
2
3
4
5
6
7
8
9

#[derive(Accounts)]
pub struct UnStake<'info> {
#[account(
mint::token_program = token_program,
)]
pub stake_token_mint: InterfaceAccount<'info, Mint>,
}

我想编写一个helper函数, 用来为 Unstake创建 CpiContext, 像这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
impl<'info> UnStake<'info> {
pub fn transfer_ctx(&self) -> CpiContext<'_, '_, '_, 'info, TransferChecked<'info>> {
let seeds: &[&[&[u8]]] =&[ &[
b"POOL_AUTH".as_ref(),
self.stake_token_mint.key().as_ref(),
]];
CpiContext::new_with_signer(
self.token_program.to_account_info(),
TransferChecked {
from: self.receive_stake_token_ata.to_account_info(),
mint: self.stake_token_mint.to_account_info(),
to: self.user_stake_token_ata.to_account_info(),
authority: self.pool_authority.to_account_info(),
},
seeds,
)
}
}

编译报错:

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
error[E0515]: cannot return value referencing temporary value
--> programs/anchor-token-staking-yqq/src/instructions/unstake.rs:218:9
|
216 | self.stake_token_mint.key().as_ref(),
| --------------------------- temporary value created here
217 | ]];
218 | / CpiContext::new_with_signer(
219 | | self.token_program.to_account_info(),
220 | | TransferChecked {
221 | | from: self.receive_stake_token_ata.to_account_info(),
... |
226 | | seeds,
227 | | )
| |_________^ returns a value referencing data owned by the current function

error[E0515]: cannot return value referencing temporary value
--> programs/anchor-token-staking-yqq/src/instructions/unstake.rs:218:9
|
214 | let seeds: &[&[&[u8]]] =&[ &[
| _____________________________________-
215 | | b"POOL_AUTH".as_ref(),
216 | | self.stake_token_mint.key().as_ref(),
217 | | ]];
| |_________- temporary value created here
218 | / CpiContext::new_with_signer(
219 | | self.token_program.to_account_info(),
220 | | TransferChecked {
221 | | from: self.receive_stake_token_ata.to_account_info(),
... |
226 | | seeds,
227 | | )
| |_________^ returns a value referencing data owned by the current function

error[E0515]: cannot return value referencing temporary value
--> programs/anchor-token-staking-yqq/src/instructions/unstake.rs:218:9
|
214 | let seeds: &[&[&[u8]]] =&[ &[
| __________________________________-
215 | | b"POOL_AUTH".as_ref(),
216 | | self.stake_token_mint.key().as_ref(),
217 | | ]];
| |__________- temporary value created here
218 | / CpiContext::new_with_signer(
219 | | self.token_program.to_account_info(),
220 | | TransferChecked {
221 | | from: self.receive_stake_token_ata.to_account_info(),
... |
226 | | seeds,
227 | | )
| |_________^ returns a value referencing data owned by the current function

For more information about this error, try `rustc --explain E0515`.
warning: `anchor-token-staking-yqq` (lib) generated 1 warning
error: could not compile `anchor-token-staking-yqq` (lib) due to 3 previous errors; 1 warning emitted

原因: 返回局部变量的引用. 即返回临时变量seeds的引用, 而seeds在函数结束之后就被释放了。

  • 深层次的原因:seeds中包含了对 self.stake_token_mint.key().as_ref() 引用, 而这是一个临时引用

下面这段代码是可以的, 因为 "POOL_AUTH"具有静态生命周期,在整个运行期间都有效,因此,seeds的生命周期静态生命周期。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
impl<'info> UnStake<'info> {
pub fn transfer_ctx(&self) -> CpiContext<'_, '_, '_, 'info, TransferChecked<'info>> {
let seeds: &[&[&[u8]]] = &[&[b"POOL_AUTH"]];
CpiContext::new_with_signer(
self.token_program.to_account_info(),
TransferChecked {
from: self.receive_stake_token_ata.to_account_info(),
mint: self.stake_token_mint.to_account_info(),
to: self.user_stake_token_ata.to_account_info(),
authority: self.pool_authority.to_account_info(),
},
seeds,
)
}
}

为什么? 看下面简化的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct Example<'a> {
data: &'a str,
}

impl<'info> Example<'info> {
fn problematic(&self) -> &'info str {
let local = self.data; // local 的生命周期被限制在函数内
local // 错误:尝试返回一个生命周期比函数更短的引用
}

fn works(&self) -> &'info str {
self.data // 直接返回,没问题
}
}

局部变量的生命周期:

  • 在Rust中,局部变量的生命周期默认仅限于它们被定义的作用域内。即使这个局部变量包含了对生命周期更长的数据的引用,变量本身的生命周期仍然被限制在函数内。
  • 引用的生命周期 vs 变量的生命周期: 虽然 self.stake_token_mint 的生命周期是 'info,但当我们创建一个包含这个引用的新局部变量时,这个新变量的生命周期被限制在函数内。
  • 生命周期的传播: 生命周期并不会自动从被引用的数据传播到包含引用的新数据结构。
  • 编译器的保守处理: 编译器会保守地处理生命周期,除非明确指定,否则它不会假设局部变量的生命周期比函数更长。

那么,不使用局部变量 seeds , 而是直接传参, 同样报错:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
impl<'info> UnStake<'info> {
pub fn transfer_ctx(&self) -> CpiContext<'_, '_, '_, 'info, TransferChecked<'info>> {
// let seeds: &[&[&[u8]]] =;
CpiContext::new_with_signer(
self.token_program.to_account_info(),
TransferChecked {
from: self.receive_stake_token_ata.to_account_info(),
mint: self.stake_token_mint.to_account_info(),
to: self.user_stake_token_ata.to_account_info(),
authority: self.pool_authority.to_account_info(),
},
&[&[b"POOL_AUTH", self.stake_token_mint.key().as_ref()]],
)
}
}

self.stake_token_mint.key() 会创建应该临时变量, 这个临时变量的生命周期也是仅限函数内部,因此参数中包含对于一个 临时变量的引用,导致生命周期不匹配的问题

最终的解决方案:

  • seeds作为参数从外部传入
  • 使用生命周期注解'a, 注解selfseeds ,确保seeds在函数执行期间有效
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
impl<'info> UnStake<'info> {
pub fn transfer_ctx<'a>(
&'a self,
seeds: &'a [&[&[u8]]],
) -> CpiContext<'_, '_, '_, 'info, TransferChecked<'info>> {
CpiContext::new_with_signer(
self.token_program.to_account_info(),
TransferChecked {
from: self.receive_stake_token_ata.to_account_info(),
mint: self.stake_token_mint.to_account_info(),
to: self.user_stake_token_ata.to_account_info(),
authority: self.pool_authority.to_account_info(),
},
seeds,
)
}
}

16_Solana_程序安全实践指南

官方列出的安全例子:

汇总如下:

  • 0-signer-authorization: 非法权限调用攻击,调用者不是交易签名者
    • 使用 Anchor的Signer账户类型检查交易签名者
  • 1-account-data-matching: 账户&数据不一致,伪造攻击
    • 使用 Anchor的约束,检查权限是否一致
  • 2-owner-check: 权限, owner不一致
  • 3-type-cosplay: 数据类型伪造
  • 4-initialization: 重复初始化攻击 + 初始化抢跑攻击
    • 重新初始化攻击: 使用Anchor的init,
    • 初始化抢跑攻击: 使用 Anchor的init, 重复初始化会报错,因为就会发现是否被抢跑
  • 5-arbitrary-cpi: CPI乱调用(programId不一致, PDA的owner不是该程序), 可以进行伪造PDA攻击
    • 使用Anchor的CpiContext进行CPI调用
  • 6-duplicate-mutable-accounts: 重复修改账户(2个账户数据结构相同,传入相同的值)
    • 注意账户&指令中包含2个相同的数据结构的账户,要做检查key检查
  • 7-bump-seed-canonicalization: PDA碰撞攻击(通过传入 seeds和bump)
  • 8-pda-sharing: (常见)伪造PDA攻击, PDA权限不清晰(共享的PDA),攻击者可以伪造一个PDA
    • 原因: seeds 中字段不唯一,没有跟账户关联起来
  • 9-closing-accounts: 账户关闭攻击(重入攻击),
    • 要在指令执行结束后,关闭一个(临时)账户, 直接使用Anchor的 close=destination 约束即可
  • 10-sysvar-address-checking: 系统变量地址检查(PDA伪造)
    • 使用Anchor的Sysvar获取系统变量

15_Solana_Token Extension

https://www.soldev.app/course/intro-to-token-extensions-program

  • Token Extension Program 是 Token Program 的超集

  • Token Program 和 Token Extension Program 是2个程序

    • 两个程序的地址,不可以互换(not interchangeable)
  • Token Extension 16种功能:

    https://spl.solana.com/token-2022/extensions

    • Account:
      • Memo: 转账时需要增加备注
      • Immutable ownership: ATA权限不可以转移
        • Token 2022的ATA权限默认是不可转移的
      • Default account state: 设置默认的账户状态,如:默认冻结
      • CPI guard: 对CPI做一些限制操作
    • Mint
      • Transfer fees: 项目方可以加入抽水功能
      • Closing mint: 关闭mint , 方便跑路
        • 需要supply为0, 即,需要销毁所有token之后才能关闭mint
      • Interest-bearing tokens: 生息, 非常适合staking项目
      • Non-transferable tokens: 不可转移, 适合做灵魂绑定(Soul-Bound)
      • Permanent delegate: 永久代理,项目方可以控制一切账户,非常适合做中心化集权场景
      • Transfer hook: token转账的钩子, 可以增加自定义相关回调
      • Metadata pointer: 为token增加metadata
        • 一般与Metadata Extension一起用, 也可以与外部账户(Metaplex)联合使用
      • Metadata: 为token增加metadata ,一般和 metadta pointer一起用
      • Group pointer: 群组,适合做合集, 如NFT合集
      • Group: 同上
      • Member pointer : 成员
      • Member: 同上
      • Confidential transfers: 私密交易

使用命令行spl-token使用 Token 2022

spl-token –create-token –help

创建 close authority token

获取solana配置信息,设置 devnet

1
2
3
4
5
6
$ solana config get
Config File: /home/yqq/.config/solana/cli/config.yml
RPC URL: https://api.devnet.solana.com
WebSocket URL: wss://api.devnet.solana.com/ (computed)
Keypair Path: /home/yqq/.config/solana/id.json
Commitment: confirmed

创建 close authority token

注意: spl-token 默认使用的 Token Program的program id, 如需使用Token 2022,则需要制定program id

  • TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb 是 Token 2022的program id
  • TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA 是 Token Program的program id
1
spl-token create-token --program-id TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb --enable-close

创建ATA账户

1
2
spl-token create-account --program-id TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb

mint token, 注意必须指定program id, 因为 spl-token 默认使用旧版Token Program作为program id

1
2
3
4
5
6
7
8
spl-token mint --program-id TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb CisqmfLH8R2JnSYSA8tgW8LSG5hQPogYZKxxHv6H5aMq 1000000000  4tQwDVgmNYPxrdhmqVAza9qefkmPPhBrF1h75oMrfd2Q

Minting 1000000000 tokens
Token: CisqmfLH8R2JnSYSA8tgW8LSG5hQPogYZKxxHv6H5aMq
Recipient: 4tQwDVgmNYPxrdhmqVAza9qefkmPPhBrF1h75oMrfd2Q

Signature: YdAcA3VJ9ehFRKzZCbkwMvEMxVAMPVB7X7Cuu4pS7pXcUKjGWSB7siCywFG1wJGXqQVXK3HuTSje2dytrmHKFJg

直接关闭会报错,因为此时 supply不是0, 必须先销毁,然后才能close

1
2
$ spl-token close-mint CisqmfLH8R2JnSYSA8tgW8LSG5hQPogYZKxxHv6H5aMq
Error: "Mint CisqmfLH8R2JnSYSA8tgW8LSG5hQPogYZKxxHv6H5aMq still has 1000000000000000000 outstanding tokens; these must be burned before closing the mint."

销毁token

1
2
3
4
5
6
7
spl-token burn --program-id TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb 4tQwDVgmNYPxrdhmqVAza9qefkmPPhBrF1h75oMrfd2Q 1000000000

Burn 1000000000 tokens
Source: 4tQwDVgmNYPxrdhmqVAza9qefkmPPhBrF1h75oMrfd2Q

Signature: K3v2mkrrdyqym9RHFWT2yo2RQ89zcZQaR6V6RW37ie44ob2Du4arTTym1FimpHLQ9FTHPd8zhdXxnjqU8tGWzZp

查看mint的信息, 此时 supply 为0, 可以进行close

1
2
3
4
5
6
7
8
9
10
11
12
$ spl-token display CisqmfLH8R2JnSYSA8tgW8LSG5hQPogYZKxxHv6H5aMq

SPL Token Mint
Address: CisqmfLH8R2JnSYSA8tgW8LSG5hQPogYZKxxHv6H5aMq
Program: TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb
Supply: 0
Decimals: 9
Mint authority: 7DxeAgFoxk9Ha3sdciWE4G4hsR9CUjPxsHAxTmuCJrop
Freeze authority: (not set)
Extensions
Close authority: 7DxeAgFoxk9Ha3sdciWE4G4hsR9CUjPxsHAxTmuCJrop

进行close

1
2
3
4
$ spl-token close-mint CisqmfLH8R2JnSYSA8tgW8LSG5hQPogYZKxxHv6H5aMq

Signature: 2h9bLrRCcK1bSbavdtKHrHLV2FVJVtEQiCUXBDKZXwZwnGFSZMhiYLDpRCJ7pxgb4KKxc75zUbCgeo9SeM7sjheH

创建ATA权限不可转移的token 2022

创建 token, token 2022 的ATA默认都是不可转移的,因此不需要制定额外参数

注意: spl-token 默认使用的 Token Program的program id, 如需使用Token 2022,则需要制定program id

  • TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb 是 Token 2022的program id
  • TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA 是 Token Program的program id
1
2
3
4
5
6
7
8
$ spl-token create-token --program-id TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb
Creating token HcGkiji8KimiZPTBf3SFCapAoR9NP63LdZtpv3719wdw under program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb

Address: HcGkiji8KimiZPTBf3SFCapAoR9NP63LdZtpv3719wdw
Decimals: 9

Signature: 411LLtiDPB6Xgpq4kqsqp4K3atJCsTuWAHnAYaV6YaaMPDCYVTkRi6PWra7ixxMtWTbwyGeBxiZmfBfLjfeyZ6Q5

创建 ATA 账户

1
2
3
4
$ spl-token create-account --program-id TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb  HcGkiji8KimiZPTBf3SFCapAoR9NP63LdZtpv3719wdw
Creating account GfuBt164MUSThb3ZnhLfra8bbHzzrCvruXs5p7rC23LW

Signature: 3eRBFR52dhVBtad7sZs9s2i2h9Lw6W9TGvDdRQJ9DAiuEZPYoaHPEnqfCuoGzX6mDfTcPiP4wW7bdLzSQHtEzZLT

mint token

1
2
3
4
5
6
7
$ spl-token mint --program-id TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb HcGkiji8KimiZPTBf3SFCapAoR9NP63LdZtpv3719wdw 10000000 GfuBt164MUSThb3ZnhLfra8bbHzzrCvruXs5p7rC23LW
Minting 10000000 tokens
Token: HcGkiji8KimiZPTBf3SFCapAoR9NP63LdZtpv3719wdw
Recipient: GfuBt164MUSThb3ZnhLfra8bbHzzrCvruXs5p7rC23LW

Signature: 2ffKaX7XsK71GbpehqCgqsx9qVdGXSA28umQQ7guXN7Pv9ZzviRpCNyRuoPmFGwAtHANGzTs8TkWDMXJUT4m6vhP

查看余额

1
2
3
$ spl-token balance HcGkiji8KimiZPTBf3SFCapAoR9NP63LdZtpv3719wdw
10000000

查看ATA账户信息, 可以看到 Immutable owner

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ spl-token display GfuBt164MUSThb3ZnhLfra8bbHzzrCvruXs5p7rC23LW

SPL Token Account
Address: GfuBt164MUSThb3ZnhLfra8bbHzzrCvruXs5p7rC23LW
Program: TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb
Balance: 10000000
Decimals: 9
Mint: HcGkiji8KimiZPTBf3SFCapAoR9NP63LdZtpv3719wdw
Owner: 7DxeAgFoxk9Ha3sdciWE4G4hsR9CUjPxsHAxTmuCJrop
State: Initialized
Delegation: (not set)
Close authority: (not set)
Extensions:
Immutable owner

创建 灵魂绑定代币

创建 token,

  • --decimals 0
  • --enable-metadata
  • --enable-non-transferable
1
2
3
4
5
6
7
8
9
$ spl-token create-token --program-id TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb --decimals 0 --enable-metadata --enable-non-transferable
Creating token 7R6uaZgMZgmfBRLJXXYHPRV8oysDokv8FHrZ7Td1xo5K under program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb
To initialize metadata inside the mint, please run `spl-token initialize-metadata 7R6uaZgMZgmfBRLJXXYHPRV8oysDokv8FHrZ7Td1xo5K <YOUR_TOKEN_NAME> <YOUR_TOKEN_SYMBOL> <YOUR_TOKEN_URI>`, and sign with the mint authority.

Address: 7R6uaZgMZgmfBRLJXXYHPRV8oysDokv8FHrZ7Td1xo5K
Decimals: 0

Signature: 4z1gNiFfLAkoe9RQD84N3vHh88ZNegZs2zL81ve1uKG8ijT6VGK4yuTYwjmLLCLgAXxRvCSUoMChneQZRCZXR7Wz

初始化metadata

1
2
3
4
$ spl-token initialize-metadata 7R6uaZgMZgmfBRLJXXYHPRV8oysDokv8FHrZ7Td1xo5K YQQTOKEN YT https://arweave.net/7q03FecPFE5JBPDakJFDS7xvdKqw5NSlNPUFZOYVVlk

Signature: 3zxSfkAg32KavFJ3UNXAMs4KyogZwRNDjDWmD1z8PmmHLHsDaYinwpQwrsC7EnqFNSSTc5e6ohMaUvAT7N5UowbZ

查看token信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
$ spl-token display 7R6uaZgMZgmfBRLJXXYHPRV8oysDokv8FHrZ7Td1xo5K

SPL Token Mint
Address: 7R6uaZgMZgmfBRLJXXYHPRV8oysDokv8FHrZ7Td1xo5K
Program: TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb
Supply: 0
Decimals: 0
Mint authority: 7DxeAgFoxk9Ha3sdciWE4G4hsR9CUjPxsHAxTmuCJrop
Freeze authority: (not set)
Extensions
Non-transferable
Metadata Pointer:
Authority: 7DxeAgFoxk9Ha3sdciWE4G4hsR9CUjPxsHAxTmuCJrop
Metadata address: 7R6uaZgMZgmfBRLJXXYHPRV8oysDokv8FHrZ7Td1xo5K
Metadata:
Update Authority: 7DxeAgFoxk9Ha3sdciWE4G4hsR9CUjPxsHAxTmuCJrop
Mint: 7R6uaZgMZgmfBRLJXXYHPRV8oysDokv8FHrZ7Td1xo5K
Name: YQQTOKEN
Symbol: YT
URI: https://arweave.net/7q03FecPFE5JBPDakJFDS7xvdKqw5NSlNPUFZOYVVlk

更新metadata

1
2
3
4
$ spl-token update-metadata 7R6uaZgMZgmfBRLJXXYHPRV8oysDokv8FHrZ7Td1xo5K name YQQNFT

Signature: 2xRQhsQiyrG8FVe3JHSFsDYz4gy1RPD16ECZWBpfdCwRpCgnTUzEhDJJ9QNRmR4Qnga2xTua2jrYhy6PFizigun3

创建 ATA

1
2
3
4
5
$ spl-token create-account --program-id TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb 7R6uaZgMZgmfBRLJXXYHPRV8oysDokv8FHrZ7Td1xo5K
Creating account 64Sxa26sViFh9JKFM6tm7dEib3hLTxbRXvARjjLTCmeG

Signature: 3HvNKDiPa6QcihbgUB4pVsojtmq7khwqCDdTb1iwfwdbuexe3s3PNCipEL8MMQMCwFYEGFNnmdfohXJ6weUCZ4tw

mint token

1
2
3
4
5
6
7
$ spl-token mint --program-id TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb 7R6uaZgMZgmfBRLJXXYHPRV8oysDokv8FHrZ7Td1xo5K 1 64Sxa26sViFh9JKFM6tm7dEib3hLTxbRXvARjjLTCmeG
Minting 1 tokens
Token: 7R6uaZgMZgmfBRLJXXYHPRV8oysDokv8FHrZ7Td1xo5K
Recipient: 64Sxa26sViFh9JKFM6tm7dEib3hLTxbRXvARjjLTCmeG

Signature: FijHaPESJUG4PgMWjxFcPwvH7GkvL1feQeg8KKHRsS2WmfDEWkutWniEFMyDNg76acU6bUeaQ97ywtWWW1Y8SbF

尝试转移 Token, 报错Transfer is disabled for this mint

1
2
3
4
5
6
7
8
9

$ spl-token transfer --program-id TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb 7R6uaZgMZgmfBRLJXXYHPRV8oysDokv8FHrZ7Td1xo5K 1 38jEaxphBTa3NEg4K6nG8Zgs6eVsSsr9AoSZCfax2pH8 --fund-recipient
Transfer 1 tokens
Sender: 64Sxa26sViFh9JKFM6tm7dEib3hLTxbRXvARjjLTCmeG
Recipient: 38jEaxphBTa3NEg4K6nG8Zgs6eVsSsr9AoSZCfax2pH8
Recipient associated token account: 3XX7DysVrERAeTYFczEoKtwxqH6QqxWfUBcUBaZy1GZ4
Funding recipient: 3XX7DysVrERAeTYFczEoKtwxqH6QqxWfUBcUBaZy1GZ4
Error: Client(Error { request: Some(SendTransaction), kind: RpcError(RpcResponseError { code: -32002, message: "Transaction simulation failed: Error processing Instruction 1: custom program error: 0x25", data: SendTransactionPreflightFailure(RpcSimulateTransactionResult { err: Some(InstructionError(1, Custom(37))), logs: Some(["Program ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL invoke [1]", "Program log: CreateIdempotent", "Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb invoke [2]", "Program log: Instruction: GetAccountDataSize", "Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb consumed 3064 of 22071 compute units", "Program return: TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb rgAAAAAAAAA=", "Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb success", "Program 11111111111111111111111111111111 invoke [2]", "Program 11111111111111111111111111111111 success", "Program log: Initialize the associated token account", "Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb invoke [2]", "Program log: Instruction: InitializeImmutableOwner", "Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb consumed 1924 of 14077 compute units", "Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb success", "Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb invoke [2]", "Program log: Instruction: InitializeAccount3", "Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb consumed 5815 of 9763 compute units", "Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb success", "Program ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL consumed 29823 of 33467 compute units", "Program ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL success", "Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb invoke [1]", "Program log: Instruction: TransferChecked", "Program log: Transfer is disabled for this mint", "Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb consumed 3644 of 3644 compute units", "Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb failed: custom program error: 0x25"]), accounts: None, units_consumed: Some(33467), return_data: None, inner_instructions: None }) }) })


在客户端中使用 Token 2022

https://www.soldev.app/course/token-extensions-in-the-client

  • spl-token默认使用 Token Program, 除非明确指定使用Token Programs Extension
    • Token Program: TOKEN_PROGRAM_ID
    • Token 2022: TOKEN_2022_PROGRAM_ID

示例代码

1
2
3
4
5
6
7
8
9
10
const mint = await createMint(
connection,
payer,
payer.publicKey,
payer.publicKey,
decimals,
undefined,
{ commitment: connection.commitment },
tokenProgramId // 指定 Program Id 即可
);

在Anchor使用 Token2022

在Anchor中使用 interface 类型来将 Token ProgramToken 2022 融合到一起

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
use {
anchor_lang::prelude::*,
anchor_spl::{token_interface},
};

#[derive(Accounts)]
pub struct Example<'info>{
// Token account
#[account(
token::token_program = token_program
)]
pub token_account: InterfaceAccount<'info, token_interface::TokenAccount>,
// Mint account
#[account(
mut,
mint::token_program = token_program
)]
pub mint_account: InterfaceAccount<'info, token_interface::Mint>,
pub token_program: Interface<'info, token_interface::TokenInterface>,
}
  • Interface: 是 Program 的wrapper支持多种Program
  • TokenInterface: 支持 Token ProgramToken 2022, 且仅支持这2种,如果传入其他的程序id会报错
  • InterfaceAccount: 和 Interface 类似,也是一个wrapper, 用于 AccountInfo. InterfaceAccount

14_Solana_程序架构

https://www.soldev.app/course/program-architecture

处理大账户 Dealing With Large Accounts

  • Solana上存储每个字节都需要支付相应的租金
  • 大数据限制:
    • Stack(栈)限制: 4KB
    • Heap(堆)限制: 32KB
      • Box: 小于32KB
      • zero copy: 处理大于32KB
    • 大于 10KB的账户,CPI有限制
  • Box,在堆上分配内存

    1
    2
    3
    4
    5
    6
    7
    8
    9
    #[account]
    pub struct SomeBigDataStruct {
    pub big_data: [u8; 5000], // 5000字节超出了solana的4KB栈限制,因此使用Heap
    }

    #[derive(Accounts)]
    pub struct SomeFunctionContext<'info> {
    pub some_big_data: Box<Account<'info, SomeBigDataStruct>>, // 在堆上分配内存
    }
  • Zero Copy

    https://docs.rs/anchor-lang/latest/anchor_lang/attr.account.html

    1
    2
    3
    4
    5
    6
    7
    8
    9
    #[account(zero_copy)]
    pub struct SomeReallyBigDataStruct {
    pub really_big_data: [u128; 1024], // 16,384 bytes
    }

    pub struct ConceptZeroCopy<'info> {
    #[account(zero)]
    pub some_really_big_data: AccountLoader<'info, SomeReallyBigDataStruct>,
    }

处理账户 Dealing With Accounts

  • 数据顺序: 变长字段放在账户结构尾部

    • 因为变长的字段放在签名,通过filter查询后面的字段时,无法确定偏移量offset,
  • 预留字段: 为账户增加一个预留字段

    • v1版本
      1
      2
      3
      4
      5
      6
      7
      #[account]
      pub struct GameState { //V1
      pub health: u64,
      pub mana: u64,
      pub for_future_use: [u8; 128],
      pub event_log: Vec<string>
      }
    • v2版本: v1 和 2 版本是兼容的
      1
      2
      3
      4
      5
      6
      7
      8
      #[account]
      pub struct GameState { //V2
      pub health: u64,
      pub mana: u64,
      pub experience: u64, // 新增
      pub for_future_use: [u8; 120],
      pub event_log: Vec<string>
      }
  • 数据优化: 通过优化账户数据结构节约空间

    • 例如: 能用 u8的,就不要用u64 ,
      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
      #[account]
      pub struct BadGameFlags { // 8 bytes , 每个 bool 是一个字节
      pub is_frozen: bool,
      pub is_poisoned: bool,
      pub is_burning: bool,
      pub is_blessed: bool,
      pub is_cursed: bool,
      pub is_stunned: bool,
      pub is_slowed: bool,
      pub is_bleeding: bool,
      }

      // 优化后:
      const IS_FROZEN_FLAG: u8 = 1 << 0;
      const IS_POISONED_FLAG: u8 = 1 << 1;
      const IS_BURNING_FLAG: u8 = 1 << 2;
      const IS_BLESSED_FLAG: u8 = 1 << 3;
      const IS_CURSED_FLAG: u8 = 1 << 4;
      const IS_STUNNED_FLAG: u8 = 1 << 5;
      const IS_SLOWED_FLAG: u8 = 1 << 6;
      const IS_BLEEDING_FLAG: u8 = 1 << 7;
      const NO_EFFECT_FLAG: u8 = 0b00000000;
      #[account]
      pub struct GoodGameFlags { // 1 byte
      pub status_flags: u8,
      }
  • PDA账户结构设计:

    PDA对应关系 示例 应用场景
    One-Per-Program (全局账户) seeds=[b"global config"] 全局配置
    One-Per-Owner seeds=[b"player", owner.key().as_ref()] 游戏/DEX/…
    Multiple-Per-Owner seeds=[b"podcast", owner.key().as_ref(), episode_title.as_bytes().as_ref()] 播客频道(多季)
    One-Per-Owner-Per-Account seeds=[b"ATA Account", owner.key().as_ref(), mint.key().as_ref()] SPL Token的 ATA

处理并发 Dealing With Concurrency

  • solana的交易可以并行处理
  • 对于互不关联账户的交易,都是并行处理
  • 对于共享的账户的写入的交易,采用类似互斥量机制,因此是串行的

对于瓶颈的优化方案:

  • 采用分离方案,减少全局共享写入的账户

例如:

1
2
3
4
5
6

[TxA] [TxB] ....[TxX]
| | |
V V V
[平台手续费金额总账户]

优化成

1
2
3
4
5
6
7
8
[TxA]  [TxB] ....[TxX]
| | | <--- 交易内执行
V V V
[PA] [PB] [PX] <--- 和用户账户关联的PDA账户
| | | <--- 异步执行
V V V
[平台手续费金额总账户]

这样, 每个用户的交易 只会和自己账户关联的PDA账户有交互,而不会互相影响


状态压缩 State Compression

https://www.soldev.app/course/generalized-state-compression

  • 压缩NFT (cNFT)

13_Solana_程序安全

参考: https://github.com/coral-xyz/sealevel-attacks/tree/master


https://www.soldev.app/course/signer-auth

案例 1: 缺少 Signer Authentication

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
use anchor_lang::prelude::*;

declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");

#[program]
pub mod insecure_update{
use super::*;
// ...

pub fn update_authority(ctx: Context<UpdateAuthority>) -> Result<()> {
ctx.accounts.vault.authority = ctx.accounts.new_authority.key();
Ok(())
}
}

#[derive(Accounts)]
pub struct UpdateAuthority<'info> {
#[account(
mut,
has_one = authority
)]
pub vault: Account<'info, Vault>,
pub new_authority: AccountInfo<'info>,
pub authority: AccountInfo<'info>,
}

#[account]
pub struct Vault {
token_account: Pubkey,
authority: Pubkey,
}

漏洞分析: 虽然有 has_one = authority, 但它仅检查 vault.authority.pubkey == authority.pubkey, 即检查调用程序的参数中的authority是否和程序中的vaultauthority是否一致, 并没有检查调用者是否是authority。 因此,存在被攻击的风险,即任何人只要将调用参数中的authority设置为和vault中的authority 一致, 都可以成功调用update_authority

漏洞修复:

  • 方案 1:使用 ctx.accounts.authority.is_signer 判断 authority 是否是交易的 signer
    • 缺点: 账户验证和指令逻辑验证是一起的(没有分离)
  • 方案 2:使用 Anchor 的 Signer
    • 优点: 账户验证和指令逻辑验证是分开, 在进入逻辑之前就已经做了校验
    • 缺点: 只能和 Singer 账户一起,不能和其他账户类型
  • 方案 3: 使用 #[account(signer)]
    • 作用和 Signer是一样,但是比 Signer 更灵活,支持更多账户类型
    • 比如,
      1
      2
      #[account(signer)]
      pub authority: Account<'info, SomeData>
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
use anchor_lang::prelude::*;

declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");

#[program]
pub mod secure_update{
use super::*;
// ...
pub fn update_authority(ctx: Context<UpdateAuthority>) -> Result<()> {
// Signer中已经做了检查
// if !ctx.accounts.authority.is_signer {
// return Err(ProgramError::MissingRequiredSignature.into());
// }

ctx.accounts.vault.authority = ctx.accounts.new_authority.key();
Ok(())
}
}

#[derive(Accounts)]
pub struct UpdateAuthority<'info> {
#[account(
mut,
has_one = authority
)]
pub vault: Account<'info, Vault>,
pub new_authority: AccountInfo<'info>,
pub authority: Signer<'info>,
}

#[account]
pub struct Vault {
token_account: Pubkey,
authority: Pubkey,
}

案例 2: Missing owner check

https://www.soldev.app/course/owner-checks

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
use anchor_lang::prelude::*;

declare_id!("Cft4eTTrt4sJU4Ar35rUQHx6PSXfJju3dixmvApzhWws");

#[program]
pub mod owner_check {
use super::*;
...

pub fn admin_instruction(ctx: Context<Unchecked>) -> Result<()> {
let account_data = ctx.accounts.admin_config.try_borrow_data()?;
let mut account_data_slice: &[u8] = &account_data;
let account_state = AdminConfig::try_deserialize(&mut account_data_slice)?;

if account_state.admin != ctx.accounts.admin.key() {
return Err(ProgramError::InvalidArgument.into());
}
msg!("Admin: {}", account_state.admin.to_string());
Ok(())
}
}

#[derive(Accounts)]
pub struct Unchecked<'info> {
admin_config: AccountInfo<'info>,
admin: Signer<'info>,
}

#[account]
pub struct AdminConfig {
admin: Pubkey,
}

漏洞分析:

  • admin_instruction: 检查的是输入参数指定的程序状态(state)与参数是否匹配, 并没有检查数据账户的 owner 是不是本程序帐户

    如下图, 攻击这将 B 数据账户传入给 A 程序,可以通过 A 程序的简单校验,从而修改 A 数据账户的状态

    1
    2
    3
    4
    [A程序账户]        [B程序账户]
    | |
    | |
    [A数据账户] [B数据账户]
  • 攻击案例-国库提币攻击

    • 攻击者的合约,

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      #[account]
      pub struct Vault {
      // 必须保持和被攻击者的账户结构体同名, 即必须Vault, 因为结构体名称的hash作为账户的 Discriminator,
      // 否则被攻击合约序列化的时候报错: Error Message: 8 byte discriminator did not match what was expected

      // 必须保持和被攻击账户的数据结构顺序一致,
      // 结构体内部变量名称可以不同,
      token_accountxx: Pubkey,
      authorityx: Pubkey,
      }
    • 漏洞修复: 将 vault 的 UncheckedAccount 改成 Account, anchor 为 Account 实现了 owner 安全检查

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      #[derive(Accounts)]
      pub struct SecureWithdraw<'info> {
      /// 具体检查如下:
      // has_one:
      // input_args.token_account.key == vault.token_account.key
      // input_args.authority.key == vault.authority.key
      // Acccount的Owner trait 安全检查
      // Account.info.owner == T::owner()
      // `!(Account.info.owner == SystemProgram && Account.info.lamports() == 0)`
      #[account(has_one=token_account, has_one=authority)]
      pub vault: Account<'info, Vault>,

      #[account(mut, seeds=[b"token"], bump)]
      pub token_account: Account<'info, TokenAccount>,
      #[account(mut)]
      pub withdraw_destination: Account<'info, TokenAccount>,
      pub token_program: Program<'info, Token>, // SPL Token Program固定是 TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA
      pub authority: Signer<'info>,
      }

案例 3: Account Data Matching

https://www.soldev.app/course/account-data-matching

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
use anchor_lang::prelude::*;

declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");

#[program]
pub mod data_validation {
use super::*;
...
pub fn update_admin(ctx: Context<UpdateAdmin>) -> Result<()> {
ctx.accounts.admin_config.admin = ctx.accounts.new_admin.key();
Ok(())
}
}

#[derive(Accounts)]
pub struct UpdateAdmin<'info> {
#[account(mut)]
pub admin_config: Account<'info, AdminConfig>,
#[account(mut)]
pub admin: Signer<'info>,
pub new_admin: SystemAccount<'info>,
}

#[account]
pub struct AdminConfig {
admin: Pubkey,
}
  • 漏洞分析:

    • update_admin缺少校验: ctx.accounts.admin_conifg.admin == ctx.accounts.admin
  • 漏洞修复:

    • 方案 1: 在update_admin增加校验 ctx.accounts.admin_conifg.admin == ctx.accounts.admin
    • 方案 2: 使用has_one约束, 为admin_config增加约束 #[account(has_one = admin)], 这样和方案 1 等效
    • 方案 3: 使用constraint约束, 为admin_config增加约束 #[account(constraint = admin_config.admin == admin.key())], 这样和方案 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
42
43
44
45
46
47
48
49
50
//...
pub fn insecure_withdraw(ctx: Context<InsecureWithdraw>) -> Result<()> {
// 缺少对 authority的校验
let amount = ctx.accounts.token_account.amount;

let seeds = &[b"vault".as_ref(), &[ctx.bumps.vault]];
let signer = [&seeds[..]];

let cpi_ctx = CpiContext::new_with_signer(
ctx.accounts.token_program.to_account_info(),
token::Transfer {
from: ctx.accounts.token_account.to_account_info(),
authority: ctx.accounts.vault.to_account_info(),
to: ctx.accounts.withdraw_destination.to_account_info(),
},
&signer,
);

token::transfer(cpi_ctx, amount)?;
Ok(())
}
// ...

#[derive(Accounts)]
pub struct InsecureWithdraw<'info> {
#[account(
seeds = [b"vault"],
bump,
// 缺少对 authority的校验
)]
pub vault: Account<'info, Vault>,

#[account(
mut,
seeds = [b"token"],
bump,
)]
pub token_account: Account<'info, TokenAccount>,
#[account(mut)]
pub withdraw_destination: Account<'info, TokenAccount>,
pub token_program: Program<'info, Token>,
pub authority: Signer<'info>,
}

#[account]
pub struct Vault {
token_account: Pubkey,
authority: Pubkey,
withdraw_destination: Pubkey,
}

修复方案: 为 vault 增加约束

1
2
3
4
5
6
7
8
9
#[account(
mut,
seeds = [b"vault"],
bump,
has_one = authority,
has_one=token_account,
has_one = withdraw_destination,
)]
pub vault: Account<'info, Vault>,

案例 4: Re-initialization Attacks (重新初始化攻击)

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
use anchor_lang::prelude::*;
use borsh::{BorshDeserialize, BorshSerialize};

declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");

#[program]
pub mod initialization_insecure {
use super::*;

pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
let mut user = User::try_from_slice(&ctx.accounts.user.data.borrow()).unwrap();
user.authority = ctx.accounts.authority.key();
user.serialize(&mut *ctx.accounts.user.data.borrow_mut())?;
Ok(())
}
}

#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(mut)]
user: AccountInfo<'info>,
#[account(mut)]
authority: Signer<'info>,
}

#[derive(BorshSerialize, BorshDeserialize)]
pub struct User {
authority: Pubkey,
}

漏洞分析:

  • Initialize的 user 采用的 手动初始化, 没有 is_initialize标识, 可以重复初始化

修复方案:

  • 方案 1: 在User中增加is_initialize字段,并且在指令处理函数中增加 is_initialize的判断, 防止重复初始化

  • 方案 2(推荐): 使用 Anchor 的init约束, init约束通过 CPI 调用 System Program 创建一个账户,并且设置账户discrimiantor,

    • init 约束可以确保每个账户只能被初始化一次
    • init约束必须和 payerspace 一起使用
      • space: 指定账户的空间大小,这决定了支付的租金大小
        • 8字节,存放账户的discrimiantor, 即账户结构体名称的哈希
      • payer: 支付初始化账户的费用
  • 方案 3: 使用 Anchor 的 init_if_needed约束, 要谨慎:

    • 如果指定的账户不存在,它会创建并初始化该账户
    • 如果账户已经存在,它会跳过初始化步骤,直接使用现有账户。
    • init_if_needed与普通 init 的区别:
      • init 总是尝试创建新账户,如果账户已存在会失败。
      • init_if_needed 在账户存在时不会失败,而是跳过初始化。

案例 5: 相同的可修改账户

https://www.soldev.app/course/duplicate-mutable-accounts

一个”石头剪刀布”游戏程序

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
use anchor_lang::prelude::*;
use borsh::{BorshDeserialize, BorshSerialize};

declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");

#[program]
pub mod duplicate_mutable_accounts {
use super::*;

pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
ctx.accounts.new_player.player = ctx.accounts.payer.key();
ctx.accounts.new_player.choice = None;
Ok(())
}

pub fn rock_paper_scissors_shoot_insecure(
ctx: Context<RockPaperScissorsInsecure>,
player_one_choice: RockPaperScissors,
player_two_choice: RockPaperScissors,
) -> Result<()> {
ctx.accounts.player_one.choice = Some(player_one_choice);

ctx.accounts.player_two.choice = Some(player_two_choice);
Ok(())
}
}

#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(
init,
payer = payer,
space = 8 + 32 + 8
)]
pub new_player: Account<'info, PlayerState>,
#[account(mut)]
pub payer: Signer<'info>,
pub system_program: Program<'info, System>,
}

#[derive(Accounts)]
pub struct RockPaperScissorsInsecure<'info> {
#[account(mut)]
pub player_one: Account<'info, PlayerState>,
#[account(mut)]
pub player_two: Account<'info, PlayerState>,
}

#[account]
pub struct PlayerState {
player: Pubkey,
choice: Option<RockPaperScissors>,
}

#[derive(Clone, Copy, BorshDeserialize, BorshSerialize)]
pub enum RockPaperScissors {
Rock,
Paper,
Scissors,
}

漏洞分析: RockPaperScissorsInsecureplayer_oneplayer_two 可以相同, 攻击可以传入 2 个相同的地址

漏洞修复:

  • 方案 1: 直接在指令处理函数中增加判断 ctx.accounts.player_one() != ctx.account.player_two.key()

  • 方案 2(推荐): 使用 Anchor 的 constraint,

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    #[derive(Accounts)]
    pub struct RockPaperScissorsSecure<'info> {
    #[account(
    mut,
    constraint = player_one.key() != player_two.key() // 检查
    )]
    pub player_one: Account<'info, PlayerState>,
    #[account(mut)]
    pub player_two: Account<'info, PlayerState>,
    }

案例 6: type-cosplay

https://www.soldev.app/course/type-cosplay

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
use anchor_lang::prelude::*;
use borsh::{BorshDeserialize, BorshSerialize};

declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");

#[program]
pub mod type_cosplay_insecure {
use super::*;

pub fn admin_instruction(ctx: Context<AdminInstruction>) -> Result<()> {
let account_data =
AdminConfig::try_from_slice(&ctx.accounts.admin_config.data.borrow()).unwrap();
if ctx.accounts.admin_config.owner != ctx.program_id {
return Err(ProgramError::IllegalOwner.into());
}
if account_data.admin != ctx.accounts.admin.key() {
return Err(ProgramError::InvalidAccountData.into());
}
msg!("Admin {}", account_data.admin);
Ok(())
}
}

#[derive(Accounts)]
pub struct AdminInstruction<'info> {
admin_config: UncheckedAccount<'info>,
admin: Signer<'info>,
}

#[derive(BorshSerialize, BorshDeserialize)]
pub struct AdminConfig {
admin: Pubkey,
}

#[derive(BorshSerialize, BorshDeserialize)]
pub struct UserConfig {
user: Pubkey,
}

漏洞分析:

  • AdminConfigUserConfig 有相同的数据结构, 因此 2 个类型可以随意传参,

漏洞修复:

  • 方案 1: 使用 Anchor 的 Account类型, 为类型增加类型标识(Discriminator)

    1
    2
    3
    4
    5
    6
    7

    #[derive(Accounts)]
    pub struct AdminInstruction<'info> {
    #[account(has_one = admin)]
    admin_config: Account<'info, AdminConfig>,
    admin: Signer<'info>,
    }

案例 7: Arbitrary CPI

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
use anchor_lang::prelude::*;
use anchor_lang::solana_program;

declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");

#[program]
pub mod arbitrary_cpi_insecure {
use super::*;

pub fn cpi(ctx: Context<Cpi>, amount: u64) -> ProgramResult {

//
solana_program::program::invoke(
&spl_token::instruction::transfer(
ctx.accounts.token_program.key,
ctx.accounts.source.key,
ctx.accounts.destination.key,
ctx.accounts.authority.key,
&[],
amount,
)?,
&[
ctx.accounts.source.clone(),
ctx.accounts.destination.clone(),
ctx.accounts.authority.clone(),
],
)
}
}

#[derive(Accounts)]
pub struct Cpi<'info> {
source: UncheckedAccount<'info>,
destination: UncheckedAccount<'info>,
authority: UncheckedAccount<'info>,
token_program: UncheckedAccount<'info>, // 没有做任何检测
}

漏洞分析:

  • 没有检查token_program , 因此,可以传入任意值
  • 直接使用原生的invoke和指令组装进行 CPI 调用,缺少安全检查

漏洞修复:

  • 方案 1: 在cpi中增加检查

    1
    2
    3
    if &spl_token::ID != ctx.accounts.token_program.key {
    return Err(ProgramError::IncorrectProgramId);
    }
  • 方案 2: 使用 Anchor 的 CPI 模块进行 CPI 调用, Anchor 在 CPI 内部做了一系列安全检查

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
use anchor_lang::prelude::*;
use anchor_spl::token::{self, Token, TokenAccount};

declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");

#[program]
pub mod arbitrary_cpi_recommended {
use super::*;

pub fn cpi(ctx: Context<Cpi>, amount: u64) -> ProgramResult {
token::transfer(ctx.accounts.transfer_ctx(), amount)
}
}

#[derive(Accounts)]
pub struct Cpi<'info> {
source: Account<'info, TokenAccount>,
destination: Account<'info, TokenAccount>,
authority: Signer<'info>,
token_program: Program<'info, Token>,
}

impl<'info> Cpi<'info> {
pub fn transfer_ctx(&self) -> CpiContext<'_, '_, '_, 'info, token::Transfer<'info>> {
let program = self.token_program.to_account_info();
let accounts = token::Transfer {
from: self.source.to_account_info(),
to: self.destination.to_account_info(),
authority: self.authority.to_account_info(),
};
CpiContext::new(program, accounts)
}
}

案例, 对战游戏: https://github.com/Unboxed-Software/solana-arbitrary-cpi/tree/starter/programs

账户结构:

1
2
3
4
5

Gameplay Program Metadata Program Metadata Fake Program

[character A] [metadata account A] [metadata account X]
[character B] [metadata account B]

漏洞分析: 因为gameplayBattleInsecure 的 metadata_program 和 player 可以任意传入,并且指令处理函数中也没有进行判断,
那么,攻击者就可以伪造 一个 Metadata Fake 程序,在 Fake 程序中为角色设置很高health, 这样,攻击者可以一直获胜

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#[derive(Accounts)]
pub struct BattleInsecure<'info> {
pub player_one: Account<'info, Character>,
pub player_two: Account<'info, Character>,


/// CHECK: manual checks 漏洞
pub player_one_metadata: UncheckedAccount<'info>,
/// CHECK: manual checks 漏洞
pub player_two_metadata: UncheckedAccount<'info>,
/// CHECK: intentionally unchecked 漏洞
pub metadata_program: UncheckedAccount<'info>,
}

漏洞修复: 使用 Anchor 自带的 Program类型, 其中做了检查account_info.key == expected_program && account_info.executable == true

1
pub metadata_program: Program<'info, CharacterMetadata>,

案例 8: Bump Seed Canonicalization

  • 对于每个 seed, 有效的 bump 值在 [0, 255]闭区间, 共256
  • 有效的 bump 值,是确保 PDA 在 ED25519 曲线之外
  • 对于单个 bump 值,有约80%概率是有效的, 因此,生成有效 bump 值是很容易的
  • Canonical bump 指的是最大有效bump值, 从255开始递减
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
use anchor_lang::prelude::*;

declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");

#[program]
pub mod bump_seed_canonicalization_insecure {
use super::*;

pub fn set_value(ctx: Context<BumpSeed>, key: u64, new_value: u64, bump: u8) -> Result<()> {
let address = Pubkey::create_program_address(
&[key.to_le_bytes().as_ref(),
&[bump]],
ctx.program_id
).unwrap();

if address != ctx.accounts.data.key() {
return Err(ProgramError::InvalidArgument.into());
}

ctx.accounts.data.value = new_value;

Ok(())
}
}

#[derive(Accounts)]
pub struct BumpSeed<'info> {
data: Account<'info, Data>,
}

#[account]
pub struct Data {
value: u64,
}
  • 漏洞分析:

    • key 和 bump 都是由外部输入,那么,就存在碰撞风险
    • PDA 没有建立与账户 1对1绑定的关系, 用户可以传入任意的有效 key 和 bump 来生成多个PDA 账户
  • 漏洞修复:

    • 方案 1: 推荐使用 find_program_address 生成有效的 canonical bump
    • 方案 2: 使用 Anchor 的 seedsbump 约束,
      • 注意: 如果不指定 bump,由 solana 自动计算,则需要消耗更多计算单元(CU)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    // initialize account at PDA
    #[derive(Accounts)]
    #[instruction(key: u64)]
    pub struct BumpSeed<'info> {
    #[account(mut)]
    payer: Signer<'info>,
    #[account(
    init,
    seeds = [key.to_le_bytes().as_ref()],
    // 会自动生成 canonical bump, 来生成 PDA, 需要消耗更多计算单元
    // derives the PDA using the canonical bump
    bump,
    payer = payer,
    space = 8 + 8
    )]
    data: Account<'info, Data>,
    system_program: Program<'info, System>
    }

    #[account]
    pub struct Data {
    value: u64,
    }

  • 空投案例

案例9: Closing Account重入攻击

原理: 因为Solana的垃圾回收是整个交易结束之后才进行,而一笔交易包含多个指令, 在交易插入一笔发送“租金”的指令,这样,账户就不会回收

示例:

彩票案例

关于Anchor的close属性约束的细节:

  1. 执行时机:
    是的,close约束是在指令执行之后关闭账户。更具体地说,它是在指令的主要逻辑执行完成后,但在指令完全结束之前执行的。

  2. 执行顺序:
    在一个Anchor指令中,执行顺序通常是:

    • 首先执行所有的前置约束(比如检查账户所有者、初始化检查等)
    • 然后执行指令的主要逻辑
    • 最后执行close等后置约束
  3. 功能:
    close约束会做以下3件事:

    • 将账户的lamports(Solana的原生代币)转移到指定的接收者账户
    • 将账户数据的前8个字节设置为CLOSED_ACCOUNT_DISCRIMINATOR
    • 将账户的大小设置为0

注意:

  • close约束是在单个指令结束之前执行3个关闭操作(退钱,清零,改owner)
  • solana垃圾回收的时机是 整个交易 执行结束

案例10: PDA Sharing(PDA账户被多个账户共享)

  • PDA账户共享, 导致一个账户可以访问别人的PDA
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
use anchor_lang::prelude::*;
use anchor_spl::token::{self, Token, TokenAccount};

declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");

#[program]
pub mod pda_sharing_insecure {
use super::*;

pub fn withdraw_tokens(ctx: Context<WithdrawTokens>) -> Result<()> {
let amount = ctx.accounts.vault.amount;
let seeds = &[ctx.accounts.pool.mint.as_ref(), &[ctx.accounts.pool.bump]];
token::transfer(ctx.accounts.transfer_ctx().with_signer(&[seeds]), amount)
}
}

#[derive(Accounts)]
pub struct WithdrawTokens<'info> {
#[account(has_one = vault, has_one = withdraw_destination)]
pool: Account<'info, TokenPool>,
vault: Account<'info, TokenAccount>,
withdraw_destination: Account<'info, TokenAccount>,
authority: AccountInfo<'info>,
token_program: Program<'info, Token>,
}

impl<'info> WithdrawTokens<'info> {
pub fn transfer_ctx(&self) -> CpiContext<'_, '_, '_, 'info, token::Transfer<'info>> {
let program = self.token_program.to_account_info();
let accounts = token::Transfer {
from: self.vault.to_account_info(),
to: self.withdraw_destination.to_account_info(),
authority: self.authority.to_account_info(),
};
CpiContext::new(program, accounts)
}
}

#[account]
pub struct TokenPool {
vault: Pubkey,
mint: Pubkey,
withdraw_destination: Pubkey,
bump: u8,
}

漏洞分析:

注意: 因为这个示例不完整,缺少详细的细节,所以只能大概表示意思即可,不必细究

  • TokenPool.vault PDA的签名seeds是 ctx.accounts.pool.mint.as_ref()&[ctx.accounts.pool.bump, 这就导致:
    • 多个Pool 会共用同一个 Vault, 这就导致所有人都可以生成一个TokenPool并将其中的 vault的余额提走
  • 指令WithdrawTokens中的 pool , 虽然做了 vault 和 withdraw_destination的比对,但是对于调用者没有进行鉴权, 任何人都可以调用

漏洞修复:

  • 使用Anchor的 seeds 和 bump约束, 将 pool.withdraw_destination作为seeds
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

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

declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");

#[program]
pub mod pda_sharing_recommended {
use super::*;

pub fn withdraw_tokens(ctx: Context<WithdrawTokens>) -> Result<()> {
let amount = ctx.accounts.vault.amount;
let seeds = &[
ctx.accounts.pool.withdraw_destination.as_ref(),
&[ctx.accounts.pool.bump],
];
token::transfer(ctx.accounts.transfer_ctx().with_signer(&[seeds]), amount)
}
}

#[derive(Accounts)]
pub struct WithdrawTokens<'info> {
#[account(
has_one = vault,
has_one = withdraw_destination,
seeds = [withdraw_destination.key().as_ref()],
bump = pool.bump,
)]
pool: Account<'info, TokenPool>,
vault: Account<'info, TokenAccount>,
withdraw_destination: Account<'info, TokenAccount>,
token_program: Program<'info, Token>,
}

impl<'info> WithdrawTokens<'info> {
pub fn transfer_ctx(&self) -> CpiContext<'_, '_, '_, 'info, token::Transfer<'info>> {
let program = self.token_program.to_account_info();
let accounts = token::Transfer {
from: self.vault.to_account_info(),
to: self.withdraw_destination.to_account_info(),
authority: self.pool.to_account_info(),
};
CpiContext::new(program, accounts)
}
}

#[account]
pub struct TokenPool {
vault: Pubkey,
mint: Pubkey,
withdraw_destination: Pubkey,
bump: u8,
}
  • Copyrights © 2021-2025 youngqqcn

请我喝杯咖啡吧~