Skip to content

Commit b1f68e8

Browse files
authored
Allow withdrawing xcm gateway protocol fees (#623)
1 parent 4cbd12c commit b1f68e8

6 files changed

Lines changed: 144 additions & 43 deletions

File tree

modules/ismp/clients/parachain/client/src/consensus.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ pub const PASSET_HUB_TESTNET_CHAIN_ID: u32 = 420420422;
5656
pub const ASSET_HUB_MAINNET_PARA_ID: u32 = 1000;
5757

5858
/// AssetHub mainnet EVM chain ID (TBD)
59-
pub const ASSET_HUB_MAINNET_CHAIN_ID: u32 = 1000;
59+
pub const ASSET_HUB_MAINNET_CHAIN_ID: u32 = 420420419;
6060

6161
/// The state machine provider that resolves to a `StateMachineClient`
6262
pub trait ParachainStateMachineProvider<T> {

modules/ismp/core/src/host.rs

Lines changed: 33 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,11 @@ use crate::{
2525
prelude::Vec,
2626
router::{IsmpRouter, PostResponse, Request, Response},
2727
};
28-
use alloc::{boxed::Box, format, string::String};
28+
use alloc::{
29+
boxed::Box,
30+
format,
31+
string::{String, ToString},
32+
};
2933
use codec::{Decode, DecodeWithMemTracking, Encode};
3034
use core::{fmt::Display, str::FromStr, time::Duration};
3135
use primitive_types::H256;
@@ -312,19 +316,23 @@ impl Display for StateMachine {
312316
},
313317
StateMachine::Polkadot(id) => format!("POLKADOT-{id}"),
314318
StateMachine::Kusama(id) => format!("KUSAMA-{id}"),
319+
// Invalid byte sequence will result in a default state machine id rendering the request
320+
// invalid and undeliverable
315321
StateMachine::Substrate(id) => {
316322
format!(
317323
"SUBSTRATE-{}",
318-
String::from_utf8(id.to_vec()).map_err(|_| core::fmt::Error)?
324+
String::from_utf8(id.to_vec()).unwrap_or("XXXX".to_string())
319325
)
320326
},
321-
StateMachine::Tendermint(id) => format!(
322-
"TNDRMINT-{}",
323-
String::from_utf8(id.to_vec()).map_err(|_| core::fmt::Error)?
324-
),
327+
// Invalid byte sequence will result in a default state machine id rendering the request
328+
// invalid and undeliverable
329+
StateMachine::Tendermint(id) =>
330+
format!("TNDRMINT-{}", String::from_utf8(id.to_vec()).unwrap_or("XXXX".to_string())),
331+
// Invalid byte sequence will result in a default state machine id rendering the request
332+
// invalid and undeliverable
325333
StateMachine::Relay { relay, para_id } => format!(
326334
"RELAY-{}-{para_id}",
327-
String::from_utf8(relay.to_vec()).map_err(|_| core::fmt::Error)?
335+
String::from_utf8(relay.to_vec()).unwrap_or("XXXX".to_string())
328336
),
329337
};
330338
write!(f, "{}", str)
@@ -430,4 +438,22 @@ mod tests {
430438
assert_eq!(beefy, StateMachine::from_str(&beefy_string).unwrap());
431439
assert_eq!(solo_relay, StateMachine::from_str(&solo_string).unwrap());
432440
}
441+
442+
#[test]
443+
fn invalid_state_machine_conversions() {
444+
let grandpa = StateMachine::Substrate(*b"\xf0\x28\x8c\xbc");
445+
let beefy = StateMachine::Tendermint(*b"\xf0\x28\x8c\xbc");
446+
let solo_relay = StateMachine::Relay { relay: *b"\xf0\x28\x8c\xbc", para_id: 1000 };
447+
448+
let grandpa_string = grandpa.to_string();
449+
let beefy_string = beefy.to_string();
450+
let solo_string = solo_relay.to_string();
451+
dbg!(&grandpa_string);
452+
dbg!(&beefy_string);
453+
dbg!(&solo_string);
454+
455+
assert_eq!(grandpa_string, "SUBSTRATE-XXXX".to_string());
456+
assert_eq!(beefy_string, "TNDRMINT-XXXX".to_string());
457+
assert_eq!(solo_string, "RELAY-XXXX-1000".to_string());
458+
}
433459
}

