tempo_precompiles/tip_fee_manager/
dispatch.rs

1use crate::{
2    Precompile,
3    error::TempoPrecompileError,
4    fill_precompile_output, input_cost, mutate, mutate_void,
5    storage::Handler,
6    tip_fee_manager::{
7        IFeeManager, ITIPFeeAMM, TipFeeManager,
8        amm::{M, MIN_LIQUIDITY, N, SCALE},
9    },
10    unknown_selector, view,
11};
12use alloy::{primitives::Address, sol_types::SolCall};
13use revm::precompile::{PrecompileError, PrecompileResult};
14
15impl Precompile for TipFeeManager {
16    fn call(&mut self, calldata: &[u8], msg_sender: Address) -> PrecompileResult {
17        self.storage
18            .deduct_gas(input_cost(calldata.len()))
19            .map_err(|_| PrecompileError::OutOfGas)?;
20
21        let selector: [u8; 4] = calldata
22            .get(..4)
23            .ok_or_else(|| {
24                PrecompileError::Other("Invalid input: missing function selector".into())
25            })?
26            .try_into()
27            .map_err(|_| PrecompileError::Other("Invalid function selector length".into()))?;
28
29        let result = match selector {
30            // View functions
31            IFeeManager::userTokensCall::SELECTOR => {
32                view::<IFeeManager::userTokensCall>(calldata, |call| self.user_tokens(call))
33            }
34            IFeeManager::validatorTokensCall::SELECTOR => {
35                view::<IFeeManager::validatorTokensCall>(calldata, |call| {
36                    self.validator_tokens(call)
37                })
38            }
39            IFeeManager::getFeeTokenBalanceCall::SELECTOR => {
40                view::<IFeeManager::getFeeTokenBalanceCall>(calldata, |call| {
41                    self.get_fee_token_balance(call)
42                })
43            }
44            ITIPFeeAMM::getPoolIdCall::SELECTOR => {
45                view::<ITIPFeeAMM::getPoolIdCall>(calldata, |call| {
46                    Ok(self.pool_id(call.userToken, call.validatorToken))
47                })
48            }
49            ITIPFeeAMM::getPoolCall::SELECTOR => {
50                view::<ITIPFeeAMM::getPoolCall>(calldata, |call| {
51                    let pool = self.get_pool(call)?;
52
53                    Ok(ITIPFeeAMM::Pool {
54                        reserveUserToken: pool.reserve_user_token,
55                        reserveValidatorToken: pool.reserve_validator_token,
56                    })
57                })
58            }
59            ITIPFeeAMM::poolsCall::SELECTOR => view::<ITIPFeeAMM::poolsCall>(calldata, |call| {
60                let pool = self.pools.at(call.poolId).read()?;
61
62                Ok(ITIPFeeAMM::Pool {
63                    reserveUserToken: pool.reserve_user_token,
64                    reserveValidatorToken: pool.reserve_validator_token,
65                })
66            }),
67            ITIPFeeAMM::totalSupplyCall::SELECTOR => {
68                view::<ITIPFeeAMM::totalSupplyCall>(calldata, |call| {
69                    self.total_supply.at(call.poolId).read()
70                })
71            }
72            ITIPFeeAMM::liquidityBalancesCall::SELECTOR => {
73                view::<ITIPFeeAMM::liquidityBalancesCall>(calldata, |call| {
74                    self.liquidity_balances.at(call.poolId).at(call.user).read()
75                })
76            }
77            ITIPFeeAMM::MCall::SELECTOR => view::<ITIPFeeAMM::MCall>(calldata, |_call| Ok(M)),
78            ITIPFeeAMM::NCall::SELECTOR => view::<ITIPFeeAMM::NCall>(calldata, |_call| Ok(N)),
79            ITIPFeeAMM::SCALECall::SELECTOR => {
80                view::<ITIPFeeAMM::SCALECall>(calldata, |_call| Ok(SCALE))
81            }
82            ITIPFeeAMM::MIN_LIQUIDITYCall::SELECTOR => {
83                view::<ITIPFeeAMM::MIN_LIQUIDITYCall>(calldata, |_call| Ok(MIN_LIQUIDITY))
84            }
85
86            // State changing functions
87            IFeeManager::setValidatorTokenCall::SELECTOR => {
88                mutate_void::<IFeeManager::setValidatorTokenCall>(
89                    calldata,
90                    msg_sender,
91                    |s, call| self.set_validator_token(s, call, self.storage.beneficiary()),
92                )
93            }
94            IFeeManager::setUserTokenCall::SELECTOR => {
95                mutate_void::<IFeeManager::setUserTokenCall>(calldata, msg_sender, |s, call| {
96                    self.set_user_token(s, call)
97                })
98            }
99            IFeeManager::executeBlockCall::SELECTOR => {
100                mutate_void::<IFeeManager::executeBlockCall>(calldata, msg_sender, |s, _call| {
101                    self.execute_block(s, self.storage.beneficiary())
102                })
103            }
104            ITIPFeeAMM::mintCall::SELECTOR => {
105                mutate::<ITIPFeeAMM::mintCall>(calldata, msg_sender, |s, call| {
106                    if self.storage.spec().is_moderato() {
107                        Err(TempoPrecompileError::UnknownFunctionSelector(
108                            ITIPFeeAMM::mintCall::SELECTOR,
109                        ))
110                    } else {
111                        self.mint(
112                            s,
113                            call.userToken,
114                            call.validatorToken,
115                            call.amountUserToken,
116                            call.amountValidatorToken,
117                            call.to,
118                        )
119                    }
120                })
121            }
122            ITIPFeeAMM::mintWithValidatorTokenCall::SELECTOR => {
123                mutate::<ITIPFeeAMM::mintWithValidatorTokenCall>(calldata, msg_sender, |s, call| {
124                    self.mint_with_validator_token(
125                        s,
126                        call.userToken,
127                        call.validatorToken,
128                        call.amountValidatorToken,
129                        call.to,
130                    )
131                })
132            }
133            ITIPFeeAMM::burnCall::SELECTOR => {
134                mutate::<ITIPFeeAMM::burnCall>(calldata, msg_sender, |s, call| {
135                    let (amount_user_token, amount_validator_token) = self.burn(
136                        s,
137                        call.userToken,
138                        call.validatorToken,
139                        call.liquidity,
140                        call.to,
141                    )?;
142
143                    Ok(ITIPFeeAMM::burnReturn {
144                        amountUserToken: amount_user_token,
145                        amountValidatorToken: amount_validator_token,
146                    })
147                })
148            }
149            ITIPFeeAMM::rebalanceSwapCall::SELECTOR => {
150                mutate::<ITIPFeeAMM::rebalanceSwapCall>(calldata, msg_sender, |s, call| {
151                    self.rebalance_swap(
152                        s,
153                        call.userToken,
154                        call.validatorToken,
155                        call.amountOut,
156                        call.to,
157                    )
158                })
159            }
160
161            _ => unknown_selector(selector, self.storage.gas_used(), self.storage.spec()),
162        };
163
164        result.map(|res| fill_precompile_output(res, &mut self.storage))
165    }
166}
167
168#[cfg(test)]
169mod tests {
170    use super::*;
171    use crate::{
172        Precompile, TIP_FEE_MANAGER_ADDRESS, expect_precompile_revert,
173        storage::{ContractStorage, StorageCtx, hashmap::HashMapStorageProvider},
174        test_util::{TIP20Setup, assert_full_coverage, check_selector_coverage},
175        tip_fee_manager::{
176            FeeManagerError,
177            amm::{MIN_LIQUIDITY, PoolKey},
178        },
179    };
180    use alloy::{
181        primitives::{Address, B256, Bytes, U256},
182        sol_types::{SolCall, SolError, SolValue},
183    };
184    use revm::precompile::PrecompileError;
185    use tempo_chainspec::hardfork::TempoHardfork;
186    use tempo_contracts::precompiles::{
187        IFeeManager::IFeeManagerCalls, ITIPFeeAMM::ITIPFeeAMMCalls, UnknownFunctionSelector,
188    };
189
190    #[test]
191    fn test_set_validator_token() -> eyre::Result<()> {
192        let mut storage = HashMapStorageProvider::new(1);
193        let admin = Address::random();
194        let validator = Address::random();
195        StorageCtx::enter(&mut storage, || {
196            let token = TIP20Setup::create("TestToken", "TST", admin).apply()?;
197            let mut fee_manager = TipFeeManager::new();
198
199            let calldata = IFeeManager::setValidatorTokenCall {
200                token: token.address(),
201            }
202            .abi_encode();
203            let result = fee_manager.call(&calldata, validator)?;
204            assert_eq!(result.gas_used, 0);
205
206            // Verify token was set
207            let calldata = IFeeManager::validatorTokensCall { validator }.abi_encode();
208            let result = fee_manager.call(&calldata, validator)?;
209            assert_eq!(result.gas_used, 0);
210            let returned_token = Address::abi_decode(&result.bytes)?;
211            assert_eq!(returned_token, token.address());
212
213            Ok(())
214        })
215    }
216
217    #[test]
218    fn test_set_validator_token_zero_address() -> eyre::Result<()> {
219        let mut storage = HashMapStorageProvider::new(1);
220        let validator = Address::random();
221        StorageCtx::enter(&mut storage, || {
222            let mut fee_manager = TipFeeManager::new();
223
224            let calldata = IFeeManager::setValidatorTokenCall {
225                token: Address::ZERO,
226            }
227            .abi_encode();
228            let result = fee_manager.call(&calldata, validator);
229            expect_precompile_revert(&result, FeeManagerError::invalid_token());
230
231            Ok(())
232        })
233    }
234
235    #[test]
236    fn test_set_user_token() -> eyre::Result<()> {
237        let mut storage = HashMapStorageProvider::new(1);
238        let admin = Address::random();
239        let user = Address::random();
240        StorageCtx::enter(&mut storage, || {
241            let token = TIP20Setup::create("TestToken", "TST", admin).apply()?;
242            let mut fee_manager = TipFeeManager::new();
243
244            let calldata = IFeeManager::setUserTokenCall {
245                token: token.address(),
246            }
247            .abi_encode();
248            let result = fee_manager.call(&calldata, user)?;
249            assert_eq!(result.gas_used, 0);
250
251            // Verify token was set
252            let calldata = IFeeManager::userTokensCall { user }.abi_encode();
253            let result = fee_manager.call(&calldata, user)?;
254            assert_eq!(result.gas_used, 0);
255            let returned_token = Address::abi_decode(&result.bytes)?;
256            assert_eq!(returned_token, token.address());
257
258            Ok(())
259        })
260    }
261
262    #[test]
263    fn test_set_user_token_zero_address() -> eyre::Result<()> {
264        let mut storage = HashMapStorageProvider::new(1);
265        let user = Address::random();
266        StorageCtx::enter(&mut storage, || {
267            let mut fee_manager = TipFeeManager::new();
268
269            let calldata = IFeeManager::setUserTokenCall {
270                token: Address::ZERO,
271            }
272            .abi_encode();
273            let result = fee_manager.call(&calldata, user);
274            expect_precompile_revert(&result, FeeManagerError::invalid_token());
275
276            Ok(())
277        })
278    }
279
280    #[test]
281    fn test_get_pool_id() -> eyre::Result<()> {
282        let mut storage = HashMapStorageProvider::new(1);
283        let token_a = Address::random();
284        let token_b = Address::random();
285        let sender = Address::random();
286        StorageCtx::enter(&mut storage, || {
287            let mut fee_manager = TipFeeManager::new();
288
289            let calldata = ITIPFeeAMM::getPoolIdCall {
290                userToken: token_a,
291                validatorToken: token_b,
292            }
293            .abi_encode();
294            let result = fee_manager.call(&calldata, sender)?;
295            assert_eq!(result.gas_used, 0);
296
297            let returned_id = B256::abi_decode(&result.bytes)?;
298            let expected_id = PoolKey::new(token_a, token_b).get_id();
299            assert_eq!(returned_id, expected_id);
300
301            Ok(())
302        })
303    }
304
305    #[test]
306    fn test_tip_fee_amm_pool_operations() -> eyre::Result<()> {
307        let mut storage = HashMapStorageProvider::new(1);
308        let token_a = Address::random();
309        let token_b = Address::random();
310        let sender = Address::random();
311        StorageCtx::enter(&mut storage, || {
312            let mut fee_manager = TipFeeManager::new();
313
314            // Get pool using ITIPFeeAMM interface
315            let get_pool_call = ITIPFeeAMM::getPoolCall {
316                userToken: token_a,
317                validatorToken: token_b,
318            };
319            let calldata = get_pool_call.abi_encode();
320            let result = fee_manager.call(&calldata, sender)?;
321            assert_eq!(result.gas_used, 0);
322
323            // Decode and verify pool (should be empty initially)
324            let pool = ITIPFeeAMM::Pool::abi_decode(&result.bytes)?;
325            assert_eq!(pool.reserveUserToken, 0);
326            assert_eq!(pool.reserveValidatorToken, 0);
327
328            Ok(())
329        })
330    }
331
332    #[test]
333    fn test_pool_id_calculation() -> eyre::Result<()> {
334        let mut storage = HashMapStorageProvider::new(1);
335        let token_a = Address::random();
336        let token_b = Address::random();
337        let sender = Address::random();
338        StorageCtx::enter(&mut storage, || {
339            let mut fee_manager = TipFeeManager::new();
340
341            // Get pool ID with tokens in order (a, b)
342            let calldata1 = ITIPFeeAMM::getPoolIdCall {
343                userToken: token_a,
344                validatorToken: token_b,
345            }
346            .abi_encode();
347            let result1 = fee_manager.call(&calldata1, sender)?;
348            let id1 = B256::abi_decode(&result1.bytes)?;
349
350            // Get pool ID with tokens reversed (b, a)
351            let calldata2 = ITIPFeeAMM::getPoolIdCall {
352                userToken: token_b,
353                validatorToken: token_a,
354            }
355            .abi_encode();
356            let result2 = fee_manager.call(&calldata2, sender)?;
357            let id2 = B256::abi_decode(&result2.bytes)?;
358
359            // Pool IDs should be different since tokens are ordered
360            assert_ne!(id1, id2);
361
362            Ok(())
363        })
364    }
365
366    #[test]
367    fn test_fee_manager_invalid_token_error() -> eyre::Result<()> {
368        let mut storage = HashMapStorageProvider::new(1);
369        let user = Address::random();
370        let validator = Address::random();
371        StorageCtx::enter(&mut storage, || {
372            let mut fee_manager = TipFeeManager::new();
373
374            // Test setValidatorToken with zero address
375            let set_validator_call = IFeeManager::setValidatorTokenCall {
376                token: Address::ZERO,
377            };
378            let result = fee_manager.call(&set_validator_call.abi_encode(), validator);
379            expect_precompile_revert(&result, FeeManagerError::invalid_token());
380
381            // Test setUserToken with zero address
382            let set_user_call = IFeeManager::setUserTokenCall {
383                token: Address::ZERO,
384            };
385            let result = fee_manager.call(&set_user_call.abi_encode(), user);
386            expect_precompile_revert(&result, FeeManagerError::invalid_token());
387
388            Ok(())
389        })
390    }
391
392    #[test]
393    fn test_execute_block() -> eyre::Result<()> {
394        let mut storage = HashMapStorageProvider::new(1);
395        StorageCtx::enter(&mut storage, || {
396            let mut fee_manager = TipFeeManager::new();
397
398            // Call executeBlock (only system contract can call, so sender = Address::ZERO)
399            let call = IFeeManager::executeBlockCall {};
400            let result = fee_manager.call(&call.abi_encode(), Address::ZERO)?;
401            assert_eq!(result.gas_used, 0);
402
403            Ok(())
404        })
405    }
406
407    #[test]
408    fn test_tip_fee_manager_selector_coverage() -> eyre::Result<()> {
409        let mut storage = HashMapStorageProvider::new(1);
410        StorageCtx::enter(&mut storage, || {
411            let mut fee_manager = TipFeeManager::new();
412
413            let fee_manager_unsupported = check_selector_coverage(
414                &mut fee_manager,
415                IFeeManagerCalls::SELECTORS,
416                "IFeeManager",
417                IFeeManagerCalls::name_by_selector,
418            );
419
420            let amm_unsupported = check_selector_coverage(
421                &mut fee_manager,
422                ITIPFeeAMMCalls::SELECTORS,
423                "ITIPFeeAMM",
424                ITIPFeeAMMCalls::name_by_selector,
425            );
426
427            assert_full_coverage([fee_manager_unsupported, amm_unsupported]);
428
429            Ok(())
430        })
431    }
432
433    #[test]
434    fn test_mint_with_validator_token() -> eyre::Result<()> {
435        let mut storage = HashMapStorageProvider::new(1);
436        let admin = Address::random();
437        let user = Address::random();
438        StorageCtx::enter(&mut storage, || {
439            let user_token = TIP20Setup::create("UserToken", "UTK", admin)
440                .with_issuer(admin)
441                .with_mint(user, U256::from(1000000_u64))
442                .with_approval(user, TIP_FEE_MANAGER_ADDRESS, U256::MAX)
443                .apply()?;
444
445            let validator_token = TIP20Setup::create("ValidatorToken", "VTK", admin)
446                .with_issuer(admin)
447                .with_mint(user, U256::from(1000000_u64))
448                .with_approval(user, TIP_FEE_MANAGER_ADDRESS, U256::MAX)
449                .apply()?;
450
451            let mut fee_manager = TipFeeManager::new();
452
453            // Get pool ID first
454            let pool_id_call = ITIPFeeAMM::getPoolIdCall {
455                userToken: user_token.address(),
456                validatorToken: validator_token.address(),
457            };
458            let pool_id_result = fee_manager.call(&pool_id_call.abi_encode(), user)?;
459            let pool_id = B256::abi_decode(&pool_id_result.bytes)?;
460
461            // Check initial total supply
462            let initial_supply_call = ITIPFeeAMM::totalSupplyCall { poolId: pool_id };
463            let initial_supply_result =
464                fee_manager.call(&initial_supply_call.abi_encode(), user)?;
465            let initial_supply = U256::abi_decode(&initial_supply_result.bytes)?;
466            assert_eq!(initial_supply, U256::ZERO);
467
468            // Mint with validator token only
469            let amount_validator_token = U256::from(10000_u64);
470            let call = ITIPFeeAMM::mintWithValidatorTokenCall {
471                userToken: user_token.address(),
472                validatorToken: validator_token.address(),
473                amountValidatorToken: amount_validator_token,
474                to: user,
475            };
476            let result = fee_manager.call(&call.abi_encode(), user)?;
477
478            // For first mint with validator token only, liquidity should be (amount / 2) - MIN_LIQUIDITY
479            // MIN_LIQUIDITY = 1000, so (10000 / 2) - 1000 = 4000
480            let liquidity = U256::abi_decode(&result.bytes)?;
481            assert_eq!(liquidity, U256::from(4000_u64));
482
483            // Check total supply after mint = liquidity + MIN_LIQUIDITY
484            let final_supply_call = ITIPFeeAMM::totalSupplyCall { poolId: pool_id };
485            let final_supply_result = fee_manager.call(&final_supply_call.abi_encode(), user)?;
486            let final_supply = U256::abi_decode(&final_supply_result.bytes)?;
487            assert_eq!(final_supply, liquidity + MIN_LIQUIDITY);
488
489            // Verify pool state
490            let pool_call = ITIPFeeAMM::getPoolCall {
491                userToken: user_token.address(),
492                validatorToken: validator_token.address(),
493            };
494            let pool_result = fee_manager.call(&pool_call.abi_encode(), user)?;
495            let pool = ITIPFeeAMM::Pool::abi_decode(&pool_result.bytes)?;
496            assert_eq!(pool.reserveUserToken, 0);
497            assert_eq!(pool.reserveValidatorToken, 10000);
498
499            // Verify LP token balance
500            let balance_call = ITIPFeeAMM::liquidityBalancesCall {
501                poolId: pool_id,
502                user,
503            };
504            let balance_result = fee_manager.call(&balance_call.abi_encode(), user)?;
505            let balance = U256::abi_decode(&balance_result.bytes)?;
506            assert_eq!(balance, liquidity);
507
508            Ok(())
509        })
510    }
511
512    #[test]
513    fn test_unknown_selector_error_pre_moderato() -> eyre::Result<()> {
514        let mut storage = HashMapStorageProvider::new(1).with_spec(TempoHardfork::Adagio);
515        let sender = Address::random();
516        StorageCtx::enter(&mut storage, || {
517            let mut fee_manager = TipFeeManager::new();
518
519            // Call with an unknown selector
520            let unknown_selector = [0x12, 0x34, 0x56, 0x78];
521            let calldata = Bytes::from(unknown_selector);
522            let result = fee_manager.call(&calldata, sender);
523
524            // Before Moderato: should return Err(PrecompileError::Other)
525            assert!(result.is_err());
526            assert!(matches!(result, Err(PrecompileError::Other(_))));
527
528            Ok(())
529        })
530    }
531
532    #[test]
533    fn test_unknown_selector_error_post_moderato() -> eyre::Result<()> {
534        let mut storage = HashMapStorageProvider::new(1).with_spec(TempoHardfork::Moderato);
535        let sender = Address::random();
536        StorageCtx::enter(&mut storage, || {
537            let mut fee_manager = TipFeeManager::new();
538
539            // Call with an unknown selector
540            let unknown_selector = [0x12, 0x34, 0x56, 0x78];
541            let calldata = Bytes::from(unknown_selector);
542            let result = fee_manager.call(&calldata, sender);
543
544            // After Moderato: should return Ok with reverted status
545            assert!(result.is_ok());
546            let output = result.unwrap();
547            assert!(output.reverted);
548
549            // Verify the error can be decoded as UnknownFunctionSelector
550            let decoded_error = UnknownFunctionSelector::abi_decode(&output.bytes);
551            assert!(
552                decoded_error.is_ok(),
553                "Should decode as UnknownFunctionSelector"
554            );
555
556            // Verify the selector matches what we sent
557            let error = decoded_error.unwrap();
558            assert_eq!(error.selector.as_slice(), &unknown_selector);
559
560            Ok(())
561        })
562    }
563
564    #[test]
565    fn test_mint_deprecated_post_moderato() -> eyre::Result<()> {
566        let mut storage = HashMapStorageProvider::new(1).with_spec(TempoHardfork::Moderato);
567        let admin = Address::random();
568        let user = Address::random();
569        StorageCtx::enter(&mut storage, || {
570            let user_token = TIP20Setup::create("UserToken", "UTK", admin)
571                .with_issuer(admin)
572                .with_mint(user, U256::from(1000000_u64))
573                .with_approval(user, TIP_FEE_MANAGER_ADDRESS, U256::MAX)
574                .apply()?;
575
576            let validator_token = TIP20Setup::create("ValidatorToken", "VTK", admin)
577                .with_issuer(admin)
578                .with_mint(user, U256::from(1000000_u64))
579                .with_approval(user, TIP_FEE_MANAGER_ADDRESS, U256::MAX)
580                .apply()?;
581
582            let mut fee_manager = TipFeeManager::new();
583
584            let call = ITIPFeeAMM::mintCall {
585                userToken: user_token.address(),
586                validatorToken: validator_token.address(),
587                amountUserToken: U256::from(1000_u64),
588                amountValidatorToken: U256::from(1000_u64),
589                to: user,
590            };
591
592            let result = fee_manager.call(&call.abi_encode(), user);
593
594            // Should return Ok with reverted status for unknown function selector
595            assert!(result.is_ok());
596            let output = result.unwrap();
597            assert!(output.reverted);
598
599            // Verify the error can be decoded as UnknownFunctionSelector
600            let decoded_error = UnknownFunctionSelector::abi_decode(&output.bytes);
601            assert!(
602                decoded_error.is_ok(),
603                "Should decode as UnknownFunctionSelector"
604            );
605
606            // Verify it's the mint selector
607            let error = decoded_error.unwrap();
608            assert_eq!(error.selector.as_slice(), &ITIPFeeAMM::mintCall::SELECTOR);
609
610            Ok(())
611        })
612    }
613}