Token-2022 Security Best Practices - Part 2: Extensions
Recap
Welcome back to Token-2022 Security Best Practices. In Part 1, we addressed the potential security issues associated with Mint and Token Accounts when supporting Token-2022. In this part, we will explore the extensions of Token-2022, analyze potential security risks, and suggest recommended solutions. Please pay close attention to each topic, especially the Attention section.
How to hedge the security risks of Token-2022 Extensions
Token-2022 expands the functionality of mint and token accounts compatible with SPL Token through an extension model. It introduces nearly 20 Extensions. If developers lack sufficient understanding of the assumptions and characteristics of these extensions, serious security issues, such as DoS or financial losses, may arise.
In this chapter, we will explore five Token-2022 extensions closely related to security, summarize their security assumptions, and outline potential security issues associated with these extensions.
1. Metadata, Group & Member
Token-2022 includes three metadata extensions to describe mint, along with three corresponding pointer extensions that pair with each of these metadata extensions.
Pointer extensions, which ensure that data accessed through the pointer is valid and trusted, are stored within the extension part of mint account. Data, however, are more flexible—they can reside either within the extension part of mint account or as a separate account.
If a contract or DApp requires the data from Metadata, Group, or Member, it is essential for the project team to verify the authenticity of the data, which depends on the mutual reference relationship between the data and the pointer.
Attention
Everyone can create Token-2022 accounts of type Metadata, Group and Group Member, fill these accounts with deliberately crafted data, and point them to a legitimate mint. However, only the metadata accounts that mint’s pointer references are considered authoritative and verified by itself.
The following diagram illustrates the expected relationship between Metadata and Metadata Pointer. There is a Mint A and its corresponding Metadata M here:
A.metadata_pointer.metadata_address == M
M.mint == A
Token-2022 allows mint creators to embed their token’s Metadata, Group and Member data directly into the mint account to simplify the use of data. In this case, the relationship diagram can be simplified as follows:
2. Permanent Delegate
Token-2022 introduces a high-risk extension to the mint account: Permanent Delegate.
The authority designated as a permanent delegate holds high privileges to directly transfer or burn any amount of mint from any token account.
For instance, in the implementation of SPL Token-2022’s process_transfer function, when verifying the transfer authority, the permanent delegate is automatically authorized without further checks.
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(),
)?
}
...
}
This diagram summarizes three types of transfer scenarios:
A standard transfer initiated directly by the authority of the source token account
A transfer initiated by a delegate account, previously approved by the authority of the source token account
A transfer initiated by the permanent delegate account, bypassing the need for a signature from the authority of the source token account
Attention
Both developers and users need to be particularly concerned at mints that include Permanent Delegate. It is important to closely monitor and manage the actions of the permanent delegate to avoid unexpected losses due to their high privileges.
As a developer, it is vital to clearly define and describe the permissions and security policies linked to the permanent delegate role in projects involving mints with this extension. The high-privilege operations of this role must not be misused. Additionally, implementing monitoring for this role is recommended to provide timely alerts and prevent unforeseen actions.
As a user, it is necessary to be aware of whether your asset's mint supports a Permanent Delegate. We strongly recommend you to transfer assets to this mint only after confirming that the delegate authority can be fully trustworthy.
3. Memo Transfer
Memo Transfer is a new Token-2022 extension that allows users to enable memos on their token accounts, enabling them to view memos attached to transfers when receiving tokens.
The Token-2022 program requires that the transfer IX must be
immediately preceded by a Memo IX when processing a Memo-enabled destination token account.
This is the code snippet from the SPL transfer related to memo. If the IX immediately preceding the transfer is not a memo, the SPL transfer will result in an error.
pub fn process_transfer(
...
) -> ProgramResult {
...
if memo_required(&destination_account) {
check_previous_sibling_instruction_is_memo()?;
}
...
}
Attention
As a protocol developer, if your contract supports Token-2022 transfers, you’ll consider whether to handle by ensuring a Memo IX added before each Transfer IX for memo-enabled token accounts or not. Otherwise, if a memo-enabled token account is used as a destination account, the transfer process will fail, potentially disrupting protocol’s functionality and effecting user experience.
4. Interest-Bearing Tokens
Interest-Bearing Tokens is an extension in Token-2022 that pertains to token interests and UI amount. By using this extension in combination with the AmountToUiAmount and UiAmountToAmount instructions, developers can convert between raw amounts and UI amounts.
Attention
Hardcoded Formula
This extension uses a fixed formula to calculate interests based on the timestamp. The extension cannot be applied when the interests calculation formula expected by developers for this mint differs from it or the interests is calculated based on a unit except the timestamp (e.g., based on slots).
For example, when converting the raw amount to the UI amount, the following formula is utilized in the extension:
Developers need to confirm whether this is consistent with the conversion rules expected in your project or not.
Unstable Network
Due to network instability on Solana (such as occasional congestion), the actual timestamp during execution may deviate, resulting in accumulated interests that may not match the expected value.
Since the Bank Timestamp Correction was updated in May 2022, clock drift issues have been significantly improved, except for timestamp jumps caused by temporary block production halts.
In summary, developers should use this extension only after verifying that it aligns with the project’s requirements. The project should not overly rely on the conversion result provided by the IX, as it is simply a UI representation and does not guarantee consistency with the actual amount.
5. Transfer Fees
Transfer Fees is a crucial extension in Token-2022, offering built-in support for handling transfer fees.
When Transfer Fees is enabled for a Token-2022 mint, each transfer operation records its associated fee amount in the extension data of the recipient’s token account. These recorded fees are inaccessible to the recipient and are not included in the available balance of their token account.
If the mint’s transfer_fee_basis_points is greater than 0, the actual amount received by the receiver is less than the amount specified in the transfer IX after the process of transfer.
Attention
If the contract calling transfer_checked IX requires additional accounting, developers must consider how to account for the transfer fee. Counting this fee as part of the amount actually received by the receivers may lead to unexpected losses in various scenarios within protocol, contract, or user accounts.
A Negative Case
Let’s have a look at a simple case where a protocol fails to account for the transfer fee, leading to a loss:
The vault handles deposits and withdrawals for a Token-2022 mint with a 20% fee rate, and it currently has a balance of 1000 in its token account.
Step 1: Alice deposits 100:
The vault calls the transfer_checked to move 100 from Alice’s token account to the vault.
Alice’s balance decreases by 100.
After the transfer fee is applied, the vault actually receives 80, bringing its balance to 1080.
The vault logs Alice’s deposit as 100.
Step 2: Alice withdraws 100:
The vault confirms Alice’s balance is at least 100 and logs her withdrawal of 100.
The vault calls the transfer_checked to send 100 from its token account back to Alice.
After the transfer fee, Alice receives 80, increasing her balance by 80.
The vault’s balance decreases by 100, leaving it with 980.
In this example, after Alice’s deposit and withdrawal, the vault ends up losing 20 units (1000 - 980 = 20). This is because the vault didn’t account for the Token-2022 transfer fee during its bookkeeping, causing an unintended loss.
The Difference Between fee and inverse_fee
In a specific scenario, the program needs to utilize the post amount (after deducting the fee from original transfer amount, i.e., pre amount) to determine the fee required for the actual transfer. The SPL struct spl_token_2022::extension::transfer_fee::TransferFee includes methods like calculate_inverse_fee and calculate_pre_fee_amount to meet this requirement.
It is worth noting that the calculate_fee method in TransferFee is commonly used to compute the transfer fee for the transfer_checked IX. However, calculate_inverse_fee is not a strict inverse operation of calculate_fee. The relationship of these two operations is:
calculate_fee(amount_in) >= calculate_inverse_fee(amount_in - calculate_fee(amount_in))
Here are some specific links for additional reference:
For a detailed implementation of these functions, please check this code link.
For a discussion on the relationship between these two operations, refer to this Github PR.
Here is an example dataset to illustrate the inequality mentioned above.
Let’s start by defining all input parameters that will be utilized throughout the following computation.
// Transfer Amount
amount_in : 5238206430131997821
// Transfer Fee Config
transfer_fee_basis_points: 1000
maximum_fee 5704976840628159154
By inputting this data into the SPL code's calculation method, the following results are obtained:
In this example, the difference between calculate_fee(amount_in)
and calculate_inverse_fee(amount_in - calculate_fee(amount_in)) is 523820643013199783 - 523820643013199782 = 1. This demonstrates that the two values can indeed be unequal.
Note: Although these two methods may yield slightly different fees, the corresponding post-amounts are identical. Which means, in the following pseudocode snippet, despite pre_amount_2 possibly is smaller than pre_amount_1, post_amount_1 and post_amount_2 are always equal:
post_amount_1 = pre_amount_1 - calculate_fee(pre_amount_1)
pre_amount_2 = calculate_pre_fee_amount(post_amount_1)
post_amount_2 = pre_amount_2 - calculate_fee(pre_amount_2)
// Despite pre_amount_2 possibly being smaller than pre_amount_1,
post_amount_1 == post_amount_2
Given this potential discrepancy, developers should avoid interchanging fees derived from these two methods to prevent unexpected issues.
If project has a specific requirement to reverse the inequality mentioned above, i.e.,
calculate_fee(amount_in) <= calculate_inverse_fee(amount_in - calculate_fee(amount_in))
There is one possible solution: In the calculate_pre_fee_amount method of struct spl_token_2022::extension::transfer_fee::TransferFee, adjust the algorithm for calculating raw_pre_fee_amount by modifying the calculation formula as follows:
For the mathematical proof, please check the appendix at the end of this article.
The Expected Transfer Fee
For contracts or users who want to ensure that the fee charged during a transfer aligns with their expectations, they can use the transfer_checked_with_fee IX provided by Token-2022.
This IX ensures that the transfer will only succeed if the correct fees are specified to help to prevent any unexpected issues during the transfer process.
Transfer Fee Effective Time
Token-2022 includes a security feature that delays fee configuration updates.
When a new Fee configuration is set, it doesn’t take effect immediately; instead, it becomes active after 2 epochs (around 4 days). During this period, the previous fee configuration remains in place. This delay serves as a safeguard for users, protecting them from potential financial losses due to frequent fee adjustments.
For developers updating the fee configuration, it is important to keep in mind that the old fee settings will continue to apply during the 2-epoch transition period before the new settings come into effect.
The getTransferFeeConfig IX and getEpochFee IX can be used to check the current transfer fees for a given mint in the current epoch.
The Maximum Fee
The
maximum_fee sets the upper limit for the fee charged per transfer. If it is not set, the default is 0, which effectively means no fees are charged during transfers.
When configuring or updating the fee config during development, it is important to ensure that the maximum_fee is properly set.
The Withheld Amount Synchronization
TransferFeeConfig.withheld_amount does not reflect real-time data. This value is stored in the mint account and is not automatically synchronized with the actual fees collected during transfers. Typically, the withheld_amount is less than the total transfer fees that have been collected.
You should invoke the HarvestWithheldTokensToMint IX, which manually updates the withheld_amount from a group of token accounts to synchronize this value.
This synchronization is optional.
Conclusion
In this post, we highlighted several extensions that are susceptible to security issues and provided detailed information on related security considerations.
Token-2022 is still at its early stages that lacks extensive publicly available security documentation. Despite the extension capabilities in Token-2022 are quite robust, notable security risks may be come with. Since extensions are directly tied to Mint and Token Accounts, many of these risks can pose significant threats to funds of users and projects.
Throughout our research, we devoted significant effort to studying the official Token-2022 documentation and SPL source code. Recently, we came across a blog by Neodyme, a well-known security research team in the Solana ecosystem, which also addressed security concerns related to Token-2022 extensions. Fortunately, many of our perspectives on the security of these extensions align with theirs. We are also looking forward to more research on Token-2022 security in the future to help the community build a more secure Solana ecosystem.
This is the second blog from Offside Labs regarding Token-2022 security best practices. Check out our last blog on:
Acknowledgements
We would like to thank the Meteora team, especially @sudoku_defi, for their valuable insights, which shaped the solution for calculate_inverse_fee.
Appendix
Mathematical Proof for Modified calculate_inverse_fee
Given x as the amount_in of transfer_checked IX and r
as the transfer_fee_basis_points, where x
and r
are natural numbers satisfying 0 < r < 10000.
The post_amount satisfies following equation:
The function implementation for calculate_fee and calculate_inverse_fee can be represented as:
The modified calculate_pre_fee_amount can be represented as:
Now let’s prove the following inequation:
Proof:
The inequation is:
As calculate_fee is a monotonically increasing function of x
, the inequality to be proved can be simplified to:
The proof follows: