Skip to main content

sendfun_sdk/dex/
quote.rs

1use crate::math::amm::{self, AmmInput, BuyArgs, SellArgs};
2use crate::nexus::EffectiveFeeArgs;
3use crate::nexus::types::DexFees;
4use crate::utils::{
5	Landing, MarketQuote, MarketQuoteError, QuoteRequest, TradeDirection,
6	TradeMode,
7};
8
9/// Read `quote_reserves`, `created_at` and `creator_fee_bps` from the `Pool`
10/// account, not from a vault.
11#[derive(Clone, Copy, Debug)]
12pub struct PoolMarket<'a> {
13	/// The base vault's token amount. The program prices from it, not from
14	/// `pool.base_reserves`.
15	pub base_vault_amount: u64,
16	pub quote_reserves: u64,
17	pub created_at: i64,
18	/// `PartnerConfig.dex` of the trade's partner on the pool's platform. It
19	/// sets the protocol rate, the LP rate and the decay.
20	pub fees: &'a DexFees,
21	pub landing: Landing,
22	/// `Pool.creator_fee_bps`, not the partner's `max_creator_fee_bps`.
23	pub creator_fee_bps: u16,
24}
25
26/// Calculates a trade on a DEX pool. The status is not checked: pass an
27/// `Active` pool.
28pub fn quote(
29	market: &PoolMarket<'_>,
30	request: QuoteRequest,
31) -> Result<MarketQuote, MarketQuoteError> {
32	let fee_bps = market
33		.fees
34		.effective_fee_bps(EffectiveFeeArgs {
35			creator_fee_bps: market.creator_fee_bps,
36			created_at: market.created_at,
37			now: market.landing.unix_timestamp,
38		})
39		.ok_or(MarketQuoteError::FeeOutOfRange)?;
40	let amm = AmmInput {
41		quote_reserves: market.quote_reserves,
42		base_reserves: market.base_vault_amount,
43		amount: request.amount,
44		fee_bps,
45	};
46	let Landing {
47		base_fee,
48		quote_fee,
49		..
50	} = market.landing;
51
52	match request.direction {
53		TradeDirection::Buy => {
54			let args = BuyArgs {
55				amm,
56				quote_fee,
57				base_fee,
58				base_reserve_cap: None,
59			};
60			let bought = match request.mode {
61				TradeMode::ExactIn => amm::buy_exact_in_with_fees(args),
62				TradeMode::ExactOut => amm::buy_exact_out_with_fees(args),
63			}?;
64			Ok(MarketQuote::bought(&bought, request, fee_bps))
65		}
66		TradeDirection::Sell => {
67			let args = SellArgs {
68				amm,
69				quote_fee,
70				base_fee,
71			};
72			let sold = match request.mode {
73				TradeMode::ExactIn => amm::sell_exact_in_with_fees(args),
74				TradeMode::ExactOut => amm::sell_exact_out_with_fees(args),
75			}?;
76			Ok(MarketQuote::sold(&sold, fee_bps))
77		}
78	}
79}
80
81#[cfg(test)]
82mod tests {
83	use super::*;
84	use crate::math::amm::{BuyQuote, SellQuote};
85
86	const NOW: i64 = 1_000;
87	const BASE_VAULT: u64 = 400_000_000_000;
88	const QUOTE_RESERVES: u64 = 25_000_000_000;
89
90	/// The partner max differs from each pool's rate in the tests.
91	const FEES: DexFees = DexFees {
92		creation_fee_cents: 0,
93		protocol_fee_bps: 80,
94		lp_fee_bps: 30,
95		max_creator_fee_bps: 100,
96		fee_decay_seconds: 0,
97		fee_decay_start_bps: 0,
98	};
99
100	const LANDING: Landing = Landing {
101		base_fee: None,
102		quote_fee: None,
103		unix_timestamp: NOW,
104	};
105
106	const MARKET: PoolMarket<'static> = PoolMarket {
107		base_vault_amount: BASE_VAULT,
108		quote_reserves: QUOTE_RESERVES,
109		created_at: 0,
110		fees: &FEES,
111		landing: LANDING,
112		creator_fee_bps: 40,
113	};
114
115	const fn priced(amount: u64) -> AmmInput {
116		AmmInput {
117			quote_reserves: QUOTE_RESERVES,
118			base_reserves: BASE_VAULT,
119			amount,
120			fee_bps: 150,
121		}
122	}
123
124	fn bought(args: BuyArgs, mode: TradeMode) -> BuyQuote {
125		match mode {
126			TradeMode::ExactIn => amm::buy_exact_in_with_fees(args),
127			TradeMode::ExactOut => amm::buy_exact_out_with_fees(args),
128		}
129		.unwrap()
130	}
131
132	fn sold(args: SellArgs, mode: TradeMode) -> SellQuote {
133		match mode {
134			TradeMode::ExactIn => amm::sell_exact_in_with_fees(args),
135			TradeMode::ExactOut => amm::sell_exact_out_with_fees(args),
136		}
137		.unwrap()
138	}
139
140	#[test]
141	fn a_buy_prices_off_the_base_vault() {
142		let quote = quote(
143			&MARKET,
144			QuoteRequest {
145				direction: TradeDirection::Buy,
146				mode: TradeMode::ExactIn,
147				amount: 1_000_000_000,
148			},
149		)
150		.unwrap();
151		assert_eq!(quote.out_amount, 15_162_593_804);
152		assert_eq!(quote.fee, 15_000_000);
153		assert_eq!(quote.fee_bps, 150);
154		assert!(!quote.supply_capped);
155	}
156
157	#[test]
158	fn every_trade_matches_the_math_over_the_programs_inputs() {
159		let amount = 1_000_000_000;
160		for mode in [TradeMode::ExactIn, TradeMode::ExactOut] {
161			let buy = bought(
162				BuyArgs {
163					amm: priced(amount),
164					quote_fee: None,
165					base_fee: None,
166					base_reserve_cap: None,
167				},
168				mode,
169			);
170			assert_eq!(
171				quote(
172					&MARKET,
173					QuoteRequest {
174						direction: TradeDirection::Buy,
175						mode,
176						amount,
177					}
178				),
179				Ok(MarketQuote::bought(
180					&buy,
181					QuoteRequest {
182						direction: TradeDirection::Buy,
183						mode,
184						amount,
185					},
186					150
187				))
188			);
189
190			let sell = sold(
191				SellArgs {
192					amm: priced(amount),
193					quote_fee: None,
194					base_fee: None,
195				},
196				mode,
197			);
198			let quote = quote(
199				&MARKET,
200				QuoteRequest {
201					direction: TradeDirection::Sell,
202					mode,
203					amount,
204				},
205			)
206			.unwrap();
207			assert_eq!(quote, MarketQuote::sold(&sell, 150));
208			assert!(!quote.supply_capped);
209		}
210	}
211
212	/// A pool's rate can be above the partner max. A partner can lower its max
213	/// after the pool's creation.
214	#[test]
215	fn the_pools_creator_rate_prices_the_trade_not_the_partner_max() {
216		let decaying = DexFees {
217			fee_decay_seconds: 12,
218			fee_decay_start_bps: 5_000,
219			..FEES
220		};
221		let buy = QuoteRequest {
222			direction: TradeDirection::Buy,
223			mode: TradeMode::ExactIn,
224			amount: 1_000_000_000,
225		};
226		// `(fee_bps, fee, out_amount)` past the decay window, then halfway
227		// through it.
228		for (creator_fee_bps, standard, halfway) in [
229			(
230				0,
231				(110, 11_000_000, 15_221_824_618),
232				(1_333, 133_300_000, 13_402_560_048),
233			),
234			(
235				40,
236				(150, 15_000_000, 15_162_593_804),
237				(1_363, 136_300_000, 13_357_717_573),
238			),
239			(
240				160,
241				(270, 27_000_000, 14_984_791_899),
242				(1_453, 145_300_000, 13_223_127_709),
243			),
244		] {
245			let market = PoolMarket {
246				creator_fee_bps,
247				fees: &decaying,
248				..MARKET
249			};
250			let past = quote(&market, buy).unwrap();
251			assert_eq!((past.fee_bps, past.fee, past.out_amount), standard);
252
253			let inside = quote(
254				&PoolMarket {
255					created_at: NOW - 6,
256					..market
257				},
258				buy,
259			)
260			.unwrap();
261			assert_eq!(
262				(inside.fee_bps, inside.fee, inside.out_amount),
263				halfway
264			);
265		}
266	}
267
268	#[test]
269	fn the_fee_decays_from_the_pools_created_at() {
270		let decaying = DexFees {
271			fee_decay_seconds: 12,
272			fee_decay_start_bps: 5_000,
273			..FEES
274		};
275		let fresh = quote(
276			&PoolMarket {
277				created_at: NOW,
278				fees: &decaying,
279				..MARKET
280			},
281			QuoteRequest {
282				direction: TradeDirection::Buy,
283				mode: TradeMode::ExactIn,
284				amount: 1_000_000_000,
285			},
286		)
287		.unwrap();
288		assert_eq!(fresh.fee_bps, 5_000);
289		assert_eq!(fresh.fee, 500_000_000);
290
291		assert_eq!(
292			quote(
293				&PoolMarket {
294					created_at: -1,
295					..MARKET
296				},
297				QuoteRequest {
298					direction: TradeDirection::Sell,
299					mode: TradeMode::ExactIn,
300					amount: 1_000,
301				}
302			),
303			Err(MarketQuoteError::FeeOutOfRange)
304		);
305	}
306}