Skip to main content

tempo_precompiles/stablecoin_dex/
dispatch.rs

1//! ABI dispatch for the [`StablecoinDEX`] precompile.
2
3use alloy::{primitives::Address, sol_types::SolInterface};
4use revm::precompile::{PrecompileError, PrecompileResult};
5use tempo_contracts::precompiles::IStablecoinDEX::IStablecoinDEXCalls;
6
7use crate::{
8    Precompile, dispatch_call, input_cost, mutate, mutate_void,
9    stablecoin_dex::{StablecoinDEX, orderbook::compute_book_key},
10    view,
11};
12
13impl Precompile for StablecoinDEX {
14    fn call(&mut self, calldata: &[u8], msg_sender: Address) -> PrecompileResult {
15        self.storage
16            .deduct_gas(input_cost(calldata.len()))
17            .map_err(|_| PrecompileError::OutOfGas)?;
18
19        dispatch_call(
20            calldata,
21            IStablecoinDEXCalls::abi_decode,
22            |call| match call {
23                IStablecoinDEXCalls::place(call) => mutate(call, msg_sender, |s, c| {
24                    self.place(s, c.token, c.amount, c.isBid, c.tick)
25                }),
26                IStablecoinDEXCalls::placeFlip(call) => mutate(call, msg_sender, |s, c| {
27                    self.place_flip(s, c.token, c.amount, c.isBid, c.tick, c.flipTick, false)
28                }),
29                IStablecoinDEXCalls::balanceOf(call) => {
30                    view(call, |c| self.balance_of(c.user, c.token))
31                }
32                IStablecoinDEXCalls::getOrder(call) => view(call, |c| {
33                    self.get_order(c.orderId).map(|order| order.into())
34                }),
35                IStablecoinDEXCalls::getTickLevel(call) => view(call, |c| {
36                    let level = self.get_price_level(c.base, c.tick, c.isBid)?;
37                    Ok((level.head, level.tail, level.total_liquidity).into())
38                }),
39                IStablecoinDEXCalls::pairKey(call) => {
40                    view(call, |c| Ok(compute_book_key(c.tokenA, c.tokenB)))
41                }
42                IStablecoinDEXCalls::books(call) => {
43                    view(call, |c| self.books(c.pairKey).map(Into::into))
44                }
45                IStablecoinDEXCalls::nextOrderId(call) => view(call, |_| self.next_order_id()),
46                IStablecoinDEXCalls::createPair(call) => {
47                    mutate(call, msg_sender, |_, c| self.create_pair(c.base))
48                }
49                IStablecoinDEXCalls::withdraw(call) => {
50                    mutate_void(call, msg_sender, |s, c| self.withdraw(s, c.token, c.amount))
51                }
52                IStablecoinDEXCalls::cancel(call) => {
53                    mutate_void(call, msg_sender, |s, c| self.cancel(s, c.orderId))
54                }
55                IStablecoinDEXCalls::cancelStaleOrder(call) => {
56                    mutate_void(call, msg_sender, |_, c| self.cancel_stale_order(c.orderId))
57                }
58                IStablecoinDEXCalls::swapExactAmountIn(call) => mutate(call, msg_sender, |s, c| {
59                    self.swap_exact_amount_in(s, c.tokenIn, c.tokenOut, c.amountIn, c.minAmountOut)
60                }),
61                IStablecoinDEXCalls::swapExactAmountOut(call) => {
62                    mutate(call, msg_sender, |s, c| {
63                        self.swap_exact_amount_out(
64                            s,
65                            c.tokenIn,
66                            c.tokenOut,
67                            c.amountOut,
68                            c.maxAmountIn,
69                        )
70                    })
71                }
72                IStablecoinDEXCalls::quoteSwapExactAmountIn(call) => view(call, |c| {
73                    self.quote_swap_exact_amount_in(c.tokenIn, c.tokenOut, c.amountIn)
74                }),
75                IStablecoinDEXCalls::quoteSwapExactAmountOut(call) => view(call, |c| {
76                    self.quote_swap_exact_amount_out(c.tokenIn, c.tokenOut, c.amountOut)
77                }),
78                IStablecoinDEXCalls::MIN_TICK(call) => {
79                    view(call, |_| Ok(crate::stablecoin_dex::MIN_TICK))
80                }
81                IStablecoinDEXCalls::MAX_TICK(call) => {
82                    view(call, |_| Ok(crate::stablecoin_dex::MAX_TICK))
83                }
84                IStablecoinDEXCalls::TICK_SPACING(call) => {
85                    view(call, |_| Ok(crate::stablecoin_dex::TICK_SPACING))
86                }
87                IStablecoinDEXCalls::PRICE_SCALE(call) => {
88                    view(call, |_| Ok(crate::stablecoin_dex::PRICE_SCALE))
89                }
90                IStablecoinDEXCalls::MIN_ORDER_AMOUNT(call) => {
91                    view(call, |_| Ok(crate::stablecoin_dex::MIN_ORDER_AMOUNT))
92                }
93                IStablecoinDEXCalls::MIN_PRICE(call) => view(call, |_| Ok(self.min_price())),
94                IStablecoinDEXCalls::MAX_PRICE(call) => view(call, |_| Ok(self.max_price())),
95                IStablecoinDEXCalls::tickToPrice(call) => {
96                    view(call, |c| self.tick_to_price(c.tick))
97                }
98                IStablecoinDEXCalls::priceToTick(call) => {
99                    view(call, |c| self.price_to_tick(c.price))
100                }
101            },
102        )
103    }
104}
105
106#[cfg(test)]
107mod tests {
108
109    use crate::{
110        Precompile,
111        stablecoin_dex::{IStablecoinDEX, MIN_ORDER_AMOUNT, StablecoinDEX},
112        storage::{ContractStorage, StorageCtx, hashmap::HashMapStorageProvider},
113        test_util::{TIP20Setup, assert_full_coverage, check_selector_coverage},
114    };
115    use alloy::{
116        primitives::{Address, U256},
117        sol_types::{SolCall, SolValue},
118    };
119    use tempo_contracts::precompiles::IStablecoinDEX::IStablecoinDEXCalls;
120
121    /// Setup a basic exchange with tokens and liquidity for swap tests
122    fn setup_exchange_with_liquidity() -> eyre::Result<(StablecoinDEX, Address, Address, Address)> {
123        let mut exchange = StablecoinDEX::new();
124        exchange.initialize()?;
125
126        let admin = Address::random();
127        let user = Address::random();
128        let amount = 200_000_000u128;
129
130        // Initialize quote token (pathUSD)
131        let quote = TIP20Setup::path_usd(admin)
132            .with_issuer(admin)
133            .with_mint(user, U256::from(amount))
134            .with_approval(user, exchange.address, U256::from(amount))
135            .apply()?;
136
137        let base = TIP20Setup::create("USDC", "USDC", admin)
138            .with_issuer(admin)
139            .with_mint(user, U256::from(amount))
140            .with_approval(user, exchange.address, U256::from(amount))
141            .apply()?;
142
143        // Create pair and add liquidity
144        exchange.create_pair(base.address())?;
145
146        // Place an order to provide liquidity
147        exchange.place(user, base.address(), MIN_ORDER_AMOUNT, true, 0)?;
148
149        Ok((exchange, base.address(), quote.address(), user))
150    }
151
152    #[test]
153    fn test_place_call() -> eyre::Result<()> {
154        let mut storage = HashMapStorageProvider::new(1);
155        StorageCtx::enter(&mut storage, || {
156            let mut exchange = StablecoinDEX::new();
157            exchange.initialize()?;
158
159            let sender = Address::random();
160            let token = Address::random();
161
162            let call = IStablecoinDEX::placeCall {
163                token,
164                amount: 100u128,
165                isBid: true,
166                tick: 0,
167            };
168            let calldata = call.abi_encode();
169
170            // Should dispatch to place function (may fail due to business logic, but dispatch works)
171            let result = exchange.call(&calldata, sender);
172            // Ok indicates successful dispatch (either success or TempoPrecompileError)
173            assert!(result.is_ok());
174
175            Ok(())
176        })
177    }
178
179    #[test]
180    fn test_place_flip_call() -> eyre::Result<()> {
181        let mut storage = HashMapStorageProvider::new(1);
182        StorageCtx::enter(&mut storage, || {
183            let mut exchange = StablecoinDEX::new();
184            exchange.initialize()?;
185
186            let sender = Address::random();
187            let token = Address::random();
188
189            let call = IStablecoinDEX::placeFlipCall {
190                token,
191                amount: 100u128,
192                isBid: true,
193                tick: 0,
194                flipTick: 10,
195            };
196            let calldata = call.abi_encode();
197
198            // Should dispatch to place_flip function
199            let result = exchange.call(&calldata, sender);
200            // Ok indicates successful dispatch (either success or TempoPrecompileError)
201            assert!(result.is_ok());
202
203            Ok(())
204        })
205    }
206
207    #[test]
208    fn test_balance_of_call() -> eyre::Result<()> {
209        let mut storage = HashMapStorageProvider::new(1);
210        StorageCtx::enter(&mut storage, || {
211            let mut exchange = StablecoinDEX::new();
212            exchange.initialize()?;
213
214            let sender = Address::random();
215            let token = Address::random();
216            let user = Address::random();
217
218            let call = IStablecoinDEX::balanceOfCall { user, token };
219            let calldata = call.abi_encode();
220
221            // Should dispatch to balance_of function and succeed (returns 0 for uninitialized)
222            let result = exchange.call(&calldata, sender);
223            assert!(result.is_ok());
224
225            Ok(())
226        })
227    }
228
229    #[test]
230    fn test_min_price() -> eyre::Result<()> {
231        let mut storage = HashMapStorageProvider::new(1);
232        StorageCtx::enter(&mut storage, || {
233            let mut exchange = StablecoinDEX::new();
234            exchange.initialize()?;
235
236            let sender = Address::ZERO;
237            let call = IStablecoinDEX::MIN_PRICECall {};
238            let calldata = call.abi_encode();
239
240            let result = exchange.call(&calldata, sender);
241            assert!(result.is_ok());
242
243            let output = result?.bytes;
244            let returned_value = u32::abi_decode(&output)?;
245
246            assert_eq!(returned_value, 98_000, "MIN_PRICE should be 98_000");
247            Ok(())
248        })
249    }
250
251    #[test]
252    fn test_tick_spacing() -> eyre::Result<()> {
253        let mut storage = HashMapStorageProvider::new(1);
254        StorageCtx::enter(&mut storage, || {
255            let mut exchange = StablecoinDEX::new();
256            exchange.initialize()?;
257
258            let sender = Address::ZERO;
259            let call = IStablecoinDEX::TICK_SPACINGCall {};
260            let calldata = call.abi_encode();
261
262            let result = exchange.call(&calldata, sender);
263            assert!(result.is_ok());
264
265            let output = result?.bytes;
266            let returned_value = i16::abi_decode(&output)?;
267
268            let expected = crate::stablecoin_dex::TICK_SPACING;
269            assert_eq!(
270                returned_value, expected,
271                "TICK_SPACING should be {expected}"
272            );
273            Ok(())
274        })
275    }
276
277    #[test]
278    fn test_max_price() -> eyre::Result<()> {
279        let mut storage = HashMapStorageProvider::new(1);
280        StorageCtx::enter(&mut storage, || {
281            let mut exchange = StablecoinDEX::new();
282            exchange.initialize()?;
283
284            let sender = Address::ZERO;
285            let call = IStablecoinDEX::MAX_PRICECall {};
286            let calldata = call.abi_encode();
287
288            let result = exchange.call(&calldata, sender);
289            assert!(result.is_ok());
290
291            let output = result?.bytes;
292            let returned_value = u32::abi_decode(&output)?;
293
294            assert_eq!(returned_value, 102_000, "MAX_PRICE should be 102_000");
295            Ok(())
296        })
297    }
298
299    #[test]
300    fn test_create_pair_call() -> eyre::Result<()> {
301        let mut storage = HashMapStorageProvider::new(1);
302        StorageCtx::enter(&mut storage, || {
303            let mut exchange = StablecoinDEX::new();
304            exchange.initialize()?;
305
306            let sender = Address::random();
307            let base = Address::from([2u8; 20]);
308
309            let call = IStablecoinDEX::createPairCall { base };
310            let calldata = call.abi_encode();
311
312            // Should dispatch to create_pair function
313            let result = exchange.call(&calldata, sender);
314            // Ok indicates successful dispatch (either success or TempoPrecompileError)
315            assert!(result.is_ok());
316            Ok(())
317        })
318    }
319
320    #[test]
321    fn test_withdraw_call() -> eyre::Result<()> {
322        let mut storage = HashMapStorageProvider::new(1);
323        StorageCtx::enter(&mut storage, || {
324            let mut exchange = StablecoinDEX::new();
325            exchange.initialize()?;
326
327            let sender = Address::random();
328            let token = Address::random();
329
330            let call = IStablecoinDEX::withdrawCall {
331                token,
332                amount: 100u128,
333            };
334            let calldata = call.abi_encode();
335
336            // Should dispatch to withdraw function
337            let result = exchange.call(&calldata, sender);
338            // Ok indicates successful dispatch (either success or TempoPrecompileError)
339            assert!(result.is_ok());
340
341            Ok(())
342        })
343    }
344
345    #[test]
346    fn test_cancel_call() -> eyre::Result<()> {
347        let mut storage = HashMapStorageProvider::new(1);
348        StorageCtx::enter(&mut storage, || {
349            let mut exchange = StablecoinDEX::new();
350            exchange.initialize()?;
351
352            let sender = Address::random();
353
354            let call = IStablecoinDEX::cancelCall { orderId: 1u128 };
355            let calldata = call.abi_encode();
356
357            // Should dispatch to cancel function
358            let result = exchange.call(&calldata, sender);
359            // Ok indicates successful dispatch (either success or TempoPrecompileError)
360            assert!(result.is_ok());
361            Ok(())
362        })
363    }
364
365    #[test]
366    fn test_swap_exact_amount_in_call() -> eyre::Result<()> {
367        let mut storage = HashMapStorageProvider::new(1);
368        StorageCtx::enter(&mut storage, || {
369            let (mut exchange, base_token, quote_token, user) = setup_exchange_with_liquidity()?;
370
371            // Set balance for the swapper
372            exchange.set_balance(user, base_token, 1_000_000u128)?;
373
374            let call = IStablecoinDEX::swapExactAmountInCall {
375                tokenIn: base_token,
376                tokenOut: quote_token,
377                amountIn: 100_000u128,
378                minAmountOut: 90_000u128,
379            };
380            let calldata = call.abi_encode();
381
382            // Should dispatch to swap_exact_amount_in function and succeed
383            let result = exchange.call(&calldata, user);
384            assert!(result.is_ok());
385
386            Ok(())
387        })
388    }
389
390    #[test]
391    fn test_swap_exact_amount_out_call() -> eyre::Result<()> {
392        let mut storage = HashMapStorageProvider::new(1);
393        StorageCtx::enter(&mut storage, || {
394            let (mut exchange, base_token, quote_token, user) = setup_exchange_with_liquidity()?;
395
396            // Place an ask order to provide liquidity for selling base
397            exchange.place(user, base_token, MIN_ORDER_AMOUNT, false, 0)?;
398
399            // Set balance for the swapper
400            exchange.set_balance(user, quote_token, 1_000_000u128)?;
401
402            let call = IStablecoinDEX::swapExactAmountOutCall {
403                tokenIn: quote_token,
404                tokenOut: base_token,
405                amountOut: 50_000u128,
406                maxAmountIn: 60_000u128,
407            };
408            let calldata = call.abi_encode();
409
410            // Should dispatch to swap_exact_amount_out function and succeed
411            let result = exchange.call(&calldata, user);
412            assert!(result.is_ok());
413
414            Ok(())
415        })
416    }
417
418    #[test]
419    fn test_quote_swap_exact_amount_in_call() -> eyre::Result<()> {
420        let mut storage = HashMapStorageProvider::new(1);
421        StorageCtx::enter(&mut storage, || {
422            let (mut exchange, base_token, quote_token, _user) = setup_exchange_with_liquidity()?;
423
424            let sender = Address::random();
425
426            let call = IStablecoinDEX::quoteSwapExactAmountInCall {
427                tokenIn: base_token,
428                tokenOut: quote_token,
429                amountIn: 100_000u128,
430            };
431            let calldata = call.abi_encode();
432
433            // Should dispatch to quote_swap_exact_amount_in function and succeed
434            let result = exchange.call(&calldata, sender);
435            assert!(result.is_ok());
436
437            Ok(())
438        })
439    }
440
441    #[test]
442    fn test_quote_swap_exact_amount_out_call() -> eyre::Result<()> {
443        let mut storage = HashMapStorageProvider::new(1);
444        StorageCtx::enter(&mut storage, || {
445            let (mut exchange, base_token, quote_token, user) = setup_exchange_with_liquidity()?;
446
447            // Place an ask order to provide liquidity for selling base
448            exchange.place(user, base_token, MIN_ORDER_AMOUNT, false, 0)?;
449
450            let sender = Address::random();
451
452            let call = IStablecoinDEX::quoteSwapExactAmountOutCall {
453                tokenIn: quote_token,
454                tokenOut: base_token,
455                amountOut: 50_000u128,
456            };
457            let calldata = call.abi_encode();
458
459            // Should dispatch to quote_swap_exact_amount_out function and succeed
460            let result = exchange.call(&calldata, sender);
461            assert!(result.is_ok());
462
463            Ok(())
464        })
465    }
466
467    #[test]
468    fn stablecoin_dex_test_selector_coverage() -> eyre::Result<()> {
469        let mut storage = HashMapStorageProvider::new(1);
470        StorageCtx::enter(&mut storage, || {
471            let mut exchange = StablecoinDEX::new();
472
473            let unsupported = check_selector_coverage(
474                &mut exchange,
475                IStablecoinDEXCalls::SELECTORS,
476                "IStablecoinDEX",
477                IStablecoinDEXCalls::name_by_selector,
478            );
479
480            // All selectors should be supported
481            assert_full_coverage([unsupported]);
482
483            Ok(())
484        })
485    }
486}