referrerpolicy=no-referrer-when-downgrade

pallet_assets_precompiles/
lib.rs

1// This file is part of Substrate.
2
3// Copyright (C) Parity Technologies (UK) Ltd.
4// SPDX-License-Identifier: Apache-2.0
5
6// Licensed under the Apache License, Version 2.0 (the "License");
7// you may not use this file except in compliance with the License.
8// You may obtain a copy of the License at
9//
10// 	http://www.apache.org/licenses/LICENSE-2.0
11//
12// Unless required by applicable law or agreed to in writing, software
13// distributed under the License is distributed on an "AS IS" BASIS,
14// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15// See the License for the specific language governing permissions and
16// limitations under the License.
17
18// Ensure we're `no_std` when compiling for Wasm.
19#![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
69/// Means of extracting the asset id from the precompile address.
70pub trait AssetIdExtractor {
71	type AssetId;
72	/// Extracts the asset id from the address.
73	fn asset_id_from_address(address: &[u8; 20]) -> Result<Self::AssetId, Error>;
74}
75
76/// The configuration of a pallet-assets precompile.
77pub trait AssetPrecompileConfig {
78	/// The Address matcher used by the precompile.
79	const MATCHER: AddressMatcher;
80
81	/// The [`AssetIdExtractor`] used by the precompile.
82	type AssetIdExtractor: AssetIdExtractor;
83}
84
85/// An `AssetIdExtractor` that stores the asset id directly inside the address.
86pub 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
97/// A precompile configuration that uses a prefix [`AddressMatcher`].
98pub 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
105/// An `AssetIdExtractor` that maps a local asset id (4 bytes taken from the address) to a foreign
106/// asset id.
107pub 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
126/// A precompile configuration that uses a prefix [`AddressMatcher`].
127pub 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
142/// An ERC20 precompile with EIP-2612 permit support.
143pub 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			// State-changing calls - check read-only
178			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			// ERC20 functions
188			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			// ERC20Permit functions (EIP-2612)
196			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			// ERC20Metadata functions
203			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	/// Get the caller as an `H160` address.
224	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	/// Convert a `U256` value to the balance type of the pallet.
232	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	/// Convert a balance to a `U256` value.
241	/// Note: this is needed because `From` is not implemented for unsigned integer types.
242	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	/// Deposit an event to the runtime.
250	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	/// Execute the transfer call.
262	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	/// Execute the total supply call.
297	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	/// Execute the balance_of call.
310	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	/// Execute the allowance call.
324	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	/// Execute the approve call.
344	///
345	/// Implements ERC-20 set semantics: `approve(spender, N)` sets the allowance to exactly `N`
346	/// rather than adding to it. When overwriting a non-zero allowance, the existing approval is
347	/// cancelled first so the new value replaces (not accumulates with) the old one.
348	///
349	/// `call.value > Balance::MAX` (the `type(uint256).max` "infinite allowance" idiom)
350	/// saturates the stored allowance at `Balance::MAX`. The `Approval` event carries the
351	/// raw `call.value`.
352	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		// Reserve worst-case gas upfront, then refund the unused portion.
360		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		// Saturate: `type(uint256).max` is the standard "infinite allowance" idiom and must
371		// not revert at the conversion boundary.
372		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				// Revoke: use the pallet's cancel logic to remove the approval and
384				// unreserve the deposit.
385				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				// 0โ†’0 no-op: only the allowance read was needed.
394				actual_weight = <Runtime as Config<Instance>>::WeightInfo::allowance();
395			}
396		} else {
397			// If there's an existing non-zero allowance, cancel it first so we
398			// overwrite (not accumulate) โ€” matching ERC-20 spec semantics.
399			// NOTE: This does not mitigate the well-known ERC-20 approve front-running
400			// race condition. Callers concerned about this should approve to 0 first,
401			// or use increaseAllowance/decreaseAllowance if available.
402			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	/// Execute the transfer_from call.
435	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	// ==================== ERC20Permit Functions (EIP-2612) ====================
472
473	/// Execute the permit call (EIP-2612).
474	///
475	/// This verifies the signature, consumes the permit (increments nonce),
476	/// and sets the approval. Saturation policy and event payload match `approve` โ€”
477	/// see its doc-comment.
478	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		// Reserve worst-case gas upfront, then refund the unused portion.
485		// The total cost is: use_permit (signature verification + nonce) +
486		// worst-case asset approval operations (allowance read + cancel + approve).
487		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		// Convert U256 values to byte arrays
498		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				// Use the permit - this validates deadline, signature, and increments nonce
506				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				// Delete-set semantic: cancel any existing approval first so
534				// do_approve_transfer sets (not accumulates) the new value.
535				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				// Saturate: see `approve` for the rationale (infinite-allowance idiom).
542				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						// clear approval if it exists, to match ERC-20 semantics of setting
554						// allowance to 0
555						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						// noop: set allowance to zerowhen it is already zero
567						actual_weight = use_permit_weight
568							.saturating_add(<Runtime as Config<Instance>>::WeightInfo::allowance());
569					}
570				} else {
571					if !current.is_zero() {
572						// If there's an existing non-zero allowance, cancel it first
573						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						// set new approval
581						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				// Emit Approval event
596				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		// permit returns void
618		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	/// Get the current nonce for an owner address.
628	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		// Convert sp_core::U256 to alloy U256
639		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	/// Get the EIP-712 domain separator for this contract.
646	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		// Fetch token name for EIP-712 domain separator (per EIP-2612 spec)
654		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	/// Execute the name call.
664	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	/// Execute the symbol call.
680	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	/// Execute the decimals call.
696	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}