Skip to content
This repository was archived by the owner on Mar 11, 2025. It is now read-only.

Commit 008809e

Browse files
committed
Add docs for migrating a program to two token programs
1 parent da27180 commit 008809e

File tree

1 file changed

+244
-0
lines changed

1 file changed

+244
-0
lines changed

docs/src/token-2022/onchain.md

Lines changed: 244 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,250 @@ do more work.
230230

231231
## Part II: Support Mixed Token Programs: trading a Token for a Token-2022
232232

233+
In Part I, we looked at the minimal amount of work to support Token-2022 in your
234+
program. This work won't cover all cases, however. Specifically, in the token-swap
235+
program, most instructions involve multiple token types. If those token types are
236+
from different token programs, then our current implementation will fail.
237+
238+
For example, if you want to swap tokens from the Token program for tokens from
239+
the Token-2022 program, then your program's instruction must provide each token
240+
program, so that your program may invoke them.
241+
242+
Let's go through the steps to support both token programs in the same instruction.
243+
244+
### Step 1: Update all instruction interfaces
245+
246+
The first step is to update all instruction interfaces to accept a token program
247+
for each token type used in the program.
248+
249+
For example, here is the previous definition for the `Swap` instruction:
250+
251+
```rust
252+
/// Swap the tokens in the pool.
253+
///
254+
/// 0. `[]` Token-swap
255+
/// 1. `[]` swap authority
256+
/// 2. `[]` user transfer authority
257+
/// 3. `[writable]` token_(A|B) SOURCE Account, amount is transferable by user transfer authority,
258+
/// 4. `[writable]` token_(A|B) Base Account to swap INTO. Must be the SOURCE token.
259+
/// 5. `[writable]` token_(A|B) Base Account to swap FROM. Must be the DESTINATION token.
260+
/// 6. `[writable]` token_(A|B) DESTINATION Account assigned to USER as the owner.
261+
/// 7. `[writable]` Pool token mint, to generate trading fees
262+
/// 8. `[writable]` Fee account, to receive trading fees
263+
/// 9. `[]` Token program id
264+
/// 10. `[optional, writable]` Host fee account to receive additional trading fees
265+
Swap {
266+
pub amount_in: u64,
267+
pub minimum_amount_out: u64
268+
}
269+
```
270+
271+
`Swap` contains 3 different token types: token A, token B, and the pool token. Let's
272+
add a separate token program for each, transforming the instruction into:
273+
274+
```rust
275+
/// Swap the tokens in the pool.
276+
///
277+
/// 0. `[]` Token-swap
278+
/// 1. `[]` swap authority
279+
/// 2. `[]` user transfer authority
280+
/// 3. `[writable]` token_(A|B) SOURCE Account, amount is transferable by user transfer authority,
281+
/// 4. `[writable]` token_(A|B) Base Account to swap INTO. Must be the SOURCE token.
282+
/// 5. `[writable]` token_(A|B) Base Account to swap FROM. Must be the DESTINATION token.
283+
/// 6. `[writable]` token_(A|B) DESTINATION Account assigned to USER as the owner.
284+
/// 7. `[writable]` Pool token mint, to generate trading fees
285+
/// 8. `[writable]` Fee account, to receive trading fees
286+
/// 9. `[]` Token (A|B) SOURCE program id
287+
/// 10. `[]` Token (A|B) DESTINATION program id
288+
/// 11. `[]` Pool Token program id
289+
/// 12. `[optional, writable]` Host fee account to receive additional trading fees
290+
Swap {
291+
pub amount_in: u64,
292+
pub minimum_amount_out: u64
293+
}
294+
```
295+
296+
Note the clarification on `9.`, and the new inputs of `10.` and `11.`.
297+
298+
All of these additional accounts may make you wonder: how big will transactions
299+
get with these new accounts? If you are using both Token and Token-2022,
300+
the additional Token-2022 program will take up space in the transaction,
301+
32 bytes for the pubkey, and 1 byte for its index.
302+
303+
On the flip side, if you're only using one token program at once, you will only
304+
incur 1 byte of overhead because of the deduplication of accounts in the Solana
305+
transaction format.
306+
307+
Also note that some instructions will remain unchanged. For example, here is the
308+
`Initialize` instruction:
309+
310+
```rust
311+
/// Initializes a new swap
312+
///
313+
/// 0. `[writable, signer]` New Token-swap to create.
314+
/// 1. `[]` swap authority derived from `create_program_address(&[Token-swap account])`
315+
/// 2. `[]` token_a Account. Must be non zero, owned by swap authority.
316+
/// 3. `[]` token_b Account. Must be non zero, owned by swap authority.
317+
/// 4. `[writable]` Pool Token Mint. Must be empty, owned by swap authority.
318+
/// 5. `[]` Pool Token Account to deposit trading and withdraw fees.
319+
/// Must be empty, not owned by swap authority
320+
/// 6. `[writable]` Pool Token Account to deposit the initial pool token
321+
/// supply. Must be empty, not owned by swap authority.
322+
/// 7. `[]` Token program id
323+
Initialize { ... } // details omitted
324+
```
325+
326+
Although we pass in token A and token B accounts, we don't actually need to invoke
327+
their respective token programs. We do, however, mint new pool tokens, so we must
328+
pass in the token program for the pool token mint.
329+
330+
This step is mostly churn since interfaces must be updated. Don't worry if some
331+
tests fail after this step. We'll fix them in the next step.
332+
333+
### Step 2: Update instruction processors
334+
335+
If your instruction processor is expecting accounts after the added token programs,
336+
you may see some test failures.
337+
338+
Specifically, in the token-swap example, the `Swap` instruction is expecting
339+
an optional account at the end, which has been clobbered by the added token
340+
programs.
341+
342+
For this step, we'll simply pull out all of the new provided accounts. For example,
343+
in the `Swap` instruction processor, we'll go from:
344+
345+
```rust
346+
let account_info_iter = &mut accounts.iter();
347+
let swap_info = next_account_info(account_info_iter)?;
348+
let authority_info = next_account_info(account_info_iter)?;
349+
let user_transfer_authority_info = next_account_info(account_info_iter)?; let source_info = next_account_info(account_info_iter)?;
350+
let swap_source_info = next_account_info(account_info_iter)?;
351+
let swap_destination_info = next_account_info(account_info_iter)?;
352+
let destination_info = next_account_info(account_info_iter)?; let pool_mint_info = next_account_info(account_info_iter)?; let pool_fee_account_info = next_account_info(account_info_iter)?;
353+
let token_program_info = next_account_info(account_info_iter)?;
354+
```
355+
356+
To:
357+
358+
```rust
359+
let account_info_iter = &mut accounts.iter();
360+
let swap_info = next_account_info(account_info_iter)?;
361+
let authority_info = next_account_info(account_info_iter)?;
362+
let user_transfer_authority_info = next_account_info(account_info_iter)?;
363+
let source_info = next_account_info(account_info_iter)?;
364+
let swap_source_info = next_account_info(account_info_iter)?;
365+
let swap_destination_info = next_account_info(account_info_iter)?;
366+
let destination_info = next_account_info(account_info_iter)?;
367+
let pool_mint_info = next_account_info(account_info_iter)?;
368+
let pool_fee_account_info = next_account_info(account_info_iter)?;
369+
let source_token_program_info = next_account_info(account_info_iter)?;
370+
let destination_token_program_info = next_account_info(account_info_iter)?;
371+
let pool_token_program_info = next_account_info(account_info_iter)?;
372+
```
373+
374+
For now, just use one of those. For example, we'll just use `pool_token_program_info`
375+
everywhere. In the next step, we'll add some tests which will properly fail since
376+
we're always using the same token program.
377+
378+
Once again, all of your tests should pass! But not for long.
379+
380+
### Step 3: Write tests using multiple token programs at once
381+
382+
In the spirit of test-driven development, let's start by writing some failing
383+
tests.
384+
385+
Previously, our `test_case`s defined only provided one program id. Now it's
386+
time to mix them up and add more cases. For full coverage, we could do all
387+
permutations of different programs, but let's go with:
388+
389+
* all mints belong to Token
390+
* all mints belong to Token-2022
391+
* the pool mint belongs to Token, but token A and B belong to Token-2022
392+
* the pool mint belongs to Token-2022, but token A and B are mixed
393+
394+
Let's update test cases to pass in three different program ids, and then use them
395+
in the tests. For example, that means transforming:
396+
397+
```rust
398+
#[test_case(spl_token::id(); "token")]
399+
#[test_case(spl_token_2022::id(); "token-2022")]
400+
fn test_initialize(token_program_id: Pubkey) {
401+
```
402+
403+
Into:
404+
405+
```rust
406+
#[test_case(spl_token::id(), spl_token::id(), spl_token::id(); "all-token")]
407+
#[test_case(spl_token_2022::id(), spl_token_2022::id(), spl_token_2022::id(); "all-token-2022")]
408+
#[test_case(spl_token::id(), spl_token_2022::id(), spl_token_2022::id(); "mixed-pool-token")]
409+
#[test_case(spl_token_2022::id(), spl_token_2022::id(), spl_token::id(); "mixed-pool-token-2022")]
410+
fn test_initialize(pool_token_program_id: Pubkey, token_a_program_id: Pubkey, token_b_program_id: Pubkey) {
411+
...
412+
}
413+
```
414+
415+
This step may also involve churn, but take your time to go through it carefully,
416+
and you'll have failing tests for the `mixed-pool-token` and `mixed-pool-token-2022`
417+
test cases.
418+
419+
### Step 4: Use appropriate token program in your processor
420+
421+
Let's fix the failing tests! The errors come up because we're trying to operate
422+
on tokens with the wrong program in a "mixed" Token and Token-2022 environment.
423+
424+
We need to properly use all of the `pool_token_program_info` / `token_a_program_info`
425+
variables that we extracted in Step 2.
426+
427+
In the token-swap example, we'll check anywhere we filled in `pool_token_program_info`
428+
by default, and instead choose the correct program info. For example, when
429+
transferring the source tokens in `process_swap`, we currently have:
430+
431+
```rust
432+
Self::token_transfer(
433+
swap_info.key,
434+
pool_token_program_info.clone(),
435+
source_info.clone(),
436+
swap_source_info.clone(),
437+
user_transfer_authority_info.clone(),
438+
token_swap.bump_seed(),
439+
to_u64(result.source_amount_swapped)?,
440+
)?;
441+
```
442+
443+
Let's use the correct token program, making this:
444+
445+
```rust
446+
Self::token_transfer(
447+
swap_info.key,
448+
source_token_program_info.clone(),
449+
source_info.clone(),
450+
swap_source_info.clone(),
451+
user_transfer_authority_info.clone(),
452+
token_swap.bump_seed(),
453+
to_u64(result.source_amount_swapped)?,
454+
)?;
455+
```
456+
457+
While going through this, if you notice any owner checks for a token account or mint
458+
in the form of:
459+
460+
```rust
461+
if token_account_info.owner != &spl_token::id() { ... }
462+
```
463+
464+
You'll need to update to a new owner check from `spl_token_2022`:
465+
466+
```rust
467+
if spl_token_2022::check_spl_token_program_account(token_account_info.owner).is_err() { ... }
468+
```
469+
470+
In this step, because of all the test cases in token-swap, we also have to update
471+
the expected error due to mismatched owner token programs.
472+
473+
It's tedious, but at this point, we have updated our program to use both Token
474+
and Token-2022 simultaneously. Congratulations! You're ready to be part of the
475+
next stage of DeFi on Solana.
476+
233477
## Part III: Support Specific Extensions
234478

235479
### Update from `transfer` to `transfer_checked`

0 commit comments

Comments
 (0)