modules/pallets/testsuite/src/tests/pallet_xcm_gateway.rs

Lines changed: 40 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,8 @@ use pallet_xcm_gateway::Module;
2222
use polkadot_sdk::{
2323
sp_runtime::traits::AccountIdConversion,
2424
xcm_simulator::{
25-
All, AllCounted, Asset, AssetFilter, AssetId, BuyExecution, DepositAsset, Fungibility,
26-
GeneralIndex, Here, InitiateTransfer, PalletInstance, ParaId, Parachain, Parent,
27-
Reanchorable, SetFeesMode, TransferAsset, TransferReserveAsset, VersionedXcm, Weight, Wild,
28-
Xcm,
25+
All, Asset, AssetId, BuyExecution, DepositAsset, Fungibility, Here, ParaId, Parachain,
26+
SetFeesMode, TransferReserveAsset, VersionedXcm, Weight, Wild, Xcm,
2927
},
3028
};
3129
use sp_core::{ByteArray, H160, H256};
@@ -46,7 +44,6 @@ fn reserve_transfer_on_ah() {
4644
Junction::GeneralIndex(1),
4745
]))
4846
.into_location();
49-
let weight_limit = WeightLimit::Unlimited;
5047

5148
let asset_location_on_assethub = Location::new(1, Here);
5249

