Solana Token 与 SPL 标准开发:从账户模型到代币发行,高性能链的资产构建

cover

一、Solana 代币开发的工程背景:账户模型与 EVM 的根本差异

Solana 的账户模型与以太坊 EVM 存在根本性差异:以太坊中合约是拥有状态的实体,代币余额存储在合约内部;Solana 中程序(Program)是无状态的,所有状态存储在独立的账户(Account)中。代币余额不是合约内的映射,而是独立的 Token Account,每个 Token Account 记录所有者地址与代币数量。

这一设计使得 Solana 的代币操作天然并行:不同用户的 Token Account 是独立的账户,可以同时更新而无需竞争锁。但这也带来了开发复杂度——转账需要指定源 Token Account 与目标 Token Account,而非简单的 transfer(to, amount)。理解账户模型是 Solana 代币开发的前提。

二、SPL Token 的账户结构与操作流程

flowchart TD
    A[创建 Mint Account] --> B[设置代币元数据]
    B --> C[创建 Token Account]
    C --> D[Mint 代币]
    D --> E[转账]
    E --> F[授权委托]

    subgraph 账户结构
        G[Mint Account: 代币定义]
        G1[mintAuthority: 铸造权限]
        G2[supply: 总供应量]
        G3[decimals: 精度]

        H[Token Account: 用户持有]
        H1[mint: 关联的 Mint]
        H2[owner: 所有者地址]
        H3[amount: 持有数量]
        H4[delegate: 委托权限]
    end

    A --> G
    C --> H

    subgraph 转账流程
        I[源 Token Account]
        J[目标 Token Account]
        K[Token Program]
        I --> K
        K --> J
    end

    E --> I

SPL Token 是 Solana 的代币标准(类似以太坊的 ERC-20),由 Token Program 实现。核心操作包括:创建 Mint(代币定义)、创建 Token Account(用户持有账户)、Mint(铸造)、Transfer(转账)、Approve(授权委托)。

三、工程实现:Solana Token 开发全流程

// solana-token.ts — Solana Token 开发工具
import {
  Connection,
  Keypair,
  PublicKey,
  Transaction,
  sendAndConfirmTransaction,
} from '@solana/web3.js';
import {
  createInitializeMintInstruction,
  createMintToInstruction,
  createTransferInstruction,
  createApproveInstruction,
  getAssociatedTokenAddressSync,
  createAssociatedTokenAccountInstruction,
  TOKEN_PROGRAM_ID,
  MINT_SIZE,
} from '@solana/spl-token';
import { BN } from '@coral-xyz/anchor';

class SolanaTokenManager {
  private connection: Connection;
  private payer: Keypair;

  constructor(rpcUrl: string, payerSecretKey: Uint8Array) {
    this.connection = new Connection(rpcUrl, 'confirmed');
    this.payer = Keypair.fromSecretKey(payerSecretKey);
  }

  // 创建新代币(Mint Account)
  async createToken(
    decimals: number = 9,
    mintAuthority?: PublicKey,
    freezeAuthority?: PublicKey,
  ): Promise<{ mint: PublicKey; signature: string }> {
    // 生成新的 Mint Account 密钥对
    const mintKeypair = Keypair.generate();

    // 计算创建 Mint Account 所需的租金
    const lamports =
      await this.connection.getMinimumBalanceForRentExemption(MINT_SIZE);

    const transaction = new Transaction().add(
      // 1. 创建 Mint Account(分配空间与租金)
      SystemProgram.createAccount({
        fromPubkey: this.payer.publicKey,
        newAccountPubkey: mintKeypair.publicKey,
        space: MINT_SIZE,
        lamports,
        programId: TOKEN_PROGRAM_ID,
      }),
      // 2. 初始化 Mint 数据
      createInitializeMintInstruction(
        mintKeypair.publicKey,
        decimals,
        mintAuthority || this.payer.publicKey,
        freezeAuthority || null,
        TOKEN_PROGRAM_ID,
      )
    );

    const signature = await sendAndConfirmTransaction(
      this.connection,
      transaction,
      [this.payer, mintKeypair],
    );

    return { mint: mintKeypair.publicKey, signature };
  }

  // 创建关联 Token Account(ATA)
  async createTokenAccount(
    mint: PublicKey,
    owner: PublicKey,
  ): Promise<{ tokenAccount: PublicKey; signature: string }> {
    // ATA 是由 owner + mint 派生的确定性地址
    const tokenAccount = getAssociatedTokenAddressSync(mint, owner);

    const transaction = new Transaction().add(
      createAssociatedTokenAccountInstruction(
        this.payer.publicKey,
        tokenAccount,
        owner,
        mint,
      )
    );

    const signature = await sendAndConfirmTransaction(
      this.connection,
      transaction,
      [this.payer],
    );

    return { tokenAccount, signature };
  }

