tempo_precompiles/stablecoin_exchange/
dispatch.rs

1//! Stablecoin DEX precompile
2//!
3//! This module provides the precompile interface for the Stablecoin DEX.
4use alloy::{primitives::Address, sol_types::SolCall};
5use revm::precompile::{PrecompileError, PrecompileResult};
6
7use crate::{
8    Precompile, fill_precompile_output, input_cost, mutate, mutate_void,
9    stablecoin_exchange::{IStablecoinExchange, StablecoinExchange},
10    unknown_selector, view,
11};
12
13impl Precompile for StablecoinExchange {
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        let selector: [u8; 4] = calldata
20            .get(..4)
21            .ok_or_else(|| {
22                PrecompileError::Other("Invalid input: missing function selector".into())
23            })?
24            .try_into()
25            .map_err(|_| PrecompileError::Other("Invalid function selector length".into()))?;
26
27        let result = match selector {
28            IStablecoinExchange::placeCall::SELECTOR => {
29                mutate::<IStablecoinExchange::placeCall>(calldata, msg_sender, |s, call| {
30                    self.place(s, call.token, call.amount, call.isBid, call.tick)
31                })
32            }
33            IStablecoinExchange::placeFlipCall::SELECTOR => {
34                mutate::<IStablecoinExchange::placeFlipCall>(calldata, msg_sender, |s, call| {
35                    self.place_flip(
36                        s,
37                        call.token,
38                        call.amount,
39                        call.isBid,
40                        call.tick,
41                        call.flipTick,
42                    )
43                })
44            }
45
46            IStablecoinExchange::balanceOfCall::SELECTOR => {
47                view::<IStablecoinExchange::balanceOfCall>(calldata, |call| {
48                    self.balance_of(call.user, call.token)
49                })
50            }
51
52            IStablecoinExchange::getOrderCall::SELECTOR => {
53                view::<IStablecoinExchange::getOrderCall>(calldata, |call| {
54                    self.get_order(call.orderId).map(|order| order.into())
55                })
56            }
57
58            IStablecoinExchange::getTickLevelCall::SELECTOR => {
59                view::<IStablecoinExchange::getTickLevelCall>(calldata, |call| {
60                    let level = self.get_price_level(call.base, call.tick, call.isBid)?;
61                    Ok((level.head, level.tail, level.total_liquidity).into())
62                })
63            }
64
65            IStablecoinExchange::pairKeyCall::SELECTOR => {
66                view::<IStablecoinExchange::pairKeyCall>(calldata, |call| {
67                    Ok(self.pair_key(call.tokenA, call.tokenB))
68                })
69            }
70
71            IStablecoinExchange::booksCall::SELECTOR => {
72                view::<IStablecoinExchange::booksCall>(calldata, |call| {
73                    self.books(call.pairKey).map(Into::into)
74                })
75            }
76
77            IStablecoinExchange::activeOrderIdCall::SELECTOR => {
78                view::<IStablecoinExchange::activeOrderIdCall>(calldata, |_call| {
79                    self.active_order_id()
80                })
81            }
82            IStablecoinExchange::pendingOrderIdCall::SELECTOR => {
83                view::<IStablecoinExchange::pendingOrderIdCall>(calldata, |_call| {
84                    self.pending_order_id()
85                })
86            }
87
88            IStablecoinExchange::createPairCall::SELECTOR => {
89                mutate::<IStablecoinExchange::createPairCall>(calldata, msg_sender, |_s, call| {
90                    self.create_pair(call.base)
91                })
92            }
93            IStablecoinExchange::withdrawCall::SELECTOR => {
94                mutate_void::<IStablecoinExchange::withdrawCall>(calldata, msg_sender, |s, call| {
95                    self.withdraw(s, call.token, call.amount)
96                })
97            }
98            IStablecoinExchange::cancelCall::SELECTOR => {
99                mutate_void::<IStablecoinExchange::cancelCall>(calldata, msg_sender, |s, call| {
100                    self.cancel(s, call.orderId)
101                })
102            }
103            IStablecoinExchange::swapExactAmountInCall::SELECTOR => {
104                mutate::<IStablecoinExchange::swapExactAmountInCall>(
105                    calldata,
106                    msg_sender,
107                    |s, call| {
108                        self.swap_exact_amount_in(
109                            s,
110                            call.tokenIn,
111                            call.tokenOut,
112                            call.amountIn,
113                            call.minAmountOut,
114                        )
115                    },
116                )
117            }
118            IStablecoinExchange::swapExactAmountOutCall::SELECTOR => {
119                mutate::<IStablecoinExchange::swapExactAmountOutCall>(
120                    calldata,
121                    msg_sender,
122                    |s, call| {
123                        self.swap_exact_amount_out(
124                            s,
125                            call.tokenIn,
126                            call.tokenOut,
127                            call.amountOut,
128                            call.maxAmountIn,
129                        )
130                    },
131                )
132            }
133            IStablecoinExchange::quoteSwapExactAmountInCall::SELECTOR => {
134                view::<IStablecoinExchange::quoteSwapExactAmountInCall>(calldata, |call| {
135                    self.quote_swap_exact_amount_in(call.tokenIn, call.tokenOut, call.amountIn)
136                })
137            }
138            IStablecoinExchange::quoteSwapExactAmountOutCall::SELECTOR => {
139                view::<IStablecoinExchange::quoteSwapExactAmountOutCall>(calldata, |call| {
140                    self.quote_swap_exact_amount_out(call.tokenIn, call.tokenOut, call.amountOut)
141                })
142            }
143            IStablecoinExchange::executeBlockCall::SELECTOR => {
144                mutate_void::<IStablecoinExchange::executeBlockCall>(
145                    calldata,
146                    msg_sender,
147                    |_s, _call| self.execute_block(msg_sender),
148                )
149            }
150            IStablecoinExchange::MIN_TICKCall::SELECTOR => {
151                view::<IStablecoinExchange::MIN_TICKCall>(calldata, |_call| {
152                    Ok(crate::stablecoin_exchange::MIN_TICK)
153                })
154            }
155            IStablecoinExchange::MAX_TICKCall::SELECTOR => {
156                view::<IStablecoinExchange::MAX_TICKCall>(calldata, |_call| {
157                    Ok(crate::stablecoin_exchange::MAX_TICK)
158                })
159            }
160            IStablecoinExchange::TICK_SPACINGCall::SELECTOR => {
161                view::<IStablecoinExchange::TICK_SPACINGCall>(calldata, |_call| {
162                    Ok(crate::stablecoin_exchange::TICK_SPACING)
163                })
164            }
165            IStablecoinExchange::PRICE_SCALECall::SELECTOR => {
166                view::<IStablecoinExchange::PRICE_SCALECall>(calldata, |_call| {
167                    Ok(crate::stablecoin_exchange::PRICE_SCALE)
168                })
169            }
170            IStablecoinExchange::MIN_PRICECall::SELECTOR => {
171                view::<IStablecoinExchange::MIN_PRICECall>(calldata, |_call| Ok(self.min_price()))
172            }
173            IStablecoinExchange::MAX_PRICECall::SELECTOR => {
174                view::<IStablecoinExchange::MAX_PRICECall>(calldata, |_call| Ok(self.max_price()))
175            }
176            IStablecoinExchange::tickToPriceCall::SELECTOR => {
177                view::<IStablecoinExchange::tickToPriceCall>(calldata, |call| {
178                    Ok(crate::stablecoin_exchange::tick_to_price(call.tick))
179                })
180            }
181            IStablecoinExchange::priceToTickCall::SELECTOR => {
182                view::<IStablecoinExchange::priceToTickCall>(calldata, |call| {
183                    self.price_to_tick(call.price)
184                })
185            }
186
187            _ => unknown_selector(selector, self.storage.gas_used(), self.storage.spec()),
188        };
189
190        result.map(|res| fill_precompile_output(res, &mut self.storage))
191    }
192}
193
194#[cfg(test)]
195mod tests {
196
197    use super::*;
198    use crate::{
199        Precompile,
200        path_usd::TRANSFER_ROLE,
201        stablecoin_exchange::{IStablecoinExchange, MIN_ORDER_AMOUNT, StablecoinExchange},
202        storage::{ContractStorage, StorageCtx, hashmap::HashMapStorageProvider},
203        test_util::{TIP20Setup, assert_full_coverage, check_selector_coverage},
204    };
205    use alloy::{
206        primitives::{Address, Bytes, U256},
207        sol_types::{SolCall, SolValue},
208    };
209    use tempo_chainspec::hardfork::TempoHardfork;
210    use tempo_contracts::precompiles::IStablecoinExchange::IStablecoinExchangeCalls;
211
212    /// Setup a basic exchange with tokens and liquidity for swap tests
213    fn setup_exchange_with_liquidity()
214    -> eyre::Result<(StablecoinExchange, Address, Address, Address)> {
215        let mut exchange = StablecoinExchange::new();
216        exchange.initialize()?;
217
218        let admin = Address::random();
219        let user = Address::random();
220        let amount = 200_000_000u128;
221
222        // Initialize quote token (PathUSD)
223        let quote = TIP20Setup::path_usd(admin)
224            .with_issuer(admin)
225            .with_role(user, *TRANSFER_ROLE)
226            .with_mint(user, U256::from(amount))
227            .with_approval(user, exchange.address, U256::from(amount))
228            .apply()?;
229
230        let base = TIP20Setup::create("USDC", "USDC", admin)
231            .with_issuer(admin)
232            .with_mint(user, U256::from(amount))
233            .with_approval(user, exchange.address, U256::from(amount))
234            .apply()?;
235
236        // Create pair and add liquidity
237        exchange.create_pair(base.address())?;
238
239        // Place an order to provide liquidity
240        exchange.place(user, base.address(), MIN_ORDER_AMOUNT, true, 0)?;
241
242        // Execute block to activate orders
243        exchange.execute_block(Address::ZERO)?;
244
245        Ok((exchange, base.address(), quote.address(), user))
246    }
247
248    #[test]
249    fn test_place_call() -> eyre::Result<()> {
250        let mut storage = HashMapStorageProvider::new(1);
251        StorageCtx::enter(&mut storage, || {
252            let mut exchange = StablecoinExchange::new();
253            exchange.initialize()?;
254
255            let sender = Address::random();
256            let token = Address::random();
257
258            let call = IStablecoinExchange::placeCall {
259                token,
260                amount: 100u128,
261                isBid: true,
262                tick: 0,
263            };
264            let calldata = call.abi_encode();
265
266            // Should dispatch to place function (may fail due to business logic, but dispatch works)
267            let result = exchange.call(&calldata, sender);
268            // Ok indicates successful dispatch (either success or TempoPrecompileError)
269            assert!(result.is_ok());
270
271            Ok(())
272        })
273    }
274
275    #[test]
276    fn test_place_flip_call() -> eyre::Result<()> {
277        let mut storage = HashMapStorageProvider::new(1);
278        StorageCtx::enter(&mut storage, || {
279            let mut exchange = StablecoinExchange::new();
280            exchange.initialize()?;
281
282            let sender = Address::random();
283            let token = Address::random();
284
285            let call = IStablecoinExchange::placeFlipCall {
286                token,
287                amount: 100u128,
288                isBid: true,
289                tick: 0,
290                flipTick: 10,
291            };
292            let calldata = call.abi_encode();
293
294            // Should dispatch to place_flip function
295            let result = exchange.call(&calldata, sender);
296            // Ok indicates successful dispatch (either success or TempoPrecompileError)
297            assert!(result.is_ok());
298
299            Ok(())
300        })
301    }
302
303    #[test]
304    fn test_balance_of_call() -> eyre::Result<()> {
305        let mut storage = HashMapStorageProvider::new(1);
306        StorageCtx::enter(&mut storage, || {
307            let mut exchange = StablecoinExchange::new();
308            exchange.initialize()?;
309
310            let sender = Address::random();
311            let token = Address::random();
312            let user = Address::random();
313
314            let call = IStablecoinExchange::balanceOfCall { user, token };
315            let calldata = call.abi_encode();
316
317            // Should dispatch to balance_of function and succeed (returns 0 for uninitialized)
318            let result = exchange.call(&calldata, sender);
319            assert!(result.is_ok());
320
321            Ok(())
322        })
323    }
324
325    #[test]
326    fn test_min_price_pre_moderato() -> eyre::Result<()> {
327        let mut storage = HashMapStorageProvider::new(1).with_spec(TempoHardfork::Adagio);
328        StorageCtx::enter(&mut storage, || {
329            let mut exchange = StablecoinExchange::new();
330            exchange.initialize()?;
331
332            let sender = Address::ZERO;
333            let call = IStablecoinExchange::MIN_PRICECall {};
334            let calldata = call.abi_encode();
335
336            let result = exchange.call(&calldata, sender);
337            assert!(result.is_ok());
338
339            let output = result?.bytes;
340            let returned_value = u32::abi_decode(&output)?;
341
342            assert_eq!(
343                returned_value, 67_232,
344                "Pre-moderato MIN_PRICE should be 67_232"
345            );
346            Ok(())
347        })
348    }
349
350    #[test]
351    fn test_min_price_post_moderato() -> eyre::Result<()> {
352        let mut storage = HashMapStorageProvider::new(1).with_spec(TempoHardfork::Moderato);
353        StorageCtx::enter(&mut storage, || {
354            let mut exchange = StablecoinExchange::new();
355            exchange.initialize()?;
356
357            let sender = Address::ZERO;
358            let call = IStablecoinExchange::MIN_PRICECall {};
359            let calldata = call.abi_encode();
360
361            let result = exchange.call(&calldata, sender);
362            assert!(result.is_ok());
363
364            let output = result?.bytes;
365            let returned_value = u32::abi_decode(&output)?;
366
367            assert_eq!(
368                returned_value, 98_000,
369                "Post-moderato MIN_PRICE should be 98_000"
370            );
371            Ok(())
372        })
373    }
374
375    #[test]
376    fn test_tick_spacing() -> eyre::Result<()> {
377        let mut storage = HashMapStorageProvider::new(1);
378        StorageCtx::enter(&mut storage, || {
379            let mut exchange = StablecoinExchange::new();
380            exchange.initialize()?;
381
382            let sender = Address::ZERO;
383            let call = IStablecoinExchange::TICK_SPACINGCall {};
384            let calldata = call.abi_encode();
385
386            let result = exchange.call(&calldata, sender);
387            assert!(result.is_ok());
388
389            let output = result?.bytes;
390            let returned_value = i16::abi_decode(&output)?;
391
392            let expected = crate::stablecoin_exchange::TICK_SPACING;
393            assert_eq!(
394                returned_value, expected,
395                "TICK_SPACING should be {expected}"
396            );
397            Ok(())
398        })
399    }
400
401    #[test]
402    fn test_max_price_pre_moderato() -> eyre::Result<()> {
403        let mut storage = HashMapStorageProvider::new(1).with_spec(TempoHardfork::Adagio);
404        StorageCtx::enter(&mut storage, || {
405            let mut exchange = StablecoinExchange::new();
406            exchange.initialize()?;
407
408            let sender = Address::ZERO;
409            let call = IStablecoinExchange::MAX_PRICECall {};
410            let calldata = call.abi_encode();
411
412            let result = exchange.call(&calldata, sender);
413            assert!(result.is_ok());
414
415            let output = result?.bytes;
416            let returned_value = u32::abi_decode(&output)?;
417
418            assert_eq!(
419                returned_value, 132_767,
420                "Pre-moderato MAX_PRICE should be 132_767"
421            );
422            Ok(())
423        })
424    }
425
426    #[test]
427    fn test_max_price_post_moderato() -> eyre::Result<()> {
428        let mut storage = HashMapStorageProvider::new(1).with_spec(TempoHardfork::Moderato);
429        StorageCtx::enter(&mut storage, || {
430            let mut exchange = StablecoinExchange::new();
431            exchange.initialize()?;
432
433            let sender = Address::ZERO;
434            let call = IStablecoinExchange::MAX_PRICECall {};
435            let calldata = call.abi_encode();
436
437            let result = exchange.call(&calldata, sender);
438            assert!(result.is_ok());
439
440            let output = result?.bytes;
441            let returned_value = u32::abi_decode(&output)?;
442
443            assert_eq!(
444                returned_value, 102_000,
445                "Post-moderato MAX_PRICE should be 102_000"
446            );
447            Ok(())
448        })
449    }
450
451    #[test]
452    fn test_create_pair_call() -> eyre::Result<()> {
453        let mut storage = HashMapStorageProvider::new(1);
454        StorageCtx::enter(&mut storage, || {
455            let mut exchange = StablecoinExchange::new();
456            exchange.initialize()?;
457
458            let sender = Address::random();
459            let base = Address::from([2u8; 20]);
460
461            let call = IStablecoinExchange::createPairCall { base };
462            let calldata = call.abi_encode();
463
464            // Should dispatch to create_pair function
465            let result = exchange.call(&calldata, sender);
466            // Ok indicates successful dispatch (either success or TempoPrecompileError)
467            assert!(result.is_ok());
468            Ok(())
469        })
470    }
471
472    #[test]
473    fn test_withdraw_call() -> eyre::Result<()> {
474        let mut storage = HashMapStorageProvider::new(1);
475        StorageCtx::enter(&mut storage, || {
476            let mut exchange = StablecoinExchange::new();
477            exchange.initialize()?;
478
479            let sender = Address::random();
480            let token = Address::random();
481
482            let call = IStablecoinExchange::withdrawCall {
483                token,
484                amount: 100u128,
485            };
486            let calldata = call.abi_encode();
487
488            // Should dispatch to withdraw function
489            let result = exchange.call(&calldata, sender);
490            // Ok indicates successful dispatch (either success or TempoPrecompileError)
491            assert!(result.is_ok());
492
493            Ok(())
494        })
495    }
496
497    #[test]
498    fn test_cancel_call() -> eyre::Result<()> {
499        let mut storage = HashMapStorageProvider::new(1);
500        StorageCtx::enter(&mut storage, || {
501            let mut exchange = StablecoinExchange::new();
502            exchange.initialize()?;
503
504            let sender = Address::random();
505
506            let call = IStablecoinExchange::cancelCall { orderId: 1u128 };
507            let calldata = call.abi_encode();
508
509            // Should dispatch to cancel function
510            let result = exchange.call(&calldata, sender);
511            // Ok indicates successful dispatch (either success or TempoPrecompileError)
512            assert!(result.is_ok());
513            Ok(())
514        })
515    }
516
517    #[test]
518    fn test_swap_exact_amount_in_call() -> eyre::Result<()> {
519        let mut storage = HashMapStorageProvider::new(1);
520        StorageCtx::enter(&mut storage, || {
521            let (mut exchange, base_token, quote_token, user) = setup_exchange_with_liquidity()?;
522
523            // Set balance for the swapper
524            exchange.set_balance(user, base_token, 1_000_000u128)?;
525
526            let call = IStablecoinExchange::swapExactAmountInCall {
527                tokenIn: base_token,
528                tokenOut: quote_token,
529                amountIn: 100_000u128,
530                minAmountOut: 90_000u128,
531            };
532            let calldata = call.abi_encode();
533
534            // Should dispatch to swap_exact_amount_in function and succeed
535            let result = exchange.call(&calldata, user);
536            assert!(result.is_ok());
537
538            Ok(())
539        })
540    }
541
542    #[test]
543    fn test_swap_exact_amount_out_call() -> eyre::Result<()> {
544        let mut storage = HashMapStorageProvider::new(1);
545        StorageCtx::enter(&mut storage, || {
546            let (mut exchange, base_token, quote_token, user) = setup_exchange_with_liquidity()?;
547
548            // Place an ask order to provide liquidity for selling base
549            exchange.place(user, base_token, MIN_ORDER_AMOUNT, false, 0)?;
550            exchange.execute_block(Address::ZERO)?;
551
552            // Set balance for the swapper
553            exchange.set_balance(user, quote_token, 1_000_000u128)?;
554
555            let call = IStablecoinExchange::swapExactAmountOutCall {
556                tokenIn: quote_token,
557                tokenOut: base_token,
558                amountOut: 50_000u128,
559                maxAmountIn: 60_000u128,
560            };
561            let calldata = call.abi_encode();
562
563            // Should dispatch to swap_exact_amount_out function and succeed
564            let result = exchange.call(&calldata, user);
565            assert!(result.is_ok());
566
567            Ok(())
568        })
569    }
570
571    #[test]
572    fn test_quote_swap_exact_amount_in_call() -> eyre::Result<()> {
573        let mut storage = HashMapStorageProvider::new(1);
574        StorageCtx::enter(&mut storage, || {
575            let (mut exchange, base_token, quote_token, _user) = setup_exchange_with_liquidity()?;
576
577            let sender = Address::random();
578
579            let call = IStablecoinExchange::quoteSwapExactAmountInCall {
580                tokenIn: base_token,
581                tokenOut: quote_token,
582                amountIn: 100_000u128,
583            };
584            let calldata = call.abi_encode();
585
586            // Should dispatch to quote_swap_exact_amount_in function and succeed
587            let result = exchange.call(&calldata, sender);
588            assert!(result.is_ok());
589
590            Ok(())
591        })
592    }
593
594    #[test]
595    fn test_quote_swap_exact_amount_out_call() -> eyre::Result<()> {
596        let mut storage = HashMapStorageProvider::new(1);
597        StorageCtx::enter(&mut storage, || {
598            let (mut exchange, base_token, quote_token, user) = setup_exchange_with_liquidity()?;
599
600            // Place an ask order to provide liquidity for selling base
601            exchange.place(user, base_token, MIN_ORDER_AMOUNT, false, 0)?;
602            exchange.execute_block(Address::ZERO)?;
603
604            let sender = Address::random();
605
606            let call = IStablecoinExchange::quoteSwapExactAmountOutCall {
607                tokenIn: quote_token,
608                tokenOut: base_token,
609                amountOut: 50_000u128,
610            };
611            let calldata = call.abi_encode();
612
613            // Should dispatch to quote_swap_exact_amount_out function and succeed
614            let result = exchange.call(&calldata, sender);
615            assert!(result.is_ok());
616
617            Ok(())
618        })
619    }
620
621    #[test]
622    fn test_active_order_id_call() -> eyre::Result<()> {
623        let mut storage = HashMapStorageProvider::new(1);
624        StorageCtx::enter(&mut storage, || {
625            let mut exchange = StablecoinExchange::new();
626            exchange.initialize()?;
627
628            let sender = Address::random();
629
630            let call = IStablecoinExchange::activeOrderIdCall {};
631            let calldata = call.abi_encode();
632
633            let result = exchange.call(&calldata, sender);
634            assert!(result.is_ok());
635
636            let output = result?;
637            let active_order_id = u128::abi_decode(&output.bytes)?;
638            assert_eq!(active_order_id, 0); // Should be 0 initially
639
640            Ok(())
641        })
642    }
643
644    #[test]
645    fn test_pending_order_id_call() -> eyre::Result<()> {
646        let mut storage = HashMapStorageProvider::new(1);
647        StorageCtx::enter(&mut storage, || {
648            let mut exchange = StablecoinExchange::new();
649            exchange.initialize()?;
650
651            let sender = Address::random();
652
653            let call = IStablecoinExchange::pendingOrderIdCall {};
654            let calldata = call.abi_encode();
655
656            let result = exchange.call(&calldata, sender);
657            assert!(result.is_ok());
658
659            let output = result?;
660            let pending_order_id = u128::abi_decode(&output.bytes)?;
661            assert_eq!(pending_order_id, 0); // Should be 0 initially
662
663            Ok(())
664        })
665    }
666
667    #[test]
668    fn test_invalid_selector() -> eyre::Result<()> {
669        let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::Moderato);
670        StorageCtx::enter(&mut storage, || {
671            let mut exchange = StablecoinExchange::new();
672            exchange.initialize()?;
673
674            let sender = Address::random();
675
676            // Use an invalid selector that doesn't match any function - should return Ok with reverted status
677            let calldata = Bytes::from([0x12, 0x34, 0x56, 0x78]);
678
679            let result = exchange.call(&calldata, sender);
680            assert!(result.is_ok());
681            assert!(result?.reverted);
682            Ok(())
683        })
684    }
685
686    #[test]
687    fn test_missing_selector() -> eyre::Result<()> {
688        let mut storage = HashMapStorageProvider::new(1);
689        StorageCtx::enter(&mut storage, || {
690            let mut exchange = StablecoinExchange::new();
691            exchange.initialize()?;
692
693            let sender = Address::random();
694
695            // Use calldata that's too short to contain a selector
696            let calldata = Bytes::from([0x12, 0x34]);
697
698            let result = exchange.call(&calldata, sender);
699            assert!(matches!(result, Err(PrecompileError::Other(_))));
700            Ok(())
701        })
702    }
703
704    #[test]
705    fn stablecoin_exchange_test_selector_coverage() -> eyre::Result<()> {
706        let mut storage = HashMapStorageProvider::new(1);
707        StorageCtx::enter(&mut storage, || {
708            let mut exchange = StablecoinExchange::new();
709
710            let unsupported = check_selector_coverage(
711                &mut exchange,
712                IStablecoinExchangeCalls::SELECTORS,
713                "IStablecoinExchange",
714                IStablecoinExchangeCalls::name_by_selector,
715            );
716
717            assert_full_coverage([unsupported]);
718
719            Ok(())
720        })
721    }
722}