Skip to main content

assets_common/
erc20_transactor.rs

1// Copyright (C) Parity Technologies (UK) Ltd.
2// This file is part of Cumulus.
3// SPDX-License-Identifier: Apache-2.0
4
5// Licensed under the Apache License, Version 2.0 (the "License");
6// you may not use this file except in compliance with the License.
7// You may obtain a copy of the License at
8//
9// 	http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing, software
12// distributed under the License is distributed on an "AS IS" BASIS,
13// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14// See the License for the specific language governing permissions and
15// limitations under the License.
16
17//! The ERC20 Asset Transactor.
18
19use alloc::boxed::Box;
20use core::marker::PhantomData;
21use ethereum_standards::IERC20;
22use frame_support::{
23	defensive_assert,
24	traits::{
25		fungible::Inspect,
26		tokens::imbalance::{
27			ImbalanceAccounting, UnsafeConstructorDestructor, UnsafeManualAccounting,
28		},
29		OriginTrait,
30	},
31};
32use frame_system::pallet_prelude::OriginFor;
33use pallet_revive::{
34	precompiles::alloy::{
35		primitives::{Address, U256 as EU256},
36		sol_types::SolCall,
37	},
38	AddressMapper, ContractResult, ExecConfig, MomentOf, TransactionLimits,
39};
40use sp_core::{Get, H160, H256, U256};
41use sp_runtime::Weight;
42use xcm::latest::prelude::*;
43use xcm_executor::{
44	traits::{ConvertLocation, Error as MatchError, MatchesFungibles, TransactAsset},
45	AssetsInHolding,
46};
47
48type BalanceOf<T> = <<T as pallet_revive::Config>::Currency as Inspect<
49	<T as frame_system::Config>::AccountId,
50>>::Balance;
51
52/// An Asset Transactor that deals with ERC20 tokens.
53pub struct ERC20Transactor<
54	T,
55	Matcher,
56	AccountIdConverter,
57	WeightLimit,
58	StorageDepositLimit,
59	AccountId,
60	TransfersCheckingAccount,
61>(
62	PhantomData<(
63		T,
64		Matcher,
65		AccountIdConverter,
66		WeightLimit,
67		StorageDepositLimit,
68		AccountId,
69		TransfersCheckingAccount,
70	)>,
71);
72
73/// A minimal imbalance tracking type that holds an ERC20 token amount.
74///
75/// This type implements the necessary imbalance accounting traits but does not perform
76/// runtime-level balance enforcement. It's used to track ERC20 token amounts within XCM
77/// asset holdings, where the actual balance constraints are enforced by the ERC20 smart
78/// contract itself rather than the runtime.
79struct Erc20Credit(u128);
80impl UnsafeConstructorDestructor<u128> for Erc20Credit {
81	fn unsafe_clone(&self) -> Box<dyn ImbalanceAccounting<u128>> {
82		Box::new(Erc20Credit(self.0))
83	}
84	fn forget_imbalance(&mut self) -> u128 {
85		let amount = self.0;
86		self.0 = 0;
87		amount
88	}
89}
90
91impl UnsafeManualAccounting<u128> for Erc20Credit {
92	fn saturating_subsume(&mut self, mut other: Box<dyn ImbalanceAccounting<u128>>) {
93		let amount = other.forget_imbalance();
94		self.0 = self.0.saturating_add(amount);
95	}
96}
97
98impl ImbalanceAccounting<u128> for Erc20Credit {
99	fn amount(&self) -> u128 {
100		self.0
101	}
102	fn saturating_take(&mut self, amount: u128) -> Box<dyn ImbalanceAccounting<u128>> {
103		let new = self.0.min(amount);
104		self.0 = self.0 - new;
105		Box::new(Erc20Credit(new))
106	}
107}
108
109impl<
110		AccountId: Eq + Clone,
111		T: pallet_revive::Config<AccountId = AccountId>,
112		AccountIdConverter: ConvertLocation<AccountId>,
113		Matcher: MatchesFungibles<H160, u128>,
114		WeightLimit: Get<Weight>,
115		StorageDepositLimit: Get<BalanceOf<T>>,
116		TransfersCheckingAccount: Get<AccountId>,
117	> TransactAsset
118	for ERC20Transactor<
119		T,
120		Matcher,
121		AccountIdConverter,
122		WeightLimit,
123		StorageDepositLimit,
124		AccountId,
125		TransfersCheckingAccount,
126	>
127where
128	BalanceOf<T>: Into<U256> + TryFrom<U256>,
129	MomentOf<T>: Into<U256>,
130	T::Hash: frame_support::traits::IsType<H256>,
131{
132	fn can_check_in(_origin: &Location, _what: &Asset, _context: &XcmContext) -> XcmResult {
133		// We don't support teleports.
134		Err(XcmError::Unimplemented)
135	}
136
137	fn check_in(_origin: &Location, _what: &Asset, _context: &XcmContext) {
138		// We don't support teleports.
139	}
140
141	fn can_check_out(_destination: &Location, _what: &Asset, _context: &XcmContext) -> XcmResult {
142		// We don't support teleports.
143		Err(XcmError::Unimplemented)
144	}
145
146	fn check_out(_destination: &Location, _what: &Asset, _context: &XcmContext) {
147		// We don't support teleports.
148	}
149
150	fn withdraw_asset_with_surplus(
151		what: &Asset,
152		who: &Location,
153		_context: Option<&XcmContext>,
154	) -> Result<(AssetsInHolding, Weight), XcmError> {
155		tracing::trace!(
156			target: "xcm::transactor::erc20::withdraw",
157			?what, ?who,
158		);
159		let (asset_id, amount) = Matcher::matches_fungibles(what)?;
160		let who = AccountIdConverter::convert_location(who)
161			.ok_or(MatchError::AccountIdConversionFailed)?;
162		// We need to map the 32 byte checking account to a 20 byte account.
163		let checking_account_eth = T::AddressMapper::to_address(&TransfersCheckingAccount::get());
164		let checking_address = Address::from(Into::<[u8; 20]>::into(checking_account_eth));
165		let weight_limit = WeightLimit::get();
166		// To withdraw, we actually transfer to the checking account.
167		// We do this using the solidity ERC20 interface.
168		let data =
169			IERC20::transferCall { to: checking_address, value: EU256::from(amount) }.abi_encode();
170		let ContractResult { result, weight_consumed, storage_deposit, .. } =
171			pallet_revive::Pallet::<T>::bare_call(
172				OriginFor::<T>::signed(who.clone()),
173				asset_id,
174				U256::zero(),
175				TransactionLimits::WeightAndDeposit {
176					weight_limit,
177					deposit_limit: StorageDepositLimit::get(),
178				},
179				data,
180				&ExecConfig::new_substrate_tx(),
181			);
182		// We need to return this surplus for the executor to allow refunding it.
183		let surplus = weight_limit.saturating_sub(weight_consumed);
184		tracing::trace!(target: "xcm::transactor::erc20::withdraw", ?weight_consumed, ?surplus, ?storage_deposit);
185		if let Ok(return_value) = result {
186			tracing::trace!(target: "xcm::transactor::erc20::withdraw", ?return_value, "Return value by withdraw_asset");
187			if return_value.did_revert() {
188				tracing::debug!(target: "xcm::transactor::erc20::withdraw", "ERC20 contract reverted");
189				Err(XcmError::FailedToTransactAsset("ERC20 contract reverted"))
190			} else {
191				let is_success = IERC20::transferCall::abi_decode_returns_validate(&return_value.data).map_err(|error| {
192					tracing::debug!(target: "xcm::transactor::erc20::withdraw", ?error, "ERC20 contract result couldn't decode");
193					XcmError::FailedToTransactAsset("ERC20 contract result couldn't decode")
194				})?;
195				if is_success {
196					tracing::trace!(target: "xcm::transactor::erc20::withdraw", "ERC20 contract was successful");
197					Ok((
198						AssetsInHolding::new_from_fungible_credit(
199							what.id.clone(),
200							Box::new(Erc20Credit(amount)),
201						),
202						surplus,
203					))
204				} else {
205					tracing::debug!(target: "xcm::transactor::erc20::withdraw", "contract transfer failed");
206					Err(XcmError::FailedToTransactAsset("ERC20 contract transfer failed"))
207				}
208			}
209		} else {
210			tracing::debug!(target: "xcm::transactor::erc20::withdraw", ?result, "Error");
211			// This error could've been duplicate smart contract, out of gas, etc.
212			// If the issue is gas, there's nothing the user can change in the XCM
213			// that will make this work since there's a hardcoded gas limit.
214			Err(XcmError::FailedToTransactAsset("ERC20 contract execution errored"))
215		}
216	}
217
218	/// Deposits assets from holding to a beneficiary account via ERC20 transfer.
219	///
220	/// Note: This implementation only handles a single fungible asset at a time. The
221	/// `AssetsInHolding` parameter is required by the `TransactAsset` trait, but callers
222	/// should ensure only one asset is passed. If multiple assets are present, only the
223	/// first fungible asset will be deposited and the rest will be silently ignored.
224	/// The `defensive_assert!` helps catch misuse during development.
225	fn deposit_asset_with_surplus(
226		what: AssetsInHolding,
227		who: &Location,
228		_context: Option<&XcmContext>,
229	) -> Result<Weight, (AssetsInHolding, XcmError)> {
230		tracing::trace!(
231			target: "xcm::transactor::erc20::deposit",
232			?what, ?who,
233		);
234		defensive_assert!(what.len() == 1, "Trying to deposit more than one asset!");
235		// Check we handle this asset.
236		let maybe = what
237			.fungible_assets_iter()
238			.next()
239			.and_then(|asset| Matcher::matches_fungibles(&asset).ok());
240		let (asset_contract_id, amount) = match maybe {
241			Some(inner) => inner,
242			None => return Err((what, MatchError::AssetNotHandled.into())),
243		};
244		let who = match AccountIdConverter::convert_location(who) {
245			Some(inner) => inner,
246			None => return Err((what, MatchError::AccountIdConversionFailed.into())),
247		};
248		// We need to map the 32 byte beneficiary account to a 20 byte account.
249		let eth_address = T::AddressMapper::to_address(&who);
250		let address = Address::from(Into::<[u8; 20]>::into(eth_address));
251		// To deposit, we actually transfer from the checking account to the beneficiary.
252		// We do this using the solidity ERC20 interface.
253		let data = IERC20::transferCall { to: address, value: EU256::from(amount) }.abi_encode();
254		let weight_limit = WeightLimit::get();
255		let ContractResult { result, weight_consumed, storage_deposit, .. } =
256			pallet_revive::Pallet::<T>::bare_call(
257				OriginFor::<T>::signed(TransfersCheckingAccount::get()),
258				asset_contract_id,
259				U256::zero(),
260				TransactionLimits::WeightAndDeposit {
261					weight_limit,
262					deposit_limit: StorageDepositLimit::get(),
263				},
264				data,
265				&ExecConfig::new_substrate_tx(),
266			);
267		// We need to return this surplus for the executor to allow refunding it.
268		let surplus = weight_limit.saturating_sub(weight_consumed);
269		tracing::trace!(target: "xcm::transactor::erc20::deposit", ?weight_consumed, ?surplus, ?storage_deposit);
270		if let Ok(return_value) = result {
271			tracing::trace!(target: "xcm::transactor::erc20::deposit", ?return_value, "Return value");
272			if return_value.did_revert() {
273				tracing::debug!(target: "xcm::transactor::erc20::deposit", "Contract reverted");
274				Err((what, XcmError::FailedToTransactAsset("ERC20 contract reverted")))
275			} else {
276				match IERC20::transferCall::abi_decode_returns_validate(&return_value.data) {
277					Ok(true) => {
278						tracing::trace!(target: "xcm::transactor::erc20::deposit", "ERC20 contract was successful");
279						Ok(surplus)
280					},
281					Ok(false) => {
282						tracing::debug!(target: "xcm::transactor::erc20::deposit", "contract transfer failed");
283						Err((
284							what,
285							XcmError::FailedToTransactAsset("ERC20 contract transfer failed"),
286						))
287					},
288					Err(error) => {
289						tracing::debug!(target: "xcm::transactor::erc20::deposit", ?error, "ERC20 contract result couldn't decode");
290						Err((
291							what,
292							XcmError::FailedToTransactAsset(
293								"ERC20 contract result couldn't decode",
294							),
295						))
296					},
297				}
298			}
299		} else {
300			tracing::debug!(target: "xcm::transactor::erc20::deposit", ?result, "Error");
301			// This error could've been duplicate smart contract, out of gas, etc.
302			// If the issue is gas, there's nothing the user can change in the XCM
303			// that will make this work since there's a hardcoded gas limit.
304			Err((what, XcmError::FailedToTransactAsset("ERC20 contract execution errored")))
305		}
306	}
307}