Skip to main content

sendfun_sdk/dex/
quote.rs

1use crate::math::amm::{self, AmmInput, BuyArgs, SellArgs};
2use crate::nexus::types::DexFees;
3use crate::utils::{
4	Landing, MarketQuote, MarketQuoteError, QuoteRequest, TradeDirection,
5	TradeMode,
6};
7
8/// Read `quote_reserves` and `created_at` from the `Pool` account, not from a
9/// vault.
10#[derive(Clone, Copy, Debug)]
11pub struct PoolMarket<'a> {
12	/// The base vault's token amount. The program prices from it, not from
13	/// `pool.base_reserves`.
14	pub base_vault_amount: u64,
15	pub quote_reserves: u64,
16	pub created_at: i64,
17	/// `PartnerConfig.dex` of the trade's partner on the pool's platform.
18	pub fees: &'a DexFees,
19	pub landing: Landing,
20}
21
22/// Calculates a trade on a DEX pool. The status is not checked: pass an
23/// `Active` pool.
24pub fn quote(
25	market: &PoolMarket<'_>,
26	request: QuoteRequest,
27) -> Result<MarketQuote, MarketQuoteError> {
28	let fee_bps = market
29		.fees
30		.effective_fee_bps(market.created_at, market.landing.unix_timestamp)
31		.ok_or(MarketQuoteError::FeeOutOfRange)?;
32	let amm = AmmInput {
33		quote_reserves: market.quote_reserves,
34		base_reserves: market.base_vault_amount,
35		amount: request.amount,
36		fee_bps,
37	};
38	let Landing {
39		base_fee,
40		quote_fee,
41		..
42	} = market.landing;
43
44	match request.direction {
45		TradeDirection::Buy => {
46			let args = BuyArgs {
47				amm,
48				quote_fee,
49				base_fee,
50				base_reserve_cap: None,
51			};
52			let bought = match request.mode {
53				TradeMode::ExactIn => amm::buy_exact_in_with_fees(args),
54				TradeMode::ExactOut => amm::buy_exact_out_with_fees(args),
55			}?;
56			Ok(MarketQuote::bought(&bought, request, fee_bps))
57		}
58		TradeDirection::Sell => {
59			let args = SellArgs {
60				amm,
61				quote_fee,
62				base_fee,
63			};
64			let sold = match request.mode {
65				TradeMode::ExactIn => amm::sell_exact_in_with_fees(args),
66				TradeMode::ExactOut => amm::sell_exact_out_with_fees(args),
67			}?;
68			Ok(MarketQuote::sold(&sold, fee_bps))
69		}
70	}
71}
72
73#[cfg(test)]
74mod tests {
75	use super::*;
76	use crate::math::amm::{BuyQuote, SellQuote};
77
78	const NOW: i64 = 1_000;
79	const BASE_VAULT: u64 = 400_000_000_000;
80	const QUOTE_RESERVES: u64 = 25_000_000_000;
81
82	const FEES: DexFees = DexFees {
83		creation_fee_cents: 0,
84		protocol_fee_bps: 80,
85		lp_fee_bps: 30,
86		creator_fee_bps: 40,
87		fee_decay_seconds: 0,
88		fee_decay_start_bps: 0,
89	};
90
91	const LANDING: Landing = Landing {
92		base_fee: None,
93		quote_fee: None,
94		unix_timestamp: NOW,
95	};
96
97	const MARKET: PoolMarket<'static> = PoolMarket {
98		base_vault_amount: BASE_VAULT,
99		quote_reserves: QUOTE_RESERVES,
100		created_at: 0,
101		fees: &FEES,
102		landing: LANDING,
103	};
104
105	const fn priced(amount: u64) -> AmmInput {
106		AmmInput {
107			quote_reserves: QUOTE_RESERVES,
108			base_reserves: BASE_VAULT,
109			amount,
110			fee_bps: 150,
111		}
112	}
113
114	fn bought(args: BuyArgs, mode: TradeMode) -> BuyQuote {
115		match mode {
116			TradeMode::ExactIn => amm::buy_exact_in_with_fees(args),
117			TradeMode::ExactOut => amm::buy_exact_out_with_fees(args),
118		}
119		.unwrap()
120	}
121
122	fn sold(args: SellArgs, mode: TradeMode) -> SellQuote {
123		match mode {
124			TradeMode::ExactIn => amm::sell_exact_in_with_fees(args),
125			TradeMode::ExactOut => amm::sell_exact_out_with_fees(args),
126		}
127		.unwrap()
128	}
129
130	#[test]
131	fn a_buy_prices_off_the_base_vault() {
132		let quote = quote(
133			&MARKET,
134			QuoteRequest {
135				direction: TradeDirection::Buy,
136				mode: TradeMode::ExactIn,
137				amount: 1_000_000_000,
138			},
139		)
140		.unwrap();
141		assert_eq!(quote.out_amount, 15_162_593_804);
142		assert_eq!(quote.fee, 15_000_000);
143		assert_eq!(quote.fee_bps, 150);
144		assert!(!quote.supply_capped);
145	}
146
147	#[test]
148	fn every_trade_matches_the_math_over_the_programs_inputs() {
149		let amount = 1_000_000_000;
150		for mode in [TradeMode::ExactIn, TradeMode::ExactOut] {
151			let buy = bought(
152				BuyArgs {
153					amm: priced(amount),
154					quote_fee: None,
155					base_fee: None,
156					base_reserve_cap: None,
157				},
158				mode,
159			);
160			assert_eq!(
161				quote(
162					&MARKET,
163					QuoteRequest {
164						direction: TradeDirection::Buy,
165						mode,
166						amount,
167					}
168				),
169				Ok(MarketQuote::bought(
170					&buy,
171					QuoteRequest {
172						direction: TradeDirection::Buy,
173						mode,
174						amount,
175					},
176					150
177				))
178			);
179
180			let sell = sold(
181				SellArgs {
182					amm: priced(amount),
183					quote_fee: None,
184					base_fee: None,
185				},
186				mode,
187			);
188			let quote = quote(
189				&MARKET,
190				QuoteRequest {
191					direction: TradeDirection::Sell,
192					mode,
193					amount,
194				},
195			)
196			.unwrap();
197			assert_eq!(quote, MarketQuote::sold(&sell, 150));
198			assert!(!quote.supply_capped);
199		}
200	}
201
202	#[test]
203	fn the_fee_decays_from_the_pools_created_at() {
204		let decaying = DexFees {
205			fee_decay_seconds: 12,
206			fee_decay_start_bps: 5_000,
207			..FEES
208		};
209		let fresh = quote(
210			&PoolMarket {
211				created_at: NOW,
212				fees: &decaying,
213				..MARKET
214			},
215			QuoteRequest {
216				direction: TradeDirection::Buy,
217				mode: TradeMode::ExactIn,
218				amount: 1_000_000_000,
219			},
220		)
221		.unwrap();
222		assert_eq!(fresh.fee_bps, 5_000);
223		assert_eq!(fresh.fee, 500_000_000);
224
225		assert_eq!(
226			quote(
227				&PoolMarket {
228					created_at: -1,
229					..MARKET
230				},
231				QuoteRequest {
232					direction: TradeDirection::Sell,
233					mode: TradeMode::ExactIn,
234					amount: 1_000,
235				}
236			),
237			Err(MarketQuoteError::FeeOutOfRange)
238		);
239	}
240}