1use 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 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 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 exchange.create_pair(base.address())?;
146
147 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 let result = exchange.call(&calldata, sender);
173 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 let result = exchange.call(&calldata, sender);
201 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 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 let result = exchange.call(&calldata, sender);
315 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 let result = exchange.call(&calldata, sender);
339 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 let result = exchange.call(&calldata, sender);
360 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 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 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 exchange.place(user, base_token, MIN_ORDER_AMOUNT, false, 0)?;
399
400 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 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 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 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 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 assert_full_coverage([unsupported]);
483
484 Ok(())
485 })
486 }
487}