861 lines
22 KiB
Markdown
861 lines
22 KiB
Markdown
# SPL Token Program - Common Patterns and Security
|
|
|
|
Common SPL Token patterns including escrow, staking, NFT creation, and account freezing. Comprehensive security considerations covering validation, authority checks, and defensive programming. Includes quick reference tables and security checklist.
|
|
|
|
**For related topics, see:**
|
|
- **[tokens-overview.md](tokens-overview.md)** - Token fundamentals and account structures
|
|
- **[tokens-operations.md](tokens-operations.md)** - Create, mint, transfer, burn, close operations
|
|
- **[tokens-validation.md](tokens-validation.md)** - Account validation patterns
|
|
- **[tokens-2022.md](tokens-2022.md)** - Token Extensions Program features
|
|
|
|
## Table of Contents
|
|
|
|
1. [Pattern 1: Token Escrow](#pattern-1-token-escrow)
|
|
2. [Pattern 2: Token Staking](#pattern-2-token-staking)
|
|
3. [Pattern 3: NFT Creation](#pattern-3-nft-creation)
|
|
4. [Pattern 4: Freezing and Thawing Accounts](#pattern-4-freezing-and-thawing-accounts)
|
|
5. [Security Considerations](#security-considerations)
|
|
6. [Summary](#summary)
|
|
|
|
---
|
|
|
|
## Pattern 1: Token Escrow
|
|
|
|
Program holds tokens temporarily on behalf of users.
|
|
|
|
### Using Anchor
|
|
|
|
```rust
|
|
use anchor_spl::token_interface::{self, TokenAccount, TokenInterface, Transfer};
|
|
|
|
#[derive(Accounts)]
|
|
pub struct InitializeEscrow<'info> {
|
|
#[account(
|
|
init,
|
|
payer = user,
|
|
space = 8 + 32 + 8 + 1,
|
|
seeds = [b"escrow", user.key().as_ref()],
|
|
bump,
|
|
)]
|
|
pub escrow_state: Account<'info, EscrowState>,
|
|
|
|
#[account(
|
|
init,
|
|
payer = user,
|
|
token::mint = mint,
|
|
token::authority = escrow_state,
|
|
token::token_program = token_program,
|
|
)]
|
|
pub escrow_token_account: InterfaceAccount<'info, TokenAccount>,
|
|
|
|
#[account(mut)]
|
|
pub user_token_account: InterfaceAccount<'info, TokenAccount>,
|
|
|
|
pub mint: InterfaceAccount<'info, Mint>,
|
|
|
|
#[account(mut)]
|
|
pub user: Signer<'info>,
|
|
|
|
pub token_program: Interface<'info, TokenInterface>,
|
|
pub system_program: Program<'info, System>,
|
|
}
|
|
|
|
#[account]
|
|
pub struct EscrowState {
|
|
pub user: Pubkey,
|
|
pub amount: u64,
|
|
pub bump: u8,
|
|
}
|
|
|
|
pub fn initialize_escrow(ctx: Context<InitializeEscrow>, amount: u64) -> Result<()> {
|
|
// Transfer tokens to escrow
|
|
token_interface::transfer(
|
|
CpiContext::new(
|
|
ctx.accounts.token_program.to_account_info(),
|
|
Transfer {
|
|
from: ctx.accounts.user_token_account.to_account_info(),
|
|
to: ctx.accounts.escrow_token_account.to_account_info(),
|
|
authority: ctx.accounts.user.to_account_info(),
|
|
},
|
|
),
|
|
amount,
|
|
)?;
|
|
|
|
// Save state
|
|
ctx.accounts.escrow_state.user = ctx.accounts.user.key();
|
|
ctx.accounts.escrow_state.amount = amount;
|
|
ctx.accounts.escrow_state.bump = ctx.bumps.escrow_state;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[derive(Accounts)]
|
|
pub struct ReleaseEscrow<'info> {
|
|
#[account(
|
|
mut,
|
|
seeds = [b"escrow", escrow_state.user.as_ref()],
|
|
bump = escrow_state.bump,
|
|
has_one = user,
|
|
close = user,
|
|
)]
|
|
pub escrow_state: Account<'info, EscrowState>,
|
|
|
|
#[account(mut)]
|
|
pub escrow_token_account: InterfaceAccount<'info, TokenAccount>,
|
|
|
|
#[account(mut)]
|
|
pub recipient_token_account: InterfaceAccount<'info, TokenAccount>,
|
|
|
|
pub user: Signer<'info>,
|
|
|
|
pub token_program: Interface<'info, TokenInterface>,
|
|
}
|
|
|
|
pub fn release_escrow(ctx: Context<ReleaseEscrow>) -> Result<()> {
|
|
let seeds = &[
|
|
b"escrow",
|
|
ctx.accounts.user.key().as_ref(),
|
|
&[ctx.accounts.escrow_state.bump],
|
|
];
|
|
let signer_seeds = &[&seeds[..]];
|
|
|
|
token_interface::transfer(
|
|
CpiContext::new(
|
|
ctx.accounts.token_program.to_account_info(),
|
|
Transfer {
|
|
from: ctx.accounts.escrow_token_account.to_account_info(),
|
|
to: ctx.accounts.recipient_token_account.to_account_info(),
|
|
authority: ctx.accounts.escrow_state.to_account_info(),
|
|
},
|
|
).with_signer(signer_seeds),
|
|
ctx.accounts.escrow_state.amount,
|
|
)?;
|
|
|
|
Ok(())
|
|
}
|
|
```
|
|
|
|
### Using Native Rust
|
|
|
|
```rust
|
|
use borsh::{BorshDeserialize, BorshSerialize};
|
|
use spl_token::instruction::transfer;
|
|
|
|
#[derive(BorshSerialize, BorshDeserialize)]
|
|
pub struct EscrowState {
|
|
pub user: Pubkey,
|
|
pub amount: u64,
|
|
pub bump: u8,
|
|
}
|
|
|
|
pub fn initialize_escrow(
|
|
program_id: &Pubkey,
|
|
user: &AccountInfo,
|
|
user_token_account: &AccountInfo,
|
|
escrow_token_account: &AccountInfo,
|
|
escrow_state: &AccountInfo,
|
|
amount: u64,
|
|
token_program: &AccountInfo,
|
|
) -> ProgramResult {
|
|
// Transfer tokens to escrow
|
|
invoke(
|
|
&transfer(
|
|
&spl_token::ID,
|
|
user_token_account.key,
|
|
escrow_token_account.key,
|
|
user.key,
|
|
&[],
|
|
amount,
|
|
)?,
|
|
&[user_token_account.clone(), escrow_token_account.clone(), user.clone()],
|
|
)?;
|
|
|
|
// Save escrow state
|
|
let (pda, bump) = Pubkey::find_program_address(&[b"escrow", user.key.as_ref()], program_id);
|
|
let escrow = EscrowState {
|
|
user: *user.key,
|
|
amount,
|
|
bump,
|
|
};
|
|
escrow.serialize(&mut &mut escrow_state.data.borrow_mut()[..])?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub fn release_escrow(
|
|
program_id: &Pubkey,
|
|
escrow_state: &AccountInfo,
|
|
escrow_token_account: &AccountInfo,
|
|
recipient_token_account: &AccountInfo,
|
|
escrow_pda: &AccountInfo,
|
|
amount: u64,
|
|
bump: u8,
|
|
user: &Pubkey,
|
|
) -> ProgramResult {
|
|
let signer_seeds: &[&[&[u8]]] = &[&[b"escrow", user.as_ref(), &[bump]]];
|
|
|
|
invoke_signed(
|
|
&transfer(
|
|
&spl_token::ID,
|
|
escrow_token_account.key,
|
|
recipient_token_account.key,
|
|
escrow_pda.key,
|
|
&[],
|
|
amount,
|
|
)?,
|
|
&[escrow_token_account.clone(), recipient_token_account.clone(), escrow_pda.clone()],
|
|
signer_seeds,
|
|
)?;
|
|
|
|
Ok(())
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Pattern 2: Token Staking
|
|
|
|
Users lock tokens to earn rewards.
|
|
|
|
### Using Anchor
|
|
|
|
```rust
|
|
use anchor_spl::token_interface::{self, Mint, TokenAccount, TokenInterface, Transfer};
|
|
|
|
#[derive(Accounts)]
|
|
pub struct StakeTokens<'info> {
|
|
#[account(
|
|
init_if_needed,
|
|
payer = user,
|
|
space = 8 + 32 + 8 + 8 + 1,
|
|
seeds = [b"stake", user.key().as_ref()],
|
|
bump,
|
|
)]
|
|
pub stake_account: Account<'info, StakeAccount>,
|
|
|
|
#[account(mut)]
|
|
pub user_token_account: InterfaceAccount<'info, TokenAccount>,
|
|
|
|
#[account(
|
|
mut,
|
|
seeds = [b"vault"],
|
|
bump,
|
|
)]
|
|
pub vault_token_account: InterfaceAccount<'info, TokenAccount>,
|
|
|
|
#[account(mut)]
|
|
pub user: Signer<'info>,
|
|
|
|
pub token_program: Interface<'info, TokenInterface>,
|
|
pub system_program: Program<'info, System>,
|
|
}
|
|
|
|
#[account]
|
|
pub struct StakeAccount {
|
|
pub user: Pubkey,
|
|
pub amount_staked: u64,
|
|
pub stake_timestamp: i64,
|
|
pub bump: u8,
|
|
}
|
|
|
|
pub fn stake_tokens(ctx: Context<StakeTokens>, amount: u64) -> Result<()> {
|
|
// Transfer tokens to vault
|
|
token_interface::transfer(
|
|
CpiContext::new(
|
|
ctx.accounts.token_program.to_account_info(),
|
|
Transfer {
|
|
from: ctx.accounts.user_token_account.to_account_info(),
|
|
to: ctx.accounts.vault_token_account.to_account_info(),
|
|
authority: ctx.accounts.user.to_account_info(),
|
|
},
|
|
),
|
|
amount,
|
|
)?;
|
|
|
|
// Update stake account
|
|
let clock = Clock::get()?;
|
|
ctx.accounts.stake_account.user = ctx.accounts.user.key();
|
|
ctx.accounts.stake_account.amount_staked += amount;
|
|
ctx.accounts.stake_account.stake_timestamp = clock.unix_timestamp;
|
|
ctx.accounts.stake_account.bump = ctx.bumps.stake_account;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[derive(Accounts)]
|
|
pub struct UnstakeTokens<'info> {
|
|
#[account(
|
|
mut,
|
|
seeds = [b"stake", user.key().as_ref()],
|
|
bump = stake_account.bump,
|
|
has_one = user,
|
|
)]
|
|
pub stake_account: Account<'info, StakeAccount>,
|
|
|
|
#[account(mut)]
|
|
pub user_token_account: InterfaceAccount<'info, TokenAccount>,
|
|
|
|
#[account(
|
|
mut,
|
|
seeds = [b"vault"],
|
|
bump,
|
|
)]
|
|
pub vault_token_account: InterfaceAccount<'info, TokenAccount>,
|
|
|
|
/// CHECK: Vault authority PDA
|
|
#[account(
|
|
seeds = [b"vault-authority"],
|
|
bump,
|
|
)]
|
|
pub vault_authority: UncheckedAccount<'info>,
|
|
|
|
pub user: Signer<'info>,
|
|
|
|
pub token_program: Interface<'info, TokenInterface>,
|
|
}
|
|
|
|
pub fn unstake_tokens(ctx: Context<UnstakeTokens>, amount: u64) -> Result<()> {
|
|
require!(
|
|
ctx.accounts.stake_account.amount_staked >= amount,
|
|
ErrorCode::InsufficientStake
|
|
);
|
|
|
|
let seeds = &[
|
|
b"vault-authority",
|
|
&[ctx.bumps.vault_authority],
|
|
];
|
|
let signer_seeds = &[&seeds[..]];
|
|
|
|
// Transfer tokens back to user
|
|
token_interface::transfer(
|
|
CpiContext::new(
|
|
ctx.accounts.token_program.to_account_info(),
|
|
Transfer {
|
|
from: ctx.accounts.vault_token_account.to_account_info(),
|
|
to: ctx.accounts.user_token_account.to_account_info(),
|
|
authority: ctx.accounts.vault_authority.to_account_info(),
|
|
},
|
|
).with_signer(signer_seeds),
|
|
amount,
|
|
)?;
|
|
|
|
// Update stake account
|
|
ctx.accounts.stake_account.amount_staked -= amount;
|
|
|
|
Ok(())
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Pattern 3: NFT Creation
|
|
|
|
Minting a non-fungible token (supply = 1, decimals = 0).
|
|
|
|
### Using Anchor
|
|
|
|
```rust
|
|
use anchor_spl::token_interface::{self, Mint, MintTo, SetAuthority, TokenAccount, TokenInterface};
|
|
use anchor_spl::token_interface::spl_token_2022::instruction::AuthorityType;
|
|
|
|
#[derive(Accounts)]
|
|
pub struct CreateNFT<'info> {
|
|
#[account(
|
|
init,
|
|
payer = payer,
|
|
mint::decimals = 0,
|
|
mint::authority = mint_authority,
|
|
mint::token_program = token_program,
|
|
)]
|
|
pub mint: InterfaceAccount<'info, Mint>,
|
|
|
|
#[account(
|
|
init,
|
|
payer = payer,
|
|
associated_token::mint = mint,
|
|
associated_token::authority = owner,
|
|
associated_token::token_program = token_program,
|
|
)]
|
|
pub token_account: InterfaceAccount<'info, TokenAccount>,
|
|
|
|
/// CHECK: Owner of the NFT
|
|
pub owner: UncheckedAccount<'info>,
|
|
|
|
pub mint_authority: Signer<'info>,
|
|
|
|
#[account(mut)]
|
|
pub payer: Signer<'info>,
|
|
|
|
pub token_program: Interface<'info, TokenInterface>,
|
|
pub associated_token_program: Program<'info, AssociatedToken>,
|
|
pub system_program: Program<'info, System>,
|
|
}
|
|
|
|
pub fn create_nft(ctx: Context<CreateNFT>) -> Result<()> {
|
|
// Mint exactly 1 token
|
|
token_interface::mint_to(
|
|
CpiContext::new(
|
|
ctx.accounts.token_program.to_account_info(),
|
|
MintTo {
|
|
mint: ctx.accounts.mint.to_account_info(),
|
|
to: ctx.accounts.token_account.to_account_info(),
|
|
authority: ctx.accounts.mint_authority.to_account_info(),
|
|
},
|
|
),
|
|
1,
|
|
)?;
|
|
|
|
// Remove mint authority to freeze supply
|
|
token_interface::set_authority(
|
|
CpiContext::new(
|
|
ctx.accounts.token_program.to_account_info(),
|
|
SetAuthority {
|
|
account_or_mint: ctx.accounts.mint.to_account_info(),
|
|
current_authority: ctx.accounts.mint_authority.to_account_info(),
|
|
},
|
|
),
|
|
AuthorityType::MintTokens,
|
|
None,
|
|
)?;
|
|
|
|
msg!("NFT created: {}", ctx.accounts.mint.key());
|
|
Ok(())
|
|
}
|
|
```
|
|
|
|
### Using Native Rust
|
|
|
|
```rust
|
|
use spl_token::instruction::{mint_to, set_authority, AuthorityType};
|
|
|
|
pub fn create_nft(
|
|
mint: &AccountInfo,
|
|
token_account: &AccountInfo,
|
|
mint_authority: &AccountInfo,
|
|
token_program: &AccountInfo,
|
|
) -> ProgramResult {
|
|
// 1. Mint exactly 1 token
|
|
invoke(
|
|
&mint_to(
|
|
&spl_token::ID,
|
|
mint.key,
|
|
token_account.key,
|
|
mint_authority.key,
|
|
&[],
|
|
1, // Exactly 1 token
|
|
)?,
|
|
&[mint.clone(), token_account.clone(), mint_authority.clone()],
|
|
)?;
|
|
|
|
// 2. Remove mint authority (make supply fixed)
|
|
invoke(
|
|
&set_authority(
|
|
&spl_token::ID,
|
|
mint.key,
|
|
None, // Set to None
|
|
AuthorityType::MintTokens,
|
|
mint_authority.key,
|
|
&[],
|
|
)?,
|
|
&[mint.clone(), mint_authority.clone()],
|
|
)?;
|
|
|
|
Ok(())
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Pattern 4: Freezing and Thawing Accounts
|
|
|
|
### Using Anchor
|
|
|
|
```rust
|
|
use anchor_spl::token_interface::{self, FreezeAccount, Mint, ThawAccount, TokenAccount, TokenInterface};
|
|
|
|
#[derive(Accounts)]
|
|
pub struct FreezeTokenAccount<'info> {
|
|
#[account(
|
|
mint::freeze_authority = freeze_authority,
|
|
)]
|
|
pub mint: InterfaceAccount<'info, Mint>,
|
|
|
|
#[account(mut)]
|
|
pub token_account: InterfaceAccount<'info, TokenAccount>,
|
|
|
|
pub freeze_authority: Signer<'info>,
|
|
|
|
pub token_program: Interface<'info, TokenInterface>,
|
|
}
|
|
|
|
pub fn freeze_account(ctx: Context<FreezeTokenAccount>) -> Result<()> {
|
|
token_interface::freeze_account(
|
|
CpiContext::new(
|
|
ctx.accounts.token_program.to_account_info(),
|
|
FreezeAccount {
|
|
account: ctx.accounts.token_account.to_account_info(),
|
|
mint: ctx.accounts.mint.to_account_info(),
|
|
authority: ctx.accounts.freeze_authority.to_account_info(),
|
|
},
|
|
),
|
|
)?;
|
|
Ok(())
|
|
}
|
|
|
|
pub fn thaw_account(ctx: Context<FreezeTokenAccount>) -> Result<()> {
|
|
token_interface::thaw_account(
|
|
CpiContext::new(
|
|
ctx.accounts.token_program.to_account_info(),
|
|
ThawAccount {
|
|
account: ctx.accounts.token_account.to_account_info(),
|
|
mint: ctx.accounts.mint.to_account_info(),
|
|
authority: ctx.accounts.freeze_authority.to_account_info(),
|
|
},
|
|
),
|
|
)?;
|
|
Ok(())
|
|
}
|
|
```
|
|
|
|
### Using Native Rust
|
|
|
|
```rust
|
|
use spl_token::instruction::{freeze_account, thaw_account};
|
|
|
|
pub fn freeze_token_account(
|
|
token_account: &AccountInfo,
|
|
mint: &AccountInfo,
|
|
freeze_authority: &AccountInfo,
|
|
token_program: &AccountInfo,
|
|
) -> ProgramResult {
|
|
invoke(
|
|
&freeze_account(
|
|
token_program.key,
|
|
token_account.key,
|
|
mint.key,
|
|
freeze_authority.key,
|
|
&[],
|
|
)?,
|
|
&[
|
|
token_account.clone(),
|
|
mint.clone(),
|
|
freeze_authority.clone(),
|
|
token_program.clone(),
|
|
],
|
|
)?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub fn thaw_token_account(
|
|
token_account: &AccountInfo,
|
|
mint: &AccountInfo,
|
|
freeze_authority: &AccountInfo,
|
|
token_program: &AccountInfo,
|
|
) -> ProgramResult {
|
|
invoke(
|
|
&thaw_account(
|
|
token_program.key,
|
|
token_account.key,
|
|
mint.key,
|
|
freeze_authority.key,
|
|
&[],
|
|
)?,
|
|
&[
|
|
token_account.clone(),
|
|
mint.clone(),
|
|
freeze_authority.clone(),
|
|
token_program.clone(),
|
|
],
|
|
)?;
|
|
|
|
Ok(())
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Security Considerations
|
|
|
|
### 1. Always Validate Token Accounts
|
|
|
|
#### Anchor Approach
|
|
|
|
```rust
|
|
#[derive(Accounts)]
|
|
pub struct SafeTransfer<'info> {
|
|
#[account(
|
|
mut,
|
|
constraint = source.mint == mint.key() @ ErrorCode::InvalidMint,
|
|
constraint = source.owner == authority.key() @ ErrorCode::InvalidOwner,
|
|
)]
|
|
pub source: InterfaceAccount<'info, TokenAccount>,
|
|
|
|
#[account(
|
|
mut,
|
|
constraint = destination.mint == mint.key() @ ErrorCode::InvalidMint,
|
|
)]
|
|
pub destination: InterfaceAccount<'info, TokenAccount>,
|
|
|
|
pub mint: InterfaceAccount<'info, Mint>,
|
|
|
|
pub authority: Signer<'info>,
|
|
|
|
pub token_program: Interface<'info, TokenInterface>,
|
|
}
|
|
```
|
|
|
|
#### Native Rust Approach
|
|
|
|
```rust
|
|
// ❌ Dangerous - no validation
|
|
pub fn unsafe_transfer(
|
|
source: &AccountInfo,
|
|
destination: &AccountInfo,
|
|
authority: &AccountInfo,
|
|
) -> ProgramResult {
|
|
// No checks! Attacker can pass any accounts
|
|
invoke(&transfer_instruction, &accounts)?;
|
|
Ok(())
|
|
}
|
|
|
|
// ✅ Safe - validates everything
|
|
pub fn safe_transfer(
|
|
source: &AccountInfo,
|
|
destination: &AccountInfo,
|
|
authority: &AccountInfo,
|
|
expected_mint: &Pubkey,
|
|
) -> ProgramResult {
|
|
// Validate source
|
|
validate_token_account(source, authority.key, expected_mint)?;
|
|
|
|
// Validate destination
|
|
let dest_token = TokenAccount::unpack(&destination.data.borrow())?;
|
|
if dest_token.mint != *expected_mint {
|
|
return Err(ProgramError::InvalidAccountData);
|
|
}
|
|
|
|
invoke(&transfer_instruction, &accounts)?;
|
|
Ok(())
|
|
}
|
|
```
|
|
|
|
### 2. Check Token Program ID
|
|
|
|
#### Anchor Approach
|
|
|
|
```rust
|
|
// Anchor automatically validates via Interface type
|
|
pub token_program: Interface<'info, TokenInterface>,
|
|
```
|
|
|
|
#### Native Rust Approach
|
|
|
|
```rust
|
|
pub fn validate_token_program(token_program: &AccountInfo) -> ProgramResult {
|
|
if token_program.key != &spl_token::ID && token_program.key != &spl_token_2022::ID {
|
|
msg!("Invalid Token Program");
|
|
return Err(ProgramError::IncorrectProgramId);
|
|
}
|
|
Ok(())
|
|
}
|
|
```
|
|
|
|
### 3. Verify Mint Matches
|
|
|
|
**Attack scenario:** Attacker passes token account for wrong mint.
|
|
|
|
#### Anchor Approach
|
|
|
|
```rust
|
|
#[account(
|
|
constraint = token_account.mint == expected_mint.key() @ ErrorCode::InvalidMint,
|
|
)]
|
|
pub token_account: InterfaceAccount<'info, TokenAccount>,
|
|
```
|
|
|
|
#### Native Rust Approach
|
|
|
|
```rust
|
|
// Always verify mint
|
|
let source_token = TokenAccount::unpack(&source.data.borrow())?;
|
|
let dest_token = TokenAccount::unpack(&dest.data.borrow())?;
|
|
|
|
if source_token.mint != dest_token.mint {
|
|
msg!("Mint mismatch between source and destination");
|
|
return Err(ProgramError::InvalidAccountData);
|
|
}
|
|
```
|
|
|
|
### 4. Authority Checks
|
|
|
|
#### Anchor Approach
|
|
|
|
```rust
|
|
#[account(
|
|
constraint = token_account.owner == authority.key() @ ErrorCode::Unauthorized,
|
|
)]
|
|
pub token_account: InterfaceAccount<'info, TokenAccount>,
|
|
|
|
pub authority: Signer<'info>, // Automatically validates is_signer
|
|
```
|
|
|
|
#### Native Rust Approach
|
|
|
|
```rust
|
|
// Verify authority matches token account owner
|
|
let token_account = TokenAccount::unpack(&token_account_info.data.borrow())?;
|
|
|
|
if token_account.owner != *authority.key {
|
|
msg!("Authority doesn't own token account");
|
|
return Err(ProgramError::IllegalOwner);
|
|
}
|
|
|
|
// Verify authority signed
|
|
if !authority.is_signer {
|
|
msg!("Authority must sign");
|
|
return Err(ProgramError::MissingRequiredSignature);
|
|
}
|
|
```
|
|
|
|
### 5. Account State Checks
|
|
|
|
#### Anchor Approach
|
|
|
|
```rust
|
|
use spl_token::state::AccountState;
|
|
|
|
pub fn check_not_frozen(ctx: Context<SomeContext>) -> Result<()> {
|
|
let token_account = &ctx.accounts.token_account;
|
|
|
|
require!(
|
|
token_account.state == AccountState::Initialized,
|
|
ErrorCode::AccountFrozen
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
```
|
|
|
|
#### Native Rust Approach
|
|
|
|
```rust
|
|
let token_account = TokenAccount::unpack(&token_account_info.data.borrow())?;
|
|
|
|
// Check not frozen
|
|
if token_account.state == spl_token::state::AccountState::Frozen {
|
|
msg!("Token account is frozen");
|
|
return Err(ProgramError::InvalidAccountData);
|
|
}
|
|
|
|
// Check initialized
|
|
if token_account.state == spl_token::state::AccountState::Uninitialized {
|
|
msg!("Token account not initialized");
|
|
return Err(ProgramError::UninitializedAccount);
|
|
}
|
|
```
|
|
|
|
### 6. Use TransferChecked Over Transfer
|
|
|
|
**Why:** `transfer_checked` validates the mint and decimals, preventing certain attack vectors.
|
|
|
|
#### Anchor Approach
|
|
|
|
```rust
|
|
// ✅ Preferred - validates mint and decimals
|
|
token_interface::transfer_checked(
|
|
cpi_context,
|
|
amount,
|
|
decimals,
|
|
)?;
|
|
|
|
// ❌ Less secure - no mint/decimal validation
|
|
token_interface::transfer(
|
|
cpi_context,
|
|
amount,
|
|
)?;
|
|
```
|
|
|
|
#### Native Rust Approach
|
|
|
|
```rust
|
|
// ✅ Preferred
|
|
invoke(
|
|
&transfer_checked(
|
|
token_program.key,
|
|
source.key,
|
|
mint.key,
|
|
destination.key,
|
|
authority.key,
|
|
&[],
|
|
amount,
|
|
decimals,
|
|
)?,
|
|
&accounts,
|
|
)?;
|
|
|
|
// ❌ Less secure
|
|
invoke(
|
|
&transfer(
|
|
token_program.key,
|
|
source.key,
|
|
destination.key,
|
|
authority.key,
|
|
&[],
|
|
amount,
|
|
)?,
|
|
&accounts,
|
|
)?;
|
|
```
|
|
|
|
---
|
|
|
|
## Summary
|
|
|
|
### Key Takeaways
|
|
|
|
**Anchor Advantages:**
|
|
- Automatic account validation through constraints
|
|
- Cleaner, more concise code
|
|
- Built-in safety checks
|
|
- Type-safe account structures
|
|
- Simplified CPI with `CpiContext`
|
|
|
|
**Native Rust Advantages:**
|
|
- Full control over all operations
|
|
- No framework overhead
|
|
- Explicit validation (can be more transparent)
|
|
- Useful for understanding low-level mechanics
|
|
|
|
### Common Operations Quick Reference
|
|
|
|
| Operation | Anchor Module | Native Rust Crate |
|
|
|-----------|---------------|-------------------|
|
|
| Mint tokens | `token_interface::mint_to` | `spl_token::instruction::mint_to` |
|
|
| Transfer tokens | `token_interface::transfer` | `spl_token::instruction::transfer` |
|
|
| Transfer checked | `token_interface::transfer_checked` | `spl_token::instruction::transfer_checked` |
|
|
| Burn tokens | `token_interface::burn` | `spl_token::instruction::burn` |
|
|
| Create ATA | `associated_token` constraint | `spl_associated_token_account` |
|
|
| Close account | `token_interface::close_account` | `spl_token::instruction::close_account` |
|
|
| Freeze account | `token_interface::freeze_account` | `spl_token::instruction::freeze_account` |
|
|
|
|
### Security Checklist
|
|
|
|
- ✅ Validate token program ID
|
|
- ✅ Verify token account ownership
|
|
- ✅ Check mint matches expected
|
|
- ✅ Confirm authority is signer
|
|
- ✅ Ensure account not frozen
|
|
- ✅ Validate ATA derivation if applicable
|
|
- ✅ Use `transfer_checked` instead of `transfer`
|
|
- ✅ Validate account state (initialized/frozen)
|
|
- ✅ Check sufficient balance before operations
|
|
|
|
### Token Account Sizes
|
|
|
|
- **Mint account:** 82 bytes
|
|
- **Token account:** 165 bytes
|
|
- **Token-2022 with extensions:** 82/165 + extension sizes
|
|
|
|
Token integration is fundamental for DeFi, NFT, and gaming programs on Solana. Whether using Anchor or native Rust, understanding both approaches provides the flexibility to choose the right tool for your use case.
|