1#![cfg_attr(not(feature = "std"), no_std)]
20
21extern crate alloc;
22
23use alloc::vec::Vec;
24use core::marker::PhantomData;
25use ethereum_standards::{
26 IERC20,
27 IERC20::{IERC20Calls, IERC20Events},
28};
29use frame_support::traits::fungibles::metadata::Inspect as MetadataInspect;
30use pallet_assets::{weights::WeightInfo as _, Call, Config, TransferFlags};
31use pallet_revive::precompiles::{
32 alloy::{
33 self,
34 primitives::IntoLogData,
35 sol_types::{Revert, SolCall},
36 },
37 AddressMapper, AddressMatcher, Error, Ext, Precompile, RuntimeCosts, H160, H256,
38};
39use sp_runtime::traits::{UniqueSaturatedInto, Zero};
40use weights::WeightInfo as _;
41
42pub mod foreign_assets;
43pub mod migration;
44pub mod permit;
45pub mod weights;
46
47#[cfg(feature = "runtime-benchmarks")]
48pub(crate) mod benchmarking;
49
50#[cfg(test)]
51mod foreign_assets_tests;
52#[cfg(test)]
53mod migration_tests;
54#[cfg(test)]
55mod mock;
56#[cfg(test)]
57mod permit_precompile_tests;
58#[cfg(test)]
59mod permit_tests;
60#[cfg(test)]
61mod test_helpers;
62#[cfg(test)]
63mod tests;
64
65pub use foreign_assets::{pallet, pallet::Config as ForeignAssetsConfig, ForeignAssetId};
66pub use migration::MigrateForeignAssetPrecompileMappings;
67pub use permit::pallet::Config as PermitConfig;
68
69pub trait AssetIdExtractor {
71 type AssetId;
72 fn asset_id_from_address(address: &[u8; 20]) -> Result<Self::AssetId, Error>;
74}
75
76pub trait AssetPrecompileConfig {
78 const MATCHER: AddressMatcher;
80
81 type AssetIdExtractor: AssetIdExtractor;
83}
84
85pub struct InlineAssetIdExtractor;
87
88impl AssetIdExtractor for InlineAssetIdExtractor {
89 type AssetId = u32;
90 fn asset_id_from_address(addr: &[u8; 20]) -> Result<Self::AssetId, Error> {
91 let bytes: [u8; 4] = addr[0..4].try_into().expect("slice is 4 bytes; qed");
92 let index = u32::from_be_bytes(bytes);
93 Ok(index)
94 }
95}
96
97pub struct InlineIdConfig<const PREFIX: u16>;
99
100impl<const P: u16> AssetPrecompileConfig for InlineIdConfig<P> {
101 const MATCHER: AddressMatcher = AddressMatcher::Prefix(core::num::NonZero::new(P).unwrap());
102 type AssetIdExtractor = InlineAssetIdExtractor;
103}
104
105pub struct ForeignAssetIdExtractor<Runtime, Instance = ()> {
108 _phantom: PhantomData<(Runtime, Instance)>,
109}
110
111impl<Runtime, Instance: 'static> AssetIdExtractor for ForeignAssetIdExtractor<Runtime, Instance>
112where
113 Runtime: pallet_assets::Config<Instance>
114 + pallet::Config<ForeignAssetId = <Runtime as pallet_assets::Config<Instance>>::AssetId>
115 + pallet_revive::Config,
116{
117 type AssetId = <Runtime as pallet_assets::Config<Instance>>::AssetId;
118 fn asset_id_from_address(addr: &[u8; 20]) -> Result<Self::AssetId, Error> {
119 let bytes: [u8; 4] = addr[0..4].try_into().expect("slice is 4 bytes; qed");
120 let index = u32::from_be_bytes(bytes);
121 pallet::Pallet::<Runtime>::asset_id_of(index)
122 .ok_or(Error::Revert(Revert { reason: "Invalid foreign asset id".into() }))
123 }
124}
125
126pub struct ForeignIdConfig<const PREFIX: u16, Runtime, Instance = ()> {
128 _phantom: PhantomData<(Runtime, Instance)>,
129}
130
131impl<const P: u16, Runtime, Instance: 'static> AssetPrecompileConfig
132 for ForeignIdConfig<P, Runtime, Instance>
133where
134 Runtime: pallet_assets::Config<Instance>
135 + pallet::Config<ForeignAssetId = <Runtime as pallet_assets::Config<Instance>>::AssetId>
136 + pallet_revive::Config,
137{
138 const MATCHER: AddressMatcher = AddressMatcher::Prefix(core::num::NonZero::new(P).unwrap());
139 type AssetIdExtractor = ForeignAssetIdExtractor<Runtime, Instance>;
140}
141
142pub struct ERC20<Runtime, PrecompileConfig, Instance = ()> {
144 _phantom: PhantomData<(Runtime, PrecompileConfig, Instance)>,
145}
146
147impl<Runtime, PrecompileConfig, Instance: 'static> Precompile
148 for ERC20<Runtime, PrecompileConfig, Instance>
149where
150 PrecompileConfig: AssetPrecompileConfig,
151 Runtime: crate::Config<Instance> + pallet_revive::Config + permit::Config,
152 <<PrecompileConfig as AssetPrecompileConfig>::AssetIdExtractor as AssetIdExtractor>::AssetId:
153 Into<<Runtime as Config<Instance>>::AssetId>,
154 Call<Runtime, Instance>: Into<<Runtime as pallet_revive::Config>::RuntimeCall>,
155 alloy::primitives::U256: TryInto<<Runtime as Config<Instance>>::Balance>,
156 alloy::primitives::U256: TryFrom<<Runtime as Config<Instance>>::Balance>,
157{
158 type T = Runtime;
159 type Interface = IERC20::IERC20Calls;
160 const MATCHER: AddressMatcher = PrecompileConfig::MATCHER;
161 const HAS_CONTRACT_INFO: bool = false;
162
163 fn call(
164 address: &[u8; 20],
165 input: &Self::Interface,
166 env: &mut impl Ext<T = Self::T>,
167 ) -> Result<Vec<u8>, Error> {
168 frame_support::ensure!(
169 !env.is_delegate_call(),
170 pallet_revive::Error::<Self::T>::PrecompileDelegateDenied,
171 );
172
173 let asset_id = PrecompileConfig::AssetIdExtractor::asset_id_from_address(address)?.into();
174 let contract_addr = H160::from(*address);
175
176 match input {
177 IERC20Calls::transfer(_) |
179 IERC20Calls::approve(_) |
180 IERC20Calls::transferFrom(_) |
181 IERC20Calls::permit(_)
182 if env.is_read_only() =>
183 {
184 Err(Error::Error(pallet_revive::Error::<Self::T>::StateChangeDenied.into()))
185 },
186
187 IERC20Calls::transfer(call) => Self::transfer(asset_id, call, env),
189 IERC20Calls::totalSupply(_) => Self::total_supply(asset_id, env),
190 IERC20Calls::balanceOf(call) => Self::balance_of(asset_id, call, env),
191 IERC20Calls::allowance(call) => Self::allowance(asset_id, call, env),
192 IERC20Calls::approve(call) => Self::approve(asset_id, call, env),
193 IERC20Calls::transferFrom(call) => Self::transfer_from(asset_id, call, env),
194
195 IERC20Calls::permit(call) => Self::permit(asset_id, contract_addr, call, env),
197 IERC20Calls::nonces(call) => Self::nonces(contract_addr, call, env),
198 IERC20Calls::DOMAIN_SEPARATOR(_) => {
199 Self::domain_separator(asset_id, contract_addr, env)
200 },
201
202 IERC20Calls::name(_) => Self::name(asset_id, env),
204 IERC20Calls::symbol(_) => Self::symbol(asset_id, env),
205 IERC20Calls::decimals(_) => Self::decimals(asset_id, env),
206 }
207 }
208}
209
210const ERR_INVALID_CALLER: &str = "Invalid caller";
211const ERR_BALANCE_CONVERSION_FAILED: &str = "Balance conversion failed";
212
213impl<Runtime, PrecompileConfig, Instance: 'static> ERC20<Runtime, PrecompileConfig, Instance>
214where
215 PrecompileConfig: AssetPrecompileConfig,
216 Runtime: crate::Config<Instance> + pallet_revive::Config + permit::Config,
217 <<PrecompileConfig as AssetPrecompileConfig>::AssetIdExtractor as AssetIdExtractor>::AssetId:
218 Into<<Runtime as Config<Instance>>::AssetId>,
219 Call<Runtime, Instance>: Into<<Runtime as pallet_revive::Config>::RuntimeCall>,
220 alloy::primitives::U256: TryInto<<Runtime as Config<Instance>>::Balance>,
221 alloy::primitives::U256: TryFrom<<Runtime as Config<Instance>>::Balance>,
222{
223 fn caller(env: &mut impl Ext<T = Runtime>) -> Result<H160, Error> {
225 env.caller()
226 .account_id()
227 .map(<Runtime as pallet_revive::Config>::AddressMapper::to_address)
228 .map_err(|_| Error::Revert(Revert { reason: ERR_INVALID_CALLER.into() }))
229 }
230
231 fn to_balance(
233 value: alloy::primitives::U256,
234 ) -> Result<<Runtime as Config<Instance>>::Balance, Error> {
235 value
236 .try_into()
237 .map_err(|_| Error::Revert(Revert { reason: ERR_BALANCE_CONVERSION_FAILED.into() }))
238 }
239
240 fn to_u256(
243 value: <Runtime as Config<Instance>>::Balance,
244 ) -> Result<alloy::primitives::U256, Error> {
245 alloy::primitives::U256::try_from(value)
246 .map_err(|_| Error::Revert(Revert { reason: ERR_BALANCE_CONVERSION_FAILED.into() }))
247 }
248
249 fn deposit_event(env: &mut impl Ext<T = Runtime>, event: IERC20Events) -> Result<(), Error> {
251 let (topics, data) = event.into_log_data().split();
252 let topics = topics.into_iter().map(|v| H256(v.0)).collect::<Vec<_>>();
253 env.frame_meter_mut().charge_weight_token(RuntimeCosts::DepositEvent {
254 num_topic: topics.len() as u32,
255 len: data.len() as u32,
256 })?;
257 env.deposit_event(topics, data.to_vec());
258 Ok(())
259 }
260
261 fn transfer(
263 asset_id: <Runtime as Config<Instance>>::AssetId,
264 call: &IERC20::transferCall,
265 env: &mut impl Ext<T = Runtime>,
266 ) -> Result<Vec<u8>, Error> {
267 env.charge(<Runtime as Config<Instance>>::WeightInfo::transfer())?;
268
269 let from = Self::caller(env)?;
270 let dest = <Runtime as pallet_revive::Config>::AddressMapper::to_account_id(
271 &call.to.into_array().into(),
272 );
273
274 let f = TransferFlags { keep_alive: false, best_effort: false, burn_dust: false };
275 pallet_assets::Pallet::<Runtime, Instance>::do_transfer(
276 asset_id,
277 &<Runtime as pallet_revive::Config>::AddressMapper::to_account_id(&from),
278 &dest,
279 Self::to_balance(call.value)?,
280 None,
281 f,
282 )?;
283
284 Self::deposit_event(
285 env,
286 IERC20Events::Transfer(IERC20::Transfer {
287 from: from.0.into(),
288 to: call.to,
289 value: call.value,
290 }),
291 )?;
292
293 Ok(IERC20::transferCall::abi_encode_returns(&true))
294 }
295
296 fn total_supply(
298 asset_id: <Runtime as Config<Instance>>::AssetId,
299 env: &mut impl Ext<T = Runtime>,
300 ) -> Result<Vec<u8>, Error> {
301 use frame_support::traits::fungibles::Inspect;
302 env.charge(<Runtime as Config<Instance>>::WeightInfo::total_issuance())?;
303
304 let value =
305 Self::to_u256(pallet_assets::Pallet::<Runtime, Instance>::total_issuance(asset_id))?;
306 Ok(IERC20::totalSupplyCall::abi_encode_returns(&value))
307 }
308
309 fn balance_of(
311 asset_id: <Runtime as Config<Instance>>::AssetId,
312 call: &IERC20::balanceOfCall,
313 env: &mut impl Ext<T = Runtime>,
314 ) -> Result<Vec<u8>, Error> {
315 env.charge(<Runtime as Config<Instance>>::WeightInfo::balance())?;
316 let account = call.account.into_array().into();
317 let account = <Runtime as pallet_revive::Config>::AddressMapper::to_account_id(&account);
318 let value =
319 Self::to_u256(pallet_assets::Pallet::<Runtime, Instance>::balance(asset_id, account))?;
320 Ok(IERC20::balanceOfCall::abi_encode_returns(&value))
321 }
322
323 fn allowance(
325 asset_id: <Runtime as Config<Instance>>::AssetId,
326 call: &IERC20::allowanceCall,
327 env: &mut impl Ext<T = Runtime>,
328 ) -> Result<Vec<u8>, Error> {
329 env.charge(<Runtime as Config<Instance>>::WeightInfo::allowance())?;
330 use frame_support::traits::fungibles::approvals::Inspect;
331 let owner = call.owner.into_array().into();
332 let owner = <Runtime as pallet_revive::Config>::AddressMapper::to_account_id(&owner);
333
334 let spender = call.spender.into_array().into();
335 let spender = <Runtime as pallet_revive::Config>::AddressMapper::to_account_id(&spender);
336 let value = Self::to_u256(pallet_assets::Pallet::<Runtime, Instance>::allowance(
337 asset_id, &owner, &spender,
338 ))?;
339
340 Ok(IERC20::allowanceCall::abi_encode_returns(&value))
341 }
342
343 fn approve(
353 asset_id: <Runtime as Config<Instance>>::AssetId,
354 call: &IERC20::approveCall,
355 env: &mut impl Ext<T = Runtime>,
356 ) -> Result<Vec<u8>, Error> {
357 use frame_support::traits::fungibles::approvals::Inspect as ApprovalsInspect;
358
359 let worst_case = <Runtime as Config<Instance>>::WeightInfo::allowance()
361 .saturating_add(<Runtime as Config<Instance>>::WeightInfo::cancel_approval())
362 .saturating_add(<Runtime as Config<Instance>>::WeightInfo::approve_transfer());
363 let charged = env.charge(worst_case)?;
364
365 let owner = Self::caller(env)?;
366 let owner_account =
367 <Runtime as pallet_revive::Config>::AddressMapper::to_account_id(&owner);
368 let spender: H160 = call.spender.into_array().into();
369 let spender_account = env.to_account_id(&spender);
370 let new_amount: <Runtime as Config<Instance>>::Balance = call.value.unique_saturated_into();
373
374 let current = pallet_assets::Pallet::<Runtime, Instance>::allowance(
375 asset_id.clone(),
376 &owner_account,
377 &spender_account,
378 );
379
380 let actual_weight;
381 if new_amount.is_zero() {
382 if !current.is_zero() {
383 pallet_assets::Pallet::<Runtime, Instance>::do_cancel_approval(
386 &asset_id,
387 &owner_account,
388 &spender_account,
389 )?;
390 actual_weight = <Runtime as Config<Instance>>::WeightInfo::allowance()
391 .saturating_add(<Runtime as Config<Instance>>::WeightInfo::cancel_approval());
392 } else {
393 actual_weight = <Runtime as Config<Instance>>::WeightInfo::allowance();
395 }
396 } else {
397 if !current.is_zero() {
403 pallet_assets::Pallet::<Runtime, Instance>::do_cancel_approval(
404 &asset_id,
405 &owner_account,
406 &spender_account,
407 )?;
408 actual_weight = worst_case;
409 } else {
410 actual_weight = <Runtime as Config<Instance>>::WeightInfo::allowance()
411 .saturating_add(<Runtime as Config<Instance>>::WeightInfo::approve_transfer());
412 }
413 pallet_assets::Pallet::<Runtime, Instance>::do_approve_transfer(
414 asset_id,
415 &owner_account,
416 &spender_account,
417 new_amount,
418 )?;
419 }
420 env.adjust_gas(charged, actual_weight);
421
422 Self::deposit_event(
423 env,
424 IERC20Events::Approval(IERC20::Approval {
425 owner: owner.0.into(),
426 spender: call.spender,
427 value: call.value,
428 }),
429 )?;
430
431 Ok(IERC20::approveCall::abi_encode_returns(&true))
432 }
433
434 fn transfer_from(
436 asset_id: <Runtime as Config<Instance>>::AssetId,
437 call: &IERC20::transferFromCall,
438 env: &mut impl Ext<T = Runtime>,
439 ) -> Result<Vec<u8>, Error> {
440 env.charge(<Runtime as Config<Instance>>::WeightInfo::transfer_approved())?;
441 let spender = Self::caller(env)?;
442 let spender = <Runtime as pallet_revive::Config>::AddressMapper::to_account_id(&spender);
443
444 let from = call.from.into_array().into();
445 let from = <Runtime as pallet_revive::Config>::AddressMapper::to_account_id(&from);
446
447 let to = call.to.into_array().into();
448 let to = <Runtime as pallet_revive::Config>::AddressMapper::to_account_id(&to);
449
450 let approval_amount = Self::to_balance(call.value)?;
451 pallet_assets::Pallet::<Runtime, Instance>::do_transfer_approved(
452 asset_id,
453 &from,
454 &spender,
455 &to,
456 approval_amount,
457 )?;
458
459 Self::deposit_event(
460 env,
461 IERC20Events::Transfer(IERC20::Transfer {
462 from: call.from,
463 to: call.to,
464 value: call.value,
465 }),
466 )?;
467
468 Ok(IERC20::transferFromCall::abi_encode_returns(&true))
469 }
470
471 pub(crate) fn permit(
479 asset_id: <Runtime as Config<Instance>>::AssetId,
480 verifying_contract: H160,
481 call: &IERC20::permitCall,
482 env: &mut impl Ext<T = Runtime>,
483 ) -> Result<Vec<u8>, Error> {
484 let use_permit_weight = <Runtime as permit::Config>::WeightInfo::use_permit();
488 let worst_case = use_permit_weight
489 .saturating_add(<Runtime as Config<Instance>>::WeightInfo::allowance())
490 .saturating_add(<Runtime as Config<Instance>>::WeightInfo::cancel_approval())
491 .saturating_add(<Runtime as Config<Instance>>::WeightInfo::approve_transfer());
492 let charged = env.charge(worst_case)?;
493
494 let owner_h160: H160 = call.owner.into_array().into();
495 let spender_h160: H160 = call.spender.into_array().into();
496
497 let value_bytes: [u8; 32] = call.value.to_be_bytes();
499 let deadline_bytes: [u8; 32] = call.deadline.to_be_bytes();
500 let r_bytes: [u8; 32] = call.r.0;
501 let s_bytes: [u8; 32] = call.s.0;
502
503 let transaction_outcome = frame_support::storage::with_transaction(|| {
504 let result = (|| {
505 permit::Pallet::<Runtime>::use_permit(
507 &verifying_contract,
508 &pallet_assets::Pallet::<Runtime, Instance>::name(asset_id.clone()),
509 &owner_h160,
510 &spender_h160,
511 &value_bytes,
512 &deadline_bytes,
513 call.v,
514 &r_bytes,
515 &s_bytes,
516 )
517 .map_err(|e| {
518 let msg = match e {
519 permit::pallet::Error::PermitExpired => "Permit expired",
520 permit::pallet::Error::InvalidSignature => "Invalid signature",
521 permit::pallet::Error::SignerMismatch => "Signer does not match owner",
522 permit::pallet::Error::SignatureSValueTooHigh => {
523 "Signature s value too high (malleability)"
524 },
525 permit::pallet::Error::InvalidVValue => "Invalid signature v value",
526 permit::pallet::Error::NonceOverflow => "Nonce overflow",
527 permit::pallet::Error::InvalidOwner => "Invalid owner address",
528 permit::pallet::Error::InvalidSpender => "Invalid spender address",
529 };
530 Error::Revert(Revert { reason: msg.into() })
531 })?;
532
533 use frame_support::traits::fungibles::approvals::Inspect as ApprovalsInspect;
536 let owner_account =
537 <Runtime as pallet_revive::Config>::AddressMapper::to_account_id(&owner_h160);
538 let spender_account =
539 <Runtime as pallet_revive::Config>::AddressMapper::to_account_id(&spender_h160);
540
541 let new_amount: <Runtime as Config<Instance>>::Balance =
543 call.value.unique_saturated_into();
544 let current = pallet_assets::Pallet::<Runtime, Instance>::allowance(
545 asset_id.clone(),
546 &owner_account,
547 &spender_account,
548 );
549
550 let actual_weight;
551 if new_amount.is_zero() {
552 if !current.is_zero() {
553 pallet_assets::Pallet::<Runtime, Instance>::do_cancel_approval(
556 &asset_id,
557 &owner_account,
558 &spender_account,
559 )?;
560 actual_weight = use_permit_weight
561 .saturating_add(<Runtime as Config<Instance>>::WeightInfo::allowance())
562 .saturating_add(
563 <Runtime as Config<Instance>>::WeightInfo::cancel_approval(),
564 );
565 } else {
566 actual_weight = use_permit_weight
568 .saturating_add(<Runtime as Config<Instance>>::WeightInfo::allowance());
569 }
570 } else {
571 if !current.is_zero() {
572 pallet_assets::Pallet::<Runtime, Instance>::do_cancel_approval(
574 &asset_id,
575 &owner_account,
576 &spender_account,
577 )?;
578 actual_weight = worst_case;
579 } else {
580 actual_weight = use_permit_weight
582 .saturating_add(<Runtime as Config<Instance>>::WeightInfo::allowance())
583 .saturating_add(
584 <Runtime as Config<Instance>>::WeightInfo::approve_transfer(),
585 );
586 }
587 pallet_assets::Pallet::<Runtime, Instance>::do_approve_transfer(
588 asset_id,
589 &owner_account,
590 &spender_account,
591 new_amount,
592 )?;
593 }
594
595 Self::deposit_event(
597 env,
598 IERC20Events::Approval(IERC20::Approval {
599 owner: call.owner,
600 spender: call.spender,
601 value: call.value,
602 }),
603 )?;
604 Ok::<_, Error>(actual_weight)
605 })();
606 match result {
607 Ok(actual_weight) => {
608 frame_support::storage::TransactionOutcome::Commit(Ok(actual_weight))
609 },
610 Err(e) => {
611 log::trace!(target: frame_support::LOG_TARGET, "Call to permit failed: {e:?}");
612 frame_support::storage::TransactionOutcome::Rollback(Err(e))
613 },
614 }
615 });
616
617 match transaction_outcome {
619 Ok(actual_weight) => {
620 env.adjust_gas(charged, actual_weight);
621 Ok(Vec::new())
622 },
623 Err(e) => Err(e),
624 }
625 }
626
627 fn nonces(
629 verifying_contract: H160,
630 call: &IERC20::noncesCall,
631 env: &mut impl Ext<T = Runtime>,
632 ) -> Result<Vec<u8>, Error> {
633 env.charge(<Runtime as permit::Config>::WeightInfo::nonces())?;
634
635 let owner_h160: H160 = call.owner.into_array().into();
636 let nonce = permit::Pallet::<Runtime>::nonce(&verifying_contract, &owner_h160);
637
638 let nonce_bytes = nonce.to_big_endian();
640 let nonce_alloy = alloy::primitives::U256::from_be_bytes(nonce_bytes);
641
642 Ok(IERC20::noncesCall::abi_encode_returns(&nonce_alloy))
643 }
644
645 fn domain_separator(
647 asset_id: <Runtime as Config<Instance>>::AssetId,
648 verifying_contract: H160,
649 env: &mut impl Ext<T = Runtime>,
650 ) -> Result<Vec<u8>, Error> {
651 env.charge(<Runtime as permit::Config>::WeightInfo::domain_separator())?;
652
653 let token_name = pallet_assets::Pallet::<Runtime, Instance>::name(asset_id);
655
656 let separator =
657 permit::Pallet::<Runtime>::compute_domain_separator(&verifying_contract, &token_name);
658 let separator_alloy: alloy::primitives::FixedBytes<32> = separator.0.into();
659
660 Ok(IERC20::DOMAIN_SEPARATORCall::abi_encode_returns(&separator_alloy))
661 }
662
663 fn name(
665 asset_id: <Runtime as Config<Instance>>::AssetId,
666 env: &mut impl Ext<T = Runtime>,
667 ) -> Result<Vec<u8>, Error> {
668 env.charge(<Runtime as Config<Instance>>::WeightInfo::get_metadata())?;
669
670 let metadata = pallet_assets::Pallet::<Runtime, Instance>::get_metadata(asset_id)
671 .ok_or(Error::Revert(Revert { reason: "Metadata not found".into() }))?;
672
673 let name = alloc::string::String::from_utf8(metadata.name.to_vec())
674 .map_err(|_| Error::Revert(Revert { reason: "Invalid UTF-8 in name".into() }))?;
675
676 Ok(IERC20::nameCall::abi_encode_returns(&name))
677 }
678
679 fn symbol(
681 asset_id: <Runtime as Config<Instance>>::AssetId,
682 env: &mut impl Ext<T = Runtime>,
683 ) -> Result<Vec<u8>, Error> {
684 env.charge(<Runtime as Config<Instance>>::WeightInfo::get_metadata())?;
685
686 let metadata = pallet_assets::Pallet::<Runtime, Instance>::get_metadata(asset_id)
687 .ok_or(Error::Revert(Revert { reason: "Metadata not found".into() }))?;
688
689 let symbol = alloc::string::String::from_utf8(metadata.symbol.to_vec())
690 .map_err(|_| Error::Revert(Revert { reason: "Invalid UTF-8 in symbol".into() }))?;
691
692 Ok(IERC20::symbolCall::abi_encode_returns(&symbol))
693 }
694
695 fn decimals(
697 asset_id: <Runtime as Config<Instance>>::AssetId,
698 env: &mut impl Ext<T = Runtime>,
699 ) -> Result<Vec<u8>, Error> {
700 env.charge(<Runtime as Config<Instance>>::WeightInfo::get_metadata())?;
701
702 let metadata = pallet_assets::Pallet::<Runtime, Instance>::get_metadata(asset_id)
703 .ok_or(Error::Revert(Revert { reason: "Metadata not found".into() }))?;
704
705 Ok(IERC20::decimalsCall::abi_encode_returns(&metadata.decimals))
706 }
707}