diff --git a/Cargo.lock b/Cargo.lock index 3f9c6abce1..f256de40bf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -162,6 +162,8 @@ dependencies = [ "bifrost-minter-reward", "bifrost-runtime-common", "bifrost-salp", + "bifrost-salp-lite", + "bifrost-salp-lite-rpc-runtime-api", "bifrost-salp-rpc-runtime-api", "bifrost-token-issuer", "bifrost-vesting", @@ -820,6 +822,8 @@ dependencies = [ "bifrost-liquidity-mining-rpc-runtime-api", "bifrost-runtime-common", "bifrost-salp", + "bifrost-salp-lite", + "bifrost-salp-lite-rpc-runtime-api", "bifrost-salp-rpc-runtime-api", "bifrost-token-issuer", "bifrost-vesting", @@ -985,6 +989,41 @@ dependencies = [ "xcm-support", ] +[[package]] +name = "bifrost-salp-lite" +version = "0.8.0" +dependencies = [ + "bifrost-bancor", + "bifrost-runtime", + "frame-benchmarking", + "frame-support", + "frame-system", + "log", + "node-primitives", + "orml-currencies", + "orml-tokens 0.4.1-dev (git+https://github.com/open-web3-stack/open-runtime-module-library?rev=dc96605342de9971c27699561723a6de6e2614c2)", + "orml-traits 0.4.1-dev (git+https://github.com/open-web3-stack/open-runtime-module-library?rev=dc96605342de9971c27699561723a6de6e2614c2)", + "pallet-balances", + "pallet-multisig", + "parity-scale-codec", + "scale-info", + "smallvec", + "sp-arithmetic", + "sp-core", + "sp-io", + "sp-runtime", + "sp-std", +] + +[[package]] +name = "bifrost-salp-lite-rpc-runtime-api" +version = "0.8.0" +dependencies = [ + "node-primitives", + "parity-scale-codec", + "sp-api", +] + [[package]] name = "bifrost-salp-rpc-api" version = "0.8.0" @@ -2274,6 +2313,8 @@ dependencies = [ "bifrost-minter-reward", "bifrost-runtime-common", "bifrost-salp", + "bifrost-salp-lite", + "bifrost-salp-lite-rpc-runtime-api", "bifrost-salp-rpc-runtime-api", "bifrost-vesting", "bifrost-vsbond-auction", diff --git a/Cargo.toml b/Cargo.toml index 5c175b69b3..9123632516 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,6 +17,9 @@ members = [ "pallets/vsbond-auction", "pallets/vtoken-mint", "pallets/vtoken-mint/rpc", + "pallets/token-issuer", + "pallets/lightening-redeem", + "pallets/salp-lite", "runtime/asgard", "runtime/bifrost", "xcm-support", diff --git a/node/primitives/src/salp.rs b/node/primitives/src/salp.rs index 2814ccf553..635c464514 100644 --- a/node/primitives/src/salp.rs +++ b/node/primitives/src/salp.rs @@ -25,6 +25,7 @@ pub enum ContributionStatus { Refunded, Unlocked, Redeemed, + MigratedIdle, } #[derive(Encode, Decode, Clone, PartialEq, Eq, Copy, RuntimeDebug)] @@ -35,6 +36,7 @@ pub enum RpcContributionStatus { Refunded, Unlocked, Redeemed, + MigratedIdle, } impl ContributionStatus @@ -62,6 +64,7 @@ where Self::Refunded => RpcContributionStatus::Refunded, Self::Unlocked => RpcContributionStatus::Unlocked, Self::Redeemed => RpcContributionStatus::Redeemed, + Self::MigratedIdle => RpcContributionStatus::MigratedIdle, }; rpc_status } diff --git a/pallets/salp-lite/Cargo.toml b/pallets/salp-lite/Cargo.toml new file mode 100644 index 0000000000..7e7ff82e33 --- /dev/null +++ b/pallets/salp-lite/Cargo.toml @@ -0,0 +1,52 @@ +[package] +name = "bifrost-salp-lite" +version = "0.8.0" +authors = ["Ron Yang "] +edition = "2018" + +[dependencies] +codec = { package = "parity-scale-codec", version = "2.0.0", default-features = false, features = ["derive"] } +scale-info = { version = "1.0.0", default-features = false, features = ["derive"] } +log = { version = "0.4.14", default-features = false } +node-primitives = { path = "../../node/primitives", default-features = false } +frame-support = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.11", default-features = false } +frame-system = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.11", default-features = false } +frame-benchmarking = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.11", default-features = false, optional = true } +sp-std = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.11", default-features = false } +sp-io = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.11", default-features = false } +sp-runtime = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.11", default-features = false } +sp-arithmetic = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.11", default-features = false } +orml-traits = { version = "0.4.1-dev", default-features = false } + +[dev-dependencies] +pallet-multisig = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.11"} +smallvec = "1.6.1" +sp-io = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.11" } +sp-core = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.11" } +sp-runtime = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.11" } +orml-tokens = "0.4.1-dev" +orml-currencies = "0.4.1-dev" +pallet-balances = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.11" } +bifrost-bancor = { path = "../../pallets/bancor" } +bifrost-runtime = { path = "../../runtime/bifrost" } + +[features] +default = ["std"] +std = [ + "codec/std", + "log/std", + "node-primitives/std", + "frame-support/std", + "frame-system/std", + "sp-std/std", + "sp-io/std", + "sp-runtime/std", + "sp-arithmetic/std", + "orml-traits/std", +] + +runtime-benchmarks = [ + "frame-benchmarking", + "sp-runtime/runtime-benchmarks", + "frame-system/runtime-benchmarks", +] diff --git a/pallets/salp-lite/rpc/Cargo.toml b/pallets/salp-lite/rpc/Cargo.toml new file mode 100644 index 0000000000..b209238d53 --- /dev/null +++ b/pallets/salp-lite/rpc/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "bifrost-salp-lite-rpc-api" +version = "0.8.0" +authors = ["Ron Yang "] +edition = "2018" + +[dependencies] +serde = { version = "1.0.124", features = ["derive"] } +codec = { package = "parity-scale-codec", version = "2.0.0", default-features = false, features = ["derive"] } +jsonrpc-core = "15.1.0" +jsonrpc-core-client = "15.1.0" +jsonrpc-derive = "15.1.0" +sp-api = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.11" } +sp-runtime = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.11" } +sp-blockchain = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.11" } +sp-core = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.11" } +sp-rpc = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.11" } +node-primitives = { path = "../../../node/primitives", default-features = false } +bifrost-salp-lite-rpc-runtime-api = { path = "./runtime-api" } diff --git a/pallets/salp-lite/rpc/runtime-api/Cargo.toml b/pallets/salp-lite/rpc/runtime-api/Cargo.toml new file mode 100644 index 0000000000..8423066cda --- /dev/null +++ b/pallets/salp-lite/rpc/runtime-api/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "bifrost-salp-lite-rpc-runtime-api" +version = "0.8.0" +authors = ["Ron Yang"] +edition = "2018" + +[dependencies] +codec = { package = "parity-scale-codec", version = "2.0.0", default-features = false, features = ["derive"] } +sp-api = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.11", default-features = false } +node-primitives = { path = "../../../../node/primitives", default-features = false } + +[features] +default = ["std"] +std = [ + "codec/std", + "sp-api/std", + "node-primitives/std", +] diff --git a/pallets/salp-lite/rpc/runtime-api/src/lib.rs b/pallets/salp-lite/rpc/runtime-api/src/lib.rs new file mode 100644 index 0000000000..1ff6fe7807 --- /dev/null +++ b/pallets/salp-lite/rpc/runtime-api/src/lib.rs @@ -0,0 +1,35 @@ +// This file is part of Bifrost. + +// Copyright (C) 2019-2021 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)] + +use codec::Codec; +use node_primitives::{Balance, RpcContributionStatus}; +use sp_api::decl_runtime_apis; + +decl_runtime_apis! { + pub trait SalpRuntimeApi where + ParaId: Codec, + AccountId: Codec, + { + fn get_contribution( + index: ParaId, + who: AccountId + ) -> (Balance,RpcContributionStatus); + } +} diff --git a/pallets/salp-lite/rpc/src/lib.rs b/pallets/salp-lite/rpc/src/lib.rs new file mode 100644 index 0000000000..1f88cd373f --- /dev/null +++ b/pallets/salp-lite/rpc/src/lib.rs @@ -0,0 +1,86 @@ +// This file is part of Bifrost. + +// Copyright (C) 2019-2021 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 std::{marker::PhantomData, sync::Arc}; + +pub use bifrost_salp_rpc_runtime_api::{self as runtime_api, SalpRuntimeApi}; +use codec::Codec; +use jsonrpc_core::{Error as RpcError, ErrorCode, Result as JsonRpcResult}; +use jsonrpc_derive::rpc; +use node_primitives::RpcContributionStatus; +use sp_api::ProvideRuntimeApi; +use sp_blockchain::HeaderBackend; +use sp_rpc::number::NumberOrHex; +use sp_runtime::{generic::BlockId, traits::Block as BlockT, SaturatedConversion}; + +pub use self::gen_client::Client as SalpClient; + +#[derive(Clone, Debug)] +pub struct SalpRpcWrapper { + client: Arc, + _marker: PhantomData, +} + +impl SalpRpcWrapper { + pub fn new(client: Arc) -> Self { + Self { client, _marker: PhantomData } + } +} + +#[rpc] +pub trait SalpRpcApi { + /// rpc method for getting current contribution + #[rpc(name = "salp_getContribution")] + fn get_contribution( + &self, + index: ParaId, + who: AccountId, + at: Option, + ) -> JsonRpcResult<(NumberOrHex, RpcContributionStatus)>; +} + +impl SalpRpcApi<::Hash, ParaId, AccountId> + for SalpRpcWrapper +where + Block: BlockT, + C: Send + Sync + 'static + ProvideRuntimeApi + HeaderBackend, + C::Api: SalpRuntimeApi, + ParaId: Codec, + AccountId: Codec, +{ + fn get_contribution( + &self, + index: ParaId, + account: AccountId, + at: Option<::Hash>, + ) -> JsonRpcResult<(NumberOrHex, RpcContributionStatus)> { + let salp_rpc_api = self.client.runtime_api(); + let at = BlockId::::hash(at.unwrap_or_else(|| self.client.info().best_hash)); + + let rs = salp_rpc_api.get_contribution(&at, index, account); + + match rs { + Ok((val, status)) => Ok((NumberOrHex::Number(val.saturated_into()), status)), + Err(e) => Err(RpcError { + code: ErrorCode::InternalError, + message: "Failed to get salp contribution.".to_owned(), + data: Some(format!("{:?}", e).into()), + }), + } + } +} diff --git a/pallets/salp-lite/src/benchmarking.rs b/pallets/salp-lite/src/benchmarking.rs new file mode 100644 index 0000000000..214b335ae4 --- /dev/null +++ b/pallets/salp-lite/src/benchmarking.rs @@ -0,0 +1,136 @@ +// This file is part of Bifrost. + +// Copyright (C) 2019-2021 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(feature = "runtime-benchmarks")] +use frame_benchmarking::{account, benchmarks, impl_benchmark_test_suite, whitelisted_caller}; +use frame_support::assert_ok; +use frame_system::RawOrigin; +use node_primitives::ParaId; +use sp_runtime::{traits::Bounded, SaturatedConversion}; +use sp_std::prelude::*; + +pub use crate::{Pallet as Salp, *}; + +fn assert_last_event(generic_event: ::Event) { + let events = frame_system::Pallet::::events(); + let system_event: ::Event = generic_event.into(); + // compare to the last event record + let frame_system::EventRecord { event, .. } = &events[events.len() - 1]; + assert_eq!(event, &system_event); +} + +fn create_fund(id: u32) -> ParaId { + let cap = BalanceOf::::max_value(); + let first_period = (0 as u32).into(); + let last_period = (7 as u32).into(); + let para_id = id; + + assert_ok!(Salp::::create(RawOrigin::Root.into(), para_id, cap, first_period, last_period)); + + para_id +} + +#[allow(dead_code)] +fn contribute_fund(who: &T::AccountId, index: ParaId) { + let value = T::MinContribution::get(); + assert_ok!(Salp::::contribute(RawOrigin::Signed(who.clone()).into(), index, value)); +} + +benchmarks! { + contribute { + let fund_index = create_fund::(1); + let caller: T::AccountId = whitelisted_caller(); + let contribution = T::MinContribution::get(); + assert_ok!(Salp::::set_balance(&caller, contribution)); + }: _(RawOrigin::Signed(caller.clone()), fund_index, contribution) + verify { + let fund = Salp::::funds(fund_index).unwrap(); + let (_, status) = Salp::::contribution(fund.trie_index, &caller); + assert_eq!(status, ContributionStatus::Contributing(contribution)); + } + + refund { + let fund_index = create_fund::(1); + let caller: T::AccountId = whitelisted_caller(); + let caller_origin: T::Origin = RawOrigin::Signed(caller.clone()).into(); + let contribution = T::MinContribution::get(); + contribute_fund::(&caller,fund_index); + assert_ok!(Salp::::fund_fail(RawOrigin::Root.into(), fund_index)); + assert_ok!(Salp::::withdraw(RawOrigin::Root.into(), fund_index)); + let fund = Salp::::funds(fund_index).unwrap(); + let (_, status) = Salp::::contribution(fund.trie_index, &caller); + assert_eq!(status, ContributionStatus::Idle); + }: _(RawOrigin::Signed(caller.clone()), fund_index) + verify { + let (_, status) = Salp::::contribution(fund.trie_index, &caller); + assert_eq!(status, ContributionStatus::Refunded); + assert_last_event::(Event::::Refunded(caller.clone(), fund_index, contribution).into()) + } + + unlock { + let fund_index = create_fund::(1); + let caller: T::AccountId = whitelisted_caller(); + let caller_origin: T::Origin = RawOrigin::Signed(caller.clone()).into(); + let contribution = T::MinContribution::get(); + contribute_fund::(&caller,fund_index); + assert_ok!(Salp::::fund_success(RawOrigin::Root.into(), fund_index)); + }: _(RawOrigin::Root, caller.clone(),fund_index) + verify { + let fund = Salp::::funds(fund_index).unwrap(); + let (_, status) = Salp::::contribution(fund.trie_index, &caller); + assert_eq!(status, ContributionStatus::Unlocked); + } + + batch_unlock { + let k in 1 .. T::BatchKeysLimit::get(); + let fund_index = create_fund::(1); + let contribution = T::MinContribution::get(); + let mut caller: T::AccountId = whitelisted_caller(); + for i in 0 .. k { + caller = account("contributor", i, 0); + contribute_fund::(&caller,fund_index); + } + assert_ok!(Salp::::fund_success(RawOrigin::Root.into(), fund_index)); + }: _(RawOrigin::Signed(caller.clone()), fund_index) + verify { + let fund = Salp::::funds(fund_index).unwrap(); + let (_, status) = Salp::::contribution(fund.trie_index, &caller); + assert_eq!(status, ContributionStatus::Unlocked); + assert_last_event::(Event::::AllUnlocked(fund_index).into()); + } + + redeem { + let fund_index = create_fund::(1); + let caller: T::AccountId = whitelisted_caller(); + let caller_origin: T::Origin = RawOrigin::Signed(caller.clone()).into(); + let contribution = T::MinContribution::get(); + contribute_fund::(&caller,fund_index); + assert_ok!(Salp::::fund_success(RawOrigin::Root.into(), fund_index)); + assert_ok!(Salp::::unlock(RawOrigin::Root.into(), caller.clone(), fund_index)); + assert_ok!(Salp::::fund_retire(RawOrigin::Root.into(), fund_index)); + assert_ok!(Salp::::withdraw(RawOrigin::Root.into(), fund_index)); + assert_eq!(Salp::::redeem_pool(), T::MinContribution::get()); + }: _(RawOrigin::Signed(caller.clone()), fund_index,contribution) + verify { + assert_eq!(Salp::::redeem_pool(), 0_u32.saturated_into()); + assert_last_event::(Event::::Redeemed(caller.clone(), fund_index, (0 as u32).into(),(7 as u32).into(),contribution).into()) + } +} + +impl_benchmark_test_suite!(Salp, crate::mock::new_test_ext(), crate::mock::Test); diff --git a/pallets/salp-lite/src/lib.rs b/pallets/salp-lite/src/lib.rs new file mode 100644 index 0000000000..3b9de56e87 --- /dev/null +++ b/pallets/salp-lite/src/lib.rs @@ -0,0 +1,925 @@ +// This file is part of Bifrost. + +// Copyright (C) 2019-2021 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)] + +pub mod migration { + pub fn migrate() { + log::info!("salp migration..."); + } +} + +#[cfg(feature = "runtime-benchmarks")] +pub mod benchmarking; +#[cfg(test)] +pub mod mock; +#[cfg(test)] +mod tests; + +// Re-export pallet items so that they can be accessed from the crate namespace. +use frame_support::{pallet_prelude::*, transactional}; +use node_primitives::{ContributionStatus, TokenInfo, TokenSymbol, TrieIndex}; +use orml_traits::MultiCurrency; +pub use pallet::*; +use scale_info::TypeInfo; +use sp_std::convert::TryFrom; + +#[allow(type_alias_bounds)] +pub type AccountIdOf = ::AccountId; + +#[allow(type_alias_bounds)] +type BalanceOf = + <::MultiCurrency as MultiCurrency>>::Balance; + +#[derive(Encode, Decode, Clone, PartialEq, Eq, RuntimeDebug, TypeInfo)] +pub enum FundStatus { + Ongoing, + Retired, + Success, + Failed, + RefundWithdrew, + RedeemWithdrew, + End, +} + +impl Default for FundStatus { + fn default() -> Self { + FundStatus::Ongoing + } +} + +/// Information on a funding effort for a pre-existing parachain. We assume that the parachain +/// ID is known as it's used for the key of the storage item for which this is the value +/// (`Funds`). +#[derive(Encode, Decode, Clone, PartialEq, Eq, RuntimeDebug, TypeInfo)] +#[codec(dumb_trait_bound)] +pub struct FundInfo { + /// The total amount raised. + raised: Balance, + /// A hard-cap on the amount that may be contributed. + cap: Balance, + /// First slot in range to bid on; it's actually a LeasePeriod, but that's the same type as + /// BlockNumber. + first_slot: LeasePeriod, + /// Last slot in range to bid on; it's actually a LeasePeriod, but that's the same type as + /// BlockNumber. + last_slot: LeasePeriod, + /// Index used for the child trie of this fund + trie_index: TrieIndex, + /// Fund status + status: FundStatus, +} + +#[frame_support::pallet] +pub mod pallet { + // Import various types used to declare pallet in scope. + + use frame_support::{ + pallet_prelude::{storage::child, *}, + sp_runtime::traits::{AccountIdConversion, CheckedAdd, Hash, Saturating, Zero}, + sp_std::convert::TryInto, + storage::ChildTriePrefixIterator, + PalletId, + }; + use frame_system::pallet_prelude::*; + use node_primitives::{BancorHandler, CurrencyId, LeasePeriod, ParaId}; + use orml_traits::{currency::TransferAll, MultiCurrency, MultiReservableCurrency}; + use sp_arithmetic::Percent; + use sp_std::prelude::*; + + use super::*; + + #[pallet::config] + pub trait Config: frame_system::Config + TypeInfo { + type Event: From> + IsType<::Event>; + + /// ModuleID for the crowdloan module. An appropriate value could be + /// ```ModuleId(*b"py/cfund")``` + #[pallet::constant] + type PalletId: Get; + + /// The minimum amount that may be contributed into a crowdloan. Should almost certainly be + /// at least ExistentialDeposit. + #[pallet::constant] + type MinContribution: Get>; + + #[pallet::constant] + type RelayChainToken: Get; + + /// The number of blocks over which a single period lasts. + #[pallet::constant] + type LeasePeriod: Get>; + + /// The time interval from 1:1 redeem-pool to bancor-pool to release. + #[pallet::constant] + type ReleaseCycle: Get>; + + /// The release ratio from the 1:1 redeem-pool to the bancor-pool per cycle. + #[pallet::constant] + type ReleaseRatio: Get; + + #[pallet::constant] + type BatchKeysLimit: Get; + + #[pallet::constant] + type SlotLength: Get; + + type MultiCurrency: TransferAll> + + MultiCurrency, CurrencyId = CurrencyId> + + MultiReservableCurrency, CurrencyId = CurrencyId>; + + type BancorPool: BancorHandler>; + + type EnsureConfirmAsMultiSig: EnsureOrigin<::Origin>; + + type EnsureConfirmAsGovernance: EnsureOrigin<::Origin>; + + /// Weight information for the extrinsics in this module. + type WeightInfo: WeightInfo; + } + + #[pallet::pallet] + #[pallet::generate_store(pub(super) trait Store)] + pub struct Pallet(_); + + #[pallet::event] + #[pallet::generate_deposit(pub(super) fn deposit_event)] + pub enum Event { + /// Create a new crowdloaning campaign. [fund_index] + Created(ParaId), + /// Contributed to a crowd sale. [who, fund_index, amount] + Contributed(AccountIdOf, ParaId, BalanceOf), + /// Withdrew full balance of a contributor. [who, fund_index, amount] + Withdrew(ParaId, BalanceOf), + /// refund to account. [who, fund_index,value] + Refunded(AccountIdOf, ParaId, BalanceOf), + /// redeem to account. [who, fund_index, first_slot, last_slot, value] + Redeemed(AccountIdOf, ParaId, LeasePeriod, LeasePeriod, BalanceOf), + /// Fund is edited. [fund_index] + Edited(ParaId), + /// Fund is dissolved. [fund_index] + Dissolved(ParaId), + /// The vsToken/vsBond was be unlocked. [who, fund_index, value] + Unlocked(AccountIdOf, ParaId, BalanceOf), + AllUnlocked(ParaId), + /// Fund status change + Failed(ParaId), + Success(ParaId), + Retired(ParaId), + End(ParaId), + /// vsAssets migrated + AllMigrated(ParaId), + } + + #[pallet::error] + pub enum Error { + /// The first slot needs to at least be less than 3 `max_value`. + FirstSlotTooFarInFuture, + /// Last slot must be greater than first slot. + LastSlotBeforeFirstSlot, + /// The last slot cannot be more then 3 slots after the first slot. + LastSlotTooFarInFuture, + /// Migrate slot must be greater than first slot + MigrateSlotBeforeFirstSlot, + /// There was an overflow. + Overflow, + /// The contribution was below the minimum, `MinContribution`. + ContributionTooSmall, + /// The account doesn't have any contribution to the fund. + ZeroContribution, + /// Invalid fund index. + InvalidParaId, + /// Invalid fund status. + InvalidFundStatus, + /// Invalid contribution status. + InvalidContributionStatus, + /// Contributions exceed maximum amount. + CapExceeded, + /// The fund has been registered. + FundAlreadyCreated, + /// Don't have enough vsToken/vsBond to refund + NotEnoughReservedAssetsToRefund, + /// Don't have enough token to refund by users + NotEnoughBalanceInRefundPool, + /// Don't have enough vsToken/vsBond to unlock + NotEnoughBalanceToUnlock, + /// Dont have enough vsToken/vsBond to redeem + NotEnoughFreeAssetsToRedeem, + /// Don't have enough token to redeem by users + NotEnoughBalanceInRedeemPool, + } + + /// Tracker for the next available fund index + #[pallet::storage] + #[pallet::getter(fn current_trie_index)] + pub(super) type CurrentTrieIndex = StorageValue<_, TrieIndex, ValueQuery>; + + /// Info on all of the funds. + #[pallet::storage] + #[pallet::getter(fn funds)] + pub(super) type Funds = StorageMap< + _, + Blake2_128Concat, + ParaId, + Option, LeasePeriod>>, + ValueQuery, + >; + + /// The balance can be redeemed to users. + #[pallet::storage] + #[pallet::getter(fn redeem_pool)] + pub(super) type RedeemPool = StorageValue<_, BalanceOf, ValueQuery>; + + #[pallet::call] + impl Pallet { + #[pallet::weight(( + 0, + DispatchClass::Normal, + Pays::No + ))] + pub fn fund_success( + origin: OriginFor, + #[pallet::compact] index: ParaId, + ) -> DispatchResult { + T::EnsureConfirmAsGovernance::ensure_origin(origin)?; + + let fund = Self::funds(index).ok_or(Error::::InvalidParaId)?; + ensure!(fund.status == FundStatus::Ongoing, Error::::InvalidFundStatus); + + let fund_new = FundInfo { status: FundStatus::Success, ..fund }; + Funds::::insert(index, Some(fund_new)); + Self::deposit_event(Event::::Success(index)); + + Ok(()) + } + + #[pallet::weight(( + 0, + DispatchClass::Normal, + Pays::No + ))] + pub fn fund_fail(origin: OriginFor, #[pallet::compact] index: ParaId) -> DispatchResult { + T::EnsureConfirmAsGovernance::ensure_origin(origin)?; + + // crownload is failed, so enable the withdrawal function of vsToken/vsBond + let fund = Self::funds(index).ok_or(Error::::InvalidParaId)?; + ensure!(fund.status == FundStatus::Ongoing, Error::::InvalidFundStatus); + + let fund_new = FundInfo { status: FundStatus::Failed, ..fund }; + Funds::::insert(index, Some(fund_new)); + Self::deposit_event(Event::::Failed(index)); + + Ok(()) + } + + #[pallet::weight(( + 0, + DispatchClass::Normal, + Pays::No + ))] + pub fn fund_retire( + origin: OriginFor, + #[pallet::compact] index: ParaId, + ) -> DispatchResult { + T::EnsureConfirmAsGovernance::ensure_origin(origin)?; + + let fund = Self::funds(index).ok_or(Error::::InvalidParaId)?; + ensure!(fund.status == FundStatus::Success, Error::::InvalidFundStatus); + + let fund_new = FundInfo { status: FundStatus::Retired, ..fund }; + Funds::::insert(index, Some(fund_new)); + Self::deposit_event(Event::::Retired(index)); + + Ok(()) + } + + #[pallet::weight(( + 0, + DispatchClass::Normal, + Pays::No + ))] + pub fn fund_end(origin: OriginFor, #[pallet::compact] index: ParaId) -> DispatchResult { + T::EnsureConfirmAsGovernance::ensure_origin(origin)?; + + let fund = Self::funds(index).ok_or(Error::::InvalidParaId)?; + ensure!( + fund.status == FundStatus::RefundWithdrew || + fund.status == FundStatus::RedeemWithdrew, + Error::::InvalidFundStatus + ); + + let fund_new = FundInfo { status: FundStatus::End, ..fund }; + Funds::::insert(index, Some(fund_new)); + Self::deposit_event(Event::::End(index)); + + Ok(()) + } + + /// Edit the configuration for an in-progress crowdloan. + /// + /// Can only be called by Root origin. + #[pallet::weight(( + 0, + DispatchClass::Normal, + Pays::No + ))] + pub fn edit( + origin: OriginFor, + #[pallet::compact] index: ParaId, + #[pallet::compact] cap: BalanceOf, + #[pallet::compact] first_slot: LeasePeriod, + #[pallet::compact] last_slot: LeasePeriod, + fund_status: Option, + ) -> DispatchResult { + T::EnsureConfirmAsGovernance::ensure_origin(origin)?; + + let fund = Self::funds(index).ok_or(Error::::InvalidParaId)?; + + let status = match fund_status { + None => fund.status, + Some(status) => status, + }; + + Funds::::insert( + index, + Some(FundInfo { + cap, + first_slot, + last_slot, + status, + raised: fund.raised, + trie_index: fund.trie_index, + }), + ); + + Self::deposit_event(Event::::Edited(index)); + Ok(()) + } + + /// Unlock the reserved vsToken/vsBond after fund success + #[pallet::weight(T::WeightInfo::batch_unlock(T::BatchKeysLimit::get()))] + #[transactional] + pub fn batch_migrate( + origin: OriginFor, + #[pallet::compact] index: ParaId, + first_slot: LeasePeriod, + last_slot: LeasePeriod, + ) -> DispatchResult { + ensure_signed(origin)?; + + let fund = Self::funds(index).ok_or(Error::::InvalidParaId)?; + ensure!( + fund.status == FundStatus::Failed || fund.status == FundStatus::RefundWithdrew, + Error::::InvalidFundStatus + ); + + ensure!(first_slot > fund.first_slot, Error::::MigrateSlotBeforeFirstSlot); + + let mut migrate_count = 0u32; + let contributions = Self::contribution_iterator(fund.trie_index); + let mut all_migrated = true; + + for (who, (contributed, status)) in contributions { + if migrate_count >= T::BatchKeysLimit::get() { + // Not everyone was able to be refunded this time around. + all_migrated = false; + break; + } + if status != ContributionStatus::MigratedIdle { + #[allow(non_snake_case)] + let (_, vsBond) = Self::vsAssets(index, fund.first_slot, fund.last_slot); + + let balance = T::MultiCurrency::slash_reserved(vsBond, &who, contributed); + ensure!(balance == Zero::zero(), Error::::NotEnoughReservedAssetsToRefund); + + let (_, vs_bond_new) = Self::vsAssets(index, first_slot, last_slot); + T::MultiCurrency::deposit(vs_bond_new, &who, contributed)?; + T::MultiCurrency::reserve(vs_bond_new, &who, contributed)?; + + Self::put_contribution( + fund.trie_index, + &who, + contributed, + ContributionStatus::MigratedIdle, + ); + migrate_count += 1; + } + } + + if all_migrated { + Self::deposit_event(Event::::AllMigrated(index)); + } + + Ok(()) + } + + /// Unlock the reserved vsToken/vsBond after fund success + #[pallet::weight(T::WeightInfo::unlock())] + #[transactional] + pub fn unlock( + _origin: OriginFor, + who: AccountIdOf, + #[pallet::compact] index: ParaId, + ) -> DispatchResult { + let fund = Self::funds(index).ok_or(Error::::InvalidParaId)?; + ensure!( + fund.status == FundStatus::Success || + fund.status == FundStatus::Retired || + fund.status == FundStatus::RedeemWithdrew || + fund.status == FundStatus::End, + Error::::InvalidFundStatus + ); + + let (contributed, status) = Self::contribution(fund.trie_index, &who); + ensure!(status == ContributionStatus::Idle, Error::::InvalidContributionStatus); + + #[allow(non_snake_case)] + let (vsToken, vsBond) = Self::vsAssets(index, fund.first_slot, fund.last_slot); + + let balance = T::MultiCurrency::unreserve(vsToken, &who, contributed); + ensure!(balance == Zero::zero(), Error::::NotEnoughBalanceToUnlock); + let balance = T::MultiCurrency::unreserve(vsBond, &who, contributed); + ensure!(balance == Zero::zero(), Error::::NotEnoughBalanceToUnlock); + + Self::put_contribution( + fund.trie_index, + &who, + contributed, + ContributionStatus::Unlocked, + ); + + Self::deposit_event(Event::::Unlocked(who, index, contributed)); + + Ok(()) + } + + /// Unlock the reserved vsToken/vsBond after fund success + #[pallet::weight(T::WeightInfo::batch_unlock(T::BatchKeysLimit::get()))] + #[transactional] + pub fn batch_unlock( + origin: OriginFor, + #[pallet::compact] index: ParaId, + ) -> DispatchResult { + ensure_signed(origin)?; + + let fund = Self::funds(index).ok_or(Error::::InvalidParaId)?; + ensure!( + fund.status == FundStatus::Success || + fund.status == FundStatus::Retired || + fund.status == FundStatus::RedeemWithdrew || + fund.status == FundStatus::End, + Error::::InvalidFundStatus + ); + + let mut unlock_count = 0u32; + let contributions = Self::contribution_iterator(fund.trie_index); + // Assume everyone will be refunded. + let mut all_unlocked = true; + + for (who, (contributed, status)) in contributions { + if unlock_count >= T::BatchKeysLimit::get() { + // Not everyone was able to be refunded this time around. + all_unlocked = false; + break; + } + if status != ContributionStatus::Unlocked { + #[allow(non_snake_case)] + let (vsToken, vsBond) = Self::vsAssets(index, fund.first_slot, fund.last_slot); + let balance = T::MultiCurrency::unreserve(vsToken, &who, contributed); + ensure!(balance == Zero::zero(), Error::::NotEnoughBalanceToUnlock); + let balance = T::MultiCurrency::unreserve(vsBond, &who, contributed); + ensure!(balance == Zero::zero(), Error::::NotEnoughBalanceToUnlock); + + Self::put_contribution( + fund.trie_index, + &who, + contributed, + ContributionStatus::Unlocked, + ); + unlock_count += 1; + } + } + + if all_unlocked { + Self::deposit_event(Event::::AllUnlocked(index)); + } + + Ok(()) + } + + /// Create a new crowdloaning campaign for a parachain slot deposit for the current auction. + #[pallet::weight(( + 0, + DispatchClass::Normal, + Pays::No + ))] + pub fn create( + origin: OriginFor, + #[pallet::compact] index: ParaId, + #[pallet::compact] cap: BalanceOf, + #[pallet::compact] first_slot: LeasePeriod, + #[pallet::compact] last_slot: LeasePeriod, + ) -> DispatchResult { + T::EnsureConfirmAsGovernance::ensure_origin(origin)?; + + ensure!(!Funds::::contains_key(index), Error::::FundAlreadyCreated); + + ensure!(first_slot <= last_slot, Error::::LastSlotBeforeFirstSlot); + + let last_slot_limit = first_slot + .checked_add(((T::SlotLength::get() as u32) - 1).into()) + .ok_or(Error::::FirstSlotTooFarInFuture)?; + ensure!(last_slot <= last_slot_limit, Error::::LastSlotTooFarInFuture); + + Funds::::insert( + index, + Some(FundInfo { + raised: Zero::zero(), + cap, + first_slot, + last_slot, + trie_index: Self::next_trie_index()?, + status: FundStatus::Ongoing, + }), + ); + + Self::deposit_event(Event::::Created(index)); + + Ok(()) + } + + /// Contribute to a crowd sale. This will transfer some balance over to fund a parachain + /// slot. It will be withdrawable in two instances: the parachain becomes retired; or the + /// slot is unable to be purchased and the timeout expires. + #[pallet::weight(( + 0, + DispatchClass::Normal, + Pays::No + ))] + #[transactional] + pub fn issue( + origin: OriginFor, + who: AccountIdOf, + #[pallet::compact] index: ParaId, + #[pallet::compact] value: BalanceOf, + ) -> DispatchResult { + T::EnsureConfirmAsMultiSig::ensure_origin(origin.clone())?; + + let fund = Self::funds(index).ok_or(Error::::InvalidParaId)?; + ensure!(fund.status == FundStatus::Ongoing, Error::::InvalidFundStatus); + + ensure!(value >= T::MinContribution::get(), Error::::ContributionTooSmall); + + let raised = fund.raised.checked_add(&value).ok_or(Error::::Overflow)?; + ensure!(raised <= fund.cap, Error::::CapExceeded); + + let (contributed, status) = Self::contribution(fund.trie_index, &who); + ensure!( + status == ContributionStatus::Idle || status == ContributionStatus::MigratedIdle, + Error::::InvalidContributionStatus + ); + + let (vs_token, vs_bond) = Self::vsAssets(index, fund.first_slot, fund.last_slot); + + // Issue reserved vsToken/vsBond to contributor + T::MultiCurrency::deposit(vs_token, &who, value)?; + T::MultiCurrency::reserve(vs_token, &who, value)?; + T::MultiCurrency::deposit(vs_bond, &who, value)?; + T::MultiCurrency::reserve(vs_bond, &who, value)?; + + // Update the raised of fund + let fund_new = FundInfo { raised: fund.raised.saturating_add(value), ..fund }; + Funds::::insert(index, Some(fund_new)); + + // Update the contribution of who + let contributed_new = contributed.saturating_add(value); + Self::put_contribution( + fund.trie_index, + &who, + contributed_new, + ContributionStatus::Idle, + ); + Self::deposit_event(Event::Contributed(who, index, value)); + + Ok(()) + } + + /// Withdraw full balance of the parachain. + /// - `index`: The parachain to whose crowdloan the contribution was made. + #[pallet::weight(( + 0, + DispatchClass::Normal, + Pays::No + ))] + #[transactional] + pub fn withdraw(origin: OriginFor, #[pallet::compact] index: ParaId) -> DispatchResult { + T::EnsureConfirmAsGovernance::ensure_origin(origin.clone())?; + + let fund = Self::funds(index).ok_or(Error::::InvalidParaId)?; + let can = fund.status == FundStatus::Failed || fund.status == FundStatus::Retired; + ensure!(can, Error::::InvalidFundStatus); + + let amount_withdrew = fund.raised; + + if fund.status == FundStatus::Retired { + let fund_new = FundInfo { status: FundStatus::RedeemWithdrew, ..fund }; + Funds::::insert(index, Some(fund_new)); + RedeemPool::::set(Self::redeem_pool().saturating_add(amount_withdrew)); + } else if fund.status == FundStatus::Failed { + let fund_new = FundInfo { status: FundStatus::RefundWithdrew, ..fund }; + Funds::::insert(index, Some(fund_new)); + } + + Self::deposit_event(Event::Withdrew(index, amount_withdrew)); + + Ok(()) + } + + #[pallet::weight(T::WeightInfo::refund())] + #[transactional] + pub fn refund(origin: OriginFor, #[pallet::compact] index: ParaId) -> DispatchResult { + let who = ensure_signed(origin.clone())?; + + let mut fund = Self::funds(index).ok_or(Error::::InvalidParaId)?; + ensure!( + fund.status == FundStatus::Failed || fund.status == FundStatus::RefundWithdrew, + Error::::InvalidFundStatus + ); + + let (contributed, status) = Self::contribution(fund.trie_index, &who); + ensure!(contributed > Zero::zero(), Error::::ZeroContribution); + ensure!(status == ContributionStatus::Idle, Error::::InvalidContributionStatus); + + ensure!(fund.raised >= contributed, Error::::NotEnoughBalanceInRefundPool); + + #[allow(non_snake_case)] + let (vsToken, vsBond) = Self::vsAssets(index, fund.first_slot, fund.last_slot); + ensure!( + T::MultiCurrency::reserved_balance(vsToken, &who) >= contributed, + Error::::NotEnoughReservedAssetsToRefund + ); + ensure!( + T::MultiCurrency::reserved_balance(vsBond, &who) >= contributed, + Error::::NotEnoughReservedAssetsToRefund + ); + + fund.raised = fund.raised.saturating_sub(contributed); + + let balance = T::MultiCurrency::slash_reserved(vsToken, &who, contributed); + ensure!(balance == Zero::zero(), Error::::NotEnoughReservedAssetsToRefund); + let balance = T::MultiCurrency::slash_reserved(vsBond, &who, contributed); + ensure!(balance == Zero::zero(), Error::::NotEnoughReservedAssetsToRefund); + + Self::kill_contribution(fund.trie_index, &who); + + Self::deposit_event(Event::Refunded(who, index, contributed)); + + Ok(()) + } + + #[pallet::weight(T::WeightInfo::redeem())] + #[transactional] + pub fn redeem( + origin: OriginFor, + #[pallet::compact] index: ParaId, + #[pallet::compact] value: BalanceOf, + ) -> DispatchResult { + let who = ensure_signed(origin.clone())?; + + let mut fund = Self::funds(index).ok_or(Error::::InvalidParaId)?; + ensure!(fund.status == FundStatus::RedeemWithdrew, Error::::InvalidFundStatus); + ensure!(fund.raised >= value, Error::::NotEnoughBalanceInRedeemPool); + + let (contributed, status) = Self::contribution(fund.trie_index, &who); + ensure!( + status == ContributionStatus::Idle || + status == ContributionStatus::Unlocked || + status == ContributionStatus::Redeemed, + Error::::InvalidContributionStatus + ); + + ensure!(Self::redeem_pool() >= value, Error::::NotEnoughBalanceInRedeemPool); + + #[allow(non_snake_case)] + let (vsToken, vsBond) = Self::vsAssets(index, fund.first_slot, fund.last_slot); + + T::MultiCurrency::ensure_can_withdraw(vsToken, &who, value) + .map_err(|_e| Error::::NotEnoughFreeAssetsToRedeem)?; + T::MultiCurrency::ensure_can_withdraw(vsBond, &who, value) + .map_err(|_e| Error::::NotEnoughFreeAssetsToRedeem)?; + T::MultiCurrency::withdraw(vsToken, &who, value)?; + T::MultiCurrency::withdraw(vsBond, &who, value)?; + + fund.raised = fund.raised.saturating_sub(value); + RedeemPool::::set(Self::redeem_pool().saturating_sub(value)); + + let contributed_new = contributed.saturating_sub(value); + Self::put_contribution( + fund.trie_index, + &who, + contributed_new, + ContributionStatus::Redeemed, + ); + Self::deposit_event(Event::Redeemed( + who, + index, + fund.first_slot, + fund.last_slot, + value, + )); + + Ok(()) + } + + /// Remove a fund after the retirement period has ended and all funds have been returned. + #[pallet::weight(( + 0, + DispatchClass::Normal, + Pays::No + ))] + #[transactional] + pub fn dissolve(origin: OriginFor, #[pallet::compact] index: ParaId) -> DispatchResult { + T::EnsureConfirmAsGovernance::ensure_origin(origin)?; + + let mut fund = Self::funds(index).ok_or(Error::::InvalidParaId)?; + ensure!(fund.status == FundStatus::End, Error::::InvalidFundStatus); + + let mut refund_count = 0u32; + // Try killing the crowdloan child trie and Assume everyone will be refunded. + let contributions = Self::contribution_iterator(fund.trie_index); + let mut all_refunded = true; + for (who, (balance, _)) in contributions { + if refund_count >= T::BatchKeysLimit::get() { + // Not everyone was able to be refunded this time around. + all_refunded = false; + break; + } + Self::kill_contribution(fund.trie_index, &who); + fund.raised = fund.raised.saturating_sub(balance); + refund_count += 1; + } + + if all_refunded == true { + Funds::::remove(index); + Self::deposit_event(Event::::Dissolved(index)); + } + + Ok(()) + } + } + + #[pallet::hooks] + impl Hooks> for Pallet { + fn on_initialize(n: BlockNumberFor) -> Weight { + // Release x% KSM/DOT from redeem-pool to bancor-pool per cycle + if n != 0 && (n % T::ReleaseCycle::get()) == 0 { + if let Ok(rp_balance) = TryInto::::try_into(Self::redeem_pool()) { + // Calculate the release amount + let release_amount = T::ReleaseRatio::get() * rp_balance; + + // Must be ok + if let Ok(release_amount) = TryInto::>::try_into(release_amount) { + // Increase the balance of bancor-pool by release-amount + if let Ok(()) = + T::BancorPool::add_token(T::RelayChainToken::get(), release_amount) + { + RedeemPool::::set( + Self::redeem_pool().saturating_sub(release_amount), + ); + } + } else { + log::warn!("Overflow: The balance of redeem-pool exceeds u128."); + } + } + } + T::DbWeight::get().reads(1) + } + } + + impl Pallet { + pub fn fund_account_id(index: ParaId) -> T::AccountId { + T::PalletId::get().into_sub_account(index) + } + + pub(crate) fn id_from_index(index: TrieIndex) -> child::ChildInfo { + let mut buf = Vec::new(); + buf.extend_from_slice(&(T::PalletId::get().0)); + buf.extend_from_slice(&index.encode()[..]); + child::ChildInfo::new_default(T::Hashing::hash(&buf[..]).as_ref()) + } + + pub(crate) fn contribution( + index: TrieIndex, + who: &AccountIdOf, + ) -> (BalanceOf, ContributionStatus>) { + who.using_encoded(|b| { + child::get_or_default::<(BalanceOf, ContributionStatus>)>( + &Self::id_from_index(index), + b, + ) + }) + } + + pub fn contribution_by_fund( + index: ParaId, + who: &AccountIdOf, + ) -> Result<(BalanceOf, ContributionStatus>), Error> { + let fund = Self::funds(index).ok_or(Error::::InvalidParaId)?; + let (contributed, status) = Self::contribution(fund.trie_index, who); + Ok((contributed, status)) + } + + pub(crate) fn contribution_iterator( + index: TrieIndex, + ) -> ChildTriePrefixIterator<( + AccountIdOf, + (BalanceOf, ContributionStatus>), + )> { + ChildTriePrefixIterator::<_>::with_prefix_over_key::( + &Self::id_from_index(index), + &[], + ) + } + + pub(crate) fn next_trie_index() -> Result> { + CurrentTrieIndex::::try_mutate(|ti| { + *ti = ti.checked_add(1).ok_or(Error::::Overflow)?; + Ok(*ti - 1) + }) + } + + #[allow(non_snake_case)] + pub(crate) fn vsAssets( + index: ParaId, + first_slot: LeasePeriod, + last_slot: LeasePeriod, + ) -> (CurrencyId, CurrencyId) { + let currency_id_u64: u64 = T::RelayChainToken::get().currency_id(); + let tokensymbo_bit = (currency_id_u64 & 0x0000_0000_0000_00ff) as u8; + let token_symbol = TokenSymbol::try_from(tokensymbo_bit).unwrap_or(TokenSymbol::DOT); + CurrencyId::vsAssets(token_symbol, index, first_slot, last_slot) + } + + fn put_contribution( + index: TrieIndex, + who: &AccountIdOf, + contributed: BalanceOf, + status: ContributionStatus>, + ) { + who.using_encoded(|b| { + child::put(&Self::id_from_index(index), b, &(contributed, status)) + }); + } + + fn kill_contribution(index: TrieIndex, who: &AccountIdOf) { + who.using_encoded(|b| child::kill(&Self::id_from_index(index), b)); + } + } +} + +pub trait WeightInfo { + fn contribute() -> Weight; + fn unlock() -> Weight; + fn batch_unlock(k: u32) -> Weight; + fn refund() -> Weight; + fn redeem() -> Weight; +} + +// For backwards compatibility and tests +impl WeightInfo for () { + fn contribute() -> Weight { + 50_000_000 as Weight + } + + fn unlock() -> Weight { + 50_000_000 as Weight + } + + fn batch_unlock(_k: u32) -> Weight { + 50_000_000 as Weight + } + + fn refund() -> Weight { + 50_000_000 as Weight + } + + fn redeem() -> Weight { + 50_000_000 as Weight + } +} diff --git a/pallets/salp-lite/src/mock.rs b/pallets/salp-lite/src/mock.rs new file mode 100644 index 0000000000..59fcac7f49 --- /dev/null +++ b/pallets/salp-lite/src/mock.rs @@ -0,0 +1,300 @@ +// This file is part of Bifrost. + +// Copyright (C) 2019-2021 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. +use frame_support::{ + construct_runtime, parameter_types, + traits::{EnsureOrigin, GenesisBuild}, + weights::{WeightToFeeCoefficient, WeightToFeeCoefficients, WeightToFeePolynomial}, + PalletId, +}; +use frame_system::RawOrigin; +use node_primitives::{Amount, Balance, CurrencyId, TokenSymbol}; +use sp_arithmetic::Percent; +use sp_core::H256; +use sp_runtime::{ + generic, + traits::{BlakeTwo256, IdentityLookup}, +}; + +use crate as salp; +use crate::WeightInfo; + +pub(crate) type AccountId = <::Signer as sp_runtime::traits::IdentifyAccount>::AccountId; +pub(crate) type Block = frame_system::mocking::MockBlock; +pub(crate) type BlockNumber = u32; +pub(crate) type Index = u32; +pub(crate) type Signature = sp_runtime::MultiSignature; +pub(crate) type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic; + +construct_runtime!( + pub enum Test 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}, + Bancor: bifrost_bancor::{Pallet, Call, Config, Storage, Event}, + Multisig: pallet_multisig::{Pallet, Call, Storage, Event}, + Salp: salp::{Pallet, Call, Storage, Event}, + } +); + +parameter_types! { + pub const NativeCurrencyId: CurrencyId = CurrencyId::Native(TokenSymbol::BNC); + pub const RelayCurrencyId: CurrencyId = CurrencyId::Token(TokenSymbol::DOT); + pub const StableCurrencyId: CurrencyId = CurrencyId::Stable(TokenSymbol::KUSD); +} + +parameter_types! { + pub const BlockHashCount: BlockNumber = 250; + pub BlockWeights: frame_system::limits::BlockWeights = + frame_system::limits::BlockWeights::simple_max(1024); +} + +impl frame_system::Config for Test { + type AccountData = pallet_balances::AccountData; + type AccountId = AccountId; + type BaseCallFilter = frame_support::traits::Everything; + type BlockHashCount = BlockHashCount; + type BlockLength = (); + type BlockNumber = BlockNumber; + type BlockWeights = (); + type Call = Call; + type DbWeight = (); + type Event = Event; + type Hash = H256; + type Hashing = BlakeTwo256; + type Header = generic::Header; + type Index = Index; + type Lookup = IdentityLookup; + type OnKilledAccount = (); + type OnNewAccount = (); + type OnSetCode = (); + type Origin = Origin; + type PalletInfo = PalletInfo; + type SS58Prefix = (); + type SystemWeightInfo = (); + type Version = (); +} + +parameter_types! { + pub const ExistentialDeposit: u128 = 0; + pub const TransferFee: u128 = 0; + pub const CreationFee: u128 = 0; + pub const TransactionByteFee: u128 = 0; + pub const MaxLocks: u32 = 999_999; + pub const MaxReserves: u32 = 999_999; +} + +impl pallet_balances::Config for Test { + 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; +} + +parameter_types! { + pub const DepositBase: Balance = 0; + pub const DepositFactor: Balance = 0; + pub const MaxSignatories: u16 = 100; +} + +impl pallet_multisig::Config for Test { + type Call = Call; + type Currency = Balances; + type DepositBase = DepositBase; + type DepositFactor = DepositFactor; + type Event = Event; + type MaxSignatories = MaxSignatories; + type WeightInfo = pallet_multisig::weights::SubstrateWeight; +} + +orml_traits::parameter_type_with_key! { + pub ExistentialDeposits: |_currency_id: CurrencyId| -> Balance { + 0 + }; +} + +impl orml_tokens::Config for Test { + 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 Test { + type Event = Event; + type GetNativeCurrencyId = NativeCurrencyId; + type MultiCurrency = Tokens; + type NativeCurrency = BifrostToken; + type WeightInfo = (); +} + +parameter_types! { + pub const InterventionPercentage: Percent = Percent::from_percent(75); + pub const DailyReleasePercentage: Percent = Percent::from_percent(5); +} + +impl bifrost_bancor::Config for Test { + type Event = Event; + type InterventionPercentage = InterventionPercentage; + type DailyReleasePercentage = DailyReleasePercentage; + type MultiCurrency = Tokens; + type WeightInfo = (); +} + +parameter_types! { + pub const MinContribution: Balance = 10; + pub const BifrostCrowdloanId: PalletId = PalletId(*b"bf/salp#"); + pub const RemoveKeysLimit: u32 = 50; + pub const SlotLength: BlockNumber = 8u32 as BlockNumber; + pub const LeasePeriod: BlockNumber = 6 * WEEKS; + pub const VSBondValidPeriod: BlockNumber = 30 * DAYS; + pub const ReleaseCycle: BlockNumber = 1 * DAYS; + pub const ReleaseRatio: Percent = Percent::from_percent(50); + pub PrimaryAccount: AccountId = ALICE; + pub ConfirmMuitiSigAccount: AccountId = Multisig::multi_account_id(&vec![ + ALICE, + BRUCE, + CATHI + ],2); +} + +pub struct EnsureConfirmAsMultiSig; +impl EnsureOrigin for EnsureConfirmAsMultiSig { + type Success = AccountId; + + fn try_origin(o: Origin) -> Result { + Into::, Origin>>::into(o).and_then(|o| match o { + RawOrigin::Signed(who) => Ok(who), + RawOrigin::Root => Ok(Default::default()), + r => Err(Origin::from(r)), + }) + } + + #[cfg(feature = "runtime-benchmarks")] + fn successful_origin() -> Origin { + Origin::from(RawOrigin::Signed(ConfirmMuitiSigAccount::get())) + } +} + +use frame_support::{traits::Nothing, weights::Weight}; +use smallvec::smallvec; +pub use sp_runtime::Perbill; + +pub struct WeightToFee; +impl WeightToFeePolynomial for WeightToFee { + type Balance = Balance; + fn polynomial() -> WeightToFeeCoefficients { + smallvec![WeightToFeeCoefficient { + degree: 1, + negative: false, + coeff_frac: Perbill::from_rational(90u32, 100u32), + coeff_integer: 1, + }] + } +} + +impl salp::Config for Test { + type BancorPool = Bancor; + type Event = Event; + type LeasePeriod = LeasePeriod; + type MinContribution = MinContribution; + type MultiCurrency = Tokens; + type PalletId = BifrostCrowdloanId; + type RelayChainToken = RelayCurrencyId; + type ReleaseCycle = ReleaseCycle; + type ReleaseRatio = ReleaseRatio; + type BatchKeysLimit = RemoveKeysLimit; + type SlotLength = SlotLength; + type WeightInfo = SalpWeightInfo; + type EnsureConfirmAsMultiSig = EnsureConfirmAsMultiSig; + type EnsureConfirmAsGovernance = EnsureConfirmAsMultiSig; +} + +pub struct SalpWeightInfo; +impl WeightInfo for SalpWeightInfo { + fn contribute() -> Weight { + 0 + } + + fn unlock() -> Weight { + 0 + } + + fn redeem() -> Weight { + 0 + } + + fn refund() -> Weight { + 0 + } + + fn batch_unlock(_k: u32) -> Weight { + 0 + } +} + +pub(crate) fn new_test_ext() -> sp_io::TestExternalities { + let mut t = frame_system::GenesisConfig::default().build_storage::().unwrap(); + + orml_tokens::GenesisConfig:: { + balances: vec![ + (ALICE, NativeCurrencyId::get(), INIT_BALANCE), + (ALICE, RelayCurrencyId::get(), INIT_BALANCE), + (BRUCE, NativeCurrencyId::get(), INIT_BALANCE), + (BRUCE, RelayCurrencyId::get(), INIT_BALANCE), + (CATHI, NativeCurrencyId::get(), INIT_BALANCE), + (CATHI, RelayCurrencyId::get(), INIT_BALANCE), + ], + } + .assimilate_storage(&mut t) + .unwrap(); + + t.into() +} + +// These time units are defined in number of blocks. +pub const MINUTES: BlockNumber = 60 / (12 as BlockNumber); +pub const HOURS: BlockNumber = MINUTES * 60; +pub const DAYS: BlockNumber = HOURS * 24; +pub const WEEKS: BlockNumber = DAYS * 7; + +pub(crate) const ALICE: AccountId = AccountId::new([0u8; 32]); +pub(crate) const BRUCE: AccountId = AccountId::new([1u8; 32]); +pub(crate) const CATHI: AccountId = AccountId::new([2u8; 32]); + +pub(crate) const INIT_BALANCE: Balance = 100_000; diff --git a/pallets/salp-lite/src/tests.rs b/pallets/salp-lite/src/tests.rs new file mode 100644 index 0000000000..302997e57f --- /dev/null +++ b/pallets/salp-lite/src/tests.rs @@ -0,0 +1,984 @@ +// This file is part of Bifrost. + +// Copyright (C) 2019-2021 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. + +use frame_support::{assert_noop, assert_ok, dispatch::DispatchError, traits::BalanceStatus as BS}; +use node_primitives::ContributionStatus; +use orml_traits::{MultiCurrency, MultiReservableCurrency}; + +use crate::{mock::*, Error, FundStatus}; + +#[test] +fn create_fund_should_work() { + new_test_ext().execute_with(|| { + assert_ok!(Salp::create(Some(ALICE).into(), 3_000, 1_000, 1, SlotLength::get())); + assert_ok!(Salp::funds(3_000).ok_or(())); + assert_eq!(Salp::current_trie_index(), 1); + }); +} + +#[test] +fn create_fund_with_wrong_origin_should_fail() { + new_test_ext().execute_with(|| { + assert_noop!( + Salp::create(Origin::none(), 3_000, 1_000, 1, SlotLength::get()), + DispatchError::BadOrigin, + ); + }); +} + +#[test] +fn create_fund_existed_should_fail() { + new_test_ext().execute_with(|| { + assert_ok!(Salp::create(Some(ALICE).into(), 3_000, 1_000, 1, SlotLength::get())); + + assert_noop!( + Salp::create(Some(ALICE).into(), 3_000, 1_000, 1, SlotLength::get()), + Error::::FundAlreadyCreated, + ); + }); +} + +#[test] +fn create_fund_exceed_slot_limit_should_fail() { + new_test_ext().execute_with(|| { + assert_noop!( + Salp::create(Some(ALICE).into(), 3_000, 1_000, 0, SlotLength::get()), + Error::::LastSlotTooFarInFuture, + ); + }); +} + +#[test] +fn create_fund_first_slot_bigger_than_last_slot_should_fail() { + new_test_ext().execute_with(|| { + assert_noop!( + Salp::create(Some(ALICE).into(), 3_000, 1_000, SlotLength::get(), 0), + Error::::LastSlotBeforeFirstSlot, + ); + }); +} + +#[test] +fn set_fund_success_should_work() { + new_test_ext().execute_with(|| { + assert_ok!(Salp::create(Some(ALICE).into(), 3_000, 1_000, 1, SlotLength::get())); + assert_ok!(Salp::fund_success(Some(ALICE).into(), 3_000)); + + // Check status + let fund = Salp::funds(3_000).unwrap(); + assert_eq!(fund.status, FundStatus::Success); + }); +} + +#[test] +fn set_fund_success_with_wrong_origin_should_fail() { + new_test_ext().execute_with(|| { + assert_ok!(Salp::create(Some(ALICE).into(), 3_000, 1_000, 1, SlotLength::get())); + assert_noop!(Salp::fund_success(Origin::none(), 3_000), DispatchError::BadOrigin); + }) +} + +#[test] +fn set_fund_success_with_wrong_para_id_should_fail() { + new_test_ext().execute_with(|| { + assert_ok!(Salp::create(Some(ALICE).into(), 3_000, 1_000, 1, SlotLength::get())); + assert_noop!(Salp::fund_success(Some(ALICE).into(), 4_000), Error::::InvalidParaId); + }); +} + +#[test] +fn set_fund_success_with_wrong_fund_status_should_fail() { + new_test_ext().execute_with(|| { + assert_ok!(Salp::create(Some(ALICE).into(), 3_000, 1_000, 1, SlotLength::get())); + assert_ok!(Salp::fund_fail(Some(ALICE).into(), 3_000)); + assert_noop!( + Salp::fund_success(Some(ALICE).into(), 3_000), + Error::::InvalidFundStatus + ); + }); +} + +#[test] +fn set_fund_fail_should_work() { + new_test_ext().execute_with(|| { + assert_ok!(Salp::create(Some(ALICE).into(), 3_000, 1_000, 1, SlotLength::get())); + assert_ok!(Salp::fund_fail(Some(ALICE).into(), 3_000)); + + // Check status + let fund = Salp::funds(3_000).unwrap(); + assert_eq!(fund.status, FundStatus::Failed); + }); +} + +#[test] +fn set_fund_fail_with_wrong_origin_should_fail() { + new_test_ext().execute_with(|| { + assert_ok!(Salp::create(Some(ALICE).into(), 3_000, 1_000, 1, SlotLength::get())); + assert_noop!(Salp::fund_fail(Origin::none(), 3_000), DispatchError::BadOrigin); + }); +} + +#[test] +fn set_fund_fail_with_wrong_para_id_should_fail() { + new_test_ext().execute_with(|| { + assert_ok!(Salp::create(Some(ALICE).into(), 3_000, 1_000, 1, SlotLength::get())); + assert_noop!(Salp::fund_fail(Some(ALICE).into(), 4_000), Error::::InvalidParaId); + }); +} + +#[test] +fn set_fund_fail_with_wrong_fund_status_should_fail() { + new_test_ext().execute_with(|| { + assert_ok!(Salp::create(Some(ALICE).into(), 3_000, 1_000, 1, SlotLength::get())); + assert_ok!(Salp::fund_success(Some(ALICE).into(), 3_000)); + assert_noop!(Salp::fund_fail(Some(ALICE).into(), 3_000), Error::::InvalidFundStatus); + }); +} + +#[test] +fn set_fund_retire_should_work() { + new_test_ext().execute_with(|| { + assert_ok!(Salp::create(Some(ALICE).into(), 3_000, 1_000, 1, SlotLength::get())); + assert_ok!(Salp::fund_success(Some(ALICE).into(), 3_000)); + assert_ok!(Salp::fund_retire(Some(ALICE).into(), 3_000)); + + // Check status + let fund = Salp::funds(3_000).unwrap(); + assert_eq!(fund.status, FundStatus::Retired); + }); +} + +#[test] +fn set_fund_retire_with_wrong_origin_should_fail() { + new_test_ext().execute_with(|| { + assert_ok!(Salp::create(Some(ALICE).into(), 3_000, 1_000, 1, SlotLength::get())); + assert_ok!(Salp::fund_success(Some(ALICE).into(), 3_000)); + assert_noop!(Salp::fund_retire(Origin::none(), 3_000), DispatchError::BadOrigin); + }); +} + +#[test] +fn set_fund_retire_with_wrong_para_id_should_fail() { + new_test_ext().execute_with(|| { + assert_ok!(Salp::create(Some(ALICE).into(), 3_000, 1_000, 1, SlotLength::get())); + assert_ok!(Salp::fund_success(Some(ALICE).into(), 3_000)); + assert_noop!(Salp::fund_retire(Some(ALICE).into(), 4_000), Error::::InvalidParaId); + }); +} + +#[test] +fn set_fund_retire_with_with_wrong_fund_status_should_fail() { + new_test_ext().execute_with(|| { + assert_ok!(Salp::create(Some(ALICE).into(), 3_000, 1_000, 1, SlotLength::get())); + assert_noop!( + Salp::fund_retire(Some(ALICE).into(), 3_000), + Error::::InvalidFundStatus + ); + }); +} + +#[test] +fn set_fund_end_should_work() { + new_test_ext().execute_with(|| { + assert_ok!(Salp::create(Some(ALICE).into(), 3_000, 1_000, 1, SlotLength::get())); + assert_ok!(Salp::fund_success(Some(ALICE).into(), 3_000)); + assert_ok!(Salp::fund_retire(Some(ALICE).into(), 3_000)); + assert_ok!(Salp::withdraw(Some(ALICE).into(), 3_000)); + assert_ok!(Salp::fund_end(Some(ALICE).into(), 3_000)); + + // Check storage + let fund = Salp::funds(3_000).unwrap(); + assert_eq!(fund.status, FundStatus::End); + }); +} + +#[test] +fn set_fund_end_with_wrong_origin_should_fail() { + new_test_ext().execute_with(|| { + assert_ok!(Salp::create(Some(ALICE).into(), 3_000, 1_000, 1, SlotLength::get())); + assert_ok!(Salp::fund_success(Some(ALICE).into(), 3_000)); + assert_ok!(Salp::fund_retire(Some(ALICE).into(), 3_000)); + assert_ok!(Salp::withdraw(Some(ALICE).into(), 3_000)); + + assert_noop!(Salp::fund_end(Origin::none(), 3_000), DispatchError::BadOrigin); + }); +} + +#[test] +fn set_fund_end_with_wrong_wrong_para_id_should_fail() { + new_test_ext().execute_with(|| { + assert_ok!(Salp::create(Some(ALICE).into(), 3_000, 1_000, 1, SlotLength::get())); + assert_ok!(Salp::fund_success(Some(ALICE).into(), 3_000)); + assert_ok!(Salp::fund_retire(Some(ALICE).into(), 3_000)); + assert_ok!(Salp::withdraw(Some(ALICE).into(), 3_000)); + + assert_noop!(Salp::fund_end(Some(ALICE).into(), 4_000), Error::::InvalidParaId); + }); +} + +#[test] +fn set_fund_end_with_wrong_fund_status_should_fail() { + new_test_ext().execute_with(|| { + assert_ok!(Salp::create(Some(ALICE).into(), 3_000, 1_000, 1, SlotLength::get())); + assert_ok!(Salp::fund_success(Some(ALICE).into(), 3_000)); + + assert_noop!(Salp::fund_end(Some(ALICE).into(), 3_000), Error::::InvalidFundStatus); + }); +} + +#[test] +fn unlock_should_work() { + new_test_ext().execute_with(|| { + assert_ok!(Salp::create(Some(ALICE).into(), 3_000, 1_000, 1, SlotLength::get())); + assert_ok!(Salp::issue(Some(ALICE).into(), BRUCE, 3_000, 100)); + assert_ok!(Salp::fund_success(Some(ALICE).into(), 3_000)); + assert_ok!(Salp::unlock(Some(BRUCE).into(), BRUCE, 3_000)); + + #[allow(non_snake_case)] + let (vsToken, vsBond) = Salp::vsAssets(3_000, 1, SlotLength::get()); + + assert_eq!(Tokens::accounts(BRUCE, vsToken).free, 100); + assert_eq!(Tokens::accounts(BRUCE, vsToken).frozen, 0); + assert_eq!(Tokens::accounts(BRUCE, vsToken).reserved, 0); + assert_eq!(Tokens::accounts(BRUCE, vsBond).free, 100); + assert_eq!(Tokens::accounts(BRUCE, vsBond).frozen, 0); + assert_eq!(Tokens::accounts(BRUCE, vsBond).reserved, 0); + }); +} + +#[test] +fn unlock_with_wrong_fund_status_should_fail() { + new_test_ext().execute_with(|| { + assert_ok!(Salp::create(Some(ALICE).into(), 3_000, 1_000, 1, SlotLength::get())); + assert_ok!(Salp::issue(Some(ALICE).into(), BRUCE, 3_000, 100)); + assert_ok!(Salp::fund_fail(Some(ALICE).into(), 3_000)); + + assert_noop!( + Salp::unlock(Some(BRUCE).into(), BRUCE, 3_000), + Error::::InvalidFundStatus + ); + }); +} + +#[test] +fn unlock_when_already_unlocked_should_fail() { + new_test_ext().execute_with(|| { + assert_ok!(Salp::create(Some(ALICE).into(), 3_000, 1_000, 1, SlotLength::get())); + assert_ok!(Salp::issue(Some(ALICE).into(), BRUCE, 3_000, 100)); + assert_ok!(Salp::fund_success(Some(ALICE).into(), 3_000)); + assert_ok!(Salp::unlock(Some(BRUCE).into(), BRUCE, 3_000)); + + assert_noop!( + Salp::unlock(Some(BRUCE).into(), BRUCE, 3_000), + Error::::InvalidContributionStatus + ); + }); +} + +#[test] +fn unlock_without_enough_reserved_vsassets_should_fail() { + new_test_ext().execute_with(|| { + assert_ok!(Salp::create(Some(ALICE).into(), 3_000, 1_000, 1, SlotLength::get())); + assert_ok!(Salp::issue(Some(ALICE).into(), BRUCE, 3_000, 100)); + assert_ok!(Salp::fund_success(Some(ALICE).into(), 3_000)); + + // ABSOLUTELY NOT HAPPEN AT NORMAL PROCESS! + #[allow(non_snake_case)] + let (vsToken, vsBond) = Salp::vsAssets(3_000, 1, SlotLength::get()); + Tokens::slash_reserved(vsToken, &BRUCE, 50); + Tokens::slash_reserved(vsBond, &BRUCE, 50); + + // ``` + // // The following code will produce a supernatural bug. + // // DONT ASK WHY, I DONT KNOW! + // assert_noop!( + // Salp::unlock(Some(BRUCE).into(), BRUCE, 3_000), + // Error::::NotEnoughBalanceToUnlock + // ); + // ``` + let result = Salp::unlock(Some(BRUCE).into(), BRUCE, 3_000); + assert_noop!(result, Error::::NotEnoughBalanceToUnlock); + }); +} + +#[test] +fn contribute_should_work() { + new_test_ext().execute_with(|| { + assert_ok!(Salp::create(Some(ALICE).into(), 3_000, 1_000, 1, SlotLength::get())); + assert_ok!(Salp::issue(Some(ALICE).into(), BRUCE, 3_000, 100)); + + let fund = Salp::funds(3_000).unwrap(); + let (contributed, status) = Salp::contribution(fund.trie_index, &BRUCE); + assert_eq!(fund.raised, 100); + assert_eq!(contributed, 100); + assert_eq!(status, ContributionStatus::Idle); + + #[allow(non_snake_case)] + let (vsToken, vsBond) = Salp::vsAssets(3_000, 1, SlotLength::get()); + assert_eq!(Tokens::accounts(BRUCE, vsToken).free, 0); + assert_eq!(Tokens::accounts(BRUCE, vsToken).frozen, 0); + assert_eq!(Tokens::accounts(BRUCE, vsToken).reserved, 100); + assert_eq!(Tokens::accounts(BRUCE, vsBond).free, 0); + assert_eq!(Tokens::accounts(BRUCE, vsBond).frozen, 0); + assert_eq!(Tokens::accounts(BRUCE, vsBond).reserved, 100); + }); +} + +#[test] +fn double_contribute_should_work() { + new_test_ext().execute_with(|| { + assert_ok!(Salp::create(Some(ALICE).into(), 3_000, 1_000, 1, SlotLength::get())); + assert_ok!(Salp::issue(Some(ALICE).into(), BRUCE, 3_000, 100)); + assert_ok!(Salp::issue(Some(ALICE).into(), BRUCE, 3_000, 100)); + + // Check the contribution + let fund = Salp::funds(3_000).unwrap(); + let (contributed, status) = Salp::contribution(fund.trie_index, &BRUCE); + assert_eq!(fund.raised, 200); + assert_eq!(contributed, 200); + assert_eq!(status, ContributionStatus::Idle); + + #[allow(non_snake_case)] + let (vsToken, vsBond) = Salp::vsAssets(3_000, 1, SlotLength::get()); + assert_eq!(Tokens::accounts(BRUCE, vsToken).free, 0); + assert_eq!(Tokens::accounts(BRUCE, vsToken).frozen, 0); + assert_eq!(Tokens::accounts(BRUCE, vsToken).reserved, 200); + assert_eq!(Tokens::accounts(BRUCE, vsBond).free, 0); + assert_eq!(Tokens::accounts(BRUCE, vsBond).frozen, 0); + assert_eq!(Tokens::accounts(BRUCE, vsBond).reserved, 200); + }); +} + +#[test] +fn confirm_contribute_later_should_work() { + new_test_ext().execute_with(|| { + assert_ok!(Salp::create(Some(ALICE).into(), 3_000, 1_000, 1, SlotLength::get())); + assert_ok!(Salp::issue(Some(ALICE).into(), BRUCE, 3_000, 100)); + assert_ok!(Salp::fund_success(Some(ALICE).into(), 3_000)); + + let fund = Salp::funds(3_000).unwrap(); + let (contributed, status) = Salp::contribution(fund.trie_index, &BRUCE); + assert_eq!(fund.raised, 100); + assert_eq!(contributed, 100); + assert_eq!(status, ContributionStatus::Idle); + + #[allow(non_snake_case)] + let (vsToken, vsBond) = Salp::vsAssets(3_000, 1, SlotLength::get()); + assert_eq!(Tokens::accounts(BRUCE, vsToken).free, 0); + assert_eq!(Tokens::accounts(BRUCE, vsToken).frozen, 0); + assert_eq!(Tokens::accounts(BRUCE, vsToken).reserved, 100); + assert_eq!(Tokens::accounts(BRUCE, vsBond).free, 0); + assert_eq!(Tokens::accounts(BRUCE, vsBond).frozen, 0); + assert_eq!(Tokens::accounts(BRUCE, vsBond).reserved, 100); + }); +} + +#[test] +fn contribute_with_low_contribution_should_fail() { + new_test_ext().execute_with(|| { + assert_ok!(Salp::create(Some(ALICE).into(), 3_000, 1_000, 1, SlotLength::get())); + assert_noop!( + Salp::issue(Some(ALICE).into(), BRUCE, 3_000, MinContribution::get() - 1), + Error::::ContributionTooSmall + ); + }); +} + +#[test] +fn contribute_with_wrong_para_id_should_fail() { + new_test_ext().execute_with(|| { + assert_ok!(Salp::create(Some(ALICE).into(), 3_000, 1_000, 1, SlotLength::get())); + assert_noop!( + Salp::issue(Some(ALICE).into(), BRUCE, 4_000, 100), + Error::::InvalidParaId + ); + }); +} + +#[test] +fn contribute_with_wrong_fund_status_should_fail() { + new_test_ext().execute_with(|| { + assert_ok!(Salp::create(Some(ALICE).into(), 3_000, 1_000, 1, SlotLength::get())); + assert_ok!(Salp::fund_success(Some(ALICE).into(), 3_000,)); + assert_noop!( + Salp::issue(Some(ALICE).into(), BRUCE, 3_000, 100), + Error::::InvalidFundStatus + ); + }); +} + +#[test] +fn contribute_exceed_cap_should_fail() { + new_test_ext().execute_with(|| { + assert_ok!(Salp::create(Some(ALICE).into(), 3_000, 1_000, 1, SlotLength::get())); + assert_noop!( + Salp::issue(Some(ALICE).into(), BRUCE, 3_000, 1_001), + Error::::CapExceeded + ); + }); +} + +#[test] +fn contribute_with_when_ump_wrong_should_fail() { + // TODO: Require an solution to settle with parallel test workflow +} + +#[test] +fn withdraw_should_work() { + new_test_ext().execute_with(|| { + assert_ok!(Salp::create(Some(ALICE).into(), 3_000, 1_000, 1, SlotLength::get())); + assert_ok!(Salp::issue(Some(ALICE).into(), BRUCE, 3_000, 100)); + assert_ok!(Salp::fund_success(Some(ALICE).into(), 3_000)); + assert_ok!(Salp::fund_retire(Some(ALICE).into(), 3_000)); + assert_ok!(Salp::withdraw(Some(ALICE).into(), 3_000)); + + let fund = Salp::funds(3_000).unwrap(); + assert_eq!(fund.status, FundStatus::RedeemWithdrew); + + assert_ok!(Salp::create(Some(ALICE).into(), 4_000, 1_000, 1, SlotLength::get())); + assert_ok!(Salp::issue(Some(ALICE).into(), BRUCE, 4_000, 100)); + assert_ok!(Salp::fund_fail(Some(ALICE).into(), 4_000)); + assert_ok!(Salp::withdraw(Some(ALICE).into(), 4_000)); + + let fund = Salp::funds(4_000).unwrap(); + assert_eq!(fund.status, FundStatus::RefundWithdrew); + }); +} + +#[test] +fn double_withdraw_same_fund_should_fail() { + new_test_ext().execute_with(|| { + assert_ok!(Salp::create(Some(ALICE).into(), 3_000, 1_000, 1, SlotLength::get())); + assert_ok!(Salp::issue(Some(ALICE).into(), BRUCE, 3_000, 100)); + assert_ok!(Salp::fund_success(Some(ALICE).into(), 3_000)); + assert_ok!(Salp::fund_retire(Some(ALICE).into(), 3_000)); + assert_ok!(Salp::withdraw(Some(ALICE).into(), 3_000)); + assert_noop!(Salp::withdraw(Some(ALICE).into(), 3_000), Error::::InvalidFundStatus); + + let fund = Salp::funds(3_000).unwrap(); + assert_eq!(fund.status, FundStatus::RedeemWithdrew); + + assert_ok!(Salp::create(Some(ALICE).into(), 4_000, 1_000, 1, SlotLength::get())); + assert_ok!(Salp::issue(Some(ALICE).into(), BRUCE, 4_000, 100)); + assert_ok!(Salp::fund_fail(Some(ALICE).into(), 4_000)); + assert_ok!(Salp::withdraw(Some(ALICE).into(), 4_000)); + assert_noop!(Salp::withdraw(Some(ALICE).into(), 4_000), Error::::InvalidFundStatus); + + let fund = Salp::funds(4_000).unwrap(); + assert_eq!(fund.status, FundStatus::RefundWithdrew); + }); +} + +#[test] +fn withdraw_with_wrong_origin_should_fail() { + new_test_ext().execute_with(|| { + assert_ok!(Salp::create(Some(ALICE).into(), 3_000, 1_000, 1, SlotLength::get())); + assert_ok!(Salp::fund_success(Some(ALICE).into(), 3_000)); + assert_ok!(Salp::fund_retire(Some(ALICE).into(), 3_000)); + + assert_noop!(Salp::withdraw(Origin::none(), 3_000), DispatchError::BadOrigin); + }); +} + +#[test] +fn withdraw_with_wrong_para_id_should_fail() { + new_test_ext().execute_with(|| { + assert_ok!(Salp::create(Some(ALICE).into(), 3_000, 1_000, 1, SlotLength::get())); + assert_ok!(Salp::fund_success(Some(ALICE).into(), 3_000)); + assert_ok!(Salp::fund_retire(Some(ALICE).into(), 3_000)); + + assert_noop!(Salp::withdraw(Some(ALICE).into(), 4_000), Error::::InvalidParaId); + }); +} + +#[test] +fn withdraw_with_wrong_fund_status_should_fail() { + new_test_ext().execute_with(|| { + assert_ok!(Salp::create(Some(ALICE).into(), 3_000, 1_000, 1, SlotLength::get())); + assert_ok!(Salp::fund_success(Some(ALICE).into(), 3_000)); + + assert_noop!(Salp::withdraw(Some(ALICE).into(), 3_000), Error::::InvalidFundStatus); + }); +} + +#[test] +fn withdraw_with_when_ump_wrong_should_fail() { + // TODO: Require an solution to settle with parallel test workflow +} + +#[test] +fn refund_should_work() { + new_test_ext().execute_with(|| { + assert_ok!(Salp::create(Some(ALICE).into(), 3_000, 1_000, 1, SlotLength::get())); + assert_ok!(Salp::issue(Some(ALICE).into(), BRUCE, 3_000, 100)); + assert_ok!(Salp::fund_fail(Some(ALICE).into(), 3_000)); + assert_ok!(Salp::withdraw(Some(ALICE).into(), 3_000)); + assert_ok!(Salp::refund(Some(BRUCE).into(), 3_000)); + + let fund = Salp::funds(3_000).unwrap(); + let (contributed, status) = Salp::contribution(fund.trie_index, &BRUCE); + assert_eq!(contributed, 0); + assert_eq!(status, ContributionStatus::Idle); + + #[allow(non_snake_case)] + let (vsToken, vsBond) = Salp::vsAssets(3_000, 1, SlotLength::get()); + assert_eq!(Tokens::accounts(BRUCE, vsToken).free, 0); + assert_eq!(Tokens::accounts(BRUCE, vsToken).frozen, 0); + assert_eq!(Tokens::accounts(BRUCE, vsToken).reserved, 0); + assert_eq!(Tokens::accounts(BRUCE, vsBond).free, 0); + assert_eq!(Tokens::accounts(BRUCE, vsBond).frozen, 0); + assert_eq!(Tokens::accounts(BRUCE, vsBond).reserved, 0); + }); +} + +#[test] +fn refund_without_enough_vsassets_should_fail() { + new_test_ext().execute_with(|| { + assert_ok!(Salp::create(Some(ALICE).into(), 3_000, 1_000, 1, SlotLength::get())); + assert_ok!(Salp::issue(Some(ALICE).into(), BRUCE, 3_000, 100)); + assert_ok!(Salp::fund_fail(Some(ALICE).into(), 3_000)); + assert_ok!(Salp::withdraw(Some(ALICE).into(), 3_000)); + + #[allow(non_snake_case)] + let (vsToken, vsBond) = Salp::vsAssets(3_000, 1, SlotLength::get()); + assert_ok!(Tokens::repatriate_reserved(vsToken, &BRUCE, &ALICE, 50, BS::Reserved)); + assert_ok!(Tokens::repatriate_reserved(vsBond, &BRUCE, &ALICE, 50, BS::Reserved)); + + assert_noop!( + Salp::refund(Some(BRUCE).into(), 3_000), + Error::::NotEnoughReservedAssetsToRefund + ); + }); +} + +#[test] +fn refund_with_zero_contribution_should_fail() { + new_test_ext().execute_with(|| { + assert_ok!(Salp::create(Some(ALICE).into(), 3_000, 1_000, 1, SlotLength::get())); + assert_ok!(Salp::fund_fail(Some(ALICE).into(), 3_000)); + assert_ok!(Salp::withdraw(Some(ALICE).into(), 3_000)); + + assert_noop!(Salp::refund(Some(BRUCE).into(), 3_000), Error::::ZeroContribution); + }); +} + +#[test] +fn refund_with_wrong_origin_should_fail() { + new_test_ext().execute_with(|| { + assert_ok!(Salp::create(Some(ALICE).into(), 3_000, 1_000, 1, SlotLength::get())); + assert_ok!(Salp::issue(Some(ALICE).into(), BRUCE, 3_000, 100)); + assert_ok!(Salp::fund_fail(Some(ALICE).into(), 3_000)); + assert_ok!(Salp::withdraw(Some(ALICE).into(), 3_000)); + + assert_noop!(Salp::refund(Origin::root(), 3_000), DispatchError::BadOrigin); + assert_noop!(Salp::refund(Origin::none(), 3_000), DispatchError::BadOrigin); + + assert_ok!(Salp::refund(Some(BRUCE).into(), 3_000)); + }); +} + +#[test] +fn refund_with_wrong_para_id_should_fail() { + new_test_ext().execute_with(|| { + assert_ok!(Salp::create(Some(ALICE).into(), 3_000, 1_000, 1, SlotLength::get())); + assert_ok!(Salp::fund_fail(Some(ALICE).into(), 3_000)); + assert_ok!(Salp::withdraw(Some(ALICE).into(), 3_000)); + + assert_noop!(Salp::refund(Some(BRUCE).into(), 4_000), Error::::InvalidParaId); + }); +} + +#[test] +fn refund_with_wrong_fund_status_should_fail() { + new_test_ext().execute_with(|| { + assert_ok!(Salp::create(Some(ALICE).into(), 3_000, 1_000, 1, SlotLength::get())); + assert_ok!(Salp::fund_fail(Some(ALICE).into(), 3_000)); + assert_ok!(Salp::withdraw(Some(ALICE).into(), 3_000)); + assert_ok!(Salp::fund_end(Some(ALICE).into(), 3_000)); + + assert_noop!(Salp::refund(Some(BRUCE).into(), 3_000), Error::::InvalidFundStatus); + + assert_ok!(Salp::create(Some(ALICE).into(), 4_000, 1_000, 1, SlotLength::get())); + assert_ok!(Salp::fund_success(Some(ALICE).into(), 4_000)); + assert_ok!(Salp::fund_retire(Some(ALICE).into(), 4_000)); + assert_ok!(Salp::withdraw(Some(ALICE).into(), 4_000)); + + assert_noop!(Salp::refund(Some(BRUCE).into(), 4_000), Error::::InvalidFundStatus); + }); +} + +#[test] +fn refund_with_when_ump_wrong_should_fail() { + // TODO: Require an solution to settle with parallel test workflow +} + +#[test] +fn dissolve_should_work() { + new_test_ext().execute_with(|| { + let remove_times = 4; + let contribute_account_num = remove_times * RemoveKeysLimit::get(); + + assert_ok!(Salp::create(Some(ALICE).into(), 3_000, 10_000, 1, SlotLength::get())); + for i in 0..contribute_account_num { + let ract = AccountId::new([(i as u8); 32]); + assert_ok!(Tokens::deposit(RelayCurrencyId::get(), &ract, 10)); + assert_ok!(Salp::issue(Some(ALICE).into(), ract.clone(), 3_000, 10)); + } + assert_ok!(Salp::fund_success(Some(ALICE).into(), 3_000)); + assert_ok!(Salp::fund_retire(Some(ALICE).into(), 3_000)); + assert_ok!(Salp::withdraw(Some(ALICE).into(), 3_000)); + assert_ok!(Salp::fund_end(Some(ALICE).into(), 3_000)); + + for _ in 0..remove_times { + assert_ok!(Salp::dissolve(Some(ALICE).into(), 3_000)); + } + + assert!(Salp::funds(3_000).is_none()); + assert!(Salp::contribution_iterator(0).next().is_none()); + }); +} + +#[test] +fn dissolve_with_wrong_origin_should_fail() { + new_test_ext().execute_with(|| { + assert_ok!(Salp::create(Some(ALICE).into(), 3_000, 1_000, 1, SlotLength::get())); + assert_ok!(Salp::fund_success(Some(ALICE).into(), 3_000)); + assert_ok!(Salp::fund_retire(Some(ALICE).into(), 3_000)); + assert_ok!(Salp::withdraw(Some(ALICE).into(), 3_000)); + assert_ok!(Salp::fund_end(Some(ALICE).into(), 3_000)); + + assert_noop!(Salp::dissolve(Origin::none(), 3_000), DispatchError::BadOrigin); + }); +} + +#[test] +fn dissolve_with_wrong_para_id_should_fail() { + new_test_ext().execute_with(|| { + assert_ok!(Salp::create(Some(ALICE).into(), 3_000, 1_000, 1, SlotLength::get())); + assert_ok!(Salp::fund_success(Some(ALICE).into(), 3_000)); + assert_ok!(Salp::fund_retire(Some(ALICE).into(), 3_000)); + assert_ok!(Salp::withdraw(Some(ALICE).into(), 3_000)); + assert_ok!(Salp::fund_end(Some(ALICE).into(), 3_000)); + + assert_noop!(Salp::dissolve(Some(ALICE).into(), 4_000), Error::::InvalidParaId); + }); +} + +#[test] +fn dissolve_with_wrong_fund_status_should_fail() { + new_test_ext().execute_with(|| { + assert_ok!(Salp::create(Some(ALICE).into(), 3_000, 1_000, 1, SlotLength::get())); + assert_ok!(Salp::fund_success(Some(ALICE).into(), 3_000)); + assert_ok!(Salp::fund_retire(Some(ALICE).into(), 3_000)); + assert_ok!(Salp::withdraw(Some(ALICE).into(), 3_000)); + + assert_noop!(Salp::dissolve(Some(ALICE).into(), 3_000), Error::::InvalidFundStatus); + }); +} + +#[test] +fn redeem_should_work() { + new_test_ext().execute_with(|| { + assert_ok!(Salp::create(Some(ALICE).into(), 3_000, 1_000, 1, SlotLength::get())); + assert_ok!(Salp::issue(Some(ALICE).into(), BRUCE, 3_000, 100)); + + assert_ok!(Salp::fund_success(Some(ALICE).into(), 3_000)); + assert_ok!(Salp::unlock(Some(BRUCE).into(), BRUCE, 3_000)); + + // Mock the BlockNumber + let block_begin_redeem = (SlotLength::get() + 1) * LeasePeriod::get(); + System::set_block_number(block_begin_redeem); + + assert_ok!(Salp::fund_retire(Some(ALICE).into(), 3_000)); + assert_ok!(Salp::withdraw(Some(ALICE).into(), 3_000)); + + #[allow(non_snake_case)] + let (vsToken, vsBond) = Salp::vsAssets(3_000, 1, SlotLength::get()); + + assert_ok!(>::transfer(vsToken, &BRUCE, &CATHI, 50)); + assert_ok!(>::transfer(vsBond, &BRUCE, &CATHI, 50)); + + assert_ok!(Salp::redeem(Some(BRUCE).into(), 3_000, 50)); + + assert_eq!(Tokens::accounts(BRUCE, vsToken).free, 0); + assert_eq!(Tokens::accounts(BRUCE, vsToken).frozen, 0); + assert_eq!(Tokens::accounts(BRUCE, vsToken).reserved, 0); + assert_eq!(Tokens::accounts(BRUCE, vsBond).free, 0); + assert_eq!(Tokens::accounts(BRUCE, vsBond).frozen, 0); + assert_eq!(Tokens::accounts(BRUCE, vsBond).reserved, 0); + + assert_ok!(Salp::redeem(Some(CATHI).into(), 3_000, 50)); + + assert_eq!(Tokens::accounts(CATHI, vsToken).free, 0); + assert_eq!(Tokens::accounts(CATHI, vsToken).frozen, 0); + assert_eq!(Tokens::accounts(CATHI, vsToken).reserved, 0); + assert_eq!(Tokens::accounts(CATHI, vsBond).free, 0); + assert_eq!(Tokens::accounts(CATHI, vsBond).frozen, 0); + assert_eq!(Tokens::accounts(CATHI, vsBond).reserved, 0); + }); +} + +#[test] +fn redeem_with_speical_vsbond_should_work() { + new_test_ext().execute_with(|| { + assert_ok!(Salp::create(Some(ALICE).into(), 2001, 1_000, 13, 20)); + assert_ok!(Salp::issue(Some(ALICE).into(), BRUCE, 2001, 100)); + + assert_ok!(Salp::fund_success(Some(ALICE).into(), 2001)); + assert_ok!(Salp::unlock(Some(BRUCE).into(), BRUCE, 2001)); + + // Mock the BlockNumber + let block_begin_redeem = (SlotLength::get() + 1) * LeasePeriod::get(); + System::set_block_number(block_begin_redeem); + + assert_ok!(Salp::fund_retire(Some(ALICE).into(), 2001)); + assert_ok!(Salp::withdraw(Some(ALICE).into(), 2001)); + + #[allow(non_snake_case)] + let (vsToken, vsBond) = Salp::vsAssets(2001, 13, 20); + + assert_ok!(>::transfer(vsToken, &BRUCE, &CATHI, 50)); + assert_ok!(>::transfer(vsBond, &BRUCE, &CATHI, 50)); + + assert_ok!(Salp::redeem(Some(BRUCE).into(), 2001, 50)); + + assert_eq!(Tokens::accounts(BRUCE, vsToken).free, 0); + assert_eq!(Tokens::accounts(BRUCE, vsToken).frozen, 0); + assert_eq!(Tokens::accounts(BRUCE, vsToken).reserved, 0); + assert_eq!(Tokens::accounts(BRUCE, vsBond).free, 0); + assert_eq!(Tokens::accounts(BRUCE, vsBond).frozen, 0); + assert_eq!(Tokens::accounts(BRUCE, vsBond).reserved, 0); + + assert_ok!(Salp::redeem(Some(CATHI).into(), 2001, 50)); + + assert_eq!(Tokens::accounts(CATHI, vsToken).free, 0); + assert_eq!(Tokens::accounts(CATHI, vsToken).frozen, 0); + assert_eq!(Tokens::accounts(CATHI, vsToken).reserved, 0); + assert_eq!(Tokens::accounts(CATHI, vsBond).free, 0); + assert_eq!(Tokens::accounts(CATHI, vsBond).frozen, 0); + assert_eq!(Tokens::accounts(CATHI, vsBond).reserved, 0); + }); +} + +#[test] +fn redeem_with_wrong_origin_should_fail() { + new_test_ext().execute_with(|| { + assert_ok!(Salp::create(Some(ALICE).into(), 3_000, 1_000, 1, SlotLength::get())); + assert_ok!(Salp::issue(Some(ALICE).into(), BRUCE, 3_000, 100)); + assert_ok!(Salp::fund_success(Some(ALICE).into(), 3_000)); + assert_ok!(Salp::unlock(Some(BRUCE).into(), BRUCE, 3_000)); + + // Mock the BlockNumber + let block_begin_redeem = (SlotLength::get() + 1) * LeasePeriod::get(); + System::set_block_number(block_begin_redeem); + + assert_ok!(Salp::fund_retire(Some(ALICE).into(), 3_000)); + assert_ok!(Salp::withdraw(Some(ALICE).into(), 3_000)); + + assert_noop!(Salp::redeem(Origin::root(), 3_000, 50), DispatchError::BadOrigin); + assert_noop!(Salp::redeem(Origin::none(), 3_000, 50), DispatchError::BadOrigin); + }); +} + +#[test] +fn redeem_with_not_redeemable_vsbond_should_fail() { + new_test_ext().execute_with(|| { + assert_ok!(Salp::create(Some(ALICE).into(), 3_000, 1_000, 1, SlotLength::get())); + assert_ok!(Salp::issue(Some(ALICE).into(), BRUCE, 3_000, 100)); + assert_ok!(Salp::fund_success(Some(ALICE).into(), 3_000)); + assert_ok!(Salp::unlock(Some(BRUCE).into(), BRUCE, 3_000)); + + // Mock the BlockNumber + let block_not_redeemable = LeasePeriod::get(); + System::set_block_number(block_not_redeemable); + + #[allow(non_snake_case)] + let (vsToken, vsBond) = Salp::vsAssets(3_000, 1, SlotLength::get()); + + assert_ok!(>::transfer(vsToken, &BRUCE, &CATHI, 50)); + assert_ok!(>::transfer(vsBond, &BRUCE, &CATHI, 50)); + + assert_noop!(Salp::redeem(Some(BRUCE).into(), 3_000, 50), Error::::InvalidFundStatus); + + assert_noop!(Salp::redeem(Some(CATHI).into(), 3_000, 50), Error::::InvalidFundStatus); + }); +} + +#[test] +fn redeem_without_enough_vsassets_should_fail() { + new_test_ext().execute_with(|| { + assert_ok!(Salp::create(Some(ALICE).into(), 3_000, 1_000, 1, SlotLength::get())); + assert_ok!(Salp::issue(Some(ALICE).into(), BRUCE, 3_000, 100)); + assert_ok!(Salp::fund_success(Some(ALICE).into(), 3_000)); + assert_ok!(Salp::unlock(Some(BRUCE).into(), BRUCE, 3_000)); + + // Mock the BlockNumber + let block_begin_redeem = (SlotLength::get() + 1) * LeasePeriod::get(); + System::set_block_number(block_begin_redeem); + + assert_ok!(Salp::fund_retire(Some(ALICE).into(), 3_000)); + assert_ok!(Salp::withdraw(Some(ALICE).into(), 3_000)); + + #[allow(non_snake_case)] + let (vsToken, vsBond) = Salp::vsAssets(3_000, 1, SlotLength::get()); + + assert_ok!(>::transfer(vsToken, &BRUCE, &CATHI, 50)); + assert_ok!(>::transfer(vsBond, &BRUCE, &CATHI, 50)); + + assert_noop!( + Salp::redeem(Some(BRUCE).into(), 3_000, 60), + Error::::NotEnoughFreeAssetsToRedeem + ); + + assert_noop!( + Salp::redeem(Some(CATHI).into(), 3_000, 60), + Error::::NotEnoughFreeAssetsToRedeem + ); + }); +} + +#[test] +fn redeem_without_enough_balance_in_pool_should_fail() { + new_test_ext().execute_with(|| { + assert_ok!(Salp::create(Some(ALICE).into(), 3_000, 1_000, 1, SlotLength::get())); + assert_ok!(Salp::issue(Some(ALICE).into(), BRUCE, 3_000, 100)); + assert_ok!(Salp::fund_success(Some(ALICE).into(), 3_000)); + assert_ok!(Salp::unlock(Some(BRUCE).into(), BRUCE, 3_000)); + + // Mock the BlockNumber + let block_begin_redeem = (SlotLength::get() + 1) * LeasePeriod::get(); + System::set_block_number(block_begin_redeem); + + assert_ok!(Salp::fund_retire(Some(ALICE).into(), 3_000)); + assert_ok!(Salp::withdraw(Some(ALICE).into(), 3_000)); + + // Before withdraw + assert_noop!( + Salp::redeem(Some(BRUCE).into(), 3_000, 200), + Error::::NotEnoughBalanceInRedeemPool + ); + }); +} + +#[test] +fn redeem_with_when_ump_wrong_should_fail() { + // TODO: Require an solution to settle with parallel test workflow +} + +#[test] +fn release_from_redeem_to_bancor_should_work() { + fn run_to_block(n: BlockNumber) { + use frame_support::traits::Hooks; + while System::block_number() <= n { + Salp::on_finalize(System::block_number()); + System::on_finalize(System::block_number()); + System::set_block_number(System::block_number() + 1); + System::on_initialize(System::block_number()); + Salp::on_initialize(System::block_number()); + } + } + + new_test_ext().execute_with(|| { + assert_ok!(Salp::create(Some(ALICE).into(), 3_000, 1_000, 1, SlotLength::get())); + assert_ok!(Salp::issue(Some(ALICE).into(), BRUCE, 3_000, 100)); + assert_ok!(Salp::fund_success(Some(ALICE).into(), 3_000)); + assert_ok!(Salp::unlock(Some(BRUCE).into(), BRUCE, 3_000)); + assert_ok!(Salp::fund_retire(Some(ALICE).into(), 3_000)); + assert_ok!(Salp::withdraw(Some(ALICE).into(), 3_000)); + + run_to_block(ReleaseCycle::get()); + + // TODO: Check the balance of bancor(Waiting Bancor to Support..) + }); +} + +// Utilities Test +#[test] +fn check_next_trie_index() { + new_test_ext().execute_with(|| { + for i in 0..100 { + assert_eq!(Salp::current_trie_index(), i); + assert_ok!(Salp::next_trie_index()); + } + }); +} + +#[test] +fn batch_unlock_should_work() { + new_test_ext().execute_with(|| { + assert_ok!(Salp::create(Some(ALICE).into(), 3_000, 1_000, 1, SlotLength::get())); + assert_ok!(Salp::issue(Some(ALICE).into(), BRUCE, 3_000, 100)); + assert_ok!(Salp::fund_success(Some(ALICE).into(), 3_000)); + assert_ok!(Salp::batch_unlock(Some(ALICE).into(), 3_000)); + }) +} + +#[test] +fn edit_fund_should_work() { + new_test_ext().execute_with(|| { + assert_ok!(Salp::create(Some(ALICE).into(), 3_000, 1_000, 1, SlotLength::get())); + assert_ok!(Salp::issue(Some(ALICE).into(), BRUCE, 3_000, 100)); + assert_ok!(Salp::fund_fail(Some(ALICE).into(), 3_000)); + assert_ok!(Salp::edit(Some(ALICE).into(), 3_000, 1_000, 2, SlotLength::get() + 1, None)); + let mut fund = Salp::funds(3_000).unwrap(); + assert_eq!(fund.first_slot, 2); + assert_eq!(fund.status, FundStatus::Failed); + assert_ok!(Salp::edit( + Some(ALICE).into(), + 3_000, + 1_000, + 2, + SlotLength::get() + 1, + Some(FundStatus::Ongoing), + )); + fund = Salp::funds(3_000).unwrap(); + assert_eq!(fund.status, FundStatus::Ongoing); + }) +} + +#[test] +fn batch_migrate_should_work() { + new_test_ext().execute_with(|| { + assert_ok!(Salp::create(Some(ALICE).into(), 3_000, 1_000, 1, SlotLength::get())); + assert_ok!(Salp::issue(Some(ALICE).into(), BRUCE, 3_000, 100)); + assert_ok!(Salp::fund_fail(Some(ALICE).into(), 3_000)); + + #[allow(non_snake_case)] + let (_, vsBond) = Salp::vsAssets(3000, 1, SlotLength::get()); + assert_eq!(Tokens::accounts(BRUCE, vsBond).reserved, 100); + assert_ok!(Salp::batch_migrate(Some(ALICE).into(), 3_000, 2, SlotLength::get() + 1)); + let (_, vs_bond) = Salp::vsAssets(3000, 1, SlotLength::get()); + assert_eq!(Tokens::accounts(BRUCE, vs_bond).reserved, 0); + let (_, vs_bond_new) = Salp::vsAssets(3000, 2, SlotLength::get() + 1); + assert_eq!(Tokens::accounts(BRUCE, vs_bond_new).reserved, 100); + + assert_ok!(Salp::edit( + Some(ALICE).into(), + 3_000, + 1_000, + 2, + SlotLength::get() + 1, + Some(FundStatus::Ongoing) + )); + let fund = Salp::funds(3_000).unwrap(); + assert_eq!(fund.first_slot, 2); + assert_eq!(fund.status, FundStatus::Ongoing); + }) +} diff --git a/pallets/salp/src/lib.rs b/pallets/salp/src/lib.rs index b70e679c1d..947448196c 100644 --- a/pallets/salp/src/lib.rs +++ b/pallets/salp/src/lib.rs @@ -127,7 +127,7 @@ pub mod pallet { use super::*; #[pallet::config] - pub trait Config: frame_system::Config { + pub trait Config: frame_system::Config + TypeInfo { type Event: From> + IsType<::Event>; /// ModuleID for the crowdloan module. An appropriate value could be diff --git a/runtime/asgard/Cargo.toml b/runtime/asgard/Cargo.toml index b08963ca7c..43a878c2cd 100644 --- a/runtime/asgard/Cargo.toml +++ b/runtime/asgard/Cargo.toml @@ -91,6 +91,8 @@ bifrost-minter-reward = { path = "../../pallets/minter-reward", default-features bifrost-runtime-common = { package = "bifrost-runtime-common", path = "../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-salp-lite = { path = "../../pallets/salp-lite", default-features = false } +bifrost-salp-lite-rpc-runtime-api = { path = "../../pallets/salp-lite/rpc/runtime-api", default-features = false } bifrost-liquidity-mining-rpc-runtime-api = { path = "../../pallets/liquidity-mining/rpc/runtime-api", default-features = false } bifrost-vsbond-auction = { path = "../../pallets/vsbond-auction", default-features = false } bifrost-vtoken-mint = { path = "../../pallets/vtoken-mint", default-features = false } @@ -182,6 +184,8 @@ std = [ "bifrost-runtime-common/std", "bifrost-salp/std", "bifrost-salp-rpc-runtime-api/std", + "bifrost-salp-lite/std", + "bifrost-salp-lite-rpc-runtime-api/std", "bifrost-vsbond-auction/std", "bifrost-vtoken-mint/std", "bifrost-liquidity-mining/std", diff --git a/runtime/asgard/src/lib.rs b/runtime/asgard/src/lib.rs index 43075cb2bd..dd28f87eed 100644 --- a/runtime/asgard/src/lib.rs +++ b/runtime/asgard/src/lib.rs @@ -189,6 +189,7 @@ parameter_types! { pub const RelayCurrencyId: CurrencyId = CurrencyId::Token(TokenSymbol::KSM); pub const StableCurrencyId: CurrencyId = CurrencyId::Stable(TokenSymbol::KUSD); pub SelfParaId: u32 = ParachainInfo::parachain_id().into(); + pub const PolkadotCurrencyId: CurrencyId = CurrencyId::Token(TokenSymbol::DOT); } parameter_types! { @@ -1242,6 +1243,25 @@ impl bifrost_salp::Config for Runtime { type RelayNetwork = RelayNetwork; } +impl bifrost_salp_lite::Config for Runtime { + type BancorPool = (); + type Event = Event; + type LeasePeriod = LeasePeriod; + type MinContribution = MinContribution; + type MultiCurrency = Currencies; + type PalletId = BifrostCrowdloanId; + type RelayChainToken = PolkadotCurrencyId; + type ReleaseCycle = ReleaseCycle; + type ReleaseRatio = ReleaseRatio; + type BatchKeysLimit = RemoveKeysLimit; + type SlotLength = SlotLength; + type WeightInfo = weights::bifrost_salp_lite::WeightInfo; + type EnsureConfirmAsMultiSig = + EnsureOneOf; + type EnsureConfirmAsGovernance = + EnsureOneOf; +} + parameter_types! { pub const InterventionPercentage: Percent = Percent::from_percent(75); pub const DailyReleasePercentage: Percent = Percent::from_percent(5); @@ -1491,6 +1511,7 @@ construct_runtime! { LiquidityMining: bifrost_liquidity_mining::{Pallet, Call, Storage, Event} = 108, TokenIssuer: bifrost_token_issuer::{Pallet, Call, Storage, Event} = 109, LighteningRedeem: bifrost_lightening_redeem::{Pallet, Call, Storage, Event} = 110, + SalpLite: bifrost_salp_lite::{Pallet, Call, Storage, Event} = 111, } } diff --git a/runtime/asgard/src/weights/bifrost_salp_lite.rs b/runtime/asgard/src/weights/bifrost_salp_lite.rs new file mode 100644 index 0000000000..a98db291b3 --- /dev/null +++ b/runtime/asgard/src/weights/bifrost_salp_lite.rs @@ -0,0 +1,103 @@ +// This file is part of Bifrost. + +// Copyright (C) 2019-2021 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 . + +//! Autogenerated weights for bifrost_salp +//! +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 3.0.0 +//! DATE: 2021-08-11, STEPS: `[50, ]`, REPEAT: 20, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! EXECUTION: Some(Wasm), WASM-EXECUTION: Compiled, CHAIN: Some("asgard-local"), DB CACHE: 128 + +// Executed Command: +// target/release/bifrost +// benchmark +// --chain=asgard-local +// --steps=50 +// --repeat=20 +// --pallet=bifrost_salp +// --extrinsic=* +// --execution=wasm +// --wasm-execution=compiled +// --heap-pages=4096 +// --header=./HEADER-GPL3 +// --output=./runtime/asgard/src/weights/bifrost_salp.rs + +#![allow(unused_parens)] +#![allow(unused_imports)] + +use frame_support::{traits::Get, weights::Weight}; +use sp_std::marker::PhantomData; + +/// Weight functions for bifrost_salp +/// @todo benchmark again later +pub struct WeightInfo(PhantomData); +impl bifrost_salp_lite::WeightInfo for WeightInfo { + // Storage: Salp Funds (r:1 w:0) + // Storage: Tokens Accounts (r:1 w:1) + // Storage: Salp CurrentNonce (r:1 w:1) + // Storage: ParachainSystem HostConfiguration (r:1 w:0) + // Storage: ParachainSystem PendingUpwardMessages (r:1 w:1) + // Storage: unknown [0xd861ea1ebf4800d4b89f4ff787ad79ee96d9a708c85b57da7eb8f9ddeda61291] (r:1 + // w:1) + fn contribute() -> Weight { + (122_561_000 as Weight) + .saturating_add(T::DbWeight::get().reads(6 as Weight)) + .saturating_add(T::DbWeight::get().writes(4 as Weight)) + } + // Storage: Salp Funds (r:1 w:0) + // Storage: Salp RefundPool (r:1 w:1) + // Storage: Tokens Accounts (r:4 w:4) + // Storage: Tokens TotalIssuance (r:2 w:2) + // Storage: System Account (r:1 w:0) + // Storage: unknown [0xd861ea1ebf4800d4b89f4ff787ad79ee96d9a708c85b57da7eb8f9ddeda61291] (r:1 + // w:1) + fn refund() -> Weight { + (139_712_000 as Weight) + .saturating_add(T::DbWeight::get().reads(10 as Weight)) + .saturating_add(T::DbWeight::get().writes(8 as Weight)) + } + // Storage: Salp Funds (r:1 w:0) + // Storage: Tokens Accounts (r:2 w:2) + // Storage: unknown [0xd861ea1ebf4800d4b89f4ff787ad79ee96d9a708c85b57da7eb8f9ddeda61291] (r:1 + // w:1) + fn unlock() -> Weight { + (99_237_000 as Weight) + .saturating_add(T::DbWeight::get().reads(4 as Weight)) + .saturating_add(T::DbWeight::get().writes(3 as Weight)) + } + // Storage: Salp Funds (r:1 w:0) + // Storage: Salp RedeemPool (r:1 w:1) + // Storage: Tokens Accounts (r:4 w:4) + // Storage: Tokens TotalIssuance (r:2 w:2) + // Storage: System Account (r:1 w:0) + // Storage: unknown [0xd861ea1ebf4800d4b89f4ff787ad79ee96d9a708c85b57da7eb8f9ddeda61291] (r:0 + // w:1) + fn redeem() -> Weight { + (169_980_000 as Weight) + .saturating_add(T::DbWeight::get().reads(9 as Weight)) + .saturating_add(T::DbWeight::get().writes(8 as Weight)) + } + + fn batch_unlock(k: u32) -> Weight { + (0 as Weight) + .saturating_add((45_890_000 as Weight).saturating_mul(k as Weight)) + .saturating_add(T::DbWeight::get().reads(3 as Weight)) + .saturating_add(T::DbWeight::get().reads((2 as Weight).saturating_mul(k as Weight))) + .saturating_add(T::DbWeight::get().writes(2 as Weight)) + .saturating_add(T::DbWeight::get().writes((2 as Weight).saturating_mul(k as Weight))) + } +} diff --git a/runtime/asgard/src/weights/mod.rs b/runtime/asgard/src/weights/mod.rs index b96ebc4160..3b34979260 100644 --- a/runtime/asgard/src/weights/mod.rs +++ b/runtime/asgard/src/weights/mod.rs @@ -25,6 +25,7 @@ pub mod bifrost_flexible_fee; pub mod bifrost_lightening_redeem; pub mod bifrost_minter_reward; pub mod bifrost_salp; +pub mod bifrost_salp_lite; pub mod bifrost_token_issuer; pub mod bifrost_vsbond_auction; pub mod bifrost_vtoken_mint; diff --git a/runtime/bifrost/Cargo.toml b/runtime/bifrost/Cargo.toml index 76f9ec1fa5..67abb047a3 100644 --- a/runtime/bifrost/Cargo.toml +++ b/runtime/bifrost/Cargo.toml @@ -85,6 +85,8 @@ bifrost-flexible-fee-rpc-runtime-api = { path = "../../pallets/flexible-fee/rpc/ bifrost-runtime-common = { package = "bifrost-runtime-common", path = "../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-salp-lite = { path = "../../pallets/salp-lite", default-features = false } +bifrost-salp-lite-rpc-runtime-api = { path = "../../pallets/salp-lite/rpc/runtime-api", default-features = false } pallet-vesting = { package = "bifrost-vesting", path = "../../pallets/vesting", default-features = false } bifrost-liquidity-mining = { path = "../../pallets/liquidity-mining", default-features = false } bifrost-liquidity-mining-rpc-runtime-api = { path = "../../pallets/liquidity-mining/rpc/runtime-api", default-features = false } @@ -170,6 +172,8 @@ std = [ "bifrost-flexible-fee-rpc-runtime-api/std", "bifrost-salp/std", "bifrost-salp-rpc-runtime-api/std", + "bifrost-salp-lite/std", + "bifrost-salp-lite-rpc-runtime-api/std", "bifrost-liquidity-mining/std", "bifrost-liquidity-mining-rpc-runtime-api/std", "bifrost-token-issuer/std", diff --git a/runtime/bifrost/src/lib.rs b/runtime/bifrost/src/lib.rs index a3c60a3a5b..d0de5f4fbd 100644 --- a/runtime/bifrost/src/lib.rs +++ b/runtime/bifrost/src/lib.rs @@ -219,6 +219,7 @@ parameter_types! { pub const RelayCurrencyId: CurrencyId = CurrencyId::Token(TokenSymbol::KSM); pub const StableCurrencyId: CurrencyId = CurrencyId::Stable(TokenSymbol::KUSD); pub SelfParaId: u32 = ParachainInfo::parachain_id().into(); + pub const PolkadotCurrencyId: CurrencyId = CurrencyId::Token(TokenSymbol::DOT); } parameter_types! { @@ -1128,6 +1129,25 @@ impl bifrost_salp::Config for Runtime { type RelayNetwork = RelayNetwork; } +impl bifrost_salp_lite::Config for Runtime { + type BancorPool = (); + type Event = Event; + type LeasePeriod = LeasePeriod; + type MinContribution = MinContribution; + type MultiCurrency = Currencies; + type PalletId = BifrostCrowdloanId; + type RelayChainToken = PolkadotCurrencyId; + type ReleaseCycle = ReleaseCycle; + type ReleaseRatio = ReleaseRatio; + type BatchKeysLimit = RemoveKeysLimit; + type SlotLength = SlotLength; + type WeightInfo = weights::bifrost_salp_lite::WeightInfo; + type EnsureConfirmAsMultiSig = + EnsureOneOf; + type EnsureConfirmAsGovernance = + EnsureOneOf; +} + parameter_types! { pub const RelayChainTokenSymbol: TokenSymbol = TokenSymbol::KSM; pub const MaximumDepositInPool: Balance = 1_000_000_000 * DOLLARS; @@ -1343,6 +1363,7 @@ construct_runtime! { LiquidityMining: bifrost_liquidity_mining::{Pallet, Call, Storage, Event} = 108, TokenIssuer: bifrost_token_issuer::{Pallet, Call, Storage, Event} = 109, LighteningRedeem: bifrost_lightening_redeem::{Pallet, Call, Storage, Event} = 110, + SalpLite: bifrost_salp_lite::{Pallet, Call, Storage, Event} = 111, } } diff --git a/runtime/bifrost/src/weights/bifrost_salp_lite.rs b/runtime/bifrost/src/weights/bifrost_salp_lite.rs new file mode 100644 index 0000000000..a98db291b3 --- /dev/null +++ b/runtime/bifrost/src/weights/bifrost_salp_lite.rs @@ -0,0 +1,103 @@ +// This file is part of Bifrost. + +// Copyright (C) 2019-2021 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 . + +//! Autogenerated weights for bifrost_salp +//! +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 3.0.0 +//! DATE: 2021-08-11, STEPS: `[50, ]`, REPEAT: 20, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! EXECUTION: Some(Wasm), WASM-EXECUTION: Compiled, CHAIN: Some("asgard-local"), DB CACHE: 128 + +// Executed Command: +// target/release/bifrost +// benchmark +// --chain=asgard-local +// --steps=50 +// --repeat=20 +// --pallet=bifrost_salp +// --extrinsic=* +// --execution=wasm +// --wasm-execution=compiled +// --heap-pages=4096 +// --header=./HEADER-GPL3 +// --output=./runtime/asgard/src/weights/bifrost_salp.rs + +#![allow(unused_parens)] +#![allow(unused_imports)] + +use frame_support::{traits::Get, weights::Weight}; +use sp_std::marker::PhantomData; + +/// Weight functions for bifrost_salp +/// @todo benchmark again later +pub struct WeightInfo(PhantomData); +impl bifrost_salp_lite::WeightInfo for WeightInfo { + // Storage: Salp Funds (r:1 w:0) + // Storage: Tokens Accounts (r:1 w:1) + // Storage: Salp CurrentNonce (r:1 w:1) + // Storage: ParachainSystem HostConfiguration (r:1 w:0) + // Storage: ParachainSystem PendingUpwardMessages (r:1 w:1) + // Storage: unknown [0xd861ea1ebf4800d4b89f4ff787ad79ee96d9a708c85b57da7eb8f9ddeda61291] (r:1 + // w:1) + fn contribute() -> Weight { + (122_561_000 as Weight) + .saturating_add(T::DbWeight::get().reads(6 as Weight)) + .saturating_add(T::DbWeight::get().writes(4 as Weight)) + } + // Storage: Salp Funds (r:1 w:0) + // Storage: Salp RefundPool (r:1 w:1) + // Storage: Tokens Accounts (r:4 w:4) + // Storage: Tokens TotalIssuance (r:2 w:2) + // Storage: System Account (r:1 w:0) + // Storage: unknown [0xd861ea1ebf4800d4b89f4ff787ad79ee96d9a708c85b57da7eb8f9ddeda61291] (r:1 + // w:1) + fn refund() -> Weight { + (139_712_000 as Weight) + .saturating_add(T::DbWeight::get().reads(10 as Weight)) + .saturating_add(T::DbWeight::get().writes(8 as Weight)) + } + // Storage: Salp Funds (r:1 w:0) + // Storage: Tokens Accounts (r:2 w:2) + // Storage: unknown [0xd861ea1ebf4800d4b89f4ff787ad79ee96d9a708c85b57da7eb8f9ddeda61291] (r:1 + // w:1) + fn unlock() -> Weight { + (99_237_000 as Weight) + .saturating_add(T::DbWeight::get().reads(4 as Weight)) + .saturating_add(T::DbWeight::get().writes(3 as Weight)) + } + // Storage: Salp Funds (r:1 w:0) + // Storage: Salp RedeemPool (r:1 w:1) + // Storage: Tokens Accounts (r:4 w:4) + // Storage: Tokens TotalIssuance (r:2 w:2) + // Storage: System Account (r:1 w:0) + // Storage: unknown [0xd861ea1ebf4800d4b89f4ff787ad79ee96d9a708c85b57da7eb8f9ddeda61291] (r:0 + // w:1) + fn redeem() -> Weight { + (169_980_000 as Weight) + .saturating_add(T::DbWeight::get().reads(9 as Weight)) + .saturating_add(T::DbWeight::get().writes(8 as Weight)) + } + + fn batch_unlock(k: u32) -> Weight { + (0 as Weight) + .saturating_add((45_890_000 as Weight).saturating_mul(k as Weight)) + .saturating_add(T::DbWeight::get().reads(3 as Weight)) + .saturating_add(T::DbWeight::get().reads((2 as Weight).saturating_mul(k as Weight))) + .saturating_add(T::DbWeight::get().writes(2 as Weight)) + .saturating_add(T::DbWeight::get().writes((2 as Weight).saturating_mul(k as Weight))) + } +} diff --git a/runtime/bifrost/src/weights/mod.rs b/runtime/bifrost/src/weights/mod.rs index 07fdb61a90..1abfb8a695 100644 --- a/runtime/bifrost/src/weights/mod.rs +++ b/runtime/bifrost/src/weights/mod.rs @@ -24,6 +24,7 @@ pub mod bifrost_flexible_fee; pub mod bifrost_lightening_redeem; pub mod bifrost_liquidity_mining; pub mod bifrost_salp; +pub mod bifrost_salp_lite; pub mod bifrost_token_issuer; pub mod frame_system; pub mod orml_tokens; diff --git a/runtime/dev/Cargo.toml b/runtime/dev/Cargo.toml index 4456ba7956..a780d58b17 100644 --- a/runtime/dev/Cargo.toml +++ b/runtime/dev/Cargo.toml @@ -90,6 +90,8 @@ bifrost-minter-reward = { path = "../../pallets/minter-reward", default-features bifrost-runtime-common = { package = "bifrost-runtime-common", path = "../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-salp-lite = { path = "../../pallets/salp-lite", default-features = false } +bifrost-salp-lite-rpc-runtime-api = { path = "../../pallets/salp-lite/rpc/runtime-api", default-features = false } bifrost-vsbond-auction = { path = "../../pallets/vsbond-auction", default-features = false } bifrost-vtoken-mint = { path = "../../pallets/vtoken-mint", default-features = false } pallet-vesting = { package = "bifrost-vesting", path = "../../pallets/vesting", default-features = false } @@ -178,6 +180,8 @@ std = [ "bifrost-runtime-common/std", "bifrost-salp/std", "bifrost-salp-rpc-runtime-api/std", + "bifrost-salp-lite/std", + "bifrost-salp-lite-rpc-runtime-api/std", "bifrost-vsbond-auction/std", "bifrost-vtoken-mint/std", "xcm-support/std", diff --git a/runtime/dev/src/lib.rs b/runtime/dev/src/lib.rs index 9a524e3f33..287c07829b 100644 --- a/runtime/dev/src/lib.rs +++ b/runtime/dev/src/lib.rs @@ -194,6 +194,7 @@ parameter_types! { pub const NativeCurrencyId: CurrencyId = CurrencyId::Native(TokenSymbol::ASG); pub const RelayCurrencyId: CurrencyId = CurrencyId::Token(TokenSymbol::KSM); pub const StableCurrencyId: CurrencyId = CurrencyId::Stable(TokenSymbol::KUSD); + pub const PolkadotCurrencyId: CurrencyId = CurrencyId::Token(TokenSymbol::DOT); } impl frame_system::Config for Runtime { @@ -1143,6 +1144,25 @@ impl bifrost_salp::Config for Runtime { type RelayNetwork = RelayNetwork; } +impl bifrost_salp_lite::Config for Runtime { + type BancorPool = (); + type Event = Event; + type LeasePeriod = LeasePeriod; + type MinContribution = MinContribution; + type MultiCurrency = Currencies; + type PalletId = BifrostCrowdloanId; + type RelayChainToken = PolkadotCurrencyId; + type ReleaseCycle = ReleaseCycle; + type ReleaseRatio = ReleaseRatio; + type BatchKeysLimit = RemoveKeysLimit; + type SlotLength = SlotLength; + type WeightInfo = weights::bifrost_salp_lite::WeightInfo; + type EnsureConfirmAsMultiSig = + EnsureOneOf; + type EnsureConfirmAsGovernance = + EnsureOneOf; +} + parameter_types! { pub const InterventionPercentage: Percent = Percent::from_percent(75); pub const DailyReleasePercentage: Percent = Percent::from_percent(5); @@ -1421,6 +1441,7 @@ construct_runtime! { Bancor: bifrost_bancor::{Pallet, Call, Storage, Event, Config} = 106, VSBondAuction: bifrost_vsbond_auction::{Pallet, Call, Storage, Event} = 107, LiquidityMining: bifrost_liquidity_mining::{Pallet, Call, Storage, Event} = 108, + SalpLite: bifrost_salp_lite::{Pallet, Call, Storage, Event} = 111, } } diff --git a/runtime/dev/src/weights/bifrost_salp_lite.rs b/runtime/dev/src/weights/bifrost_salp_lite.rs new file mode 100644 index 0000000000..a98db291b3 --- /dev/null +++ b/runtime/dev/src/weights/bifrost_salp_lite.rs @@ -0,0 +1,103 @@ +// This file is part of Bifrost. + +// Copyright (C) 2019-2021 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 . + +//! Autogenerated weights for bifrost_salp +//! +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 3.0.0 +//! DATE: 2021-08-11, STEPS: `[50, ]`, REPEAT: 20, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! EXECUTION: Some(Wasm), WASM-EXECUTION: Compiled, CHAIN: Some("asgard-local"), DB CACHE: 128 + +// Executed Command: +// target/release/bifrost +// benchmark +// --chain=asgard-local +// --steps=50 +// --repeat=20 +// --pallet=bifrost_salp +// --extrinsic=* +// --execution=wasm +// --wasm-execution=compiled +// --heap-pages=4096 +// --header=./HEADER-GPL3 +// --output=./runtime/asgard/src/weights/bifrost_salp.rs + +#![allow(unused_parens)] +#![allow(unused_imports)] + +use frame_support::{traits::Get, weights::Weight}; +use sp_std::marker::PhantomData; + +/// Weight functions for bifrost_salp +/// @todo benchmark again later +pub struct WeightInfo(PhantomData); +impl bifrost_salp_lite::WeightInfo for WeightInfo { + // Storage: Salp Funds (r:1 w:0) + // Storage: Tokens Accounts (r:1 w:1) + // Storage: Salp CurrentNonce (r:1 w:1) + // Storage: ParachainSystem HostConfiguration (r:1 w:0) + // Storage: ParachainSystem PendingUpwardMessages (r:1 w:1) + // Storage: unknown [0xd861ea1ebf4800d4b89f4ff787ad79ee96d9a708c85b57da7eb8f9ddeda61291] (r:1 + // w:1) + fn contribute() -> Weight { + (122_561_000 as Weight) + .saturating_add(T::DbWeight::get().reads(6 as Weight)) + .saturating_add(T::DbWeight::get().writes(4 as Weight)) + } + // Storage: Salp Funds (r:1 w:0) + // Storage: Salp RefundPool (r:1 w:1) + // Storage: Tokens Accounts (r:4 w:4) + // Storage: Tokens TotalIssuance (r:2 w:2) + // Storage: System Account (r:1 w:0) + // Storage: unknown [0xd861ea1ebf4800d4b89f4ff787ad79ee96d9a708c85b57da7eb8f9ddeda61291] (r:1 + // w:1) + fn refund() -> Weight { + (139_712_000 as Weight) + .saturating_add(T::DbWeight::get().reads(10 as Weight)) + .saturating_add(T::DbWeight::get().writes(8 as Weight)) + } + // Storage: Salp Funds (r:1 w:0) + // Storage: Tokens Accounts (r:2 w:2) + // Storage: unknown [0xd861ea1ebf4800d4b89f4ff787ad79ee96d9a708c85b57da7eb8f9ddeda61291] (r:1 + // w:1) + fn unlock() -> Weight { + (99_237_000 as Weight) + .saturating_add(T::DbWeight::get().reads(4 as Weight)) + .saturating_add(T::DbWeight::get().writes(3 as Weight)) + } + // Storage: Salp Funds (r:1 w:0) + // Storage: Salp RedeemPool (r:1 w:1) + // Storage: Tokens Accounts (r:4 w:4) + // Storage: Tokens TotalIssuance (r:2 w:2) + // Storage: System Account (r:1 w:0) + // Storage: unknown [0xd861ea1ebf4800d4b89f4ff787ad79ee96d9a708c85b57da7eb8f9ddeda61291] (r:0 + // w:1) + fn redeem() -> Weight { + (169_980_000 as Weight) + .saturating_add(T::DbWeight::get().reads(9 as Weight)) + .saturating_add(T::DbWeight::get().writes(8 as Weight)) + } + + fn batch_unlock(k: u32) -> Weight { + (0 as Weight) + .saturating_add((45_890_000 as Weight).saturating_mul(k as Weight)) + .saturating_add(T::DbWeight::get().reads(3 as Weight)) + .saturating_add(T::DbWeight::get().reads((2 as Weight).saturating_mul(k as Weight))) + .saturating_add(T::DbWeight::get().writes(2 as Weight)) + .saturating_add(T::DbWeight::get().writes((2 as Weight).saturating_mul(k as Weight))) + } +} diff --git a/runtime/dev/src/weights/mod.rs b/runtime/dev/src/weights/mod.rs index f1981db56f..e47f78514b 100644 --- a/runtime/dev/src/weights/mod.rs +++ b/runtime/dev/src/weights/mod.rs @@ -24,6 +24,7 @@ pub mod bifrost_bancor; pub mod bifrost_flexible_fee; pub mod bifrost_minter_reward; pub mod bifrost_salp; +pub mod bifrost_salp_lite; pub mod bifrost_vsbond_auction; pub mod bifrost_vtoken_mint; pub mod pallet_vesting;