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 pallet_assets::{weights::WeightInfo, Call, Config, TransferFlags};
30use pallet_revive::precompiles::{
31	alloy::{
32		self,
33		primitives::IntoLogData,
34		sol_types::{Revert, SolCall},
35	},
36	AddressMapper, AddressMatcher, Error, Ext, Precompile, RuntimeCosts, H160, H256,
37};
38
39#[cfg(test)]
40mod mock;
41#[cfg(test)]
42mod tests;
43
44/// Mean of extracting the asset id from the precompile address.
45pub trait AssetIdExtractor {
46	type AssetId;
47	/// Extracts the asset id from the address.
48	fn asset_id_from_address(address: &[u8; 20]) -> Result<Self::AssetId, Error>;
49}
50
51/// The configuration of a pallet-assets precompile.
52pub trait AssetPrecompileConfig {
53	/// The Address matcher used by the precompile.
54	const MATCHER: AddressMatcher;
55
56	/// The [`AssetIdExtractor`] used by the precompile.
57	type AssetIdExtractor: AssetIdExtractor;
58}
59
60/// An `AssetIdExtractor` that stores the asset id directly inside the address.
61pub struct InlineAssetIdExtractor;
62
63impl AssetIdExtractor for InlineAssetIdExtractor {
64	type AssetId = u32;
65	fn asset_id_from_address(addr: &[u8; 20]) -> Result<Self::AssetId, Error> {
66		let bytes: [u8; 4] = addr[0..4].try_into().expect("slice is 4 bytes; qed");
67		let index = u32::from_be_bytes(bytes);
68		return Ok(index.into());
69	}
70}
71
72/// A precompile configuration that uses a prefix [`AddressMatcher`].
73pub struct InlineIdConfig<const PREFIX: u16>;
74
75impl<const P: u16> AssetPrecompileConfig for InlineIdConfig<P> {
76	const MATCHER: AddressMatcher = AddressMatcher::Prefix(core::num::NonZero::new(P).unwrap());
77	type AssetIdExtractor = InlineAssetIdExtractor;
78}
79
80/// An ERC20 precompile.
81pub struct ERC20<Runtime, PrecompileConfig, Instance = ()> {
82	_phantom: PhantomData<(Runtime, PrecompileConfig, Instance)>,
83}
84
85impl<Runtime, PrecompileConfig, Instance: 'static> Precompile
86	for ERC20<Runtime, PrecompileConfig, Instance>
87where
88	PrecompileConfig: AssetPrecompileConfig,
89	Runtime: crate::Config<Instance> + pallet_revive::Config,
90	<<PrecompileConfig as AssetPrecompileConfig>::AssetIdExtractor as AssetIdExtractor>::AssetId:
91		Into<<Runtime as Config<Instance>>::AssetId>,
92	Call<Runtime, Instance>: Into<<Runtime as pallet_revive::Config>::RuntimeCall>,
93	alloy::primitives::U256: TryInto<<Runtime as Config<Instance>>::Balance>,
94
95	// Note can't use From as it's not implemented for alloy::primitives::U256 for unsigned types
96	alloy::primitives::U256: TryFrom<<Runtime as Config<Instance>>::Balance>,
97{
98	type T = Runtime;
99	type Interface = IERC20::IERC20Calls;
100	const MATCHER: AddressMatcher = PrecompileConfig::MATCHER;
101	const HAS_CONTRACT_INFO: bool = false;
102
103	fn call(
104		address: &[u8; 20],
105		input: &Self::Interface,
106		env: &mut impl Ext<T = Self::T>,
107	) -> Result<Vec<u8>, Error> {
108		let asset_id = PrecompileConfig::AssetIdExtractor::asset_id_from_address(address)?.into();
109
110		match input {
111			IERC20Calls::transfer(_) | IERC20Calls::approve(_) | IERC20Calls::transferFrom(_)
112				if env.is_read_only() =>
113				Err(Error::Error(pallet_revive::Error::<Self::T>::StateChangeDenied.into())),
114
115			IERC20Calls::transfer(call) => Self::transfer(asset_id, call, env),
116			IERC20Calls::totalSupply(_) => Self::total_supply(asset_id, env),
117			IERC20Calls::balanceOf(call) => Self::balance_of(asset_id, call, env),
118			IERC20Calls::allowance(call) => Self::allowance(asset_id, call, env),
119			IERC20Calls::approve(call) => Self::approve(asset_id, call, env),
120			IERC20Calls::transferFrom(call) => Self::transfer_from(asset_id, call, env),
121		}
122	}
123}
124
125const ERR_INVALID_CALLER: &str = "Invalid caller";
126const ERR_BALANCE_CONVERSION_FAILED: &str = "Balance conversion failed";
127
128impl<Runtime, PrecompileConfig, Instance: 'static> ERC20<Runtime, PrecompileConfig, Instance>
129where
130	PrecompileConfig: AssetPrecompileConfig,
131	Runtime: crate::Config<Instance> + pallet_revive::Config,
132	<<PrecompileConfig as AssetPrecompileConfig>::AssetIdExtractor as AssetIdExtractor>::AssetId:
133		Into<<Runtime as Config<Instance>>::AssetId>,
134	Call<Runtime, Instance>: Into<<Runtime as pallet_revive::Config>::RuntimeCall>,
135	alloy::primitives::U256: TryInto<<Runtime as Config<Instance>>::Balance>,
136
137	// Note can't use From as it's not implemented for alloy::primitives::U256 for unsigned types
138	alloy::primitives::U256: TryFrom<<Runtime as Config<Instance>>::Balance>,
139{
140	/// Get the caller as an `H160` address.
141	fn caller(env: &mut impl Ext<T = Runtime>) -> Result<H160, Error> {
142		env.caller()
143			.account_id()
144			.map(<Runtime as pallet_revive::Config>::AddressMapper::to_address)
145			.map_err(|_| Error::Revert(Revert { reason: ERR_INVALID_CALLER.into() }))
146	}
147
148	/// Convert a `U256` value to the balance type of the pallet.
149	fn to_balance(
150		value: alloy::primitives::U256,
151	) -> Result<<Runtime as Config<Instance>>::Balance, Error> {
152		value
153			.try_into()
154			.map_err(|_| Error::Revert(Revert { reason: ERR_BALANCE_CONVERSION_FAILED.into() }))
155	}
156
157	/// Convert a balance to a `U256` value.
158	/// Note this is needed cause From is not implemented for unsigned integer types
159	fn to_u256(
160		value: <Runtime as Config<Instance>>::Balance,
161	) -> Result<alloy::primitives::U256, Error> {
162		alloy::primitives::U256::try_from(value)
163			.map_err(|_| Error::Revert(Revert { reason: ERR_BALANCE_CONVERSION_FAILED.into() }))
164	}
165
166	/// Deposit an event to the runtime.
167	fn deposit_event(env: &mut impl Ext<T = Runtime>, event: IERC20Events) -> Result<(), Error> {
168		let (topics, data) = event.into_log_data().split();
169		let topics = topics.into_iter().map(|v| H256(v.0)).collect::<Vec<_>>();
170		env.frame_meter_mut().charge_weight_token(RuntimeCosts::DepositEvent {
171			num_topic: topics.len() as u32,
172			len: topics.len() as u32,
173		})?;
174		env.deposit_event(topics, data.to_vec());
175		Ok(())
176	}
177
178	/// Execute the transfer call.
179	fn transfer(
180		asset_id: <Runtime as Config<Instance>>::AssetId,
181		call: &IERC20::transferCall,
182		env: &mut impl Ext<T = Runtime>,
183	) -> Result<Vec<u8>, Error> {
184		env.charge(<Runtime as Config<Instance>>::WeightInfo::transfer())?;
185
186		let from = Self::caller(env)?;
187		let dest = <Runtime as pallet_revive::Config>::AddressMapper::to_account_id(
188			&call.to.into_array().into(),
189		);
190
191		let f = TransferFlags { keep_alive: false, best_effort: false, burn_dust: false };
192		pallet_assets::Pallet::<Runtime, Instance>::do_transfer(
193			asset_id,
194			&<Runtime as pallet_revive::Config>::AddressMapper::to_account_id(&from),
195			&dest,
196			Self::to_balance(call.value)?,
197			None,
198			f,
199		)?;
200
201		Self::deposit_event(
202			env,
203			IERC20Events::Transfer(IERC20::Transfer {
204				from: from.0.into(),
205				to: call.to,
206				value: call.value,
207			}),
208		)?;
209
210		return Ok(IERC20::transferCall::abi_encode_returns(&true));
211	}
212
213	/// Execute the total supply call.
214	fn total_supply(
215		asset_id: <Runtime as Config<Instance>>::AssetId,
216		env: &mut impl Ext<T = Runtime>,
217	) -> Result<Vec<u8>, Error> {
218		use frame_support::traits::fungibles::Inspect;
219		env.charge(<Runtime as Config<Instance>>::WeightInfo::total_issuance())?;
220
221		let value =
222			Self::to_u256(pallet_assets::Pallet::<Runtime, Instance>::total_issuance(asset_id))?;
223		return Ok(IERC20::totalSupplyCall::abi_encode_returns(&value));
224	}
225
226	/// Execute the balance_of call.
227	fn balance_of(
228		asset_id: <Runtime as Config<Instance>>::AssetId,
229		call: &IERC20::balanceOfCall,
230		env: &mut impl Ext<T = Runtime>,
231	) -> Result<Vec<u8>, Error> {
232		env.charge(<Runtime as Config<Instance>>::WeightInfo::balance())?;
233		let account = call.account.into_array().into();
234		let account = <Runtime as pallet_revive::Config>::AddressMapper::to_account_id(&account);
235		let value =
236			Self::to_u256(pallet_assets::Pallet::<Runtime, Instance>::balance(asset_id, account))?;
237		return Ok(IERC20::balanceOfCall::abi_encode_returns(&value));
238	}
239
240	/// Execute the allowance call.
241	fn allowance(
242		asset_id: <Runtime as Config<Instance>>::AssetId,
243		call: &IERC20::allowanceCall,
244		env: &mut impl Ext<T = Runtime>,
245	) -> Result<Vec<u8>, Error> {
246		env.charge(<Runtime as Config<Instance>>::WeightInfo::allowance())?;
247		use frame_support::traits::fungibles::approvals::Inspect;
248		let owner = call.owner.into_array().into();
249		let owner = <Runtime as pallet_revive::Config>::AddressMapper::to_account_id(&owner);
250
251		let spender = call.spender.into_array().into();
252		let spender = <Runtime as pallet_revive::Config>::AddressMapper::to_account_id(&spender);
253		let value = Self::to_u256(pallet_assets::Pallet::<Runtime, Instance>::allowance(
254			asset_id, &owner, &spender,
255		))?;
256
257		return Ok(IERC20::balanceOfCall::abi_encode_returns(&value));
258	}
259
260	/// Execute the approve call.
261	fn approve(
262		asset_id: <Runtime as Config<Instance>>::AssetId,
263		call: &IERC20::approveCall,
264		env: &mut impl Ext<T = Runtime>,
265	) -> Result<Vec<u8>, Error> {
266		env.charge(<Runtime as Config<Instance>>::WeightInfo::approve_transfer())?;
267		let owner = Self::caller(env)?;
268		let spender = call.spender.into_array().into();
269		let spender = <Runtime as pallet_revive::Config>::AddressMapper::to_account_id(&spender);
270
271		pallet_assets::Pallet::<Runtime, Instance>::do_approve_transfer(
272			asset_id,
273			&<Runtime as pallet_revive::Config>::AddressMapper::to_account_id(&owner),
274			&spender,
275			Self::to_balance(call.value)?,
276		)?;
277
278		Self::deposit_event(
279			env,
280			IERC20Events::Approval(IERC20::Approval {
281				owner: owner.0.into(),
282				spender: call.spender,
283				value: call.value,
284			}),
285		)?;
286
287		return Ok(IERC20::approveCall::abi_encode_returns(&true));
288	}
289
290	/// Execute the transfer_from call.
291	fn transfer_from(
292		asset_id: <Runtime as Config<Instance>>::AssetId,
293		call: &IERC20::transferFromCall,
294		env: &mut impl Ext<T = Runtime>,
295	) -> Result<Vec<u8>, Error> {
296		env.charge(<Runtime as Config<Instance>>::WeightInfo::transfer_approved())?;
297		let spender = Self::caller(env)?;
298		let spender = <Runtime as pallet_revive::Config>::AddressMapper::to_account_id(&spender);
299
300		let from = call.from.into_array().into();
301		let from = <Runtime as pallet_revive::Config>::AddressMapper::to_account_id(&from);
302
303		let to = call.to.into_array().into();
304		let to = <Runtime as pallet_revive::Config>::AddressMapper::to_account_id(&to);
305
306		pallet_assets::Pallet::<Runtime, Instance>::do_transfer_approved(
307			asset_id,
308			&from,
309			&spender,
310			&to,
311			Self::to_balance(call.value)?,
312		)?;
313
314		Self::deposit_event(
315			env,
316			IERC20Events::Transfer(IERC20::Transfer {
317				from: call.from,
318				to: call.to,
319				value: call.value,
320			}),
321		)?;
322
323		return Ok(IERC20::transferFromCall::abi_encode_returns(&true));
324	}
325}