diff --git a/Cargo.lock b/Cargo.lock index 05699b526..41f027c6c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5994,6 +5994,7 @@ dependencies = [ "pallet-balances", "pallet-insecure-randomness-collective-flip", "pallet-linear-release", + "parachains-common", "parity-scale-codec", "polimec-traits", "scale-info", diff --git a/integration-tests/src/lib.rs b/integration-tests/src/lib.rs index f2fd07b1a..b9ae7961e 100644 --- a/integration-tests/src/lib.rs +++ b/integration-tests/src/lib.rs @@ -16,7 +16,6 @@ #![cfg(test)] - use frame_support::{assert_ok, pallet_prelude::Weight, traits::GenesisBuild}; use parity_scale_codec::Encode; use polimec_parachain_runtime as polimec_runtime; diff --git a/pallets/funding/Cargo.toml b/pallets/funding/Cargo.toml index 0780b4562..55d5a055c 100644 --- a/pallets/funding/Cargo.toml +++ b/pallets/funding/Cargo.toml @@ -28,6 +28,7 @@ sp-std.workspace = true sp-runtime.workspace = true sp-arithmetic.workspace = true polimec-traits.workspace = true +parachains-common.workspace = true [dev-dependencies] sp-core.workspace = true @@ -50,6 +51,7 @@ std = [ "frame-system/std", "pallet-assets/std", "pallet-balances/std", + "parachains-common/std", "polimec-traits/std", "pallet-linear-release/std", "frame-benchmarking?/std", diff --git a/pallets/funding/src/functions.rs b/pallets/funding/src/functions.rs index 4af678c62..a2ff23ecb 100644 --- a/pallets/funding/src/functions.rs +++ b/pallets/funding/src/functions.rs @@ -36,7 +36,8 @@ use frame_support::{ use sp_arithmetic::Perquintill; -use sp_arithmetic::traits::{CheckedSub, Zero}; +use polimec_traits::ReleaseSchedule; +use sp_arithmetic::traits::{CheckedDiv, CheckedSub, Zero}; use sp_std::prelude::*; // Round transition functions @@ -107,6 +108,7 @@ impl Pallet { total_bonded_plmc: Zero::zero(), evaluators_outcome: EvaluatorsOutcome::Unchanged, }, + funding_end_block: None, }; let project_metadata = initial_metadata; @@ -125,6 +127,7 @@ impl Pallet { Ok(()) } + //noinspection ALL /// Called by user extrinsic /// Starts the evaluation round of a project. It needs to be called by the project issuer. /// @@ -650,6 +653,7 @@ impl Pallet { let mut project_details = ProjectsDetails::::get(project_id).ok_or(Error::::ProjectInfoNotFound)?; let token_information = ProjectsMetadata::::get(project_id).ok_or(Error::::ProjectNotFound)?.token_information; + let now = >::block_number(); // * Validity checks * ensure!( @@ -658,10 +662,13 @@ impl Pallet { Error::::NotAllowed ); - // * Update storage * - + // * Calculate new variables * project_details.cleanup = Cleaner::try_from(project_details.status.clone()).map_err(|_| Error::::NotAllowed)?; + project_details.funding_end_block = Some(now); + + // * Update storage * + ProjectsDetails::::insert(project_id, project_details.clone()); if project_details.status == ProjectStatus::FundingSuccessful { @@ -738,7 +745,7 @@ impl Pallet { ensure!(project_details.issuer == issuer, Error::::NotAllowed); ensure!(!project_details.is_frozen, Error::::Frozen); ensure!(!Images::::contains_key(project_metadata_hash), Error::::MetadataAlreadyExists); - + // * Calculate new variables * // * Update Storage * @@ -879,6 +886,7 @@ impl Pallet { let ticket_size = ct_usd_price.checked_mul_int(ct_amount).ok_or(Error::::BadMath)?; let funding_asset_usd_price = T::PriceProvider::get_price(funding_asset.to_statemint_id()).ok_or(Error::::PriceNotFound)?; + let plmc_usd_price = T::PriceProvider::get_price(PLMC_STATEMINT_ID).ok_or(Error::::PriceNotFound)?; let multiplier = multiplier.unwrap_or_default(); // * Validity checks * @@ -896,11 +904,10 @@ impl Pallet { ensure!(funding_asset == project_metadata.participation_currencies, Error::::FundingAssetNotAccepted); // * Calculate new variables * - let (plmc_vesting_period, ct_vesting_period) = - Self::calculate_vesting_periods(bidder.clone(), multiplier, ct_amount, ct_usd_price) - .map_err(|_| Error::::BadMath)?; - let required_plmc_bond = plmc_vesting_period.amount; - let required_funding_asset_transfer = + let plmc_bond = + Self::calculate_plmc_bond(ticket_size, multiplier, plmc_usd_price).map_err(|_| Error::::BadMath)?; + + let funding_asset_amount_locked = funding_asset_usd_price.reciprocal().ok_or(Error::::BadMath)?.saturating_mul_int(ticket_size); let asset_id = funding_asset.to_statemint_id(); @@ -914,12 +921,11 @@ impl Pallet { final_ct_amount: ct_amount, final_ct_usd_price: ct_usd_price, funding_asset, - funding_asset_amount_locked: required_funding_asset_transfer, + funding_asset_amount_locked, multiplier, - plmc_bond: required_plmc_bond, + plmc_bond, + plmc_vesting_info: None, funded: false, - plmc_vesting_period, - ct_vesting_period, when: now, funds_released: false, ct_minted: false, @@ -927,8 +933,8 @@ impl Pallet { // * Update storage * if existing_bids.len() < T::MaxBidsPerUser::get() as usize { - Self::try_plmc_participation_lock(&bidder, project_id, required_plmc_bond)?; - Self::try_funding_asset_hold(&bidder, project_id, required_funding_asset_transfer, asset_id)?; + Self::try_plmc_participation_lock(&bidder, project_id, plmc_bond)?; + Self::try_funding_asset_hold(&bidder, project_id, funding_asset_amount_locked, asset_id)?; } else { let lowest_bid = existing_bids.iter().min_by_key(|bid| bid.plmc_bond).ok_or(Error::::ImpossibleState)?.clone(); @@ -950,8 +956,8 @@ impl Pallet { )?; Bids::::remove((project_id, lowest_bid.bidder, lowest_bid.id)); - Self::try_plmc_participation_lock(&bidder, project_id, required_plmc_bond)?; - Self::try_funding_asset_hold(&bidder, project_id, required_funding_asset_transfer, asset_id)?; + Self::try_plmc_participation_lock(&bidder, project_id, plmc_bond)?; + Self::try_funding_asset_hold(&bidder, project_id, funding_asset_amount_locked, asset_id)?; } Bids::::insert((project_id, bidder, bid_id), new_bid); @@ -992,6 +998,7 @@ impl Pallet { Contributions::::iter_prefix_values((project_id, contributor.clone())).collect::>(); let ct_usd_price = project_details.weighted_average_price.ok_or(Error::::AuctionNotStarted)?; + let plmc_usd_price = T::PriceProvider::get_price(PLMC_STATEMINT_ID).ok_or(Error::::PriceNotFound)?; let mut ticket_size = ct_usd_price.checked_mul_int(token_amount).ok_or(Error::::BadMath)?; let funding_asset_usd_price = T::PriceProvider::get_price(asset.to_statemint_id()).ok_or(Error::::PriceNotFound)?; @@ -1015,7 +1022,7 @@ impl Pallet { ensure!(ticket_size <= maximum_ticket_size, Error::::ContributionTooHigh); }; ensure!(project_metadata.participation_currencies == asset, Error::::FundingAssetNotAccepted); - + // * Calculate variables * let buyable_tokens = if project_details.remaining_contribution_tokens > token_amount { token_amount @@ -1024,11 +1031,8 @@ impl Pallet { ticket_size = ct_usd_price.checked_mul_int(remaining_amount).ok_or(Error::::BadMath)?; remaining_amount }; - let (plmc_vesting_period, ct_vesting_period) = - Self::calculate_vesting_periods(contributor.clone(), multiplier.clone(), buyable_tokens, ct_usd_price) - .map_err(|_| Error::::BadMath)?; - let required_plmc_bond = plmc_vesting_period.amount; - let required_funding_asset_transfer = + let plmc_bond = Self::calculate_plmc_bond(ticket_size, multiplier, plmc_usd_price)?; + let funding_asset_amount = funding_asset_usd_price.reciprocal().ok_or(Error::::BadMath)?.saturating_mul_int(ticket_size); let asset_id = asset.to_statemint_id(); let remaining_cts_after_purchase = project_details.remaining_contribution_tokens.saturating_sub(buyable_tokens); @@ -1037,13 +1041,13 @@ impl Pallet { id: contribution_id, project_id, contributor: contributor.clone(), - ct_amount: ct_vesting_period.amount, + ct_amount: token_amount, usd_contribution_amount: ticket_size, + multiplier, funding_asset: asset, - funding_asset_amount: required_funding_asset_transfer, - plmc_bond: required_plmc_bond, - plmc_vesting_period, - ct_vesting_period, + funding_asset_amount, + plmc_bond, + plmc_vesting_info: None, funds_released: false, ct_minted: false, }; @@ -1051,8 +1055,8 @@ impl Pallet { // * Update storage * // Try adding the new contribution to the system if existing_contributions.len() < T::MaxContributionsPerUser::get() as usize { - Self::try_plmc_participation_lock(&contributor, project_id, required_plmc_bond)?; - Self::try_funding_asset_hold(&contributor, project_id, required_funding_asset_transfer, asset_id)?; + Self::try_plmc_participation_lock(&contributor, project_id, plmc_bond)?; + Self::try_funding_asset_hold(&contributor, project_id, funding_asset_amount, asset_id)?; } else { let lowest_contribution = existing_contributions .iter() @@ -1076,8 +1080,8 @@ impl Pallet { )?; Contributions::::remove((project_id, lowest_contribution.contributor.clone(), lowest_contribution.id)); - Self::try_plmc_participation_lock(&contributor, project_id, required_plmc_bond)?; - Self::try_funding_asset_hold(&contributor, project_id, required_funding_asset_transfer, asset_id)?; + Self::try_plmc_participation_lock(&contributor, project_id, plmc_bond)?; + Self::try_funding_asset_hold(&contributor, project_id, funding_asset_amount, asset_id)?; project_details.remaining_contribution_tokens = project_details.remaining_contribution_tokens.saturating_add(lowest_contribution.ct_amount); @@ -1126,65 +1130,6 @@ impl Pallet { Ok(()) } - /// Unbond some plmc from a successful bid, after a step in the vesting period has passed. - /// - /// # Arguments - /// * bid: The bid to unbond from - /// - /// # Storage access - /// * [`Bids`] - Check if its time to unbond some plmc based on the bid vesting period, and update the bid after unbonding. - /// * [`BiddingBonds`] - Update the bid with the new vesting period struct, reflecting this withdrawal - /// * [`T::NativeCurrency`] - Unreserve the unbonded amount - pub fn do_vested_plmc_bid_unbond_for( - releaser: AccountIdOf, - project_id: T::ProjectIdentifier, - bidder: AccountIdOf, - ) -> Result<(), DispatchError> { - // * Get variables * - let bids = Bids::::iter_prefix_values((project_id, bidder.clone())); - let now = >::block_number(); - for mut bid in bids { - let mut plmc_vesting = bid.plmc_vesting_period; - - // * Validity checks * - // check that it is not too early to withdraw the next amount - if plmc_vesting.next_withdrawal > now { - continue - } - - // * Calculate variables * - let mut unbond_amount: BalanceOf = 0u32.into(); - - // update vesting period until the next withdrawal is in the future - while let Ok(amount) = plmc_vesting.calculate_next_withdrawal() { - unbond_amount = unbond_amount.saturating_add(amount); - if plmc_vesting.next_withdrawal > now { - break - } - } - bid.plmc_vesting_period = plmc_vesting; - - // * Update storage * - T::NativeCurrency::release( - &LockType::Participation(project_id), - &bid.bidder, - unbond_amount, - Precision::Exact, - )?; - Bids::::insert((project_id, bidder.clone(), bid.id), bid.clone()); - - // * Emit events * - Self::deposit_event(Event::::BondReleased { - project_id: bid.project_id, - amount: unbond_amount, - bonder: bid.bidder, - releaser: releaser.clone(), - }); - } - - Ok(()) - } - pub fn do_bid_ct_mint_for( releaser: AccountIdOf, project_id: T::ProjectIdentifier, @@ -1255,70 +1200,6 @@ impl Pallet { Ok(()) } - /// Unbond some plmc from a contribution, after a step in the vesting period has passed. - /// - /// # Arguments - /// * bid: The bid to unbond from - /// - /// # Storage access - /// * [`Bids`] - Check if its time to unbond some plmc based on the bid vesting period, and update the bid after unbonding. - /// * [`BiddingBonds`] - Update the bid with the new vesting period struct, reflecting this withdrawal - /// * [`T::NativeCurrency`] - Unreserve the unbonded amount - pub fn do_vested_plmc_purchase_unbond_for( - releaser: AccountIdOf, - project_id: T::ProjectIdentifier, - claimer: AccountIdOf, - ) -> Result<(), DispatchError> { - // * Get variables * - let project_details = ProjectsDetails::::get(project_id).ok_or(Error::::ProjectNotFound)?; - let contributions = Contributions::::iter_prefix_values((project_id, &claimer)); - let now = >::block_number(); - - // * Validity checks * - ensure!(project_details.status == ProjectStatus::FundingSuccessful, Error::::CannotClaimYet); - - for mut contribution in contributions { - let mut plmc_vesting = contribution.plmc_vesting_period; - let mut unbond_amount: BalanceOf = 0u32.into(); - - // * Validity checks * - // check that it is not too early to withdraw the next amount - if plmc_vesting.next_withdrawal > now { - continue - } - - // * Calculate variables * - // Update vesting period until the next withdrawal is in the future - while let Ok(amount) = plmc_vesting.calculate_next_withdrawal() { - unbond_amount = unbond_amount.saturating_add(amount); - if plmc_vesting.next_withdrawal > now { - break - } - } - contribution.plmc_vesting_period = plmc_vesting; - - // * Update storage * - // Unreserve the funds for the user - T::NativeCurrency::release( - &LockType::Participation(project_id), - &claimer, - unbond_amount, - Precision::Exact, - )?; - Contributions::::insert((project_id, &claimer, contribution.id), contribution.clone()); - - // * Emit events * - Self::deposit_event(Event::BondReleased { - project_id, - amount: unbond_amount, - bonder: claimer.clone(), - releaser: releaser.clone(), - }) - } - - Ok(()) - } - pub fn do_evaluation_unbond_for( releaser: AccountIdOf, project_id: T::ProjectIdentifier, @@ -1472,6 +1353,124 @@ impl Pallet { Ok(()) } + pub fn do_start_bid_vesting_schedule_for( + caller: AccountIdOf, + project_id: T::ProjectIdentifier, + bidder: AccountIdOf, + bid_id: StorageItemIdOf, + ) -> Result<(), DispatchError> { + // * Get variables * + let project_details = ProjectsDetails::::get(project_id).ok_or(Error::::ProjectInfoNotFound)?; + let mut bid = Bids::::get((project_id, bidder.clone(), bid_id)).ok_or(Error::::BidNotFound)?; + let funding_end_block = project_details.funding_end_block.ok_or(Error::::ImpossibleState)?; + + // * Validity checks * + ensure!( + matches!(bid.plmc_vesting_info, None) && + project_details.status == ProjectStatus::FundingSuccessful && + matches!(bid.status, BidStatus::Accepted | BidStatus::PartiallyAccepted(..)), + Error::::NotAllowed + ); + + // * Calculate variables * + let vest_info = Self::calculate_vesting_info(bidder.clone(), bid.multiplier, bid.plmc_bond) + .map_err(|_| Error::::BadMath)?; + bid.plmc_vesting_info = Some(vest_info); + + // * Update storage * + T::Vesting::add_release_schedule( + &bidder, + vest_info.total_amount, + vest_info.amount_per_block, + funding_end_block, + LockType::Participation(project_id), + )?; + Bids::::insert((project_id, bidder.clone(), bid_id), bid); + + // * Emit events * + Self::deposit_event(Event::::BidPlmcVestingScheduled { + project_id, + bidder: bidder.clone(), + id: bid_id, + amount: vest_info.total_amount, + caller, + }); + + Ok(()) + } + + pub fn do_start_contribution_vesting_schedule_for( + caller: AccountIdOf, + project_id: T::ProjectIdentifier, + contributor: AccountIdOf, + contribution_id: StorageItemIdOf, + ) -> Result<(), DispatchError> { + // * Get variables * + let project_details = ProjectsDetails::::get(project_id).ok_or(Error::::ProjectInfoNotFound)?; + let mut contribution = Contributions::::get((project_id, contributor.clone(), contribution_id)) + .ok_or(Error::::BidNotFound)?; + let funding_end_block = project_details.funding_end_block.ok_or(Error::::ImpossibleState)?; + + // * Validity checks * + ensure!( + matches!(contribution.plmc_vesting_info, None) && + project_details.status == ProjectStatus::FundingSuccessful, + Error::::NotAllowed + ); + + // * Calculate variables * + let vest_info = + Self::calculate_vesting_info(contributor.clone(), contribution.multiplier, contribution.plmc_bond) + .map_err(|_| Error::::BadMath)?; + contribution.plmc_vesting_info = Some(vest_info); + + // * Update storage * + T::Vesting::add_release_schedule( + &contributor, + vest_info.total_amount, + vest_info.amount_per_block, + funding_end_block, + LockType::Participation(project_id), + )?; + Contributions::::insert((project_id, contributor.clone(), contribution_id), contribution); + + // * Emit events * + Self::deposit_event(Event::::ContributionPlmcVestingScheduled { + project_id, + contributor: contributor.clone(), + id: contribution_id, + amount: vest_info.total_amount, + caller, + }); + + Ok(()) + } + + pub fn do_vest_plmc_for( + caller: AccountIdOf, + project_id: T::ProjectIdentifier, + participant: AccountIdOf, + ) -> DispatchResult { + // * Get variables * + let project_details = ProjectsDetails::::get(project_id).ok_or(Error::::ProjectInfoNotFound)?; + + // * Validity checks * + ensure!(matches!(project_details.status, ProjectStatus::FundingSuccessful), Error::::NotAllowed); + + // * Update storage * + let vested_amount = T::Vesting::vest(participant.clone(), LockType::Participation(project_id))?; + + // * Emit events * + Self::deposit_event(Event::::ParticipantPlmcVested { + project_id, + participant: participant.clone(), + amount: vested_amount, + caller, + }); + + Ok(()) + } + pub fn do_release_bid_funds_for( _caller: AccountIdOf, _project_id: T::ProjectIdentifier, @@ -1543,7 +1542,7 @@ impl Pallet { // Try to get the project into the earliest possible block to update. // There is a limit for how many projects can update each block, so we need to make sure we don't exceed that limit let mut block_number = block_number; - while ProjectsToUpdate::::try_append(block_number, store.clone()).is_err() { + while ProjectsToUpdate::::try_append(block_number.clone(), store.clone()).is_err() { // TODO: Should we end the loop if we iterated over too many blocks? block_number += 1u32.into(); } @@ -1564,42 +1563,31 @@ impl Pallet { Ok(()) } + pub fn calculate_plmc_bond( + ticket_size: BalanceOf, + multiplier: MultiplierOf, + plmc_price: PriceOf, + ) -> Result, DispatchError> { + let usd_bond = multiplier.calculate_bonding_requirement(ticket_size).map_err(|_| Error::::BadMath)?; + plmc_price.reciprocal().ok_or(Error::::BadMath)?.checked_mul_int(usd_bond).ok_or(Error::::BadMath.into()) + } + /// Based on the amount of tokens and price to buy, a desired multiplier, and the type of investor the caller is, /// calculate the amount and vesting periods of bonded PLMC and reward CT tokens. - pub fn calculate_vesting_periods( + pub fn calculate_vesting_info( _caller: AccountIdOf, - multiplier: MultiplierOf, - token_amount: BalanceOf, - token_price: T::Price, - ) -> Result<(Vesting>, Vesting>), DispatchError> { - let plmc_start: T::BlockNumber = 0u32.into(); - let ct_start: T::BlockNumber = (T::MaxProjectsToUpdatePerBlock::get() * 7).into(); - // TODO: Calculate real vesting periods based on multiplier and caller type - let ticket_size = token_price.checked_mul_int(token_amount).ok_or(Error::::BadMath)?; - let usd_bonding_amount = - multiplier.calculate_bonding_requirement(ticket_size).map_err(|_| Error::::BadMath)?; - let plmc_price = T::PriceProvider::get_price(PLMC_STATEMINT_ID).ok_or(Error::::PLMCPriceNotAvailable)?; - let plmc_bonding_amount = plmc_price - .reciprocal() - .ok_or(Error::::BadMath)? - .checked_mul_int(usd_bonding_amount) - .ok_or(Error::::BadMath)?; - Ok(( - Vesting { - amount: plmc_bonding_amount, - start: plmc_start, - end: plmc_start, - step: 0u32.into(), - next_withdrawal: 0u32.into(), - }, - Vesting { - amount: token_amount, - start: ct_start, - end: ct_start, - step: 0u32.into(), - next_withdrawal: 0u32.into(), - }, - )) + _multiplier: MultiplierOf, + bonded_amount: BalanceOf, + ) -> Result>, DispatchError> { + // TODO: duration should depend on `_multiplier` and `_caller` credential + let duration: u32 = 1u32 * parachains_common::DAYS; + let amount_per_block = bonded_amount.checked_div(&duration.into()).ok_or(Error::::BadMath)?; + + Ok(VestingInfo { + total_amount: bonded_amount, + amount_per_block, + duration: >::from(duration), + }) } /// Calculates the price (in USD) of contribution tokens for the Community and Remainder Rounds diff --git a/pallets/funding/src/impls.rs b/pallets/funding/src/impls.rs index ea1a8ba25..c61ceb9d7 100644 --- a/pallets/funding/src/impls.rs +++ b/pallets/funding/src/impls.rs @@ -50,36 +50,39 @@ impl DoRemainingOperation for CleanerState { }, CleanerState::EvaluationUnbonding(remaining, PhantomData) => if *remaining == 0 { - *self = - CleanerState::BidPLMCVesting(remaining_bids_without_plmc_vesting::(project_id), PhantomData); + *self = CleanerState::StartBidderVestingSchedule( + remaining_successful_bids::(project_id), + PhantomData, + ); Ok(Weight::zero()) } else { let (consumed_weight, remaining_evaluations) = unbond_one_evaluation::(project_id); *self = CleanerState::EvaluationUnbonding(remaining_evaluations, PhantomData); Ok(consumed_weight) }, - CleanerState::BidPLMCVesting(remaining, PhantomData) => + CleanerState::StartBidderVestingSchedule(remaining, PhantomData) => if *remaining == 0 { - *self = CleanerState::BidCTMint(remaining_bids_without_ct_minted::(project_id), PhantomData); + *self = CleanerState::StartContributorVestingSchedule( + remaining_contributions::(project_id), + PhantomData, + ); Ok(Weight::zero()) } else { - let (consumed_weight, remaining_bids) = start_bid_plmc_vesting_schedule::(project_id); - *self = CleanerState::BidPLMCVesting(remaining_bids, PhantomData); + let (consumed_weight, remaining_evaluations) = start_one_bid_vesting_schedule::(project_id); + *self = CleanerState::StartBidderVestingSchedule(remaining_evaluations, PhantomData); Ok(consumed_weight) }, - CleanerState::BidCTMint(remaining, PhantomData) => + CleanerState::StartContributorVestingSchedule(remaining, PhantomData) => if *remaining == 0 { - *self = CleanerState::ContributionPLMCVesting( - remaining_contributions_without_plmc_vesting::(project_id), - PhantomData, - ); + *self = CleanerState::BidCTMint(remaining_bids_without_ct_minted::(project_id), PhantomData); Ok(Weight::zero()) } else { - let (consumed_weight, remaining_bids) = mint_ct_for_one_bid::(project_id); - *self = CleanerState::BidCTMint(remaining_bids, PhantomData); + let (consumed_weight, remaining_evaluations) = + start_one_contribution_vesting_schedule::(project_id); + *self = CleanerState::StartContributorVestingSchedule(remaining_evaluations, PhantomData); Ok(consumed_weight) }, - CleanerState::ContributionPLMCVesting(remaining, PhantomData) => + CleanerState::BidCTMint(remaining, PhantomData) => if *remaining == 0 { *self = CleanerState::ContributionCTMint( remaining_contributions_without_ct_minted::(project_id), @@ -87,9 +90,8 @@ impl DoRemainingOperation for CleanerState { ); Ok(Weight::zero()) } else { - let (consumed_weight, remaining_contributions) = - start_contribution_plmc_vesting_schedule::(project_id); - *self = CleanerState::ContributionPLMCVesting(remaining_contributions, PhantomData); + let (consumed_weight, remaining_bids) = mint_ct_for_one_bid::(project_id); + *self = CleanerState::BidCTMint(remaining_bids, PhantomData); Ok(consumed_weight) }, CleanerState::ContributionCTMint(remaining, PhantomData) => @@ -255,6 +257,12 @@ fn remaining_bids(project_id: T::ProjectIdentifier) -> u64 { Bids::::iter_prefix_values((project_id,)).count() as u64 } +fn remaining_successful_bids(project_id: T::ProjectIdentifier) -> u64 { + Bids::::iter_prefix_values((project_id,)) + .filter(|bid| matches!(bid.status, BidStatus::Accepted | BidStatus::PartiallyAccepted(..))) + .count() as u64 +} + fn remaining_contributions_to_release_funds(project_id: T::ProjectIdentifier) -> u64 { Contributions::::iter_prefix_values((project_id,)).filter(|contribution| !contribution.funds_released).count() as u64 @@ -264,23 +272,11 @@ fn remaining_contributions(project_id: T::ProjectIdentifier) -> u64 { Contributions::::iter_prefix_values((project_id,)).count() as u64 } -fn remaining_bids_without_plmc_vesting(_project_id: T::ProjectIdentifier) -> u64 { - // TODO: current vesting implementation starts the schedule on bid creation. We should later on use pallet_vesting - // and add a check in the bid struct for initializing the vesting schedule - 0u64 -} - fn remaining_bids_without_ct_minted(project_id: T::ProjectIdentifier) -> u64 { let project_bids = Bids::::iter_prefix_values((project_id,)); project_bids.filter(|bid| !bid.ct_minted).count() as u64 } -fn remaining_contributions_without_plmc_vesting(_project_id: T::ProjectIdentifier) -> u64 { - // TODO: current vesting implementation starts the schedule on contribution creation. We should later on use pallet_vesting - // and add a check in the contribution struct for initializing the vesting schedule - 0u64 -} - fn remaining_contributions_without_ct_minted(project_id: T::ProjectIdentifier) -> u64 { let project_contributions = Contributions::::iter_prefix_values((project_id,)); project_contributions.filter(|contribution| !contribution.ct_minted).count() as u64 @@ -489,14 +485,64 @@ fn unbond_one_contribution(project_id: T::ProjectIdentifier) -> (Weig } } -fn start_bid_plmc_vesting_schedule(_project_id: T::ProjectIdentifier) -> (Weight, u64) { - // TODO: change when new vesting schedule is implemented - (Weight::zero(), 0u64) +fn start_one_bid_vesting_schedule(project_id: T::ProjectIdentifier) -> (Weight, u64) { + let project_bids = Bids::::iter_prefix_values((project_id,)); + let mut unscheduled_bids = project_bids.filter(|bid| { + matches!(bid.plmc_vesting_info, None) && + matches!(bid.status, BidStatus::Accepted | BidStatus::PartiallyAccepted(..)) + }); + + if let Some(bid) = unscheduled_bids.next() { + match Pallet::::do_start_bid_vesting_schedule_for( + T::PalletId::get().into_account_truncating(), + project_id, + bid.bidder.clone(), + bid.id, + ) { + Ok(_) => {}, + Err(e) => { + Pallet::::deposit_event(Event::StartBidderVestingScheduleFailed { + project_id: bid.project_id, + bidder: bid.bidder.clone(), + id: bid.id, + error: e, + }); + }, + } + + (Weight::zero(), unscheduled_bids.count() as u64) + } else { + (Weight::zero(), 0u64) + } } -fn start_contribution_plmc_vesting_schedule(_project_id: T::ProjectIdentifier) -> (Weight, u64) { - // TODO: change when new vesting schedule is implemented - (Weight::zero(), 0u64) +fn start_one_contribution_vesting_schedule(project_id: T::ProjectIdentifier) -> (Weight, u64) { + let project_bids = Contributions::::iter_prefix_values((project_id,)); + let mut unscheduled_contributions = + project_bids.filter(|contribution| matches!(contribution.plmc_vesting_info, None)); + + if let Some(contribution) = unscheduled_contributions.next() { + match Pallet::::do_start_contribution_vesting_schedule_for( + T::PalletId::get().into_account_truncating(), + project_id, + contribution.contributor.clone(), + contribution.id, + ) { + Ok(_) => {}, + Err(e) => { + Pallet::::deposit_event(Event::StartContributionVestingScheduleFailed { + project_id: contribution.project_id, + contributor: contribution.contributor.clone(), + id: contribution.id, + error: e, + }); + }, + } + + (Weight::zero(), unscheduled_contributions.count() as u64) + } else { + (Weight::zero(), 0u64) + } } fn mint_ct_for_one_bid(project_id: T::ProjectIdentifier) -> (Weight, u64) { diff --git a/pallets/funding/src/lib.rs b/pallets/funding/src/lib.rs index 496855513..f83772274 100644 --- a/pallets/funding/src/lib.rs +++ b/pallets/funding/src/lib.rs @@ -231,7 +231,7 @@ pub type ProjectMetadataOf = pub type ProjectDetailsOf = ProjectDetails, BlockNumberOf, PriceOf, BalanceOf, EvaluationRoundInfoOf>; pub type EvaluationRoundInfoOf = EvaluationRoundInfo>; -pub type VestingOf = Vesting, BalanceOf>; +pub type VestingInfoOf = VestingInfo, BalanceOf>; pub type EvaluationInfoOf = EvaluationInfo, ProjectIdOf, AccountIdOf, BalanceOf, BlockNumberOf>; pub type BidInfoOf = BidInfo< @@ -241,12 +241,17 @@ pub type BidInfoOf = BidInfo< PriceOf, AccountIdOf, BlockNumberOf, - VestingOf, - VestingOf, MultiplierOf, + VestingInfoOf, +>; +pub type ContributionInfoOf = ContributionInfo< + StorageItemIdOf, + ProjectIdOf, + AccountIdOf, + BalanceOf, + MultiplierOf, + VestingInfoOf, >; -pub type ContributionInfoOf = - ContributionInfo, ProjectIdOf, AccountIdOf, BalanceOf, VestingOf, VestingOf>; pub type BondTypeOf = LockType>; const PLMC_STATEMINT_ID: u32 = 2069; @@ -377,7 +382,12 @@ pub mod pallet { type EvaluationSuccessThreshold: Get; - type Vesting: polimec_traits::ReleaseSchedule, BondTypeOf>; + type Vesting: polimec_traits::ReleaseSchedule< + AccountIdOf, + BondTypeOf, + Currency = Self::NativeCurrency, + Moment = BlockNumberOf, + >; /// For now we expect 3 days until the project is automatically accepted. Timeline decided by MiCA regulations. type ManualAcceptanceDuration: Get; /// For now we expect 4 days from acceptance to settlement due to MiCA regulations. @@ -616,6 +626,38 @@ pub mod pallet { id: StorageItemIdOf, error: DispatchError, }, + StartBidderVestingScheduleFailed { + project_id: ProjectIdOf, + bidder: AccountIdOf, + id: StorageItemIdOf, + error: DispatchError, + }, + StartContributionVestingScheduleFailed { + project_id: ProjectIdOf, + contributor: AccountIdOf, + id: StorageItemIdOf, + error: DispatchError, + }, + BidPlmcVestingScheduled { + project_id: ProjectIdOf, + bidder: AccountIdOf, + id: StorageItemIdOf, + amount: BalanceOf, + caller: AccountIdOf, + }, + ContributionPlmcVestingScheduled { + project_id: ProjectIdOf, + contributor: AccountIdOf, + id: StorageItemIdOf, + amount: BalanceOf, + caller: AccountIdOf, + }, + ParticipantPlmcVested { + project_id: ProjectIdOf, + participant: AccountIdOf, + amount: BalanceOf, + caller: AccountIdOf, + }, } #[pallet::error] @@ -858,25 +900,26 @@ pub mod pallet { Self::do_contribution_ct_mint_for(caller, project_id, contributor, contribution_id) } - /// Unbond some plmc from a contribution, after a step in the vesting period has passed. - pub fn vested_plmc_bid_unbond_for( + #[pallet::weight(Weight::from_parts(0, 0))] + pub fn start_bid_vesting_schedule_for( origin: OriginFor, project_id: T::ProjectIdentifier, bidder: AccountIdOf, + bid_id: T::StorageItemId, ) -> DispatchResult { - let releaser = ensure_signed(origin)?; - - Self::do_vested_plmc_bid_unbond_for(releaser, project_id, bidder) + let caller = ensure_signed(origin)?; + Self::do_start_bid_vesting_schedule_for(caller, project_id, bidder, bid_id) } - /// Unbond some plmc from a contribution, after a step in the vesting period has passed. - pub fn vested_plmc_purchase_unbond_for( + #[pallet::weight(Weight::from_parts(0, 0))] + pub fn start_contribution_vesting_schedule_for( origin: OriginFor, project_id: T::ProjectIdentifier, - purchaser: AccountIdOf, + contributor: AccountIdOf, + contribution_id: T::StorageItemId, ) -> DispatchResult { - let releaser = ensure_signed(origin)?; - Self::do_vested_plmc_purchase_unbond_for(releaser, project_id, purchaser) + let caller = ensure_signed(origin)?; + Self::do_start_contribution_vesting_schedule_for(caller, project_id, contributor, contribution_id) } } diff --git a/pallets/funding/src/mock.rs b/pallets/funding/src/mock.rs index 59c6d6719..1ebde34b4 100644 --- a/pallets/funding/src/mock.rs +++ b/pallets/funding/src/mock.rs @@ -270,7 +270,7 @@ impl pallet_funding::Config for TestRuntime { type Randomness = RandomnessCollectiveFlip; type RemainderFundingDuration = RemainderFundingDuration; type RuntimeEvent = RuntimeEvent; - type StorageItemId = u128; + type StorageItemId = u32; type StringLimit = ConstU32<64>; type SuccessToSettlementTime = SuccessToSettlementTime; type TreasuryAccount = TreasuryAccount; diff --git a/pallets/funding/src/tests.rs b/pallets/funding/src/tests.rs index df23756fa..b276caa41 100644 --- a/pallets/funding/src/tests.rs +++ b/pallets/funding/src/tests.rs @@ -14,34 +14,40 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -// If you feel like getting in touch with us, you can do so at info@polimec.org +// If you feel like getting in touch with us, you ca ,n do so at info@polimec.org //! Tests for Funding pallet. use super::*; use crate as pallet_funding; use crate::{ mock::{FundingModule, *}, - traits::ProvideStatemintPrice, + tests::testing_macros::{assert_close_enough, call_and_is_ok, extract_from_event}, + traits::{BondingRequirementCalculation, ProvideStatemintPrice}, CurrencyMetadata, Error, ParticipantsSize, ProjectMetadata, TicketSize, + UpdateType::{CommunityFundingStart, RemainderFundingStart}, }; use defaults::*; use frame_support::{ assert_noop, assert_ok, traits::{ - fungible::{InspectHold as FungibleInspectHold, Mutate as FungibleMutate}, - fungibles::Mutate as FungiblesMutate, + fungible::{Inspect as FungibleInspect, InspectHold as FungibleInspectHold, Mutate as FungibleMutate}, + fungibles::{ + metadata::Inspect as MetadataInspect, roles::Inspect as RolesInspect, Inspect as FungiblesInspect, + Mutate as FungiblesMutate, + }, tokens::Balance as BalanceT, OnFinalize, OnIdle, OnInitialize, }, weights::Weight, }; use helper_functions::*; - -use crate::traits::BondingRequirementCalculation; +use parachains_common::DAYS; +use polimec_traits::ReleaseSchedule; use sp_arithmetic::{traits::Zero, Percent, Perquintill}; +use sp_core::H256; use sp_runtime::{DispatchError, Either}; use sp_std::marker::PhantomData; -use std::{cell::RefCell, iter::zip}; +use std::{assert_matches::assert_matches, cell::RefCell, collections::BTreeMap, iter::zip, ops::Div}; type ProjectIdOf = ::ProjectIdentifier; type UserToPLMCBalance = Vec<(AccountId, BalanceOf)>; @@ -90,17 +96,7 @@ impl TestContribution { pub type TestContributions = Vec; #[derive(Clone, PartialEq, Eq, Debug)] -pub struct BidInfoFilter< - Id, - ProjectId, - Balance: BalanceT, - Price, - AccountId, - Multiplier, - BlockNumber, - PlmcVesting, - CTVesting, -> { +pub struct BidInfoFilter { pub id: Option, pub project_id: Option, pub bidder: Option, @@ -114,8 +110,7 @@ pub struct BidInfoFilter< pub multiplier: Option, pub plmc_bond: Option, pub funded: Option, - pub plmc_vesting_period: Option, - pub ct_vesting_period: Option, + pub plmc_vesting_info: Option, pub when: Option, pub funds_released: Option, } @@ -127,8 +122,7 @@ type BidInfoFilterOf = BidInfoFilter< ::AccountId, MultiplierOf, BlockNumberOf, - VestingOf, - VestingOf, + Option>, >; impl Default for BidInfoFilterOf { fn default() -> Self { @@ -146,8 +140,7 @@ impl Default for BidInfoFilterOf { multiplier: None, plmc_bond: None, funded: None, - plmc_vesting_period: None, - ct_vesting_period: None, + plmc_vesting_info: None, when: None, funds_released: None, } @@ -196,10 +189,7 @@ impl BidInfoFilterOf { if self.funded.is_some() && self.funded.unwrap() != bid.funded { return false } - if self.plmc_vesting_period.is_some() && self.plmc_vesting_period.unwrap() != bid.plmc_vesting_period { - return false - } - if self.ct_vesting_period.is_some() && self.ct_vesting_period.unwrap() != bid.ct_vesting_period { + if self.plmc_vesting_info.is_some() && self.plmc_vesting_info.unwrap() != bid.plmc_vesting_info { return false } if self.when.is_some() && self.when.unwrap() != bid.when { @@ -595,6 +585,7 @@ impl<'a> CreatedProject<'a> { total_bonded_plmc: Zero::zero(), evaluators_outcome: EvaluatorsOutcome::Unchanged, }, + funding_end_block: None, }; assert_eq!(metadata, expected_metadata); assert_eq!(details, expected_details); @@ -1275,12 +1266,6 @@ mod defaults { pub mod helper_functions { use super::*; - use frame_support::traits::fungibles::{ - metadata::Inspect as MetadataInspect, roles::Inspect as RolesInspect, Inspect, - }; - use sp_arithmetic::{traits::Zero, Percent}; - use sp_core::H256; - use std::collections::BTreeMap; pub fn get_ed() -> BalanceOf { ::ExistentialDeposit::get() @@ -1805,8 +1790,6 @@ mod creation_round_failure { mod evaluation_round_success { use super::*; - use sp_arithmetic::Perquintill; - use testing_macros::assert_close_enough; #[test] fn evaluation_round_completed() { @@ -2689,6 +2672,282 @@ mod auction_round_success { }) } } + + #[test] + pub fn plmc_vesting_schedule_starts_automatically() { + let test_env = TestEnvironment::new(); + let issuer = ISSUER; + let project = default_project(test_env.get_new_nonce()); + let evaluations = default_evaluations(); + + let mut bids = default_bids(); + let median_price = bids[bids.len().div(2)].price; + let new_bids = vec![ + TestBid::new(BIDDER_4, 30_000 * US_DOLLAR, median_price, None, AcceptedFundingAsset::USDT), + TestBid::new(BIDDER_5, 167_000 * US_DOLLAR, median_price, None, AcceptedFundingAsset::USDT), + ]; + bids.extend(new_bids.clone()); + + let community_contributions = default_community_buys(); + let remainder_contributions = vec![]; + + let finished_project = FinishedProject::new_with( + &test_env, + project, + issuer, + evaluations, + bids, + community_contributions, + remainder_contributions, + ); + + test_env.advance_time(10u64).unwrap(); + let details = finished_project.get_project_details(); + assert_eq!(details.cleanup, Cleaner::Success(CleanerState::Finished(PhantomData))); + + let final_price = details.weighted_average_price.unwrap(); + let plmc_locked_for_bids = calculate_auction_plmc_spent_after_price_calculation(new_bids, final_price); + + for (user, amount) in plmc_locked_for_bids { + let schedule = test_env.in_ext(|| { + ::Vesting::total_scheduled_amount( + &user, + LockType::Participation(finished_project.project_id), + ) + }); + + assert_eq!(schedule.unwrap(), amount); + } + } + + #[test] + pub fn plmc_vesting_schedule_starts_manually() { + let test_env = TestEnvironment::new(); + let issuer = ISSUER; + let project = default_project(test_env.get_new_nonce()); + let evaluations = default_evaluations(); + let bids = default_bids(); + let community_contributions = default_community_buys(); + let remainder_contributions = vec![]; + + let finished_project = FinishedProject::new_with( + &test_env, + project, + issuer, + evaluations, + bids.clone(), + community_contributions, + remainder_contributions, + ); + + let details = finished_project.get_project_details(); + assert_eq!(details.status, ProjectStatus::FundingSuccessful); + assert_eq!(details.cleanup, Cleaner::NotReady); + + test_env.advance_time(1u64).unwrap(); + let details = finished_project.get_project_details(); + assert_eq!(details.cleanup, Cleaner::Success(CleanerState::Initialized(PhantomData))); + + let stored_bids = test_env + .in_ext(|| Bids::::iter_prefix_values((finished_project.project_id,)).collect::>()); + for bid in stored_bids { + call_and_is_ok!( + test_env, + Pallet::::start_bid_vesting_schedule_for( + RuntimeOrigin::signed(bid.bidder), + finished_project.project_id, + bid.bidder, + bid.id, + ) + ); + + let schedule = test_env.in_ext(|| { + ::Vesting::total_scheduled_amount( + &bid.bidder, + LockType::Participation(finished_project.project_id), + ) + }); + + let bid = test_env + .in_ext(|| Bids::::get((finished_project.project_id, bid.bidder, bid.id)).unwrap()); + assert_eq!(schedule.unwrap(), bid.plmc_vesting_info.unwrap().total_amount); + } + } + + #[test] + pub fn plmc_vesting_full_amount() { + let test_env = TestEnvironment::new(); + let issuer = ISSUER; + let project = default_project(test_env.get_new_nonce()); + let evaluations = default_evaluations(); + let bids = default_bids(); + let community_contributions = default_community_buys(); + let remainder_contributions = vec![]; + + let finished_project = FinishedProject::new_with( + &test_env, + project, + issuer, + evaluations, + bids, + community_contributions, + remainder_contributions, + ); + + test_env.advance_time(10u64).unwrap(); + let details = finished_project.get_project_details(); + assert_eq!(details.cleanup, Cleaner::Success(CleanerState::Finished(PhantomData))); + + let stored_bids = test_env + .in_ext(|| Bids::::iter_prefix_values((finished_project.project_id,)).collect::>()); + + test_env.advance_time((10 * DAYS).into()).unwrap(); + + for bid in stored_bids { + let vesting_info = bid.plmc_vesting_info.unwrap(); + let locked_amount = vesting_info.total_amount; + + let prev_free_balance = test_env.in_ext(|| ::NativeCurrency::balance(&bid.bidder)); + + test_env + .in_ext(|| { + Pallet::::do_vest_plmc_for( + bid.bidder.clone(), + finished_project.project_id, + bid.bidder.clone(), + ) + }) + .unwrap(); + + let post_free_balance = test_env.in_ext(|| ::NativeCurrency::balance(&bid.bidder)); + assert_eq!(locked_amount, post_free_balance - prev_free_balance); + } + } + + #[test] + pub fn plmc_vesting_partial_amount() { + let test_env = TestEnvironment::new(); + let issuer = ISSUER; + let project = default_project(test_env.get_new_nonce()); + let evaluations = default_evaluations(); + let bids = default_bids(); + let community_contributions = default_community_buys(); + let remainder_contributions = vec![]; + + let finished_project = FinishedProject::new_with( + &test_env, + project, + issuer, + evaluations, + bids, + community_contributions, + remainder_contributions, + ); + + test_env.advance_time(15u64).unwrap(); + let details = finished_project.get_project_details(); + assert_eq!(details.cleanup, Cleaner::Success(CleanerState::Finished(PhantomData))); + let vest_start_block = details.funding_end_block.unwrap(); + let stored_bids = test_env + .in_ext(|| Bids::::iter_prefix_values((finished_project.project_id,)).collect::>()); + + for bid in stored_bids { + let vesting_info = bid.plmc_vesting_info.unwrap(); + + let now = test_env.current_block(); + let blocks_passed = now - vest_start_block; + let vested_amount = vesting_info.amount_per_block * blocks_passed as u128; + + let prev_free_balance = test_env.in_ext(|| ::NativeCurrency::balance(&bid.bidder)); + + test_env + .in_ext(|| { + Pallet::::do_vest_plmc_for( + bid.bidder.clone(), + finished_project.project_id, + bid.bidder.clone(), + ) + }) + .unwrap(); + + let post_free_balance = test_env.in_ext(|| ::NativeCurrency::balance(&bid.bidder)); + assert_eq!(vested_amount, post_free_balance - prev_free_balance); + } + } + + #[test] + pub fn unsuccessful_bids_dont_get_vest_schedule() { + let test_env = TestEnvironment::new(); + let issuer = ISSUER; + let project = default_project(test_env.get_new_nonce()); + let evaluations = default_evaluations(); + let mut bids = default_bids(); + + let available_tokens = + project.total_allocation_size.saturating_sub(bids.iter().fold(0, |acc, bid| acc + bid.amount)); + + let median_price = bids[bids.len().div(2)].price; + let accepted_bid = + vec![TestBid::new(BIDDER_4, available_tokens, median_price, None, AcceptedFundingAsset::USDT)]; + let rejected_bid = + vec![TestBid::new(BIDDER_5, 50_000 * ASSET_UNIT, median_price, None, AcceptedFundingAsset::USDT)]; + bids.extend(accepted_bid.clone()); + bids.extend(rejected_bid.clone()); + + let community_contributions = default_community_buys(); + + let auctioning_project = AuctioningProject::new_with(&test_env, project, issuer, evaluations); + let mut bidders_plmc = calculate_auction_plmc_spent(bids.clone()); + bidders_plmc.iter_mut().for_each(|(acc, amount)| *amount += get_ed()); + test_env.mint_plmc_to(bidders_plmc.clone()); + + let bidders_funding_assets = calculate_auction_funding_asset_spent(bids.clone()); + test_env.mint_statemint_asset_to(bidders_funding_assets.clone()); + + auctioning_project.bid_for_users(bids).unwrap(); + + let community_funding_project = auctioning_project.start_community_funding(); + let final_price = community_funding_project.get_project_details().weighted_average_price.unwrap(); + let mut contributors_plmc = calculate_contributed_plmc_spent(community_contributions.clone(), final_price); + contributors_plmc.iter_mut().for_each(|(acc, amount)| *amount += get_ed()); + test_env.mint_plmc_to(contributors_plmc.clone()); + + let contributors_funding_assets = + calculate_contributed_funding_asset_spent(community_contributions.clone(), final_price); + test_env.mint_statemint_asset_to(contributors_funding_assets.clone()); + + community_funding_project.buy_for_retail_users(community_contributions).unwrap(); + let finished_project = community_funding_project.finish_funding(); + + test_env.advance_time(10u64).unwrap(); + let details = finished_project.get_project_details(); + assert_eq!(details.cleanup, Cleaner::Success(CleanerState::Finished(PhantomData))); + + let plmc_locked_for_accepted_bid = + calculate_auction_plmc_spent_after_price_calculation(accepted_bid, final_price); + let plmc_locked_for_rejected_bid = + calculate_auction_plmc_spent_after_price_calculation(rejected_bid, final_price); + + let (accepted_user, accepted_plmc_amount) = plmc_locked_for_accepted_bid[0]; + let schedule = test_env.in_ext(|| { + ::Vesting::total_scheduled_amount( + &accepted_user, + LockType::Participation(finished_project.project_id), + ) + }); + assert_eq!(schedule.unwrap(), accepted_plmc_amount); + + let (rejected_user, _rejected_plmc_amount) = plmc_locked_for_rejected_bid[0]; + let schedule_exists = test_env + .in_ext(|| { + ::Vesting::total_scheduled_amount( + &rejected_user, + LockType::Participation(finished_project.project_id), + ) + }) + .is_some(); + assert!(!schedule_exists); + } } mod auction_round_failure { @@ -2939,8 +3198,6 @@ mod auction_round_failure { mod community_round_success { use super::*; - use frame_support::traits::fungible::Inspect; - use std::assert_matches::assert_matches; pub const HOURS: BlockNumber = 300u64; @@ -3760,11 +4017,217 @@ mod community_round_success { }) } } + + #[test] + pub fn plmc_vesting_schedule_starts_automatically() { + let test_env = TestEnvironment::new(); + let issuer = ISSUER; + let project = default_project(test_env.get_new_nonce()); + let evaluations = default_evaluations(); + let bids = default_bids(); + let community_contributions = default_community_buys(); + let remainder_contributions = vec![]; + + let finished_project = FinishedProject::new_with( + &test_env, + project, + issuer, + evaluations, + bids, + community_contributions.clone(), + remainder_contributions, + ); + + let price = finished_project.get_project_details().weighted_average_price.unwrap(); + let contribution_locked_plmc = calculate_contributed_plmc_spent(community_contributions, price); + + test_env.advance_time(10u64).unwrap(); + let details = finished_project.get_project_details(); + assert_eq!(details.cleanup, Cleaner::Success(CleanerState::Finished(PhantomData))); + + for (user, amount) in contribution_locked_plmc { + let schedule = test_env.in_ext(|| { + ::Vesting::total_scheduled_amount( + &user, + LockType::Participation(finished_project.project_id), + ) + }); + + assert_eq!(schedule.unwrap(), amount); + } + } + + #[test] + pub fn plmc_vesting_schedule_starts_manually() { + let test_env = TestEnvironment::new(); + let issuer = ISSUER; + let project = default_project(test_env.get_new_nonce()); + let evaluations = default_evaluations(); + let bids = default_bids(); + let community_contributions = default_community_buys(); + let remainder_contributions = vec![]; + + let finished_project = FinishedProject::new_with( + &test_env, + project, + issuer, + evaluations, + bids, + community_contributions.clone(), + remainder_contributions, + ); + + let details = finished_project.get_project_details(); + assert_eq!(details.status, ProjectStatus::FundingSuccessful); + assert_eq!(details.cleanup, Cleaner::NotReady); + + test_env.advance_time(1u64).unwrap(); + let details = finished_project.get_project_details(); + assert_eq!(details.cleanup, Cleaner::Success(CleanerState::Initialized(PhantomData))); + + let contributions = test_env.in_ext(|| { + Contributions::::iter_prefix_values((finished_project.project_id,)).collect::>() + }); + for contribution in contributions { + call_and_is_ok!( + test_env, + Pallet::::start_contribution_vesting_schedule_for( + RuntimeOrigin::signed(contribution.contributor), + finished_project.project_id, + contribution.contributor, + contribution.id, + ) + ); + + let schedule = test_env.in_ext(|| { + ::Vesting::total_scheduled_amount( + &contribution.contributor, + LockType::Participation(finished_project.project_id), + ) + }); + + let contribution = test_env.in_ext(|| { + Contributions::::get(( + finished_project.project_id, + contribution.contributor, + contribution.id, + )) + .unwrap() + }); + assert_eq!(schedule.unwrap(), contribution.plmc_vesting_info.unwrap().total_amount); + } + } + + #[test] + pub fn plmc_vesting_full_amount() { + let test_env = TestEnvironment::new(); + let issuer = ISSUER; + let project = default_project(test_env.get_new_nonce()); + let evaluations = default_evaluations(); + let bids = default_bids(); + let community_contributions = default_community_buys(); + let remainder_contributions = vec![]; + + let finished_project = FinishedProject::new_with( + &test_env, + project, + issuer, + evaluations, + bids, + community_contributions, + remainder_contributions, + ); + + test_env.advance_time(10u64).unwrap(); + let details = finished_project.get_project_details(); + assert_eq!(details.cleanup, Cleaner::Success(CleanerState::Finished(PhantomData))); + + let stored_contributions = test_env.in_ext(|| { + Contributions::::iter_prefix_values((finished_project.project_id,)).collect::>() + }); + + test_env.advance_time((10 * DAYS).into()).unwrap(); + + for contribution in stored_contributions { + let vesting_info = contribution.plmc_vesting_info.unwrap(); + let locked_amount = vesting_info.total_amount; + + let prev_free_balance = + test_env.in_ext(|| ::NativeCurrency::balance(&contribution.contributor)); + + test_env + .in_ext(|| { + Pallet::::do_vest_plmc_for( + contribution.contributor.clone(), + finished_project.project_id, + contribution.contributor.clone(), + ) + }) + .unwrap(); + + let post_free_balance = + test_env.in_ext(|| ::NativeCurrency::balance(&contribution.contributor)); + assert_eq!(locked_amount, post_free_balance - prev_free_balance); + } + } + + #[test] + pub fn plmc_vesting_partial_amount() { + let test_env = TestEnvironment::new(); + let issuer = ISSUER; + let project = default_project(test_env.get_new_nonce()); + let evaluations = default_evaluations(); + let bids = default_bids(); + let community_contributions = default_community_buys(); + let remainder_contributions = vec![]; + + let finished_project = FinishedProject::new_with( + &test_env, + project, + issuer, + evaluations, + bids, + community_contributions, + remainder_contributions, + ); + + test_env.advance_time(15u64).unwrap(); + let details = finished_project.get_project_details(); + assert_eq!(details.cleanup, Cleaner::Success(CleanerState::Finished(PhantomData))); + let vest_start_block = details.funding_end_block.unwrap(); + let stored_contributions = test_env.in_ext(|| { + Contributions::::iter_prefix_values((finished_project.project_id,)).collect::>() + }); + + for contribution in stored_contributions { + let vesting_info = contribution.plmc_vesting_info.unwrap(); + + let now = test_env.current_block(); + let blocks_passed = now - vest_start_block; + let vested_amount = vesting_info.amount_per_block * blocks_passed as u128; + + let prev_free_balance = + test_env.in_ext(|| ::NativeCurrency::balance(&contribution.contributor)); + + test_env + .in_ext(|| { + Pallet::::do_vest_plmc_for( + contribution.contributor.clone(), + finished_project.project_id, + contribution.contributor.clone(), + ) + }) + .unwrap(); + + let post_free_balance = + test_env.in_ext(|| ::NativeCurrency::balance(&contribution.contributor)); + assert_eq!(vested_amount, post_free_balance - prev_free_balance); + } + } } mod remainder_round_success { use super::*; - use crate::tests::testing_macros::extract_from_event; #[test] fn remainder_round_works() { @@ -4340,13 +4803,253 @@ mod remainder_round_success { }); } } + + #[test] + pub fn plmc_vesting_schedule_starts_automatically() { + let test_env = TestEnvironment::new(); + let issuer = ISSUER; + let project = default_project(test_env.get_new_nonce()); + let evaluations = default_evaluations(); + let bids = default_bids(); + let community_contributions = default_community_buys(); + let remainder_contributions = default_remainder_buys(); + + let finished_project = FinishedProject::new_with( + &test_env, + project, + issuer, + evaluations, + bids.clone(), + community_contributions.clone(), + remainder_contributions.clone(), + ); + + let price = finished_project.get_project_details().weighted_average_price.unwrap(); + let auction_locked_plmc = calculate_auction_plmc_spent_after_price_calculation(bids, price); + let community_locked_plmc = calculate_contributed_plmc_spent(community_contributions, price); + let remainder_locked_plmc = calculate_contributed_plmc_spent(remainder_contributions, price); + let all_plmc_locks = + merge_add_mappings_by_user(vec![auction_locked_plmc, community_locked_plmc, remainder_locked_plmc]); + + test_env.advance_time(10u64).unwrap(); + let details = finished_project.get_project_details(); + assert_eq!(details.cleanup, Cleaner::Success(CleanerState::Finished(PhantomData))); + + for (user, amount) in all_plmc_locks { + let schedule = test_env.in_ext(|| { + ::Vesting::total_scheduled_amount( + &user, + LockType::Participation(finished_project.project_id), + ) + }); + + assert_eq!(schedule.unwrap(), amount); + } + } + + #[test] + pub fn plmc_vesting_schedule_starts_manually() { + let test_env = TestEnvironment::new(); + let issuer = ISSUER; + let project = default_project(test_env.get_new_nonce()); + let evaluations = default_evaluations(); + let bids = default_bids(); + let community_contributions = default_community_buys(); + let remainder_contributions = default_remainder_buys(); + + let finished_project = FinishedProject::new_with( + &test_env, + project, + issuer, + evaluations, + bids.clone(), + community_contributions.clone(), + remainder_contributions.clone(), + ); + + let details = finished_project.get_project_details(); + assert_eq!(details.status, ProjectStatus::FundingSuccessful); + assert_eq!(details.cleanup, Cleaner::NotReady); + + test_env.advance_time(1u64).unwrap(); + let details = finished_project.get_project_details(); + assert_eq!(details.cleanup, Cleaner::Success(CleanerState::Initialized(PhantomData))); + + let contributions = test_env.in_ext(|| { + Contributions::::iter_prefix_values((finished_project.project_id,)).collect::>() + }); + for contribution in contributions { + let prev_scheduled = test_env + .in_ext(|| { + ::Vesting::total_scheduled_amount( + &contribution.contributor, + LockType::Participation(finished_project.project_id), + ) + }) + .unwrap_or(Zero::zero()); + + call_and_is_ok!( + test_env, + Pallet::::start_contribution_vesting_schedule_for( + RuntimeOrigin::signed(contribution.contributor), + finished_project.project_id, + contribution.contributor, + contribution.id, + ) + ); + + let post_scheduled = test_env + .in_ext(|| { + ::Vesting::total_scheduled_amount( + &contribution.contributor, + LockType::Participation(finished_project.project_id), + ) + }) + .unwrap(); + + let new_scheduled = post_scheduled - prev_scheduled; + + let contribution = test_env.in_ext(|| { + Contributions::::get(( + finished_project.project_id, + contribution.contributor, + contribution.id, + )) + .unwrap() + }); + assert_eq!(new_scheduled, contribution.plmc_vesting_info.unwrap().total_amount); + } + } + + #[test] + pub fn plmc_vesting_full_amount() { + let test_env = TestEnvironment::new(); + let issuer = ISSUER; + let project = default_project(test_env.get_new_nonce()); + let evaluations = default_evaluations(); + let bids = default_bids(); + let community_contributions = default_community_buys(); + let remainder_contributions = default_remainder_buys(); + + let finished_project = FinishedProject::new_with( + &test_env, + project, + issuer, + evaluations, + bids, + community_contributions, + remainder_contributions, + ); + + test_env.advance_time(10u64).unwrap(); + let details = finished_project.get_project_details(); + assert_eq!(details.cleanup, Cleaner::Success(CleanerState::Finished(PhantomData))); + + let stored_bids = test_env + .in_ext(|| Bids::::iter_prefix_values((finished_project.project_id,)).collect::>()); + let stored_contributions = test_env.in_ext(|| { + Contributions::::iter_prefix_values((finished_project.project_id,)).collect::>() + }); + + let bid_plmc_balances = + stored_bids.into_iter().map(|b| (b.bidder, b.plmc_vesting_info.unwrap().total_amount)).collect::>(); + let contributed_plmc_balances = stored_contributions + .into_iter() + .map(|c| (c.contributor, c.plmc_vesting_info.unwrap().total_amount)) + .collect::>(); + + let merged_plmc_balances = generic_map_merge_reduce( + vec![contributed_plmc_balances.clone(), bid_plmc_balances.clone()], + |(account, amount)| account.clone(), + BalanceOf::::zero(), + |(account, amount), total| total + amount, + ); + test_env.advance_time((1 * DAYS + 1u32).into()).unwrap(); + + for (contributor, plmc_amount) in merged_plmc_balances { + let prev_free_balance = test_env.in_ext(|| ::NativeCurrency::balance(&contributor)); + test_env + .in_ext(|| { + Pallet::::do_vest_plmc_for( + contributor.clone(), + finished_project.project_id, + contributor.clone(), + ) + }) + .unwrap(); + + let post_free_balance = test_env.in_ext(|| ::NativeCurrency::balance(&contributor)); + assert_eq!(plmc_amount, post_free_balance - prev_free_balance); + } + } + + #[test] + pub fn plmc_vesting_partial_amount() { + let test_env = TestEnvironment::new(); + let issuer = ISSUER; + let project = default_project(test_env.get_new_nonce()); + let evaluations = default_evaluations(); + let bids = default_bids(); + let community_contributions = default_community_buys(); + let remainder_contributions = default_remainder_buys(); + + let finished_project = FinishedProject::new_with( + &test_env, + project, + issuer, + evaluations, + bids, + community_contributions, + remainder_contributions, + ); + + test_env.advance_time(15u64).unwrap(); + let details = finished_project.get_project_details(); + assert_eq!(details.cleanup, Cleaner::Success(CleanerState::Finished(PhantomData))); + let vest_start_block = details.funding_end_block.unwrap(); + + let stored_bids = test_env + .in_ext(|| Bids::::iter_prefix_values((finished_project.project_id,)).collect::>()); + let stored_contributions = test_env.in_ext(|| { + Contributions::::iter_prefix_values((finished_project.project_id,)).collect::>() + }); + + let now = test_env.current_block(); + let blocks_passed = now - vest_start_block; + + let bid_plmc_balances = stored_bids + .into_iter() + .map(|b| (b.bidder, b.plmc_vesting_info.unwrap().amount_per_block * blocks_passed as u128)) + .collect::>(); + let contributed_plmc_balances = stored_contributions + .into_iter() + .map(|c| (c.contributor, c.plmc_vesting_info.unwrap().amount_per_block * blocks_passed as u128)) + .collect::>(); + + let merged_plmc_balances = generic_map_merge_reduce( + vec![contributed_plmc_balances.clone(), bid_plmc_balances.clone()], + |(account, amount)| account.clone(), + BalanceOf::::zero(), + |(account, amount), total| total + amount, + ); + + for (contributor, amount) in merged_plmc_balances { + let prev_free_balance = test_env.in_ext(|| ::NativeCurrency::balance(&contributor)); + + test_env + .in_ext(|| { + Pallet::::do_vest_plmc_for(contributor, finished_project.project_id, contributor) + }) + .unwrap(); + + let post_free_balance = test_env.in_ext(|| ::NativeCurrency::balance(&contributor)); + assert_eq!(amount, post_free_balance - prev_free_balance); + } + } } mod funding_end { use super::*; - use sp_arithmetic::{Percent, Perquintill}; - use std::assert_matches::assert_matches; - use testing_macros::call_and_is_ok; #[test] fn automatic_fail_less_eq_33_percent() { @@ -4928,7 +5631,6 @@ mod test_helper_functions { mod misc_features { use super::*; - use crate::UpdateType::{CommunityFundingStart, RemainderFundingStart}; #[test] fn remove_from_update_store_works() { diff --git a/pallets/funding/src/types.rs b/pallets/funding/src/types.rs index 325e469bd..861021772 100644 --- a/pallets/funding/src/types.rs +++ b/pallets/funding/src/types.rs @@ -23,7 +23,7 @@ use crate::{ BalanceOf, }; use frame_support::{pallet_prelude::*, traits::tokens::Balance as BalanceT}; -use sp_arithmetic::{traits::Saturating, FixedPointNumber, FixedPointOperand}; +use sp_arithmetic::{FixedPointNumber, FixedPointOperand}; use sp_runtime::traits::CheckedDiv; use sp_std::{cmp::Eq, collections::btree_map::*, prelude::*}; @@ -138,6 +138,8 @@ pub mod storage_types { pub cleanup: Cleaner, /// Information about the total amount bonded, and the outcome in regards to reward/slash/nothing pub evaluation_round_info: EvaluationRoundInfo, + + pub funding_end_block: Option, } /// Tells on_initialize what to do with the project @@ -175,9 +177,8 @@ pub mod storage_types { Price: FixedPointNumber, AccountId, BlockNumber, - PlmcVesting, - CTVesting, Multiplier, + VestingInfo, > { pub id: Id, pub project_id: ProjectId, @@ -192,9 +193,8 @@ pub mod storage_types { pub funding_asset_amount_locked: Balance, pub multiplier: Multiplier, pub plmc_bond: Balance, + pub plmc_vesting_info: Option, pub funded: bool, - pub plmc_vesting_period: PlmcVesting, - pub ct_vesting_period: CTVesting, pub when: BlockNumber, pub funds_released: bool, pub ct_minted: bool, @@ -207,10 +207,9 @@ pub mod storage_types { Price: FixedPointNumber, AccountId: Eq, BlockNumber: Eq + Ord, - PlmcVesting: Eq, - CTVesting: Eq, Multiplier: Eq, - > Ord for BidInfo + VestingInfo: Eq, + > Ord for BidInfo { fn cmp(&self, other: &Self) -> sp_std::cmp::Ordering { match self.original_ct_usd_price.cmp(&other.original_ct_usd_price) { @@ -227,10 +226,9 @@ pub mod storage_types { Price: FixedPointNumber, AccountId: Eq, BlockNumber: Eq + Ord, - PlmcVesting: Eq, - CTVesting: Eq, Multiplier: Eq, - > PartialOrd for BidInfo + VestingInfo: Eq, + > PartialOrd for BidInfo { fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) @@ -238,17 +236,17 @@ pub mod storage_types { } #[derive(Clone, Copy, Encode, Decode, Eq, PartialEq, RuntimeDebug, MaxEncodedLen, TypeInfo)] - pub struct ContributionInfo { + pub struct ContributionInfo { pub id: Id, pub project_id: ProjectId, pub contributor: AccountId, pub ct_amount: Balance, pub usd_contribution_amount: Balance, + pub multiplier: Multiplier, pub funding_asset: AcceptedFundingAsset, pub funding_asset_amount: Balance, pub plmc_bond: Balance, - pub plmc_vesting_period: PLMCVesting, - pub ct_vesting_period: CTVesting, + pub plmc_vesting_info: Option, pub funds_released: bool, pub ct_minted: bool, } @@ -438,35 +436,10 @@ pub mod inner_types { } #[derive(Clone, Copy, Encode, Decode, Eq, PartialEq, RuntimeDebug, TypeInfo, MaxEncodedLen)] - pub struct Vesting { - // Amount of tokens vested - pub amount: Balance, - // number of blocks after project ends, when vesting starts - pub start: BlockNumber, - // number of blocks after project ends, when vesting ends - pub end: BlockNumber, - // number of blocks between each withdrawal - pub step: BlockNumber, - // absolute block number of next block where withdrawal is possible - pub next_withdrawal: BlockNumber, - } - - impl< - BlockNumber: Saturating + Copy + CheckedDiv, - Balance: Saturating + CheckedDiv + Copy + From + Eq + sp_std::ops::SubAssign, - > Vesting - { - pub fn calculate_next_withdrawal(&mut self) -> Result { - if self.amount == 0u32.into() { - Err(()) - } else { - let next_withdrawal = self.next_withdrawal.saturating_add(self.step); - let withdraw_amount = self.amount; - self.next_withdrawal = next_withdrawal; - self.amount -= withdraw_amount; - Ok(withdraw_amount) - } - } + pub struct VestingInfo { + pub total_amount: Balance, + pub amount_per_block: Balance, + pub duration: BlockNumber, } #[derive(Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug, TypeInfo, MaxEncodedLen)] @@ -505,10 +478,10 @@ pub mod inner_types { EvaluationUnbonding(u64, PhantomData), // Branch // A. Success only - BidPLMCVesting(u64, PhantomData), BidCTMint(u64, PhantomData), - ContributionPLMCVesting(u64, PhantomData), ContributionCTMint(u64, PhantomData), + StartBidderVestingSchedule(u64, PhantomData), + StartContributorVestingSchedule(u64, PhantomData), BidFundingPayout(u64, PhantomData), ContributionFundingPayout(u64, PhantomData), // B. Failure only diff --git a/pallets/linear-release/src/impls.rs b/pallets/linear-release/src/impls.rs index 4d1894986..085f53a49 100644 --- a/pallets/linear-release/src/impls.rs +++ b/pallets/linear-release/src/impls.rs @@ -97,7 +97,7 @@ impl Pallet { Self::deposit_event(Event::::VestingTransferred { to: target.clone(), amount: amount_transferred }); // We can't let this fail because the currency transfer has already happened. - let res = Self::add_release_schedule( + let res = Self::set_release_schedule( &target, amount_transferred, schedule.per_block(), @@ -148,18 +148,13 @@ impl Pallet { reason: ReasonOf, ) -> Result<(), DispatchError> { if total_held_now.is_zero() { - T::Currency::release( - &reason, - who, - T::Currency::balance_on_hold(&reason, who), - frame_support::traits::tokens::Precision::BestEffort, - )?; + T::Currency::release(&reason, who, T::Currency::balance_on_hold(&reason, who), Precision::BestEffort)?; Self::deposit_event(Event::::VestingCompleted { account: who.clone() }); } else { let already_held = T::Currency::balance_on_hold(&reason, who); let to_release = already_held.saturating_sub(total_held_now); T::Currency::release(&reason, who, to_release, Precision::BestEffort)?; - Self::deposit_event(Event::::VestingUpdated { account: who.clone(), unvested: to_release }); + Self::deposit_event(Event::::VestingUpdated { account: who.clone(), unvested: total_held_now }); }; Ok(()) @@ -240,7 +235,7 @@ impl ReleaseSchedule, ReasonOf> for Pallet { type Currency = T::Currency; type Moment = BlockNumberFor; - /// Get the amount that is currently being held and cannot be transferred out of this account. + /// Get the amount that is possible to vest (i.e release) at this block. fn vesting_balance(who: &T::AccountId, reason: ReasonOf) -> Option> { if let Some(v) = Self::vesting(who, reason) { let now = >::block_number(); @@ -253,6 +248,15 @@ impl ReleaseSchedule, ReasonOf> for Pallet { } } + fn total_scheduled_amount(who: &T::AccountId, reason: ReasonOf) -> Option> { + if let Some(v) = Self::vesting(who, reason) { + let total = v.iter().fold(Zero::zero(), |total, schedule| schedule.locked.saturating_add(total)); + Some(total) + } else { + None + } + } + /// Adds a vesting schedule to a given account. /// /// If the account has `MaxVestingSchedules`, an Error is returned and nothing @@ -265,7 +269,7 @@ impl ReleaseSchedule, ReasonOf> for Pallet { /// Is a no-op if the amount to be vested is zero. /// /// NOTE: This doesn't alter the free balance of the account. - fn add_release_schedule( + fn set_release_schedule( who: &T::AccountId, locked: BalanceOf, per_block: BalanceOf, @@ -317,7 +321,7 @@ impl ReleaseSchedule, ReasonOf> for Pallet { Ok(()) } - fn set_release_schedule( + fn add_release_schedule( who: &T::AccountId, locked: >::Balance, per_block: >::Balance, @@ -357,4 +361,15 @@ impl ReleaseSchedule, ReasonOf> for Pallet { Self::write_release(who, locked_now, reason)?; Ok(()) } + + fn vest( + who: AccountIdOf, + reason: ReasonOf, + ) -> Result<>::Balance, DispatchError> { + let prev_locked = T::Currency::balance_on_hold(&reason, &who); + Self::do_vest(who.clone(), reason.clone())?; + let post_locked = T::Currency::balance_on_hold(&reason, &who); + + Ok(prev_locked.saturating_sub(post_locked)) + } } diff --git a/pallets/linear-release/src/tests.rs b/pallets/linear-release/src/tests.rs index 409de08f3..f46f3e7e3 100644 --- a/pallets/linear-release/src/tests.rs +++ b/pallets/linear-release/src/tests.rs @@ -1117,7 +1117,6 @@ fn vested_transfer_less_than_existential_deposit_fails() { }); } -// TODO #[test] fn set_release_schedule() { ExtBuilder::default().existential_deposit(ED).build().execute_with(|| { @@ -1177,7 +1176,6 @@ fn set_release_schedule() { }); } -// TODO #[test] fn cannot_release_different_reason() { ExtBuilder::default().existential_deposit(ED).build().execute_with(|| { @@ -1221,7 +1219,6 @@ fn cannot_release_different_reason() { }); } -// TODO #[test] fn multile_holds_release_schedule() { ExtBuilder::default().existential_deposit(ED).build().execute_with(|| { @@ -1352,7 +1349,7 @@ fn vest_all_different_reason() { // Set release schedule to release the locked amount, starting from now, one ED per block. let user3_vesting_schedule = VestingInfo::new(user_3_on_hold_balance, ED, 0); - assert_ok!(Vesting::set_release_schedule( + assert_ok!(Vesting::add_release_schedule( &3, user3_vesting_schedule.locked, user3_vesting_schedule.per_block, @@ -1423,7 +1420,7 @@ fn manual_vest_all_different_reason() { // Set release schedule to release the locked amount, starting from now, one ED per block. let user3_vesting_schedule = VestingInfo::new(user_3_on_hold_balance, ED, 0); - assert_ok!(Vesting::set_release_schedule( + assert_ok!(Vesting::add_release_schedule( &3, user3_vesting_schedule.locked, user3_vesting_schedule.per_block, diff --git a/runtimes/testnet/src/xcm_config.rs b/runtimes/testnet/src/xcm_config.rs index 3d7a63c99..17302ce1b 100644 --- a/runtimes/testnet/src/xcm_config.rs +++ b/runtimes/testnet/src/xcm_config.rs @@ -66,7 +66,7 @@ parameter_types! { pub RelayChainOrigin: RuntimeOrigin = cumulus_pallet_xcm::Origin::Relay.into(); pub UniversalLocation: InteriorMultiLocation = ( GlobalConsensus(Polkadot), - Parachain(ParachainInfo::parachain_id().into()), + Parachain(ParachainInfo::parachain_id().into()), ).into(); pub const HereLocation: MultiLocation = MultiLocation::here(); diff --git a/traits/src/lib.rs b/traits/src/lib.rs index 7d998747c..94367d778 100644 --- a/traits/src/lib.rs +++ b/traits/src/lib.rs @@ -29,13 +29,24 @@ pub trait ReleaseSchedule { + fungible::MutateHold + fungible::BalancedHold; - /// Get the amount that is currently being vested and cannot be transferred out of this account. - /// Returns `None` if the account has no vesting schedule. + /// Get the amount that is possible to vest (i.e release) at the current block fn vesting_balance( who: &AccountId, reason: Reason, ) -> Option<>::Balance>; + /// Get the amount that was scheduled, regardless if it was already vested or not + fn total_scheduled_amount( + who: &AccountId, + reason: Reason, + ) -> Option<>::Balance>; + + /// Release the vested amount of the given account. + fn vest( + who: AccountId, + reason: Reason, + ) -> Result<>::Balance, DispatchError>; + /// Adds a release schedule to a given account. /// /// If the account has `MaxVestingSchedules`, an Error is returned and nothing