1use crate::math::amm::{
2 self, AmmError, AmmInput, BuyArgs, QuoteError, SellArgs,
3};
4use crate::nexus::EffectiveFeeArgs;
5use crate::nexus::types::LaunchpadFees;
6use crate::utils::{
7 Landing, MarketQuote, MarketQuoteError, QuoteRequest, TradeDirection,
8 TradeMode,
9};
10
11#[derive(Clone, Copy, Debug)]
12pub struct CurveMarket<'a> {
13 pub virtual_base_reserves: u64,
14 pub virtual_quote_reserves: u64,
15 pub real_base_reserves: u64,
16 pub real_quote_reserves: u64,
17 pub created_at: i64,
18 pub fees: &'a LaunchpadFees,
21 pub landing: Landing,
22 pub creator_fee_bps: u16,
25}
26
27pub fn quote(
30 market: &CurveMarket<'_>,
31 request: QuoteRequest,
32) -> Result<MarketQuote, MarketQuoteError> {
33 if market.real_base_reserves == 0 {
35 return Err(MarketQuoteError::SupplyExhausted);
36 }
37 let fee_bps = market
38 .fees
39 .effective_fee_bps(EffectiveFeeArgs {
40 creator_fee_bps: market.creator_fee_bps,
41 created_at: market.created_at,
42 now: market.landing.unix_timestamp,
43 })
44 .ok_or(MarketQuoteError::FeeOutOfRange)?;
45 let amm = AmmInput {
46 quote_reserves: market.virtual_quote_reserves,
47 base_reserves: market.virtual_base_reserves,
48 amount: request.amount,
49 fee_bps,
50 };
51 let Landing {
52 base_fee,
53 quote_fee,
54 ..
55 } = market.landing;
56
57 match request.direction {
58 TradeDirection::Buy => {
59 let args = BuyArgs {
60 amm,
61 quote_fee,
62 base_fee,
63 base_reserve_cap: Some(market.real_base_reserves),
64 };
65 let bought = match request.mode {
66 TradeMode::ExactIn => amm::buy_exact_in_with_fees(args),
67 TradeMode::ExactOut => amm::buy_exact_out_with_fees(args),
68 }?;
69 Ok(MarketQuote::bought(&bought, request, fee_bps))
70 }
71 TradeDirection::Sell => {
72 let args = SellArgs {
73 amm,
74 quote_fee,
75 base_fee,
76 };
77 let sold = match request.mode {
78 TradeMode::ExactIn => amm::sell_exact_in_with_fees(args),
79 TradeMode::ExactOut => amm::sell_exact_out_with_fees(args),
80 }?;
81 let debit = sold
84 .quote_amount
85 .checked_add(sold.fee)
86 .ok_or(QuoteError::Amm(AmmError::Overflow))?;
87 if debit > market.real_quote_reserves {
88 return Err(MarketQuoteError::ExceedsQuoteHeld);
89 }
90 Ok(MarketQuote::sold(&sold, fee_bps))
91 }
92 }
93}
94
95#[cfg(test)]
96mod tests {
97 use super::*;
98 use crate::math::amm::MintFee;
99
100 const NOW: i64 = 1_000;
101 const ONE_SOL: u64 = 1_000_000_000;
102
103 const FEES: LaunchpadFees = LaunchpadFees {
105 creation_fee_cents: 0,
106 protocol_fee_bps: 100,
107 max_creator_fee_bps: 50,
108 fee_decay_seconds: 0,
109 fee_decay_start_bps: 0,
110 };
111
112 const LANDING: Landing = Landing {
113 base_fee: None,
114 quote_fee: None,
115 unix_timestamp: NOW,
116 };
117
118 const MARKET: CurveMarket<'static> = CurveMarket {
119 virtual_base_reserves: 1_000_000_000_000_000,
120 virtual_quote_reserves: 30_000_000_000,
121 real_base_reserves: 500_000_000_000_000,
122 real_quote_reserves: 0,
123 created_at: 0,
124 fees: &FEES,
125 landing: LANDING,
126 creator_fee_bps: 0,
127 };
128
129 const BASE_100_BPS: MintFee = MintFee {
130 bps: 100,
131 maximum_fee: u64::MAX,
132 };
133
134 const fn request(
135 direction: TradeDirection,
136 mode: TradeMode,
137 amount: u64,
138 ) -> QuoteRequest {
139 QuoteRequest {
140 direction,
141 mode,
142 amount,
143 }
144 }
145
146 #[test]
147 fn a_buy_prices_off_the_virtual_reserves() {
148 let quote = quote(
149 &MARKET,
150 request(TradeDirection::Buy, TradeMode::ExactIn, ONE_SOL),
151 )
152 .unwrap();
153 assert_eq!(quote.in_amount, ONE_SOL);
154 assert_eq!(quote.out_amount, 31_945_788_964_181);
155 assert_eq!(quote.fee, 10_000_000);
156 assert_eq!(quote.fee_bps, 100);
157 assert!(!quote.supply_capped);
158 }
159
160 #[test]
161 fn the_fee_decays_from_the_curves_creation() {
162 let decaying = LaunchpadFees {
163 fee_decay_seconds: 12,
164 fee_decay_start_bps: 5_000,
165 ..FEES
166 };
167 let fresh = quote(
168 &CurveMarket {
169 created_at: NOW,
170 fees: &decaying,
171 ..MARKET
172 },
173 request(TradeDirection::Buy, TradeMode::ExactIn, ONE_SOL),
174 )
175 .unwrap();
176 assert_eq!(fresh.fee_bps, 5_000);
177 assert_eq!(fresh.fee, 500_000_000);
178 assert_eq!(fresh.out_amount, 16_393_442_622_950);
179
180 assert_eq!(
181 quote(
182 &CurveMarket {
183 created_at: -1,
184 ..MARKET
185 },
186 request(TradeDirection::Buy, TradeMode::ExactIn, ONE_SOL)
187 ),
188 Err(MarketQuoteError::FeeOutOfRange)
189 );
190 }
191
192 #[test]
195 fn the_curves_creator_rate_prices_the_trade_not_the_partner_max() {
196 let decaying = LaunchpadFees {
197 fee_decay_seconds: 12,
198 fee_decay_start_bps: 5_000,
199 ..FEES
200 };
201 let buy = request(TradeDirection::Buy, TradeMode::ExactIn, ONE_SOL);
202 for (creator_fee_bps, standard, halfway) in [
205 (
206 0,
207 (100, 10_000_000, 31_945_788_964_181),
208 (1_325, 132_500_000, 28_103_992_872_762),
209 ),
210 (
211 30,
212 (130, 13_000_000, 31_852_066_995_836),
213 (1_348, 134_800_000, 28_031_569_534_621),
214 ),
215 (
216 80,
217 (180, 18_000_000, 31_695_823_381_318),
218 (1_385, 138_500_000, 27_915_039_774_476),
219 ),
220 ] {
221 let market = CurveMarket {
222 creator_fee_bps,
223 fees: &decaying,
224 ..MARKET
225 };
226 let past = quote(&market, buy).unwrap();
227 assert_eq!((past.fee_bps, past.fee, past.out_amount), standard);
228
229 let inside = quote(
230 &CurveMarket {
231 created_at: NOW - 6,
232 ..market
233 },
234 buy,
235 )
236 .unwrap();
237 assert_eq!(
238 (inside.fee_bps, inside.fee, inside.out_amount),
239 halfway
240 );
241 }
242 }
243
244 #[test]
245 fn an_exact_in_buy_past_the_supply_left_fills_the_cap() {
246 let quote = quote(
247 &CurveMarket {
248 real_base_reserves: 1_000_000_000,
249 ..MARKET
250 },
251 request(TradeDirection::Buy, TradeMode::ExactIn, ONE_SOL),
252 )
253 .unwrap();
254 assert_eq!(quote.out_amount, 1_000_000_000);
255 assert_eq!(quote.in_amount, 30_305);
256 assert!(quote.supply_capped);
257 }
258
259 #[test]
262 fn an_exact_out_buy_past_the_supply_left_is_flagged() {
263 let wanted = 10_000_000_000_000;
264 let landing = Landing {
265 base_fee: Some(BASE_100_BPS),
266 ..LANDING
267 };
268 let holding = |real_base_reserves| CurveMarket {
269 real_base_reserves,
270 landing,
271 ..MARKET
272 };
273 let just_enough = quote(
274 &holding(10_101_010_101_011),
275 request(TradeDirection::Buy, TradeMode::ExactOut, wanted),
276 )
277 .unwrap();
278 assert_eq!(just_enough.out_amount, wanted);
279 assert!(!just_enough.supply_capped);
280
281 let one_short = quote(
282 &holding(10_101_010_101_010),
283 request(TradeDirection::Buy, TradeMode::ExactOut, wanted),
284 )
285 .unwrap();
286 assert_eq!(one_short.out_amount, 9_999_999_999_999);
287 assert!(one_short.supply_capped);
288 }
289
290 #[test]
291 fn a_sell_is_bounded_by_the_quote_the_curve_holds() {
292 let sell = request(
293 TradeDirection::Sell,
294 TradeMode::ExactIn,
295 10_000_000_000_000,
296 );
297 let sold = amm::sell_exact_in_with_fees(SellArgs {
298 amm: AmmInput {
299 quote_reserves: 30_000_000_000,
300 base_reserves: 1_000_000_000_000_000,
301 amount: 10_000_000_000_000,
302 fee_bps: 100,
303 },
304 quote_fee: None,
305 base_fee: None,
306 })
307 .unwrap();
308 let debit = sold.quote_amount + sold.fee;
309 assert_eq!(debit, 297_029_702);
310
311 let holding = |real_quote_reserves| CurveMarket {
312 real_quote_reserves,
313 ..MARKET
314 };
315 let covered = quote(&holding(debit), sell).unwrap();
316 assert_eq!(covered.in_amount, 10_000_000_000_000);
317 assert_eq!(covered.out_amount, sold.quote_to_user);
318 assert!(!covered.supply_capped);
319 assert_eq!(
320 quote(&holding(debit - 1), sell),
321 Err(MarketQuoteError::ExceedsQuoteHeld)
322 );
323 }
324
325 #[test]
326 fn a_curve_with_no_supply_left_quotes_nothing() {
327 let complete = CurveMarket {
328 real_base_reserves: 0,
329 real_quote_reserves: 85_000_000_000,
330 ..MARKET
331 };
332 for direction in [TradeDirection::Buy, TradeDirection::Sell] {
333 for mode in [TradeMode::ExactIn, TradeMode::ExactOut] {
334 assert_eq!(
335 quote(&complete, request(direction, mode, 1_000)),
336 Err(MarketQuoteError::SupplyExhausted)
337 );
338 }
339 }
340 }
341}