  // 铸造代币
  async mintTokens(
    mint: PublicKey,
    destination: PublicKey,
    amount: number,
    decimals: number = 9,
  ): Promise<string> {
    const transaction = new Transaction().add(
      createMintToInstruction(
        mint,
        destination,
        this.payer.publicKey,  // mint authority
        amount * Math.pow(10, decimals),
        [],                    // multi-signers
        TOKEN_PROGRAM_ID,
      )
    );

    const signature = await sendAndConfirmTransaction(
      this.connection,
      transaction,
      [this.payer],
    );

    return signature;
  }

  // 转账代币
  async transferTokens(
    mint: PublicKey,
    from: PublicKey,
    to: PublicKey,
    amount: number,
    decimals: number = 9,
  ): Promise<string> {
    const fromATA = getAssociatedTokenAddressSync(mint, from);
    const toATA = getAssociatedTokenAddressSync(mint, to);

    const transaction = new Transaction().add(
      createTransferInstruction(
        fromATA,
        toATA,
        this.payer.publicKey,  // owner of fromATA
        amount * Math.pow(10, decimals),
        [],
        TOKEN_PROGRAM_ID,
      )
    );

    const signature = await sendAndConfirmTransaction(
      this.connection,
      transaction,
      [this.payer],
    );

    return signature;
  }
}
// custom_token_program/src/lib.rs — 自定义代币程序(Anchor 框架)
use anchor_lang::prelude::*;
use anchor_spl::token::{self, Token, Mint, TokenAccount};

declare_id!("CustomTokenProgram1111111111111111111111");

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

    // 带时间锁的转账:接收方在指定时间前不可转移
    pub fn time_locked_transfer(
        ctx: Context<TimeLockedTransfer>,
        amount: u64,
        unlock_timestamp: i64,
    ) -> Result<()> {
        // 验证解锁时间在未来
        let clock = Clock::get()?;
        require!(
            unlock_timestamp > clock.unix_timestamp,
            ErrorCode::UnlockTimeInPast
        );

        // 执行代币转账
        token::transfer(
            CpiContext::new(
                ctx.accounts.token_program.to_account_info(),
                token::Transfer {
                    from: ctx.accounts.from.to_account_info(),
                    to: ctx.accounts.to.to_account_info(),
                    authority: ctx.accounts.authority.to_account_info(),
                },
            ),
            amount,
        )?;

        // 记录时间锁信息
        let lock_account = &mut ctx.accounts.lock_account;
        lock_account.token_account = ctx.accounts.to.key();
        lock_account.unlock_timestamp = unlock_timestamp;
        lock_account.locked_amount = amount;

        Ok(())
    }
}

#[derive(Accounts)]
pub struct TimeLockedTransfer<'info> {
    #[account(mut)]
    pub from: Account<'info, TokenAccount>,
    #[account(mut)]
    pub to: Account<'info, TokenAccount>,
    pub authority: Signer<'info>,
    #[account(init, payer = authority, space = 8 + 32 + 8 + 8)]
    pub lock_account: Account<'info, TimeLock>,
    pub token_program: Program<'info, Token>,
    pub system_program: Program<'info, System>,
}

#[account]
pub struct TimeLock {
    pub token_account: Pubkey,
    pub unlock_timestamp: i64,
    pub locked_amount: u64,
}

#[error_code]
pub enum ErrorCode {
    #[msg("解锁时间不能在过去")]
    UnlockTimeInPast,
    #[msg("代币仍在锁定期内")]
    TokensLocked,
}

四、Solana 代币开发的边界与权衡

账户模型的租金成本:Solana 的每个账户都需要存入租金(SOL)以保持活跃。大量用户的 Token Account 意味着大量租金锁定。关闭不再使用的 Token Account 可回收租金,但需要用户主动操作。

Token Account 的创建摩擦:接收方必须先创建 Token Account 才能接收代币,这增加了用户操作步骤。ATA(Associated Token Account)通过确定性派生简化了这一过程,但首次接收仍需创建交易。

并行执行的编程约束:Solana 的并行执行要求交易明确声明读写的账户列表。如果两个交易写同一个账户,它们必须串行执行。代币设计中应避免"热点账户"(如全局计数器),改用每个用户独立的账户。

Program 升级的风险:Solana Program 默认可升级(通过 BPF Loader),但升级可能导致状态不兼容。建议使用 Proxy 模式:用户交互的是不可变的 Proxy Program,逻辑委托给可升级的实现 Program。

五、总结

Solana 的 SPL Token 标准基于账户模型实现代币发行与管理,核心概念包括 Mint Account(代币定义)、Token Account(用户持有)、ATA(确定性关联地址)。工程落地的关键在于:理解账户模型与 EVM 的差异、ATA 简化 Token Account 管理、避免热点账户保障并行执行、Program 升级需谨慎处理状态兼容性。Solana 的账户模型虽然增加了开发复杂度,但换来了天然并行执行的吞吐量优势——这是架构设计中的经典权衡。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