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 _, AssetsCallback, 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};
40
41pub mod foreign_assets;
42pub mod migration;
43pub mod permit;
44pub mod weights;
45pub use weights::WeightInfo;
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	/// The `u32` index `id` maps to, encoded into its precompile address; `None` if unmapped.
75	fn asset_index(id: &Self::AssetId) -> Option<u32>;
76}
77
78/// The configuration of a pallet-assets precompile.
79pub trait AssetPrecompileConfig {
80	/// The Address matcher used by the precompile.
81	const MATCHER: AddressMatcher;
82
83	/// The [`AssetIdExtractor`] used by the precompile.
84	type AssetIdExtractor: AssetIdExtractor;
85}
86
87/// An `AssetIdExtractor` that stores the asset id directly inside the address.
88pub struct InlineAssetIdExtractor;
89
90impl AssetIdExtractor for InlineAssetIdExtractor {
91	type AssetId = u32;
92	fn asset_id_from_address(addr: &[u8; 20]) -> Result<Self::AssetId, Error> {
93		let bytes: [u8; 4] = addr[0..4].try_into().expect("slice is 4 bytes; qed");
94		let index = u32::from_be_bytes(bytes);
95		Ok(index)
96	}
97	fn asset_index(id: &Self::AssetId) -> Option<u32> {
98		Some(*id)
99	}
100}
101
102/// A precompile configuration that uses a prefix [`AddressMatcher`].
103pub struct InlineIdConfig<const PREFIX: u16>;
104
105impl<const P: u16> AssetPrecompileConfig for InlineIdConfig<P> {
106	const MATCHER: AddressMatcher = AddressMatcher::Prefix(core::num::NonZero::new(P).unwrap());
107	type AssetIdExtractor = InlineAssetIdExtractor;
108}
109
110/// An `AssetIdExtractor` that maps a local asset id (4 bytes taken from the address) to a foreign
111/// asset id.
112pub struct ForeignAssetIdExtractor<Runtime, Instance = ()> {
113	_phantom: PhantomData<(Runtime, Instance)>,
114}
115
116impl<Runtime, Instance: 'static> AssetIdExtractor for ForeignAssetIdExtractor<Runtime, Instance>
117where
118	Runtime: pallet_assets::Config<Instance>
119		+ pallet::Config<ForeignAssetId = <Runtime as pallet_assets::Config<Instance>>::AssetId>
120		+ pallet_revive::Config,
121{
122	type AssetId = <Runtime as pallet_assets::Config<Instance>>::AssetId;
123	fn asset_id_from_address(addr: &[u8; 20]) -> Result<Self::AssetId, Error> {
124		let bytes: [u8; 4] = addr[0..4].try_into().expect("slice is 4 bytes; qed");
125		let index = u32::from_be_bytes(bytes);
126		pallet::Pallet::<Runtime>::asset_id_of(index)
127			.ok_or(Error::Revert(Revert { reason: "Invalid foreign asset id".into() }))
128	}
129	fn asset_index(id: &Self::AssetId) -> Option<u32> {
130		pallet::Pallet::<Runtime>::asset_index_of(id)
131	}
132}
133
134/// A precompile configuration that uses a prefix [`AddressMatcher`].
135pub struct ForeignIdConfig<const PREFIX: u16, Runtime, Instance = ()> {
136	_phantom: PhantomData<(Runtime, Instance)>,
137}
138
139impl<const P: u16, Runtime, Instance: 'static> AssetPrecompileConfig
140	for ForeignIdConfig<P, Runtime, Instance>
141where
142	Runtime: pallet_assets::Config<Instance>
143		+ pallet::Config<ForeignAssetId = <Runtime as pallet_assets::Config<Instance>>::AssetId>
144		+ pallet_revive::Config,
145{
146	const MATCHER: AddressMatcher = AddressMatcher::Prefix(core::num::NonZero::new(P).unwrap());
147	type AssetIdExtractor = ForeignAssetIdExtractor<Runtime, Instance>;
148}
149
150/// An ERC20 precompile with EIP-2612 permit support.
151///
152/// # Wiring
153///
154/// This precompile does not emit the `Transfer` log for a balance-changing call.
155/// [`Erc20TransferLogsCallback`] produces it and must be wired as the instance's
156/// `pallet_assets::Config::CallbackHandle` for the token to be EIP-20 compliant, covering changes
157/// made through pallet-assets directly as well as through this precompile. A runtime that
158/// registers the precompile without it compiles and answers calls, but emits no `Transfer` log for
159/// any non-zero transfer. See [`Erc20TransferLogsCallback`] for the changes that have no mirror.
160pub struct ERC20<Runtime, PrecompileConfig, Instance = ()> {
161	_phantom: PhantomData<(Runtime, PrecompileConfig, Instance)>,
162}
163
164impl<Runtime, PrecompileConfig, Instance: 'static> Precompile
165	for ERC20<Runtime, PrecompileConfig, Instance>
166where
167	PrecompileConfig: AssetPrecompileConfig,
168	Runtime: crate::Config<Instance> + pallet_revive::Config + permit::Config,
169	<<PrecompileConfig as AssetPrecompileConfig>::AssetIdExtractor as AssetIdExtractor>::AssetId:
170		Into<<Runtime as Config<Instance>>::AssetId>,
171	Call<Runtime, Instance>: Into<<Runtime as pallet_revive::Config>::RuntimeCall>,
172	alloy::primitives::U256: TryInto<<Runtime as Config<Instance>>::Balance>,
173	alloy::primitives::U256: TryFrom<<Runtime as Config<Instance>>::Balance>,
174{
175	type T = Runtime;
176	type Interface = IERC20::IERC20Calls;
177	const MATCHER: AddressMatcher = PrecompileConfig::MATCHER;
178	const HAS_CONTRACT_INFO: bool = false;
179
180	fn call(
181		address: &[u8; 20],
182		input: &Self::Interface,
183		env: &mut impl Ext<T = Self::T>,
184	) -> Result<Vec<u8>, Error> {
185		frame_support::ensure!(
186			!env.is_delegate_call(),
187			pallet_revive::Error::<Self::T>::PrecompileDelegateDenied,
188		);
189
190		let asset_id = PrecompileConfig::AssetIdExtractor::asset_id_from_address(address)?.into();
191		let contract_addr = H160::from(*address);
192
193		match input {
194			// State-changing calls - check read-only
195			IERC20Calls::transfer(_) |
196			IERC20Calls::approve(_) |
197			IERC20Calls::transferFrom(_) |
198			IERC20Calls::permit(_)
199				if env.is_read_only() =>
200			{
201				Err(Error::Error(pallet_revive::Error::<Self::T>::StateChangeDenied.into()))
202			},
203
204			// ERC20 functions
205			IERC20Calls::transfer(call) => Self::transfer(asset_id, contract_addr, call, env),
206			IERC20Calls::totalSupply(_) => Self::total_supply(asset_id, env),
207			IERC20Calls::balanceOf(call) => Self::balance_of(asset_id, call, env),
208			IERC20Calls::allowance(call) => Self::allowance(asset_id, call, env),
209			IERC20Calls::approve(call) => Self::approve(asset_id, call, env),
210			IERC20Calls::transferFrom(call) => {
211				Self::transfer_from(asset_id, contract_addr, call, env)
212			},
213
214			// ERC20Permit functions (EIP-2612)
215			IERC20Calls::permit(call) => Self::permit(asset_id, contract_addr, call, env),
216			IERC20Calls::nonces(call) => Self::nonces(contract_addr, call, env),
217			IERC20Calls::DOMAIN_SEPARATOR(_) => {
218				Self::domain_separator(asset_id, contract_addr, env)
219			},
220
221			// ERC20Metadata functions
222			IERC20Calls::name(_) => Self::name(asset_id, env),
223			IERC20Calls::symbol(_) => Self::symbol(asset_id, env),
224			IERC20Calls::decimals(_) => Self::decimals(asset_id, env),
225		}
226	}
227}
228
229/// `CallbackHandle` emitting an ERC-20 `Transfer` log for each pallet-assets balance change it is
230/// notified of, at the asset's [`ERC20`] precompile address.
231///
232/// A same-account hold is not a balance change here: `balanceOf` reports
233/// `fungibles::Inspect::total_balance`, so a hold or release moves balance between the free and
234/// held portions of one account without moving `balanceOf`, and needs no log. The hold paths that
235/// move value between accounts — `transfer_on_hold`, `transfer_and_hold`, `burn_held` — do move it,
236/// and reach pallet-assets through its `Unbalanced` impl, below the `Mutate` impl these hooks hang
237/// off. `pallet-assets-holder` reports them from `done_transfer_on_hold` and `done_burn_held`; a
238/// `Holder` that does not do the same moves `balanceOf` with no `Transfer` log.
239///
240/// Asset destruction has no mirror either: ERC-20 has no equivalent concept, and firing a burn log
241/// per holder would emit an unbounded number of events for one destruction. Consumers must treat
242/// the token as dead once the asset is destroyed and drop any log-derived balances, since for an
243/// inline id later reused by `force_create` the address would otherwise inherit them.
244pub struct Erc20TransferLogsCallback<Runtime, PrecompileConfig, Instance = ()> {
245	_phantom: PhantomData<(Runtime, PrecompileConfig, Instance)>,
246}
247
248impl<Runtime, PrecompileConfig, Instance>
249	AssetsCallback<
250		<Runtime as Config<Instance>>::AssetId,
251		<Runtime as frame_system::Config>::AccountId,
252		<Runtime as Config<Instance>>::Balance,
253	> for Erc20TransferLogsCallback<Runtime, PrecompileConfig, Instance>
254where
255	PrecompileConfig: AssetPrecompileConfig,
256	Runtime: Config<Instance> + pallet_revive::Config,
257	<Runtime as Config<Instance>>::AssetId:
258		Clone + Into<<PrecompileConfig::AssetIdExtractor as AssetIdExtractor>::AssetId>,
259	alloy::primitives::U256: TryFrom<<Runtime as Config<Instance>>::Balance>,
260	Instance: 'static,
261{
262	fn issued(
263		id: &<Runtime as Config<Instance>>::AssetId,
264		owner: &<Runtime as frame_system::Config>::AccountId,
265		amount: <Runtime as Config<Instance>>::Balance,
266	) {
267		Self::emit(id, alloy::primitives::Address::ZERO, Self::evm_address(owner), amount);
268	}
269
270	fn transferred(
271		id: &<Runtime as Config<Instance>>::AssetId,
272		from: &<Runtime as frame_system::Config>::AccountId,
273		to: &<Runtime as frame_system::Config>::AccountId,
274		amount: <Runtime as Config<Instance>>::Balance,
275	) {
276		Self::emit(id, Self::evm_address(from), Self::evm_address(to), amount);
277	}
278
279	fn burned(
280		id: &<Runtime as Config<Instance>>::AssetId,
281		owner: &<Runtime as frame_system::Config>::AccountId,
282		amount: <Runtime as Config<Instance>>::Balance,
283	) {
284		Self::emit(id, Self::evm_address(owner), alloy::primitives::Address::ZERO, amount);
285	}
286
287	fn deposited(
288		id: &<Runtime as Config<Instance>>::AssetId,
289		who: &<Runtime as frame_system::Config>::AccountId,
290		amount: <Runtime as Config<Instance>>::Balance,
291	) {
292		Self::emit(id, alloy::primitives::Address::ZERO, Self::evm_address(who), amount);
293	}
294
295	fn withdrawn(
296		id: &<Runtime as Config<Instance>>::AssetId,
297		who: &<Runtime as frame_system::Config>::AccountId,
298		amount: <Runtime as Config<Instance>>::Balance,
299	) {
300		Self::emit(id, Self::evm_address(who), alloy::primitives::Address::ZERO, amount);
301	}
302}
303
304impl<Runtime, PrecompileConfig, Instance>
305	Erc20TransferLogsCallback<Runtime, PrecompileConfig, Instance>
306where
307	PrecompileConfig: AssetPrecompileConfig,
308	Runtime: Config<Instance> + pallet_revive::Config,
309	<Runtime as Config<Instance>>::AssetId:
310		Clone + Into<<PrecompileConfig::AssetIdExtractor as AssetIdExtractor>::AssetId>,
311	alloy::primitives::U256: TryFrom<<Runtime as Config<Instance>>::Balance>,
312	Instance: 'static,
313{
314	/// The precompile/token address of `id`: the matcher's base address with the asset's index
315	/// inlined big-endian into the first four bytes (the inverse of `asset_id_from_address`).
316	fn token_address(id: &<Runtime as Config<Instance>>::AssetId) -> Option<H160> {
317		let index = PrecompileConfig::AssetIdExtractor::asset_index(&id.clone().into())?;
318		let mut address = PrecompileConfig::MATCHER.base_address();
319		address[..4].copy_from_slice(&index.to_be_bytes());
320		Some(H160(address))
321	}
322
323	fn evm_address(
324		account: &<Runtime as frame_system::Config>::AccountId,
325	) -> alloy::primitives::Address {
326		<Runtime as pallet_revive::Config>::AddressMapper::to_address(account).0.into()
327	}
328
329	fn emit(
330		id: &<Runtime as Config<Instance>>::AssetId,
331		from: alloy::primitives::Address,
332		to: alloy::primitives::Address,
333		amount: <Runtime as Config<Instance>>::Balance,
334	) {
335		let Ok(value) = alloy::primitives::U256::try_from(amount) else {
336			frame_support::defensive!(
337				"asset balance exceeds U256; Transfer log dropped (unreachable for balance types up to 256 bit)",
338				(id, from, to, amount)
339			);
340			return;
341		};
342		let Some(token) = Self::token_address(id) else {
343			log::error!(
344				target: frame_support::LOG_TARGET,
345				"asset has no precompile address mapping; Transfer log dropped (a foreign asset predating the index-map migration): {:?}",
346				(id, from, to, amount)
347			);
348			return;
349		};
350		emit_transfer_log::<Runtime>(token, from, to, value);
351	}
352}
353
354/// Emit the canonical ERC-20 `Transfer` log for `token`.
355///
356/// The single place the log bytes are built, so the mirror and the precompile's own EIP-20
357/// zero-value log cannot drift apart.
358fn emit_transfer_log<Runtime: pallet_revive::Config>(
359	token: H160,
360	from: alloy::primitives::Address,
361	to: alloy::primitives::Address,
362	value: alloy::primitives::U256,
363) {
364	let (topics, data) = IERC20Events::Transfer(IERC20::Transfer { from, to, value })
365		.into_log_data()
366		.split();
367	let topics = topics.into_iter().map(|t| H256(t.0)).collect::<Vec<_>>();
368	let topics = pallet_revive::ContractLogTopics::try_from(topics)
369		.expect("Transfer has 3 topics, within NUM_EVENT_TOPICS of 4; qed");
370	let data = pallet_revive::ContractLogData::try_from(data.to_vec())
371		.expect("Transfer data is a single 32 byte word, within EVENT_BYTES of 64 KiB; qed");
372	pallet_revive::Pallet::<Runtime>::emit_contract_log_outside_frame(token, topics, data);
373}
374
375/// Shape of the mirrored ERC-20 `Transfer` log: the event signature plus the two indexed
376/// addresses, and the value word.
377const TRANSFER_LOG_TOPICS: u32 = 3;
378const TRANSFER_LOG_DATA_LEN: u32 = 32;
379
380const ERR_INVALID_CALLER: &str = "Invalid caller";
381const ERR_BALANCE_CONVERSION_FAILED: &str = "Balance conversion failed";
382
383impl<Runtime, PrecompileConfig, Instance: 'static> ERC20<Runtime, PrecompileConfig, Instance>
384where
385	PrecompileConfig: AssetPrecompileConfig,
386	Runtime: crate::Config<Instance> + pallet_revive::Config + permit::Config,
387	<<PrecompileConfig as AssetPrecompileConfig>::AssetIdExtractor as AssetIdExtractor>::AssetId:
388		Into<<Runtime as Config<Instance>>::AssetId>,
389	Call<Runtime, Instance>: Into<<Runtime as pallet_revive::Config>::RuntimeCall>,
390	alloy::primitives::U256: TryInto<<Runtime as Config<Instance>>::Balance>,
391	alloy::primitives::U256: TryFrom<<Runtime as Config<Instance>>::Balance>,
392{
393	/// Get the caller as an `H160` address.
394	fn caller(env: &mut impl Ext<T = Runtime>) -> Result<H160, Error> {
395		env.caller()
396			.account_id()
397			.map(<Runtime as pallet_revive::Config>::AddressMapper::to_address)
398			.map_err(|_| Error::Revert(Revert { reason: ERR_INVALID_CALLER.into() }))
399	}
400
401	/// Convert a `U256` value to the balance type of the pallet.
402	fn to_balance(
403		value: alloy::primitives::U256,
404	) -> Result<<Runtime as Config<Instance>>::Balance, Error> {
405		value
406			.try_into()
407			.map_err(|_| Error::Revert(Revert { reason: ERR_BALANCE_CONVERSION_FAILED.into() }))
408	}
409
410	/// Convert a balance to a `U256` value.
411	/// Note: this is needed because `From` is not implemented for unsigned integer types.
412	fn to_u256(
413		value: <Runtime as Config<Instance>>::Balance,
414	) -> Result<alloy::primitives::U256, Error> {
415		alloy::primitives::U256::try_from(value)
416			.map_err(|_| Error::Revert(Revert { reason: ERR_BALANCE_CONVERSION_FAILED.into() }))
417	}
418
419	/// Deposit an event to the runtime.
420	fn deposit_event(env: &mut impl Ext<T = Runtime>, event: IERC20Events) -> Result<(), Error> {
421		let (topics, data) = event.into_log_data().split();
422		let topics = topics.into_iter().map(|v| H256(v.0)).collect::<Vec<_>>();
423		env.frame_meter_mut().charge_weight_token(RuntimeCosts::DepositEvent {
424			num_topic: topics.len() as u32,
425			len: data.len() as u32,
426		})?;
427		env.deposit_event(topics, data.to_vec());
428		Ok(())
429	}
430
431	/// Execute the transfer call.
432	///
433	/// Only the zero-value `Transfer` is emitted here; every other one comes from the instance's
434	/// `CallbackHandle` (see [`ERC20`]).
435	fn transfer(
436		asset_id: <Runtime as Config<Instance>>::AssetId,
437		contract_addr: H160,
438		call: &IERC20::transferCall,
439		env: &mut impl Ext<T = Runtime>,
440	) -> Result<Vec<u8>, Error> {
441		// `transfer()` is benchmarked with the mirror-log callback wired, so the mirrored log's
442		// buffer insert is part of that charge. The `DepositEvent` charge covers the log's other
443		// destination: inside an ethereum transaction the mirror lands on this call's receipt,
444		// which no pallet benchmark executes.
445		env.charge(<Runtime as Config<Instance>>::WeightInfo::transfer())?;
446		env.frame_meter_mut().charge_weight_token(RuntimeCosts::DepositEvent {
447			num_topic: TRANSFER_LOG_TOPICS,
448			len: TRANSFER_LOG_DATA_LEN,
449		})?;
450
451		let from = Self::caller(env)?;
452		let from_account = <Runtime as pallet_revive::Config>::AddressMapper::to_account_id(&from);
453		let dest = <Runtime as pallet_revive::Config>::AddressMapper::to_account_id(
454			&call.to.into_array().into(),
455		);
456		let amount = Self::to_balance(call.value)?;
457
458		let f = TransferFlags { keep_alive: false, best_effort: false, burn_dust: false };
459		pallet_assets::Pallet::<Runtime, Instance>::do_transfer(
460			asset_id,
461			&from_account,
462			&dest,
463			amount,
464			None,
465			f,
466		)?;
467
468		// A zero-value transfer is a no-op in `do_transfer` and fires no callback, but EIP-20
469		// requires it to still emit a `Transfer` log. Emitted here rather than through
470		// `CallbackHandle`, which is the whole callback tuple: reporting a balance change the
471		// pallet did not perform to every observer on the instance is not this precompile's to do.
472		if call.value.is_zero() {
473			emit_transfer_log::<Runtime>(contract_addr, from.0.into(), call.to, call.value);
474		}
475
476		Ok(IERC20::transferCall::abi_encode_returns(&true))
477	}
478
479	/// Execute the total supply call.
480	fn total_supply(
481		asset_id: <Runtime as Config<Instance>>::AssetId,
482		env: &mut impl Ext<T = Runtime>,
483	) -> Result<Vec<u8>, Error> {
484		use frame_support::traits::fungibles::Inspect;
485		env.charge(<Runtime as Config<Instance>>::WeightInfo::total_issuance())?;
486
487		let value =
488			Self::to_u256(pallet_assets::Pallet::<Runtime, Instance>::total_issuance(asset_id))?;
489		Ok(IERC20::totalSupplyCall::abi_encode_returns(&value))
490	}
491
492	/// Execute the balance_of call.
493	///
494	/// Reports the free *and* held balance, so that moving funds between those two portions of the
495	/// same account — a `fungibles::MutateHold` hold or release, which emits no `Transfer` log —
496	/// does not move `balanceOf`.
497	fn balance_of(
498		asset_id: <Runtime as Config<Instance>>::AssetId,
499		call: &IERC20::balanceOfCall,
500		env: &mut impl Ext<T = Runtime>,
501	) -> Result<Vec<u8>, Error> {
502		use frame_support::traits::fungibles::Inspect;
503
504		env.charge(<Runtime as Config<Instance>>::WeightInfo::balance())?;
505		let account = call.account.into_array().into();
506		let account = <Runtime as pallet_revive::Config>::AddressMapper::to_account_id(&account);
507		let value = Self::to_u256(pallet_assets::Pallet::<Runtime, Instance>::total_balance(
508			asset_id, &account,
509		))?;
510		Ok(IERC20::balanceOfCall::abi_encode_returns(&value))
511	}
512
513	/// Execute the allowance call.
514	fn allowance(
515		asset_id: <Runtime as Config<Instance>>::AssetId,
516		call: &IERC20::allowanceCall,
517		env: &mut impl Ext<T = Runtime>,
518	) -> Result<Vec<u8>, Error> {
519		env.charge(<Runtime as Config<Instance>>::WeightInfo::allowance())?;
520		use frame_support::traits::fungibles::approvals::Inspect;
521		let owner = call.owner.into_array().into();
522		let owner = <Runtime as pallet_revive::Config>::AddressMapper::to_account_id(&owner);
523
524		let spender = call.spender.into_array().into();
525		let spender = <Runtime as pallet_revive::Config>::AddressMapper::to_account_id(&spender);
526		let value = Self::to_u256(pallet_assets::Pallet::<Runtime, Instance>::allowance(
527			asset_id, &owner, &spender,
528		))?;
529
530		Ok(IERC20::allowanceCall::abi_encode_returns(&value))
531	}
532
533	/// Execute the approve call.
534	///
535	/// Implements ERC-20 set semantics: `approve(spender, N)` sets the allowance to exactly `N`
536	/// rather than adding to it. When overwriting a non-zero allowance, the existing approval is
537	/// cancelled first so the new value replaces (not accumulates with) the old one.
538	///
539	/// `call.value > Balance::MAX` (the `type(uint256).max` "infinite allowance" idiom)
540	/// saturates the stored allowance at `Balance::MAX`. The `Approval` event carries the
541	/// raw `call.value`.
542	fn approve(
543		asset_id: <Runtime as Config<Instance>>::AssetId,
544		call: &IERC20::approveCall,
545		env: &mut impl Ext<T = Runtime>,
546	) -> Result<Vec<u8>, Error> {
547		use frame_support::traits::fungibles::approvals::Inspect as ApprovalsInspect;
548
549		// Reserve worst-case gas upfront, then refund the unused portion.
550		let worst_case = <Runtime as Config<Instance>>::WeightInfo::allowance()
551			.saturating_add(<Runtime as Config<Instance>>::WeightInfo::cancel_approval())
552			.saturating_add(<Runtime as Config<Instance>>::WeightInfo::approve_transfer());
553		let charged = env.charge(worst_case)?;
554
555		let owner = Self::caller(env)?;
556		let owner_account =
557			<Runtime as pallet_revive::Config>::AddressMapper::to_account_id(&owner);
558		let spender: H160 = call.spender.into_array().into();
559		let spender_account = env.to_account_id(&spender);
560		// Saturate: `type(uint256).max` is the standard "infinite allowance" idiom and must
561		// not revert at the conversion boundary.
562		let new_amount: <Runtime as Config<Instance>>::Balance = call.value.unique_saturated_into();
563
564		let current = pallet_assets::Pallet::<Runtime, Instance>::allowance(
565			asset_id.clone(),
566			&owner_account,
567			&spender_account,
568		);
569
570		let actual_weight;
571		if new_amount.is_zero() {
572			if !current.is_zero() {
573				// Revoke: use the pallet's cancel logic to remove the approval and
574				// unreserve the deposit.
575				pallet_assets::Pallet::<Runtime, Instance>::do_cancel_approval(
576					&asset_id,
577					&owner_account,
578					&spender_account,
579				)?;
580				actual_weight = <Runtime as Config<Instance>>::WeightInfo::allowance()
581					.saturating_add(<Runtime as Config<Instance>>::WeightInfo::cancel_approval());
582			} else {
583				// 0→0 no-op: only the allowance read was needed.
584				actual_weight = <Runtime as Config<Instance>>::WeightInfo::allowance();
585			}
586		} else {
587			// If there's an existing non-zero allowance, cancel it first so we
588			// overwrite (not accumulate) — matching ERC-20 spec semantics.
589			// NOTE: This does not mitigate the well-known ERC-20 approve front-running
590			// race condition. Callers concerned about this should approve to 0 first,
591			// or use increaseAllowance/decreaseAllowance if available.
592			if !current.is_zero() {
593				pallet_assets::Pallet::<Runtime, Instance>::do_cancel_approval(
594					&asset_id,
595					&owner_account,
596					&spender_account,
597				)?;
598				actual_weight = worst_case;
599			} else {
600				actual_weight = <Runtime as Config<Instance>>::WeightInfo::allowance()
601					.saturating_add(<Runtime as Config<Instance>>::WeightInfo::approve_transfer());
602			}
603			pallet_assets::Pallet::<Runtime, Instance>::do_approve_transfer(
604				asset_id,
605				&owner_account,
606				&spender_account,
607				new_amount,
608			)?;
609		}
610		env.adjust_gas(charged, actual_weight);
611
612		Self::deposit_event(
613			env,
614			IERC20Events::Approval(IERC20::Approval {
615				owner: owner.0.into(),
616				spender: call.spender,
617				value: call.value,
618			}),
619		)?;
620
621		Ok(IERC20::approveCall::abi_encode_returns(&true))
622	}
623
624	/// Execute the transfer_from call.
625	///
626	/// Only the zero-value `Transfer` is emitted here; every other one comes from the instance's
627	/// `CallbackHandle` (see [`ERC20`]).
628	fn transfer_from(
629		asset_id: <Runtime as Config<Instance>>::AssetId,
630		contract_addr: H160,
631		call: &IERC20::transferFromCall,
632		env: &mut impl Ext<T = Runtime>,
633	) -> Result<Vec<u8>, Error> {
634		// `transfer_approved()` accounts for the mirror-log callback's buffer insert; the
635		// `DepositEvent` charge covers its receipt capture, as in `transfer`.
636		env.charge(<Runtime as Config<Instance>>::WeightInfo::transfer_approved())?;
637		env.frame_meter_mut().charge_weight_token(RuntimeCosts::DepositEvent {
638			num_topic: TRANSFER_LOG_TOPICS,
639			len: TRANSFER_LOG_DATA_LEN,
640		})?;
641		let spender = Self::caller(env)?;
642		let spender = <Runtime as pallet_revive::Config>::AddressMapper::to_account_id(&spender);
643
644		let from = call.from.into_array().into();
645		let from = <Runtime as pallet_revive::Config>::AddressMapper::to_account_id(&from);
646
647		let to = call.to.into_array().into();
648		let to = <Runtime as pallet_revive::Config>::AddressMapper::to_account_id(&to);
649
650		let approval_amount = Self::to_balance(call.value)?;
651		pallet_assets::Pallet::<Runtime, Instance>::do_transfer_approved(
652			asset_id,
653			&from,
654			&spender,
655			&to,
656			approval_amount,
657		)?;
658
659		// A zero-value transfer is a no-op in `do_transfer_approved` and fires no callback, but
660		// EIP-20 requires it to still emit a `Transfer` log; see `transfer` for why this is emitted
661		// directly rather than through `CallbackHandle`.
662		if call.value.is_zero() {
663			emit_transfer_log::<Runtime>(contract_addr, call.from, call.to, call.value);
664		}
665
666		Ok(IERC20::transferFromCall::abi_encode_returns(&true))
667	}
668
669	// ==================== ERC20Permit Functions (EIP-2612) ====================
670
671	/// Execute the permit call (EIP-2612).
672	///
673	/// This verifies the signature, consumes the permit (increments nonce),
674	/// and sets the approval. Saturation policy and event payload match `approve` —
675	/// see its doc-comment.
676	pub(crate) fn permit(
677		asset_id: <Runtime as Config<Instance>>::AssetId,
678		verifying_contract: H160,
679		call: &IERC20::permitCall,
680		env: &mut impl Ext<T = Runtime>,
681	) -> Result<Vec<u8>, Error> {
682		// Reserve worst-case gas upfront, then refund the unused portion.
683		// The total cost is: use_permit (signature verification + nonce) +
684		// worst-case asset approval operations (allowance read + cancel + approve).
685		let use_permit_weight = <Runtime as permit::Config>::WeightInfo::use_permit();
686		let worst_case = use_permit_weight
687			.saturating_add(<Runtime as Config<Instance>>::WeightInfo::allowance())
688			.saturating_add(<Runtime as Config<Instance>>::WeightInfo::cancel_approval())
689			.saturating_add(<Runtime as Config<Instance>>::WeightInfo::approve_transfer());
690		let charged = env.charge(worst_case)?;
691
692		let owner_h160: H160 = call.owner.into_array().into();
693		let spender_h160: H160 = call.spender.into_array().into();
694
695		// Convert U256 values to byte arrays
696		let value_bytes: [u8; 32] = call.value.to_be_bytes();
697		let deadline_bytes: [u8; 32] = call.deadline.to_be_bytes();
698		let r_bytes: [u8; 32] = call.r.0;
699		let s_bytes: [u8; 32] = call.s.0;
700
701		let transaction_outcome = frame_support::storage::with_transaction(|| {
702			let result = (|| {
703				// Use the permit - this validates deadline, signature, and increments nonce
704				permit::Pallet::<Runtime>::use_permit(
705					&verifying_contract,
706					&pallet_assets::Pallet::<Runtime, Instance>::name(asset_id.clone()),
707					&owner_h160,
708					&spender_h160,
709					&value_bytes,
710					&deadline_bytes,
711					call.v,
712					&r_bytes,
713					&s_bytes,
714				)
715				.map_err(|e| {
716					let msg = match e {
717						permit::pallet::Error::PermitExpired => "Permit expired",
718						permit::pallet::Error::InvalidSignature => "Invalid signature",
719						permit::pallet::Error::SignerMismatch => "Signer does not match owner",
720						permit::pallet::Error::SignatureSValueTooHigh => {
721							"Signature s value too high (malleability)"
722						},
723						permit::pallet::Error::InvalidVValue => "Invalid signature v value",
724						permit::pallet::Error::NonceOverflow => "Nonce overflow",
725						permit::pallet::Error::InvalidOwner => "Invalid owner address",
726						permit::pallet::Error::InvalidSpender => "Invalid spender address",
727					};
728					Error::Revert(Revert { reason: msg.into() })
729				})?;
730
731				// Delete-set semantic: cancel any existing approval first so
732				// do_approve_transfer sets (not accumulates) the new value.
733				use frame_support::traits::fungibles::approvals::Inspect as ApprovalsInspect;
734				let owner_account =
735					<Runtime as pallet_revive::Config>::AddressMapper::to_account_id(&owner_h160);
736				let spender_account =
737					<Runtime as pallet_revive::Config>::AddressMapper::to_account_id(&spender_h160);
738
739				// Saturate: see `approve` for the rationale (infinite-allowance idiom).
740				let new_amount: <Runtime as Config<Instance>>::Balance =
741					call.value.unique_saturated_into();
742				let current = pallet_assets::Pallet::<Runtime, Instance>::allowance(
743					asset_id.clone(),
744					&owner_account,
745					&spender_account,
746				);
747
748				let actual_weight;
749				if new_amount.is_zero() {
750					if !current.is_zero() {
751						// clear approval if it exists, to match ERC-20 semantics of setting
752						// allowance to 0
753						pallet_assets::Pallet::<Runtime, Instance>::do_cancel_approval(
754							&asset_id,
755							&owner_account,
756							&spender_account,
757						)?;
758						actual_weight = use_permit_weight
759							.saturating_add(<Runtime as Config<Instance>>::WeightInfo::allowance())
760							.saturating_add(
761								<Runtime as Config<Instance>>::WeightInfo::cancel_approval(),
762							);
763					} else {
764						// noop: set allowance to zerowhen it is already zero
765						actual_weight = use_permit_weight
766							.saturating_add(<Runtime as Config<Instance>>::WeightInfo::allowance());
767					}
768				} else {
769					if !current.is_zero() {
770						// If there's an existing non-zero allowance, cancel it first
771						pallet_assets::Pallet::<Runtime, Instance>::do_cancel_approval(
772							&asset_id,
773							&owner_account,
774							&spender_account,
775						)?;
776						actual_weight = worst_case;
777					} else {
778						// set new approval
779						actual_weight = use_permit_weight
780							.saturating_add(<Runtime as Config<Instance>>::WeightInfo::allowance())
781							.saturating_add(
782								<Runtime as Config<Instance>>::WeightInfo::approve_transfer(),
783							);
784					}
785					pallet_assets::Pallet::<Runtime, Instance>::do_approve_transfer(
786						asset_id,
787						&owner_account,
788						&spender_account,
789						new_amount,
790					)?;
791				}
792
793				// Emit Approval event
794				Self::deposit_event(
795					env,
796					IERC20Events::Approval(IERC20::Approval {
797						owner: call.owner,
798						spender: call.spender,
799						value: call.value,
800					}),
801				)?;
802				Ok::<_, Error>(actual_weight)
803			})();
804			match result {
805				Ok(actual_weight) => {
806					frame_support::storage::TransactionOutcome::Commit(Ok(actual_weight))
807				},
808				Err(e) => {
809					log::trace!(target: frame_support::LOG_TARGET, "Call to permit failed: {e:?}");
810					frame_support::storage::TransactionOutcome::Rollback(Err(e))
811				},
812			}
813		});
814
815		// permit returns void
816		match transaction_outcome {
817			Ok(actual_weight) => {
818				env.adjust_gas(charged, actual_weight);
819				Ok(Vec::new())
820			},
821			Err(e) => Err(e),
822		}
823	}
824
825	/// Get the current nonce for an owner address.
826	fn nonces(
827		verifying_contract: H160,
828		call: &IERC20::noncesCall,
829		env: &mut impl Ext<T = Runtime>,
830	) -> Result<Vec<u8>, Error> {
831		env.charge(<Runtime as permit::Config>::WeightInfo::nonces())?;
832
833		let owner_h160: H160 = call.owner.into_array().into();
834		let nonce = permit::Pallet::<Runtime>::nonce(&verifying_contract, &owner_h160);
835
836		// Convert sp_core::U256 to alloy U256
837		let nonce_bytes = nonce.to_big_endian();
838		let nonce_alloy = alloy::primitives::U256::from_be_bytes(nonce_bytes);
839
840		Ok(IERC20::noncesCall::abi_encode_returns(&nonce_alloy))
841	}
842
843	/// Get the EIP-712 domain separator for this contract.
844	fn domain_separator(
845		asset_id: <Runtime as Config<Instance>>::AssetId,
846		verifying_contract: H160,
847		env: &mut impl Ext<T = Runtime>,
848	) -> Result<Vec<u8>, Error> {
849		env.charge(<Runtime as permit::Config>::WeightInfo::domain_separator())?;
850
851		// Fetch token name for EIP-712 domain separator (per EIP-2612 spec)
852		let token_name = pallet_assets::Pallet::<Runtime, Instance>::name(asset_id);
853
854		let separator =
855			permit::Pallet::<Runtime>::compute_domain_separator(&verifying_contract, &token_name);
856		let separator_alloy: alloy::primitives::FixedBytes<32> = separator.0.into();
857
858		Ok(IERC20::DOMAIN_SEPARATORCall::abi_encode_returns(&separator_alloy))
859	}
860
861	/// Execute the name call.
862	fn name(
863		asset_id: <Runtime as Config<Instance>>::AssetId,
864		env: &mut impl Ext<T = Runtime>,
865	) -> Result<Vec<u8>, Error> {
866		env.charge(<Runtime as Config<Instance>>::WeightInfo::get_metadata())?;
867
868		let metadata = pallet_assets::Pallet::<Runtime, Instance>::get_metadata(asset_id)
869			.ok_or(Error::Revert(Revert { reason: "Metadata not found".into() }))?;
870
871		let name = alloc::string::String::from_utf8(metadata.name.to_vec())
872			.map_err(|_| Error::Revert(Revert { reason: "Invalid UTF-8 in name".into() }))?;
873
874		Ok(IERC20::nameCall::abi_encode_returns(&name))
875	}
876
877	/// Execute the symbol call.
878	fn symbol(
879		asset_id: <Runtime as Config<Instance>>::AssetId,
880		env: &mut impl Ext<T = Runtime>,
881	) -> Result<Vec<u8>, Error> {
882		env.charge(<Runtime as Config<Instance>>::WeightInfo::get_metadata())?;
883
884		let metadata = pallet_assets::Pallet::<Runtime, Instance>::get_metadata(asset_id)
885			.ok_or(Error::Revert(Revert { reason: "Metadata not found".into() }))?;
886
887		let symbol = alloc::string::String::from_utf8(metadata.symbol.to_vec())
888			.map_err(|_| Error::Revert(Revert { reason: "Invalid UTF-8 in symbol".into() }))?;
889
890		Ok(IERC20::symbolCall::abi_encode_returns(&symbol))
891	}
892
893	/// Execute the decimals call.
894	fn decimals(
895		asset_id: <Runtime as Config<Instance>>::AssetId,
896		env: &mut impl Ext<T = Runtime>,
897	) -> Result<Vec<u8>, Error> {
898		env.charge(<Runtime as Config<Instance>>::WeightInfo::get_metadata())?;
899
900		let metadata = pallet_assets::Pallet::<Runtime, Instance>::get_metadata(asset_id)
901			.ok_or(Error::Revert(Revert { reason: "Metadata not found".into() }))?;
902
903		Ok(IERC20::decimalsCall::abi_encode_returns(&metadata.decimals))
904	}
905}