Skip to main content

tempo_precompiles/tip_fee_manager/
dispatch.rs

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