@@ -83,8 +80,6 @@ fn should_dispatch_ismp_request_when_assets_are_received_from_assethub() {
8380
let asset_id_on_paraa: H256 =
8481
sp_io::hashing::keccak_256(&Location::new(1, Here).encode()).into();
8582

86-
let asset_location_on_assethub = Location::new(1, Here);
87-
8883
reserve_transfer_on_ah();
8984

9085
ParaA::execute_with(|| {
@@ -119,18 +114,6 @@ fn should_dispatch_ismp_request_when_assets_are_received_from_assethub() {
119114
fn should_process_on_accept_module_callback_correctly() {
120115
MockNet::reset();
121116

122-
let beneficiary: Location = Junctions::X4(Arc::new([
123-
Junction::AccountId32 { network: None, id: ALICE.into() },
124-
Junction::AccountKey20 {
125-
network: Some(NetworkId::Ethereum { chain_id: 97 }),
126-
key: [1u8; 20],
127-
},
128-
Junction::GeneralIndex(60 * 60),
129-
Junction::GeneralIndex(1),
130-
]))
131-
.into_location();
132-
let weight_limit = WeightLimit::Unlimited;
133-
134117
let asset_id: H256 = sp_io::hashing::keccak_256(&Location::new(1, Here).encode()).into();
135118

136119
reserve_transfer_on_ah();
@@ -224,18 +207,6 @@ fn should_process_on_timeout_module_callback_correctly() {
224207

225208
let asset_id: H256 = sp_io::hashing::keccak_256(&Location::new(1, Here).encode()).into();
226209

227-
let beneficiary: Location = Junctions::X4(Arc::new([
228-
Junction::AccountId32 { network: None, id: ALICE.into() },
229-
Junction::AccountKey20 {
230-
network: Some(NetworkId::Ethereum { chain_id: 97 }),
231-
key: [0u8; 20],
232-
},
233-
Junction::GeneralIndex(60 * 60),
234-
Junction::GeneralIndex(1),
235-
]))
236-
.into_location();
237-
let weight_limit = WeightLimit::Unlimited;
238-
239210
reserve_transfer_on_ah();
240211

241212
let alice_balance = ParaB::execute_with(|| {
@@ -321,3 +292,41 @@ fn should_process_on_timeout_module_callback_correctly() {
321292
assert_eq!(current_balance, alice_balance + transferred);
322293
})
323294
}
295+
296+
#[test]
297+
fn should_withdraw_protocol_fees_successfully() {
298+
MockNet::reset();
299+
300+
let asset_id: H256 = sp_io::hashing::keccak_256(&Location::new(1, Here).encode()).into();
301+
302+
// First, trigger a reserve transfer to accumulate protocol fees
303+
reserve_transfer_on_ah();
304+
305+
ParaA::execute_with(|| {
306+
let protocol_account = pallet_xcm_gateway::Pallet::<Test>::protocol_account_id();
307+
308+
// Verify protocol fees have accumulated
309+
let protocol_balance_before = <runtime::Assets as Inspect<
310+
<Test as frame_system::Config>::AccountId,
311+
>>::balance(asset_id, &protocol_account);
312+
313+
let protocol_fee_percentage = pallet_xcm_gateway::Pallet::<Test>::protocol_fee_percentage();
314+
let expected_protocol_fees = protocol_fee_percentage * SEND_AMOUNT;
315+
316+
assert_eq!(protocol_balance_before, expected_protocol_fees);
317+
assert!(protocol_balance_before > 0, "Protocol fees should have accumulated");
318+
319+
// Withdraw protocol fees to BOB as beneficiary
320+
assert_ok!(pallet_xcm_gateway::Pallet::<Test>::withdraw_protocol_fees(
321+
frame_system::RawOrigin::Root.into(),
322+
BOB
323+
));
324+
325+
// Verify protocol account balance is now zero (all fees withdrawn)
326+
let protocol_balance_after = <runtime::Assets as Inspect<
327+
<Test as frame_system::Config>::AccountId,
328+
>>::balance(asset_id, &protocol_account);
329+
330+
assert_eq!(protocol_balance_after, 0, "Protocol account should be empty after withdrawal");
331+
});
332+
}

modules/pallets/xcm-gateway/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ features = [
3636
"staging-xcm",
3737
"staging-xcm-builder",
3838
"staging-xcm-executor",
39+
"cumulus-primitives-core"
3940
]
4041

4142
[features]

modules/pallets/xcm-gateway/src/lib.rs

Lines changed: 68 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -123,13 +123,15 @@ pub mod pallet {
123123
DispatchPostError,
124124
/// Pallet has not been initialized
125125
NotInitialized,
126+
/// No protocol fees
127+
ProtocolFeeBalanceExhausted,
126128
}
127129

128130
/// Events emiited by the relayer pallet
129131
#[pallet::event]
130132
#[pallet::generate_deposit(pub(super) fn deposit_event)]
131133
pub enum Event<T: Config> {
132-
/// An XCM transfer from the relay chain has been transformed into a crosschain message
134+
/// An XCM transfer from assethub has been transformed into a crosschain message
133135
AssetTeleported {
134136
/// Source account on the relaychain
135137
from: T::AccountId,
@@ -148,7 +150,7 @@ pub mod pallet {
148150
/// An asset has been received and transferred to the beneficiary's account on the
149151
/// relaychain
150152
AssetReceived {
151-
/// beneficiary account on relaychain
153+
/// beneficiary account on assethub
152154
beneficiary: T::AccountId,
153155
/// Amount transferred
154156
amount: <T::Assets as fungibles::Inspect<T::AccountId>>::Balance,
@@ -159,13 +161,21 @@ pub mod pallet {
159161
/// An asset has been refunded and transferred to the beneficiary's account on the
160162
/// relaychain
161163
AssetRefunded {
162-
/// beneficiary account on relaychain
164+
/// beneficiary account on assethub
163165
beneficiary: T::AccountId,
164166
/// Amount transferred
165167
amount: <T::Assets as fungibles::Inspect<T::AccountId>>::Balance,
166168
/// Destination chain
167169
source: StateMachine,
168170
},
171+
172+
/// Protocol Fees have been withdrawn to an account on assethub
173+
ProtocolFeesWithdrawn {
174+
/// beneficiary account on assethub
175+
beneficiary: T::AccountId,
176+
/// Amount transferred
177+
amount: <T::Assets as fungibles::Inspect<T::AccountId>>::Balance,
178+
},
169179
}
170180

171181
#[derive(
@@ -207,6 +217,9 @@ pub mod pallet {
207217
impl<T: Config> Pallet<T>
208218
where
209219
T::AccountId: From<[u8; 32]>,
220+
T::AccountId: Into<[u8; 32]>,
221+
u128: From<<T::Assets as fungibles::Inspect<T::AccountId>>::Balance>,
222+
<T::Assets as fungibles::Inspect<T::AccountId>>::AssetId: From<[u8; 32]>,
210223
{
211224
#[pallet::weight(T::DbWeight::get().writes(1))]
212225
#[pallet::call_index(0)]
@@ -224,6 +237,58 @@ pub mod pallet {
224237
Params::<T>::put(current_params);
225238
Ok(())
226239
}
240+
241+
#[pallet::weight(T::DbWeight::get().writes(1))]
242+
#[pallet::call_index(1)]
243+
pub fn withdraw_protocol_fees(
244+
origin: OriginFor<T>,
245+
beneficiary: T::AccountId,
246+
) -> DispatchResult {
247+
T::GatewayOrigin::ensure_origin(origin)?;
248+
let asset_id = Location::parent();
249+
250+
let local_asset_id = sp_io::hashing::keccak_256(&asset_id.encode());
251+
let balance = <<T as Config>::Assets as fungibles::Inspect<T::AccountId>>::balance(
252+
local_asset_id.into(),
253+
&Self::protocol_account_id(),
254+
);
255+
256+
if u128::from(balance) <= 0 {
257+
Err(Error::<T>::ProtocolFeeBalanceExhausted)?
258+
}
259+
260+
// Send the dot back to assethub using xcm
261+
let xcm_beneficiary: Location =
262+
Junction::AccountId32 { network: None, id: beneficiary.clone().into() }.into();
263+
264+
let xcm_dest = VersionedLocation::V5(Location::new(1, [Parachain(ASSET_HUB_PARA_ID)]));
265+
let weight_limit = WeightLimit::Unlimited;
266+
let asset = Asset { id: AssetId(asset_id), fun: Fungibility::Fungible(balance.into()) };
267+
268+
let mut assets = Assets::new();
269+
assets.push(asset.clone());
270+
271+
let custom_xcm_on_dest = Xcm::<()>(vec![DepositAsset {
272+
assets: Wild(AllCounted(assets.len() as u32)),
273+
beneficiary: xcm_beneficiary.clone(),
274+
}]);
275+
276+
// Send xcm back to assethub
277+
pallet_xcm::Pallet::<T>::transfer_assets_using_type_and_then(
278+
frame_system::RawOrigin::Signed(Pallet::<T>::protocol_account_id()).into(),
279+
Box::new(xcm_dest),
280+
Box::new(VersionedAssets::V5(assets.clone())),
281+
Box::new(TransferType::DestinationReserve),
282+
Box::new(asset.id.into()),
283+
Box::new(TransferType::DestinationReserve),
284+
Box::new(VersionedXcm::from(custom_xcm_on_dest)),
285+
weight_limit,
286+
)?;
287+
288+
Self::deposit_event(Event::<T>::ProtocolFeesWithdrawn { beneficiary, amount: balance });
289+
290+
Ok(())
291+
}
227292
}
228293
}
229294

parachain/runtimes/nexus/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -229,7 +229,7 @@ pub const VERSION: RuntimeVersion = RuntimeVersion {
229229
spec_name: Cow::Borrowed("nexus"),
230230
impl_name: Cow::Borrowed("nexus"),
231231
authoring_version: 1,
232-
spec_version: 6_100,
232+
spec_version: 6_200,
233233
impl_version: 0,
234234
apis: RUNTIME_API_VERSIONS,
235235
transaction_version: 1,

0 commit comments

Comments
 (0)