Skip to main content

Token2022 安全开发指南

随着 Solana 的发展,对更复杂、适应性更强的代币功能的需求也在增长。虽然 Solana 上当前的代币计划通过一组简单的功能满足了可替代和不可替代代币的基本需求,但其简单性带来了阻碍创新的限制。为了应对这种不断变化的情况,Solana 推出了 Token2022——一套附加功能和增强功能,旨在拓宽生态系统中代币的功能。

Token2022中包含有将近20个新的extension,为了支持这些新的extension,solana 的开发人员需要认真阅读solana官方的开发文档和样例代码。但是现有的开发文档和开发样例都十分匮乏,开发和适配Token2022的过程中需要关注和留心哪些安全问题,这些都还没有太多公开的文档来介绍。

我们希望通过这篇文章,为 solana Token2022 的安全开发shed the light,让solana project开发者能在项目的开发过程中就考虑到这些问题,尽量规避一些潜在的安全风险,最大程度地保证自己的项目能够安全地支持Token2022 extensions。

Extensions

1. Transfer Fees

Token2022的Mint中开启了Transfer Fee后,在每次transfer时,都会将这次转账的fee amount记录在receiver的一个独立的Extension Account中,这部分fee是收款人无法取出的,也不会作为余额体现在收款人的Token Account中。

当mint的 transfer_fee_basis_points 大于0时,在这类mint上转账完成后,receiver实际收到的mint amount是少于transfer IX参数中指定的amount,此时如果在调用transfer的合约中还需要进行额外的记账,开发者需要考虑如何处理transfer fee的这部分记账。如果将这部分fee也算成了receiver实际收到的mint,可能会在不同的场景下,导致 协议/合约/用户 的账户中发生额外的损失。

一个简单的错误案例

先来看一个简单的例子,在这个例子中,由于协议没有考虑Transfer过程中产生的fee,从而导致自己受到了损失:

vault 支持 deposit / withdraw 一个fee rate为20%的Token2022 mint,vault的Token Account中现在有1000的余额。

  • User Alice 通过Deposit(100)存入100
    • vault中通过调用Token2022 Transfer 从 Alice 的Token Account中转入Vault 100
      • User Alice 余额减去100
      • 扣除Transfer fee后,vault实际收到的只有80,此时vault的Token Account余额为1080
    • vault 记账 Alice 存入100
  • User Alice 通过Withdraw(100)取出100
    • vault中确认Alice 存入的余额是大于等于100,记账 Alice 取出100
    • vault中通过调用Token2022 Transfer 从 Vault 的Token Account中转给Alice 100
      • 扣除 Transfer fee后,Alice 实际收到80,Alice的Token Accout 余额增加 80
      • Vault 的Token Account 余额扣除100,此时余额为980。

可以看到在 Alice的 deposit / withdraw 这一组操作之后,Vault 凭空损失了一部分 1000 - 980 = 20。这里由于Vault在记账时没有考虑发生在Transfer过程中的Token2022 Transfer Fee,导致自身损失了20。

一种推荐的fee算法

合约中如果要支持带Transfer Fee的Token2022 Mint,需要谨慎地考虑transfer 过程中fee的扣除对合约内部状态的影响。此时可能会涉及到计算pre fee amount和post fee amount,可以从SPL的实现中找到Transfer Fee的计算代码,这里截取了核心的计算部分 ( 其中满足 0<transferfeebasispoints<100000 < transfer_fee_basis_points < 10000):

    fn ceil_div(numerator: u128, denominator: u128) -> Option<u128> {
numerator
.checked_add(denominator)?
.checked_sub(1)?
.checked_div(denominator)
}

pub fn calculate_fee(&self, pre_fee_amount: u64) -> Option<u64> {
...
let numerator = (pre_fee_amount as u128).checked_mul(transfer_fee_basis_points)?;
let raw_fee = Self::ceil_div(numerator, ONE_IN_BASIS_POINTS)?
.try_into() // guaranteed to be okay
.ok()?;
...
}

