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