//! # Crowdloan Pallet //! //! A pallet allowing users to create generic crowdloans and contribute to them, //! then finalize them through exactly one configured route: transfer the raised //! funds to a target address or dispatch an extrinsic, making it reusable for any //! crowdloan type. #![cfg_attr(not(feature = "std"), no_std)] extern crate alloc; use alloc::{boxed::Box, vec}; use codec::{Decode, Encode}; use frame_support::{ PalletId, dispatch::GetDispatchInfo, pallet_prelude::*, sp_runtime::{ RuntimeDebug, Saturating, 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 sp_std::vec::Vec; use subtensor_runtime_common::TaoBalance; use weights::WeightInfo; pub use pallet::*; use subtensor_macros::freeze_struct; pub type CrowdloanId = u32; mod benchmarking; mod migrations; mod mock; mod tests; pub mod weights; pub type CurrencyOf = ::Currency; pub type BalanceOf = as fungible::Inspect<::AccountId>>::Balance; // Define a maximum length for the migration key type MigrationKeyMaxLen = ConstU32<128>; pub type BoundedCallOf = Bounded<::RuntimeCall, ::Hashing>; /// A struct containing the information about a crowdloan. #[freeze_struct("5db9538284491545")] #[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, /// The number of contributors to the crowdloan. pub contributors_count: u32, } pub type CrowdloanInfoOf = CrowdloanInfo< ::AccountId, BalanceOf, BlockNumberFor, BoundedCallOf, >; #[frame_support::pallet] #[allow(clippy::expect_used)] pub mod pallet { use super::*; #[pallet::pallet] pub struct Pallet(_); /// Configuration trait. #[pallet::config] pub trait Config: frame_system::Config { /// 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; // The maximum number of contributors that can contribute to a crowdloan. #[pallet::constant] type MaxContributors: 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, >; /// A map of crowdloan ids to their optional maximum cumulative contribution per contributor. #[pallet::storage] pub type MaxContributions = StorageMap<_, Twox64Concat, CrowdloanId, 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>; /// Storage for the migration run status. #[pallet::storage] pub type HasMigrationRun = StorageMap<_, Identity, BoundedVec, bool, ValueQuery>; #[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, }, /// The maximum contribution was updated. MaxContributionUpdated { crowdloan_id: CrowdloanId, new_max_contribution: Option>, }, } #[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, /// A crowdloan finalization is already in progress. AlreadyFinalizing, /// 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, /// The deposit cannot be withdrawn from the crowdloan. DepositCannotBeWithdrawn, /// The maximum number of contributors has been reached. MaxContributorsReached, /// Exactly one of call or target address must be provided. InvalidFinalizationConfig, /// The contributor has already reached the maximum contribution. MaxContributionReached, /// The maximum contribution is too low. MaximumContributionTooLow, /// The minimum contribution is too high. MinimumContributionTooHigh, } #[pallet::hooks] impl Hooks> for Pallet { fn on_runtime_upgrade() -> frame_support::weights::Weight { let mut weight = frame_support::weights::Weight::from_parts(0, 0); weight = weight // Add the contributors count for each crowdloan .saturating_add(migrations::migrate_add_contributors_count::()); weight } } #[pallet::call] impl Pallet { #![deny(clippy::expect_used)] /// Create a crowdloan that will raise funds up to a maximum cap and if successful, /// will either transfer funds to the target address or dispatch the call /// (using creator origin). Exactly one of call or target address must be provided. /// Providing both, or providing neither, is rejected. /// /// The initial deposit will be transferred 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. #[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.call_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 ); ensure!( call.is_some() != target_address.is_some(), Error::::InvalidFinalizationConfig ); 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, contributors_count: 1, }; 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 transferred 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 the crowdloan has not reached the maximum number of contributors ensure!( crowdloan.contributors_count < T::MaxContributors::get(), Error::::MaxContributorsReached ); // Compute how much room is left before the crowdloan reaches its cap. let left_to_raise = crowdloan .cap .checked_sub(&crowdloan.raised) .ok_or(Error::::Underflow)?; // The requested contribution must meet the minimum contribution, but // the accepted amount may be lower when only a smaller remainder can // be accepted before reaching the crowdloan cap or the contributor's // maximum contribution. let amount = if let Some(max_contribution) = MaxContributions::::get(crowdloan_id) { let current_contribution = Contributions::::get(crowdloan_id, &contributor).unwrap_or_else(Zero::zero); ensure!( current_contribution < max_contribution, Error::::MaxContributionReached ); let left_to_contribute = max_contribution .checked_sub(¤t_contribution) .ok_or(Error::::Underflow)?; amount.min(left_to_contribute).min(left_to_raise) } else { 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, we // also increment the contributor count if the contribution is new. let contribution = if let Some(contribution) = Contributions::::get(crowdloan_id, &contributor) { contribution .checked_add(&amount) .ok_or(Error::::Overflow)? } else { // We have a new contribution crowdloan.contributors_count = crowdloan .contributors_count .checked_add(1) .ok_or(Error::::Overflow)?; amount }; // 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. /// /// Only contributions over the deposit can be withdrawn by the creator. /// /// The dispatch origin for this call must be _Signed_. /// /// Parameters: /// - `crowdloan_id`: The id of the crowdloan to withdraw from. #[pallet::call_index(2)] #[pallet::weight(T::WeightInfo::withdraw())] pub fn withdraw( origin: OriginFor, #[pallet::compact] crowdloan_id: CrowdloanId, ) -> DispatchResult { let who = 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 mut amount = Contributions::::get(crowdloan_id, &who).unwrap_or_else(Zero::zero); ensure!(amount > Zero::zero(), Error::::NoContribution); if who == crowdloan.creator { // Ensure the deposit is kept amount = amount.saturating_sub(crowdloan.deposit); ensure!(amount > Zero::zero(), Error::::DepositCannotBeWithdrawn); Contributions::::insert(crowdloan_id, &who, crowdloan.deposit); } else { Contributions::::remove(crowdloan_id, &who); crowdloan.contributors_count = crowdloan .contributors_count .checked_sub(1) .ok_or(Error::::Underflow)?; } CurrencyOf::::transfer( &crowdloan.funds_account, &who, amount, Preservation::Expendable, )?; // Update the crowdloan raised amount to reflect the withdrawal. crowdloan.raised = crowdloan.raised.saturating_sub(amount); Crowdloans::::insert(crowdloan_id, &crowdloan); Self::deposit_event(Event::::Withdrew { contributor: who, crowdloan_id, amount, }); Ok(()) } /// Finalize crowdloan that has reached the cap. /// /// The call will either transfer the raised amount to the configured target address /// or dispatch the configured call using the creator origin. The stored crowdloan /// must contain exactly one of target address or call; if both or neither are set, /// finalization fails before transfer or dispatch. /// /// When dispatching a call, 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 mut crowdloan = Self::ensure_crowdloan_exists(crowdloan_id)?; // Ensure the origin is the creator of the crowdloan and the crowdloan has raised the cap // and is not finalized. ensure!(who == crowdloan.creator, Error::::InvalidOrigin); ensure!(crowdloan.raised == crowdloan.cap, Error::::CapNotRaised); ensure!(!crowdloan.finalized, Error::::AlreadyFinalized); ensure!( CurrentCrowdloanId::::get().is_none(), Error::::AlreadyFinalizing ); crowdloan.finalized = true; Crowdloans::::insert(crowdloan_id, &crowdloan); match (&crowdloan.call, &crowdloan.target_address) { (Some(call), None) => { // 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(); } (None, Some(target_address)) => { CurrencyOf::::transfer( &crowdloan.funds_account, target_address, crowdloan.raised, Preservation::Expendable, )?; } (_, _) => { return Err(Error::::InvalidFinalizationConfig)?; } } Self::deposit_event(Event::::Finalized { crowdloan_id }); Ok(()) } /// Refund contributors of a non-finalized crowdloan. /// /// The call will try to refund all contributors (excluding the creator) 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 who = ensure_signed(origin)?; let mut crowdloan = Self::ensure_crowdloan_exists(crowdloan_id)?; // Ensure the crowdloan is not finalized ensure!(!crowdloan.finalized, Error::::AlreadyFinalized); // Only the creator can refund the crowdloan ensure!(who == crowdloan.creator, Error::::InvalidOrigin); let mut refunded_contributors: Vec = vec![]; let mut refund_count = 0; // Assume everyone can be refunded let mut all_refunded = true; // We try to refund all contributors (excluding the creator) let contributions = Contributions::::iter_prefix(crowdloan_id) .filter(|(contributor, _)| *contributor != crowdloan.creator); 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)?; } crowdloan.contributors_count = crowdloan .contributors_count .checked_sub(refund_count) .ok_or(Error::::Underflow)?; 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 (except the creator's one). /// /// 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 the creator's contribution, // meaning there is no contributions or every contribution has been refunded let creator_contribution = Contributions::::get(crowdloan_id, &crowdloan.creator) .ok_or(Error::::NoContribution)?; ensure!( creator_contribution == crowdloan.raised, Error::::NotReadyToDissolve ); // Refund the creator's contribution CurrencyOf::::transfer( &crowdloan.funds_account, &crowdloan.creator, creator_contribution, Preservation::Expendable, )?; Contributions::::remove(crowdloan_id, &crowdloan.creator); // 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); MaxContributions::::remove(crowdloan_id); Self::deposit_event(Event::::Dissolved { crowdloan_id }); Ok(()) } /// Update the minimum contribution of a non-finalized crowdloan. /// /// If a maximum contribution is configured, the new minimum contribution /// must not exceed it. /// /// 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 ); if let Some(max_contribution) = MaxContributions::::get(crowdloan_id) { ensure!( new_min_contribution <= max_contribution, Error::::MinimumContributionTooHigh ); } 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(()) } /// Set or clear the maximum cumulative contribution allowed per contributor /// for 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 maximum contribution of. /// - `new_max_contribution`: The new optional maximum contribution. #[pallet::call_index(9)] #[pallet::weight(T::WeightInfo::set_max_contribution())] pub fn set_max_contribution( origin: OriginFor, #[pallet::compact] crowdloan_id: CrowdloanId, new_max_contribution: Option>, ) -> DispatchResult { let who = ensure_signed(origin)?; let crowdloan = Self::ensure_crowdloan_exists(crowdloan_id)?; ensure!(!crowdloan.finalized, Error::::AlreadyFinalized); // Only the creator can update the max contribution. ensure!(who == crowdloan.creator, Error::::InvalidOrigin); if let Some(max_contribution) = new_max_contribution { let creator_contribution = Contributions::::get(crowdloan_id, &crowdloan.creator) .unwrap_or_else(Zero::zero); ensure!( max_contribution >= crowdloan.min_contribution && max_contribution >= creator_contribution, Error::::MaximumContributionTooLow ); MaxContributions::::insert(crowdloan_id, max_contribution); } else { MaxContributions::::remove(crowdloan_id); } Self::deposit_event(Event::::MaxContributionUpdated { crowdloan_id, new_max_contribution, }); 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(()) } }