referrerpolicy=no-referrer-when-downgrade

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