Skip to content

Commit f3c1559

Browse files
committed
token-swap-native: Add swap_exact_tokens_for_tokens
1 parent eb89006 commit f3c1559

File tree

10 files changed

+464
-43
lines changed

10 files changed

+464
-43
lines changed

tokens/token-swap/native/program/src/errors.rs

+2
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ pub enum AmmError {
55
InvalidMint,
66
InvalidAuthority,
77
DepositTooSmall,
8+
OutputTooSmall,
9+
InvariantViolated,
810
}
911

1012
impl From<AmmError> for ProgramError {

tokens/token-swap/native/program/src/instructions/mod.rs

+5
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,19 @@ use borsh::{BorshDeserialize, BorshSerialize};
33
pub mod create_amm;
44
pub mod create_pool;
55
pub mod deposit_liquidity;
6+
pub mod swap_exact_tokens_for_tokens;
67

78
pub use create_amm::{process_create_amm, CreateAmmArgs};
89
pub use create_pool::{process_create_pool, CreatePoolArgs};
910
pub use deposit_liquidity::{process_deposit_liquidity, DepositLiquidityArgs};
11+
pub use swap_exact_tokens_for_tokens::{
12+
process_swap_exact_tokens_for_tokens, SwapExactTokensForTokensArgs,
13+
};
1014

1115
#[derive(BorshSerialize, BorshDeserialize, Debug)]
1216
pub enum AmmInstruction {
1317
CreateAmm(CreateAmmArgs),
1418
CreatePool(CreatePoolArgs),
1519
DepositLiquidity(DepositLiquidityArgs),
20+
SwapExactTokensForToken(SwapExactTokensForTokensArgs),
1621
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
use borsh::{BorshDeserialize, BorshSerialize};
2+
use fixed::types::I64F64;
3+
use solana_program::{
4+
account_info::{next_account_info, AccountInfo},
5+
entrypoint::ProgramResult,
6+
msg,
7+
program::{invoke, invoke_signed},
8+
program_error::ProgramError,
9+
program_pack::Pack,
10+
pubkey::Pubkey,
11+
};
12+
use spl_token::{instruction::transfer, state::Account as TokenAccount};
13+
14+
use crate::{
15+
errors::AmmError,
16+
state::{Amm, Pool},
17+
};
18+
19+
#[derive(BorshDeserialize, BorshSerialize, Debug)]
20+
pub struct SwapExactTokensForTokensArgs {
21+
swap_a: bool,
22+
input_amount: u64,
23+
min_output_amount: u64,
24+
}
25+
26+
pub fn process_swap_exact_tokens_for_tokens(
27+
program_id: &Pubkey,
28+
accounts: &[AccountInfo],
29+
args: SwapExactTokensForTokensArgs,
30+
) -> ProgramResult {
31+
let accounts_iter = &mut accounts.iter();
32+
33+
let amm = next_account_info(accounts_iter)?;
34+
let pool = next_account_info(accounts_iter)?;
35+
let pool_authority = next_account_info(accounts_iter)?;
36+
let trader = next_account_info(accounts_iter)?;
37+
let mint_a = next_account_info(accounts_iter)?;
38+
let mint_b = next_account_info(accounts_iter)?;
39+
let pool_account_a = next_account_info(accounts_iter)?;
40+
let pool_account_b = next_account_info(accounts_iter)?;
41+
let trader_account_a = next_account_info(accounts_iter)?;
42+
let trader_account_b = next_account_info(accounts_iter)?;
43+
let token_program = next_account_info(accounts_iter)?;
44+
45+
// Check that the pool corresponds to the amm and target mints
46+
let pool_data = Pool::try_from_slice(&pool.data.borrow())?;
47+
if &pool_data.amm != amm.key
48+
|| &pool_data.mint_a != mint_a.key
49+
|| &pool_data.mint_b != mint_b.key
50+
{
51+
return Err(ProgramError::InvalidAccountData);
52+
}
53+
54+
// Verify pool_authority PDA
55+
let pool_authority_seeds = &[
56+
Pool::AUTHORITY_PREFIX.as_ref(),
57+
pool_data.amm.as_ref(),
58+
mint_a.key.as_ref(),
59+
mint_b.key.as_ref(),
60+
];
61+
let (pool_authority_pda, pool_authority_bump) =
62+
Pubkey::find_program_address(pool_authority_seeds, program_id);
63+
if pool_authority.key != &pool_authority_pda {
64+
return Err(AmmError::InvalidAuthority.into());
65+
}
66+
67+
// If the user specified amounts greater than held, use the total amounts they do have
68+
let trader_token_account_data_a = TokenAccount::unpack(&trader_account_a.data.borrow())?;
69+
let trader_token_account_data_b = TokenAccount::unpack(&trader_account_b.data.borrow())?;
70+
let input = if args.swap_a && args.input_amount > trader_token_account_data_a.amount {
71+
trader_token_account_data_a.amount
72+
} else if !args.swap_a && args.input_amount > trader_token_account_data_b.amount {
73+
trader_token_account_data_b.amount
74+
} else {
75+
args.input_amount
76+
};
77+
78+
// Apply trading fee, used to compute the output
79+
let amm_data = Amm::try_from_slice(&amm.data.borrow())?;
80+
let taxed_input = input - input * amm_data.fee as u64 / 10000;
81+
82+
let pool_token_account_data_a = TokenAccount::unpack(&pool_account_a.data.borrow())?;
83+
let pool_token_account_data_b = TokenAccount::unpack(&pool_account_b.data.borrow())?;
84+
let output = if args.swap_a {
85+
I64F64::from_num(taxed_input)
86+
.checked_mul(I64F64::from_num(pool_token_account_data_b.amount))
87+
.unwrap()
88+
.checked_div(
89+
I64F64::from_num(pool_token_account_data_a.amount)
90+
.checked_add(I64F64::from_num(taxed_input))
91+
.unwrap(),
92+
)
93+
.unwrap()
94+
} else {
95+
I64F64::from_num(taxed_input)
96+
.checked_mul(I64F64::from_num(pool_token_account_data_a.amount))
97+
.unwrap()
98+
.checked_div(
99+
I64F64::from_num(pool_token_account_data_b.amount)
100+
.checked_add(I64F64::from_num(taxed_input))
101+
.unwrap(),
102+
)
103+
.unwrap()
104+
}
105+
.to_num::<u64>();
106+
107+
if output < args.min_output_amount {
108+
return Err(AmmError::OutputTooSmall.into());
109+
}
110+
111+
// Compute the invariant before the trade
112+
let invariant = pool_token_account_data_a.amount * pool_token_account_data_b.amount;
113+
114+
// Transfer tokens to the pool
115+
if args.swap_a {
116+
invoke(
117+
&transfer(
118+
token_program.key,
119+
trader_account_a.key,
120+
pool_account_a.key,
121+
trader.key,
122+
&[],
123+
input,
124+
)?,
125+
&[
126+
trader_account_a.clone(),
127+
pool_account_a.clone(),
128+
trader.clone(),
129+
token_program.clone(),
130+
],
131+
)?;
132+
invoke_signed(
133+
&transfer(
134+
token_program.key,
135+
pool_account_b.key,
136+
trader_account_b.key,
137+
pool_authority.key,
138+
&[],
139+
output,
140+
)?,
141+
&[
142+
trader_account_b.clone(),
143+
pool_account_b.clone(),
144+
pool_authority.clone(),
145+
token_program.clone(),
146+
],
147+
&[&[
148+
Pool::AUTHORITY_PREFIX.as_ref(),
149+
amm.key.as_ref(),
150+
mint_a.key.as_ref(),
151+
mint_b.key.as_ref(),
152+
&[pool_authority_bump],
153+
]],
154+
)?;
155+
} else {
156+
invoke(
157+
&transfer(
158+
token_program.key,
159+
trader_account_b.key,
160+
pool_account_b.key,
161+
trader.key,
162+
&[],
163+
input,
164+
)?,
165+
&[
166+
trader_account_b.clone(),
167+
pool_account_b.clone(),
168+
trader.clone(),
169+
token_program.clone(),
170+
],
171+
)?;
172+
invoke_signed(
173+
&transfer(
174+
token_program.key,
175+
pool_account_a.key,
176+
trader_account_a.key,
177+
pool_authority.key,
178+
&[],
179+
output,
180+
)?,
181+
&[
182+
trader_account_a.clone(),
183+
pool_account_a.clone(),
184+
pool_authority.clone(),
185+
token_program.clone(),
186+
],
187+
&[&[
188+
Pool::AUTHORITY_PREFIX.as_ref(),
189+
amm.key.as_ref(),
190+
mint_a.key.as_ref(),
191+
mint_b.key.as_ref(),
192+
&[pool_authority_bump],
193+
]],
194+
)?;
195+
}
196+
197+
// Verify the invariant still holds
198+
// Reload accounts because of the CPIs
199+
// We tolerate if the new invariant is higher because it means a rounding error for LPs
200+
let pool_token_account_data_a = TokenAccount::unpack(&pool_account_a.data.borrow())?;
201+
let pool_token_account_data_b = TokenAccount::unpack(&pool_account_b.data.borrow())?;
202+
if invariant > pool_token_account_data_a.amount * pool_token_account_data_b.amount {
203+
return Err(AmmError::InvariantViolated.into());
204+
}
205+
206+
Ok(())
207+
}

tokens/token-swap/native/program/src/lib.rs

+5-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ use solana_program::{
99
};
1010

1111
use crate::instructions::{
12-
process_create_amm, process_create_pool, process_deposit_liquidity, AmmInstruction,
12+
process_create_amm, process_create_pool, process_deposit_liquidity,
13+
process_swap_exact_tokens_for_tokens, AmmInstruction,
1314
};
1415

1516
declare_id!("5tS77fBNSDtMSuyBfizp3bdBCcgmVPuLTKzYpZjgoMjq");
@@ -27,5 +28,8 @@ pub fn process_instruction(
2728
AmmInstruction::DepositLiquidity(args) => {
2829
process_deposit_liquidity(program_id, accounts, args)
2930
}
31+
AmmInstruction::SwapExactTokensForToken(args) => {
32+
process_swap_exact_tokens_for_tokens(program_id, accounts, args)
33+
}
3034
}
3135
}

tokens/token-swap/native/tests/deposit_liquidity.ts

+31-40
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { getAssociatedTokenAddressSync } from "@solana/spl-token";
77
import { createCreatePoolInstruction } from "./ts/instructions/create_pool";
88
import { createDepositLiquidityInstruction } from "./ts/instructions/deposit_liquidity";
99
import { expect } from "chai";
10+
import { getTokenBalance } from "./utils";
1011

1112

1213
describe('Deposit liquidity', async () => {
@@ -17,16 +18,6 @@ describe('Deposit liquidity', async () => {
1718
let depositorAccountLiquidity, depositorAccountA, depositorAccountB;
1819
const default_mint_amount = 100 * 10 ** 6;
1920
const minimum_liquidity = 100; // Matches rust constant
20-
const getTokenBalance = async (tokenAccount) => {
21-
const account = await client.getAccount(tokenAccount);
22-
if (!account) {
23-
throw new Error(`Account ${tokenAccount.toString()} not found`);
24-
}
25-
// Token account data has an 8-byte discriminator followed by mint, owner, amount
26-
// Amount is stored as a u64 at offset 64
27-
const balance = Buffer.from(account.data).readBigUInt64LE(64);
28-
return Number(balance);
29-
};
3021
beforeEach(async () => {
3122
programId = PublicKey.unique();
3223
context = await start([{ name: "token_swap_native", programId }], []);
@@ -84,11 +75,11 @@ describe('Deposit liquidity', async () => {
8475
tx.add(ix).sign(payer, admin);
8576
await client.processTransaction(tx);
8677

87-
const depositorLiquidityBalance = await getTokenBalance(depositorAccountLiquidity);
88-
const depositorABalance = await getTokenBalance(depositorAccountA);
89-
const depositorBBalance = await getTokenBalance(depositorAccountB);
90-
const poolABalance = await getTokenBalance(poolAccountA);
91-
const poolBBalance = await getTokenBalance(poolAccountB);
78+
const depositorLiquidityBalance = await getTokenBalance(client, depositorAccountLiquidity);
79+
const depositorABalance = await getTokenBalance(client, depositorAccountA);
80+
const depositorBBalance = await getTokenBalance(client, depositorAccountB);
81+
const poolABalance = await getTokenBalance(client, poolAccountA);
82+
const poolBBalance = await getTokenBalance(client, poolAccountB);
9283

9384
expect(depositorLiquidityBalance).to.equal(amount_a - minimum_liquidity);
9485
expect(depositorABalance).to.equal(default_mint_amount - amount_a);
@@ -105,11 +96,11 @@ describe('Deposit liquidity', async () => {
10596
tx2.add(ix2).sign(payer, admin);
10697
await client.processTransaction(tx2);
10798

108-
const depositorLiquidityBalance2 = await getTokenBalance(depositorAccountLiquidity);
109-
const depositorABalance2 = await getTokenBalance(depositorAccountA);
110-
const depositorBBalance2 = await getTokenBalance(depositorAccountB);
111-
const poolABalance2 = await getTokenBalance(poolAccountA);
112-
const poolBBalance2 = await getTokenBalance(poolAccountB);
99+
const depositorLiquidityBalance2 = await getTokenBalance(client, depositorAccountLiquidity);
100+
const depositorABalance2 = await getTokenBalance(client, depositorAccountA);
101+
const depositorBBalance2 = await getTokenBalance(client, depositorAccountB);
102+
const poolABalance2 = await getTokenBalance(client, poolAccountA);
103+
const poolBBalance2 = await getTokenBalance(client, poolAccountB);
113104

114105
expect(depositorLiquidityBalance2).to.equal(depositorLiquidityBalance + amount_a);
115106
expect(depositorABalance2).to.equal(depositorABalance - amount_a);
@@ -128,11 +119,11 @@ describe('Deposit liquidity', async () => {
128119
tx.add(ix).sign(payer, admin);
129120
await client.processTransaction(tx);
130121

131-
const depositorLiquidityBalance = await getTokenBalance(depositorAccountLiquidity);
132-
const depositorABalance = await getTokenBalance(depositorAccountA);
133-
const depositorBBalance = await getTokenBalance(depositorAccountB);
134-
const poolABalance = await getTokenBalance(poolAccountA);
135-
const poolBBalance = await getTokenBalance(poolAccountB);
122+
const depositorLiquidityBalance = await getTokenBalance(client, depositorAccountLiquidity);
123+
const depositorABalance = await getTokenBalance(client, depositorAccountA);
124+
const depositorBBalance = await getTokenBalance(client, depositorAccountB);
125+
const poolABalance = await getTokenBalance(client, poolAccountA);
126+
const poolBBalance = await getTokenBalance(client, poolAccountB);
136127

137128
expect(depositorLiquidityBalance).to.equal(6 * 10 ** 6 - minimum_liquidity);
138129
expect(depositorABalance).to.equal(default_mint_amount - amount_a);
@@ -155,11 +146,11 @@ describe('Deposit liquidity', async () => {
155146
tx2.add(ix2).sign(payer, admin);
156147
await client.processTransaction(tx2);
157148

158-
const depositorLiquidityBalance2 = await getTokenBalance(depositorAccountLiquidity);
159-
const depositorABalance2 = await getTokenBalance(depositorAccountA);
160-
const depositorBBalance2 = await getTokenBalance(depositorAccountB);
161-
const poolABalance2 = await getTokenBalance(poolAccountA);
162-
const poolBBalance2 = await getTokenBalance(poolAccountB);
149+
const depositorLiquidityBalance2 = await getTokenBalance(client, depositorAccountLiquidity);
150+
const depositorABalance2 = await getTokenBalance(client, depositorAccountA);
151+
const depositorBBalance2 = await getTokenBalance(client, depositorAccountB);
152+
const poolABalance2 = await getTokenBalance(client, poolAccountA);
153+
const poolBBalance2 = await getTokenBalance(client, poolAccountB);
163154

164155
expect(depositorLiquidityBalance2).to.equal(depositorLiquidityBalance + 40.5 * 10 ** 6);
165156
expect(depositorABalance2).to.equal(depositorABalance - 60.75 * 10 ** 6);
@@ -178,11 +169,11 @@ describe('Deposit liquidity', async () => {
178169
tx.add(ix).sign(payer, admin);
179170
await client.processTransaction(tx);
180171

181-
const depositorLiquidityBalance = await getTokenBalance(depositorAccountLiquidity);
182-
const depositorABalance = await getTokenBalance(depositorAccountA);
183-
const depositorBBalance = await getTokenBalance(depositorAccountB);
184-
const poolABalance = await getTokenBalance(poolAccountA);
185-
const poolBBalance = await getTokenBalance(poolAccountB);
172+
const depositorLiquidityBalance = await getTokenBalance(client, depositorAccountLiquidity);
173+
const depositorABalance = await getTokenBalance(client, depositorAccountA);
174+
const depositorBBalance = await getTokenBalance(client, depositorAccountB);
175+
const poolABalance = await getTokenBalance(client, poolAccountA);
176+
const poolBBalance = await getTokenBalance(client, poolAccountB);
186177

187178
expect(depositorLiquidityBalance).to.equal(6 * 10 ** 6 - minimum_liquidity);
188179
expect(depositorABalance).to.equal(default_mint_amount - amount_a);
@@ -205,11 +196,11 @@ describe('Deposit liquidity', async () => {
205196
tx2.add(ix2).sign(payer, admin);
206197
await client.processTransaction(tx2);
207198

208-
const depositorLiquidityBalance2 = await getTokenBalance(depositorAccountLiquidity);
209-
const depositorABalance2 = await getTokenBalance(depositorAccountA);
210-
const depositorBBalance2 = await getTokenBalance(depositorAccountB);
211-
const poolABalance2 = await getTokenBalance(poolAccountA);
212-
const poolBBalance2 = await getTokenBalance(poolAccountB);
199+
const depositorLiquidityBalance2 = await getTokenBalance(client, depositorAccountLiquidity);
200+
const depositorABalance2 = await getTokenBalance(client, depositorAccountA);
201+
const depositorBBalance2 = await getTokenBalance(client, depositorAccountB);
202+
const poolABalance2 = await getTokenBalance(client, poolAccountA);
203+
const poolBBalance2 = await getTokenBalance(client, poolAccountB);
213204

214205
expect(depositorLiquidityBalance2).to.equal(depositorLiquidityBalance + 40.5 * 10 ** 6);
215206
expect(depositorABalance2).to.equal(depositorABalance - amount_a2);

0 commit comments

Comments
 (0)