pub fn calculate_pre_fee_amount(&self, post_fee_amount: u64) -> Option<u64> {
...
let numerator = (post_fee_amount as u128).checked_mul(ONE_IN_BASIS_POINTS)?;
let denominator = ONE_IN_BASIS_POINTS.checked_sub(transfer_fee_basis_points)?;
let raw_pre_fee_amount = Self::ceil_div(numerator, denominator)?;

if raw_pre_fee_amount.checked_sub(post_fee_amount as u128)? >= maximum_fee as u128 {
post_fee_amount.checked_add(maximum_fee)
} else {
// should return `None` if `pre_fee_amount` overflows
u64::try_from(raw_pre_fee_amount).ok()
}
...
}
post\_fee\_amount = pre\_fee\_amount - \lfloor \frac{pre\_fee\_amount * transfer\_fee\_basis\_points  + ONE\_IN\_BASIS\_POINTS - 1}{ONE\_IN\_BASIS\_POINTS} \rfloor 


raw\_pre\_fee\_amount = \lfloor\frac{post\_fee\_amount * ONE\_IN\_BASIS\_POINTS + ONE\_IN\_BASIS\_POINTS - transfer\_fee\_basis\_points - 1}{ONE\_IN\_BASIS\_POINTS - transfer\_fee\_basis\_points}\rfloor $


但是这里存在一个小问题:由于整数运算过程中的精度损失问题,这里没法保证下列等式始终成立: x=calculate_pre_fee_amount(xcalculate_fee(x))x = calculate\_pre\_fee\_amount(x - calculate\_fee(x))

此时站在合约开发者的视角来看,需要尽量避免自身合约中的reserve资金因为fee计算过程精度的问题而蒙受损失,因此通常需要由用户方来承担由于精度问题产生的surplus。

这里一种推荐的做法是在 calculate_pre_fee_amount 中调整计算raw_pre_fee_amount的算法,将计算的公式改成:

        let raw_pre_fee_amount = numerator
.checked_add(ONE_IN_BASIS_POINTS)?
.checked_sub(1)?
.checked_div(denominator)?;

这样在数学上可以严格保证 xcalculate_pre_fee_amount(xcalculate_fee(x))x \le calculate\_pre\_fee\_amount(x - calculate\_fee(x)) 详细证明见附录1。

计算 pre fee amount时,如果原始的input存在一个max最大值限制,此时计算出来的 pre fee amount还需要与 max input一起做一个min操作:min(pre_fee_amount, max_input)。这样才能保证不会因为反向的计算而导致非预期的资金损失。

符合预期的transfer fee

对于合约 / 用户,如果希望在转账的过程中收取的fee是符合预期的,可以使用Token2022中提供的transfer_checked_with_fee IX,在这个IX中,只有传入了计算正确的费用,转账才会成功,这样就能避免转账过程中出现任何意外。

Transfer fee 生效时间

Token2022中还增加一个延迟更新Fee配置的安全保证,修改后的新的Fee配置不会立刻生效,而是在配置完 2 个 epoch (≈4 days)后才会生效,在此之前仍然会按照 旧的 Fee配置来收取fee。这种延迟更新是对用户的安全保证,以避免频繁地修改fee而导致的用户资金损失。

如果开发过程中涉及到更新Fee 配置,开发者需要意识到新配置生效前,还存在2个epoch的过渡期,仍是用的旧的Fee配置。

可以使用 getTransferFeeConfiggetEpochFee 来获取mint在当前epoch的transferFee。

maximum_fee必须设置

maximum_fee 是每次transfer 收取fee的最大值,如果没有设置,则为默认值0。这样Transfer时等价于没有收Fee

开发过程中需要需要设置或更新 Fee Config,一定记得确认maximum_fee是否设置。

TransferFeeConfig.withheld_amount 不是最新的数据

TransferFeeConfig.withheld_amount 是 Mint 中的数据,这个值和实际已经发生的 transfer fee是不同步的,一般TransferFeeConfig.withheld_amount会小于实际已经收取的所有Transfer fee。

如果要更新数据,需要invoke HarvestWithheldTokensToMint IX从一组TokenAccount的 TransferFeeCAmount.withheld_amount 来手动同步。

注意这个同步不是必须的。

2. Metadata Pointer / Group Pointer / Group Member Pointer

如果合约 / DApp中需要用到 Metadata / Group / Group Member的数据,为了确认数据的真实性,需要确保Mint 和 Pointer之间的互指关系。

