diff --git a/polkadot/xcm/pallet-xcm/src/precompiles.rs b/polkadot/xcm/pallet-xcm/src/precompiles.rs index b7ae813565587..f56b65e2cc592 100644 --- a/polkadot/xcm/pallet-xcm/src/precompiles.rs +++ b/polkadot/xcm/pallet-xcm/src/precompiles.rs @@ -25,7 +25,7 @@ use pallet_revive::{ DispatchInfo, Origin, }; use tracing::error; -use xcm::MAX_XCM_DECODE_DEPTH; +use xcm::{v5, IdentifyVersion, MAX_XCM_DECODE_DEPTH}; use xcm_executor::traits::WeightBounds; alloy::sol!("src/precompiles/IXcm.sol"); @@ -39,6 +39,15 @@ fn revert(error: &impl fmt::Debug, message: &str) -> Error { Error::Revert(message.into()) } +// We don't allow XCM versions older than 5. +fn ensure_xcm_version(input: &V) -> Result<(), Error> { + let version = input.identify_version(); + if version < v5::VERSION { + return Err(Error::Revert("Only XCM version 5 and onwards are supported.".into())); + } + Ok(()) +} + pub struct XcmPrecompile(PhantomData); impl Precompile for XcmPrecompile @@ -71,12 +80,16 @@ where revert(&error, "XCM send failed: Invalid destination format") })?; + ensure_xcm_version(&final_destination)?; + let final_message = VersionedXcm::<()>::decode_all_with_depth_limit( MAX_XCM_DECODE_DEPTH, &mut &message[..], ) .map_err(|error| revert(&error, "XCM send failed: Invalid message format"))?; + ensure_xcm_version(&final_message)?; + crate::Pallet::::send( frame_origin, final_destination.into(), @@ -102,6 +115,8 @@ where ) .map_err(|error| revert(&error, "XCM execute failed: Invalid message format"))?; + ensure_xcm_version(&final_message)?; + let result = crate::Pallet::::execute( frame_origin, final_message.into(), @@ -134,6 +149,8 @@ where ) .map_err(|error| revert(&error, "XCM weightMessage: Invalid message format"))?; + ensure_xcm_version(&converted_message)?; + let mut final_message = converted_message.try_into().map_err(|error| { revert(&error, "XCM weightMessage: Conversion to Xcm failed") })?; @@ -172,7 +189,7 @@ mod test { }; use polkadot_parachain_primitives::primitives::Id as ParaId; use sp_runtime::traits::AccountIdConversion; - use xcm::prelude::*; + use xcm::{prelude::*, v3, v4}; const BOB: AccountId = AccountId::new([1u8; 32]); const CHARLIE: AccountId = AccountId::new([2u8; 32]); @@ -297,6 +314,81 @@ mod test { ]); let destination: VersionedLocation = VersionedLocation::from(Location::ancestor(8)); + let versioned_message: VersionedXcm<()> = VersionedXcm::from(message.clone()); + + let xcm_send_params = IXcm::sendCall { + destination: destination.encode().into(), + message: versioned_message.encode().into(), + }; + let call = IXcm::IXcmCalls::send(xcm_send_params); + let encoded_call = call.abi_encode(); + + let result = pallet_revive::Pallet::::bare_call( + RuntimeOrigin::signed(ALICE), + xcm_precompile_addr, + 0u128, + Weight::MAX, + DepositLimit::UnsafeOnlyForDryRun, + encoded_call, + ); + let return_value = match result.result { + Ok(value) => value, + Err(err) => panic!("XcmSendPrecompile call failed with error: {err:?}"), + }; + assert!(return_value.did_revert()); + }); + } + + #[test] + fn send_fails_on_old_location_version() { + use codec::Encode; + + let balances = vec![ + (ALICE, CUSTOM_INITIAL_BALANCE), + (ParaId::from(OTHER_PARA_ID).into_account_truncating(), CUSTOM_INITIAL_BALANCE), + ]; + new_test_ext_with_balances(balances).execute_with(|| { + let xcm_precompile_addr = H160::from( + hex::const_decode_to_array(b"00000000000000000000000000000000000A0000").unwrap(), + ); + + let sender: Location = AccountId32 { network: None, id: ALICE.into() }.into(); + let message = Xcm(vec![ + ReserveAssetDeposited((Parent, SEND_AMOUNT).into()), + ClearOrigin, + buy_execution((Parent, SEND_AMOUNT)), + DepositAsset { assets: AllCounted(1).into(), beneficiary: sender.clone() }, + ]); + + // V4 location is old and will fail. + let destination: VersionedLocation = + VersionedLocation::V4(v4::Junction::Parachain(OTHER_PARA_ID).into()); + let versioned_message: VersionedXcm = VersionedXcm::from(message.clone()); + + let xcm_send_params = IXcm::sendCall { + destination: destination.encode().into(), + message: versioned_message.encode().into(), + }; + let call = IXcm::IXcmCalls::send(xcm_send_params); + let encoded_call = call.abi_encode(); + + let result = pallet_revive::Pallet::::bare_call( + RuntimeOrigin::signed(ALICE), + xcm_precompile_addr, + 0u128, + Weight::MAX, + DepositLimit::UnsafeOnlyForDryRun, + encoded_call, + ); + let return_value = match result.result { + Ok(value) => value, + Err(err) => panic!("XcmSendPrecompile call failed with error: {err:?}"), + }; + assert!(return_value.did_revert()); + + // V3 also fails. + let destination: VersionedLocation = + VersionedLocation::V3(v3::Junction::Parachain(OTHER_PARA_ID).into()); let versioned_message: VersionedXcm = VersionedXcm::from(message); let xcm_send_params = IXcm::sendCall { @@ -322,6 +414,82 @@ mod test { }); } + #[test] + fn send_fails_on_old_xcm_version() { + use codec::Encode; + + let balances = vec![ + (ALICE, CUSTOM_INITIAL_BALANCE), + (ParaId::from(OTHER_PARA_ID).into_account_truncating(), CUSTOM_INITIAL_BALANCE), + ]; + new_test_ext_with_balances(balances).execute_with(|| { + let xcm_precompile_addr = H160::from( + hex::const_decode_to_array(b"00000000000000000000000000000000000A0000").unwrap(), + ); + + let sender: Location = AccountId32 { network: None, id: ALICE.into() }.into(); + let message = Xcm(vec![ + ReserveAssetDeposited((Parent, SEND_AMOUNT).into()), + ClearOrigin, + buy_execution((Parent, SEND_AMOUNT)), + DepositAsset { assets: AllCounted(1).into(), beneficiary: sender.clone() }, + ]); + // V4 is old and fails. + let v4_message: v4::Xcm = message.try_into().unwrap(); + + let destination: VersionedLocation = Parachain(OTHER_PARA_ID).into(); + let versioned_message: VersionedXcm = VersionedXcm::V4(v4_message.clone()); + + let xcm_send_params = IXcm::sendCall { + destination: destination.encode().into(), + message: versioned_message.encode().into(), + }; + let call = IXcm::IXcmCalls::send(xcm_send_params); + let encoded_call = call.abi_encode(); + + let result = pallet_revive::Pallet::::bare_call( + RuntimeOrigin::signed(ALICE), + xcm_precompile_addr, + 0u128, + Weight::MAX, + DepositLimit::UnsafeOnlyForDryRun, + encoded_call, + ); + let return_value = match result.result { + Ok(value) => value, + Err(err) => panic!("XcmSendPrecompile call failed with error: {err:?}"), + }; + assert!(return_value.did_revert()); + + // With V3 it also fails. + let v3_message: v3::Xcm = v4_message.try_into().unwrap(); + + let destination: VersionedLocation = Parachain(OTHER_PARA_ID).into(); + let versioned_message: VersionedXcm = VersionedXcm::V3(v3_message); + + let xcm_send_params = IXcm::sendCall { + destination: destination.encode().into(), + message: versioned_message.encode().into(), + }; + let call = IXcm::IXcmCalls::send(xcm_send_params); + let encoded_call = call.abi_encode(); + + let result = pallet_revive::Pallet::::bare_call( + RuntimeOrigin::signed(ALICE), + xcm_precompile_addr, + 0u128, + Weight::MAX, + DepositLimit::UnsafeOnlyForDryRun, + encoded_call, + ); + let return_value = match result.result { + Ok(value) => value, + Err(err) => panic!("XcmSendPrecompile call failed with error: {err:?}"), + }; + assert!(return_value.did_revert()); + }); + } + #[test] fn test_xcm_execute_precompile_works() { use codec::Encode; @@ -515,4 +683,176 @@ mod test { assert_eq!(Balances::total_balance(&BOB), CUSTOM_INITIAL_BALANCE); }); } + + #[test] + fn execute_fails_on_old_version() { + use codec::Encode; + + let balances = vec![ + (ALICE, CUSTOM_INITIAL_BALANCE), + (ParaId::from(OTHER_PARA_ID).into_account_truncating(), CUSTOM_INITIAL_BALANCE), + ]; + new_test_ext_with_balances(balances).execute_with(|| { + let xcm_precompile_addr = H160::from( + hex::const_decode_to_array(b"00000000000000000000000000000000000A0000").unwrap(), + ); + + let dest: Location = Junction::AccountId32 { network: None, id: BOB.into() }.into(); + assert_eq!(Balances::total_balance(&ALICE), CUSTOM_INITIAL_BALANCE); + + let message = Xcm(vec![ + WithdrawAsset((Here, SEND_AMOUNT).into()), + buy_execution((Here, SEND_AMOUNT)), + DepositAsset { assets: AllCounted(1).into(), beneficiary: dest }, + ]); + let versioned_message = VersionedXcm::from(message.clone()); + + let weight_params = weighMessageCall { message: versioned_message.encode().into() }; + let weight_call = IXcm::IXcmCalls::weighMessage(weight_params); + let encoded_weight_call = weight_call.abi_encode(); + + let xcm_weight_results = pallet_revive::Pallet::::bare_call( + RuntimeOrigin::signed(ALICE), + xcm_precompile_addr, + 0u128, + Weight::MAX, + DepositLimit::UnsafeOnlyForDryRun, + encoded_weight_call, + ); + + let weight_result = match xcm_weight_results.result { + Ok(value) => value, + Err(err) => + panic!("XcmExecutePrecompile Failed to decode weight with error {err:?}"), + }; + + let weight: IXcm::Weight = IXcm::Weight::abi_decode(&weight_result.data[..]) + .expect("XcmExecutePrecompile Failed to decode weight"); + + // Using a V4 message to check that it fails. + let v4_message: v4::Xcm = message.clone().try_into().unwrap(); + let versioned_message = VersionedXcm::V4(v4_message.clone()); + + let xcm_execute_params = IXcm::executeCall { + message: versioned_message.encode().into(), + weight: weight.clone(), + }; + let call = IXcm::IXcmCalls::execute(xcm_execute_params); + let encoded_call = call.abi_encode(); + + let result = pallet_revive::Pallet::::bare_call( + RuntimeOrigin::signed(ALICE), + xcm_precompile_addr, + 0u128, + Weight::MAX, + DepositLimit::UnsafeOnlyForDryRun, + encoded_call, + ); + + let return_value = match result.result { + Ok(value) => value, + Err(err) => panic!("XcmExecutePrecompile call failed with error: {err:?}"), + }; + assert!(return_value.did_revert()); + assert_eq!(Balances::total_balance(&ALICE), CUSTOM_INITIAL_BALANCE); + assert_eq!(Balances::total_balance(&BOB), 0); + + // Now using a V3 message. + let v3_message: v3::Xcm = v4_message.try_into().unwrap(); + let versioned_message = VersionedXcm::V3(v3_message); + + let xcm_execute_params = + IXcm::executeCall { message: versioned_message.encode().into(), weight }; + let call = IXcm::IXcmCalls::execute(xcm_execute_params); + let encoded_call = call.abi_encode(); + + let result = pallet_revive::Pallet::::bare_call( + RuntimeOrigin::signed(ALICE), + xcm_precompile_addr, + 0u128, + Weight::MAX, + DepositLimit::UnsafeOnlyForDryRun, + encoded_call, + ); + + let return_value = match result.result { + Ok(value) => value, + Err(err) => panic!("XcmExecutePrecompile call failed with error: {err:?}"), + }; + assert!(return_value.did_revert()); + assert_eq!(Balances::total_balance(&ALICE), CUSTOM_INITIAL_BALANCE); + assert_eq!(Balances::total_balance(&BOB), 0); + }); + } + + #[test] + fn weight_fails_on_old_version() { + use codec::Encode; + + let balances = vec![ + (ALICE, CUSTOM_INITIAL_BALANCE), + (ParaId::from(OTHER_PARA_ID).into_account_truncating(), CUSTOM_INITIAL_BALANCE), + ]; + new_test_ext_with_balances(balances).execute_with(|| { + let xcm_precompile_addr = H160::from( + hex::const_decode_to_array(b"00000000000000000000000000000000000A0000").unwrap(), + ); + + let dest: Location = Junction::AccountId32 { network: None, id: BOB.into() }.into(); + assert_eq!(Balances::total_balance(&ALICE), CUSTOM_INITIAL_BALANCE); + + let message: Xcm = Xcm(vec![ + WithdrawAsset((Here, SEND_AMOUNT).into()), + buy_execution((Here, SEND_AMOUNT)), + DepositAsset { assets: AllCounted(1).into(), beneficiary: dest }, + ]); + // V4 version is old, fails. + let v4_message: v4::Xcm = message.try_into().unwrap(); + let versioned_message = VersionedXcm::V4(v4_message.clone()); + + let weight_params = weighMessageCall { message: versioned_message.encode().into() }; + let weight_call = IXcm::IXcmCalls::weighMessage(weight_params); + let encoded_weight_call = weight_call.abi_encode(); + + let xcm_weight_results = pallet_revive::Pallet::::bare_call( + RuntimeOrigin::signed(ALICE), + xcm_precompile_addr, + 0u128, + Weight::MAX, + DepositLimit::UnsafeOnlyForDryRun, + encoded_weight_call, + ); + + let result = match xcm_weight_results.result { + Ok(value) => value, + Err(err) => + panic!("XcmExecutePrecompile Failed to decode weight with error {err:?}"), + }; + assert!(result.did_revert()); + + // Now we also try V3. + let v3_message: v3::Xcm = v4_message.try_into().unwrap(); + let versioned_message = VersionedXcm::V3(v3_message); + + let weight_params = weighMessageCall { message: versioned_message.encode().into() }; + let weight_call = IXcm::IXcmCalls::weighMessage(weight_params); + let encoded_weight_call = weight_call.abi_encode(); + + let xcm_weight_results = pallet_revive::Pallet::::bare_call( + RuntimeOrigin::signed(ALICE), + xcm_precompile_addr, + 0u128, + Weight::MAX, + DepositLimit::UnsafeOnlyForDryRun, + encoded_weight_call, + ); + + let result = match xcm_weight_results.result { + Ok(value) => value, + Err(err) => + panic!("XcmExecutePrecompile Failed to decode weight with error {err:?}"), + }; + assert!(result.did_revert()); + }); + } } diff --git a/prdoc/pr_9126.prdoc b/prdoc/pr_9126.prdoc new file mode 100644 index 0000000000000..81d131cc75050 --- /dev/null +++ b/prdoc/pr_9126.prdoc @@ -0,0 +1,20 @@ +# Schema: Polkadot SDK PRDoc Schema (prdoc) v1.0.0 +# See doc at https://raw.githubusercontent.com/paritytech/polkadot-sdk/master/prdoc/schema_user.json + +title: XCM precompile - don't support older XCM versions (3 and 4) + +doc: + - audience: Runtime Dev + description: | + The latest XCM version is 5. + A lot of parachains are still running V3 or V4 which is why we haven't removed them, but the XCM precompile is new and should only have to deal with versions 5 and onwards. + No need to keep dragging 3 and 4 in contracts. + This is the default behavior, there's no need to configure anything in pallet-xcm. + - audience: Runtime User + description: | + The XCM precompile will only support XCM from version 5 (current) onwards. + Versions 3 and 4 won't be supported. + +crates: + - name: pallet-xcm + bump: patch