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 core::marker::PhantomData;
20use ethereum_standards::IERC20;
21use frame_support::traits::{fungible::Inspect, OriginTrait};
22use frame_system::pallet_prelude::OriginFor;
23use pallet_revive::{
24	precompiles::alloy::{
25		primitives::{Address, U256 as EU256},
26		sol_types::SolCall,
27	},
28	AddressMapper, ContractResult, ExecConfig, MomentOf, TransactionLimits,
29};
30use sp_core::{Get, H160, H256, U256};
31use sp_runtime::Weight;
32use xcm::latest::prelude::*;
33use xcm_executor::{
34	traits::{ConvertLocation, Error as MatchError, MatchesFungibles, TransactAsset},
35	AssetsInHolding,
36};
37
38type BalanceOf<T> = <<T as pallet_revive::Config>::Currency as Inspect<
39	<T as frame_system::Config>::AccountId,
40>>::Balance;
41
42/// An Asset Transactor that deals with ERC20 tokens.
43pub struct ERC20Transactor<
44	T,
45	Matcher,
46	AccountIdConverter,
47	WeightLimit,
48	StorageDepositLimit,
49	AccountId,
50	TransfersCheckingAccount,
51>(
52	PhantomData<(
53		T,
54		Matcher,
55		AccountIdConverter,
56		WeightLimit,
57		StorageDepositLimit,
58		AccountId,
59		TransfersCheckingAccount,
60	)>,
61);
62
63impl<
64		AccountId: Eq + Clone,
65		T: pallet_revive::Config<AccountId = AccountId>,
66		AccountIdConverter: ConvertLocation<AccountId>,
67		Matcher: MatchesFungibles<H160, u128>,
68		WeightLimit: Get<Weight>,
69		StorageDepositLimit: Get<BalanceOf<T>>,
70		TransfersCheckingAccount: Get<AccountId>,
71	> TransactAsset
72	for ERC20Transactor<
73		T,
74		Matcher,
75		AccountIdConverter,
76		WeightLimit,
77		StorageDepositLimit,
78		AccountId,
79		TransfersCheckingAccount,
80	>
81where
82	BalanceOf<T>: Into<U256> + TryFrom<U256>,
83	MomentOf<T>: Into<U256>,
84	T::Hash: frame_support::traits::IsType<H256>,
85{
86	fn can_check_in(_origin: &Location, _what: &Asset, _context: &XcmContext) -> XcmResult {
87		// We don't support teleports.
88		Err(XcmError::Unimplemented)
89	}
90
91	fn check_in(_origin: &Location, _what: &Asset, _context: &XcmContext) {
92		// We don't support teleports.
93	}
94
95	fn can_check_out(_destination: &Location, _what: &Asset, _context: &XcmContext) -> XcmResult {
96		// We don't support teleports.
97		Err(XcmError::Unimplemented)
98	}
99
100	fn check_out(_destination: &Location, _what: &Asset, _context: &XcmContext) {
101		// We don't support teleports.
102	}
103
104	fn withdraw_asset_with_surplus(
105		what: &Asset,
106		who: &Location,
107		_context: Option<&XcmContext>,
108	) -> Result<(AssetsInHolding, Weight), XcmError> {
109		tracing::trace!(
110			target: "xcm::transactor::erc20::withdraw",
111			?what, ?who,
112		);
113		let (asset_id, amount) = Matcher::matches_fungibles(what)?;
114		let who = AccountIdConverter::convert_location(who)
115			.ok_or(MatchError::AccountIdConversionFailed)?;
116		// We need to map the 32 byte checking account to a 20 byte account.
117		let checking_account_eth = T::AddressMapper::to_address(&TransfersCheckingAccount::get());
118		let checking_address = Address::from(Into::<[u8; 20]>::into(checking_account_eth));
119		let weight_limit = WeightLimit::get();
120		// To withdraw, we actually transfer to the checking account.
121		// We do this using the solidity ERC20 interface.
122		let data =
123			IERC20::transferCall { to: checking_address, value: EU256::from(amount) }.abi_encode();
124		let ContractResult { result, weight_consumed, storage_deposit, .. } =
125			pallet_revive::Pallet::<T>::bare_call(
126				OriginFor::<T>::signed(who.clone()),
127				asset_id,
128				U256::zero(),
129				TransactionLimits::WeightAndDeposit {
130					weight_limit,
131					deposit_limit: StorageDepositLimit::get(),
132				},
133				data,
134				ExecConfig::new_substrate_tx(),
135			);
136		// We need to return this surplus for the executor to allow refunding it.
137		let surplus = weight_limit.saturating_sub(weight_consumed);
138		tracing::trace!(target: "xcm::transactor::erc20::withdraw", ?weight_consumed, ?surplus, ?storage_deposit);
139		if let Ok(return_value) = result {
140			tracing::trace!(target: "xcm::transactor::erc20::withdraw", ?return_value, "Return value by withdraw_asset");
141			if return_value.did_revert() {
142				tracing::debug!(target: "xcm::transactor::erc20::withdraw", "ERC20 contract reverted");
143				Err(XcmError::FailedToTransactAsset("ERC20 contract reverted"))
144			} else {
145				let is_success = IERC20::transferCall::abi_decode_returns_validate(&return_value.data).map_err(|error| {
146					tracing::debug!(target: "xcm::transactor::erc20::withdraw", ?error, "ERC20 contract result couldn't decode");
147					XcmError::FailedToTransactAsset("ERC20 contract result couldn't decode")
148				})?;
149				if is_success {
150					tracing::trace!(target: "xcm::transactor::erc20::withdraw", "ERC20 contract was successful");
151					Ok((what.clone().into(), surplus))
152				} else {
153					tracing::debug!(target: "xcm::transactor::erc20::withdraw", "contract transfer failed");
154					Err(XcmError::FailedToTransactAsset("ERC20 contract transfer failed"))
155				}
156			}
157		} else {
158			tracing::debug!(target: "xcm::transactor::erc20::withdraw", ?result, "Error");
159			// This error could've been duplicate smart contract, out of gas, etc.
160			// If the issue is gas, there's nothing the user can change in the XCM
161			// that will make this work since there's a hardcoded gas limit.
162			Err(XcmError::FailedToTransactAsset("ERC20 contract execution errored"))
163		}
164	}
165
166	fn deposit_asset_with_surplus(
167		what: &Asset,
168		who: &Location,
169		_context: Option<&XcmContext>,
170	) -> Result<Weight, XcmError> {
171		tracing::trace!(
172			target: "xcm::transactor::erc20::deposit",
173			?what, ?who,
174		);
175		let (asset_id, amount) = Matcher::matches_fungibles(what)?;
176		let who = AccountIdConverter::convert_location(who)
177			.ok_or(MatchError::AccountIdConversionFailed)?;
178		// We need to map the 32 byte beneficiary account to a 20 byte account.
179		let eth_address = T::AddressMapper::to_address(&who);
180		let address = Address::from(Into::<[u8; 20]>::into(eth_address));
181		// To deposit, we actually transfer from the checking account to the beneficiary.
182		// We do this using the solidity ERC20 interface.
183		let data = IERC20::transferCall { to: address, value: EU256::from(amount) }.abi_encode();
184		let weight_limit = WeightLimit::get();
185		let ContractResult { result, weight_consumed, storage_deposit, .. } =
186			pallet_revive::Pallet::<T>::bare_call(
187				OriginFor::<T>::signed(TransfersCheckingAccount::get()),
188				asset_id,
189				U256::zero(),
190				TransactionLimits::WeightAndDeposit {
191					weight_limit,
192					deposit_limit: StorageDepositLimit::get(),
193				},
194				data,
195				ExecConfig::new_substrate_tx(),
196			);
197		// We need to return this surplus for the executor to allow refunding it.
198		let surplus = weight_limit.saturating_sub(weight_consumed);
199		tracing::trace!(target: "xcm::transactor::erc20::deposit", ?weight_consumed, ?surplus, ?storage_deposit);
200		if let Ok(return_value) = result {
201			tracing::trace!(target: "xcm::transactor::erc20::deposit", ?return_value, "Return value");
202			if return_value.did_revert() {
203				tracing::debug!(target: "xcm::transactor::erc20::deposit", "Contract reverted");
204				Err(XcmError::FailedToTransactAsset("ERC20 contract reverted"))
205			} else {
206				let is_success = IERC20::transferCall::abi_decode_returns_validate(&return_value.data).map_err(|error| {
207					tracing::debug!(target: "xcm::transactor::erc20::deposit", ?error, "ERC20 contract result couldn't decode");
208					XcmError::FailedToTransactAsset("ERC20 contract result couldn't decode")
209				})?;
210				if is_success {
211					tracing::trace!(target: "xcm::transactor::erc20::deposit", "ERC20 contract was successful");
212					Ok(surplus)
213				} else {
214					tracing::debug!(target: "xcm::transactor::erc20::deposit", "contract transfer failed");
215					Err(XcmError::FailedToTransactAsset("ERC20 contract transfer failed"))
216				}
217			}
218		} else {
219			tracing::debug!(target: "xcm::transactor::erc20::deposit", ?result, "Error");
220			// This error could've been duplicate smart contract, out of gas, etc.
221			// If the issue is gas, there's nothing the user can change in the XCM
222			// that will make this work since there's a hardcoded gas limit.
223			Err(XcmError::FailedToTransactAsset("ERC20 contract execution errored"))
224		}
225	}
226}