Pointer 能够保证数据的真实有效性。

对于一个Mint A,任何人都可以create Metadata / Group/ Group Member account,在这些account中填入任意构造的虚假数据,并将Mint指向一个合法的Mint A。但是只有Mint A的Pointer指向的 Metadata / Group/ Group Member account,才是Mint A认证过的权威数据。

下面以Metadata和Metadata Pointer的指向关系图为例,展示一个符合预期的Metadata 与 Metadata Pointer关系。 这里有1个Mint A和Mint A的Metadata M,他们之间需要满足

  • Mint.Metadata_Pointer.Metadata_Address == M
  • Metadata.Mint == A

为了方便代币元数据的使用,Token-2022 允许铸币创建者将其代币的元数据直接包含在铸币账户中。此时这个关系图可以简化成这样:

3. Transfer Hook

如果Mint 开启了Transfer Hook,则每次在该mint上发生的transfer都会触发一个CPI调用到mint关联的Transfer Hook program上的invoke。

invoke前可能需要提前准备这个hook program 所需要的account,对于复杂的hook程序,这些accounts可能不能满足需要,因此使用该方法 spl_tlv_account_resolution::state::ExtraAccountMetaList::add_to_cpi_instruction,将 account_info_iter 中剩下的 accounts 也按需添加到 CPI 中。

注意

这里开发者能做的是什么??

  • Transfer Hook的调用是在Transfer 结束之前调用的,此时已经完成了Transfer的所有操作(包括Transfer Fee计算)。如果依赖Transfer开始之前的状态,则开发者需要自行注意这一点。
  • Transfer Hook如果依赖Extra Account Meta,需要确保Meta生成的正确性:对生成Meta的account,包括remaining_account,需要做相应的检查,避免因为顺序导致生成了错误的Account Meta。

[TODO]

4. Interest-Bearing Tokens

开发者可以使用 AmountToUiAmount 和 UiAmountToAmount 这两个IX来进行 raw amount 与 ui amount之间的转换。

这里需要注意的是:

  • 这个extension是基于timestamp 使用一个固定的公式进行利息计算的,如果开发者在这个mint上预期的利息计算公式与extension中所使用的不一致,或者利息的计算不是以timestamp来作为基本单位(比如基于slot),都不应该使用这个extension。
  • 由于solana network的不稳定(比如阶段性的网络拥塞),可能会导致实际执行时的timestamp出现偏差,累计的利息可能与实际预期的值不一致。

在确认项目的需求与这个extension相符之后,再谨慎使用这个extension,不要过度依赖IX给出的转化结果。

5. Permanent Delegate

被设置为permanent delegate的 authority拥有非常高的权限,可以直接对任意token account进行transfer或burn任意数量的 mint。

