tempo_precompiles/path_usd/
dispatch.rs

1use crate::{
2    Precompile, fill_precompile_output, input_cost, metadata, mutate, mutate_void,
3    path_usd::PathUSD,
4    storage::ContractStorage,
5    tip20::{IRolesAuth, ITIP20},
6    view,
7};
8
9use alloy::{primitives::Address, sol_types::SolCall};
10use revm::precompile::{PrecompileError, PrecompileResult};
11use tempo_contracts::precompiles::{IPathUSD, TIP20Error};
12
13impl Precompile for PathUSD {
14    fn call(&mut self, calldata: &[u8], msg_sender: Address) -> PrecompileResult {
15        let selector: [u8; 4] = if let Some(bytes) = calldata.get(..4) {
16            bytes.try_into().unwrap()
17        } else {
18            self.token
19                .storage()
20                .deduct_gas(input_cost(calldata.len()))
21                .map_err(|_| PrecompileError::OutOfGas)?;
22
23            return Err(PrecompileError::Other(
24                "Invalid input: missing function selector".into(),
25            ));
26        };
27
28        // Post allegretto hardfork, treat pathUSD as a default TIP20 without extra permissions
29        // For calls to name() or symbol(), since this contract is already deployed pre hardfork,
30        // we override name/symbol to PathUSD rather than treating these calls with default TIP20 logic
31        if self.token.storage().spec().is_allegretto()
32            && selector != ITIP20::nameCall::SELECTOR
33            && selector != ITIP20::symbolCall::SELECTOR
34        {
35            return self.token.call(calldata, msg_sender);
36        }
37
38        self.token
39            .storage()
40            .deduct_gas(input_cost(calldata.len()))
41            .map_err(|_| PrecompileError::OutOfGas)?;
42
43        let result = match selector {
44            // Metadata
45            ITIP20::nameCall::SELECTOR => metadata::<ITIP20::nameCall>(|| self.name()),
46            ITIP20::symbolCall::SELECTOR => metadata::<ITIP20::symbolCall>(|| self.symbol()),
47            ITIP20::decimalsCall::SELECTOR => metadata::<ITIP20::decimalsCall>(|| self.decimals()),
48            ITIP20::totalSupplyCall::SELECTOR => {
49                metadata::<ITIP20::totalSupplyCall>(|| self.total_supply())
50            }
51            ITIP20::currencyCall::SELECTOR => metadata::<ITIP20::currencyCall>(|| self.currency()),
52            ITIP20::quoteTokenCall::SELECTOR => {
53                view::<ITIP20::quoteTokenCall>(calldata, |_| self.token.quote_token())
54            }
55            ITIP20::pausedCall::SELECTOR => metadata::<ITIP20::pausedCall>(|| self.paused()),
56            ITIP20::supplyCapCall::SELECTOR => {
57                metadata::<ITIP20::supplyCapCall>(|| self.token.supply_cap())
58            }
59            ITIP20::transferPolicyIdCall::SELECTOR => {
60                metadata::<ITIP20::transferPolicyIdCall>(|| self.token.transfer_policy_id())
61            }
62
63            // View functions
64            ITIP20::balanceOfCall::SELECTOR => {
65                view::<ITIP20::balanceOfCall>(calldata, |call| self.balance_of(call))
66            }
67            ITIP20::allowanceCall::SELECTOR => {
68                view::<ITIP20::allowanceCall>(calldata, |call| self.allowance(call))
69            }
70            ITIP20::PAUSE_ROLECall::SELECTOR => {
71                view::<ITIP20::PAUSE_ROLECall>(calldata, |_| Ok(Self::pause_role()))
72            }
73            ITIP20::UNPAUSE_ROLECall::SELECTOR => {
74                view::<ITIP20::UNPAUSE_ROLECall>(calldata, |_| Ok(Self::unpause_role()))
75            }
76            ITIP20::ISSUER_ROLECall::SELECTOR => {
77                view::<ITIP20::ISSUER_ROLECall>(calldata, |_| Ok(Self::issuer_role()))
78            }
79            ITIP20::BURN_BLOCKED_ROLECall::SELECTOR => {
80                view::<ITIP20::BURN_BLOCKED_ROLECall>(calldata, |_| Ok(Self::burn_blocked_role()))
81            }
82            IPathUSD::TRANSFER_ROLECall::SELECTOR => {
83                view::<IPathUSD::TRANSFER_ROLECall>(calldata, |_| Ok(Self::transfer_role()))
84            }
85            IPathUSD::RECEIVE_WITH_MEMO_ROLECall::SELECTOR => {
86                view::<IPathUSD::RECEIVE_WITH_MEMO_ROLECall>(calldata, |_| {
87                    Ok(Self::receive_with_memo_role())
88                })
89            }
90
91            // Mutating functions that work normally
92            ITIP20::approveCall::SELECTOR => {
93                mutate::<ITIP20::approveCall>(calldata, msg_sender, |sender, call| {
94                    self.approve(sender, call)
95                })
96            }
97            ITIP20::mintCall::SELECTOR => {
98                mutate_void::<ITIP20::mintCall>(calldata, msg_sender, |sender, call| {
99                    self.mint(sender, call)
100                })
101            }
102            ITIP20::mintWithMemoCall::SELECTOR => {
103                mutate_void::<ITIP20::mintWithMemoCall>(calldata, msg_sender, |sender, call| {
104                    self.token.mint_with_memo(sender, call)
105                })
106            }
107            ITIP20::burnCall::SELECTOR => {
108                mutate_void::<ITIP20::burnCall>(calldata, msg_sender, |sender, call| {
109                    self.burn(sender, call)
110                })
111            }
112            ITIP20::burnWithMemoCall::SELECTOR => {
113                mutate_void::<ITIP20::burnWithMemoCall>(calldata, msg_sender, |sender, call| {
114                    self.token.burn_with_memo(sender, call)
115                })
116            }
117            ITIP20::burnBlockedCall::SELECTOR => {
118                mutate_void::<ITIP20::burnBlockedCall>(calldata, msg_sender, |sender, call| {
119                    self.token.burn_blocked(sender, call)
120                })
121            }
122            ITIP20::pauseCall::SELECTOR => {
123                mutate_void::<ITIP20::pauseCall>(calldata, msg_sender, |sender, call| {
124                    self.pause(sender, call)
125                })
126            }
127            ITIP20::unpauseCall::SELECTOR => {
128                mutate_void::<ITIP20::unpauseCall>(calldata, msg_sender, |sender, call| {
129                    self.unpause(sender, call)
130                })
131            }
132            ITIP20::changeTransferPolicyIdCall::SELECTOR => {
133                mutate_void::<ITIP20::changeTransferPolicyIdCall>(
134                    calldata,
135                    msg_sender,
136                    |sender, call| self.token.change_transfer_policy_id(sender, call),
137                )
138            }
139            ITIP20::setSupplyCapCall::SELECTOR => {
140                mutate_void::<ITIP20::setSupplyCapCall>(calldata, msg_sender, |sender, call| {
141                    self.token.set_supply_cap(sender, call)
142                })
143            }
144
145            // Transfer functions that are disabled for PathUSD
146            ITIP20::transferCall::SELECTOR => {
147                mutate::<ITIP20::transferCall>(calldata, msg_sender, |sender, call| {
148                    self.transfer(sender, call)
149                })
150            }
151            ITIP20::transferFromCall::SELECTOR => {
152                mutate::<ITIP20::transferFromCall>(calldata, msg_sender, |sender, call| {
153                    self.transfer_from(sender, call)
154                })
155            }
156            ITIP20::transferWithMemoCall::SELECTOR => {
157                mutate_void::<ITIP20::transferWithMemoCall>(calldata, msg_sender, |sender, call| {
158                    self.transfer_with_memo(sender, call)
159                })
160            }
161            ITIP20::transferFromWithMemoCall::SELECTOR => {
162                mutate::<ITIP20::transferFromWithMemoCall>(calldata, msg_sender, |sender, call| {
163                    self.transfer_from_with_memo(sender, call)
164                })
165            }
166
167            ITIP20::startRewardCall::SELECTOR => {
168                mutate::<ITIP20::startRewardCall>(calldata, msg_sender, |_s, _call| {
169                    Err(TIP20Error::rewards_disabled().into())
170                })
171            }
172            ITIP20::setRewardRecipientCall::SELECTOR => {
173                mutate_void::<ITIP20::setRewardRecipientCall>(calldata, msg_sender, |_s, _call| {
174                    Err(TIP20Error::rewards_disabled().into())
175                })
176            }
177            ITIP20::cancelRewardCall::SELECTOR => {
178                mutate::<ITIP20::cancelRewardCall>(calldata, msg_sender, |_s, _call| {
179                    Err(TIP20Error::rewards_disabled().into())
180                })
181            }
182            ITIP20::claimRewardsCall::SELECTOR => {
183                mutate::<ITIP20::claimRewardsCall>(calldata, msg_sender, |_, _| {
184                    Err(TIP20Error::rewards_disabled().into())
185                })
186            }
187
188            // RolesAuth functions
189            IRolesAuth::hasRoleCall::SELECTOR => {
190                view::<IRolesAuth::hasRoleCall>(calldata, |call| self.token.has_role(call))
191            }
192            IRolesAuth::getRoleAdminCall::SELECTOR => {
193                view::<IRolesAuth::getRoleAdminCall>(calldata, |call| {
194                    self.token.get_role_admin(call)
195                })
196            }
197            IRolesAuth::grantRoleCall::SELECTOR => {
198                mutate_void::<IRolesAuth::grantRoleCall>(calldata, msg_sender, |sender, call| {
199                    self.token.grant_role(sender, call)
200                })
201            }
202            IRolesAuth::revokeRoleCall::SELECTOR => {
203                mutate_void::<IRolesAuth::revokeRoleCall>(calldata, msg_sender, |sender, call| {
204                    self.token.revoke_role(sender, call)
205                })
206            }
207            IRolesAuth::renounceRoleCall::SELECTOR => {
208                mutate_void::<IRolesAuth::renounceRoleCall>(calldata, msg_sender, |sender, call| {
209                    self.token.renounce_role(sender, call)
210                })
211            }
212            IRolesAuth::setRoleAdminCall::SELECTOR => {
213                mutate_void::<IRolesAuth::setRoleAdminCall>(calldata, msg_sender, |sender, call| {
214                    self.token.set_role_admin(sender, call)
215                })
216            }
217
218            _ => Err(PrecompileError::Other("Unknown selector".into())),
219        };
220
221        result.map(|res| fill_precompile_output(res, self.token.storage()))
222    }
223}
224
225#[cfg(test)]
226mod tests {
227    use super::*;
228    use crate::{
229        storage::{StorageCtx, hashmap::HashMapStorageProvider},
230        test_util::{assert_full_coverage, check_selector_coverage, setup_storage},
231        tip20::tests::initialize_path_usd,
232    };
233    use alloy::{
234        primitives::{Bytes, U256},
235        sol_types::SolInterface,
236    };
237    use tempo_chainspec::hardfork::TempoHardfork;
238    use tempo_contracts::precompiles::{
239        IRolesAuth::IRolesAuthCalls, ITIP20::ITIP20Calls, TIP20Error,
240    };
241
242    #[test]
243    fn path_usd_test_selector_coverage_pre_allegretto() -> eyre::Result<()> {
244        let mut storage = HashMapStorageProvider::new(1).with_spec(TempoHardfork::Moderato);
245
246        StorageCtx::enter(&mut storage, || {
247            initialize_path_usd(Address::random())?;
248
249            let mut path_usd = PathUSD::new();
250            let itip20_unsupported =
251                check_selector_coverage(&mut path_usd, ITIP20Calls::SELECTORS, "ITIP20", |s| {
252                    ITIP20Calls::name_by_selector(s)
253                });
254
255            let roles_unsupported = check_selector_coverage(
256                &mut path_usd,
257                IRolesAuthCalls::SELECTORS,
258                "IRolesAuth",
259                IRolesAuthCalls::name_by_selector,
260            );
261
262            assert_full_coverage([itip20_unsupported, roles_unsupported]);
263
264            Ok(())
265        })
266    }
267
268    #[test]
269    fn path_usd_test_selector_coverage_post_allegretto() -> eyre::Result<()> {
270        let mut storage = HashMapStorageProvider::new(1).with_spec(TempoHardfork::Allegretto);
271
272        StorageCtx::enter(&mut storage, || {
273            initialize_path_usd(Address::random())?;
274
275            let mut path_usd = PathUSD::new();
276            let itip20_unsupported =
277                check_selector_coverage(&mut path_usd, ITIP20Calls::SELECTORS, "ITIP20", |s| {
278                    ITIP20Calls::name_by_selector(s)
279                });
280
281            let roles_unsupported = check_selector_coverage(
282                &mut path_usd,
283                IRolesAuthCalls::SELECTORS,
284                "IRolesAuth",
285                IRolesAuthCalls::name_by_selector,
286            );
287
288            assert_full_coverage([itip20_unsupported, roles_unsupported]);
289            Ok(())
290        })
291    }
292
293    #[test]
294    fn test_start_reward_disabled_post_moderato() -> eyre::Result<()> {
295        let mut storage = HashMapStorageProvider::new(1).with_spec(TempoHardfork::Moderato);
296        let sender = Address::random();
297
298        StorageCtx::enter(&mut storage, || {
299            let mut token = PathUSD::new();
300            token.initialize(sender)?;
301
302            let calldata = ITIP20::startRewardCall {
303                amount: U256::from(1000),
304                secs: 100,
305            }
306            .abi_encode();
307
308            let output = token.call(&calldata, sender)?;
309            assert!(output.reverted);
310            let expected: Bytes = TIP20Error::rewards_disabled().selector().into();
311            assert_eq!(output.bytes, expected);
312
313            Ok(())
314        })
315    }
316
317    #[test]
318    fn test_set_reward_recipient_disabled_post_moderato() -> eyre::Result<()> {
319        let mut storage = HashMapStorageProvider::new(1).with_spec(TempoHardfork::Moderato);
320        let sender = Address::random();
321        let recipient = Address::random();
322
323        StorageCtx::enter(&mut storage, || {
324            let mut token = PathUSD::new();
325            token.initialize(sender)?;
326
327            let calldata = ITIP20::setRewardRecipientCall { recipient }.abi_encode();
328            let output = token.call(&calldata, sender)?;
329            assert!(output.reverted);
330            let expected: Bytes = TIP20Error::rewards_disabled().selector().into();
331            assert_eq!(output.bytes, expected);
332
333            Ok(())
334        })
335    }
336
337    #[test]
338    fn test_cancel_reward_disabled_post_moderato() -> eyre::Result<()> {
339        let mut storage = HashMapStorageProvider::new(1).with_spec(TempoHardfork::Moderato);
340        let sender = Address::random();
341
342        StorageCtx::enter(&mut storage, || {
343            let mut token = PathUSD::new();
344            token.initialize(sender)?;
345
346            let calldata = ITIP20::cancelRewardCall { id: 1 }.abi_encode();
347
348            let output = token.call(&calldata, sender)?;
349            assert!(output.reverted);
350            let expected: Bytes = TIP20Error::rewards_disabled().selector().into();
351            assert_eq!(output.bytes, expected);
352
353            Ok(())
354        })
355    }
356
357    #[test]
358    fn test_claim_rewards_disabled_post_moderato() -> eyre::Result<()> {
359        let mut storage = HashMapStorageProvider::new(1).with_spec(TempoHardfork::Moderato);
360        let sender = Address::random();
361
362        StorageCtx::enter(&mut storage, || {
363            let mut token = PathUSD::new();
364            token.initialize(sender)?;
365
366            let calldata = ITIP20::claimRewardsCall {}.abi_encode();
367
368            let output = token.call(&calldata, sender)?;
369            assert!(output.reverted);
370            let expected: Bytes = TIP20Error::rewards_disabled().selector().into();
371            assert_eq!(output.bytes, expected);
372
373            Ok(())
374        })
375    }
376
377    #[test]
378    fn test_pre_allegretto_name_symbol() -> eyre::Result<()> {
379        let (mut storage, sender) = setup_storage();
380        storage.set_spec(TempoHardfork::Moderato);
381
382        StorageCtx::enter(&mut storage, || {
383            let mut token = PathUSD::new();
384            token.initialize(sender)?;
385
386            let name_calldata = ITIP20::nameCall {}.abi_encode();
387            let name_output = token.call(&Bytes::from(name_calldata), sender)?;
388            let name = ITIP20::nameCall::abi_decode_returns(&name_output.bytes)?;
389            assert_eq!(name, "linkingUSD");
390
391            let symbol_calldata = ITIP20::symbolCall {}.abi_encode();
392            let symbol_output = token.call(&Bytes::from(symbol_calldata), sender)?;
393            let symbol = ITIP20::symbolCall::abi_decode_returns(&symbol_output.bytes)?;
394            assert_eq!(symbol, "linkingUSD");
395
396            Ok(())
397        })
398    }
399
400    #[test]
401    fn test_post_allegretto_name_symbol() -> eyre::Result<()> {
402        let (mut storage, sender) = setup_storage();
403        storage.set_spec(TempoHardfork::Allegretto);
404
405        StorageCtx::enter(&mut storage, || {
406            let mut token = PathUSD::new();
407            token.initialize(sender)?;
408
409            let name_calldata = ITIP20::nameCall {}.abi_encode();
410            let name_output = token.call(&Bytes::from(name_calldata), sender)?;
411            let name = ITIP20::nameCall::abi_decode_returns(&name_output.bytes)?;
412            assert_eq!(name, "pathUSD");
413
414            let symbol_calldata = ITIP20::symbolCall {}.abi_encode();
415            let symbol_output = token.call(&Bytes::from(symbol_calldata), sender)?;
416            let symbol = ITIP20::symbolCall::abi_decode_returns(&symbol_output.bytes)?;
417            assert_eq!(symbol, "pathUSD");
418
419            Ok(())
420        })
421    }
422}