diff --git a/Cargo.lock b/Cargo.lock index a4125a1157..0836d06b84 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -626,10 +626,12 @@ dependencies = [ "bifrost-salp", "bifrost-salp-lite", "bifrost-salp-rpc-runtime-api", + "bifrost-slp", "bifrost-token-issuer", "bifrost-vesting", "bifrost-vsbond-auction", "bifrost-vstoken-conversion", + "bifrost-vtoken-minting", "cumulus-pallet-aura-ext", "cumulus-pallet-dmp-queue", "cumulus-pallet-parachain-system", @@ -982,6 +984,34 @@ dependencies = [ "sp-api", ] +[[package]] +name = "bifrost-slp" +version = "0.8.0" +dependencies = [ + "bifrost-vtoken-minting", + "cumulus-primitives-core", + "frame-benchmarking", + "frame-support", + "frame-system", + "hex-literal", + "node-primitives", + "orml-currencies", + "orml-tokens", + "orml-traits", + "pallet-balances", + "pallet-utility", + "pallet-xcm", + "parity-scale-codec", + "scale-info", + "serde", + "sp-arithmetic", + "sp-core", + "sp-io", + "sp-runtime", + "sp-std", + "xcm", +] + [[package]] name = "bifrost-token-issuer" version = "0.8.0" @@ -1060,6 +1090,28 @@ dependencies = [ "sp-std", ] +[[package]] +name = "bifrost-vtoken-minting" +version = "0.8.0" +dependencies = [ + "frame-benchmarking", + "frame-support", + "frame-system", + "hex-literal", + "log", + "node-primitives", + "orml-currencies", + "orml-tokens", + "orml-traits", + "pallet-balances", + "parity-scale-codec", + "scale-info", + "sp-core", + "sp-io", + "sp-runtime", + "sp-std", +] + [[package]] name = "bimap" version = "0.6.2" @@ -5578,7 +5630,7 @@ dependencies = [ [[package]] name = "node-cli" -version = "0.9.32" +version = "0.9.40" dependencies = [ "clap", "cumulus-client-cli", @@ -9141,8 +9193,10 @@ dependencies = [ "bifrost-runtime-common", "bifrost-salp", "bifrost-salp-rpc-runtime-api", + "bifrost-slp", "bifrost-vesting", "bifrost-vsbond-auction", + "bifrost-vtoken-minting", "cumulus-pallet-aura-ext", "cumulus-pallet-dmp-queue", "cumulus-pallet-parachain-system", @@ -9186,6 +9240,7 @@ dependencies = [ "pallet-proxy", "pallet-scheduler", "pallet-session", + "pallet-staking", "pallet-sudo", "pallet-timestamp", "pallet-tips", diff --git a/Cargo.toml b/Cargo.toml index 4659695305..36a6b48ae1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,6 +16,8 @@ members = [ "pallets/vsbond-auction", "pallets/call-switchgear", "pallets/xcm-interface", + "pallets/slp", + "pallets/vtoken-minting", "pallets/vstoken-conversion", "runtime/bifrost-kusama", "runtime/bifrost-polkadot", @@ -93,12 +95,12 @@ yamux = { opt-level = 3 } zeroize = { opt-level = 3 } [patch.crates-io] -zenlink-protocol = { git = "https://github.com/zenlinkpro/Zenlink-DEX-Module", rev = "30486888d4ae2621b87ab025fc16e47aad800f67"} +zenlink-protocol = { git = "https://github.com/zenlinkpro/Zenlink-DEX-Module", rev = "30486888d4ae2621b87ab025fc16e47aad800f67" } zenlink-protocol-rpc = { git = "https://github.com/zenlinkpro/Zenlink-DEX-Module", rev = "30486888d4ae2621b87ab025fc16e47aad800f67" } zenlink-protocol-runtime-api = { git = "https://github.com/zenlinkpro/Zenlink-DEX-Module", rev = "30486888d4ae2621b87ab025fc16e47aad800f67" } merkle-distributor = { git = "https://github.com/zenlinkpro/merkle-distributor", rev = "72b1b16ae98b33bfaf14489f089302049d167130" } orml-traits = { git = "https://github.com/open-web3-stack/open-runtime-module-library", rev = "2b5d4ce1d08fb54c0007c2055653892d2c93a92e" } -orml-currencies = {git = "https://github.com/open-web3-stack/open-runtime-module-library", rev = "2b5d4ce1d08fb54c0007c2055653892d2c93a92e" } +orml-currencies = { git = "https://github.com/open-web3-stack/open-runtime-module-library", rev = "2b5d4ce1d08fb54c0007c2055653892d2c93a92e" } orml-tokens = { git = "https://github.com/open-web3-stack/open-runtime-module-library", rev = "2b5d4ce1d08fb54c0007c2055653892d2c93a92e" } orml-xtokens = { git = "https://github.com/open-web3-stack/open-runtime-module-library", rev = "2b5d4ce1d08fb54c0007c2055653892d2c93a92e" } orml-unknown-tokens = { git = "https://github.com/open-web3-stack/open-runtime-module-library", rev = "2b5d4ce1d08fb54c0007c2055653892d2c93a92e" } diff --git a/integration-tests/Cargo.toml b/integration-tests/Cargo.toml index bc05f88836..dbe5f1c177 100644 --- a/integration-tests/Cargo.toml +++ b/integration-tests/Cargo.toml @@ -6,12 +6,15 @@ edition = "2021" [dependencies] # third-party dependencies -codec = { package = "parity-scale-codec", version = "3.0.0", default-features = false, features = ["derive", "max-encoded-len"] } +codec = { package = "parity-scale-codec", version = "3.0.0", default-features = false, features = [ + "derive", + "max-encoded-len", +] } log = { version = "0.4.16", default-features = false } serde = { version = "1.0.124", optional = true } static_assertions = "1.1.0" hex = { version = "0.4", default-features = false, optional = true } -hex-literal = { version = "0.3.4"} +hex-literal = { version = "0.3.4" } smallvec = "1.7.0" # primitives @@ -27,12 +30,12 @@ sp-runtime = { git = "https://github.com/paritytech/substrate", branch = "polkad sp-session = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.18", default-features = false } sp-transaction-pool = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.18", default-features = false } sp-version = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.18", default-features = false } -sp-consensus-aura = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.18", default-features = false } +sp-consensus-aura = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.18", default-features = false } sp-arithmetic = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.18", default-features = false } # frame dependencies frame-benchmarking = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.18", default-features = false, optional = true } -frame-try-runtime = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.18", default-features = false, optional = true } +frame-try-runtime = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.18", default-features = false, optional = true } frame-executive = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.18", default-features = false } frame-support = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.18", default-features = false } frame-system = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.18", default-features = false } @@ -57,6 +60,7 @@ pallet-transaction-payment = { git = "https://github.com/paritytech/substrate", pallet-transaction-payment-rpc-runtime-api = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.18", default-features = false } pallet-treasury = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.18", default-features = false } pallet-utility = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.18", default-features = false } +pallet-staking = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.18", default-features = false } # Cumulus dependencies cumulus-pallet-aura-ext = { git = "https://github.com/paritytech/cumulus", default-features = false, branch = "polkadot-v0.9.18" } @@ -86,7 +90,9 @@ bifrost-liquidity-mining = { path = "../pallets/liquidity-mining", default-featu bifrost-runtime-common = { path = "../runtime/common", default-features = false } bifrost-salp = { path = "../pallets/salp", default-features = false } bifrost-salp-rpc-runtime-api = { path = "../pallets/salp/rpc/runtime-api", default-features = false } +bifrost-slp = { path = "../pallets/slp", default-features = false } bifrost-vsbond-auction = { path = "../pallets/vsbond-auction", default-features = false } +bifrost-vtoken-minting = { path = "../pallets/vtoken-minting", default-features = false } pallet-vesting = { package = "bifrost-vesting", path = "../pallets/vesting", default-features = false } bifrost-kusama-runtime = { path = "../runtime/bifrost-kusama", default-features = false } bifrost-polkadot-runtime = { path = "../runtime/bifrost-polkadot", default-features = false } @@ -123,16 +129,12 @@ westmint-runtime = { git = "https://github.com/paritytech/cumulus", branch = "po [features] default = ["std"] no_std = [] -with-bifrost-runtime=[ +with-bifrost-runtime = [ "node-service/with-bifrost-kusama-runtime", - "node-service/with-bifrost-polkadot-runtime" -] -with-bifrost-kusama-runtime=[ - "node-service/with-bifrost-kusama-runtime" -] -with-bifrost-polkadot-runtime=[ - "node-service/with-bifrost-polkadot-runtime" + "node-service/with-bifrost-polkadot-runtime", ] +with-bifrost-kusama-runtime = ["node-service/with-bifrost-kusama-runtime"] +with-bifrost-polkadot-runtime = ["node-service/with-bifrost-polkadot-runtime"] std = [ "codec/std", "log/std", @@ -193,7 +195,9 @@ std = [ "bifrost-runtime-common/std", "bifrost-salp/std", "bifrost-salp-rpc-runtime-api/std", + "bifrost-slp/std", "bifrost-vsbond-auction/std", + "bifrost-vtoken-minting/std", "orml-currencies/std", "orml-traits/std", "orml-tokens/std", @@ -203,5 +207,5 @@ std = [ "zenlink-protocol/std", "zenlink-protocol-runtime-api/std", "bifrost-kusama-runtime/std", - "bifrost-polkadot-runtime/std" + "bifrost-polkadot-runtime/std", ] diff --git a/integration-tests/src/integration_tests.rs b/integration-tests/src/integration_tests.rs index 4ba90d9821..e66a2dae29 100644 --- a/integration-tests/src/integration_tests.rs +++ b/integration-tests/src/integration_tests.rs @@ -43,7 +43,7 @@ mod bifrost_imports { create_x2_multilocation, AccountId, Balance, Balances, BifrostCrowdloanId, BlockNumber, Call, Currencies, CurrencyId, Event, ExistentialDeposit, ExistentialDeposits, NativeCurrencyId, Origin, OriginCaller, ParachainInfo, ParachainSystem, Proxy, - RelayCurrencyId, Runtime, Salp, Scheduler, Session, SlotLength, System, Tokens, + RelayCurrencyId, Runtime, Salp, Scheduler, Session, SlotLength, Slp, System, Tokens, TreasuryPalletId, Utility, Vesting, XTokens, XcmConfig, }; pub use bifrost_runtime_common::dollar; diff --git a/integration-tests/src/kusama_test_net.rs b/integration-tests/src/kusama_test_net.rs index 63fa43bdfa..06a82b1fe2 100644 --- a/integration-tests/src/kusama_test_net.rs +++ b/integration-tests/src/kusama_test_net.rs @@ -94,7 +94,7 @@ fn default_parachains_host_configuration() -> HostConfiguration { max_upward_queue_size: 1024 * 1024, max_downward_message_size: 1024, ump_service_total_weight: 4 * 1_000_000_000, - max_upward_message_size: 1024 * 1024, + max_upward_message_size: 1024 * 50, max_upward_message_num_per_candidate: 5, hrmp_sender_deposit: 0, hrmp_recipient_deposit: 0, diff --git a/integration-tests/src/lib.rs b/integration-tests/src/lib.rs index 55eea40123..f639da9825 100644 --- a/integration-tests/src/lib.rs +++ b/integration-tests/src/lib.rs @@ -26,4 +26,6 @@ mod kusama_cross_chain_transfer; #[cfg(feature = "with-bifrost-kusama-runtime")] mod kusama_test_net; #[cfg(feature = "with-bifrost-kusama-runtime")] +mod slp_tests; +#[cfg(feature = "with-bifrost-kusama-runtime")] mod statemine; diff --git a/integration-tests/src/slp_tests.rs b/integration-tests/src/slp_tests.rs new file mode 100644 index 0000000000..a8857eaab2 --- /dev/null +++ b/integration-tests/src/slp_tests.rs @@ -0,0 +1,2161 @@ +// This file is part of Bifrost. + +// Copyright (C) 2019-2022 Liebi Technologies (UK) Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +// This program 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. + +// This program 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 this program. If not, see . + +//! Cross-chain transfer tests within Kusama network. + +use bifrost_polkadot_runtime::PolkadotXcm; +use bifrost_slp::{ + primitives::{ + SubstrateLedgerUpdateEntry, SubstrateValidatorsByDelegatorUpdateEntry, UnlockChunk, + }, + Delays, Ledger, LedgerUpdateEntry, MinimumsMaximums, SubstrateLedger, + ValidatorsByDelegatorUpdateEntry, XcmOperation, +}; +use cumulus_primitives_core::relay_chain::HashT; +use frame_support::{assert_ok, BoundedVec}; +use node_primitives::TimeUnit; +use orml_traits::MultiCurrency; +use pallet_staking::{Nominations, StakingLedger}; +use pallet_xcm::QueryStatus; +use xcm::{latest::prelude::*, VersionedMultiAssets, VersionedMultiLocation}; +use xcm_emulator::TestExt; + +use crate::{integration_tests::*, kusama_test_net::*, slp_tests::VersionedMultiLocation::V1}; + +/// **************************************************** +/// ********* Preparation section ******************** +/// **************************************************** + +// parachain 2001 subaccount index 0 +fn subaccount_0() -> AccountId { + // 5E78xTBiaN3nAGYtcNnqTJQJqYAkSDGggKqaDfpNsKyPpbcb + let subaccount_0: AccountId = + hex_literal::hex!["5a53736d8e96f1c007cf0d630acf5209b20611617af23ce924c8e25328eb5d28"] + .into(); + + subaccount_0 +} + +fn para_account_2001() -> AccountId { + // 5Ec4AhPV91i9yNuiWuNunPf6AQCYDhFTTA4G5QCbtqYApH9E + let para_account_2001: AccountId = + hex_literal::hex!["70617261d1070000000000000000000000000000000000000000000000000000"] + .into(); + + para_account_2001 +} + +// Preparation: register sub-account index 0. +fn register_subaccount_index_0() { + let subaccount_0 = subaccount_0(); + + Bifrost::execute_with(|| { + let subaccount_0_32: [u8; 32] = Slp::account_id_to_account_32(subaccount_0).unwrap(); + + let subaccount_0_location: MultiLocation = + Slp::account_32_to_parent_location(subaccount_0_32).unwrap(); + + // Initialize ongoing timeunit as 0. + assert_ok!(Slp::update_ongoing_time_unit( + Origin::root(), + RelayCurrencyId::get(), + TimeUnit::Era(0) + )); + + // Initialize currency delays. + let delay = Delays { unlock_delay: TimeUnit::Era(10) }; + assert_ok!(Slp::set_currency_delays(Origin::root(), RelayCurrencyId::get(), Some(delay))); + + // First to setup index-multilocation relationship of subaccount_0 + assert_ok!(Slp::add_delegator( + Origin::root(), + RelayCurrencyId::get(), + 0u16, + subaccount_0_location.clone(), + )); + + // Register Operation weight and fee + assert_ok!(Slp::set_xcm_dest_weight_and_fee( + Origin::root(), + RelayCurrencyId::get(), + XcmOperation::TransferTo, + Some((20_000_000_000, 10_000_000_000)), + )); + + assert_ok!(Slp::set_xcm_dest_weight_and_fee( + Origin::root(), + RelayCurrencyId::get(), + XcmOperation::Bond, + Some((20_000_000_000, 10_000_000_000)), + )); + + assert_ok!(Slp::set_xcm_dest_weight_and_fee( + Origin::root(), + RelayCurrencyId::get(), + XcmOperation::BondExtra, + Some((20_000_000_000, 10_000_000_000)), + )); + + assert_ok!(Slp::set_xcm_dest_weight_and_fee( + Origin::root(), + RelayCurrencyId::get(), + XcmOperation::Unbond, + Some((20_000_000_000, 10_000_000_000)), + )); + + assert_ok!(Slp::set_xcm_dest_weight_and_fee( + Origin::root(), + RelayCurrencyId::get(), + XcmOperation::Rebond, + Some((20_000_000_000, 10_000_000_000)), + )); + + assert_ok!(Slp::set_xcm_dest_weight_and_fee( + Origin::root(), + RelayCurrencyId::get(), + XcmOperation::Delegate, + Some((20_000_000_000, 10_000_000_000)), + )); + + assert_ok!(Slp::set_xcm_dest_weight_and_fee( + Origin::root(), + RelayCurrencyId::get(), + XcmOperation::Payout, + Some((20_000_000_000, 10_000_000_000)), + )); + + assert_ok!(Slp::set_xcm_dest_weight_and_fee( + Origin::root(), + RelayCurrencyId::get(), + XcmOperation::Liquidize, + Some((20_000_000_000, 10_000_000_000)), + )); + + assert_ok!(Slp::set_xcm_dest_weight_and_fee( + Origin::root(), + RelayCurrencyId::get(), + XcmOperation::Chill, + Some((20_000_000_000, 10_000_000_000)), + )); + + assert_ok!(Slp::set_xcm_dest_weight_and_fee( + Origin::root(), + RelayCurrencyId::get(), + XcmOperation::TransferBack, + Some((20_000_000_000, 10_000_000_000)), + )); + + let mins_and_maxs = MinimumsMaximums { + delegator_bonded_minimum: 100_000_000_000, + bond_extra_minimum: 0, + unbond_minimum: 0, + rebond_minimum: 0, + unbond_record_maximum: 32, + validators_back_maximum: 36, + delegator_active_staking_maximum: 200_000_000_000_000, + }; + + // Set minimums and maximums + assert_ok!(Slp::set_minimums_and_maximums( + Origin::root(), + RelayCurrencyId::get(), + Some(mins_and_maxs) + )); + }); +} + +fn register_delegator_ledger() { + let subaccount_0 = subaccount_0(); + Bifrost::execute_with(|| { + let subaccount_0_32: [u8; 32] = Slp::account_id_to_account_32(subaccount_0).unwrap(); + let subaccount_0_location: MultiLocation = + Slp::account_32_to_parent_location(subaccount_0_32).unwrap(); + + let sb_ledger = SubstrateLedger { + account: subaccount_0_location.clone(), + total: dollar(RelayCurrencyId::get()), + active: dollar(RelayCurrencyId::get()), + unlocking: vec![], + }; + let ledger = Ledger::Substrate(sb_ledger); + + // Set delegator ledger + assert_ok!(Slp::set_delegator_ledger( + Origin::root(), + RelayCurrencyId::get(), + Box::new(subaccount_0_location.clone()), + Box::new(Some(ledger)) + )); + }); +} + +#[test] +fn register_validators() { + // GsvVmjr1CBHwQHw84pPHMDxgNY3iBLz6Qn7qS3CH8qPhrHz + let validator_0: AccountId = + hex_literal::hex!["be5ddb1579b72e84524fc29e78609e3caf42e85aa118ebfe0b0ad404b5bdd25f"] + .into(); + + // JKspFU6ohf1Grg3Phdzj2pSgWvsYWzSfKghhfzMbdhNBWs5 + let validator_1: AccountId = + hex_literal::hex!["fe65717dad0447d715f660a0a58411de509b42e6efb8375f562f58a554d5860e"] + .into(); + + Bifrost::execute_with(|| { + let mut valis = vec![]; + + let validator_0_32: [u8; 32] = Slp::account_id_to_account_32(validator_0).unwrap(); + let validator_0_location: MultiLocation = + Slp::account_32_to_parent_location(validator_0_32).unwrap(); + let multi_hash_0 = + ::Hashing::hash(&validator_0_location.encode()); + + // Set delegator ledger + assert_ok!(Slp::add_validator( + Origin::root(), + RelayCurrencyId::get(), + validator_0_location.clone(), + )); + + let validator_1_32: [u8; 32] = Slp::account_id_to_account_32(validator_1).unwrap(); + let validator_1_location: MultiLocation = + Slp::account_32_to_parent_location(validator_1_32).unwrap(); + let multi_hash_1 = + ::Hashing::hash(&validator_1_location.encode()); + + // The storage is reordered by hash. So we need to adjust the push order here. + valis.push((validator_1_location.clone(), multi_hash_1)); + valis.push((validator_0_location.clone(), multi_hash_0)); + + // Set delegator ledger + assert_ok!(Slp::add_validator( + Origin::root(), + RelayCurrencyId::get(), + validator_1_location, + )); + + assert_eq!(Slp::get_validators(RelayCurrencyId::get()), Some(valis)); + }); +} + +// Preparation: transfer 1 KSM from Alice in Kusama to Bob in Bifrost. +// Bob has a balance of +#[test] +fn transfer_2_ksm_to_entrance_account_in_bifrost() { + let para_account_2001 = para_account_2001(); + + let entrance_account_32: [u8; 32] = + hex_literal::hex!["6d6f646c62662f76746b696e0000000000000000000000000000000000000000"] + .into(); + + // Cross-chain transfer some KSM to Bob account in Bifrost + KusamaNet::execute_with(|| { + assert_ok!(kusama_runtime::XcmPallet::reserve_transfer_assets( + kusama_runtime::Origin::signed(ALICE.into()), + Box::new(VersionedMultiLocation::V1(X1(Parachain(2001)).into())), + Box::new(VersionedMultiLocation::V1( + X1(Junction::AccountId32 { id: entrance_account_32, network: NetworkId::Any }) + .into() + )), + Box::new(VersionedMultiAssets::V1((Here, 2 * dollar(RelayCurrencyId::get())).into())), + 0, + )); + + // predefined 2 dollars + 2 dollar from cross-chain transfer = 3 dollars + assert_eq!( + kusama_runtime::Balances::free_balance(¶_account_2001.clone()), + 4 * dollar(RelayCurrencyId::get()) + ); + }); + + Bifrost::execute_with(|| { + // entrance_account get the cross-transferred 2 dollar KSM minus transaction fee. + assert_eq!( + Tokens::free_balance(RelayCurrencyId::get(), &entrance_account_32.into()), + 1999936000000 + ); + }); +} + +// Preparation: transfer 1 KSM from Alice in Kusama to Bob in Bifrost. +// Bob has a balance of +#[test] +fn transfer_2_ksm_to_subaccount_in_kusama() { + let subaccount_0 = subaccount_0(); + + KusamaNet::execute_with(|| { + assert_ok!(kusama_runtime::Balances::transfer( + kusama_runtime::Origin::signed(ALICE.into()), + MultiAddress::Id(subaccount_0.clone()), + 2 * dollar(RelayCurrencyId::get()) + )); + + assert_eq!( + kusama_runtime::Balances::free_balance(&subaccount_0.clone()), + 2 * dollar(RelayCurrencyId::get()) + ); + }); +} + +#[test] +fn locally_bond_subaccount_0_1ksm_in_kusama() { + transfer_2_ksm_to_subaccount_in_kusama(); + let subaccount_0 = subaccount_0(); + + KusamaNet::execute_with(|| { + assert_ok!(kusama_runtime::Staking::bond( + kusama_runtime::Origin::signed(subaccount_0.clone()), + MultiAddress::Id(subaccount_0.clone()), + dollar(RelayCurrencyId::get()), + pallet_staking::RewardDestination::::Staked, + )); + + assert_eq!( + kusama_runtime::Staking::ledger(&subaccount_0), + Some(StakingLedger { + stash: subaccount_0.clone(), + total: dollar(RelayCurrencyId::get()), + active: dollar(RelayCurrencyId::get()), + unlocking: BoundedVec::try_from(vec![]).unwrap(), + claimed_rewards: vec![], + }) + ); + }); +} + +/// **************************************************** +/// ********* Test section ******************** +/// **************************************************** + +#[test] +fn transfer_to_works() { + register_subaccount_index_0(); + transfer_2_ksm_to_entrance_account_in_bifrost(); + transfer_2_ksm_to_subaccount_in_kusama(); + let subaccount_0 = subaccount_0(); + let para_account_2001 = para_account_2001(); + + let entrance_account: AccountId = + hex_literal::hex!["6d6f646c62662f76746b696e0000000000000000000000000000000000000000"] + .into(); + + let entrance_account_32 = Slp::account_id_to_account_32(entrance_account.clone()).unwrap(); + let entrance_account_location: MultiLocation = + Slp::account_32_to_local_location(entrance_account_32).unwrap(); + + Bifrost::execute_with(|| { + let subaccount_0_32: [u8; 32] = + Slp::account_id_to_account_32(subaccount_0.clone()).unwrap(); + + let subaccount_0_location: MultiLocation = + Slp::account_32_to_parent_location(subaccount_0_32).unwrap(); + + // We use transfer_to to transfer some KSM to subaccount_0 + assert_ok!(Slp::transfer_to( + Origin::root(), + RelayCurrencyId::get(), + Box::new(entrance_account_location), + Box::new(subaccount_0_location), + dollar(RelayCurrencyId::get()), + )); + }); + + KusamaNet::execute_with(|| { + assert_eq!( + kusama_runtime::Balances::free_balance(¶_account_2001.clone()), + 3 * dollar(RelayCurrencyId::get()) + ); + + // Why not the transferred amount reach the sub-account? + assert_eq!(kusama_runtime::Balances::free_balance(&subaccount_0.clone()), 2999893333340); + }); +} + +#[test] +fn bond_works() { + register_subaccount_index_0(); + transfer_2_ksm_to_subaccount_in_kusama(); + let subaccount_0 = subaccount_0(); + + Bifrost::execute_with(|| { + let subaccount_0_32: [u8; 32] = + Slp::account_id_to_account_32(subaccount_0.clone()).unwrap(); + + let subaccount_0_location: MultiLocation = + Slp::account_32_to_parent_location(subaccount_0_32).unwrap(); + + // Bond 1 ksm for sub-account index 0 + assert_ok!(Slp::bond( + Origin::root(), + RelayCurrencyId::get(), + subaccount_0_location, + dollar(RelayCurrencyId::get()), + )); + }); + + KusamaNet::execute_with(|| { + assert_eq!( + kusama_runtime::Staking::ledger(&subaccount_0), + Some(StakingLedger { + stash: subaccount_0.clone(), + total: dollar(RelayCurrencyId::get()), + active: dollar(RelayCurrencyId::get()), + unlocking: BoundedVec::try_from(vec![]).unwrap(), + claimed_rewards: vec![], + }) + ); + + assert!(kusama_runtime::System::events().iter().any(|r| matches!( + r.event, + kusama_runtime::Event::System(frame_system::Event::Remarked { sender: _, hash: _ }) + ))); + }); +} + +#[test] +fn bond_extra_works() { + // bond 1 ksm for sub-account index 0 + locally_bond_subaccount_0_1ksm_in_kusama(); + register_subaccount_index_0(); + register_delegator_ledger(); + let subaccount_0 = subaccount_0(); + + Bifrost::execute_with(|| { + let subaccount_0_32: [u8; 32] = + Slp::account_id_to_account_32(subaccount_0.clone()).unwrap(); + + let subaccount_0_location: MultiLocation = + Slp::account_32_to_parent_location(subaccount_0_32).unwrap(); + + // Bond_extra 1 ksm for sub-account index 0 + assert_ok!(Slp::bond_extra( + Origin::root(), + RelayCurrencyId::get(), + subaccount_0_location, + dollar(RelayCurrencyId::get()), + )); + }); + + // So the bonded amount should be 2 ksm + KusamaNet::execute_with(|| { + assert_eq!( + kusama_runtime::Staking::ledger(&subaccount_0), + Some(StakingLedger { + stash: subaccount_0.clone(), + total: 2 * dollar(RelayCurrencyId::get()), + active: 2 * dollar(RelayCurrencyId::get()), + unlocking: BoundedVec::try_from(vec![]).unwrap(), + claimed_rewards: vec![], + }) + ); + }); +} + +#[test] +fn unbond_works() { + // bond 1 ksm for sub-account index 0 + locally_bond_subaccount_0_1ksm_in_kusama(); + register_subaccount_index_0(); + register_delegator_ledger(); + let subaccount_0 = subaccount_0(); + + KusamaNet::execute_with(|| { + kusama_runtime::Staking::trigger_new_era(0, vec![]); + }); + + Bifrost::execute_with(|| { + let subaccount_0_32: [u8; 32] = + Slp::account_id_to_account_32(subaccount_0.clone()).unwrap(); + + let subaccount_0_location: MultiLocation = + Slp::account_32_to_parent_location(subaccount_0_32).unwrap(); + + // Unbond 0.5 ksm, 0.5 ksm left. + assert_ok!(Slp::unbond( + Origin::root(), + RelayCurrencyId::get(), + subaccount_0_location, + 500_000_000_000, + )); + }); + + // Can be uncommented to check if the result is correct. + // Due to the reason of private fields for struct UnlockChunk, + // it is not able to construct an instance of UnlockChunk directly. + // // So the bonded amount should be 2 ksm + // KusamaNet::execute_with(|| { + // assert_eq!( + // kusama_runtime::Staking::ledger(&subaccount_0), + // Some(StakingLedger { + // stash: subaccount_0.clone(), + // total: dollar(RelayCurrencyId::get()), + // active: 500_000_000_000, + // unlocking: vec![UnlockChunk { value: 500000000000, era: 28 }], + // claimed_rewards: vec![], + // }) + // ); + // }); +} + +#[test] +fn unbond_all_works() { + // bond 1 ksm for sub-account index 0 + locally_bond_subaccount_0_1ksm_in_kusama(); + register_subaccount_index_0(); + register_delegator_ledger(); + let subaccount_0 = subaccount_0(); + + Bifrost::execute_with(|| { + let subaccount_0_32: [u8; 32] = + Slp::account_id_to_account_32(subaccount_0.clone()).unwrap(); + + let subaccount_0_location: MultiLocation = + Slp::account_32_to_parent_location(subaccount_0_32).unwrap(); + + // Unbond the only bonded 1 ksm. + assert_ok!(Slp::unbond_all(Origin::root(), RelayCurrencyId::get(), subaccount_0_location,)); + }); + + // Can be uncommented to check if the result is correct. + // Due to the reason of private fields for struct UnlockChunk, + // it is not able to construct an instance of UnlockChunk directly. + // KusamaNet::execute_with(|| { + // assert_eq!( + // kusama_runtime::Staking::ledger(&subaccount_0), + // Some(StakingLedger { + // stash: subaccount_0.clone(), + // total: dollar(RelayCurrencyId::get()), + // active: 0, + // unlocking: vec![UnlockChunk { value: 1000000000000, era: 28 }], + // claimed_rewards: vec![], + // }) + // ); + // }); +} + +#[test] +fn rebond_works() { + // bond 1 ksm for sub-account index 0 + locally_bond_subaccount_0_1ksm_in_kusama(); + register_subaccount_index_0(); + register_delegator_ledger(); + let subaccount_0 = subaccount_0(); + + Bifrost::execute_with(|| { + let subaccount_0_32: [u8; 32] = + Slp::account_id_to_account_32(subaccount_0.clone()).unwrap(); + + let subaccount_0_location: MultiLocation = + Slp::account_32_to_parent_location(subaccount_0_32).unwrap(); + + // Unbond 0.5 ksm, 0.5 ksm left. + assert_ok!(Slp::unbond( + Origin::root(), + RelayCurrencyId::get(), + subaccount_0_location.clone(), + 500_000_000_000, + )); + + // Update Bifrost local ledger. This should be done by backend services. + let chunk = UnlockChunk { value: 500_000_000_000, unlock_time: TimeUnit::Era(8) }; + let sb_ledger = SubstrateLedger { + account: subaccount_0_location.clone(), + total: dollar(RelayCurrencyId::get()), + active: 500_000_000_000, + unlocking: vec![chunk], + }; + let ledger = Ledger::Substrate(sb_ledger); + + assert_ok!(Slp::set_delegator_ledger( + Origin::root(), + RelayCurrencyId::get(), + Box::new(subaccount_0_location.clone()), + Box::new(Some(ledger)) + )); + + // rebond 0.5 ksm. + assert_ok!(Slp::rebond( + Origin::root(), + RelayCurrencyId::get(), + subaccount_0_location, + 500_000_000_000, + )); + }); + + // So the bonded amount should be 1 ksm + KusamaNet::execute_with(|| { + assert_eq!( + kusama_runtime::Staking::ledger(&subaccount_0), + Some(StakingLedger { + stash: subaccount_0.clone(), + total: dollar(RelayCurrencyId::get()), + active: dollar(RelayCurrencyId::get()), + unlocking: BoundedVec::try_from(vec![]).unwrap(), + claimed_rewards: vec![], + }) + ); + }); +} + +#[test] +fn delegate_works() { + // bond 1 ksm for sub-account index 0 + register_validators(); + locally_bond_subaccount_0_1ksm_in_kusama(); + register_subaccount_index_0(); + register_delegator_ledger(); + let subaccount_0 = subaccount_0(); + + // GsvVmjr1CBHwQHw84pPHMDxgNY3iBLz6Qn7qS3CH8qPhrHz + let validator_0: AccountId = + hex_literal::hex!["be5ddb1579b72e84524fc29e78609e3caf42e85aa118ebfe0b0ad404b5bdd25f"] + .into(); + + // JKspFU6ohf1Grg3Phdzj2pSgWvsYWzSfKghhfzMbdhNBWs5 + let validator_1: AccountId = + hex_literal::hex!["fe65717dad0447d715f660a0a58411de509b42e6efb8375f562f58a554d5860e"] + .into(); + + Bifrost::execute_with(|| { + let subaccount_0_32: [u8; 32] = + Slp::account_id_to_account_32(subaccount_0.clone()).unwrap(); + let subaccount_0_location: MultiLocation = + Slp::account_32_to_parent_location(subaccount_0_32).unwrap(); + + let mut targets = vec![]; + + let validator_0_32: [u8; 32] = Slp::account_id_to_account_32(validator_0.clone()).unwrap(); + let validator_0_location: MultiLocation = + Slp::account_32_to_parent_location(validator_0_32).unwrap(); + targets.push(validator_0_location.clone()); + + let validator_1_32: [u8; 32] = Slp::account_id_to_account_32(validator_1.clone()).unwrap(); + let validator_1_location: MultiLocation = + Slp::account_32_to_parent_location(validator_1_32).unwrap(); + targets.push(validator_1_location.clone()); + + // delegate + assert_ok!(Slp::delegate( + Origin::root(), + RelayCurrencyId::get(), + subaccount_0_location.clone(), + targets.clone(), + )); + + assert_ok!(Slp::set_validators_by_delegator( + Origin::root(), + RelayCurrencyId::get(), + subaccount_0_location.clone(), + targets, + )); + }); + + KusamaNet::execute_with(|| { + assert_eq!( + kusama_runtime::Staking::nominators(&subaccount_0), + Some(Nominations { + targets: BoundedVec::try_from(vec![validator_1, validator_0]).unwrap(), + submitted_in: 0, + suppressed: false + },) + ); + }); +} + +#[test] +fn undelegate_works() { + delegate_works(); + + let subaccount_0 = subaccount_0(); + + // GsvVmjr1CBHwQHw84pPHMDxgNY3iBLz6Qn7qS3CH8qPhrHz + let validator_0: AccountId = + hex_literal::hex!["be5ddb1579b72e84524fc29e78609e3caf42e85aa118ebfe0b0ad404b5bdd25f"] + .into(); + + // JKspFU6ohf1Grg3Phdzj2pSgWvsYWzSfKghhfzMbdhNBWs5 + let validator_1: AccountId = + hex_literal::hex!["fe65717dad0447d715f660a0a58411de509b42e6efb8375f562f58a554d5860e"] + .into(); + + Bifrost::execute_with(|| { + let subaccount_0_32: [u8; 32] = + Slp::account_id_to_account_32(subaccount_0.clone()).unwrap(); + let subaccount_0_location: MultiLocation = + Slp::account_32_to_parent_location(subaccount_0_32).unwrap(); + + let mut targets = vec![]; + + let validator_0_32: [u8; 32] = Slp::account_id_to_account_32(validator_0.clone()).unwrap(); + let validator_0_location: MultiLocation = + Slp::account_32_to_parent_location(validator_0_32).unwrap(); + targets.push(validator_0_location.clone()); + + // Undelegate validator 0. Only validator 1 left. + assert_ok!(Slp::undelegate( + Origin::root(), + RelayCurrencyId::get(), + subaccount_0_location, + targets.clone(), + )); + }); + + KusamaNet::execute_with(|| { + assert_eq!( + kusama_runtime::Staking::nominators(&subaccount_0), + Some(Nominations { + targets: BoundedVec::try_from(vec![validator_1]).unwrap(), + submitted_in: 0, + suppressed: false + },) + ); + }); +} + +#[test] +fn redelegate_works() { + undelegate_works(); + + let subaccount_0 = subaccount_0(); + + // GsvVmjr1CBHwQHw84pPHMDxgNY3iBLz6Qn7qS3CH8qPhrHz + let validator_0: AccountId = + hex_literal::hex!["be5ddb1579b72e84524fc29e78609e3caf42e85aa118ebfe0b0ad404b5bdd25f"] + .into(); + + // JKspFU6ohf1Grg3Phdzj2pSgWvsYWzSfKghhfzMbdhNBWs5 + let validator_1: AccountId = + hex_literal::hex!["fe65717dad0447d715f660a0a58411de509b42e6efb8375f562f58a554d5860e"] + .into(); + + Bifrost::execute_with(|| { + let subaccount_0_32: [u8; 32] = + Slp::account_id_to_account_32(subaccount_0.clone()).unwrap(); + let subaccount_0_location: MultiLocation = + Slp::account_32_to_parent_location(subaccount_0_32).unwrap(); + + let mut targets = vec![]; + + let validator_0_32: [u8; 32] = Slp::account_id_to_account_32(validator_0.clone()).unwrap(); + let validator_0_location: MultiLocation = + Slp::account_32_to_parent_location(validator_0_32).unwrap(); + + let validator_1_32: [u8; 32] = Slp::account_id_to_account_32(validator_1.clone()).unwrap(); + let validator_1_location: MultiLocation = + Slp::account_32_to_parent_location(validator_1_32).unwrap(); + + targets.push(validator_1_location.clone()); + targets.push(validator_0_location.clone()); + + // Redelegate to a set of validator_0 and validator_1. + assert_ok!(Slp::redelegate( + Origin::root(), + RelayCurrencyId::get(), + subaccount_0_location, + targets.clone(), + )); + }); + + KusamaNet::execute_with(|| { + assert_eq!( + kusama_runtime::Staking::nominators(&subaccount_0), + Some(Nominations { + targets: BoundedVec::try_from(vec![validator_1, validator_0]).unwrap(), + submitted_in: 0, + suppressed: false + },) + ); + }); +} + +#[test] +fn payout_works() { + register_subaccount_index_0(); + transfer_2_ksm_to_subaccount_in_kusama(); + let subaccount_0 = subaccount_0(); + + // GsvVmjr1CBHwQHw84pPHMDxgNY3iBLz6Qn7qS3CH8qPhrHz + let validator_0: AccountId = + hex_literal::hex!["be5ddb1579b72e84524fc29e78609e3caf42e85aa118ebfe0b0ad404b5bdd25f"] + .into(); + + Bifrost::execute_with(|| { + let subaccount_0_32: [u8; 32] = + Slp::account_id_to_account_32(subaccount_0.clone()).unwrap(); + + let subaccount_0_location: MultiLocation = + Slp::account_32_to_parent_location(subaccount_0_32).unwrap(); + + let validator_0_32: [u8; 32] = Slp::account_id_to_account_32(validator_0.clone()).unwrap(); + let validator_0_location: MultiLocation = + Slp::account_32_to_parent_location(validator_0_32).unwrap(); + + // Bond 1 ksm for sub-account index 0 + assert_ok!(Slp::payout( + Origin::root(), + RelayCurrencyId::get(), + Box::new(subaccount_0_location), + Box::new(validator_0_location), + Some(TimeUnit::Era(27)) + )); + }); +} + +#[test] +fn liquidize_works() { + unbond_works(); + let subaccount_0 = subaccount_0(); + + KusamaNet::execute_with(|| { + // Kusama's unbonding period is 27 days = 100_800 blocks + kusama_runtime::System::set_block_number(101_000); + for _i in 0..29 { + kusama_runtime::Staking::trigger_new_era(0, vec![]); + } + + assert_eq!( + kusama_runtime::Balances::free_balance(&subaccount_0.clone()), + 2 * dollar(RelayCurrencyId::get()) + ); + + // 1ksm is locked for half bonded and half unbonding. + assert_eq!( + kusama_runtime::Balances::usable_balance(&subaccount_0.clone()), + dollar(RelayCurrencyId::get()) + ); + }); + + Bifrost::execute_with(|| { + let subaccount_0_32: [u8; 32] = + Slp::account_id_to_account_32(subaccount_0.clone()).unwrap(); + + let subaccount_0_location: MultiLocation = + Slp::account_32_to_parent_location(subaccount_0_32).unwrap(); + + assert_ok!(Slp::liquidize( + Origin::root(), + RelayCurrencyId::get(), + subaccount_0_location, + Some(TimeUnit::SlashingSpan(5)) + )); + }); + + KusamaNet::execute_with(|| { + assert_eq!( + kusama_runtime::Balances::free_balance(&subaccount_0.clone()), + 2 * dollar(RelayCurrencyId::get()) + ); + + // half of 1ksm unlocking has been freed. So the usable balance should be 1.5 ksm + assert_eq!( + kusama_runtime::Balances::usable_balance(&subaccount_0.clone()), + 1_500_000_000_000 + ); + }); +} + +#[test] +fn chill_works() { + delegate_works(); + let subaccount_0 = subaccount_0(); + + // check if sub-account index 0 belongs to the group of nominators + KusamaNet::execute_with(|| { + assert_eq!(kusama_runtime::Staking::nominators(&subaccount_0.clone()).is_some(), true); + }); + + Bifrost::execute_with(|| { + let subaccount_0_32: [u8; 32] = + Slp::account_id_to_account_32(subaccount_0.clone()).unwrap(); + + let subaccount_0_location: MultiLocation = + Slp::account_32_to_parent_location(subaccount_0_32).unwrap(); + + assert_ok!(Slp::chill(Origin::root(), RelayCurrencyId::get(), subaccount_0_location,)); + }); + + // check if sub-account index 0 belongs to the group of nominators + KusamaNet::execute_with(|| { + assert_eq!(kusama_runtime::Staking::nominators(&subaccount_0.clone()).is_some(), false); + }); +} + +#[test] +fn transfer_back_works() { + bond_works(); + let subaccount_0 = subaccount_0(); + let para_account_2001 = para_account_2001(); + + let exit_account: AccountId = + hex_literal::hex!["6d6f646c62662f76746f75740000000000000000000000000000000000000000"] + .into(); + + let exit_account_32 = Slp::account_id_to_account_32(exit_account.clone()).unwrap(); + let exit_account_location: MultiLocation = + Slp::account_32_to_local_location(exit_account_32).unwrap(); + + KusamaNet::execute_with(|| { + // 1ksm is locked for half bonded and half unbonding. + assert_eq!( + kusama_runtime::Balances::usable_balance(&subaccount_0.clone()), + dollar(RelayCurrencyId::get()) + ); + + assert_eq!( + kusama_runtime::Balances::free_balance(¶_account_2001.clone()), + 1999333333375 + ); + }); + + Bifrost::execute_with(|| { + let subaccount_0_32: [u8; 32] = + Slp::account_id_to_account_32(subaccount_0.clone()).unwrap(); + + let subaccount_0_location: MultiLocation = + Slp::account_32_to_parent_location(subaccount_0_32).unwrap(); + + assert_eq!(Tokens::free_balance(RelayCurrencyId::get(), &exit_account), 0); + + assert_ok!(Slp::transfer_back( + Origin::root(), + RelayCurrencyId::get(), + Box::new(subaccount_0_location), + Box::new(exit_account_location), + 500_000_000_000 + )); + }); + + // Parachain account has been deposited the transferred amount. + KusamaNet::execute_with(|| { + assert_eq!( + kusama_runtime::Balances::usable_balance(&subaccount_0.clone()), + 500_000_000_000 + ); + assert_eq!( + kusama_runtime::Balances::free_balance(¶_account_2001.clone()), + 2_498_666_666_750 + ); + }); + + Bifrost::execute_with(|| { + assert_eq!(Tokens::free_balance(RelayCurrencyId::get(), &exit_account), 499_936_000_000); + }); +} + +#[test] +fn supplement_fee_reserve_works() { + let subaccount_0 = subaccount_0(); + delegate_works(); + KusamaNet::execute_with(|| { + assert_eq!( + kusama_runtime::Balances::free_balance(&subaccount_0.clone()), + 2 * dollar(RelayCurrencyId::get()) + ); + }); + + Bifrost::execute_with(|| { + // set fee source + let alice_location = Slp::account_32_to_local_location(ALICE).unwrap(); + assert_ok!(Slp::set_fee_source( + Origin::root(), + RelayCurrencyId::get(), + Some((alice_location.clone(), dollar(RelayCurrencyId::get()))) + )); + + // We use supplement_fee_reserve to transfer some KSM to subaccount_0 + let subaccount_0_32: [u8; 32] = + Slp::account_id_to_account_32(subaccount_0.clone()).unwrap(); + + let subaccount_0_location: MultiLocation = + Slp::account_32_to_parent_location(subaccount_0_32).unwrap(); + + assert_ok!(Slp::supplement_fee_reserve( + Origin::root(), + RelayCurrencyId::get(), + subaccount_0_location, + )); + }); + + KusamaNet::execute_with(|| { + assert_eq!( + kusama_runtime::Balances::free_balance(&subaccount_0.clone()), + 2_999_893_333_340 + ); + }); +} + +#[test] +fn confirm_delegator_ledger_query_response_with_bond_works() { + register_subaccount_index_0(); + transfer_2_ksm_to_subaccount_in_kusama(); + let subaccount_0 = subaccount_0(); + + Bifrost::execute_with(|| { + let subaccount_0_32: [u8; 32] = + Slp::account_id_to_account_32(subaccount_0.clone()).unwrap(); + + let subaccount_0_location: MultiLocation = + Slp::account_32_to_parent_location(subaccount_0_32).unwrap(); + + // First call bond function, it will insert a query. + // Bond 1 ksm for sub-account index 0 + assert_ok!(Slp::bond( + Origin::root(), + RelayCurrencyId::get(), + subaccount_0_location.clone(), + dollar(RelayCurrencyId::get()), + )); + + // Check the existence of the query in pallet_xcm Queries storage. + assert_eq!( + PolkadotXcm::query(0), + Some(QueryStatus::Pending { + responder: V1(MultiLocation { parents: 1, interior: Here }), + maybe_notify: None, + timeout: 1001 + }) + ); + + // Check the existence of query in the response update queue storage. + assert_eq!( + Slp::get_delegator_ledger_update_entry(0), + Some(( + LedgerUpdateEntry::Substrate(SubstrateLedgerUpdateEntry { + currency_id: RelayCurrencyId::get(), + delegator_id: subaccount_0_location.clone(), + if_bond: true, + if_unlock: false, + if_rebond: false, + amount: dollar(RelayCurrencyId::get()), + unlock_time: None + }), + 1001 + )) + ); + }); + + KusamaNet::execute_with(|| { + assert_eq!( + kusama_runtime::Staking::ledger(&subaccount_0), + Some(StakingLedger { + stash: subaccount_0.clone(), + total: dollar(RelayCurrencyId::get()), + active: dollar(RelayCurrencyId::get()), + unlocking: BoundedVec::try_from(vec![]).unwrap(), + claimed_rewards: vec![], + }) + ); + }); + + Bifrost::execute_with(|| { + let subaccount_0_32: [u8; 32] = + Slp::account_id_to_account_32(subaccount_0.clone()).unwrap(); + + let subaccount_0_location: MultiLocation = + Slp::account_32_to_parent_location(subaccount_0_32).unwrap(); + + // Call confirm_delegator_ledger_query_response. + assert_ok!(Slp::confirm_delegator_ledger_query_response( + Origin::root(), + RelayCurrencyId::get(), + 0 + )); + + // Check the ledger update. + assert_eq!( + Slp::get_delegator_ledger(RelayCurrencyId::get(), subaccount_0_location.clone()), + Some(Ledger::Substrate(SubstrateLedger { + account: subaccount_0_location.clone(), + total: dollar(RelayCurrencyId::get()), + active: dollar(RelayCurrencyId::get()), + unlocking: vec![] + })) + ); + + // Check the existence of the query in pallet_xcm Queries storage. + // If xcm version 3 is introduced. We'll add instruction ReportTransactStatus into the xcm + // message. And this query will be set to ready status after we received a query response. + // At that point, this check should be set to equal None. + assert_eq!( + PolkadotXcm::query(0), + Some(QueryStatus::Pending { + responder: V1(MultiLocation { parents: 1, interior: Here }), + maybe_notify: None, + timeout: 1001 + }) + ); + + // Check the inexistence of query in the response update queue storage. + assert_eq!(Slp::get_delegator_ledger_update_entry(0), None); + }); +} + +#[test] +fn confirm_delegator_ledger_query_response_with_bond_extra_works() { + // bond 1 ksm for sub-account index 0 + locally_bond_subaccount_0_1ksm_in_kusama(); + register_subaccount_index_0(); + register_delegator_ledger(); + let subaccount_0 = subaccount_0(); + + Bifrost::execute_with(|| { + let subaccount_0_32: [u8; 32] = + Slp::account_id_to_account_32(subaccount_0.clone()).unwrap(); + + let subaccount_0_location: MultiLocation = + Slp::account_32_to_parent_location(subaccount_0_32).unwrap(); + + // Bond_extra 1 ksm for sub-account index 0 + assert_ok!(Slp::bond_extra( + Origin::root(), + RelayCurrencyId::get(), + subaccount_0_location.clone(), + dollar(RelayCurrencyId::get()), + )); + + assert_eq!( + Slp::get_delegator_ledger(RelayCurrencyId::get(), subaccount_0_location.clone()), + Some(Ledger::Substrate(SubstrateLedger { + account: subaccount_0_location.clone(), + total: dollar(RelayCurrencyId::get()), + active: dollar(RelayCurrencyId::get()), + unlocking: vec![] + })) + ); + + // Check the existence of the query in pallet_xcm Queries storage. + assert_eq!( + PolkadotXcm::query(0), + Some(QueryStatus::Pending { + responder: V1(MultiLocation { parents: 1, interior: Here }), + maybe_notify: None, + timeout: 1001 + }) + ); + + // Check the existence of query in the response update queue storage. + assert_eq!( + Slp::get_delegator_ledger_update_entry(0), + Some(( + LedgerUpdateEntry::Substrate(SubstrateLedgerUpdateEntry { + currency_id: RelayCurrencyId::get(), + delegator_id: subaccount_0_location.clone(), + if_bond: true, + if_unlock: false, + if_rebond: false, + amount: dollar(RelayCurrencyId::get()), + unlock_time: None + }), + 1001 + )) + ); + }); + + KusamaNet::execute_with(|| { + assert_eq!( + kusama_runtime::Staking::ledger(&subaccount_0), + Some(StakingLedger { + stash: subaccount_0.clone(), + total: 2 * dollar(RelayCurrencyId::get()), + active: 2 * dollar(RelayCurrencyId::get()), + unlocking: BoundedVec::try_from(vec![]).unwrap(), + claimed_rewards: vec![], + }) + ); + }); + + Bifrost::execute_with(|| { + let subaccount_0_32: [u8; 32] = + Slp::account_id_to_account_32(subaccount_0.clone()).unwrap(); + + let subaccount_0_location: MultiLocation = + Slp::account_32_to_parent_location(subaccount_0_32).unwrap(); + + // Call confirm_delegator_ledger_query_response. + assert_ok!(Slp::confirm_delegator_ledger_query_response( + Origin::root(), + RelayCurrencyId::get(), + 0 + )); + + // Check the ledger update. + assert_eq!( + Slp::get_delegator_ledger(RelayCurrencyId::get(), subaccount_0_location.clone()), + Some(Ledger::Substrate(SubstrateLedger { + account: subaccount_0_location.clone(), + total: 2 * dollar(RelayCurrencyId::get()), + active: 2 * dollar(RelayCurrencyId::get()), + unlocking: vec![] + })) + ); + + // Check the existence of the query in pallet_xcm Queries storage. + // If xcm version 3 is introduced. We'll add instruction ReportTransactStatus into the xcm + // message. And this query will be set to ready status after we received a query response. + // At that point, this check should be set to equal None. + assert_eq!( + PolkadotXcm::query(0), + Some(QueryStatus::Pending { + responder: V1(MultiLocation { parents: 1, interior: Here }), + maybe_notify: None, + timeout: 1001 + }) + ); + + // Check the inexistence of query in the response update queue storage. + assert_eq!(Slp::get_delegator_ledger_update_entry(0), None); + }); +} + +#[test] +fn confirm_delegator_ledger_query_response_with_unbond_works() { + // bond 1 ksm for sub-account index 0 + locally_bond_subaccount_0_1ksm_in_kusama(); + register_subaccount_index_0(); + register_delegator_ledger(); + let subaccount_0 = subaccount_0(); + + Bifrost::execute_with(|| { + let subaccount_0_32: [u8; 32] = + Slp::account_id_to_account_32(subaccount_0.clone()).unwrap(); + + let subaccount_0_location: MultiLocation = + Slp::account_32_to_parent_location(subaccount_0_32).unwrap(); + + // Unbond 0.5 ksm, 0.5 ksm left. + assert_ok!(Slp::unbond( + Origin::root(), + RelayCurrencyId::get(), + subaccount_0_location.clone(), + 500_000_000_000, + )); + + assert_eq!( + Slp::get_delegator_ledger(RelayCurrencyId::get(), subaccount_0_location.clone()), + Some(Ledger::Substrate(SubstrateLedger { + account: subaccount_0_location.clone(), + total: dollar(RelayCurrencyId::get()), + active: dollar(RelayCurrencyId::get()), + unlocking: vec![] + })) + ); + + // Check the existence of the query in pallet_xcm Queries storage. + assert_eq!( + PolkadotXcm::query(0), + Some(QueryStatus::Pending { + responder: V1(MultiLocation { parents: 1, interior: Here }), + maybe_notify: None, + timeout: 1001 + }) + ); + + // Check the existence of query in the response update queue storage. + assert_eq!( + Slp::get_delegator_ledger_update_entry(0), + Some(( + LedgerUpdateEntry::Substrate(SubstrateLedgerUpdateEntry { + currency_id: RelayCurrencyId::get(), + delegator_id: subaccount_0_location.clone(), + if_bond: false, + if_unlock: true, + if_rebond: false, + amount: 500_000_000_000, + unlock_time: Some(TimeUnit::Era(10)) + }), + 1001 + )) + ); + }); + + Bifrost::execute_with(|| { + let subaccount_0_32: [u8; 32] = + Slp::account_id_to_account_32(subaccount_0.clone()).unwrap(); + + let subaccount_0_location: MultiLocation = + Slp::account_32_to_parent_location(subaccount_0_32).unwrap(); + + // Call confirm_delegator_ledger_query_response. + assert_ok!(Slp::confirm_delegator_ledger_query_response( + Origin::root(), + RelayCurrencyId::get(), + 0 + )); + + // Check the ledger update. + assert_eq!( + Slp::get_delegator_ledger(RelayCurrencyId::get(), subaccount_0_location.clone()), + Some(Ledger::Substrate(SubstrateLedger { + account: subaccount_0_location.clone(), + total: dollar(RelayCurrencyId::get()), + active: 500_000_000_000, + unlocking: vec![UnlockChunk { + value: 500000000000, + unlock_time: TimeUnit::Era(10) + }] + })) + ); + + // Check the existence of the query in pallet_xcm Queries storage. + // If xcm version 3 is introduced. We'll add instruction ReportTransactStatus into the xcm + // message. And this query will be set to ready status after we received a query response. + // At that point, this check should be set to equal None. + assert_eq!( + PolkadotXcm::query(0), + Some(QueryStatus::Pending { + responder: V1(MultiLocation { parents: 1, interior: Here }), + maybe_notify: None, + timeout: 1001 + }) + ); + + // Check the inexistence of query in the response update queue storage. + assert_eq!(Slp::get_delegator_ledger_update_entry(0), None); + }); +} + +#[test] +fn confirm_delegator_ledger_query_response_with_unbond_all_works() { + // bond 1 ksm for sub-account index 0 + locally_bond_subaccount_0_1ksm_in_kusama(); + register_subaccount_index_0(); + register_delegator_ledger(); + let subaccount_0 = subaccount_0(); + + Bifrost::execute_with(|| { + let subaccount_0_32: [u8; 32] = + Slp::account_id_to_account_32(subaccount_0.clone()).unwrap(); + + let subaccount_0_location: MultiLocation = + Slp::account_32_to_parent_location(subaccount_0_32).unwrap(); + + // Unbond the only bonded 1 ksm. + assert_ok!(Slp::unbond_all( + Origin::root(), + RelayCurrencyId::get(), + subaccount_0_location.clone(), + )); + + assert_eq!( + Slp::get_delegator_ledger(RelayCurrencyId::get(), subaccount_0_location.clone()), + Some(Ledger::Substrate(SubstrateLedger { + account: subaccount_0_location.clone(), + total: dollar(RelayCurrencyId::get()), + active: dollar(RelayCurrencyId::get()), + unlocking: vec![] + })) + ); + + // Check the existence of the query in pallet_xcm Queries storage. + assert_eq!( + PolkadotXcm::query(0), + Some(QueryStatus::Pending { + responder: V1(MultiLocation { parents: 1, interior: Here }), + maybe_notify: None, + timeout: 1001 + }) + ); + + // Check the existence of query in the response update queue storage. + assert_eq!( + Slp::get_delegator_ledger_update_entry(0), + Some(( + LedgerUpdateEntry::Substrate(SubstrateLedgerUpdateEntry { + currency_id: RelayCurrencyId::get(), + delegator_id: subaccount_0_location.clone(), + if_bond: false, + if_unlock: true, + if_rebond: false, + amount: dollar(RelayCurrencyId::get()), + unlock_time: Some(TimeUnit::Era(10)) + }), + 1001 + )) + ); + }); + + Bifrost::execute_with(|| { + let subaccount_0_32: [u8; 32] = + Slp::account_id_to_account_32(subaccount_0.clone()).unwrap(); + + let subaccount_0_location: MultiLocation = + Slp::account_32_to_parent_location(subaccount_0_32).unwrap(); + + // Call confirm_delegator_ledger_query_response. + assert_ok!(Slp::confirm_delegator_ledger_query_response( + Origin::root(), + RelayCurrencyId::get(), + 0 + )); + + // Check the ledger update. + assert_eq!( + Slp::get_delegator_ledger(RelayCurrencyId::get(), subaccount_0_location.clone()), + Some(Ledger::Substrate(SubstrateLedger { + account: subaccount_0_location.clone(), + total: dollar(RelayCurrencyId::get()), + active: 0, + unlocking: vec![UnlockChunk { + value: dollar(RelayCurrencyId::get()), + unlock_time: TimeUnit::Era(10) + }] + })) + ); + + // Check the existence of the query in pallet_xcm Queries storage. + // If xcm version 3 is introduced. We'll add instruction ReportTransactStatus into the xcm + // message. And this query will be set to ready status after we received a query response. + // At that point, this check should be set to equal None. + assert_eq!( + PolkadotXcm::query(0), + Some(QueryStatus::Pending { + responder: V1(MultiLocation { parents: 1, interior: Here }), + maybe_notify: None, + timeout: 1001 + }) + ); + + // Check the inexistence of query in the response update queue storage. + assert_eq!(Slp::get_delegator_ledger_update_entry(0), None); + }); +} + +#[test] +fn confirm_delegator_ledger_query_response_with_rebond_works() { + // bond 1 ksm for sub-account index 0 + locally_bond_subaccount_0_1ksm_in_kusama(); + register_subaccount_index_0(); + register_delegator_ledger(); + let subaccount_0 = subaccount_0(); + + Bifrost::execute_with(|| { + let subaccount_0_32: [u8; 32] = + Slp::account_id_to_account_32(subaccount_0.clone()).unwrap(); + + let subaccount_0_location: MultiLocation = + Slp::account_32_to_parent_location(subaccount_0_32).unwrap(); + + // Unbond 0.5 ksm, 0.5 ksm left. + assert_ok!(Slp::unbond( + Origin::root(), + RelayCurrencyId::get(), + subaccount_0_location.clone(), + 500_000_000_000, + )); + + // Update Bifrost local ledger. This should be done by backend services. + let chunk = UnlockChunk { value: 500_000_000_000, unlock_time: TimeUnit::Era(10) }; + let sb_ledger = SubstrateLedger { + account: subaccount_0_location.clone(), + total: dollar(RelayCurrencyId::get()), + active: 500_000_000_000, + unlocking: vec![chunk], + }; + let ledger = Ledger::Substrate(sb_ledger); + + assert_ok!(Slp::set_delegator_ledger( + Origin::root(), + RelayCurrencyId::get(), + Box::new(subaccount_0_location.clone()), + Box::new(Some(ledger)) + )); + + // rebond 0.5 ksm. + assert_ok!(Slp::rebond( + Origin::root(), + RelayCurrencyId::get(), + subaccount_0_location.clone(), + 500_000_000_000, + )); + + assert_eq!( + Slp::get_delegator_ledger(RelayCurrencyId::get(), subaccount_0_location.clone()), + Some(Ledger::Substrate(SubstrateLedger { + account: subaccount_0_location.clone(), + total: dollar(RelayCurrencyId::get()), + active: 500_000_000_000, + unlocking: vec![UnlockChunk { + value: 500_000_000_000, + unlock_time: TimeUnit::Era(10) + }] + })) + ); + + // Check the existence of the query in pallet_xcm Queries storage. + assert_eq!( + PolkadotXcm::query(1), + Some(QueryStatus::Pending { + responder: V1(MultiLocation { parents: 1, interior: Here }), + maybe_notify: None, + timeout: 1001 + }) + ); + + // Check the existence of query in the response update queue storage. + assert_eq!( + Slp::get_delegator_ledger_update_entry(1), + Some(( + LedgerUpdateEntry::Substrate(SubstrateLedgerUpdateEntry { + currency_id: RelayCurrencyId::get(), + delegator_id: subaccount_0_location.clone(), + if_bond: false, + if_unlock: false, + if_rebond: true, + amount: 500_000_000_000, + unlock_time: None + }), + 1001 + )) + ); + }); + + Bifrost::execute_with(|| { + let subaccount_0_32: [u8; 32] = + Slp::account_id_to_account_32(subaccount_0.clone()).unwrap(); + + let subaccount_0_location: MultiLocation = + Slp::account_32_to_parent_location(subaccount_0_32).unwrap(); + + // Call confirm_delegator_ledger_query_response. + assert_ok!(Slp::confirm_delegator_ledger_query_response( + Origin::root(), + RelayCurrencyId::get(), + 1 + )); + + // Check the ledger update. + assert_eq!( + Slp::get_delegator_ledger(RelayCurrencyId::get(), subaccount_0_location.clone()), + Some(Ledger::Substrate(SubstrateLedger { + account: subaccount_0_location.clone(), + total: dollar(RelayCurrencyId::get()), + active: dollar(RelayCurrencyId::get()), + unlocking: vec![] + })) + ); + + // Check the existence of the query in pallet_xcm Queries storage. + // If xcm version 3 is introduced. We'll add instruction ReportTransactStatus into the xcm + // message. And this query will be set to ready status after we received a query response. + // At that point, this check should be set to equal None. + assert_eq!( + PolkadotXcm::query(1), + Some(QueryStatus::Pending { + responder: V1(MultiLocation { parents: 1, interior: Here }), + maybe_notify: None, + timeout: 1001 + }) + ); + + // Check the inexistence of query in the response update queue storage. + assert_eq!(Slp::get_delegator_ledger_update_entry(1), None); + }); +} + +#[test] +fn confirm_delegator_ledger_query_response_with_liquidize_works() { + confirm_delegator_ledger_query_response_with_unbond_works(); + let subaccount_0 = subaccount_0(); + + KusamaNet::execute_with(|| { + // Kusama's unbonding period is 27 days = 100_800 blocks + kusama_runtime::System::set_block_number(101_000); + for _i in 0..29 { + kusama_runtime::Staking::trigger_new_era(0, vec![]); + } + + assert_eq!( + kusama_runtime::Balances::free_balance(&subaccount_0.clone()), + 2 * dollar(RelayCurrencyId::get()) + ); + + // 1ksm is locked for half bonded and half unbonding. + assert_eq!( + kusama_runtime::Balances::usable_balance(&subaccount_0.clone()), + dollar(RelayCurrencyId::get()) + ); + }); + + Bifrost::execute_with(|| { + let subaccount_0_32: [u8; 32] = + Slp::account_id_to_account_32(subaccount_0.clone()).unwrap(); + + let subaccount_0_location: MultiLocation = + Slp::account_32_to_parent_location(subaccount_0_32).unwrap(); + + // set ongoing era to be 11 which is greater than due era 10. + assert_ok!(Slp::update_ongoing_time_unit( + Origin::root(), + RelayCurrencyId::get(), + TimeUnit::Era(11) + )); + + assert_ok!(Slp::liquidize( + Origin::root(), + RelayCurrencyId::get(), + subaccount_0_location.clone(), + Some(TimeUnit::SlashingSpan(5)) + )); + + assert_eq!( + Slp::get_delegator_ledger(RelayCurrencyId::get(), subaccount_0_location.clone()), + Some(Ledger::Substrate(SubstrateLedger { + account: subaccount_0_location.clone(), + total: dollar(RelayCurrencyId::get()), + active: 500_000_000_000, + unlocking: vec![UnlockChunk { + value: 500_000_000_000, + unlock_time: TimeUnit::Era(10) + }] + })) + ); + + // Check the existence of the query in pallet_xcm Queries storage. + assert_eq!( + PolkadotXcm::query(1), + Some(QueryStatus::Pending { + responder: V1(MultiLocation { parents: 1, interior: Here }), + maybe_notify: None, + timeout: 1001 + }) + ); + + // Check the existence of query in the response update queue storage. + assert_eq!( + Slp::get_delegator_ledger_update_entry(1), + Some(( + LedgerUpdateEntry::Substrate(SubstrateLedgerUpdateEntry { + currency_id: RelayCurrencyId::get(), + delegator_id: subaccount_0_location.clone(), + if_bond: false, + if_unlock: false, + if_rebond: false, + amount: 0, + unlock_time: Some(TimeUnit::Era(11)) + }), + 1001 + )) + ); + }); + + Bifrost::execute_with(|| { + let subaccount_0_32: [u8; 32] = + Slp::account_id_to_account_32(subaccount_0.clone()).unwrap(); + + let subaccount_0_location: MultiLocation = + Slp::account_32_to_parent_location(subaccount_0_32).unwrap(); + + // Call confirm_delegator_ledger_query_response. + assert_ok!(Slp::confirm_delegator_ledger_query_response( + Origin::root(), + RelayCurrencyId::get(), + 1 + )); + + // Check the ledger update. + assert_eq!( + Slp::get_delegator_ledger(RelayCurrencyId::get(), subaccount_0_location.clone()), + Some(Ledger::Substrate(SubstrateLedger { + account: subaccount_0_location.clone(), + total: 500_000_000_000, + active: 500_000_000_000, + unlocking: vec![] + })) + ); + + // Check the existence of the query in pallet_xcm Queries storage. + // If xcm version 3 is introduced. We'll add instruction ReportTransactStatus into the xcm + // message. And this query will be set to ready status after we received a query response. + // At that point, this check should be set to equal None. + assert_eq!( + PolkadotXcm::query(1), + Some(QueryStatus::Pending { + responder: V1(MultiLocation { parents: 1, interior: Here }), + maybe_notify: None, + timeout: 1001 + }) + ); + + // Check the inexistence of query in the response update queue storage. + assert_eq!(Slp::get_delegator_ledger_update_entry(1), None); + }); + + KusamaNet::execute_with(|| { + assert_eq!( + kusama_runtime::Balances::free_balance(&subaccount_0.clone()), + 2 * dollar(RelayCurrencyId::get()) + ); + + // half of 1ksm unlocking has been freed. So the usable balance should be 1.5 ksm + assert_eq!( + kusama_runtime::Balances::usable_balance(&subaccount_0.clone()), + 1_500_000_000_000 + ); + }); +} + +#[test] +fn fail_delegator_ledger_query_response_works() { + register_subaccount_index_0(); + transfer_2_ksm_to_subaccount_in_kusama(); + let subaccount_0 = subaccount_0(); + + Bifrost::execute_with(|| { + let subaccount_0_32: [u8; 32] = + Slp::account_id_to_account_32(subaccount_0.clone()).unwrap(); + + let subaccount_0_location: MultiLocation = + Slp::account_32_to_parent_location(subaccount_0_32).unwrap(); + + // First call bond function, it will insert a query. + // Bond 1 ksm for sub-account index 0 + assert_ok!(Slp::bond( + Origin::root(), + RelayCurrencyId::get(), + subaccount_0_location.clone(), + dollar(RelayCurrencyId::get()), + )); + + // Check the existence of the query in pallet_xcm Queries storage. + assert_eq!( + PolkadotXcm::query(0), + Some(QueryStatus::Pending { + responder: V1(MultiLocation { parents: 1, interior: Here }), + maybe_notify: None, + timeout: 1001 + }) + ); + + // Check the existence of query in the response update queue storage. + assert_eq!( + Slp::get_delegator_ledger_update_entry(0), + Some(( + LedgerUpdateEntry::Substrate(SubstrateLedgerUpdateEntry { + currency_id: RelayCurrencyId::get(), + delegator_id: subaccount_0_location.clone(), + if_bond: true, + if_unlock: false, + if_rebond: false, + amount: dollar(RelayCurrencyId::get()), + unlock_time: None + }), + 1001 + )) + ); + }); + + Bifrost::execute_with(|| { + let subaccount_0_32: [u8; 32] = + Slp::account_id_to_account_32(subaccount_0.clone()).unwrap(); + + let subaccount_0_location: MultiLocation = + Slp::account_32_to_parent_location(subaccount_0_32).unwrap(); + + // Call confirm_delegator_ledger_query_response. + assert_ok!(Slp::fail_delegator_ledger_query_response( + Origin::root(), + RelayCurrencyId::get(), + 0 + )); + + // Check the ledger update. + assert_eq!( + Slp::get_delegator_ledger(RelayCurrencyId::get(), subaccount_0_location.clone()), + Some(Ledger::Substrate(SubstrateLedger { + account: subaccount_0_location.clone(), + total: 0, + active: 0, + unlocking: vec![] + })) + ); + + // Check the existence of the query in pallet_xcm Queries storage. + // If xcm version 3 is introduced. We'll add instruction ReportTransactStatus into the xcm + // message. And this query will be set to ready status after we received a query response. + // At that point, this check should be set to equal None. + assert_eq!( + PolkadotXcm::query(0), + Some(QueryStatus::Pending { + responder: V1(MultiLocation { parents: 1, interior: Here }), + maybe_notify: None, + timeout: 1001 + }) + ); + + // Check the inexistence of query in the response update queue storage. + assert_eq!(Slp::get_delegator_ledger_update_entry(0), None); + }); +} + +#[test] +fn confirm_validators_by_delegator_query_response_with_delegate_works() { + // bond 1 ksm for sub-account index 0 + register_validators(); + locally_bond_subaccount_0_1ksm_in_kusama(); + register_subaccount_index_0(); + register_delegator_ledger(); + let subaccount_0 = subaccount_0(); + + // GsvVmjr1CBHwQHw84pPHMDxgNY3iBLz6Qn7qS3CH8qPhrHz + let validator_0: AccountId = + hex_literal::hex!["be5ddb1579b72e84524fc29e78609e3caf42e85aa118ebfe0b0ad404b5bdd25f"] + .into(); + + // JKspFU6ohf1Grg3Phdzj2pSgWvsYWzSfKghhfzMbdhNBWs5 + let validator_1: AccountId = + hex_literal::hex!["fe65717dad0447d715f660a0a58411de509b42e6efb8375f562f58a554d5860e"] + .into(); + + Bifrost::execute_with(|| { + let subaccount_0_32: [u8; 32] = + Slp::account_id_to_account_32(subaccount_0.clone()).unwrap(); + let subaccount_0_location: MultiLocation = + Slp::account_32_to_parent_location(subaccount_0_32).unwrap(); + + let mut targets = vec![]; + let mut valis = vec![]; + + let validator_0_32: [u8; 32] = Slp::account_id_to_account_32(validator_0.clone()).unwrap(); + let validator_0_location: MultiLocation = + Slp::account_32_to_parent_location(validator_0_32).unwrap(); + targets.push(validator_0_location.clone()); + let multi_hash_0 = + ::Hashing::hash(&validator_0_location.encode()); + + let validator_1_32: [u8; 32] = Slp::account_id_to_account_32(validator_1.clone()).unwrap(); + let validator_1_location: MultiLocation = + Slp::account_32_to_parent_location(validator_1_32).unwrap(); + targets.push(validator_1_location.clone()); + let multi_hash_1 = + ::Hashing::hash(&validator_1_location.encode()); + + valis.push((validator_1_location.clone(), multi_hash_1)); + valis.push((validator_0_location.clone(), multi_hash_0)); + + // delegate + assert_ok!(Slp::delegate( + Origin::root(), + RelayCurrencyId::get(), + subaccount_0_location.clone(), + targets.clone(), + )); + + // Before data: Delegate nobody. + assert_eq!( + Slp::get_validators_by_delegator(RelayCurrencyId::get(), subaccount_0_location.clone()), + None + ); + + assert_eq!( + Slp::get_validators_by_delegator_update_entry(0), + Some(( + ValidatorsByDelegatorUpdateEntry::Substrate( + SubstrateValidatorsByDelegatorUpdateEntry { + currency_id: RelayCurrencyId::get(), + delegator_id: subaccount_0_location.clone(), + validators: valis.clone(), + } + ), + 1001 + )) + ); + + // confirm call + assert_ok!(Slp::confirm_validators_by_delegator_query_response( + Origin::root(), + RelayCurrencyId::get(), + 0 + )); + + // After delegation data. + assert_eq!( + Slp::get_validators_by_delegator(RelayCurrencyId::get(), subaccount_0_location.clone()), + Some(valis) + ); + + assert_eq!(Slp::get_validators_by_delegator_update_entry(0), None); + }); +} + +#[test] +fn confirm_validators_by_delegator_query_response_with_undelegate_works() { + delegate_works(); + + let subaccount_0 = subaccount_0(); + + // GsvVmjr1CBHwQHw84pPHMDxgNY3iBLz6Qn7qS3CH8qPhrHz + let validator_0: AccountId = + hex_literal::hex!["be5ddb1579b72e84524fc29e78609e3caf42e85aa118ebfe0b0ad404b5bdd25f"] + .into(); + + // JKspFU6ohf1Grg3Phdzj2pSgWvsYWzSfKghhfzMbdhNBWs5 + let validator_1: AccountId = + hex_literal::hex!["fe65717dad0447d715f660a0a58411de509b42e6efb8375f562f58a554d5860e"] + .into(); + + Bifrost::execute_with(|| { + let subaccount_0_32: [u8; 32] = + Slp::account_id_to_account_32(subaccount_0.clone()).unwrap(); + let subaccount_0_location: MultiLocation = + Slp::account_32_to_parent_location(subaccount_0_32).unwrap(); + + let mut targets = vec![]; + let mut valis_1 = vec![]; + let mut valis_2 = vec![]; + + let validator_0_32: [u8; 32] = Slp::account_id_to_account_32(validator_0.clone()).unwrap(); + let validator_0_location: MultiLocation = + Slp::account_32_to_parent_location(validator_0_32).unwrap(); + targets.push(validator_0_location.clone()); + let multi_hash_0 = + ::Hashing::hash(&validator_0_location.encode()); + + let validator_1_32: [u8; 32] = Slp::account_id_to_account_32(validator_1.clone()).unwrap(); + let validator_1_location: MultiLocation = + Slp::account_32_to_parent_location(validator_1_32).unwrap(); + let multi_hash_1 = + ::Hashing::hash(&validator_1_location.encode()); + + valis_1.push((validator_1_location.clone(), multi_hash_1.clone())); + + valis_2.push((validator_1_location.clone(), multi_hash_1)); + valis_2.push((validator_0_location.clone(), multi_hash_0)); + + // Undelegate validator 0. Only validator 1 left. + assert_ok!(Slp::undelegate( + Origin::root(), + RelayCurrencyId::get(), + subaccount_0_location.clone(), + targets.clone(), + )); + + // Before data: Delegate 2 validators. + assert_eq!( + Slp::get_validators_by_delegator(RelayCurrencyId::get(), subaccount_0_location.clone()), + Some(valis_2) + ); + + assert_eq!( + Slp::get_validators_by_delegator_update_entry(1), + Some(( + ValidatorsByDelegatorUpdateEntry::Substrate( + SubstrateValidatorsByDelegatorUpdateEntry { + currency_id: RelayCurrencyId::get(), + delegator_id: subaccount_0_location.clone(), + validators: valis_1.clone(), + } + ), + 1001 + )) + ); + + // confirm call + assert_ok!(Slp::confirm_validators_by_delegator_query_response( + Origin::root(), + RelayCurrencyId::get(), + 1, + )); + + // After delegation data: delegate only 1 validator. + assert_eq!( + Slp::get_validators_by_delegator(RelayCurrencyId::get(), subaccount_0_location.clone()), + Some(valis_1) + ); + + assert_eq!(Slp::get_validators_by_delegator_update_entry(1), None); + }); +} + +#[test] +fn confirm_validators_by_delegator_query_response_with_redelegate_works() { + confirm_validators_by_delegator_query_response_with_undelegate_works(); + + let subaccount_0 = subaccount_0(); + + // GsvVmjr1CBHwQHw84pPHMDxgNY3iBLz6Qn7qS3CH8qPhrHz + let validator_0: AccountId = + hex_literal::hex!["be5ddb1579b72e84524fc29e78609e3caf42e85aa118ebfe0b0ad404b5bdd25f"] + .into(); + + // JKspFU6ohf1Grg3Phdzj2pSgWvsYWzSfKghhfzMbdhNBWs5 + let validator_1: AccountId = + hex_literal::hex!["fe65717dad0447d715f660a0a58411de509b42e6efb8375f562f58a554d5860e"] + .into(); + + Bifrost::execute_with(|| { + let subaccount_0_32: [u8; 32] = + Slp::account_id_to_account_32(subaccount_0.clone()).unwrap(); + let subaccount_0_location: MultiLocation = + Slp::account_32_to_parent_location(subaccount_0_32).unwrap(); + + let mut targets = vec![]; + let mut valis_1 = vec![]; + let mut valis_2 = vec![]; + + let validator_0_32: [u8; 32] = Slp::account_id_to_account_32(validator_0.clone()).unwrap(); + let validator_0_location: MultiLocation = + Slp::account_32_to_parent_location(validator_0_32).unwrap(); + targets.push(validator_0_location.clone()); + let multi_hash_0 = + ::Hashing::hash(&validator_0_location.encode()); + + let validator_1_32: [u8; 32] = Slp::account_id_to_account_32(validator_1.clone()).unwrap(); + let validator_1_location: MultiLocation = + Slp::account_32_to_parent_location(validator_1_32).unwrap(); + targets.push(validator_1_location.clone()); + let multi_hash_1 = + ::Hashing::hash(&validator_1_location.encode()); + + valis_1.push((validator_1_location.clone(), multi_hash_1.clone())); + valis_2.push((validator_1_location.clone(), multi_hash_1)); + valis_2.push((validator_0_location.clone(), multi_hash_0)); + + // Redelegate to a set of validator_0 and validator_1. + assert_ok!(Slp::redelegate( + Origin::root(), + RelayCurrencyId::get(), + subaccount_0_location.clone(), + targets.clone(), + )); + + // Before data: Delegate only 1 validator. + assert_eq!( + Slp::get_validators_by_delegator(RelayCurrencyId::get(), subaccount_0_location.clone()), + Some(valis_1) + ); + + assert_eq!( + Slp::get_validators_by_delegator_update_entry(2), + Some(( + ValidatorsByDelegatorUpdateEntry::Substrate( + SubstrateValidatorsByDelegatorUpdateEntry { + currency_id: RelayCurrencyId::get(), + delegator_id: subaccount_0_location.clone(), + validators: valis_2.clone(), + } + ), + 1001 + )) + ); + + // confirm call + assert_ok!(Slp::confirm_validators_by_delegator_query_response( + Origin::root(), + RelayCurrencyId::get(), + 2, + )); + + // After delegation data: delegate 2 validators. + assert_eq!( + Slp::get_validators_by_delegator(RelayCurrencyId::get(), subaccount_0_location.clone()), + Some(valis_2) + ); + + assert_eq!(Slp::get_validators_by_delegator_update_entry(2), None); + }); +} + +#[test] +fn fail_validators_by_delegator_query_response_works() { + // bond 1 ksm for sub-account index 0 + register_validators(); + locally_bond_subaccount_0_1ksm_in_kusama(); + register_subaccount_index_0(); + register_delegator_ledger(); + let subaccount_0 = subaccount_0(); + + // GsvVmjr1CBHwQHw84pPHMDxgNY3iBLz6Qn7qS3CH8qPhrHz + let validator_0: AccountId = + hex_literal::hex!["be5ddb1579b72e84524fc29e78609e3caf42e85aa118ebfe0b0ad404b5bdd25f"] + .into(); + + // JKspFU6ohf1Grg3Phdzj2pSgWvsYWzSfKghhfzMbdhNBWs5 + let validator_1: AccountId = + hex_literal::hex!["fe65717dad0447d715f660a0a58411de509b42e6efb8375f562f58a554d5860e"] + .into(); + + Bifrost::execute_with(|| { + let subaccount_0_32: [u8; 32] = + Slp::account_id_to_account_32(subaccount_0.clone()).unwrap(); + let subaccount_0_location: MultiLocation = + Slp::account_32_to_parent_location(subaccount_0_32).unwrap(); + + let mut targets = vec![]; + let mut valis = vec![]; + + let validator_0_32: [u8; 32] = Slp::account_id_to_account_32(validator_0.clone()).unwrap(); + let validator_0_location: MultiLocation = + Slp::account_32_to_parent_location(validator_0_32).unwrap(); + targets.push(validator_0_location.clone()); + let multi_hash_0 = + ::Hashing::hash(&validator_0_location.encode()); + + let validator_1_32: [u8; 32] = Slp::account_id_to_account_32(validator_1.clone()).unwrap(); + let validator_1_location: MultiLocation = + Slp::account_32_to_parent_location(validator_1_32).unwrap(); + targets.push(validator_1_location.clone()); + let multi_hash_1 = + ::Hashing::hash(&validator_1_location.encode()); + + valis.push((validator_1_location.clone(), multi_hash_1)); + valis.push((validator_0_location.clone(), multi_hash_0)); + + // delegate + assert_ok!(Slp::delegate( + Origin::root(), + RelayCurrencyId::get(), + subaccount_0_location.clone(), + targets.clone(), + )); + + // check before data: delegate nobody. + assert_eq!( + Slp::get_validators_by_delegator(RelayCurrencyId::get(), subaccount_0_location.clone()), + None + ); + + assert_eq!( + Slp::get_validators_by_delegator_update_entry(0), + Some(( + ValidatorsByDelegatorUpdateEntry::Substrate( + SubstrateValidatorsByDelegatorUpdateEntry { + currency_id: RelayCurrencyId::get(), + delegator_id: subaccount_0_location.clone(), + validators: valis, + } + ), + 1001 + )) + ); + + // call fail function + assert_ok!(Slp::fail_validators_by_delegator_query_response( + Origin::root(), + RelayCurrencyId::get(), + 0 + )); + + // check after data + assert_eq!( + Slp::get_validators_by_delegator(RelayCurrencyId::get(), subaccount_0_location.clone()), + None + ); + + assert_eq!(Slp::get_validators_by_delegator_update_entry(0), None); + }); +} diff --git a/node/cli/Cargo.toml b/node/cli/Cargo.toml index 7fb6fe2c39..754b12c239 100644 --- a/node/cli/Cargo.toml +++ b/node/cli/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "node-cli" -version = "0.9.32" +version = "0.9.40" authors = ["Liebi Technologies "] description = "Bifrost Parachain Node" build = "build.rs" @@ -37,7 +37,7 @@ node-primitives = { path = "../primitives" } # CLI-specific dependencies sc-cli = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.18", optional = true } try-runtime-cli = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.18", optional = true } -frame-try-runtime = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.18", optional = true } +frame-try-runtime = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.18", optional = true } frame-benchmarking-cli = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.18", optional = true } node-inspect = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.18", optional = true } @@ -65,22 +65,13 @@ cli = [ "sc-service", "substrate-build-script-utils", ] -runtime-benchmarks = [ "node-service/runtime-benchmarks" ] +runtime-benchmarks = ["node-service/runtime-benchmarks"] with-bifrost-runtime = [ "node-service/with-bifrost-kusama-runtime", "node-service/with-bifrost-polkadot-runtime", ] -with-bifrost-kusama-runtime = [ - "node-service/with-bifrost-kusama-runtime", -] -with-bifrost-polkadot-runtime = [ - "node-service/with-bifrost-polkadot-runtime", -] -with-all-runtime = [ - "with-bifrost-runtime", -] -try-runtime = [ - "node-service/try-runtime", - "try-runtime-cli", -] -fast-runtime = [ "node-service/fast-runtime" ] +with-bifrost-kusama-runtime = ["node-service/with-bifrost-kusama-runtime"] +with-bifrost-polkadot-runtime = ["node-service/with-bifrost-polkadot-runtime"] +with-all-runtime = ["with-bifrost-runtime"] +try-runtime = ["node-service/try-runtime", "try-runtime-cli"] +fast-runtime = ["node-service/fast-runtime"] diff --git a/node/primitives/src/lib.rs b/node/primitives/src/lib.rs index 532afe4245..0a78b6ff9a 100644 --- a/node/primitives/src/lib.rs +++ b/node/primitives/src/lib.rs @@ -20,6 +20,8 @@ #![cfg_attr(not(feature = "std"), no_std)] +use codec::MaxEncodedLen; +use scale_info::TypeInfo; use sp_core::{Decode, Encode, RuntimeDebug}; use sp_runtime::{ generic, @@ -137,3 +139,16 @@ pub enum ExtraFeeName { StatemineTransfer, NoExtraFee, } + +// For vtoken-minting and slp modules +#[derive(Encode, Decode, Clone, RuntimeDebug, PartialEq, Eq, TypeInfo, MaxEncodedLen)] +pub enum TimeUnit { + Era(#[codec(compact)] u32), + SlashingSpan(#[codec(compact)] u32), +} + +impl Default for TimeUnit { + fn default() -> Self { + TimeUnit::Era(0u32) + } +} diff --git a/node/primitives/src/traits.rs b/node/primitives/src/traits.rs index af929fd40a..c12095aa26 100644 --- a/node/primitives/src/traits.rs +++ b/node/primitives/src/traits.rs @@ -197,6 +197,46 @@ where } } +/// The interface to call VtokenMinting module functions. +pub trait VtokenMintingOperator { + /// Get the currency tokenpool amount. + fn get_token_pool(currency_id: CurrencyId) -> Balance; + + /// Increase the token amount for the storage "token_pool" in the VtokenMining module. + fn increase_token_pool(currency_id: CurrencyId, token_amount: Balance) -> DispatchResult; + + /// Decrease the token amount for the storage "token_pool" in the VtokenMining module. + fn decrease_token_pool(currency_id: CurrencyId, token_amount: Balance) -> DispatchResult; + + /// Update the ongoing era for a CurrencyId. + fn update_ongoing_time_unit(currency_id: CurrencyId, time_unit: TimeUnit) -> DispatchResult; + + /// Get the current era of a CurrencyId. + fn get_ongoing_time_unit(currency_id: CurrencyId) -> Option; + + /// Get the the unlocking records of a certain time unit. + fn get_unlock_records( + currency_id: CurrencyId, + time_unit: TimeUnit, + ) -> Option<(Balance, Vec)>; + + /// Revise the currency indexed unlocking record by some amount. + fn deduct_unlock_amount( + currency_id: CurrencyId, + index: u32, + deduct_amount: Balance, + ) -> DispatchResult; + + /// Get currency Entrance and Exit accounts.【entrance_account, exit_account】 + fn get_entrance_and_exit_accounts() -> (AccountId, AccountId); + + /// Get the token_unlock_ledger storage info to refund to the due era unlocking users. + fn get_token_unlock_ledger( + currency_id: CurrencyId, + index: u32, + ) -> Option<(AccountId, Balance, TimeUnit)>; +} + /// A mapping between AssetId and AssetMetadata. pub trait AssetIdMapping { /// Returns the AssetMetadata associated with a given ForeignAssetId. diff --git a/pallets/call-switchgear/src/mock.rs b/pallets/call-switchgear/src/mock.rs index d544073832..a273c895e0 100644 --- a/pallets/call-switchgear/src/mock.rs +++ b/pallets/call-switchgear/src/mock.rs @@ -16,8 +16,6 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -//! Mocks for the transaction pause module. - #![cfg(test)] use frame_support::{construct_runtime, ord_parameter_types, parameter_types}; diff --git a/pallets/call-switchgear/src/tests.rs b/pallets/call-switchgear/src/tests.rs index 54fb26e6ce..6da4a36754 100644 --- a/pallets/call-switchgear/src/tests.rs +++ b/pallets/call-switchgear/src/tests.rs @@ -16,8 +16,6 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -//! Unit tests for the transaction pause module. - #![cfg(test)] use frame_support::{assert_noop, assert_ok}; diff --git a/pallets/slp/Cargo.toml b/pallets/slp/Cargo.toml new file mode 100644 index 0000000000..901976afcf --- /dev/null +++ b/pallets/slp/Cargo.toml @@ -0,0 +1,69 @@ +[package] +name = "bifrost-slp" +version = "0.8.0" +authors = ["Herry Ho "] +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +codec = { package = "parity-scale-codec", version = "3.0.0", default-features = false, features = [ + "derive", +] } +scale-info = { version = "2.0.1", default-features = false, features = [ + "derive", +] } +serde = { version = "1.0.124", optional = true } +orml-traits = { version = "0.4.1-dev", default-features = false } +orml-tokens = { version = "0.4.1-dev", default-features = false } +orml-currencies = { version = "0.4.1-dev", default-features = false } +node-primitives = { path = "../../node/primitives", default-features = false } +frame-benchmarking = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.18", default-features = false, optional = true } +frame-support = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.18", default-features = false } +frame-system = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.18", default-features = false } +sp-arithmetic = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.18", default-features = false } +sp-std = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.18", default-features = false } +sp-runtime = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.18", default-features = false } +sp-core = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.18", default-features = false } +sp-io = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.18", default-features = false } +xcm = { git = "https://github.com/paritytech/polkadot", branch = "release-v0.9.18", default-features = false } +pallet-xcm = { git = "https://github.com/paritytech/polkadot", branch = "release-v0.9.18", default-features = false } +cumulus-primitives-core = { git = "https://github.com/paritytech/cumulus", branch = "polkadot-v0.9.18", default-features = false } +pallet-balances = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.18", default-features = false } +pallet-utility = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.18", default-features = false } + + +[dev-dependencies] +hex-literal = "0.3.1" +bifrost-vtoken-minting = { path = "../../pallets/vtoken-minting", default-features = false } + +[features] +default = ["std"] +std = [ + "codec/std", + "scale-info/std", + "frame-system/std", + "frame-support/std", + "sp-runtime/std", + "sp-arithmetic/std", + "serde/std", + "orml-traits/std", + "orml-tokens/std", + "orml-currencies/std", + "node-primitives/std", + "sp-core/std", + "sp-io/std", + "sp-std/std", + "xcm/std", + "pallet-xcm/std", + "cumulus-primitives-core/std", + "pallet-balances/std", + "pallet-utility/std", +] + +runtime-benchmarks = [ + "frame-benchmarking", + "frame-support/runtime-benchmarks", + "frame-system/runtime-benchmarks", + "pallet-xcm/runtime-benchmarks", +] diff --git a/pallets/slp/src/agents/kusama_agent/agent.rs b/pallets/slp/src/agents/kusama_agent/agent.rs new file mode 100644 index 0000000000..8aadba1fb5 --- /dev/null +++ b/pallets/slp/src/agents/kusama_agent/agent.rs @@ -0,0 +1,1378 @@ +// This file is part of Bifrost. + +// Copyright (C) 2019-2022 Liebi Technologies (UK) Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +// This program 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. + +// This program 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 this program. If not, see . + +use core::marker::PhantomData; + +use codec::Encode; +use cumulus_primitives_core::relay_chain::HashT; +pub use cumulus_primitives_core::ParaId; +use frame_support::{ensure, traits::Get, weights::Weight}; +use frame_system::pallet_prelude::BlockNumberFor; +use node_primitives::{CurrencyId, TokenSymbol, VtokenMintingOperator}; +use orml_traits::MultiCurrency; +use sp_core::U256; +use sp_runtime::{ + traits::{ + CheckedAdd, CheckedSub, Convert, Saturating, StaticLookup, UniqueSaturatedFrom, + UniqueSaturatedInto, Zero, + }, + DispatchResult, +}; +use sp_std::prelude::*; +use xcm::{ + latest::prelude::*, + opaque::latest::{ + Junction::{AccountId32, Parachain}, + Junctions::X1, + MultiLocation, + }, + VersionedMultiAssets, VersionedMultiLocation, +}; + +use crate::{ + agents::{KusamaCall, RewardDestination, StakingCall, SystemCall, UtilityCall, XcmCall}, + pallet::{Error, Event}, + primitives::{ + Ledger, SubstrateLedger, SubstrateLedgerUpdateEntry, + SubstrateValidatorsByDelegatorUpdateEntry, UnlockChunk, ValidatorsByDelegatorUpdateEntry, + XcmOperation, KSM, + }, + traits::{QueryResponseManager, StakingAgent, XcmBuilder}, + AccountIdOf, BalanceOf, Config, CurrencyDelays, DelegatorLedgerXcmUpdateQueue, + DelegatorLedgers, DelegatorNextIndex, DelegatorsIndex2Multilocation, + DelegatorsMultilocation2Index, Hash, LedgerUpdateEntry, MinimumsAndMaximums, Pallet, QueryId, + TimeUnit, Validators, ValidatorsByDelegator, ValidatorsByDelegatorXcmUpdateQueue, + XcmDestWeightAndFee, TIMEOUT_BLOCKS, +}; + +/// StakingAgent implementation for Kusama +pub struct KusamaAgent(PhantomData); + +impl KusamaAgent { + pub fn new() -> Self { + KusamaAgent(PhantomData::) + } +} + +impl + StakingAgent< + MultiLocation, + MultiLocation, + BalanceOf, + TimeUnit, + AccountIdOf, + MultiLocation, + QueryId, + LedgerUpdateEntry, MultiLocation>, + ValidatorsByDelegatorUpdateEntry>, + Error, + > for KusamaAgent +{ + fn initialize_delegator(&self) -> Result> { + let new_delegator_id = DelegatorNextIndex::::get(KSM); + DelegatorNextIndex::::mutate(KSM, |id| -> Result<(), Error> { + let option_new_id = id.checked_add(1).ok_or(Error::::OverFlow)?; + *id = option_new_id; + Ok(()) + })?; + + // Generate multi-location by id. + let delegator_multilocation = T::AccountConverter::convert(new_delegator_id); + + // Add the new delegator into storage + Self::add_delegator(&self, new_delegator_id, &delegator_multilocation) + .map_err(|_| Error::::FailToAddDelegator)?; + + Ok(delegator_multilocation) + } + + /// First time bonding some amount to a delegator. + fn bond(&self, who: &MultiLocation, amount: BalanceOf) -> Result> { + // Check if it is bonded already. + ensure!(DelegatorLedgers::::get(KSM, who).is_none(), Error::::AlreadyBonded); + + // Check if the amount exceeds the minimum requirement. + let mins_maxs = MinimumsAndMaximums::::get(KSM).ok_or(Error::::NotExist)?; + ensure!(amount >= mins_maxs.delegator_bonded_minimum, Error::::LowerThanMinimum); + + // Ensure the bond doesn't exceeds delegator_active_staking_maximum + ensure!( + amount <= mins_maxs.delegator_active_staking_maximum, + Error::::ExceedActiveMaximum + ); + + // Get the delegator account id in Kusama network + let delegator_account = Pallet::::multilocation_to_account(who)?; + + // Construct xcm message. + let call = KusamaCall::Staking(StakingCall::Bond( + T::Lookup::unlookup(delegator_account), + amount, + RewardDestination::>::Staked, + )); + + // Wrap the xcm message as it is sent from a subaccount of the parachain account, and + // send it out. + let (query_id, timeout, xcm_message) = + Self::construct_xcm_as_subaccount_with_query_id(XcmOperation::Bond, call, who)?; + + // Create a new delegator ledger + // The real bonded amount will be updated by services once the xcm transaction succeeds. + let ledger = SubstrateLedger::> { + account: who.clone(), + total: Zero::zero(), + active: Zero::zero(), + unlocking: vec![], + }; + let sub_ledger = Ledger::>::Substrate(ledger); + + DelegatorLedgers::::insert(KSM, who, sub_ledger); + + // Insert a delegator ledger update record into DelegatorLedgerXcmUpdateQueue. + Self::insert_delegator_ledger_update_entry( + who, true, false, false, amount, query_id, timeout, + )?; + + // Send out the xcm message. + T::XcmRouter::send_xcm(Parent, xcm_message).map_err(|_e| Error::::XcmFailure)?; + + Ok(query_id) + } + + /// Bond extra amount to a delegator. + fn bond_extra(&self, who: &MultiLocation, amount: BalanceOf) -> Result> { + // Check if it is bonded already. + let ledger = DelegatorLedgers::::get(KSM, who).ok_or(Error::::DelegatorNotBonded)?; + + // Check if the amount exceeds the minimum requirement. + let mins_maxs = MinimumsAndMaximums::::get(KSM).ok_or(Error::::NotExist)?; + ensure!(amount >= mins_maxs.bond_extra_minimum, Error::::LowerThanMinimum); + + // Check if the new_add_amount + active_staking_amount doesn't exceeds + // delegator_active_staking_maximum + let active = if let Ledger::Substrate(substrate_ledger) = ledger { + substrate_ledger.active + } else { + Err(Error::::Unexpected)? + }; + + let total = amount.checked_add(&active).ok_or(Error::::OverFlow)?; + ensure!( + total <= mins_maxs.delegator_active_staking_maximum, + Error::::ExceedActiveMaximum + ); + // Construct xcm message.. + let call = KusamaCall::Staking(StakingCall::BondExtra(amount)); + + // Wrap the xcm message as it is sent from a subaccount of the parachain account, and + // send it out. + let (query_id, timeout, xcm_message) = + Self::construct_xcm_as_subaccount_with_query_id(XcmOperation::BondExtra, call, who)?; + + // Insert a delegator ledger update record into DelegatorLedgerXcmUpdateQueue. + Self::insert_delegator_ledger_update_entry( + who, true, false, false, amount, query_id, timeout, + )?; + + // Send out the xcm message. + T::XcmRouter::send_xcm(Parent, xcm_message).map_err(|_e| Error::::XcmFailure)?; + + Ok(query_id) + } + + /// Decrease bonding amount to a delegator. + fn unbond(&self, who: &MultiLocation, amount: BalanceOf) -> Result> { + // Check if it is bonded already. + let ledger = DelegatorLedgers::::get(KSM, who).ok_or(Error::::DelegatorNotBonded)?; + + let (active_staking, unlocking_num) = if let Ledger::Substrate(substrate_ledger) = ledger { + (substrate_ledger.active, substrate_ledger.unlocking.len() as u32) + } else { + Err(Error::::Unexpected)? + }; + + // Check if the unbonding amount exceeds minimum requirement. + let mins_maxs = MinimumsAndMaximums::::get(KSM).ok_or(Error::::NotExist)?; + ensure!(amount >= mins_maxs.unbond_minimum, Error::::LowerThanMinimum); + + // Check if the remaining active balance is enough for (unbonding amount + minimum + // bonded amount) + let remaining = active_staking.checked_sub(&amount).ok_or(Error::::NotEnoughToUnbond)?; + ensure!(remaining >= mins_maxs.delegator_bonded_minimum, Error::::NotEnoughToUnbond); + + // Check if this unbonding will exceed the maximum unlocking records bound for a single + // delegator. + ensure!( + unlocking_num < mins_maxs.unbond_record_maximum, + Error::::ExceedUnlockingRecords + ); + + // Construct xcm message. + let call = KusamaCall::Staking(StakingCall::Unbond(amount)); + + // Wrap the xcm message as it is sent from a subaccount of the parachain account, and + // send it out. + let (query_id, timeout, xcm_message) = + Self::construct_xcm_as_subaccount_with_query_id(XcmOperation::Unbond, call, who)?; + + // Insert a delegator ledger update record into DelegatorLedgerXcmUpdateQueue. + Self::insert_delegator_ledger_update_entry( + who, false, true, false, amount, query_id, timeout, + )?; + + // Send out the xcm message. + T::XcmRouter::send_xcm(Parent, xcm_message).map_err(|_e| Error::::XcmFailure)?; + + Ok(query_id) + } + + /// Unbonding all amount of a delegator. Differentiate from regular unbonding. + fn unbond_all(&self, who: &MultiLocation) -> Result> { + // Get the active amount of a delegator. + let ledger = DelegatorLedgers::::get(KSM, who).ok_or(Error::::DelegatorNotBonded)?; + + let amount = if let Ledger::Substrate(substrate_ledger) = ledger { + substrate_ledger.active + } else { + Err(Error::::Unexpected)? + }; + + // Construct xcm message. + let call = KusamaCall::Staking(StakingCall::Unbond(amount)); + + // Wrap the xcm message as it is sent from a subaccount of the parachain account, and + // send it out. + let (query_id, timeout, xcm_message) = + Self::construct_xcm_as_subaccount_with_query_id(XcmOperation::Unbond, call, who)?; + + // Insert a delegator ledger update record into DelegatorLedgerXcmUpdateQueue. + Self::insert_delegator_ledger_update_entry( + who, false, true, false, amount, query_id, timeout, + )?; + + // Send out the xcm message. + T::XcmRouter::send_xcm(Parent, xcm_message).map_err(|_e| Error::::XcmFailure)?; + + Ok(query_id) + } + + /// Cancel some unbonding amount. + fn rebond(&self, who: &MultiLocation, amount: BalanceOf) -> Result> { + // Check if it is bonded already. + let ledger = DelegatorLedgers::::get(KSM, who).ok_or(Error::::DelegatorNotBonded)?; + + // Check if the rebonding amount exceeds minimum requirement. + let mins_maxs = MinimumsAndMaximums::::get(KSM).ok_or(Error::::NotExist)?; + ensure!(amount >= mins_maxs.rebond_minimum, Error::::LowerThanMinimum); + + // Get the delegator ledger + let unlock_chunk_list = if let Ledger::Substrate(substrate_ledger) = ledger { + substrate_ledger.unlocking + } else { + Err(Error::::Unexpected)? + }; + + // Check if the delegator unlocking amount is greater than or equal to the rebond amount. + let mut total_unlocking: BalanceOf = Zero::zero(); + for UnlockChunk { value, unlock_time: _ } in unlock_chunk_list.iter() { + total_unlocking = total_unlocking.checked_add(value).ok_or(Error::::OverFlow)?; + } + ensure!(total_unlocking >= amount, Error::::RebondExceedUnlockingAmount); + + // Construct xcm message. + let call = KusamaCall::Staking(StakingCall::Rebond(amount)); + + // Wrap the xcm message as it is sent from a subaccount of the parachain account, and + // send it out. + let (query_id, timeout, xcm_message) = + Self::construct_xcm_as_subaccount_with_query_id(XcmOperation::Rebond, call, who)?; + + // Insert a delegator ledger update record into DelegatorLedgerXcmUpdateQueue. + Self::insert_delegator_ledger_update_entry( + who, false, false, true, amount, query_id, timeout, + )?; + + // Send out the xcm message. + T::XcmRouter::send_xcm(Parent, xcm_message).map_err(|_e| Error::::XcmFailure)?; + + Ok(query_id) + } + + /// Delegate to some validators. For Kusama, it equals function Nominate. + fn delegate( + &self, + who: &MultiLocation, + targets: &Vec, + ) -> Result> { + // Check if it is bonded already. + ensure!(DelegatorLedgers::::contains_key(KSM, who), Error::::DelegatorNotBonded); + + // Check if targets vec is empty. + let vec_len = targets.len() as u32; + ensure!(vec_len > Zero::zero(), Error::::VectorEmpty); + + // Check if targets exceeds validators_back_maximum requirement. + let mins_maxs = MinimumsAndMaximums::::get(KSM).ok_or(Error::::NotExist)?; + ensure!(vec_len <= mins_maxs.validators_back_maximum, Error::::GreaterThanMaximum); + + // Sort validators and remove duplicates + let sorted_dedup_list = Pallet::::sort_validators_and_remove_duplicates(KSM, targets)?; + + // Convert vec of multilocations into accounts. + let mut accounts = vec![]; + for (multilocation_account, _hash) in sorted_dedup_list.clone().iter() { + let account = Pallet::::multilocation_to_account(multilocation_account)?; + let unlookup_account = T::Lookup::unlookup(account); + accounts.push(unlookup_account); + } + + // Construct xcm message. + let call = KusamaCall::Staking(StakingCall::Nominate(accounts)); + + // Wrap the xcm message as it is sent from a subaccount of the parachain account, and + // send it out. + let (query_id, timeout, xcm_message) = + Self::construct_xcm_as_subaccount_with_query_id(XcmOperation::Delegate, call, who)?; + + // Insert a query record to the ValidatorsByDelegatorXcmUpdateQueue storage. + Self::insert_validators_by_delegator_update_entry( + who, + sorted_dedup_list, + query_id, + timeout, + )?; + + // Send out the xcm message. + T::XcmRouter::send_xcm(Parent, xcm_message).map_err(|_e| Error::::XcmFailure)?; + + Ok(query_id) + } + + /// Remove delegation relationship with some validators. + fn undelegate( + &self, + who: &MultiLocation, + targets: &Vec, + ) -> Result> { + // Check if it is bonded already. + ensure!(DelegatorLedgers::::contains_key(KSM, who), Error::::DelegatorNotBonded); + + // Check if targets vec is empty. + let vec_len = targets.len() as u32; + ensure!(vec_len > Zero::zero(), Error::::VectorEmpty); + + // Get the original delegated validators. + let original_set = + ValidatorsByDelegator::::get(KSM, who).ok_or(Error::::ValidatorSetNotExist)?; + + // Remove targets from the original set to make a new set. + let mut new_set: Vec<(MultiLocation, Hash)> = vec![]; + for (acc, acc_hash) in original_set.iter() { + if !targets.contains(acc) { + new_set.push((acc.clone(), acc_hash.clone())) + } + } + + // Ensure new set is not empty. + ensure!(new_set.len() > Zero::zero(), Error::::VectorEmpty); + + // Convert new targets into account vec. + let mut accounts = vec![]; + for (multilocation_account, _hash) in new_set.iter() { + let account = Pallet::::multilocation_to_account(multilocation_account)?; + let unlookup_account = T::Lookup::unlookup(account); + accounts.push(unlookup_account); + } + + // Construct xcm message. + let call = KusamaCall::Staking(StakingCall::Nominate(accounts)); + + // Wrap the xcm message as it is sent from a subaccount of the parachain account, and + // send it out. + let (query_id, timeout, xcm_message) = + Self::construct_xcm_as_subaccount_with_query_id(XcmOperation::Delegate, call, who)?; + + // Insert a query record to the ValidatorsByDelegatorXcmUpdateQueue storage. + Self::insert_validators_by_delegator_update_entry(who, new_set, query_id, timeout)?; + + // Send out the xcm message. + T::XcmRouter::send_xcm(Parent, xcm_message).map_err(|_e| Error::::XcmFailure)?; + + Ok(query_id) + } + + /// Re-delegate existing delegation to a new validator set. + fn redelegate( + &self, + who: &MultiLocation, + targets: &Vec, + ) -> Result> { + let query_id = Self::delegate(&self, who, targets)?; + Ok(query_id) + } + + /// Initiate payout for a certain delegator. + fn payout( + &self, + who: &MultiLocation, + validator: &MultiLocation, + when: &Option, + ) -> Result<(), Error> { + // Get the validator account + let validator_account = Pallet::::multilocation_to_account(&validator)?; + + // Get the payout era + let payout_era = if let Some(TimeUnit::Era(payout_era)) = *when { + payout_era + } else { + Err(Error::::InvalidTimeUnit)? + }; + // Construct xcm message. + let call = KusamaCall::Staking(StakingCall::PayoutStakers(validator_account, payout_era)); + + // Wrap the xcm message as it is sent from a subaccount of the parachain account, and + // send it out. + Self::construct_xcm_and_send_as_subaccount_without_query_id( + XcmOperation::Payout, + call, + who, + )?; + + // Both tokenpool increment and delegator ledger update need to be conducted by backend + // services. + + Ok(()) + } + + /// Withdraw the due payout into free balance. + fn liquidize(&self, who: &MultiLocation, when: &Option) -> Result> { + // Check if it is in the delegator set. + ensure!( + DelegatorsMultilocation2Index::::contains_key(KSM, who), + Error::::DelegatorNotExist + ); + + // Get the slashing span param. + let num_slashing_spans = if let Some(TimeUnit::SlashingSpan(num_slashing_spans)) = *when { + num_slashing_spans + } else { + Err(Error::::InvalidTimeUnit)? + }; + + // Construct xcm message. + let call = KusamaCall::Staking(StakingCall::WithdrawUnbonded(num_slashing_spans)); + + // Wrap the xcm message as it is sent from a subaccount of the parachain account, and + // send it out. + let (query_id, timeout, xcm_message) = + Self::construct_xcm_as_subaccount_with_query_id(XcmOperation::Liquidize, call, who)?; + + // Insert a delegator ledger update record into DelegatorLedgerXcmUpdateQueue. + Self::insert_delegator_ledger_update_entry( + who, + false, + false, + false, + Zero::zero(), + query_id, + timeout, + )?; + + // Send out the xcm message. + T::XcmRouter::send_xcm(Parent, xcm_message).map_err(|_e| Error::::XcmFailure)?; + + Ok(query_id) + } + + /// Chill self. Cancel the identity of delegator in the Relay chain side. + /// Unbonding all the active amount should be done before or after chill, + /// so that we can collect back all the bonded amount. + fn chill(&self, who: &MultiLocation) -> Result> { + // Check if it is in the delegator set. + ensure!( + DelegatorsMultilocation2Index::::contains_key(KSM, who), + Error::::DelegatorNotExist + ); + + // Construct xcm message. + let call = KusamaCall::Staking(StakingCall::Chill); + + // Wrap the xcm message as it is sent from a subaccount of the parachain account, and + // send it out. + let (query_id, timeout, xcm_message) = + Self::construct_xcm_as_subaccount_with_query_id(XcmOperation::Chill, call, who)?; + + // Get active amount, if not zero, create an update entry. + let ledger = DelegatorLedgers::::get(KSM, who).ok_or(Error::::DelegatorNotBonded)?; + + let amount = if let Ledger::Substrate(substrate_ledger) = ledger { + substrate_ledger.active + } else { + Err(Error::::Unexpected)? + }; + + // Insert a delegator ledger update record into DelegatorLedgerXcmUpdateQueue. + Self::insert_delegator_ledger_update_entry( + who, false, true, false, amount, query_id, timeout, + )?; + + // Send out the xcm message. + T::XcmRouter::send_xcm(Parent, xcm_message).map_err(|_e| Error::::XcmFailure)?; + + Ok(query_id) + } + + /// Make token transferred back to Bifrost chain account. + fn transfer_back( + &self, + from: &MultiLocation, + to: &MultiLocation, + amount: BalanceOf, + ) -> Result<(), Error> { + // Ensure amount is greater than zero. + ensure!(!amount.is_zero(), Error::::AmountZero); + + // Check if from is one of our delegators. If not, return error. + DelegatorsMultilocation2Index::::get(KSM, from).ok_or(Error::::DelegatorNotExist)?; + + // Make sure the receiving account is the Exit_account from vtoken-minting module. + let to_account_id = Pallet::::multilocation_to_account(&to)?; + let (_, exit_account) = T::VtokenMinting::get_entrance_and_exit_accounts(); + ensure!(to_account_id == exit_account, Error::::InvalidAccount); + + // Prepare parameter dest and beneficiary. + let to_32: [u8; 32] = Pallet::::multilocation_to_account_32(&to)?; + + let dest = + Box::new(VersionedMultiLocation::from(X1(Parachain(T::ParachainId::get().into())))); + let beneficiary = + Box::new(VersionedMultiLocation::from(X1(AccountId32 { network: Any, id: to_32 }))); + + // Prepare parameter assets. + let asset = MultiAsset { + fun: Fungible(amount.unique_saturated_into()), + id: Concrete(MultiLocation { parents: 0, interior: Here }), + }; + let assets: Box = + Box::new(VersionedMultiAssets::from(MultiAssets::from(asset))); + + // Prepare parameter fee_asset_item. + let fee_asset_item: u32 = 0; + + // Construct xcm message. + let call = KusamaCall::Xcm(Box::new(XcmCall::ReserveTransferAssets( + dest, + beneficiary, + assets, + fee_asset_item, + ))); + + // Wrap the xcm message as it is sent from a subaccount of the parachain account, and + // send it out. + Self::construct_xcm_and_send_as_subaccount_without_query_id( + XcmOperation::TransferBack, + call, + from, + )?; + + Ok(()) + } + + /// Make token from Bifrost chain account to the staking chain account. + /// Receiving account must be one of the KSM delegators. + fn transfer_to( + &self, + from: &MultiLocation, + to: &MultiLocation, + amount: BalanceOf, + ) -> Result<(), Error> { + // Make sure receiving account is one of the KSM delegators. + ensure!( + DelegatorsMultilocation2Index::::contains_key(KSM, to), + Error::::DelegatorNotExist + ); + + // Make sure from account is the entrance account of vtoken-minting module. + let from_account_id = Pallet::::multilocation_to_account(&from)?; + let (entrance_account, _) = T::VtokenMinting::get_entrance_and_exit_accounts(); + ensure!(from_account_id == entrance_account, Error::::InvalidAccount); + + Self::do_transfer_to(from, to, amount)?; + + Ok(()) + } + + fn tune_vtoken_exchange_rate( + &self, + who: &MultiLocation, + token_amount: BalanceOf, + _vtoken_amount: BalanceOf, + ) -> Result<(), Error> { + ensure!(!token_amount.is_zero(), Error::::AmountZero); + + // Check whether "who" is an existing delegator. + ensure!(DelegatorLedgers::::contains_key(KSM, who), Error::::DelegatorNotBonded); + + // Tune the vtoken exchange rate. + T::VtokenMinting::increase_token_pool(KSM, token_amount) + .map_err(|_| Error::::IncreaseTokenPoolError)?; + + // update delegator ledger + DelegatorLedgers::::mutate(KSM, who, |old_ledger| -> Result<(), Error> { + if let Some(Ledger::Substrate(ref mut old_sub_ledger)) = old_ledger { + // Increase both the active and total amount. + old_sub_ledger.active = + old_sub_ledger.active.checked_add(&token_amount).ok_or(Error::::OverFlow)?; + + old_sub_ledger.total = + old_sub_ledger.total.checked_add(&token_amount).ok_or(Error::::OverFlow)?; + Ok(()) + } else { + Err(Error::::Unexpected)? + } + })?; + + Ok(()) + } + + /// Add a new serving delegator for a particular currency. + fn add_delegator(&self, index: u16, who: &MultiLocation) -> DispatchResult { + // Check if the delegator already exists. If yes, return error. + ensure!( + !DelegatorsIndex2Multilocation::::contains_key(KSM, index), + Error::::AlreadyExist + ); + + // Revise two delegator storages. + DelegatorsIndex2Multilocation::::insert(KSM, index, who); + DelegatorsMultilocation2Index::::insert(KSM, who, index); + + Ok(()) + } + + /// Remove an existing serving delegator for a particular currency. + fn remove_delegator(&self, who: &MultiLocation) -> DispatchResult { + // Check if the delegator exists. + let index = DelegatorsMultilocation2Index::::get(KSM, who) + .ok_or(Error::::DelegatorNotExist)?; + + // Get the delegator ledger + let ledger = DelegatorLedgers::::get(KSM, who).ok_or(Error::::DelegatorNotBonded)?; + + let total = if let Ledger::Substrate(substrate_ledger) = ledger { + substrate_ledger.total + } else { + Err(Error::::Unexpected)? + }; + + // Check if ledger total amount is zero. If not, return error. + ensure!(total.is_zero(), Error::::AmountNotZero); + + // Remove corresponding storage. + DelegatorsIndex2Multilocation::::remove(KSM, index); + DelegatorsMultilocation2Index::::remove(KSM, who); + DelegatorLedgers::::remove(KSM, who); + + Ok(()) + } + + /// Add a new serving delegator for a particular currency. + fn add_validator(&self, who: &MultiLocation) -> DispatchResult { + let multi_hash = T::Hashing::hash(&who.encode()); + // Check if the validator already exists. + let validators_set = Validators::::get(KSM); + if validators_set.is_none() { + Validators::::insert(KSM, vec![(who, multi_hash)]); + } else { + // Change corresponding storage. + Validators::::mutate(KSM, |validator_vec| -> Result<(), Error> { + if let Some(ref mut validator_list) = validator_vec { + let rs = + validator_list.binary_search_by_key(&multi_hash, |(_multi, hash)| *hash); + + if let Err(index) = rs { + validator_list.insert(index, (who.clone(), multi_hash)); + } else { + Err(Error::::AlreadyExist)? + } + } + Ok(()) + })?; + } + + Ok(()) + } + + /// Remove an existing serving delegator for a particular currency. + fn remove_validator(&self, who: &MultiLocation) -> DispatchResult { + // Check if the validator already exists. + let validators_set = Validators::::get(KSM).ok_or(Error::::ValidatorSetNotExist)?; + + let multi_hash = T::Hashing::hash(&who.encode()); + ensure!(validators_set.contains(&(who.clone(), multi_hash)), Error::::ValidatorNotExist); + + // Check if ValidatorsByDelegator involves this validator. If yes, return error. + for validator_list in ValidatorsByDelegator::::iter_prefix_values(KSM) { + if validator_list.contains(&(who.clone(), multi_hash)) { + Err(Error::::ValidatorStillInUse)?; + } + } + // Update corresponding storage. + Validators::::mutate(KSM, |validator_vec| { + if let Some(ref mut validator_list) = validator_vec { + let rs = validator_list.binary_search_by_key(&multi_hash, |(_multi, hash)| *hash); + + if let Ok(index) = rs { + validator_list.remove(index); + } + } + }); + + Ok(()) + } + + /// Charge hosting fee. + fn charge_hosting_fee( + &self, + amount: BalanceOf, + _from: &MultiLocation, + to: &MultiLocation, + ) -> DispatchResult { + // Get current VKSM/KSM exchange rate. + let vksm_issuance = T::MultiCurrency::total_issuance(CurrencyId::VToken(TokenSymbol::KSM)); + let ksm_pool = T::VtokenMinting::get_token_pool(KSM); + // Calculate how much vksm the beneficiary account can get. + let amount: u128 = amount.unique_saturated_into(); + let vksm_issuance: u128 = vksm_issuance.unique_saturated_into(); + let ksm_pool: u128 = ksm_pool.unique_saturated_into(); + let can_get_vksm = U256::from(amount) + .checked_mul(U256::from(vksm_issuance)) + .and_then(|n| n.checked_div(U256::from(ksm_pool))) + .and_then(|n| TryInto::::try_into(n).ok()) + .unwrap_or_else(Zero::zero); + + let beneficiary = Pallet::::multilocation_to_account(&to)?; + // Issue corresponding vksm to beneficiary account. + T::MultiCurrency::deposit( + CurrencyId::VToken(TokenSymbol::KSM), + &beneficiary, + BalanceOf::::unique_saturated_from(can_get_vksm), + )?; + + Ok(()) + } + + /// Deposit some amount as fee to nominator accounts. + fn supplement_fee_reserve( + &self, + amount: BalanceOf, + from: &MultiLocation, + to: &MultiLocation, + ) -> DispatchResult { + Self::do_transfer_to(from, to, amount)?; + + Ok(()) + } + + fn check_delegator_ledger_query_response( + &self, + query_id: QueryId, + entry: LedgerUpdateEntry, MultiLocation>, + manual_mode: bool, + ) -> Result> { + // If this is manual mode, it is always updatable. + let should_update = if manual_mode { + true + } else { + T::SubstrateResponseManager::get_query_response_record(query_id) + }; + + // Update corresponding storages. + if should_update { + Self::update_ledger_query_response_storage(query_id, entry.clone())?; + + // Deposit event. + Pallet::::deposit_event(Event::DelegatorLedgerQueryResponseConfirmed { + query_id, + entry, + }); + } + + Ok(should_update) + } + + fn check_validators_by_delegator_query_response( + &self, + query_id: QueryId, + entry: ValidatorsByDelegatorUpdateEntry>, + manual_mode: bool, + ) -> Result> { + let should_update = if manual_mode { + true + } else { + T::SubstrateResponseManager::get_query_response_record(query_id) + }; + + // Update corresponding storages. + if should_update { + Self::update_validators_by_delegator_query_response_storage(query_id, entry.clone())?; + + // Deposit event. + Pallet::::deposit_event(Event::ValidatorsByDelegatorQueryResponseConfirmed { + query_id, + entry, + }); + } + + Ok(should_update) + } + + fn fail_delegator_ledger_query_response(&self, query_id: QueryId) -> Result<(), Error> { + // delete pallet_xcm query + T::SubstrateResponseManager::remove_query_record(query_id); + + // delete update entry + DelegatorLedgerXcmUpdateQueue::::remove(query_id); + + // Deposit event. + Pallet::::deposit_event(Event::DelegatorLedgerQueryResponseFailSuccessfully { + query_id, + }); + + Ok(()) + } + + fn fail_validators_by_delegator_query_response( + &self, + query_id: QueryId, + ) -> Result<(), Error> { + // delete pallet_xcm query + T::SubstrateResponseManager::remove_query_record(query_id); + + // delete update entry + ValidatorsByDelegatorXcmUpdateQueue::::remove(query_id); + + // Deposit event. + Pallet::::deposit_event(Event::ValidatorsByDelegatorQueryResponseFailSuccessfully { + query_id, + }); + + Ok(()) + } +} + +/// Trait XcmBuilder implementation for Kusama +impl + XcmBuilder< + BalanceOf, + KusamaCall, // , MultiLocation, + > for KusamaAgent +{ + fn construct_xcm_message_with_query_id( + call: KusamaCall, + extra_fee: BalanceOf, + weight: Weight, + _query_id: QueryId, + // response_back_location: MultiLocation + ) -> Xcm<()> { + let asset = MultiAsset { + id: Concrete(MultiLocation::here()), + fun: Fungibility::Fungible(extra_fee.unique_saturated_into()), + }; + + //【For xcm v3】 + // // Add one more field for reporting transact status + // Xcm(vec![ + // WithdrawAsset(asset.clone().into()), + // BuyExecution { fees: asset, weight_limit: Unlimited }, + // Transact { + // origin_type: OriginKind::SovereignAccount, + // require_weight_at_most: weight, + // call: call.encode().into(), + // }, + // ReportTransactStatus(QueryResponseInfo {query_id, response_back_location, max_weight:0}), + // RefundSurplus, + // DepositAsset { + // assets: All.into(), + // max_assets: u32::max_value(), + // beneficiary: MultiLocation { + // parents: 0, + // interior: X1(Parachain(T::ParachainId::get().into())), + // }, + // }, + // ]) + + Xcm(vec![ + WithdrawAsset(asset.clone().into()), + BuyExecution { fees: asset, weight_limit: Unlimited }, + Transact { + origin_type: OriginKind::SovereignAccount, + require_weight_at_most: weight, + call: call.encode().into(), + }, + RefundSurplus, + DepositAsset { + assets: All.into(), + max_assets: u32::max_value(), + beneficiary: MultiLocation { + parents: 0, + interior: X1(Parachain(T::ParachainId::get().into())), + }, + }, + ]) + // } + } + + fn construct_xcm_message_without_query_id( + call: KusamaCall, + extra_fee: BalanceOf, + weight: Weight, + ) -> Xcm<()> { + let asset = MultiAsset { + id: Concrete(MultiLocation::here()), + fun: Fungibility::Fungible(extra_fee.unique_saturated_into()), + }; + + Xcm(vec![ + WithdrawAsset(asset.clone().into()), + BuyExecution { fees: asset, weight_limit: Unlimited }, + Transact { + origin_type: OriginKind::SovereignAccount, + require_weight_at_most: weight, + call: call.encode().into(), + }, + RefundSurplus, + DepositAsset { + assets: All.into(), + max_assets: u32::max_value(), + beneficiary: MultiLocation { + parents: 0, + interior: X1(Parachain(T::ParachainId::get().into())), + }, + }, + ]) + } +} + +/// Internal functions. +impl KusamaAgent { + fn prepare_send_as_subaccount_call_params_with_query_id( + operation: XcmOperation, + call: KusamaCall, + who: &MultiLocation, + query_id: QueryId, + ) -> Result<(KusamaCall, BalanceOf, Weight), Error> { + // Get the delegator sub-account index. + let sub_account_index = DelegatorsMultilocation2Index::::get(KSM, who) + .ok_or(Error::::DelegatorNotExist)?; + + // Temporary wrapping remark event in Kusama for ease use of backend service. + let remark_call = + KusamaCall::System(SystemCall::RemarkWithEvent(Box::new(query_id.encode()))); + + let call_batched_with_remark = + KusamaCall::Utility(Box::new(UtilityCall::BatchAll(Box::new(vec![ + Box::new(call), + Box::new(remark_call), + ])))); + + let call_as_subaccount = KusamaCall::Utility(Box::new(UtilityCall::AsDerivative( + sub_account_index, + Box::new(call_batched_with_remark), + ))); + + let (weight, fee) = XcmDestWeightAndFee::::get(KSM, operation) + .ok_or(Error::::WeightAndFeeNotExists)?; + + Ok((call_as_subaccount, fee, weight)) + } + + fn prepare_send_as_subaccount_call_params_without_query_id( + operation: XcmOperation, + call: KusamaCall, + who: &MultiLocation, + ) -> Result<(KusamaCall, BalanceOf, Weight), Error> { + // Get the delegator sub-account index. + let sub_account_index = DelegatorsMultilocation2Index::::get(KSM, who) + .ok_or(Error::::DelegatorNotExist)?; + + let call_as_subaccount = KusamaCall::Utility(Box::new(UtilityCall::AsDerivative( + sub_account_index, + Box::new(call), + ))); + + let (weight, fee) = XcmDestWeightAndFee::::get(KSM, operation) + .ok_or(Error::::WeightAndFeeNotExists)?; + + Ok((call_as_subaccount, fee, weight)) + } + + fn construct_xcm_as_subaccount_with_query_id( + operation: XcmOperation, + call: KusamaCall, + who: &MultiLocation, + ) -> Result<(QueryId, BlockNumberFor, Xcm<()>), Error> { + // prepare the query_id for reporting back transact status + let responder = MultiLocation::parent(); + let now = frame_system::Pallet::::block_number(); + let timeout = T::BlockNumber::from(TIMEOUT_BLOCKS).saturating_add(now); + let query_id = T::SubstrateResponseManager::create_query_record(&responder, timeout); + + let (call_as_subaccount, fee, weight) = + Self::prepare_send_as_subaccount_call_params_with_query_id( + operation, call, who, query_id, + )?; + + let xcm_message = + Self::construct_xcm_message_with_query_id(call_as_subaccount, fee, weight, query_id); + + //【For xcm v3】 + // let response_back_location = T::UniversalLocation::get() + // .invert_target(&responder) + // .map_err(|()| XcmError::MultiLocationNotInvertible)?; + + // let xcm_message = Self::construct_xcm_message( + // call_as_subaccount, + // fee, + // weight, + // query_id, + // response_back_location, + // ); + + Ok((query_id, timeout, xcm_message)) + } + + fn construct_xcm_and_send_as_subaccount_without_query_id( + operation: XcmOperation, + call: KusamaCall, + who: &MultiLocation, + ) -> Result<(), Error> { + let (call_as_subaccount, fee, weight) = + Self::prepare_send_as_subaccount_call_params_without_query_id(operation, call, who)?; + + let xcm_message = + Self::construct_xcm_message_without_query_id(call_as_subaccount, fee, weight); + + T::XcmRouter::send_xcm(Parent, xcm_message).map_err(|_e| Error::::XcmFailure)?; + + Ok(()) + } + + fn update_ledger_query_response_storage( + query_id: QueryId, + query_entry: LedgerUpdateEntry, MultiLocation>, + ) -> Result<(), Error> { + // update DelegatorLedgers storage + if let LedgerUpdateEntry::Substrate(SubstrateLedgerUpdateEntry { + currency_id: _, + delegator_id, + if_bond, + if_unlock, + if_rebond, + amount, + unlock_time, + }) = query_entry + { + DelegatorLedgers::::mutate( + KSM, + delegator_id, + |old_ledger| -> Result<(), Error> { + if let Some(Ledger::Substrate(ref mut old_sub_ledger)) = old_ledger { + // If this an unlocking xcm message update record + // Decrease the active amount and add an unlocking record. + if if_bond { + // If this is a bonding operation. + // Increase both the active and total amount. + old_sub_ledger.active = old_sub_ledger + .active + .checked_add(&amount) + .ok_or(Error::::OverFlow)?; + + old_sub_ledger.total = old_sub_ledger + .total + .checked_add(&amount) + .ok_or(Error::::OverFlow)?; + } else if if_unlock { + old_sub_ledger.active = old_sub_ledger + .active + .checked_sub(&amount) + .ok_or(Error::::UnderFlow)?; + + let unlock_time_unit = + unlock_time.ok_or(Error::::TimeUnitNotExist)?; + + let new_unlock_record = + UnlockChunk { value: amount, unlock_time: unlock_time_unit }; + + old_sub_ledger.unlocking.push(new_unlock_record); + } else if if_rebond { + // If it is a rebonding operation. + // Reduce the unlocking records. + let mut remaining_amount = amount; + + loop { + if let Some(record) = old_sub_ledger.unlocking.pop() { + if remaining_amount >= record.value { + remaining_amount = remaining_amount - record.value; + } else { + let remain_unlock_chunk = UnlockChunk { + value: record.value - remaining_amount, + unlock_time: record.unlock_time, + }; + old_sub_ledger.unlocking.push(remain_unlock_chunk); + break; + } + } else { + break; + } + } + + // Increase the active amount. + old_sub_ledger.active = old_sub_ledger + .active + .checked_add(&amount) + .ok_or(Error::::OverFlow)?; + } else { + // If it is a liquidize operation. + let unlock_unit = unlock_time.ok_or(Error::::InvalidTimeUnit)?; + let unlock_era = if let TimeUnit::Era(unlock_era) = unlock_unit { + unlock_era + } else { + Err(Error::::InvalidTimeUnit)? + }; + + let mut accumulated: BalanceOf = Zero::zero(); + let mut pop_first_num = 0; + + // for each unlocking record, check whether its unlocking era is smaller + // or equal to unlock_time. If yes, pop it out and accumulate its + // amount. + for record in old_sub_ledger.unlocking.iter() { + if let TimeUnit::Era(due_era) = record.unlock_time { + if due_era <= unlock_era { + accumulated = accumulated + .checked_add(&record.value) + .ok_or(Error::::OverFlow)?; + + pop_first_num = pop_first_num + .checked_add(&1) + .ok_or(Error::::OverFlow)?; + } else { + break; + } + } else { + Err(Error::::Unexpected)?; + } + } + + // Remove the first pop_first_num elements from unlocking records. + old_sub_ledger.unlocking.drain(0..pop_first_num); + + // Finally deduct the accumulated amount from ledger total field. + old_sub_ledger.total = old_sub_ledger + .total + .checked_sub(&accumulated) + .ok_or(Error::::OverFlow)?; + } + } + + Ok(()) + }, + )?; + + // Delete the DelegatorLedgerXcmUpdateQueue query + DelegatorLedgerXcmUpdateQueue::::remove(query_id); + + // Delete the query in pallet_xcm. + ensure!( + T::SubstrateResponseManager::remove_query_record(query_id), + Error::::QueryResponseRemoveError + ); + + Ok(()) + } else { + Err(Error::::Unexpected) + } + } + + fn update_validators_by_delegator_query_response_storage( + query_id: QueryId, + query_entry: ValidatorsByDelegatorUpdateEntry>, + ) -> Result<(), Error> { + // update ValidatorsByDelegator storage + if let ValidatorsByDelegatorUpdateEntry::Substrate( + SubstrateValidatorsByDelegatorUpdateEntry { currency_id, delegator_id, validators }, + ) = query_entry + { + ValidatorsByDelegator::::insert(currency_id, delegator_id, validators); + + // update ValidatorsByDelegatorXcmUpdateQueue storage + ValidatorsByDelegatorXcmUpdateQueue::::remove(query_id); + + // Delete the query in pallet_xcm. + + ensure!( + T::SubstrateResponseManager::remove_query_record(query_id), + Error::::QueryResponseRemoveError + ); + + Ok(()) + } else { + Err(Error::::Unexpected) + } + } + + fn get_unlocking_era_from_current() -> Result, Error> { + let current_time_unit = + T::VtokenMinting::get_ongoing_time_unit(KSM).ok_or(Error::::TimeUnitNotExist)?; + let delays = CurrencyDelays::::get(KSM).ok_or(Error::::DelaysNotExist)?; + + let unlock_era = if let TimeUnit::Era(current_era) = current_time_unit { + if let TimeUnit::Era(delay_era) = delays.unlock_delay { + current_era.checked_add(delay_era).ok_or(Error::::OverFlow) + } else { + Err(Error::::InvalidTimeUnit) + } + } else { + Err(Error::::InvalidTimeUnit) + }?; + + let unlock_time_unit = TimeUnit::Era(unlock_era); + Ok(Some(unlock_time_unit)) + } + + fn insert_delegator_ledger_update_entry( + who: &MultiLocation, + if_bond: bool, + if_unlock: bool, + if_rebond: bool, + amount: BalanceOf, + query_id: QueryId, + timeout: BlockNumberFor, + ) -> Result<(), Error> { + // Insert a delegator ledger update record into DelegatorLedgerXcmUpdateQueue. + let unlock_time = if if_unlock { + Self::get_unlocking_era_from_current()? + } else if if_bond || if_rebond { + None + } else { + T::VtokenMinting::get_ongoing_time_unit(KSM) + }; + + let entry = LedgerUpdateEntry::Substrate(SubstrateLedgerUpdateEntry { + currency_id: KSM, + delegator_id: who.clone(), + if_bond, + if_unlock, + if_rebond, + amount, + unlock_time, + }); + DelegatorLedgerXcmUpdateQueue::::insert(query_id, (entry, timeout)); + + Ok(()) + } + + fn insert_validators_by_delegator_update_entry( + who: &MultiLocation, + validator_list: Vec<(MultiLocation, Hash)>, + query_id: QueryId, + timeout: BlockNumberFor, + ) -> Result<(), Error> { + // Insert a query record to the ValidatorsByDelegatorXcmUpdateQueue storage. + let entry = ValidatorsByDelegatorUpdateEntry::Substrate( + SubstrateValidatorsByDelegatorUpdateEntry { + currency_id: KSM, + delegator_id: who.clone(), + validators: validator_list, + }, + ); + ValidatorsByDelegatorXcmUpdateQueue::::insert(query_id, (entry, timeout)); + + Ok(()) + } + + fn do_transfer_to( + from: &MultiLocation, + to: &MultiLocation, + amount: BalanceOf, + ) -> Result<(), Error> { + // Ensure amount is greater than zero. + ensure!(!amount.is_zero(), Error::::AmountZero); + + // Ensure the from account is located within Bifrost chain. Otherwise, the xcm massage will + // not succeed. + ensure!(from.parents.is_zero(), Error::::InvalidTransferSource); + + let (weight, fee_amount) = XcmDestWeightAndFee::::get(KSM, XcmOperation::TransferTo) + .ok_or(Error::::WeightAndFeeNotExists)?; + + // Prepare parameter dest and beneficiary. + let dest = MultiLocation::parent(); + let to_32: [u8; 32] = Pallet::::multilocation_to_account_32(&to)?; + let beneficiary = Pallet::::account_32_to_local_location(to_32)?; + + // Prepare parameter assets. + let asset = MultiAsset { + fun: Fungible(amount.unique_saturated_into()), + id: Concrete(MultiLocation::parent()), + }; + let assets = MultiAssets::from(asset); + + // Prepare fee asset. + let fee_asset = MultiAsset { + fun: Fungible(fee_amount.unique_saturated_into()), + id: Concrete(MultiLocation { parents: 0, interior: Here }), + }; + + // prepare for xcm message + let msg = Xcm(vec![ + WithdrawAsset(assets.clone()), + InitiateReserveWithdraw { + assets: All.into(), + reserve: dest.clone(), + xcm: Xcm(vec![ + BuyExecution { fees: fee_asset, weight_limit: WeightLimit::Limited(weight) }, + DepositAsset { assets: All.into(), max_assets: 1, beneficiary }, + ]), + }, + ]); + + //【For xcm v3】 + // let now = frame_system::Pallet::::block_number(); + // let timeout = T::BlockNumber::from(TIMEOUT_BLOCKS).saturating_add(now); + // let query_id = T::SubstrateResponseManager::create_query_record(dest.clone(), timeout); + // // Report the Error message of the xcm. + // // from the responder's point of view to get Here's MultiLocation. + // let destination = T::UniversalLocation::get() + // .invert_target(&dest) + // .map_err(|()| XcmError::MultiLocationNotInvertible)?; + + // // Set the error reporting. + // let response_info = QueryResponseInfo { destination, query_id, max_weight: 0 }; + // let report_error = Xcm(vec![ReportError(response_info)]); + // msg.0.insert(0, SetAppendix(report_error)); + + // Execute the xcm message. + T::XcmExecutor::execute_xcm_in_credit(from.clone(), msg, weight, weight) + .ensure_complete() + .map_err(|_| Error::::XcmExecutionFailed)?; + + Ok(()) + } +} diff --git a/pallets/slp/src/agents/kusama_agent/mod.rs b/pallets/slp/src/agents/kusama_agent/mod.rs new file mode 100644 index 0000000000..f6042191e0 --- /dev/null +++ b/pallets/slp/src/agents/kusama_agent/mod.rs @@ -0,0 +1,23 @@ +// This file is part of Bifrost. + +// Copyright (C) 2019-2022 Liebi Technologies (UK) Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +// This program 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. + +// This program 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 this program. If not, see . + +mod agent; +mod types; + +pub use agent::*; +pub use types::*; diff --git a/pallets/slp/src/agents/kusama_agent/types.rs b/pallets/slp/src/agents/kusama_agent/types.rs new file mode 100644 index 0000000000..d0b4884cef --- /dev/null +++ b/pallets/slp/src/agents/kusama_agent/types.rs @@ -0,0 +1,111 @@ +// This file is part of Bifrost. + +// Copyright (C) 2019-2022 Liebi Technologies (UK) Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +// This program 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. + +// This program 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 this program. If not, see . + +use codec::{Decode, Encode}; +use frame_support::RuntimeDebug; +use scale_info::TypeInfo; +use sp_runtime::traits::StaticLookup; +use sp_std::{boxed::Box, vec::Vec}; +use xcm::{VersionedMultiAssets, VersionedMultiLocation}; + +use crate::{BalanceOf, Config}; + +#[derive(Encode, Decode, RuntimeDebug)] +pub enum KusamaCall { + #[codec(index = 0)] + System(SystemCall), + #[codec(index = 4)] + Balances(BalancesCall), + #[codec(index = 6)] + Staking(StakingCall), + #[codec(index = 24)] + Utility(Box>), + #[codec(index = 99)] + Xcm(Box), +} + +#[derive(Encode, Decode, RuntimeDebug)] +pub enum SystemCall { + #[codec(index = 8)] + RemarkWithEvent(Box>), +} + +#[derive(Encode, Decode, RuntimeDebug)] +pub enum BalancesCall { + #[codec(index = 3)] + TransferKeepAlive(::Source, #[codec(compact)] BalanceOf), +} + +#[derive(Encode, Decode, RuntimeDebug)] +pub enum UtilityCall { + #[codec(index = 1)] + AsDerivative(u16, Box), + #[codec(index = 2)] + BatchAll(Box>>), +} + +#[derive(Encode, Decode, RuntimeDebug)] +pub enum StakingCall { + /// Kusama has the same account Id type as Bifrost. + #[codec(index = 0)] + Bond( + ::Source, + #[codec(compact)] BalanceOf, + RewardDestination, + ), + #[codec(index = 1)] + BondExtra(#[codec(compact)] BalanceOf), + #[codec(index = 2)] + Unbond(#[codec(compact)] BalanceOf), + #[codec(index = 3)] + WithdrawUnbonded(u32), + #[codec(index = 5)] + Nominate(Vec<::Source>), + #[codec(index = 6)] + Chill, + #[codec(index = 18)] + PayoutStakers(T::AccountId, u32), + #[codec(index = 19)] + Rebond(#[codec(compact)] BalanceOf), +} + +#[derive(Encode, Decode, RuntimeDebug)] +pub enum XcmCall { + #[codec(index = 2)] + ReserveTransferAssets( + Box, + Box, + Box, + u32, + ), +} + +/// A destination account for payment. +#[derive(PartialEq, Eq, Copy, Clone, Encode, Decode, RuntimeDebug, TypeInfo)] +pub enum RewardDestination { + /// Pay into the stash account, increasing the amount at stake accordingly. + Staked, + /// Pay into the stash account, not increasing the amount at stake. + Stash, + /// Pay into the controller account. + Controller, + /// Pay into a specified account. + Account(AccountId), + /// Receive no reward. + None, +} diff --git a/pallets/slp/src/agents/mod.rs b/pallets/slp/src/agents/mod.rs new file mode 100644 index 0000000000..e438133d79 --- /dev/null +++ b/pallets/slp/src/agents/mod.rs @@ -0,0 +1,20 @@ +// This file is part of Bifrost. + +// Copyright (C) 2019-2022 Liebi Technologies (UK) Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +// This program 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. + +// This program 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 this program. If not, see . + +mod kusama_agent; +pub use kusama_agent::*; diff --git a/pallets/slp/src/benchmarking.rs b/pallets/slp/src/benchmarking.rs new file mode 100644 index 0000000000..ac118b2034 --- /dev/null +++ b/pallets/slp/src/benchmarking.rs @@ -0,0 +1,17 @@ +// This file is part of Bifrost. + +// Copyright (C) 2019-2022 Liebi Technologies (UK) Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +// This program 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. + +// This program 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 this program. If not, see . diff --git a/pallets/slp/src/lib.rs b/pallets/slp/src/lib.rs new file mode 100644 index 0000000000..408f01959a --- /dev/null +++ b/pallets/slp/src/lib.rs @@ -0,0 +1,1866 @@ +// This file is part of Bifrost. + +// Copyright (C) 2019-2022 Liebi Technologies (UK) Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +// This program 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. + +// This program 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 this program. If not, see . + +#![cfg_attr(not(feature = "std"), no_std)] + +pub use agents::KusamaAgent; +use cumulus_primitives_core::{relay_chain::HashT, ParaId}; +use frame_support::{pallet_prelude::*, transactional, weights::Weight}; +use frame_system::{ + pallet_prelude::{BlockNumberFor, OriginFor}, + RawOrigin, +}; +use node_primitives::{CurrencyId, CurrencyIdExt, TimeUnit, VtokenMintingOperator}; +use orml_traits::MultiCurrency; +pub use primitives::Ledger; +use sp_arithmetic::{per_things::Permill, traits::Zero}; +use sp_runtime::traits::{CheckedSub, Convert}; +use sp_std::{boxed::Box, vec, vec::Vec}; +pub use weights::WeightInfo; +use xcm::{ + latest::*, + opaque::latest::{Junction::Parachain, Junctions::X2, NetworkId::Any}, +}; + +pub use crate::{ + primitives::{ + Delays, LedgerUpdateEntry, MinimumsMaximums, SubstrateLedger, + ValidatorsByDelegatorUpdateEntry, XcmOperation, KSM, + }, + traits::{QueryResponseManager, StakingAgent}, + Junction::AccountId32, + Junctions::X1, +}; + +mod agents; +mod mock; +pub mod primitives; +mod tests; +pub mod traits; +pub mod weights; + +#[cfg(feature = "runtime-benchmarks")] +mod benchmarking; + +pub use pallet::*; + +pub type Result = core::result::Result; + +pub type QueryId = u64; +pub const TIMEOUT_BLOCKS: u32 = 1000; +pub const BASE_WEIGHT: Weight = 1000; +type Hash = ::Hash; +type AccountIdOf = ::AccountId; +type BalanceOf = <::MultiCurrency as MultiCurrency>>::Balance; +type StakingAgentBoxType = Box< + dyn StakingAgent< + MultiLocation, + MultiLocation, + BalanceOf, + TimeUnit, + AccountIdOf, + MultiLocation, + QueryId, + LedgerUpdateEntry, MultiLocation>, + ValidatorsByDelegatorUpdateEntry>, + pallet::Error, + >, +>; + +#[frame_support::pallet] +pub mod pallet { + use super::*; + + #[pallet::config] + pub trait Config: frame_system::Config { + type Event: From> + IsType<::Event>; + + /// Currency operations handler + type MultiCurrency: MultiCurrency, CurrencyId = CurrencyId>; + /// The only origin that can modify pallet params + type ControlOrigin: EnsureOrigin<::Origin>; + + /// Set default weight. + type WeightInfo: WeightInfo; + + /// The interface to call VtokenMinting module functions. + type VtokenMinting: VtokenMintingOperator< + CurrencyId, + BalanceOf, + AccountIdOf, + TimeUnit, + >; + + /// Substrate account converter, which can convert a u16 number into a sub-account with + /// MultiLocation format. + type AccountConverter: Convert; + + /// Parachain Id which is gotten from the runtime. + type ParachainId: Get; + + /// Routes the XCM message outbound. + type XcmRouter: SendXcm; + + /// XCM executor. + type XcmExecutor: ExecuteXcm; + + /// Substrate response manager. + type SubstrateResponseManager: QueryResponseManager< + QueryId, + MultiLocation, + BlockNumberFor, + >; + + //【For xcm v3】 + // /// This chain's Universal Location. Enabled only for xcm v3 version. + // type UniversalLocation: Get; + + /// The maximum number of entries to be confirmed in a block for update queue in the + /// on_initialize queue. + #[pallet::constant] + type MaxTypeEntryPerBlock: Get; + + #[pallet::constant] + type MaxRefundPerBlock: Get; + } + + #[pallet::error] + pub enum Error { + OperateOriginNotSet, + NotAuthorized, + NotSupportedCurrencyId, + FailToAddDelegator, + FailToBond, + OverFlow, + UnderFlow, + NotExist, + LowerThanMinimum, + GreaterThanMaximum, + AlreadyBonded, + AccountNotExist, + DelegatorNotExist, + XcmFailure, + DelegatorNotBonded, + ExceedActiveMaximum, + ProblematicLedger, + NotEnoughToUnbond, + ExceedUnlockingRecords, + RebondExceedUnlockingAmount, + DecodingError, + EncodingError, + VectorEmpty, + ValidatorSetNotExist, + ValidatorNotExist, + InvalidTimeUnit, + AmountZero, + AmountNotZero, + AlreadyExist, + ValidatorStillInUse, + TimeUnitNotExist, + FeeSourceNotExist, + BalanceLow, + WeightAndFeeNotExists, + OperateOriginNotExists, + MinimumsAndMaximumsNotExist, + XcmExecutionFailed, + QueryNotExist, + DelaysNotExist, + Unexpected, + UnlockingRecordNotExist, + QueryResponseRemoveError, + ValidatorsByDelegatorResponseCheckError, + LedgerResponseCheckError, + InvalidHostingFee, + InvalidAccount, + IncreaseTokenPoolError, + TuneExchangeRateLimitNotSet, + DelegatorLatestTuneRecordNotExist, + InvalidTransferSource, + } + + #[pallet::event] + #[pallet::generate_deposit(pub (crate) fn deposit_event)] + pub enum Event { + DelegatorInitialized { + currency_id: CurrencyId, + delegator_id: MultiLocation, + }, + DelegatorBonded { + currency_id: CurrencyId, + delegator_id: MultiLocation, + #[codec(compact)] + bonded_amount: BalanceOf, + #[codec(compact)] + query_id: QueryId, + query_id_hash: Hash, + }, + DelegatorBondExtra { + currency_id: CurrencyId, + delegator_id: MultiLocation, + #[codec(compact)] + extra_bonded_amount: BalanceOf, + #[codec(compact)] + query_id: QueryId, + query_id_hash: Hash, + }, + DelegatorUnbond { + currency_id: CurrencyId, + delegator_id: MultiLocation, + #[codec(compact)] + unbond_amount: BalanceOf, + #[codec(compact)] + query_id: QueryId, + query_id_hash: Hash, + }, + DelegatorUnbondAll { + currency_id: CurrencyId, + delegator_id: MultiLocation, + #[codec(compact)] + query_id: QueryId, + query_id_hash: Hash, + }, + DelegatorRebond { + currency_id: CurrencyId, + delegator_id: MultiLocation, + #[codec(compact)] + rebond_amount: BalanceOf, + #[codec(compact)] + query_id: QueryId, + query_id_hash: Hash, + }, + Delegated { + currency_id: CurrencyId, + delegator_id: MultiLocation, + targets: Vec, + #[codec(compact)] + query_id: QueryId, + query_id_hash: Hash, + }, + Undelegated { + currency_id: CurrencyId, + delegator_id: MultiLocation, + targets: Vec, + #[codec(compact)] + query_id: QueryId, + query_id_hash: Hash, + }, + Payout { + currency_id: CurrencyId, + validator: MultiLocation, + time_unit: Option, + }, + Liquidize { + currency_id: CurrencyId, + delegator_id: MultiLocation, + time_unit: Option, + #[codec(compact)] + query_id: QueryId, + query_id_hash: Hash, + }, + Chill { + currency_id: CurrencyId, + delegator_id: MultiLocation, + #[codec(compact)] + query_id: QueryId, + query_id_hash: Hash, + }, + TransferBack { + currency_id: CurrencyId, + from: MultiLocation, + to: MultiLocation, + #[codec(compact)] + amount: BalanceOf, + }, + TransferTo { + currency_id: CurrencyId, + from: MultiLocation, + to: MultiLocation, + #[codec(compact)] + amount: BalanceOf, + }, + DelegatorAdded { + currency_id: CurrencyId, + #[codec(compact)] + index: u16, + delegator_id: MultiLocation, + }, + DelegatorRemoved { + currency_id: CurrencyId, + delegator_id: MultiLocation, + }, + ValidatorsAdded { + currency_id: CurrencyId, + validator_id: MultiLocation, + }, + ValidatorsRemoved { + currency_id: CurrencyId, + validator_id: MultiLocation, + }, + Refund { + currency_id: CurrencyId, + time_unit: TimeUnit, + #[codec(compact)] + index: u32, + #[codec(compact)] + amount: BalanceOf, + }, + FundMoveFromExitToEntrance { + currency_id: CurrencyId, + #[codec(compact)] + amount: BalanceOf, + }, + TimeUnitUpdated { + currency_id: CurrencyId, + old: TimeUnit, + new: TimeUnit, + }, + PoolTokenIncreased { + currency_id: CurrencyId, + #[codec(compact)] + amount: BalanceOf, + }, + HostingFeeCharged { + currency_id: CurrencyId, + #[codec(compact)] + amount: BalanceOf, + }, + PoolTokenDecreased { + currency_id: CurrencyId, + #[codec(compact)] + amount: BalanceOf, + }, + FeeSupplemented { + currency_id: CurrencyId, + #[codec(compact)] + amount: BalanceOf, + from: MultiLocation, + to: MultiLocation, + }, + ValidatorsByDelegatorSet { + currency_id: CurrencyId, + validators_list: Vec<(MultiLocation, Hash)>, + }, + XcmDestWeightAndFeeSet { + currency_id: CurrencyId, + operation: XcmOperation, + weight_and_fee: Option<(Weight, BalanceOf)>, + }, + OperateOriginSet { + currency_id: CurrencyId, + operator: Option>, + }, + FeeSourceSet { + currency_id: CurrencyId, + who_and_fee: Option<(MultiLocation, BalanceOf)>, + }, + DelegatorLedgerSet { + currency_id: CurrencyId, + delegator: MultiLocation, + ledger: Option>>, + }, + DelegatorLedgerQueryResponseConfirmed { + #[codec(compact)] + query_id: QueryId, + entry: LedgerUpdateEntry, MultiLocation>, + }, + DelegatorLedgerQueryResponseFailSuccessfully { + #[codec(compact)] + query_id: QueryId, + }, + ValidatorsByDelegatorQueryResponseConfirmed { + #[codec(compact)] + query_id: QueryId, + entry: ValidatorsByDelegatorUpdateEntry>, + }, + ValidatorsByDelegatorQueryResponseFailSuccessfully { + #[codec(compact)] + query_id: QueryId, + }, + MinimumsMaximumsSet { + currency_id: CurrencyId, + minimums_and_maximums: Option>>, + }, + CurrencyDelaysSet { + currency_id: CurrencyId, + delays: Option, + }, + HostingFeesSet { + currency_id: CurrencyId, + fees: Option<(Permill, MultiLocation)>, + }, + CurrencyTuneExchangeRateLimitSet { + currency_id: CurrencyId, + tune_exchange_rate_limit: Option<(u32, Permill)>, + }, + } + + /// The dest weight limit and fee for execution XCM msg sended out. Must be + /// sufficient, otherwise the execution of XCM msg on the dest chain will fail. + /// + /// XcmDestWeightAndFee: DoubleMap: CurrencyId, XcmOperation => (Weight, Balance) + #[pallet::storage] + #[pallet::getter(fn xcm_dest_weight_and_fee)] + pub type XcmDestWeightAndFee = StorageDoubleMap< + _, + Blake2_128Concat, + CurrencyId, + Blake2_128Concat, + XcmOperation, + (Weight, BalanceOf), + OptionQuery, + >; + + /// One operate origin(can be a multisig account) for a currency. An operating origins are + /// normal account in Bifrost chain. + #[pallet::storage] + #[pallet::getter(fn get_operate_origin)] + pub type OperateOrigins = StorageMap<_, Blake2_128Concat, CurrencyId, AccountIdOf>; + + /// Origins and Amounts for the staking operating account fee supplement. An operating account + /// is identified in MultiLocation format. + #[pallet::storage] + #[pallet::getter(fn get_fee_source)] + pub type FeeSources = + StorageMap<_, Blake2_128Concat, CurrencyId, (MultiLocation, BalanceOf)>; + + /// Hosting fee percentage and beneficiary account for different chains + #[pallet::storage] + #[pallet::getter(fn get_hosting_fee)] + pub type HostingFees = StorageMap<_, Blake2_128Concat, CurrencyId, (Permill, MultiLocation)>; + + /// Delegators in service. A delegator is identified in MultiLocation format. + /// Currency Id + Sub-account index => MultiLocation + #[pallet::storage] + #[pallet::getter(fn get_delegator_multilocation_by_index)] + pub type DelegatorsIndex2Multilocation = StorageDoubleMap< + _, + Blake2_128Concat, + CurrencyId, + Blake2_128Concat, + u16, + MultiLocation, + OptionQuery, + >; + + /// Delegators in service. Currency Id + MultiLocation => Sub-account index + #[pallet::storage] + #[pallet::getter(fn get_delegator_index_by_multilocation)] + pub type DelegatorsMultilocation2Index = StorageDoubleMap< + _, + Blake2_128Concat, + CurrencyId, + Blake2_128Concat, + MultiLocation, + u16, + OptionQuery, + >; + + /// Next index of different currency delegators. + #[pallet::storage] + #[pallet::getter(fn get_delegator_next_index)] + pub type DelegatorNextIndex = StorageMap<_, Blake2_128Concat, CurrencyId, u16, ValueQuery>; + + /// Validator in service. A validator is identified in MultiLocation format. + #[pallet::storage] + #[pallet::getter(fn get_validators)] + pub type Validators = + StorageMap<_, Blake2_128Concat, CurrencyId, Vec<(MultiLocation, Hash)>>; + + /// Validators for each delegator. CurrencyId + Delegator => Vec + #[pallet::storage] + #[pallet::getter(fn get_validators_by_delegator)] + pub type ValidatorsByDelegator = StorageDoubleMap< + _, + Blake2_128Concat, + CurrencyId, + Blake2_128Concat, + MultiLocation, + Vec<(MultiLocation, Hash)>, + OptionQuery, + >; + + #[pallet::storage] + #[pallet::getter(fn get_validators_by_delegator_update_entry)] + pub type ValidatorsByDelegatorXcmUpdateQueue = StorageMap< + _, + Blake2_128Concat, + QueryId, + ( + ValidatorsByDelegatorUpdateEntry>, + BlockNumberFor, + ), + >; + + /// Delegator ledgers. A delegator is identified in MultiLocation format. + #[pallet::storage] + #[pallet::getter(fn get_delegator_ledger)] + pub type DelegatorLedgers = StorageDoubleMap< + _, + Blake2_128Concat, + CurrencyId, + Blake2_128Concat, + MultiLocation, + Ledger>, + OptionQuery, + >; + + #[pallet::storage] + #[pallet::getter(fn get_delegator_ledger_update_entry)] + pub type DelegatorLedgerXcmUpdateQueue = StorageMap< + _, + Blake2_128Concat, + QueryId, + (LedgerUpdateEntry, MultiLocation>, BlockNumberFor), + >; + + /// Minimum and Maximum constraints for different chains. + #[pallet::storage] + #[pallet::getter(fn get_minimums_maximums)] + pub type MinimumsAndMaximums = + StorageMap<_, Blake2_128Concat, CurrencyId, MinimumsMaximums>>; + + /// TimeUnit delay params for different chains. + #[pallet::storage] + #[pallet::getter(fn get_currency_delays)] + pub type CurrencyDelays = StorageMap<_, Blake2_128Concat, CurrencyId, Delays>; + + /// A delegator's tuning record of exchange rate for the current time unit. + /// Currency Id + Delegator Id => (latest tuned TimeUnit, number of tuning times) + #[pallet::storage] + #[pallet::getter(fn get_delegator_latest_tune_record)] + pub type DelegatorLatestTuneRecord = StorageDoubleMap< + _, + Blake2_128Concat, + CurrencyId, + Blake2_128Concat, + MultiLocation, + (TimeUnit, u32), + OptionQuery, + >; + + /// For each currencyId: how many times that a delegator can tune the exchange rate for a single + /// time unit, and how much at most each time a delegator can tune the exchange rate + #[pallet::storage] + #[pallet::getter(fn get_currency_tune_exchange_rate_limit)] + pub type CurrencyTuneExchangeRateLimit = + StorageMap<_, Blake2_128Concat, CurrencyId, (u32, Permill)>; + + #[pallet::pallet] + #[pallet::without_storage_info] + pub struct Pallet(PhantomData); + + #[pallet::hooks] + impl Hooks> for Pallet { + fn on_initialize(_n: BlockNumberFor) -> Weight { + // For queries in update entry queues, search responses in pallet_xcm Queries storage. + let counter = Self::process_query_entry_records().unwrap_or(0); + + // Calculate weight + BASE_WEIGHT.saturating_mul(counter.into()) + } + } + + #[pallet::call] + impl Pallet { + /// *****************************/ + /// ****** Outer Calls ******/ + /// *****************************/ + /// + /// Delegator initialization work. Generate a new delegator and return its ID. + #[transactional] + #[pallet::weight(T::WeightInfo::initialize_delegator())] + pub fn initialize_delegator( + origin: OriginFor, + currency_id: CurrencyId, + ) -> DispatchResult { + // Check the validity of origin + T::ControlOrigin::ensure_origin(origin)?; + + let staking_agent = Self::get_currency_staking_agent(currency_id)?; + let delegator_id = staking_agent.initialize_delegator()?; + + // Deposit event. + Pallet::::deposit_event(Event::DelegatorInitialized { currency_id, delegator_id }); + Ok(()) + } + + /// First time bonding some amount to a delegator. + #[transactional] + #[pallet::weight(T::WeightInfo::bond())] + pub fn bond( + origin: OriginFor, + currency_id: CurrencyId, + who: MultiLocation, + #[pallet::compact] amount: BalanceOf, + ) -> DispatchResult { + // Ensure origin + Self::ensure_authorized(origin, currency_id)?; + + let staking_agent = Self::get_currency_staking_agent(currency_id)?; + let query_id = staking_agent.bond(&who, amount)?; + let query_id_hash = T::Hashing::hash(&query_id.encode()); + + // Deposit event. + Pallet::::deposit_event(Event::DelegatorBonded { + currency_id, + delegator_id: who, + bonded_amount: amount, + query_id, + query_id_hash, + }); + Ok(()) + } + + /// Bond extra amount to a delegator. + #[transactional] + #[pallet::weight(T::WeightInfo::bond_extra())] + pub fn bond_extra( + origin: OriginFor, + currency_id: CurrencyId, + who: MultiLocation, + #[pallet::compact] amount: BalanceOf, + ) -> DispatchResult { + // Ensure origin + Self::ensure_authorized(origin, currency_id)?; + + let staking_agent = Self::get_currency_staking_agent(currency_id)?; + let query_id = staking_agent.bond_extra(&who, amount)?; + let query_id_hash = ::Hashing::hash(&query_id.encode()); + + // Deposit event. + Pallet::::deposit_event(Event::DelegatorBondExtra { + currency_id, + delegator_id: who, + extra_bonded_amount: amount, + query_id, + query_id_hash, + }); + Ok(()) + } + + /// Decrease some amount to a delegator. Leave no less than the minimum delegator + /// requirement. + #[transactional] + #[pallet::weight(T::WeightInfo::unbond())] + pub fn unbond( + origin: OriginFor, + currency_id: CurrencyId, + who: MultiLocation, + #[pallet::compact] amount: BalanceOf, + ) -> DispatchResult { + // Ensure origin + Self::ensure_authorized(origin, currency_id)?; + + let staking_agent = Self::get_currency_staking_agent(currency_id)?; + let query_id = staking_agent.unbond(&who, amount)?; + let query_id_hash = ::Hashing::hash(&query_id.encode()); + + // Deposit event. + Pallet::::deposit_event(Event::DelegatorUnbond { + currency_id, + delegator_id: who, + unbond_amount: amount, + query_id, + query_id_hash, + }); + Ok(()) + } + + /// Unbond all the active amount of a delegator. + #[transactional] + #[pallet::weight(T::WeightInfo::unbond_all())] + pub fn unbond_all( + origin: OriginFor, + currency_id: CurrencyId, + who: MultiLocation, + ) -> DispatchResult { + // Ensure origin + Self::ensure_authorized(origin, currency_id)?; + + let staking_agent = Self::get_currency_staking_agent(currency_id)?; + let query_id = staking_agent.unbond_all(&who)?; + let query_id_hash = ::Hashing::hash(&query_id.encode()); + + // Deposit event. + Pallet::::deposit_event(Event::DelegatorUnbondAll { + currency_id, + delegator_id: who, + query_id, + query_id_hash, + }); + Ok(()) + } + + /// Rebond some unlocking amount to a delegator. + #[transactional] + #[pallet::weight(T::WeightInfo::rebond())] + pub fn rebond( + origin: OriginFor, + currency_id: CurrencyId, + who: MultiLocation, + #[pallet::compact] amount: BalanceOf, + ) -> DispatchResult { + // Ensure origin + Self::ensure_authorized(origin, currency_id)?; + + let staking_agent = Self::get_currency_staking_agent(currency_id)?; + let query_id = staking_agent.rebond(&who, amount)?; + let query_id_hash = T::Hashing::hash(&query_id.encode()); + + // Deposit event. + Pallet::::deposit_event(Event::DelegatorRebond { + currency_id, + delegator_id: who, + rebond_amount: amount, + query_id, + query_id_hash, + }); + Ok(()) + } + + /// Delegate to some validator set. + #[transactional] + #[pallet::weight(T::WeightInfo::delegate())] + pub fn delegate( + origin: OriginFor, + currency_id: CurrencyId, + who: MultiLocation, + targets: Vec, + ) -> DispatchResult { + // Ensure origin + Self::ensure_authorized(origin, currency_id)?; + + let staking_agent = Self::get_currency_staking_agent(currency_id)?; + let query_id = staking_agent.delegate(&who, &targets)?; + let query_id_hash = ::Hashing::hash(&query_id.encode()); + + // Deposit event. + Pallet::::deposit_event(Event::Delegated { + currency_id, + delegator_id: who, + targets, + query_id, + query_id_hash, + }); + Ok(()) + } + + /// Re-delegate existing delegation to a new validator set. + #[transactional] + #[pallet::weight(T::WeightInfo::undelegate())] + pub fn undelegate( + origin: OriginFor, + currency_id: CurrencyId, + who: MultiLocation, + targets: Vec, + ) -> DispatchResult { + // Ensure origin + Self::ensure_authorized(origin, currency_id)?; + + let staking_agent = Self::get_currency_staking_agent(currency_id)?; + let query_id = staking_agent.undelegate(&who, &targets)?; + let query_id_hash = ::Hashing::hash(&query_id.encode()); + + // Deposit event. + Pallet::::deposit_event(Event::Undelegated { + currency_id, + delegator_id: who, + targets, + query_id, + query_id_hash, + }); + Ok(()) + } + + /// Re-delegate existing delegation to a new validator set. + #[transactional] + #[pallet::weight(T::WeightInfo::redelegate())] + pub fn redelegate( + origin: OriginFor, + currency_id: CurrencyId, + who: MultiLocation, + targets: Vec, + ) -> DispatchResult { + // Ensure origin + Self::ensure_authorized(origin, currency_id)?; + + let staking_agent = Self::get_currency_staking_agent(currency_id)?; + let query_id = staking_agent.redelegate(&who, &targets)?; + let query_id_hash = ::Hashing::hash(&query_id.encode()); + + // Deposit event. + Pallet::::deposit_event(Event::Delegated { + currency_id, + delegator_id: who, + targets, + query_id, + query_id_hash, + }); + Ok(()) + } + + /// Initiate payout for a certain delegator. + #[transactional] + #[pallet::weight(T::WeightInfo::payout())] + pub fn payout( + origin: OriginFor, + currency_id: CurrencyId, + who: Box, + validator: Box, + when: Option, + ) -> DispatchResult { + // Ensure origin + Self::ensure_authorized(origin, currency_id)?; + + let staking_agent = Self::get_currency_staking_agent(currency_id)?; + staking_agent.payout(&who, &validator, &when)?; + + // Deposit event. + Pallet::::deposit_event(Event::Payout { + currency_id, + validator: *validator, + time_unit: when, + }); + Ok(()) + } + + /// Withdraw the due payout into free balance. + #[transactional] + #[pallet::weight(T::WeightInfo::liquidize())] + pub fn liquidize( + origin: OriginFor, + currency_id: CurrencyId, + who: MultiLocation, + when: Option, + ) -> DispatchResult { + // Ensure origin + Self::ensure_authorized(origin, currency_id)?; + + let staking_agent = Self::get_currency_staking_agent(currency_id)?; + let query_id = staking_agent.liquidize(&who, &when)?; + let query_id_hash = ::Hashing::hash(&query_id.encode()); + + // Deposit event. + Pallet::::deposit_event(Event::Liquidize { + currency_id, + delegator_id: who, + time_unit: when, + query_id, + query_id_hash, + }); + Ok(()) + } + + /// Initiate payout for a certain delegator. + #[transactional] + #[pallet::weight(T::WeightInfo::chill())] + pub fn chill( + origin: OriginFor, + currency_id: CurrencyId, + who: MultiLocation, + ) -> DispatchResult { + // Ensure origin + Self::ensure_authorized(origin, currency_id)?; + + let staking_agent = Self::get_currency_staking_agent(currency_id)?; + let query_id = staking_agent.chill(&who)?; + let query_id_hash = ::Hashing::hash(&query_id.encode()); + + // Deposit event. + Pallet::::deposit_event(Event::Chill { + currency_id, + delegator_id: who, + query_id, + query_id_hash, + }); + Ok(()) + } + + #[transactional] + #[pallet::weight(T::WeightInfo::transfer_back())] + pub fn transfer_back( + origin: OriginFor, + currency_id: CurrencyId, + from: Box, + to: Box, + #[pallet::compact] amount: BalanceOf, + ) -> DispatchResult { + // Ensure origin + Self::ensure_authorized(origin, currency_id)?; + + let staking_agent = Self::get_currency_staking_agent(currency_id)?; + staking_agent.transfer_back(&from, &to, amount)?; + + // Deposit event. + Pallet::::deposit_event(Event::TransferBack { + currency_id, + from: *from, + to: *to, + amount, + }); + + Ok(()) + } + + #[transactional] + #[pallet::weight(T::WeightInfo::transfer_to())] + pub fn transfer_to( + origin: OriginFor, + currency_id: CurrencyId, + from: Box, + to: Box, + #[pallet::compact] amount: BalanceOf, + ) -> DispatchResult { + // Ensure origin + Self::ensure_authorized(origin, currency_id)?; + + let staking_agent = Self::get_currency_staking_agent(currency_id)?; + staking_agent.transfer_to(&from, &to, amount)?; + + // Deposit event. + Pallet::::deposit_event(Event::TransferTo { + currency_id, + from: *from, + to: *to, + amount, + }); + + Ok(()) + } + + #[pallet::weight(T::WeightInfo::increase_token_pool())] + pub fn increase_token_pool( + origin: OriginFor, + currency_id: CurrencyId, + #[pallet::compact] amount: BalanceOf, + ) -> DispatchResult { + // Check the validity of origin + T::ControlOrigin::ensure_origin(origin)?; + + // Ensure the amount is valid. + ensure!(amount > Zero::zero(), Error::::AmountZero); + + T::VtokenMinting::increase_token_pool(currency_id, amount)?; + + // Deposit event. + Pallet::::deposit_event(Event::PoolTokenIncreased { currency_id, amount }); + Ok(()) + } + + #[pallet::weight(T::WeightInfo::decrease_token_pool())] + pub fn decrease_token_pool( + origin: OriginFor, + currency_id: CurrencyId, + #[pallet::compact] amount: BalanceOf, + ) -> DispatchResult { + // Check the validity of origin + T::ControlOrigin::ensure_origin(origin)?; + + // Ensure the amount is valid. + ensure!(amount > Zero::zero(), Error::::AmountZero); + + T::VtokenMinting::decrease_token_pool(currency_id, amount)?; + + // Deposit event. + Pallet::::deposit_event(Event::PoolTokenDecreased { currency_id, amount }); + Ok(()) + } + + #[pallet::weight(T::WeightInfo::update_ongoing_time_unit())] + pub fn update_ongoing_time_unit( + origin: OriginFor, + currency_id: CurrencyId, + time_unit: TimeUnit, + ) -> DispatchResult { + // Ensure origin + Self::ensure_authorized(origin, currency_id)?; + + let old = T::VtokenMinting::get_ongoing_time_unit(currency_id).unwrap_or_default(); + T::VtokenMinting::update_ongoing_time_unit(currency_id, time_unit.clone())?; + + // Deposit event. + Pallet::::deposit_event(Event::TimeUnitUpdated { currency_id, old, new: time_unit }); + + Ok(()) + } + + #[transactional] + #[pallet::weight(T::WeightInfo::refund_currency_due_unbond())] + pub fn refund_currency_due_unbond( + origin: OriginFor, + currency_id: CurrencyId, + ) -> DispatchResult { + // Ensure origin + Self::ensure_authorized(origin, currency_id)?; + + // Get entrance_account and exit_account, as well as their currency balances. + let (entrance_account, exit_account) = + T::VtokenMinting::get_entrance_and_exit_accounts(); + let mut exit_account_balance = + T::MultiCurrency::free_balance(currency_id, &exit_account); + + if exit_account_balance.is_zero() { + return Ok(()); + } + + // Get the currency due unlocking records + let time_unit = T::VtokenMinting::get_ongoing_time_unit(currency_id) + .ok_or(Error::::TimeUnitNotExist)?; + let rs = T::VtokenMinting::get_unlock_records(currency_id, time_unit.clone()); + + // Refund due unlocking records one by one. + if let Some((_locked_amount, idx_vec)) = rs { + let mut counter = 0; + + for idx in idx_vec.iter() { + if counter >= T::MaxRefundPerBlock::get() { + break; + } + // get idx record amount + let idx_record_amount_op = + T::VtokenMinting::get_token_unlock_ledger(currency_id, *idx); + + if let Some((user_account, idx_record_amount, _unlock_era)) = + idx_record_amount_op + { + let mut deduct_amount = idx_record_amount; + if exit_account_balance < idx_record_amount { + deduct_amount = exit_account_balance; + } + // Transfer some amount from the exit_account to the user's account + T::MultiCurrency::transfer( + KSM, + &exit_account, + &user_account, + deduct_amount, + )?; + // Delete the corresponding unlocking record storage. + T::VtokenMinting::deduct_unlock_amount(currency_id, *idx, deduct_amount)?; + + // Deposit event. + Pallet::::deposit_event(Event::Refund { + currency_id, + time_unit: time_unit.clone(), + index: *idx, + amount: deduct_amount, + }); + + counter = counter.saturating_add(1); + + exit_account_balance = exit_account_balance + .checked_sub(&deduct_amount) + .ok_or(Error::::UnderFlow)?; + if exit_account_balance == Zero::zero() { + break; + } + } + } + } else { + // Automatically move the rest amount in exit account to entrance account. + T::MultiCurrency::transfer( + currency_id, + &exit_account, + &entrance_account, + exit_account_balance, + )?; + } + + Ok(()) + } + + #[transactional] + #[pallet::weight(T::WeightInfo::supplement_fee_reserve())] + pub fn supplement_fee_reserve( + origin: OriginFor, + currency_id: CurrencyId, + dest: MultiLocation, + ) -> DispatchResult { + // Ensure origin + Self::ensure_authorized(origin, currency_id)?; + + // Get the fee source account and reserve amount from the FeeSources storage. + let (source_location, reserved_fee) = + FeeSources::::get(currency_id).ok_or(Error::::FeeSourceNotExist)?; + + // If currency is BNC, transfer directly. + // Otherwise, call supplement_fee_reserve of StakingFeeManager trait. + if currency_id.is_native() { + let source_account = Self::native_multilocation_to_account(&source_location)?; + let dest_account = Self::native_multilocation_to_account(&dest)?; + T::MultiCurrency::transfer( + currency_id, + &source_account, + &dest_account, + reserved_fee, + )?; + } else { + let staking_agent = Self::get_currency_staking_agent(currency_id)?; + staking_agent.supplement_fee_reserve(reserved_fee, &source_location, &dest)?; + } + + // Deposit event. + Pallet::::deposit_event(Event::FeeSupplemented { + currency_id, + amount: reserved_fee, + from: source_location, + to: dest, + }); + + Ok(()) + } + + #[transactional] + #[pallet::weight(T::WeightInfo::charge_host_fee_and_tune_vtoken_exchange_rate())] + /// Charge staking host fee, tune vtoken/token exchange rate, and update delegator ledger + /// for single delegator. + pub fn charge_host_fee_and_tune_vtoken_exchange_rate( + origin: OriginFor, + currency_id: CurrencyId, + #[pallet::compact] value: BalanceOf, + who: MultiLocation, + ) -> DispatchResult { + // Ensure origin + Self::ensure_authorized(origin, currency_id)?; + + // Ensure the value is valid. + ensure!(value > Zero::zero(), Error::::AmountZero); + + // Ensure the value is valid. + let (limit_num, max_permill) = Self::get_currency_tune_exchange_rate_limit(currency_id) + .ok_or(Error::::TuneExchangeRateLimitNotSet)?; + // Get pool token value + let pool_token = T::VtokenMinting::get_token_pool(currency_id); + // Calculate max increase allowed. + let max_to_increase = max_permill.mul_floor(pool_token); + ensure!(value <= max_to_increase, Error::::GreaterThanMaximum); + + // Ensure this tune is within limit. + // Get current TimeUnit. + let current_time_unit = T::VtokenMinting::get_ongoing_time_unit(currency_id) + .ok_or(Error::::TimeUnitNotExist)?; + // If this is the first time. + if !DelegatorLatestTuneRecord::::contains_key(currency_id, &who) { + // ensure who is a valid delegator + ensure!( + DelegatorsMultilocation2Index::::contains_key(currency_id, &who), + Error::::DelegatorNotExist + ); + // Insert an empty record into DelegatorLatestTuneRecord storage. + DelegatorLatestTuneRecord::::insert( + currency_id, + who.clone(), + (current_time_unit.clone(), 0), + ); + } + + // Get DelegatorLatestTuneRecord for the currencyId. + let (latest_time_unit, tune_num) = + Self::get_delegator_latest_tune_record(currency_id, &who) + .ok_or(Error::::DelegatorLatestTuneRecordNotExist)?; + + // See if exceeds tuning limit. + // If it has been tuned in the current time unit, ensure this tuning is within limit. + if latest_time_unit == current_time_unit { + ensure!(tune_num < limit_num, Error::::GreaterThanMaximum); + } + + let new_tune_num = tune_num.checked_add(1).ok_or(Error::::OverFlow)?; + + // Get charged fee value + let (fee_permill, beneficiary) = + Self::get_hosting_fee(currency_id).ok_or(Error::::InvalidHostingFee)?; + let fee_to_charge = fee_permill.mul_floor(value); + + // Should first charge fee, and then tune exchange rate. Otherwise, the rate will be + // wrong. + let staking_agent = Self::get_currency_staking_agent(currency_id)?; + staking_agent.charge_hosting_fee( + fee_to_charge, + // Dummy value for 【from】account + &beneficiary, + &beneficiary, + )?; + + // Tune the new exchange rate. + staking_agent.tune_vtoken_exchange_rate( + &who, + value, + // Dummy value for vtoken amount + Zero::zero(), + )?; + + // Update the DelegatorLatestTuneRecord storage. + DelegatorLatestTuneRecord::::insert( + currency_id, + who.clone(), + (current_time_unit, new_tune_num), + ); + + // Deposit event. + Pallet::::deposit_event(Event::HostingFeeCharged { + currency_id, + amount: fee_to_charge, + }); + Pallet::::deposit_event(Event::PoolTokenIncreased { currency_id, amount: value }); + Ok(()) + } + + /// *****************************/ + /// ****** Storage Setters ******/ + /// *****************************/ + /// + /// Update storage XcmDestWeightAndFee. + #[pallet::weight(T::WeightInfo::set_xcm_dest_weight_and_fee())] + pub fn set_xcm_dest_weight_and_fee( + origin: OriginFor, + currency_id: CurrencyId, + operation: XcmOperation, + weight_and_fee: Option<(Weight, BalanceOf)>, + ) -> DispatchResult { + // Check the validity of origin + T::ControlOrigin::ensure_origin(origin)?; + + // If param weight_and_fee is a none, it will delete the storage. Otherwise, revise the + // storage to the new value if exists, or insert a new record if not exists before. + XcmDestWeightAndFee::::mutate_exists(currency_id, &operation, |wt_n_f| { + *wt_n_f = weight_and_fee.clone(); + }); + + // Deposit event. + Pallet::::deposit_event(Event::XcmDestWeightAndFeeSet { + currency_id, + operation, + weight_and_fee, + }); + + Ok(()) + } + + /// Update storage OperateOrigins. + #[pallet::weight(T::WeightInfo::set_operate_origin())] + pub fn set_operate_origin( + origin: OriginFor, + currency_id: CurrencyId, + who: Option>, + ) -> DispatchResult { + // Check the validity of origin + T::ControlOrigin::ensure_origin(origin)?; + + OperateOrigins::::mutate_exists(currency_id, |operator| { + *operator = who.clone(); + }); + + // Deposit event. + Pallet::::deposit_event(Event::OperateOriginSet { currency_id, operator: who }); + + Ok(()) + } + + /// Update storage FeeSources. + #[pallet::weight(T::WeightInfo::set_fee_source())] + pub fn set_fee_source( + origin: OriginFor, + currency_id: CurrencyId, + who_and_fee: Option<(MultiLocation, BalanceOf)>, + ) -> DispatchResult { + // Check the validity of origin + T::ControlOrigin::ensure_origin(origin)?; + + FeeSources::::mutate_exists(currency_id, |w_n_f| { + *w_n_f = who_and_fee.clone(); + }); + + // Deposit event. + Pallet::::deposit_event(Event::FeeSourceSet { currency_id, who_and_fee }); + + Ok(()) + } + + /// Update storage DelegatorsIndex2Multilocation 和 DelegatorsMultilocation2Index. + #[transactional] + #[pallet::weight(T::WeightInfo::add_delegator())] + pub fn add_delegator( + origin: OriginFor, + currency_id: CurrencyId, + #[pallet::compact] index: u16, + who: MultiLocation, + ) -> DispatchResult { + // Check the validity of origin + T::ControlOrigin::ensure_origin(origin)?; + + let staking_agent = Self::get_currency_staking_agent(currency_id)?; + staking_agent.add_delegator(index, &who)?; + + // Deposit event. + Pallet::::deposit_event(Event::DelegatorAdded { + currency_id, + index, + delegator_id: who, + }); + Ok(()) + } + + /// Update storage DelegatorsIndex2Multilocation 和 DelegatorsMultilocation2Index. + #[transactional] + #[pallet::weight(T::WeightInfo::remove_delegator())] + pub fn remove_delegator( + origin: OriginFor, + currency_id: CurrencyId, + who: MultiLocation, + ) -> DispatchResult { + // Check the validity of origin + T::ControlOrigin::ensure_origin(origin)?; + + let staking_agent = Self::get_currency_staking_agent(currency_id)?; + staking_agent.remove_delegator(&who)?; + + // Deposit event. + Pallet::::deposit_event(Event::DelegatorRemoved { currency_id, delegator_id: who }); + Ok(()) + } + + /// Update storage Validators. + #[transactional] + #[pallet::weight(T::WeightInfo::add_validator())] + pub fn add_validator( + origin: OriginFor, + currency_id: CurrencyId, + who: MultiLocation, + ) -> DispatchResult { + // Check the validity of origin + T::ControlOrigin::ensure_origin(origin)?; + + let staking_agent = Self::get_currency_staking_agent(currency_id)?; + staking_agent.add_validator(&who)?; + + // Deposit event. + Pallet::::deposit_event(Event::ValidatorsAdded { currency_id, validator_id: who }); + Ok(()) + } + + /// Update storage Validators. + #[transactional] + #[pallet::weight(T::WeightInfo::remove_validator())] + pub fn remove_validator( + origin: OriginFor, + currency_id: CurrencyId, + who: MultiLocation, + ) -> DispatchResult { + // Check the validity of origin + T::ControlOrigin::ensure_origin(origin)?; + + let staking_agent = Self::get_currency_staking_agent(currency_id)?; + staking_agent.remove_validator(&who)?; + + // Deposit event. + Pallet::::deposit_event(Event::ValidatorsRemoved { currency_id, validator_id: who }); + Ok(()) + } + + /// Update storage ValidatorsByDelegator. + #[transactional] + #[pallet::weight(T::WeightInfo::set_validators_by_delegator())] + pub fn set_validators_by_delegator( + origin: OriginFor, + currency_id: CurrencyId, + who: MultiLocation, + validators: Vec, + ) -> DispatchResult { + // Check the validity of origin + T::ControlOrigin::ensure_origin(origin)?; + + // Check the length of validators + let minimums_and_maximums = MinimumsAndMaximums::::get(currency_id) + .ok_or(Error::::MinimumsAndMaximumsNotExist)?; + ensure!( + validators.len() as u32 <= minimums_and_maximums.validators_back_maximum, + Error::::GreaterThanMaximum + ); + + // check delegator + // Check if it is bonded already. + ensure!(DelegatorLedgers::::contains_key(KSM, &who), Error::::DelegatorNotBonded); + + let validators_list = + Self::sort_validators_and_remove_duplicates(currency_id, &validators)?; + + // Update ValidatorsByDelegator storage + ValidatorsByDelegator::::insert(currency_id, who, validators_list.clone()); + + // Deposit event. + Pallet::::deposit_event(Event::ValidatorsByDelegatorSet { + currency_id, + validators_list, + }); + + Ok(()) + } + + /// Update storage DelegatorLedgers. + #[pallet::weight(T::WeightInfo::set_delegator_ledger())] + pub fn set_delegator_ledger( + origin: OriginFor, + currency_id: CurrencyId, + who: Box, + ledger: Box>>>, + ) -> DispatchResult { + // Check the validity of origin + Self::ensure_authorized(origin, currency_id)?; + + let mins_maxs = MinimumsAndMaximums::::get(KSM).ok_or(Error::::NotExist)?; + // Check the new ledger must has at lease minimum active amount. + if let Some(ref ldgr) = *ledger { + if let Ledger::Substrate(lg) = ldgr { + ensure!( + lg.active >= mins_maxs.delegator_bonded_minimum, + Error::::LowerThanMinimum + ); + } + } + + // Update the ledger. + DelegatorLedgers::::mutate_exists(currency_id, &*who, |old_ledger| { + *old_ledger = *ledger.clone(); + }); + + // Deposit event. + Pallet::::deposit_event(Event::DelegatorLedgerSet { + currency_id, + delegator: *who, + ledger: *ledger, + }); + + Ok(()) + } + + /// Update storage MinimumsAndMaximums. + #[pallet::weight(T::WeightInfo::set_minimums_and_maximums())] + pub fn set_minimums_and_maximums( + origin: OriginFor, + currency_id: CurrencyId, + constraints: Option>>, + ) -> DispatchResult { + // Check the validity of origin + T::ControlOrigin::ensure_origin(origin)?; + + MinimumsAndMaximums::::mutate_exists(currency_id, |minimums_maximums| { + *minimums_maximums = constraints.clone(); + }); + + // Deposit event. + Pallet::::deposit_event(Event::MinimumsMaximumsSet { + currency_id, + minimums_and_maximums: constraints, + }); + + Ok(()) + } + + /// Update storage Delays. + #[pallet::weight(T::WeightInfo::set_currency_delays())] + pub fn set_currency_delays( + origin: OriginFor, + currency_id: CurrencyId, + maybe_delays: Option, + ) -> DispatchResult { + // Check the validity of origin + T::ControlOrigin::ensure_origin(origin)?; + + CurrencyDelays::::mutate_exists(currency_id, |delays| { + *delays = maybe_delays.clone(); + }); + + // Deposit event. + Pallet::::deposit_event(Event::CurrencyDelaysSet { + currency_id, + delays: maybe_delays, + }); + + Ok(()) + } + + /// Set HostingFees storage. + #[pallet::weight(T::WeightInfo::set_hosting_fees())] + pub fn set_hosting_fees( + origin: OriginFor, + currency_id: CurrencyId, + maybe_fee_set: Option<(Permill, MultiLocation)>, + ) -> DispatchResult { + // Check the validity of origin + T::ControlOrigin::ensure_origin(origin)?; + + HostingFees::::mutate_exists(currency_id, |fee_set| { + *fee_set = maybe_fee_set.clone(); + }); + + Pallet::::deposit_event(Event::HostingFeesSet { currency_id, fees: maybe_fee_set }); + + Ok(()) + } + + /// Set CurrencyTuneExchangeRateLimit storage. + #[pallet::weight(T::WeightInfo::set_currency_tune_exchange_rate_limit())] + pub fn set_currency_tune_exchange_rate_limit( + origin: OriginFor, + currency_id: CurrencyId, + maybe_tune_exchange_rate_limit: Option<(u32, Permill)>, + ) -> DispatchResult { + // Check the validity of origin + T::ControlOrigin::ensure_origin(origin)?; + + CurrencyTuneExchangeRateLimit::::mutate_exists(currency_id, |exchange_rate_limit| { + *exchange_rate_limit = maybe_tune_exchange_rate_limit.clone(); + }); + + Pallet::::deposit_event(Event::CurrencyTuneExchangeRateLimitSet { + currency_id, + tune_exchange_rate_limit: maybe_tune_exchange_rate_limit, + }); + + Ok(()) + } + + /// ******************************************************************** + /// *************Outer Confirming Xcm queries functions **************** + /// ******************************************************************** + #[transactional] + #[pallet::weight(T::WeightInfo::confirm_delegator_ledger_query_response())] + pub fn confirm_delegator_ledger_query_response( + origin: OriginFor, + currency_id: CurrencyId, + #[pallet::compact] query_id: QueryId, + ) -> DispatchResult { + // Ensure origin + Self::ensure_authorized(origin, currency_id)?; + Self::get_ledger_update_agent_then_process(query_id, true)?; + Ok(()) + } + + #[transactional] + #[pallet::weight(T::WeightInfo::fail_delegator_ledger_query_response())] + pub fn fail_delegator_ledger_query_response( + origin: OriginFor, + currency_id: CurrencyId, + #[pallet::compact] query_id: QueryId, + ) -> DispatchResult { + // Ensure origin + Self::ensure_authorized(origin, currency_id)?; + + Self::do_fail_delegator_ledger_query_response(query_id)?; + Ok(()) + } + + #[transactional] + #[pallet::weight(T::WeightInfo::confirm_validators_by_delegator_query_response())] + pub fn confirm_validators_by_delegator_query_response( + origin: OriginFor, + currency_id: CurrencyId, + #[pallet::compact] query_id: QueryId, + ) -> DispatchResult { + // Ensure origin + Self::ensure_authorized(origin, currency_id)?; + Self::get_validators_by_delegator_update_agent_then_process(query_id, true)?; + + Ok(()) + } + + #[transactional] + #[pallet::weight(T::WeightInfo::fail_validators_by_delegator_query_response())] + pub fn fail_validators_by_delegator_query_response( + origin: OriginFor, + currency_id: CurrencyId, + #[pallet::compact] query_id: QueryId, + ) -> DispatchResult { + // Ensure origin + Self::ensure_authorized(origin, currency_id)?; + + Self::do_fail_validators_by_delegator_query_response(query_id)?; + Ok(()) + } + } + + impl Pallet { + /// Ensure privileged origin + fn ensure_authorized( + origin: OriginFor, + currency_id: CurrencyId, + ) -> Result<(), Error> { + match origin.clone().into() { + Ok(RawOrigin::Root) => Ok(()), + Ok(RawOrigin::Signed(ref signer)) + if Some(signer) == >::get(currency_id).as_ref() => + Ok(()), + Ok(RawOrigin::Signed(_)) => T::ControlOrigin::ensure_origin(origin) + .map(|_| ()) + .map_err(|_| Error::::NotAuthorized), + _ => Err(Error::::NotAuthorized), + } + } + + /// Convert native multiLocation to account. + fn native_multilocation_to_account( + who: &MultiLocation, + ) -> Result, Error> { + // Get the delegator account id in Kusama network + let account_32 = match who { + MultiLocation { + parents: 0, + interior: X1(AccountId32 { network: _network_id, id: account_id }), + } => account_id, + _ => Err(Error::::AccountNotExist)?, + }; + + let account = T::AccountId::decode(&mut &account_32[..]) + .map_err(|_| Error::::DecodingError)?; + + Ok(account) + } + + fn get_currency_staking_agent( + currency_id: CurrencyId, + ) -> Result, Error> { + match currency_id { + KSM => Ok(Box::new(KusamaAgent::::new())), + _ => Err(Error::::NotSupportedCurrencyId), + } + } + + pub fn sort_validators_and_remove_duplicates( + currency_id: CurrencyId, + validators: &Vec, + ) -> Result)>, Error> { + let validators_set = + Validators::::get(currency_id).ok_or(Error::::ValidatorSetNotExist)?; + let mut validators_list: Vec<(MultiLocation, Hash)> = vec![]; + for validator in validators.iter() { + // Check if the validator is in the validator whitelist + let multi_hash = ::Hashing::hash(&validator.encode()); + ensure!( + validators_set.contains(&(validator.clone(), multi_hash)), + Error::::ValidatorNotExist + ); + + // sort the validators and remove duplicates + let rs = validators_list.binary_search_by_key(&multi_hash, |(_multi, hash)| *hash); + + if let Err(index) = rs { + validators_list.insert(index, (validator.clone(), multi_hash)); + } + } + + Ok(validators_list) + } + + pub fn multilocation_to_account(who: &MultiLocation) -> Result, Error> { + // Get the delegator account id in Kusama network + let account_32 = Self::multilocation_to_account_32(who)?; + let account = T::AccountId::decode(&mut &account_32[..]) + .map_err(|_| Error::::DecodingError)?; + Ok(account) + } + + pub fn multilocation_to_account_32(who: &MultiLocation) -> Result<[u8; 32], Error> { + // Get the delegator account id in Kusama network + let account_32 = match who { + MultiLocation { + parents: _, + interior: X1(AccountId32 { network: _network_id, id: account_id }), + } => account_id, + _ => Err(Error::::AccountNotExist)?, + }; + Ok(*account_32) + } + + pub fn account_id_to_account_32(account_id: AccountIdOf) -> Result<[u8; 32], Error> { + let account_32 = T::AccountId::encode(&account_id) + .try_into() + .map_err(|_| Error::::EncodingError)?; + + Ok(account_32) + } + + pub fn account_32_to_local_location( + account_32: [u8; 32], + ) -> Result> { + let local_location = MultiLocation { + parents: 0, + interior: X1(AccountId32 { network: Any, id: account_32 }), + }; + + Ok(local_location) + } + + pub fn account_32_to_parent_location( + account_32: [u8; 32], + ) -> Result> { + let parent_location = MultiLocation { + parents: 1, + interior: X1(AccountId32 { network: Any, id: account_32 }), + }; + + Ok(parent_location) + } + + pub fn account_32_to_parachain_location( + account_32: [u8; 32], + chain_id: u32, + ) -> Result> { + let parachain_location = MultiLocation { + parents: 1, + interior: X2(Parachain(chain_id), AccountId32 { network: Any, id: account_32 }), + }; + + Ok(parachain_location) + } + + /// **************************************/ + /// ****** XCM confirming Functions ******/ + /// **************************************/ + #[transactional] + pub fn process_query_entry_records() -> Result> { + let mut counter = 0u32; + + // Deal with DelegatorLedgerXcmUpdateQueue storage + for query_id in DelegatorLedgerXcmUpdateQueue::::iter_keys() { + if counter >= T::MaxTypeEntryPerBlock::get() { + break; + } + + let updated = Self::get_ledger_update_agent_then_process(query_id, false)?; + if updated { + counter = counter.saturating_add(1); + } + } + + // Deal with ValidatorsByDelegator storage + for query_id in ValidatorsByDelegatorXcmUpdateQueue::::iter_keys() { + if counter >= T::MaxTypeEntryPerBlock::get() { + break; + } + let updated = + Self::get_validators_by_delegator_update_agent_then_process(query_id, false)?; + + if updated { + counter = counter.saturating_add(1); + } + } + + Ok(counter) + } + + pub fn get_ledger_update_agent_then_process( + query_id: QueryId, + manual_mode: bool, + ) -> Result> { + // See if the query exists. If it exists, call corresponding chain storage update + // function. + let (entry, timeout) = Self::get_delegator_ledger_update_entry(query_id) + .ok_or(Error::::QueryNotExist)?; + + let now = frame_system::Pallet::::block_number(); + let mut updated = true; + if now <= timeout { + let currency_id = match entry.clone() { + LedgerUpdateEntry::Substrate(substrate_entry) => + Some(substrate_entry.currency_id), + _ => None, + } + .ok_or(Error::::NotSupportedCurrencyId)?; + + let staking_agent = Self::get_currency_staking_agent(currency_id)?; + updated = staking_agent.check_delegator_ledger_query_response( + query_id, + entry, + manual_mode, + )?; + } else { + Self::do_fail_delegator_ledger_query_response(query_id)?; + } + + Ok(updated) + } + + pub fn get_validators_by_delegator_update_agent_then_process( + query_id: QueryId, + manual_mode: bool, + ) -> Result> { + // See if the query exists. If it exists, call corresponding chain storage update + // function. + let (entry, timeout) = Self::get_validators_by_delegator_update_entry(query_id) + .ok_or(Error::::QueryNotExist)?; + + let now = frame_system::Pallet::::block_number(); + let mut updated = true; + if now <= timeout { + let currency_id = match entry.clone() { + ValidatorsByDelegatorUpdateEntry::Substrate(substrate_entry) => + Some(substrate_entry.currency_id), + _ => None, + } + .ok_or(Error::::NotSupportedCurrencyId)?; + + let staking_agent = Self::get_currency_staking_agent(currency_id)?; + updated = staking_agent.check_validators_by_delegator_query_response( + query_id, + entry, + manual_mode, + )?; + } else { + Self::do_fail_validators_by_delegator_query_response(query_id)?; + } + Ok(updated) + } + + fn do_fail_delegator_ledger_query_response(query_id: QueryId) -> Result<(), Error> { + // See if the query exists. If it exists, call corresponding chain storage update + // function. + let (entry, _) = Self::get_delegator_ledger_update_entry(query_id) + .ok_or(Error::::QueryNotExist)?; + let currency_id = match entry { + LedgerUpdateEntry::Substrate(substrate_entry) => Some(substrate_entry.currency_id), + _ => None, + } + .ok_or(Error::::NotSupportedCurrencyId)?; + + let staking_agent = Self::get_currency_staking_agent(currency_id)?; + staking_agent.fail_delegator_ledger_query_response(query_id)?; + + Ok(()) + } + + fn do_fail_validators_by_delegator_query_response( + query_id: QueryId, + ) -> Result<(), Error> { + // See if the query exists. If it exists, call corresponding chain storage update + // function. + let (entry, _) = Self::get_validators_by_delegator_update_entry(query_id) + .ok_or(Error::::QueryNotExist)?; + let currency_id = match entry { + ValidatorsByDelegatorUpdateEntry::Substrate(substrate_entry) => + Some(substrate_entry.currency_id), + _ => None, + } + .ok_or(Error::::NotSupportedCurrencyId)?; + + let staking_agent = Self::get_currency_staking_agent(currency_id)?; + staking_agent.fail_validators_by_delegator_query_response(query_id)?; + + Ok(()) + } + } +} diff --git a/pallets/slp/src/mock.rs b/pallets/slp/src/mock.rs new file mode 100644 index 0000000000..f9c5d6d367 --- /dev/null +++ b/pallets/slp/src/mock.rs @@ -0,0 +1,316 @@ +// This file is part of Bifrost. + +// Copyright (C) 2019-2022 Liebi Technologies (UK) Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +// This program 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. + +// This program 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 this program. If not, see . + +// Ensure we're `no_std` when compiling for Wasm. + +#![cfg(test)] + +use codec::{Decode, Encode}; +pub use cumulus_primitives_core::ParaId; +use frame_support::{ + construct_runtime, ord_parameter_types, + pallet_prelude::Get, + parameter_types, + traits::{GenesisBuild, Nothing}, + PalletId, +}; +use frame_system::EnsureSignedBy; +use hex_literal::hex; +use node_primitives::{Amount, Balance, CurrencyId, TokenSymbol}; +use sp_core::{blake2_256, H256}; +pub use sp_runtime::{testing::Header, Perbill}; +use sp_runtime::{ + traits::{AccountIdConversion, Convert, IdentityLookup, TrailingZeroInput}, + AccountId32, +}; +use sp_std::{boxed::Box, vec::Vec}; +use xcm::latest::prelude::*; + +use crate as bifrost_slp; +use crate::{Config, QueryResponseManager}; + +pub type AccountId = AccountId32; +pub type Block = frame_system::mocking::MockBlock; +pub type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic; + +pub const ALICE: AccountId = AccountId32::new([1u8; 32]); +pub const BOB: AccountId = AccountId32::new([2u8; 32]); +pub const CHARLIE: AccountId = AccountId32::new([3u8; 32]); +pub const DAVE: AccountId = AccountId32::new([4u8; 32]); +pub const EDDIE: AccountId = AccountId32::new([5u8; 32]); + +pub const BNC: CurrencyId = CurrencyId::Native(TokenSymbol::BNC); +pub const KSM: CurrencyId = CurrencyId::Token(TokenSymbol::KSM); +pub const VKSM: CurrencyId = CurrencyId::VToken(TokenSymbol::KSM); + +construct_runtime!( + pub enum Runtime where + Block = Block, + NodeBlock = Block, + UncheckedExtrinsic = UncheckedExtrinsic, + { + System: frame_system::{Pallet, Call, Config, Storage, Event}, + Balances: pallet_balances::{Pallet, Call, Storage, Config, Event}, + Currencies: orml_currencies::{Pallet, Call, Event}, + Tokens: orml_tokens::{Pallet, Call, Storage, Event}, + Slp: bifrost_slp::{Pallet, Call, Storage, Event}, + VtokenMinting: bifrost_vtoken_minting::{Pallet, Call, Storage, Event}, + } +); + +parameter_types! { + pub const NativeCurrencyId: CurrencyId = BNC; + pub const RelayCurrencyId: CurrencyId = KSM; +} + +parameter_types! { + pub const BlockHashCount: u64 = 250; +} + +impl frame_system::Config for Runtime { + type Origin = Origin; + type Index = u64; + type BlockNumber = u64; + type Call = Call; + type Hash = H256; + type Hashing = ::sp_runtime::traits::BlakeTwo256; + type AccountId = AccountId; + type Lookup = IdentityLookup; + type Header = Header; + type Event = Event; + type BlockHashCount = BlockHashCount; + type BlockWeights = (); + type BlockLength = (); + type Version = (); + type PalletInfo = PalletInfo; + type AccountData = pallet_balances::AccountData; + type OnNewAccount = (); + type OnKilledAccount = (); + type DbWeight = (); + type BaseCallFilter = frame_support::traits::Everything; + type SystemWeightInfo = (); + type SS58Prefix = (); + type OnSetCode = (); + type MaxConsumers = frame_support::traits::ConstU32<16>; +} + +parameter_types! { + pub const ExistentialDeposit: u128 = 0; + pub const MaxLocks: u32 = 999_999; + pub const MaxReserves: u32 = 999_999; +} + +impl pallet_balances::Config for Runtime { + type AccountStore = System; + /// The type for recording an account's balance. + type Balance = Balance; + type DustRemoval = (); + /// The ubiquitous event type. + type Event = Event; + type ExistentialDeposit = ExistentialDeposit; + type MaxLocks = MaxLocks; + type MaxReserves = MaxReserves; + type ReserveIdentifier = [u8; 8]; + type WeightInfo = pallet_balances::weights::SubstrateWeight; +} + +orml_traits::parameter_type_with_key! { + pub ExistentialDeposits: |_currency_id: CurrencyId| -> Balance { + 0 + }; +} + +impl orml_tokens::Config for Runtime { + type Amount = Amount; + type Balance = Balance; + type CurrencyId = CurrencyId; + type DustRemovalWhitelist = Nothing; + type Event = Event; + type ExistentialDeposits = ExistentialDeposits; + type MaxLocks = MaxLocks; + type OnDust = (); + type WeightInfo = (); +} + +pub type BifrostToken = orml_currencies::BasicCurrencyAdapter; + +impl orml_currencies::Config for Runtime { + type Event = Event; + type GetNativeCurrencyId = NativeCurrencyId; + type MultiCurrency = Tokens; + type NativeCurrency = BifrostToken; + type WeightInfo = (); +} + +parameter_types! { + pub const MaximumUnlockIdOfUser: u32 = 10; + pub const MaximumUnlockIdOfTimeUnit: u32 = 50; + pub BifrostEntranceAccount: PalletId = PalletId(*b"bf/vtkin"); + pub BifrostExitAccount: PalletId = PalletId(*b"bf/vtout"); + pub BifrostFeeAccount: AccountId = hex!["e4da05f08e89bf6c43260d96f26fffcfc7deae5b465da08669a9d008e64c2c63"].into(); +} + +impl bifrost_vtoken_minting::Config for Runtime { + type Event = Event; + type MultiCurrency = Currencies; + type ControlOrigin = EnsureSignedBy; + type MaximumUnlockIdOfUser = MaximumUnlockIdOfUser; + type MaximumUnlockIdOfTimeUnit = MaximumUnlockIdOfTimeUnit; + type EntranceAccount = BifrostEntranceAccount; + type ExitAccount = BifrostExitAccount; + type FeeAccount = BifrostFeeAccount; + type WeightInfo = (); +} + +ord_parameter_types! { + pub const One: AccountId = AccountId32::new([1u8; 32]); +} + +pub struct SubAccountIndexMultiLocationConvertor; +impl Convert for SubAccountIndexMultiLocationConvertor { + fn convert(sub_account_index: u16) -> MultiLocation { + MultiLocation::new( + 1, + X1(Junction::AccountId32 { + network: NetworkId::Any, + // id: Utility::derivative_account_id( + // ParaId::from(2001u32).into_account(), + // sub_account_index, + // ) + // .into(), + id: Self::derivative_account_id( + ParaId::from(2001u32).into_account(), + sub_account_index, + ) + .into(), + }), + ) + } +} + +// Mock Utility::derivative_account_id function. +impl SubAccountIndexMultiLocationConvertor { + pub fn derivative_account_id(who: AccountId, index: u16) -> AccountId { + let entropy = (b"modlpy/utilisuba", who, index).using_encoded(blake2_256); + Decode::decode(&mut TrailingZeroInput::new(entropy.as_ref())) + .expect("infinite length input; no invalid inputs for type; qed") + } +} + +pub struct ParachainId; +impl Get for ParachainId { + fn get() -> ParaId { + 2001.into() + } +} + +parameter_types! { + pub const MaxTypeEntryPerBlock: u32 = 10; + pub const MaxRefundPerBlock: u32 = 10; +} + +impl QueryResponseManager for () { + fn get_query_response_record(query_id: QueryId) -> bool { + Default::default() + } + fn create_query_record(responder: &MultiLocation, timeout: u64) -> u64 { + Default::default() + } + fn remove_query_record(query_id: QueryId) -> bool { + Default::default() + } +} + +impl Config for Runtime { + type Event = Event; + type MultiCurrency = Currencies; + type ControlOrigin = EnsureSignedBy; + type WeightInfo = (); + type VtokenMinting = VtokenMinting; + type AccountConverter = SubAccountIndexMultiLocationConvertor; + type ParachainId = ParachainId; + type XcmRouter = (); + type XcmExecutor = (); + type SubstrateResponseManager = (); + type MaxTypeEntryPerBlock = MaxTypeEntryPerBlock; + type MaxRefundPerBlock = MaxRefundPerBlock; +} + +pub struct ExtBuilder { + endowed_accounts: Vec<(AccountId, CurrencyId, Balance)>, +} + +impl Default for ExtBuilder { + fn default() -> Self { + Self { endowed_accounts: vec![] } + } +} + +impl ExtBuilder { + pub fn balances(mut self, endowed_accounts: Vec<(AccountId, CurrencyId, Balance)>) -> Self { + self.endowed_accounts = endowed_accounts; + self + } + + pub fn one_hundred_for_alice(self) -> Self { + self.balances(vec![(ALICE, BNC, 100), (ALICE, KSM, 100), (ALICE, VKSM, 100)]) + } + + #[cfg(feature = "runtime-benchmarks")] + pub fn one_hundred_precision_for_each_currency_type_for_whitelist_account(self) -> Self { + use frame_benchmarking::whitelisted_caller; + use sp_runtime::traits::AccountIdConversion; + let whitelist_caller: AccountId = whitelisted_caller(); + let pool_account: AccountId = LighteningRedeemPalletId::get().into_account(); + + self.balances(vec![ + (whitelist_caller.clone(), KSM, 100_000_000_000_000), + (whitelist_caller.clone(), VKSM, 100_000_000_000_000), + (pool_account.clone(), KSM, 100_000_000_000_000), + ]) + } + + pub fn build(self) -> sp_io::TestExternalities { + let mut t = frame_system::GenesisConfig::default().build_storage::().unwrap(); + + pallet_balances::GenesisConfig:: { + balances: self + .endowed_accounts + .clone() + .into_iter() + .filter(|(_, currency_id, _)| *currency_id == BNC) + .map(|(account_id, _, initial_balance)| (account_id, initial_balance)) + .collect::>(), + } + .assimilate_storage(&mut t) + .unwrap(); + + orml_tokens::GenesisConfig:: { + balances: self + .endowed_accounts + .clone() + .into_iter() + .filter(|(_, currency_id, _)| *currency_id != BNC) + .collect::>(), + } + .assimilate_storage(&mut t) + .unwrap(); + + t.into() + } +} diff --git a/pallets/slp/src/primitives.rs b/pallets/slp/src/primitives.rs new file mode 100644 index 0000000000..61432d3fed --- /dev/null +++ b/pallets/slp/src/primitives.rs @@ -0,0 +1,158 @@ +// This file is part of Bifrost. + +// Copyright (C) 2019-2022 Liebi Technologies (UK) Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +// This program 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. + +// This program 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 this program. If not, see . + +use codec::{Decode, Encode}; +use frame_support::RuntimeDebug; +use node_primitives::{CurrencyId, TimeUnit, TokenSymbol}; +use scale_info::TypeInfo; +use sp_std::vec::Vec; + +/// Simplify the CurrencyId. +pub const KSM: CurrencyId = CurrencyId::Token(TokenSymbol::KSM); + +#[derive(PartialEq, Eq, Clone, Encode, Decode, RuntimeDebug, TypeInfo)] +pub enum Ledger { + Substrate(SubstrateLedger), +} + +#[derive(PartialEq, Eq, Clone, Encode, Decode, RuntimeDebug, TypeInfo)] +pub struct SubstrateLedger { + /// The delegator account Id + pub account: DelegatorId, + /// The total amount of the delegator's balance that we are currently accounting for. + /// It's just `active` plus all the `unlocking` balances. + #[codec(compact)] + pub total: Balance, + /// The total amount of the delegator's balance that will be at stake in any forthcoming + /// rounds. + #[codec(compact)] + pub active: Balance, + /// Any balance that is becoming free, which may eventually be transferred out + /// of the delegator (assuming it doesn't get slashed first). + pub unlocking: Vec>, +} + +/// Just a Balance/BlockNumber tuple to encode when a chunk of funds will be unlocked. +#[derive(PartialEq, Eq, Clone, Encode, Decode, RuntimeDebug, TypeInfo)] +pub struct UnlockChunk { + /// Amount of funds to be unlocked. + #[codec(compact)] + pub value: Balance, + /// Era number at which point it'll be unlocked. + pub unlock_time: TimeUnit, +} + +/// A type for accommodating delegator update entries for different kinds of currencies. +#[derive(PartialEq, Eq, Clone, Encode, Decode, RuntimeDebug, TypeInfo)] +pub enum LedgerUpdateEntry { + /// A type for substrate ledger updating entires + Substrate(SubstrateLedgerUpdateEntry), +} + +/// A type for substrate ledger updating entires +#[derive(PartialEq, Eq, Clone, Encode, Decode, RuntimeDebug, TypeInfo)] +pub struct SubstrateLedgerUpdateEntry { + /// The currency id of the delegator that needs to be update + pub currency_id: CurrencyId, + /// The delegator id that needs to be update + pub delegator_id: DelegatorId, + /// If this is true, then this is a bonding entry. + pub if_bond: bool, + /// If this is true and if_bond is false, then this is an unlocking entry. + pub if_unlock: bool, + /// If if_bond and if_unlock is false but if_rebond is true. Then it is a rebonding operation. + /// If if_bond, if_unlock and if_rebond are all false, then it is a liquidize operation. + pub if_rebond: bool, + /// The unlocking/bonding amount. + #[codec(compact)] + pub amount: Balance, + /// If this entry is an unlocking entry, it should have unlock_time value. If it is a bonding + /// entry, this field should be None. If it is a liquidize entry, this filed is the ongoing + /// timeunit when the xcm message is sent. + pub unlock_time: Option, +} + +/// A type for accommodating validators by delegator update entries for different kinds of +/// currencies. +#[derive(PartialEq, Eq, Clone, Encode, Decode, RuntimeDebug, TypeInfo)] +pub enum ValidatorsByDelegatorUpdateEntry { + /// A type for substrate validators by delegator updating entires + Substrate(SubstrateValidatorsByDelegatorUpdateEntry), +} + +/// A type for substrate validators by delegator updating entires +#[derive(PartialEq, Eq, Clone, Encode, Decode, RuntimeDebug, TypeInfo)] +pub struct SubstrateValidatorsByDelegatorUpdateEntry { + /// The currency id of the delegator that needs to be update + pub currency_id: CurrencyId, + /// The delegator id that needs to be update + pub delegator_id: DelegatorId, + /// Validators vec to be updated + pub validators: Vec<(ValidatorId, HashT)>, +} + +/// Different minimum and maximum requirements for different chain +#[derive(PartialEq, Eq, Clone, Encode, Decode, RuntimeDebug, TypeInfo)] +pub struct MinimumsMaximums { + /// The minimum bonded amount for a delegator at any time. + #[codec(compact)] + pub delegator_bonded_minimum: Balance, + /// The minimum amount each time a delegator needs to bond for extra + #[codec(compact)] + pub bond_extra_minimum: Balance, + /// The minimum unbond amount each time a delegator to unbond. + #[codec(compact)] + pub unbond_minimum: Balance, + /// The minimum amount each time a delegator needs to rebond + #[codec(compact)] + pub rebond_minimum: Balance, + /// The maximum number of unbond records at the same time. + #[codec(compact)] + pub unbond_record_maximum: u32, + /// The maximum number of validators for a delegator to support at the same time. + #[codec(compact)] + pub validators_back_maximum: u32, + /// The maximum amount of active staking for a delegator. It is used to control ROI. + #[codec(compact)] + pub delegator_active_staking_maximum: Balance, +} + +/// Different delay params for different chain +#[derive(PartialEq, Eq, Clone, Encode, Decode, RuntimeDebug, TypeInfo)] +pub struct Delays { + /// The unlock delay for the unlocking amount to be able to be liquidized. + pub unlock_delay: TimeUnit, +} + +/// XCM operations list +#[derive(Encode, Decode, Eq, PartialEq, Copy, Clone, RuntimeDebug, TypeInfo)] +pub enum XcmOperation { + // XTokens + XtokensTransfer, + Bond, + WithdrawUnbonded, + BondExtra, + Unbond, + Rebond, + Delegate, + Payout, + Liquidize, + TransferBack, + TransferTo, + Chill, +} diff --git a/pallets/slp/src/tests.rs b/pallets/slp/src/tests.rs new file mode 100644 index 0000000000..a594d39a0d --- /dev/null +++ b/pallets/slp/src/tests.rs @@ -0,0 +1,508 @@ +// This file is part of Bifrost. + +// Copyright (C) 2019-2022 Liebi Technologies (UK) Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +// This program 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. + +// This program 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 this program. If not, see . + +#![cfg(test)] + +use frame_support::assert_ok; +use mock::*; +use orml_traits::MultiCurrency; + +use super::*; +use crate::KSM; + +#[test] +fn set_xcm_dest_weight_and_fee_should_work() { + ExtBuilder::default().build().execute_with(|| { + System::set_block_number(1); + + // Insert a new record. + assert_ok!(Slp::set_xcm_dest_weight_and_fee( + Origin::signed(ALICE), + KSM, + XcmOperation::Bond, + Some((5_000_000_000, 5_000_000_000)) + )); + + assert_eq!( + XcmDestWeightAndFee::::get(KSM, XcmOperation::Bond), + Some((5_000_000_000, 5_000_000_000)) + ); + + // Delete a record. + assert_ok!(Slp::set_xcm_dest_weight_and_fee( + Origin::signed(ALICE), + KSM, + XcmOperation::Bond, + None + )); + + assert_eq!(XcmDestWeightAndFee::::get(KSM, XcmOperation::Bond), None); + }); +} + +#[test] +fn construct_xcm_and_send_as_subaccount_should_work() { + let para_chain_account: AccountId = + hex_literal::hex!["70617261d1070000000000000000000000000000000000000000000000000000"] + .into(); + + let sub_account_id = SubAccountIndexMultiLocationConvertor::derivative_account_id( + para_chain_account.clone(), + 0u16, + ); + + // parachain_account 2001: 5Ec4AhPV91i9yNuiWuNunPf6AQCYDhFTTA4G5QCbtqYApH9E + // hex: 70617261d1070000000000000000000000000000000000000000000000000000 + println!("para_string: {:?}", para_chain_account); + // sub_account index:0(sub_account_id.to_string())) + // 5E78xTBiaN3nAGYtcNnqTJQJqYAkSDGggKqaDfpNsKyPpbcb + // hex: 5a53736d8e96f1c007cf0d630acf5209b20611617af23ce924c8e25328eb5d28 + println!("sub_string: {:?}", sub_account_id); +} + +#[test] +fn set_fee_source_works() { + ExtBuilder::default().build().execute_with(|| { + System::set_block_number(1); + + let alice_32 = Pallet::::account_id_to_account_32(ALICE).unwrap(); + let alice_location = Pallet::::account_32_to_local_location(alice_32).unwrap(); + + // Insert a new record. + assert_ok!(Slp::set_fee_source( + Origin::signed(ALICE), + KSM, + Some((alice_location.clone(), 1_000_000_000_000)) + )); + assert_eq!(FeeSources::::get(KSM), Some((alice_location, 1_000_000_000_000))); + }); +} + +// test native token fee supplement. Non-native will be tested in the integration tests. +#[test] +fn supplement_fee_reserve_works() { + ExtBuilder::default().one_hundred_for_alice().build().execute_with(|| { + // set fee source + let alice_32 = Pallet::::account_id_to_account_32(ALICE).unwrap(); + let alice_location = Pallet::::account_32_to_local_location(alice_32).unwrap(); + assert_ok!(Slp::set_fee_source( + Origin::signed(ALICE), + BNC, + Some((alice_location.clone(), 10)) + )); + + // supplement fee + let bob_32 = Pallet::::account_id_to_account_32(BOB).unwrap(); + let bob_location = Pallet::::account_32_to_local_location(bob_32).unwrap(); + assert_eq!(Balances::free_balance(&ALICE), 100); + assert_eq!(Balances::free_balance(&BOB), 0); + + assert_ok!(Slp::supplement_fee_reserve(Origin::signed(ALICE), BNC, bob_location.clone())); + + assert_eq!(Balances::free_balance(&ALICE), 90); + assert_eq!(Balances::free_balance(&BOB), 10); + }); +} + +#[test] +fn remove_delegator_works() { + ExtBuilder::default().build().execute_with(|| { + // 5E78xTBiaN3nAGYtcNnqTJQJqYAkSDGggKqaDfpNsKyPpbcb + let subaccount_0: AccountId = + hex_literal::hex!["5a53736d8e96f1c007cf0d630acf5209b20611617af23ce924c8e25328eb5d28"] + .into(); + let subaccount_0_32: [u8; 32] = + Slp::account_id_to_account_32(subaccount_0.clone()).unwrap(); + let subaccount_0_location: MultiLocation = + Slp::account_32_to_parent_location(subaccount_0_32).unwrap(); + + DelegatorsIndex2Multilocation::::insert(KSM, 0, subaccount_0_location.clone()); + DelegatorsMultilocation2Index::::insert(KSM, subaccount_0_location.clone(), 0); + + let mins_and_maxs = MinimumsMaximums { + delegator_bonded_minimum: 100_000_000_000, + bond_extra_minimum: 0, + unbond_minimum: 0, + rebond_minimum: 0, + unbond_record_maximum: 32, + validators_back_maximum: 36, + delegator_active_staking_maximum: 200_000_000_000_000, + }; + + // Set minimums and maximums + MinimumsAndMaximums::::insert(KSM, mins_and_maxs); + + let sb_ledger = SubstrateLedger { + account: subaccount_0_location.clone(), + total: 0, + active: 0, + unlocking: vec![], + }; + let ledger = Ledger::Substrate(sb_ledger); + + // Set delegator ledger + DelegatorLedgers::::insert(KSM, subaccount_0_location.clone(), ledger); + + assert_ok!(Slp::remove_delegator( + Origin::signed(ALICE), + KSM, + subaccount_0_location.clone() + )); + + assert_eq!(DelegatorsIndex2Multilocation::::get(KSM, 0), None); + assert_eq!( + DelegatorsMultilocation2Index::::get(KSM, subaccount_0_location.clone()), + None + ); + assert_eq!(DelegatorLedgers::::get(KSM, subaccount_0_location), None); + }); +} + +/// **************************************** +// Below is the VtokenMinting api testing * +/// **************************************** + +#[test] +fn decrease_token_pool_works() { + ExtBuilder::default().build().execute_with(|| { + // Set token pool as 100. + bifrost_vtoken_minting::TokenPool::::insert(KSM, 100); + + // Decrease token pool by 10. + assert_ok!(Slp::decrease_token_pool(Origin::signed(ALICE), KSM, 10)); + + // Check the value after decreasing + assert_eq!(VtokenMinting::token_pool(KSM), 90); + }); +} + +#[test] +fn update_ongoing_time_unit_works() { + ExtBuilder::default().build().execute_with(|| { + // Set the era to be 8. + bifrost_vtoken_minting::OngoingTimeUnit::::insert(KSM, TimeUnit::Era(8)); + + // Update the era to be 9. + assert_ok!(Slp::update_ongoing_time_unit(Origin::signed(ALICE), KSM, TimeUnit::Era(9))); + + // Check the value after updating. + assert_eq!(VtokenMinting::ongoing_time_unit(KSM), Some(TimeUnit::Era(9))); + }); +} + +#[test] +fn refund_currency_due_unbond_works() { + ExtBuilder::default().build().execute_with(|| { + // Preparations + // get entrance and exit accounts + let (entrance_acc, exit_acc) = VtokenMinting::get_entrance_and_exit_accounts(); + // Set exit account balance to be 50. + assert_ok!(Tokens::set_balance(Origin::root(), exit_acc.clone(), KSM, 50, 0)); + + // set current era to be 100. + bifrost_vtoken_minting::OngoingTimeUnit::::insert(KSM, TimeUnit::Era(100)); + + // Set TokenUnlockLedger records. + let record_bob = (BOB, 10, TimeUnit::Era(90)); + bifrost_vtoken_minting::TokenUnlockLedger::::insert(KSM, 0, record_bob); + + let record_charlie = (CHARLIE, 28, TimeUnit::Era(100)); + bifrost_vtoken_minting::TokenUnlockLedger::::insert(KSM, 1, record_charlie); + + let record_dave = (DAVE, 30, TimeUnit::Era(100)); + bifrost_vtoken_minting::TokenUnlockLedger::::insert(KSM, 2, record_dave); + + let record_eddie_1 = (EDDIE, 7, TimeUnit::Era(110)); + bifrost_vtoken_minting::TokenUnlockLedger::::insert(KSM, 3, record_eddie_1); + + let record_eddie_2 = (EDDIE, 6, TimeUnit::Era(110)); + bifrost_vtoken_minting::TokenUnlockLedger::::insert(KSM, 4, record_eddie_2); + + // insert TimeUnitUnlockLedger records + let bounded_vec_90 = BoundedVec::try_from(vec![0]).unwrap(); + let time_record_90 = (10, bounded_vec_90, KSM); + bifrost_vtoken_minting::TimeUnitUnlockLedger::::insert( + TimeUnit::Era(90), + KSM, + time_record_90.clone(), + ); + + let bounded_vec_100 = BoundedVec::try_from(vec![1, 2]).unwrap(); + let time_record_100 = (58, bounded_vec_100, KSM); + bifrost_vtoken_minting::TimeUnitUnlockLedger::::insert( + TimeUnit::Era(100), + KSM, + time_record_100, + ); + + let bounded_vec_110 = BoundedVec::try_from(vec![3, 4]).unwrap(); + let time_record_110 = (13, bounded_vec_110, KSM); + bifrost_vtoken_minting::TimeUnitUnlockLedger::::insert( + TimeUnit::Era(110), + KSM, + time_record_110.clone(), + ); + + // insert UserUnlockLedger records. + let bounded_vec_bob = BoundedVec::try_from(vec![0]).unwrap(); + bifrost_vtoken_minting::UserUnlockLedger::::insert( + BOB, + KSM, + (10, bounded_vec_bob.clone()), + ); + + let bounded_vec_charlie = BoundedVec::try_from(vec![1]).unwrap(); + bifrost_vtoken_minting::UserUnlockLedger::::insert( + CHARLIE, + KSM, + (28, bounded_vec_charlie.clone()), + ); + + let bounded_vec_dave = BoundedVec::try_from(vec![2]).unwrap(); + bifrost_vtoken_minting::UserUnlockLedger::::insert( + DAVE, + KSM, + (30, bounded_vec_dave.clone()), + ); + + let bounded_vec_eddie = BoundedVec::try_from(vec![3, 4]).unwrap(); + bifrost_vtoken_minting::UserUnlockLedger::::insert( + EDDIE, + KSM, + (13, bounded_vec_eddie.clone()), + ); + + bifrost_vtoken_minting::CurrencyUnlockingTotal::::set(1000); + + // check account balances before refund + assert_eq!(Tokens::free_balance(KSM, &exit_acc), 50); + assert_eq!(Tokens::free_balance(KSM, &BOB), 0); + assert_eq!(Tokens::free_balance(KSM, &CHARLIE), 0); + assert_eq!(Tokens::free_balance(KSM, &DAVE), 0); + assert_eq!(Tokens::free_balance(KSM, &EDDIE), 0); + + // Refund user + assert_ok!(Slp::refund_currency_due_unbond(Origin::signed(ALICE), KSM)); + + // Check account balances after refund + assert_eq!(Tokens::free_balance(KSM, &exit_acc), 0); + assert_eq!(Tokens::free_balance(KSM, &BOB), 0); + assert_eq!(Tokens::free_balance(KSM, &CHARLIE), 28); + assert_eq!(Tokens::free_balance(KSM, &DAVE), 22); + assert_eq!(Tokens::free_balance(KSM, &EDDIE), 0); + + // Check storage + // Unlocking records for era 90 + assert_eq!( + bifrost_vtoken_minting::TimeUnitUnlockLedger::::get(TimeUnit::Era(90), KSM,), + Some(time_record_90) + ); + assert_eq!( + bifrost_vtoken_minting::UserUnlockLedger::::get(BOB, KSM,), + Some((10, bounded_vec_bob.clone())) + ); + + // Unlocking records for era 100 + let bounded_vec_100_new = BoundedVec::try_from(vec![2]).unwrap(); + let time_record_100_new = (8, bounded_vec_100_new, KSM); + let record_dave_new = (DAVE, 8, TimeUnit::Era(100)); + assert_eq!( + bifrost_vtoken_minting::TokenUnlockLedger::::get(KSM, 2), + Some(record_dave_new.clone()) + ); + + assert_eq!( + bifrost_vtoken_minting::TokenUnlockLedger::::get(KSM, 2), + Some(record_dave_new) + ); + + assert_eq!( + bifrost_vtoken_minting::TimeUnitUnlockLedger::::get(TimeUnit::Era(100), KSM,), + Some(time_record_100_new) + ); + + assert_eq!(bifrost_vtoken_minting::UserUnlockLedger::::get(CHARLIE, KSM,), None); + assert_eq!( + bifrost_vtoken_minting::UserUnlockLedger::::get(DAVE, KSM,), + Some((8, bounded_vec_dave.clone())) + ); + + // Unlocking records for era 110 + assert_eq!( + bifrost_vtoken_minting::TimeUnitUnlockLedger::::get(TimeUnit::Era(110), KSM,), + Some(time_record_110) + ); + + assert_eq!( + bifrost_vtoken_minting::UserUnlockLedger::::get(EDDIE, KSM,), + Some((13, bounded_vec_eddie.clone())) + ); + + // Set some more balance to exit account. + assert_ok!(Tokens::set_balance(Origin::root(), exit_acc.clone(), KSM, 30, 0)); + + // set era to 110 + bifrost_vtoken_minting::OngoingTimeUnit::::insert(KSM, TimeUnit::Era(110)); + + // Refund user + assert_ok!(Slp::refund_currency_due_unbond(Origin::signed(ALICE), KSM)); + + // Check storages + assert_eq!( + bifrost_vtoken_minting::TimeUnitUnlockLedger::::get(TimeUnit::Era(110), KSM,), + None + ); + + assert_eq!(bifrost_vtoken_minting::TokenUnlockLedger::::get(KSM, 3), None); + assert_eq!(bifrost_vtoken_minting::TokenUnlockLedger::::get(KSM, 4), None); + + assert_eq!(bifrost_vtoken_minting::UserUnlockLedger::::get(EDDIE, KSM,), None); + + // check account balances + assert_eq!(Tokens::free_balance(KSM, &exit_acc), 17); + assert_eq!(Tokens::free_balance(KSM, &entrance_acc), 0); + assert_eq!(Tokens::free_balance(KSM, &BOB), 0); + assert_eq!(Tokens::free_balance(KSM, &CHARLIE), 28); + assert_eq!(Tokens::free_balance(KSM, &DAVE), 22); + assert_eq!(Tokens::free_balance(KSM, &EDDIE), 13); + assert_ok!(Slp::refund_currency_due_unbond(Origin::signed(ALICE), KSM)); + + // check account balances + assert_eq!(Tokens::free_balance(KSM, &exit_acc), 0); + assert_eq!(Tokens::free_balance(KSM, &entrance_acc), 17); + }); +} + +#[test] +fn charge_host_fee_and_tune_vtoken_exchange_rate_works() { + // 5E78xTBiaN3nAGYtcNnqTJQJqYAkSDGggKqaDfpNsKyPpbcb + let subaccount_0: AccountId = + hex_literal::hex!["5a53736d8e96f1c007cf0d630acf5209b20611617af23ce924c8e25328eb5d28"] + .into(); + let subaccount_0_32: [u8; 32] = Slp::account_id_to_account_32(subaccount_0.clone()).unwrap(); + let subaccount_0_location: MultiLocation = + Slp::account_32_to_parent_location(subaccount_0_32).unwrap(); + + ExtBuilder::default().build().execute_with(|| { + let treasury_id: AccountId = + hex_literal::hex!["6d6f646c62662f74727372790000000000000000000000000000000000000000"] + .into(); + let treasury_32: [u8; 32] = + hex_literal::hex!["6d6f646c62662f74727372790000000000000000000000000000000000000000"] + .into(); + + bifrost_vtoken_minting::OngoingTimeUnit::::insert(KSM, TimeUnit::Era(1)); + + DelegatorsIndex2Multilocation::::insert(KSM, 0, subaccount_0_location.clone()); + DelegatorsMultilocation2Index::::insert(KSM, subaccount_0_location.clone(), 0); + + let mins_and_maxs = MinimumsMaximums { + delegator_bonded_minimum: 100_000_000_000, + bond_extra_minimum: 0, + unbond_minimum: 0, + rebond_minimum: 0, + unbond_record_maximum: 32, + validators_back_maximum: 36, + delegator_active_staking_maximum: 200_000_000_000_000, + }; + + // Set minimums and maximums + MinimumsAndMaximums::::insert(KSM, mins_and_maxs); + + let sb_ledger = SubstrateLedger { + account: subaccount_0_location.clone(), + total: 0, + active: 0, + unlocking: vec![], + }; + let ledger = Ledger::Substrate(sb_ledger); + + // Set delegator ledger + DelegatorLedgers::::insert(KSM, subaccount_0_location.clone(), ledger); + + // Set the hosting fee to be 20%, and the beneficiary to be bifrost treasury account. + let pct = Permill::from_percent(20); + let treasury_location = MultiLocation { + parents: 0, + interior: X1(AccountId32 { network: Any, id: treasury_32 }), + }; + + assert_ok!(Slp::set_hosting_fees( + Origin::signed(ALICE), + KSM, + Some((pct, treasury_location)) + )); + + let pct_100 = Permill::from_percent(100); + assert_ok!(Slp::set_currency_tune_exchange_rate_limit( + Origin::signed(ALICE), + KSM, + Some((1, pct_100)) + )); + + // First set base vtoken exchange rate. Should be 1:1. + assert_ok!(Currencies::deposit(VKSM, &ALICE, 100)); + assert_ok!(Slp::increase_token_pool(Origin::signed(ALICE), KSM, 100)); + + // call the charge_host_fee_and_tune_vtoken_exchange_rate + assert_ok!(Slp::charge_host_fee_and_tune_vtoken_exchange_rate( + Origin::signed(ALICE), + KSM, + 100, + subaccount_0_location.clone() + )); + + // Tokenpool should have been added 100. + let new_token_pool_amount = ::VtokenMinting::get_token_pool(KSM); + assert_eq!(new_token_pool_amount, 200); + + let tune_record = DelegatorLatestTuneRecord::::get(KSM, &subaccount_0_location); + assert_eq!(tune_record, Some((TimeUnit::Era(1), 1))); + + // Treasury account has been issued a fee of 20 vksm which equals to the value of 20 ksm + // before new exchange rate tuned. + let treasury_vksm = Currencies::free_balance(VKSM, &treasury_id); + assert_eq!(treasury_vksm, 20); + }); +} + +#[test] +fn set_hosting_fees_works() { + ExtBuilder::default().build().execute_with(|| { + let treasury_32: [u8; 32] = + hex_literal::hex!["6d6f646c62662f74727372790000000000000000000000000000000000000000"] + .into(); + + // Set the hosting fee to be 20%, and the beneficiary to be bifrost treasury account. + let pct = Permill::from_percent(20); + let treasury_location = MultiLocation { + parents: 0, + interior: X1(AccountId32 { network: Any, id: treasury_32 }), + }; + + assert_ok!(Slp::set_hosting_fees( + Origin::signed(ALICE), + KSM, + Some((pct, treasury_location.clone())) + )); + + let (fee, location) = Slp::get_hosting_fee(KSM).unwrap(); + assert_eq!(fee, pct); + assert_eq!(location, treasury_location); + }); +} diff --git a/pallets/slp/src/traits.rs b/pallets/slp/src/traits.rs new file mode 100644 index 0000000000..d965ea99d1 --- /dev/null +++ b/pallets/slp/src/traits.rs @@ -0,0 +1,190 @@ +// This file is part of Bifrost. + +// Copyright (C) 2019-2022 Liebi Technologies (UK) Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +// This program 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. + +// This program 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 this program. If not, see . + +use sp_runtime::DispatchResult; +use sp_std::vec::Vec; + +use crate::{QueryId, Weight, Xcm}; + +/// Abstraction over a staking agent for a certain POS chain. +pub trait StakingAgent< + DelegatorId, + ValidatorId, + Balance, + TimeUnit, + AccountId, + GenericAccountId, + QueryId, + LedgerUpdateEntry, + ValidatorsByDelegatorUpdateEntry, + Error, +> +{ + /// Delegator initialization work. Generate a new delegator and return its ID. + fn initialize_delegator(&self) -> Result; + + /// First time bonding some amount to a delegator. + fn bond(&self, who: &DelegatorId, amount: Balance) -> Result; + + /// Bond extra amount to a delegator. + fn bond_extra(&self, who: &DelegatorId, amount: Balance) -> Result; + + /// Decrease the bonding amount of a delegator. + fn unbond(&self, who: &DelegatorId, amount: Balance) -> Result; + + /// Unbonding all amount of a delegator. Differentiate from regular unbonding. + fn unbond_all(&self, who: &DelegatorId) -> Result; + + /// Cancel some unbonding amount. + fn rebond(&self, who: &DelegatorId, amount: Balance) -> Result; + + /// Delegate to some validators. + fn delegate(&self, who: &DelegatorId, targets: &Vec) -> Result; + + /// Remove delegation relationship with some validators. + fn undelegate(&self, who: &DelegatorId, targets: &Vec) -> Result; + + /// Re-delegate existing delegation to a new validator set. + fn redelegate(&self, who: &DelegatorId, targets: &Vec) -> Result; + + /// Initiate payout for a certain delegator. + fn payout( + &self, + who: &DelegatorId, + validator: &ValidatorId, + when: &Option, + ) -> Result<(), Error>; + + /// Withdraw the due payout into free balance. + fn liquidize(&self, who: &DelegatorId, when: &Option) -> Result; + + /// Cancel the identity of delegator. + fn chill(&self, who: &DelegatorId) -> Result; + + /// Make token transferred back to Bifrost chain account. + fn transfer_back( + &self, + from: &DelegatorId, + to: &DelegatorId, + amount: Balance, + ) -> Result<(), Error>; + + /// Make token from Bifrost chain account to the staking chain account. + fn transfer_to( + &self, + from: &DelegatorId, + to: &DelegatorId, + amount: Balance, + ) -> Result<(), Error>; + + /// Tune the vtoken exchage rate. + fn tune_vtoken_exchange_rate( + &self, + who: &DelegatorId, + token_amount: Balance, + vtoken_amount: Balance, + ) -> Result<(), Error>; + + /// ************************************ + /// Abstraction over a fee manager for charging fee from the origin chain(Bifrost) + /// or deposit fee reserves for the destination chain nominator accounts. + /// ************************************ + /// Charge hosting fee. + fn charge_hosting_fee( + &self, + amount: Balance, + from: &GenericAccountId, + to: &GenericAccountId, + ) -> DispatchResult; + + /// Deposit some amount as fee to nominator accounts. + fn supplement_fee_reserve( + &self, + amount: Balance, + from: &GenericAccountId, + to: &GenericAccountId, + ) -> DispatchResult; + + /// ************************************ + /// Add a new serving delegator for a particular currency. + /// ************************************ + fn add_delegator(&self, index: u16, who: &DelegatorId) -> DispatchResult; + + /// Remove an existing serving delegator for a particular currency. + fn remove_delegator(&self, who: &DelegatorId) -> DispatchResult; + + /// ************************************ + /// Abstraction over a validator manager. + /// ************************************ + + /// Add a new serving validator for a particular currency. + fn add_validator(&self, who: &ValidatorId) -> DispatchResult; + + /// Remove an existing serving validator for a particular currency. + fn remove_validator(&self, who: &ValidatorId) -> DispatchResult; + + /// ************************************ + /// Abstraction over a QueryResponseChecker. + /// ************************************ + + fn check_delegator_ledger_query_response( + &self, + query_id: QueryId, + query_entry: LedgerUpdateEntry, + manual_mode: bool, + ) -> Result; + + fn check_validators_by_delegator_query_response( + &self, + query_id: QueryId, + query_entry: ValidatorsByDelegatorUpdateEntry, + manual_mode: bool, + ) -> Result; + + fn fail_delegator_ledger_query_response(&self, query_id: QueryId) -> Result<(), Error>; + + fn fail_validators_by_delegator_query_response(&self, query_id: QueryId) -> Result<(), Error>; +} + +/// Helper to build xcm message +//【For xcm v3】 +// pub trait XcmBuilder { +pub trait XcmBuilder { + fn construct_xcm_message_with_query_id( + call: ChainCallType, + extra_fee: Balance, + weight: Weight, + query_id: QueryId, + // response_back_location: AccountId + ) -> Xcm<()>; + + fn construct_xcm_message_without_query_id( + call: ChainCallType, + extra_fee: Balance, + weight: Weight, + ) -> Xcm<()>; +} + +/// Helper to communicate with pallet_xcm's Queries storage for Substrate chains in runtime. +pub trait QueryResponseManager { + // If the query exists and we've already got the Response, then True is returned. Otherwise, + // False is returned. + fn get_query_response_record(query_id: QueryId) -> bool; + fn create_query_record(responder: &AccountId, timeout: BlockNumber) -> u64; + fn remove_query_record(query_id: QueryId) -> bool; +} diff --git a/pallets/slp/src/weights.rs b/pallets/slp/src/weights.rs new file mode 100644 index 0000000000..4aba62c85b --- /dev/null +++ b/pallets/slp/src/weights.rs @@ -0,0 +1,228 @@ +// This file is part of Bifrost. + +// Copyright (C) 2019-2022 Liebi Technologies (UK) Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +// This program 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. + +// This program 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 this program. If not, see . + +#![allow(unused_parens)] +#![allow(unused_imports)] + +use frame_support::{ + traits::Get, + weights::{constants::RocksDbWeight, Weight}, +}; +use sp_std::marker::PhantomData; + +/// Weight functions needed for the pallet. +pub trait WeightInfo { + // Outer calls + fn initialize_delegator() -> Weight; + fn bond() -> Weight; + fn bond_extra() -> Weight; + fn unbond() -> Weight; + fn unbond_all() -> Weight; + fn rebond() -> Weight; + fn delegate() -> Weight; + fn redelegate() -> Weight; + fn undelegate() -> Weight; + fn payout() -> Weight; + fn liquidize() -> Weight; + fn chill() -> Weight; + fn transfer_back() -> Weight; + fn transfer_to() -> Weight; + fn increase_token_pool() -> Weight; + fn decrease_token_pool() -> Weight; + fn update_ongoing_time_unit() -> Weight; + fn refund_currency_due_unbond() -> Weight; + fn supplement_fee_reserve() -> Weight; + fn charge_host_fee_and_tune_vtoken_exchange_rate() -> Weight; + fn confirm_delegator_ledger_query_response() -> Weight; + fn fail_delegator_ledger_query_response() -> Weight; + fn confirm_validators_by_delegator_query_response() -> Weight; + fn fail_validators_by_delegator_query_response() -> Weight; + + // Storage setters + fn set_xcm_dest_weight_and_fee() -> Weight; + fn set_operate_origin() -> Weight; + fn set_current_time_unit() -> Weight; + fn set_fee_source() -> Weight; + fn add_delegator() -> Weight; + fn remove_delegator() -> Weight; + fn add_validator() -> Weight; + fn remove_validator() -> Weight; + fn set_validators_by_delegator() -> Weight; + fn set_delegator_ledger() -> Weight; + fn set_minimums_and_maximums() -> Weight; + fn set_currency_delays() -> Weight; + fn set_hosting_fees() -> Weight; + fn set_currency_tune_exchange_rate_limit() -> Weight; +} + +// For backwards compatibility and tests +impl WeightInfo for () { + // Outer calls + fn initialize_delegator() -> Weight { + (50_000_000 as Weight) + } + + fn bond() -> Weight { + (50_000_000 as Weight) + } + + fn bond_extra() -> Weight { + (50_000_000 as Weight) + } + + fn unbond() -> Weight { + (50_000_000 as Weight) + } + + fn unbond_all() -> Weight { + (50_000_000 as Weight) + } + + fn rebond() -> Weight { + (50_000_000 as Weight) + } + + fn delegate() -> Weight { + (50_000_000 as Weight) + } + + fn redelegate() -> Weight { + (50_000_000 as Weight) + } + + fn undelegate() -> Weight { + (50_000_000 as Weight) + } + + fn payout() -> Weight { + (50_000_000 as Weight) + } + + fn liquidize() -> Weight { + (50_000_000 as Weight) + } + + fn chill() -> Weight { + (50_000_000 as Weight) + } + + fn transfer_back() -> Weight { + (50_000_000 as Weight) + } + + fn transfer_to() -> Weight { + (50_000_000 as Weight) + } + + fn increase_token_pool() -> Weight { + (50_000_000 as Weight) + } + + fn decrease_token_pool() -> Weight { + (50_000_000 as Weight) + } + + fn update_ongoing_time_unit() -> Weight { + (50_000_000 as Weight) + } + + fn refund_currency_due_unbond() -> Weight { + (50_000_000 as Weight) + } + + fn supplement_fee_reserve() -> Weight { + (50_000_000 as Weight) + } + + fn charge_host_fee_and_tune_vtoken_exchange_rate() -> Weight { + (50_000_000 as Weight) + } + + // Storage setters + fn set_xcm_dest_weight_and_fee() -> Weight { + (50_000_000 as Weight) + } + + fn set_operate_origin() -> Weight { + (50_000_000 as Weight) + } + + fn set_current_time_unit() -> Weight { + (50_000_000 as Weight) + } + + fn set_fee_source() -> Weight { + (50_000_000 as Weight) + } + + fn add_delegator() -> Weight { + (50_000_000 as Weight) + } + + fn remove_delegator() -> Weight { + (50_000_000 as Weight) + } + + fn add_validator() -> Weight { + (50_000_000 as Weight) + } + + fn remove_validator() -> Weight { + (50_000_000 as Weight) + } + + fn set_validators_by_delegator() -> Weight { + (50_000_000 as Weight) + } + + fn set_delegator_ledger() -> Weight { + (50_000_000 as Weight) + } + + fn set_minimums_and_maximums() -> Weight { + (50_000_000 as Weight) + } + + fn set_currency_delays() -> Weight { + (50_000_000 as Weight) + } + + fn set_hosting_fees() -> Weight { + (50_000_000 as Weight) + } + + fn set_currency_tune_exchange_rate_limit() -> Weight { + (50_000_000 as Weight) + } + + fn confirm_delegator_ledger_query_response() -> Weight { + (50_000_000 as Weight) + } + + fn fail_delegator_ledger_query_response() -> Weight { + (50_000_000 as Weight) + } + + fn confirm_validators_by_delegator_query_response() -> Weight { + (50_000_000 as Weight) + } + + fn fail_validators_by_delegator_query_response() -> Weight { + (50_000_000 as Weight) + } +} diff --git a/pallets/vstoken-conversion/src/lib.rs b/pallets/vstoken-conversion/src/lib.rs index 4a759103df..cb76f79b53 100644 --- a/pallets/vstoken-conversion/src/lib.rs +++ b/pallets/vstoken-conversion/src/lib.rs @@ -86,21 +86,25 @@ pub mod pallet { #[pallet::generate_deposit(pub(super) fn deposit_event)] pub enum Event { VsbondConvertToVsksm { + address: AccountIdOf, currency_id: CurrencyIdOf, vsbond_amount: BalanceOf, vsksm_amount: BalanceOf, }, VsksmConvertToVsbond { + address: AccountIdOf, currency_id: CurrencyIdOf, vsbond_amount: BalanceOf, vsksm_amount: BalanceOf, }, VsbondConvertToVsdot { + address: AccountIdOf, currency_id: CurrencyIdOf, vsbond_amount: BalanceOf, vsdot_amount: BalanceOf, }, VsdotConvertToVsbond { + address: AccountIdOf, currency_id: CurrencyIdOf, vsbond_amount: BalanceOf, vsdot_amount: BalanceOf, @@ -216,6 +220,7 @@ pub mod pallet { )?; Self::deposit_event(Event::VsbondConvertToVsksm { + address: exchanger, currency_id, vsbond_amount, vsksm_amount: vsksm_balance, @@ -297,6 +302,7 @@ pub mod pallet { )?; Self::deposit_event(Event::VsksmConvertToVsbond { + address: exchanger, currency_id, vsbond_amount: vsbond_balance, vsksm_amount: vsksm_balance, @@ -369,6 +375,7 @@ pub mod pallet { )?; Self::deposit_event(Event::VsbondConvertToVsdot { + address: exchanger, currency_id, vsbond_amount, vsdot_amount: vsdot_balance, @@ -447,6 +454,7 @@ pub mod pallet { )?; Self::deposit_event(Event::VsdotConvertToVsbond { + address: exchanger, currency_id, vsbond_amount: vsbond_balance, vsdot_amount: vsdot_balance, diff --git a/pallets/vtoken-minting/Cargo.toml b/pallets/vtoken-minting/Cargo.toml new file mode 100644 index 0000000000..77434d983e --- /dev/null +++ b/pallets/vtoken-minting/Cargo.toml @@ -0,0 +1,47 @@ +[package] +name = "bifrost-vtoken-minting" +version = "0.8.0" +authors = ["Kadokura "] +edition = "2021" + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] + +[dependencies] +codec = { package = "parity-scale-codec", version = "3.0.0", default-features = false, features = [ + "derive", +] } +scale-info = { version = "2.0.1", default-features = false, features = [ + "derive", +] } +log = { version = "0.4.14", default-features = false } +frame-support = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.18", default-features = false } +frame-system = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.18", default-features = false } +frame-benchmarking = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.18", default-features = false, optional = true } +node-primitives = { path = "../../node/primitives", default-features = false } +orml-traits = { version = "0.4.1-dev", default-features = false } +sp-std = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.18", default-features = false } +hex-literal = { version = "0.3.1" } +orml-tokens = { version = "0.4.1-dev", default-features = false } +orml-currencies = { version = "0.4.1-dev", default-features = false } +pallet-balances = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.18", default-features = false } + +[dev-dependencies] +sp-io = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.18" } +sp-core = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.18" } +sp-runtime = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.18" } + +[features] +default = ["std"] +std = [ + "codec/std", + "scale-info/std", + "frame-support/std", + "frame-system/std", + "frame-benchmarking/std", + "node-primitives/std", + "orml-traits/std", +] + +runtime-benchmarks = ["frame-benchmarking/runtime-benchmarks"] +try-runtime = ["frame-support/try-runtime"] diff --git a/pallets/vtoken-minting/src/benchmarking.rs b/pallets/vtoken-minting/src/benchmarking.rs new file mode 100644 index 0000000000..efc55041f0 --- /dev/null +++ b/pallets/vtoken-minting/src/benchmarking.rs @@ -0,0 +1,19 @@ +// This file is part of Bifrost. + +// Copyright (C) 2019-2022 Liebi Technologies (UK) Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +// This program 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. + +// This program 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 this program. If not, see . + +// Ensure we're `no_std` when compiling for Wasm. diff --git a/pallets/vtoken-minting/src/lib.rs b/pallets/vtoken-minting/src/lib.rs new file mode 100644 index 0000000000..8a14e211d8 --- /dev/null +++ b/pallets/vtoken-minting/src/lib.rs @@ -0,0 +1,1240 @@ +// This file is part of Bifrost. + +// Copyright (C) 2019-2022 Liebi Technologies (UK) Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +// This program 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. + +// This program 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 this program. If not, see . + +// Ensure we're `no_std` when compiling for Wasm. +#![cfg_attr(not(feature = "std"), no_std)] + +#[cfg(test)] +mod mock; + +#[cfg(test)] +mod tests; + +#[cfg(feature = "runtime-benchmarks")] +mod benchmarking; + +pub mod weights; + +use frame_support::{ + pallet_prelude::*, + sp_runtime::{ + traits::{ + AccountIdConversion, CheckedAdd, CheckedDiv, CheckedMul, CheckedSub, Saturating, Zero, + }, + DispatchError, Permill, + }, + transactional, BoundedVec, PalletId, +}; +use frame_system::pallet_prelude::*; +use node_primitives::{CurrencyId, TimeUnit, TokenSymbol, VtokenMintingOperator}; +use orml_traits::MultiCurrency; +pub use pallet::*; +use sp_std::vec::Vec; +pub use weights::WeightInfo; + +#[allow(type_alias_bounds)] +pub type AccountIdOf = ::AccountId; + +#[allow(type_alias_bounds)] +pub type CurrencyIdOf = <::MultiCurrency as MultiCurrency< + ::AccountId, +>>::CurrencyId; + +#[allow(type_alias_bounds)] +type BalanceOf = + <::MultiCurrency as MultiCurrency>>::Balance; + +pub type UnlockId = u32; + +#[frame_support::pallet] +pub mod pallet { + use super::*; + + #[pallet::pallet] + #[pallet::generate_store(pub(super) trait Store)] + pub struct Pallet(_); + + #[pallet::config] + pub trait Config: frame_system::Config { + type Event: From> + IsType<::Event>; + + type MultiCurrency: MultiCurrency, CurrencyId = CurrencyId>; + // + MultiReservableCurrency, CurrencyId = CurrencyId>; + + /// The only origin that can edit token issuer list + type ControlOrigin: EnsureOrigin; + + /// The amount of mint + #[pallet::constant] + type MaximumUnlockIdOfUser: Get; + + #[pallet::constant] + type MaximumUnlockIdOfTimeUnit: Get; + + #[pallet::constant] + type EntranceAccount: Get; + + #[pallet::constant] + type ExitAccount: Get; + + #[pallet::constant] + type FeeAccount: Get; + + /// Set default weight. + type WeightInfo: WeightInfo; + } + + #[pallet::event] + #[pallet::generate_deposit(pub(super) fn deposit_event)] + pub enum Event { + Minted { + address: AccountIdOf, + token_id: CurrencyIdOf, + token_amount: BalanceOf, + vtoken_amount: BalanceOf, + fee: BalanceOf, + }, + Redeemed { + address: AccountIdOf, + token_id: CurrencyIdOf, + token_amount: BalanceOf, + vtoken_amount: BalanceOf, + fee: BalanceOf, + }, + RedeemSuccess { + unlock_id: UnlockId, + token_id: CurrencyIdOf, + to: AccountIdOf, + token_amount: BalanceOf, + }, + Rebonded { + address: AccountIdOf, + token_id: CurrencyIdOf, + token_amount: BalanceOf, + vtoken_amount: BalanceOf, + fee: BalanceOf, + }, + RebondedByUnlockId { + address: AccountIdOf, + token_id: CurrencyIdOf, + token_amount: BalanceOf, + vtoken_amount: BalanceOf, + fee: BalanceOf, + }, + UnlockDurationSet { + token_id: CurrencyIdOf, + unlock_duration: TimeUnit, + }, + MinimumMintSet { + token_id: CurrencyIdOf, + amount: BalanceOf, + }, + MinimumRedeemSet { + token_id: CurrencyIdOf, + amount: BalanceOf, + }, + SupportRebondTokenAdded { + token_id: CurrencyIdOf, + }, + SupportRebondTokenRemoved { + token_id: CurrencyIdOf, + }, + /// Several fees has been set. + FeeSet { + mint_fee: Permill, + redeem_fee: Permill, + // hosting_fee: BalanceOf, + }, + HookIterationLimitSet { + limit: u32, + }, + } + + #[pallet::error] + pub enum Error { + BelowMinimumMint, + BelowMinimumRedeem, + /// Invalid token to rebond. + InvalidRebondToken, + /// Token type not support. + NotSupportTokenType, + NotEnoughBalanceToUnlock, + TokenToRebondNotZero, + OngoingTimeUnitNotSet, + TokenUnlockLedgerNotFound, + UserUnlockLedgerNotFound, + TimeUnitUnlockLedgerNotFound, + UnlockDurationNotFound, + Unexpected, + CalculationOverflow, + ExceedMaximumUnlockId, + TooManyRedeems, + } + + #[pallet::storage] + #[pallet::getter(fn fees)] + pub type Fees = StorageValue<_, (Permill, Permill), ValueQuery>; + + #[pallet::storage] + #[pallet::getter(fn token_pool)] + pub type TokenPool = + StorageMap<_, Twox64Concat, CurrencyIdOf, BalanceOf, ValueQuery>; + + #[pallet::storage] + #[pallet::getter(fn unlock_duration)] + pub type UnlockDuration = StorageMap<_, Twox64Concat, CurrencyIdOf, TimeUnit>; + + #[pallet::storage] + #[pallet::getter(fn ongoing_time_unit)] + pub type OngoingTimeUnit = StorageMap<_, Twox64Concat, CurrencyIdOf, TimeUnit>; + + #[pallet::storage] + #[pallet::getter(fn minimum_mint)] + pub type MinimumMint = + StorageMap<_, Twox64Concat, CurrencyIdOf, BalanceOf, ValueQuery>; + + #[pallet::storage] + #[pallet::getter(fn minimum_redeem)] + pub type MinimumRedeem = + StorageMap<_, Twox64Concat, CurrencyIdOf, BalanceOf, ValueQuery>; + + #[pallet::storage] + #[pallet::getter(fn token_unlock_next_id)] + pub type TokenUnlockNextId = + StorageMap<_, Twox64Concat, CurrencyIdOf, u32, ValueQuery>; + + #[pallet::storage] + #[pallet::getter(fn token_unlock_ledger)] + pub type TokenUnlockLedger = StorageDoubleMap< + _, + Blake2_128Concat, + CurrencyIdOf, + Blake2_128Concat, + UnlockId, + (T::AccountId, BalanceOf, TimeUnit), + OptionQuery, + >; + + #[pallet::storage] + #[pallet::getter(fn user_unlock_ledger)] + pub type UserUnlockLedger = StorageDoubleMap< + _, + Blake2_128Concat, + AccountIdOf, + Blake2_128Concat, + CurrencyIdOf, + (BalanceOf, BoundedVec), + OptionQuery, + >; + + #[pallet::storage] + #[pallet::getter(fn time_unit_unlock_ledger)] + pub type TimeUnitUnlockLedger = StorageDoubleMap< + _, + Blake2_128Concat, + TimeUnit, + Blake2_128Concat, + CurrencyIdOf, + (BalanceOf, BoundedVec, CurrencyIdOf), + OptionQuery, + >; + + #[pallet::storage] + #[pallet::getter(fn token_to_rebond)] + pub type TokenToRebond = StorageMap<_, Twox64Concat, CurrencyIdOf, BalanceOf>; + + #[pallet::storage] + #[pallet::getter(fn min_time_unit)] + pub type MinTimeUnit = + StorageMap<_, Twox64Concat, CurrencyIdOf, TimeUnit, ValueQuery>; + + #[pallet::storage] + #[pallet::getter(fn currency_unlocking_total)] + pub type CurrencyUnlockingTotal = StorageValue<_, BalanceOf, ValueQuery>; + + #[pallet::storage] + #[pallet::getter(fn hook_iteration_limit)] + pub type HookIterationLimit = StorageValue<_, u32, ValueQuery>; + + #[pallet::hooks] + impl Hooks for Pallet { + fn on_initialize(_n: T::BlockNumber) -> Weight { + Self::handle_on_initialize().map_err(|e| { + log::error!( + target: "runtime::vtoken-minting", + "Received invalid justification for {:?}", + e, + ); + e + }); + + T::WeightInfo::on_initialize() + } + } + + #[pallet::call] + impl Pallet { + // #[pallet::weight(T::WeightInfo::mint())] + #[transactional] + #[pallet::weight(10000)] + pub fn mint( + origin: OriginFor, + token_id: CurrencyIdOf, + token_amount: BalanceOf, + ) -> DispatchResult { + // Check origin + let exchanger = ensure_signed(origin)?; + ensure!(token_amount >= MinimumMint::::get(token_id), Error::::BelowMinimumMint); + + let vtoken_id = token_id.to_vtoken().map_err(|_| Error::::NotSupportTokenType)?; + let (token_amount_excluding_fee, vtoken_amount, fee) = + Self::mint_without_tranfer(&exchanger, vtoken_id, token_id, token_amount) + .map_err(|e| e)?; + // Transfer the user's token to EntranceAccount. + T::MultiCurrency::transfer( + token_id, + &exchanger, + &T::EntranceAccount::get().into_account(), + token_amount_excluding_fee, + )?; + + Self::deposit_event(Event::Minted { + address: exchanger, + token_id, + token_amount, + vtoken_amount, + fee, + }); + Ok(()) + } + + #[transactional] + #[pallet::weight(10000)] + pub fn redeem( + origin: OriginFor, + vtoken_id: CurrencyIdOf, + mut vtoken_amount: BalanceOf, + ) -> DispatchResult { + let exchanger = ensure_signed(origin)?; + let token_id = vtoken_id.to_token().map_err(|_| Error::::NotSupportTokenType)?; + ensure!( + vtoken_amount >= MinimumRedeem::::get(vtoken_id), + Error::::BelowMinimumRedeem + ); + let (_mint_rate, redeem_rate) = Fees::::get(); + let redeem_fee = redeem_rate * vtoken_amount; + vtoken_amount = + vtoken_amount.checked_sub(&redeem_fee).ok_or(Error::::CalculationOverflow)?; + // Charging fees + T::MultiCurrency::transfer(vtoken_id, &exchanger, &T::FeeAccount::get(), redeem_fee)?; + + let token_pool_amount = Self::token_pool(token_id); + let vtoken_total_issuance = T::MultiCurrency::total_issuance(vtoken_id); + let token_amount = vtoken_amount + .checked_mul(&token_pool_amount.into()) + .ok_or(Error::::CalculationOverflow)? + .checked_div(&vtoken_total_issuance.into()) + .ok_or(Error::::CalculationOverflow)?; + + match OngoingTimeUnit::::get(token_id) { + Some(time_unit) => { + let result_time_unit = Self::add_time_unit( + Self::unlock_duration(token_id) + .ok_or(Error::::UnlockDurationNotFound)?, + time_unit.clone(), + ) + .map_err(|e| e)?; + + T::MultiCurrency::withdraw(vtoken_id, &exchanger, vtoken_amount)?; + TokenPool::::mutate(&token_id, |pool| -> Result<(), Error> { + *pool = pool + .checked_sub(&token_amount) + .ok_or(Error::::CalculationOverflow)?; + Ok(()) + })?; + CurrencyUnlockingTotal::::mutate(|pool| -> Result<(), Error> { + *pool = pool + .checked_add(&token_amount) + .ok_or(Error::::CalculationOverflow)?; + Ok(()) + })?; + let next_id = Self::token_unlock_next_id(token_id); + TokenUnlockLedger::::insert( + &token_id, + &next_id, + (&exchanger, token_amount, &result_time_unit), + ); + + if let Some(_) = UserUnlockLedger::::get(&exchanger, &token_id) { + UserUnlockLedger::::mutate( + &exchanger, + &token_id, + |value| -> Result<(), Error> { + if let Some((total_locked, ledger_list)) = value { + ledger_list + .try_push(next_id) + .map_err(|_| Error::::TooManyRedeems)?; + + *total_locked = total_locked + .checked_add(&token_amount) + .ok_or(Error::::CalculationOverflow)?; + }; + return Ok(()); + }, + )?; + } else { + let mut ledger_list_origin = + BoundedVec::::default(); + ledger_list_origin + .try_push(next_id) + .map_err(|_| Error::::TooManyRedeems)?; + UserUnlockLedger::::insert( + &exchanger, + &token_id, + (token_amount, ledger_list_origin), + ); + } + + if let Some((_, _, _token_id)) = + TimeUnitUnlockLedger::::get(&result_time_unit, &token_id) + { + TimeUnitUnlockLedger::::mutate( + &result_time_unit, + &token_id, + |value| -> Result<(), Error> { + if let Some((total_locked, ledger_list, _token_id)) = value { + ledger_list + .try_push(next_id) + .map_err(|_| Error::::TooManyRedeems)?; + *total_locked = total_locked + .checked_add(&token_amount) + .ok_or(Error::::CalculationOverflow)?; + }; + Ok(()) + }, + )?; + } else { + let mut ledger_list_origin = + BoundedVec::::default(); + ledger_list_origin + .try_push(next_id) + .map_err(|_| Error::::TooManyRedeems)?; + + TimeUnitUnlockLedger::::insert( + &result_time_unit, + &token_id, + (token_amount, ledger_list_origin, token_id), + ); + } + }, + None => return Err(Error::::OngoingTimeUnitNotSet.into()), + } + + TokenUnlockNextId::::mutate(&token_id, |unlock_id| -> Result<(), Error> { + *unlock_id = unlock_id.checked_add(1).ok_or(Error::::CalculationOverflow)?; + Ok(()) + })?; + + Self::deposit_event(Event::Redeemed { + address: exchanger, + token_id, + vtoken_amount, + token_amount, + fee: redeem_fee, + }); + Ok(()) + } + + #[transactional] + #[pallet::weight(10000)] + pub fn rebond( + origin: OriginFor, + token_id: CurrencyIdOf, + token_amount: BalanceOf, + ) -> DispatchResult { + let exchanger = ensure_signed(origin)?; + + let vtoken_id = token_id.to_vtoken().map_err(|_| Error::::NotSupportTokenType)?; + let _token_amount_to_rebond = + Self::token_to_rebond(token_id).ok_or(Error::::InvalidRebondToken)?; + if let Some((user_unlock_amount, mut ledger_list)) = + Self::user_unlock_ledger(&exchanger, token_id) + { + ensure!(user_unlock_amount >= token_amount, Error::::NotEnoughBalanceToUnlock); + let mut tmp_amount = token_amount; + let ledger_list_rev: Vec = ledger_list.into_iter().rev().collect(); + ledger_list = + BoundedVec::::try_from(ledger_list_rev) + .map_err(|_| Error::::ExceedMaximumUnlockId)?; + ledger_list.retain(|index| { + if let Some((_, unlock_amount, time_unit)) = + Self::token_unlock_ledger(token_id, index) + { + if tmp_amount >= unlock_amount { + if let Some((_, _, time_unit)) = + TokenUnlockLedger::::take(&token_id, &index) + { + TimeUnitUnlockLedger::::mutate_exists( + &time_unit, + &token_id, + |value| -> Result<(), Error> { + if let Some((total_locked_origin, ledger_list_origin, _)) = + value + { + if total_locked_origin == &unlock_amount { + *value = None; + return Ok(()); + } + *total_locked_origin = total_locked_origin + .checked_sub(&unlock_amount) + .ok_or(Error::::CalculationOverflow)?; + ledger_list_origin.retain(|x| x != index); + } else { + return Err( + Error::::TimeUnitUnlockLedgerNotFound.into() + ); + } + return Ok(()); + }, + ); + tmp_amount = tmp_amount.saturating_sub(unlock_amount); + // } else { + // return Err(Error::::TokenUnlockLedgerNotFound.into()); + } + return false; + } else { + TokenUnlockLedger::::mutate_exists( + &token_id, + &index, + |value| -> Result<(), Error> { + if let Some((_, total_locked_origin, _)) = value { + if total_locked_origin == &tmp_amount { + *value = None; + return Ok(()); + } + *total_locked_origin = total_locked_origin + .checked_sub(&tmp_amount) + .ok_or(Error::::CalculationOverflow)?; + } else { + return Err(Error::::TokenUnlockLedgerNotFound.into()); + } + return Ok(()); + }, + ); + TimeUnitUnlockLedger::::mutate_exists( + &time_unit, + &token_id, + |value| -> Result<(), Error> { + if let Some((total_locked_origin, _, _)) = value { + if total_locked_origin == &tmp_amount { + *value = None; + return Ok(()); + } + *total_locked_origin = total_locked_origin + .checked_sub(&tmp_amount) + .ok_or(Error::::CalculationOverflow)?; + } else { + return Err(Error::::TimeUnitUnlockLedgerNotFound.into()); + } + return Ok(()); + }, + ); + return true; + } + } else { + return true; + } + }); + let ledger_list_tmp: Vec = ledger_list.into_iter().rev().collect(); + + ledger_list = + BoundedVec::::try_from(ledger_list_tmp) + .map_err(|_| Error::::ExceedMaximumUnlockId)?; + + CurrencyUnlockingTotal::::mutate(|pool| -> Result<(), Error> { + *pool = + pool.checked_sub(&token_amount).ok_or(Error::::CalculationOverflow)?; + Ok(()) + })?; + UserUnlockLedger::::mutate_exists( + &exchanger, + &token_id, + |value| -> Result<(), Error> { + if let Some((total_locked_origin, ledger_list_origin)) = value { + if total_locked_origin == &token_amount { + *value = None; + return Ok(()); + } + *ledger_list_origin = ledger_list; + *total_locked_origin = total_locked_origin + .checked_sub(&token_amount) + .ok_or(Error::::CalculationOverflow)?; + } else { + return Err(Error::::UserUnlockLedgerNotFound.into()); + } + return Ok(()); + }, + )?; + } else { + return Err(Error::::UserUnlockLedgerNotFound.into()); + } + + let (_, vtoken_amount, fee) = + Self::mint_without_tranfer(&exchanger, vtoken_id, token_id, token_amount) + .map_err(|e| e)?; + + TokenToRebond::::mutate(&token_id, |value| -> Result<(), Error> { + if let Some(value_info) = value { + *value_info = value_info + .checked_add(&token_amount) + .ok_or(Error::::CalculationOverflow)?; + } else { + return Err(Error::::InvalidRebondToken.into()); + } + Ok(()) + })?; + + Self::deposit_event(Event::Rebonded { + address: exchanger, + token_id, + token_amount, + vtoken_amount, + fee, + }); + Ok(()) + } + + #[transactional] + #[pallet::weight(10000)] + pub fn rebond_by_unlock_id( + origin: OriginFor, + token_id: CurrencyIdOf, + unlock_id: UnlockId, + ) -> DispatchResult { + let exchanger = ensure_signed(origin)?; + + let vtoken_id = token_id.to_vtoken().map_err(|_| Error::::NotSupportTokenType)?; + let _token_amount_to_rebond = + Self::token_to_rebond(token_id).ok_or(Error::::InvalidRebondToken)?; + + let unlock_amount = match Self::token_unlock_ledger(token_id, unlock_id) { + Some((who, unlock_amount, time_unit)) => { + TimeUnitUnlockLedger::::mutate_exists( + &time_unit, + &token_id, + |value| -> Result<(), Error> { + if let Some((total_locked_origin, ledger_list_origin, _)) = value { + if total_locked_origin == &unlock_amount { + *value = None; + return Ok(()); + } + *total_locked_origin = total_locked_origin + .checked_sub(&unlock_amount) + .ok_or(Error::::CalculationOverflow)?; + ledger_list_origin.retain(|&x| x != unlock_id); + } else { + return Err(Error::::TimeUnitUnlockLedgerNotFound.into()); + } + return Ok(()); + }, + )?; + + UserUnlockLedger::::mutate_exists( + &who, + &token_id, + |value| -> Result<(), Error> { + if let Some((total_locked_origin, ledger_list_origin)) = value { + if total_locked_origin == &unlock_amount { + *value = None; + return Ok(()); + } + *total_locked_origin = total_locked_origin + .checked_sub(&unlock_amount) + .ok_or(Error::::CalculationOverflow)?; + ledger_list_origin.retain(|&x| x != unlock_id); + } else { + return Err(Error::::UserUnlockLedgerNotFound.into()); + } + return Ok(()); + }, + )?; + CurrencyUnlockingTotal::::mutate(|pool| -> Result<(), Error> { + *pool = pool + .checked_sub(&unlock_amount) + .ok_or(Error::::CalculationOverflow)?; + Ok(()) + })?; + + TokenUnlockLedger::::remove(&token_id, &unlock_id); + unlock_amount + }, + _ => return Err(Error::::TokenUnlockLedgerNotFound.into()), + }; + + let (token_amount, vtoken_amount, fee) = + Self::mint_without_tranfer(&exchanger, vtoken_id, token_id, unlock_amount) + .map_err(|e| e)?; + + TokenToRebond::::mutate(&token_id, |value| -> Result<(), Error> { + if let Some(value_info) = value { + *value_info = value_info + .checked_add(&token_amount) + .ok_or(Error::::CalculationOverflow)?; + } else { + return Err(Error::::InvalidRebondToken.into()); + } + Ok(()) + })?; + + Self::deposit_event(Event::RebondedByUnlockId { + address: exchanger, + token_id, + token_amount: unlock_amount, + vtoken_amount, + fee, + }); + Ok(()) + } + + #[pallet::weight(0)] + pub fn set_unlock_duration( + origin: OriginFor, + token_id: CurrencyIdOf, + unlock_duration: TimeUnit, + ) -> DispatchResult { + ensure_root(origin)?; + + UnlockDuration::::mutate(token_id, |old_unlock_duration| { + *old_unlock_duration = Some(unlock_duration.clone()); + }); + + Self::deposit_event(Event::UnlockDurationSet { token_id, unlock_duration }); + + Ok(()) + } + + #[pallet::weight(0)] + pub fn set_minimum_mint( + origin: OriginFor, + token_id: CurrencyIdOf, + amount: BalanceOf, + ) -> DispatchResult { + ensure_root(origin)?; + + if !MinimumMint::::contains_key(token_id) { + // mutate_exists + MinimumMint::::insert(token_id, amount); + } else { + MinimumMint::::mutate(token_id, |old_amount| { + *old_amount = amount; + }); + } + + Self::deposit_event(Event::MinimumMintSet { token_id, amount }); + + Ok(()) + } + + #[pallet::weight(0)] + pub fn set_minimum_redeem( + origin: OriginFor, + token_id: CurrencyIdOf, + amount: BalanceOf, + ) -> DispatchResult { + ensure_root(origin)?; + + MinimumRedeem::::mutate(token_id, |old_amount| { + *old_amount = amount; + }); + + Self::deposit_event(Event::MinimumRedeemSet { token_id, amount }); + Ok(()) + } + + #[pallet::weight(0)] + pub fn add_support_rebond_token( + origin: OriginFor, + token_id: CurrencyIdOf, + ) -> DispatchResult { + ensure_root(origin)?; + + if !TokenToRebond::::contains_key(token_id) { + TokenToRebond::::insert(token_id, BalanceOf::::zero()); + Self::deposit_event(Event::SupportRebondTokenAdded { token_id }); + } + + Ok(()) + } + + #[pallet::weight(0)] + pub fn remove_support_rebond_token( + origin: OriginFor, + token_id: CurrencyIdOf, + ) -> DispatchResult { + ensure_root(origin)?; + + if TokenToRebond::::contains_key(token_id) { + let token_amount_to_rebond = + Self::token_to_rebond(token_id).ok_or(Error::::InvalidRebondToken)?; + ensure!( + token_amount_to_rebond == BalanceOf::::zero(), + Error::::TokenToRebondNotZero + ); + + TokenToRebond::::remove(token_id); + Self::deposit_event(Event::SupportRebondTokenRemoved { token_id }); + } + Ok(()) + } + + #[pallet::weight(0)] + pub fn set_fees( + origin: OriginFor, + mint_fee: Permill, + redeem_fee: Permill, + ) -> DispatchResult { + ensure_root(origin)?; + + Fees::::mutate(|fees| *fees = (mint_fee, redeem_fee)); + + Self::deposit_event(Event::FeeSet { mint_fee, redeem_fee }); + Ok(()) + } + + #[pallet::weight(0)] + pub fn set_hook_iteration_limit(origin: OriginFor, limit: u32) -> DispatchResult { + T::ControlOrigin::ensure_origin(origin)?; + + HookIterationLimit::::mutate(|old_limit| { + *old_limit = limit; + }); + + Self::deposit_event(Event::HookIterationLimitSet { limit }); + Ok(()) + } + } + + impl Pallet { + #[transactional] + fn add_time_unit(a: TimeUnit, b: TimeUnit) -> Result { + let result = match a { + TimeUnit::Era(era_a) => match b { + TimeUnit::Era(era_b) => TimeUnit::Era(era_a + era_b), + _ => return Err(Error::::Unexpected.into()), + }, + TimeUnit::SlashingSpan(slashing_span_a) => match b { + TimeUnit::SlashingSpan(slashing_span_b) => + TimeUnit::SlashingSpan(slashing_span_a + slashing_span_b), + _ => return Err(Error::::Unexpected.into()), + }, + }; + + Ok(result) + } + + #[transactional] + fn mint_without_tranfer( + exchanger: &AccountIdOf, + vtoken_id: CurrencyId, + token_id: CurrencyId, + token_amount: BalanceOf, + ) -> Result<(BalanceOf, BalanceOf, BalanceOf), DispatchError> { + let token_pool_amount = Self::token_pool(token_id); + let vtoken_total_issuance = T::MultiCurrency::total_issuance(vtoken_id); + let (mint_rate, _redeem_rate) = Fees::::get(); + let mint_fee = mint_rate * token_amount; + let token_amount_excluding_fee = + token_amount.checked_sub(&mint_fee).ok_or(Error::::CalculationOverflow)?; + let mut vtoken_amount = token_amount_excluding_fee; + if token_pool_amount != BalanceOf::::zero() { + vtoken_amount = token_amount_excluding_fee + .checked_mul(&vtoken_total_issuance.into()) + .ok_or(Error::::CalculationOverflow)? + .checked_div(&token_pool_amount.into()) + .ok_or(Error::::CalculationOverflow)?; + } + + // Charging fees + T::MultiCurrency::transfer(token_id, &exchanger, &T::FeeAccount::get(), mint_fee)?; + // Issue the corresponding vtoken to the user's account. + T::MultiCurrency::deposit(vtoken_id, &exchanger, vtoken_amount)?; + TokenPool::::mutate(&token_id, |pool| -> Result<(), Error> { + *pool = pool + .checked_add(&token_amount_excluding_fee) + .ok_or(Error::::CalculationOverflow)?; + Ok(()) + })?; + Ok((token_amount_excluding_fee, vtoken_amount, mint_fee)) + } + + #[transactional] + fn on_initialize_update_ledger( + token_id: CurrencyId, + account: AccountIdOf, + index: &UnlockId, + mut unlock_amount: BalanceOf, + mut entrance_account_balance: BalanceOf, + time_unit: TimeUnit, + ) -> DispatchResult { + if entrance_account_balance >= unlock_amount { + TokenUnlockLedger::::remove(&token_id, &index); + + TimeUnitUnlockLedger::::mutate_exists( + &time_unit, + &token_id, + |value| -> Result<(), Error> { + if let Some((total_locked_origin, ledger_list_origin, _)) = value { + if total_locked_origin == &unlock_amount { + *value = None; + return Ok(()); + } + *total_locked_origin = total_locked_origin + .checked_sub(&unlock_amount) + .ok_or(Error::::CalculationOverflow)?; + ledger_list_origin.retain(|x| x != index); + } else { + return Err(Error::::TimeUnitUnlockLedgerNotFound.into()); + } + return Ok(()); + }, + )?; + + UserUnlockLedger::::mutate_exists( + &account, + &token_id, + |value| -> Result<(), Error> { + if let Some((total_locked_origin, ledger_list_origin)) = value { + if total_locked_origin == &unlock_amount { + *value = None; + return Ok(()); + } + ledger_list_origin.retain(|x| x != index); + *total_locked_origin = total_locked_origin + .checked_sub(&unlock_amount) + .ok_or(Error::::CalculationOverflow)?; + } else { + return Err(Error::::UserUnlockLedgerNotFound.into()); + } + return Ok(()); + }, + )?; + } else { + unlock_amount = entrance_account_balance; + TokenUnlockLedger::::mutate_exists( + &token_id, + &index, + |value| -> Result<(), Error> { + if let Some((_, total_locked_origin, _)) = value { + if total_locked_origin == &unlock_amount { + *value = None; + return Ok(()); + } + *total_locked_origin = total_locked_origin + .checked_sub(&unlock_amount) + .ok_or(Error::::CalculationOverflow)?; + } else { + return Err(Error::::TokenUnlockLedgerNotFound.into()); + } + return Ok(()); + }, + )?; + + TimeUnitUnlockLedger::::mutate_exists( + &time_unit, + &token_id, + |value| -> Result<(), Error> { + if let Some((total_locked_origin, _ledger_list_origin, _)) = value { + if total_locked_origin == &unlock_amount { + *value = None; + return Ok(()); + } + *total_locked_origin = total_locked_origin + .checked_sub(&unlock_amount) + .ok_or(Error::::CalculationOverflow)?; + } else { + return Err(Error::::TimeUnitUnlockLedgerNotFound.into()); + } + return Ok(()); + }, + )?; + + UserUnlockLedger::::mutate_exists( + &account, + &token_id, + |value| -> Result<(), Error> { + if let Some((total_locked_origin, _ledger_list_origin)) = value { + if total_locked_origin == &unlock_amount { + *value = None; + return Ok(()); + } + + *total_locked_origin = total_locked_origin + .checked_sub(&unlock_amount) + .ok_or(Error::::CalculationOverflow)?; + } else { + return Err(Error::::UserUnlockLedgerNotFound.into()); + } + return Ok(()); + }, + )?; + } + + entrance_account_balance = entrance_account_balance + .checked_sub(&unlock_amount) + .ok_or(Error::::CalculationOverflow)?; + + T::MultiCurrency::transfer( + token_id, + &T::EntranceAccount::get().into_account(), + &account, + unlock_amount, + )?; + + CurrencyUnlockingTotal::::mutate(|pool| -> Result<(), Error> { + *pool = pool.checked_sub(&unlock_amount).ok_or(Error::::CalculationOverflow)?; + Ok(()) + })?; + + Self::deposit_event(Event::RedeemSuccess { + unlock_id: *index, + token_id, + to: account, + token_amount: unlock_amount, + }); + Ok(()) + } + + #[transactional] + fn handle_on_initialize() -> DispatchResult { + let ksm = CurrencyId::Token(TokenSymbol::KSM); + let time_unit = MinTimeUnit::::get(ksm); + TimeUnitUnlockLedger::::iter_prefix_values(time_unit.clone()).for_each( + |(_total_locked, ledger_list, token_id)| { + let entrance_account_balance = T::MultiCurrency::free_balance( + token_id, + &T::EntranceAccount::get().into_account(), + ); + for index in ledger_list.iter().take(Self::hook_iteration_limit() as usize) { + if let Some((account, unlock_amount, time_unit)) = + Self::token_unlock_ledger(token_id, index) + { + if entrance_account_balance == BalanceOf::::zero() { + return; + } + Self::on_initialize_update_ledger( + token_id, + account, + index, + unlock_amount, + entrance_account_balance, + time_unit, + ) + .map_err(|e| e); + } + } + }, + ); + + let unlock_duration_era = match UnlockDuration::::get(ksm) { + Some(TimeUnit::Era(unlock_duration_era)) => unlock_duration_era, + _ => 0, + }; + let ongoing_era = match OngoingTimeUnit::::get(ksm) { + Some(TimeUnit::Era(ongoing_era)) => ongoing_era, + _ => 0, + }; + match time_unit { + TimeUnit::Era(min_era) => + if ongoing_era + unlock_duration_era > min_era { + let time_unit_ledger_list: Vec<( + BalanceOf, + BoundedVec, + CurrencyIdOf, + )> = TimeUnitUnlockLedger::::iter_prefix_values(time_unit).collect(); + if time_unit_ledger_list.len() == 0 { + MinTimeUnit::::mutate(ksm, |time_unit| -> Result<(), Error> { + match time_unit { + TimeUnit::Era(era) => { + *era = era + .checked_add(1) + .ok_or(Error::::CalculationOverflow)?; + Ok(()) + }, + _ => Ok(()), + } + })?; + } + }, + _ => (), + } + Ok(()) + } + } +} + +impl VtokenMintingOperator, AccountIdOf, TimeUnit> + for Pallet +{ + fn get_token_pool(currency_id: CurrencyId) -> BalanceOf { + Self::token_pool(currency_id) + } + + fn increase_token_pool(currency_id: CurrencyId, token_amount: BalanceOf) -> DispatchResult { + TokenPool::::mutate(currency_id, |pool| -> Result<(), Error> { + *pool = pool.checked_add(&token_amount).ok_or(Error::::CalculationOverflow)?; + + Ok(()) + })?; + + Ok(()) + } + + fn decrease_token_pool(currency_id: CurrencyId, token_amount: BalanceOf) -> DispatchResult { + TokenPool::::mutate(currency_id, |pool| -> Result<(), Error> { + *pool = pool.checked_sub(&token_amount).ok_or(Error::::CalculationOverflow)?; + Ok(()) + })?; + + Ok(()) + } + + fn update_ongoing_time_unit(currency_id: CurrencyId, time_unit: TimeUnit) -> DispatchResult { + OngoingTimeUnit::::mutate(currency_id, |time_unit_old| -> Result<(), Error> { + *time_unit_old = Some(time_unit); + Ok(()) + })?; + + Ok(()) + } + + fn get_ongoing_time_unit(currency_id: CurrencyId) -> Option { + Self::ongoing_time_unit(currency_id) + } + + fn get_unlock_records( + currency_id: CurrencyId, + time_unit: TimeUnit, + ) -> Option<(BalanceOf, Vec)> { + if let Some((balance, list, _)) = Self::time_unit_unlock_ledger(&time_unit, currency_id) { + Some((balance, list.into_inner())) + } else { + None + } + } + + #[transactional] + fn deduct_unlock_amount( + currency_id: CurrencyId, + index: u32, + deduct_amount: BalanceOf, + ) -> DispatchResult { + if let Some((who, unlock_amount, time_unit)) = Self::token_unlock_ledger(currency_id, index) + { + ensure!(unlock_amount >= deduct_amount, Error::::NotEnoughBalanceToUnlock); + + TokenPool::::mutate(¤cy_id, |pool| -> Result<(), Error> { + *pool = pool.checked_add(&deduct_amount).ok_or(Error::::CalculationOverflow)?; + Ok(()) + })?; + + CurrencyUnlockingTotal::::mutate(|pool| -> Result<(), Error> { + *pool = pool.checked_sub(&deduct_amount).ok_or(Error::::CalculationOverflow)?; + Ok(()) + })?; + + TimeUnitUnlockLedger::::mutate_exists( + &time_unit, + ¤cy_id, + |value| -> Result<(), Error> { + if let Some((total_locked_origin, ledger_list_origin, _)) = value { + if total_locked_origin == &deduct_amount { + *value = None; + return Ok(()); + } + *total_locked_origin = total_locked_origin + .checked_sub(&deduct_amount) + .ok_or(Error::::CalculationOverflow)?; + if unlock_amount == deduct_amount { + ledger_list_origin.retain(|&x| x != index); + } + } else { + return Err(Error::::TimeUnitUnlockLedgerNotFound.into()); + } + return Ok(()); + }, + )?; + + UserUnlockLedger::::mutate_exists( + &who, + ¤cy_id, + |value| -> Result<(), Error> { + if let Some((total_locked_origin, ledger_list_origin)) = value { + if total_locked_origin == &deduct_amount { + *value = None; + return Ok(()); + } + *total_locked_origin = total_locked_origin + .checked_sub(&deduct_amount) + .ok_or(Error::::CalculationOverflow)?; + if unlock_amount == deduct_amount { + ledger_list_origin.retain(|&x| x != index); + } + } else { + return Err(Error::::UserUnlockLedgerNotFound.into()); + } + return Ok(()); + }, + )?; + + if unlock_amount == deduct_amount { + TokenUnlockLedger::::remove(¤cy_id, &index); + } else { + TokenUnlockLedger::::mutate_exists( + ¤cy_id, + &index, + |value| -> Result<(), Error> { + if let Some((_, total_locked_origin, _)) = value { + if total_locked_origin == &deduct_amount { + *value = None; + return Ok(()); + } + *total_locked_origin = total_locked_origin + .checked_sub(&deduct_amount) + .ok_or(Error::::CalculationOverflow)?; + } else { + return Err(Error::::TokenUnlockLedgerNotFound.into()); + } + return Ok(()); + }, + )?; + } + } + Ok(()) + } + + fn get_entrance_and_exit_accounts() -> (AccountIdOf, AccountIdOf) { + (T::EntranceAccount::get().into_account(), T::ExitAccount::get().into_account()) + } + + fn get_token_unlock_ledger( + currency_id: CurrencyId, + index: u32, + ) -> Option<(AccountIdOf, BalanceOf, TimeUnit)> { + Self::token_unlock_ledger(currency_id, index) + } +} diff --git a/pallets/vtoken-minting/src/mock.rs b/pallets/vtoken-minting/src/mock.rs new file mode 100644 index 0000000000..ec3f6a193d --- /dev/null +++ b/pallets/vtoken-minting/src/mock.rs @@ -0,0 +1,239 @@ +// This file is part of Bifrost. + +// Copyright (C) 2019-2022 Liebi Technologies (UK) Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +// This program 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. + +// This program 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 this program. If not, see . + +// Ensure we're `no_std` when compiling for Wasm. + +#![cfg(test)] +#![allow(non_upper_case_globals)] + +use frame_support::{ + ord_parameter_types, parameter_types, + traits::{GenesisBuild, Nothing}, + PalletId, +}; +use frame_system::EnsureSignedBy; +use hex_literal::hex; +use node_primitives::{CurrencyId, TokenSymbol}; +use sp_core::H256; +use sp_runtime::{ + testing::Header, + traits::{BlakeTwo256, IdentityLookup}, + AccountId32, +}; + +use crate as vtoken_minting; + +pub type BlockNumber = u64; +pub type Amount = i128; +pub type Balance = u64; + +pub type AccountId = AccountId32; +pub const BNC: CurrencyId = CurrencyId::Native(TokenSymbol::ASG); +pub const DOT: CurrencyId = CurrencyId::Token(TokenSymbol::DOT); +pub const vDOT: CurrencyId = CurrencyId::VToken(TokenSymbol::DOT); +pub const KSM: CurrencyId = CurrencyId::Token(TokenSymbol::KSM); +pub const vKSM: CurrencyId = CurrencyId::VToken(TokenSymbol::KSM); +pub const ALICE: AccountId = AccountId32::new([0u8; 32]); +pub const BOB: AccountId = AccountId32::new([1u8; 32]); +pub const CHARLIE: AccountId = AccountId32::new([3u8; 32]); + +frame_support::construct_runtime!( + pub enum Runtime where + Block = Block, + NodeBlock = Block, + UncheckedExtrinsic = UncheckedExtrinsic, + { + System: frame_system::{Pallet, Call, Config, Storage, Event}, + Tokens: orml_tokens::{Pallet, Call, Storage, Config, Event}, + Balances: pallet_balances::{Pallet, Call, Storage, Config, Event}, + Currencies: orml_currencies::{Pallet, Call, Storage, Event}, + VtokenMinting: vtoken_minting::{Pallet, Call, Storage, Event} + } +); + +type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic; +type Block = frame_system::mocking::MockBlock; + +parameter_types! { + pub const BlockHashCount: u64 = 250; + pub BlockWeights: frame_system::limits::BlockWeights = + frame_system::limits::BlockWeights::simple_max(1024); +} +impl frame_system::Config for Runtime { + type AccountData = pallet_balances::AccountData; + type AccountId = AccountId; + type BaseCallFilter = frame_support::traits::Everything; + type BlockHashCount = BlockHashCount; + type BlockLength = (); + type BlockNumber = u64; + type BlockWeights = (); + type Call = Call; + type DbWeight = (); + type Event = Event; + type Hash = H256; + type Hashing = BlakeTwo256; + type Header = Header; + type Index = u64; + type Lookup = IdentityLookup; + type OnKilledAccount = (); + type OnNewAccount = (); + type OnSetCode = (); + type Origin = Origin; + type PalletInfo = PalletInfo; + type SS58Prefix = (); + type SystemWeightInfo = (); + type Version = (); + type MaxConsumers = frame_support::traits::ConstU32<16>; +} + +parameter_types! { + pub const GetNativeCurrencyId: CurrencyId = CurrencyId::Native(TokenSymbol::ASG); +} + +pub type AdaptedBasicCurrency = + orml_currencies::BasicCurrencyAdapter; + +impl orml_currencies::Config for Runtime { + type Event = Event; + type GetNativeCurrencyId = GetNativeCurrencyId; + type MultiCurrency = Tokens; + type NativeCurrency = AdaptedBasicCurrency; + type WeightInfo = (); +} + +parameter_types! { + pub const ExistentialDeposit: Balance = 1; +} + +impl pallet_balances::Config for Runtime { + type AccountStore = frame_system::Pallet; + type Balance = Balance; + type DustRemoval = (); + type Event = Event; + type ExistentialDeposit = ExistentialDeposit; + type MaxLocks = (); + type MaxReserves = (); + type ReserveIdentifier = [u8; 8]; + type WeightInfo = (); +} + +orml_traits::parameter_type_with_key! { + pub ExistentialDeposits: |_currency_id: CurrencyId| -> Balance { + 0 + }; +} +impl orml_tokens::Config for Runtime { + type Amount = i128; + type Balance = Balance; + type CurrencyId = CurrencyId; + type DustRemovalWhitelist = Nothing; + type Event = Event; + type ExistentialDeposits = ExistentialDeposits; + type MaxLocks = (); + type OnDust = (); + type WeightInfo = (); +} + +parameter_types! { + pub const MaximumUnlockIdOfUser: u32 = 1_000; + pub const MaximumUnlockIdOfTimeUnit: u32 = 1_000; + pub BifrostEntranceAccount: PalletId = PalletId(*b"bf/vtkin"); + pub BifrostExitAccount: PalletId = PalletId(*b"bf/vtout"); + pub BifrostFeeAccount: AccountId = hex!["e4da05f08e89bf6c43260d96f26fffcfc7deae5b465da08669a9d008e64c2c63"].into(); +} + +ord_parameter_types! { + pub const One: AccountId = ALICE; +} + +impl vtoken_minting::Config for Runtime { + type Event = Event; + type MultiCurrency = Currencies; + type ControlOrigin = EnsureSignedBy; + type MaximumUnlockIdOfUser = MaximumUnlockIdOfUser; + type MaximumUnlockIdOfTimeUnit = MaximumUnlockIdOfTimeUnit; + type EntranceAccount = BifrostEntranceAccount; + type ExitAccount = BifrostExitAccount; + type FeeAccount = BifrostFeeAccount; + type WeightInfo = (); +} + +pub struct ExtBuilder { + endowed_accounts: Vec<(AccountId, CurrencyId, Balance)>, +} + +impl Default for ExtBuilder { + fn default() -> Self { + Self { endowed_accounts: vec![] } + } +} + +impl ExtBuilder { + pub fn balances(mut self, endowed_accounts: Vec<(AccountId, CurrencyId, Balance)>) -> Self { + self.endowed_accounts = endowed_accounts; + self + } + + pub fn one_hundred_for_alice_n_bob(self) -> Self { + self.balances(vec![ + (ALICE, BNC, 100), + (BOB, BNC, 100), + (CHARLIE, BNC, 100), + (ALICE, DOT, 100), + (ALICE, vDOT, 400), + (BOB, vKSM, 1000), + (BOB, KSM, 10000000000), + ]) + } + + #[cfg(feature = "runtime-benchmarks")] + pub fn one_hundred_precision_for_each_currency_type_for_whitelist_account(self) -> Self { + use frame_benchmarking::whitelisted_caller; + let whitelist_caller: AccountId = whitelisted_caller(); + self.balances(vec![(whitelist_caller.clone(), KSM, 100_000_000_000_000)]) + } + + pub fn build(self) -> sp_io::TestExternalities { + let mut t = frame_system::GenesisConfig::default().build_storage::().unwrap(); + + pallet_balances::GenesisConfig:: { + balances: self + .endowed_accounts + .clone() + .into_iter() + .filter(|(_, currency_id, _)| *currency_id == BNC) + .map(|(account_id, _, initial_balance)| (account_id, initial_balance)) + .collect::>(), + } + .assimilate_storage(&mut t) + .unwrap(); + + orml_tokens::GenesisConfig:: { + balances: self + .endowed_accounts + .clone() + .into_iter() + .filter(|(_, currency_id, _)| *currency_id != BNC) + .collect::>(), + } + .assimilate_storage(&mut t) + .unwrap(); + + t.into() + } +} diff --git a/pallets/vtoken-minting/src/tests.rs b/pallets/vtoken-minting/src/tests.rs new file mode 100644 index 0000000000..de6454696c --- /dev/null +++ b/pallets/vtoken-minting/src/tests.rs @@ -0,0 +1,230 @@ +// This file is part of Bifrost. + +// Copyright (C) 2019-2022 Liebi Technologies (UK) Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +// This program 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. + +// This program 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 this program. If not, see . + +// Ensure we're `no_std` when compiling for Wasm. + +#![cfg(test)] + +use frame_support::{assert_noop, assert_ok, sp_runtime::Permill, BoundedVec}; + +use crate::{mock::*, *}; + +#[test] +fn mint() { + ExtBuilder::default().one_hundred_for_alice_n_bob().build().execute_with(|| { + assert_ok!(VtokenMinting::set_minimum_mint(Origin::root(), KSM, 200)); + pub const FEE: Permill = Permill::from_percent(5); + assert_ok!(VtokenMinting::set_fees(Origin::root(), FEE, FEE)); + assert_noop!( + VtokenMinting::mint(Some(BOB).into(), KSM, 100), + Error::::BelowMinimumMint + ); + assert_ok!(VtokenMinting::mint(Some(BOB).into(), KSM, 1000)); + assert_eq!(VtokenMinting::token_pool(KSM), 950); + assert_eq!(VtokenMinting::minimum_mint(KSM), 200); + assert_eq!(Tokens::total_issuance(vKSM), 1950); + + let (entrance_account, _exit_account) = VtokenMinting::get_entrance_and_exit_accounts(); + assert_eq!(Tokens::free_balance(KSM, &entrance_account), 950); + let fee_account: AccountId = ::FeeAccount::get(); + assert_eq!(Tokens::free_balance(KSM, &fee_account), 50); + }); +} + +#[test] +fn redeem() { + ExtBuilder::default().one_hundred_for_alice_n_bob().build().execute_with(|| { + pub const FEE: Permill = Permill::from_percent(2); + assert_ok!(VtokenMinting::set_fees(Origin::root(), FEE, FEE)); + assert_ok!(VtokenMinting::set_unlock_duration(Origin::root(), KSM, TimeUnit::Era(1))); + assert_ok!(VtokenMinting::increase_token_pool(KSM, 1000)); + assert_ok!(VtokenMinting::update_ongoing_time_unit(KSM, TimeUnit::Era(1))); + assert_ok!(VtokenMinting::set_minimum_redeem(Origin::root(), vKSM, 90)); + assert_ok!(VtokenMinting::mint(Some(BOB).into(), KSM, 1000)); + assert_noop!( + VtokenMinting::redeem(Some(BOB).into(), vKSM, 80), + Error::::BelowMinimumRedeem + ); + assert_noop!( + VtokenMinting::redeem(Some(BOB).into(), KSM, 80), + Error::::NotSupportTokenType + ); + assert_ok!(VtokenMinting::redeem(Some(BOB).into(), vKSM, 100)); + assert_ok!(VtokenMinting::redeem(Some(BOB).into(), vKSM, 200)); + assert_eq!(VtokenMinting::token_pool(KSM), 1686); // 1000 + 980 - 98 - 196 + assert_eq!(VtokenMinting::currency_unlocking_total(), 294); // 98 + 196 + let (entrance_account, _exit_account) = VtokenMinting::get_entrance_and_exit_accounts(); + assert_eq!(Tokens::free_balance(KSM, &entrance_account), 980); + let mut ledger_list_origin = BoundedVec::default(); + assert_ok!(ledger_list_origin.try_push(0)); + assert_ok!(ledger_list_origin.try_push(1)); + assert_eq!( + VtokenMinting::user_unlock_ledger(BOB, KSM), + Some((294, ledger_list_origin.clone())) + ); + assert_eq!(VtokenMinting::token_unlock_ledger(KSM, 0), Some((BOB, 98, TimeUnit::Era(2)))); + let mut ledger_list_origin2 = BoundedVec::default(); + assert_ok!(ledger_list_origin2.try_push(0)); + assert_ok!(ledger_list_origin2.try_push(1)); + assert_eq!( + VtokenMinting::time_unit_unlock_ledger(TimeUnit::Era(2), KSM), + Some((294, ledger_list_origin2, KSM)) + ); + }); +} + +#[test] +fn rebond() { + ExtBuilder::default().one_hundred_for_alice_n_bob().build().execute_with(|| { + pub const FEE: Permill = Permill::from_percent(0); + assert_ok!(VtokenMinting::set_fees(Origin::root(), FEE, FEE)); + assert_ok!(VtokenMinting::set_unlock_duration(Origin::root(), KSM, TimeUnit::Era(0))); + assert_ok!(VtokenMinting::increase_token_pool(KSM, 1000)); + assert_ok!(VtokenMinting::update_ongoing_time_unit(KSM, TimeUnit::Era(1))); + let mut ledger_list_origin = BoundedVec::default(); + assert_ok!(ledger_list_origin.try_push(0)); + let mut ledger_list_origin2 = BoundedVec::default(); + assert_ok!(ledger_list_origin2.try_push(0)); + assert_ok!(VtokenMinting::mint(Some(BOB).into(), KSM, 200)); + assert_ok!(VtokenMinting::mint(Some(BOB).into(), KSM, 100)); + assert_ok!(VtokenMinting::redeem(Some(BOB).into(), vKSM, 200)); + assert_ok!(VtokenMinting::redeem(Some(BOB).into(), vKSM, 100)); + assert_eq!(VtokenMinting::token_unlock_ledger(KSM, 1), Some((BOB, 100, TimeUnit::Era(1)))); + assert_noop!( + VtokenMinting::rebond(Some(BOB).into(), KSM, 100), + Error::::InvalidRebondToken + ); + assert_ok!(VtokenMinting::add_support_rebond_token(Origin::root(), KSM)); + assert_ok!(VtokenMinting::rebond(Some(BOB).into(), KSM, 200)); + assert_eq!( + VtokenMinting::time_unit_unlock_ledger(TimeUnit::Era(1), KSM), + Some((100, ledger_list_origin.clone(), KSM)) + ); + assert_eq!( + VtokenMinting::user_unlock_ledger(BOB, KSM), + Some((100, ledger_list_origin2.clone())) + ); + assert_eq!(VtokenMinting::token_unlock_ledger(KSM, 0), Some((BOB, 100, TimeUnit::Era(1)))); + assert_eq!(VtokenMinting::token_unlock_ledger(KSM, 1), None); + assert_eq!(VtokenMinting::token_pool(KSM), 1200); + assert_eq!(VtokenMinting::currency_unlocking_total(), 100); // 200 + 100 - 200 + let (entrance_account, _exit_account) = VtokenMinting::get_entrance_and_exit_accounts(); + assert_eq!(Tokens::free_balance(KSM, &entrance_account), 300); + }); +} + +#[test] +fn hook() { + ExtBuilder::default().one_hundred_for_alice_n_bob().build().execute_with(|| { + assert_eq!(VtokenMinting::min_time_unit(KSM), TimeUnit::Era(0)); + assert_ok!(VtokenMinting::update_ongoing_time_unit(KSM, TimeUnit::Era(3))); + assert_eq!(VtokenMinting::ongoing_time_unit(KSM), Some(TimeUnit::Era(3))); + assert_ok!(VtokenMinting::set_unlock_duration(Origin::root(), KSM, TimeUnit::Era(1))); + assert_ok!(VtokenMinting::set_hook_iteration_limit(Origin::signed(ALICE), 1)); + assert_eq!(VtokenMinting::unlock_duration(KSM), Some(TimeUnit::Era(1))); + VtokenMinting::on_initialize(100); + VtokenMinting::on_initialize(100); + VtokenMinting::on_initialize(100); + VtokenMinting::on_initialize(100); + VtokenMinting::on_initialize(100); + assert_eq!(VtokenMinting::min_time_unit(KSM), TimeUnit::Era(4)); + assert_ok!(VtokenMinting::increase_token_pool(KSM, 1000)); + assert_ok!(VtokenMinting::mint(Some(BOB).into(), KSM, 200)); + assert_ok!(VtokenMinting::mint(Some(BOB).into(), KSM, 100)); + assert_ok!(VtokenMinting::redeem(Some(BOB).into(), vKSM, 200)); + assert_ok!(VtokenMinting::redeem(Some(BOB).into(), vKSM, 100)); + assert_eq!(VtokenMinting::currency_unlocking_total(), 300); // 200 + 100 + assert_noop!( + VtokenMinting::rebond(Some(BOB).into(), KSM, 100), + Error::::InvalidRebondToken + ); + assert_ok!(VtokenMinting::add_support_rebond_token(Origin::root(), KSM)); + let (entrance_account, _exit_account) = VtokenMinting::get_entrance_and_exit_accounts(); + assert_eq!(Tokens::free_balance(KSM, &entrance_account), 300); + VtokenMinting::on_initialize(100); + assert_eq!(VtokenMinting::min_time_unit(KSM), TimeUnit::Era(4)); + VtokenMinting::on_initialize(100); + assert_eq!(VtokenMinting::token_unlock_ledger(KSM, 0), None); + assert_eq!(VtokenMinting::token_unlock_ledger(KSM, 1), None); + assert_eq!(VtokenMinting::time_unit_unlock_ledger(TimeUnit::Era(4), KSM), None); + assert_eq!(VtokenMinting::time_unit_unlock_ledger(TimeUnit::Era(5), KSM), None); + assert_eq!(VtokenMinting::user_unlock_ledger(BOB, KSM), None); + assert_eq!(VtokenMinting::token_pool(KSM), 1000); + assert_eq!(Tokens::free_balance(KSM, &entrance_account), 0); + assert_ok!(VtokenMinting::update_ongoing_time_unit(KSM, TimeUnit::Era(5))); + VtokenMinting::on_initialize(100); + VtokenMinting::on_initialize(0); + VtokenMinting::on_initialize(1); + assert_eq!(VtokenMinting::min_time_unit(KSM), TimeUnit::Era(6)); + assert_eq!(VtokenMinting::currency_unlocking_total(), 0); + assert_ok!(VtokenMinting::mint(Some(BOB).into(), KSM, 100)); + assert_ok!(VtokenMinting::redeem(Some(BOB).into(), vKSM, 200)); + VtokenMinting::on_initialize(0); + assert_eq!(VtokenMinting::token_unlock_ledger(KSM, 2), Some((BOB, 100, TimeUnit::Era(6)))); + let mut ledger_list_origin = BoundedVec::default(); + assert_ok!(ledger_list_origin.try_push(2)); + let mut ledger_list_origin2 = BoundedVec::default(); + assert_ok!(ledger_list_origin2.try_push(2)); + assert_eq!( + VtokenMinting::time_unit_unlock_ledger(TimeUnit::Era(6), KSM), + Some((100, ledger_list_origin.clone(), KSM)) + ); + assert_eq!( + VtokenMinting::user_unlock_ledger(BOB, KSM), + Some((100, ledger_list_origin2.clone())) + ); + }); +} + +#[test] +fn rebond_by_unlock_id() { + ExtBuilder::default().one_hundred_for_alice_n_bob().build().execute_with(|| { + assert_ok!(VtokenMinting::set_unlock_duration(Origin::root(), KSM, TimeUnit::Era(0))); + assert_ok!(VtokenMinting::increase_token_pool(KSM, 1000)); + assert_ok!(VtokenMinting::update_ongoing_time_unit(KSM, TimeUnit::Era(1))); + let mut ledger_list_origin = BoundedVec::default(); + assert_ok!(ledger_list_origin.try_push(1)); + let mut ledger_list_origin2 = BoundedVec::default(); + assert_ok!(ledger_list_origin2.try_push(1)); + assert_ok!(VtokenMinting::mint(Some(BOB).into(), KSM, 200)); + assert_ok!(VtokenMinting::mint(Some(BOB).into(), KSM, 100)); + assert_ok!(VtokenMinting::redeem(Some(BOB).into(), vKSM, 200)); + assert_ok!(VtokenMinting::redeem(Some(BOB).into(), vKSM, 100)); + assert_eq!(VtokenMinting::token_pool(KSM), 1000); + assert_noop!( + VtokenMinting::rebond_by_unlock_id(Some(BOB).into(), KSM, 0), + Error::::InvalidRebondToken + ); + assert_ok!(VtokenMinting::add_support_rebond_token(Origin::root(), KSM)); + assert_ok!(VtokenMinting::rebond_by_unlock_id(Some(BOB).into(), KSM, 0)); + assert_eq!( + VtokenMinting::time_unit_unlock_ledger(TimeUnit::Era(1), KSM), + Some((100, ledger_list_origin.clone(), KSM)) + ); + assert_eq!( + VtokenMinting::user_unlock_ledger(BOB, KSM), + Some((100, ledger_list_origin2.clone())) + ); + assert_eq!(VtokenMinting::token_unlock_ledger(KSM, 0), None); + assert_eq!(VtokenMinting::token_unlock_ledger(KSM, 1), Some((BOB, 100, TimeUnit::Era(1)))); + assert_eq!(VtokenMinting::token_pool(KSM), 1200); + assert_eq!(VtokenMinting::currency_unlocking_total(), 100); // 200 + 100 - 200 + let (entrance_account, _exit_account) = VtokenMinting::get_entrance_and_exit_accounts(); + assert_eq!(Tokens::free_balance(KSM, &entrance_account), 300); + }); +} diff --git a/pallets/vtoken-minting/src/weights.rs b/pallets/vtoken-minting/src/weights.rs new file mode 100644 index 0000000000..061de7e820 --- /dev/null +++ b/pallets/vtoken-minting/src/weights.rs @@ -0,0 +1,38 @@ +// This file is part of Bifrost. + +// Copyright (C) 2019-2022 Liebi Technologies (UK) Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +// This program 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. + +// This program 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 this program. If not, see . + +#![allow(unused_parens)] +#![allow(unused_imports)] + +use frame_support::{ + traits::Get, + weights::{constants::RocksDbWeight, Weight}, +}; +use sp_std::marker::PhantomData; + +/// Weight functions needed for the pallet. +pub trait WeightInfo { + fn on_initialize() -> Weight; +} + +// For backwards compatibility and tests +impl WeightInfo for () { + fn on_initialize() -> Weight { + (50_000_000 as Weight) + } +} diff --git a/runtime/bifrost-kusama/Cargo.toml b/runtime/bifrost-kusama/Cargo.toml index 8353d6cfa0..24ee80b936 100644 --- a/runtime/bifrost-kusama/Cargo.toml +++ b/runtime/bifrost-kusama/Cargo.toml @@ -13,7 +13,9 @@ codec = { package = "parity-scale-codec", version = "3.0.0", default-features = ] } hex-literal = { version = "0.3.1" } log = { version = "0.4.16", default-features = false } -scale-info = { version = "2.0.1", default-features = false, features = ["derive"] } +scale-info = { version = "2.0.1", default-features = false, features = [ + "derive", +] } smallvec = "1.6.1" static_assertions = "1.1.0" # primitives @@ -43,7 +45,7 @@ pallet-aura = { git = "https://github.com/paritytech/substrate", branch = "polka pallet-authorship = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.18", default-features = false } pallet-balances = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.18", default-features = false } pallet-bounties = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.18", default-features = false } -pallet-collective = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.18",default-features = false } +pallet-collective = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.18", default-features = false } pallet-democracy = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.18", default-features = false } pallet-elections-phragmen = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.18", default-features = false } pallet-identity = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.18", default-features = false } @@ -74,11 +76,11 @@ pallet-collator-selection = { git = "https://github.com/paritytech/cumulus", bra parachain-info = { git = "https://github.com/paritytech/cumulus", branch = "polkadot-v0.9.18", default-features = false } # Polkadot dependencies -pallet-xcm = { git = "https://github.com/paritytech/polkadot", branch = "release-v0.9.18",default-features = false} +pallet-xcm = { git = "https://github.com/paritytech/polkadot", branch = "release-v0.9.18", default-features = false } polkadot-parachain = { git = "https://github.com/paritytech/polkadot", branch = "release-v0.9.18", default-features = false } polkadot-primitives = { git = "https://github.com/paritytech/polkadot", branch = "release-v0.9.18", default-features = false } xcm = { git = "https://github.com/paritytech/polkadot", branch = "release-v0.9.18", default-features = false } -xcm-builder = { git = "https://github.com/paritytech/polkadot", branch = "release-v0.9.18",default-features = false} +xcm-builder = { git = "https://github.com/paritytech/polkadot", branch = "release-v0.9.18", default-features = false } xcm-executor = { git = "https://github.com/paritytech/polkadot", branch = "release-v0.9.18", default-features = false } # Bifrost @@ -95,6 +97,10 @@ bifrost-vsbond-auction = { path = "../../pallets/vsbond-auction", default-featur bifrost-token-issuer = { path = "../../pallets/token-issuer", default-features = false } bifrost-lightening-redeem = { path = "../../pallets/lightening-redeem", default-features = false } bifrost-call-switchgear = { path = "../../pallets/call-switchgear", default-features = false } +bifrost-slp = { path = "../../pallets/slp", default-features = false } +# xcm-support = { path = "../../xcm-support", default-features = false } +bifrost-vtoken-minting = { path = "../../pallets/vtoken-minting", default-features = false } + bifrost-asset-registry = { path = "../../pallets/asset-registry", default-features = false } bifrost-vstoken-conversion = { path = "../../pallets/vstoken-conversion", default-features = false } @@ -107,7 +113,7 @@ orml-unknown-tokens = { version = "0.4.1-dev", default-features = false } orml-xcm = { version = "0.4.1-dev", default-features = false } orml-xcm-support = { version = "0.4.1-dev", default-features = false } -merkle-distributor = {version = "*", default-features = false } +merkle-distributor = { version = "*", default-features = false } zenlink-protocol = { version = "*", default-features = false } zenlink-protocol-runtime-api = { version = "*", default-features = false } parachain-staking = { version = "*", default-features = false } @@ -190,6 +196,8 @@ std = [ "bifrost-liquidity-mining-rpc-runtime-api/std", "bifrost-token-issuer/std", "bifrost-lightening-redeem/std", + "bifrost-slp/std", + "bifrost-vtoken-minting/std", "bifrost-asset-registry/std", "zenlink-protocol/std", "zenlink-protocol-runtime-api/std", @@ -250,9 +258,7 @@ disable-runtime-api = [] # A feature that should be enabled when the runtime should be build for on-chain # deployment. This will disable stuff that shouldn't be part of the on-chain wasm # to make it smaller like logging for example. -on-chain-release-build = [ - "sp-api/disable-logging", -] +on-chain-release-build = ["sp-api/disable-logging"] # Set timing constants (e.g. session period) to faster versions to speed up testing. fast-runtime = [] diff --git a/runtime/bifrost-kusama/src/lib.rs b/runtime/bifrost-kusama/src/lib.rs index 6ad39709e8..1d343ea0fb 100644 --- a/runtime/bifrost-kusama/src/lib.rs +++ b/runtime/bifrost-kusama/src/lib.rs @@ -28,6 +28,7 @@ include!(concat!(env!("OUT_DIR"), "/wasm_binary.rs")); use core::convert::TryInto; +use bifrost_slp::QueryResponseManager; // A few exports that help ease life for downstream crates. pub use frame_support::{ construct_runtime, match_type, parameter_types, @@ -41,8 +42,10 @@ pub use frame_support::{ PalletId, RuntimeDebug, StorageValue, }; use frame_system::limits::{BlockLength, BlockWeights}; +use node_primitives::AssetIdMapping; pub use pallet_balances::Call as BalancesCall; pub use pallet_timestamp::Call as TimestampCall; +use pallet_xcm::QueryStatus; pub use parachain_staking::{InflationInfo, Range}; use sp_api::impl_runtime_apis; use sp_core::OpaqueMetadata; @@ -75,6 +78,7 @@ use bifrost_runtime_common::{ CouncilCollective, EnsureRootOrAllTechnicalCommittee, MoreThanHalfCouncil, SlowAdjustingFeeUpdate, TechnicalCollective, }; +use bifrost_slp::QueryId; use codec::{Decode, Encode, MaxEncodedLen}; use constants::currency::*; use cumulus_primitives_core::ParaId as CumulusParaId; @@ -85,8 +89,9 @@ use frame_support::{ use frame_system::EnsureRoot; use hex_literal::hex; pub use node_primitives::{ - traits::CheckSubAccount, AccountId, Amount, AssetIdMapping, Balance, BlockNumber, CurrencyId, - ExtraFeeName, Moment, Nonce, ParaId, PoolId, RpcContributionStatus, TokenSymbol, + traits::{CheckSubAccount, VtokenMintingOperator}, + AccountId, Amount, Balance, BlockNumber, CurrencyId, ExtraFeeName, Moment, Nonce, ParaId, + PoolId, RpcContributionStatus, TimeUnit, TokenSymbol, }; // orml imports use orml_currencies::BasicCurrencyAdapter; @@ -135,7 +140,7 @@ pub const VERSION: RuntimeVersion = RuntimeVersion { spec_name: create_runtime_str!("bifrost"), impl_name: create_runtime_str!("bifrost"), authoring_version: 1, - spec_version: 932, + spec_version: 940, impl_version: 0, apis: RUNTIME_API_VERSIONS, transaction_version: 1, @@ -295,6 +300,8 @@ parameter_types! { pub const VsbondAuctionPalletId: PalletId = PalletId(*b"bf/vsbnd"); pub const ParachainStakingPalletId: PalletId = PalletId(*b"bf/stake"); pub const BifrostVsbondPalletId: PalletId = PalletId(*b"bf/salpb"); + pub const SlpEntrancePalletId: PalletId = PalletId(*b"bf/vtkin"); + pub const SlpExitPalletId: PalletId = PalletId(*b"bf/vtout"); } impl frame_system::Config for Runtime { @@ -1339,6 +1346,7 @@ parameter_type_with_key! { &CurrencyId::VSBond(TokenSymbol::KSM, ..) => 10 * millicent(RelayCurrencyId::get()), &CurrencyId::VSBond(TokenSymbol::DOT, ..) => 1 * cent(PolkadotCurrencyId::get()), &CurrencyId::LPToken(..) => 10 * millicent(NativeCurrencyId::get()), + &CurrencyId::VToken(TokenSymbol::KSM) => 10 * millicent(RelayCurrencyId::get()), // 0.0001 vKSM &CurrencyId::Token(TokenSymbol::RMRK) => 1 * micro(CurrencyId::Token(TokenSymbol::RMRK)), &CurrencyId::Token(TokenSymbol::MOVR) => 1 * micro(CurrencyId::Token(TokenSymbol::MOVR)), // MOVR has a decimals of 10e18 CurrencyId::ForeignAsset(foreign_asset_id) => { @@ -1362,7 +1370,8 @@ impl Contains for DustRemovalWhitelist { LiquidityMiningDOTPalletId::get().check_sub_account::(a) || AccountIdConversion::::into_account(&ParachainStakingPalletId::get()) .eq(a) || AccountIdConversion::::into_account(&BifrostVsbondPalletId::get()) - .eq(a) + .eq(a) || AccountIdConversion::::into_account(&SlpEntrancePalletId::get()).eq(a) || + AccountIdConversion::::into_account(&SlpExitPalletId::get()).eq(a) } } @@ -1513,6 +1522,13 @@ pub fn create_x2_multilocation(index: u16) -> MultiLocation { ) } +pub struct SubAccountIndexMultiLocationConvertor; +impl Convert for SubAccountIndexMultiLocationConvertor { + fn convert(sub_account_index: u16) -> MultiLocation { + create_x2_multilocation(sub_account_index) + } +} + parameter_types! { pub MinContribution: Balance = dollar(RelayCurrencyId::get()) / 10; pub const RemoveKeysLimit: u32 = 500; @@ -1674,6 +1690,53 @@ impl xcm_interface::Config for Runtime { type ContributionFee = UmpTransactFee; } +parameter_types! { + pub const MaxTypeEntryPerBlock: u32 = 10; + pub const MaxRefundPerBlock: u32 = 10; +} + +pub struct SubstrateResponseManager; +impl QueryResponseManager for SubstrateResponseManager { + fn get_query_response_record(query_id: QueryId) -> bool { + if let Some(QueryStatus::Ready { .. }) = PolkadotXcm::query(query_id) { + true + } else { + false + } + } + + fn create_query_record(responder: &MultiLocation, timeout: BlockNumber) -> u64 { + PolkadotXcm::new_query(responder.clone(), timeout) + // for xcm v3 version see the following + // PolkadotXcm::new_query(responder, timeout, Here) + } + + fn remove_query_record(query_id: QueryId) -> bool { + // Temporarily banned. Querries from pallet_xcm cannot be removed unless it is in ready + // status. And we are not allowed to mannually change query status. + // So in the manual mode, it is not possible to remove the query at all. + // PolkadotXcm::take_response(query_id).is_some() + + PolkadotXcm::take_response(query_id); + true + } +} + +impl bifrost_slp::Config for Runtime { + type Event = Event; + type MultiCurrency = Currencies; + type ControlOrigin = EnsureOneOf; + type WeightInfo = (); + type VtokenMinting = VtokenMinting; + type AccountConverter = SubAccountIndexMultiLocationConvertor; + type ParachainId = SelfParaChainId; + type XcmRouter = XcmRouter; + type XcmExecutor = XcmExecutor; + type SubstrateResponseManager = SubstrateResponseManager; + type MaxTypeEntryPerBlock = MaxTypeEntryPerBlock; + type MaxRefundPerBlock = MaxRefundPerBlock; +} + impl bifrost_vstoken_conversion::Config for Runtime { type Event = Event; type MultiCurrency = Currencies; @@ -1745,6 +1808,24 @@ pub type ZenlinkLocationToAccountId = ( AccountId32Aliases, ); +parameter_types! { + pub const MaximumUnlockIdOfUser: u32 = 10; + pub const MaximumUnlockIdOfTimeUnit: u32 = 50; + pub BifrostFeeAccount: AccountId = TreasuryPalletId::get().into_account(); +} + +impl bifrost_vtoken_minting::Config for Runtime { + type Event = Event; + type MultiCurrency = Currencies; + type ControlOrigin = EnsureOneOf; + type MaximumUnlockIdOfUser = MaximumUnlockIdOfUser; + type MaximumUnlockIdOfTimeUnit = MaximumUnlockIdOfTimeUnit; + type EntranceAccount = SlpEntrancePalletId; + type ExitAccount = SlpExitPalletId; + type FeeAccount = BifrostFeeAccount; + type WeightInfo = (); +} + // Below is the implementation of tokens manipulation functions other than native token. pub struct LocalAssetAdaptor(PhantomData); @@ -1880,6 +1961,8 @@ construct_runtime! { CallSwitchgear: bifrost_call_switchgear::{Pallet, Storage, Call, Event} = 112, VSBondAuction: bifrost_vsbond_auction::{Pallet, Call, Storage, Event} = 113, AssetRegistry: bifrost_asset_registry::{Pallet, Call, Storage, Event} = 114, + VtokenMinting: bifrost_vtoken_minting::{Pallet, Call, Storage, Event} = 115, + Slp: bifrost_slp::{Pallet, Call, Storage, Event} = 116, XcmInterface: xcm_interface::{Pallet, Call, Storage, Event} = 117, VstokenConversion: bifrost_vstoken_conversion::{Pallet, Call, Storage, Event} = 118, }