diff --git a/Cargo.lock b/Cargo.lock index ac3eda25f..0a1229be8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6551,8 +6551,10 @@ dependencies = [ "cumulus-primitives-core 0.17.0", "emulated-integration-tests-common", "frame-support 39.0.0", + "log", "pallet-assets 41.0.0", "pallet-balances 40.0.0", + "pallet-contracts", "pallet-message-queue 42.0.0", "pallet-xcm 18.0.0", "parity-scale-codec", @@ -6570,6 +6572,8 @@ dependencies = [ "sp-consensus-beefy 23.0.0", "sp-consensus-grandpa 22.0.0", "sp-core 35.0.0", + "sp-io 39.0.0", + "sp-keyring 40.0.0", "sp-runtime 40.0.0", "staging-xcm 15.0.1", "staging-xcm-executor 18.0.0", @@ -8948,6 +8952,27 @@ dependencies = [ "sp-runtime 40.0.0", ] +[[package]] +name = "pallet-asset-manager" +version = "0.1.0" +dependencies = [ + "frame-benchmarking 39.0.0", + "frame-support 39.0.0", + "frame-system 39.0.0", + "log", + "pallet-balances 40.0.0", + "parity-scale-codec", + "scale-info", + "serde", + "sp-core 35.0.0", + "sp-io 39.0.0", + "sp-runtime 40.0.0", + "sp-std 14.0.0 (git+https://github.com/paritytech/polkadot-sdk?branch=stable2412)", + "staging-xcm 15.0.1", + "staging-xcm-builder 18.0.0", + "staging-xcm-executor 18.0.0", +] + [[package]] name = "pallet-asset-rate" version = "15.0.0" @@ -13790,6 +13815,7 @@ dependencies = [ "ismp-parachain-runtime-api", "log", "pallet-api", + "pallet-asset-manager", "pallet-assets 41.0.0", "pallet-aura 38.0.0", "pallet-authorship 39.0.0", @@ -13829,6 +13855,7 @@ dependencies = [ "sp-genesis-builder 0.16.0", "sp-inherents 35.0.0", "sp-io 39.0.0", + "sp-keyring 40.0.0", "sp-mmr-primitives 35.0.0", "sp-offchain 35.0.0", "sp-runtime 40.0.0", diff --git a/Cargo.toml b/Cargo.toml index d1109b76c..540809762 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -68,13 +68,14 @@ substrate-wasm-builder = { git = "https://github.com/paritytech/polkadot-sdk", b # Local pallet-api = { path = "pallets/api", default-features = false } +pallet-asset-manager = { path = "pallets/asset-manager", default-features = false } pallet-nfts = { path = "pallets/nfts", default-features = false } pop-chain-extension = { path = "./extension", default-features = false } pop-primitives = { path = "./primitives", default-features = false } pop-runtime-common = { path = "runtime/common", default-features = false } -pop-runtime-devnet = { path = "runtime/devnet", default-features = true } # default-features=true required for `-p pop-node` builds -pop-runtime-mainnet = { path = "runtime/mainnet", default-features = true } # default-features=true required for `-p pop-node` builds -pop-runtime-testnet = { path = "runtime/testnet", default-features = true } # default-features=true required for `-p pop-node` builds +pop-runtime-devnet = { path = "runtime/devnet", default-features = true } # default-features=true required for `-p pop-node` builds +pop-runtime-mainnet = { path = "runtime/mainnet", default-features = true } # default-features=true required for `-p pop-node` builds +pop-runtime-testnet = { path = "runtime/testnet", default-features = true } # default-features=true required for `-p pop-node` builds # Substrate frame-benchmarking = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2412", default-features = false } diff --git a/integration-tests/Cargo.toml b/integration-tests/Cargo.toml index 93d37669b..eefd46477 100644 --- a/integration-tests/Cargo.toml +++ b/integration-tests/Cargo.toml @@ -8,6 +8,7 @@ repository.workspace = true [dependencies] codec.workspace = true +log.workspace = true tracing-subscriber = { workspace = true, features = [ "env-filter", "fmt", @@ -19,6 +20,7 @@ tracing-subscriber = { workspace = true, features = [ frame-support.workspace = true pallet-assets.workspace = true pallet-balances.workspace = true +pallet-contracts.workspace = true pallet-message-queue.workspace = true sp-authority-discovery.workspace = true sp-consensus-aura.workspace = true @@ -26,6 +28,8 @@ sp-consensus-babe.workspace = true sp-consensus-beefy.workspace = true sp-consensus-grandpa.workspace = true sp-core.workspace = true +sp-io.workspace = true +sp-keyring.workspace = true sp-runtime.workspace = true # Polkadot @@ -70,6 +74,7 @@ runtime-benchmarks = [ "frame-support/runtime-benchmarks", "pallet-assets/runtime-benchmarks", "pallet-balances/runtime-benchmarks", + "pallet-contracts/runtime-benchmarks", "pallet-message-queue/runtime-benchmarks", "pallet-xcm/runtime-benchmarks", "paseo-runtime?/runtime-benchmarks", @@ -92,6 +97,7 @@ std = [ "frame-support/std", "pallet-assets/std", "pallet-balances/std", + "pallet-contracts/std", "pallet-message-queue/std", "pallet-xcm/std", "paseo-runtime-constants?/std", @@ -107,6 +113,7 @@ std = [ "sp-consensus-beefy/std", "sp-consensus-grandpa/std", "sp-core/std", + "sp-io/std", "sp-runtime/std", "tracing-subscriber/std", "westend-runtime-constants?/std", @@ -121,6 +128,7 @@ try-runtime = [ "frame-support/try-runtime", "pallet-assets/try-runtime", "pallet-balances/std", + "pallet-contracts/try-runtime", "pallet-message-queue/try-runtime", "paseo-runtime?/try-runtime", "pop-runtime-devnet/try-runtime", diff --git a/integration-tests/src/chains/asset_hub/genesis.rs b/integration-tests/src/chains/asset_hub/genesis.rs index b61d2308f..c91ea45f5 100644 --- a/integration-tests/src/chains/asset_hub/genesis.rs +++ b/integration-tests/src/chains/asset_hub/genesis.rs @@ -2,22 +2,42 @@ use cumulus_primitives_core::relay_chain::Balance; use emulated_integration_tests_common::{ accounts, build_genesis_storage, collators::invulnerables, SAFE_XCM_VERSION, }; +use frame_support::parameter_types; +use polkadot_primitives::AccountId; use sp_core::storage::Storage; +use super::*; use crate::chains::asset_hub::{ constants::currency::EXISTENTIAL_DEPOSIT, runtime::{ - BalancesConfig, CollatorSelectionConfig, ParachainInfoConfig, PolkadotXcmConfig, - RuntimeGenesisConfig, SessionConfig, SessionKeys, SystemConfig, WASM_BINARY, + AssetsConfig, BalancesConfig, CollatorSelectionConfig, ParachainInfoConfig, + PolkadotXcmConfig, RuntimeGenesisConfig, SessionConfig, SessionKeys, SystemConfig, + WASM_BINARY, }, }; pub(crate) const ED: Balance = EXISTENTIAL_DEPOSIT; pub(crate) const PARA_ID: u32 = 1000; +pub(crate) const USDC: u32 = 1337; +pub(crate) const USDT: u32 = 1984; +pub(crate) const USD_MIN_BALANCE: Balance = 70_000; + +parameter_types! { + pub AssetOwner: AccountId = Keyring::Alice.to_account_id(); +} pub(crate) fn genesis() -> Storage { let genesis_config = RuntimeGenesisConfig { system: SystemConfig::default(), + assets: AssetsConfig { + assets: vec![ + // Stablecoins + (USDC, AssetOwner::get(), true, USD_MIN_BALANCE), + (USDT, AssetOwner::get(), true, USD_MIN_BALANCE), + ], + next_asset_id: Some(USDC), + ..Default::default() + }, balances: BalancesConfig { balances: accounts::init_balances() .iter() diff --git a/integration-tests/src/chains/asset_hub/mod.rs b/integration-tests/src/chains/asset_hub/mod.rs index b15a86657..985ad4934 100644 --- a/integration-tests/src/chains/asset_hub/mod.rs +++ b/integration-tests/src/chains/asset_hub/mod.rs @@ -9,7 +9,10 @@ pub(crate) use {asset_hub_paseo_runtime as runtime, paseo_runtime_constants as c #[cfg(feature = "westend")] pub(crate) use {asset_hub_westend_runtime as runtime, westend_runtime_constants as constants}; +use super::*; + pub(crate) mod genesis; +pub(crate) use genesis::*; // AssetHub Parachain declaration. decl_test_parachains! { diff --git a/integration-tests/src/chains/mod.rs b/integration-tests/src/chains/mod.rs index 604bbeef0..84c11eaa1 100644 --- a/integration-tests/src/chains/mod.rs +++ b/integration-tests/src/chains/mod.rs @@ -1,3 +1,5 @@ +use sp_keyring::Sr25519Keyring as Keyring; + pub(crate) mod asset_hub; pub(crate) mod pop_network; pub(crate) mod relay; diff --git a/integration-tests/src/chains/pop_network/genesis.rs b/integration-tests/src/chains/pop_network/genesis.rs index d4ecf0904..f86db5e24 100644 --- a/integration-tests/src/chains/pop_network/genesis.rs +++ b/integration-tests/src/chains/pop_network/genesis.rs @@ -10,6 +10,7 @@ const SAFE_XCM_VERSION: u32 = xcm::prelude::XCM_VERSION; pub(crate) fn genesis() -> Storage { let genesis_config = runtime::RuntimeGenesisConfig { system: runtime::SystemConfig::default(), + assets: runtime::AssetsConfig { next_asset_id: Some(1), ..Default::default() }, balances: runtime::BalancesConfig { ..Default::default() }, parachain_info: runtime::ParachainInfoConfig { parachain_id: PARA_ID.into(), diff --git a/integration-tests/src/chains/pop_network/mod.rs b/integration-tests/src/chains/pop_network/mod.rs index 7c579defd..7e31bda7d 100644 --- a/integration-tests/src/chains/pop_network/mod.rs +++ b/integration-tests/src/chains/pop_network/mod.rs @@ -6,11 +6,11 @@ use emulated_integration_tests_common::{ }; use frame_support::traits::OnInitialize; #[cfg(feature = "devnet")] -use pop_runtime_devnet as runtime; +pub(crate) use pop_runtime_devnet as runtime; #[cfg(feature = "mainnet")] -use pop_runtime_mainnet as runtime; +pub(crate) use pop_runtime_mainnet as runtime; #[cfg(feature = "testnet")] -use pop_runtime_testnet as runtime; +pub(crate) use pop_runtime_testnet as runtime; // PopNetwork Parachain declaration decl_test_parachains! { @@ -29,6 +29,9 @@ decl_test_parachains! { pallets = { PolkadotXcm: runtime::PolkadotXcm, Balances: runtime::Balances, + Assets: runtime::Assets, + AssetManager: runtime::AssetManager, + Contracts: runtime::Contracts, } }, } diff --git a/integration-tests/src/lib.rs b/integration-tests/src/lib.rs index fce13ea9c..0421f1e5d 100644 --- a/integration-tests/src/lib.rs +++ b/integration-tests/src/lib.rs @@ -3,10 +3,12 @@ use asset_test_utils::xcm_helpers; use chains::{ asset_hub::{ - genesis::ED as ASSET_HUB_ED, runtime::xcm_config::XcmConfig as AssetHubXcmConfig, AssetHub, - AssetHubParaPallet, + self, + genesis::{ED as ASSET_HUB_ED, USDC}, + runtime::xcm_config::XcmConfig as AssetHubXcmConfig, + AssetHub, AssetHubParaPallet, }, - pop_network::{PopNetwork, PopNetworkParaPallet}, + pop_network::{genesis, runtime, PopNetwork, PopNetworkParaPallet}, relay::{ genesis::ED as RELAY_ED, runtime::xcm_config::XcmConfig as RelayXcmConfig, Relay, RelayRelayPallet as RelayPallet, @@ -20,7 +22,10 @@ use emulated_integration_tests_common::{ Test, TestArgs, TestContext, TestExt, }, }; -use frame_support::{pallet_prelude::Weight, sp_runtime::DispatchResult}; +use frame_support::{ + pallet_prelude::Weight, sp_runtime::DispatchResult, traits::fungibles::Inspect, +}; +use polkadot_primitives::AccountId; use pop_runtime_common::Balance; use pop_runtime_devnet::config::xcm::XcmConfig as PopNetworkXcmConfig; use xcm::prelude::*; @@ -181,9 +186,9 @@ fn para_to_system_para_reserve_transfer_assets(t: ParaToSystemParaTest) -> Dispa // Funds Pop with relay tokens from system para fn fund_pop_from_system_para( - sender: sp_runtime::AccountId32, + sender: AccountId, amount_to_send: Balance, - beneficiary: sp_runtime::AccountId32, + beneficiary: AccountId, assets: Assets, ) { let destination = AssetHubPara::sibling_location_of(PopNetworkPara::para_id()); @@ -198,6 +203,247 @@ fn fund_pop_from_system_para( test.assert(); } +use pop_runtime_devnet::{config::assets::AssetRegistrarMetadata, RuntimeOrigin}; + +/// Asset Hub. +pub const ASSET_HUB: Junction = Parachain(1000); +/// Assets on Asset Hub. +pub const ASSET_HUB_ASSETS: Junction = PalletInstance(50); +pub const USDC_POP_ASSET_ID: u32 = 1; + +#[cfg(feature = "devnet")] +#[test] +fn receive_usdc_on_pop() { + use codec::Encode; + use pallet_contracts::{Code, CollectEvents, Determinism}; + const GAS_LIMIT: Weight = Weight::from_parts(100_000_000_000, 3 * 1024 * 1024); + const DEBUG_OUTPUT: pallet_contracts::DebugInfo = pallet_contracts::DebugInfo::UnsafeDebug; + init_tracing(); + + // Provide bob DOT. + let amount: Balance = ASSET_HUB_ED * 1000; + let bob_on_pop = PopNetworkParaReceiver::get(); + fund_pop_from_system_para( + AssetHubParaSender::get(), + amount * 20, + bob_on_pop.clone(), + (Parent, amount * 20).into(), + ); + // Fund Pop Network's SA on AH with DOT. + let pop_net_location_as_seen_by_ahr = + AssetHubPara::sibling_location_of(PopNetworkPara::para_id()); + let sov_pop_net_on_ahr = AssetHubPara::sovereign_account_id_of(pop_net_location_as_seen_by_ahr); + AssetHubPara::fund_accounts(vec![(sov_pop_net_on_ahr.into(), amount * 2)]); + // Amounts used for doing transfers. + let transfer_amount = amount / 2; + + // Create a foreign asset for usdc on Pop. + // ------ + let usdc = AssetId([ASSET_HUB_ASSETS, GeneralIndex(USDC.into())].into()); + let usdc_location = Location::new(1, [ASSET_HUB, ASSET_HUB_ASSETS, GeneralIndex(USDC.into())]); + let metadata = AssetRegistrarMetadata { + name: "USDC".encode(), + symbol: "USDC".encode(), + decimals: 10, + is_frozen: false, + }; + let mut contract = AccountId::from([0u8; 32]); + PopNetwork::::execute_with(|| { + as PopNetworkParaPallet>::AssetManager::register_foreign_asset( + RuntimeOrigin::root(), + usdc_location, + metadata, + 1, + true, + ); + + let path = "../pop-api/examples/foreign_fungibles/target/ink/foreign_fungibles.wasm"; + let wasm_binary = std::fs::read(path).expect("could not read .wasm file"); + let instantiate_result = + as PopNetworkParaPallet>::Contracts::bare_instantiate( + bob_on_pop.clone(), + amount * 5, + GAS_LIMIT, + None, + Code::Upload(wasm_binary), + function_selector("new"), + vec![], + DEBUG_OUTPUT, + CollectEvents::Skip, + ) + .result + .expect("Contract instantiation failed"); + assert!(!instantiate_result.result.did_revert()); + contract = instantiate_result.account_id; + }); + // ------ + + // Provide sender on AH with usdc to transfer. + // ------ + let sender = AssetHubParaSender::get(); + let usdc_owner = asset_hub::AssetOwner::get(); + let usdc_owner_signer = ::RuntimeOrigin::signed(usdc_owner.clone()); + let usdc_amount_to_send = asset_hub::USD_MIN_BALANCE * 100_000; + AssetHubPara::mint_asset( + usdc_owner_signer.clone(), + USDC, + sender.clone(), + usdc_amount_to_send + asset_hub::USD_MIN_BALANCE, + ); + let usdc_asset = (usdc, usdc_amount_to_send).into(); + // ------ + + // Transfer usdc to contract. + let fee_amount_to_send = runtime::UNIT / 2; + let fee_asset = (Parent, fee_amount_to_send).into(); + let assets: Assets = vec![usdc_asset, fee_asset].into(); + let destination = AssetHubPara::sibling_location_of(PopNetworkPara::para_id()); + let receiver = contract.clone(); + let location_usdc = Location::new(1, [ASSET_HUB, ASSET_HUB_ASSETS, GeneralIndex(USDC.into())]); // Init Test + let test_args = TestContext { + sender: sender.clone(), + receiver: receiver.clone(), + args: TestArgs::new_para( + destination.clone(), + receiver.clone(), + usdc_amount_to_send, + assets, + Some(USDC), + 1, + ), + }; + let mut test = Test::::new(test_args); + // ---- Assertions + let sender_usdc_balance_before = AssetHubPara::execute_with(|| { + <::Assets as Inspect<_>>::balance(USDC, &sender) + }); + let receiver_usdc_balance_before = PopNetworkPara::execute_with(|| { + <::Assets as Inspect<_>>::balance( + USDC_POP_ASSET_ID, + &receiver, + ) + }); + test.set_assertion::(asset_hub_to_para_assets_sender_assertions); + test.set_assertion::(asset_hub_to_para_assets_receiver_assertions); + test.set_dispatchable::(asset_hub_to_para_transfer_assets); + test.assert(); + let sender_usdc_balance_after = AssetHubPara::execute_with(|| { + <::Assets as Inspect<_>>::balance(USDC, &sender) + }); + let receiver_usdc_balance_after = PopNetworkPara::execute_with(|| { + <::Assets as Inspect<_>>::balance( + USDC_POP_ASSET_ID, + &receiver, + ) + }); + + assert_eq!(sender_usdc_balance_after, sender_usdc_balance_before - usdc_amount_to_send); + assert_eq!(receiver_usdc_balance_after, receiver_usdc_balance_before + usdc_amount_to_send); + + // ------ + + let ah_transfer_amount = usdc_amount_to_send / 2; + // Step 4: Interact with the contract on Pop Network + PopNetworkPara::execute_with(|| { + // Call contract to transfer half USDC to another account on Pop. + let local_transfer_amount = usdc_amount_to_send / 2; + let local_receiver = AccountId::from([2u8; 32]); + let function = function_selector("transfer"); + let params = [local_receiver.encode(), local_transfer_amount.encode()].concat(); + let input = [function, params].concat(); + let call_result = as PopNetworkParaPallet>::Contracts::bare_call( + bob_on_pop.clone(), + contract.clone(), + 0, + GAS_LIMIT, + None, + input, + DEBUG_OUTPUT, + CollectEvents::Skip, + Determinism::Enforced, + ); + let result = decoded::>(call_result.result.unwrap()).unwrap(); + assert!(result.is_ok()); + // Verify local transfer + let local_receiver_balance = + < as PopNetworkParaPallet>::Assets as Inspect<_>>::balance( + USDC_POP_ASSET_ID, + &local_receiver, + ); + assert_eq!(local_receiver_balance, local_transfer_amount); + let contract_usdc_balance_after_local = + < as PopNetworkParaPallet>::Assets as Inspect<_>>::balance( + USDC_POP_ASSET_ID, + &contract, + ); + assert_eq!(contract_usdc_balance_after_local, usdc_amount_to_send - local_transfer_amount); + + // Call contract to transfer remaining USDC back to AH + let ah_receiver = AssetHubParaReceiver::get(); + let function = function_selector("ah_transfer"); + let params = + [ah_receiver.encode(), ah_transfer_amount.encode(), 2500000000000u128.encode()] + .concat(); + let input = [function, params].concat(); + let call_result = as PopNetworkParaPallet>::Contracts::bare_call( + bob_on_pop.clone(), + contract.clone(), + 0, + GAS_LIMIT, + None, + input, + DEBUG_OUTPUT, + CollectEvents::Skip, + Determinism::Enforced, + ); + let result = decoded::>(call_result.result.unwrap()).unwrap(); + assert!(result.is_ok()); + + // Check Pop Network events for USDC burn + type RuntimeEvent = ::RuntimeEvent; + let events = PopNetworkPara::events(); + println!("\nPopNetwork events after contract calls: {:?}", events); + assert_event_matches!( + events, + RuntimeEvent::Assets(pallet_assets::Event::Burned { asset_id, owner, balance }) + if *asset_id == USDC_POP_ASSET_ID && *owner == contract && *balance == + ah_transfer_amount ); + }); + + // Step 5: Verify the transfer back to Asset Hub + AssetHubPara::execute_with(|| { + // Check AH events for USDC mint + type RuntimeEventAH = ::RuntimeEvent; + let sov_pop_net_on_ahr = AssetHubPara::sovereign_account_id_of(destination); + let ah_receiver = AssetHubParaReceiver::get(); + let events = AssetHubPara::events(); + println!("\nAssetHub events after contract call: {:?}", events); + assert_event_matches!( + events, + RuntimeEventAH::Assets(pallet_assets::Event::Burned { asset_id, owner, balance }) + if *asset_id == USDC && *owner == sov_pop_net_on_ahr && *balance == ah_transfer_amount + ); + assert_event_matches!( + events, + RuntimeEventAH::Assets(pallet_assets::Event::Issued { asset_id, owner, amount }) + if *asset_id == USDC && *owner == ah_receiver && *amount <= ah_transfer_amount && + *amount > 0 ); + assert_event_matches!( + events, + RuntimeEventAH::MessageQueue(pallet_message_queue::Event::Processed { success, .. }) + if *success == true + ); + + // Verify receiver’s USDC balance on AH + let receiver_usdc_balance = + <::Assets as Inspect<_>>::balance( + USDC, + &ah_receiver, + ); + assert!(receiver_usdc_balance == ah_transfer_amount); + }); +} + /// Reserve Transfers of native asset from Relay to Parachain should work #[test] #[should_panic] @@ -364,7 +610,7 @@ fn reserve_transfer_native_asset_from_para_to_system_para() { // fn place_coretime_spot_order_from_para_to_relay() { // init_tracing(); // -// let beneficiary: sp_runtime::AccountId32 = [1u8; 32].into(); +// let beneficiary: AccountId = [1u8; 32].into(); // // // Setup: reserve transfer from relay to Pop, so that sovereign account accurate for return // transfer let amount_to_send: Balance = pop_runtime::UNIT * 1000; @@ -372,7 +618,7 @@ fn reserve_transfer_native_asset_from_para_to_system_para() { // // let message = { // let assets: Asset = (Here, 10 * pop_runtime::UNIT).into(); -// let beneficiary = AccountId32 { id: beneficiary.clone().into(), network: None }.into(); +// let beneficiary = AccountId { id: beneficiary.clone().into(), network: None }.into(); // let spot_order = ::RuntimeCall::OnDemandAssignmentProvider( // assigner_on_demand::Call::<::Runtime>::place_order_keep_alive { // max_amount: 1 * pop_runtime::UNIT, @@ -474,3 +720,66 @@ fn init_tracing() { .init(); }); } + +/// Asserts that an event has been emitted using pattern matching. +macro_rules! assert_event_matches { + ($expression:expr, $pattern:pat $(if $guard:expr)? $(,)?) => { + assert!($expression.iter().any(|e| matches!(e, $pattern $(if $guard)?)), "expected event not found in {:#?}", $expression); + }; +} +pub(crate) use assert_event_matches; + +fn asset_hub_to_para_transfer_assets(t: Test) -> DispatchResult { + ::PolkadotXcm::limited_reserve_transfer_assets( + t.signed_origin, + bx!(t.args.dest.into()), + bx!(t.args.beneficiary.into()), + bx!(t.args.assets.into()), + t.args.fee_asset_item, + t.args.weight_limit, + ) +} + +fn asset_hub_to_para_assets_sender_assertions(t: Test) { + type RuntimeEvent = ::RuntimeEvent; + AssetHubPara::assert_xcm_pallet_attempted_complete(Some(Weight::from_parts( + 864_610_000, + 8_799, + ))); + let sovereign_account = AssetHubPara::sovereign_account_id_of(t.args.dest.clone()); + assert_event_matches!( + AssetHubPara::events(), + RuntimeEvent::Assets(pallet_assets::Event::Transferred { asset_id, from, to, amount }) + if *asset_id == USDC && *from == t.sender.account_id && *to == sovereign_account && *amount == t.args.amount + ); +} + +fn asset_hub_to_para_assets_receiver_assertions(t: Test) { + type RuntimeEvent = ::RuntimeEvent; + let events = PopNetworkPara::events(); + println!("PopNetwork events: {:?}", events); // Debug output + assert_event_matches!( + events, + RuntimeEvent::Assets(pallet_assets::Event::Issued { asset_id, owner, amount }) + if *asset_id == USDC_POP_ASSET_ID && *owner == t.receiver.account_id && *amount == t.args.amount + ); + assert_event_matches!( + events, + RuntimeEvent::MessageQueue(pallet_message_queue::Event::Processed { success, .. }) + if *success == true + ); +} + +use codec::Decode; +use pallet_contracts::{Code, CollectEvents}; + +fn function_selector(name: &str) -> Vec { + let hash = sp_io::hashing::blake2_256(name.as_bytes()); + [hash[0..4].to_vec()].concat() +} + +fn decoded( + result: pallet_contracts::ExecReturnValue, +) -> Result { + ::decode(&mut &result.data[1..]).map_err(|_| result) +} diff --git a/pallets/asset-manager/Cargo.toml b/pallets/asset-manager/Cargo.toml new file mode 100644 index 000000000..d9550b5b4 --- /dev/null +++ b/pallets/asset-manager/Cargo.toml @@ -0,0 +1,49 @@ +[package] +authors = { workspace = true } +edition = "2021" +name = "pallet-asset-manager" +version = "0.1.0" + +[dependencies] +log = { workspace = true } +serde = { workspace = true, optional = true } + +# Substrate +codec = { workspace = true, features = [ "derive" ] } +frame-support = { workspace = true } +frame-system = { workspace = true } +scale-info = { workspace = true, features = [ "derive" ] } +sp-io = { workspace = true } +sp-runtime = { workspace = true } +sp-std = { workspace = true } + +# Polkadot +xcm = { workspace = true } +xcm-builder = { workspace = true } +xcm-executor = { workspace = true } + +# Benchmarks +frame-benchmarking = { workspace = true, optional = true } + +[dev-dependencies] +pallet-balances = { workspace = true, features = [ "insecure_zero_ed", "std" ] } +sp-core = { workspace = true, features = [ "std" ] } + +[features] +default = [ "std" ] +std = [ + "codec/std", + "frame-support/std", + "frame-system/std", + "scale-info/std", + "serde", + "sp-io/std", + "sp-runtime/std", + "sp-std/std", + "xcm-builder/std", + "xcm-executor/std", + "xcm/std", +] + +runtime-benchmarks = [ "frame-benchmarking" ] +try-runtime = [ "frame-support/try-runtime" ] diff --git a/pallets/asset-manager/src/benchmarks.rs b/pallets/asset-manager/src/benchmarks.rs new file mode 100644 index 000000000..bc515335d --- /dev/null +++ b/pallets/asset-manager/src/benchmarks.rs @@ -0,0 +1,93 @@ +// Copyright 2019-2022 PureStake Inc. +// This file is part of Moonbeam. + +// Moonbeam is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Moonbeam is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Moonbeam. If not, see . + +#![cfg(feature = "runtime-benchmarks")] + +use frame_benchmarking::{benchmarks, impl_benchmark_test_suite}; +use frame_system::RawOrigin; +use xcm::v3::prelude::*; + +use crate::{Call, Config, Pallet}; + +benchmarks! { + // This where clause allows us to create ForeignAssetTypes + where_clause { where T::ForeignAssetType: From } + register_foreign_asset { + // does not really matter what we register + let asset_type = T::ForeignAssetType::default(); + let metadata = T::AssetRegistrarMetadata::default(); + let amount = 1u32.into(); + let asset_id: T::AssetId = asset_type.clone().into(); + + }: _(RawOrigin::Root, asset_type.clone(), metadata, amount, true) + verify { + assert_eq!(Pallet::::asset_id_type(asset_id), Some(asset_type)); + } + + change_existing_asset_type { + let asset_type: T::ForeignAssetType = Location::new(0, X1(GeneralIndex(1 as u128))).into(); + let metadata = T::AssetRegistrarMetadata::default(); + let amount = 1u32.into(); + Pallet::::register_foreign_asset( + RawOrigin::Root.into(), + asset_type.clone(), + metadata, + amount, + true + )?; + + let new_asset_type = T::ForeignAssetType::default(); + let asset_id_to_be_changed = asset_type.clone().into(); + }: _(RawOrigin::Root, asset_id_to_be_changed, new_asset_type.clone(), 1) + verify { + assert_eq!(Pallet::::asset_id_type(asset_id_to_be_changed), Some(new_asset_type.clone())); + assert_eq!(Pallet::::asset_type_id(new_asset_type.clone()), Some(asset_id_to_be_changed)); + assert!(Pallet::::asset_type_id(asset_type).is_none()); + } + + remove_existing_asset_type { + let asset_type: T::ForeignAssetType = Location::new(0, X1(GeneralIndex(1 as u128))).into(); + let metadata = T::AssetRegistrarMetadata::default(); + let amount = 1u32.into(); + Pallet::::register_foreign_asset( + RawOrigin::Root.into(), + asset_type.clone(), + metadata, + amount, + true + )?; + let asset_id: T::AssetId = asset_type.clone().into(); + }: _(RawOrigin::Root, asset_id, 1) + verify { + assert!(Pallet::::asset_id_type(asset_id).is_none()); + assert!(Pallet::::asset_type_id(asset_type).is_none()); + } +} + +#[cfg(test)] +mod tests { + use sp_io::TestExternalities; + use sp_runtime::BuildStorage; + + use crate::mock::Test; + + pub fn new_test_ext() -> TestExternalities { + let t = frame_system::GenesisConfig::::default().build_storage().unwrap(); + TestExternalities::new(t) + } +} + +impl_benchmark_test_suite!(Pallet, crate::benchmarks::tests::new_test_ext(), crate::mock::Test); diff --git a/pallets/asset-manager/src/lib.rs b/pallets/asset-manager/src/lib.rs new file mode 100644 index 000000000..903da4811 --- /dev/null +++ b/pallets/asset-manager/src/lib.rs @@ -0,0 +1,312 @@ +// Copyright 2019-2022 PureStake Inc. +// This file is part of Moonbeam. + +// Moonbeam is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Moonbeam is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Moonbeam. If not, see . + +//! # Asset Manager Pallet +//! +//! This pallet allows to register new assets if certain conditions are met +//! The main goal of this pallet is to allow moonbeam to register XCM assets +//! The assumption is we work with AssetTypes, which can then be compared to AssetIds +//! +//! This pallet has five storage items: AssetIdType, which holds a mapping from AssetId->AssetType +//! AssetTypeUnitsPerSecond: an AssetType->u128 mapping that holds how much each AssetType should +//! be charged per unit of second, in the case such an Asset is received as a XCM asset. Finally, +//! AssetTypeId holds a mapping from AssetType -> AssetId. +//! +//! This pallet has eight extrinsics: register_foreign_asset, which registers a foreign +//! asset in this pallet and creates the asset as dictated by the AssetRegistrar trait. +//! change_existing_asset_type: which allows to update the correspondence between AssetId and +//! AssetType +//! remove_supported_asset: which removes an asset from the supported assets for fee payment +//! remove_existing_asset_type: which removes a mapping from a foreign asset to an assetId +//! destroy_foreign_asset: which destroys a foreign asset and all its associated data + +#![cfg_attr(not(feature = "std"), no_std)] + +use frame_support::pallet; +pub use pallet::*; +#[cfg(any(test, feature = "runtime-benchmarks"))] +mod benchmarks; +pub mod migrations; +#[cfg(test)] +pub mod mock; +#[cfg(test)] +pub mod tests; +pub mod weights; +pub mod xcm_primitives; + +pub use crate::weights::WeightInfo; + +#[pallet] +pub mod pallet { + use codec::HasCompact; + use frame_support::{pallet_prelude::*, PalletId}; + use frame_system::pallet_prelude::*; + use sp_runtime::traits::{AccountIdConversion, AtLeast32BitUnsigned}; + + use super::*; + + #[pallet::pallet] + #[pallet::without_storage_info] + pub struct Pallet(PhantomData); + + /// The AssetManagers's pallet id + pub const PALLET_ID: PalletId = PalletId(*b"asstmngr"); + + // The registrar trait. We need to comply with this + pub trait AssetRegistrar { + fn next_asset_id() -> T::AssetId; + // How to create a foreign asset, meaning an asset whose reserve chain + // is not our chain + fn create_foreign_asset( + _asset: T::AssetId, + _min_balance: T::Balance, + _metadata: T::AssetRegistrarMetadata, + // Wether or not an asset-receiving account increments the sufficient counter + _is_sufficient: bool, + ) -> DispatchResult { + unimplemented!() + } + + // How to destroy a foreign asset + fn destroy_foreign_asset(_asset: T::AssetId) -> DispatchResult { + unimplemented!() + } + + // Get destroy asset weight dispatch info + fn destroy_asset_dispatch_info_weight(_asset: T::AssetId) -> Weight; + } + + // We implement this trait to be able to get the AssetType and units per second registered + impl xcm_primitives::AssetTypeGetter for Pallet { + fn get_asset_type(asset_id: T::AssetId) -> Option { + AssetIdType::::get(asset_id) + } + + fn get_asset_id(asset_type: T::ForeignAssetType) -> Option { + AssetTypeId::::get(asset_type) + } + + #[cfg(feature = "runtime-benchmarks")] + fn set_asset_type_asset_id(asset_type: T::ForeignAssetType, asset_id: T::AssetId) { + AssetTypeId::::insert(&asset_type, asset_id); + AssetIdType::::insert(&asset_id, asset_type); + } + } + + #[pallet::config] + pub trait Config: frame_system::Config { + type RuntimeEvent: From> + IsType<::RuntimeEvent>; + + /// The Asset Id. This will be used to create the asset and to associate it with + /// a assetType + type AssetId: Member + Parameter + Default + Copy + HasCompact + MaxEncodedLen; + + /// The Asset Metadata we want to store + type AssetRegistrarMetadata: Member + Parameter + Default; + + /// The Foreign Asset Kind. + type ForeignAssetType: Parameter + Member + Ord + PartialOrd + Default; + + /// The units in which we record balances. + type Balance: Member + Parameter + AtLeast32BitUnsigned + Default + Copy + MaxEncodedLen; + + /// The asset Registrar. + /// The trait we use to register Assets + type AssetRegistrar: AssetRegistrar; + + /// Origin that is allowed to create and modify asset information for foreign assets + type ForeignAssetModifierOrigin: EnsureOrigin; + + type WeightInfo: WeightInfo; + } + + /// An error that can occur while executing the mapping pallet's logic. + #[pallet::error] + pub enum Error { + ErrorCreatingAsset, + AssetAlreadyExists, + AssetDoesNotExist, + TooLowNumAssetsWeightHint, + LocalAssetLimitReached, + ErrorDestroyingAsset, + NotSufficientDeposit, + NonExistentLocalAsset, + } + + #[pallet::event] + #[pallet::generate_deposit(pub(crate) fn deposit_event)] + pub enum Event { + /// New asset with the asset manager is registered + ForeignAssetRegistered { + asset_id: T::AssetId, + asset: T::ForeignAssetType, + metadata: T::AssetRegistrarMetadata, + }, + /// Changed the amount of units we are charging per execution second for a given asset + #[deprecated(note = "Should not be used")] + UnitsPerSecondChanged, + /// Changed the xcm type mapping for a given asset id + ForeignAssetXcmLocationChanged { asset_id: T::AssetId, new_asset_type: T::ForeignAssetType }, + /// Removed all information related to an assetId + ForeignAssetRemoved { asset_id: T::AssetId, asset_type: T::ForeignAssetType }, + /// Supported asset type for fee payment removed + SupportedAssetRemoved { asset_type: T::ForeignAssetType }, + /// Removed all information related to an assetId and destroyed asset + ForeignAssetDestroyed { asset_id: T::AssetId, asset_type: T::ForeignAssetType }, + /// Removed all information related to an assetId and destroyed asset + LocalAssetDestroyed { asset_id: T::AssetId }, + } + + /// Mapping from an asset id to asset type. + /// This is mostly used when receiving transaction specifying an asset directly, + /// like transferring an asset from this chain to another. + #[pallet::storage] + #[pallet::getter(fn asset_id_type)] + pub type AssetIdType = + StorageMap<_, Blake2_128Concat, T::AssetId, T::ForeignAssetType>; + + /// Reverse mapping of AssetIdType. Mapping from an asset type to an asset id. + /// This is mostly used when receiving a multilocation XCM message to retrieve + /// the corresponding asset in which tokens should me minted. + #[pallet::storage] + #[pallet::getter(fn asset_type_id)] + pub type AssetTypeId = + StorageMap<_, Blake2_128Concat, T::ForeignAssetType, T::AssetId>; + + #[pallet::call] + impl Pallet { + /// Register new asset with the asset manager + #[pallet::call_index(0)] + #[pallet::weight(T::WeightInfo::register_foreign_asset())] + pub fn register_foreign_asset( + origin: OriginFor, + asset: T::ForeignAssetType, + metadata: T::AssetRegistrarMetadata, + min_amount: T::Balance, + is_sufficient: bool, + ) -> DispatchResult { + T::ForeignAssetModifierOrigin::ensure_origin(origin)?; + + let asset_id = T::AssetRegistrar::next_asset_id(); + // Ensure such an assetId does not exist + ensure!(AssetIdType::::get(&asset_id).is_none(), Error::::AssetAlreadyExists); + T::AssetRegistrar::create_foreign_asset( + asset_id, + min_amount, + metadata.clone(), + is_sufficient, + ) + .map_err(|_| Error::::ErrorCreatingAsset)?; + + // Insert the association assetId->assetType + AssetIdType::::insert(&asset_id, &asset); + AssetTypeId::::insert(&asset, &asset_id); + + Self::deposit_event(Event::ForeignAssetRegistered { asset_id, asset, metadata }); + Ok(()) + } + + /// Change the xcm type mapping for a given assetId + /// We also change this if the previous units per second where pointing at the old + /// assetType + #[pallet::call_index(2)] + #[pallet::weight(T::WeightInfo::change_existing_asset_type())] + pub fn change_existing_asset_type( + origin: OriginFor, + asset_id: T::AssetId, + new_asset_type: T::ForeignAssetType, + _num_assets_weight_hint: u32, + ) -> DispatchResult { + T::ForeignAssetModifierOrigin::ensure_origin(origin)?; + + let previous_asset_type = + AssetIdType::::get(&asset_id).ok_or(Error::::AssetDoesNotExist)?; + + // Insert new asset type info + AssetIdType::::insert(&asset_id, &new_asset_type); + AssetTypeId::::insert(&new_asset_type, &asset_id); + + // Remove previous asset type info + AssetTypeId::::remove(&previous_asset_type); + + Self::deposit_event(Event::ForeignAssetXcmLocationChanged { asset_id, new_asset_type }); + Ok(()) + } + + /// Remove a given assetId -> assetType association + #[pallet::call_index(4)] + #[pallet::weight(T::WeightInfo::remove_existing_asset_type())] + pub fn remove_existing_asset_type( + origin: OriginFor, + asset_id: T::AssetId, + _num_assets_weight_hint: u32, + ) -> DispatchResult { + T::ForeignAssetModifierOrigin::ensure_origin(origin)?; + + let asset_type = + AssetIdType::::get(&asset_id).ok_or(Error::::AssetDoesNotExist)?; + + // Remove from AssetIdType + AssetIdType::::remove(&asset_id); + // Remove from AssetTypeId + AssetTypeId::::remove(&asset_type); + + Self::deposit_event(Event::ForeignAssetRemoved { asset_id, asset_type }); + Ok(()) + } + + /// Destroy a given foreign assetId + /// The weight in this case is the one returned by the trait + /// plus the db writes and reads from removing all the associated + /// data + #[pallet::call_index(6)] + #[pallet::weight({ + let dispatch_info_weight = T::AssetRegistrar::destroy_asset_dispatch_info_weight( + *asset_id + ); + T::WeightInfo::remove_existing_asset_type() + .saturating_add(dispatch_info_weight) + })] + pub fn destroy_foreign_asset( + origin: OriginFor, + asset_id: T::AssetId, + _num_assets_weight_hint: u32, + ) -> DispatchResult { + T::ForeignAssetModifierOrigin::ensure_origin(origin)?; + + T::AssetRegistrar::destroy_foreign_asset(asset_id) + .map_err(|_| Error::::ErrorDestroyingAsset)?; + + let asset_type = + AssetIdType::::get(&asset_id).ok_or(Error::::AssetDoesNotExist)?; + + // Remove from AssetIdType + AssetIdType::::remove(&asset_id); + // Remove from AssetTypeId + AssetTypeId::::remove(&asset_type); + + Self::deposit_event(Event::ForeignAssetDestroyed { asset_id, asset_type }); + Ok(()) + } + } + + impl Pallet { + /// The account ID of AssetManager + pub fn account_id() -> T::AccountId { + PALLET_ID.into_account_truncating() + } + } +} diff --git a/pallets/asset-manager/src/migrations.rs b/pallets/asset-manager/src/migrations.rs new file mode 100644 index 000000000..2e08649a7 --- /dev/null +++ b/pallets/asset-manager/src/migrations.rs @@ -0,0 +1,15 @@ +// Copyright 2019-2022 PureStake Inc. +// This file is part of Moonbeam. + +// Moonbeam is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Moonbeam is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Moonbeam. If not, see . diff --git a/pallets/asset-manager/src/mock.rs b/pallets/asset-manager/src/mock.rs new file mode 100644 index 000000000..9a4fca559 --- /dev/null +++ b/pallets/asset-manager/src/mock.rs @@ -0,0 +1,232 @@ +// Copyright 2019-2022 PureStake Inc. +// This file is part of Moonbeam. + +// Moonbeam is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Moonbeam is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Moonbeam. If not, see . + +use codec::{Decode, Encode}; +use frame_support::{ + construct_runtime, derive_impl, parameter_types, traits::Everything, weights::Weight, +}; +use frame_system::EnsureRoot; +use scale_info::TypeInfo; +use sp_core::{RuntimeDebug, H256}; +use sp_runtime::{ + traits::{BlakeTwo256, Hash as THash, IdentityLookup}, + BuildStorage, DispatchError, +}; +use xcm::v3::prelude::*; + +use super::*; +use crate as pallet_asset_manager; + +type Block = frame_system::mocking::MockBlock; + +pub type AccountId = u64; +pub type Balance = u64; + +construct_runtime!( + pub enum Test + { + System: frame_system, + Balances: pallet_balances, + AssetManager: pallet_asset_manager, + } +); + +parameter_types! { + pub const BlockHashCount: u32 = 250; +} + +#[derive_impl(frame_system::config_preludes::TestDefaultConfig as frame_system::DefaultConfig)] +impl frame_system::Config for Test { + type AccountData = pallet_balances::AccountData; + type AccountId = AccountId; + type BaseCallFilter = Everything; + type Block = Block; + type BlockHashCount = BlockHashCount; + type BlockLength = (); + type BlockWeights = (); + type DbWeight = (); + type Hash = H256; + type Hashing = BlakeTwo256; + type Lookup = IdentityLookup; + type MaxConsumers = frame_support::traits::ConstU32<16>; + type MultiBlockMigrator = (); + type Nonce = u64; + type OnKilledAccount = (); + type OnNewAccount = (); + type OnSetCode = (); + type PalletInfo = PalletInfo; + type PostInherents = (); + type PostTransactions = (); + type PreInherents = (); + type RuntimeCall = RuntimeCall; + type RuntimeEvent = RuntimeEvent; + type RuntimeOrigin = RuntimeOrigin; + type RuntimeTask = RuntimeTask; + type SS58Prefix = (); + type SingleBlockMigrations = (); + type SystemWeightInfo = (); + type Version = (); +} + +parameter_types! { + pub const ExistentialDeposit: u64 = 0; +} + +impl pallet_balances::Config for Test { + type AccountStore = System; + type Balance = Balance; + type DoneSlashHandler = (); + type DustRemoval = (); + type ExistentialDeposit = ExistentialDeposit; + type FreezeIdentifier = (); + type MaxFreezes = (); + type MaxLocks = (); + type MaxReserves = (); + type ReserveIdentifier = [u8; 8]; + type RuntimeEvent = RuntimeEvent; + type RuntimeFreezeReason = (); + type RuntimeHoldReason = (); + type WeightInfo = (); +} + +parameter_types! { + pub const AssetDeposit: u64 = 1; + pub const ApprovalDeposit: u64 = 1; + pub const StringLimit: u32 = 50; + pub const MetadataDepositBase: u64 = 1; + pub const MetadataDepositPerByte: u64 = 1; +} + +parameter_types! { + pub const StatemineParaIdInfo: u32 = 1000u32; + pub const StatemineAssetsInstanceInfo: u8 = 50u8; +} + +pub type AssetId = u128; +#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Encode, Decode, RuntimeDebug, TypeInfo)] +pub enum MockAssetType { + Xcm(Location), + MockAsset(AssetId), +} + +impl Default for MockAssetType { + fn default() -> Self { + Self::MockAsset(0) + } +} + +impl From for AssetId { + fn from(asset: MockAssetType) -> AssetId { + match asset { + MockAssetType::MockAsset(id) => id, + MockAssetType::Xcm(id) => { + let mut result: [u8; 16] = [0u8; 16]; + let hash: H256 = id.using_encoded(::Hashing::hash); + result.copy_from_slice(&hash.as_fixed_bytes()[0..16]); + u128::from_le_bytes(result) + }, + } + } +} + +impl From for MockAssetType { + fn from(location: Location) -> Self { + Self::Xcm(location) + } +} + +impl Into> for MockAssetType { + fn into(self) -> Option { + match self { + Self::Xcm(location) => Some(location), + _ => None, + } + } +} + +pub struct MockAssetPalletRegistrar; + +impl AssetRegistrar for MockAssetPalletRegistrar { + fn next_asset_id() -> AssetId { + 42u128 + } + + fn create_foreign_asset( + _asset: u128, + _min_balance: u64, + _metadata: u32, + _is_sufficient: bool, + ) -> Result<(), DispatchError> { + Ok(()) + } + + fn destroy_foreign_asset(_asset: u128) -> Result<(), DispatchError> { + Ok(()) + } + + fn destroy_asset_dispatch_info_weight(_asset: u128) -> Weight { + Weight::from_parts(0, 0) + } +} + +impl Config for Test { + type AssetId = u128; + type AssetRegistrar = MockAssetPalletRegistrar; + type AssetRegistrarMetadata = u32; + type Balance = u64; + type ForeignAssetModifierOrigin = EnsureRoot; + type ForeignAssetType = MockAssetType; + type RuntimeEvent = RuntimeEvent; + type WeightInfo = (); +} + +pub(crate) struct ExtBuilder { + // endowed accounts with balances + balances: Vec<(AccountId, Balance)>, +} + +impl Default for ExtBuilder { + fn default() -> ExtBuilder { + ExtBuilder { balances: vec![] } + } +} + +impl ExtBuilder { + pub(crate) fn build(self) -> sp_io::TestExternalities { + let mut t = frame_system::GenesisConfig::::default() + .build_storage() + .expect("Frame system builds valid default genesis config"); + + pallet_balances::GenesisConfig:: { balances: self.balances } + .assimilate_storage(&mut t) + .expect("Pallet balances storage can be assimilated"); + let mut ext = sp_io::TestExternalities::new(t); + ext.execute_with(|| System::set_block_number(1)); + ext + } +} + +pub(crate) fn events() -> Vec> { + System::events() + .into_iter() + .map(|r| r.event) + .filter_map(|e| if let RuntimeEvent::AssetManager(inner) = e { Some(inner) } else { None }) + .collect::>() +} + +pub fn expect_events(e: Vec>) { + assert_eq!(events(), e); +} diff --git a/pallets/asset-manager/src/tests.rs b/pallets/asset-manager/src/tests.rs new file mode 100644 index 000000000..d1d670a82 --- /dev/null +++ b/pallets/asset-manager/src/tests.rs @@ -0,0 +1,240 @@ +// Copyright 2019-2022 PureStake Inc. +// This file is part of Moonbeam. + +// Moonbeam is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Moonbeam is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Moonbeam. If not, see . + +// Tests for AssetManager Pallet +use frame_support::{assert_noop, assert_ok}; +use mock::*; + +use crate::*; + +#[test] +fn registering_foreign_works() { + ExtBuilder::default().build().execute_with(|| { + assert_ok!(AssetManager::register_foreign_asset( + RuntimeOrigin::root(), + MockAssetType::MockAsset(1), + 0u32.into(), + 1u32.into(), + true + )); + + assert_eq!(AssetManager::asset_id_type(42).unwrap(), MockAssetType::MockAsset(1)); + assert_eq!(AssetManager::asset_type_id(MockAssetType::MockAsset(1)).unwrap(), 42); + expect_events(vec![crate::Event::ForeignAssetRegistered { + asset_id: 42, + asset: MockAssetType::MockAsset(1), + metadata: 0u32, + }]) + }); +} + +#[test] +fn test_asset_exists_error() { + ExtBuilder::default().build().execute_with(|| { + assert_ok!(AssetManager::register_foreign_asset( + RuntimeOrigin::root(), + MockAssetType::MockAsset(1), + 0u32.into(), + 1u32.into(), + true + )); + + assert_eq!(AssetManager::asset_id_type(42).unwrap(), MockAssetType::MockAsset(1)); + assert_noop!( + AssetManager::register_foreign_asset( + RuntimeOrigin::root(), + MockAssetType::MockAsset(1), + 0u32.into(), + 1u32.into(), + true + ), + Error::::AssetAlreadyExists + ); + }); +} + +#[test] +fn test_regular_user_cannot_call_extrinsics() { + ExtBuilder::default().build().execute_with(|| { + assert_noop!( + AssetManager::register_foreign_asset( + RuntimeOrigin::signed(1), + MockAssetType::MockAsset(1), + 0u32.into(), + 1u32.into(), + true + ), + sp_runtime::DispatchError::BadOrigin + ); + + assert_noop!( + AssetManager::change_existing_asset_type( + RuntimeOrigin::signed(1), + 1, + MockAssetType::MockAsset(2), + 1 + ), + sp_runtime::DispatchError::BadOrigin + ); + }); +} + +#[test] +fn test_root_can_change_asset_id_type() { + ExtBuilder::default().build().execute_with(|| { + assert_ok!(AssetManager::register_foreign_asset( + RuntimeOrigin::root(), + MockAssetType::MockAsset(1), + 0u32.into(), + 1u32.into(), + true + )); + + assert_ok!(AssetManager::change_existing_asset_type( + RuntimeOrigin::root(), + 42, + MockAssetType::MockAsset(2), + 1 + )); + + // New associations are established + assert_eq!(AssetManager::asset_id_type(42).unwrap(), MockAssetType::MockAsset(2)); + assert_eq!(AssetManager::asset_type_id(MockAssetType::MockAsset(2)).unwrap(), 42); + + // Old ones are deleted + assert!(AssetManager::asset_type_id(MockAssetType::MockAsset(1)).is_none()); + + expect_events(vec![ + crate::Event::ForeignAssetRegistered { + asset_id: 42, + asset: MockAssetType::MockAsset(1), + metadata: 0, + }, + crate::Event::ForeignAssetXcmLocationChanged { + asset_id: 42, + new_asset_type: MockAssetType::MockAsset(2), + }, + ]) + }); +} + +#[test] +fn test_asset_id_non_existent_error() { + ExtBuilder::default().build().execute_with(|| { + assert_noop!( + AssetManager::change_existing_asset_type( + RuntimeOrigin::root(), + 1, + MockAssetType::MockAsset(2), + 1 + ), + Error::::AssetDoesNotExist + ); + }); +} + +#[test] +fn test_root_can_remove_asset_association() { + ExtBuilder::default().build().execute_with(|| { + assert_ok!(AssetManager::register_foreign_asset( + RuntimeOrigin::root(), + MockAssetType::MockAsset(1), + 0u32.into(), + 1u32.into(), + true + )); + + assert_ok!(AssetManager::remove_existing_asset_type(RuntimeOrigin::root(), 42, 1)); + + // Mappings are deleted + assert!(AssetManager::asset_type_id(MockAssetType::MockAsset(1)).is_none()); + assert!(AssetManager::asset_id_type(42).is_none()); + + expect_events(vec![ + crate::Event::ForeignAssetRegistered { + asset_id: 42, + asset: MockAssetType::MockAsset(1), + metadata: 0, + }, + crate::Event::ForeignAssetRemoved { + asset_id: 42, + asset_type: MockAssetType::MockAsset(1), + }, + ]) + }); +} + +#[test] +fn test_removing_without_asset_units_per_second_does_not_panic() { + ExtBuilder::default().build().execute_with(|| { + assert_ok!(AssetManager::register_foreign_asset( + RuntimeOrigin::root(), + MockAssetType::MockAsset(1), + 0u32.into(), + 1u32.into(), + true + )); + + assert_ok!(AssetManager::remove_existing_asset_type(RuntimeOrigin::root(), 42, 0)); + + // Mappings are deleted + assert!(AssetManager::asset_type_id(MockAssetType::MockAsset(1)).is_none()); + assert!(AssetManager::asset_id_type(42).is_none()); + + expect_events(vec![ + crate::Event::ForeignAssetRegistered { + asset_id: 42, + asset: MockAssetType::MockAsset(1), + metadata: 0, + }, + crate::Event::ForeignAssetRemoved { + asset_id: 42, + asset_type: MockAssetType::MockAsset(1), + }, + ]) + }); +} + +#[test] +fn test_destroy_foreign_asset_also_removes_everything() { + ExtBuilder::default().build().execute_with(|| { + assert_ok!(AssetManager::register_foreign_asset( + RuntimeOrigin::root(), + MockAssetType::MockAsset(1), + 0u32.into(), + 1u32.into(), + true + )); + + assert_ok!(AssetManager::destroy_foreign_asset(RuntimeOrigin::root(), 42, 0,)); + + // Mappings are deleted + assert!(AssetManager::asset_type_id(MockAssetType::MockAsset(1)).is_none()); + assert!(AssetManager::asset_id_type(42).is_none()); + + expect_events(vec![ + crate::Event::ForeignAssetRegistered { + asset_id: 42, + asset: MockAssetType::MockAsset(1), + metadata: 0, + }, + crate::Event::ForeignAssetDestroyed { + asset_id: 42, + asset_type: MockAssetType::MockAsset(1), + }, + ]) + }); +} diff --git a/pallets/asset-manager/src/weights.rs b/pallets/asset-manager/src/weights.rs new file mode 100644 index 000000000..5bab822ae --- /dev/null +++ b/pallets/asset-manager/src/weights.rs @@ -0,0 +1,185 @@ +// Copyright 2019-2022 PureStake Inc. +// This file is part of Moonbeam. + +// Moonbeam is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Moonbeam is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Moonbeam. If not, see . + + +//! Autogenerated weights for pallet_asset_manager +//! +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 4.0.0-dev +//! DATE: 2023-04-28, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! WORST CASE MAP SIZE: `1000000` +//! HOSTNAME: `benchmarker`, CPU: `Intel(R) Core(TM) i7-7700K CPU @ 4.20GHz` +//! EXECUTION: Some(Wasm), WASM-EXECUTION: Compiled, CHAIN: None, DB CACHE: 1024 + +// Executed Command: +// ./target/release/moonbeam +// benchmark +// pallet +// --execution=wasm +// --wasm-execution=compiled +// --pallet +// * +// --extrinsic +// * +// --steps +// 50 +// --repeat +// 20 +// --template=./benchmarking/frame-weight-template.hbs +// --json-file +// raw.json +// --output +// weights/ + +#![cfg_attr(rustfmt, rustfmt_skip)] +#![allow(unused_parens)] +#![allow(unused_imports)] + +use frame_support::{traits::Get, weights::{Weight, constants::RocksDbWeight}}; +use sp_std::marker::PhantomData; + +/// Weight functions needed for pallet_asset_manager. +pub trait WeightInfo { + fn register_foreign_asset() -> Weight; + fn change_existing_asset_type() -> Weight; + fn remove_existing_asset_type() -> Weight; +} + +/// Weights for pallet_asset_manager using the Substrate node and recommended hardware. +pub struct SubstrateWeight(PhantomData); +impl WeightInfo for SubstrateWeight { + /// Storage: AssetManager AssetIdType (r:1 w:1) + /// Proof Skipped: AssetManager AssetIdType (max_values: None, max_size: None, mode: Measured) + /// Storage: Assets Asset (r:1 w:1) + /// Proof: Assets Asset (max_values: None, max_size: Some(174), added: 2649, mode: MaxEncodedLen) + /// Storage: Assets Metadata (r:1 w:1) + /// Proof: Assets Metadata (max_values: None, max_size: Some(152), added: 2627, mode: MaxEncodedLen) + /// Storage: AssetManager AssetTypeId (r:0 w:1) + /// Proof Skipped: AssetManager AssetTypeId (max_values: None, max_size: None, mode: Measured) + fn register_foreign_asset() -> Weight { + // Proof Size summary in bytes: + // Measured: `82` + // Estimated: `10885` + // Minimum execution time: 51_631_000 picoseconds. + Weight::from_parts(52_681_000, 10885) + .saturating_add(T::DbWeight::get().reads(3_u64)) + .saturating_add(T::DbWeight::get().writes(4_u64)) + } + /// Storage: AssetManager SupportedFeePaymentAssets (r:1 w:1) + /// Proof Skipped: AssetManager SupportedFeePaymentAssets (max_values: Some(1), max_size: None, mode: Measured) + /// Storage: AssetManager AssetIdType (r:1 w:1) + /// Proof Skipped: AssetManager AssetIdType (max_values: None, max_size: None, mode: Measured) + /// Storage: AssetManager AssetTypeUnitsPerSecond (r:1 w:2) + /// Proof Skipped: AssetManager AssetTypeUnitsPerSecond (max_values: None, max_size: None, mode: Measured) + /// Storage: AssetManager AssetTypeId (r:0 w:2) + /// Proof Skipped: AssetManager AssetTypeId (max_values: None, max_size: None, mode: Measured) + /// The range of component `x` is `[5, 100]`. + fn change_existing_asset_type() -> Weight { + // Proof Size summary in bytes: + // Measured: `926 + x * (13 ±0)` + // Estimated: `11791 + x * (60 ±0)` + // Minimum execution time: 42_959_000 picoseconds. + Weight::from_parts(43_255_055, 11791) + // Standard Error: 3_394 + .saturating_add(Weight::from_parts(543_897, 0)) + .saturating_add(T::DbWeight::get().reads(3_u64)) + .saturating_add(T::DbWeight::get().writes(6_u64)) + .saturating_add(Weight::from_parts(0, 60)) + } + /// Storage: AssetManager SupportedFeePaymentAssets (r:1 w:1) + /// Proof Skipped: AssetManager SupportedFeePaymentAssets (max_values: Some(1), max_size: None, mode: Measured) + /// Storage: AssetManager AssetIdType (r:1 w:1) + /// Proof Skipped: AssetManager AssetIdType (max_values: None, max_size: None, mode: Measured) + /// Storage: AssetManager AssetTypeUnitsPerSecond (r:0 w:1) + /// Proof Skipped: AssetManager AssetTypeUnitsPerSecond (max_values: None, max_size: None, mode: Measured) + /// Storage: AssetManager AssetTypeId (r:0 w:1) + /// Proof Skipped: AssetManager AssetTypeId (max_values: None, max_size: None, mode: Measured) + /// The range of component `x` is `[5, 100]`. + fn remove_existing_asset_type() -> Weight { + // Proof Size summary in bytes: + // Measured: `482 + x * (10 ±0)` + // Estimated: `6910 + x * (40 ±0)` + // Minimum execution time: 32_960_000 picoseconds. + Weight::from_parts(33_257_599, 6910) + // Standard Error: 2_430 + .saturating_add(Weight::from_parts(421_651, 0)) + .saturating_add(T::DbWeight::get().reads(2_u64)) + .saturating_add(T::DbWeight::get().writes(4_u64)) + .saturating_add(Weight::from_parts(0, 40)) + } +} + +// For backwards compatibility and tests +impl WeightInfo for () { + /// Storage: AssetManager AssetIdType (r:1 w:1) + /// Proof Skipped: AssetManager AssetIdType (max_values: None, max_size: None, mode: Measured) + /// Storage: Assets Asset (r:1 w:1) + /// Proof: Assets Asset (max_values: None, max_size: Some(174), added: 2649, mode: MaxEncodedLen) + /// Storage: Assets Metadata (r:1 w:1) + /// Proof: Assets Metadata (max_values: None, max_size: Some(152), added: 2627, mode: MaxEncodedLen) + /// Storage: AssetManager AssetTypeId (r:0 w:1) + /// Proof Skipped: AssetManager AssetTypeId (max_values: None, max_size: None, mode: Measured) + fn register_foreign_asset() -> Weight { + // Proof Size summary in bytes: + // Measured: `82` + // Estimated: `10885` + // Minimum execution time: 51_631_000 picoseconds. + Weight::from_parts(52_681_000, 10885) + .saturating_add(RocksDbWeight::get().reads(3_u64)) + .saturating_add(RocksDbWeight::get().writes(4_u64)) + } + /// Storage: AssetManager SupportedFeePaymentAssets (r:1 w:1) + /// Proof Skipped: AssetManager SupportedFeePaymentAssets (max_values: Some(1), max_size: None, mode: Measured) + /// Storage: AssetManager AssetIdType (r:1 w:1) + /// Proof Skipped: AssetManager AssetIdType (max_values: None, max_size: None, mode: Measured) + /// Storage: AssetManager AssetTypeUnitsPerSecond (r:1 w:2) + /// Proof Skipped: AssetManager AssetTypeUnitsPerSecond (max_values: None, max_size: None, mode: Measured) + /// Storage: AssetManager AssetTypeId (r:0 w:2) + /// Proof Skipped: AssetManager AssetTypeId (max_values: None, max_size: None, mode: Measured) + /// The range of component `x` is `[5, 100]`. + fn change_existing_asset_type() -> Weight { + // Proof Size summary in bytes: + // Measured: `926 + x * (13 ±0)` + // Estimated: `11791 + x * (60 ±0)` + // Minimum execution time: 42_959_000 picoseconds. + Weight::from_parts(43_255_055, 11791) + // Standard Error: 3_394 + .saturating_add(Weight::from_parts(543_897, 0)) + .saturating_add(RocksDbWeight::get().reads(3_u64)) + .saturating_add(RocksDbWeight::get().writes(6_u64)) + .saturating_add(Weight::from_parts(0, 60)) + } + /// Storage: AssetManager SupportedFeePaymentAssets (r:1 w:1) + /// Proof Skipped: AssetManager SupportedFeePaymentAssets (max_values: Some(1), max_size: None, mode: Measured) + /// Storage: AssetManager AssetIdType (r:1 w:1) + /// Proof Skipped: AssetManager AssetIdType (max_values: None, max_size: None, mode: Measured) + /// Storage: AssetManager AssetTypeUnitsPerSecond (r:0 w:1) + /// Proof Skipped: AssetManager AssetTypeUnitsPerSecond (max_values: None, max_size: None, mode: Measured) + /// Storage: AssetManager AssetTypeId (r:0 w:1) + /// Proof Skipped: AssetManager AssetTypeId (max_values: None, max_size: None, mode: Measured) + /// The range of component `x` is `[5, 100]`. + fn remove_existing_asset_type() -> Weight { + // Proof Size summary in bytes: + // Measured: `482 + x * (10 ±0)` + // Estimated: `6910 + x * (40 ±0)` + // Minimum execution time: 32_960_000 picoseconds. + Weight::from_parts(33_257_599, 6910) + // Standard Error: 2_430 + .saturating_add(Weight::from_parts(421_651, 0)) + .saturating_add(RocksDbWeight::get().reads(2_u64)) + .saturating_add(RocksDbWeight::get().writes(4_u64)) + .saturating_add(Weight::from_parts(0, 40)) + } +} \ No newline at end of file diff --git a/pallets/asset-manager/src/xcm_primitives.rs b/pallets/asset-manager/src/xcm_primitives.rs new file mode 100644 index 000000000..201efebf8 --- /dev/null +++ b/pallets/asset-manager/src/xcm_primitives.rs @@ -0,0 +1,95 @@ +// Copyright 2019-2022 PureStake Inc. +// This file is part of Moonbeam. + +// Moonbeam is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Moonbeam is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Moonbeam. If not, see . + +use sp_runtime::traits::MaybeEquivalence; +use sp_std::marker::PhantomData; +use xcm::v5::Location; +use xcm_executor::traits::ConvertLocation; + +/// Converter struct implementing `AssetIdConversion` converting a numeric asset ID +/// (must be `TryFrom/TryInto`) into a Location Value and vice versa through +/// an intermediate generic type AssetType. +/// The trait bounds enforce is that the AssetTypeGetter trait is also implemented for +/// AssetIdInfoGetter +pub struct AsAssetType(PhantomData<(AssetId, AssetIdInfoGetter)>); +impl MaybeEquivalence + for AsAssetType +where + AssetId: Clone, + // AssetType: From + Into> + Clone, + AssetIdInfoGetter: AssetTypeGetter, +{ + fn convert(id: &Location) -> Option { + AssetIdInfoGetter::get_asset_id(id.clone().into()) + } + + fn convert_back(what: &AssetId) -> Option { + AssetIdInfoGetter::get_asset_type(what.clone()).and_then(Into::into) + } +} + +// impl MaybeEquivalence +// for AsAssetType +// where +// AssetId: Clone, +// AssetType: From + Into> + Clone, +// AssetIdInfoGetter: AssetTypeGetter, +// { +// fn convert(id: &xcm::v4::Location) -> Option { +// let v3_location = +// xcm_builder::WithLatestLocationConverter::::convert(id)?; +// AssetIdInfoGetter::get_asset_id(v3_location.clone().into()) +// } +// +// fn convert_back(what: &AssetId) -> Option { +// let v3_location: Location = +// AssetIdInfoGetter::get_asset_type(what.clone()).and_then(Into::into)?; +// xcm_builder::WithLatestLocationConverter::convert_back(&v3_location) +// } +// } + +impl ConvertLocation + for AsAssetType +where + AssetId: Clone, + // AssetType: From + Into> + Clone, + AssetIdInfoGetter: AssetTypeGetter, +{ + fn convert_location(id: &Location) -> Option { + let v5_location = xcm_builder::WithLatestLocationConverter::::convert(id)?; + AssetIdInfoGetter::get_asset_id(v5_location.clone().into()) + } +} + +/// Defines the trait to obtain a generic AssetType from a generic AssetId and vice versa +pub trait AssetTypeGetter { + // Get asset type from assetId + fn get_asset_type(asset_id: AssetId) -> Option; + + // Get assetId from assetType + fn get_asset_id(asset_type: AssetType) -> Option; + + // Set assetId and assetType + #[cfg(feature = "runtime-benchmarks")] + fn set_asset_type_asset_id(asset_type: AssetType, asset_id: AssetId); +} + +/// This trait ensure we can convert AccountIds to CurrencyIds +/// We will require Runtime to have this trait implemented +pub trait AccountIdToCurrencyId { + // Get assetId from account + fn account_to_currency_id(account: Account) -> Option; +} diff --git a/pop-api/Cargo.toml b/pop-api/Cargo.toml index e3597236a..80962774f 100644 --- a/pop-api/Cargo.toml +++ b/pop-api/Cargo.toml @@ -21,6 +21,7 @@ sp-io = { version = "38.0.0", default-features = false, features = [ # ink! dependencies. ink = { version = "5.1.0", default-features = false } +#ink = { git = "https://github.com/use-ink/ink", branch = "xcm", default-features = false } [dev-dependencies] pallet-nfts = { path = "../pallets/nfts" } diff --git a/pop-api/examples/foreign_fungibles/Cargo.toml b/pop-api/examples/foreign_fungibles/Cargo.toml new file mode 100644 index 000000000..130e535d5 --- /dev/null +++ b/pop-api/examples/foreign_fungibles/Cargo.toml @@ -0,0 +1,28 @@ +[package] +authors = [ "R0GUE " ] +edition = "2021" +name = "foreign_fungibles" +version = "0.1.0" + +[dependencies] +ink = { version = "5.1.0", default-features = false, features = [ "ink-debug" ] } +#ink = { git = "https://github.com/use-ink/ink", branch = "xcm", default-features = false } +pop-api = { path = "../../../pop-api", default-features = false, features = [ + "fungibles", +] } + +[dev-dependencies] +env_logger = { version = "0.11.3" } +serde_json = "1.0.114" + +[lib] +path = "lib.rs" + +[features] +default = [ "std" ] +e2e-tests = [ ] +ink-as-dependency = [ ] +std = [ + "ink/std", + "pop-api/std", +] diff --git a/pop-api/examples/foreign_fungibles/lib.rs b/pop-api/examples/foreign_fungibles/lib.rs new file mode 100644 index 000000000..a1bc6996c --- /dev/null +++ b/pop-api/examples/foreign_fungibles/lib.rs @@ -0,0 +1,109 @@ +#![cfg_attr(not(feature = "std"), no_std, no_main)] + +use ink::{prelude::vec, xcm::prelude::*}; +use pop_api::{ + primitives::{AccountId, TokenId}, + v0::fungibles as api, +}; + +#[cfg(test)] +mod tests; + +#[ink::contract] +mod fungibles { + use ink::env::Error as EnvError; + + use super::*; + + #[ink(storage)] + pub struct Fungible { + id: TokenId, + } + + #[derive(Debug, PartialEq, Eq)] + #[ink::scale_derive(Encode, Decode, TypeInfo)] + pub enum Error { + TransferFailed, + AHTransferFailed, + } + + // Constants for USDC and Asset Hub + const ASSET_HUB_PARA_ID: u32 = 1000; + const ASSET_HUB_ASSETS_PALLET: u8 = 50; + const USDC_ASSET_ID_ON_AH: u128 = 1337; + const USDC_ASSET_ID_ON_POP: TokenId = 1; + + impl Fungible { + #[ink(constructor, payable)] + pub fn new() -> Self { + Self { id: USDC_ASSET_ID_ON_POP } + } + + #[ink(message)] + pub fn ah_transfer( + &mut self, + to: AccountId, + value: Balance, + fee: Balance, + ) -> Result<(), Error> { + // Define the destination (Asset Hub) + let destination = Location::new(1, [Parachain(ASSET_HUB_PARA_ID)]); + + // Define the beneficiary (the 'to' account on Asset Hub) + let beneficiary = Location::new(0, [AccountId32 { network: None, id: to.0.into() }]); + + // Define USDC asset location as seen from Pop Network (foreign asset from Asset Hub) + let usdc_location_on_pop = Location::new( + 1, + [ + Parachain(ASSET_HUB_PARA_ID), + PalletInstance(ASSET_HUB_ASSETS_PALLET), + GeneralIndex(USDC_ASSET_ID_ON_AH), + ], + ); + let usdc_asset_on_pop: Asset = (AssetId(usdc_location_on_pop.clone()), value).into(); + + // Define USDC asset location as seen from Asset Hub (local asset) + let usdc_location_on_ah = Location::new( + 0, + [PalletInstance(ASSET_HUB_ASSETS_PALLET), GeneralIndex(USDC_ASSET_ID_ON_AH)], + ); + let usdc_asset_on_ah: Asset = (AssetId(usdc_location_on_ah.clone()), value).into(); + + let fee_asset: Asset = (AssetId(Location::parent()), fee).into(); + + // XCM instructions to be executed on Asset Hub + let xcm_on_destination = Xcm(vec![ + // Buy execution with the DOT + BuyExecution { fees: fee_asset.clone(), weight_limit: WeightLimit::Unlimited }, + // Deposit USDC to the beneficiary + DepositAsset { assets: Wild(All.into()), beneficiary: beneficiary.clone() }, + ]); + + // Construct the full XCM message from Pop Network + let message: Xcm<()> = Xcm(vec![ + // Withdraw the USDC and DOT. + WithdrawAsset((vec![usdc_asset_on_pop.clone(), fee_asset.clone()]).into()), + // Initiate the reserve-based transfer to Asset Hub + InitiateReserveWithdraw { + assets: vec![usdc_asset_on_pop.clone(), fee_asset].into(), + reserve: destination.clone(), + xcm: xcm_on_destination, + }, + ]); + + // Execute the XCM message + self.env() + .xcm_execute(&VersionedXcm::V4(message)) + .map_err(|_| Error::AHTransferFailed)?; + + Ok(()) + } + + #[ink(message)] + pub fn transfer(&mut self, to: AccountId, value: Balance) -> Result<(), Error> { + api::transfer(self.id, to, value).map_err(|_| Error::TransferFailed)?; + Ok(()) + } + } +} diff --git a/runtime/devnet/Cargo.toml b/runtime/devnet/Cargo.toml index 41e875184..dd3f93ef0 100644 --- a/runtime/devnet/Cargo.toml +++ b/runtime/devnet/Cargo.toml @@ -25,6 +25,7 @@ smallvec.workspace = true # Local pallet-api.workspace = true +pallet-asset-manager.workspace = true pallet-nfts.workspace = true pop-chain-extension.workspace = true pop-primitives.workspace = true @@ -104,6 +105,7 @@ pallet-ismp-runtime-api.workspace = true enumflags2.workspace = true env_logger.workspace = true hex.workspace = true +sp-keyring.workspace = true [features] default = [ "std" ] @@ -132,6 +134,7 @@ std = [ "ismp/std", "log/std", "pallet-api/std", + "pallet-asset-manager/std", "pallet-assets/std", "pallet-aura/std", "pallet-authorship/std", diff --git a/runtime/devnet/src/config/assets.rs b/runtime/devnet/src/config/assets.rs index 704cf4c37..0f8a58cc2 100644 --- a/runtime/devnet/src/config/assets.rs +++ b/runtime/devnet/src/config/assets.rs @@ -1,3 +1,4 @@ +use codec::{Decode, Encode}; use frame_support::{ pallet_prelude::Get, parameter_types, @@ -7,11 +8,12 @@ use frame_support::{ use frame_system::{EnsureRoot, EnsureSigned}; use pallet_nfts::PalletFeatures; use parachains_common::{AssetIdForTrustBackedAssets, CollectionId, ItemId, Signature}; -use sp_runtime::traits::Verify; +use sp_runtime::traits::{StaticLookup, Verify}; use crate::{ - deposit, AccountId, Assets, Balance, Balances, BlockNumber, Nfts, Runtime, RuntimeEvent, - RuntimeHoldReason, DAYS, EXISTENTIAL_DEPOSIT, UNIT, + deposit, AccountId, AssetManager, Assets, Balance, Balances, BlockNumber, Nfts, Runtime, + RuntimeCall, RuntimeEvent, RuntimeHoldReason, RuntimeOrigin, Vec, Weight, DAYS, + EXISTENTIAL_DEPOSIT, UNIT, }; /// We allow root to execute privileged asset operations. @@ -132,7 +134,7 @@ impl pallet_assets::Config for Runtime { type Balance = Balance; #[cfg(feature = "runtime-benchmarks")] type BenchmarkHelper = (); - type CallbackHandle = (); + type CallbackHandle = pallet_assets::AutoIncAssetId; type CreateOrigin = AsEnsureOriginWithArg>; type Currency = Balances; type Extra = (); @@ -146,11 +148,152 @@ impl pallet_assets::Config for Runtime { type WeightInfo = pallet_assets::weights::SubstrateWeight; } +pub struct AssetRegistrar; +use frame_support::{dispatch::GetDispatchInfo, pallet_prelude::DispatchResult, transactional}; + +impl pallet_asset_manager::AssetRegistrar for AssetRegistrar { + fn next_asset_id() -> AssetIdForTrustBackedAssets { + pallet_assets::NextAssetId::::get().unwrap() + } + + #[transactional] + fn create_foreign_asset( + asset: AssetIdForTrustBackedAssets, + min_balance: Balance, + metadata: AssetRegistrarMetadata, + is_sufficient: bool, + ) -> DispatchResult { + // Create the asset. Unlike `create`, no funds are reserved. + Assets::force_create( + RuntimeOrigin::root(), + asset.into(), + // Asset manager pallet is the owner of the asset. This means changes to the asset can + // only be made through this pallet. + ::Lookup::unlookup(AssetManager::account_id()), + is_sufficient, + min_balance, + )?; + // Set metadata for created asset. Deposit is left alone, meaning that also no deposit will + // be taken unless metadata was already set for this asset which in this case wouldn't be + // the case. + Assets::force_set_metadata( + RuntimeOrigin::root(), + asset.into(), + metadata.name, + metadata.symbol, + metadata.decimals, + metadata.is_frozen, + ) + } + + #[transactional] + fn destroy_foreign_asset(asset: AssetIdForTrustBackedAssets) -> DispatchResult { + Assets::start_destroy(RuntimeOrigin::root(), asset.into()) + } + + fn destroy_asset_dispatch_info_weight(asset: AssetIdForTrustBackedAssets) -> Weight { + use pallet_assets::WeightInfo; + >::WeightInfo::start_destroy() + } +} + +use frame_support::pallet_prelude::TypeInfo; +#[derive(Clone, Default, Eq, Debug, PartialEq, Ord, PartialOrd, Encode, Decode, TypeInfo)] +pub struct AssetRegistrarMetadata { + pub name: Vec, + pub symbol: Vec, + pub decimals: u8, + pub is_frozen: bool, +} + +impl pallet_asset_manager::Config for Runtime { + type AssetId = >::AssetId; + type AssetRegistrar = AssetRegistrar; + type AssetRegistrarMetadata = AssetRegistrarMetadata; + type Balance = Balance; + type ForeignAssetModifierOrigin = EnsureRoot; + type ForeignAssetType = xcm::v5::Location; + type RuntimeEvent = RuntimeEvent; + type WeightInfo = pallet_asset_manager::weights::SubstrateWeight; +} + #[cfg(test)] mod tests { - use frame_support::traits::StorageInfoTrait; + use frame_support::{ + assert_ok, + traits::{fungibles::Inspect, StorageInfoTrait}, + }; + use sp_keyring::AccountKeyring as Keyring; + use sp_runtime::BuildStorage; use super::*; + use crate::{config::xcm::AssetHub, ExistentialDeposit, System}; + + fn new_test_ext() -> sp_io::TestExternalities { + let initial_balance = 100_000_000 * UNIT; + let mut t = frame_system::GenesisConfig::::default().build_storage().unwrap(); + pallet_balances::GenesisConfig:: { + balances: vec![(Keyring::Alice.to_account_id(), initial_balance)], + } + .assimilate_storage(&mut t) + .unwrap(); + pallet_assets::GenesisConfig:: { + next_asset_id: Some(1), + ..Default::default() + } + .assimilate_storage(&mut t) + .unwrap(); + let mut ext = sp_io::TestExternalities::new(t); + ext.execute_with(|| System::set_block_number(1)); + ext + } + + #[test] + fn test_foreign_asset_creation() { + new_test_ext().execute_with(|| { + let metadata = AssetRegistrarMetadata { + name: "DOT".encode(), + symbol: "DOT".encode(), + decimals: 10, + is_frozen: false, + }; + // Obtain the next asset id from pallet assets. + let asset_id = + pallet_assets::NextAssetId::::get().unwrap(); + // Register a foreign asset in the asset manager pallet. + assert_ok!(AssetManager::register_foreign_asset( + RuntimeOrigin::root(), + xcm::v5::Parent.into(), + metadata, + ExistentialDeposit::get(), + true + )); + // Foreign asset has been created with the next asset id queried. + assert!(Assets::asset_exists(asset_id)); + // Next asset id is incremented for the next foreign asset created. + let next_asset_id = + pallet_assets::NextAssetId::::get().unwrap(); + assert_eq!(next_asset_id, asset_id + 1); + let metadata = AssetRegistrarMetadata { + name: "AH".encode(), + symbol: "AH".encode(), + decimals: 12, + is_frozen: false, + }; + assert_ok!(AssetManager::register_foreign_asset( + RuntimeOrigin::root(), + AssetHub::get(), + metadata, + ExistentialDeposit::get(), + true + )); + assert!(Assets::asset_exists(next_asset_id)); + + // There is no way to modify the assets metadata, mint or burn tokens or any other + // action with owner privileges because the asset manager pallet is the owner and + // doesn't have this functionality. + }); + } #[test] fn ensure_account_balance_deposit() { diff --git a/runtime/devnet/src/config/xcm.rs b/runtime/devnet/src/config/xcm.rs index 555ec4ad6..01458db5f 100644 --- a/runtime/devnet/src/config/xcm.rs +++ b/runtime/devnet/src/config/xcm.rs @@ -7,24 +7,30 @@ use frame_support::{ }; use frame_system::EnsureRoot; use pallet_xcm::XcmPassthrough; +use parachains_common::{AssetIdForTrustBackedAssets, Balance}; use polkadot_parachain_primitives::primitives::Sibling; use polkadot_runtime_common::impls::ToAuthor; -use xcm::latest::prelude::*; +use xcm::{latest::prelude::*, VersionedLocation}; use xcm_builder::{ AccountId32Aliases, AllowExplicitUnpaidExecutionFrom, AllowKnownQueryResponses, - AllowTopLevelPaidExecutionFrom, EnsureXcmOrigin, FixedWeightBounds, - FrameTransactionalProcessor, FungibleAdapter, IsConcrete, ParentIsPreset, RelayChainAsNative, - SiblingParachainAsNative, SiblingParachainConvertsVia, SignedAccountId32AsNative, - SignedToAccountId32, SovereignSignedViaLocation, TakeWeightCredit, TrailingSetTopicAsId, - UsingComponents, WithComputedOrigin, WithUniqueTopic, + AllowTopLevelPaidExecutionFrom, ConvertedConcreteId, EnsureXcmOrigin, FixedWeightBounds, + FrameTransactionalProcessor, FungibleAdapter, FungiblesAdapter, IsConcrete, NoChecking, + ParentIsPreset, RelayChainAsNative, SiblingParachainAsNative, SiblingParachainConvertsVia, + SignedAccountId32AsNative, SignedToAccountId32, SovereignSignedViaLocation, TakeWeightCredit, + TrailingSetTopicAsId, UsingComponents, WithComputedOrigin, WithUniqueTopic, }; -use xcm_executor::XcmExecutor; +use xcm_executor::{traits::JustTry, XcmExecutor}; use crate::{ - AccountId, AllPalletsWithSystem, Balances, ParachainInfo, ParachainSystem, PolkadotXcm, - Runtime, RuntimeCall, RuntimeEvent, RuntimeOrigin, WeightToFee, XcmpQueue, + AccountId, AllPalletsWithSystem, AssetManager, Assets, Balances, ParachainInfo, + ParachainSystem, PolkadotXcm, Runtime, RuntimeCall, RuntimeEvent, RuntimeOrigin, WeightToFee, + XcmpQueue, }; +pub const ASSET_HUB: Junction = Parachain(1000); +/// Assets on Asset Hub. +pub const ASSET_HUB_ASSETS: Junction = PalletInstance(50); + parameter_types! { pub const RelayLocation: Location = Location::parent(); pub AssetHub: Location = Location::new(1, [Parachain(1000)]); @@ -34,6 +40,8 @@ parameter_types! { // For the real deployment, it is recommended to set `RelayNetwork` according to the relay chain // and prepend `UniversalLocation` with `GlobalConsensus(RelayNetwork::get())`. pub UniversalLocation: InteriorLocation = Parachain(ParachainInfo::parachain_id().into()).into(); + pub CheckAccount: AccountId = crate::PolkadotXcm::check_account(); + pub USDCAssetReserve: Location = Location::new(1, [ASSET_HUB, ASSET_HUB_ASSETS, GeneralIndex(1337)]); } /// Type for specifying how a `Location` can be converted into an `AccountId`. This is used @@ -63,6 +71,33 @@ pub type LocalAssetTransactor = FungibleAdapter< (), >; +// The non-reserve fungible transactor type +// It will use pallet-assets, and the Id will be matched against AsAssetType +pub type ForeignFungiblesTransactor = FungiblesAdapter< + // Use this fungibles implementation: + crate::Assets, + // Use this currency when it is a fungible asset matching the given location or name: + ( + ConvertedConcreteId< + AssetIdForTrustBackedAssets, + Balance, + pallet_asset_manager::xcm_primitives::AsAssetType< + AssetIdForTrustBackedAssets, + AssetManager, + >, + JustTry, + >, + ), + // Do a simple punn to convert an AccountId32 Location into a native chain account ID: + LocationToAccountId, + // Our chain's account ID type (we can't get away without mentioning it explicitly): + AccountId, + // We dont allow teleports. + NoChecking, + // We dont track any teleports + CheckAccount, +>; + /// This is the type we use to convert an (incoming) XCM origin into a local `Origin` instance, /// ready for dispatching a transaction with Xcm's `Transact`. There is an `OriginKind` which can /// biases the kind of local `Origin` it will become. @@ -142,8 +177,20 @@ impl ContainsPair for NativeAssetExceptRelay { } } +/// Asset filter that allows specific asset if coming from a certain location. +pub struct AssetFrom(PhantomData<(Asset, Origin)>); +impl, Origin: Get> ContainsPair + for AssetFrom +{ + fn contains(asset: &xcm::prelude::Asset, origin: &Location) -> bool { + log::trace!(target: "xcm::contains", "AssetFrom asset: {:?}, origin: {:?}", asset, origin); + *origin == Origin::get() && matches!(asset.id.clone(), AssetId(id) if id == Asset::get()) + } +} + /// Combinations of (Asset, Location) pairs which we trust as reserves. -pub type TrustedReserves = (NativeAssetFrom, NativeAssetExceptRelay); +pub type TrustedReserves = + (NativeAssetFrom, NativeAssetExceptRelay, AssetFrom); pub struct XcmConfig; impl xcm_executor::Config for XcmConfig { @@ -152,7 +199,7 @@ impl xcm_executor::Config for XcmConfig { type AssetExchanger = (); type AssetLocker = (); // How to withdraw and deposit an asset. - type AssetTransactor = LocalAssetTransactor; + type AssetTransactor = (LocalAssetTransactor, ForeignFungiblesTransactor); type AssetTrap = PolkadotXcm; type Barrier = Barrier; type CallDispatcher = RuntimeCall; diff --git a/runtime/devnet/src/lib.rs b/runtime/devnet/src/lib.rs index aca3c865a..d1ab10df0 100644 --- a/runtime/devnet/src/lib.rs +++ b/runtime/devnet/src/lib.rs @@ -648,6 +648,8 @@ mod runtime { pub type NftFractionalization = pallet_nft_fractionalization::Pallet; #[runtime::pallet_index(52)] pub type Assets = pallet_assets::Pallet; + #[runtime::pallet_index(53)] + pub type AssetManager = pallet_asset_manager::Pallet; // Pop API #[runtime::pallet_index(150)]