以SPL Token2022的process_transfer为例,这里在验证transfer authority时,如果authority是permanent delegate,即可通过认证。

    pub fn process_transfer(
...
) -> ProgramResult {
...
match (source_account.base.delegate, maybe_permanent_delegate) {
(_, Some(ref delegate)) if cmp_pubkeys(authority_info.key, delegate) => {
Self::validate_owner(
program_id,
delegate,
authority_info,
authority_info_data_len,
account_info_iter.as_slice(),
)?
}
...
}

注意

无论是作为开发者还是用户,对于带有Permanent Delegate extension的mint,都需要额外关注和控制好delegate的行为,以避免非预期的财产损失。

6. Memo Transfer

用户可以选择为自己的Token Account开启Memo,以便于在收到一笔transfer的同时,能够看到附带的memo。Token2022 program在处理开启了Memo 的Token Account时,强制要求在transfer IX前需要附带一条Memo IX。

这是 SPL Transfer与Memo相关的代码片段,可以看到,当Transfer之前的IX不是memo时,会直接导致Transfer返回错误。

    pub fn process_transfer(
...
) -> ProgramResult {
...
if memo_required(&destination_account) {
check_previous_sibling_instruction_is_memo()?;
}
...
}

作为protocol 开发方,如果项目合约中支持Token2022 的Transfer,就需要考虑是否需要为开启的Memo Transfer的Token Account做单独的支持(在Transfer前加一条Memo IX),否则在Memo enabled Token Account作为Transfer的Destination Account时,Token2022的Transfer无法完成,这可能会影响到整个协议或合约的用户体验。

\

Usage

在Token2022的开发适配过程中,除了在上文中提到extension安全提醒和事项,还有一些更上层的开发细节需要注意。这里我们尝试从以下几个角度来分别进行讨论。

1. Token Account 生命周期

Token2022 中的Token Account与 SPL Token中的Token Account有一些区别。这些区别主要体现在新的Token Account中会带上更多的Token2022 extension相关的数据。

1.1. Create Token Account

Token2022中的Token Account 占用的space可能会比SPL Token中的Token Account更大,并且这个大小是会随着其中包含的extension种类和数量而动态变化的。

因此,在开发过程中,如果需要动态为User 创建一个Token2022的Token Account,不能对Token Account硬编码一个固定的space大小,或者固定space大小需要的最小rent exempt amount。

Vulnerable Case

下面这个函数会尝试Create一个支持 Token2022 的 Token Account。


export const getCostToOpenAta = async (mint, price) => {
const cost = 0.00203928 * price.data.SOL.price * 10 ** mint.decimals;
return cost;
};

create Token Account时使用了硬编码的SOL amount 0.00203928,这个值正好对应SPL Token Account 占用的165 Bytes 所需的rent。 这里的问题就是,如果需要这个Token Account支持Token2022 extension,这个数量是不够cover加上了Token2022 account extension space之后的rent值,那么之后的create with Token2022 extension的 token account也就会失败。

如果这个space大小可以由用户任意指定,并且由keeper来负责Create Token Account,那么这里可能存在Keeper会多付rent的情况,导致Keeper发生资金损失。

推荐项目开发中尽量避免让keeper来为用户建立Token Account。

Recommendation

@solana/spl-token 中提供了api接口帮助计算带了extension之后的account length和最小rent。

  • getExtensionTypes:获取mint支持的所有extension
  • getMinimumBalanceForRentExemptAccountWithExtensions:获取带上extension后 token account所需要的最小rent。

下面是个针对上述问题的改进代码

 export const getCostToOpenAta = async (mint, price) => {
const extensions = getExtensionTypes(mint.tlvData);
const rent = await getMinimumBalanceForRentExemptAccountWithExtensions(
connection,
[...extensions]
);
const cost = rent * price.data.SOL.price * 10 ** mint.decimals;
return cost;
}

1.2. Reallocate

有的Account Extension可以在Token Account Create之后再添加上来的,比如Memo Transfer。因此Token Account的大小可能是会动态发生变化。

为了支持Token Account创建之后还能添加新的Account Extension,Token2022 增加了一个Reallocate IX来为一个已存在Token Account增加大小,以容纳更多的extension bytes。

在Reallocate IX中,如果发现当前的Token Account大小已经满足需求,则会直接返回,不会做进一步的realloc操作。

注意

当需要reallocate一个更大的Token Account时,是会产生额外的rent,这部分rent 该由哪方来承担,是需要在开发的过程中考虑进来的,如果忽略了这一点,可能会造成协议/项目方自己承担这部分不在预期范围内的额外开销。

1.3. Close Token Account

一个Token Account是可以被close的。 在SPL Token中,对于一个非WSOL的Token Account,只要满足 account.amount == 0 ,即可由 account.authority 将这个Token Account close掉。

但是在Token2022中,close 一个 token account需要满足更多的条件:

  • account.amount == 0
  • 如果Token Account支持CPI Guard extension,还需要确认lamport destination必须是 Token Account的owner
  • 如果Token Account支持 Confidential Transfer extension,需要满足 pending_balance == 0 && available_balance == 0
  • 如果Token Account支持 Confidential Transfer Fee extension,需要满足 withheld_amount == 0
  • 如果Token Account支持 Transfer Fee extension,需要满足 withheld_amount == 0

注意

如果合约中需要动态判断Token2022 Token Account是否满足close条件,至少需要满足上述列出来的所有条件才可以执行close,否则Token Account无法在合约中直接close。

2. Mint Account 生命周期

Create Mint

Token2022中,所有的extension都要在 initialize_mint IX 创建新的 Mint account 之前初始化好。

这些extension包括:

  • confidential transfer
  • confidential transfer fee
  • default account state
  • group member pointer
  • group pointer
  • metadata pointer
  • interest-bearing mint
  • Transfer fee
  • Transfer hook
  • Immutable Owner
  • Mint Close Authority
  • Non Transferable
  • Permanent Delegate

并且这些extension之间还存在一些绑定的约束关系,会在initialize_mint中一起检查

  • 如果开启了confidential transfer fee,则一定要开启transfer fee和confidential transfer
  • 如果开启了transfer fee和confidential transfer,则一定要开启confidential transfer fee

注意

Mint account 不像 Token Account,所有支持的extension必须在initialize之前初始化好。当Mint account已经created后,无法再为其添加新的extension。

Close Mint

在SPL Token program中,是无法关闭Mint account的。

但是,在Token2022中,可以通过 MintCloseAuthority extension来关闭一个Mint。

注意

MintCloseAuthority 要求 Mint.supply == 0

如果一个Mint是处于一个可以被关闭的状态,通常不会造成任何损害,因为此时所有token account都是空的。

但是,若项目中存储了有关mint的任何信息,那么当mint关闭并在同一个address重新创建时,可能会出现不一致。比如该account可能用于完全不同的目的。

如果项目中存储了mint信息,请找到一种方法来重新设计解决方案,使其始终直接使用来自mint的信息。

3. SPL Token vs Token2022

transfer VS transfer_checked

Token2022中,transfer是一个已经deprecated的function

/// Creates a `Transfer` instruction.
#[deprecated(
since = "4.0.0",
note = "please use `transfer_checked` or `transfer_checked_with_fee` instead"
)]
pub fn transfer(
token_program_id: &Pubkey,
source_pubkey: &Pubkey,
destination_pubkey: &Pubkey,
authority_pubkey: &Pubkey,
signer_pubkeys: &[&Pubkey],
amount: u64,
) -> Result<Instruction, ProgramError> {

代码注释中给了非常明确的标注,建议使用 transfer_checkedtransfer_checked_with_fee

先看看推荐的这两个function的定义:

/// Creates a `TransferChecked` instruction.
#[allow(clippy::too_many_arguments)]
pub fn transfer_checked(
token_program_id: &Pubkey,
source_pubkey: &Pubkey,
mint_pubkey: &Pubkey,
destination_pubkey: &Pubkey,
authority_pubkey: &Pubkey,
signer_pubkeys: &[&Pubkey],
amount: u64,
decimals: u8,
) -> Result<Instruction, ProgramError> {
...
}

/// Create a `TransferCheckedWithFee` instruction
#[allow(clippy::too_many_arguments)]
pub fn transfer_checked_with_fee(
token_program_id: &Pubkey,
source: &Pubkey,
mint: &Pubkey,
destination: &Pubkey,
authority: &Pubkey,
signers: &[&Pubkey],
amount: u64,
decimals: u8,
fee: u64,
) -> Result<Instruction, ProgramError> {
...
}

可以看到

  • `transfer_checked` 比 `transfer`多了2个参数:`mint_pubkey` 和 `decimals`
  • `transfer_checked_with_fee` 比 `transfer`多了3个参数:`mint` , `decimals` 和 fee

通过SPL transfer IX 的实现,可以发现,如果坚持继续使用 transfer ,可能会在 transfer 的过程中因为Token Account支持 Transfer HookTransfer Fees 这两个extension而导致Transfer无法完成。

    /// Processes a [Transfer](enum.TokenInstruction.html) instruction.
pub fn process_transfer(
program_id: &Pubkey,
accounts: &[AccountInfo],
amount: u64,
expected_decimals: Option<u8>,
expected_fee: Option<u64>,
) -> ProgramResult {
...
let expected_mint_info = if let Some(expected_decimals) = expected_decimals {
Some((next_account_info(account_info_iter)?, expected_decimals))
} else {
None
};
...
let (fee, maybe_permanent_delegate, maybe_transfer_hook_program_id) =
if let Some((mint_info, expected_decimals)) = expected_mint_info {
...
} else {
// Transfer hook extension exists on the account, but no mint
// was provided to figure out required accounts, abort
if source_account
.get_extension::<TransferHookAccount>()
.is_ok()
{
return Err(TokenError::MintRequiredForTransfer.into());
}

// Transfer fee amount extension exists on the account, but no mint
// was provided to calculate the fee, abort
if source_account
.get_extension_mut::<TransferFeeAmount>()
.is_ok()
{
return Err(TokenError::MintRequiredForTransfer.into());
} else {
(0, None, None)
}
};
...

从代码中可以看到,当调用transfer function时,传入的 expected_decimals 为 None,此时会走入上面代码片段中的else branch。此时如果Token Account 中支持 Transfer HookTransfer Fees,此时就会直接在transfer中返回 MintRequiredForTransfer Error 而退出。

因此,推荐在 Token2022 中,不要再使用transfer,而是使用 transfer_checkedtransfer_checked_with_fee

\

2个版本的wsol

SPL Token中,WSOL的account id为 So11111111111111111111111111111111111111112 Token2022 中,重新定义了一个新的WSOL,account id为 9pan9bMn5HatX4EJdBwg9VgCa7Uz5HL8N1m5D3NdXejP

如果合约中涉及到需要特殊处理WSOL相关的case,此时则需要注意,是否需要分别处理SPL Token和Token2022中的WSOL。

截止到2024年8月,Token2022中的WSOL目前还没有什么交易量,通常WSOL还是指 SPL Token中的 So11111111111111111111111111111111111111112

Program ID

当前很多实现中,在涉及到Token Program ID时,都是硬编码了SPL Token Program。但是Token2022的Program ID与SPL不同,因此在这些地方需要注意,避免硬编码。

  • SPL Token Program ID: TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA
  • Token2022 Program ID: TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb

solana/@spl_token 中的 createAssociatedTokenAccountInstruction 为例。

createAssociatedTokenAccountInstruction(payer, associatedToken, owner, mint, programId?, associatedTokenProgramId?): TransactionInstruction

对于其中的参数programId ,文档如下:

  • programId: PublicKey = TOKEN_PROGRAM_ID

    SPL Token program account

可以看到,这两个参数如果使用默认值,就直接使用了 TOKEN_PROGRAM_ID。

SPL 兼容接口

  • GetAccountDataSize
  • InitializeImmutableOwner

[TODO]

4. Mint Extension VS Token Account Extension

Token2022 Extension在实现时,会将Extension的数据以TLV sequence的格式拼接在Account data最后。

这里谈论的Account其实可以分为 Mint Account和Token Account这两类。

Token2022的一些Extension只在Mint Account后添加数据,一些Extension只在Token Account添加数据,还有些Extension会同时在Mint Account 和Token Account后同时拼接数据。

这里分别将这些Extension以及相应的类型列举出来。

ExtensionExtension Data TypeAccount Type
Immutable OwnerImmutableOwnerToken Account
CPI GuardCpiGuardToken Account
Required Memo on TransferMemoTransferToken Account
Non-Transferable TokensNonTransferableAccountToken Account
Non-Transferable TokensNonTransferableMint
Transfer FeesTransferFeeAmountToken Account
Transfer FeesTransferFeeConfigMint
Transfer HookTransferHookAccountToken Account
Transfer HookTransferHookMint
Confidential TransferConfidentialTransferAccountToken Account
Confidential TransferConfidentialTransferMintMint
Confidential Transfer FeeConfidentialTransferFeeAmountToken Account
Confidential Transfer FeeConfidentialTransferFeeConfigMint
Mint Close AuthorityMintCloseAuthorityMint
Default Account StateDefaultAccountStateMint
Interest-Bearing TokensInterestBearingConfigMint
Permanent DelegatePermanentDelegateMint
Metadata PointerMetadataPointerMint
MetadataTokenMetadataMint
Group PointerGroupPointerMint
GroupTokenGroupMint
Group Member PointerGroupMemberPointerMint
Group MemberTokenGroupMemberMint

5. Anchor Token2022

Anchor是现在主流的Solana合约开发框架,其中也添加了对Token2022的支持,包括各种类型以及相关IX的封装,开发者可以直接使用anchor封装的这些类型和方法。

类型

这里总结了各种类型的定义,以及支持的Token Program类型。

Anchor 类型use路径Token Program
TokenAccountanchor_spl::token_interface::TokenAccountSPL_TOKEN, SPL_TOKEN_2022
anchor_spl::token::TokenAccountSPL_TOKEN
Mintanchor_spl::token_interface::MintSPL_TOKEN, SPL_TOKEN_2022
anchor_spl::token::MintSPL_TOKEN
TokenInterfaceanchor_spl::token_interface::TokenInterfaceSPL_TOKEN, SPL_TOKEN_2022
Token2022anchor_spl::token_2022::Token2022SPL_TOKEN_2022
Tokenanchor_spl::token::TokenSPL_TOKEN

可以看到:

  • 路径token_interface下的类型,Token Program和Token2022 Program都支持
  • 路径token下的类型,只支持Token Program
  • 路径token_2022下的类型,只支持Token2022 Program

开发的过程中需要明确合约是否需要支持Token2022

  • 支持Token2022,建议使用路径 anchor_spl::token_interface 下的Types
  • 不支持 Token2022,建议使用路径 anchor_spl::token::Token 下的Types

如果不打算支持Token2022,但是却在合约中使用了路径 anchor_spl::token_interface 下的Type,可能会因为这里的二义性而引入一些非预期的问题。

Methods

Anchor中同样封装了 Token和Token2022 的method,底层分别调用了spl_token和spl_token_2022相关的SPL method。

Methoduse 路径Token Program
transferanchor_spl::token::transferSPL_TOKEN
[deprecated] anchor_spl::token_2022::transferSPL_TOKEN_2022
transfer_checkedanchor_spl::token::transfer_checkedSPL_TOKEN
anchor_spl::token_2022::transfer_checkedSPL_TOKEN_2022
mint_toanchor_spl::token::mint_toSPL_TOKEN
anchor_spl::token_2022::mint_toSPL_TOKEN_2022
burnanchor_spl::token::burnSPL_TOKEN
anchor_spl::token_2022::burnSPL_TOKEN_2022
approveanchor_spl::token::approveSPL_TOKEN
anchor_spl::token_2022::approveSPL_TOKEN_2022
revokeanchor_spl::token::revokeSPL_TOKEN
anchor_spl::token_2022::revokeSPL_TOKEN_2022
initialize_accountanchor_spl::token::initialize_accountSPL_TOKEN
anchor_spl::token_2022::initialize_accountSPL_TOKEN_2022
initialize_account3anchor_spl::token::initialize_account3SPL_TOKEN
anchor_spl::token_2022::initialize_account3SPL_TOKEN_2022
close_accountanchor_spl::token::close_accountSPL_TOKEN
anchor_spl::token_2022::close_accountSPL_TOKEN_2022
freeze_accountanchor_spl::token::freeze_accountSPL_TOKEN
anchor_spl::token_2022::freeze_accountSPL_TOKEN_2022
thaw_accountanchor_spl::token::thaw_accountSPL_TOKEN
anchor_spl::token_2022::thaw_accountSPL_TOKEN_2022
initialize_mintanchor_spl::token::initialize_mintSPL_TOKEN
anchor_spl::token_2022::initialize_mintSPL_TOKEN_2022
initialize_mint2anchor_spl::token::initialize_mint2SPL_TOKEN
anchor_spl::token_2022::initialize_mint2SPL_TOKEN_2022
set_authorityanchor_spl::token::set_authoritySPL_TOKEN
anchor_spl::token_2022::set_authoritySPL_TOKEN_2022
sync_nativeanchor_spl::token::sync_nativeSPL_TOKEN
anchor_spl::token_2022::sync_nativeSPL_TOKEN_2022
approve_checkedanchor_spl::token::approve_checkedSPL_TOKEN
get_account_data_sizeanchor_spl::token_2022::get_account_data_sizeSPL_TOKEN_2022
initialize_mint_close_authorityanchor_spl::token_2022::initialize_mint_close_authoritySPL_TOKEN_2022
initialize_immutable_owneranchor_spl::token_2022::initialize_immutable_ownerSPL_TOKEN_2022
amount_to_ui_amountanchor_spl::token_2022::amount_to_ui_amountSPL_TOKEN_2022
ui_amount_to_amountanchor_spl::token_2022::ui_amount_to_amountSPL_TOKEN_2022
cpi_guard_enableanchor_spl::token_2022_extensions::cpi_guard
cpi_guard_disableanchor_spl::token_2022_extensions::cpi_guard
default_account_state_updateanchor_spl::token_2022_extensions::default_account_state
default_account_state_initializeanchor_spl::token_2022_extensions::default_account_state
group_member_pointer_initializeanchor_spl::token_2022_extensions::group_member_pointer
group_member_pointer_updateanchor_spl::token_2022_extensions::group_member_pointer
group_pointer_initializeanchor_spl::token_2022_extensions::group_pointer
group_pointer_updateanchor_spl::token_2022_extensions::group_pointer
immutable_owner_initializeanchor_spl::token_2022_extensions::immutable_owner
interest_bearing_mint_initializeanchor_spl::token_2022_extensions::interest_bearing_mint
interest_bearing_mint_update_rateanchor_spl::token_2022_extensions::interest_bearing_mint
memo_transfer_initializeanchor_spl::token_2022_extensions::memo_transfer
memo_transfer_disableanchor_spl::token_2022_extensions::memo_transfer
metadata_pointer_initializeanchor_spl::token_2022_extensions::metadata_pointer
mint_close_authority_initializeanchor_spl::token_2022_extensions::mint_close_authority
non_transferable_mint_initializeanchor_spl::token_2022_extensions::non_transferable
permanent_delegate_initializeanchor_spl::token_2022_extensions::permanent_delegate
token_group_initializeanchor_spl::token_2022_extensions::token_group
token_member_initializeanchor_spl::token_2022_extensions::token_group
token_metadata_initializeanchor_spl::token_2022_extensions::token_metadata
token_metadata_update_authorityanchor_spl::token_2022_extensions::token_metadata
token_metadata_update_fieldanchor_spl::token_2022_extensions::token_metadata
transfer_fee_initializeanchor_spl::token_2022_extensions::transfer_fee
transfer_fee_setanchor_spl::token_2022_extensions::transfer_fee
transfer_checked_with_feeanchor_spl::token_2022_extensions::transfer_fee
harvest_withheld_tokens_to_mintanchor_spl::token_2022_extensions::transfer_fee
withdraw_withheld_tokens_from_mintanchor_spl::token_2022_extensions::transfer_fee
transfer_hook_initializeanchor_spl::token_2022_extensions::transfer_hook
transfer_hook_updateanchor_spl::token_2022_extensions::transfer_hook

可以看到,SPL Token 和Token 2022有很多同名函数,开发的过程中需要明确是否支持Token2022

  • 支持Token2022,建议使用路径 anchor_spl::token 下的 Methods
  • 不支持 Token2022,建议使用路径 anchor_spl::token_2022 下的 Methods

\

零碎问题

想到了再补上

[TODO]

  • reject native mint of Token-2022 Program to avoid SOL liquidity fragmentation

Migration

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

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

附录

1. Transfer Fee 数学证明

设x为transfer的原始amount,r为 transfer fee rate,x,rN,0<r<10000x, r \in N, 0 < r < 10000 ,现在证明下面等式成立: xcalculate_pre_fee_amount(xcalculate_fee(x))x \le calculate\_pre\_fee\_amount(x - calculate\_fee(x))

Proof:

 $$ \because x - calculate\_fee(x) = x - \lfloor \frac{x * r + 10000 -1}{10000} \rfloor $$

$$ \therefore calculate\_pre\_fee\_amount(x - calculate\_fee(x)) $$

$$ = \lfloor \frac{10000 * x - 10000 * \lfloor \frac{x * r + 10000 -1}{10000} \rfloor + 10000 -1}{10000 - r} \rfloor $$

$$ \ge \lfloor \frac{10000 * x - 10000 * \frac{x * r + 10000 -1}{10000} + 10000 -1}{10000 - r} \rfloor $$

$$ = \lfloor \frac{10000 * x - x * r}{10000 - r} \rfloor $$

$$ = \lfloor x * \frac{10000 - r}{10000 - r} \rfloor$$

$$ = x $$