continuation_router/
lib.rs

1//! Atomically routes a swap between multiple pools.
2//!
3//! To use this, create a transaction consisting of the following instructions:
4//! 1. A [Begin] instruction
5//! 2. Action instructions
6//! 3. An [End] instruction
7
8use continuation_router_syn::router_action;
9
10use anchor_lang::{prelude::*, solana_program::pubkey::PUBKEY_BYTES};
11use anchor_spl::token::{Token, TokenAccount};
12use num_enum::{IntoPrimitive, TryFromPrimitive};
13use vipers::prelude::*;
14
15pub mod action;
16pub mod processor;
17
18use crate::action::ProcessAction;
19use crate::processor::{ActionContext, Processor};
20
21declare_id!("Crt7UoUR6QgrFrN7j8rmSQpUTNWNSitSwWvsWGf1qZ5t");
22
23macro_rules! process_action {
24    ($ctx:expr) => {{
25        let ctx = $ctx;
26        let cont = &mut ctx.accounts.continuation.continuation;
27        let action = &ctx.accounts.action;
28        let action_ctx = &ActionContext {
29            program_id: ctx.program_id,
30            action,
31            remaining_accounts: ctx.remaining_accounts,
32            token_program: ctx.accounts.continuation.token_program.clone(),
33            swap_program: ctx.accounts.continuation.swap_program.to_account_info(),
34            owner: ctx.accounts.continuation.owner.to_account_info(),
35        };
36        Processor::process(action_ctx, cont)
37    }};
38}
39
40#[program]
41pub mod continuation_router {
42    use super::*;
43
44    /// Creates an ATA if it does not yet exist.
45    pub fn create_ata_if_not_exists(ctx: Context<CreateATAIfNotExists>) -> Result<()> {
46        if !ctx.accounts.ata.try_borrow_data()?.is_empty() {
47            // ata already exists.
48            return Ok(());
49        }
50        anchor_spl::associated_token::create(CpiContext::new(
51            ctx.accounts.associated_token_program.to_account_info(),
52            anchor_spl::associated_token::Create {
53                payer: ctx.accounts.payer.to_account_info(),
54                associated_token: ctx.accounts.ata.to_account_info(),
55                authority: ctx.accounts.authority.to_account_info(),
56                mint: ctx.accounts.mint.to_account_info(),
57                rent: ctx.accounts.rent.to_account_info(),
58                system_program: ctx.accounts.system_program.to_account_info(),
59                token_program: ctx.accounts.token_program.to_account_info(),
60            },
61        ))?;
62        Ok(())
63    }
64
65    /// Begins a swap transaction.
66    pub fn begin(
67        ctx: Context<Begin>,
68        amount_in: u64,
69        minimum_amount_out: u64,
70        num_steps: u16,
71    ) -> Result<()> {
72        let continuation = &mut ctx.accounts.continuation;
73        continuation.owner = *ctx.accounts.owner.key;
74        continuation.payer = *ctx.accounts.payer.key;
75
76        continuation.input = *ctx.accounts.input.to_account_info().key;
77        continuation.initial_amount_in = TokenAmount::new(ctx.accounts.input.mint, amount_in);
78        continuation.output = *ctx.accounts.output.to_account_info().key;
79        continuation.output_initial_balance = ctx.accounts.output.amount;
80
81        continuation.amount_in = TokenAmount::new(ctx.accounts.input.mint, amount_in);
82        continuation.minimum_amount_out =
83            TokenAmount::new(ctx.accounts.output.mint, minimum_amount_out);
84        continuation.steps_left = num_steps;
85        continuation.__nonce = *unwrap_int!(ctx.bumps.get("continuation"));
86
87        Ok(())
88    }
89
90    /// Begins a swap transaction.
91    /// More optimized.
92    pub fn begin_v2(
93        ctx: Context<BeginV2>,
94        amount_in: u64,
95        minimum_amount_out: u64,
96        num_steps: u16,
97    ) -> Result<()> {
98        let continuation = &mut ctx.accounts.continuation;
99        continuation.owner = ctx.accounts.owner.key();
100        continuation.payer = ctx.accounts.owner.key();
101
102        continuation.input = ctx.accounts.input.key();
103        continuation.initial_amount_in = TokenAmount::new(ctx.accounts.input.mint, amount_in);
104        continuation.output = ctx.accounts.output.key();
105        continuation.output_initial_balance = ctx.accounts.output.amount;
106
107        continuation.amount_in = TokenAmount::new(ctx.accounts.input.mint, amount_in);
108        continuation.minimum_amount_out =
109            TokenAmount::new(ctx.accounts.output.mint, minimum_amount_out);
110        continuation.steps_left = num_steps;
111        Ok(())
112    }
113
114    /// Cleans up the transaction and checks several invariants.
115    pub fn end(ctx: Context<End>) -> Result<()> {
116        let continuation = &ctx.accounts.continuation;
117        require!(continuation.steps_left == 0, EndIncomplete);
118
119        let result_balance = ctx.accounts.output.amount;
120        require!(
121            result_balance >= continuation.output_initial_balance,
122            BalanceLower
123        );
124        require!(
125            ctx.accounts.output.mint == continuation.minimum_amount_out.mint,
126            OutputMintMismatch,
127        );
128
129        let mut amount_out = result_balance - continuation.output_initial_balance;
130        // if input token = output token, add the initial amount in to the difference
131        if continuation.initial_amount_in.mint == ctx.accounts.output.mint {
132            amount_out += continuation.initial_amount_in.amount;
133        }
134
135        require!(
136            amount_out >= continuation.minimum_amount_out.amount,
137            MinimumOutNotMet,
138        );
139
140        emit!(SwapCompleteEvent {
141            owner: continuation.owner,
142            amount_in: continuation.initial_amount_in,
143            amount_out: TokenAmount::new(continuation.minimum_amount_out.mint, amount_out),
144        });
145        Ok(())
146    }
147
148    pub fn ss_swap<'info>(ctx: Context<'_, '_, '_, 'info, SSSwapAccounts<'info>>) -> Result<()> {
149        process_action!(ctx)
150    }
151
152    pub fn ss_withdraw_one<'info>(
153        ctx: Context<'_, '_, '_, 'info, SSWithdrawOneAccounts<'info>>,
154    ) -> Result<()> {
155        process_action!(ctx)
156    }
157
158    pub fn ss_deposit_a<'info>(
159        ctx: Context<'_, '_, '_, 'info, SSDepositAAccounts<'info>>,
160    ) -> Result<()> {
161        process_action!(ctx)
162    }
163
164    pub fn ss_deposit_b<'info>(
165        ctx: Context<'_, '_, '_, 'info, SSDepositBAccounts<'info>>,
166    ) -> Result<()> {
167        process_action!(ctx)
168    }
169
170    pub fn ad_withdraw<'info>(
171        ctx: Context<'_, '_, '_, 'info, ADWithdrawAccounts<'info>>,
172    ) -> Result<()> {
173        process_action!(ctx)
174    }
175
176    pub fn ad_deposit<'info>(
177        ctx: Context<'_, '_, '_, 'info, ADDepositAccounts<'info>>,
178    ) -> Result<()> {
179        process_action!(ctx)
180    }
181}
182
183// --------------------------------
184// Actions
185// --------------------------------
186
187#[router_action]
188#[derive(Accounts)]
189pub struct SSSwap<'info> {
190    /// Swap and authority
191    pub swap: StableSwap<'info>,
192    /// The input token of this component of the route.
193    pub input: SwapToken<'info>,
194    /// The output token of this component of the route.
195    pub output: SwapOutput<'info>,
196}
197
198#[router_action]
199#[derive(Accounts)]
200pub struct SSWithdrawOne<'info> {
201    /// Swap and authority
202    pub swap: StableSwap<'info>,
203    /// The pool mint of the swap.
204    /// CHECK: Checked by [stable_swap_anchor] program.
205    #[account(mut)]
206    pub pool_mint: AccountInfo<'info>,
207    /// The input account for LP tokens.
208    #[account(mut)]
209    pub input_lp: Account<'info, TokenAccount>,
210    /// The output of the unused token of this component of the route.
211    /// CHECK: Checked by [stable_swap_anchor] program.
212    #[account(mut)]
213    pub quote_reserves: AccountInfo<'info>,
214    /// The output of this component of the route.
215    pub output: SwapOutput<'info>,
216}
217
218#[router_action]
219#[derive(Accounts)]
220pub struct SSDepositA<'info> {
221    pub inner: SSDeposit<'info>,
222}
223
224#[router_action]
225#[derive(Accounts)]
226pub struct SSDepositB<'info> {
227    pub inner: SSDeposit<'info>,
228}
229
230#[router_action(pass_through)]
231#[derive(Accounts)]
232pub struct ADWithdraw<'info> {
233    pub input: Account<'info, TokenAccount>,
234    pub output: Account<'info, TokenAccount>,
235}
236
237#[router_action(pass_through)]
238#[derive(Accounts)]
239pub struct ADDeposit<'info> {
240    pub input: Account<'info, TokenAccount>,
241    pub output: Account<'info, TokenAccount>,
242}
243
244// --------------------------------
245// Instructions
246// --------------------------------
247
248/// Token accounts for the destination of a [StableSwap] instruction.
249#[derive(Accounts)]
250pub struct CreateATAIfNotExists<'info> {
251    /// The token accounts of the user and the token.
252    #[account(mut)]
253    pub payer: Signer<'info>,
254
255    /// The ATA to create.
256    #[account(mut)]
257    pub ata: SystemAccount<'info>,
258
259    /// Authority of the created ATA.
260    /// CHECK: Passed to ATA program.
261    pub authority: UncheckedAccount<'info>,
262
263    /// Mint.
264    /// CHECK: Not necessary to deserialize.
265    pub mint: UncheckedAccount<'info>,
266
267    /// Rent.
268    pub rent: Sysvar<'info, Rent>,
269
270    /// System program.
271    pub system_program: Program<'info, System>,
272
273    /// Token program.
274    pub token_program: Program<'info, Token>,
275
276    /// The associated token program.
277    pub associated_token_program: Program<'info, anchor_spl::associated_token::AssociatedToken>,
278}
279
280/// Begins a route.
281#[derive(Accounts)]
282pub struct Begin<'info> {
283    /// Continuation state.
284    #[account(
285        init,
286        seeds = [
287            b"anchor".as_ref(),
288            owner.key().as_ref(),
289            random.key().as_ref()
290        ],
291        bump,
292        space = 8 + Continuation::LEN,
293        payer = payer
294    )]
295    pub continuation: Box<Account<'info, Continuation>>,
296
297    /// Nonce used for associating the continuation. Any arbitrary [Pubkey] can be passed here.
298    /// CHECK: Arbitrary.
299    pub random: UncheckedAccount<'info>,
300
301    /// Input token account.
302    #[account(has_one = owner)]
303    pub input: Box<Account<'info, TokenAccount>>,
304
305    /// Output token account.
306    #[account(has_one = owner)]
307    pub output: Box<Account<'info, TokenAccount>>,
308
309    /// Owner of all token accounts in the chain.
310    pub owner: Signer<'info>,
311
312    /// Funds the continuation in the beginning transaction and receives
313    /// the staked lamports of the continuation in the end transaction.
314    #[account(mut)]
315    pub payer: Signer<'info>,
316
317    /// Rent sysvar.
318    pub rent: Sysvar<'info, Rent>,
319
320    /// System program.
321    pub system_program: Program<'info, System>,
322}
323
324/// Begins a route.
325#[derive(Accounts)]
326pub struct BeginV2<'info> {
327    /// Continuation state.
328    #[account(zero)]
329    pub continuation: Box<Account<'info, Continuation>>,
330
331    /// Input token account.
332    #[account(has_one = owner)]
333    pub input: Box<Account<'info, TokenAccount>>,
334
335    /// Output token account.
336    #[account(has_one = owner)]
337    pub output: Box<Account<'info, TokenAccount>>,
338
339    /// Owner of all token accounts in the chain.
340    pub owner: Signer<'info>,
341}
342
343/// Ends a route.
344#[derive(Accounts)]
345pub struct End<'info> {
346    /// Continuation state.
347    #[account(
348        mut,
349        close = payer,
350        has_one = owner,
351        has_one = payer,
352        has_one = output,
353    )]
354    pub continuation: Box<Account<'info, Continuation>>,
355
356    /// Output token account
357    pub output: Box<Account<'info, TokenAccount>>,
358
359    /// Owner of all accounts in the chain.
360    pub owner: Signer<'info>,
361
362    /// Funds the continuation in the beginning transaction and receives
363    /// the staked lamports of the continuation in the end transaction.
364    /// CHECK: Arbitrary.
365    #[account(mut)]
366    pub payer: UncheckedAccount<'info>,
367}
368
369#[derive(Accounts)]
370pub struct SSSwapAccounts<'info> {
371    pub continuation: ContinuationAccounts<'info>,
372    pub action: SSSwap<'info>,
373}
374
375#[derive(Accounts)]
376pub struct SSWithdrawOneAccounts<'info> {
377    pub continuation: ContinuationAccounts<'info>,
378    pub action: SSWithdrawOne<'info>,
379}
380
381#[derive(Accounts)]
382pub struct SSDepositAAccounts<'info> {
383    pub continuation: ContinuationAccounts<'info>,
384    pub action: SSDepositA<'info>,
385}
386
387#[derive(Accounts)]
388pub struct SSDepositBAccounts<'info> {
389    pub continuation: ContinuationAccounts<'info>,
390    pub action: SSDepositB<'info>,
391}
392
393#[derive(Accounts)]
394pub struct ADWithdrawAccounts<'info> {
395    pub continuation: ContinuationAccounts<'info>,
396    pub action: ADWithdraw<'info>,
397}
398
399#[derive(Accounts)]
400pub struct ADDepositAccounts<'info> {
401    pub continuation: ContinuationAccounts<'info>,
402    pub action: ADDeposit<'info>,
403}
404
405// --------------------------------
406// Various accounts
407// --------------------------------
408
409/// Context common to all router operations.
410#[derive(Accounts)]
411pub struct ContinuationAccounts<'info> {
412    /// Continuation state
413    #[account(
414        mut,
415        has_one = owner,
416    )]
417    pub continuation: Box<Account<'info, Continuation>>,
418
419    /// The spl_token program.
420    pub token_program: Program<'info, Token>,
421
422    /// The relevant swap program.
423    /// CHECK: Arbitrary.
424    pub swap_program: UncheckedAccount<'info>,
425
426    /// The owner of all involved token accounts.
427    pub owner: Signer<'info>,
428}
429
430/// Deposit accounts
431#[derive(Accounts)]
432pub struct SSDeposit<'info> {
433    /// Swap and authority
434    pub swap: StableSwap<'info>,
435    /// The input of token A of this component of the route.
436    pub input_a: SwapToken<'info>,
437    /// The input of token B of this component of the route.
438    pub input_b: SwapToken<'info>,
439    /// The pool mint of the swap.
440    /// CHECK: Checked by [stable_swap_anchor] program.
441    #[account(mut)]
442    pub pool_mint: AccountInfo<'info>,
443    /// The destination account for LP tokens.
444    #[account(mut)]
445    pub output_lp: Account<'info, TokenAccount>,
446}
447
448/// Accounts for interacting with a StableSwap pool.
449#[derive(Accounts)]
450pub struct StableSwap<'info> {
451    /// The swap account
452    /// CHECK: Checked by [stable_swap_anchor] program.
453    pub swap: AccountInfo<'info>,
454    /// The authority of the swap.
455    /// CHECK: Checked by [stable_swap_anchor] program.
456    pub swap_authority: AccountInfo<'info>,
457    /// The clock.
458    pub clock: Sysvar<'info, Clock>,
459}
460
461/// Token accounts for a [StableSwap] instruction.
462#[derive(Accounts)]
463pub struct SwapToken<'info> {
464    /// The token account associated with the user.
465    #[account(mut)]
466    pub user: Box<Account<'info, TokenAccount>>,
467    /// The token account for the pool's reserves of this token.
468    /// CHECK: Checked by [stable_swap_anchor] program.
469    #[account(mut)]
470    pub reserve: AccountInfo<'info>,
471}
472
473/// Token accounts for the destination of a [StableSwap] instruction.
474#[derive(Accounts)]
475pub struct SwapOutput<'info> {
476    /// The token accounts of the user and the token.
477    pub user_token: SwapToken<'info>,
478    /// The token account for the fees associated with the token.
479    /// CHECK: Checked by [stable_swap_anchor] program.
480    #[account(mut)]
481    pub fees: AccountInfo<'info>,
482}
483
484/// Continuation state of the owner.
485#[account]
486#[derive(Default)]
487pub struct Continuation {
488    /// The owner of the continuation.
489    pub owner: Pubkey,
490
491    /// The payer of the continuation.
492    pub payer: Pubkey,
493
494    /// The initial amount of tokens in.
495    pub initial_amount_in: TokenAmount,
496
497    /// The next input account.
498    pub input: Pubkey,
499
500    /// The next amount of tokens to input.
501    pub amount_in: TokenAmount,
502
503    /// The total number of steps that still need to be executed.
504    pub steps_left: u16,
505
506    /// The final output account.
507    pub output: Pubkey,
508
509    /// The initial balance of the output account.
510    pub output_initial_balance: u64,
511
512    /// The minimum amount of tokens to output at the end of the transaction.
513    pub minimum_amount_out: TokenAmount,
514
515    /// Nonce field to the struct to hold the bump seed for the program derived address,
516    /// sourced from `<https://github.com/project-serum/anchor/blob/ec6888a3b9f702bc41bd3266e7dd70116df3549c/lang/attribute/account/src/lib.rs#L220-L221.>`.
517    __nonce: u8,
518}
519
520impl Continuation {
521    pub const LEN: usize = PUBKEY_BYTES * 2
522        + TokenAmount::LEN
523        + PUBKEY_BYTES
524        + TokenAmount::LEN
525        + 2
526        + PUBKEY_BYTES
527        + 8
528        + TokenAmount::LEN
529        + 1;
530}
531
532/// --------------------------------
533/// Error codes
534/// --------------------------------
535#[error_code]
536pub enum ErrorCode {
537    #[msg("Path input does not match prior output.")]
538    PathInputOutputMismatch,
539    #[msg("Error in a transitive swap input/output calculation.")]
540    TransitiveSwapCalculationError,
541    #[msg("Swap result overflowed when checking balance difference.")]
542    OverflowSwapResult,
543    #[msg("Swap resulted in a balance lower than the original balance.")]
544    BalanceLower,
545    #[msg("Cannot perform a zero swap.")]
546    ZeroSwap,
547    #[msg("Input owner does not match continuation owner.")]
548    InputOwnerMismatch,
549    #[msg("Input mint does not match continuation input mint.")]
550    InputMintMismatch,
551    #[msg("Output owner does not match continuation owner.")]
552    OutputOwnerMismatch,
553    #[msg("No more steps to process.")]
554    NoMoreSteps,
555    #[msg("Insufficient input balance")]
556    InsufficientInputBalance,
557
558    #[msg("Not all steps were processed.")]
559    EndIncomplete,
560    #[msg("Minimum amount out not met.")]
561    MinimumOutNotMet,
562    #[msg("Output mint does not match continuation output mint.")]
563    OutputMintMismatch,
564}
565
566// --------------------------------
567// Events
568// --------------------------------
569
570#[event]
571pub struct SwapActionEvent {
572    pub action_type: ActionType,
573    pub owner: Pubkey,
574    pub input_amount: TokenAmount,
575    pub output_account: Pubkey,
576    pub output_amount: TokenAmount,
577}
578
579#[event]
580pub struct SwapCompleteEvent {
581    pub owner: Pubkey,
582    pub amount_in: TokenAmount,
583    pub amount_out: TokenAmount,
584}
585
586/// An amount of tokens.
587#[derive(AnchorSerialize, AnchorDeserialize, Clone, Copy, Debug, Default, Eq, PartialEq)]
588pub struct TokenAmount {
589    /// Mint of the token.
590    pub mint: Pubkey,
591    /// Amount of the token.
592    pub amount: u64,
593}
594
595impl TokenAmount {
596    pub const LEN: usize = PUBKEY_BYTES + 8;
597
598    fn new(mint: Pubkey, amount: u64) -> TokenAmount {
599        TokenAmount { mint, amount }
600    }
601}
602
603/// An action.
604pub trait Action {
605    const TYPE: ActionType;
606}
607
608/// Interface for programs that can be routed through.
609#[interface]
610pub trait RouterActionProcessor<'info, T: Accounts<'info>> {
611    fn process_action(
612        ctx: Context<T>,
613        action: u16,
614        amount_in: u64,
615        minimum_amount_out: u64,
616    ) -> Result<()>;
617}
618
619/// Represents a swap from one token to another.
620#[derive(
621    AnchorSerialize, AnchorDeserialize, IntoPrimitive, TryFromPrimitive, Copy, Clone, Debug,
622)]
623#[repr(u16)]
624pub enum ActionType {
625    SSSwap = 0,
626    SSWithdrawOne = 1,
627    SSDepositA = 2,
628    SSDepositB = 3,
629
630    ADWithdraw = 10,
631    ADDeposit = 11,
632}