1
// Copyright 2019-2022 PureStake Inc.
2
// This file is part of Moonbeam.
3

            
4
// Moonbeam is free software: you can redistribute it and/or modify
5
// it under the terms of the GNU General Public License as published by
6
// the Free Software Foundation, either version 3 of the License, or
7
// (at your option) any later version.
8

            
9
// Moonbeam is distributed in the hope that it will be useful,
10
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
// GNU General Public License for more details.
13

            
14
// You should have received a copy of the GNU General Public License
15
// along with Moonbeam.  If not, see <http://www.gnu.org/licenses/>.
16

            
17
//! Pallet that allow to transact erc20 tokens trought xcm directly.
18

            
19
#![cfg_attr(not(feature = "std"), no_std)]
20

            
21
#[cfg(test)]
22
mod mock;
23
#[cfg(test)]
24
mod tests;
25

            
26
mod erc20_matcher;
27
mod erc20_trap;
28
mod errors;
29
mod xcm_holding_ext;
30

            
31
use frame_support::pallet;
32

            
33
pub use erc20_trap::AssetTrapWrapper;
34
pub use pallet::*;
35
pub use xcm_holding_ext::XcmExecutorWrapper;
36

            
37
44
#[pallet]
38
pub mod pallet {
39

            
40
	use crate::erc20_matcher::*;
41
	use crate::errors::*;
42
	use crate::xcm_holding_ext::*;
43
	use ethereum_types::BigEndianHash;
44
	use fp_evm::{ExitReason, ExitSucceed};
45
	use frame_support::pallet_prelude::*;
46
	use pallet_evm::{GasWeightMapping, Runner};
47
	use sp_core::{H160, H256, U256};
48
	use sp_std::vec::Vec;
49
	use xcm::latest::{
50
		Asset, AssetId, Error as XcmError, Junction, Location, Result as XcmResult, XcmContext,
51
	};
52
	use xcm_executor::traits::ConvertLocation;
53
	use xcm_executor::traits::{Error as MatchError, MatchesFungibles};
54
	use xcm_executor::AssetsInHolding;
55

            
56
	const ERC20_TRANSFER_CALL_DATA_SIZE: usize = 4 + 32 + 32; // selector + from + amount
57
	const ERC20_TRANSFER_SELECTOR: [u8; 4] = [0xa9, 0x05, 0x9c, 0xbb];
58

            
59
76
	#[pallet::pallet]
60
	pub struct Pallet<T>(PhantomData<T>);
61

            
62
	#[pallet::config]
63
	pub trait Config: frame_system::Config + pallet_evm::Config {
64
		type AccountIdConverter: ConvertLocation<H160>;
65
		type Erc20MultilocationPrefix: Get<Location>;
66
		type Erc20TransferGasLimit: Get<u64>;
67
		type EvmRunner: Runner<Self>;
68
	}
69

            
70
	impl<T: Config> Pallet<T> {
71
124
		pub fn is_erc20_asset(asset: &Asset) -> bool {
72
124
			Erc20Matcher::<T::Erc20MultilocationPrefix>::is_erc20_asset(asset)
73
124
		}
74
3
		pub fn gas_limit_of_erc20_transfer(asset_id: &AssetId) -> u64 {
75
3
			let location = &asset_id.0;
76
			if let Some(Junction::GeneralKey {
77
				length: _,
78
3
				ref data,
79
3
			}) = location.interior().into_iter().next_back()
80
			{
81
				// As GeneralKey definition might change in future versions of XCM, this is meant
82
				// to throw a compile error as a warning that data type has changed.
83
				// If that happens, a new check is needed to ensure that data has at least 18
84
				// bytes (size of b"gas_limit:" + u64)
85
3
				let data: &[u8; 32] = &data;
86
3
				if let Ok(content) = core::str::from_utf8(&data[0..10]) {
87
2
					if content == "gas_limit:" {
88
1
						let mut bytes: [u8; 8] = Default::default();
89
1
						bytes.copy_from_slice(&data[10..18]);
90
1
						return u64::from_le_bytes(bytes);
91
1
					}
92
1
				}
93
			}
94
2
			T::Erc20TransferGasLimit::get()
95
3
		}
96
		pub fn weight_of_erc20_transfer(asset_id: &AssetId) -> Weight {
97
			T::GasWeightMapping::gas_to_weight(Self::gas_limit_of_erc20_transfer(asset_id), true)
98
		}
99
		fn erc20_transfer(
100
			erc20_contract_address: H160,
101
			from: H160,
102
			to: H160,
103
			amount: U256,
104
			gas_limit: u64,
105
		) -> Result<(), Erc20TransferError> {
106
			let mut input = Vec::with_capacity(ERC20_TRANSFER_CALL_DATA_SIZE);
107
			// ERC20.transfer method hash
108
			input.extend_from_slice(&ERC20_TRANSFER_SELECTOR);
109
			// append receiver address
110
			input.extend_from_slice(H256::from(to).as_bytes());
111
			// append amount to be transferred
112
			input.extend_from_slice(H256::from_uint(&amount).as_bytes());
113

            
114
			let weight_limit: Weight = T::GasWeightMapping::gas_to_weight(gas_limit, true);
115

            
116
			let exec_info = T::EvmRunner::call(
117
				from,
118
				erc20_contract_address,
119
				input,
120
				U256::default(),
121
				gas_limit,
122
				None,
123
				None,
124
				None,
125
				Default::default(),
126
				false,
127
				false,
128
				Some(weight_limit),
129
				Some(0),
130
				&<T as pallet_evm::Config>::config(),
131
			)
132
			.map_err(|_| Erc20TransferError::EvmCallFail)?;
133

            
134
			ensure!(
135
				matches!(
136
					exec_info.exit_reason,
137
					ExitReason::Succeed(ExitSucceed::Returned | ExitSucceed::Stopped)
138
				),
139
				Erc20TransferError::ContractTransferFail
140
			);
141

            
142
			// return value is true.
143
			let mut bytes = [0u8; 32];
144
			U256::from(1).to_big_endian(&mut bytes);
145

            
146
			// Check return value to make sure not calling on empty contracts.
147
			ensure!(
148
				!exec_info.value.is_empty() && exec_info.value == bytes,
149
				Erc20TransferError::ContractReturnInvalidValue
150
			);
151

            
152
			Ok(())
153
		}
154
	}
155

            
156
	impl<T: Config> xcm_executor::traits::TransactAsset for Pallet<T> {
157
		// For optimization reasons, the asset we want to deposit has not really been withdrawn,
158
		// we have just traced from which account it should have been withdrawn.
159
		// So we will retrieve these information and make the transfer from the origin account.
160
		fn deposit_asset(what: &Asset, who: &Location, _context: Option<&XcmContext>) -> XcmResult {
161
			let (contract_address, amount) =
162
				Erc20Matcher::<T::Erc20MultilocationPrefix>::matches_fungibles(what)?;
163

            
164
			let beneficiary = T::AccountIdConverter::convert_location(who)
165
				.ok_or(MatchError::AccountIdConversionFailed)?;
166

            
167
			let gas_limit = Self::gas_limit_of_erc20_transfer(&what.id);
168

            
169
			// Get the global context to recover accounts origins.
170
			XcmHoldingErc20sOrigins::with(|erc20s_origins| {
171
				match erc20s_origins.drain(contract_address, amount) {
172
					// We perform the evm transfers in a storage transaction to ensure that if one
173
					// of them fails all the changes of the previous evm calls are rolled back.
174
					Ok(tokens_to_transfer) => frame_support::storage::with_storage_layer(|| {
175
						tokens_to_transfer
176
							.into_iter()
177
							.try_for_each(|(from, subamount)| {
178
								Self::erc20_transfer(
179
									contract_address,
180
									from,
181
									beneficiary,
182
									subamount,
183
									gas_limit,
184
								)
185
							})
186
					})
187
					.map_err(Into::into),
188
					Err(DrainError::AssetNotFound) => Err(XcmError::AssetNotFound),
189
					Err(DrainError::NotEnoughFounds) => Err(XcmError::FailedToTransactAsset(
190
						"not enough founds in xcm holding",
191
					)),
192
					Err(DrainError::SplitError) => Err(XcmError::FailedToTransactAsset(
193
						"SplitError: each withdrawal of erc20 tokens must be deposited at once",
194
					)),
195
				}
196
			})
197
			.ok_or(XcmError::FailedToTransactAsset(
198
				"missing erc20 executor context",
199
			))?
200
		}
201

            
202
		fn internal_transfer_asset(
203
			asset: &Asset,
204
			from: &Location,
205
			to: &Location,
206
			_context: &XcmContext,
207
		) -> Result<AssetsInHolding, XcmError> {
208
			let (contract_address, amount) =
209
				Erc20Matcher::<T::Erc20MultilocationPrefix>::matches_fungibles(asset)?;
210

            
211
			let from = T::AccountIdConverter::convert_location(from)
212
				.ok_or(MatchError::AccountIdConversionFailed)?;
213

            
214
			let to = T::AccountIdConverter::convert_location(to)
215
				.ok_or(MatchError::AccountIdConversionFailed)?;
216

            
217
			let gas_limit = Self::gas_limit_of_erc20_transfer(&asset.id);
218

            
219
			// We perform the evm transfers in a storage transaction to ensure that if it fail
220
			// any contract storage changes are rolled back.
221
			frame_support::storage::with_storage_layer(|| {
222
				Self::erc20_transfer(contract_address, from, to, amount, gas_limit)
223
			})?;
224

            
225
			Ok(asset.clone().into())
226
		}
227

            
228
		// Since we don't control the erc20 contract that manages the asset we want to withdraw,
229
		// we can't really withdraw this asset, we can only transfer it to another account.
230
		// It would be possible to transfer the asset to a dedicated account that would reflect
231
		// the content of the xcm holding, but this would imply to perform two evm calls instead of
232
		// one (1 to withdraw the asset and a second one to deposit it).
233
		// In order to perform only one evm call, we just trace the origin of the asset,
234
		// and then the transfer will only really be performed in the deposit instruction.
235
		fn withdraw_asset(
236
			what: &Asset,
237
			who: &Location,
238
			_context: Option<&XcmContext>,
239
		) -> Result<AssetsInHolding, XcmError> {
240
			let (contract_address, amount) =
241
				Erc20Matcher::<T::Erc20MultilocationPrefix>::matches_fungibles(what)?;
242
			let who = T::AccountIdConverter::convert_location(who)
243
				.ok_or(MatchError::AccountIdConversionFailed)?;
244

            
245
			XcmHoldingErc20sOrigins::with(|erc20s_origins| {
246
				erc20s_origins.insert(contract_address, who, amount)
247
			})
248
			.ok_or(XcmError::FailedToTransactAsset(
249
				"missing erc20 executor context",
250
			))?;
251

            
252
			Ok(what.clone().into())
253
		}
254
	}
255
}