Skip to main content

sendfun_sdk/launchpad/
quote.rs

1use crate::math::amm::{
2	self, AmmError, AmmInput, BuyArgs, QuoteError, SellArgs,
3};
4use crate::nexus::types::LaunchpadFees;
5use crate::utils::{
6	Landing, MarketQuote, MarketQuoteError, QuoteRequest, TradeDirection,
7	TradeMode,
8};
9
10#[derive(Clone, Copy, Debug)]
11pub struct CurveMarket<'a> {
12	pub virtual_base_reserves: u64,
13	pub virtual_quote_reserves: u64,
14	pub real_base_reserves: u64,
15	pub real_quote_reserves: u64,
16	pub created_at: i64,
17	/// `PartnerConfig.launchpad` of the trade's partner on the curve's
18	/// platform.
19	pub fees: &'a LaunchpadFees,
20	pub landing: Landing,
21}
22
23/// Calculates a trade on a bonding curve. The price comes from the virtual
24/// reserves. The status is not checked: pass a `Funding` curve.
25pub fn quote(
26	market: &CurveMarket<'_>,
27	request: QuoteRequest,
28) -> Result<MarketQuote, MarketQuoteError> {
29	// The program refuses every trade here with `ThresholdReached`.
30	if market.real_base_reserves == 0 {
31		return Err(MarketQuoteError::SupplyExhausted);
32	}
33	let fee_bps = market
34		.fees
35		.effective_fee_bps(market.created_at, market.landing.unix_timestamp)
36		.ok_or(MarketQuoteError::FeeOutOfRange)?;
37	let amm = AmmInput {
38		quote_reserves: market.virtual_quote_reserves,
39		base_reserves: market.virtual_base_reserves,
40		amount: request.amount,
41		fee_bps,
42	};
43	let Landing {
44		base_fee,
45		quote_fee,
46		..
47	} = market.landing;
48
49	match request.direction {
50		TradeDirection::Buy => {
51			let args = BuyArgs {
52				amm,
53				quote_fee,
54				base_fee,
55				base_reserve_cap: Some(market.real_base_reserves),
56			};
57			let bought = match request.mode {
58				TradeMode::ExactIn => amm::buy_exact_in_with_fees(args),
59				TradeMode::ExactOut => amm::buy_exact_out_with_fees(args),
60			}?;
61			Ok(MarketQuote::bought(&bought, request, fee_bps))
62		}
63		TradeDirection::Sell => {
64			let args = SellArgs {
65				amm,
66				quote_fee,
67				base_fee,
68			};
69			let sold = match request.mode {
70				TradeMode::ExactIn => amm::sell_exact_in_with_fees(args),
71				TradeMode::ExactOut => amm::sell_exact_out_with_fees(args),
72			}?;
73			// The virtual reserves can price more quote than the curve holds.
74			// The debit includes the fee.
75			let debit = sold
76				.quote_amount
77				.checked_add(sold.fee)
78				.ok_or(QuoteError::Amm(AmmError::Overflow))?;
79			if debit > market.real_quote_reserves {
80				return Err(MarketQuoteError::ExceedsQuoteHeld);
81			}
82			Ok(MarketQuote::sold(&sold, fee_bps))
83		}
84	}
85}
86
87#[cfg(test)]
88mod tests {
89	use super::*;
90	use crate::math::amm::MintFee;
91
92	const NOW: i64 = 1_000;
93	const ONE_SOL: u64 = 1_000_000_000;
94
95	const FEES: LaunchpadFees = LaunchpadFees {
96		creation_fee_cents: 0,
97		protocol_fee_bps: 100,
98		creator_fee_bps: 0,
99		fee_decay_seconds: 0,
100		fee_decay_start_bps: 0,
101	};
102
103	const LANDING: Landing = Landing {
104		base_fee: None,
105		quote_fee: None,
106		unix_timestamp: NOW,
107	};
108
109	const MARKET: CurveMarket<'static> = CurveMarket {
110		virtual_base_reserves: 1_000_000_000_000_000,
111		virtual_quote_reserves: 30_000_000_000,
112		real_base_reserves: 500_000_000_000_000,
113		real_quote_reserves: 0,
114		created_at: 0,
115		fees: &FEES,
116		landing: LANDING,
117	};
118
119	const BASE_100_BPS: MintFee = MintFee {
120		bps: 100,
121		maximum_fee: u64::MAX,
122	};
123
124	const fn request(
125		direction: TradeDirection,
126		mode: TradeMode,
127		amount: u64,
128	) -> QuoteRequest {
129		QuoteRequest {
130			direction,
131			mode,
132			amount,
133		}
134	}
135
136	#[test]
137	fn a_buy_prices_off_the_virtual_reserves() {
138		let quote = quote(
139			&MARKET,
140			request(TradeDirection::Buy, TradeMode::ExactIn, ONE_SOL),
141		)
142		.unwrap();
143		assert_eq!(quote.in_amount, ONE_SOL);
144		assert_eq!(quote.out_amount, 31_945_788_964_181);
145		assert_eq!(quote.fee, 10_000_000);
146		assert_eq!(quote.fee_bps, 100);
147		assert!(!quote.supply_capped);
148	}
149
150	#[test]
151	fn the_fee_decays_from_the_curves_creation() {
152		let decaying = LaunchpadFees {
153			fee_decay_seconds: 12,
154			fee_decay_start_bps: 5_000,
155			..FEES
156		};
157		let fresh = quote(
158			&CurveMarket {
159				created_at: NOW,
160				fees: &decaying,
161				..MARKET
162			},
163			request(TradeDirection::Buy, TradeMode::ExactIn, ONE_SOL),
164		)
165		.unwrap();
166		assert_eq!(fresh.fee_bps, 5_000);
167		assert_eq!(fresh.fee, 500_000_000);
168		assert_eq!(fresh.out_amount, 16_393_442_622_950);
169
170		assert_eq!(
171			quote(
172				&CurveMarket {
173					created_at: -1,
174					..MARKET
175				},
176				request(TradeDirection::Buy, TradeMode::ExactIn, ONE_SOL)
177			),
178			Err(MarketQuoteError::FeeOutOfRange)
179		);
180	}
181
182	#[test]
183	fn an_exact_in_buy_past_the_supply_left_fills_the_cap() {
184		let quote = quote(
185			&CurveMarket {
186				real_base_reserves: 1_000_000_000,
187				..MARKET
188			},
189			request(TradeDirection::Buy, TradeMode::ExactIn, ONE_SOL),
190		)
191		.unwrap();
192		assert_eq!(quote.out_amount, 1_000_000_000);
193		assert_eq!(quote.in_amount, 30_305);
194		assert!(quote.supply_capped);
195	}
196
197	/// With a base transfer fee, the cap applies to the base that leaves the
198	/// vault, not to the base that the buyer receives.
199	#[test]
200	fn an_exact_out_buy_past_the_supply_left_is_flagged() {
201		let wanted = 10_000_000_000_000;
202		let landing = Landing {
203			base_fee: Some(BASE_100_BPS),
204			..LANDING
205		};
206		let holding = |real_base_reserves| CurveMarket {
207			real_base_reserves,
208			landing,
209			..MARKET
210		};
211		let just_enough = quote(
212			&holding(10_101_010_101_011),
213			request(TradeDirection::Buy, TradeMode::ExactOut, wanted),
214		)
215		.unwrap();
216		assert_eq!(just_enough.out_amount, wanted);
217		assert!(!just_enough.supply_capped);
218
219		let one_short = quote(
220			&holding(10_101_010_101_010),
221			request(TradeDirection::Buy, TradeMode::ExactOut, wanted),
222		)
223		.unwrap();
224		assert_eq!(one_short.out_amount, 9_999_999_999_999);
225		assert!(one_short.supply_capped);
226	}
227
228	#[test]
229	fn a_sell_is_bounded_by_the_quote_the_curve_holds() {
230		let sell = request(
231			TradeDirection::Sell,
232			TradeMode::ExactIn,
233			10_000_000_000_000,
234		);
235		let sold = amm::sell_exact_in_with_fees(SellArgs {
236			amm: AmmInput {
237				quote_reserves: 30_000_000_000,
238				base_reserves: 1_000_000_000_000_000,
239				amount: 10_000_000_000_000,
240				fee_bps: 100,
241			},
242			quote_fee: None,
243			base_fee: None,
244		})
245		.unwrap();
246		let debit = sold.quote_amount + sold.fee;
247		assert_eq!(debit, 297_029_702);
248
249		let holding = |real_quote_reserves| CurveMarket {
250			real_quote_reserves,
251			..MARKET
252		};
253		let covered = quote(&holding(debit), sell).unwrap();
254		assert_eq!(covered.in_amount, 10_000_000_000_000);
255		assert_eq!(covered.out_amount, sold.quote_to_user);
256		assert!(!covered.supply_capped);
257		assert_eq!(
258			quote(&holding(debit - 1), sell),
259			Err(MarketQuoteError::ExceedsQuoteHeld)
260		);
261	}
262
263	#[test]
264	fn a_curve_with_no_supply_left_quotes_nothing() {
265		let complete = CurveMarket {
266			real_base_reserves: 0,
267			real_quote_reserves: 85_000_000_000,
268			..MARKET
269		};
270		for direction in [TradeDirection::Buy, TradeDirection::Sell] {
271			for mode in [TradeMode::ExactIn, TradeMode::ExactOut] {
272				assert_eq!(
273					quote(&complete, request(direction, mode, 1_000)),
274					Err(MarketQuoteError::SupplyExhausted)
275				);
276			}
277		}
278	}
279}