Skip to content

Commit 2539eba

Browse files
Support Snowbridge bridge reward payouts on AssetHub (#865)
Adds handling Snowbridge relayer payouts on AssetHub, since relayer rewards are accumulated on BridgeHub in ether.
1 parent 5ad7d96 commit 2539eba

File tree

5 files changed

+305
-11
lines changed

5 files changed

+305
-11
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
99
### Changed
1010

1111
- [#861](https://github.com/polkadot-fellows/runtimes/pull/861) Removed the custom fungible adapter used by Kusama AssetHub
12+
- Support Snowbridge bridge reward payouts on AssetHub ([polkadot-fellows/runtimes/pull/865](https://github.com/polkadot-fellows/runtimes/pull/865))
1213

1314
## [1.7.0] 22.08.2025
1415

integration-tests/emulated/tests/bridges/bridge-hub-polkadot/src/tests/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ mod snowbridge_v2_inbound_to_kusama;
2828
mod snowbridge_v2_outbound;
2929
mod snowbridge_v2_outbound_edge_case;
3030
mod snowbridge_v2_outbound_from_kusama;
31+
mod snowbridge_v2_rewards;
3132
mod teleport;
3233

3334
pub(crate) fn asset_hub_kusama_location() -> Location {
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
// Copyright (C) Parity Technologies (UK) Ltd.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
//
8+
// http://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
16+
use crate::{
17+
tests::snowbridge_common::{
18+
eth_location, set_up_eth_and_dot_pool_on_polkadot_asset_hub, INITIAL_FUND,
19+
},
20+
*,
21+
};
22+
use bridge_hub_polkadot_runtime::bridge_common_config::{BridgeReward, BridgeRewardBeneficiaries};
23+
use pallet_bridge_relayers::{Error::FailedToPayReward, RewardLedger};
24+
25+
#[test]
26+
fn claim_rewards_works() {
27+
let assethub_location = BridgeHubPolkadot::sibling_location_of(AssetHubPolkadot::para_id());
28+
let assethub_sovereign = BridgeHubPolkadot::sovereign_account_id_of(assethub_location);
29+
30+
let relayer_account = BridgeHubPolkadotSender::get();
31+
let reward_address = AssetHubPolkadotReceiver::get();
32+
33+
BridgeHubPolkadot::fund_accounts(vec![
34+
(assethub_sovereign.clone(), INITIAL_FUND),
35+
(relayer_account.clone(), INITIAL_FUND),
36+
]);
37+
set_up_eth_and_dot_pool_on_polkadot_asset_hub();
38+
39+
BridgeHubPolkadot::execute_with(|| {
40+
type RuntimeEvent = <BridgeHubPolkadot as Chain>::RuntimeEvent;
41+
type RuntimeOrigin = <BridgeHubPolkadot as Chain>::RuntimeOrigin;
42+
let reward_amount = MIN_ETHER_BALANCE * 2; // Reward should be more than Ether min balance
43+
44+
type BridgeRelayers = <BridgeHubPolkadot as BridgeHubPolkadotPallet>::BridgeRelayers;
45+
BridgeRelayers::register_reward(
46+
&relayer_account.clone(),
47+
BridgeReward::Snowbridge,
48+
reward_amount,
49+
);
50+
51+
// Check that the reward was registered.
52+
assert_expected_events!(
53+
BridgeHubPolkadot,
54+
vec![
55+
RuntimeEvent::BridgeRelayers(pallet_bridge_relayers::Event::RewardRegistered { relayer, reward_kind, reward_balance }) => {
56+
relayer: *relayer == relayer_account,
57+
reward_kind: *reward_kind == BridgeReward::Snowbridge,
58+
reward_balance: *reward_balance == reward_amount,
59+
},
60+
]
61+
);
62+
63+
let relayer_location = Location::new(
64+
0,
65+
[Junction::AccountId32 { id: reward_address.clone().into(), network: None }],
66+
);
67+
let reward_beneficiary = BridgeRewardBeneficiaries::AssetHubLocation(Box::new(
68+
VersionedLocation::V5(relayer_location),
69+
));
70+
let result = BridgeRelayers::claim_rewards_to(
71+
RuntimeOrigin::signed(relayer_account.clone()),
72+
BridgeReward::Snowbridge,
73+
reward_beneficiary.clone(),
74+
);
75+
assert_ok!(result);
76+
77+
assert_expected_events!(
78+
BridgeHubPolkadot,
79+
vec![
80+
// Check that the pay reward event was emitted on BH
81+
RuntimeEvent::BridgeRelayers(pallet_bridge_relayers::Event::RewardPaid { relayer, reward_kind, reward_balance, beneficiary }) => {
82+
relayer: *relayer == relayer_account,
83+
reward_kind: *reward_kind == BridgeReward::Snowbridge,
84+
reward_balance: *reward_balance == reward_amount,
85+
beneficiary: *beneficiary == reward_beneficiary,
86+
},
87+
]
88+
);
89+
});
90+
91+
AssetHubPolkadot::execute_with(|| {
92+
type RuntimeEvent = <AssetHubPolkadot as Chain>::RuntimeEvent;
93+
assert_expected_events!(
94+
AssetHubPolkadot,
95+
vec![
96+
// Check that the reward was paid on AH
97+
RuntimeEvent::ForeignAssets(pallet_assets::Event::Issued { asset_id, owner, .. }) => {
98+
asset_id: *asset_id == eth_location(),
99+
owner: *owner == reward_address.clone(),
100+
},
101+
]
102+
);
103+
})
104+
}
105+
106+
#[test]
107+
fn claim_snowbridge_rewards_to_local_account_fails() {
108+
let assethub_location = BridgeHubPolkadot::sibling_location_of(AssetHubPolkadot::para_id());
109+
let assethub_sovereign = BridgeHubPolkadot::sovereign_account_id_of(assethub_location);
110+
111+
let relayer_account = BridgeHubPolkadotSender::get();
112+
let reward_address = AssetHubPolkadotReceiver::get();
113+
114+
BridgeHubPolkadot::fund_accounts(vec![
115+
(assethub_sovereign.clone(), INITIAL_FUND),
116+
(relayer_account.clone(), INITIAL_FUND),
117+
]);
118+
set_up_eth_and_dot_pool_on_polkadot_asset_hub();
119+
120+
BridgeHubPolkadot::execute_with(|| {
121+
type Runtime = <BridgeHubPolkadot as Chain>::Runtime;
122+
type RuntimeEvent = <BridgeHubPolkadot as Chain>::RuntimeEvent;
123+
type RuntimeOrigin = <BridgeHubPolkadot as Chain>::RuntimeOrigin;
124+
let reward_amount = MIN_ETHER_BALANCE * 2; // Reward should be more than Ether min balance
125+
126+
type BridgeRelayers = <BridgeHubPolkadot as BridgeHubPolkadotPallet>::BridgeRelayers;
127+
BridgeRelayers::register_reward(
128+
&relayer_account.clone(),
129+
BridgeReward::Snowbridge,
130+
reward_amount,
131+
);
132+
133+
// Check that the reward was registered.
134+
assert_expected_events!(
135+
BridgeHubPolkadot,
136+
vec![
137+
RuntimeEvent::BridgeRelayers(pallet_bridge_relayers::Event::RewardRegistered { relayer, reward_kind, reward_balance }) => {
138+
relayer: *relayer == relayer_account,
139+
reward_kind: *reward_kind == BridgeReward::Snowbridge,
140+
reward_balance: *reward_balance == reward_amount,
141+
},
142+
]
143+
);
144+
145+
let reward_beneficiary = BridgeRewardBeneficiaries::LocalAccount(reward_address);
146+
let result = BridgeRelayers::claim_rewards_to(
147+
RuntimeOrigin::signed(relayer_account.clone()),
148+
BridgeReward::Snowbridge,
149+
reward_beneficiary.clone(),
150+
);
151+
assert_err!(result, FailedToPayReward::<Runtime, ()>);
152+
})
153+
}

system-parachains/bridge-hubs/bridge-hub-polkadot/src/bridge_common_config.rs

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,23 @@
1616

1717
//! Bridge definitions that can be used by multiple bridges.
1818
19-
use crate::{weights, AccountId, Balance, Balances, BlockNumber, Runtime, RuntimeEvent};
19+
use crate::{
20+
bridge_to_ethereum_config::InboundQueueV2Location,
21+
weights,
22+
xcm_config::{XcmConfig, XcmRouter},
23+
AccountId, Balance, Balances, BlockNumber, Runtime, RuntimeCall, RuntimeEvent,
24+
};
2025
use alloc::boxed::Box;
26+
use bp_bridge_hub_polkadot::snowbridge::EthereumNetwork;
2127
use bp_messages::LegacyLaneId;
2228
use bp_relayers::RewardsAccountParams;
2329
use codec::{Decode, DecodeWithMemTracking, Encode, MaxEncodedLen};
2430
use frame_support::parameter_types;
2531
use polkadot_runtime_constants as constants;
2632
use scale_info::TypeInfo;
27-
use xcm::VersionedLocation;
33+
use system_parachains_constants::polkadot::locations::AssetHubLocation;
34+
use xcm::{opaque::latest::Location, VersionedLocation};
35+
use xcm_executor::XcmExecutor;
2836

2937
parameter_types! {
3038
/// Reserve identifier, used by the `pallet_bridge_relayers` to hold funds of registered relayer.
@@ -111,7 +119,27 @@ impl bp_relayers::PaymentProcedure<AccountId, BridgeReward, u128> for BridgeRewa
111119
BridgeRewardBeneficiaries::AssetHubLocation(_) => Err(Self::Error::Other("`AssetHubLocation` beneficiary is not supported for `PolkadotKusamaBridge` rewards!")),
112120
}
113121
},
114-
BridgeReward::Snowbridge => Err(Self::Error::Other("Not supported for `Snowbridge` rewards yet!")),
122+
BridgeReward::Snowbridge => {
123+
match beneficiary {
124+
BridgeRewardBeneficiaries::LocalAccount(_) => Err(Self::Error::Other("`LocalAccount` beneficiary is not supported for `Snowbridge` rewards!")),
125+
BridgeRewardBeneficiaries::AssetHubLocation(account_location) => {
126+
let account_location = Location::try_from(account_location.as_ref().clone())
127+
.map_err(|_| Self::Error::Other("`AssetHubLocation` beneficiary location version is not supported for `Snowbridge` rewards!"))?;
128+
snowbridge_core::reward::PayAccountOnLocation::<
129+
AccountId,
130+
u128,
131+
EthereumNetwork,
132+
AssetHubLocation,
133+
InboundQueueV2Location,
134+
XcmRouter,
135+
XcmExecutor<XcmConfig>,
136+
RuntimeCall
137+
>::pay_reward(
138+
relayer, (), reward, account_location
139+
)
140+
}
141+
}
142+
}
115143
}
116144
}
117145
}

system-parachains/bridge-hubs/bridge-hub-polkadot/tests/tests.rs

Lines changed: 119 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,12 @@
1717
use bp_bridge_hub_kusama::Perbill;
1818
use bp_messages::LegacyLaneId;
1919
use bp_polkadot_core::Signature;
20+
use bp_relayers::{PayRewardFromAccount, RewardsAccountOwner, RewardsAccountParams};
2021
use bridge_hub_polkadot_runtime::{
21-
bridge_common_config::{BridgeRelayersInstance, RequiredStakeForStakeAndSlash},
22+
bridge_common_config::{
23+
BridgeRelayersInstance, BridgeReward, BridgeRewardBeneficiaries,
24+
RequiredStakeForStakeAndSlash,
25+
},
2226
bridge_to_kusama_config::{
2327
BridgeGrandpaKusamaInstance, BridgeHubKusamaLocation, BridgeParachainKusamaInstance,
2428
DeliveryRewardInBalance, KusamaGlobalConsensusNetwork,
@@ -29,28 +33,38 @@ use bridge_hub_polkadot_runtime::{
2933
DotRelayLocation, GovernanceLocation, LocationToAccountId, RelayNetwork,
3034
RelayTreasuryLocation, RelayTreasuryPalletAccount, XcmConfig,
3135
},
32-
AllPalletsWithoutSystem, Block, BridgeRejectObsoleteHeadersAndMessages, Executive,
33-
ExistentialDeposit, ParachainSystem, PolkadotXcm, Runtime, RuntimeCall, RuntimeEvent,
34-
RuntimeOrigin, SessionKeys, TransactionPayment, TxExtension, UncheckedExtrinsic, SLOT_DURATION,
36+
AllPalletsWithoutSystem, Balances, Block, BridgeRejectObsoleteHeadersAndMessages,
37+
BridgeRelayers, Executive, ExistentialDeposit, ParachainSystem, PolkadotXcm, Runtime,
38+
RuntimeCall, RuntimeEvent, RuntimeOrigin, SessionKeys, TransactionPayment, TxExtension,
39+
UncheckedExtrinsic, SLOT_DURATION,
40+
};
41+
use bridge_hub_test_utils::{
42+
test_cases::{from_parachain, run_test},
43+
GovernanceOrigin, SlotDurations,
3544
};
36-
use bridge_hub_test_utils::{test_cases::from_parachain, GovernanceOrigin, SlotDurations};
3745
use codec::{Decode, Encode};
3846
use cumulus_primitives_core::UpwardMessageSender;
3947
use frame_support::{
40-
assert_err, assert_ok, dispatch::GetDispatchInfo, parameter_types, traits::ConstU8,
48+
assert_err, assert_ok,
49+
dispatch::GetDispatchInfo,
50+
parameter_types,
51+
traits::{
52+
fungible::{Inspect, Mutate},
53+
ConstU8,
54+
},
4155
};
4256
use parachains_common::{AccountId, AuraId, Balance};
4357
use sp_consensus_aura::SlotDuration;
4458
use sp_core::crypto::Ss58Codec;
45-
use sp_keyring::Sr25519Keyring::Alice;
59+
use sp_keyring::Sr25519Keyring::{Alice, Bob};
4660
use sp_runtime::{
4761
generic::{Era, SignedPayload},
4862
AccountId32, Either,
4963
};
5064
use system_parachains_constants::polkadot::{
5165
consensus::RELAY_CHAIN_SLOT_DURATION_MILLIS, fee::WeightToFee,
5266
};
53-
use xcm::latest::prelude::*;
67+
use xcm::{latest::prelude::*, VersionedLocation};
5468
use xcm_executor::traits::ConvertLocation;
5569
use xcm_runtime_apis::conversions::LocationToAccountHelper;
5670

@@ -569,6 +583,103 @@ fn xcm_payment_api_works() {
569583
>();
570584
}
571585

586+
#[test]
587+
pub fn bridge_rewards_works() {
588+
run_test::<Runtime, _>(
589+
collator_session_keys(),
590+
bp_bridge_hub_polkadot::BRIDGE_HUB_POLKADOT_PARACHAIN_ID,
591+
vec![],
592+
|| {
593+
// reward in DOT
594+
let reward1: u128 = 2_000_000_000;
595+
// reward in ETH
596+
let reward2: u128 = 3_000_000_000;
597+
598+
// prepare accounts
599+
let account1 = AccountId32::from(Alice);
600+
let account2 = AccountId32::from(Bob);
601+
let reward1_for = RewardsAccountParams::new(
602+
LegacyLaneId([1; 4]),
603+
*b"test",
604+
RewardsAccountOwner::ThisChain,
605+
);
606+
let expected_reward1_account =
607+
PayRewardFromAccount::<(), AccountId, LegacyLaneId, ()>::rewards_account(
608+
reward1_for,
609+
);
610+
assert_ok!(Balances::mint_into(&expected_reward1_account, ExistentialDeposit::get()));
611+
assert_ok!(Balances::mint_into(&expected_reward1_account, reward1));
612+
assert_ok!(Balances::mint_into(&account1, ExistentialDeposit::get()));
613+
614+
// register rewards
615+
use bp_relayers::RewardLedger;
616+
BridgeRelayers::register_reward(&account1, BridgeReward::from(reward1_for), reward1);
617+
BridgeRelayers::register_reward(&account2, BridgeReward::Snowbridge, reward2);
618+
619+
// check stored rewards
620+
assert_eq!(
621+
BridgeRelayers::relayer_reward(&account1, BridgeReward::from(reward1_for)),
622+
Some(reward1)
623+
);
624+
assert_eq!(BridgeRelayers::relayer_reward(&account1, BridgeReward::Snowbridge), None,);
625+
assert_eq!(
626+
BridgeRelayers::relayer_reward(&account2, BridgeReward::Snowbridge),
627+
Some(reward2),
628+
);
629+
assert_eq!(
630+
BridgeRelayers::relayer_reward(&account2, BridgeReward::from(reward1_for)),
631+
None,
632+
);
633+
634+
// claim rewards
635+
assert_ok!(BridgeRelayers::claim_rewards(
636+
RuntimeOrigin::signed(account1.clone()),
637+
reward1_for.into()
638+
));
639+
assert_eq!(Balances::total_balance(&account1), ExistentialDeposit::get() + reward1);
640+
assert_eq!(
641+
BridgeRelayers::relayer_reward(&account1, BridgeReward::from(reward1_for)),
642+
None,
643+
);
644+
645+
// already claimed
646+
assert_err!(
647+
BridgeRelayers::claim_rewards(
648+
RuntimeOrigin::signed(account1.clone()),
649+
reward1_for.into()
650+
),
651+
pallet_bridge_relayers::Error::<Runtime, BridgeRelayersInstance>::NoRewardForRelayer
652+
);
653+
654+
// Local account claiming is not supported for Snowbridge
655+
assert_err!(
656+
BridgeRelayers::claim_rewards(
657+
RuntimeOrigin::signed(account2.clone()),
658+
BridgeReward::Snowbridge
659+
),
660+
pallet_bridge_relayers::Error::<Runtime, BridgeRelayersInstance>::FailedToPayReward
661+
);
662+
663+
let claim_location = VersionedLocation::V5(Location::new(
664+
1,
665+
[
666+
Parachain(bp_asset_hub_polkadot::ASSET_HUB_POLKADOT_PARACHAIN_ID),
667+
Junction::AccountId32 { id: account2.clone().into(), network: None },
668+
],
669+
));
670+
// Without proper HRMP channel setup, the claim will fail at XCM sending.
671+
assert_err!(
672+
BridgeRelayers::claim_rewards_to(
673+
RuntimeOrigin::signed(account2.clone()),
674+
BridgeReward::Snowbridge,
675+
BridgeRewardBeneficiaries::AssetHubLocation(Box::new(claim_location))
676+
),
677+
pallet_bridge_relayers::Error::<Runtime, BridgeRelayersInstance>::FailedToPayReward
678+
);
679+
},
680+
);
681+
}
682+
572683
#[test]
573684
fn governance_authorize_upgrade_works() {
574685
use polkadot_runtime_constants::system_parachain::{ASSET_HUB_ID, COLLECTIVES_ID};

0 commit comments

Comments
 (0)