Skip to main content

tempo_precompiles/tip_fee_manager/
dispatch.rs

1//! ABI dispatch for the [`TipFeeManager`] precompile.
2
3use crate::{
4    Precompile, charge_input_cost, dispatch_call, metadata, mutate, mutate_void,
5    storage::Handler,
6    tip_fee_manager::{
7        ITIPFeeAMM, TipFeeManager,
8        amm::{M, MIN_LIQUIDITY, N, SCALE},
9    },
10    view,
11};
12use alloy::{primitives::Address, sol_types::SolInterface};
13use revm::precompile::PrecompileResult;
14use tempo_contracts::precompiles::{IFeeManager::IFeeManagerCalls, ITIPFeeAMM::ITIPFeeAMMCalls};
15
16/// Unified calldata discriminant for both `IFeeManager` and `ITIPFeeAMM` selectors.
17enum TipFeeManagerCall {
18    FeeManager(IFeeManagerCalls),
19    Amm(ITIPFeeAMMCalls),
20}
21
22impl TipFeeManagerCall {
23    fn decode(calldata: &[u8]) -> Result<Self, alloy::sol_types::Error> {
24        // safe to expect as `dispatch_call` pre-validates calldata len
25        let selector: [u8; 4] = calldata[..4].try_into().expect("calldata len >= 4");
26
27        if IFeeManagerCalls::valid_selector(selector) {
28            IFeeManagerCalls::abi_decode(calldata).map(Self::FeeManager)
29        } else {
30            ITIPFeeAMMCalls::abi_decode(calldata).map(Self::Amm)
31        }
32    }
33}
34
35impl Precompile for TipFeeManager {
36    fn call(&mut self, calldata: &[u8], msg_sender: Address) -> PrecompileResult {
37        if let Some(err) = charge_input_cost(&mut self.storage, calldata) {
38            return err;
39        }
40
41        dispatch_call(
42            calldata,
43            &[],
44            TipFeeManagerCall::decode,
45            |call| match call {
46                // IFeeManager view functions
47                TipFeeManagerCall::FeeManager(IFeeManagerCalls::userTokens(call)) => {
48                    view(call, |c| self.user_tokens(c))
49                }
50                TipFeeManagerCall::FeeManager(IFeeManagerCalls::validatorTokens(call)) => {
51                    view(call, |c| self.get_validator_token(c.validator))
52                }
53                TipFeeManagerCall::FeeManager(IFeeManagerCalls::collectedFees(call)) => {
54                    view(call, |c| self.collected_fees[c.validator][c.token].read())
55                }
56
57                // IFeeManager mutate functions
58                TipFeeManagerCall::FeeManager(IFeeManagerCalls::setValidatorToken(call)) => {
59                    mutate_void(call, msg_sender, |s, c| {
60                        let beneficiary = self.storage.beneficiary();
61                        self.set_validator_token(s, c, beneficiary)
62                    })
63                }
64                TipFeeManagerCall::FeeManager(IFeeManagerCalls::setUserToken(call)) => {
65                    mutate_void(call, msg_sender, |s, c| self.set_user_token(s, c))
66                }
67                TipFeeManagerCall::FeeManager(IFeeManagerCalls::distributeFees(call)) => {
68                    mutate_void(call, msg_sender, |_, c| {
69                        self.distribute_fees(c.validator, c.token)
70                    })
71                }
72
73                // ITIPFeeAMM metadata functions
74                TipFeeManagerCall::Amm(ITIPFeeAMMCalls::M(_)) => {
75                    metadata::<ITIPFeeAMM::MCall>(|| Ok(M))
76                }
77                TipFeeManagerCall::Amm(ITIPFeeAMMCalls::N(_)) => {
78                    metadata::<ITIPFeeAMM::NCall>(|| Ok(N))
79                }
80                TipFeeManagerCall::Amm(ITIPFeeAMMCalls::SCALE(_)) => {
81                    metadata::<ITIPFeeAMM::SCALECall>(|| Ok(SCALE))
82                }
83                TipFeeManagerCall::Amm(ITIPFeeAMMCalls::MIN_LIQUIDITY(_)) => {
84                    metadata::<ITIPFeeAMM::MIN_LIQUIDITYCall>(|| Ok(MIN_LIQUIDITY))
85                }
86
87                // ITIPFeeAMM view functions
88                TipFeeManagerCall::Amm(ITIPFeeAMMCalls::getPoolId(call)) => {
89                    view(call, |c| Ok(self.pool_id(c.userToken, c.validatorToken)))
90                }
91                TipFeeManagerCall::Amm(ITIPFeeAMMCalls::getPool(call)) => {
92                    view(call, |c| Ok(self.get_pool(c)?.into()))
93                }
94                TipFeeManagerCall::Amm(ITIPFeeAMMCalls::pools(call)) => {
95                    view(call, |c| Ok(self.pools[c.poolId].read()?.into()))
96                }
97                TipFeeManagerCall::Amm(ITIPFeeAMMCalls::totalSupply(call)) => {
98                    view(call, |c| self.total_supply[c.poolId].read())
99                }
100                TipFeeManagerCall::Amm(ITIPFeeAMMCalls::liquidityBalances(call)) => {
101                    view(call, |c| self.liquidity_balances[c.poolId][c.user].read())
102                }
103
104                // ITIPFeeAMM mutate functions
105                TipFeeManagerCall::Amm(ITIPFeeAMMCalls::mint(call)) => {
106                    mutate(call, msg_sender, |s, c| {
107                        self.mint(
108                            s,
109                            c.userToken,
110                            c.validatorToken,
111                            c.amountValidatorToken,
112                            c.to,
113                        )
114                    })
115                }
116                TipFeeManagerCall::Amm(ITIPFeeAMMCalls::burn(call)) => {
117                    mutate(call, msg_sender, |s, c| {
118                        let (amount_user_token, amount_validator_token) =
119                            self.burn(s, c.userToken, c.validatorToken, c.liquidity, c.to)?;
120                        Ok(ITIPFeeAMM::burnReturn {
121                            amountUserToken: amount_user_token,
122                            amountValidatorToken: amount_validator_token,
123                        })
124                    })
125                }
126                TipFeeManagerCall::Amm(ITIPFeeAMMCalls::rebalanceSwap(call)) => {
127                    mutate(call, msg_sender, |s, c| {
128                        self.rebalance_swap(s, c.userToken, c.validatorToken, c.amountOut, c.to)
129                    })
130                }
131            },
132        )
133    }
134}
135
136#[cfg(test)]
137mod tests {
138    use super::*;
139    use crate::{
140        Precompile, expect_precompile_revert,
141        storage::{ContractStorage, StorageCtx, hashmap::HashMapStorageProvider},
142        test_util::{TIP20Setup, assert_full_coverage, check_selector_coverage},
143        tip_fee_manager::{
144            FeeManagerError,
145            amm::{M, MIN_LIQUIDITY, N, PoolKey, SCALE},
146        },
147    };
148    use alloy::{
149        primitives::{Address, B256, U256},
150        sol_types::{SolCall, SolValue},
151    };
152    use tempo_contracts::precompiles::{
153        IFeeManager, IFeeManager::IFeeManagerCalls, ITIPFeeAMM, ITIPFeeAMM::ITIPFeeAMMCalls,
154    };
155
156    #[test]
157    fn test_set_validator_token() -> eyre::Result<()> {
158        let mut storage = HashMapStorageProvider::new(1);
159        let admin = Address::random();
160        let validator = Address::random();
161        StorageCtx::enter(&mut storage, || {
162            let token = TIP20Setup::create("TestToken", "TST", admin).apply()?;
163            let mut fee_manager = TipFeeManager::new();
164
165            let calldata = IFeeManager::setValidatorTokenCall {
166                token: token.address(),
167            }
168            .abi_encode();
169            let result = fee_manager.call(&calldata, validator)?;
170            assert!(result.status.is_success());
171
172            // Verify token was set
173            let calldata = IFeeManager::validatorTokensCall { validator }.abi_encode();
174            let result = fee_manager.call(&calldata, validator)?;
175            assert!(result.status.is_success());
176            let returned_token = Address::abi_decode(&result.bytes)?;
177            assert_eq!(returned_token, token.address());
178
179            Ok(())
180        })
181    }
182
183    #[test]
184    fn test_set_validator_token_zero_address() -> eyre::Result<()> {
185        let mut storage = HashMapStorageProvider::new(1);
186        let validator = Address::random();
187        StorageCtx::enter(&mut storage, || {
188            let mut fee_manager = TipFeeManager::new();
189
190            let calldata = IFeeManager::setValidatorTokenCall {
191                token: Address::ZERO,
192            }
193            .abi_encode();
194            let result = fee_manager.call(&calldata, validator);
195            expect_precompile_revert(&result, FeeManagerError::invalid_token());
196
197            Ok(())
198        })
199    }
200
201    #[test]
202    fn test_set_user_token() -> eyre::Result<()> {
203        let mut storage = HashMapStorageProvider::new(1);
204        let admin = Address::random();
205        let user = Address::random();
206        StorageCtx::enter(&mut storage, || {
207            let token = TIP20Setup::create("TestToken", "TST", admin).apply()?;
208            let mut fee_manager = TipFeeManager::new();
209
210            let calldata = IFeeManager::setUserTokenCall {
211                token: token.address(),
212            }
213            .abi_encode();
214            let result = fee_manager.call(&calldata, user)?;
215            assert!(result.status.is_success());
216
217            // Verify token was set
218            let calldata = IFeeManager::userTokensCall { user }.abi_encode();
219            let result = fee_manager.call(&calldata, user)?;
220            assert!(result.status.is_success());
221            let returned_token = Address::abi_decode(&result.bytes)?;
222            assert_eq!(returned_token, token.address());
223
224            Ok(())
225        })
226    }
227
228    #[test]
229    fn test_set_user_token_zero_address() -> eyre::Result<()> {
230        let mut storage = HashMapStorageProvider::new(1);
231        let user = Address::random();
232        StorageCtx::enter(&mut storage, || {
233            let mut fee_manager = TipFeeManager::new();
234
235            let calldata = IFeeManager::setUserTokenCall {
236                token: Address::ZERO,
237            }
238            .abi_encode();
239            let result = fee_manager.call(&calldata, user);
240            expect_precompile_revert(&result, FeeManagerError::invalid_token());
241
242            Ok(())
243        })
244    }
245
246    #[test]
247    fn test_get_pool_id() -> eyre::Result<()> {
248        let mut storage = HashMapStorageProvider::new(1);
249        let token_a = Address::random();
250        let token_b = Address::random();
251        let sender = Address::random();
252        StorageCtx::enter(&mut storage, || {
253            let mut fee_manager = TipFeeManager::new();
254
255            let calldata = ITIPFeeAMM::getPoolIdCall {
256                userToken: token_a,
257                validatorToken: token_b,
258            }
259            .abi_encode();
260            let result = fee_manager.call(&calldata, sender)?;
261            assert!(result.status.is_success());
262
263            let returned_id = B256::abi_decode(&result.bytes)?;
264            let expected_id = PoolKey::new(token_a, token_b).get_id();
265            assert_eq!(returned_id, expected_id);
266
267            Ok(())
268        })
269    }
270
271    #[test]
272    fn test_tip_fee_amm_pool_operations() -> eyre::Result<()> {
273        let mut storage = HashMapStorageProvider::new(1);
274        let token_a = Address::random();
275        let token_b = Address::random();
276        let sender = Address::random();
277        StorageCtx::enter(&mut storage, || {
278            let mut fee_manager = TipFeeManager::new();
279
280            // Get pool using ITIPFeeAMM interface
281            let get_pool_call = ITIPFeeAMM::getPoolCall {
282                userToken: token_a,
283                validatorToken: token_b,
284            };
285            let calldata = get_pool_call.abi_encode();
286            let result = fee_manager.call(&calldata, sender)?;
287            assert!(result.status.is_success());
288
289            // Decode and verify pool (should be empty initially)
290            let pool = ITIPFeeAMM::Pool::abi_decode(&result.bytes)?;
291            assert_eq!(pool.reserveUserToken, 0);
292            assert_eq!(pool.reserveValidatorToken, 0);
293
294            Ok(())
295        })
296    }
297
298    #[test]
299    fn test_pool_id_calculation() -> eyre::Result<()> {
300        let mut storage = HashMapStorageProvider::new(1);
301        let token_a = Address::random();
302        let token_b = Address::random();
303        let sender = Address::random();
304        StorageCtx::enter(&mut storage, || {
305            let mut fee_manager = TipFeeManager::new();
306
307            // Get pool ID with tokens in order (a, b)
308            let calldata1 = ITIPFeeAMM::getPoolIdCall {
309                userToken: token_a,
310                validatorToken: token_b,
311            }
312            .abi_encode();
313            let result1 = fee_manager.call(&calldata1, sender)?;
314            let id1 = B256::abi_decode(&result1.bytes)?;
315
316            // Get pool ID with tokens reversed (b, a)
317            let calldata2 = ITIPFeeAMM::getPoolIdCall {
318                userToken: token_b,
319                validatorToken: token_a,
320            }
321            .abi_encode();
322            let result2 = fee_manager.call(&calldata2, sender)?;
323            let id2 = B256::abi_decode(&result2.bytes)?;
324
325            // Pool IDs should be different since tokens are ordered
326            assert_ne!(id1, id2);
327
328            Ok(())
329        })
330    }
331
332    #[test]
333    fn test_fee_manager_invalid_token_error() -> eyre::Result<()> {
334        let mut storage = HashMapStorageProvider::new(1);
335        let user = Address::random();
336        let validator = Address::random();
337        StorageCtx::enter(&mut storage, || {
338            let mut fee_manager = TipFeeManager::new();
339
340            // Test setValidatorToken with zero address
341            let set_validator_call = IFeeManager::setValidatorTokenCall {
342                token: Address::ZERO,
343            };
344            let result = fee_manager.call(&set_validator_call.abi_encode(), validator);
345            expect_precompile_revert(&result, FeeManagerError::invalid_token());
346
347            // Test setUserToken with zero address
348            let set_user_call = IFeeManager::setUserTokenCall {
349                token: Address::ZERO,
350            };
351            let result = fee_manager.call(&set_user_call.abi_encode(), user);
352            expect_precompile_revert(&result, FeeManagerError::invalid_token());
353
354            Ok(())
355        })
356    }
357
358    #[test]
359    fn test_amm_constants() -> eyre::Result<()> {
360        let mut storage = HashMapStorageProvider::new(1);
361        let sender = Address::random();
362        StorageCtx::enter(&mut storage, || {
363            let mut fee_manager = TipFeeManager::new();
364
365            let result =
366                fee_manager.call(&ITIPFeeAMM::MIN_LIQUIDITYCall {}.abi_encode(), sender)?;
367            assert!(!result.is_revert());
368            assert_eq!(U256::abi_decode(&result.bytes)?, MIN_LIQUIDITY);
369
370            let result = fee_manager.call(&ITIPFeeAMM::MCall {}.abi_encode(), sender)?;
371            assert_eq!(U256::abi_decode(&result.bytes)?, M);
372
373            let result = fee_manager.call(&ITIPFeeAMM::NCall {}.abi_encode(), sender)?;
374            assert_eq!(U256::abi_decode(&result.bytes)?, N);
375
376            let result = fee_manager.call(&ITIPFeeAMM::SCALECall {}.abi_encode(), sender)?;
377            assert_eq!(U256::abi_decode(&result.bytes)?, SCALE);
378
379            Ok(())
380        })
381    }
382
383    #[test]
384    fn test_tip_fee_manager_selector_coverage() -> eyre::Result<()> {
385        let mut storage = HashMapStorageProvider::new(1);
386        StorageCtx::enter(&mut storage, || {
387            let mut fee_manager = TipFeeManager::new();
388
389            let fee_manager_unsupported = check_selector_coverage(
390                &mut fee_manager,
391                IFeeManagerCalls::SELECTORS,
392                "IFeeManager",
393                IFeeManagerCalls::name_by_selector,
394            );
395
396            let amm_unsupported = check_selector_coverage(
397                &mut fee_manager,
398                ITIPFeeAMMCalls::SELECTORS,
399                "ITIPFeeAMM",
400                ITIPFeeAMMCalls::name_by_selector,
401            );
402
403            assert_full_coverage([fee_manager_unsupported, amm_unsupported]);
404
405            Ok(())
406        })
407    }
408}