T11 · Token Program
为什么 Solana 上的 Token 不是一份独立合约?
- 练习的能力
- Builder
- 动手
- 在 Devnet 创建一个 Mint,给两个钱包各建 ATA,完成一次转账。
- AI Lab
- 让 AI 解释 Token-2022 的扩展能力,再核对哪些扩展会让常见钱包显示异常。
一个现实问题
在 EVM 上发一个代币,流程你已经很熟了:写一份合约、部署、拿到一个地址,用户的余额存在你这份合约的 mapping 里。你的代币 = 你的一份代码。
你到 Solana 上做同一件事,三个意外接连出现。
第一个:你没有部署任何程序,只发了一笔交易,代币就有了。总量、精度、谁能增发,全部设置好了。你写的代码行数是零。
第二个:你想给朋友转 100 个,失败了。原因是「对方还没有这个代币的账户」。你得先替他开一个户,而且开户要交一笔押金。
第三个:你想在转账时加一点自定义逻辑——比如每笔抽 1%。你找不到任何地方可以写这段代码。
三个意外指向同一个问题:这个代币的逻辑到底跑在哪里?如果不是我的代码,那是谁的?
思想实验
对比两种发币方式。
方式 A:每个代币自己开一家银行。
你盖一栋楼(部署合约),楼里放一本账本(mapping),账本上记着「谁有多少」。一千个代币就是一千栋楼、一千本账本、一千份几乎一模一样的代码。
钱包要支持一千个代币,理论上要认识一千栋楼——好在大家盖的楼长得差不多(T9 的标准),所以钱包按同一套接口去问就行。但「长得差不多」不是强制的,所以就有了 T9 那三天的事故。
方式 B:全世界只有一套银行软件。
不盖楼了。所有代币共用同一份程序。每个代币只是一条发行信息:总量多少、精度几位、谁能增发、谁能冻结。而每个人的余额,是一个属于他自己的独立账户,账户里写着三件事:这是哪个代币、属于谁、有多少。
现在把这个模型的后果一条条推出来,这一章的所有内容都在里面:
- 钱包只要认识一份程序,就认识所有代币。 显示、转账、授权全部统一,不需要为每个代币适配。
- 代币作者没有地方写自定义逻辑。 转账的代码是那份共享程序的,不是你的。于是 T9 里的「转账扣费」「余额自变」这些东西,在这个模型下根本不可能存在。
- 余额账户必须显式创建。 因为它是一个独立的账户,得有人创建它、有人为它的存储付押金。「给谁转账,谁得先有户」就是从这来的。
- 想要自定义行为,只能由那份共享程序开放扩展点。 于是有了新版本的代币程序和它的扩展机制。
第 2 条和第 4 条合起来是这一章最有意思的地方:Solana 先用「共享程序」消灭了 T9 的那一整类坑,然后又用「扩展」把其中一部分有节制地放了回来。
你来决定
你要给 500 个新用户空投代币,他们大多数还没有这个代币的账户。怎么处理?
观察结果
四个选项本质上都在回答同一个问题:这块存储的押金,由谁出,归谁。
| 做法 | 谁付押金 | 押金归谁 | 失败风险 | 适合场景 |
|---|---|---|---|---|
| 直接转 | 无 | — | 高,对方没户就失败 | 已知对方有账户 |
| 你替他开户 | 你 | 用户 | 低 | 主动推送、要求高转化 |
| 用户自己开 | 用户 | 用户 | 低 | 主动领取式发放 |
| 同笔交易幂等创建 | 由你指定 | 用户 | 最低 | 几乎所有场景的默认做法 |
一条值得单独记住的结论:
同一笔存储成本,在 EVM 上藏在 Gas 里由转账者一次性烧掉,在 Solana 上变成一笔可退还的押金,归账户所有者。
这不是谁更便宜的问题,是成本被放在了不同的位置,因此可以被不同的人利用。上面那个「领空投再关户拿押金」的套路,在 EVM 上根本不存在,因为那笔钱一开始就烧掉了,没有人能退。
设计任何 Solana 上的发放机制时,把这一条放进风险清单:你付出去的押金,最终会落到谁手里。
建立模型
两种账户
Solana 上的代币由两类账户构成,它们都是 T10 讲的普通账户,owner 是那份共享的代币程序。
Mint 账户(一个代币一个)
├─ supply 当前总量
├─ decimals 精度
├─ mintAuthority 谁能增发。可以设成「无」,从此不可增发
└─ freezeAuthority 谁能冻结持有人的账户。可以设成「无」
Token 账户(一个人一个代币一个)
├─ mint 这是哪个代币的账户
├─ owner 这个账户属于谁
├─ amount 余额
├─ delegate 授权给谁,以及授权多少
└─ state 正常 / 已冻结关联账户是其中最重要的一个约定:一个人的某个代币账户,地址由「持有人地址 + 代币程序 + Mint 地址」派生出来。这正是 T10 的派生地址在起作用——任何人都能算出「某某人的某某代币账户在哪」,不需要查询。
钱包能列出你所有的代币余额,靠的就是这个:按 owner 过滤出所有属于你的代币账户。
和 ERC20 逐条对照
这张表是这一章的核心产出,值得反复看:
| 问题 | ERC20 | Solana 上的 Token |
|---|---|---|
| 代码在哪 | 每个代币一份,各写各的 | 全网共享一份程序 |
| 发一个新代币 | 部署一份合约 | 发一笔交易创建 Mint 账户 |
| 余额存在哪 | 代币合约里的 mapping | 持有人各自独立的账户 |
| 收款前需要准备吗 | 不需要 | 需要先有账户并付押金 |
| 能否自定义转账逻辑 | 能,这是 T9 所有坑的来源 | 基础版不能;新版通过扩展有限开放 |
| 授权模型 | approve 额度 | delegate,一个账户同时只能有一个 |
| 谁付存储成本 | 转账者,烧在 Gas 里 | 账户创建者,一笔可退押金 |
| 名称、图标等元数据 | 写在合约里 | 由另外的程序或扩展提供 |
| 增发权限 | 合约代码里的逻辑 | Mint 账户上的一个字段,可以被放弃 |
| 冻结某人 | 需要代币自己实现黑名单 | 程序原生支持,看 freezeAuthority 在不在 |
倒数两行特别值得注意:增发和冻结在这里是程序原生能力,写在数据字段上,任何人都能一眼查到。 在 EVM 上你得读源码才知道有没有后门;在这里,只要读一下 Mint 账户,mintAuthority 和 freezeAuthority 是不是空,一目了然。
这是一个被低估的优点:把权限变成数据,比把权限藏在代码里更容易审计。
新版代币程序与扩展
共享程序解决了一致性,也带来了刚性:没有人能加任何新功能。于是出现了一个新版本的代币程序,它在保持同样模型的同时,允许在创建 Mint 时选择开启若干扩展。
常见的扩展覆盖这几类能力:转账时收取费用、代币不可转让、新账户默认冻结、元数据直接存在 Mint 上、利息累积、以及在转账时调用一个外部程序的钩子。
到这里,一件值得停下来想的事发生了:
转账收费、余额自变、转账时调用外部代码——这些正是 T9 里让集成方翻车的那几类行为。
区别在哪?在于它们现在是显式声明在账户上的,而不是藏在代码里的。集成方可以在接受一个代币之前,读一遍它开了哪些扩展,然后决定支不支持。T9 里你必须读源码才能发现的东西,在这里变成了一个可以被程序化检查的字段。
但风险并没有消失,只是换了形态:
- 钱包和协议需要逐个扩展去适配。 一个开了冷门扩展的代币,在很多界面上会显示异常,或者干脆不显示。
- 转账钩子意味着转账会执行外部代码。 集成方必须把它当成不可信的外部调用来对待——这和 T9 里那个带回调的代币标准是同一类风险。
- 老的集成方完全不认识这些扩展。 按旧假设写的代码,遇到收费扩展照样记错账。
所以这一章的真正结论是:共享程序消灭了「行为不可预测」,扩展把它以「可声明、可检查」的形式放了回来。可检查不等于安全,只是让检查成为可能。
它叫什么
一份被所有代币共享的程序。它定义了创建、增发、转账、授权、冻结、销毁这些动作。
因为所有代币共用它,钱包和协议只需要适配一次。代币作者不部署代码,只创建数据。
一个代币的发行信息账户:总量、精度、增发权限、冻结权限。
它就是这个代币的「地址」——人们说「某某代币的地址」时,指的是它的 Mint 账户地址。
某个人持有某个代币的账户,记录着所属代币、所有者、余额、授权和冻结状态。
一个人持有 N 种代币,就有 N 个这样的账户,每个都需要免租押金。
由「持有人 + 代币」派生出来的那个代币账户,地址可以被任何人算出来。
它让「某某人的某某代币在哪」成为一个确定的答案,而不需要查询或维护索引。这是 PDA 在实际系统里最成功的一个应用。
写在 Mint 账户上的两个字段。可以被永久放弃,设成「无」。
研究任何一个 Solana 代币时,第一件事就是读这两个字段。增发权限还在,意味着总量随时可以变;冻结权限还在,意味着发行方可以随时让任何人的余额动不了。
新版代币程序的一个扩展:转账时调用一个指定的外部程序。
它让代币重新获得了自定义转账行为的能力,也把「转账会执行不可信代码」这个风险带了回来。集成时要按外部调用对待,而不是按一次普通转账。
动手
全程用命令行,不写一行代码。目的是让账户模型在你手上变成具体的东西。
切到 Devnet 并领水。
solana config set --url devnet
solana address
solana airdrop 2
solana balance全程 Devnet,不要用任何持有真实资产的钱包。 给这个练习单独生成一个密钥文件。
创建一个 Mint。
spl-token create-token --decimals 6记下输出的 Mint 地址。注意这一步的本质:你只是创建了一个账户,没有部署任何程序。
然后去读它:
solana account <MINT 地址>
spl-token display <MINT 地址>看一眼 mintAuthority 和 freezeAuthority 现在是谁——是你。
给自己创建代币账户并增发。
spl-token create-account <MINT 地址>
spl-token mint <MINT 地址> 1000
spl-token balance <MINT 地址>创建账户那一步,注意命令输出里的押金金额,再用 solana balance 对比前后的变化。那笔钱没有消失,它躺在你刚创建的账户里。
生成第二个钱包,给它转账。
solana-keygen new --no-bip39-passphrase -o ./wallet2.json
solana address -k ./wallet2.json先试一次不加任何额外参数的转账,看它怎么失败:
spl-token transfer <MINT 地址> 10 <WALLET2 地址>错误信息会告诉你对方没有账户。这就是本章第二个意外的现场。
再加上「顺便帮对方创建账户」的参数重试:
spl-token transfer <MINT 地址> 10 <WALLET2 地址> --fund-recipient这一次成功了,而且你的 SOL 余额又少了一笔——你替对方付了押金。
验证关联账户地址是「算」出来的。
spl-token address --token <MINT 地址> --owner <WALLET2 地址> --verbose
spl-token accounts --owner <WALLET2 地址>你在对方什么都没做的情况下,就知道了他的代币账户地址。这就是派生地址的价值。
放弃增发权限,再试着增发。
spl-token authorize <MINT 地址> mint --disable
spl-token mint <MINT 地址> 1第二条命令会失败。现在再 spl-token display 一次,mintAuthority 已经是空的了。
这个字段就是别人研究你的代币时会看的第一样东西。 你刚刚做的,是一次公开且不可逆的承诺。
用新版代币程序再来一遍。 先查参数:
spl-token create-token --help从帮助里找到指定新版程序的参数,以及开启扩展的参数。挑一个扩展(比如转账费)创建第二个代币,走完同样的流程,然后用一个常见钱包导入这两个代币,对比显示效果。
不同钱包的支持程度差别很大,这一步的观察正是下面 AI Lab 要核对的东西。
AI Lab
分三步,第三步是这个 Lab 真正的产出。
第一步:
列出新版 Solana 代币程序提供的扩展能力。每一个说明:
它改变了什么行为、必须在什么时候开启、能不能事后关闭。
如果你不确定某个扩展是否存在,明确标注出来。
第二步:
在这些扩展里,哪些会让按旧假设写的集成方出错?
具体说明:会错在哪一步、错的表现是什么。
第三步:
把这些扩展和 ERC20 世界里的非标准实现做一个对照表:
哪些是同一类问题的两种形态?哪些是 Solana 独有的?第一步你会拿到一份看起来很完整的清单。逐个去官方文档搜。 模型在这类「枚举一个规范里有哪些条目」的任务上,命中率高,但几乎一定会混进一两个不存在的——而它编出来的名字通常非常合理,因为它是按「这里应该有一个这样的能力」推出来的。
第二步质量通常不错,因为这是推理题不是记忆题。记住这条分界线:需要精确回忆的交给核对,需要归纳推理的交给模型。 T9 的 AI Lab 里你已经见过一次,这里是同一条规律的第二个样本。
第三步是给你自己的收获。做完这张对照表,你会发现 EVM 和 Solana 在代币这件事上是同一组权衡的两种不同排布:一个默认开放、靠生态约定收敛;一个默认统一、靠显式扩展放开。哪种更好取决于你在意什么,但两边的集成方都必须做同一件事:在接受一个代币之前,搞清楚它到底会怎么行动。
最后动手验一次:在 Devnet 上创建几个开了不同扩展的代币,用两三个常见钱包导入,截图对比。模型说的「钱包支持情况」几乎全部需要实测,因为这是一个每周都在变的事实。
AI 说完之后,你必须自己验证
- 它列出的每一个扩展名,你都在官方文档里查到了;模型很擅长编出听起来很合理的扩展名
- 它说「某某钱包支持或不支持」的每一条结论,你都在 Devnet 上用真实钱包试过
- 它有没有把扩展说成默认开启,实际上扩展必须在创建 Mint 时显式选择
- 它给的 CLI 参数,你都跑过 --help 核对过是否存在于你装的这个版本
- 它有没有说清哪些扩展会改变转账的实际到账金额,哪些不会
- 它有没有把这些扩展和 ERC20 的非标准实现做对比,如果没有,追问它
真实案例
一个交易所在支持某个代币的提现时,直接调用转账,没有处理「用户还没有这个代币账户」的情况。
结果是大量提现失败,用户反复重试,客服被淹没。更糟的是有些系统把失败当成了成功,账目对不上。
在 Solana 上做任何转出,都要先回答「对方有账户吗,没有的话谁来创建」。 这是一个在 EVM 上根本不存在的必答题。
一个代币开启了转账费扩展。某个按「转出金额」记账的协议没有处理它,收到的比记的少。
这和 T9 里那个扣费代币的事故一模一样,只是换了一条链和一种表达方式。
区别在于:这一次,协议本来可以在接受这个代币之前,读一下它的 Mint 账户就发现这个扩展。检查的可能性存在了,但检查还是得有人去做。
一些代币发行方保留了冻结权限,意味着他们可以随时让任意持有人的账户无法转出。
对合规型稳定币来说这是必需的功能;对一个声称去中心化的项目来说,这是一个很多人没注意到的后门。
好消息是这件事完全公开且一秒就能查到:读 Mint 账户,看 freezeAuthority 在不在。这应该出现在你研究任何 Solana 代币的第一步。
代币账户在余额清零后可以被关闭,押金退还给账户所有者。
在「项目方替用户创建账户」的空投里,这条规则被系统性地利用:脚本批量领取、卖出、关户,把项目方付出的押金一笔笔收走。领的人越多,项目方流出的 SOL 越多。
教训是那句已经说过的话:你付出去的押金,最终会落到谁手里。 设计发放机制时,这一项必须算进成本模型。
改一个变量
总量随时可以变。任何基于「固定总量」的估值、分配、锁仓承诺,都只是一句话而不是一个保证。
这不一定是坏事——很多协议需要持续增发来做激励。但它必须被明确披露,而不是让用户自己去读账户才发现。
研究一个代币时,这一条排在所有链上指标之前。
发行方可以让任何人的余额动不了,包括被存进 DeFi 协议的那部分。
对集成方来说,这意味着一个新的风险维度:你的清算路径可能在最需要它的时候被冻结掉。 这和 T9 里「代币可被暂停」是同一类风险,只是在这里它是一个字段,不是一段代码。
池子收到的比记录的少,每一笔交易都在往外漏一点点。
和 T9 的解法完全一样:用余额差记账,而不是用声明的金额。 换了链、换了模型,防御手段是同一个——因为问题的本质从来都是「声明的金额不等于实际到账」。
账户消失,押金退回。下次有人给他转账,会再次遇到「对方没有账户」。
如果你的系统缓存了「这个用户的代币账户地址」并假设它一直存在,就会出错。正确做法是每次转出前检查账户是否存在,或者直接用幂等创建。
账户可以被创建,也可以被销毁。 这是 EVM 的 mapping 心智模型里完全没有的一个状态。
带走的问题
为什么需要 Token?这一章给出了一个结构性的答案:在这个模型里,Token 甚至不需要自己的代码,它只是一份被共享程序解释的数据。这提示了一件事——很多你以为必须写成合约的东西,其实只需要一份被公认的数据格式。
谁在支付?这一章的答案非常具体:每一个代币账户的免租押金,都有一个明确的出资人和一个明确的受益人,而这两者可以不是同一个人。 任何发放、空投、激励机制的成本模型里,这一项都要单独列出来。
谁承担风险?增发权限、冻结权限、转账钩子——三样都是发行方持有、持有人承担后果的东西。好消息是它们在这个模型里全部是可以被一次查询读出来的数据。坏消息是绝大多数人从来没查过。
本章自测
因为转账、增发、授权、冻结这些逻辑,全部写在一份被所有代币共享的程序里。
发一个新代币只是创建一个记录发行信息的账户,程序会按同一套规则解释它。你没有写代码,所以也没有地方可以写自定义逻辑。
好处是所有代币行为一致,钱包只需适配一次;代价是刚性,任何新能力都必须由那份共享程序统一提供。
因为余额不是记在代币合约的一张表里,而是一个独立的账户。账户占用链上存储,存储要有人出押金,否则会被回收。
押金归账户所有者,关闭账户时可以退回。所以它不是手续费,是一笔被锁住的保证金。
实践上的处理方式是把「不存在则创建」和转账放进同一笔交易,并想清楚这笔押金由谁出。
没有本质区别,ATA 只是一个地址约定:由持有人地址和代币地址派生出来的那一个。
它的价值在于可预测:任何人不查询就能算出「某某人的某某代币账户在哪」。钱包、协议、脚本都默认用它。
一个人可以为同一个代币创建多个账户,但只有那一个是 ATA,其余的需要自己记住地址。
因为「行为完全统一」和「行为可以定制」是一对矛盾,任何系统都必须在中间选一个位置。
共享程序选了极端的统一,代价是所有人都不能加功能。扩展是在统一的基础上开一批受控的、必须显式声明的口子。
关键差别是:T9 里那些行为藏在源码中,必须读代码才能发现;这里它们是账户上的字段,可以被程序化地检查。从「难以发现」变成「容易发现」是真实的进步,但发现之后要不要支持,还是集成方自己的决定。
没有标准答案,参考这几条:
mintAuthority是不是空?不是的话在谁手里,是不是多签?freezeAuthority是不是空?发行方能不能冻结你的池子?- 它用的是旧版还是新版代币程序?开了哪些扩展?
- 有没有转账费扩展?有的话,你的记账是按声明金额还是按余额差?
- 有没有转账钩子?有的话,那个被调用的程序是谁写的、能做什么?
- 精度是多少?你的计算有没有把精度硬编码?
- 元数据由谁提供,能不能被改?
- 前几大持有人占比多少?有没有一个账户拿着绝大部分?
对照 T9 最后那份 ERC20 清单看一遍,你会发现两份清单问的是同一组问题。换链不换问题,这是这一阶段最值得带走的一条。
一句话带走
Token 是一份被所有人共享的程序,加上一堆属于各自持有人的账户。