1use 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 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 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 exchange.create_pair(base.address())?;
145
146 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 let result = exchange.call(&calldata, sender);
172 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 let result = exchange.call(&calldata, sender);
200 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 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 let result = exchange.call(&calldata, sender);
314 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 let result = exchange.call(&calldata, sender);
338 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 let result = exchange.call(&calldata, sender);
359 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 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 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 exchange.place(user, base_token, MIN_ORDER_AMOUNT, false, 0)?;
398
399 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 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 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 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 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 assert_full_coverage([unsupported]);
482
483 Ok(())
484 })
485 }
486}