diff --git a/pallets/funding/src/functions/6_settlement.rs b/pallets/funding/src/functions/6_settlement.rs index 5aee5e971..7e2b17342 100644 --- a/pallets/funding/src/functions/6_settlement.rs +++ b/pallets/funding/src/functions/6_settlement.rs @@ -1,6 +1,6 @@ #[allow(clippy::wildcard_imports)] use super::*; -use crate::{traits::VestingDurationCalculation, Balance}; +use crate::{traits::VestingDurationCalculation, Balance, HoldReason::Participation}; use frame_support::{ dispatch::DispatchResult, ensure, @@ -156,17 +156,21 @@ impl Pallet { if funding_success && bid.status != BidStatus::Rejected { let funding_end_block = project_details.funding_end_block.ok_or(Error::::ImpossibleState)?; - let plmc_vesting_info = + let vesting_info = Self::calculate_vesting_info(&bid.bidder, bid.multiplier, bid.plmc_bond.saturating_sub(refunded_plmc)) .map_err(|_| Error::::BadMath)?; - VestingOf::::add_release_schedule( - &bid.bidder, - plmc_vesting_info.total_amount, - plmc_vesting_info.amount_per_block, - funding_end_block, - HoldReason::Participation.into(), - )?; + if vesting_info.duration == 1u32.into() { + Self::release_participation_bond(&bid.bidder, vesting_info.total_amount)?; + } else { + VestingOf::::add_release_schedule( + &bid.bidder, + vesting_info.total_amount, + vesting_info.amount_per_block, + funding_end_block, + HoldReason::Participation.into(), + )?; + } Self::mint_contribution_tokens(project_id, &bid.bidder, final_ct_amount)?; @@ -176,7 +180,7 @@ impl Pallet { bid.id, ParticipationType::Bid, final_ct_amount, - plmc_vesting_info.duration, + vesting_info.duration, )?; Self::release_funding_asset( @@ -250,20 +254,25 @@ impl Pallet { )?; } else { // Calculate the vesting info and add the release schedule - let vest_info = Self::calculate_vesting_info( + let vesting_info = Self::calculate_vesting_info( &contribution.contributor, contribution.multiplier, contribution.plmc_bond, ) .map_err(|_| Error::::BadMath)?; - VestingOf::::add_release_schedule( - &contribution.contributor, - vest_info.total_amount, - vest_info.amount_per_block, - funding_end_block, - HoldReason::Participation.into(), - )?; + if vesting_info.duration == 1u32.into() { + Self::release_participation_bond(&contribution.contributor, vesting_info.total_amount)?; + } else { + VestingOf::::add_release_schedule( + &contribution.contributor, + vesting_info.total_amount, + vesting_info.amount_per_block, + funding_end_block, + HoldReason::Participation.into(), + )?; + } + // Mint the contribution tokens Self::mint_contribution_tokens(project_id, &contribution.contributor, contribution.ct_amount)?; @@ -282,7 +291,7 @@ impl Pallet { contribution.id, ParticipationType::Contribution, contribution.ct_amount, - vest_info.duration, + vesting_info.duration, )?; final_ct_amount = contribution.ct_amount; diff --git a/pallets/funding/src/instantiator/chain_interactions.rs b/pallets/funding/src/instantiator/chain_interactions.rs index a6c93df29..a51f29871 100644 --- a/pallets/funding/src/instantiator/chain_interactions.rs +++ b/pallets/funding/src/instantiator/chain_interactions.rs @@ -622,7 +622,8 @@ impl< ProjectStatus::CommunityRound(..) => for cont in contributions { let did = generate_did_from_account(cont.contributor.clone()); - let investor_type = InvestorType::Retail; + // We use institutional to be able to test most multipliers. + let investor_type = InvestorType::Institutional; let params = DoContributeParams:: { contributor: cont.contributor, project_id, diff --git a/pallets/funding/src/lib.rs b/pallets/funding/src/lib.rs index 3fbdb5a92..8d53f605d 100644 --- a/pallets/funding/src/lib.rs +++ b/pallets/funding/src/lib.rs @@ -136,6 +136,7 @@ pub type ContributionInfoOf = pub type BucketOf = Bucket>; pub type WeightInfoOf = ::WeightInfo; pub type VestingOf = pallet_linear_release::Pallet; +pub type BlockNumberToBalanceOf = ::BlockNumberToBalance; pub const PLMC_FOREIGN_ID: u32 = 3344; pub const PLMC_DECIMALS: u8 = 10; @@ -329,7 +330,7 @@ pub mod pallet { + Member; /// The hold reason enum constructed by the construct_runtime macro - type RuntimeHoldReason: From; + type RuntimeHoldReason: From + Parameter + MaxEncodedLen + Copy; /// The origin enum constructed by the construct_runtime macro type RuntimeOrigin: IsType<::RuntimeOrigin> diff --git a/pallets/funding/src/mock.rs b/pallets/funding/src/mock.rs index 117558c6a..7afcbab3c 100644 --- a/pallets/funding/src/mock.rs +++ b/pallets/funding/src/mock.rs @@ -552,5 +552,10 @@ sp_api::mock_impl_runtime_apis! { fn funding_asset_to_ct_amount(project_id: ProjectId, asset: AcceptedFundingAsset, asset_amount: Balance) -> Balance { PolimecFunding::funding_asset_to_ct_amount(project_id, asset, asset_amount) } + fn get_next_vesting_schedule_merge_candidates(account: AccountId, hold_reason: RuntimeHoldReason, end_max_delta: Balance) -> Option<(u32, u32)> { + PolimecFunding::get_next_vesting_schedule_merge_candidates(account, hold_reason, end_max_delta) + } + + } } diff --git a/pallets/funding/src/runtime_api.rs b/pallets/funding/src/runtime_api.rs index 9ddd81865..feacf36b8 100644 --- a/pallets/funding/src/runtime_api.rs +++ b/pallets/funding/src/runtime_api.rs @@ -53,11 +53,15 @@ sp_api::decl_runtime_apis! { fn projects_by_did(did: Did) -> Vec; } - #[api_version(1)] + #[api_version(2)] pub trait ExtrinsicHelpers { /// Get the current price of a contribution token (either current bucket in the auction, or WAP in contribution phase), /// and calculate the amount of tokens that can be bought with the given amount USDT/USDC/DOT. fn funding_asset_to_ct_amount(project_id: ProjectId, asset: AcceptedFundingAsset, asset_amount: Balance) -> Balance; + + /// Get the indexes of vesting schedules that are good candidates to be merged. + /// Schedules that have not yet started are de-facto bad candidates. + fn get_next_vesting_schedule_merge_candidates(account_id: AccountIdOf, hold_reason: ::RuntimeHoldReason, end_max_delta: Balance) -> Option<(u32, u32)>; } } @@ -169,6 +173,36 @@ impl Pallet { ct_amount } + pub fn get_next_vesting_schedule_merge_candidates( + account_id: AccountIdOf, + hold_reason: ::RuntimeHoldReason, + end_max_delta: Balance, + ) -> Option<(u32, u32)> { + let schedules = pallet_linear_release::Vesting::::get(account_id, hold_reason) + .expect("No vesting schedules found") + .to_vec(); + + let mut inspected_schedules: Vec<(usize, &pallet_linear_release::VestingInfoOf)> = Vec::new(); + let now = >::block_number(); + for (i, schedule) in schedules.iter().enumerate() { + if schedule.starting_block > now { + continue; + } + + let schedule_end = schedule.ending_block_as_balance::>(); + + for (j, other_schedule) in inspected_schedules.iter() { + let other_end = other_schedule.ending_block_as_balance::>(); + let end_delta = schedule_end.abs_diff(other_end); + if end_delta <= end_max_delta { + return Some((*j as u32, i as u32)); + } + } + inspected_schedules.push((i, schedule)); + } + None + } + pub fn all_project_participations_by_did(project_id: ProjectId, did: Did) -> Vec> { let evaluations = Evaluations::::iter_prefix((project_id,)) .filter(|((_account_id, _evaluation_id), evaluation)| evaluation.did == did) diff --git a/pallets/funding/src/tests/runtime_api.rs b/pallets/funding/src/tests/runtime_api.rs index 23273e91c..9ccd0cd95 100644 --- a/pallets/funding/src/tests/runtime_api.rs +++ b/pallets/funding/src/tests/runtime_api.rs @@ -469,6 +469,82 @@ fn funding_asset_to_ct_amount() { }); } +#[test] +fn get_next_vesting_schedule_merge_candidates() { + let mut inst = MockInstantiator::new(Some(RefCell::new(new_test_ext()))); + let evaluations = vec![ + UserToUSDBalance::new(EVALUATOR_1, 500_000 * USD_UNIT), + UserToUSDBalance::new(EVALUATOR_2, 250_000 * USD_UNIT), + UserToUSDBalance::new(BIDDER_1, 320_000 * USD_UNIT), + ]; + let bids = vec![ + BidParams::new(BIDDER_1, 50_000 * CT_UNIT, 10u8, AcceptedFundingAsset::USDT), + BidParams::new(BIDDER_1, 400_000 * CT_UNIT, 5u8, AcceptedFundingAsset::USDT), + BidParams::new(BIDDER_2, 50_000 * CT_UNIT, 1u8, AcceptedFundingAsset::USDT), + ]; + let remaining_contributions = vec![ + ContributionParams::new(BIDDER_1, 1_000 * CT_UNIT, 5u8, AcceptedFundingAsset::USDT), + ContributionParams::new(BIDDER_1, 15_000 * CT_UNIT, 10u8, AcceptedFundingAsset::USDT), + ContributionParams::new(BIDDER_1, 100 * CT_UNIT, 1u8, AcceptedFundingAsset::USDT), + ]; + + let project_id = inst.create_finished_project( + default_project_metadata(ISSUER_1), + ISSUER_1, + None, + evaluations.clone(), + bids.clone(), + default_community_contributions(), + remaining_contributions.clone(), + ); + assert_eq!(ProjectStatus::SettlementStarted(FundingOutcome::Success), inst.go_to_next_state(project_id)); + inst.execute(|| { + PolimecFunding::settle_evaluation(RuntimeOrigin::signed(BIDDER_1), project_id, BIDDER_1, 2).unwrap(); + PolimecFunding::settle_bid(RuntimeOrigin::signed(BIDDER_1), project_id, BIDDER_1, 0).unwrap(); + PolimecFunding::settle_bid(RuntimeOrigin::signed(BIDDER_1), project_id, BIDDER_1, 1).unwrap(); + PolimecFunding::settle_contribution(RuntimeOrigin::signed(BIDDER_1), project_id, BIDDER_1, 5).unwrap(); + PolimecFunding::settle_contribution(RuntimeOrigin::signed(BIDDER_1), project_id, BIDDER_1, 6).unwrap(); + PolimecFunding::settle_contribution(RuntimeOrigin::signed(BIDDER_1), project_id, BIDDER_1, 7).unwrap(); + }); + + let hold_reason: mock::RuntimeHoldReason = HoldReason::Participation.into(); + let bidder_1_schedules = + inst.execute(|| pallet_linear_release::Vesting::::get(BIDDER_1, hold_reason).unwrap().to_vec()); + // Evaluations didn't get a vesting schedule + assert_eq!(bidder_1_schedules.len(), 4); + for schedule in bidder_1_schedules.iter() {} + + inst.execute(|| { + let block_hash = System::block_hash(System::block_number()); + let (idx_1, idx_2) = TestRuntime::get_next_vesting_schedule_merge_candidates( + &TestRuntime, + block_hash, + BIDDER_1, + HoldReason::Participation.into(), + // within 100 blocks + 100u128, + ) + .unwrap() + .unwrap(); + assert_eq!((idx_1, idx_2), (1, 2)); + + // Merging the two schedules deletes them and creates a new one at the end of the vec. + LinearRelease::merge_schedules(RuntimeOrigin::signed(BIDDER_1), idx_1, idx_2, hold_reason).unwrap(); + + let (idx_1, idx_2) = TestRuntime::get_next_vesting_schedule_merge_candidates( + &TestRuntime, + block_hash, + BIDDER_1, + HoldReason::Participation.into(), + // within 100 blocks + 100u128, + ) + .unwrap() + .unwrap(); + assert_eq!((idx_1, idx_2), (0, 1)); + }); +} + #[test] fn all_project_participations_by_did() { let mut inst = MockInstantiator::new(Some(RefCell::new(new_test_ext()))); diff --git a/runtimes/polimec/src/lib.rs b/runtimes/polimec/src/lib.rs index 4257e9ba8..03a22703d 100644 --- a/runtimes/polimec/src/lib.rs +++ b/runtimes/polimec/src/lib.rs @@ -1429,6 +1429,9 @@ impl_runtime_apis! { fn funding_asset_to_ct_amount(project_id: ProjectId, asset: AcceptedFundingAsset, asset_amount: Balance) -> Balance { Funding::funding_asset_to_ct_amount(project_id, asset, asset_amount) } + fn get_next_vesting_schedule_merge_candidates(account: AccountId, hold_reason: RuntimeHoldReason, end_max_delta: Balance) -> Option<(u32, u32)> { + Funding::get_next_vesting_schedule_merge_candidates(account, hold_reason, end_max_delta) + } } #[cfg(feature = "try-runtime")]