pallet_erc20_xcm_bridge/
xcm_holding_ext.rs

1// Copyright 2019-2025 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//! Module that provides types to extend xcm holding.
18
19use core::marker::PhantomData;
20use sp_core::{H160, U256};
21use sp_std::collections::btree_map::BTreeMap;
22use sp_std::vec::Vec;
23use xcm::prelude::*;
24use xcm_executor::traits::{FeeManager, FeeReason, XcmAssetTransfers};
25
26environmental::environmental!(XCM_HOLDING_ERC20_ORIGINS: XcmHoldingErc20sOrigins);
27
28#[cfg_attr(test, derive(PartialEq, Debug))]
29pub(crate) enum DrainError {
30	AssetNotFound,
31	NotEnoughFounds,
32	SplitError,
33}
34
35/// Xcm holding erc20 origins extension.
36/// This extension track down the origin of alls erc20 tokens in the xcm holding.
37#[derive(Default)]
38pub(crate) struct XcmHoldingErc20sOrigins {
39	map: BTreeMap<H160, Vec<(H160, U256)>>,
40}
41impl XcmHoldingErc20sOrigins {
42	/// Take and remove a given amounts of erc20 tokens from the XCM holding.
43	/// These tokens can come from one or more holders that we had tracked earlier in the XCM
44	/// execution, so we return an array of (holder, balance).
45	pub(crate) fn drain(
46		&mut self,
47		contract_address: H160,
48		amount: U256,
49	) -> Result<Vec<(H160, U256)>, DrainError> {
50		let tokens_to_transfer = self.drain_inner(&contract_address, amount)?;
51
52		self.map
53			.entry(contract_address)
54			.and_modify(|erc20_origins| {
55				*erc20_origins = erc20_origins.split_off(tokens_to_transfer.len());
56			});
57
58		Ok(tokens_to_transfer)
59	}
60	fn drain_inner(
61		&self,
62		contract_address: &H160,
63		mut amount: U256,
64	) -> Result<Vec<(H160, U256)>, DrainError> {
65		let mut tokens_to_transfer = Vec::new();
66		if let Some(erc20_origins) = self.map.get(contract_address) {
67			for (from, subamount) in erc20_origins {
68				if &amount > subamount {
69					tokens_to_transfer.push((*from, *subamount));
70					amount -= *subamount;
71				} else if &amount == subamount {
72					tokens_to_transfer.push((*from, *subamount));
73					return Ok(tokens_to_transfer);
74				} else {
75					// Each insertion of tokens must be drain at once
76					return Err(DrainError::SplitError);
77				}
78			}
79			// If there were enough tokens, we had to return in the for loop
80			Err(DrainError::NotEnoughFounds)
81		} else {
82			Err(DrainError::AssetNotFound)
83		}
84	}
85	pub(crate) fn insert(&mut self, contract_address: H160, who: H160, amount: U256) {
86		self.map
87			.entry(contract_address)
88			.or_default()
89			.push((who, amount));
90	}
91	pub(crate) fn with<R, F>(f: F) -> Option<R>
92	where
93		F: FnOnce(&mut Self) -> R,
94	{
95		XCM_HOLDING_ERC20_ORIGINS::with(|erc20s_origins| f(erc20s_origins))
96	}
97}
98
99/// Xcm executor wrapper that inject xcm holding extension "XcmHoldingErc20sOrigins"
100pub struct XcmExecutorWrapper<Config, InnerXcmExecutor>(PhantomData<(Config, InnerXcmExecutor)>);
101impl<Config, InnerXcmExecutor> xcm::latest::ExecuteXcm<Config::RuntimeCall>
102	for XcmExecutorWrapper<Config, InnerXcmExecutor>
103where
104	Config: xcm_executor::Config,
105	InnerXcmExecutor: xcm::latest::ExecuteXcm<Config::RuntimeCall>,
106{
107	type Prepared = InnerXcmExecutor::Prepared;
108
109	fn prepare(
110		message: xcm::latest::Xcm<Config::RuntimeCall>,
111		weight_limit: Weight,
112	) -> Result<Self::Prepared, xcm::latest::InstructionError> {
113		InnerXcmExecutor::prepare(message, weight_limit)
114	}
115
116	fn execute(
117		origin: impl Into<xcm::latest::Location>,
118		pre: Self::Prepared,
119		hash: &mut xcm::latest::XcmHash,
120		weight_credit: xcm::latest::Weight,
121	) -> xcm::latest::Outcome {
122		let mut erc20s_origins = Default::default();
123		XCM_HOLDING_ERC20_ORIGINS::using(&mut erc20s_origins, || {
124			InnerXcmExecutor::execute(origin, pre, hash, weight_credit)
125		})
126	}
127
128	fn charge_fees(
129		location: impl Into<xcm::latest::Location>,
130		fees: xcm::latest::Assets,
131	) -> Result<(), xcm::latest::Error> {
132		InnerXcmExecutor::charge_fees(location, fees)
133	}
134}
135
136impl<Config, InnerXcmExecutor> XcmAssetTransfers for XcmExecutorWrapper<Config, InnerXcmExecutor>
137where
138	Config: xcm_executor::Config,
139{
140	type IsReserve = Config::IsReserve;
141	type IsTeleporter = Config::IsTeleporter;
142	type AssetTransactor = Config::AssetTransactor;
143}
144
145impl<Config, InnerXcmExecutor> FeeManager for XcmExecutorWrapper<Config, InnerXcmExecutor>
146where
147	Config: xcm_executor::Config,
148	InnerXcmExecutor: FeeManager,
149{
150	fn is_waived(origin: Option<&Location>, r: FeeReason) -> bool {
151		InnerXcmExecutor::is_waived(origin, r)
152	}
153
154	fn handle_fee(fee: Assets, context: Option<&XcmContext>, r: FeeReason) {
155		InnerXcmExecutor::handle_fee(fee, context, r)
156	}
157}
158
159#[cfg(test)]
160mod tests {
161	use super::*;
162
163	#[test]
164	fn test_xcm_holding_ext_erc20s_origins() {
165		const TOKEN1: H160 = H160([1; 20]);
166		const TOKEN2: H160 = H160([2; 20]);
167		const USER1: H160 = H160([3; 20]);
168		const USER2: H160 = H160([4; 20]);
169
170		// Simple case
171		let mut erc20s_origins_ = Default::default();
172		XCM_HOLDING_ERC20_ORIGINS::using(&mut erc20s_origins_, || {
173			XcmHoldingErc20sOrigins::with(|erc20s_origins| {
174				erc20s_origins.insert(TOKEN1, USER1, U256::from(100));
175				assert_eq!(
176					erc20s_origins.drain(TOKEN2, U256::from(1)),
177					Err(DrainError::AssetNotFound)
178				);
179				assert_eq!(
180					erc20s_origins.drain(TOKEN1, U256::from(100)),
181					Ok(vec![(USER1, U256::from(100))])
182				);
183			})
184		});
185
186		// Complex case
187		let mut erc20s_origins_ = Default::default();
188		XCM_HOLDING_ERC20_ORIGINS::using(&mut erc20s_origins_, || {
189			XcmHoldingErc20sOrigins::with(|erc20s_origins| {
190				erc20s_origins.insert(TOKEN1, USER1, U256::from(100));
191				erc20s_origins.insert(TOKEN1, USER2, U256::from(200));
192				assert_eq!(
193					erc20s_origins.drain(TOKEN1, U256::from(100)),
194					Ok(vec![(USER1, U256::from(100))])
195				);
196				assert_eq!(
197					erc20s_origins.drain(TOKEN1, U256::from(201)),
198					Err(DrainError::NotEnoughFounds)
199				);
200				assert_eq!(
201					erc20s_origins.drain(TOKEN1, U256::from(199)),
202					Err(DrainError::SplitError)
203				);
204				assert_eq!(
205					erc20s_origins.drain(TOKEN1, U256::from(200)),
206					Ok(vec![(USER2, U256::from(200))])
207				);
208			})
209		});
210	}
211}