diff --git a/Cargo.lock b/Cargo.lock index 70a8e72fe36f..61ba9d99e1f5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -893,9 +893,11 @@ dependencies = [ "sp-core", "sp-genesis-builder", "sp-inherents", + "sp-io", "sp-offchain", "sp-runtime", "sp-session", + "sp-std 14.0.0", "sp-storage 19.0.0", "sp-transaction-pool", "sp-version", diff --git a/bridges/snowbridge/primitives/router/src/outbound/mod.rs b/bridges/snowbridge/primitives/router/src/outbound/mod.rs index ddc36ce8cb61..3d705f7cdeb0 100644 --- a/bridges/snowbridge/primitives/router/src/outbound/mod.rs +++ b/bridges/snowbridge/primitives/router/src/outbound/mod.rs @@ -117,7 +117,7 @@ where })?; // convert fee to Asset - let fee = Asset::from((Location::parent(), fee.total())).into(); + let fee = Asset::from((Location::parent(), fee.local)).into(); Ok(((ticket.encode(), message_id), fee)) } @@ -148,12 +148,12 @@ enum XcmConverterError { DepositAssetExpected, NoReserveAssets, FilterDoesNotConsumeAllAssets, - TooManyAssets, ZeroAssetTransfer, BeneficiaryResolutionFailed, AssetResolutionFailed, - InvalidFeeAsset, SetTopicExpected, + FeeAssetExpected, + FeeAssetInvalid, } macro_rules! match_expression { @@ -202,10 +202,13 @@ impl<'a, Call> XcmConverter<'a, Call> { } // Get the fee asset item from BuyExecution or continue parsing. - let fee_asset = match_expression!(self.peek(), Ok(BuyExecution { fees, .. }), fees); - if fee_asset.is_some() { - let _ = self.next(); - } + let fee_asset = match_expression!(self.next()?, BuyExecution { fees, .. }, fees) + .ok_or(FeeAssetExpected)?; + ensure!(fee_asset.clone().id == AssetId::from(Location::parent()), FeeAssetInvalid); + let _fee_amount = match fee_asset.clone().fun { + Fungible(fee_amount) => Ok(fee_amount), + _ => Err(FeeAssetInvalid), + }?; let (deposit_assets, beneficiary) = match_expression!( self.next()?, @@ -233,18 +236,8 @@ impl<'a, Call> XcmConverter<'a, Call> { return Err(FilterDoesNotConsumeAllAssets) } - // We only support a single asset at a time. - ensure!(reserve_assets.len() == 1, TooManyAssets); let reserve_asset = reserve_assets.get(0).ok_or(AssetResolutionFailed)?; - // If there was a fee specified verify it. - if let Some(fee_asset) = fee_asset { - // The fee asset must be the same as the reserve asset. - if fee_asset.id != reserve_asset.id || fee_asset.fun > reserve_asset.fun { - return Err(InvalidFeeAsset) - } - } - let (token, amount) = match reserve_asset { Asset { id: AssetId(inner_location), fun: Fungible(amount) } => match inner_location.unpack() { diff --git a/bridges/snowbridge/primitives/router/src/outbound/tests.rs b/bridges/snowbridge/primitives/router/src/outbound/tests.rs index 111243bb45a7..54513396c765 100644 --- a/bridges/snowbridge/primitives/router/src/outbound/tests.rs +++ b/bridges/snowbridge/primitives/router/src/outbound/tests.rs @@ -345,14 +345,14 @@ fn exporter_validate_xcm_success_case_1() { fun: Fungible(1000), }] .into(); - let fee = assets.clone().get(0).unwrap().clone(); + let fee_asset = Asset { id: AssetId::from(Location::parent()), fun: Fungible(1000) }; let filter: AssetFilter = assets.clone().into(); let mut message: Option> = Some( vec![ WithdrawAsset(assets.clone()), ClearOrigin, - BuyExecution { fees: fee, weight_limit: Unlimited }, + BuyExecution { fees: fee_asset, weight_limit: Unlimited }, DepositAsset { assets: filter, beneficiary: AccountKey20 { network: None, key: beneficiary_address }.into(), @@ -398,44 +398,12 @@ fn xcm_converter_convert_success() { }] .into(); let filter: AssetFilter = assets.clone().into(); + let fee_asset = Asset { id: AssetId::from(Location::parent()), fun: Fungible(1000) }; let message: Xcm<()> = vec![ WithdrawAsset(assets.clone()), ClearOrigin, - BuyExecution { fees: assets.get(0).unwrap().clone(), weight_limit: Unlimited }, - DepositAsset { - assets: filter, - beneficiary: AccountKey20 { network: None, key: beneficiary_address }.into(), - }, - SetTopic([0; 32]), - ] - .into(); - let mut converter = XcmConverter::new(&message, &network); - let expected_payload = AgentExecuteCommand::TransferToken { - token: token_address.into(), - recipient: beneficiary_address.into(), - amount: 1000, - }; - let result = converter.convert(); - assert_eq!(result, Ok((expected_payload, [0; 32]))); -} - -#[test] -fn xcm_converter_convert_without_buy_execution_yields_success() { - let network = BridgedNetwork::get(); - - let token_address: [u8; 20] = hex!("1000000000000000000000000000000000000000"); - let beneficiary_address: [u8; 20] = hex!("2000000000000000000000000000000000000000"); - - let assets: Assets = vec![Asset { - id: AssetId([AccountKey20 { network: None, key: token_address }].into()), - fun: Fungible(1000), - }] - .into(); - let filter: AssetFilter = assets.clone().into(); - - let message: Xcm<()> = vec![ - WithdrawAsset(assets.clone()), + BuyExecution { fees: fee_asset, weight_limit: Unlimited }, DepositAsset { assets: filter, beneficiary: AccountKey20 { network: None, key: beneficiary_address }.into(), @@ -467,40 +435,7 @@ fn xcm_converter_convert_with_wildcard_all_asset_filter_succeeds() { .into(); let filter: AssetFilter = Wild(All); - let message: Xcm<()> = vec![ - WithdrawAsset(assets.clone()), - ClearOrigin, - BuyExecution { fees: assets.get(0).unwrap().clone(), weight_limit: Unlimited }, - DepositAsset { - assets: filter, - beneficiary: AccountKey20 { network: None, key: beneficiary_address }.into(), - }, - SetTopic([0; 32]), - ] - .into(); - let mut converter = XcmConverter::new(&message, &network); - let expected_payload = AgentExecuteCommand::TransferToken { - token: token_address.into(), - recipient: beneficiary_address.into(), - amount: 1000, - }; - let result = converter.convert(); - assert_eq!(result, Ok((expected_payload, [0; 32]))); -} - -#[test] -fn xcm_converter_convert_with_fees_less_than_reserve_yields_success() { - let network = BridgedNetwork::get(); - - let token_address: [u8; 20] = hex!("1000000000000000000000000000000000000000"); - let beneficiary_address: [u8; 20] = hex!("2000000000000000000000000000000000000000"); - - let asset_location: Location = [AccountKey20 { network: None, key: token_address }].into(); - let fee_asset = Asset { id: AssetId(asset_location.clone()), fun: Fungible(500) }; - - let assets: Assets = vec![Asset { id: AssetId(asset_location), fun: Fungible(1000) }].into(); - - let filter: AssetFilter = assets.clone().into(); + let fee_asset = Asset { id: AssetId::from(Location::parent()), fun: Fungible(1000) }; let message: Xcm<()> = vec![ WithdrawAsset(assets.clone()), @@ -537,9 +472,11 @@ fn xcm_converter_convert_without_set_topic_yields_set_topic_expected() { .into(); let filter: AssetFilter = assets.clone().into(); + let fee_asset = Asset { id: AssetId::from(Location::parent()), fun: Fungible(1000) }; + let message: Xcm<()> = vec![ WithdrawAsset(assets.clone()), - BuyExecution { fees: assets.get(0).unwrap().clone(), weight_limit: Unlimited }, + BuyExecution { fees: fee_asset, weight_limit: Unlimited }, DepositAsset { assets: filter, beneficiary: AccountKey20 { network: None, key: beneficiary_address }.into(), @@ -569,67 +506,6 @@ fn xcm_converter_convert_with_partial_message_yields_unexpected_end_of_xcm() { assert_eq!(result.err(), Some(XcmConverterError::UnexpectedEndOfXcm)); } -#[test] -fn xcm_converter_with_different_fee_asset_fails() { - let network = BridgedNetwork::get(); - - let token_address: [u8; 20] = hex!("1000000000000000000000000000000000000000"); - let beneficiary_address: [u8; 20] = hex!("2000000000000000000000000000000000000000"); - - let asset_location = [AccountKey20 { network: None, key: token_address }].into(); - let fee_asset = - Asset { id: AssetId(Location { parents: 0, interior: Here }), fun: Fungible(1000) }; - - let assets: Assets = vec![Asset { id: AssetId(asset_location), fun: Fungible(1000) }].into(); - - let filter: AssetFilter = assets.clone().into(); - - let message: Xcm<()> = vec![ - WithdrawAsset(assets.clone()), - ClearOrigin, - BuyExecution { fees: fee_asset, weight_limit: Unlimited }, - DepositAsset { - assets: filter, - beneficiary: AccountKey20 { network: None, key: beneficiary_address }.into(), - }, - SetTopic([0; 32]), - ] - .into(); - let mut converter = XcmConverter::new(&message, &network); - let result = converter.convert(); - assert_eq!(result.err(), Some(XcmConverterError::InvalidFeeAsset)); -} - -#[test] -fn xcm_converter_with_fees_greater_than_reserve_fails() { - let network = BridgedNetwork::get(); - - let token_address: [u8; 20] = hex!("1000000000000000000000000000000000000000"); - let beneficiary_address: [u8; 20] = hex!("2000000000000000000000000000000000000000"); - - let asset_location: Location = [AccountKey20 { network: None, key: token_address }].into(); - let fee_asset = Asset { id: AssetId(asset_location.clone()), fun: Fungible(1001) }; - - let assets: Assets = vec![Asset { id: AssetId(asset_location), fun: Fungible(1000) }].into(); - - let filter: AssetFilter = assets.clone().into(); - - let message: Xcm<()> = vec![ - WithdrawAsset(assets.clone()), - ClearOrigin, - BuyExecution { fees: fee_asset, weight_limit: Unlimited }, - DepositAsset { - assets: filter, - beneficiary: AccountKey20 { network: None, key: beneficiary_address }.into(), - }, - SetTopic([0; 32]), - ] - .into(); - let mut converter = XcmConverter::new(&message, &network); - let result = converter.convert(); - assert_eq!(result.err(), Some(XcmConverterError::InvalidFeeAsset)); -} - #[test] fn xcm_converter_convert_with_empty_xcm_yields_unexpected_end_of_xcm() { let network = BridgedNetwork::get(); @@ -656,10 +532,12 @@ fn xcm_converter_convert_with_extra_instructions_yields_end_of_xcm_message_expec .into(); let filter: AssetFilter = assets.clone().into(); + let fee_asset = Asset { id: AssetId::from(Location::parent()), fun: Fungible(1000) }; + let message: Xcm<()> = vec![ WithdrawAsset(assets.clone()), ClearOrigin, - BuyExecution { fees: assets.get(0).unwrap().clone(), weight_limit: Unlimited }, + BuyExecution { fees: fee_asset, weight_limit: Unlimited }, DepositAsset { assets: filter, beneficiary: AccountKey20 { network: None, key: beneficiary_address }.into(), @@ -716,10 +594,12 @@ fn xcm_converter_convert_without_withdraw_asset_yields_deposit_expected() { }] .into(); + let fee_asset = Asset { id: AssetId::from(Location::parent()), fun: Fungible(1000) }; + let message: Xcm<()> = vec![ WithdrawAsset(assets.clone()), ClearOrigin, - BuyExecution { fees: assets.get(0).unwrap().clone(), weight_limit: Unlimited }, + BuyExecution { fees: fee_asset, weight_limit: Unlimited }, SetTopic([0; 32]), ] .into(); @@ -733,22 +613,17 @@ fn xcm_converter_convert_without_withdraw_asset_yields_deposit_expected() { fn xcm_converter_convert_without_assets_yields_no_reserve_assets() { let network = BridgedNetwork::get(); - let token_address: [u8; 20] = hex!("1000000000000000000000000000000000000000"); - let beneficiary_address: [u8; 20] = hex!("2000000000000000000000000000000000000000"); let assets: Assets = vec![].into(); let filter: AssetFilter = assets.clone().into(); - let fee = Asset { - id: AssetId(AccountKey20 { network: None, key: token_address }.into()), - fun: Fungible(1000), - }; + let fee_asset = Asset { id: AssetId::from(Location::parent()), fun: Fungible(1000) }; let message: Xcm<()> = vec![ WithdrawAsset(assets.clone()), ClearOrigin, - BuyExecution { fees: fee, weight_limit: Unlimited }, + BuyExecution { fees: fee_asset, weight_limit: Unlimited }, DepositAsset { assets: filter, beneficiary: AccountKey20 { network: None, key: beneficiary_address }.into(), @@ -762,44 +637,6 @@ fn xcm_converter_convert_without_assets_yields_no_reserve_assets() { assert_eq!(result.err(), Some(XcmConverterError::NoReserveAssets)); } -#[test] -fn xcm_converter_convert_with_two_assets_yields_too_many_assets() { - let network = BridgedNetwork::get(); - - let token_address_1: [u8; 20] = hex!("1000000000000000000000000000000000000000"); - let token_address_2: [u8; 20] = hex!("1100000000000000000000000000000000000000"); - let beneficiary_address: [u8; 20] = hex!("2000000000000000000000000000000000000000"); - - let assets: Assets = vec![ - Asset { - id: AssetId(AccountKey20 { network: None, key: token_address_1 }.into()), - fun: Fungible(1000), - }, - Asset { - id: AssetId(AccountKey20 { network: None, key: token_address_2 }.into()), - fun: Fungible(500), - }, - ] - .into(); - let filter: AssetFilter = assets.clone().into(); - - let message: Xcm<()> = vec![ - WithdrawAsset(assets.clone()), - ClearOrigin, - BuyExecution { fees: assets.get(0).unwrap().clone(), weight_limit: Unlimited }, - DepositAsset { - assets: filter, - beneficiary: AccountKey20 { network: None, key: beneficiary_address }.into(), - }, - SetTopic([0; 32]), - ] - .into(); - let mut converter = XcmConverter::new(&message, &network); - - let result = converter.convert(); - assert_eq!(result.err(), Some(XcmConverterError::TooManyAssets)); -} - #[test] fn xcm_converter_convert_without_consuming_filter_yields_filter_does_not_consume_all_assets() { let network = BridgedNetwork::get(); @@ -814,10 +651,12 @@ fn xcm_converter_convert_without_consuming_filter_yields_filter_does_not_consume .into(); let filter: AssetFilter = Wild(WildAsset::AllCounted(0)); + let fee_asset = Asset { id: AssetId::from(Location::parent()), fun: Fungible(1000) }; + let message: Xcm<()> = vec![ WithdrawAsset(assets.clone()), ClearOrigin, - BuyExecution { fees: assets.get(0).unwrap().clone(), weight_limit: Unlimited }, + BuyExecution { fees: fee_asset, weight_limit: Unlimited }, DepositAsset { assets: filter, beneficiary: AccountKey20 { network: None, key: beneficiary_address }.into(), @@ -845,10 +684,12 @@ fn xcm_converter_convert_with_zero_amount_asset_yields_zero_asset_transfer() { .into(); let filter: AssetFilter = Wild(WildAsset::AllCounted(1)); + let fee_asset = Asset { id: AssetId::from(Location::parent()), fun: Fungible(1000) }; + let message: Xcm<()> = vec![ WithdrawAsset(assets.clone()), ClearOrigin, - BuyExecution { fees: assets.get(0).unwrap().clone(), weight_limit: Unlimited }, + BuyExecution { fees: fee_asset, weight_limit: Unlimited }, DepositAsset { assets: filter, beneficiary: AccountKey20 { network: None, key: beneficiary_address }.into(), @@ -875,10 +716,12 @@ fn xcm_converter_convert_non_ethereum_asset_yields_asset_resolution_failed() { .into(); let filter: AssetFilter = Wild(WildAsset::AllCounted(1)); + let fee_asset = Asset { id: AssetId::from(Location::parent()), fun: Fungible(1000) }; + let message: Xcm<()> = vec![ WithdrawAsset(assets.clone()), ClearOrigin, - BuyExecution { fees: assets.get(0).unwrap().clone(), weight_limit: Unlimited }, + BuyExecution { fees: fee_asset, weight_limit: Unlimited }, DepositAsset { assets: filter, beneficiary: AccountKey20 { network: None, key: beneficiary_address }.into(), @@ -908,10 +751,12 @@ fn xcm_converter_convert_non_ethereum_chain_asset_yields_asset_resolution_failed .into(); let filter: AssetFilter = Wild(WildAsset::AllCounted(1)); + let fee_asset = Asset { id: AssetId::from(Location::parent()), fun: Fungible(1000) }; + let message: Xcm<()> = vec![ WithdrawAsset(assets.clone()), ClearOrigin, - BuyExecution { fees: assets.get(0).unwrap().clone(), weight_limit: Unlimited }, + BuyExecution { fees: fee_asset, weight_limit: Unlimited }, DepositAsset { assets: filter, beneficiary: AccountKey20 { network: None, key: beneficiary_address }.into(), @@ -941,10 +786,12 @@ fn xcm_converter_convert_non_ethereum_chain_yields_asset_resolution_failed() { .into(); let filter: AssetFilter = Wild(WildAsset::AllCounted(1)); + let fee_asset = Asset { id: AssetId::from(Location::parent()), fun: Fungible(1000) }; + let message: Xcm<()> = vec![ WithdrawAsset(assets.clone()), ClearOrigin, - BuyExecution { fees: assets.get(0).unwrap().clone(), weight_limit: Unlimited }, + BuyExecution { fees: fee_asset, weight_limit: Unlimited }, DepositAsset { assets: filter, beneficiary: AccountKey20 { network: None, key: beneficiary_address }.into(), @@ -973,10 +820,13 @@ fn xcm_converter_convert_with_non_ethereum_beneficiary_yields_beneficiary_resolu }] .into(); let filter: AssetFilter = Wild(WildAsset::AllCounted(1)); + + let fee_asset = Asset { id: AssetId::from(Location::parent()), fun: Fungible(1000) }; + let message: Xcm<()> = vec![ WithdrawAsset(assets.clone()), ClearOrigin, - BuyExecution { fees: assets.get(0).unwrap().clone(), weight_limit: Unlimited }, + BuyExecution { fees: fee_asset, weight_limit: Unlimited }, DepositAsset { assets: filter, beneficiary: [ @@ -1010,10 +860,12 @@ fn xcm_converter_convert_with_non_ethereum_chain_beneficiary_yields_beneficiary_ .into(); let filter: AssetFilter = Wild(WildAsset::AllCounted(1)); + let fee_asset = Asset { id: AssetId::from(Location::parent()), fun: Fungible(1000) }; + let message: Xcm<()> = vec![ WithdrawAsset(assets.clone()), ClearOrigin, - BuyExecution { fees: assets.get(0).unwrap().clone(), weight_limit: Unlimited }, + BuyExecution { fees: fee_asset, weight_limit: Unlimited }, DepositAsset { assets: filter, beneficiary: AccountKey20 { diff --git a/bridges/snowbridge/runtime/test-common/src/lib.rs b/bridges/snowbridge/runtime/test-common/src/lib.rs index 8f36313e360f..51bf04eb30d1 100644 --- a/bridges/snowbridge/runtime/test-common/src/lib.rs +++ b/bridges/snowbridge/runtime/test-common/src/lib.rs @@ -67,10 +67,12 @@ where }; let assets = vec![asset.clone()]; + let fee_asset = Asset { id: AssetId::from(Location::parent()), fun: Fungible(1000) }; + let inner_xcm = Xcm(vec![ WithdrawAsset(Assets::from(assets.clone())), ClearOrigin, - BuyExecution { fees: asset, weight_limit: Unlimited }, + BuyExecution { fees: fee_asset, weight_limit: Unlimited }, DepositAsset { assets: Wild(All), beneficiary: Location::new( diff --git a/cumulus/parachains/integration-tests/emulated/tests/bridges/bridge-hub-rococo/src/tests/snowbridge.rs b/cumulus/parachains/integration-tests/emulated/tests/bridges/bridge-hub-rococo/src/tests/snowbridge.rs index 4cb8680686e8..ca5e761ea5af 100644 --- a/cumulus/parachains/integration-tests/emulated/tests/bridges/bridge-hub-rococo/src/tests/snowbridge.rs +++ b/cumulus/parachains/integration-tests/emulated/tests/bridges/bridge-hub-rococo/src/tests/snowbridge.rs @@ -34,8 +34,6 @@ use testnet_parachains_constants::rococo::snowbridge::EthereumNetwork; const INITIAL_FUND: u128 = 5_000_000_000 * ROCOCO_ED; pub const CHAIN_ID: u64 = 11155111; -const TREASURY_ACCOUNT: [u8; 32] = - hex!("6d6f646c70792f74727372790000000000000000000000000000000000000000"); pub const WETH: [u8; 20] = hex!("87d1f7fdfEe7f651FaBc8bFCB6E086C278b77A7d"); const ETHEREUM_DESTINATION_ADDRESS: [u8; 20] = hex!("44a57ee2f2FCcb85FDa2B0B18EBD0D8D2333700e"); const INSUFFICIENT_XCM_FEE: u128 = 1000; @@ -382,88 +380,92 @@ fn send_weth_asset_from_asset_hub_to_ethereum() { let assethub_location = BridgeHubRococo::sibling_location_of(AssetHubRococo::para_id()); let assethub_sovereign = BridgeHubRococo::sovereign_account_id_of(assethub_location); - AssetHubRococo::force_default_xcm_version(Some(XCM_VERSION)); - BridgeHubRococo::force_default_xcm_version(Some(XCM_VERSION)); AssetHubRococo::force_xcm_version( Location::new(2, [GlobalConsensus(Ethereum { chain_id: CHAIN_ID })]), XCM_VERSION, ); - BridgeHubRococo::fund_accounts(vec![(assethub_sovereign.clone(), INITIAL_FUND)]); - AssetHubRococo::fund_accounts(vec![(AssetHubRococoReceiver::get(), INITIAL_FUND)]); - const WETH_AMOUNT: u128 = 1_000_000_000; + const FEE_AMOUNT: u128 = 2_750_872_500_000; + // To cover the delivery cost on BH and + let local_fee_amount: u128 = DefaultBridgeHubEthereumBaseFee::get(); + // To cover the delivery cost on Ethereum + let remote_fee_amount: u128 = FEE_AMOUNT - local_fee_amount; - BridgeHubRococo::execute_with(|| { - type RuntimeEvent = ::RuntimeEvent; - - // Construct RegisterToken message and sent to inbound queue - assert_ok!(send_inbound_message(make_register_token_message())); + let weth_asset_location: Location = Location::new( + 2, + [EthereumNetwork::get().into(), AccountKey20 { network: None, key: WETH }], + ); - // Check that the register token message was sent using xcm - assert_expected_events!( - BridgeHubRococo, - vec![ - RuntimeEvent::XcmpQueue(cumulus_pallet_xcmp_queue::Event::XcmpMessageSent { .. }) => {}, - ] - ); + // Register WETH and Mint some to AssetHubRococoReceiver + AssetHubRococo::execute_with(|| { + type RuntimeOrigin = ::RuntimeOrigin; - // Construct SendToken message and sent to inbound queue - assert_ok!(send_inbound_message(make_send_token_message())); + assert_ok!(::ForeignAssets::force_create( + RuntimeOrigin::root(), + weth_asset_location.clone().try_into().unwrap(), + AssetHubRococoReceiver::get().into(), + false, + 1, + )); - // Check that the send token message was sent using xcm - assert_expected_events!( - BridgeHubRococo, - vec![ - RuntimeEvent::XcmpQueue(cumulus_pallet_xcmp_queue::Event::XcmpMessageSent { .. }) => {}, - ] - ); + assert_ok!(::ForeignAssets::mint( + RuntimeOrigin::signed(AssetHubRococoReceiver::get()), + weth_asset_location.clone().try_into().unwrap(), + AssetHubRococoReceiver::get().into(), + WETH_AMOUNT, + )); }); AssetHubRococo::execute_with(|| { - type RuntimeEvent = ::RuntimeEvent; type RuntimeOrigin = ::RuntimeOrigin; - // Check that AssetHub has issued the foreign asset - assert_expected_events!( - AssetHubRococo, - vec![ - RuntimeEvent::ForeignAssets(pallet_assets::Event::Issued { .. }) => {}, - ] - ); - let assets = vec![Asset { - id: AssetId(Location::new( - 2, - [ - GlobalConsensus(Ethereum { chain_id: CHAIN_ID }), - AccountKey20 { network: None, key: WETH }, - ], - )), - fun: Fungible(WETH_AMOUNT), - }]; - let multi_assets = VersionedAssets::V4(Assets::from(assets)); - - let destination = VersionedLocation::V4(Location::new( - 2, - [GlobalConsensus(Ethereum { chain_id: CHAIN_ID })], - )); + let remote_fee_asset = + Asset { id: AssetId(Location::parent()), fun: Fungible(remote_fee_amount) }; + + let weth_asset = Asset { id: weth_asset_location.into(), fun: Fungible(WETH_AMOUNT) }; + + // Send both assets to BH + let assets = vec![remote_fee_asset.clone(), weth_asset.clone()]; + + let destination = Location::new(2, [GlobalConsensus(Ethereum { chain_id: CHAIN_ID })]); - let beneficiary = VersionedLocation::V4(Location::new( + let beneficiary = Location::new( 0, [AccountKey20 { network: None, key: ETHEREUM_DESTINATION_ADDRESS.into() }], - )); + ); + // Internal xcm of InitiateReserveWithdraw, WithdrawAssets + ClearOrigin instructions will + // be appended to the front of the list by the xcm executor + let withdraw_xcm_on_bh = Xcm(vec![ + BuyExecution { fees: remote_fee_asset.clone(), weight_limit: Unlimited }, + DepositAsset { assets: Wild(AllCounted(2)), beneficiary }, + ]); + + let xcms = VersionedXcm::from(Xcm(vec![ + WithdrawAsset(assets.clone().into()), + SetFeesMode { jit_withdraw: true }, + InitiateReserveWithdraw { + assets: Definite(assets.clone().into()), + // with reserve set to Ethereum destination, the ExportMessage will + // be appended to the front of the list by the SovereignPaidRemoteExporter + reserve: destination, + xcm: withdraw_xcm_on_bh, + }, + ])); let free_balance_before = ::Balances::free_balance( AssetHubRococoReceiver::get(), ); - // Send the Weth back to Ethereum - ::PolkadotXcm::limited_reserve_transfer_assets( + // Assert there is no balance left in the assethub_sovereign on BH + let free_balance_of_sovereign_on_bh_before = + ::Balances::free_balance( + assethub_sovereign.clone(), + ); + assert_eq!(free_balance_of_sovereign_on_bh_before, 0); + ::PolkadotXcm::execute( RuntimeOrigin::signed(AssetHubRococoReceiver::get()), - Box::new(destination), - Box::new(beneficiary), - Box::new(multi_assets), - 0, - Unlimited, + bx!(xcms), + Weight::from(8_000_000_000), ) .unwrap(); let free_balance_after = ::Balances::free_balance( @@ -484,25 +486,10 @@ fn send_weth_asset_from_asset_hub_to_ethereum() { RuntimeEvent::EthereumOutboundQueue(snowbridge_pallet_outbound_queue::Event::MessageQueued {..}) => {}, ] ); - let events = BridgeHubRococo::events(); - // Check that the local fee was credited to the Snowbridge sovereign account - assert!( - events.iter().any(|event| matches!( - event, - RuntimeEvent::Balances(pallet_balances::Event::Minted { who, amount }) - if *who == TREASURY_ACCOUNT.into() && *amount == 16903333 - )), - "Snowbridge sovereign takes local fee." - ); - // Check that the remote fee was credited to the AssetHub sovereign account - assert!( - events.iter().any(|event| matches!( - event, - RuntimeEvent::Balances(pallet_balances::Event::Minted { who, amount }) - if *who == assethub_sovereign && *amount == 2680000000000, - )), - "AssetHub sovereign takes remote fee." - ); + // Assert there is still some fee left in sov account after the transfer + let free_balance_of_sovereign_on_bh_after = + ::Balances::free_balance(assethub_sovereign); + assert_eq!(free_balance_of_sovereign_on_bh_after, 15590000); }); } diff --git a/cumulus/parachains/runtimes/assets/asset-hub-rococo/Cargo.toml b/cumulus/parachains/runtimes/assets/asset-hub-rococo/Cargo.toml index 98df41090a40..6e48248b7c72 100644 --- a/cumulus/parachains/runtimes/assets/asset-hub-rococo/Cargo.toml +++ b/cumulus/parachains/runtimes/assets/asset-hub-rococo/Cargo.toml @@ -49,10 +49,12 @@ sp-block-builder = { workspace = true } sp-consensus-aura = { workspace = true } sp-core = { workspace = true } sp-inherents = { workspace = true } +sp-io = { workspace = true } sp-genesis-builder = { workspace = true } sp-offchain = { workspace = true } sp-runtime = { workspace = true } sp-session = { workspace = true } +sp-std = { workspace = true } sp-storage = { workspace = true } sp-transaction-pool = { workspace = true } sp-version = { workspace = true } @@ -237,9 +239,11 @@ std = [ "sp-core/std", "sp-genesis-builder/std", "sp-inherents/std", + "sp-io/std", "sp-offchain/std", "sp-runtime/std", "sp-session/std", + "sp-std/std", "sp-storage/std", "sp-transaction-pool/std", "sp-version/std", diff --git a/cumulus/parachains/runtimes/assets/asset-hub-rococo/src/xcm_config.rs b/cumulus/parachains/runtimes/assets/asset-hub-rococo/src/xcm_config.rs index a11dca4f6d7c..65c470870303 100644 --- a/cumulus/parachains/runtimes/assets/asset-hub-rococo/src/xcm_config.rs +++ b/cumulus/parachains/runtimes/assets/asset-hub-rococo/src/xcm_config.rs @@ -20,15 +20,17 @@ use super::{ ToWestendXcmRouter, TransactionByteFee, TrustBackedAssetsInstance, Uniques, WeightToFee, XcmpQueue, }; +use crate::{vec, Vec}; use assets_common::{ matching::{FromNetwork, FromSiblingParachain, IsForeignConcreteAsset}, TrustBackedAssetsAsLocation, }; +use codec::Encode; use frame_support::{ parameter_types, traits::{ tokens::imbalance::{ResolveAssetTo, ResolveTo}, - ConstU32, Contains, Equals, Everything, Nothing, PalletInfoAccess, + ConstU32, Contains, Equals, Everything, Get, Nothing, PalletInfoAccess, }, }; use frame_system::EnsureRoot; @@ -44,22 +46,27 @@ use polkadot_parachain_primitives::primitives::Sibling; use polkadot_runtime_common::xcm_sender::ExponentialPrice; use snowbridge_router_primitives::inbound::GlobalConsensusEthereumConvertsFor; use sp_runtime::traits::{AccountIdConversion, ConvertInto}; +use sp_std::marker::PhantomData; use testnet_parachains_constants::rococo::snowbridge::{ EthereumNetwork, INBOUND_QUEUE_PALLET_INDEX, }; -use xcm::latest::prelude::*; +use xcm::{ + latest::prelude::*, + prelude::SendError::{MissingArgument, NotApplicable, Unroutable}, + VersionedLocation, VersionedXcm, +}; use xcm_builder::{ - AccountId32Aliases, AllowExplicitUnpaidExecutionFrom, AllowHrmpNotificationsFromRelayChain, - AllowKnownQueryResponses, AllowSubscriptionsFrom, AllowTopLevelPaidExecutionFrom, - DenyReserveTransferToRelayChain, DenyThenTry, DescribeAllTerminal, DescribeFamily, - EnsureXcmOrigin, FrameTransactionalProcessor, FungibleAdapter, FungiblesAdapter, - GlobalConsensusParachainConvertsFor, HashedDescription, IsConcrete, LocalMint, - NetworkExportTableItem, NoChecking, NonFungiblesAdapter, ParentAsSuperuser, ParentIsPreset, - RelayChainAsNative, SendXcmFeeToAccount, SiblingParachainAsNative, SiblingParachainConvertsVia, - SignedAccountId32AsNative, SignedToAccountId32, SovereignPaidRemoteExporter, - SovereignSignedViaLocation, StartsWith, StartsWithExplicitGlobalConsensus, TakeWeightCredit, - TrailingSetTopicAsId, UsingComponents, WeightInfoBounds, WithComputedOrigin, WithUniqueTopic, - XcmFeeManagerFromComponents, + ensure_is_remote, AccountId32Aliases, AllowExplicitUnpaidExecutionFrom, + AllowHrmpNotificationsFromRelayChain, AllowKnownQueryResponses, AllowSubscriptionsFrom, + AllowTopLevelPaidExecutionFrom, DenyReserveTransferToRelayChain, DenyThenTry, + DescribeAllTerminal, DescribeFamily, EnsureXcmOrigin, ExporterFor, FrameTransactionalProcessor, + FungibleAdapter, FungiblesAdapter, GlobalConsensusParachainConvertsFor, HashedDescription, + InspectMessageQueues, IsConcrete, LocalMint, NetworkExportTableItem, NoChecking, + NonFungiblesAdapter, ParentAsSuperuser, ParentIsPreset, RelayChainAsNative, + SendXcmFeeToAccount, SiblingParachainAsNative, SiblingParachainConvertsVia, + SignedAccountId32AsNative, SignedToAccountId32, SovereignSignedViaLocation, StartsWith, + StartsWithExplicitGlobalConsensus, TakeWeightCredit, TrailingSetTopicAsId, UsingComponents, + WeightInfoBounds, WithComputedOrigin, WithUniqueTopic, XcmFeeManagerFromComponents, }; use xcm_executor::XcmExecutor; @@ -443,6 +450,92 @@ type LocalXcmRouter = ( XcmpQueue, ); +pub struct SovereignReceiveTeleportRemoteExporter( + PhantomData<(Bridges, Router, UniversalLocation)>, +); +impl> SendXcm + for SovereignReceiveTeleportRemoteExporter +{ + type Ticket = Router::Ticket; + + fn validate( + dest: &mut Option, + msg: &mut Option>, + ) -> SendResult { + let d = dest.as_ref().ok_or(MissingArgument)?; + let devolved = + ensure_is_remote(UniversalLocation::get(), d.clone()).map_err(|_| NotApplicable)?; + let (remote_network, remote_location) = devolved; + let xcm = msg.take().ok_or(MissingArgument)?; + + // find exporter + let Some((bridge, maybe_payment)) = + Bridges::exporter_for(&remote_network, &remote_location, &xcm) + else { + // We need to make sure that msg is not consumed in case of `NotApplicable`. + *msg = Some(xcm); + return Err(NotApplicable) + }; + + // `xcm` should already end with `SetTopic` - if it does, then extract and derive into + // an onward topic ID. + let maybe_forward_id = match xcm.last() { + Some(SetTopic(t)) => + Some((b"forward_id_for", t).using_encoded(sp_io::hashing::blake2_256)), + _ => None, + }; + + let local_from_bridge = + UniversalLocation::get().invert_target(&bridge).map_err(|_| Unroutable)?; + let export_instruction = + ExportMessage { network: remote_network, destination: remote_location, xcm }; + + let mut message = Xcm(if let Some(ref payment) = maybe_payment { + let fees = payment + .clone() + .reanchored(&bridge, &UniversalLocation::get()) + .map_err(|_| Unroutable)?; + vec![ + ReceiveTeleportedAsset(fees.clone().into()), + BuyExecution { fees, weight_limit: Unlimited }, + // `SetAppendix` ensures that `fees` are not trapped in any case, for example, when + // `ExportXcm::validate` encounters an error during the processing of + // `ExportMessage`. + SetAppendix(Xcm(vec![DepositAsset { + assets: AllCounted(1).into(), + beneficiary: local_from_bridge, + }])), + export_instruction, + ] + } else { + vec![export_instruction] + }); + if let Some(forward_id) = maybe_forward_id { + message.0.push(SetTopic(forward_id)); + } + + // We then send a normal message to the bridge asking it to export the prepended + // message to the remote chain. + let (v, mut cost) = validate_send::(bridge, message)?; + if let Some(bridge_payment) = maybe_payment { + cost.push(bridge_payment); + } + Ok((v, cost)) + } + + fn deliver(ticket: Router::Ticket) -> Result { + Router::deliver(ticket) + } +} + +impl InspectMessageQueues + for SovereignReceiveTeleportRemoteExporter +{ + fn get_messages() -> Vec<(VersionedLocation, Vec>)> { + Router::get_messages() + } +} + /// The means for routing XCM messages which are not for local execution into the right message /// queues. pub type XcmRouter = WithUniqueTopic<( @@ -452,7 +545,11 @@ pub type XcmRouter = WithUniqueTopic<( ToWestendXcmRouter, // Router which wraps and sends xcm to BridgeHub to be delivered to the Ethereum // GlobalConsensus - SovereignPaidRemoteExporter, + SovereignReceiveTeleportRemoteExporter< + bridging::EthereumNetworkExportTable, + XcmpQueue, + UniversalLocation, + >, )>; impl pallet_xcm::Config for Runtime { @@ -627,11 +724,9 @@ pub mod bridging { use super::*; parameter_types! { - /// User fee for ERC20 token transfer back to Ethereum. - /// (initially was calculated by test `OutboundQueue::calculate_fees` - ETH/ROC 1/400 and fee_per_gas 20 GWEI = 2200698000000 + *25%) - /// Needs to be more than fee calculated from DefaultFeeConfig FeeConfigRecord in snowbridge:parachain/pallets/outbound-queue/src/lib.rs - /// Polkadot uses 10 decimals, Kusama and Rococo 12 decimals. - pub const DefaultBridgeHubEthereumBaseFee: Balance = 2_750_872_500_000; + /// User fee for delivery cost on bridge hub. Leave some buffer here for avoid spamming + /// should cover at least the BuyExecution on BH + pub const DefaultBridgeHubEthereumBaseFee: Balance = 48_000_000; pub storage BridgeHubEthereumBaseFee: Balance = DefaultBridgeHubEthereumBaseFee::get(); pub SiblingBridgeHubWithEthereumInboundQueueInstance: Location = Location::new( 1, diff --git a/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-rococo/src/xcm_config.rs b/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-rococo/src/xcm_config.rs index 2f11b4694e3b..511f6f750de2 100644 --- a/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-rococo/src/xcm_config.rs +++ b/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-rococo/src/xcm_config.rs @@ -22,7 +22,6 @@ use super::{ use bp_messages::LaneId; use bp_relayers::{PayRewardFromAccount, RewardsAccountOwner, RewardsAccountParams}; use bp_runtime::ChainId; -use core::marker::PhantomData; use frame_support::{ parameter_types, traits::{tokens::imbalance::ResolveTo, ConstU32, Contains, Equals, Everything, Nothing}, @@ -39,10 +38,9 @@ use parachains_common::{ }; use polkadot_parachain_primitives::primitives::Sibling; use polkadot_runtime_common::xcm_sender::ExponentialPrice; -use snowbridge_runtime_common::XcmExportFeeToSibling; use sp_core::Get; use sp_runtime::traits::AccountIdConversion; -use testnet_parachains_constants::rococo::snowbridge::EthereumNetwork; +use sp_std::marker::PhantomData; use xcm::latest::prelude::*; use xcm_builder::{ deposit_or_burn_fee, AccountId32Aliases, AllowExplicitUnpaidExecutionFrom, @@ -52,10 +50,10 @@ use xcm_builder::{ ParentIsPreset, RelayChainAsNative, SendXcmFeeToAccount, SiblingParachainAsNative, SiblingParachainConvertsVia, SignedAccountId32AsNative, SignedToAccountId32, SovereignSignedViaLocation, TakeWeightCredit, TrailingSetTopicAsId, UsingComponents, - WeightInfoBounds, WithComputedOrigin, WithUniqueTopic, + WeightInfoBounds, WithComputedOrigin, WithUniqueTopic, XcmFeeManagerFromComponents, }; use xcm_executor::{ - traits::{FeeManager, FeeReason, FeeReason::Export, TransactAsset}, + traits::{FeeReason, TransactAsset}, XcmExecutor, }; @@ -205,7 +203,7 @@ impl xcm_executor::Config for XcmConfig { type SubscriptionService = PolkadotXcm; type PalletInstancesInfo = AllPalletsWithSystem; type MaxAssetsIntoHolding = MaxAssetsIntoHolding; - type FeeManager = XcmFeeManagerFromComponentsBridgeHub< + type FeeManager = XcmFeeManagerFromComponents< WaivedLocations, ( XcmExportFeeToRelayerRewardAccounts< @@ -215,14 +213,6 @@ impl xcm_executor::Config for XcmConfig { crate::bridge_to_westend_config::BridgeHubWestendChainId, crate::bridge_to_westend_config::AssetHubRococoToAssetHubWestendMessagesLane, >, - XcmExportFeeToSibling< - bp_rococo::Balance, - AccountId, - TokenLocation, - EthereumNetwork, - Self::AssetTransactor, - crate::EthereumOutboundQueue, - >, SendXcmFeeToAccount, ), >; @@ -386,22 +376,3 @@ impl< fee } } - -pub struct XcmFeeManagerFromComponentsBridgeHub( - PhantomData<(WaivedLocations, HandleFee)>, -); -impl, FeeHandler: HandleFee> FeeManager - for XcmFeeManagerFromComponentsBridgeHub -{ - fn is_waived(origin: Option<&Location>, fee_reason: FeeReason) -> bool { - let Some(loc) = origin else { return false }; - if let Export { network, destination: Here } = fee_reason { - return !(network == EthereumNetwork::get()) - } - WaivedLocations::contains(loc) - } - - fn handle_fee(fee: Assets, context: Option<&XcmContext>, reason: FeeReason) { - FeeHandler::handle_fee(fee, context, reason); - } -} diff --git a/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-rococo/tests/snowbridge.rs b/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-rococo/tests/snowbridge.rs index 5960ab7b5505..733126c03461 100644 --- a/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-rococo/tests/snowbridge.rs +++ b/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-rococo/tests/snowbridge.rs @@ -25,7 +25,7 @@ use bridge_hub_rococo_runtime::{ SignedExtra, UncheckedExtrinsic, }; use codec::{Decode, Encode}; -use cumulus_primitives_core::XcmError::{FailedToTransactAsset, NotHoldingFees}; +use cumulus_primitives_core::XcmError::{FailedToTransactAsset, TooExpensive}; use frame_support::parameter_types; use parachains_common::{AccountId, AuraId, Balance}; use snowbridge_pallet_ethereum_client::WeightInfo; @@ -90,8 +90,8 @@ pub fn transfer_token_to_ethereum_fee_not_enough() { H160::random(), H160::random(), // fee not enough - 1_000_000_000, - NotHoldingFees, + 4_000_000, + TooExpensive, ) }