From f93fc74ae344b5eeb4b527d261a854695d50f1f4 Mon Sep 17 00:00:00 2001 From: Juan Ignacio Rios Date: Tue, 3 Sep 2024 11:01:01 +0200 Subject: [PATCH] schedule merge runtime api --- pallets/funding/src/benchmarking.rs | 2 +- pallets/funding/src/functions/6_settlement.rs | 45 ++++++----- .../funding/src/instantiator/calculations.rs | 1 - .../src/instantiator/chain_interactions.rs | 3 +- pallets/funding/src/lib.rs | 3 +- pallets/funding/src/mock.rs | 5 ++ pallets/funding/src/runtime_api.rs | 41 +++++++++- pallets/funding/src/tests/6_settlement.rs | 8 +- pallets/funding/src/tests/runtime_api.rs | 75 +++++++++++++++++++ runtimes/polimec/src/lib.rs | 3 + 10 files changed, 159 insertions(+), 27 deletions(-) diff --git a/pallets/funding/src/benchmarking.rs b/pallets/funding/src/benchmarking.rs index f02fcc179..1c7921c2c 100644 --- a/pallets/funding/src/benchmarking.rs +++ b/pallets/funding/src/benchmarking.rs @@ -248,7 +248,7 @@ pub fn default_bidder_multipliers() -> Vec { vec![10u8, 3u8, 1u8, 7u8, 4u8] } pub fn default_community_contributor_multipliers() -> Vec { - vec![1u8, 1u8, 1u8, 1u8, 1u8] + vec![2u8, 1u8, 3u8, 1u8, 1u8] } pub fn default_remainder_contributor_multipliers() -> Vec { vec![1u8, 11u8, 1u8, 1u8, 1u8] diff --git a/pallets/funding/src/functions/6_settlement.rs b/pallets/funding/src/functions/6_settlement.rs index 5aee5e971..69635a103 100644 --- a/pallets/funding/src/functions/6_settlement.rs +++ b/pallets/funding/src/functions/6_settlement.rs @@ -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/calculations.rs b/pallets/funding/src/instantiator/calculations.rs index e90deadc1..46d31bf3f 100644 --- a/pallets/funding/src/instantiator/calculations.rs +++ b/pallets/funding/src/instantiator/calculations.rs @@ -121,7 +121,6 @@ impl< output.merge_accounts(MergeOperation::Add) } - // WARNING: Only put bids that you are sure will be done before the random end of the closing auction pub fn calculate_auction_plmc_returned_from_all_bids_made( &mut self, // bids in the order they were made 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 47ae6af37..09e16d2e0 100644 --- a/pallets/funding/src/lib.rs +++ b/pallets/funding/src/lib.rs @@ -139,6 +139,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; @@ -332,7 +333,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..00e19eb7d 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,41 @@ 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)? + .into_iter() + .enumerate() + // Filter out schedules with future starting blocks before collecting them into a vector. + .filter_map(|(i, schedule)| { + if schedule.starting_block > >::block_number() { + None + } else { + Some((i, schedule.ending_block_as_balance::>())) + } + }) + .collect::>(); + + let mut inspected_schedules = BTreeMap::new(); + + for (i, schedule_end) in schedules { + let range_start = schedule_end.saturating_sub(end_max_delta); + let range_end = schedule_end.saturating_add(end_max_delta); + + // All entries where the ending_block is between range_start and range_end. + if let Some((_, &j)) = inspected_schedules.range(range_start..=range_end).next() { + return Some((j as u32, i as u32)); + } + + inspected_schedules.insert(schedule_end, i); + } + + 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/6_settlement.rs b/pallets/funding/src/tests/6_settlement.rs index 198e1ad43..c023bdbac 100644 --- a/pallets/funding/src/tests/6_settlement.rs +++ b/pallets/funding/src/tests/6_settlement.rs @@ -396,7 +396,7 @@ mod settle_bid_extrinsic { let auction_allocation = project_metadata.auction_round_allocation_percentage * project_metadata.total_allocation_size; let partial_amount_bid_params = - BidParams::new(BIDDER_1, auction_allocation, 1u8, AcceptedFundingAsset::USDT); + BidParams::new(BIDDER_1, auction_allocation, 3u8, AcceptedFundingAsset::USDT); let lower_price_bid_params = BidParams::new(BIDDER_2, 2000 * CT_UNIT, 5u8, AcceptedFundingAsset::DOT); let bids = vec![partial_amount_bid_params.clone(), lower_price_bid_params.clone()]; @@ -461,10 +461,10 @@ mod settle_bid_extrinsic { true, ); - // Multiplier one should be fully unbonded the next block - inst.advance_time(1_u64); - let hold_reason: RuntimeHoldReason = HoldReason::Participation.into(); + let vesting_time = Multiplier::force_new(3).calculate_vesting_duration::(); + let now = inst.current_block(); + inst.jump_to_block(now + vesting_time + 1u64); inst.execute(|| LinearRelease::vest(RuntimeOrigin::signed(BIDDER_1), hold_reason).expect("Vesting failed")); inst.assert_plmc_free_balance(BIDDER_1, expected_plmc_refund + expected_final_plmc_bonded + ed); diff --git a/pallets/funding/src/tests/runtime_api.rs b/pallets/funding/src/tests/runtime_api.rs index 23273e91c..bff7d66e7 100644 --- a/pallets/funding/src/tests/runtime_api.rs +++ b/pallets/funding/src/tests/runtime_api.rs @@ -469,6 +469,81 @@ 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); + + 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")]