@@ -230,6 +230,250 @@ do more work.
230
230
231
231
## Part II: Support Mixed Token Programs: trading a Token for a Token-2022
232
232
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
+
233
477
## Part III: Support Specific Extensions
234
478
235
479
### Update from ` transfer ` to ` transfer_checked `
0 commit comments