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 pub fees: &'a LaunchpadFees,
20 pub landing: Landing,
21}
22
23pub fn quote(
26 market: &CurveMarket<'_>,
27 request: QuoteRequest,
28) -> Result<MarketQuote, MarketQuoteError> {
29 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 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 #[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}