diff --git a/Cargo.lock b/Cargo.lock index fdbb435945..65578627f9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5628,6 +5628,7 @@ dependencies = [ "pallet-base-fee", "pallet-collective", "pallet-commitments", + "pallet-crowdloan", "pallet-drand", "pallet-ethereum", "pallet-evm", @@ -6112,6 +6113,24 @@ dependencies = [ "w3f-bls", ] +[[package]] +name = "pallet-crowdloan" +version = "0.1.0" +dependencies = [ + "frame-benchmarking", + "frame-support", + "frame-system", + "pallet-balances", + "pallet-preimage", + "parity-scale-codec", + "scale-info", + "sp-core", + "sp-io", + "sp-runtime", + "sp-std 14.0.0 (git+https://github.com/paritytech/polkadot-sdk.git?tag=polkadot-stable2409)", + "subtensor-macros", +] + [[package]] name = "pallet-drand" version = "0.0.1" diff --git a/Cargo.toml b/Cargo.toml index b7fc20b678..08cdfbf91e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -46,6 +46,7 @@ pallet-admin-utils = { default-features = false, path = "pallets/admin-utils" } pallet-collective = { default-features = false, path = "pallets/collective" } pallet-commitments = { default-features = false, path = "pallets/commitments" } pallet-registry = { default-features = false, path = "pallets/registry" } +pallet-crowdloan = { default-features = false, path = "pallets/crowdloan" } pallet-subtensor = { default-features = false, path = "pallets/subtensor" } subtensor-custom-rpc = { default-features = false, path = "pallets/subtensor/rpc" } subtensor-custom-rpc-runtime-api = { default-features = false, path = "pallets/subtensor/runtime-api" } diff --git a/pallets/crowdloan/Cargo.toml b/pallets/crowdloan/Cargo.toml new file mode 100644 index 0000000000..1739a85b7c --- /dev/null +++ b/pallets/crowdloan/Cargo.toml @@ -0,0 +1,60 @@ +[package] +name = "pallet-crowdloan" +version = "0.1.0" +edition = "2024" +authors = ["Bittensor Nucleus Team"] +license = "Apache-2.0" +homepage = "https://bittensor.com" +description = "FRAME crowdloan pallet" +publish = false +repository = "https://github.com/opentensor/subtensor" + +[lints] +workspace = true + +[dependencies] +subtensor-macros.workspace = true +scale-info = { workspace = true, features = ["derive"] } +codec = { workspace = true, features = ["max-encoded-len"] } +frame-benchmarking = { optional = true, workspace = true } +frame-support.workspace = true +frame-system.workspace = true +sp-runtime.workspace = true +sp-std.workspace = true + +[dev-dependencies] +pallet-balances = { default-features = true, workspace = true } +pallet-preimage = { default-features = true, workspace = true } +sp-core = { default-features = true, workspace = true } +sp-io = { default-features = true, workspace = true } + +[features] +default = ["std"] +std = [ + "codec/std", + "frame-benchmarking?/std", + "frame-support/std", + "frame-system/std", + "scale-info/std", + "sp-runtime/std", + "sp-std/std", + "sp-io/std", + "sp-core/std", + "pallet-balances/std", + "pallet-preimage/std", +] +runtime-benchmarks = [ + "frame-benchmarking/runtime-benchmarks", + "frame-support/runtime-benchmarks", + "frame-system/runtime-benchmarks", + "sp-runtime/runtime-benchmarks", + "pallet-balances/runtime-benchmarks", + "pallet-preimage/runtime-benchmarks", +] +try-runtime = [ + "frame-support/try-runtime", + "frame-system/try-runtime", + "sp-runtime/try-runtime", + "pallet-balances/try-runtime", + "pallet-preimage/try-runtime", +] diff --git a/pallets/crowdloan/README.md b/pallets/crowdloan/README.md new file mode 100644 index 0000000000..f0b084b9ce --- /dev/null +++ b/pallets/crowdloan/README.md @@ -0,0 +1,19 @@ +# Crowdloan Pallet + +A pallet that enables the creation and management of generic crowdloans for transferring funds and executing an arbitrary call. + +Users of this pallet can create a crowdloan by providing a deposit, a cap, an end block, an optional target address and an optional call. + +Users can contribute to a crowdloan by providing funds to the crowdloan they choose to support. + +Once the crowdloan is finalized, the funds will be transferred to the target address if provided; otherwise, the end user is expected to transfer them manually on-chain if the call is a pallet extrinsic. The call will be dispatched with the current crowdloan ID stored as a temporary item. + +If the crowdloan fails to reach the cap, the initial deposit will be returned to the creator, and contributions will be refunded to the contributors. + +## Overview + +## Interface + +## Dispatchable Functions + +License: Apache-2.0 diff --git a/pallets/crowdloan/src/benchmarking.rs b/pallets/crowdloan/src/benchmarking.rs new file mode 100644 index 0000000000..5dab0c1b91 --- /dev/null +++ b/pallets/crowdloan/src/benchmarking.rs @@ -0,0 +1,498 @@ +//! Benchmarks for Crowdloan Pallet +#![cfg(feature = "runtime-benchmarks")] +#![allow( + clippy::arithmetic_side_effects, + clippy::indexing_slicing, + clippy::unwrap_used +)] +use crate::{BalanceOf, CrowdloanId, CrowdloanInfo, CurrencyOf, pallet::*}; +use frame_benchmarking::{account, v2::*}; +use frame_support::traits::{Get, StorePreimage, fungible::*}; +use frame_system::{RawOrigin, pallet_prelude::BlockNumberFor}; + +extern crate alloc; + +const SEED: u32 = 0; + +use alloc::{boxed::Box, vec}; + +fn assert_last_event(generic_event: ::RuntimeEvent) { + let events = frame_system::Pallet::::events(); + let system_event: ::RuntimeEvent = generic_event.into(); + // compare to the last event record + let frame_system::EventRecord { event, .. } = &events[events.len() - 1]; + assert_eq!(event, &system_event); +} + +#[benchmarks] +mod benchmarks { + use super::*; + + #[benchmark] + fn create() { + let creator: T::AccountId = account::("creator", 0, SEED); + let deposit = T::MinimumDeposit::get(); + let min_contribution = T::AbsoluteMinimumContribution::get(); + let cap = deposit + deposit; + let now = frame_system::Pallet::::block_number(); + let end = now + T::MaximumBlockDuration::get(); + let target_address = account::("target_address", 0, SEED); + let call: Box<::RuntimeCall> = + Box::new(frame_system::Call::::remark { remark: vec![] }.into()); + let _ = CurrencyOf::::set_balance(&creator, deposit); + + #[extrinsic_call] + _( + RawOrigin::Signed(creator.clone()), + deposit, + min_contribution, + cap, + end, + Some(call.clone()), + Some(target_address.clone()), + ); + + // ensure the crowdloan is stored correctly + let crowdloan_id = 0; + let funds_account = Pallet::::funds_account(crowdloan_id); + assert_eq!( + Crowdloans::::get(crowdloan_id), + Some(CrowdloanInfo { + creator: creator.clone(), + deposit, + min_contribution, + cap, + end, + funds_account: funds_account.clone(), + raised: deposit, + target_address: Some(target_address.clone()), + call: Some(T::Preimages::bound(*call).unwrap()), + finalized: false, + }) + ); + // ensure the creator has been deducted the deposit + assert!(CurrencyOf::::balance(&creator) == 0); + // ensure the initial deposit is stored correctly as contribution + assert_eq!( + Contributions::::get(crowdloan_id, &creator), + Some(deposit) + ); + // ensure the raised amount is updated correctly + assert!(Crowdloans::::get(crowdloan_id).is_some_and(|c| c.raised == deposit)); + // ensure the crowdloan account has the deposit + assert_eq!(CurrencyOf::::balance(&funds_account), deposit); + // ensure the event is emitted + assert_last_event::( + Event::::Created { + crowdloan_id, + creator, + end, + cap, + } + .into(), + ); + // ensure next crowdloan id is incremented + assert_eq!(NextCrowdloanId::::get(), crowdloan_id + 1); + } + + #[benchmark] + fn contribute() { + // create a crowdloan + let creator: T::AccountId = account::("creator", 0, SEED); + let deposit = T::MinimumDeposit::get(); + let min_contribution = T::AbsoluteMinimumContribution::get(); + let cap = deposit + deposit; + let now = frame_system::Pallet::::block_number(); + let end = now + T::MaximumBlockDuration::get(); + let target_address: T::AccountId = account::("target_address", 0, SEED); + let call: Box<::RuntimeCall> = + Box::new(frame_system::Call::::remark { remark: vec![] }.into()); + let _ = CurrencyOf::::set_balance(&creator, deposit); + let _ = Pallet::::create( + RawOrigin::Signed(creator.clone()).into(), + deposit, + min_contribution, + cap, + end, + Some(call), + Some(target_address), + ); + + // setup contributor + let contributor: T::AccountId = account::("contributor", 0, SEED); + let amount: BalanceOf = min_contribution; + let crowdloan_id: CrowdloanId = 0; + let _ = CurrencyOf::::set_balance(&contributor, amount); + + #[extrinsic_call] + _(RawOrigin::Signed(contributor.clone()), crowdloan_id, amount); + + // ensure the contribution is stored correctly + assert_eq!( + Contributions::::get(crowdloan_id, &contributor), + Some(amount) + ); + // ensure the contributor has been deducted the amount + assert!(CurrencyOf::::balance(&contributor) == 0); + // ensure the crowdloan raised amount is updated correctly + assert!(Crowdloans::::get(crowdloan_id).is_some_and(|c| c.raised == deposit + amount)); + // ensure the contribution is present in the crowdloan account + assert_eq!( + CurrencyOf::::balance(&Pallet::::funds_account(crowdloan_id)), + deposit + amount + ); + // ensure the event is emitted + assert_last_event::( + Event::::Contributed { + contributor, + crowdloan_id, + amount, + } + .into(), + ); + } + + #[benchmark] + fn withdraw() { + // create a crowdloan + let creator: T::AccountId = account::("creator", 0, SEED); + let deposit = T::MinimumDeposit::get(); + let min_contribution = T::AbsoluteMinimumContribution::get(); + let cap = deposit + deposit; + let now = frame_system::Pallet::::block_number(); + let end = now + T::MaximumBlockDuration::get(); + let target_address: T::AccountId = account::("target_address", 0, SEED); + let call: Box<::RuntimeCall> = + Box::new(frame_system::Call::::remark { remark: vec![] }.into()); + let _ = CurrencyOf::::set_balance(&creator, deposit); + let _ = Pallet::::create( + RawOrigin::Signed(creator.clone()).into(), + deposit, + min_contribution, + cap, + end, + Some(call), + Some(target_address), + ); + + // create contribution + let contributor: T::AccountId = account::("contributor", 0, SEED); + let amount: BalanceOf = min_contribution; + let crowdloan_id: CrowdloanId = 0; + let _ = CurrencyOf::::set_balance(&contributor, amount); + let _ = Pallet::::contribute( + RawOrigin::Signed(contributor.clone()).into(), + crowdloan_id, + amount, + ); + + // run to the end of the contribution period + frame_system::Pallet::::set_block_number(end); + + #[extrinsic_call] + _( + RawOrigin::Signed(contributor.clone()), + contributor.clone(), + crowdloan_id, + ); + + // ensure the creator contribution has been removed + assert_eq!(Contributions::::get(crowdloan_id, &contributor), None); + // ensure the contributor has his contribution back in his balance + assert_eq!(CurrencyOf::::balance(&contributor), amount); + // ensure the crowdloan account has been deducted the contribution + assert_eq!( + CurrencyOf::::balance(&Pallet::::funds_account(crowdloan_id)), + deposit + ); + // ensure the crowdloan raised amount is updated correctly + assert!(Crowdloans::::get(crowdloan_id).is_some_and(|c| c.raised == deposit)); + // ensure the event is emitted + assert_last_event::( + Event::::Withdrew { + contributor, + crowdloan_id, + amount, + } + .into(), + ); + } + + #[benchmark] + fn finalize() { + // create a crowdloan + let creator: T::AccountId = account::("creator", 0, SEED); + let deposit = T::MinimumDeposit::get(); + let min_contribution = T::AbsoluteMinimumContribution::get(); + let cap = deposit + deposit; + let now = frame_system::Pallet::::block_number(); + let end = now + T::MaximumBlockDuration::get(); + let target_address: T::AccountId = account::("target_address", 0, SEED); + let call: Box<::RuntimeCall> = + Box::new(frame_system::Call::::remark { remark: vec![] }.into()); + let _ = CurrencyOf::::set_balance(&creator, deposit); + let _ = Pallet::::create( + RawOrigin::Signed(creator.clone()).into(), + deposit, + min_contribution, + cap, + end, + Some(call), + Some(target_address.clone()), + ); + + // create contribution fullfilling the cap + let crowdloan_id: CrowdloanId = 0; + let contributor: T::AccountId = account::("contributor", 0, SEED); + let amount: BalanceOf = cap - deposit; + let _ = CurrencyOf::::set_balance(&contributor, amount); + let _ = Pallet::::contribute( + RawOrigin::Signed(contributor.clone()).into(), + crowdloan_id, + amount, + ); + + // run to the end of the contribution period + frame_system::Pallet::::set_block_number(end); + + #[extrinsic_call] + _(RawOrigin::Signed(creator.clone()), crowdloan_id); + + // ensure the target address has received the raised amount + assert_eq!(CurrencyOf::::balance(&target_address), deposit + amount); + // ensure the crowdloan has been finalized + assert!(Crowdloans::::get(crowdloan_id).is_some_and(|c| c.finalized)); + // ensure the event is emitted + assert_last_event::(Event::::Finalized { crowdloan_id }.into()); + } + + #[benchmark] + fn refund(k: Linear<3, { T::RefundContributorsLimit::get() }>) { + // create a crowdloan + let creator: T::AccountId = account::("creator", 0, SEED); + let deposit = T::MinimumDeposit::get(); + let min_contribution = T::AbsoluteMinimumContribution::get(); + let cap = deposit + deposit; + let now = frame_system::Pallet::::block_number(); + let end = now + T::MaximumBlockDuration::get(); + let target_address: T::AccountId = account::("target_address", 0, SEED); + let call: Box<::RuntimeCall> = + Box::new(frame_system::Call::::remark { remark: vec![] }.into()); + let _ = CurrencyOf::::set_balance(&creator, deposit); + let _ = Pallet::::create( + RawOrigin::Signed(creator.clone()).into(), + deposit, + min_contribution, + cap, + end, + Some(call), + Some(target_address), + ); + + let crowdloan_id: CrowdloanId = 0; + let amount: BalanceOf = min_contribution; + // create the worst case count of contributors k to be refunded minus the creator + // who is already a contributor + let contributors = k - 1; + for i in 0..contributors { + let contributor: T::AccountId = account::("contributor", i, SEED); + let _ = CurrencyOf::::set_balance(&contributor, amount); + let _ = Pallet::::contribute( + RawOrigin::Signed(contributor.clone()).into(), + crowdloan_id, + amount, + ); + } + + // run to the end of the contribution period + frame_system::Pallet::::set_block_number(end); + + #[extrinsic_call] + _(RawOrigin::Signed(creator.clone()), crowdloan_id); + + // ensure the creator has been refunded and the contributions is removed + assert_eq!(CurrencyOf::::balance(&creator), deposit); + assert_eq!(Contributions::::get(crowdloan_id, &creator), None); + // ensure each contributor has been refunded and the contributions is removed + for i in 0..contributors { + let contributor: T::AccountId = account::("contributor", i, SEED); + assert_eq!(CurrencyOf::::balance(&contributor), amount); + assert_eq!(Contributions::::get(crowdloan_id, &contributor), None); + } + // ensure the crowdloan account has been deducted the contributions + assert_eq!( + CurrencyOf::::balance(&Pallet::::funds_account(crowdloan_id)), + 0 + ); + // ensure the raised amount is updated correctly + assert!(Crowdloans::::get(crowdloan_id).is_some_and(|c| c.raised == 0)); + // ensure the event is emitted + assert_last_event::(Event::::AllRefunded { crowdloan_id }.into()); + } + + #[benchmark] + fn dissolve() { + // create a crowdloan + let creator: T::AccountId = account::("creator", 0, SEED); + let deposit = T::MinimumDeposit::get(); + let min_contribution = T::AbsoluteMinimumContribution::get(); + let cap = deposit + deposit; + let now = frame_system::Pallet::::block_number(); + let end = now + T::MaximumBlockDuration::get(); + let target_address: T::AccountId = account::("target_address", 0, SEED); + let call: Box<::RuntimeCall> = + Box::new(frame_system::Call::::remark { remark: vec![] }.into()); + let _ = CurrencyOf::::set_balance(&creator, deposit); + let _ = Pallet::::create( + RawOrigin::Signed(creator.clone()).into(), + deposit, + min_contribution, + cap, + end, + Some(call), + Some(target_address), + ); + + // run to the end of the contribution period + frame_system::Pallet::::set_block_number(end); + + // refund the contributions + let crowdloan_id: CrowdloanId = 0; + let _ = Pallet::::refund(RawOrigin::Signed(creator.clone()).into(), crowdloan_id); + + #[extrinsic_call] + _(RawOrigin::Signed(creator.clone()), crowdloan_id); + + // ensure the crowdloan has been dissolved + assert!(Crowdloans::::get(crowdloan_id).is_none()); + // ensure the event is emitted + assert_last_event::(Event::::Dissolved { crowdloan_id }.into()); + } + + #[benchmark] + fn update_min_contribution() { + // create a crowdloan + let creator: T::AccountId = account::("creator", 0, SEED); + let deposit = T::MinimumDeposit::get(); + let min_contribution = T::AbsoluteMinimumContribution::get(); + let cap = deposit + deposit; + let end = frame_system::Pallet::::block_number() + T::MaximumBlockDuration::get(); + let call: Box<::RuntimeCall> = + Box::new(frame_system::Call::::remark { remark: vec![] }.into()); + let _ = CurrencyOf::::set_balance(&creator, deposit); + let _ = Pallet::::create( + RawOrigin::Signed(creator.clone()).into(), + deposit, + min_contribution, + cap, + end, + Some(call), + None, + ); + + let crowdloan_id: CrowdloanId = 0; + let new_min_contribution: BalanceOf = min_contribution + min_contribution; + + #[extrinsic_call] + _( + RawOrigin::Signed(creator.clone()), + crowdloan_id, + new_min_contribution, + ); + + // ensure the min contribution is updated correctly + assert!( + Crowdloans::::get(crowdloan_id) + .is_some_and(|c| c.min_contribution == new_min_contribution) + ); + // ensure the event is emitted + assert_last_event::( + Event::::MinContributionUpdated { + crowdloan_id, + new_min_contribution, + } + .into(), + ); + } + + #[benchmark] + fn update_end() { + // create a crowdloan + let creator: T::AccountId = account::("creator", 0, SEED); + let deposit = T::MinimumDeposit::get(); + let min_contribution = T::AbsoluteMinimumContribution::get(); + let cap = deposit + deposit; + let now = frame_system::Pallet::::block_number(); + let end = now + T::MinimumBlockDuration::get(); + let call: Box<::RuntimeCall> = + Box::new(frame_system::Call::::remark { remark: vec![] }.into()); + let _ = CurrencyOf::::set_balance(&creator, deposit); + let _ = Pallet::::create( + RawOrigin::Signed(creator.clone()).into(), + deposit, + min_contribution, + cap, + end, + Some(call), + None, + ); + + let crowdloan_id: CrowdloanId = 0; + let new_end: BlockNumberFor = now + T::MaximumBlockDuration::get(); + + #[extrinsic_call] + _(RawOrigin::Signed(creator.clone()), crowdloan_id, new_end); + + // ensure the end is updated correctly + assert!(Crowdloans::::get(crowdloan_id).is_some_and(|c| c.end == new_end)); + // ensure the event is emitted + assert_last_event::( + Event::::EndUpdated { + crowdloan_id, + new_end, + } + .into(), + ); + } + + #[benchmark] + fn update_cap() { + // create a crowdloan + let creator: T::AccountId = account::("creator", 0, SEED); + let deposit = T::MinimumDeposit::get(); + let min_contribution = T::AbsoluteMinimumContribution::get(); + let cap = deposit + deposit; + let end = frame_system::Pallet::::block_number() + T::MaximumBlockDuration::get(); + let call: Box<::RuntimeCall> = + Box::new(frame_system::Call::::remark { remark: vec![] }.into()); + let _ = CurrencyOf::::set_balance(&creator, deposit); + let _ = Pallet::::create( + RawOrigin::Signed(creator.clone()).into(), + deposit, + min_contribution, + cap, + end, + Some(call), + None, + ); + + let crowdloan_id: CrowdloanId = 0; + let new_cap: BalanceOf = cap + cap; + + #[extrinsic_call] + _(RawOrigin::Signed(creator.clone()), crowdloan_id, new_cap); + + // ensure the cap is updated correctly + assert!(Crowdloans::::get(crowdloan_id).is_some_and(|c| c.cap == new_cap)); + // ensure the event is emitted + assert_last_event::( + Event::::CapUpdated { + crowdloan_id, + new_cap, + } + .into(), + ); + } + + impl_benchmark_test_suite!(Pallet, crate::mock::new_test_ext(), crate::mock::Test); +} diff --git a/pallets/crowdloan/src/lib.rs b/pallets/crowdloan/src/lib.rs new file mode 100644 index 0000000000..5413bd689b --- /dev/null +++ b/pallets/crowdloan/src/lib.rs @@ -0,0 +1,812 @@ +//! # Crowdloan Pallet +//! +//! A pallet allowing users to create generic crowdloans and contribute to them, +//! the raised funds are then transferred to a target address and an extrinsic +//! is dispatched, making it reusable for any crowdloan type. +#![cfg_attr(not(feature = "std"), no_std)] + +extern crate alloc; + +use alloc::{boxed::Box, vec, vec::Vec}; +use codec::{Decode, Encode}; +use frame_support::{ + PalletId, + dispatch::GetDispatchInfo, + pallet_prelude::*, + sp_runtime::{ + RuntimeDebug, + traits::{AccountIdConversion, Dispatchable, Zero}, + }, + traits::{ + Bounded, Defensive, Get, IsSubType, QueryPreimage, StorePreimage, fungible, fungible::*, + tokens::Preservation, + }, +}; +use frame_system::pallet_prelude::*; +use scale_info::TypeInfo; +use sp_runtime::traits::CheckedSub; +use weights::WeightInfo; + +pub use pallet::*; +use subtensor_macros::freeze_struct; + +pub type CrowdloanId = u32; + +mod benchmarking; +mod mock; +mod tests; +pub mod weights; + +pub type CurrencyOf = ::Currency; + +pub type BalanceOf = + as fungible::Inspect<::AccountId>>::Balance; + +pub type BoundedCallOf = + Bounded<::RuntimeCall, ::Hashing>; + +/// A struct containing the information about a crowdloan. +#[freeze_struct("6b86ccf70fc1b8f1")] +#[derive(Encode, Decode, Eq, PartialEq, Ord, PartialOrd, RuntimeDebug, TypeInfo, MaxEncodedLen)] +pub struct CrowdloanInfo { + /// The creator of the crowdloan. + pub creator: AccountId, + /// The initial deposit of the crowdloan from the creator. + pub deposit: Balance, + /// Minimum contribution to the crowdloan. + pub min_contribution: Balance, + /// The end block of the crowdloan. + pub end: BlockNumber, + /// The cap to raise. + pub cap: Balance, + /// The account holding the funds for this crowdloan. Derived on chain but put here for ease of use. + pub funds_account: AccountId, + /// The amount raised so far. + pub raised: Balance, + /// The optional target address to transfer the raised funds to, if not + /// provided, it means the funds will be transferred from on chain logic + /// inside the provided call to dispatch. + pub target_address: Option, + /// The optional call to dispatch when the crowdloan is finalized. + pub call: Option, + /// Whether the crowdloan has been finalized. + pub finalized: bool, +} + +pub type CrowdloanInfoOf = CrowdloanInfo< + ::AccountId, + BalanceOf, + BlockNumberFor, + BoundedCallOf, +>; + +#[frame_support::pallet] +pub mod pallet { + use super::*; + + #[pallet::pallet] + pub struct Pallet(_); + + /// Configuration trait. + #[pallet::config] + pub trait Config: frame_system::Config { + /// The overarching event type. + type RuntimeEvent: From> + IsType<::RuntimeEvent>; + + /// The overarching call type. + type RuntimeCall: Parameter + + Dispatchable + + GetDispatchInfo + + From> + + IsSubType> + + IsType<::RuntimeCall>; + + /// The currency mechanism. + type Currency: fungible::Balanced + + fungible::Mutate; + + /// The weight information for the pallet. + type WeightInfo: WeightInfo; + + /// The preimage provider which will be used to store the call to dispatch. + type Preimages: QueryPreimage + StorePreimage; + + /// The pallet id that will be used to derive crowdloan account ids. + #[pallet::constant] + type PalletId: Get; + + /// The minimum deposit required to create a crowdloan. + #[pallet::constant] + type MinimumDeposit: Get>; + + /// The absolute minimum contribution required to contribute to a crowdloan. + #[pallet::constant] + type AbsoluteMinimumContribution: Get>; + + /// The minimum block duration for a crowdloan. + #[pallet::constant] + type MinimumBlockDuration: Get>; + + /// The maximum block duration for a crowdloan. + #[pallet::constant] + type MaximumBlockDuration: Get>; + + /// The maximum number of contributors that can be refunded in a single refund. + #[pallet::constant] + type RefundContributorsLimit: Get; + } + + /// A map of crowdloan ids to their information. + #[pallet::storage] + pub type Crowdloans = + StorageMap<_, Twox64Concat, CrowdloanId, CrowdloanInfoOf, OptionQuery>; + + /// The next incrementing crowdloan id. + #[pallet::storage] + pub type NextCrowdloanId = StorageValue<_, CrowdloanId, ValueQuery, ConstU32<0>>; + + /// A map of crowdloan ids to their contributors and their contributions. + #[pallet::storage] + pub type Contributions = StorageDoubleMap< + _, + Twox64Concat, + CrowdloanId, + Identity, + T::AccountId, + BalanceOf, + OptionQuery, + >; + + /// The current crowdloan id that will be set during the finalize call, making it + /// temporarily accessible to the dispatched call. + #[pallet::storage] + pub type CurrentCrowdloanId = StorageValue<_, CrowdloanId, OptionQuery>; + + #[pallet::event] + #[pallet::generate_deposit(pub(super) fn deposit_event)] + pub enum Event { + /// A crowdloan was created. + Created { + crowdloan_id: CrowdloanId, + creator: T::AccountId, + end: BlockNumberFor, + cap: BalanceOf, + }, + /// A contribution was made to an active crowdloan. + Contributed { + crowdloan_id: CrowdloanId, + contributor: T::AccountId, + amount: BalanceOf, + }, + /// A contribution was withdrawn from a failed crowdloan. + Withdrew { + crowdloan_id: CrowdloanId, + contributor: T::AccountId, + amount: BalanceOf, + }, + /// A refund was partially processed for a failed crowdloan. + PartiallyRefunded { crowdloan_id: CrowdloanId }, + /// A refund was fully processed for a failed crowdloan. + AllRefunded { crowdloan_id: CrowdloanId }, + /// A crowdloan was finalized, funds were transferred and the call was dispatched. + Finalized { crowdloan_id: CrowdloanId }, + /// A crowdloan was dissolved. + Dissolved { crowdloan_id: CrowdloanId }, + /// The minimum contribution was updated. + MinContributionUpdated { + crowdloan_id: CrowdloanId, + new_min_contribution: BalanceOf, + }, + /// The end was updated. + EndUpdated { + crowdloan_id: CrowdloanId, + new_end: BlockNumberFor, + }, + /// The cap was updated. + CapUpdated { + crowdloan_id: CrowdloanId, + new_cap: BalanceOf, + }, + } + + #[pallet::error] + pub enum Error { + /// The crowdloan initial deposit is too low. + DepositTooLow, + /// The crowdloan cap is too low. + CapTooLow, + /// The minimum contribution is too low. + MinimumContributionTooLow, + /// The crowdloan cannot end in the past. + CannotEndInPast, + /// The crowdloan block duration is too short. + BlockDurationTooShort, + /// The block duration is too long. + BlockDurationTooLong, + /// The account does not have enough balance to pay for the initial deposit/contribution. + InsufficientBalance, + /// An overflow occurred. + Overflow, + /// The crowdloan id is invalid. + InvalidCrowdloanId, + /// The crowdloan cap has been fully raised. + CapRaised, + /// The contribution period has ended. + ContributionPeriodEnded, + /// The contribution is too low. + ContributionTooLow, + /// The origin of this call is invalid. + InvalidOrigin, + /// The crowdloan has already been finalized. + AlreadyFinalized, + /// The crowdloan contribution period has not ended yet. + ContributionPeriodNotEnded, + /// The contributor has no contribution for this crowdloan. + NoContribution, + /// The crowdloan cap has not been raised. + CapNotRaised, + /// An underflow occurred. + Underflow, + /// Call to dispatch was not found in the preimage storage. + CallUnavailable, + /// The crowdloan is not ready to be dissolved, it still has contributions. + NotReadyToDissolve, + } + + #[pallet::call] + impl Pallet { + /// Create a crowdloan that will raise funds up to a maximum cap and if successful, + /// will transfer funds to the target address if provided and dispatch the call + /// (using creator origin). + /// + /// The initial deposit will be transfered to the crowdloan account and will be refunded + /// in case the crowdloan fails to raise the cap. Additionally, the creator will pay for + /// the execution of the call + /// + /// The dispatch origin for this call must be _Signed_. + /// + /// Parameters: + /// - `deposit`: The initial deposit from the creator. + /// - `min_contribution`: The minimum contribution required to contribute to the crowdloan. + /// - `cap`: The maximum amount of funds that can be raised. + /// - `end`: The block number at which the crowdloan will end. + /// - `call`: The call to dispatch when the crowdloan is finalized. + /// - `target_address`: The address to transfer the raised funds to if provided. + #[pallet::call_index(0)] + #[pallet::weight({ + let di = call.as_ref().map(|c| c.get_dispatch_info()); + let inner_call_weight = match di { + Some(di) => di.weight, + None => Weight::zero(), + }; + let base_weight = T::WeightInfo::create(); + (base_weight.saturating_add(inner_call_weight), Pays::Yes) + })] + pub fn create( + origin: OriginFor, + #[pallet::compact] deposit: BalanceOf, + #[pallet::compact] min_contribution: BalanceOf, + #[pallet::compact] cap: BalanceOf, + #[pallet::compact] end: BlockNumberFor, + call: Option::RuntimeCall>>, + target_address: Option, + ) -> DispatchResult { + let creator = ensure_signed(origin)?; + let now = frame_system::Pallet::::block_number(); + + // Ensure the deposit is at least the minimum deposit, cap is greater than deposit + // and the minimum contribution is greater than the absolute minimum contribution. + ensure!( + deposit >= T::MinimumDeposit::get(), + Error::::DepositTooLow + ); + ensure!(cap > deposit, Error::::CapTooLow); + ensure!( + min_contribution >= T::AbsoluteMinimumContribution::get(), + Error::::MinimumContributionTooLow + ); + + Self::ensure_valid_end(now, end)?; + + // Ensure the creator has enough balance to pay the initial deposit + ensure!( + CurrencyOf::::balance(&creator) >= deposit, + Error::::InsufficientBalance + ); + + let crowdloan_id = NextCrowdloanId::::get(); + let next_crowdloan_id = crowdloan_id.checked_add(1).ok_or(Error::::Overflow)?; + NextCrowdloanId::::put(next_crowdloan_id); + + // Derive the funds account and keep track of it + let funds_account = Self::funds_account(crowdloan_id); + frame_system::Pallet::::inc_providers(&funds_account); + + // If the call is provided, bound it and store it in the preimage storage + let call = if let Some(call) = call { + Some(T::Preimages::bound(*call)?) + } else { + None + }; + + let crowdloan = CrowdloanInfo { + creator: creator.clone(), + deposit, + min_contribution, + end, + cap, + funds_account, + raised: deposit, + target_address, + call, + finalized: false, + }; + Crowdloans::::insert(crowdloan_id, &crowdloan); + + // Transfer the deposit to the funds account + CurrencyOf::::transfer( + &creator, + &crowdloan.funds_account, + deposit, + Preservation::Expendable, + )?; + + Contributions::::insert(crowdloan_id, &creator, deposit); + + Self::deposit_event(Event::::Created { + crowdloan_id, + creator, + end, + cap, + }); + + Ok(()) + } + + /// Contribute to an active crowdloan. + /// + /// The contribution will be transfered to the crowdloan account and will be refunded + /// if the crowdloan fails to raise the cap. If the contribution would raise the amount above the cap, + /// the contribution will be set to the amount that is left to be raised. + /// + /// The dispatch origin for this call must be _Signed_. + /// + /// Parameters: + /// - `crowdloan_id`: The id of the crowdloan to contribute to. + /// - `amount`: The amount to contribute. + #[pallet::call_index(1)] + #[pallet::weight(T::WeightInfo::contribute())] + pub fn contribute( + origin: OriginFor, + #[pallet::compact] crowdloan_id: CrowdloanId, + #[pallet::compact] amount: BalanceOf, + ) -> DispatchResult { + let contributor = ensure_signed(origin)?; + let now = frame_system::Pallet::::block_number(); + + let mut crowdloan = Self::ensure_crowdloan_exists(crowdloan_id)?; + + // Ensure crowdloan has not ended and has not raised cap + ensure!(now < crowdloan.end, Error::::ContributionPeriodEnded); + ensure!(crowdloan.raised < crowdloan.cap, Error::::CapRaised); + + // Ensure contribution is at least the minimum contribution + ensure!( + amount >= crowdloan.min_contribution, + Error::::ContributionTooLow + ); + + // Ensure contribution does not overflow the actual raised amount + // and it does not exceed the cap + let left_to_raise = crowdloan + .cap + .checked_sub(crowdloan.raised) + .ok_or(Error::::Underflow)?; + + // If the contribution would raise the amount above the cap, + // set the contribution to the amount that is left to be raised + let amount = amount.min(left_to_raise); + + // Ensure contribution does not overflow the actual raised amount + crowdloan.raised = crowdloan + .raised + .checked_add(amount) + .ok_or(Error::::Overflow)?; + + // Compute the new total contribution and ensure it does not overflow. + let contribution = Contributions::::get(crowdloan_id, &contributor) + .unwrap_or(Zero::zero()) + .checked_add(amount) + .ok_or(Error::::Overflow)?; + + // Ensure contributor has enough balance to pay + ensure!( + CurrencyOf::::balance(&contributor) >= amount, + Error::::InsufficientBalance + ); + + CurrencyOf::::transfer( + &contributor, + &crowdloan.funds_account, + amount, + Preservation::Expendable, + )?; + + Contributions::::insert(crowdloan_id, &contributor, contribution); + Crowdloans::::insert(crowdloan_id, &crowdloan); + + Self::deposit_event(Event::::Contributed { + crowdloan_id, + contributor, + amount, + }); + + Ok(()) + } + + /// Withdraw a contribution from an active (not yet finalized or dissolved) crowdloan. + /// + /// The origin doesn't needs to be the contributor, it can be any account, + /// making it possible for someone to trigger a refund for a contributor. + /// + /// The dispatch origin for this call must be _Signed_. + /// + /// Parameters: + /// - `contributor`: The contributor to withdraw from. + /// - `crowdloan_id`: The id of the crowdloan to withdraw from. + #[pallet::call_index(2)] + #[pallet::weight(T::WeightInfo::withdraw())] + pub fn withdraw( + origin: OriginFor, + contributor: T::AccountId, + #[pallet::compact] crowdloan_id: CrowdloanId, + ) -> DispatchResult { + ensure_signed(origin)?; + + let mut crowdloan = Self::ensure_crowdloan_exists(crowdloan_id)?; + ensure!(!crowdloan.finalized, Error::::AlreadyFinalized); + + // Ensure contributor has balance left in the crowdloan account + let amount = + Contributions::::get(crowdloan_id, &contributor).unwrap_or_else(Zero::zero); + ensure!(amount > Zero::zero(), Error::::NoContribution); + + CurrencyOf::::transfer( + &crowdloan.funds_account, + &contributor, + amount, + Preservation::Expendable, + )?; + + // Remove the contribution from the contributions map and update + // crowdloan raised amount to reflect the withdrawal. + Contributions::::remove(crowdloan_id, &contributor); + crowdloan.raised = crowdloan.raised.saturating_sub(amount); + + Crowdloans::::insert(crowdloan_id, &crowdloan); + + Self::deposit_event(Event::::Withdrew { + contributor, + crowdloan_id, + amount, + }); + + Ok(()) + } + + /// Finalize a successful crowdloan. + /// + /// The call will transfer the raised amount to the target address if it was provided when the crowdloan was created + /// and dispatch the call that was provided using the creator origin. The CurrentCrowdloanId will be set to the + /// crowdloan id being finalized so the dispatched call can access it temporarily by accessing + /// the `CurrentCrowdloanId` storage item. + /// + /// The dispatch origin for this call must be _Signed_ and must be the creator of the crowdloan. + /// + /// Parameters: + /// - `crowdloan_id`: The id of the crowdloan to finalize. + #[pallet::call_index(3)] + #[pallet::weight(T::WeightInfo::finalize())] + pub fn finalize( + origin: OriginFor, + #[pallet::compact] crowdloan_id: CrowdloanId, + ) -> DispatchResult { + let who = ensure_signed(origin)?; + let now = frame_system::Pallet::::block_number(); + + let mut crowdloan = Self::ensure_crowdloan_exists(crowdloan_id)?; + + // Ensure the origin is the creator of the crowdloan and the crowdloan has ended, + // raised the cap and is not finalized. + ensure!(who == crowdloan.creator, Error::::InvalidOrigin); + ensure!(now >= crowdloan.end, Error::::ContributionPeriodNotEnded); + ensure!(crowdloan.raised == crowdloan.cap, Error::::CapNotRaised); + ensure!(!crowdloan.finalized, Error::::AlreadyFinalized); + + // If the target address is provided, transfer the raised amount to it. + if let Some(ref target_address) = crowdloan.target_address { + CurrencyOf::::transfer( + &crowdloan.funds_account, + target_address, + crowdloan.raised, + Preservation::Expendable, + )?; + } + + // If the call is provided, dispatch it. + if let Some(ref call) = crowdloan.call { + // Set the current crowdloan id so the dispatched call + // can access it temporarily + CurrentCrowdloanId::::put(crowdloan_id); + + // Retrieve the call from the preimage storage + let stored_call = match T::Preimages::peek(call) { + Ok((call, _)) => call, + Err(_) => { + // If the call is not found, we drop it from the preimage storage + // because it's not needed anymore + T::Preimages::drop(call); + return Err(Error::::CallUnavailable)?; + } + }; + + // Dispatch the call with creator origin + stored_call + .dispatch(frame_system::RawOrigin::Signed(who).into()) + .map(|_| ()) + .map_err(|e| e.error)?; + + // Clear the current crowdloan id + CurrentCrowdloanId::::kill(); + } + + crowdloan.finalized = true; + Crowdloans::::insert(crowdloan_id, &crowdloan); + + Self::deposit_event(Event::::Finalized { crowdloan_id }); + + Ok(()) + } + + /// Refund a failed crowdloan. + /// + /// The call will try to refund all contributors up to the limit defined by the `RefundContributorsLimit`. + /// If the limit is reached, the call will stop and the crowdloan will be marked as partially refunded. + /// It may be needed to dispatch this call multiple times to refund all contributors. + /// + /// The dispatch origin for this call must be _Signed_ and doesn't need to be the creator of the crowdloan. + /// + /// Parameters: + /// - `crowdloan_id`: The id of the crowdloan to refund. + #[pallet::call_index(4)] + #[pallet::weight(T::WeightInfo::refund(T::RefundContributorsLimit::get()))] + pub fn refund( + origin: OriginFor, + #[pallet::compact] crowdloan_id: CrowdloanId, + ) -> DispatchResultWithPostInfo { + let now = frame_system::Pallet::::block_number(); + ensure_signed(origin)?; + + let mut crowdloan = Self::ensure_crowdloan_exists(crowdloan_id)?; + + // Ensure the crowdloan has ended and is not finalized + ensure!(now >= crowdloan.end, Error::::ContributionPeriodNotEnded); + ensure!(!crowdloan.finalized, Error::::AlreadyFinalized); + + let mut refunded_contributors: Vec = vec![]; + let mut refund_count = 0; + // Assume everyone can be refunded + let mut all_refunded = true; + let contributions = Contributions::::iter_prefix(crowdloan_id); + for (contributor, amount) in contributions { + if refund_count >= T::RefundContributorsLimit::get() { + // Not everyone can be refunded + all_refunded = false; + break; + } + + CurrencyOf::::transfer( + &crowdloan.funds_account, + &contributor, + amount, + Preservation::Expendable, + )?; + + refunded_contributors.push(contributor); + crowdloan.raised = crowdloan.raised.saturating_sub(amount); + refund_count = refund_count.checked_add(1).ok_or(Error::::Overflow)?; + } + + Crowdloans::::insert(crowdloan_id, &crowdloan); + + // Clear refunded contributors + for contributor in refunded_contributors { + Contributions::::remove(crowdloan_id, &contributor); + } + + if all_refunded { + Self::deposit_event(Event::::AllRefunded { crowdloan_id }); + // The loop didn't run fully, we refund the unused weights. + Ok(Some(T::WeightInfo::refund(refund_count)).into()) + } else { + Self::deposit_event(Event::::PartiallyRefunded { crowdloan_id }); + // The loop ran fully, we don't refund anything. + Ok(().into()) + } + } + + /// Dissolve a crowdloan. + /// + /// The crowdloan will be removed from the storage. + /// All contributions must have been refunded before the crowdloan can be dissolved. + /// + /// The dispatch origin for this call must be _Signed_ and must be the creator of the crowdloan. + /// + /// Parameters: + /// - `crowdloan_id`: The id of the crowdloan to dissolve. + #[pallet::call_index(5)] + #[pallet::weight(T::WeightInfo::dissolve())] + pub fn dissolve( + origin: OriginFor, + #[pallet::compact] crowdloan_id: CrowdloanId, + ) -> DispatchResult { + let who = ensure_signed(origin)?; + + let crowdloan = Self::ensure_crowdloan_exists(crowdloan_id)?; + ensure!(!crowdloan.finalized, Error::::AlreadyFinalized); + + // Only the creator can dissolve the crowdloan + ensure!(who == crowdloan.creator, Error::::InvalidOrigin); + // It can only be dissolved if the raised amount is 0, meaning + // there is no contributions or every contribution has been refunded + ensure!(crowdloan.raised == 0, Error::::NotReadyToDissolve); + + // Clear the call from the preimage storage + if let Some(call) = crowdloan.call { + T::Preimages::drop(&call); + } + + // Remove the crowdloan + let _ = frame_system::Pallet::::dec_providers(&crowdloan.funds_account).defensive(); + Crowdloans::::remove(crowdloan_id); + + Self::deposit_event(Event::::Dissolved { crowdloan_id }); + Ok(()) + } + + /// Update the minimum contribution of a non-finalized crowdloan. + /// + /// The dispatch origin for this call must be _Signed_ and must be the creator of the crowdloan. + /// + /// Parameters: + /// - `crowdloan_id`: The id of the crowdloan to update the minimum contribution of. + /// - `new_min_contribution`: The new minimum contribution. + #[pallet::call_index(6)] + #[pallet::weight(T::WeightInfo::update_min_contribution())] + pub fn update_min_contribution( + origin: OriginFor, + #[pallet::compact] crowdloan_id: CrowdloanId, + #[pallet::compact] new_min_contribution: BalanceOf, + ) -> DispatchResult { + let who = ensure_signed(origin)?; + + let mut crowdloan = Self::ensure_crowdloan_exists(crowdloan_id)?; + ensure!(!crowdloan.finalized, Error::::AlreadyFinalized); + + // Only the creator can update the min contribution. + ensure!(who == crowdloan.creator, Error::::InvalidOrigin); + + // The new min contribution should be greater than absolute minimum contribution. + ensure!( + new_min_contribution > T::AbsoluteMinimumContribution::get(), + Error::::MinimumContributionTooLow + ); + + crowdloan.min_contribution = new_min_contribution; + Crowdloans::::insert(crowdloan_id, &crowdloan); + + Self::deposit_event(Event::::MinContributionUpdated { + crowdloan_id, + new_min_contribution, + }); + Ok(()) + } + + /// Update the end block of a non-finalized crowdloan. + /// + /// The dispatch origin for this call must be _Signed_ and must be the creator of the crowdloan. + /// + /// Parameters: + /// - `crowdloan_id`: The id of the crowdloan to update the end block of. + /// - `new_end`: The new end block. + #[pallet::call_index(7)] + #[pallet::weight(T::WeightInfo::update_end())] + pub fn update_end( + origin: OriginFor, + #[pallet::compact] crowdloan_id: CrowdloanId, + #[pallet::compact] new_end: BlockNumberFor, + ) -> DispatchResult { + let who = ensure_signed(origin)?; + let now = frame_system::Pallet::::block_number(); + + let mut crowdloan = Self::ensure_crowdloan_exists(crowdloan_id)?; + ensure!(!crowdloan.finalized, Error::::AlreadyFinalized); + + // Only the creator can update the min contribution. + ensure!(who == crowdloan.creator, Error::::InvalidOrigin); + + Self::ensure_valid_end(now, new_end)?; + + crowdloan.end = new_end; + Crowdloans::::insert(crowdloan_id, &crowdloan); + + Self::deposit_event(Event::::EndUpdated { + crowdloan_id, + new_end, + }); + Ok(()) + } + + /// Update the cap of a non-finalized crowdloan. + /// + /// The dispatch origin for this call must be _Signed_ and must be the creator of the crowdloan. + /// + /// Parameters: + /// - `crowdloan_id`: The id of the crowdloan to update the cap of. + /// - `new_cap`: The new cap. + #[pallet::call_index(8)] + #[pallet::weight(T::WeightInfo::update_cap())] + pub fn update_cap( + origin: OriginFor, + #[pallet::compact] crowdloan_id: CrowdloanId, + #[pallet::compact] new_cap: BalanceOf, + ) -> DispatchResult { + let who = ensure_signed(origin)?; + + // The cap can only be updated if the crowdloan has not been finalized. + let mut crowdloan = Self::ensure_crowdloan_exists(crowdloan_id)?; + ensure!(!crowdloan.finalized, Error::::AlreadyFinalized); + + // Only the creator can update the cap. + ensure!(who == crowdloan.creator, Error::::InvalidOrigin); + + // The new cap should be greater than the actual raised amount. + ensure!(new_cap > crowdloan.raised, Error::::CapTooLow); + + crowdloan.cap = new_cap; + Crowdloans::::insert(crowdloan_id, &crowdloan); + + Self::deposit_event(Event::::CapUpdated { + crowdloan_id, + new_cap, + }); + Ok(()) + } + } +} + +impl Pallet { + fn funds_account(id: CrowdloanId) -> T::AccountId { + T::PalletId::get().into_sub_account_truncating(id) + } + + fn ensure_crowdloan_exists(crowdloan_id: CrowdloanId) -> Result, Error> { + Crowdloans::::get(crowdloan_id).ok_or(Error::::InvalidCrowdloanId) + } + + // Ensure the provided end block is after the current block and the duration is + // between the minimum and maximum block duration + fn ensure_valid_end(now: BlockNumberFor, end: BlockNumberFor) -> Result<(), Error> { + ensure!(now < end, Error::::CannotEndInPast); + let block_duration = end.checked_sub(&now).ok_or(Error::::Underflow)?; + ensure!( + block_duration >= T::MinimumBlockDuration::get(), + Error::::BlockDurationTooShort + ); + ensure!( + block_duration <= T::MaximumBlockDuration::get(), + Error::::BlockDurationTooLong + ); + Ok(()) + } +} diff --git a/pallets/crowdloan/src/mock.rs b/pallets/crowdloan/src/mock.rs new file mode 100644 index 0000000000..980b9fa26b --- /dev/null +++ b/pallets/crowdloan/src/mock.rs @@ -0,0 +1,270 @@ +#![cfg(test)] +#![allow(clippy::arithmetic_side_effects, clippy::unwrap_used)] +use frame_support::{ + PalletId, derive_impl, parameter_types, + traits::{OnFinalize, OnInitialize, fungible, fungible::*, tokens::Preservation}, + weights::Weight, +}; +use frame_system::{EnsureRoot, pallet_prelude::BlockNumberFor}; +use sp_core::U256; +use sp_runtime::{BuildStorage, traits::IdentityLookup}; + +use crate::{BalanceOf, CrowdloanId, pallet as pallet_crowdloan, weights::WeightInfo}; + +type Block = frame_system::mocking::MockBlock; +pub(crate) type AccountOf = ::AccountId; + +frame_support::construct_runtime!( + pub enum Test + { + System: frame_system = 1, + Balances: pallet_balances = 2, + Crowdloan: pallet_crowdloan = 3, + Preimage: pallet_preimage = 4, + TestPallet: pallet_test = 5, + } +); + +#[allow(unused)] +pub(crate) fn new_test_ext() -> sp_io::TestExternalities { + let mut t = frame_system::GenesisConfig::::default() + .build_storage() + .expect("Expected to not panic"); + pallet_balances::GenesisConfig:: { + balances: vec![ + (U256::from(1), 10), + (U256::from(2), 10), + (U256::from(3), 10), + (U256::from(4), 10), + (U256::from(5), 3), + ], + } + .assimilate_storage(&mut t) + .expect("Expected to not panic"); + let mut ext = sp_io::TestExternalities::new(t); + ext.execute_with(|| System::set_block_number(1)); + ext +} + +#[derive_impl(frame_system::config_preludes::TestDefaultConfig)] +impl frame_system::Config for Test { + type Block = Block; + type AccountId = U256; + type AccountData = pallet_balances::AccountData; + type Lookup = IdentityLookup; +} + +#[derive_impl(pallet_balances::config_preludes::TestDefaultConfig)] +impl pallet_balances::Config for Test { + type AccountStore = System; +} + +pub struct TestWeightInfo; +impl WeightInfo for TestWeightInfo { + fn create() -> Weight { + Weight::zero() + } + fn contribute() -> Weight { + Weight::zero() + } + fn withdraw() -> Weight { + Weight::zero() + } + fn refund(_k: u32) -> Weight { + Weight::zero() + } + fn finalize() -> Weight { + Weight::zero() + } + fn dissolve() -> Weight { + Weight::zero() + } + fn update_min_contribution() -> Weight { + Weight::zero() + } + fn update_end() -> Weight { + Weight::zero() + } + fn update_cap() -> Weight { + Weight::zero() + } +} + +parameter_types! { + pub const PreimageMaxSize: u32 = 4096 * 1024; + pub const PreimageBaseDeposit: u64 = 1; + pub const PreimageByteDeposit: u64 = 1; +} + +impl pallet_preimage::Config for Test { + type WeightInfo = pallet_preimage::weights::SubstrateWeight; + type RuntimeEvent = RuntimeEvent; + type Currency = Balances; + type ManagerOrigin = EnsureRoot>; + type Consideration = (); +} + +parameter_types! { + pub const CrowdloanPalletId: PalletId = PalletId(*b"bt/cloan"); + pub const MinimumDeposit: u64 = 50; + pub const AbsoluteMinimumContribution: u64 = 10; + pub const MinimumBlockDuration: u64 = 20; + pub const MaximumBlockDuration: u64 = 100; + pub const RefundContributorsLimit: u32 = 5; +} + +impl pallet_crowdloan::Config for Test { + type PalletId = CrowdloanPalletId; + type Currency = Balances; + type RuntimeCall = RuntimeCall; + type RuntimeEvent = RuntimeEvent; + type WeightInfo = TestWeightInfo; + type Preimages = Preimage; + type MinimumDeposit = MinimumDeposit; + type AbsoluteMinimumContribution = AbsoluteMinimumContribution; + type MinimumBlockDuration = MinimumBlockDuration; + type MaximumBlockDuration = MaximumBlockDuration; + type RefundContributorsLimit = RefundContributorsLimit; +} + +// A test pallet used to test some behavior of the crowdloan pallet +#[allow(unused)] +#[frame_support::pallet(dev_mode)] +pub(crate) mod pallet_test { + use super::*; + use frame_support::{ + dispatch::DispatchResult, + pallet_prelude::{OptionQuery, StorageValue}, + }; + use frame_system::pallet_prelude::OriginFor; + + #[pallet::pallet] + pub struct Pallet(_); + + #[pallet::config] + pub trait Config: frame_system::Config + pallet_crowdloan::Config { + type Currency: fungible::Balanced + + fungible::Mutate; + } + + #[pallet::error] + pub enum Error { + ShouldFail, + MissingCurrentCrowdloanId, + CrowdloanDoesNotExist, + } + + #[pallet::storage] + pub type PassedCrowdloanId = StorageValue<_, CrowdloanId, OptionQuery>; + + #[pallet::call] + impl Pallet { + #[pallet::call_index(0)] + pub fn noop(origin: OriginFor) -> DispatchResult { + Ok(()) + } + + #[pallet::call_index(1)] + pub fn transfer_funds(origin: OriginFor, dest: AccountOf) -> DispatchResult { + let crowdloan_id = pallet_crowdloan::CurrentCrowdloanId::::get() + .ok_or(Error::::MissingCurrentCrowdloanId)?; + let crowdloan = pallet_crowdloan::Crowdloans::::get(crowdloan_id) + .ok_or(Error::::CrowdloanDoesNotExist)?; + + PassedCrowdloanId::::put(crowdloan_id); + + ::Currency::transfer( + &crowdloan.funds_account, + &dest, + crowdloan.raised, + Preservation::Expendable, + )?; + + Ok(()) + } + + #[pallet::call_index(2)] + pub fn set_passed_crowdloan_id(origin: OriginFor) -> DispatchResult { + let crowdloan_id = pallet_crowdloan::CurrentCrowdloanId::::get() + .ok_or(Error::::MissingCurrentCrowdloanId)?; + + PassedCrowdloanId::::put(crowdloan_id); + + Ok(()) + } + + #[pallet::call_index(3)] + pub fn failing_extrinsic(origin: OriginFor) -> DispatchResult { + Err(Error::::ShouldFail.into()) + } + } +} + +impl pallet_test::Config for Test { + type Currency = Balances; +} + +pub(crate) struct TestState { + block_number: BlockNumberFor, + balances: Vec<(AccountOf, BalanceOf)>, +} + +impl Default for TestState { + fn default() -> Self { + Self { + block_number: 1, + balances: vec![], + } + } +} + +impl TestState { + pub(crate) fn with_block_number(mut self, block_number: BlockNumberFor) -> Self { + self.block_number = block_number; + self + } + + pub(crate) fn with_balance(mut self, who: AccountOf, balance: BalanceOf) -> Self { + self.balances.push((who, balance)); + self + } + + pub(crate) fn build_and_execute(self, test: impl FnOnce()) { + let mut t = frame_system::GenesisConfig::::default() + .build_storage() + .unwrap(); + + pallet_balances::GenesisConfig:: { + balances: self + .balances + .iter() + .map(|(who, balance)| (*who, *balance)) + .collect::>(), + } + .assimilate_storage(&mut t) + .unwrap(); + + let mut ext = sp_io::TestExternalities::new(t); + ext.execute_with(|| System::set_block_number(self.block_number)); + ext.execute_with(test); + } +} + +pub(crate) fn last_event() -> RuntimeEvent { + System::events().pop().expect("RuntimeEvent expected").event +} + +pub(crate) fn run_to_block(n: u64) { + while System::block_number() < n { + System::on_finalize(System::block_number()); + Balances::on_finalize(System::block_number()); + System::reset_events(); + System::set_block_number(System::block_number() + 1); + Balances::on_initialize(System::block_number()); + System::on_initialize(System::block_number()); + } +} + +pub(crate) fn noop_call() -> Box { + Box::new(RuntimeCall::TestPallet(pallet_test::Call::::noop {})) +} diff --git a/pallets/crowdloan/src/tests.rs b/pallets/crowdloan/src/tests.rs new file mode 100644 index 0000000000..59bfea6b83 --- /dev/null +++ b/pallets/crowdloan/src/tests.rs @@ -0,0 +1,2398 @@ +#![cfg(test)] +#![allow(clippy::arithmetic_side_effects, clippy::unwrap_used)] + +use frame_support::{assert_err, assert_ok, traits::StorePreimage}; +use frame_system::pallet_prelude::BlockNumberFor; +use sp_core::U256; +use sp_runtime::DispatchError; + +use crate::{BalanceOf, CrowdloanId, CrowdloanInfo, mock::*, pallet as pallet_crowdloan}; + +#[test] +fn test_create_succeeds() { + TestState::default() + .with_balance(U256::from(1), 100) + .build_and_execute(|| { + let creator: AccountOf = U256::from(1); + let deposit: BalanceOf = 50; + let min_contribution: BalanceOf = 10; + let cap: BalanceOf = 300; + let end: BlockNumberFor = 50; + + assert_ok!(Crowdloan::create( + RuntimeOrigin::signed(creator), + deposit, + min_contribution, + cap, + end, + Some(noop_call()), + None, + )); + + let crowdloan_id = 0; + let funds_account = pallet_crowdloan::Pallet::::funds_account(crowdloan_id); + // ensure the crowdloan is stored correctly + let call = pallet_preimage::Pallet::::bound(*noop_call()).unwrap(); + assert_eq!( + pallet_crowdloan::Crowdloans::::get(crowdloan_id), + Some(CrowdloanInfo { + creator, + deposit, + min_contribution, + cap, + end, + funds_account, + raised: deposit, + target_address: None, + call: Some(call), + finalized: false, + }) + ); + // ensure the crowdloan account has the deposit + assert_eq!(Balances::free_balance(funds_account), deposit); + // ensure the creator has been deducted the deposit + assert_eq!(Balances::free_balance(creator), 100 - deposit); + // ensure the contributions have been updated + assert_eq!( + pallet_crowdloan::Contributions::::iter_prefix(crowdloan_id) + .collect::>(), + vec![(creator, deposit)] + ); + // ensure the raised amount is updated correctly + assert!( + pallet_crowdloan::Crowdloans::::get(crowdloan_id) + .is_some_and(|c| c.raised == deposit) + ); + // ensure the event is emitted + assert_eq!( + last_event(), + pallet_crowdloan::Event::::Created { + crowdloan_id, + creator, + end, + cap, + } + .into() + ); + // ensure next crowdloan id is incremented + assert_eq!( + pallet_crowdloan::NextCrowdloanId::::get(), + crowdloan_id + 1 + ); + }); +} + +#[test] +fn test_create_fails_if_bad_origin() { + TestState::default().build_and_execute(|| { + let deposit: BalanceOf = 50; + let min_contribution: BalanceOf = 10; + let cap: BalanceOf = 300; + let end: BlockNumberFor = 50; + + assert_err!( + Crowdloan::create( + RuntimeOrigin::none(), + deposit, + min_contribution, + cap, + end, + Some(noop_call()), + None + ), + DispatchError::BadOrigin + ); + + assert_err!( + Crowdloan::create( + RuntimeOrigin::root(), + deposit, + min_contribution, + cap, + end, + Some(noop_call()), + None + ), + DispatchError::BadOrigin + ); + }); +} + +#[test] +fn test_create_fails_if_deposit_is_too_low() { + TestState::default() + .with_balance(U256::from(1), 100) + .build_and_execute(|| { + let creator: AccountOf = U256::from(1); + let deposit: BalanceOf = 20; + let min_contribution: BalanceOf = 10; + let cap: BalanceOf = 300; + let end: BlockNumberFor = 50; + + assert_err!( + Crowdloan::create( + RuntimeOrigin::signed(creator), + deposit, + min_contribution, + cap, + end, + Some(noop_call()), + None + ), + pallet_crowdloan::Error::::DepositTooLow + ); + }); +} + +#[test] +fn test_create_fails_if_cap_is_not_greater_than_deposit() { + TestState::default() + .with_balance(U256::from(1), 100) + .build_and_execute(|| { + let creator: AccountOf = U256::from(1); + let deposit: BalanceOf = 50; + let min_contribution: BalanceOf = 10; + let cap: BalanceOf = 40; + let end: BlockNumberFor = 50; + + assert_err!( + Crowdloan::create( + RuntimeOrigin::signed(creator), + deposit, + min_contribution, + cap, + end, + Some(noop_call()), + None + ), + pallet_crowdloan::Error::::CapTooLow + ); + }); +} + +#[test] +fn test_create_fails_if_min_contribution_is_too_low() { + TestState::default() + .with_balance(U256::from(1), 100) + .build_and_execute(|| { + let creator: AccountOf = U256::from(1); + let deposit: BalanceOf = 50; + let min_contribution: BalanceOf = 5; + let cap: BalanceOf = 300; + let end: BlockNumberFor = 50; + + assert_err!( + Crowdloan::create( + RuntimeOrigin::signed(creator), + deposit, + min_contribution, + cap, + end, + Some(noop_call()), + None + ), + pallet_crowdloan::Error::::MinimumContributionTooLow + ); + }); +} + +#[test] +fn test_create_fails_if_end_is_in_the_past() { + let current_block_number: BlockNumberFor = 10; + + TestState::default() + .with_block_number(current_block_number) + .with_balance(U256::from(1), 100) + .build_and_execute(|| { + let creator: AccountOf = U256::from(1); + let deposit: BalanceOf = 50; + let min_contribution: BalanceOf = 10; + let cap: BalanceOf = 300; + let end: BlockNumberFor = current_block_number - 5; + + assert_err!( + Crowdloan::create( + RuntimeOrigin::signed(creator), + deposit, + min_contribution, + cap, + end, + Some(noop_call()), + None + ), + pallet_crowdloan::Error::::CannotEndInPast + ); + }); +} + +#[test] +fn test_create_fails_if_block_duration_is_too_short() { + TestState::default() + .with_balance(U256::from(1), 100) + .build_and_execute(|| { + let creator: AccountOf = U256::from(1); + let deposit: BalanceOf = 50; + let min_contribution: BalanceOf = 10; + let cap: BalanceOf = 300; + let end: BlockNumberFor = 11; + + assert_err!( + Crowdloan::create( + RuntimeOrigin::signed(creator), + deposit, + min_contribution, + cap, + end, + Some(noop_call()), + None + ), + pallet_crowdloan::Error::::BlockDurationTooShort + ); + }); +} + +#[test] +fn test_create_fails_if_block_duration_is_too_long() { + TestState::default() + .with_balance(U256::from(1), 100) + .build_and_execute(|| { + let creator: AccountOf = U256::from(1); + let deposit: BalanceOf = 50; + let min_contribution: BalanceOf = 10; + let cap: BalanceOf = 300; + let end: BlockNumberFor = 1000; + + assert_err!( + Crowdloan::create( + RuntimeOrigin::signed(creator), + deposit, + min_contribution, + cap, + end, + Some(noop_call()), + None + ), + pallet_crowdloan::Error::::BlockDurationTooLong + ); + }); +} + +#[test] +fn test_create_fails_if_creator_has_insufficient_balance() { + TestState::default() + .with_balance(U256::from(1), 100) + .build_and_execute(|| { + let creator: AccountOf = U256::from(1); + let deposit: BalanceOf = 200; + let min_contribution: BalanceOf = 10; + let cap: BalanceOf = 300; + let end: BlockNumberFor = 50; + + assert_err!( + Crowdloan::create( + RuntimeOrigin::signed(creator), + deposit, + min_contribution, + cap, + end, + Some(noop_call()), + None + ), + pallet_crowdloan::Error::::InsufficientBalance + ); + }); +} + +#[test] +fn test_contribute_succeeds() { + TestState::default() + .with_balance(U256::from(1), 200) + .with_balance(U256::from(2), 500) + .with_balance(U256::from(3), 200) + .build_and_execute(|| { + // create a crowdloan + let creator: AccountOf = U256::from(1); + let initial_deposit: BalanceOf = 50; + let min_contribution: BalanceOf = 10; + let cap: BalanceOf = 300; + let end: BlockNumberFor = 50; + + assert_ok!(Crowdloan::create( + RuntimeOrigin::signed(creator), + initial_deposit, + min_contribution, + cap, + end, + Some(noop_call()), + None + )); + + // run some blocks + run_to_block(10); + + // first contribution to the crowdloan from creator + let crowdloan_id: CrowdloanId = 0; + let amount: BalanceOf = 50; + assert_ok!(Crowdloan::contribute( + RuntimeOrigin::signed(creator), + crowdloan_id, + amount + )); + assert_eq!( + last_event(), + pallet_crowdloan::Event::::Contributed { + crowdloan_id, + contributor: creator, + amount, + } + .into() + ); + assert_eq!( + pallet_crowdloan::Contributions::::get(crowdloan_id, creator), + Some(100) + ); + assert_eq!( + Balances::free_balance(creator), + 200 - amount - initial_deposit + ); + + // second contribution to the crowdloan + let contributor1: AccountOf = U256::from(2); + let amount: BalanceOf = 100; + assert_ok!(Crowdloan::contribute( + RuntimeOrigin::signed(contributor1), + crowdloan_id, + amount + )); + assert_eq!( + last_event(), + pallet_crowdloan::Event::::Contributed { + crowdloan_id, + contributor: contributor1, + amount, + } + .into() + ); + assert_eq!( + pallet_crowdloan::Contributions::::get(crowdloan_id, contributor1), + Some(100) + ); + assert_eq!(Balances::free_balance(contributor1), 500 - amount); + + // third contribution to the crowdloan + let contributor2: AccountOf = U256::from(3); + let amount: BalanceOf = 50; + assert_ok!(Crowdloan::contribute( + RuntimeOrigin::signed(contributor2), + crowdloan_id, + amount + )); + assert_eq!( + last_event(), + pallet_crowdloan::Event::::Contributed { + crowdloan_id, + contributor: contributor2, + amount, + } + .into() + ); + assert_eq!( + pallet_crowdloan::Contributions::::get(crowdloan_id, contributor2), + Some(50) + ); + assert_eq!(Balances::free_balance(contributor2), 200 - amount); + + // ensure the contributions are present in the funds account + let funds_account = pallet_crowdloan::Pallet::::funds_account(crowdloan_id); + assert_eq!(Balances::free_balance(funds_account), 250); + + // ensure the crowdloan raised amount is updated correctly + assert!( + pallet_crowdloan::Crowdloans::::get(crowdloan_id) + .is_some_and(|c| c.raised == 250) + ); + }); +} + +#[test] +fn test_contribute_succeeds_if_contribution_will_make_the_raised_amount_exceed_the_cap() { + TestState::default() + .with_balance(U256::from(1), 200) + .with_balance(U256::from(2), 500) + .build_and_execute(|| { + // create a crowdloan + let creator: AccountOf = U256::from(1); + let initial_deposit: BalanceOf = 50; + let min_contribution: BalanceOf = 10; + let cap: BalanceOf = 300; + let end: BlockNumberFor = 50; + + assert_ok!(Crowdloan::create( + RuntimeOrigin::signed(creator), + initial_deposit, + min_contribution, + cap, + end, + Some(noop_call()), + None + )); + + // run some blocks + run_to_block(10); + + // first contribution to the crowdloan from creator + let crowdloan_id: CrowdloanId = 0; + let amount: BalanceOf = 50; + assert_ok!(Crowdloan::contribute( + RuntimeOrigin::signed(creator), + crowdloan_id, + amount + )); + assert_eq!( + last_event(), + pallet_crowdloan::Event::::Contributed { + crowdloan_id, + contributor: creator, + amount, + } + .into() + ); + assert_eq!( + pallet_crowdloan::Contributions::::get(crowdloan_id, creator), + Some(100) + ); + assert_eq!( + Balances::free_balance(creator), + 200 - amount - initial_deposit + ); + + // second contribution to the crowdloan above the cap + let contributor1: AccountOf = U256::from(2); + let amount: BalanceOf = 300; + assert_ok!(Crowdloan::contribute( + RuntimeOrigin::signed(contributor1), + crowdloan_id, + amount + )); + assert_eq!( + last_event(), + pallet_crowdloan::Event::::Contributed { + crowdloan_id, + contributor: contributor1, + amount: 200, // the amount is capped at the cap + } + .into() + ); + assert_eq!( + pallet_crowdloan::Contributions::::get(crowdloan_id, contributor1), + Some(200) + ); + assert_eq!(Balances::free_balance(contributor1), 500 - 200); + + // ensure the contributions are present in the crowdloan account up to the cap + let funds_account = pallet_crowdloan::Pallet::::funds_account(crowdloan_id); + assert_eq!(Balances::free_balance(funds_account), 300); + + // ensure the crowdloan raised amount is updated correctly + assert!( + pallet_crowdloan::Crowdloans::::get(crowdloan_id) + .is_some_and(|c| c.raised == 300) + ); + }); +} + +#[test] +fn test_contribute_fails_if_bad_origin() { + TestState::default().build_and_execute(|| { + let crowdloan_id: CrowdloanId = 0; + let amount: BalanceOf = 100; + + assert_err!( + Crowdloan::contribute(RuntimeOrigin::none(), crowdloan_id, amount), + DispatchError::BadOrigin + ); + + assert_err!( + Crowdloan::contribute(RuntimeOrigin::root(), crowdloan_id, amount), + DispatchError::BadOrigin + ); + }); +} + +#[test] +fn test_contribute_fails_if_crowdloan_does_not_exist() { + TestState::default() + .with_balance(U256::from(1), 100) + .build_and_execute(|| { + let contributor: AccountOf = U256::from(1); + let crowdloan_id: CrowdloanId = 0; + let amount: BalanceOf = 20; + + assert_err!( + Crowdloan::contribute(RuntimeOrigin::signed(contributor), crowdloan_id, amount), + pallet_crowdloan::Error::::InvalidCrowdloanId + ); + }); +} + +#[test] +fn test_contribute_fails_if_contribution_period_ended() { + TestState::default() + .with_balance(U256::from(1), 100) + .with_balance(U256::from(2), 100) + .build_and_execute(|| { + // create a crowdloan + let creator: AccountOf = U256::from(1); + let initial_deposit: BalanceOf = 50; + let min_contribution: BalanceOf = 10; + let cap: BalanceOf = 300; + let end: BlockNumberFor = 50; + + assert_ok!(Crowdloan::create( + RuntimeOrigin::signed(creator), + initial_deposit, + min_contribution, + cap, + end, + Some(noop_call()), + None + )); + + // run past the end of the crowdloan + run_to_block(60); + + // contribute to the crowdloan + let contributor: AccountOf = U256::from(2); + let crowdloan_id: CrowdloanId = 0; + let amount: BalanceOf = 20; + assert_err!( + Crowdloan::contribute(RuntimeOrigin::signed(contributor), crowdloan_id, amount), + pallet_crowdloan::Error::::ContributionPeriodEnded + ); + }); +} + +#[test] +fn test_contribute_fails_if_cap_has_been_raised() { + TestState::default() + .with_balance(U256::from(1), 100) + .with_balance(U256::from(2), 1000) + .with_balance(U256::from(3), 100) + .build_and_execute(|| { + // create a crowdloan + let creator: AccountOf = U256::from(1); + let initial_deposit: BalanceOf = 50; + let min_contribution: BalanceOf = 10; + let cap: BalanceOf = 300; + let end: BlockNumberFor = 50; + + assert_ok!(Crowdloan::create( + RuntimeOrigin::signed(creator), + initial_deposit, + min_contribution, + cap, + end, + Some(noop_call()), + None + )); + + // run some blocks + run_to_block(10); + + // first contribution to the crowdloan fully raise the cap + let crowdloan_id: CrowdloanId = 0; + let contributor1: AccountOf = U256::from(2); + let amount: BalanceOf = cap - initial_deposit; + assert_ok!(Crowdloan::contribute( + RuntimeOrigin::signed(contributor1), + crowdloan_id, + amount + )); + + // second contribution to the crowdloan + let contributor2: AccountOf = U256::from(3); + let amount: BalanceOf = 10; + assert_err!( + Crowdloan::contribute(RuntimeOrigin::signed(contributor2), crowdloan_id, amount), + pallet_crowdloan::Error::::CapRaised + ); + }); +} + +#[test] +fn test_contribute_fails_if_contribution_is_below_minimum_contribution() { + TestState::default() + .with_balance(U256::from(1), 100) + .with_balance(U256::from(2), 100) + .build_and_execute(|| { + // create a crowdloan + let creator: AccountOf = U256::from(1); + let initial_deposit: BalanceOf = 50; + let min_contribution: BalanceOf = 10; + let cap: BalanceOf = 300; + let end: BlockNumberFor = 50; + + assert_ok!(Crowdloan::create( + RuntimeOrigin::signed(creator), + initial_deposit, + min_contribution, + cap, + end, + Some(noop_call()), + None + )); + + // run some blocks + run_to_block(10); + + // contribute to the crowdloan + let contributor: AccountOf = U256::from(2); + let crowdloan_id: CrowdloanId = 0; + let amount: BalanceOf = 5; + assert_err!( + Crowdloan::contribute(RuntimeOrigin::signed(contributor), crowdloan_id, amount), + pallet_crowdloan::Error::::ContributionTooLow + ) + }); +} + +#[test] +fn test_contribute_fails_if_contributor_has_insufficient_balance() { + TestState::default() + .with_balance(U256::from(1), 100) + .with_balance(U256::from(2), 50) + .build_and_execute(|| { + // create a crowdloan + let creator: AccountOf = U256::from(1); + let initial_deposit: BalanceOf = 50; + let min_contribution: BalanceOf = 10; + let cap: BalanceOf = 300; + let end: BlockNumberFor = 50; + + assert_ok!(Crowdloan::create( + RuntimeOrigin::signed(creator), + initial_deposit, + min_contribution, + cap, + end, + Some(noop_call()), + None + )); + + // run some blocks + run_to_block(10); + + // contribute to the crowdloan + let crowdloan_id: CrowdloanId = 0; + let contributor: AccountOf = U256::from(2); + let amount: BalanceOf = 100; + + assert_err!( + Crowdloan::contribute(RuntimeOrigin::signed(contributor), crowdloan_id, amount), + pallet_crowdloan::Error::::InsufficientBalance + ); + }); +} + +#[test] +fn test_withdraw_succeeds() { + TestState::default() + .with_balance(U256::from(1), 100) + .with_balance(U256::from(2), 100) + .with_balance(U256::from(3), 100) + .build_and_execute(|| { + // create a crowdloan + let creator: AccountOf = U256::from(1); + let initial_deposit: BalanceOf = 50; + let min_contribution: BalanceOf = 10; + let cap: BalanceOf = 300; + let end: BlockNumberFor = 50; + + assert_ok!(Crowdloan::create( + RuntimeOrigin::signed(creator), + initial_deposit, + min_contribution, + cap, + end, + Some(noop_call()), + None + )); + + // run some blocks + run_to_block(10); + + // contribute to the crowdloan + let crowdloan_id: CrowdloanId = 0; + let contributor: AccountOf = U256::from(2); + let amount: BalanceOf = 100; + + assert_ok!(Crowdloan::contribute( + RuntimeOrigin::signed(contributor), + crowdloan_id, + amount + )); + + // run some more blocks past the end of the contribution period + run_to_block(60); + + // withdraw from creator + assert_ok!(Crowdloan::withdraw( + RuntimeOrigin::signed(creator), + creator, + crowdloan_id + )); + // ensure the creator contribution has been removed + assert_eq!( + pallet_crowdloan::Contributions::::get(crowdloan_id, creator), + None, + ); + // ensure the creator has the correct amount + assert_eq!(pallet_balances::Pallet::::free_balance(creator), 100); + + // withdraw from contributor + assert_ok!(Crowdloan::withdraw( + RuntimeOrigin::signed(contributor), + contributor, + crowdloan_id + )); + // ensure the creator contribution has been removed + assert_eq!( + pallet_crowdloan::Contributions::::get(crowdloan_id, contributor), + None, + ); + // ensure the contributor has the correct amount + assert_eq!( + pallet_balances::Pallet::::free_balance(contributor), + 100 + ); + + // ensure the crowdloan account has the correct amount + let funds_account = pallet_crowdloan::Pallet::::funds_account(crowdloan_id); + assert_eq!(Balances::free_balance(funds_account), 0); + // ensure the crowdloan raised amount is updated correctly + assert!( + pallet_crowdloan::Crowdloans::::get(crowdloan_id) + .is_some_and(|c| c.raised == 0) + ); + }); +} + +#[test] +fn test_withdraw_succeeds_for_another_contributor() { + TestState::default() + .with_balance(U256::from(1), 100) + .with_balance(U256::from(2), 100) + .build_and_execute(|| { + // create a crowdloan + let creator: AccountOf = U256::from(1); + let initial_deposit: BalanceOf = 50; + let min_contribution: BalanceOf = 10; + let cap: BalanceOf = 300; + let end: BlockNumberFor = 50; + + assert_ok!(Crowdloan::create( + RuntimeOrigin::signed(creator), + initial_deposit, + min_contribution, + cap, + end, + Some(noop_call()), + None + )); + + // run some blocks + run_to_block(10); + + // contribute to the crowdloan + let crowdloan_id: CrowdloanId = 0; + let contributor: AccountOf = U256::from(2); + let amount: BalanceOf = 100; + + assert_ok!(Crowdloan::contribute( + RuntimeOrigin::signed(contributor), + crowdloan_id, + amount + )); + + // run some more blocks past the end of the contribution period + run_to_block(60); + + // withdraw for creator as a contributor + assert_ok!(Crowdloan::withdraw( + RuntimeOrigin::signed(contributor), + creator, + crowdloan_id + )); + // ensure the creator has the correct amount + assert_eq!(pallet_balances::Pallet::::free_balance(creator), 100); + // ensure the contributor has the correct amount + assert_eq!( + pallet_balances::Pallet::::free_balance(contributor), + 0 + ); + + // ensure the crowdloan account has the correct amount + let funds_account = pallet_crowdloan::Pallet::::funds_account(crowdloan_id); + assert_eq!(Balances::free_balance(funds_account), 100); + // ensure the crowdloan raised amount is updated correctly + assert!( + pallet_crowdloan::Crowdloans::::get(crowdloan_id) + .is_some_and(|c| c.raised == 100) + ); + }); +} + +#[test] +fn test_withdraw_fails_if_bad_origin() { + TestState::default().build_and_execute(|| { + let crowdloan_id: CrowdloanId = 0; + + assert_err!( + Crowdloan::withdraw(RuntimeOrigin::none(), U256::from(1), crowdloan_id), + DispatchError::BadOrigin + ); + + assert_err!( + Crowdloan::withdraw(RuntimeOrigin::root(), U256::from(1), crowdloan_id), + DispatchError::BadOrigin + ); + }); +} + +#[test] +fn test_withdraw_fails_if_crowdloan_does_not_exists() { + TestState::default().build_and_execute(|| { + let contributor: AccountOf = U256::from(1); + let crowdloan_id: CrowdloanId = 0; + + assert_err!( + Crowdloan::withdraw( + RuntimeOrigin::signed(contributor), + contributor, + crowdloan_id + ), + pallet_crowdloan::Error::::InvalidCrowdloanId + ); + }); +} + +#[test] +fn test_withdraw_fails_if_no_contribution_exists() { + TestState::default() + .with_balance(U256::from(1), 100) + .with_balance(U256::from(2), 200) + .with_balance(U256::from(3), 100) + .build_and_execute(|| { + // create a crowdloan + let creator: AccountOf = U256::from(1); + let initial_deposit: BalanceOf = 50; + let min_contribution: BalanceOf = 10; + let cap: BalanceOf = 300; + let end: BlockNumberFor = 50; + + assert_ok!(Crowdloan::create( + RuntimeOrigin::signed(creator), + initial_deposit, + min_contribution, + cap, + end, + Some(noop_call()), + None + )); + + // run some blocks + run_to_block(10); + + // contribute to the crowdloan + let contributor: AccountOf = U256::from(2); + let crowdloan_id: CrowdloanId = 0; + let amount: BalanceOf = 100; + + assert_ok!(Crowdloan::contribute( + RuntimeOrigin::signed(contributor), + crowdloan_id, + amount + )); + + // run some more blocks past the end of the contribution period + run_to_block(60); + + // try to withdraw + let contributor2: AccountOf = U256::from(3); + assert_err!( + Crowdloan::withdraw( + RuntimeOrigin::signed(contributor2), + contributor2, + crowdloan_id + ), + pallet_crowdloan::Error::::NoContribution + ); + }); +} + +#[test] +fn test_finalize_succeeds() { + TestState::default() + .with_balance(U256::from(1), 100) + .with_balance(U256::from(2), 100) + .build_and_execute(|| { + // create a crowdloan + let creator: AccountOf = U256::from(1); + let deposit: BalanceOf = 50; + let min_contribution: BalanceOf = 10; + let cap: BalanceOf = 100; + let end: BlockNumberFor = 50; + let call = Box::new(RuntimeCall::TestPallet( + pallet_test::Call::::transfer_funds { + dest: U256::from(42), + }, + )); + + assert_ok!(Crowdloan::create( + RuntimeOrigin::signed(creator), + deposit, + min_contribution, + cap, + end, + Some(call), + None + )); + + // run some blocks + run_to_block(10); + + // some contribution + let crowdloan_id: CrowdloanId = 0; + let contributor: AccountOf = U256::from(2); + let amount: BalanceOf = 50; + + assert_ok!(Crowdloan::contribute( + RuntimeOrigin::signed(contributor), + crowdloan_id, + amount + )); + + // run some more blocks past the end of the contribution period + run_to_block(60); + + // finalize the crowdloan + assert_ok!(Crowdloan::finalize( + RuntimeOrigin::signed(creator), + crowdloan_id + )); + + // ensure the transfer was a success from the dispatched call + assert_eq!( + pallet_balances::Pallet::::free_balance(U256::from(42)), + 100 + ); + + // ensure the crowdloan is marked as finalized + assert!( + pallet_crowdloan::Crowdloans::::get(crowdloan_id) + .is_some_and(|c| c.finalized) + ); + + // ensure the event is emitted + assert_eq!( + last_event(), + pallet_crowdloan::Event::::Finalized { crowdloan_id }.into() + ); + + // ensure the current crowdloan id was accessible from the dispatched call + assert_eq!( + pallet_test::PassedCrowdloanId::::get(), + Some(crowdloan_id) + ); + }); +} + +#[test] +fn test_finalize_succeeds_with_target_address() { + TestState::default() + .with_balance(U256::from(1), 100) + .with_balance(U256::from(2), 100) + .build_and_execute(|| { + // create a crowdloan + let creator: AccountOf = U256::from(1); + let deposit: BalanceOf = 50; + let min_contribution: BalanceOf = 10; + let cap: BalanceOf = 100; + let end: BlockNumberFor = 50; + let target_address: AccountOf = U256::from(42); + let call = Box::new(RuntimeCall::TestPallet( + pallet_test::Call::::set_passed_crowdloan_id {}, + )); + + assert_ok!(Crowdloan::create( + RuntimeOrigin::signed(creator), + deposit, + min_contribution, + cap, + end, + Some(call), + Some(target_address), + )); + + // run some blocks + run_to_block(10); + + // some contribution + let crowdloan_id: CrowdloanId = 0; + let contributor: AccountOf = U256::from(2); + let amount: BalanceOf = 50; + + assert_ok!(Crowdloan::contribute( + RuntimeOrigin::signed(contributor), + crowdloan_id, + amount + )); + + // run some more blocks past the end of the contribution period + run_to_block(60); + + // finalize the crowdloan + assert_ok!(Crowdloan::finalize( + RuntimeOrigin::signed(creator), + crowdloan_id + )); + + // ensure the target address has received the funds + assert_eq!( + pallet_balances::Pallet::::free_balance(target_address), + 100 + ); + + // ensure the crowdloan is marked as finalized + assert!( + pallet_crowdloan::Crowdloans::::get(crowdloan_id) + .is_some_and(|c| c.finalized) + ); + + // ensure the event is emitted + assert_eq!( + last_event(), + pallet_crowdloan::Event::::Finalized { crowdloan_id }.into() + ); + + // ensure the current crowdloan id was accessible from the dispatched call + assert_eq!( + pallet_test::PassedCrowdloanId::::get(), + Some(crowdloan_id) + ); + }) +} + +#[test] +fn test_finalize_fails_if_bad_origin() { + TestState::default() + .with_balance(U256::from(1), 100) + .build_and_execute(|| { + let crowdloan_id: CrowdloanId = 0; + + assert_err!( + Crowdloan::finalize(RuntimeOrigin::none(), crowdloan_id), + DispatchError::BadOrigin + ); + + assert_err!( + Crowdloan::finalize(RuntimeOrigin::root(), crowdloan_id), + DispatchError::BadOrigin + ); + }); +} + +#[test] +fn test_finalize_fails_if_crowdloan_does_not_exist() { + TestState::default() + .with_balance(U256::from(1), 100) + .build_and_execute(|| { + let creator: AccountOf = U256::from(1); + let crowdloan_id: CrowdloanId = 0; + + // try to finalize + assert_err!( + Crowdloan::finalize(RuntimeOrigin::signed(creator), crowdloan_id), + pallet_crowdloan::Error::::InvalidCrowdloanId + ); + }); +} + +#[test] +fn test_finalize_fails_if_not_creator_origin() { + TestState::default() + .with_balance(U256::from(1), 100) + .with_balance(U256::from(2), 100) + .build_and_execute(|| { + // create a crowdloan + let creator: AccountOf = U256::from(1); + let deposit: BalanceOf = 50; + let min_contribution: BalanceOf = 10; + let cap: BalanceOf = 100; + let end: BlockNumberFor = 50; + + assert_ok!(Crowdloan::create( + RuntimeOrigin::signed(creator), + deposit, + min_contribution, + cap, + end, + Some(noop_call()), + None + )); + + // run some blocks + run_to_block(10); + + // some contribution + let crowdloan_id: CrowdloanId = 0; + let contributor: AccountOf = U256::from(2); + let amount: BalanceOf = 50; + assert_ok!(Crowdloan::contribute( + RuntimeOrigin::signed(contributor), + crowdloan_id, + amount + )); + + // run some more blocks past the end of the contribution period + run_to_block(60); + + // try finalize the crowdloan + assert_err!( + Crowdloan::finalize(RuntimeOrigin::signed(contributor), crowdloan_id), + pallet_crowdloan::Error::::InvalidOrigin + ); + }); +} + +#[test] +fn test_finalize_fails_if_crowdloan_has_not_ended() { + TestState::default() + .with_balance(U256::from(1), 100) + .with_balance(U256::from(2), 100) + .build_and_execute(|| { + // create a crowdloan + let creator: AccountOf = U256::from(1); + let deposit: BalanceOf = 50; + let min_contribution: BalanceOf = 10; + let cap: BalanceOf = 100; + let end: BlockNumberFor = 50; + + assert_ok!(Crowdloan::create( + RuntimeOrigin::signed(creator), + deposit, + min_contribution, + cap, + end, + Some(noop_call()), + None, + )); + + // run some blocks + run_to_block(10); + + // some contribution + let crowdloan_id: CrowdloanId = 0; + let contributor: AccountOf = U256::from(2); + let amount: BalanceOf = 50; + + assert_ok!(Crowdloan::contribute( + RuntimeOrigin::signed(contributor), + crowdloan_id, + amount + )); + + // run some more blocks before end of contribution period + run_to_block(10); + + // try to finalize + assert_err!( + Crowdloan::finalize(RuntimeOrigin::signed(creator), crowdloan_id), + pallet_crowdloan::Error::::ContributionPeriodNotEnded + ); + }); +} + +#[test] +fn test_finalize_fails_if_crowdloan_cap_is_not_raised() { + TestState::default() + .with_balance(U256::from(1), 100) + .with_balance(U256::from(2), 100) + .build_and_execute(|| { + // create a crowdloan + let creator: AccountOf = U256::from(1); + let deposit: BalanceOf = 50; + let min_contribution: BalanceOf = 10; + let cap: BalanceOf = 100; + let end: BlockNumberFor = 50; + + assert_ok!(Crowdloan::create( + RuntimeOrigin::signed(creator), + deposit, + min_contribution, + cap, + end, + Some(noop_call()), + None, + )); + + // run some blocks + run_to_block(10); + + // some contribution + let crowdloan_id: CrowdloanId = 0; + let contributor: AccountOf = U256::from(2); + let amount: BalanceOf = 49; // below cap + + assert_ok!(Crowdloan::contribute( + RuntimeOrigin::signed(contributor), + crowdloan_id, + amount + )); + + // run some more blocks past the end of the contribution period + run_to_block(60); + + // try finalize the crowdloan + assert_err!( + Crowdloan::finalize(RuntimeOrigin::signed(creator), crowdloan_id), + pallet_crowdloan::Error::::CapNotRaised + ); + }); +} + +#[test] +fn test_finalize_fails_if_crowdloan_has_already_been_finalized() { + TestState::default() + .with_balance(U256::from(1), 100) + .with_balance(U256::from(2), 100) + .build_and_execute(|| { + // create a crowdloan + let creator: AccountOf = U256::from(1); + let deposit: BalanceOf = 50; + let min_contribution: BalanceOf = 10; + let cap: BalanceOf = 100; + let end: BlockNumberFor = 50; + + assert_ok!(Crowdloan::create( + RuntimeOrigin::signed(creator), + deposit, + min_contribution, + cap, + end, + Some(noop_call()), + None, + )); + + // some contribution + let crowdloan_id: CrowdloanId = 0; + let contributor: AccountOf = U256::from(2); + let amount: BalanceOf = 50; + + assert_ok!(Crowdloan::contribute( + RuntimeOrigin::signed(contributor), + crowdloan_id, + amount + )); + + // run some more blocks past the end of the contribution period + run_to_block(60); + + // finalize the crowdloan + assert_ok!(Crowdloan::finalize( + RuntimeOrigin::signed(creator), + crowdloan_id + )); + + // try finalize the crowdloan a second time + assert_err!( + Crowdloan::finalize(RuntimeOrigin::signed(creator), crowdloan_id), + pallet_crowdloan::Error::::AlreadyFinalized + ); + }); +} + +#[test] +fn test_finalize_fails_if_call_fails() { + TestState::default() + .with_balance(U256::from(1), 100) + .with_balance(U256::from(2), 100) + .build_and_execute(|| { + // create a crowdloan + let creator: AccountOf = U256::from(1); + let deposit: BalanceOf = 50; + let min_contribution: BalanceOf = 10; + let cap: BalanceOf = 100; + let end: BlockNumberFor = 50; + let call = Box::new(RuntimeCall::TestPallet( + pallet_test::Call::::failing_extrinsic {}, + )); + + assert_ok!(Crowdloan::create( + RuntimeOrigin::signed(creator), + deposit, + min_contribution, + cap, + end, + Some(call), + None, + )); + + // run some blocks + run_to_block(10); + + // some contribution + let crowdloan_id: CrowdloanId = 0; + let contributor: AccountOf = U256::from(2); + let amount: BalanceOf = 50; + assert_ok!(Crowdloan::contribute( + RuntimeOrigin::signed(contributor), + crowdloan_id, + amount + )); + + // run some more blocks past the end of the contribution period + run_to_block(60); + + // try finalize the crowdloan + assert_err!( + Crowdloan::finalize(RuntimeOrigin::signed(creator), crowdloan_id), + pallet_test::Error::::ShouldFail + ); + }); +} + +#[test] +fn test_refund_succeeds() { + TestState::default() + .with_balance(U256::from(1), 100) + .with_balance(U256::from(2), 100) + .with_balance(U256::from(3), 100) + .with_balance(U256::from(4), 100) + .with_balance(U256::from(5), 100) + .with_balance(U256::from(6), 100) + .with_balance(U256::from(7), 100) + .build_and_execute(|| { + // create a crowdloan + let creator: AccountOf = U256::from(1); + let initial_deposit: BalanceOf = 50; + let min_contribution: BalanceOf = 10; + let cap: BalanceOf = 400; + let end: BlockNumberFor = 50; + assert_ok!(Crowdloan::create( + RuntimeOrigin::signed(creator), + initial_deposit, + min_contribution, + cap, + end, + Some(noop_call()), + None, + )); + + // run some blocks + run_to_block(10); + + // make 6 contributions to reach 350 raised amount (initial deposit + contributions) + let crowdloan_id: CrowdloanId = 0; + let amount: BalanceOf = 50; + for i in 2..8 { + let contributor: AccountOf = U256::from(i); + assert_ok!(Crowdloan::contribute( + RuntimeOrigin::signed(contributor), + crowdloan_id, + amount + )); + } + + // run some more blocks past the end of the contribution period + run_to_block(60); + + // first round of refund + assert_ok!(Crowdloan::refund( + RuntimeOrigin::signed(creator), + crowdloan_id + )); + + // ensure the crowdloan account has the correct amount + let funds_account = pallet_crowdloan::Pallet::::funds_account(crowdloan_id); + assert_eq!(Balances::free_balance(funds_account), 350 - 5 * amount); + // ensure raised amount is updated correctly + assert!( + pallet_crowdloan::Crowdloans::::get(crowdloan_id) + .is_some_and(|c| c.raised == 350 - 5 * amount) + ); + // ensure the event is emitted + assert_eq!( + last_event(), + pallet_crowdloan::Event::::PartiallyRefunded { crowdloan_id }.into() + ); + + // run some more blocks + run_to_block(70); + + // second round of refund + assert_ok!(Crowdloan::refund( + RuntimeOrigin::signed(creator), + crowdloan_id + )); + + // ensure the crowdloan account has the correct amount + assert_eq!( + pallet_balances::Pallet::::free_balance(funds_account), + 0 + ); + // ensure the raised amount is updated correctly + assert!( + pallet_crowdloan::Crowdloans::::get(crowdloan_id) + .is_some_and(|c| c.raised == 0) + ); + + // ensure creator has the correct amount + assert_eq!(pallet_balances::Pallet::::free_balance(creator), 100); + + // ensure each contributor has been refunded and removed from the crowdloan + for i in 2..8 { + let contributor: AccountOf = U256::from(i); + assert_eq!( + pallet_balances::Pallet::::free_balance(contributor), + 100 + ); + assert_eq!( + pallet_crowdloan::Contributions::::get(crowdloan_id, contributor), + None, + ); + } + + // ensure the event is emitted + assert_eq!( + last_event(), + pallet_crowdloan::Event::::AllRefunded { crowdloan_id }.into() + ); + }) +} + +#[test] +fn test_refund_fails_if_bad_origin() { + TestState::default().build_and_execute(|| { + let crowdloan_id: CrowdloanId = 0; + + assert_err!( + Crowdloan::refund(RuntimeOrigin::none(), crowdloan_id), + DispatchError::BadOrigin + ); + + assert_err!( + Crowdloan::refund(RuntimeOrigin::root(), crowdloan_id), + DispatchError::BadOrigin + ); + }); +} + +#[test] +fn test_refund_fails_if_crowdloan_does_not_exist() { + TestState::default() + .with_balance(U256::from(1), 100) + .build_and_execute(|| { + let creator: AccountOf = U256::from(1); + let crowdloan_id: CrowdloanId = 0; + + assert_err!( + Crowdloan::refund(RuntimeOrigin::signed(creator), crowdloan_id), + pallet_crowdloan::Error::::InvalidCrowdloanId + ); + }); +} + +#[test] +fn test_refund_fails_if_crowdloan_has_not_ended() { + TestState::default() + .with_balance(U256::from(1), 100) + .build_and_execute(|| { + // create a crowdloan + let creator: AccountOf = U256::from(1); + let initial_deposit: BalanceOf = 50; + let min_contribution: BalanceOf = 10; + let cap: BalanceOf = 300; + let end: BlockNumberFor = 50; + assert_ok!(Crowdloan::create( + RuntimeOrigin::signed(creator), + initial_deposit, + min_contribution, + cap, + end, + Some(noop_call()), + None, + )); + + // run some blocks + run_to_block(10); + + // try to refund + let crowdloan_id: CrowdloanId = 0; + assert_err!( + Crowdloan::refund(RuntimeOrigin::signed(creator), crowdloan_id), + pallet_crowdloan::Error::::ContributionPeriodNotEnded + ); + }); +} + +#[test] +fn test_dissolve_succeeds() { + TestState::default() + .with_balance(U256::from(1), 100) + .build_and_execute(|| { + // create a crowdloan + let creator: AccountOf = U256::from(1); + let deposit: BalanceOf = 50; + let min_contribution: BalanceOf = 10; + let cap: BalanceOf = 100; + let end: BlockNumberFor = 50; + + assert_ok!(Crowdloan::create( + RuntimeOrigin::signed(creator), + deposit, + min_contribution, + cap, + end, + Some(noop_call()), + None, + )); + + // run some blocks past end + run_to_block(60); + + // refund the contributions + let crowdloan_id: CrowdloanId = 0; + assert_ok!(Crowdloan::refund( + RuntimeOrigin::signed(creator), + crowdloan_id + )); + + // dissolve the crowdloan + let crowdloan_id: CrowdloanId = 0; + assert_ok!(Crowdloan::dissolve( + RuntimeOrigin::signed(creator), + crowdloan_id + )); + + // ensure the crowdloan is removed from the crowdloans map + assert!(pallet_crowdloan::Crowdloans::::get(crowdloan_id).is_none()); + + // ensure the event is emitted + assert_eq!( + last_event(), + pallet_crowdloan::Event::::Dissolved { crowdloan_id }.into() + ) + }); +} + +#[test] +fn test_dissolve_fails_if_bad_origin() { + TestState::default().build_and_execute(|| { + let crowdloan_id: CrowdloanId = 0; + + assert_err!( + Crowdloan::dissolve(RuntimeOrigin::none(), crowdloan_id), + DispatchError::BadOrigin + ); + + assert_err!( + Crowdloan::dissolve(RuntimeOrigin::root(), crowdloan_id), + DispatchError::BadOrigin + ); + }); +} + +#[test] +fn test_dissolve_fails_if_crowdloan_does_not_exist() { + TestState::default().build_and_execute(|| { + let crowdloan_id: CrowdloanId = 0; + assert_err!( + Crowdloan::dissolve(RuntimeOrigin::signed(U256::from(1)), crowdloan_id), + pallet_crowdloan::Error::::InvalidCrowdloanId + ); + }); +} + +#[test] +fn test_dissolve_fails_if_crowdloan_has_been_finalized() { + TestState::default() + .with_balance(U256::from(1), 100) + .with_balance(U256::from(2), 100) + .build_and_execute(|| { + let creator: AccountOf = U256::from(1); + let deposit: BalanceOf = 50; + let min_contribution: BalanceOf = 10; + let cap: BalanceOf = 100; + let end: BlockNumberFor = 50; + + assert_ok!(Crowdloan::create( + RuntimeOrigin::signed(creator), + deposit, + min_contribution, + cap, + end, + Some(noop_call()), + None, + )); + + // run some blocks + run_to_block(10); + + // some contribution + let crowdloan_id: CrowdloanId = 0; + let contributor: AccountOf = U256::from(2); + let amount: BalanceOf = 50; + + assert_ok!(Crowdloan::contribute( + RuntimeOrigin::signed(contributor), + crowdloan_id, + amount + )); + + // run some more blocks past the end of the contribution period + run_to_block(60); + + // finalize the crowdloan + assert_ok!(Crowdloan::finalize( + RuntimeOrigin::signed(creator), + crowdloan_id + )); + + // try dissolve the crowdloan + assert_err!( + Crowdloan::dissolve(RuntimeOrigin::signed(creator), crowdloan_id), + pallet_crowdloan::Error::::AlreadyFinalized + ); + }); +} + +#[test] +fn test_dissolve_fails_if_origin_is_not_creator() { + TestState::default() + .with_balance(U256::from(1), 100) + .build_and_execute(|| { + let creator: AccountOf = U256::from(1); + let deposit: BalanceOf = 50; + let min_contribution: BalanceOf = 10; + let cap: BalanceOf = 100; + let end: BlockNumberFor = 50; + + assert_ok!(Crowdloan::create( + RuntimeOrigin::signed(creator), + deposit, + min_contribution, + cap, + end, + Some(noop_call()), + None, + )); + + // run some blocks + run_to_block(10); + + // some contribution + let crowdloan_id: CrowdloanId = 0; + + // try dissolve the crowdloan + assert_err!( + Crowdloan::dissolve(RuntimeOrigin::signed(U256::from(2)), crowdloan_id), + pallet_crowdloan::Error::::InvalidOrigin + ); + }); +} + +#[test] +fn test_dissolve_fails_if_not_everyone_has_been_refunded() { + TestState::default() + .with_balance(U256::from(1), 100) + .build_and_execute(|| { + // create a crowdloan + let creator: AccountOf = U256::from(1); + let deposit: BalanceOf = 50; + let min_contribution: BalanceOf = 10; + let cap: BalanceOf = 100; + let end: BlockNumberFor = 50; + + assert_ok!(Crowdloan::create( + RuntimeOrigin::signed(creator), + deposit, + min_contribution, + cap, + end, + Some(noop_call()), + None, + )); + + // run some blocks past end + run_to_block(10); + + // try to dissolve the crowdloan + let crowdloan_id = 0; + assert_err!( + Crowdloan::dissolve(RuntimeOrigin::signed(creator), crowdloan_id), + pallet_crowdloan::Error::::NotReadyToDissolve + ); + }); +} + +#[test] +fn test_update_min_contribution_succeeds() { + TestState::default() + .with_balance(U256::from(1), 100) + .build_and_execute(|| { + // create a crowdloan + let creator: AccountOf = U256::from(1); + let deposit: BalanceOf = 50; + let min_contribution: BalanceOf = 10; + let cap: BalanceOf = 100; + let end: BlockNumberFor = 50; + + assert_ok!(Crowdloan::create( + RuntimeOrigin::signed(creator), + deposit, + min_contribution, + cap, + end, + Some(noop_call()), + None, + )); + + let crowdloan_id: CrowdloanId = 0; + let new_min_contribution: BalanceOf = 20; + + // update the min contribution + assert_ok!(Crowdloan::update_min_contribution( + RuntimeOrigin::signed(creator), + crowdloan_id, + new_min_contribution + )); + + // ensure the min contribution is updated + assert!( + pallet_crowdloan::Crowdloans::::get(crowdloan_id) + .is_some_and(|c| c.min_contribution == new_min_contribution) + ); + // ensure the event is emitted + assert_eq!( + last_event(), + pallet_crowdloan::Event::::MinContributionUpdated { + crowdloan_id, + new_min_contribution + } + .into() + ); + }); +} + +#[test] +fn test_update_min_contribution_fails_if_bad_origin() { + TestState::default().build_and_execute(|| { + let crowdloan_id: CrowdloanId = 0; + + assert_err!( + Crowdloan::update_min_contribution(RuntimeOrigin::none(), crowdloan_id, 20), + DispatchError::BadOrigin + ); + + assert_err!( + Crowdloan::update_min_contribution(RuntimeOrigin::root(), crowdloan_id, 20), + DispatchError::BadOrigin + ); + }); +} + +#[test] +fn test_update_min_contribution_fails_if_crowdloan_does_not_exist() { + TestState::default().build_and_execute(|| { + let crowdloan_id: CrowdloanId = 0; + + assert_err!( + Crowdloan::update_min_contribution( + RuntimeOrigin::signed(U256::from(1)), + crowdloan_id, + 20 + ), + pallet_crowdloan::Error::::InvalidCrowdloanId + ); + }); +} + +#[test] +fn test_update_min_contribution_fails_if_crowdloan_has_been_finalized() { + TestState::default() + .with_balance(U256::from(1), 100) + .with_balance(U256::from(2), 100) + .build_and_execute(|| { + let creator: AccountOf = U256::from(1); + let deposit: BalanceOf = 50; + let min_contribution: BalanceOf = 10; + let cap: BalanceOf = 100; + let end: BlockNumberFor = 50; + + assert_ok!(Crowdloan::create( + RuntimeOrigin::signed(creator), + deposit, + min_contribution, + cap, + end, + Some(noop_call()), + None, + )); + + // some contribution + let crowdloan_id: CrowdloanId = 0; + let contributor: AccountOf = U256::from(2); + let amount: BalanceOf = 50; + assert_ok!(Crowdloan::contribute( + RuntimeOrigin::signed(contributor), + crowdloan_id, + amount + )); + + // run some blocks + run_to_block(50); + + // finalize the crowdloan + let crowdloan_id: CrowdloanId = 0; + assert_ok!(Crowdloan::finalize( + RuntimeOrigin::signed(creator), + crowdloan_id + )); + + // try update the min contribution + let new_min_contribution: BalanceOf = 20; + assert_err!( + Crowdloan::update_min_contribution( + RuntimeOrigin::signed(creator), + crowdloan_id, + new_min_contribution + ), + pallet_crowdloan::Error::::AlreadyFinalized + ); + }); +} + +#[test] +fn test_update_min_contribution_fails_if_not_creator() { + TestState::default() + .with_balance(U256::from(1), 100) + .with_balance(U256::from(2), 100) + .build_and_execute(|| { + let creator: AccountOf = U256::from(1); + let deposit: BalanceOf = 50; + let min_contribution: BalanceOf = 10; + let cap: BalanceOf = 100; + let end: BlockNumberFor = 50; + + assert_ok!(Crowdloan::create( + RuntimeOrigin::signed(creator), + deposit, + min_contribution, + cap, + end, + Some(noop_call()), + None, + )); + + let crowdloan_id: CrowdloanId = 0; + let new_min_contribution: BalanceOf = 20; + + // try update the min contribution + assert_err!( + Crowdloan::update_min_contribution( + RuntimeOrigin::signed(U256::from(2)), + crowdloan_id, + new_min_contribution + ), + pallet_crowdloan::Error::::InvalidOrigin + ); + }); +} + +#[test] +fn test_update_min_contribution_fails_if_new_min_contribution_is_too_low() { + TestState::default() + .with_balance(U256::from(1), 100) + .build_and_execute(|| { + let creator: AccountOf = U256::from(1); + let deposit: BalanceOf = 50; + let min_contribution: BalanceOf = 10; + let cap: BalanceOf = 100; + let end: BlockNumberFor = 50; + + assert_ok!(Crowdloan::create( + RuntimeOrigin::signed(creator), + deposit, + min_contribution, + cap, + end, + Some(noop_call()), + None, + )); + + let crowdloan_id: CrowdloanId = 0; + let new_min_contribution: BalanceOf = 10; + + // try update the min contribution + assert_err!( + Crowdloan::update_min_contribution( + RuntimeOrigin::signed(creator), + crowdloan_id, + new_min_contribution + ), + pallet_crowdloan::Error::::MinimumContributionTooLow + ); + }); +} + +#[test] +fn test_update_end_succeeds() { + TestState::default() + .with_balance(U256::from(1), 100) + .build_and_execute(|| { + let creator: AccountOf = U256::from(1); + let deposit: BalanceOf = 50; + let min_contribution: BalanceOf = 10; + let cap: BalanceOf = 100; + let end: BlockNumberFor = 50; + + assert_ok!(Crowdloan::create( + RuntimeOrigin::signed(creator), + deposit, + min_contribution, + cap, + end, + Some(noop_call()), + None, + )); + + let crowdloan_id: CrowdloanId = 0; + let new_end: BlockNumberFor = 60; + + // update the end + assert_ok!(Crowdloan::update_end( + RuntimeOrigin::signed(creator), + crowdloan_id, + new_end + )); + + // ensure the end is updated + assert!( + pallet_crowdloan::Crowdloans::::get(crowdloan_id) + .is_some_and(|c| c.end == new_end) + ); + // ensure the event is emitted + assert_eq!( + last_event(), + pallet_crowdloan::Event::::EndUpdated { + crowdloan_id, + new_end + } + .into() + ); + }); +} + +#[test] +fn test_update_end_fails_if_bad_origin() { + TestState::default().build_and_execute(|| { + let crowdloan_id: CrowdloanId = 0; + + assert_err!( + Crowdloan::update_end(RuntimeOrigin::none(), crowdloan_id, 60), + DispatchError::BadOrigin + ); + + assert_err!( + Crowdloan::update_end(RuntimeOrigin::root(), crowdloan_id, 60), + DispatchError::BadOrigin + ); + }); +} + +#[test] +fn test_update_end_fails_if_crowdloan_does_not_exist() { + TestState::default().build_and_execute(|| { + let crowdloan_id: CrowdloanId = 0; + + assert_err!( + Crowdloan::update_end(RuntimeOrigin::signed(U256::from(1)), crowdloan_id, 60), + pallet_crowdloan::Error::::InvalidCrowdloanId + ); + }); +} + +#[test] +fn test_update_end_fails_if_crowdloan_has_been_finalized() { + TestState::default() + .with_balance(U256::from(1), 100) + .with_balance(U256::from(2), 100) + .build_and_execute(|| { + let creator: AccountOf = U256::from(1); + let deposit: BalanceOf = 50; + let min_contribution: BalanceOf = 10; + let cap: BalanceOf = 100; + let end: BlockNumberFor = 50; + + assert_ok!(Crowdloan::create( + RuntimeOrigin::signed(creator), + deposit, + min_contribution, + cap, + end, + Some(noop_call()), + None, + )); + + let crowdloan_id: CrowdloanId = 0; + + // some contribution + let contributor: AccountOf = U256::from(2); + let amount: BalanceOf = 50; + assert_ok!(Crowdloan::contribute( + RuntimeOrigin::signed(contributor), + crowdloan_id, + amount + )); + + // run some blocks + run_to_block(60); + + // finalize the crowdloan + assert_ok!(Crowdloan::finalize( + RuntimeOrigin::signed(creator), + crowdloan_id + )); + + // try update the end + let new_end: BlockNumberFor = 60; + assert_err!( + Crowdloan::update_end(RuntimeOrigin::signed(creator), crowdloan_id, new_end), + pallet_crowdloan::Error::::AlreadyFinalized + ); + }); +} + +#[test] +fn test_update_end_fails_if_not_creator() { + TestState::default() + .with_balance(U256::from(1), 100) + .with_balance(U256::from(2), 100) + .build_and_execute(|| { + let creator: AccountOf = U256::from(1); + let deposit: BalanceOf = 50; + let min_contribution: BalanceOf = 10; + let cap: BalanceOf = 100; + let end: BlockNumberFor = 50; + + assert_ok!(Crowdloan::create( + RuntimeOrigin::signed(creator), + deposit, + min_contribution, + cap, + end, + Some(noop_call()), + None, + )); + + let crowdloan_id: CrowdloanId = 0; + let new_end: BlockNumberFor = 60; + + // try update the end + assert_err!( + Crowdloan::update_end(RuntimeOrigin::signed(U256::from(2)), crowdloan_id, new_end), + pallet_crowdloan::Error::::InvalidOrigin + ); + }); +} + +#[test] +fn test_update_end_fails_if_new_end_is_in_past() { + TestState::default() + .with_block_number(50) + .with_balance(U256::from(1), 100) + .build_and_execute(|| { + let creator: AccountOf = U256::from(1); + let deposit: BalanceOf = 50; + let min_contribution: BalanceOf = 10; + let cap: BalanceOf = 100; + let end: BlockNumberFor = 100; + + assert_ok!(Crowdloan::create( + RuntimeOrigin::signed(creator), + deposit, + min_contribution, + cap, + end, + Some(noop_call()), + None, + )); + + let crowdloan_id: CrowdloanId = 0; + let new_end: BlockNumberFor = 40; + + // try update the end to a past block number + assert_err!( + Crowdloan::update_end(RuntimeOrigin::signed(creator), crowdloan_id, new_end), + pallet_crowdloan::Error::::CannotEndInPast + ); + }); +} + +#[test] +fn test_update_end_fails_if_block_duration_is_too_short() { + TestState::default() + .with_balance(U256::from(1), 100) + .build_and_execute(|| { + let creator: AccountOf = U256::from(1); + let deposit: BalanceOf = 50; + let min_contribution: BalanceOf = 10; + let cap: BalanceOf = 100; + let end: BlockNumberFor = 50; + + assert_ok!(Crowdloan::create( + RuntimeOrigin::signed(creator), + deposit, + min_contribution, + cap, + end, + Some(noop_call()), + None, + )); + + // run some blocks + run_to_block(50); + + let crowdloan_id: CrowdloanId = 0; + let new_end: BlockNumberFor = 51; + + // try update the end to a block number that is too long + assert_err!( + Crowdloan::update_end(RuntimeOrigin::signed(creator), crowdloan_id, new_end), + pallet_crowdloan::Error::::BlockDurationTooShort + ); + }); +} + +#[test] +fn test_update_end_fails_if_block_duration_is_too_long() { + TestState::default() + .with_balance(U256::from(1), 100) + .build_and_execute(|| { + let creator: AccountOf = U256::from(1); + let deposit: BalanceOf = 50; + let min_contribution: BalanceOf = 10; + let cap: BalanceOf = 100; + let end: BlockNumberFor = 50; + + assert_ok!(Crowdloan::create( + RuntimeOrigin::signed(creator), + deposit, + min_contribution, + cap, + end, + Some(noop_call()), + None, + )); + + let crowdloan_id: CrowdloanId = 0; + let new_end: BlockNumberFor = 1000; + + // try update the end to a block number that is too long + assert_err!( + Crowdloan::update_end(RuntimeOrigin::signed(creator), crowdloan_id, new_end), + pallet_crowdloan::Error::::BlockDurationTooLong + ); + }); +} + +#[test] +fn test_update_cap_succeeds() { + TestState::default() + .with_balance(U256::from(1), 100) + .build_and_execute(|| { + let creator: AccountOf = U256::from(1); + let deposit: BalanceOf = 50; + let min_contribution: BalanceOf = 10; + let cap: BalanceOf = 100; + let end: BlockNumberFor = 50; + + assert_ok!(Crowdloan::create( + RuntimeOrigin::signed(creator), + deposit, + min_contribution, + cap, + end, + Some(noop_call()), + None, + )); + + // try update the cap + let crowdloan_id: CrowdloanId = 0; + let new_cap: BalanceOf = 200; + assert_ok!(Crowdloan::update_cap( + RuntimeOrigin::signed(creator), + crowdloan_id, + new_cap + )); + + // ensure the cap is updated + assert!( + pallet_crowdloan::Crowdloans::::get(crowdloan_id) + .is_some_and(|c| c.cap == new_cap) + ); + // ensure the event is emitted + assert_eq!( + last_event(), + pallet_crowdloan::Event::::CapUpdated { + crowdloan_id, + new_cap + } + .into() + ); + }); +} + +#[test] +fn test_update_cap_fails_if_bad_origin() { + TestState::default().build_and_execute(|| { + let crowdloan_id: CrowdloanId = 0; + + assert_err!( + Crowdloan::update_cap(RuntimeOrigin::none(), crowdloan_id, 200), + DispatchError::BadOrigin + ); + + assert_err!( + Crowdloan::update_cap(RuntimeOrigin::root(), crowdloan_id, 200), + DispatchError::BadOrigin + ); + }); +} + +#[test] +fn test_update_cap_fails_if_crowdloan_does_not_exist() { + TestState::default().build_and_execute(|| { + let crowdloan_id: CrowdloanId = 0; + + assert_err!( + Crowdloan::update_cap(RuntimeOrigin::signed(U256::from(1)), crowdloan_id, 200), + pallet_crowdloan::Error::::InvalidCrowdloanId + ); + }); +} + +#[test] +fn test_update_cap_fails_if_crowdloan_has_been_finalized() { + TestState::default() + .with_balance(U256::from(1), 100) + .with_balance(U256::from(2), 100) + .build_and_execute(|| { + let creator: AccountOf = U256::from(1); + let deposit: BalanceOf = 50; + let min_contribution: BalanceOf = 10; + let cap: BalanceOf = 100; + let end: BlockNumberFor = 50; + + assert_ok!(Crowdloan::create( + RuntimeOrigin::signed(creator), + deposit, + min_contribution, + cap, + end, + Some(noop_call()), + None, + )); + + // some contribution + let crowdloan_id: CrowdloanId = 0; + let contributor: AccountOf = U256::from(2); + let amount: BalanceOf = 50; + assert_ok!(Crowdloan::contribute( + RuntimeOrigin::signed(contributor), + crowdloan_id, + amount + )); + + // run some blocks + run_to_block(60); + + // finalize the crowdloan + let crowdloan_id: CrowdloanId = 0; + assert_ok!(Crowdloan::finalize( + RuntimeOrigin::signed(creator), + crowdloan_id + )); + + // try update the cap + let new_cap: BalanceOf = 200; + assert_err!( + Crowdloan::update_cap(RuntimeOrigin::signed(creator), crowdloan_id, new_cap), + pallet_crowdloan::Error::::AlreadyFinalized + ); + }); +} + +#[test] +fn test_update_cap_fails_if_not_creator() { + TestState::default() + .with_balance(U256::from(1), 100) + .with_balance(U256::from(2), 100) + .build_and_execute(|| { + let creator: AccountOf = U256::from(1); + let deposit: BalanceOf = 50; + let min_contribution: BalanceOf = 10; + let cap: BalanceOf = 100; + let end: BlockNumberFor = 50; + + assert_ok!(Crowdloan::create( + RuntimeOrigin::signed(creator), + deposit, + min_contribution, + cap, + end, + Some(noop_call()), + None, + )); + + // try update the cap + let crowdloan_id: CrowdloanId = 0; + let new_cap: BalanceOf = 200; + assert_err!( + Crowdloan::update_cap(RuntimeOrigin::signed(U256::from(2)), crowdloan_id, new_cap), + pallet_crowdloan::Error::::InvalidOrigin + ); + }); +} + +#[test] +fn test_update_cap_fails_if_new_cap_is_too_low() { + TestState::default() + .with_balance(U256::from(1), 100) + .build_and_execute(|| { + let creator: AccountOf = U256::from(1); + let deposit: BalanceOf = 50; + let min_contribution: BalanceOf = 10; + let cap: BalanceOf = 100; + let end: BlockNumberFor = 50; + + assert_ok!(Crowdloan::create( + RuntimeOrigin::signed(creator), + deposit, + min_contribution, + cap, + end, + Some(noop_call()), + None, + )); + + // try update the cap + let crowdloan_id: CrowdloanId = 0; + let new_cap: BalanceOf = 50; + assert_err!( + Crowdloan::update_cap(RuntimeOrigin::signed(creator), crowdloan_id, new_cap), + pallet_crowdloan::Error::::CapTooLow + ); + }); +} diff --git a/pallets/crowdloan/src/weights.rs b/pallets/crowdloan/src/weights.rs new file mode 100644 index 0000000000..988f7a4efa --- /dev/null +++ b/pallets/crowdloan/src/weights.rs @@ -0,0 +1,314 @@ + +//! Autogenerated weights for `pallet_crowdloan` +//! +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 43.0.0 +//! DATE: 2025-04-22, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! WORST CASE MAP SIZE: `1000000` +//! HOSTNAME: `Ubuntu-2404-noble-amd64-base`, CPU: `AMD Ryzen 9 7950X3D 16-Core Processor` +//! WASM-EXECUTION: `Compiled`, CHAIN: `Some("local")`, DB CACHE: `1024` + +// Executed Command: +// ./target/production/node-subtensor +// benchmark +// pallet +// --chain=local +// --wasm-execution=compiled +// --pallet=pallet-crowdloan +// --extrinsic=* +// --steps=50 +// --repeat=20 +// --output=pallets/crowdloan/src/weights.rs +// --template=./.maintain/frame-weight-template.hbs +// --allow-missing-host-functions + +#![cfg_attr(rustfmt, rustfmt_skip)] +#![allow(unused_parens)] +#![allow(unused_imports)] +#![allow(missing_docs)] + +use frame_support::{traits::Get, weights::{Weight, constants::RocksDbWeight}}; +use core::marker::PhantomData; + +/// Weight functions needed for `pallet_crowdloan`. +pub trait WeightInfo { + fn create() -> Weight; + fn contribute() -> Weight; + fn withdraw() -> Weight; + fn finalize() -> Weight; + fn refund(k: u32, ) -> Weight; + fn dissolve() -> Weight; + fn update_min_contribution() -> Weight; + fn update_end() -> Weight; + fn update_cap() -> Weight; +} + +/// Weights for `pallet_crowdloan` using the Substrate node and recommended hardware. +pub struct SubstrateWeight(PhantomData); +impl WeightInfo for SubstrateWeight { + /// Storage: `System::Account` (r:2 w:2) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(104), added: 2579, mode: `MaxEncodedLen`) + /// Storage: `Crowdloan::NextCrowdloanId` (r:1 w:1) + /// Proof: `Crowdloan::NextCrowdloanId` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `Crowdloan::Contributions` (r:0 w:1) + /// Proof: `Crowdloan::Contributions` (`max_values`: None, `max_size`: Some(52), added: 2527, mode: `MaxEncodedLen`) + /// Storage: `Crowdloan::Crowdloans` (r:0 w:1) + /// Proof: `Crowdloan::Crowdloans` (`max_values`: None, `max_size`: Some(278), added: 2753, mode: `MaxEncodedLen`) + fn create() -> Weight { + // Proof Size summary in bytes: + // Measured: `156` + // Estimated: `6148` + // Minimum execution time: 40_556_000 picoseconds. + Weight::from_parts(41_318_000, 6148) + .saturating_add(T::DbWeight::get().reads(3_u64)) + .saturating_add(T::DbWeight::get().writes(5_u64)) + } + /// Storage: `Crowdloan::Crowdloans` (r:1 w:1) + /// Proof: `Crowdloan::Crowdloans` (`max_values`: None, `max_size`: Some(278), added: 2753, mode: `MaxEncodedLen`) + /// Storage: `Crowdloan::Contributions` (r:1 w:1) + /// Proof: `Crowdloan::Contributions` (`max_values`: None, `max_size`: Some(52), added: 2527, mode: `MaxEncodedLen`) + /// Storage: `System::Account` (r:2 w:2) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(104), added: 2579, mode: `MaxEncodedLen`) + fn contribute() -> Weight { + // Proof Size summary in bytes: + // Measured: `476` + // Estimated: `6148` + // Minimum execution time: 42_900_000 picoseconds. + Weight::from_parts(43_682_000, 6148) + .saturating_add(T::DbWeight::get().reads(4_u64)) + .saturating_add(T::DbWeight::get().writes(4_u64)) + } + /// Storage: `Crowdloan::Crowdloans` (r:1 w:1) + /// Proof: `Crowdloan::Crowdloans` (`max_values`: None, `max_size`: Some(278), added: 2753, mode: `MaxEncodedLen`) + /// Storage: `Crowdloan::Contributions` (r:1 w:1) + /// Proof: `Crowdloan::Contributions` (`max_values`: None, `max_size`: Some(52), added: 2527, mode: `MaxEncodedLen`) + /// Storage: `System::Account` (r:2 w:2) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(104), added: 2579, mode: `MaxEncodedLen`) + fn withdraw() -> Weight { + // Proof Size summary in bytes: + // Measured: `436` + // Estimated: `6148` + // Minimum execution time: 41_037_000 picoseconds. + Weight::from_parts(41_968_000, 6148) + .saturating_add(T::DbWeight::get().reads(4_u64)) + .saturating_add(T::DbWeight::get().writes(4_u64)) + } + /// Storage: `Crowdloan::Crowdloans` (r:1 w:1) + /// Proof: `Crowdloan::Crowdloans` (`max_values`: None, `max_size`: Some(278), added: 2753, mode: `MaxEncodedLen`) + /// Storage: `System::Account` (r:2 w:2) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(104), added: 2579, mode: `MaxEncodedLen`) + /// Storage: `SafeMode::EnteredUntil` (r:1 w:0) + /// Proof: `SafeMode::EnteredUntil` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `Crowdloan::CurrentCrowdloanId` (r:0 w:1) + /// Proof: `Crowdloan::CurrentCrowdloanId` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + fn finalize() -> Weight { + // Proof Size summary in bytes: + // Measured: `376` + // Estimated: `6148` + // Minimum execution time: 41_567_000 picoseconds. + Weight::from_parts(42_088_000, 6148) + .saturating_add(T::DbWeight::get().reads(4_u64)) + .saturating_add(T::DbWeight::get().writes(4_u64)) + } + /// Storage: `Crowdloan::Crowdloans` (r:1 w:1) + /// Proof: `Crowdloan::Crowdloans` (`max_values`: None, `max_size`: Some(278), added: 2753, mode: `MaxEncodedLen`) + /// Storage: `Crowdloan::Contributions` (r:51 w:50) + /// Proof: `Crowdloan::Contributions` (`max_values`: None, `max_size`: Some(52), added: 2527, mode: `MaxEncodedLen`) + /// Storage: `System::Account` (r:51 w:51) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(104), added: 2579, mode: `MaxEncodedLen`) + /// The range of component `k` is `[3, 50]`. + fn refund(k: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `440 + k * (48 ±0)` + // Estimated: `3743 + k * (2579 ±0)` + // Minimum execution time: 97_612_000 picoseconds. + Weight::from_parts(36_327_787, 3743) + // Standard Error: 81_635 + .saturating_add(Weight::from_parts(25_989_645, 0).saturating_mul(k.into())) + .saturating_add(T::DbWeight::get().reads(3_u64)) + .saturating_add(T::DbWeight::get().reads((2_u64).saturating_mul(k.into()))) + .saturating_add(T::DbWeight::get().writes(2_u64)) + .saturating_add(T::DbWeight::get().writes((2_u64).saturating_mul(k.into()))) + .saturating_add(Weight::from_parts(0, 2579).saturating_mul(k.into())) + } + /// Storage: `Crowdloan::Crowdloans` (r:1 w:1) + /// Proof: `Crowdloan::Crowdloans` (`max_values`: None, `max_size`: Some(278), added: 2753, mode: `MaxEncodedLen`) + /// Storage: `System::Account` (r:1 w:1) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(104), added: 2579, mode: `MaxEncodedLen`) + fn dissolve() -> Weight { + // Proof Size summary in bytes: + // Measured: `321` + // Estimated: `3743` + // Minimum execution time: 11_832_000 picoseconds. + Weight::from_parts(12_293_000, 3743) + .saturating_add(T::DbWeight::get().reads(2_u64)) + .saturating_add(T::DbWeight::get().writes(2_u64)) + } + /// Storage: `Crowdloan::Crowdloans` (r:1 w:1) + /// Proof: `Crowdloan::Crowdloans` (`max_values`: None, `max_size`: Some(278), added: 2753, mode: `MaxEncodedLen`) + fn update_min_contribution() -> Weight { + // Proof Size summary in bytes: + // Measured: `224` + // Estimated: `3743` + // Minimum execution time: 8_776_000 picoseconds. + Weight::from_parts(9_057_000, 3743) + .saturating_add(T::DbWeight::get().reads(1_u64)) + .saturating_add(T::DbWeight::get().writes(1_u64)) + } + /// Storage: `Crowdloan::Crowdloans` (r:1 w:1) + /// Proof: `Crowdloan::Crowdloans` (`max_values`: None, `max_size`: Some(278), added: 2753, mode: `MaxEncodedLen`) + fn update_end() -> Weight { + // Proof Size summary in bytes: + // Measured: `224` + // Estimated: `3743` + // Minimum execution time: 9_067_000 picoseconds. + Weight::from_parts(9_368_000, 3743) + .saturating_add(T::DbWeight::get().reads(1_u64)) + .saturating_add(T::DbWeight::get().writes(1_u64)) + } + /// Storage: `Crowdloan::Crowdloans` (r:1 w:1) + /// Proof: `Crowdloan::Crowdloans` (`max_values`: None, `max_size`: Some(278), added: 2753, mode: `MaxEncodedLen`) + fn update_cap() -> Weight { + // Proof Size summary in bytes: + // Measured: `224` + // Estimated: `3743` + // Minimum execution time: 8_636_000 picoseconds. + Weight::from_parts(9_027_000, 3743) + .saturating_add(T::DbWeight::get().reads(1_u64)) + .saturating_add(T::DbWeight::get().writes(1_u64)) + } +} + +// For backwards compatibility and tests. +impl WeightInfo for () { + /// Storage: `System::Account` (r:2 w:2) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(104), added: 2579, mode: `MaxEncodedLen`) + /// Storage: `Crowdloan::NextCrowdloanId` (r:1 w:1) + /// Proof: `Crowdloan::NextCrowdloanId` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `Crowdloan::Contributions` (r:0 w:1) + /// Proof: `Crowdloan::Contributions` (`max_values`: None, `max_size`: Some(52), added: 2527, mode: `MaxEncodedLen`) + /// Storage: `Crowdloan::Crowdloans` (r:0 w:1) + /// Proof: `Crowdloan::Crowdloans` (`max_values`: None, `max_size`: Some(278), added: 2753, mode: `MaxEncodedLen`) + fn create() -> Weight { + // Proof Size summary in bytes: + // Measured: `156` + // Estimated: `6148` + // Minimum execution time: 40_556_000 picoseconds. + Weight::from_parts(41_318_000, 6148) + .saturating_add(RocksDbWeight::get().reads(3_u64)) + .saturating_add(RocksDbWeight::get().writes(5_u64)) + } + /// Storage: `Crowdloan::Crowdloans` (r:1 w:1) + /// Proof: `Crowdloan::Crowdloans` (`max_values`: None, `max_size`: Some(278), added: 2753, mode: `MaxEncodedLen`) + /// Storage: `Crowdloan::Contributions` (r:1 w:1) + /// Proof: `Crowdloan::Contributions` (`max_values`: None, `max_size`: Some(52), added: 2527, mode: `MaxEncodedLen`) + /// Storage: `System::Account` (r:2 w:2) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(104), added: 2579, mode: `MaxEncodedLen`) + fn contribute() -> Weight { + // Proof Size summary in bytes: + // Measured: `476` + // Estimated: `6148` + // Minimum execution time: 42_900_000 picoseconds. + Weight::from_parts(43_682_000, 6148) + .saturating_add(RocksDbWeight::get().reads(4_u64)) + .saturating_add(RocksDbWeight::get().writes(4_u64)) + } + /// Storage: `Crowdloan::Crowdloans` (r:1 w:1) + /// Proof: `Crowdloan::Crowdloans` (`max_values`: None, `max_size`: Some(278), added: 2753, mode: `MaxEncodedLen`) + /// Storage: `Crowdloan::Contributions` (r:1 w:1) + /// Proof: `Crowdloan::Contributions` (`max_values`: None, `max_size`: Some(52), added: 2527, mode: `MaxEncodedLen`) + /// Storage: `System::Account` (r:2 w:2) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(104), added: 2579, mode: `MaxEncodedLen`) + fn withdraw() -> Weight { + // Proof Size summary in bytes: + // Measured: `436` + // Estimated: `6148` + // Minimum execution time: 41_037_000 picoseconds. + Weight::from_parts(41_968_000, 6148) + .saturating_add(RocksDbWeight::get().reads(4_u64)) + .saturating_add(RocksDbWeight::get().writes(4_u64)) + } + /// Storage: `Crowdloan::Crowdloans` (r:1 w:1) + /// Proof: `Crowdloan::Crowdloans` (`max_values`: None, `max_size`: Some(278), added: 2753, mode: `MaxEncodedLen`) + /// Storage: `System::Account` (r:2 w:2) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(104), added: 2579, mode: `MaxEncodedLen`) + /// Storage: `SafeMode::EnteredUntil` (r:1 w:0) + /// Proof: `SafeMode::EnteredUntil` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `Crowdloan::CurrentCrowdloanId` (r:0 w:1) + /// Proof: `Crowdloan::CurrentCrowdloanId` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + fn finalize() -> Weight { + // Proof Size summary in bytes: + // Measured: `376` + // Estimated: `6148` + // Minimum execution time: 41_567_000 picoseconds. + Weight::from_parts(42_088_000, 6148) + .saturating_add(RocksDbWeight::get().reads(4_u64)) + .saturating_add(RocksDbWeight::get().writes(4_u64)) + } + /// Storage: `Crowdloan::Crowdloans` (r:1 w:1) + /// Proof: `Crowdloan::Crowdloans` (`max_values`: None, `max_size`: Some(278), added: 2753, mode: `MaxEncodedLen`) + /// Storage: `Crowdloan::Contributions` (r:51 w:50) + /// Proof: `Crowdloan::Contributions` (`max_values`: None, `max_size`: Some(52), added: 2527, mode: `MaxEncodedLen`) + /// Storage: `System::Account` (r:51 w:51) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(104), added: 2579, mode: `MaxEncodedLen`) + /// The range of component `k` is `[3, 50]`. + fn refund(k: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `440 + k * (48 ±0)` + // Estimated: `3743 + k * (2579 ±0)` + // Minimum execution time: 97_612_000 picoseconds. + Weight::from_parts(36_327_787, 3743) + // Standard Error: 81_635 + .saturating_add(Weight::from_parts(25_989_645, 0).saturating_mul(k.into())) + .saturating_add(RocksDbWeight::get().reads(3_u64)) + .saturating_add(RocksDbWeight::get().reads((2_u64).saturating_mul(k.into()))) + .saturating_add(RocksDbWeight::get().writes(2_u64)) + .saturating_add(RocksDbWeight::get().writes((2_u64).saturating_mul(k.into()))) + .saturating_add(Weight::from_parts(0, 2579).saturating_mul(k.into())) + } + /// Storage: `Crowdloan::Crowdloans` (r:1 w:1) + /// Proof: `Crowdloan::Crowdloans` (`max_values`: None, `max_size`: Some(278), added: 2753, mode: `MaxEncodedLen`) + /// Storage: `System::Account` (r:1 w:1) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(104), added: 2579, mode: `MaxEncodedLen`) + fn dissolve() -> Weight { + // Proof Size summary in bytes: + // Measured: `321` + // Estimated: `3743` + // Minimum execution time: 11_832_000 picoseconds. + Weight::from_parts(12_293_000, 3743) + .saturating_add(RocksDbWeight::get().reads(2_u64)) + .saturating_add(RocksDbWeight::get().writes(2_u64)) + } + /// Storage: `Crowdloan::Crowdloans` (r:1 w:1) + /// Proof: `Crowdloan::Crowdloans` (`max_values`: None, `max_size`: Some(278), added: 2753, mode: `MaxEncodedLen`) + fn update_min_contribution() -> Weight { + // Proof Size summary in bytes: + // Measured: `224` + // Estimated: `3743` + // Minimum execution time: 8_776_000 picoseconds. + Weight::from_parts(9_057_000, 3743) + .saturating_add(RocksDbWeight::get().reads(1_u64)) + .saturating_add(RocksDbWeight::get().writes(1_u64)) + } + /// Storage: `Crowdloan::Crowdloans` (r:1 w:1) + /// Proof: `Crowdloan::Crowdloans` (`max_values`: None, `max_size`: Some(278), added: 2753, mode: `MaxEncodedLen`) + fn update_end() -> Weight { + // Proof Size summary in bytes: + // Measured: `224` + // Estimated: `3743` + // Minimum execution time: 9_067_000 picoseconds. + Weight::from_parts(9_368_000, 3743) + .saturating_add(RocksDbWeight::get().reads(1_u64)) + .saturating_add(RocksDbWeight::get().writes(1_u64)) + } + /// Storage: `Crowdloan::Crowdloans` (r:1 w:1) + /// Proof: `Crowdloan::Crowdloans` (`max_values`: None, `max_size`: Some(278), added: 2753, mode: `MaxEncodedLen`) + fn update_cap() -> Weight { + // Proof Size summary in bytes: + // Measured: `224` + // Estimated: `3743` + // Minimum execution time: 8_636_000 picoseconds. + Weight::from_parts(9_027_000, 3743) + .saturating_add(RocksDbWeight::get().reads(1_u64)) + .saturating_add(RocksDbWeight::get().writes(1_u64)) + } +} \ No newline at end of file diff --git a/runtime/Cargo.toml b/runtime/Cargo.toml index 67add266a4..f417a18afc 100644 --- a/runtime/Cargo.toml +++ b/runtime/Cargo.toml @@ -122,6 +122,9 @@ w3f-bls = { workspace = true } sha2 = { workspace = true } ark-serialize = { workspace = true } +# Crowdloan +pallet-crowdloan = { workspace = true } + [dev-dependencies] frame-metadata = { workspace = true } sp-io = { workspace = true } @@ -191,6 +194,7 @@ std = [ "sp-genesis-builder/std", "subtensor-precompiles/std", "subtensor-runtime-common/std", + "pallet-crowdloan/std", # Frontier "fp-evm/std", "fp-rpc/std", @@ -236,6 +240,7 @@ runtime-benchmarks = [ "pallet-preimage/runtime-benchmarks", "pallet-scheduler/runtime-benchmarks", "pallet-sudo/runtime-benchmarks", + "pallet-crowdloan/runtime-benchmarks", # EVM + Frontier "pallet-ethereum/runtime-benchmarks", @@ -269,6 +274,7 @@ try-runtime = [ "pallet-admin-utils/try-runtime", "pallet-commitments/try-runtime", "pallet-registry/try-runtime", + "pallet-crowdloan/try-runtime", # EVM + Frontier "fp-self-contained/try-runtime", diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index 7e94c66bc8..a7a89608c9 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -14,6 +14,7 @@ mod migrations; use codec::{Compact, Decode, Encode}; use frame_support::traits::Imbalance; use frame_support::{ + PalletId, dispatch::DispatchResultWithPostInfo, genesis_builder_helper::{build_state, get_preset}, pallet_prelude::Get, @@ -1430,6 +1431,38 @@ impl fp_self_contained::SelfContainedCall for RuntimeCall { } } +// Crowdloan +parameter_types! { + pub const CrowdloanPalletId: PalletId = PalletId(*b"bt/cloan"); + pub const MinimumDeposit: Balance = 10_000_000_000; // 10 TAO + pub const AbsoluteMinimumContribution: Balance = 100_000_000; // 0.1 TAO + pub const MinimumBlockDuration: BlockNumber = if cfg!(feature = "fast-blocks") { + 50 + } else { + 50400 // 7 days minimum (7 * 24 * 60 * 60 / 12) + }; + pub const MaximumBlockDuration: BlockNumber = if cfg!(feature = "fast-blocks") { + 20000 + } else { + 432000 // 60 days maximum (60 * 24 * 60 * 60 / 12) + }; + pub const RefundContributorsLimit: u32 = 50; +} + +impl pallet_crowdloan::Config for Runtime { + type PalletId = CrowdloanPalletId; + type RuntimeEvent = RuntimeEvent; + type RuntimeCall = RuntimeCall; + type Currency = Balances; + type WeightInfo = pallet_crowdloan::weights::SubstrateWeight; + type Preimages = Preimage; + type MinimumDeposit = MinimumDeposit; + type AbsoluteMinimumContribution = AbsoluteMinimumContribution; + type MinimumBlockDuration = MinimumBlockDuration; + type MaximumBlockDuration = MaximumBlockDuration; + type RefundContributorsLimit = RefundContributorsLimit; +} + // Create the runtime by composing the FRAME pallets that were previously configured. construct_runtime!( pub struct Runtime @@ -1464,6 +1497,8 @@ construct_runtime!( BaseFee: pallet_base_fee = 25, Drand: pallet_drand = 26, + + Crowdloan: pallet_crowdloan = 27, } ); @@ -1533,6 +1568,7 @@ mod benches { [pallet_admin_utils, AdminUtils] [pallet_subtensor, SubtensorModule] [pallet_drand, Drand] + [pallet_crowdloan, Crowdloan] ); }