pallet_erc20_xcm_bridge/
lib.rs1#![cfg_attr(not(feature = "std"), no_std)]
20
21#[cfg(test)]
22mod mock;
23#[cfg(test)]
24mod tests;
25
26mod erc20_matcher;
27mod erc20_trap;
28mod errors;
29mod xcm_holding_ext;
30
31use frame_support::pallet;
32
33pub use erc20_trap::AssetTrapWrapper;
34pub use pallet::*;
35pub use xcm_holding_ext::XcmExecutorWrapper;
36
37#[pallet]
38pub 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; const ERC20_TRANSFER_SELECTOR: [u8; 4] = [0xa9, 0x05, 0x9c, 0xbb];
58
59 #[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 pub fn is_erc20_asset(asset: &Asset) -> bool {
72 Erc20Matcher::<T::Erc20MultilocationPrefix>::is_erc20_asset(asset)
73 }
74 pub fn gas_limit_of_erc20_transfer(asset_id: &AssetId) -> u64 {
75 let location = &asset_id.0;
76 if let Some(Junction::GeneralKey {
77 length: _,
78 ref data,
79 }) = location.interior().into_iter().next_back()
80 {
81 let data: &[u8; 32] = &data;
86 if let Ok(content) = core::str::from_utf8(&data[0..10]) {
87 if content == "gas_limit:" {
88 let mut bytes: [u8; 8] = Default::default();
89 bytes.copy_from_slice(&data[10..18]);
90 return u64::from_le_bytes(bytes);
91 }
92 }
93 }
94 T::Erc20TransferGasLimit::get()
95 }
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 input.extend_from_slice(&ERC20_TRANSFER_SELECTOR);
109 input.extend_from_slice(H256::from(to).as_bytes());
111 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 Default::default(),
127 false,
128 false,
129 Some(weight_limit),
130 Some(0),
131 &<T as pallet_evm::Config>::config(),
132 )
133 .map_err(|_| Erc20TransferError::EvmCallFail)?;
134
135 ensure!(
136 matches!(
137 exec_info.exit_reason,
138 ExitReason::Succeed(ExitSucceed::Returned | ExitSucceed::Stopped)
139 ),
140 Erc20TransferError::ContractTransferFail
141 );
142
143 let bytes: [u8; 32] = U256::from(1).to_big_endian();
145
146 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 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 XcmHoldingErc20sOrigins::with(|erc20s_origins| {
171 match erc20s_origins.drain(contract_address, amount) {
172 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 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 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}