这篇文章以官方 Merkle-Distributor 为核心,拆解 Solana 智能合约发币、白名单分发、安全验证 的完整思路,帮助你 15 分钟写出可生产环境部署的 Solana Token 分发程序。
1. Solana 发币为什么推荐 Merkle 空投
传统逐笔转账在 Solana 存在 存储租金昂贵 的痛点:
- 每新建一个 Token Account 需 0.002039 SOL 租金;
- 空投 1 万地址即消耗 ≈ 20 SOL。
Merkle 空投把 Gas 转移给 领取人,官方实测单账户领取仅 0.00001 SOL,成本低 2000 倍。同时白名单上链后不可篡改,运营仅需一次交易即可发布全局根哈希。
👉 零 Gas 空投新范式:从 20 SOL 到 0.001 SOL 的实测对比
2. 核心数据结构:MerkleDistributor 与 ClaimStatus
#[account]
pub struct MerkleDistributor {
pub base: Pubkey, // 创建者签名地址,用于生成 PDA
pub bump: u8, // PDA bump
pub root: [u8; 32], // 256 bit Merkle root
pub mint: Pubkey, // 待分发 token 地址
pub max_total_claim: u64, // 可领取总额上限
pub max_num_nodes: u64, // 最大领取地址数
pub total_amount_claimed: u64, // 实时已领取总额
pub num_nodes_claimed: u64, // 实时已领取地址数
}#[account]
pub struct ClaimStatus {
pub is_claimed: bool, // 是否已领取
pub claimant: Pubkey, // 领取人
pub claimed_at: i64, // 领取时间戳
pub amount: u64, // 本次领取数量
}两个数据结构只需一次链上存储即可覆盖无限领取场景,逻辑极度精简。
3. 合约入口:new_distributor 完成功能初始化
pub fn new_distributor(
ctx: Context<NewDistributor>,
_bump: u8,
root: [u8; 32],
max_total_claim: u64,
max_num_nodes: u64,
) -> Result<()> {
let d = &mut ctx.accounts.distributor;
d.base = ctx.accounts.base.key();
d.bump = unwrap_bump!(ctx, "distributor");
d.root = root;
d.mint = ctx.accounts.mint.key();
d.max_total_claim = max_total_claim;
d.max_num_nodes = max_num_nodes;
d.total_amount_claimed = 0;
d.num_nodes_claimed = 0;
Ok(())
}要点:
root由链下生成,所有叶子节点格式(index, address, amount);max_total_claim和max_num_nodes用于二次校验,防止过度领取;- 创建完成后,把 token 打进
distributorATA 即可开始空投。
4. claim 逻辑:零知识校验与 SPL 转账
领取函数共分三步:
- Proof 验证:将链下生成的 Merkle Proof 与链上 root 比对。
- 防重放:若当前
(index, distributor)对应的ClaimStatus已存在则直接拒绝。 - Token 分发:通过 PDA 签名完成 SPL 转账。
let node = keccak::hashv(&[
&index.to_le_bytes(),
&claimant_account.key().to_bytes(),
&amount.to_le_bytes(),
]);
invariant!(
merkle_proof::verify(proof, distributor.root, node.0),
InvalidProof
);转账时使用 Anchor 的 CPI 封装,避免手写 instruction,安全且易维护。
token::transfer(
CpiContext::new(
ctx.accounts.token_program.to_account_info(),
token::Transfer {
from: from_ata.to_account_info(),
to: to_ata.to_account_info(),
authority: distributor.to_account_info(),
},
)
.with_signer(&[&seeds[..]]),
amount,
)?;领取结束后自动发出 ClaimedEvent,前端实时监听即可更新进度条。
5. 动态扩容技巧:更新 root 的取舍
上述程序 不允许更改 root。如需追加空投,可升级逻辑:
- 新建新的
MerkleDistributor,用base区分; - 或预留
update_root权限,引入多签控制。
安全提示:随意升级 root 可能破坏原有领取承诺,必须 同步公告 最新白名单。
6. Merkle 树 & Proof 构建脚本(Python 精简版)
import hashlib, json
def encode_leaf(i: int, addr: str, amt: int) -> bytes:
return hashlib.keccak256(
i.to_bytes(8, 'little') +
bytes.fromhex(addr) +
amt.to_bytes(8, 'little')
).digest()
def build_tree(leaves: list[bytes]) -> tuple:
# 标准 Merkle 树实现,略
return root, proofs
if __name__ == '__main__':
data = [
{"index": 0, "address": "A9...", "amount": 1000},
{"index": 1, "address": "B8...", "amount": 500},
]
leaves = [encode_leaf(d["index"], d["address"][2:], d["amount"]) for d in data]
root, proofs = build_tree(leaves)
# 输出 proofs.json 供前端调用脚本跑出的 root 可直接填入 new_distributor 参数,proofs.json 发布在前端 CDN,减少服务器压力。
FAQ
Q1:领取失败提示 InvalidProof 怎么办?
A:先校验链下脚本与合约使用的叶子序列化格式是否一致;确认 index、地址、金额顺序及小端序无误。
Q2:可以一次发给几十万地址吗?
A:逻辑支持,但建议单 Merkle 树控制在 < 10 万节点,否则 Proof 太大导致交易超出 1.2 MB 上限。
Q3:领取后还能重复领吗?
A:不能,ClaimStatus 作为唯一 PDA 阻止双花,同时合约校验 is_claimed flag。
Q4:SOL 转账与 SPL 转账傻傻分不清?
A:
- SOL 使用
system_instruction::transfer; - SPL token 调用
spl_token::instruction::transfer,签名者不同。请勿混用,否则报invalid account data。
Q5:官方 code 是 GPL-3.0,我能否闭源商用?
A:需遵循 GPL-3.0 开源协议;如必须闭源,可参考思路自行从零开发,确保无 GPL 代码痕迹。
Q6:前端如何拿到领取进度?
A:订阅 ClaimedEvent,按 index 与本地 merkle_proofs 列表比对,即可实时渲染已领取/未领取状态。
7. 小结:三步完成 Solana Token 分发
- 链下生成白名单 + Merkle root;
- 调用
new_distributor初始化; - 用户前端调用
claim,带 Proof 实时领取。
整个过程零后端、零延时、零单点故障,真正做到了 去中心化空投。现在就在测试网部署试试吧!