tempo_precompiles/path_usd/
dispatch.rs1use 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 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 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 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 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 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 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}