referrerpolicy=no-referrer-when-downgrade

pallet_assets/
precompiles.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
18use crate::{weights::WeightInfo, Call, Config, PhantomData, TransferFlags};
19use alloc::vec::Vec;
20use ethereum_standards::{
21	IERC20,
22	IERC20::{IERC20Calls, IERC20Events},
23};
24use pallet_revive::precompiles::{
25	alloy::{
26		self,
27		primitives::IntoLogData,
28		sol_types::{Revert, SolCall},
29	},
30	AddressMapper, AddressMatcher, Error, Ext, Precompile, RuntimeCosts, H160, H256,
31};
32
33/// Mean of extracting the asset id from the precompile address.
34pub trait AssetIdExtractor {
35	type AssetId;
36	/// Extracts the asset id from the address.
37	fn asset_id_from_address(address: &[u8; 20]) -> Result<Self::AssetId, Error>;
38}
39
40/// The configuration of a pallet-assets precompile.
41pub trait AssetPrecompileConfig {
42	/// The Address matcher used by the precompile.
43	const MATCHER: AddressMatcher;
44
45	/// The [`AssetIdExtractor`] used by the precompile.
46	type AssetIdExtractor: AssetIdExtractor;
47}
48
49/// An `AssetIdExtractor` that stores the asset id directly inside the address.
50pub struct InlineAssetIdExtractor;
51
52impl AssetIdExtractor for InlineAssetIdExtractor {
53	type AssetId = u32;
54	fn asset_id_from_address(addr: &[u8; 20]) -> Result<Self::AssetId, Error> {
55		let bytes: [u8; 4] = addr[0..4].try_into().expect("slice is 4 bytes; qed");
56		let index = u32::from_be_bytes(bytes);
57		return Ok(index.into());
58	}
59}
60
61/// A precompile configuration that uses a prefix [`AddressMatcher`].
62pub struct InlineIdConfig<const PREFIX: u16>;
63
64impl<const P: u16> AssetPrecompileConfig for InlineIdConfig<P> {
65	const MATCHER: AddressMatcher = AddressMatcher::Prefix(core::num::NonZero::new(P).unwrap());
66	type AssetIdExtractor = InlineAssetIdExtractor;
67}
68
69/// An ERC20 precompile.
70pub struct ERC20<Runtime, PrecompileConfig, Instance = ()> {
71	_phantom: PhantomData<(Runtime, PrecompileConfig, Instance)>,
72}
73
74impl<Runtime, PrecompileConfig, Instance: 'static> Precompile
75	for ERC20<Runtime, PrecompileConfig, Instance>
76where
77	PrecompileConfig: AssetPrecompileConfig,
78	Runtime: crate::Config<Instance> + pallet_revive::Config,
79	<<PrecompileConfig as AssetPrecompileConfig>::AssetIdExtractor as AssetIdExtractor>::AssetId:
80		Into<<Runtime as Config<Instance>>::AssetId>,
81	Call<Runtime, Instance>: Into<<Runtime as pallet_revive::Config>::RuntimeCall>,
82	alloy::primitives::U256: TryInto<<Runtime as Config<Instance>>::Balance>,
83
84	// Note can't use From as it's not implemented for alloy::primitives::U256 for unsigned types
85	alloy::primitives::U256: TryFrom<<Runtime as Config<Instance>>::Balance>,
86{
87	type T = Runtime;
88	type Interface = IERC20::IERC20Calls;
89	const MATCHER: AddressMatcher = PrecompileConfig::MATCHER;
90	const HAS_CONTRACT_INFO: bool = false;
91
92	fn call(
93		address: &[u8; 20],
94		input: &Self::Interface,
95		env: &mut impl Ext<T = Self::T>,
96	) -> Result<Vec<u8>, Error> {
97		let asset_id = PrecompileConfig::AssetIdExtractor::asset_id_from_address(address)?.into();
98
99		match input {
100			IERC20Calls::transfer(call) => Self::transfer(asset_id, call, env),
101			IERC20Calls::totalSupply(_) => Self::total_supply(asset_id, env),
102			IERC20Calls::balanceOf(call) => Self::balance_of(asset_id, call, env),
103			IERC20Calls::allowance(call) => Self::allowance(asset_id, call, env),
104			IERC20Calls::approve(call) => Self::approve(asset_id, call, env),
105			IERC20Calls::transferFrom(call) => Self::transfer_from(asset_id, call, env),
106		}
107	}
108}
109
110const ERR_INVALID_CALLER: &str = "Invalid caller";
111const ERR_BALANCE_CONVERSION_FAILED: &str = "Balance conversion failed";
112
113impl<Runtime, PrecompileConfig, Instance: 'static> ERC20<Runtime, PrecompileConfig, Instance>
114where
115	PrecompileConfig: AssetPrecompileConfig,
116	Runtime: crate::Config<Instance> + pallet_revive::Config,
117	<<PrecompileConfig as AssetPrecompileConfig>::AssetIdExtractor as AssetIdExtractor>::AssetId:
118		Into<<Runtime as Config<Instance>>::AssetId>,
119	Call<Runtime, Instance>: Into<<Runtime as pallet_revive::Config>::RuntimeCall>,
120	alloy::primitives::U256: TryInto<<Runtime as Config<Instance>>::Balance>,
121
122	// Note can't use From as it's not implemented for alloy::primitives::U256 for unsigned types
123	alloy::primitives::U256: TryFrom<<Runtime as Config<Instance>>::Balance>,
124{
125	/// Get the caller as an `H160` address.
126	fn caller(env: &mut impl Ext<T = Runtime>) -> Result<H160, Error> {
127		env.caller()
128			.account_id()
129			.map(<Runtime as pallet_revive::Config>::AddressMapper::to_address)
130			.map_err(|_| Error::Revert(Revert { reason: ERR_INVALID_CALLER.into() }))
131	}
132
133	/// Convert a `U256` value to the balance type of the pallet.
134	fn to_balance(
135		value: alloy::primitives::U256,
136	) -> Result<<Runtime as Config<Instance>>::Balance, Error> {
137		value
138			.try_into()
139			.map_err(|_| Error::Revert(Revert { reason: ERR_BALANCE_CONVERSION_FAILED.into() }))
140	}
141
142	/// Convert a balance to a `U256` value.
143	/// Note this is needed cause From is not implemented for unsigned integer types
144	fn to_u256(
145		value: <Runtime as Config<Instance>>::Balance,
146	) -> Result<alloy::primitives::U256, Error> {
147		alloy::primitives::U256::try_from(value)
148			.map_err(|_| Error::Revert(Revert { reason: ERR_BALANCE_CONVERSION_FAILED.into() }))
149	}
150
151	/// Deposit an event to the runtime.
152	fn deposit_event(env: &mut impl Ext<T = Runtime>, event: IERC20Events) -> Result<(), Error> {
153		let (topics, data) = event.into_log_data().split();
154		let topics = topics.into_iter().map(|v| H256(v.0)).collect::<Vec<_>>();
155		env.gas_meter_mut().charge(RuntimeCosts::DepositEvent {
156			num_topic: topics.len() as u32,
157			len: topics.len() as u32,
158		})?;
159		env.deposit_event(topics, data.to_vec());
160		Ok(())
161	}
162
163	/// Execute the transfer call.
164	fn transfer(
165		asset_id: <Runtime as Config<Instance>>::AssetId,
166		call: &IERC20::transferCall,
167		env: &mut impl Ext<T = Runtime>,
168	) -> Result<Vec<u8>, Error> {
169		env.charge(<Runtime as Config<Instance>>::WeightInfo::transfer())?;
170
171		let from = Self::caller(env)?;
172		let dest = <Runtime as pallet_revive::Config>::AddressMapper::to_account_id(
173			&call.to.into_array().into(),
174		);
175
176		let f = TransferFlags { keep_alive: false, best_effort: false, burn_dust: false };
177		crate::Pallet::<Runtime, Instance>::do_transfer(
178			asset_id,
179			&<Runtime as pallet_revive::Config>::AddressMapper::to_account_id(&from),
180			&dest,
181			Self::to_balance(call.value)?,
182			None,
183			f,
184		)?;
185
186		Self::deposit_event(
187			env,
188			IERC20Events::Transfer(IERC20::Transfer {
189				from: from.0.into(),
190				to: call.to,
191				value: call.value,
192			}),
193		)?;
194
195		return Ok(IERC20::transferCall::abi_encode_returns(&true));
196	}
197
198	/// Execute the total supply call.
199	fn total_supply(
200		asset_id: <Runtime as Config<Instance>>::AssetId,
201		env: &mut impl Ext<T = Runtime>,
202	) -> Result<Vec<u8>, Error> {
203		use frame_support::traits::fungibles::Inspect;
204		env.charge(<Runtime as Config<Instance>>::WeightInfo::total_issuance())?;
205
206		let value = Self::to_u256(crate::Pallet::<Runtime, Instance>::total_issuance(asset_id))?;
207		return Ok(IERC20::totalSupplyCall::abi_encode_returns(&value));
208	}
209
210	/// Execute the balance_of call.
211	fn balance_of(
212		asset_id: <Runtime as Config<Instance>>::AssetId,
213		call: &IERC20::balanceOfCall,
214		env: &mut impl Ext<T = Runtime>,
215	) -> Result<Vec<u8>, Error> {
216		env.charge(<Runtime as Config<Instance>>::WeightInfo::balance())?;
217		let account = call.account.into_array().into();
218		let account = <Runtime as pallet_revive::Config>::AddressMapper::to_account_id(&account);
219		let value = Self::to_u256(crate::Pallet::<Runtime, Instance>::balance(asset_id, account))?;
220		return Ok(IERC20::balanceOfCall::abi_encode_returns(&value));
221	}
222
223	/// Execute the allowance call.
224	fn allowance(
225		asset_id: <Runtime as Config<Instance>>::AssetId,
226		call: &IERC20::allowanceCall,
227		env: &mut impl Ext<T = Runtime>,
228	) -> Result<Vec<u8>, Error> {
229		env.charge(<Runtime as Config<Instance>>::WeightInfo::allowance())?;
230		use frame_support::traits::fungibles::approvals::Inspect;
231		let owner = call.owner.into_array().into();
232		let owner = <Runtime as pallet_revive::Config>::AddressMapper::to_account_id(&owner);
233
234		let spender = call.spender.into_array().into();
235		let spender = <Runtime as pallet_revive::Config>::AddressMapper::to_account_id(&spender);
236		let value = Self::to_u256(crate::Pallet::<Runtime, Instance>::allowance(
237			asset_id, &owner, &spender,
238		))?;
239
240		return Ok(IERC20::balanceOfCall::abi_encode_returns(&value));
241	}
242
243	/// Execute the approve call.
244	fn approve(
245		asset_id: <Runtime as Config<Instance>>::AssetId,
246		call: &IERC20::approveCall,
247		env: &mut impl Ext<T = Runtime>,
248	) -> Result<Vec<u8>, Error> {
249		env.charge(<Runtime as Config<Instance>>::WeightInfo::approve_transfer())?;
250		let owner = Self::caller(env)?;
251		let spender = call.spender.into_array().into();
252		let spender = <Runtime as pallet_revive::Config>::AddressMapper::to_account_id(&spender);
253
254		crate::Pallet::<Runtime, Instance>::do_approve_transfer(
255			asset_id,
256			&<Runtime as pallet_revive::Config>::AddressMapper::to_account_id(&owner),
257			&spender,
258			Self::to_balance(call.value)?,
259		)?;
260
261		Self::deposit_event(
262			env,
263			IERC20Events::Approval(IERC20::Approval {
264				owner: owner.0.into(),
265				spender: call.spender,
266				value: call.value,
267			}),
268		)?;
269
270		return Ok(IERC20::approveCall::abi_encode_returns(&true));
271	}
272
273	/// Execute the transfer_from call.
274	fn transfer_from(
275		asset_id: <Runtime as Config<Instance>>::AssetId,
276		call: &IERC20::transferFromCall,
277		env: &mut impl Ext<T = Runtime>,
278	) -> Result<Vec<u8>, Error> {
279		env.charge(<Runtime as Config<Instance>>::WeightInfo::transfer_approved())?;
280		let spender = Self::caller(env)?;
281		let spender = <Runtime as pallet_revive::Config>::AddressMapper::to_account_id(&spender);
282
283		let from = call.from.into_array().into();
284		let from = <Runtime as pallet_revive::Config>::AddressMapper::to_account_id(&from);
285
286		let to = call.to.into_array().into();
287		let to = <Runtime as pallet_revive::Config>::AddressMapper::to_account_id(&to);
288
289		crate::Pallet::<Runtime, Instance>::do_transfer_approved(
290			asset_id,
291			&from,
292			&spender,
293			&to,
294			Self::to_balance(call.value)?,
295		)?;
296
297		Self::deposit_event(
298			env,
299			IERC20Events::Transfer(IERC20::Transfer {
300				from: call.from,
301				to: call.to,
302				value: call.value,
303			}),
304		)?;
305
306		return Ok(IERC20::transferFromCall::abi_encode_returns(&true));
307	}
308}
309
310#[cfg(test)]
311mod test {
312	use super::*;
313	use crate::{
314		mock::{new_test_ext, Assets, Balances, RuntimeEvent, RuntimeOrigin, System, Test},
315		precompiles::alloy::hex,
316	};
317	use alloy::primitives::U256;
318	use frame_support::{assert_ok, traits::Currency};
319	use pallet_revive::DepositLimit;
320	use sp_core::H160;
321	use sp_runtime::Weight;
322
323	fn assert_contract_event(contract: H160, event: IERC20Events) {
324		let (topics, data) = event.into_log_data().split();
325		let topics = topics.into_iter().map(|v| H256(v.0)).collect::<Vec<_>>();
326		System::assert_has_event(RuntimeEvent::Revive(pallet_revive::Event::ContractEmitted {
327			contract,
328			data: data.to_vec(),
329			topics,
330		}));
331	}
332
333	#[test]
334	fn asset_id_extractor_works() {
335		let address: [u8; 20] =
336			hex::const_decode_to_array(b"0000053900000000000000000000000001200000").unwrap();
337		assert!(InlineIdConfig::<0x0120>::MATCHER.matches(&address));
338		assert_eq!(
339			<InlineIdConfig<0x0120> as AssetPrecompileConfig>::AssetIdExtractor::asset_id_from_address(
340				&address
341			)
342			.unwrap(),
343			1337u32
344		);
345	}
346
347	#[test]
348	fn precompile_transfer_works() {
349		new_test_ext().execute_with(|| {
350			let asset_id = 0u32;
351			let asset_addr = H160::from(
352				hex::const_decode_to_array(b"0000000000000000000000000000000001200000").unwrap(),
353			);
354
355			let from = 123456789;
356			let to = 987654321;
357
358			Balances::make_free_balance_be(&from, 100);
359			Balances::make_free_balance_be(&to, 100);
360
361			let from_addr = <Test as pallet_revive::Config>::AddressMapper::to_address(&from);
362			let to_addr = <Test as pallet_revive::Config>::AddressMapper::to_address(&to);
363			assert_ok!(Assets::force_create(RuntimeOrigin::root(), asset_id, from, true, 1));
364			assert_ok!(Assets::mint(RuntimeOrigin::signed(from), asset_id, from, 100));
365
366			let data =
367				IERC20::transferCall { to: to_addr.0.into(), value: U256::from(10) }.abi_encode();
368
369			pallet_revive::Pallet::<Test>::bare_call(
370				RuntimeOrigin::signed(from),
371				H160::from(asset_addr),
372				0u32.into(),
373				Weight::MAX,
374				DepositLimit::UnsafeOnlyForDryRun,
375				data,
376			);
377
378			assert_contract_event(
379				asset_addr,
380				IERC20Events::Transfer(IERC20::Transfer {
381					from: from_addr.0.into(),
382					to: to_addr.0.into(),
383					value: U256::from(10),
384				}),
385			);
386
387			assert_eq!(Assets::balance(asset_id, from), 90);
388			assert_eq!(Assets::balance(asset_id, to), 10);
389		});
390	}
391
392	#[test]
393	fn total_supply_works() {
394		new_test_ext().execute_with(|| {
395			let asset_id = 0u32;
396			let asset_addr =
397				hex::const_decode_to_array(b"0000000000000000000000000000000001200000").unwrap();
398
399			let owner = 123456789;
400
401			Balances::make_free_balance_be(&owner, 100);
402			assert_ok!(Assets::force_create(RuntimeOrigin::root(), asset_id, owner, true, 1));
403			assert_ok!(Assets::mint(RuntimeOrigin::signed(owner), asset_id, owner, 1000));
404
405			let data = IERC20::totalSupplyCall {}.abi_encode();
406
407			let data = pallet_revive::Pallet::<Test>::bare_call(
408				RuntimeOrigin::signed(owner),
409				H160::from(asset_addr),
410				0u32.into(),
411				Weight::MAX,
412				DepositLimit::UnsafeOnlyForDryRun,
413				data,
414			)
415			.result
416			.unwrap()
417			.data;
418
419			let ret = IERC20::totalSupplyCall::abi_decode_returns(&data).unwrap();
420			assert_eq!(ret, U256::from(1000));
421		});
422	}
423
424	#[test]
425	fn balance_of_works() {
426		new_test_ext().execute_with(|| {
427			let asset_id = 0u32;
428			let asset_addr =
429				hex::const_decode_to_array(b"0000000000000000000000000000000001200000").unwrap();
430
431			let owner = 123456789;
432
433			assert_ok!(Assets::force_create(RuntimeOrigin::root(), asset_id, owner, true, 1));
434			assert_ok!(Assets::mint(RuntimeOrigin::signed(owner), asset_id, owner, 1000));
435
436			let account =
437				<Test as pallet_revive::Config>::AddressMapper::to_address(&owner).0.into();
438			let data = IERC20::balanceOfCall { account }.abi_encode();
439
440			let data = pallet_revive::Pallet::<Test>::bare_call(
441				RuntimeOrigin::signed(owner),
442				H160::from(asset_addr),
443				0u32.into(),
444				Weight::MAX,
445				DepositLimit::UnsafeOnlyForDryRun,
446				data,
447			)
448			.result
449			.unwrap()
450			.data;
451
452			let ret = IERC20::balanceOfCall::abi_decode_returns(&data).unwrap();
453			assert_eq!(ret, U256::from(1000));
454		});
455	}
456
457	#[test]
458	fn approval_works() {
459		use frame_support::traits::fungibles::approvals::Inspect;
460
461		new_test_ext().execute_with(|| {
462			let asset_id = 0u32;
463			let asset_addr = H160::from(
464				hex::const_decode_to_array(b"0000000000000000000000000000000001200000").unwrap(),
465			);
466
467			let owner = 123456789;
468			let spender = 987654321;
469			let other = 1122334455;
470
471			Balances::make_free_balance_be(&owner, 100);
472			Balances::make_free_balance_be(&spender, 100);
473			Balances::make_free_balance_be(&other, 100);
474
475			let owner_addr = <Test as pallet_revive::Config>::AddressMapper::to_address(&owner);
476			let spender_addr = <Test as pallet_revive::Config>::AddressMapper::to_address(&spender);
477			let other_addr = <Test as pallet_revive::Config>::AddressMapper::to_address(&other);
478
479			assert_ok!(Assets::force_create(RuntimeOrigin::root(), asset_id, owner, true, 1));
480			assert_ok!(Assets::mint(RuntimeOrigin::signed(owner), asset_id, owner, 100));
481
482			let data =
483				IERC20::approveCall { spender: spender_addr.0.into(), value: U256::from(25) }
484					.abi_encode();
485
486			pallet_revive::Pallet::<Test>::bare_call(
487				RuntimeOrigin::signed(owner),
488				H160::from(asset_addr),
489				0u32.into(),
490				Weight::MAX,
491				DepositLimit::UnsafeOnlyForDryRun,
492				data,
493			);
494
495			assert_contract_event(
496				asset_addr,
497				IERC20Events::Approval(IERC20::Approval {
498					owner: owner_addr.0.into(),
499					spender: spender_addr.0.into(),
500					value: U256::from(25),
501				}),
502			);
503
504			let data = IERC20::allowanceCall {
505				owner: owner_addr.0.into(),
506				spender: spender_addr.0.into(),
507			}
508			.abi_encode();
509
510			let data = pallet_revive::Pallet::<Test>::bare_call(
511				RuntimeOrigin::signed(owner),
512				H160::from(asset_addr),
513				0u32.into(),
514				Weight::MAX,
515				DepositLimit::UnsafeOnlyForDryRun,
516				data,
517			)
518			.result
519			.unwrap()
520			.data;
521
522			let ret = IERC20::allowanceCall::abi_decode_returns(&data).unwrap();
523			assert_eq!(ret, U256::from(25));
524
525			let data = IERC20::transferFromCall {
526				from: owner_addr.0.into(),
527				to: other_addr.0.into(),
528				value: U256::from(10),
529			}
530			.abi_encode();
531
532			pallet_revive::Pallet::<Test>::bare_call(
533				RuntimeOrigin::signed(spender),
534				H160::from(asset_addr),
535				0u32.into(),
536				Weight::MAX,
537				DepositLimit::UnsafeOnlyForDryRun,
538				data,
539			);
540			assert_eq!(Assets::balance(asset_id, owner), 90);
541			assert_eq!(Assets::allowance(asset_id, &owner, &spender), 15);
542			assert_eq!(Assets::balance(asset_id, other), 10);
543
544			assert_contract_event(
545				asset_addr,
546				IERC20Events::Transfer(IERC20::Transfer {
547					from: owner_addr.0.into(),
548					to: other_addr.0.into(),
549					value: U256::from(10),
550				}),
551			);
552		});
553	}
554}