diff --git a/xcm/xcm-simulator/example/src/lib.rs b/xcm/xcm-simulator/example/src/lib.rs index 299007a307e3..9e5a2658eace 100644 --- a/xcm/xcm-simulator/example/src/lib.rs +++ b/xcm/xcm-simulator/example/src/lib.rs @@ -18,7 +18,7 @@ mod parachain; mod relay_chain; use frame_support::sp_tracing; -use xcm::prelude::*; +use xcm::{latest::Error, prelude::*}; use xcm_executor::traits::Convert; use xcm_simulator::{decl_test_network, decl_test_parachain, decl_test_relay_chain}; @@ -267,40 +267,100 @@ mod tests { }); } + ////////////////////////////////////////////////////// + ///////////////// SCENARIOS START///////////////////// + ////////////////////////////////////////////////////// + + /// Scenario: + /// + /// Original: + /// A parachain wants to be notified that a transfer worked correctly. + /// It sends a `QueryHolding` after the deposit to get notified on success. + /// + /// Modified: + /// The example has been modified slightly to demonstrate that correct asset amount is returned. + /// We withdraw certain amount, but deposit less. We expect that `QueryHolding` will report the remainder. + /// + /// Asserts that the balances are updated correctly and the expected XCM is sent. #[test] - fn remote_locking() { + fn query_holding() { MockNet::reset(); - let locked_amount = 100; + let withdraw_amount = 10; + let deposit_amount = 3; + let first_query_id_set = 1234; + let second_query_id_set = 5678; - ParaB::execute_with(|| { - let message = Xcm(vec![LockAsset { - asset: (Here, locked_amount).into(), - unlocker: (Parachain(1),).into(), - }]); - assert_ok!(ParachainPalletXcm::send_xcm(Here, Parent, message.clone())); + // Send a message which fully succeeds on the relay chain + ParaA::execute_with(|| { + let message = Xcm(vec![ + WithdrawAsset((Here, withdraw_amount).into()), + // We don't deposit everything intentionally, so we can check that `ReportHolding` works as expected + DepositAsset { + assets: MultiAsset { id: Concrete(Here.into()), fun: Fungible(deposit_amount) } + .into(), + beneficiary: Parachain(2).into(), + }, + ReportHolding { + response_info: QueryResponseInfo { + destination: Parachain(1).into(), + query_id: first_query_id_set, + max_weight: 1_000_000_000, + }, + assets: All.into(), // we choose to report everything, but we could limit it if we wanted to + }, + ReportHolding { + response_info: QueryResponseInfo { + destination: Parachain(1).into(), + query_id: second_query_id_set, + max_weight: 1_000_000_000, + }, + // We repeat almost the same query but we limit it to only native asset and to 1 amount! + // So even if we have more than 1, at max, 1 can be reported (0 or 1). + assets: MultiAsset { id: Concrete(Here.into()), fun: Fungible(1) }.into(), + }, + ]); + // Send withdraw and deposit with query holding + assert_ok!(ParachainPalletXcm::send_xcm(Here, Parent, message.clone(),)); }); + // Check that transfer was executed Relay::execute_with(|| { - use pallet_balances::{BalanceLock, Reasons}; + // Withdraw executed assert_eq!( - relay_chain::Balances::locks(&child_account_id(2)), - vec![BalanceLock { - id: *b"py/xcmlk", - amount: locked_amount, - reasons: Reasons::All - }] + relay_chain::Balances::free_balance(child_account_id(1)), + INITIAL_BALANCE - withdraw_amount + ); + // Deposit executed + assert_eq!( + relay_chain::Balances::free_balance(child_account_id(2)), + INITIAL_BALANCE + deposit_amount ); }); + // Check that QueryResponse message was received ParaA::execute_with(|| { assert_eq!( parachain::MsgQueue::received_dmp(), - vec![Xcm(vec![NoteUnlockable { - owner: (Parent, Parachain(2)).into(), - asset: (Parent, locked_amount).into() - }])] + vec![ + Xcm(vec![QueryResponse { + query_id: first_query_id_set, + response: Response::Assets( + (Parent, withdraw_amount - deposit_amount).into() + ), + max_weight: 1_000_000_000, + querier: Some(Here.into()), + },]), + Xcm(vec![QueryResponse { + query_id: second_query_id_set, + response: Response::Assets((Parent, 1).into()), + max_weight: 1_000_000_000, + querier: Some(Here.into()), + },]) // Notice that we define 2 separate XCM sequences. This is because each `QueryHolding`'s reponse is done individually. + ], ); + + // TODO: check what happened with the unused funds left in the holding register }); } @@ -309,7 +369,7 @@ mod tests { /// /// Asserts that the parachain accounts are updated as expected. #[test] - fn withdraw_and_deposit_nft() { + fn transfer_asset_nft() { MockNet::reset(); Relay::execute_with(|| { @@ -317,19 +377,415 @@ mod tests { }); ParaA::execute_with(|| { + // We want to transfer asset owned by `ParaA` over to `Para2` let message = Xcm(vec![TransferAsset { assets: (GeneralIndex(1), 42u32).into(), beneficiary: Parachain(2).into(), }]); - // Send withdraw and deposit + // We send the message to the `Parent`, the relay chain, since asset is there assert_ok!(ParachainPalletXcm::send_xcm(Here, Parent, message)); }); + // Asset ownership has changed Relay::execute_with(|| { assert_eq!(relay_chain::Uniques::owner(1, 42), Some(child_account_id(2))); }); } + /// Scenario: + /// A parachain configures error handler on relay chain, [ReportError] + /// A parachain attempts to withdraw on relay chain more than it has, causing an error. + /// We expect to receive error back in a report. + #[test] + fn report_error() { + MockNet::reset(); + + let first_query_id = 1234; + let second_query_id = 5678; + let max_weight = 1_000_000_000; + + // Send a message which will result in an error on the relay chain + ParaA::execute_with(|| { + // First we prepare the sequence for the error handler. + // The idea is to report actual error, clear error and then report it again. + // In the first report we expect to see the error but we don't expect it in the second one. + let error_handler_sequence = Xcm(vec![ + ReportError(QueryResponseInfo { + destination: Parachain(1).into(), + query_id: first_query_id, + max_weight, + }), + ClearError, + ReportError(QueryResponseInfo { + destination: Parachain(1).into(), + query_id: second_query_id, + max_weight, + }), + ]); + + let message = Xcm(vec![ + // This will set the error handler on the relay chain + SetErrorHandler(error_handler_sequence), + // We expect this to fail since it's more than the ParaA account has + WithdrawAsset((Here, INITIAL_BALANCE + 1).into()), + ]); + // Send withdraw and deposit with query holding + assert_ok!(ParachainPalletXcm::send_xcm(Here, Parent, message.clone(),)); + }); + + Relay::execute_with(|| { + // Withdraw was attempted & failed, balance hasn't changed + assert_eq!(relay_chain::Balances::free_balance(child_account_id(1)), INITIAL_BALANCE); + }); + + // Check that QueryResponse message was received and correct error was reported + ParaA::execute_with(|| { + assert_eq!( + parachain::MsgQueue::received_dmp(), + vec![ + // We expect the first response to contain an error since we failed to withdraw assets + Xcm(vec![QueryResponse { + query_id: first_query_id, + response: Response::ExecutionResult(Some(( + 1, // this is the instruction index at which the error occured + Error::FailedToTransactAsset("") + ))), + max_weight: 1_000_000_000, + querier: Some(Here.into()), + },]), + // The second response shouldn't contain any errors since we cleared all errors + Xcm(vec![QueryResponse { + query_id: second_query_id, + response: Response::ExecutionResult(None), + max_weight: 1_000_000_000, + querier: Some(Here.into()), + },]) + ], + ); + }); + + // Error enum can be found here: polkadot/xcm/src/v3/traits.rs + } + + /// Scenario: + /// A parachain sets appendix (callback after XCM execution finishes) and executes a remote transaction. + /// It expects to replies, one with execution error and one with 'success' (should be empty since result was cleared). + #[test] + fn set_appendix() { + MockNet::reset(); + + let first_query_id = 1234; + let second_query_id = 5678; + let max_weight = 1_000_000_000; + + // Send a message which will set appendix and perform remote transact on the relay chain + ParaA::execute_with(|| { + // First we prepare the sequence for the appendix - to be executed after XCM execution finishes (post-call hook). + // The idea is to report transact status, clear it and then report it again. + // In the first report we expect to see the execution result (error), while in second we expect success (state after `ClearOrigin`) + let appendix_sequence = Xcm(vec![ + ReportTransactStatus(QueryResponseInfo { + destination: Parachain(1).into(), + query_id: first_query_id, + max_weight, + }), + ClearTransactStatus, + ReportTransactStatus(QueryResponseInfo { + destination: Parachain(1).into(), + query_id: second_query_id, + max_weight, + }), + ]); + + // This is what we'll execute on the remote chain, relay in particular. + // It's a priviliged action and we expect it to fail + let priviliged_action = + relay_chain::RuntimeCall::System( + frame_system::Call::::set_heap_pages { pages: 1337 }, + ); + + let message = Xcm(vec![ + // This will set the post process handler on the relay chain + SetAppendix(appendix_sequence), + // We expect this to fail since it's more than the ParaA account has + Transact { + origin_kind: OriginKind::SovereignAccount, + require_weight_at_most: 1_000_000_000, + call: priviliged_action.encode().into(), + }, + ]); + + // Send XCM instructions to the relay chain + assert_ok!(ParachainPalletXcm::send_xcm(Here, Parent, message.clone(),)); + }); + + // process received XCM instructions from the ParaA + Relay::execute_with(|| { + // just execute received XCM instructions on the relay chain side + }); + + ParaA::execute_with(|| { + assert_eq!( + parachain::MsgQueue::received_dmp(), + vec![ + // We expect the first response to contain DispatchResult error since we tried to transact root-only priviliged action + Xcm(vec![QueryResponse { + query_id: first_query_id, + response: Response::DispatchResult(MaybeErrorCode::Error(vec![2])), + max_weight: 1_000_000_000, + querier: Some(Here.into()), + },]), + // The second response should be ok since we cleared the transact status prior to sending the response + Xcm(vec![QueryResponse { + query_id: second_query_id, + response: Response::DispatchResult(MaybeErrorCode::Success), + max_weight: 1_000_000_000, + querier: Some(Here.into()), + },]) + ], + ); + }); + } + + /// Scenario: + /// ParaA sets topic on relay chain. + /// + /// At the moment, I'm not sure how to verify it works in a nice & convenient way. One possible approach would be to + /// add custom handlers for certain actions which would make use of the topic. + #[test] + fn set_topic() { + MockNet::reset(); + + ParaA::execute_with(|| { + // we want to set a custom topic, which must be [u8; 32] + let mut topic = vec![0x1u8, 0x3, 0x3, 0x7]; + topic.extend(vec![0u8; 28]); + + let message = Xcm(vec![SetTopic(topic.try_into().unwrap())]); + assert_ok!(ParachainPalletXcm::send_xcm(Here, Parent, message)); + }); + + Relay::execute_with(|| { + // VM is created when XCM execution starts, and is dropped afterwards so I cannot check internal state. + // TODO: add custom handler so e.g. asset withdraw will work differently based on context? + }); + } + + /// Scenario: + /// We get some assets trapped and then claim them using `ClaimAsset` instruction. + /// We verify the balance state changes on the destination chain, right after they were trapped + /// and after they were claimed. + #[test] + fn claim_asset() { + MockNet::reset(); + + let trap_value = 61; + + ParaA::execute_with(|| { + // Just withdraw assets and don't do anything with them. Post-processing will trap them. + let message = Xcm(vec![WithdrawAsset((Here, trap_value).into())]); + assert_ok!(ParachainPalletXcm::send_xcm(Here, Parent, message)); + }); + + Relay::execute_with(|| { + // some assets were withdrawn - and lost! Or trapped, to be more precise. + assert_eq!( + relay_chain::Balances::free_balance(child_account_id(1)), + INITIAL_BALANCE - trap_value + ); + }); + + ParaA::execute_with(|| { + // Now we can attempt to claim the trapped assets. + // It's important we specify EXACTLY the amount of assets that were trapped and use the same origin we used when assets got trapped. + let message = Xcm(vec![ + ClaimAsset { + assets: (Here, trap_value).into(), + ticket: Here.into(), // not important, this value is equivalent to `Don't care` + }, + DepositAsset { assets: AllCounted(1).into(), beneficiary: Parachain(1).into() }, + ]); + assert_ok!(ParachainPalletXcm::send_xcm(Here, Parent, message)); + }); + + Relay::execute_with(|| { + // We can verify that previous balance was restored - trapped assets have been re-claimed. + assert_eq!(relay_chain::Balances::free_balance(child_account_id(1)), INITIAL_BALANCE); + }); + } + + /// Scenario: + /// We register appendix and error handler on the relay chain. + /// In one case, assets will get deposited to ParaA account, in other to `ParaB` account. + /// We rely on `ExpectAsset` instruction to raise an error in case expected condition isn't met. + /// Both scenarios are tested, with and without the error being raised. + #[test] + fn expect_asset_branching() { + MockNet::reset(); + + // In case execution results in an error, deposit asset to `ParaA` account + let error_handler_seq = Xcm(vec![DepositAsset { + assets: AllCounted(1).into(), + beneficiary: Parachain(1).into(), + }]); + + // In case all goes fine, deposit asset to `ParaB` account + let appendix_handler_seq = Xcm(vec![DepositAsset { + assets: AllCounted(1).into(), + beneficiary: Parachain(2).into(), + }]); + + let send_amount = 29; + + // We will withdraw some assets but will expect to have more in the holding register than we actually have. + // This is intentional to produce an error. + ParaA::execute_with(|| { + let message = Xcm(vec![ + SetErrorHandler(error_handler_seq.clone()), + SetAppendix(appendix_handler_seq.clone()), + WithdrawAsset((Here, send_amount).into()), + ExpectAsset((Here, send_amount + 1).into()), + ]); + // Send XCM instructions to the relay chain + assert_ok!(ParachainPalletXcm::send_xcm(Here, Parent, message.clone(),)); + }); + + // process received XCM instructions from the ParaA. + // We expect that error was raised since we expected too much assets in the holding register. + // In that scenario, all assets that were withdrawn should hav been deposited back to `ParaA` + Relay::execute_with(|| { + assert_eq!(relay_chain::Balances::free_balance(child_account_id(1)), INITIAL_BALANCE); + + assert_eq!(relay_chain::Balances::free_balance(child_account_id(2)), INITIAL_BALANCE); + }); + + // We repeat the same sequence but this time we'll expect a bit less than what we have. + // This shoudl pass and no error should be raised. + ParaA::execute_with(|| { + let message = Xcm(vec![ + SetErrorHandler(error_handler_seq.clone()), + SetAppendix(appendix_handler_seq.clone()), + WithdrawAsset((Here, send_amount).into()), + ExpectAsset((Here, send_amount - 1).into()), + ]); + // Send XCM instructions to the relay chain + assert_ok!(ParachainPalletXcm::send_xcm(Here, Parent, message.clone(),)); + }); + + // process received XCM instructions from the `ParaA`. + // We expect that assets were deposited to `ParaB` since appendix handler should have been executed. + Relay::execute_with(|| { + assert_eq!( + relay_chain::Balances::free_balance(child_account_id(1)), + INITIAL_BALANCE - send_amount + ); + + assert_eq!( + relay_chain::Balances::free_balance(child_account_id(2)), + INITIAL_BALANCE + send_amount + ); + }); + } + + /// Scenario + /// Query system pallet on the relay chain and ensure it's present with expected version. + #[test] + fn query_pallet() { + MockNet::reset(); + + let query_id = 1234; + let max_weight = 1_000_000_000; + let module_name = "frame_system"; + + // Query the relay chain for the system pallet by it's module name + ParaA::execute_with(|| { + let message = Xcm(vec![QueryPallet { + module_name: module_name.into(), + response_info: QueryResponseInfo { + destination: Parachain(1).into(), + query_id, + max_weight, + }, + }]); + assert_ok!(ParachainPalletXcm::send_xcm(Here, Parent, message.clone(),)); + }); + + Relay::execute_with(|| { + // execute message received from ParaA, send response back to `ParaA` + }); + + // Check that QueryResponse message was received and pallet info is as expected + ParaA::execute_with(|| { + assert_eq!( + parachain::MsgQueue::received_dmp(), + vec![ + // We expect the first response to contain an error since we failed to withdraw assets + Xcm(vec![QueryResponse { + query_id, + response: Response::PalletsInfo( + vec![PalletInfo::new( + 0, // index + "System".into(), // name + module_name.into(), // module_name + 4, // major + 0, // minor + 0, // patch + ) + .unwrap()] + .try_into() + .unwrap() + ), + max_weight: 1_000_000_000, + querier: Some(Here.into()), + },]) + ], + ); + }); + } + + // TODO: Idea - maybe demonstrate recursive error handling or appendix setting? + + ////////////////////////////////////////////////////// + ///////////////// SCENARIOS END ////////////////////// + ////////////////////////////////////////////////////// + + #[test] + fn remote_locking() { + MockNet::reset(); + + let locked_amount = 100; + + ParaB::execute_with(|| { + let message = Xcm(vec![LockAsset { + asset: (Here, locked_amount).into(), + unlocker: (Parachain(1),).into(), + }]); + assert_ok!(ParachainPalletXcm::send_xcm(Here, Parent, message.clone())); + }); + + Relay::execute_with(|| { + use pallet_balances::{BalanceLock, Reasons}; + assert_eq!( + relay_chain::Balances::locks(&child_account_id(2)), + vec![BalanceLock { + id: *b"py/xcmlk", + amount: locked_amount, + reasons: Reasons::All + }] + ); + }); + + ParaA::execute_with(|| { + assert_eq!( + parachain::MsgQueue::received_dmp(), + vec![Xcm(vec![NoteUnlockable { + owner: (Parent, Parachain(2)).into(), + asset: (Parent, locked_amount).into() + }])] + ); + }); + } + /// Scenario: /// The relay-chain teleports an NFT to a parachain. /// @@ -563,63 +1019,4 @@ mod tests { ); }); } - - /// Scenario: - /// A parachain wants to be notified that a transfer worked correctly. - /// It sends a `QueryHolding` after the deposit to get notified on success. - /// - /// Asserts that the balances are updated correctly and the expected XCM is sent. - #[test] - fn query_holding() { - MockNet::reset(); - - let send_amount = 10; - let query_id_set = 1234; - - // Send a message which fully succeeds on the relay chain - ParaA::execute_with(|| { - let message = Xcm(vec![ - WithdrawAsset((Here, send_amount).into()), - buy_execution((Here, send_amount)), - DepositAsset { assets: AllCounted(1).into(), beneficiary: Parachain(2).into() }, - ReportHolding { - response_info: QueryResponseInfo { - destination: Parachain(1).into(), - query_id: query_id_set, - max_weight: 1_000_000_000, - }, - assets: All.into(), - }, - ]); - // Send withdraw and deposit with query holding - assert_ok!(ParachainPalletXcm::send_xcm(Here, Parent, message.clone(),)); - }); - - // Check that transfer was executed - Relay::execute_with(|| { - // Withdraw executed - assert_eq!( - relay_chain::Balances::free_balance(child_account_id(1)), - INITIAL_BALANCE - send_amount - ); - // Deposit executed - assert_eq!( - relay_chain::Balances::free_balance(child_account_id(2)), - INITIAL_BALANCE + send_amount - ); - }); - - // Check that QueryResponse message was received - ParaA::execute_with(|| { - assert_eq!( - parachain::MsgQueue::received_dmp(), - vec![Xcm(vec![QueryResponse { - query_id: query_id_set, - response: Response::Assets(MultiAssets::new()), - max_weight: 1_000_000_000, - querier: Some(Here.into()), - }])], - ); - }); - } } diff --git a/xcm/xcm-simulator/example/src/relay_chain.rs b/xcm/xcm-simulator/example/src/relay_chain.rs index f743b84ab280..e56fcf888490 100644 --- a/xcm/xcm-simulator/example/src/relay_chain.rs +++ b/xcm/xcm-simulator/example/src/relay_chain.rs @@ -170,12 +170,12 @@ impl Config for XcmConfig { type Weigher = FixedWeightBounds; type Trader = FixedRateOfFungible; type ResponseHandler = (); - type AssetTrap = (); + type AssetTrap = XcmPallet; type AssetLocker = XcmPallet; type AssetExchanger = (); - type AssetClaims = (); + type AssetClaims = XcmPallet; type SubscriptionService = (); - type PalletInstancesInfo = (); + type PalletInstancesInfo = AllPalletsWithSystem; type FeeManager = (); type MaxAssetsIntoHolding = MaxAssetsIntoHolding; type MessageExporter = ();