pallet_evm_precompile_balances_erc20/
lib.rs

1// Copyright 2019-2025 PureStake Inc.
2// This file is part of Moonbeam.
3
4// Moonbeam is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8
9// Moonbeam is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12// GNU General Public License for more details.
13
14// You should have received a copy of the GNU General Public License
15// along with Moonbeam.  If not, see <http://www.gnu.org/licenses/>.
16
17//! Precompile to interact with pallet_balances instances using the ERC20 interface standard.
18
19#![cfg_attr(not(feature = "std"), no_std)]
20
21use account::SYSTEM_ACCOUNT_SIZE;
22use fp_evm::PrecompileHandle;
23use frame_support::{
24	dispatch::{GetDispatchInfo, PostDispatchInfo},
25	sp_runtime::traits::{Bounded, CheckedSub, Dispatchable, StaticLookup},
26	storage::types::{StorageDoubleMap, StorageMap, ValueQuery},
27	traits::StorageInstance,
28	Blake2_128Concat,
29};
30use pallet_balances::pallet::{
31	Instance1, Instance10, Instance11, Instance12, Instance13, Instance14, Instance15, Instance16,
32	Instance2, Instance3, Instance4, Instance5, Instance6, Instance7, Instance8, Instance9,
33};
34use pallet_evm::AddressMapping;
35use precompile_utils::prelude::*;
36use sp_core::{H160, H256, U256};
37use sp_std::{
38	convert::{TryFrom, TryInto},
39	marker::PhantomData,
40};
41
42mod eip2612;
43use eip2612::Eip2612;
44
45#[cfg(test)]
46mod mock;
47#[cfg(test)]
48mod tests;
49
50/// Solidity selector of the Transfer log, which is the Keccak of the Log signature.
51pub const SELECTOR_LOG_TRANSFER: [u8; 32] = keccak256!("Transfer(address,address,uint256)");
52
53/// Solidity selector of the Approval log, which is the Keccak of the Log signature.
54pub const SELECTOR_LOG_APPROVAL: [u8; 32] = keccak256!("Approval(address,address,uint256)");
55
56/// Solidity selector of the Deposit log, which is the Keccak of the Log signature.
57pub const SELECTOR_LOG_DEPOSIT: [u8; 32] = keccak256!("Deposit(address,uint256)");
58
59/// Solidity selector of the Withdraw log, which is the Keccak of the Log signature.
60pub const SELECTOR_LOG_WITHDRAWAL: [u8; 32] = keccak256!("Withdrawal(address,uint256)");
61
62/// Associates pallet Instance to a prefix used for the Approves storage.
63/// This trait is implemented for () and the 16 substrate Instance.
64pub trait InstanceToPrefix {
65	/// Prefix used for the Approves storage.
66	type ApprovesPrefix: StorageInstance;
67
68	/// Prefix used for the Approves storage.
69	type NoncesPrefix: StorageInstance;
70}
71
72// We use a macro to implement the trait for () and the 16 substrate Instance.
73macro_rules! impl_prefix {
74	($instance:ident, $name:literal) => {
75		// Using `paste!` we generate a dedicated module to avoid collisions
76		// between each instance `Approves` struct.
77		paste::paste! {
78			mod [<_impl_prefix_ $instance:snake>] {
79				use super::*;
80
81				pub struct Approves;
82
83				impl StorageInstance for Approves {
84					const STORAGE_PREFIX: &'static str = "Approves";
85
86					fn pallet_prefix() -> &'static str {
87						$name
88					}
89				}
90
91				pub struct Nonces;
92
93				impl StorageInstance for Nonces {
94					const STORAGE_PREFIX: &'static str = "Nonces";
95
96					fn pallet_prefix() -> &'static str {
97						$name
98					}
99				}
100
101				impl InstanceToPrefix for $instance {
102					type ApprovesPrefix = Approves;
103					type NoncesPrefix = Nonces;
104				}
105			}
106		}
107	};
108}
109
110// Since the macro expect a `ident` to be used with `paste!` we cannot provide `()` directly.
111type Instance0 = ();
112
113impl_prefix!(Instance0, "Erc20Instance0Balances");
114impl_prefix!(Instance1, "Erc20Instance1Balances");
115impl_prefix!(Instance2, "Erc20Instance2Balances");
116impl_prefix!(Instance3, "Erc20Instance3Balances");
117impl_prefix!(Instance4, "Erc20Instance4Balances");
118impl_prefix!(Instance5, "Erc20Instance5Balances");
119impl_prefix!(Instance6, "Erc20Instance6Balances");
120impl_prefix!(Instance7, "Erc20Instance7Balances");
121impl_prefix!(Instance8, "Erc20Instance8Balances");
122impl_prefix!(Instance9, "Erc20Instance9Balances");
123impl_prefix!(Instance10, "Erc20Instance10Balances");
124impl_prefix!(Instance11, "Erc20Instance11Balances");
125impl_prefix!(Instance12, "Erc20Instance12Balances");
126impl_prefix!(Instance13, "Erc20Instance13Balances");
127impl_prefix!(Instance14, "Erc20Instance14Balances");
128impl_prefix!(Instance15, "Erc20Instance15Balances");
129impl_prefix!(Instance16, "Erc20Instance16Balances");
130
131/// Alias for the Balance type for the provided Runtime and Instance.
132pub type BalanceOf<Runtime, Instance = ()> =
133	<Runtime as pallet_balances::Config<Instance>>::Balance;
134
135/// Storage type used to store approvals, since `pallet_balances` doesn't
136/// handle this behavior.
137/// (Owner => Allowed => Amount)
138pub type ApprovesStorage<Runtime, Instance> = StorageDoubleMap<
139	<Instance as InstanceToPrefix>::ApprovesPrefix,
140	Blake2_128Concat,
141	<Runtime as frame_system::Config>::AccountId,
142	Blake2_128Concat,
143	<Runtime as frame_system::Config>::AccountId,
144	BalanceOf<Runtime, Instance>,
145>;
146
147/// Storage type used to store EIP2612 nonces.
148pub type NoncesStorage<Instance> = StorageMap<
149	<Instance as InstanceToPrefix>::NoncesPrefix,
150	// Owner
151	Blake2_128Concat,
152	H160,
153	// Nonce
154	U256,
155	ValueQuery,
156>;
157
158/// Metadata of an ERC20 token.
159pub trait Erc20Metadata {
160	/// Returns the name of the token.
161	fn name() -> &'static str;
162
163	/// Returns the symbol of the token.
164	fn symbol() -> &'static str;
165
166	/// Returns the decimals places of the token.
167	fn decimals() -> u8;
168
169	/// Must return `true` only if it represents the main native currency of
170	/// the network. It must be the currency used in `pallet_evm`.
171	fn is_native_currency() -> bool;
172}
173
174/// Precompile exposing a pallet_balance as an ERC20.
175/// Multiple precompiles can support instances of pallet_balance.
176/// The precompile uses an additional storage to store approvals.
177pub struct Erc20BalancesPrecompile<Runtime, Metadata: Erc20Metadata, Instance: 'static = ()>(
178	PhantomData<(Runtime, Metadata, Instance)>,
179);
180
181#[precompile_utils::precompile]
182impl<Runtime, Metadata, Instance> Erc20BalancesPrecompile<Runtime, Metadata, Instance>
183where
184	Runtime: pallet_balances::Config<Instance> + pallet_evm::Config,
185	Runtime::RuntimeCall: Dispatchable<PostInfo = PostDispatchInfo> + GetDispatchInfo,
186	Runtime::RuntimeCall: From<pallet_balances::Call<Runtime, Instance>>,
187	BalanceOf<Runtime, Instance>: TryFrom<U256> + Into<U256>,
188	Metadata: Erc20Metadata,
189	Instance: InstanceToPrefix + 'static,
190	<Runtime as pallet_evm::Config>::AddressMapping: AddressMapping<Runtime::AccountId>,
191{
192	#[precompile::public("totalSupply()")]
193	#[precompile::view]
194	fn total_supply(handle: &mut impl PrecompileHandle) -> EvmResult<U256> {
195		// TotalIssuance: Balance(16)
196		handle.record_db_read::<Runtime>(16)?;
197
198		Ok(pallet_balances::Pallet::<Runtime, Instance>::total_issuance().into())
199	}
200
201	#[precompile::public("balanceOf(address)")]
202	#[precompile::view]
203	fn balance_of(handle: &mut impl PrecompileHandle, owner: Address) -> EvmResult<U256> {
204		// frame_system::Account:
205		// Blake2128(16) + AccountId(20) + AccountInfo ((4 * 4) + AccountData(16 * 4))
206		handle.record_db_read::<Runtime>(116)?;
207
208		let owner: H160 = owner.into();
209		let owner: Runtime::AccountId = Runtime::AddressMapping::into_account_id(owner);
210
211		Ok(pallet_balances::Pallet::<Runtime, Instance>::usable_balance(&owner).into())
212	}
213
214	#[precompile::public("allowance(address,address)")]
215	#[precompile::view]
216	fn allowance(
217		handle: &mut impl PrecompileHandle,
218		owner: Address,
219		spender: Address,
220	) -> EvmResult<U256> {
221		// frame_system::ApprovesStorage:
222		// (2 * (Blake2128(16) + AccountId(20)) + Balanceof(16)
223		handle.record_db_read::<Runtime>(88)?;
224
225		let owner: H160 = owner.into();
226		let spender: H160 = spender.into();
227
228		let owner: Runtime::AccountId = Runtime::AddressMapping::into_account_id(owner);
229		let spender: Runtime::AccountId = Runtime::AddressMapping::into_account_id(spender);
230
231		Ok(ApprovesStorage::<Runtime, Instance>::get(owner, spender)
232			.unwrap_or_default()
233			.into())
234	}
235
236	#[precompile::public("approve(address,uint256)")]
237	fn approve(
238		handle: &mut impl PrecompileHandle,
239		spender: Address,
240		value: U256,
241	) -> EvmResult<bool> {
242		handle.record_cost(RuntimeHelper::<Runtime>::db_write_gas_cost())?;
243		handle.record_log_costs_manual(3, 32)?;
244
245		let spender: H160 = spender.into();
246
247		// Write into storage.
248		{
249			let caller: Runtime::AccountId =
250				Runtime::AddressMapping::into_account_id(handle.context().caller);
251			let spender: Runtime::AccountId = Runtime::AddressMapping::into_account_id(spender);
252			// Amount saturate if too high.
253			let value = Self::u256_to_amount(value).unwrap_or_else(|_| Bounded::max_value());
254
255			ApprovesStorage::<Runtime, Instance>::insert(caller, spender, value);
256		}
257
258		log3(
259			handle.context().address,
260			SELECTOR_LOG_APPROVAL,
261			handle.context().caller,
262			spender,
263			solidity::encode_event_data(value),
264		)
265		.record(handle)?;
266
267		// Build output.
268		Ok(true)
269	}
270
271	#[precompile::public("transfer(address,uint256)")]
272	fn transfer(handle: &mut impl PrecompileHandle, to: Address, value: U256) -> EvmResult<bool> {
273		handle.record_log_costs_manual(3, 32)?;
274
275		let to: H160 = to.into();
276
277		// Build call with origin.
278		{
279			let origin = Runtime::AddressMapping::into_account_id(handle.context().caller);
280			let to = Runtime::AddressMapping::into_account_id(to);
281			let value = Self::u256_to_amount(value).in_field("value")?;
282
283			// Dispatch call (if enough gas).
284			RuntimeHelper::<Runtime>::try_dispatch(
285				handle,
286				frame_system::RawOrigin::Signed(origin).into(),
287				pallet_balances::Call::<Runtime, Instance>::transfer_allow_death {
288					dest: Runtime::Lookup::unlookup(to),
289					value,
290				},
291				SYSTEM_ACCOUNT_SIZE,
292			)?;
293		}
294
295		log3(
296			handle.context().address,
297			SELECTOR_LOG_TRANSFER,
298			handle.context().caller,
299			to,
300			solidity::encode_event_data(value),
301		)
302		.record(handle)?;
303
304		Ok(true)
305	}
306
307	#[precompile::public("transferFrom(address,address,uint256)")]
308	fn transfer_from(
309		handle: &mut impl PrecompileHandle,
310		from: Address,
311		to: Address,
312		value: U256,
313	) -> EvmResult<bool> {
314		// frame_system::ApprovesStorage:
315		// (2 * (Blake2128(16) + AccountId(20)) + Balanceof(16)
316		handle.record_db_read::<Runtime>(88)?;
317		handle.record_cost(RuntimeHelper::<Runtime>::db_write_gas_cost())?;
318		handle.record_log_costs_manual(3, 32)?;
319
320		let from: H160 = from.into();
321		let to: H160 = to.into();
322
323		{
324			let caller: Runtime::AccountId =
325				Runtime::AddressMapping::into_account_id(handle.context().caller);
326			let from: Runtime::AccountId = Runtime::AddressMapping::into_account_id(from);
327			let to: Runtime::AccountId = Runtime::AddressMapping::into_account_id(to);
328			let value = Self::u256_to_amount(value).in_field("value")?;
329
330			// If caller is "from", it can spend as much as it wants.
331			if caller != from {
332				ApprovesStorage::<Runtime, Instance>::mutate(from.clone(), caller, |entry| {
333					// Get current allowed value, exit if None.
334					let allowed = entry.ok_or(revert("spender not allowed"))?;
335
336					// Remove "value" from allowed, exit if underflow.
337					let allowed = allowed
338						.checked_sub(&value)
339						.ok_or_else(|| revert("trying to spend more than allowed"))?;
340
341					// Update allowed value.
342					*entry = Some(allowed);
343
344					EvmResult::Ok(())
345				})?;
346			}
347
348			// Build call with origin. Here origin is the "from"/owner field.
349			// Dispatch call (if enough gas).
350			RuntimeHelper::<Runtime>::try_dispatch(
351				handle,
352				frame_system::RawOrigin::Signed(from).into(),
353				pallet_balances::Call::<Runtime, Instance>::transfer_allow_death {
354					dest: Runtime::Lookup::unlookup(to),
355					value,
356				},
357				SYSTEM_ACCOUNT_SIZE,
358			)?;
359		}
360
361		log3(
362			handle.context().address,
363			SELECTOR_LOG_TRANSFER,
364			from,
365			to,
366			solidity::encode_event_data(value),
367		)
368		.record(handle)?;
369
370		Ok(true)
371	}
372
373	#[precompile::public("name()")]
374	#[precompile::view]
375	fn name(_handle: &mut impl PrecompileHandle) -> EvmResult<UnboundedBytes> {
376		Ok(Metadata::name().into())
377	}
378
379	#[precompile::public("symbol()")]
380	#[precompile::view]
381	fn symbol(_handle: &mut impl PrecompileHandle) -> EvmResult<UnboundedBytes> {
382		Ok(Metadata::symbol().into())
383	}
384
385	#[precompile::public("decimals()")]
386	#[precompile::view]
387	fn decimals(_handle: &mut impl PrecompileHandle) -> EvmResult<u8> {
388		Ok(Metadata::decimals())
389	}
390
391	#[precompile::public("deposit()")]
392	#[precompile::fallback]
393	#[precompile::payable]
394	fn deposit(handle: &mut impl PrecompileHandle) -> EvmResult {
395		// Deposit only makes sense for the native currency.
396		if !Metadata::is_native_currency() {
397			return Err(RevertReason::UnknownSelector.into());
398		}
399
400		let caller: Runtime::AccountId =
401			Runtime::AddressMapping::into_account_id(handle.context().caller);
402		let precompile = Runtime::AddressMapping::into_account_id(handle.context().address);
403		let amount = Self::u256_to_amount(handle.context().apparent_value)?;
404
405		if amount.into() == U256::from(0u32) {
406			return Err(revert("deposited amount must be non-zero"));
407		}
408
409		handle.record_log_costs_manual(2, 32)?;
410
411		// Send back funds received by the precompile.
412		RuntimeHelper::<Runtime>::try_dispatch(
413			handle,
414			frame_system::RawOrigin::Signed(precompile).into(),
415			pallet_balances::Call::<Runtime, Instance>::transfer_allow_death {
416				dest: Runtime::Lookup::unlookup(caller),
417				value: amount,
418			},
419			SYSTEM_ACCOUNT_SIZE,
420		)?;
421
422		log2(
423			handle.context().address,
424			SELECTOR_LOG_DEPOSIT,
425			handle.context().caller,
426			solidity::encode_event_data(handle.context().apparent_value),
427		)
428		.record(handle)?;
429
430		Ok(())
431	}
432
433	#[precompile::public("withdraw(uint256)")]
434	fn withdraw(handle: &mut impl PrecompileHandle, value: U256) -> EvmResult {
435		// Withdraw only makes sense for the native currency.
436		if !Metadata::is_native_currency() {
437			return Err(RevertReason::UnknownSelector.into());
438		}
439
440		handle.record_log_costs_manual(2, 32)?;
441
442		let account_amount: U256 = {
443			let owner: Runtime::AccountId =
444				Runtime::AddressMapping::into_account_id(handle.context().caller);
445			pallet_balances::Pallet::<Runtime, Instance>::usable_balance(&owner).into()
446		};
447
448		if value > account_amount {
449			return Err(revert("Trying to withdraw more than owned"));
450		}
451
452		log2(
453			handle.context().address,
454			SELECTOR_LOG_WITHDRAWAL,
455			handle.context().caller,
456			solidity::encode_event_data(value),
457		)
458		.record(handle)?;
459
460		Ok(())
461	}
462
463	#[precompile::public("permit(address,address,uint256,uint256,uint8,bytes32,bytes32)")]
464	#[allow(clippy::too_many_arguments)]
465	fn eip2612_permit(
466		handle: &mut impl PrecompileHandle,
467		owner: Address,
468		spender: Address,
469		value: U256,
470		deadline: U256,
471		v: u8,
472		r: H256,
473		s: H256,
474	) -> EvmResult {
475		<Eip2612<Runtime, Metadata, Instance>>::permit(
476			handle, owner, spender, value, deadline, v, r, s,
477		)
478	}
479
480	#[precompile::public("nonces(address)")]
481	#[precompile::view]
482	fn eip2612_nonces(handle: &mut impl PrecompileHandle, owner: Address) -> EvmResult<U256> {
483		<Eip2612<Runtime, Metadata, Instance>>::nonces(handle, owner)
484	}
485
486	#[precompile::public("DOMAIN_SEPARATOR()")]
487	#[precompile::view]
488	fn eip2612_domain_separator(handle: &mut impl PrecompileHandle) -> EvmResult<H256> {
489		<Eip2612<Runtime, Metadata, Instance>>::domain_separator(handle)
490	}
491
492	fn u256_to_amount(value: U256) -> MayRevert<BalanceOf<Runtime, Instance>> {
493		value
494			.try_into()
495			.map_err(|_| RevertReason::value_is_too_large("balance type").into())
496	}
497}