hodor_program/swap/
instruction.rs

1use solana_program::program_error::ProgramError;
2use solana_program::program_error::ProgramError::InvalidInstructionData;
3
4#[derive(Debug, PartialEq)]
5pub enum SwapInstruction {
6    // 1-0
7    // Create swap pool
8    // 0. [signer] Fee payer, swap pool creator
9    // 1. [writeable] Swap pool state account - PDA
10    // 2. [] Token A mint
11    // 3. [writeable] Token A pool account
12    // 4. [] Token B mint
13    // 5. [writeable] Token B pool account
14    // 6. [writeable] LP mint
15    // 7. [] SPL token program
16    // 8. [] System program
17    CreatePool {
18        seed: [u8; 32],
19        lp_fee_rate: u32,
20        creator_fee_rate: u32,
21    },
22
23    // 1-1
24    // Swap tokens
25    // 0. [signer] Fee payer, token accounts owner
26    // 1. [writeable] Swap pool state account - PDA
27    // 2. [writeable] Source input token account
28    // 3. [writeable] Destination input token account
29    // 4. [writeable] Source output token account
30    // 5. [writeable] Destination output token account
31    // 6. [] SPL token program
32    // todo: add hodor config account - read dao fee rate from it
33    Swap {
34        in_amount: u64,
35        min_out_amount: u64,
36    },
37
38    // 1-2
39    // Deposit into pool
40    // 0. [signer] Fee payer, token accounts owner
41    // 1. [writeable] Swap pool state account - PDA
42    // 2. [writeable] Source token A account
43    // 3. [writeable] Destination token A account
44    // 4. [writeable] Source token B account
45    // 5. [writeable] Destination token B account
46    // 6. [writeable] LP mint
47    // 7. [writeable] Destination LP token account
48    // 8. [] SPL token program
49    Deposit {
50        // todo: document properties
51        min_a: u64,
52        max_a: u64,
53        min_b: u64,
54        max_b: u64,
55    },
56
57    // 1-3
58    // Withdraw tokens from pool
59    // 0. [signer] Fee payer, token accounts owner
60    // 1. [writeable] Swap pool state account - PDA
61    // 2. [writeable] Source token A account
62    // 3. [writeable] Destination token A account
63    // 4. [writeable] Source token B account
64    // 5. [writeable] Destination token B account
65    // 6. [writeable] LP mint
66    // 7. [writeable] Source LP token account
67    // 8. [] SPL token program
68    Withdraw {
69        lp_amount: u64,
70        min_a: u64,
71        min_b: u64,
72    },
73
74    // 1-4 ChangeCreatorWithdrawAuthority
75    // 1-5 WithdrawCreatorFee
76}
77
78// todo: unit test pack/unpack swap instruction
79impl SwapInstruction {
80    const MODULE_TAG: u8 = 1;
81
82    pub fn pack(&self) -> Vec<u8> {
83        let mut buffer = Vec::new();
84        buffer.push(SwapInstruction::MODULE_TAG);
85
86        match self {
87            SwapInstruction::CreatePool { seed, lp_fee_rate, creator_fee_rate } => {
88                buffer.push(0);
89                buffer.extend_from_slice(seed);
90                buffer.extend_from_slice(&lp_fee_rate.to_le_bytes());
91                buffer.extend_from_slice(&creator_fee_rate.to_le_bytes());
92            }
93            SwapInstruction::Swap { in_amount, min_out_amount } => {
94                buffer.push(1);
95                buffer.extend_from_slice(&in_amount.to_le_bytes());
96                buffer.extend_from_slice(&min_out_amount.to_le_bytes());
97            }
98            SwapInstruction::Deposit { min_a, max_a, min_b, max_b } => {
99                buffer.push(2);
100                buffer.extend_from_slice(&min_a.to_le_bytes());
101                buffer.extend_from_slice(&max_a.to_le_bytes());
102                buffer.extend_from_slice(&min_b.to_le_bytes());
103                buffer.extend_from_slice(&max_b.to_le_bytes());
104            }
105            SwapInstruction::Withdraw { lp_amount, min_a, min_b } => {
106                buffer.push(3);
107                buffer.extend_from_slice(&lp_amount.to_le_bytes());
108                buffer.extend_from_slice(&min_a.to_le_bytes());
109                buffer.extend_from_slice(&min_b.to_le_bytes())
110            }
111        };
112
113        buffer
114    }
115
116    pub fn unpack(input: &[u8]) -> Result<Self, ProgramError> {
117        let (module_tag, rest) = input.split_first().ok_or(InvalidInstructionData)?;
118        if *module_tag != SwapInstruction::MODULE_TAG {
119            return Err(InvalidInstructionData);
120        }
121
122        let (tag, rest) = rest.split_first().ok_or(InvalidInstructionData)?;
123
124        match tag {
125            0 => {
126                let seed = rest
127                    .get(..32)
128                    .and_then(|slice| slice.try_into().ok())
129                    .ok_or(InvalidInstructionData)?;
130
131                let lp_fee_rate = rest.get(32..36)
132                    .and_then(|slice| slice.try_into().ok())
133                    .map(u32::from_le_bytes)
134                    .ok_or(InvalidInstructionData)?;
135
136                let creator_fee_rate = rest.get(36..40)
137                    .and_then(|slice| slice.try_into().ok())
138                    .map(u32::from_le_bytes)
139                    .ok_or(InvalidInstructionData)?;
140
141                Ok(SwapInstruction::CreatePool { seed, lp_fee_rate, creator_fee_rate })
142            }
143            1 => {
144                let in_amount = rest.get(..8)
145                    .and_then(|slice| slice.try_into().ok())
146                    .map(u64::from_le_bytes)
147                    .ok_or(InvalidInstructionData)?;
148
149                let min_out_amount = rest.get(8..16)
150                    .and_then(|slice| slice.try_into().ok())
151                    .map(u64::from_le_bytes)
152                    .ok_or(InvalidInstructionData)?;
153
154                Ok(SwapInstruction::Swap { in_amount, min_out_amount })
155            }
156            2 => {
157                let min_a = rest.get(..8)
158                    .and_then(|slice| slice.try_into().ok())
159                    .map(u64::from_le_bytes)
160                    .ok_or(InvalidInstructionData)?;
161
162                let max_a = rest.get(8..16)
163                    .and_then(|slice| slice.try_into().ok())
164                    .map(u64::from_le_bytes)
165                    .ok_or(InvalidInstructionData)?;
166
167                let min_b = rest.get(16..24)
168                    .and_then(|slice| slice.try_into().ok())
169                    .map(u64::from_le_bytes)
170                    .ok_or(InvalidInstructionData)?;
171
172                let max_b = rest.get(24..32)
173                    .and_then(|slice| slice.try_into().ok())
174                    .map(u64::from_le_bytes)
175                    .ok_or(InvalidInstructionData)?;
176
177                Ok(SwapInstruction::Deposit { min_a, max_a, min_b, max_b })
178            }
179            3 => {
180                let lp_amount = rest.get(..8)
181                    .and_then(|slice| slice.try_into().ok())
182                    .map(u64::from_le_bytes)
183                    .ok_or(InvalidInstructionData)?;
184
185                let min_a = rest.get(8..16)
186                    .and_then(|slice| slice.try_into().ok())
187                    .map(u64::from_le_bytes)
188                    .ok_or(InvalidInstructionData)?;
189
190                let min_b = rest.get(16..24)
191                    .and_then(|slice| slice.try_into().ok())
192                    .map(u64::from_le_bytes)
193                    .ok_or(InvalidInstructionData)?;
194
195                Ok(SwapInstruction::Withdraw { lp_amount, min_a, min_b })
196            }
197            _ => Err(InvalidInstructionData)
198        }
199    }
200}
201
202
203pub fn calculate_deposit_amounts(pool_a_amount: u64, pool_b_amount: u64, lp_supply: u64,
204                                 deposit_max_a: u64, deposit_max_b: u64) -> Option<(u64, u64, u64)> {
205    if lp_supply == 0 {
206        // Deposit to empty pool
207        return Some((deposit_max_a, deposit_max_b, 10_000_000_000 as u64));
208    }
209
210    let pool_ratio = (pool_a_amount as u128)
211        .checked_mul(u64::MAX as u128)?
212        .checked_div(pool_b_amount as u128)?;
213
214    let deposit_ratio = (deposit_max_a as u128)
215        .checked_mul(u64::MAX as u128)?
216        .checked_div(deposit_max_b as u128)?;
217
218    let (deposit_a, deposit_b) = if deposit_ratio >= pool_ratio {
219        let deposit_a: u64 = (deposit_max_b as u128)
220            .checked_mul(pool_ratio)?
221            .checked_div(u64::MAX as u128)?
222            .try_into().ok()?;
223
224        (deposit_a, deposit_max_b)
225    } else {
226        let deposit_b: u64 = (deposit_max_a as u128)
227            .checked_mul(u64::MAX as u128)?
228            .checked_div(pool_ratio)?
229            .try_into().ok()?;
230
231        (deposit_max_a, deposit_b)
232    };
233
234    let lp_mint_amount = (deposit_a as u128)
235        .checked_mul(u64::MAX as u128)?
236        .checked_div(pool_a_amount as u128)?
237        .checked_mul(lp_supply as u128)?
238        .checked_div(u64::MAX as u128)?
239        .try_into().ok()?;
240
241    Some((deposit_a, deposit_b, lp_mint_amount))
242}
243
244const FEE_RATE_BASE_DIVIDER: u128 = 100_000_000;
245
246fn calculate_fee_amount(amount: u128, fee_rate: u32) -> Option<u128> {
247    Some(if fee_rate == 0 {
248        0
249    } else {
250        amount
251            .checked_mul(fee_rate as u128)?
252            .checked_div(FEE_RATE_BASE_DIVIDER)?
253    })
254}
255
256pub fn calculate_swap_amounts(pool_balance_in_token: u64, pool_balance_out_token: u64, swap_in_amount: u64,
257                              dao_fee_rate: u32, lp_fee_rate: u32, creator_fee_rate: u32) -> Option<(u64, u64, u64, u64)> {
258    let swap_in_amount = swap_in_amount as u128;
259
260    let dao_fee_amount = calculate_fee_amount(swap_in_amount, dao_fee_rate)?;
261    let lp_fee_amount = calculate_fee_amount(swap_in_amount, lp_fee_rate)?;
262    let creator_fee_amount = calculate_fee_amount(swap_in_amount, creator_fee_rate)?;
263
264    let pool_balance_in_token_after_fees = (pool_balance_in_token as u128)
265        .checked_add(lp_fee_amount)?;
266    let swap_in_amount_after_fees = swap_in_amount
267        .checked_sub(dao_fee_amount)?
268        .checked_sub(lp_fee_amount)?
269        .checked_sub(creator_fee_amount)?;
270
271    // x * y = k
272    // (x + a)(y - b) = k
273    // b = y * a / (x + a)
274    let swap_out_amount = (pool_balance_out_token as u128)
275        .checked_mul(swap_in_amount_after_fees)?
276        .checked_div(
277            pool_balance_in_token_after_fees
278                .checked_add(swap_in_amount_after_fees)?
279        )?;
280
281    Some((
282        swap_out_amount.try_into().ok()?,
283        dao_fee_amount.try_into().ok()?,
284        lp_fee_amount.try_into().ok()?,
285        creator_fee_amount.try_into().ok()?
286    ))
287}
288
289pub fn calculate_withdraw_amounts(pool_a_amount: u64, pool_b_amount: u64, lp_supply: u64,
290                                  withdraw_lp_amount: u64) -> Option<(u64, u64)> {
291    if withdraw_lp_amount == lp_supply {
292        return Some((pool_a_amount, pool_b_amount));
293    }
294
295    let withdraw_ratio = (withdraw_lp_amount as u128)
296        .checked_mul(u64::MAX as u128)?
297        .checked_div(lp_supply as u128)?;
298
299    let withdraw_a_amount = (pool_a_amount as u128)
300        .checked_mul(withdraw_ratio)?
301        .checked_div(u64::MAX as u128)?
302        .try_into().ok()?;
303
304    let withdraw_b_amount = (pool_b_amount as u128)
305        .checked_mul(withdraw_ratio)?
306        .checked_div(u64::MAX as u128)?
307        .try_into().ok()?;
308
309    Some((withdraw_a_amount, withdraw_b_amount))
310}
311
312
313#[cfg(test)]
314mod tests {
315    use solana_program::pubkey::Pubkey;
316    use super::*;
317
318    #[test]
319    fn test_pack_unpack_swap_instruction() {
320        let create_instruction = SwapInstruction::CreatePool {
321            seed: Pubkey::new_unique().to_bytes(),
322            lp_fee_rate: 5,
323            creator_fee_rate: 60,
324        };
325        assert_eq!(create_instruction, SwapInstruction::unpack(&create_instruction.pack()).unwrap());
326        assert_ne!(create_instruction, SwapInstruction::unpack(&SwapInstruction::CreatePool {
327            seed: Default::default(),
328            lp_fee_rate: 0,
329            creator_fee_rate: 0,
330        }.pack()).unwrap());
331
332
333        let swap_instruction = SwapInstruction::Swap { in_amount: 1, min_out_amount: 2 };
334        assert_eq!(swap_instruction, SwapInstruction::unpack(&swap_instruction.pack()).unwrap());
335        assert_ne!(swap_instruction, SwapInstruction::unpack(&SwapInstruction::Swap {
336            in_amount: 0,
337            min_out_amount: 0,
338        }.pack()).unwrap());
339
340        let deposit_instruction = SwapInstruction::Deposit {
341            min_a: 1,
342            max_a: 2,
343            min_b: 3,
344            max_b: 4,
345        };
346
347        assert_eq!(deposit_instruction, SwapInstruction::unpack(&deposit_instruction.pack()).unwrap());
348        assert_ne!(deposit_instruction, SwapInstruction::unpack(&SwapInstruction::Deposit {
349            min_a: 1,
350            max_a: 1,
351            min_b: 1,
352            max_b: 1,
353        }.pack()).unwrap());
354
355
356        let withdraw_instruction = SwapInstruction::Withdraw {
357            lp_amount: 1,
358            min_a: 2,
359            min_b: 3,
360        };
361        assert_eq!(withdraw_instruction, SwapInstruction::unpack(&withdraw_instruction.pack()).unwrap());
362        assert_ne!(withdraw_instruction, SwapInstruction::unpack(&SwapInstruction::Withdraw {
363            lp_amount: 1,
364            min_a: 1,
365            min_b: 1,
366        }.pack()).unwrap());
367    }
368
369
370    #[test]
371    fn test_calculate_deposit_amounts() {
372        assert_eq!(
373            Some((69, 420, 10_000_000_000)),
374            calculate_deposit_amounts(0, 0, 0, 69, 420)
375        );
376
377        assert_eq!(
378            Some((100, 100, 10_000)),
379            calculate_deposit_amounts(100, 100, 10_000, 100, 100)
380        );
381        assert_eq!(
382            Some((100, 100, 10_000)),
383            calculate_deposit_amounts(100, 100, 10_000, 110, 100)
384        );
385        assert_eq!(
386            Some((100, 100, 10_000)),
387            calculate_deposit_amounts(100, 100, 10_000, 100, 110)
388        );
389
390
391        // todo: add more unit tests
392
393        // todo: test input u64::MAX
394
395        // todo: test for rounding error- printing money
396    }
397
398    #[test]
399    fn test_calculate_swap_amounts_with_fees() {
400        // 1% for every fee type
401        assert_eq!(
402            Some((88_342, 1_000, 1_000, 1_000)),
403            calculate_swap_amounts(1_000_000, 1_000_000, 100_000,
404                                   1_000_000, 1_000_000, 1_000_000)
405        );
406
407        // 90.99% fee
408        assert_eq!(
409            Some((8920, 90000, 990, 0)),
410            calculate_swap_amounts(1_000_000, 1_000_000, 100_000,
411                                   90_000_000, 990_000, 0)
412        );
413
414        // 90.99% fee
415        assert_eq!(
416            Some((8198, 990, 90000, 0)),
417            calculate_swap_amounts(1_000_000, 1_000_000, 100_000,
418                                   990_000, 90_000_000, 0)
419        );
420
421        // over 100% total fee
422        assert_eq!(
423            None,
424            calculate_swap_amounts(1_000_000, 1_000_000, 100_000,
425                                   50_000_000, 50_000_000, 1_000_000)
426        );
427
428        // todo: add more unit tests
429    }
430
431    #[test]
432    fn test_calculate_swap_amounts_without_fees() {
433        assert_eq!(Some((0, 0, 0, 0)), calculate_swap_amounts(1, 100, 0, 0, 0, 0));
434        assert_eq!(Some((0, 0, 0, 0)), calculate_swap_amounts(100, 10, 11, 0, 0, 0));
435        assert_eq!(Some((1, 0, 0, 0)), calculate_swap_amounts(100_000_000, 100, 1_011_000, 0, 0, 0));
436        assert_eq!(Some((4, 0, 0, 0)), calculate_swap_amounts(100, 100, 5, 0, 0, 0));
437        assert_eq!(Some((49_950_049, 0, 0, 0)), calculate_swap_amounts(100_000_000_000, 50_000_000_000, 100_000_000, 0, 0, 0));
438        assert_eq!(Some((372_208_436, 0, 0, 0)), calculate_swap_amounts(100_000_000_000, 50_000_000_000, 750_000_000, 0, 0, 0));
439        assert_eq!(Some((3_333_333, 0, 0, 0)), calculate_swap_amounts(10_000_000, 10_000_000, 5_000_000, 0, 0, 0));
440        assert_eq!(Some((6_666_666, 0, 0, 0)), calculate_swap_amounts(10_000_000, 10_000_000, 20_000_000, 0, 0, 0));
441        assert_eq!(Some((8_000_000, 0, 0, 0)), calculate_swap_amounts(10_000_000, 10_000_000, 40_000_000, 0, 0, 0));
442        assert_eq!(Some((12_990_906, 0, 0, 0)), calculate_swap_amounts(70_000_000, 13_000_000, 100_000_000_000, 0, 0, 0));
443    }
444
445    #[test]
446    fn test_calculate_withdraw_amounts() {
447        assert_eq!(
448            Some((10, 10)),
449            calculate_withdraw_amounts(10, 10, 100, 100)
450        );
451
452        assert_eq!(
453            Some((4_999, 4_999)),
454            calculate_withdraw_amounts(10_000, 10_000, 100_000, 50_000)
455        );
456
457        assert_eq!(
458            Some((4_999_999_999, 4_999_999_999)),
459            calculate_withdraw_amounts(10_000_000_000, 10_000_000_000, 100_000_000_000, 50_000_000_000)
460        );
461
462        assert_eq!(
463            Some((5_000, 5_000)),
464            calculate_withdraw_amounts(10_000, 10_000, 100_000, 50_001)
465        );
466
467        assert_eq!(
468            Some((5_000, 2_500)),
469            calculate_withdraw_amounts(10_000, 5_000, 100_000, 50_001)
470        );
471
472        assert_eq!(
473            Some((2_500, 5_000)),
474            calculate_withdraw_amounts(5_000, 10_000, 100_000, 50_001)
475        );
476
477        assert_eq!(
478            Some((0, 0)),
479            calculate_withdraw_amounts(5_000, 10_000, 100_000, 1)
480        );
481
482        assert_eq!(
483            Some((49, 29)),
484            calculate_withdraw_amounts(5_000_000, 3_000_000, 100_000, 1)
485        );
486
487        assert_eq!(
488            Some((0, 0)),
489            calculate_withdraw_amounts(10, 10, 100, 9)
490        );
491
492        // todo: tests with rounding errors
493        // todo: tests with overflow
494    }
495}