1use 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 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 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 exchange.create_pair(base.address())?;
238
239 exchange.place(user, base.address(), MIN_ORDER_AMOUNT, true, 0)?;
241
242 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 let result = exchange.call(&calldata, sender);
268 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 let result = exchange.call(&calldata, sender);
296 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 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 let result = exchange.call(&calldata, sender);
466 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 let result = exchange.call(&calldata, sender);
490 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 let result = exchange.call(&calldata, sender);
511 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 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 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 exchange.place(user, base_token, MIN_ORDER_AMOUNT, false, 0)?;
550 exchange.execute_block(Address::ZERO)?;
551
552 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 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 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 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 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); 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); 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 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 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}