From ede7645d2099d776d6f592d121c4eb19b7a10c4b Mon Sep 17 00:00:00 2001 From: Juan Ignacio Rios Date: Wed, 5 Feb 2025 17:05:44 +0100 Subject: [PATCH] Auction Round Improvements --- integration-tests/src/tests/ct_migration.rs | 25 +- integration-tests/src/tests/defaults.rs | 2 +- integration-tests/src/tests/e2e.rs | 16 +- integration-tests/src/tests/oracle.rs | 4 +- integration-tests/src/tests/runtime_apis.rs | 3 +- justfile | 4 +- pallets/funding/src/benchmarking.rs | 572 +++++---------- pallets/funding/src/functions/2_evaluation.rs | 16 +- pallets/funding/src/functions/3_auction.rs | 116 ++- .../funding/src/functions/5_funding_end.rs | 61 +- pallets/funding/src/functions/6_settlement.rs | 63 +- .../funding/src/functions/7_ct_migration.rs | 4 - pallets/funding/src/functions/misc.rs | 127 ++-- .../funding/src/instantiator/calculations.rs | 91 +-- .../src/instantiator/chain_interactions.rs | 104 ++- pallets/funding/src/instantiator/tests.rs | 11 +- pallets/funding/src/lib.rs | 189 +++-- pallets/funding/src/mock.rs | 22 +- pallets/funding/src/runtime_api.rs | 76 +- pallets/funding/src/storage_migrations.rs | 142 +--- pallets/funding/src/tests/1_application.rs | 4 +- pallets/funding/src/tests/2_evaluation.rs | 57 -- pallets/funding/src/tests/3_auction.rs | 659 +++++++++--------- pallets/funding/src/tests/5_funding_end.rs | 71 +- pallets/funding/src/tests/6_settlement.rs | 112 +-- pallets/funding/src/tests/7_ct_migration.rs | 12 +- pallets/funding/src/tests/misc.rs | 33 +- pallets/funding/src/tests/mod.rs | 21 +- pallets/funding/src/tests/runtime_api.rs | 126 ++-- pallets/funding/src/types.rs | 24 +- pallets/funding/src/weights.rs | 151 ++-- pallets/oracle-ocw/src/tests.rs | 1 - runtimes/polimec/src/lib.rs | 24 +- .../polimec/src/weights/pallet_funding.rs | 75 +- 34 files changed, 1307 insertions(+), 1711 deletions(-) diff --git a/integration-tests/src/tests/ct_migration.rs b/integration-tests/src/tests/ct_migration.rs index b5fac0c73..7ffea6b54 100644 --- a/integration-tests/src/tests/ct_migration.rs +++ b/integration-tests/src/tests/ct_migration.rs @@ -70,7 +70,7 @@ fn get_migrations_for_participants( for participant in participants { let (status, migrations) = pallet_funding::UserMigrations::::get((project_id, participant.clone())).unwrap(); - user_migrations.insert(participant, (status, Migrations::from(migrations.into()))); + user_migrations.insert(participant, (status, Migrations::from(migrations.to_vec()))); } }); user_migrations @@ -159,8 +159,8 @@ fn create_settled_project() -> (ProjectId, Vec) { let mut inst = IntegrationInstantiator::new(None); let project_metadata = default_project_metadata(ISSUER.into()); - let evaluations = inst.generate_successful_evaluations(project_metadata.clone(), 5); - let bids = inst.generate_bids_from_total_ct_percent(project_metadata.clone(), 95, 8); + let evaluations = inst.generate_successful_evaluations(project_metadata.clone(), 10); + let bids = inst.generate_bids_from_total_ct_percent(project_metadata.clone(), 95, 30); PolimecNet::execute_with(|| { let project_id = inst.create_finished_project(project_metadata, ISSUER.into(), None, evaluations, bids); assert_eq!( @@ -171,10 +171,6 @@ fn create_settled_project() -> (ProjectId, Vec) { pallet_funding::Evaluations::::iter_prefix_values((project_id,)) .map(|eval| eval.evaluator) .chain(pallet_funding::Bids::::iter_prefix_values((project_id,)).map(|bid| bid.bidder)) - .chain( - pallet_funding::Contributions::::iter_prefix_values((project_id,)) - .map(|contribution| contribution.contributor), - ) .collect(); participants.sort(); participants.dedup(); @@ -211,8 +207,8 @@ fn create_project_with_unsettled_participation(participation_type: Participation let mut inst = IntegrationInstantiator::new(None); PolimecNet::execute_with(|| { let project_metadata = default_project_metadata(ISSUER.into()); - let evaluations = inst.generate_successful_evaluations(project_metadata.clone(), 5); - let bids = inst.generate_bids_from_total_ct_percent(project_metadata.clone(), 95, 8); + let evaluations = inst.generate_successful_evaluations(project_metadata.clone(), 10); + let bids = inst.generate_bids_from_total_ct_percent(project_metadata.clone(), 95, 30); let project_id = inst.create_finished_project(project_metadata, ISSUER.into(), None, evaluations, bids); assert_eq!( @@ -221,12 +217,12 @@ fn create_project_with_unsettled_participation(participation_type: Participation ); let evaluations_to_settle = pallet_funding::Evaluations::::iter_prefix_values((project_id,)).collect_vec(); - let bids_to_settle = pallet_funding::Bids::::iter_prefix_values((project_id,)).collect_vec(); + let bids_to_settle = inst.get_bids(project_id); let mut participants: Vec = evaluations_to_settle .iter() .map(|eval| eval.evaluator.clone()) - .chain(bids_to_settle.iter().map(|bid| bid.bidder.clone())) + .chain(bids_to_settle.iter().map(|x| x.bidder.clone())) .collect(); participants.sort(); participants.dedup(); @@ -242,9 +238,10 @@ fn create_project_with_unsettled_participation(participation_type: Participation .unwrap() } - let start = if participation_type == ParticipationType::Bid { 1 } else { 0 }; - for bid in bids_to_settle[start..].iter() { - PolimecFunding::settle_bid(RuntimeOrigin::signed(alice()), project_id, bid.bidder.clone(), bid.id).unwrap() + let proposed_start = if participation_type == ParticipationType::Bid { 1 } else { 0 }; + let end = if proposed_start == 1 { bids_to_settle.len() - 1 } else { bids_to_settle.len() }; + for bid in bids_to_settle[..end].iter() { + PolimecFunding::settle_bid(RuntimeOrigin::signed(alice()), project_id, bid.id).unwrap() } let evaluations = diff --git a/integration-tests/src/tests/defaults.rs b/integration-tests/src/tests/defaults.rs index 3ad2b3037..362e2c2ae 100644 --- a/integration-tests/src/tests/defaults.rs +++ b/integration-tests/src/tests/defaults.rs @@ -68,7 +68,7 @@ pub fn default_project_metadata(issuer: AccountId) -> ProjectMetadataOf::get((project_id, user.clone(), participation_id)); - // dbg!(&stored_bid); - assert!(stored_bid.is_some()); } let post_participation_free_plmc = PolimecBalances::free_balance(user.clone()); @@ -392,14 +388,13 @@ fn e2e_test() { assert_eq!(inst.go_to_next_state(project_id), ProjectStatus::AuctionRound); - for (bid_id, user, mode, investor_type, ct_amount, _price, funding_asset, funding_asset_ticket, plmc_bonded) in + for (_bid_id, user, mode, investor_type, ct_amount, _price, funding_asset, funding_asset_ticket, plmc_bonded) in pre_wap_bids() { inst = participate_with_checks( inst, project_id, ParticipationType::Bid, - bid_id, user, mode, investor_type, @@ -425,13 +420,8 @@ fn e2e_test() { let prev_treasury_usdt_balance = PolimecForeignAssets::balance(USDT.id(), otm_project_sub_account.clone()); let prev_escrow_usdt_balance = PolimecForeignAssets::balance(USDT.id(), funding_escrow_account.clone()); - PolimecFunding::settle_bid( - PolimecOrigin::signed(rejected_bidder.clone()), - project_id, - rejected_bidder.clone(), - rejected_bid_id, - ) - .unwrap(); + PolimecFunding::settle_bid(PolimecOrigin::signed(rejected_bidder.clone()), project_id, rejected_bid_id) + .unwrap(); let post_bid_free_plmc = PolimecBalances::free_balance(rejected_bidder.clone()); let post_bid_reserved_plmc = PolimecBalances::reserved_balance(rejected_bidder.clone()); diff --git a/integration-tests/src/tests/oracle.rs b/integration-tests/src/tests/oracle.rs index c4564bb37..42c3a5485 100644 --- a/integration-tests/src/tests/oracle.rs +++ b/integration-tests/src/tests/oracle.rs @@ -131,8 +131,8 @@ fn pallet_funding_works() { assert_ok!(Oracle::feed_values(RuntimeOrigin::signed(charlie.clone()), values([4.84, 1.0, 1.0, 2500.0, 0.4]))); let project_metadata = default_project_metadata(ISSUER.into()); - let evaluations = inst.generate_successful_evaluations(project_metadata.clone(), 5); - let bids = inst.generate_bids_from_total_ct_percent(project_metadata.clone(), 95, 8); + let evaluations = inst.generate_successful_evaluations(project_metadata.clone(), 10); + let bids = inst.generate_bids_from_total_ct_percent(project_metadata.clone(), 95, 30); let _project_id = inst.create_finished_project(project_metadata, ISSUER.into(), None, evaluations, bids); }); } diff --git a/integration-tests/src/tests/runtime_apis.rs b/integration-tests/src/tests/runtime_apis.rs index 5093ac46f..7ec0e708a 100644 --- a/integration-tests/src/tests/runtime_apis.rs +++ b/integration-tests/src/tests/runtime_apis.rs @@ -1,8 +1,7 @@ use crate::{constants::*, *}; use assets_common::runtime_api::runtime_decl_for_fungibles_api::FungiblesApiV2; -use frame_support::traits::{fungible::Mutate as FMutate, fungibles::Mutate, tokens::ConversionToAssetBalance}; +use frame_support::traits::{fungible::Mutate as FMutate, fungibles::Mutate}; use polimec_common::assets::AcceptedFundingAsset; -use polimec_runtime::PLMCToAssetBalance; use sp_arithmetic::FixedU128; use xcm::v4::Junctions::X3; use xcm_fee_payment_runtime_api::fees::runtime_decl_for_xcm_payment_api::XcmPaymentApiV1; diff --git a/justfile b/justfile index 8ba10b791..babf2428e 100644 --- a/justfile +++ b/justfile @@ -99,8 +99,8 @@ benchmark-pallet chain="polimec-paseo-local" pallet="pallet-dispenser": benchmark-extrinsics pallet="pallet-funding" extrinsics="*" : cargo run --features runtime-benchmarks --profile=production -p polimec-node benchmark pallet \ --chain=polimec-paseo-local \ - --steps=10 \ - --repeat=5 \ + --steps=50 \ + --repeat=20 \ --pallet={{ pallet }} \ --no-storage-info \ --no-median-slopes \ diff --git a/pallets/funding/src/benchmarking.rs b/pallets/funding/src/benchmarking.rs index 728ce9d0d..408187afd 100644 --- a/pallets/funding/src/benchmarking.rs +++ b/pallets/funding/src/benchmarking.rs @@ -21,7 +21,6 @@ use super::*; use crate::{instantiator::*, traits::SetPrices}; use polimec_common::assets::AcceptedFundingAsset; -use ParticipationMode::{Classic, OTM}; use frame_benchmarking::v2::*; use frame_support::{ @@ -84,90 +83,6 @@ where } } -pub fn default_evaluations() -> Vec> -where - ::Price: From, - T::Hash: From, -{ - let threshold = ::EvaluationSuccessThreshold::get(); - let default_project_metadata: ProjectMetadataOf = - default_project_metadata::(account::>("issuer", 0, 0)); - let funding_target = - default_project_metadata.minimum_price.saturating_mul_int(default_project_metadata.total_allocation_size); - let evaluation_target = threshold * funding_target; - - vec![ - EvaluationParams::from(( - account::>("evaluator_1", 0, 0), - Percent::from_percent(35) * evaluation_target, - )), - EvaluationParams::from(( - account::>("evaluator_2", 0, 0), - Percent::from_percent(35) * evaluation_target, - )), - EvaluationParams::from(( - account::>("evaluator_3", 0, 0), - Percent::from_percent(35) * evaluation_target, - )), - ] -} - -pub fn default_bids() -> Vec> -where - ::Price: From, - T::Hash: From, -{ - let default_project_metadata = default_project_metadata::(account::>("issuer", 0, 0)); - let auction_funding_target = - default_project_metadata.minimum_price.saturating_mul_int(default_project_metadata.total_allocation_size); - let inst = BenchInstantiator::::new(None); - - inst.generate_bids_from_total_usd( - default_project_metadata.clone(), - Percent::from_percent(95) * auction_funding_target, - 5, - ) -} - -pub fn full_bids() -> Vec> -where - T: Config, - ::Price: From, - T::Hash: From, -{ - let inst = BenchInstantiator::::new(None); - let default_project = default_project_metadata::(account::>("issuer", 0, 0)); - let total_ct_for_bids = default_project.total_allocation_size; - let total_usd_for_bids = default_project.minimum_price.checked_mul_int(total_ct_for_bids).unwrap(); - inst.generate_bids_from_total_usd(default_project.clone(), total_usd_for_bids, 5) -} - -pub fn default_weights() -> Vec { - vec![20u8, 15u8, 10u8, 25u8, 30u8] -} - -pub fn default_evaluators() -> Vec> { - vec![ - account::>("evaluator_1", 0, 0), - account::>("evaluator_2", 0, 0), - account::>("evaluator_3", 0, 0), - account::>("evaluator_4", 0, 0), - account::>("evaluator_5", 0, 0), - ] -} -pub fn default_bidders() -> Vec> { - vec![ - account::>("bidder_1", 0, 0), - account::>("bidder_2", 0, 0), - account::>("bidder_3", 0, 0), - account::>("bidder_4", 0, 0), - account::>("bidder_5", 0, 0), - ] -} - -pub fn default_bidder_modes() -> Vec { - vec![Classic(10u8), Classic(3u8), OTM, OTM, Classic(4u8)] -} /// Grab an account, seeded by a name and index. pub fn string_account( name: scale_info::prelude::string::String, @@ -314,7 +229,7 @@ mod benchmarks { bidding_ticket_sizes: BiddingTicketSizes { professional: TicketSize::new(5000 * USD_UNIT, Some(10_000 * USD_UNIT)), institutional: TicketSize::new(5000 * USD_UNIT, Some(10_000 * USD_UNIT)), - retail: TicketSize::new(10 * USD_UNIT, Some(10_000 * USD_UNIT)), + retail: TicketSize::new(100 * USD_UNIT, Some(10_000 * USD_UNIT)), phantom: Default::default(), }, participation_currencies: vec![AcceptedFundingAsset::USDT, AcceptedFundingAsset::USDC].try_into().unwrap(), @@ -393,15 +308,13 @@ mod benchmarks { } #[benchmark] - fn evaluate( - // How many other evaluations the user did for that same project - x: Linear<0, { T::MaxEvaluationsPerUser::get() - 1 }>, - ) { + fn evaluate() { // setup let mut inst = BenchInstantiator::::new(None); ::SetPrices::set_prices(); - // We can't see events at block 0 inst.advance_time(1u32.into()); + // We can't see events at block 0 + inst.advance_time(1u32.into()); let issuer = account::>("issuer", 0, 0); let test_evaluator = account::>("evaluator", 0, 0); @@ -410,25 +323,16 @@ mod benchmarks { let project_metadata = default_project_metadata::(issuer.clone()); let project_id = inst.create_evaluating_project(project_metadata.clone(), issuer, None); - let existing_evaluation = EvaluationParams::from((test_evaluator.clone(), (200 * USD_UNIT).into())); let extrinsic_evaluation = EvaluationParams::from((test_evaluator.clone(), (1_000 * USD_UNIT).into())); - let existing_evaluations = vec![existing_evaluation; x as usize]; - let plmc_for_existing_evaluations = inst.calculate_evaluation_plmc_spent(existing_evaluations.clone()); let plmc_for_extrinsic_evaluation = inst.calculate_evaluation_plmc_spent(vec![extrinsic_evaluation.clone()]); let existential_plmc: Vec> = plmc_for_extrinsic_evaluation.accounts().existential_deposits(); inst.mint_plmc_to(existential_plmc); - inst.mint_plmc_to(plmc_for_existing_evaluations.clone()); inst.mint_plmc_to(plmc_for_extrinsic_evaluation.clone()); - // do "x" evaluations for this user - inst.evaluate_for_users(project_id, existing_evaluations).expect("All evaluations are accepted"); - let extrinsic_plmc_bonded = plmc_for_extrinsic_evaluation[0].plmc_amount; - let total_expected_plmc_bonded = inst - .sum_balance_mappings(vec![plmc_for_existing_evaluations.clone(), plmc_for_extrinsic_evaluation.clone()]); let jwt = get_mock_jwt_with_cid( extrinsic_evaluation.account.clone(), @@ -468,7 +372,7 @@ mod benchmarks { let bonded_plmc = inst .get_reserved_plmc_balances_for(vec![extrinsic_evaluation.account.clone()], HoldReason::Evaluation.into())[0] .plmc_amount; - assert_eq!(bonded_plmc, total_expected_plmc_bonded); + assert_eq!(bonded_plmc, extrinsic_plmc_bonded); // Events frame_system::Pallet::::assert_last_event( @@ -544,10 +448,8 @@ mod benchmarks { #[benchmark] fn bid( - // amount of already made bids by the same user. Leave 10 bids available to make the extrinsic pass in case y = max (10) - x: Linear<0, { T::MaxBidsPerUser::get() - 10 }>, - // amount of times when `perform_bid` is called (i.e. into how many buckets the bid is spread) - y: Linear<0, 10>, + // Amount of buckets this bid is split into + x: Linear<1, 10>, ) { // * setup * let mut inst = BenchInstantiator::::new(None); @@ -560,97 +462,32 @@ mod benchmarks { let bidder = account::>("bidder", 0, 0); whitelist_account!(bidder); - let mut project_metadata = default_project_metadata::(issuer.clone()); - project_metadata.mainnet_token_max_supply = 100_000 * CT_UNIT; - project_metadata.total_allocation_size = 100_000 * CT_UNIT; - project_metadata.minimum_price = PriceProviderOf::::calculate_decimals_aware_price( - PriceOf::::checked_from_rational(100, 1).unwrap(), - USD_DECIMALS, - CT_DECIMALS, - ) - .unwrap(); - + let project_metadata = default_project_metadata::(issuer.clone()); let evaluations = inst.generate_successful_evaluations(project_metadata.clone(), 5); - let project_id = inst.create_auctioning_project(project_metadata.clone(), issuer, None, evaluations); - let existing_bid = BidParams::from(( - bidder.clone(), - Institutional, - (50 * CT_UNIT).into(), - ParticipationMode::Classic(5u8), - AcceptedFundingAsset::USDT, - )); - - let existing_bids = vec![existing_bid; x as usize]; - let existing_bids_post_bucketing = - inst.get_actual_price_charged_for_bucketed_bids(&existing_bids, project_metadata.clone(), None); - let plmc_for_existing_bids = inst.calculate_auction_plmc_charged_from_all_bids_made_or_with_bucket( - &existing_bids, + let first_bucket_bid = inst.generate_bids_from_total_ct_percent(project_metadata.clone(), 100, 1); + let first_bucket_bid_plmc = inst.calculate_auction_plmc_charged_from_all_bids_made_or_with_bucket( + &first_bucket_bid, project_metadata.clone(), None, ); - - let usdt_for_existing_bids: Vec> = inst + let first_bucket_bid_funding_asset = inst .calculate_auction_funding_asset_charged_from_all_bids_made_or_with_bucket( - &existing_bids, + &first_bucket_bid, project_metadata.clone(), None, ); - let escrow_account = Pallet::::fund_account_id(project_id); - let prev_total_escrow_usdt_locked = - inst.get_free_funding_asset_balances_for(vec![(escrow_account.clone(), usdt_id())]); - - inst.mint_plmc_ed_if_required(plmc_for_existing_bids.accounts()); - inst.mint_plmc_to(plmc_for_existing_bids.clone()); - inst.mint_funding_asset_ed_if_required(usdt_for_existing_bids.to_account_asset_map()); - inst.mint_funding_asset_to(usdt_for_existing_bids.clone()); - - // do "x" contributions for this user - inst.bid_for_users(project_id, existing_bids.clone()).unwrap(); - - // to call do_perform_bid several times, we need the bucket to reach its limit. You can only bid over 10 buckets - // in a single bid, since the increase delta is 10% of the total allocation, and you cannot bid more than the allocation. - let mut ct_amount = (50 * CT_UNIT).into(); - let mut maybe_filler_bid = None; - let new_bidder = account::>("new_bidder", 0, 0); - - let mut usdt_for_filler_bidder = - vec![UserToFundingAsset::::new(new_bidder.clone(), Zero::zero(), AcceptedFundingAsset::USDT.id())]; - if y > 0 { - let current_bucket = Buckets::::get(project_id).unwrap(); - // first lets bring the bucket to almost its limit with another bidder: - assert!(new_bidder.clone() != bidder.clone()); - let bid_params = BidParams::from(( - new_bidder.clone(), - Institutional, - current_bucket.amount_left, - ParticipationMode::Classic(1u8), - AcceptedFundingAsset::USDT, - )); - maybe_filler_bid = Some(bid_params.clone()); - let plmc_for_new_bidder = inst.calculate_auction_plmc_charged_with_given_price( - &vec![bid_params.clone()], - current_bucket.current_price, - ); - let usdt_for_new_bidder = inst.calculate_auction_funding_asset_charged_with_given_price( - &vec![bid_params.clone()], - current_bucket.current_price, - ); - - inst.mint_plmc_ed_if_required(vec![(new_bidder.clone())]); - inst.mint_plmc_to(plmc_for_new_bidder); + inst.mint_plmc_ed_if_required(first_bucket_bid_plmc.accounts()); + inst.mint_plmc_to(first_bucket_bid_plmc.clone()); + inst.mint_funding_asset_ed_if_required(first_bucket_bid_funding_asset.to_account_asset_map()); + inst.mint_funding_asset_to(first_bucket_bid_funding_asset.clone()); + inst.bid_for_users(project_id, first_bucket_bid).unwrap(); - inst.mint_funding_asset_ed_if_required(vec![(new_bidder, AcceptedFundingAsset::USDT.id())]); - inst.mint_funding_asset_to(usdt_for_new_bidder.clone()); + let current_bucket = Buckets::::get(project_id).unwrap(); - inst.bid_for_users(project_id, vec![bid_params]).unwrap(); + let ct_amount = (Percent::from_percent(10) * project_metadata.total_allocation_size) * x as u128; - let auction_allocation = project_metadata.total_allocation_size; - let bucket_size = Percent::from_percent(10) * auction_allocation; - ct_amount = bucket_size * (y as u128); - usdt_for_filler_bidder = usdt_for_new_bidder; - } let extrinsic_bid = BidParams::from(( bidder.clone(), Institutional, @@ -658,64 +495,50 @@ mod benchmarks { ParticipationMode::Classic(1u8), AcceptedFundingAsset::USDT, )); - let original_extrinsic_bid = extrinsic_bid.clone(); - let current_bucket = Buckets::::get(project_id).unwrap(); - // we need to call this after bidding `x` amount of times, to get the latest bucket from storage - let extrinsic_bids_post_bucketing = inst.get_actual_price_charged_for_bucketed_bids( + + let extrinsic_bid_plmc = inst.calculate_auction_plmc_charged_from_all_bids_made_or_with_bucket( &vec![extrinsic_bid.clone()], project_metadata.clone(), Some(current_bucket), ); - - assert_eq!(extrinsic_bids_post_bucketing.len(), (y as usize).max(1usize)); - - let plmc_for_extrinsic_bids: Vec> = inst - .calculate_auction_plmc_charged_from_all_bids_made_or_with_bucket( - &vec![extrinsic_bid.clone()], - project_metadata.clone(), - Some(current_bucket), - ); - let usdt_for_extrinsic_bids: Vec> = inst + let extrinsic_bid_funding_asset = inst .calculate_auction_funding_asset_charged_from_all_bids_made_or_with_bucket( - &vec![extrinsic_bid], + &vec![extrinsic_bid.clone()], project_metadata.clone(), Some(current_bucket), ); - inst.mint_plmc_ed_if_required(plmc_for_extrinsic_bids.accounts()); - inst.mint_plmc_to(plmc_for_extrinsic_bids.clone()); - inst.mint_funding_asset_ed_if_required(usdt_for_extrinsic_bids.to_account_asset_map()); - inst.mint_funding_asset_to(usdt_for_extrinsic_bids.clone()); - - let total_free_plmc = inst.get_ed(); - let total_plmc_participation_bonded = - inst.sum_balance_mappings(vec![plmc_for_extrinsic_bids.clone(), plmc_for_existing_bids.clone()]); - let total_free_usdt = inst.get_funding_asset_ed(AcceptedFundingAsset::USDT.id()); - let total_escrow_usdt_locked = inst.sum_funding_asset_mappings(vec![ - prev_total_escrow_usdt_locked.clone(), - usdt_for_extrinsic_bids.clone(), - usdt_for_existing_bids.clone(), - usdt_for_filler_bidder.clone(), - ])[0] - .1; + + inst.mint_plmc_ed_if_required(extrinsic_bid_plmc.accounts()); + inst.mint_plmc_to(extrinsic_bid_plmc.clone()); + inst.mint_funding_asset_ed_if_required(extrinsic_bid_funding_asset.to_account_asset_map()); + inst.mint_funding_asset_to(extrinsic_bid_funding_asset.clone()); + + let extrinsic_bids_post_bucketing = inst.get_actual_price_charged_for_bucketed_bids( + &vec![extrinsic_bid.clone()], + project_metadata.clone(), + Some(current_bucket), + ); let jwt = get_mock_jwt_with_cid( - original_extrinsic_bid.bidder.clone(), + bidder.clone(), InvestorType::Institutional, - generate_did_from_account(original_extrinsic_bid.bidder.clone()), + generate_did_from_account(bidder.clone()), project_metadata.clone().policy_ipfs_cid.unwrap(), ); #[extrinsic_call] bid( - RawOrigin::Signed(original_extrinsic_bid.bidder.clone()), + RawOrigin::Signed(bidder.clone()), jwt, project_id, - original_extrinsic_bid.amount, - original_extrinsic_bid.mode, - original_extrinsic_bid.asset, + extrinsic_bid.amount, + extrinsic_bid.mode, + extrinsic_bid.asset, ); // * validity checks * + let bids_count = Bids::::iter_prefix_values((project_id,)).collect_vec().len(); + assert_eq!(bids_count, extrinsic_bids_post_bucketing.len() + 1); // Storage for (bid_params, price) in extrinsic_bids_post_bucketing.clone() { @@ -732,66 +555,67 @@ mod benchmarks { plmc_bond: None, when: None, }; - Bids::::iter_prefix_values((project_id, bidder.clone())) + Bids::::iter_prefix_values((project_id,)) .find(|stored_bid| bid_filter.matches_bid(stored_bid)) .expect("bid not found"); } + } - // Bucket Storage Check - let bucket_delta_amount = Percent::from_percent(10) * project_metadata.total_allocation_size; - let ten_percent_in_price: ::Price = - PriceOf::::checked_from_rational(1, 10).unwrap() * project_metadata.minimum_price; + // We benchmark the worst case, which is a new cutoff being calculated. + // This doesn't happen when the first bid we read is partially accepted instead of rejected. + #[benchmark] + fn process_next_oversubscribed_bid() { + // * setup * + let mut inst = BenchInstantiator::::new(None); + ::SetPrices::set_prices(); - let mut expected_bucket = Bucket::new( - project_metadata.total_allocation_size, - project_metadata.minimum_price, - ten_percent_in_price, - bucket_delta_amount, - ); + // We can't see events at block 0 + inst.advance_time(1u32.into()); - for (bid_params, _price_) in existing_bids_post_bucketing.clone() { - expected_bucket.update(bid_params.amount); - } - if let Some(bid_params) = maybe_filler_bid { - expected_bucket.update(bid_params.amount); - } - for (bid_params, _price_) in extrinsic_bids_post_bucketing.clone() { - expected_bucket.update(bid_params.amount); - } + let issuer = account::>("issuer", 0, 0); + let bidder = account::>("bidder", 0, 0); + whitelist_account!(bidder); - let current_bucket = Buckets::::get(project_id).unwrap(); - assert_eq!(current_bucket, expected_bucket); + let project_metadata = default_project_metadata::(issuer.clone()); + let evaluations = inst.generate_successful_evaluations(project_metadata.clone(), 5); + let first_bucket_bids = inst.generate_bids_from_total_ct_percent(project_metadata.clone(), 100, 10); + let project_id = inst.create_auctioning_project(project_metadata.clone(), issuer, None, evaluations); - // Balances - let bonded_plmc = - inst.get_reserved_plmc_balances_for(vec![bidder.clone()], HoldReason::Participation.into())[0].plmc_amount; - assert_eq!(bonded_plmc, total_plmc_participation_bonded); + inst.mint_necessary_tokens_for_bids(project_id, first_bucket_bids.clone()); + inst.bid_for_users(project_id, first_bucket_bids.clone()).unwrap(); - let free_plmc = inst.get_free_plmc_balances_for(vec![bidder.clone()])[0].plmc_amount; - assert_eq!(free_plmc, total_free_plmc); + let oversubscribing_bid_amount = first_bucket_bids[9].amount; + let oversubscribing_bid = BidParams::from(( + bidder.clone(), + Institutional, + oversubscribing_bid_amount, + ParticipationMode::Classic(1u8), + AcceptedFundingAsset::USDT, + )); + inst.mint_necessary_tokens_for_bids(project_id, vec![oversubscribing_bid.clone()]); + inst.bid_for_users(project_id, vec![oversubscribing_bid.clone()]).unwrap(); - let escrow_account = Pallet::::fund_account_id(project_id); - let locked_usdt = inst.get_free_funding_asset_balance_for(usdt_id(), escrow_account.clone()); - assert_eq!(locked_usdt, total_escrow_usdt_locked); + Pallet::::do_process_next_oversubscribed_bid(project_id).unwrap(); + let pre_cutoff = OutbidBidsCutoff::::get(project_id).unwrap(); - let free_usdt = inst.get_free_funding_asset_balance_for(usdt_id(), bidder); - assert_eq!(free_usdt, total_free_usdt); + inst.mint_necessary_tokens_for_bids(project_id, vec![oversubscribing_bid.clone()]); + inst.bid_for_users(project_id, vec![oversubscribing_bid.clone()]).unwrap(); - // Events - for (bid_params, _price_) in extrinsic_bids_post_bucketing { - let maybe_event = find_event! { - T, - Event::::Bid { - project_id, - ct_amount, - mode, .. - }, - project_id == project_id, - ct_amount == bid_params.amount, - mode == bid_params.mode - }; - assert!(maybe_event.is_some(), "Event not found"); - } + #[extrinsic_call] + process_next_oversubscribed_bid(RawOrigin::Signed(bidder), project_id); + + // * validity checks * + let oversubscribed_amount = CTAmountOversubscribed::::get(project_id); + assert!(oversubscribed_amount.is_zero()); + + let rejected_bid = Bids::::get((project_id, 9)).unwrap(); + assert_eq!(rejected_bid.status, BidStatus::Rejected); + + let rejected_bid = Bids::::get((project_id, 8)).unwrap(); + assert_eq!(rejected_bid.status, BidStatus::Rejected); + + let post_cutoff = OutbidBidsCutoff::::get(project_id).unwrap(); + assert_ne!(pre_cutoff, post_cutoff); } // end_funding has 2 logic paths: @@ -868,13 +692,10 @@ mod benchmarks { let issuer = account::>("issuer", 0, 0); whitelist_account!(anyone); - let project_id = inst.create_finished_project( - default_project_metadata::(issuer.clone()), - issuer, - None, - default_evaluations::(), - default_bids::(), - ); + let project_metadata = default_project_metadata::(issuer.clone()); + let evaluations = inst.generate_successful_evaluations(project_metadata.clone(), 10); + let bids = inst.generate_bids_from_total_ct_percent(project_metadata.clone(), 95, 30); + let project_id = inst.create_finished_project(project_metadata, issuer, None, evaluations, bids); #[extrinsic_call] start_settlement(RawOrigin::Signed(anyone), project_id); @@ -900,16 +721,19 @@ mod benchmarks { inst.advance_time(1u32.into()); let issuer = account::>("issuer", 0, 0); - let evaluations: Vec> = default_evaluations::(); + let project_metadata = default_project_metadata::(issuer.clone()); + let evaluations = inst.generate_successful_evaluations(project_metadata.clone(), 10); let evaluator: AccountIdOf = evaluations[0].account.clone(); whitelist_account!(evaluator); + let bids = inst.generate_bids_from_total_ct_percent(project_metadata.clone(), 95, 30); + let project_id = inst.create_finished_project( default_project_metadata::(issuer.clone()), issuer, None, evaluations, - default_bids::(), + bids, ); let evaluation_to_settle = @@ -961,53 +785,41 @@ mod benchmarks { inst.advance_time(1u32.into()); let issuer = account::>("issuer", 0, 0); - let mut bidder_accounts = default_bidders::().into_iter(); let project_metadata = default_project_metadata::(issuer.clone()); - // let target_wap = project_metadata.minimum_price + project_metadata.minimum_price * >::saturating_from_rational(1, 10); - let mut target_bucket = >::create_bucket_from_metadata(&project_metadata.clone()).unwrap(); - target_bucket.update(target_bucket.amount_left); - target_bucket.update(target_bucket.amount_left); - - let bids = inst.generate_bids_from_bucket( - project_metadata.clone(), - target_bucket, - bidder_accounts.next().unwrap(), - |_| bidder_accounts.next().unwrap(), - AcceptedFundingAsset::USDT, - ); + let increase = project_metadata.minimum_price * PriceOf::::saturating_from_rational(5, 10); + let target_wap = project_metadata.minimum_price + increase; - let project_id = inst.create_finished_project( - project_metadata.clone(), - issuer, - None, - default_evaluations::(), - bids.clone(), - ); + let evaluations = inst.generate_successful_evaluations(project_metadata.clone(), 10); + let bids = inst.generate_bids_that_take_price_to(project_metadata.clone(), target_wap); - let wap = inst.get_project_details(project_id).weighted_average_price.unwrap(); - - let bidder = bids.last().unwrap().bidder.clone(); - whitelist_account!(bidder); + let project_id = + inst.create_finished_project(project_metadata.clone(), issuer, None, evaluations, bids.clone()); assert_ok!(>::do_start_settlement(project_id)); - let bid_to_settle = - inst.execute(|| Bids::::iter_prefix_values((project_id, bidder.clone())).next().unwrap()); + let bid_to_settle = inst.execute(|| { + let mut bids_iter = Bids::::iter_prefix_values((project_id,)); + bids_iter.find(|b| matches!(b.status, BidStatus::PartiallyAccepted(_))).unwrap() + }); - // Make sure a refund has to happen - assert!(bid_to_settle.original_ct_usd_price > wap); + let bidder = bid_to_settle.bidder.clone(); + whitelist_account!(bidder); + + let BidStatus::PartiallyAccepted(expected_ct_amount) = bid_to_settle.status else { + unreachable!(); + }; #[extrinsic_call] - settle_bid(RawOrigin::Signed(bidder.clone()), project_id, bidder.clone(), bid_to_settle.id); + settle_bid(RawOrigin::Signed(bidder.clone()), project_id, bid_to_settle.id); // * validity checks * // Storage - assert!(Bids::::get((project_id, bidder.clone(), bid_to_settle.id)).is_none()); + assert!(Bids::::get((project_id, bid_to_settle.id)).is_none()); // Balances let ct_amount = inst.get_ct_asset_balances_for(project_id, vec![bidder.clone()])[0]; - assert_eq!(bid_to_settle.original_ct_amount, ct_amount); + assert_eq!(expected_ct_amount, ct_amount); // Events frame_system::Pallet::::assert_last_event( @@ -1015,8 +827,9 @@ mod benchmarks { project_id, account: bidder.clone(), id: bid_to_settle.id, - final_ct_amount: bid_to_settle.original_ct_amount, - final_ct_usd_price: wap, + status: bid_to_settle.status, + final_ct_amount: expected_ct_amount, + final_ct_usd_price: bid_to_settle.original_ct_usd_price, } .into(), ); @@ -1033,14 +846,11 @@ mod benchmarks { whitelist_account!(anyone); let project_metadata = default_project_metadata::(issuer.clone()); - let project_id = inst.create_settled_project( - project_metadata.clone(), - issuer.clone(), - None, - default_evaluations::(), - default_bids::(), - false, - ); + let evaluations = inst.generate_successful_evaluations(project_metadata.clone(), 10); + let bids = inst.generate_bids_from_total_ct_percent(project_metadata.clone(), 95, 30); + + let project_id = + inst.create_settled_project(project_metadata.clone(), issuer.clone(), None, evaluations, bids, false); #[extrinsic_call] mark_project_as_settled(RawOrigin::Signed(anyone), project_id); @@ -1059,8 +869,8 @@ mod benchmarks { let issuer = account::>("issuer", 0, 0); let project_metadata = default_project_metadata::(issuer.clone()); - let evaluations = inst.generate_successful_evaluations(project_metadata.clone(), 1); - let bids = inst.generate_bids_from_total_ct_percent(project_metadata.clone(), 34, 1); + let evaluations = inst.generate_successful_evaluations(project_metadata.clone(), 10); + let bids = inst.generate_bids_from_total_ct_percent(project_metadata.clone(), 34, 30); let project_id = inst.create_settled_project(project_metadata.clone(), issuer.clone(), None, evaluations, bids, true); @@ -1077,13 +887,13 @@ mod benchmarks { // * validity checks * let project_details = inst.get_project_details(project_id); assert_eq!(project_details.status, ProjectStatus::CTMigrationStarted); - assert_eq!(UnmigratedCounter::::get(project_id), 2); + assert_eq!(UnmigratedCounter::::get(project_id), 40); } #[benchmark] fn confirm_offchain_migration( // Amount of migrations to confirm for a single user - x: Linear<1, { <::MaxBidsPerUser>::get() }>, + x: Linear<1, 100>, ) { // setup let mut inst = BenchInstantiator::::new(None); @@ -1092,6 +902,11 @@ mod benchmarks { let issuer = account::>("issuer", 0, 0); let participant = account::>("test_participant", 0, 0); + let project_metadata = default_project_metadata::(issuer.clone()); + + let evaluations = inst.generate_successful_evaluations(project_metadata.clone(), 10); + + let mut bids = inst.generate_bids_from_total_ct_percent(project_metadata.clone(), 34, 50); let max_bids = x; let participant_bids = (0..max_bids) .map(|_| { @@ -1104,12 +919,6 @@ mod benchmarks { )) }) .collect_vec(); - - let project_metadata = default_project_metadata::(issuer.clone()); - - let evaluations = inst.generate_successful_evaluations(project_metadata.clone(), 1); - - let mut bids = inst.generate_bids_from_total_ct_percent(project_metadata.clone(), 34, 1); bids.extend(participant_bids); let project_id = @@ -1140,8 +949,8 @@ mod benchmarks { // * validity checks * let project_details = inst.get_project_details(project_id); assert_eq!(project_details.status, ProjectStatus::CTMigrationStarted); - // Evaluations and Bids had 1 each where it was a different account that the one we just confirmed. - assert_eq!(UnmigratedCounter::::get(project_id), 2); + + assert_eq!(UnmigratedCounter::::get(project_id), 10 + 50); } #[benchmark] @@ -1153,14 +962,10 @@ mod benchmarks { let issuer = account::>("issuer", 0, 0); let project_metadata = default_project_metadata::(issuer.clone()); - let project_id = inst.create_settled_project( - project_metadata.clone(), - issuer.clone(), - None, - default_evaluations::(), - default_bids::(), - true, - ); + let evaluations = inst.generate_successful_evaluations(project_metadata.clone(), 10); + let bids = inst.generate_bids_from_total_ct_percent(project_metadata.clone(), 34, 30); + let project_id = + inst.create_settled_project(project_metadata.clone(), issuer.clone(), None, evaluations, bids, true); let jwt = get_mock_jwt_with_cid( issuer.clone(), @@ -1197,14 +1002,10 @@ mod benchmarks { let issuer = account::>("issuer", 0, 0); let project_metadata = default_project_metadata::(issuer.clone()); - let project_id = inst.create_settled_project( - project_metadata.clone(), - issuer.clone(), - None, - default_evaluations::(), - default_bids::(), - true, - ); + let evaluations = inst.generate_successful_evaluations(project_metadata.clone(), 10); + let bids = inst.generate_bids_from_total_ct_percent(project_metadata.clone(), 34, 30); + let project_id = + inst.create_settled_project(project_metadata.clone(), issuer.clone(), None, evaluations, bids, true); let jwt = get_mock_jwt_with_cid( issuer.clone(), @@ -1254,14 +1055,10 @@ mod benchmarks { let issuer = account::>("issuer", 0, 0); let project_metadata = default_project_metadata::(issuer.clone()); - let project_id = inst.create_settled_project( - project_metadata.clone(), - issuer.clone(), - None, - default_evaluations::(), - default_bids::(), - true, - ); + let evaluations = inst.generate_successful_evaluations(project_metadata.clone(), 10); + let bids = inst.generate_bids_from_total_ct_percent(project_metadata.clone(), 34, 30); + let project_id = + inst.create_settled_project(project_metadata.clone(), issuer.clone(), None, evaluations, bids, true); let jwt = get_mock_jwt_with_cid( issuer.clone(), @@ -1328,14 +1125,10 @@ mod benchmarks { let issuer = account::>("issuer", 0, 0); let project_metadata = default_project_metadata::(issuer.clone()); - let project_id = inst.create_settled_project( - project_metadata.clone(), - issuer.clone(), - None, - default_evaluations::(), - default_bids::(), - true, - ); + let evaluations = inst.generate_successful_evaluations(project_metadata.clone(), 10); + let bids = inst.generate_bids_from_total_ct_percent(project_metadata.clone(), 34, 30); + let project_id = + inst.create_settled_project(project_metadata.clone(), issuer.clone(), None, evaluations, bids, true); let jwt = get_mock_jwt_with_cid( issuer.clone(), @@ -1408,7 +1201,7 @@ mod benchmarks { #[benchmark] fn send_pallet_migration_for( // Amount of migrations to confirm for a single user - x: Linear<1, { ::MaxBidsPerUser::get() }>, + x: Linear<1, 100>, ) { // setup let mut inst = BenchInstantiator::::new(None); @@ -1424,7 +1217,7 @@ mod benchmarks { BidParams::from(( participant.clone(), Institutional, - (500 * CT_UNIT).into(), + (50 * CT_UNIT).into(), ParticipationMode::Classic(1u8), AcceptedFundingAsset::USDT, )) @@ -1432,8 +1225,8 @@ mod benchmarks { .collect_vec(); let project_metadata = default_project_metadata::(issuer.clone()); - let evaluations = inst.generate_successful_evaluations(project_metadata.clone(), 5); - let mut bids = inst.generate_bids_from_total_ct_percent(project_metadata.clone(), 34, 1); + let evaluations = inst.generate_successful_evaluations(project_metadata.clone(), 10); + let mut bids = inst.generate_bids_from_total_ct_percent(project_metadata.clone(), 34, 40); bids.extend(participant_bids); let project_id = @@ -1480,9 +1273,9 @@ mod benchmarks { } #[benchmark] - fn confirm_pallet_migrations( + fn confirm_pallet_migration( // Amount of migrations to confirm for a single user - x: Linear<1, { ::MaxBidsPerUser::get() }>, + x: Linear<1, 100>, ) { // setup let mut inst = BenchInstantiator::::new(None); @@ -1498,7 +1291,7 @@ mod benchmarks { BidParams::from(( participant.clone(), Institutional, - (500 * CT_UNIT).into(), + (50 * CT_UNIT).into(), ParticipationMode::Classic(1u8), AcceptedFundingAsset::USDT, )) @@ -1506,9 +1299,10 @@ mod benchmarks { .collect_vec(); let project_metadata = default_project_metadata::(issuer.clone()); - let evaluations = inst.generate_successful_evaluations(project_metadata.clone(), 1); + let evaluations = inst.generate_successful_evaluations(project_metadata.clone(), 10); + + let mut bids = inst.generate_bids_from_total_ct_percent(project_metadata.clone(), 34, 50); - let mut bids = inst.generate_bids_from_total_ct_percent(project_metadata.clone(), 34, 1); bids.extend(participant_bids); let project_id = @@ -1571,14 +1365,10 @@ mod benchmarks { let issuer = account::>("issuer", 0, 0); let project_metadata = default_project_metadata::(issuer.clone()); - let project_id = inst.create_settled_project( - project_metadata.clone(), - issuer.clone(), - None, - default_evaluations::(), - default_bids::(), - true, - ); + let evaluations = inst.generate_successful_evaluations(project_metadata.clone(), 10); + let bids = inst.generate_bids_from_total_ct_percent(project_metadata.clone(), 34, 30); + let project_id = + inst.create_settled_project(project_metadata.clone(), issuer.clone(), None, evaluations, bids, true); let jwt = get_mock_jwt_with_cid( issuer.clone(), @@ -1610,14 +1400,10 @@ mod benchmarks { let issuer = account::>("issuer", 0, 0); let project_metadata = default_project_metadata::(issuer.clone()); - let project_id = inst.create_settled_project( - project_metadata.clone(), - issuer.clone(), - None, - default_evaluations::(), - default_bids::(), - true, - ); + let evaluations = inst.generate_successful_evaluations(project_metadata.clone(), 10); + let bids = inst.generate_bids_from_total_ct_percent(project_metadata.clone(), 34, 30); + let project_id = + inst.create_settled_project(project_metadata.clone(), issuer.clone(), None, evaluations, bids, true); let jwt = get_mock_jwt_with_cid( issuer.clone(), @@ -1651,14 +1437,10 @@ mod benchmarks { let issuer = account::>("issuer", 0, 0); let project_metadata = default_project_metadata::(issuer.clone()); - let project_id = inst.create_settled_project( - project_metadata.clone(), - issuer.clone(), - None, - default_evaluations::(), - default_bids::(), - true, - ); + let evaluations = inst.generate_successful_evaluations(project_metadata.clone(), 10); + let bids = inst.generate_bids_from_total_ct_percent(project_metadata.clone(), 34, 30); + let project_id = + inst.create_settled_project(project_metadata.clone(), issuer.clone(), None, evaluations, bids, true); let jwt = get_mock_jwt_with_cid( issuer.clone(), diff --git a/pallets/funding/src/functions/2_evaluation.rs b/pallets/funding/src/functions/2_evaluation.rs index a859dc519..018854ae5 100644 --- a/pallets/funding/src/functions/2_evaluation.rs +++ b/pallets/funding/src/functions/2_evaluation.rs @@ -45,6 +45,10 @@ impl Pallet { // * Branch in possible project paths * // Successful path return if is_funded { + let mut project_ids = ProjectsInAuctionRound::::get().to_vec(); + project_ids.push(project_id); + let project_ids = WeakBoundedVec::force_from(project_ids, None); + ProjectsInAuctionRound::::put(project_ids); Self::transition_project( project_id, project_details, @@ -76,7 +80,7 @@ impl Pallet { did: Did, whitelisted_policy: Cid, receiving_account: Junction, - ) -> DispatchResultWithPostInfo { + ) -> DispatchResult { // * Get variables * let project_metadata = ProjectsMetadata::::get(project_id).ok_or(Error::::ProjectMetadataNotFound)?; let mut project_details = ProjectsDetails::::get(project_id).ok_or(Error::::ProjectDetailsNotFound)?; @@ -88,8 +92,6 @@ impl Pallet { let early_evaluation_reward_threshold_usd = T::EvaluationSuccessThreshold::get() * project_details.fundraising_target_usd; let evaluation_round_info = &mut project_details.evaluation_round_info; - let total_evaluations_count = EvaluationCounts::::get(project_id); - let user_evaluations_count = Evaluations::::iter_prefix((project_id, evaluator)).count() as u32; let project_policy = project_metadata.policy_ipfs_cid.ok_or(Error::::ImpossibleState)?; let project_metadata = ProjectsMetadata::::get(project_id).ok_or(Error::::ProjectDetailsNotFound)?; @@ -102,8 +104,6 @@ impl Pallet { project_details.round_duration.started(now) && !project_details.round_duration.ended(now), Error::::IncorrectRound ); - ensure!(total_evaluations_count < T::MaxEvaluationsPerProject::get(), Error::::TooManyProjectParticipations); - ensure!(user_evaluations_count < T::MaxEvaluationsPerUser::get(), Error::::TooManyUserParticipations); ensure!( project_metadata.participants_account_type.junction_is_supported(&receiving_account), Error::::UnsupportedReceiverAccountJunction @@ -146,7 +146,6 @@ impl Pallet { evaluation_round_info.total_bonded_usd = evaluation_round_info.total_bonded_usd.saturating_add(usd_amount); evaluation_round_info.total_bonded_plmc = evaluation_round_info.total_bonded_plmc.saturating_add(plmc_bond); ProjectsDetails::::insert(project_id, project_details); - EvaluationCounts::::mutate(project_id, |c| *c = c.saturating_add(1)); // * Emit events * Self::deposit_event(Event::Evaluation { @@ -156,9 +155,6 @@ impl Pallet { plmc_amount: plmc_bond, }); - Ok(PostDispatchInfo { - actual_weight: Some(WeightInfoOf::::evaluate(user_evaluations_count)), - pays_fee: Pays::No, - }) + Ok(()) } } diff --git a/pallets/funding/src/functions/3_auction.rs b/pallets/funding/src/functions/3_auction.rs index 67894453c..8694c60ec 100644 --- a/pallets/funding/src/functions/3_auction.rs +++ b/pallets/funding/src/functions/3_auction.rs @@ -3,7 +3,7 @@ use super::*; impl Pallet { #[transactional] - pub fn do_bid(params: DoBidParams) -> DispatchResultWithPostInfo { + pub fn do_bid(params: DoBidParams) -> DispatchResult { // * Get variables * let DoBidParams { bidder, @@ -23,7 +23,6 @@ impl Pallet { let mut current_bucket = Buckets::::get(project_id).ok_or(Error::::BucketNotFound)?; let now = >::block_number(); let mut amount_to_bid = ct_amount; - let total_bids_for_project = BidCounts::::get(project_id); let project_policy = project_metadata.policy_ipfs_cid.ok_or(Error::::ImpossibleState)?; // User will spend at least this amount of USD for his bid(s). More if the bid gets split into different buckets @@ -32,9 +31,6 @@ impl Pallet { // weight return variables let mut perform_bid_calls = 0; - let existing_bids = Bids::::iter_prefix_values((project_id, bidder.clone())).collect::>(); - let existing_bids_amount = existing_bids.len() as u32; - let metadata_ticket_size_bounds = match investor_type { InvestorType::Institutional => project_metadata.bidding_ticket_sizes.institutional, InvestorType::Professional => project_metadata.bidding_ticket_sizes.professional, @@ -68,7 +64,6 @@ impl Pallet { // Note: We limit the CT Amount to the auction allocation size, to avoid long-running loops. ensure!(ct_amount <= project_metadata.total_allocation_size, Error::::TooHigh); - ensure!(existing_bids.len() < T::MaxBidsPerUser::get() as usize, Error::::TooManyUserParticipations); ensure!( project_metadata.participants_account_type.junction_is_supported(&receiving_account), Error::::UnsupportedReceiverAccountJunction @@ -84,6 +79,7 @@ impl Pallet { current_bucket.amount_left }; let bid_id = NextBidId::::get(); + let auction_oversubscribed = current_bucket.current_price > current_bucket.initial_price; let perform_params = DoPerformBidParams { bidder: bidder.clone(), @@ -96,10 +92,18 @@ impl Pallet { now, did: did.clone(), metadata_ticket_size_bounds, - total_bids_by_bidder: existing_bids_amount.saturating_add(perform_bid_calls), - total_bids_for_project: total_bids_for_project.saturating_add(perform_bid_calls), receiving_account, + auction_oversubscribed, }; + + BidBucketBounds::::mutate(project_id, current_bucket.current_price, |maybe_indexes| { + if let Some((i, j)) = maybe_indexes { + *maybe_indexes = Some((*i, bid_id)); + } else { + *maybe_indexes = Some((bid_id, bid_id)); + } + }); + Self::do_perform_bid(perform_params)?; perform_bid_calls = perform_bid_calls.saturating_add(1); @@ -112,10 +116,7 @@ impl Pallet { // Note: If the bucket has been exhausted, the 'update' function has already made the 'current_bucket' point to the next one. Buckets::::insert(project_id, current_bucket); - Ok(PostDispatchInfo { - actual_weight: Some(WeightInfoOf::::bid(existing_bids_amount, perform_bid_calls)), - pays_fee: Pays::No, - }) + Ok(()) } #[transactional] @@ -131,26 +132,23 @@ impl Pallet { now, did, metadata_ticket_size_bounds, - total_bids_by_bidder, - total_bids_for_project, receiving_account, + auction_oversubscribed, } = do_perform_bid_params; - let ticket_size = ct_usd_price.checked_mul_int(ct_amount).ok_or(Error::::BadMath)?; + let usd_ticket_size = ct_usd_price.checked_mul_int(ct_amount).ok_or(Error::::BadMath)?; let total_usd_bid_by_did = AuctionBoughtUSD::::get((project_id, did.clone())); let multiplier: MultiplierOf = mode.multiplier().try_into().map_err(|_| Error::::BadMath)?; ensure!( metadata_ticket_size_bounds - .usd_ticket_below_maximum_per_did(total_usd_bid_by_did.saturating_add(ticket_size)), + .usd_ticket_below_maximum_per_did(total_usd_bid_by_did.saturating_add(usd_ticket_size)), Error::::TooHigh ); - ensure!(total_bids_by_bidder < T::MaxBidsPerUser::get(), Error::::TooManyUserParticipations); - ensure!(total_bids_for_project < T::MaxBidsPerProject::get(), Error::::TooManyProjectParticipations); // * Calculate new variables * - let plmc_bond = Self::calculate_plmc_bond(ticket_size, multiplier).map_err(|_| Error::::BadMath)?; - let funding_asset_amount_locked = Self::calculate_funding_asset_amount(ticket_size, funding_asset)?; + let plmc_bond = Self::calculate_plmc_bond(usd_ticket_size, multiplier).map_err(|_| Error::::BadMath)?; + let funding_asset_amount_locked = Self::calculate_funding_asset_amount(usd_ticket_size, funding_asset)?; let new_bid = BidInfoOf:: { id: bid_id, @@ -171,10 +169,13 @@ impl Pallet { Self::bond_plmc_with_mode(&bidder, project_id, plmc_bond, mode, funding_asset)?; Self::try_funding_asset_hold(&bidder, project_id, funding_asset_amount_locked, funding_asset.id())?; - Bids::::insert((project_id, bidder.clone(), bid_id), &new_bid); + Bids::::insert((project_id, bid_id), &new_bid); NextBidId::::set(bid_id.saturating_add(One::one())); - BidCounts::::mutate(project_id, |c| *c = c.saturating_add(1)); - AuctionBoughtUSD::::mutate((project_id, did), |amount| *amount = amount.saturating_add(ticket_size)); + AuctionBoughtUSD::::mutate((project_id, did), |amount| *amount = amount.saturating_add(usd_ticket_size)); + + if auction_oversubscribed { + CTAmountOversubscribed::::mutate(project_id, |amount| *amount = amount.saturating_add(ct_amount)); + } Self::deposit_event(Event::Bid { project_id, @@ -190,4 +191,73 @@ impl Pallet { Ok(new_bid) } + + /// Go over one oversubscribed bid and update the cutoff. + pub fn do_process_next_oversubscribed_bid(project_id: ProjectId) -> DispatchResult { + let project_metadata = ProjectsMetadata::::get(project_id).ok_or(Error::::ProjectMetadataNotFound)?; + let bucket = Buckets::::get(project_id).ok_or(Error::::BucketNotFound)?; + let maybe_current_cutoff = OutbidBidsCutoff::::get(project_id); + let mut ct_amount_oversubscribed = CTAmountOversubscribed::::get(project_id); + + ensure!(ct_amount_oversubscribed > Zero::zero(), Error::::NoBidsOversubscribed); + + let current_cutoff: (PriceOf, u32); + + // Adjust initial cutoff if necessary + if let Some((price, index)) = maybe_current_cutoff { + let bid = Bids::::get((project_id, index)).ok_or(Error::::ImpossibleState)?; + if matches!(bid.status, BidStatus::PartiallyAccepted(_)) { + current_cutoff = (price, index); + } else { + let (new_price, new_index) = Self::get_next_cutoff(project_id, bucket.delta_price, price, index)?; + current_cutoff = (new_price, new_index); + } + } else { + // Initialize to the first bucket + let first_price = project_metadata.minimum_price; + let first_bucket_bounds = + BidBucketBounds::::get(project_id, first_price).ok_or(Error::::ImpossibleState)?; + current_cutoff = (first_price, first_bucket_bounds.1); + } + + let (_price, index) = current_cutoff; + + let mut bid = Bids::::get((project_id, index)).ok_or(Error::::ImpossibleState)?; + + let bid_amount = match bid.status { + BidStatus::PartiallyAccepted(ct_amount_accepted) => ct_amount_accepted, + _ => bid.original_ct_amount, + }; + + if bid_amount > ct_amount_oversubscribed { + bid.status = BidStatus::PartiallyAccepted(bid_amount.saturating_sub(ct_amount_oversubscribed)); + Bids::::insert((project_id, bid.id), bid); + ct_amount_oversubscribed = Zero::zero(); + } else { + bid.status = BidStatus::Rejected; + Bids::::insert((project_id, bid.id), bid); + ct_amount_oversubscribed = ct_amount_oversubscribed.saturating_sub(bid_amount); + } + + OutbidBidsCutoff::::set(project_id, Some(current_cutoff)); + CTAmountOversubscribed::::insert(project_id, ct_amount_oversubscribed); + + Ok(()) + } + + pub fn get_next_cutoff( + project_id: ProjectId, + delta_price: PriceOf, + current_price: PriceOf, + current_index: u32, + ) -> Result<(PriceOf, u32), DispatchError> { + let bounds = BidBucketBounds::::get(project_id, current_price).ok_or(Error::::ImpossibleState)?; + if current_index == bounds.0 { + let new_price = current_price.saturating_add(delta_price); + let new_bounds = BidBucketBounds::::get(project_id, new_price).ok_or(Error::::ImpossibleState)?; + Ok((new_price, new_bounds.1)) + } else { + Ok((current_price, current_index.saturating_sub(1))) + } + } } diff --git a/pallets/funding/src/functions/5_funding_end.rs b/pallets/funding/src/functions/5_funding_end.rs index 6ff0cede1..52902420b 100644 --- a/pallets/funding/src/functions/5_funding_end.rs +++ b/pallets/funding/src/functions/5_funding_end.rs @@ -1,62 +1,61 @@ #[allow(clippy::wildcard_imports)] use super::*; +use itertools::Itertools; impl Pallet { #[transactional] pub fn do_end_funding(project_id: ProjectId) -> DispatchResult { // * Get variables * let project_metadata = ProjectsMetadata::::get(project_id).ok_or(Error::::ProjectMetadataNotFound)?; - let project_details = ProjectsDetails::::get(project_id).ok_or(Error::::ProjectDetailsNotFound)?; + let mut project_details = ProjectsDetails::::get(project_id).ok_or(Error::::ProjectDetailsNotFound)?; let bucket = Buckets::::get(project_id).ok_or(Error::::BucketNotFound)?; - let remaining_cts = project_details.remaining_contribution_tokens; let now = >::block_number(); let issuer_did = project_details.issuer_did.clone(); + let ct_amount_oversubscribed = CTAmountOversubscribed::::get(project_id); // * Validity checks * ensure!( - // Can end due to running out of CTs - remaining_cts == Zero::zero() || - // or the last funding round ending - project_details.round_duration.ended(now) && matches!(project_details.status, ProjectStatus::AuctionRound), + project_details.round_duration.ended(now) && matches!(project_details.status, ProjectStatus::AuctionRound), Error::::TooEarlyForRound ); + ensure!(ct_amount_oversubscribed.is_zero(), Error::::OversubscribedBidsRemaining); + + let mut project_ids = ProjectsInAuctionRound::::get().to_vec(); + let (pos, _) = project_ids.iter().find_position(|id| **id == project_id).ok_or(Error::::ImpossibleState)?; + project_ids.remove(pos); + ProjectsInAuctionRound::::put(WeakBoundedVec::force_from(project_ids, None)); - // * Calculate WAP * let auction_allocation_size = project_metadata.total_allocation_size; - let weighted_token_price = bucket.calculate_wap(auction_allocation_size); + let weighted_average_price = bucket.calculate_wap(auction_allocation_size); + project_details.weighted_average_price = Some(weighted_average_price); + + let bucket_price_higher_than_initial = bucket.current_price > bucket.initial_price; + let sold_percent = + Perquintill::from_rational(auction_allocation_size - bucket.amount_left, auction_allocation_size); + let threshold = T::FundingSuccessThreshold::get(); + let sold_more_than_min = sold_percent >= threshold; - // * Update Storage * - let _calculation_result = - Self::decide_winning_bids(project_id, project_metadata.total_allocation_size, weighted_token_price)?; - let mut updated_project_details = - ProjectsDetails::::get(project_id).ok_or(Error::::ProjectDetailsNotFound)?; + let funding_successful = bucket_price_higher_than_initial || sold_more_than_min; DidWithActiveProjects::::set(issuer_did, None); - // * Calculate new variables * - let funding_target = updated_project_details.fundraising_target_usd; - let funding_reached = updated_project_details.funding_amount_reached_usd; - let funding_ratio = Perquintill::from_rational(funding_reached, funding_target); + let usd_raised = Self::calculate_usd_sold_from_bucket(bucket.clone(), project_metadata.total_allocation_size); + project_details.funding_amount_reached_usd = usd_raised; + project_details.remaining_contribution_tokens = + if bucket.current_price == bucket.initial_price { bucket.amount_left } else { Zero::zero() }; + ProjectsDetails::::insert(project_id, project_details.clone()); // * Update project status * - let next_status = if funding_ratio < T::FundingSuccessThreshold::get() { - updated_project_details.evaluation_round_info.evaluators_outcome = Some(EvaluatorsOutcome::Slashed); - ProjectStatus::FundingFailed - } else { + let next_status = if funding_successful { let reward_info = Self::generate_evaluator_rewards_info(project_id)?; - updated_project_details.evaluation_round_info.evaluators_outcome = - Some(EvaluatorsOutcome::Rewarded(reward_info)); + project_details.evaluation_round_info.evaluators_outcome = Some(EvaluatorsOutcome::Rewarded(reward_info)); ProjectStatus::FundingSuccessful + } else { + project_details.evaluation_round_info.evaluators_outcome = Some(EvaluatorsOutcome::Slashed); + ProjectStatus::FundingFailed }; - Self::transition_project( - project_id, - updated_project_details.clone(), - project_details.status, - next_status, - None, - true, - )?; + Self::transition_project(project_id, project_details.clone(), project_details.status, next_status, None, true)?; Ok(()) } diff --git a/pallets/funding/src/functions/6_settlement.rs b/pallets/funding/src/functions/6_settlement.rs index e406f8d5a..e9b31a64d 100644 --- a/pallets/funding/src/functions/6_settlement.rs +++ b/pallets/funding/src/functions/6_settlement.rs @@ -154,18 +154,23 @@ impl Pallet { Ok(()) } - pub fn do_settle_bid(bid: BidInfoOf, project_id: ProjectId) -> DispatchResult { + pub fn do_settle_bid(project_id: ProjectId, bid_id: u32) -> DispatchResult { let project_details = ProjectsDetails::::get(project_id).ok_or(Error::::ProjectDetailsNotFound)?; let project_metadata = ProjectsMetadata::::get(project_id).ok_or(Error::::ProjectMetadataNotFound)?; let funding_success = matches!(project_details.status, ProjectStatus::SettlementStarted(FundingOutcome::Success)); - let wap = project_details.weighted_average_price.ok_or(Error::::ImpossibleState)?; + let wap = project_details.weighted_average_price.unwrap_or(project_metadata.minimum_price); + let mut bid = Bids::::get((project_id, bid_id)).ok_or(Error::::ParticipationNotFound)?; ensure!( matches!(project_details.status, ProjectStatus::SettlementStarted(..)) || bid.status == BidStatus::Rejected, Error::::SettlementNotStarted ); + if bid.status == BidStatus::YetUnknown { + bid.status = BidStatus::Accepted; + } + // Return the full bid amount to refund if bid is rejected or project failed, // Return a partial amount if the project succeeded, and the wap > paid price or bid is partially accepted let BidRefund { final_ct_usd_price, final_ct_amount, refunded_plmc, refunded_funding_asset_amount } = @@ -186,7 +191,7 @@ impl Pallet { Self::release_participation_bond_for(&bid.bidder, refunded_plmc)?; } - if funding_success && bid.status != BidStatus::Rejected { + if funding_success { let ct_vesting_duration = Self::set_plmc_bond_release_with_mode( bid.bidder.clone(), bid.plmc_bond.saturating_sub(refunded_plmc), @@ -214,12 +219,13 @@ impl Pallet { )?; } - Bids::::remove((project_id, bid.bidder.clone(), bid.id)); + Bids::::remove((project_id, bid.id)); Self::deposit_event(Event::BidSettled { project_id, account: bid.bidder, id: bid.id, + status: bid.status, final_ct_amount, final_ct_usd_price, }); @@ -236,7 +242,7 @@ impl Pallet { ) -> Result, DispatchError> { let final_ct_usd_price = if bid.original_ct_usd_price > wap { wap } else { bid.original_ct_usd_price }; let multiplier: MultiplierOf = bid.mode.multiplier().try_into().map_err(|_| Error::::BadMath)?; - if bid.status == BidStatus::Rejected || !funding_success { + if !funding_success || bid.status == BidStatus::Rejected { return Ok(BidRefund:: { final_ct_usd_price, final_ct_amount: Zero::zero(), @@ -244,7 +250,11 @@ impl Pallet { refunded_funding_asset_amount: bid.funding_asset_amount_locked, }); } - let final_ct_amount = bid.final_ct_amount(); + let final_ct_amount = match bid.status { + BidStatus::Accepted => bid.original_ct_amount, + BidStatus::PartiallyAccepted(accepted_amount) => accepted_amount, + _ => Zero::zero(), + }; let new_ticket_size = final_ct_usd_price.checked_mul_int(final_ct_amount).ok_or(Error::::BadMath)?; let new_plmc_bond = Self::calculate_plmc_bond(new_ticket_size, multiplier)?; @@ -265,13 +275,9 @@ impl Pallet { // We use closers to do an early return if just one of these storage iterators returns a value. let no_evaluations_remaining = || Evaluations::::iter_prefix((project_id,)).next().is_none(); let no_bids_remaining = || Bids::::iter_prefix((project_id,)).next().is_none(); - let no_contributions_remaining = || Contributions::::iter_prefix((project_id,)).next().is_none(); // Check if there are any evaluations, bids or contributions remaining - ensure!( - no_evaluations_remaining() && no_bids_remaining() && no_contributions_remaining(), - Error::::SettlementNotComplete - ); + ensure!(no_evaluations_remaining() && no_bids_remaining(), Error::::SettlementNotComplete); // Mark the project as settled Self::transition_project( @@ -414,22 +420,25 @@ impl Pallet { vesting_time: BlockNumberFor, receiving_account: Junction, ) -> DispatchResult { - UserMigrations::::try_mutate((project_id, origin), |maybe_migrations| -> DispatchResult { - let migration_origin = MigrationOrigin { user: receiving_account, id, participation_type }; - let vesting_time: u64 = vesting_time.try_into().map_err(|_| Error::::BadMath)?; - let migration_info: MigrationInfo = (ct_amount, vesting_time).into(); - let migration = Migration::new(migration_origin, migration_info); - if let Some((_, migrations)) = maybe_migrations { - migrations.try_push(migration).map_err(|_| Error::::TooManyMigrations)?; - } else { - let mut migrations = BoundedVec::<_, MaxParticipationsPerUser>::new(); - migrations.try_push(migration).map_err(|_| Error::::TooManyMigrations)?; - *maybe_migrations = Some((MigrationStatus::NotStarted, migrations)); - - UnmigratedCounter::::mutate(project_id, |counter| *counter = counter.saturating_add(1)); - } + let (status, user_migrations) = UserMigrations::::get((project_id, origin)) + .unwrap_or((MigrationStatus::NotStarted, WeakBoundedVec::<_, ConstU32<10_000>>::force_from(vec![], None))); - Ok(()) - }) + if user_migrations.is_empty() { + UnmigratedCounter::::mutate(project_id, |counter| *counter = counter.saturating_add(1)); + } + + let mut user_migrations = user_migrations.to_vec(); + let migration_origin = MigrationOrigin { user: receiving_account, id, participation_type }; + let vesting_time: u64 = vesting_time.try_into().map_err(|_| Error::::BadMath)?; + let migration_info: MigrationInfo = (ct_amount, vesting_time).into(); + let migration = Migration::new(migration_origin, migration_info); + user_migrations.push(migration); + + UserMigrations::::insert( + (project_id, origin), + (status, WeakBoundedVec::<_, ConstU32<10_000>>::force_from(user_migrations, None)), + ); + + Ok(()) } } diff --git a/pallets/funding/src/functions/7_ct_migration.rs b/pallets/funding/src/functions/7_ct_migration.rs index e5bac6ec8..3f4eb97b7 100644 --- a/pallets/funding/src/functions/7_ct_migration.rs +++ b/pallets/funding/src/functions/7_ct_migration.rs @@ -418,10 +418,6 @@ impl Pallet { let migration_readiness_check = migration_info.migration_readiness_check.ok_or(Error::::ChannelNotReady)?; let project_para_id = migration_info.parachain_id; let now = >::block_number(); - ensure!( - Self::user_has_no_participations(project_id, participant.clone()), - Error::::ParticipationsNotSettled - ); let (_, migrations) = UserMigrations::::get((project_id, participant.clone())).ok_or(Error::::NoMigrationsFound)?; diff --git a/pallets/funding/src/functions/misc.rs b/pallets/funding/src/functions/misc.rs index dfebae732..4d21a0cf4 100644 --- a/pallets/funding/src/functions/misc.rs +++ b/pallets/funding/src/functions/misc.rs @@ -80,65 +80,6 @@ impl Pallet { Ok(VestingInfo { total_amount: bonded_amount, amount_per_block, duration }) } - pub fn decide_winning_bids( - project_id: ProjectId, - auction_allocation_size: Balance, - wap: PriceOf, - ) -> Result<(u32, u32), DispatchError> { - let mut bids = Bids::::iter_prefix_values((project_id,)).collect::>(); - // temp variable to store the sum of the bids - let mut bid_token_amount_sum = Zero::zero(); - // sort bids by price, and equal prices sorted by id - bids.sort_by(|a, b| b.cmp(a)); - let (accepted_bids, rejected_bids): (Vec<_>, Vec<_>) = bids - .into_iter() - .map(|mut bid| { - let buyable_amount = auction_allocation_size.saturating_sub(bid_token_amount_sum); - if buyable_amount.is_zero() { - bid.status = BidStatus::Rejected; - } else if bid.original_ct_amount <= buyable_amount { - bid_token_amount_sum.saturating_accrue(bid.original_ct_amount); - bid.status = BidStatus::Accepted; - DidWithWinningBids::::mutate(project_id, bid.did.clone(), |flag| { - *flag = true; - }); - } else { - bid_token_amount_sum.saturating_accrue(buyable_amount); - bid.status = BidStatus::PartiallyAccepted(buyable_amount); - DidWithWinningBids::::mutate(project_id, bid.did.clone(), |flag| { - *flag = true; - }); - } - Bids::::insert((project_id, &bid.bidder, &bid.id), &bid); - bid - }) - .partition(|bid| matches!(bid.status, BidStatus::Accepted | BidStatus::PartiallyAccepted(..))); - - let accepted_bid_len = accepted_bids.len() as u32; - let total_auction_allocation_usd: Balance = accepted_bids - .into_iter() - .try_fold(Zero::zero(), |acc: Balance, bid: BidInfoOf| { - let final_ct_usd_price = if bid.original_ct_usd_price > wap { wap } else { bid.original_ct_usd_price }; - let final_ct_amount = bid.final_ct_amount(); - final_ct_usd_price.checked_mul_int(final_ct_amount).and_then(|usd_ticket| acc.checked_add(usd_ticket)) - }) - .ok_or(Error::::BadMath)?; - - ProjectsDetails::::mutate(project_id, |maybe_info| -> DispatchResult { - if let Some(info) = maybe_info { - info.remaining_contribution_tokens.saturating_reduce(bid_token_amount_sum); - info.funding_amount_reached_usd.saturating_accrue(total_auction_allocation_usd); - info.weighted_average_price = Some(wap); - - Ok(()) - } else { - Err(Error::::ProjectDetailsNotFound.into()) - } - })?; - - Ok((accepted_bid_len, rejected_bids.len() as u32)) - } - pub fn bond_plmc_with_mode( who: &T::AccountId, project_id: ProjectId, @@ -203,7 +144,7 @@ impl Pallet { // Calculate the total fee allocation for a project, based on the funding reached. fn calculate_fee_allocation(project_id: ProjectId) -> Result { let project_metadata = ProjectsMetadata::::get(project_id).ok_or(Error::::ProjectMetadataNotFound)?; - + let bucket = Buckets::::get(project_id).ok_or(Error::::BucketNotFound)?; // Fetching the necessary data for a specific project. let project_details = ProjectsDetails::::get(project_id).ok_or(Error::::ProjectDetailsNotFound)?; @@ -212,14 +153,11 @@ impl Pallet { let fee_usd = Self::compute_total_fee_from_brackets(funding_amount_reached); let fee_percentage = Perquintill::from_rational(fee_usd, funding_amount_reached); - let initial_token_allocation_size = project_metadata.total_allocation_size; - let final_remaining_contribution_tokens = project_details.remaining_contribution_tokens; - - // Calculate the number of tokens sold for the project. - let token_sold = initial_token_allocation_size - .checked_sub(final_remaining_contribution_tokens) - // Ensure safety by providing a default in case of unexpected situations. - .unwrap_or(initial_token_allocation_size); + let token_sold = if bucket.current_price == bucket.initial_price { + project_metadata.total_allocation_size.saturating_sub(bucket.amount_left) + } else { + project_metadata.total_allocation_size + }; let total_fee_allocation = fee_percentage * token_sold; Ok(total_fee_allocation) @@ -328,22 +266,21 @@ impl Pallet { available_bytes_for_migration_per_message.saturating_div(one_migration_bytes) } - /// Check if the user has no participations (left) in the project. - pub fn user_has_no_participations(project_id: ProjectId, user: AccountIdOf) -> bool { - Evaluations::::iter_prefix_values((project_id, user.clone())).next().is_none() && - Bids::::iter_prefix_values((project_id, user.clone())).next().is_none() && - Contributions::::iter_prefix_values((project_id, user)).next().is_none() - } + // /// Check if the user has no participations (left) in the project. + // pub fn user_has_no_participations(project_id: ProjectId, user: AccountIdOf) -> bool { + // Evaluations::::iter_prefix_values((project_id, user.clone())).next().is_none() && + // Bids::::iter_prefix_values((project_id, user.clone())).next().is_none() + // } pub fn construct_migration_xcm_message( - migrations: BoundedVec>, + migrations: WeakBoundedVec>, query_id: QueryId, pallet_index: PalletIndex, ) -> Xcm<()> { // TODO: adjust this as benchmarks for polimec-receiver are written const MAX_WEIGHT: Weight = Weight::from_parts(10_000, 0); const MAX_RESPONSE_WEIGHT: Weight = Weight::from_parts(700_000_000, 50_000); - let migrations_item = Migrations::from(migrations.into()); + let migrations_item = Migrations::from(migrations.to_vec()); // First byte is the pallet index, second byte is the call index let mut encoded_call = vec![pallet_index, 0]; @@ -519,6 +456,44 @@ impl Pallet { let funding_asset_decimals = T::FundingCurrency::decimals(funding_asset_id.clone()); >::get_decimals_aware_price(funding_asset_id, USD_DECIMALS, funding_asset_decimals) } + + pub fn calculate_usd_sold_from_bucket(mut bucket: BucketOf, auction_allocation_size: Balance) -> Balance { + if bucket.current_price == bucket.initial_price { + return bucket.initial_price.saturating_mul_int(auction_allocation_size.saturating_sub(bucket.amount_left)) + } + + let mut total_usd_sold = 0u128; + let wap = bucket.calculate_wap(auction_allocation_size); + + let mut total_ct_amount_left = auction_allocation_size; + + // Latest bucket will be partially sold + let ct_sold = bucket.delta_amount.saturating_sub(bucket.amount_left); + let usd_sold = wap.saturating_mul_int(ct_sold); + total_usd_sold = total_usd_sold.saturating_add(usd_sold); + total_ct_amount_left = total_ct_amount_left.saturating_sub(ct_sold); + bucket.current_price = bucket.current_price.saturating_sub(bucket.delta_price); + + while total_ct_amount_left > 0 { + // If we reached the inital bucket, all the CTs remaining are taken from this bucket + if bucket.current_price == bucket.initial_price { + let ct_sold = total_ct_amount_left; + let usd_sold = bucket.initial_price.saturating_mul_int(ct_sold); + total_usd_sold = total_usd_sold.saturating_add(usd_sold); + break + } + + let price_charged = wap.min(bucket.current_price); + let ct_sold = total_ct_amount_left.min(bucket.delta_amount); + let usd_raised = price_charged.saturating_mul_int(ct_sold); + total_usd_sold = total_usd_sold.saturating_add(usd_raised); + total_ct_amount_left = total_ct_amount_left.saturating_sub(ct_sold); + + bucket.current_price = bucket.current_price.saturating_sub(bucket.delta_price); + } + + total_usd_sold + } } pub mod typed_data_v4 { diff --git a/pallets/funding/src/instantiator/calculations.rs b/pallets/funding/src/instantiator/calculations.rs index 06135f82a..3040eb057 100644 --- a/pallets/funding/src/instantiator/calculations.rs +++ b/pallets/funding/src/instantiator/calculations.rs @@ -509,8 +509,8 @@ impl< self.generate_evaluations_from_total_usd(usd_threshold, evaluations_count) } - pub fn generate_bids_from_total_ct_amount(&self, bids_count: u8, total_ct_bid: Balance) -> Vec> { - // This range should be allowed for all investor types. + pub fn generate_bids_from_total_ct_amount(&self, bids_count: u32, total_ct_bid: Balance) -> Vec> { + // Use u128 for multipliers to allow for larger values let mut multipliers = (1u8..=5u8).cycle(); let modes = (0..bids_count) @@ -528,25 +528,32 @@ impl< let funding_assets = vec![USDT, USDC, DOT, WETH, USDT].into_iter().cycle().take(bids_count as usize).collect_vec(); - // Even distribution of weights totaling 100% among bids. + // Use Perquintill for precise weight distribution let weights = { if bids_count == 0 { return vec![]; } - let base = 100 / bids_count; - let remainder = 100 % bids_count; - let mut result = vec![base; bids_count as usize]; - for i in 0..remainder { - result[i as usize] += 1; + // Convert to Perquintill for higher precision division + let one = Perquintill::from_percent(100); + let per_bid = one / bids_count; + let mut remaining = one; + let mut result = Vec::with_capacity(bids_count as usize); + + // Distribute weights evenly with maximum precision + for _ in 0..bids_count - 1 { + result.push(per_bid); + remaining = remaining - per_bid; } + // Add remaining weight to the last bid to ensure total is exactly 100% + result.push(remaining); result }; - let bidders = (0..bids_count as u32).map(|i| self.account_from_u32(i, "BIDDER")).collect_vec(); + let bidders = (0..bids_count).map(|i| self.account_from_u32(i, "BIDDER")).collect_vec(); izip!(weights, bidders, modes, investor_types, funding_assets) .map(|(weight, bidder, mode, investor_type, funding_asset)| { - let token_amount = Percent::from_percent(weight) * total_ct_bid; + let token_amount = weight * total_ct_bid; BidParams::from((bidder, investor_type, token_amount, mode, funding_asset)) }) .collect() @@ -556,7 +563,7 @@ impl< &self, project_metadata: ProjectMetadataOf, usd_amount: Balance, - bids_count: u8, + bids_count: u32, ) -> Vec> { let min_price = project_metadata.minimum_price; let total_allocation_size = project_metadata.total_allocation_size; @@ -581,24 +588,10 @@ impl< let mut target_wap: PriceOf = min_price * target_wap_multiplicator; let mut bucket = self.find_bucket_for_wap(project_metadata.clone(), target_wap); - let first_account = self.account_from_u32(0, "BIDDER"); - let next_account = |acc: AccountIdOf| -> AccountIdOf { - let acc_bytes = acc.encode(); - let account_string = String::from_utf8_lossy(&acc_bytes); - let entropy = (0, account_string).using_encoded(blake2_256); - Decode::decode(&mut TrailingZeroInput::new(entropy.as_ref())) - .expect("infinite length input; no invalid inputs for type; qed") - }; - let evaluations = self.generate_successful_evaluations(project_metadata.clone(), 5); // Initial bid generation - let mut bids = self.generate_bids_that_take_price_to( - project_metadata.clone(), - target_wap, - first_account.clone(), - next_account, - ); + let mut bids = self.generate_bids_that_take_price_to(project_metadata.clone(), target_wap); // Get initial USD amount let project_id = self.create_finished_project( @@ -638,12 +631,7 @@ impl< previous_wap = target_wap; bucket = self.find_bucket_for_wap(project_metadata.clone(), target_wap); - bids = self.generate_bids_that_take_price_to( - project_metadata.clone(), - target_wap, - first_account.clone(), - next_account, - ); + bids = self.generate_bids_that_take_price_to(project_metadata.clone(), target_wap); let project_id = self.create_finished_project( project_metadata.clone(), @@ -664,7 +652,7 @@ impl< &self, project_metadata: ProjectMetadataOf, percent_funding: u8, - bids_count: u8, + bids_count: u32, ) -> Vec> { let total_allocation_size = project_metadata.total_allocation_size; let total_ct_bid = Percent::from_percent(percent_funding) * total_allocation_size; @@ -747,24 +735,28 @@ impl< } // We assume a single bid can cover the whole first bucket. Make sure the ticket sizes allow this. - pub fn generate_bids_from_bucket( + pub fn generate_bids_from_bucket( &self, project_metadata: ProjectMetadataOf, bucket: BucketOf, - mut starting_account: AccountIdOf, - mut increment_account: F, funding_asset: AcceptedFundingAsset, - ) -> Vec> - where - F: FnMut(AccountIdOf) -> AccountIdOf, - { + ) -> Vec> { if bucket.current_price == bucket.initial_price { return vec![] } let auction_allocation = project_metadata.total_allocation_size; + let mut starting_account = self.account_from_u32(0, "BIDDER"); + let increment_account = |acc: AccountIdOf| -> AccountIdOf { + let acc_bytes = acc.encode(); + let account_string = String::from_utf8_lossy(&acc_bytes); + let entropy = (0, account_string).using_encoded(blake2_256); + Decode::decode(&mut TrailingZeroInput::new(entropy.as_ref())) + .expect("infinite length input; no invalid inputs for type; qed") + }; + let mut generate_bid = |ct_amount| -> BidParams { - let bid = (starting_account.clone(), Retail, ct_amount, funding_asset).into(); + let bid = BidParams::::from((starting_account.clone(), Retail, ct_amount, funding_asset)); starting_account = increment_account(starting_account.clone()); bid }; @@ -793,24 +785,13 @@ impl< bids } - pub fn generate_bids_that_take_price_to( + pub fn generate_bids_that_take_price_to( &self, project_metadata: ProjectMetadataOf, desired_price: PriceOf, - bidder_account: AccountIdOf, - next_bidder_account: F, - ) -> Vec> - where - F: FnMut(AccountIdOf) -> AccountIdOf, - { + ) -> Vec> { let necessary_bucket = self.find_bucket_for_wap(project_metadata.clone(), desired_price); - self.generate_bids_from_bucket( - project_metadata, - necessary_bucket, - bidder_account, - next_bidder_account, - AcceptedFundingAsset::USDT, - ) + self.generate_bids_from_bucket(project_metadata, necessary_bucket, AcceptedFundingAsset::USDT) } // Make sure the bids are 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 427bbfe12..82c76c9af 100644 --- a/pallets/funding/src/instantiator/chain_interactions.rs +++ b/pallets/funding/src/instantiator/chain_interactions.rs @@ -415,6 +415,7 @@ impl< self.execute(|| >::do_end_evaluation(project_id).unwrap()); }, ProjectStatus::AuctionRound => { + self.process_oversubscribed_bids(project_id); self.execute(|| >::do_end_funding(project_id).unwrap()); }, ProjectStatus::FundingSuccessful | ProjectStatus::FundingFailed => { @@ -428,11 +429,7 @@ impl< new_details.status } - pub fn evaluate_for_users( - &mut self, - project_id: ProjectId, - bonds: Vec>, - ) -> DispatchResultWithPostInfo { + pub fn evaluate_for_users(&mut self, project_id: ProjectId, bonds: Vec>) -> DispatchResult { let project_policy = self.get_project_metadata(project_id).policy_ipfs_cid.unwrap(); for EvaluationParams { account, usd_amount, receiving_account } in bonds { self.execute(|| { @@ -446,7 +443,28 @@ impl< ) })?; } - Ok(().into()) + Ok(()) + } + + pub fn mint_necessary_tokens_for_bids(&mut self, project_id: ProjectId, bids: Vec>) { + let current_bucket = self.execute(|| Buckets::::get(project_id).unwrap()); + let project_metadata = self.get_project_metadata(project_id); + + let necessary_plmc = self.calculate_auction_plmc_charged_from_all_bids_made_or_with_bucket( + &bids, + project_metadata.clone(), + Some(current_bucket), + ); + let necessary_funding_assets = self.calculate_auction_funding_asset_charged_from_all_bids_made_or_with_bucket( + &bids, + project_metadata, + Some(current_bucket), + ); + + self.mint_plmc_ed_if_required(necessary_plmc.accounts()); + self.mint_funding_asset_ed_if_required(necessary_funding_assets.to_account_asset_map()); + self.mint_plmc_to(necessary_plmc); + self.mint_funding_asset_to(necessary_funding_assets); } pub fn bid_for_users(&mut self, project_id: ProjectId, bids: Vec>) -> DispatchResultWithPostInfo { @@ -472,13 +490,17 @@ impl< Ok(().into()) } + pub fn process_oversubscribed_bids(&mut self, project_id: ProjectId) { + self.execute(|| while Pallet::::do_process_next_oversubscribed_bid(project_id).is_ok() {}); + } + pub fn settle_project(&mut self, project_id: ProjectId, mark_as_settled: bool) { self.execute(|| { Evaluations::::iter_prefix((project_id,)) .for_each(|(_, evaluation)| Pallet::::do_settle_evaluation(evaluation, project_id).unwrap()); Bids::::iter_prefix((project_id,)) - .for_each(|(_, bid)| Pallet::::do_settle_bid(bid, project_id).unwrap()); + .for_each(|(_, bid)| Pallet::::do_settle_bid(project_id, bid.id).unwrap()); if mark_as_settled { crate::Pallet::::do_mark_project_as_settled(project_id).unwrap(); @@ -494,6 +516,10 @@ impl< self.execute(|| Bids::::iter_prefix_values((project_id,)).collect()) } + pub fn get_bid(&mut self, bid_id: u32) -> BidInfoOf { + self.execute(|| Bids::::iter_values().find(|bid| bid.id == bid_id).unwrap()) + } + // Used to check all the USDT/USDC/DOT was paid to the issuer funding account pub fn assert_total_funding_paid_out(&mut self, project_id: ProjectId, bids: Vec>) { let project_metadata = self.get_project_metadata(project_id); @@ -580,14 +606,37 @@ impl< bids: Vec>, is_successful: bool, ) { + assert_eq!(self.execute(|| { Bids::::iter_prefix_values((&project_id,)).count() }), 0); + + let maybe_outbid_bids_cutoff = self.execute(|| OutbidBidsCutoff::::get(project_id)); for bid in bids { - let account = bid.bidder.clone(); - assert_eq!(self.execute(|| { Bids::::iter_prefix_values((&project_id, &account)).count() }), 0); - let amount: Balance = bid.final_ct_amount(); + // Determine if the bid is outbid + let bid_is_outbid = match maybe_outbid_bids_cutoff { + Some((cutoff_price, cutoff_index)) => + cutoff_price > bid.original_ct_usd_price || + (cutoff_price == bid.original_ct_usd_price && cutoff_index <= bid.id), + None => false, // If there's no cutoff, the bid is not outbid + }; + + let bid_ct_amount = if let Some((bucket_price, index)) = maybe_outbid_bids_cutoff { + if bucket_price == bid.original_ct_usd_price && index == bid.id { + match bid.status { + BidStatus::PartiallyAccepted(ct_amount) => ct_amount, + _ => Zero::zero(), + } + } else if bid_is_outbid { + Zero::zero() + } else { + bid.original_ct_amount + } + } else { + bid.original_ct_amount + }; + self.assert_migration( project_id, - account, - amount, + bid.bidder, + bid_ct_amount, bid.id, ParticipationType::Bid, bid.receiving_account, @@ -759,20 +808,7 @@ impl< let status = self.go_to_next_state(project_id); if status == ProjectStatus::FundingSuccessful { - // Check that remaining CTs are updated - let project_details = self.get_project_details(project_id); - // if our bids were creating an oversubscription, then just take the total allocation size - let auction_bought_tokens = bids - .iter() - .map(|bid| bid.amount) - .fold(Balance::zero(), |acc, item| item + acc) - .min(project_metadata.total_allocation_size); - - assert_eq!( - project_details.remaining_contribution_tokens, - project_metadata.total_allocation_size - auction_bought_tokens, - "Remaining CTs are incorrect" - ); + self.test_ct_not_created_for(project_id); } else if status == ProjectStatus::FundingFailed { self.test_ct_not_created_for(project_id); } else { @@ -802,6 +838,22 @@ impl< assert!(matches!(self.go_to_next_state(project_id), ProjectStatus::SettlementStarted(_))); self.settle_project(project_id, mark_as_settled); + + // Check that remaining CTs are updated + let project_details = self.get_project_details(project_id); + // if our bids were creating an oversubscription, then just take the total allocation size + let auction_bought_tokens = bids + .iter() + .map(|bid| bid.amount) + .fold(Balance::zero(), |acc, item| item + acc) + .min(project_metadata.total_allocation_size); + + assert_eq!( + project_details.remaining_contribution_tokens, + project_metadata.total_allocation_size - auction_bought_tokens, + "Remaining CTs are incorrect" + ); + project_id } } diff --git a/pallets/funding/src/instantiator/tests.rs b/pallets/funding/src/instantiator/tests.rs index 592dfde9e..f6091dac7 100644 --- a/pallets/funding/src/instantiator/tests.rs +++ b/pallets/funding/src/instantiator/tests.rs @@ -77,7 +77,8 @@ fn dry_run_wap() { inst.bid_for_users(project_id, bids).unwrap(); - assert!(matches!(inst.go_to_next_state(project_id), ProjectStatus::FundingSuccessful)); + let next_state = inst.go_to_next_state(project_id); + assert!(matches!(next_state, ProjectStatus::FundingSuccessful)); let project_details = inst.get_project_details(project_id); let wap = project_details.weighted_average_price.unwrap(); @@ -176,13 +177,7 @@ fn generate_bids_from_bucket() { PriceProviderOf::::calculate_decimals_aware_price(desired_real_wap, USD_DECIMALS, CT_DECIMALS) .unwrap(); let necessary_bucket = inst.find_bucket_for_wap(project_metadata.clone(), desired_price_aware_wap); - let bids = inst.generate_bids_from_bucket( - project_metadata.clone(), - necessary_bucket, - 420, - |x| x + 1, - AcceptedFundingAsset::USDT, - ); + let bids = inst.generate_bids_from_bucket(project_metadata.clone(), necessary_bucket, AcceptedFundingAsset::USDT); let evaluations = inst.generate_successful_evaluations(project_metadata.clone(), 5); let project_id = inst.create_finished_project(project_metadata.clone(), 0, None, evaluations, bids); let project_details = inst.get_project_details(project_id); diff --git a/pallets/funding/src/lib.rs b/pallets/funding/src/lib.rs index 586099a9a..c0db0704d 100644 --- a/pallets/funding/src/lib.rs +++ b/pallets/funding/src/lib.rs @@ -83,7 +83,7 @@ use frame_support::{ tokens::{fungible, fungibles}, AccountTouch, ContainsPair, }, - BoundedVec, PalletId, + BoundedVec, PalletId, WeakBoundedVec, }; use frame_system::pallet_prelude::BlockNumberFor; pub use pallet::*; @@ -96,7 +96,7 @@ use polimec_common::{ use polkadot_parachain_primitives::primitives::Id as ParaId; use sp_arithmetic::traits::{One, Saturating}; use sp_runtime::{traits::AccountIdConversion, FixedPointNumber, FixedU128}; -use sp_std::{marker::PhantomData, prelude::*}; +use sp_std::prelude::*; pub use types::*; use xcm::v4::{prelude::*, SendXcm}; mod functions; @@ -264,30 +264,10 @@ pub mod pallet { Success = (AccountIdOf, Did, InvestorType, Cid), >; - /// Max individual bids per project. Used to estimate worst case weight for price calculation - #[pallet::constant] - type MaxBidsPerProject: Get; - - /// Max individual bids per project. Used to estimate worst case weight for price calculation - #[pallet::constant] - type MaxBidsPerUser: Get; - /// Range of max_capacity_thresholds values for the hrmp config where we accept the incoming channel request #[pallet::constant] type MaxCapacityThresholds: Get>; - /// Max individual contributions per project per user. Used to estimate worst case weight for price calculation - #[pallet::constant] - type MaxContributionsPerUser: Get; - - /// Max individual evaluations per project. Used to estimate worst case weight for price calculation - #[pallet::constant] - type MaxEvaluationsPerProject: Get; - - /// How many distinct evaluations per user per project - #[pallet::constant] - type MaxEvaluationsPerUser: Get; - #[pallet::constant] type MinUsdPerEvaluation: Get; @@ -393,12 +373,6 @@ pub mod pallet { #[pallet::storage] pub type NextContributionId = StorageValue<_, u32, ValueQuery>; - #[pallet::storage] - pub type BidCounts = StorageMap<_, Blake2_128Concat, ProjectId, u32, ValueQuery>; - - #[pallet::storage] - pub type EvaluationCounts = StorageMap<_, Blake2_128Concat, ProjectId, u32, ValueQuery>; - #[pallet::storage] /// A StorageMap containing the primary project information of projects pub type ProjectsMetadata = StorageMap<_, Blake2_128Concat, ProjectId, ProjectMetadataOf>; @@ -411,6 +385,9 @@ pub mod pallet { /// StorageMap containing additional information for the projects, relevant for correctness of the protocol pub type ProjectsDetails = StorageMap<_, Blake2_128Concat, ProjectId, ProjectDetailsOf>; + #[pallet::storage] + pub type ProjectsInAuctionRound = StorageValue<_, WeakBoundedVec>, ValueQuery>; + #[pallet::storage] /// Keep track of the PLMC bonds made to each project by each evaluator pub type Evaluations = StorageNMap< @@ -425,41 +402,37 @@ pub mod pallet { #[pallet::storage] /// StorageMap containing the bids for each project and user - pub type Bids = StorageNMap< - _, - ( - NMapKey, - NMapKey>, - NMapKey, - ), - BidInfoOf, - >; + pub type Bids = + StorageNMap<_, (NMapKey, NMapKey), BidInfoOf>; + /// StorageMap containing the first bid that should be settled at a certain price point, and the last bid available at that price point. + /// Bids should be settled from the higest price first, and then from the lowest index first. Both indexes are inclusive. #[pallet::storage] - /// Contributions made during the Community and Remainder round. i.e token buys - pub type Contributions = StorageNMap< - _, - ( - NMapKey, - NMapKey>, - NMapKey, - ), - ContributionInfoOf, - >; + pub type BidBucketBounds = + StorageDoubleMap<_, Blake2_128Concat, ProjectId, Blake2_128Concat, PriceOf, (u32, u32), OptionQuery>; + + /// This map allows bidders to release their bid early if they were outbid. + /// The map contains the bucket price and bid index of the last bid to be outbid. + /// Indexes higher than the one stored here in the same bucket can be released. + /// All bids in buckets lower than the one stored here can also be released. + /// The last bid to be considered outbid might be partially rejected, and so that should be refunded by the new + /// bidder in the "bid" call. + #[pallet::storage] + pub type OutbidBidsCutoff = StorageMap<_, Blake2_128Concat, ProjectId, (PriceOf, u32), OptionQuery>; #[pallet::storage] - pub type AuctionBoughtUSD = - StorageNMap<_, (NMapKey, NMapKey), Balance, ValueQuery>; + pub type CTAmountOversubscribed = StorageMap<_, Blake2_128Concat, ProjectId, Balance, ValueQuery>; #[pallet::storage] - pub type ContributionBoughtUSD = + pub type AuctionBoughtUSD = StorageNMap<_, (NMapKey, NMapKey), Balance, ValueQuery>; #[pallet::storage] pub type UserMigrations = StorageNMap< _, (NMapKey, NMapKey>), - (MigrationStatus, BoundedVec>), + // We assume an upper bound of 10k migrations per user. This is not tracked, but is a sensible amount. + (MigrationStatus, WeakBoundedVec>), >; /// Counts how many participants have not yet migrated their CTs. Counter goes up on each settlement, and goes @@ -467,13 +440,6 @@ pub mod pallet { #[pallet::storage] pub type UnmigratedCounter = StorageMap<_, Blake2_128Concat, ProjectId, u32, ValueQuery>; - pub struct MaxParticipationsPerUser(PhantomData); - impl Get for MaxParticipationsPerUser { - fn get() -> u32 { - T::MaxBidsPerUser::get() + T::MaxEvaluationsPerUser::get() - } - } - #[pallet::storage] pub type ActiveMigrationQueue = StorageMap<_, Blake2_128Concat, QueryId, (ProjectId, T::AccountId)>; @@ -481,10 +447,6 @@ pub mod pallet { #[pallet::storage] pub type DidWithActiveProjects = StorageMap<_, Blake2_128Concat, Did, ProjectId, OptionQuery>; - #[pallet::storage] - pub type DidWithWinningBids = - StorageDoubleMap<_, Blake2_128Concat, ProjectId, Blake2_128Concat, Did, bool, ValueQuery>; - #[pallet::event] #[pallet::generate_deposit(pub (super) fn deposit_event)] pub enum Event { @@ -536,6 +498,7 @@ pub mod pallet { project_id: ProjectId, account: AccountIdOf, id: u32, + status: BidStatus, final_ct_amount: Balance, final_ct_usd_price: PriceOf, }, @@ -655,8 +618,6 @@ pub mod pallet { FundingAssetNotAccepted, /// The user already has the maximum number of participations in this project. TooManyUserParticipations, - /// The project already has the maximum number of participations. - TooManyProjectParticipations, /// The user is not allowed to use the selected multiplier. ForbiddenMultiplier, /// The user has a winning bid in the auction round and is not allowed to participate @@ -668,6 +629,12 @@ pub mod pallet { PolicyMismatch, /// Contribution tokens have all been sold ProjectSoldOut, + /// Tried to process an oversubscribed bid, but none remain. + NoBidsOversubscribed, + /// Tried to transition the project, but some oversubscribed bid are still pending to be processed. + OversubscribedBidsRemaining, + /// User has a partially accepted bid and needs to first process the rejected amount before settling the accepted amount. + RejectedAmountInPartialBidRemaining, // * An error related to the migration process. * /// Tried to start a migration check but the bidirectional channel is not yet open @@ -757,13 +724,14 @@ pub mod pallet { /// Bond PLMC for a project in the evaluation stage #[pallet::call_index(4)] - #[pallet::weight(WeightInfoOf::::evaluate(::MaxEvaluationsPerUser::get()))] + #[pallet::weight(WeightInfoOf::::start_settlement())] + // TODO: Change weight after benchmarks pub fn evaluate( origin: OriginFor, jwt: UntrustedToken, project_id: ProjectId, #[pallet::compact] usd_amount: Balance, - ) -> DispatchResultWithPostInfo { + ) -> DispatchResult { let (account, did, _investor_type, whitelisted_policy) = T::InvestorOrigin::ensure_origin(origin, &jwt, T::VerifierPublicKey::get())?; @@ -776,7 +744,8 @@ pub mod pallet { } #[pallet::call_index(40)] - #[pallet::weight(WeightInfoOf::::evaluate(::MaxEvaluationsPerUser::get()))] + #[pallet::weight(WeightInfoOf::::start_settlement())] + // TODO: Change weight after benchmarks pub fn evaluate_with_receiving_account( origin: OriginFor, jwt: UntrustedToken, @@ -784,7 +753,7 @@ pub mod pallet { #[pallet::compact] usd_amount: Balance, receiving_account: Junction, signature_bytes: [u8; 65], - ) -> DispatchResultWithPostInfo { + ) -> DispatchResult { let (account, did, _investor_type, whitelisted_policy) = T::InvestorOrigin::ensure_origin(origin, &jwt, T::VerifierPublicKey::get())?; @@ -802,14 +771,8 @@ pub mod pallet { /// Bid for a project in the Auction round #[pallet::call_index(7)] - #[pallet::weight( - WeightInfoOf::::bid( - ::MaxBidsPerUser::get(), - // Assuming the current bucket is full, and has a price higher than the minimum. - // This user is buying 100% of the bid allocation. - // Since each bucket has 10% of the allocation, one bid can be split into a max of 10 - 10 - ))] + #[pallet::weight(WeightInfoOf::::start_settlement())] + // TODO: Change weight after benchmarks pub fn bid( origin: OriginFor, jwt: UntrustedToken, @@ -817,7 +780,7 @@ pub mod pallet { #[pallet::compact] ct_amount: Balance, mode: ParticipationMode, funding_asset: AcceptedFundingAsset, - ) -> DispatchResultWithPostInfo { + ) -> DispatchResult { let (bidder, did, investor_type, whitelisted_policy) = T::InvestorOrigin::ensure_origin(origin, &jwt, T::VerifierPublicKey::get())?; @@ -842,14 +805,8 @@ pub mod pallet { } #[pallet::call_index(70)] - #[pallet::weight( - WeightInfoOf::::bid( - ::MaxBidsPerUser::get(), - // Assuming the current bucket is full, and has a price higher than the minimum. - // This user is buying 100% of the bid allocation. - // Since each bucket has 10% of the allocation, one bid can be split into a max of 10 - 10 - ))] + #[pallet::weight(WeightInfoOf::::start_settlement())] + // TODO: Change weight after benchmarks pub fn bid_with_receiving_account( origin: OriginFor, jwt: UntrustedToken, @@ -859,7 +816,7 @@ pub mod pallet { funding_asset: AcceptedFundingAsset, receiving_account: Junction, signature_bytes: [u8; 65], - ) -> DispatchResultWithPostInfo { + ) -> DispatchResult { let (bidder, did, investor_type, whitelisted_policy) = T::InvestorOrigin::ensure_origin(origin, &jwt, T::VerifierPublicKey::get())?; @@ -880,6 +837,13 @@ pub mod pallet { Self::do_bid(params) } + #[pallet::call_index(15)] + #[pallet::weight(WeightInfoOf::::settle_accepted_bid_with_refund())] + pub fn process_next_oversubscribed_bid(origin: OriginFor, project_id: ProjectId) -> DispatchResult { + let _caller = ensure_signed(origin)?; + Self::do_process_next_oversubscribed_bid(project_id) + } + #[pallet::call_index(10)] #[pallet::weight(WeightInfoOf::::end_funding_project_successful())] pub fn end_funding(origin: OriginFor, project_id: ProjectId) -> DispatchResult { @@ -910,15 +874,9 @@ pub mod pallet { #[pallet::call_index(13)] #[pallet::weight(WeightInfoOf::::settle_accepted_bid_with_refund())] - pub fn settle_bid( - origin: OriginFor, - project_id: ProjectId, - bidder: AccountIdOf, - bid_id: u32, - ) -> DispatchResult { + pub fn settle_bid(origin: OriginFor, project_id: ProjectId, bid_id: u32) -> DispatchResult { let _caller = ensure_signed(origin)?; - let bid = Bids::::get((project_id, bidder, bid_id)).ok_or(Error::::ParticipationNotFound)?; - Self::do_settle_bid(bid, project_id) + Self::do_settle_bid(project_id, bid_id) } #[pallet::call_index(18)] @@ -943,7 +901,8 @@ pub mod pallet { } #[pallet::call_index(20)] - #[pallet::weight(WeightInfoOf::::confirm_offchain_migration(MaxParticipationsPerUser::::get()))] + #[pallet::weight(WeightInfoOf::::start_settlement())] + // TODO: Change weight after benchmarks pub fn confirm_offchain_migration( origin: OriginFor, project_id: ProjectId, @@ -997,7 +956,8 @@ pub mod pallet { } #[pallet::call_index(24)] - #[pallet::weight(WeightInfoOf::::send_pallet_migration_for(MaxParticipationsPerUser::::get()))] + #[pallet::weight(WeightInfoOf::::start_settlement())] + // TODO: Change weight after benchmarks pub fn send_pallet_migration_for( origin: OriginFor, project_id: ProjectId, @@ -1008,7 +968,8 @@ pub mod pallet { } #[pallet::call_index(25)] - #[pallet::weight(WeightInfoOf::::confirm_pallet_migrations(MaxParticipationsPerUser::::get()))] + #[pallet::weight(WeightInfoOf::::start_settlement())] + // TODO: Change weight after benchmarks pub fn confirm_pallet_migrations( origin: OriginFor, query_id: QueryId, @@ -1027,6 +988,40 @@ pub mod pallet { Self::do_mark_project_ct_migration_as_finished(project_id) } } + + #[pallet::hooks] + impl Hooks> for Pallet { + fn on_idle(_n: BlockNumberFor, available_weight: Weight) -> Weight { + let projects = ProjectsInAuctionRound::::get(); + if projects.is_empty() { + return ::DbWeight::get().reads(1); + } + + let mut weight_consumed = ::DbWeight::get().reads(1); + let process_weight = ::DbWeight::get().reads_writes(1, 1); + + for project_id in projects { + loop { + // Check weight before processing each bid + if weight_consumed.saturating_add(process_weight).all_gt(available_weight) { + return weight_consumed; + } + + weight_consumed.saturating_accrue(process_weight); + match Self::do_process_next_oversubscribed_bid(project_id) { + // Returns Ok if a bid was processed successfully + Ok(_) => continue, + // Returns Err if there are no more bids to process, so we go to next project in the auction round + Err(_) => { + break; + }, + } + } + } + + weight_consumed + } + } } pub mod xcm_executor_impl { diff --git a/pallets/funding/src/mock.rs b/pallets/funding/src/mock.rs index 72d9a0054..977988a94 100644 --- a/pallets/funding/src/mock.rs +++ b/pallets/funding/src/mock.rs @@ -20,9 +20,7 @@ use super::*; use crate as pallet_funding; -use crate::runtime_api::{ - ExtrinsicHelpers, Leaderboards, ProjectInformation, ProjectParticipationIds, UserInformation, -}; +use crate::runtime_api::{ExtrinsicHelpers, Leaderboards, ProjectInformation, UserInformation}; use alloc::string::String; use core::ops::RangeInclusive; use frame_support::{ @@ -42,12 +40,13 @@ use sp_core::{ crypto::{Ss58AddressFormat, Ss58Codec}, ConstU8, H256, }; + use sp_runtime::{ traits::{BlakeTwo256, Convert, ConvertBack, ConvertInto, Get, IdentityLookup, TryConvert}, BuildStorage, Perquintill, }; use sp_std::collections::btree_map::BTreeMap; -use std::cell::RefCell; +use std::{cell::RefCell, marker::PhantomData}; use system::EnsureSigned; use xcm::v4::PalletInfo as XcmPalletInfo; use xcm_builder::{EnsureXcmOrigin, FixedWeightBounds, ParentIsPreset, SiblingParachainConvertsVia}; @@ -417,12 +416,7 @@ impl Config for TestRuntime { type FundingCurrency = ForeignAssets; type FundingSuccessThreshold = FundingSuccessThreshold; type InvestorOrigin = EnsureInvestor; - type MaxBidsPerProject = ConstU32<512>; - type MaxBidsPerUser = ConstU32<25>; type MaxCapacityThresholds = MaxCapacityThresholds; - type MaxContributionsPerUser = ConstU32<25>; - type MaxEvaluationsPerProject = ConstU32<512>; - type MaxEvaluationsPerUser = ConstU32<4>; type MaxMessageSizeThresholds = MaxMessageSizeThresholds; type MinUsdPerEvaluation = MinUsdPerEvaluation; type Multiplier = Multiplier; @@ -559,10 +553,6 @@ sp_api::mock_impl_runtime_apis! { PolimecFunding::top_bids(project_id, amount) } - fn top_contributions(project_id: ProjectId, amount: u32) -> Vec> { - PolimecFunding::top_contributions(project_id, amount) - } - fn top_projects_by_usd_raised(amount: u32) -> Vec<(ProjectId, ProjectMetadataOf, ProjectDetailsOf)> { PolimecFunding::top_projects_by_usd_raised(amount) } @@ -576,10 +566,6 @@ sp_api::mock_impl_runtime_apis! { fn contribution_tokens(account: AccountId) -> Vec<(ProjectId, Balance)> { PolimecFunding::contribution_tokens(account) } - - fn all_project_participations_by_did(project_id: ProjectId, did: Did) -> Vec> { - PolimecFunding::all_project_participations_by_did(project_id, did) - } } impl ProjectInformation for TestRuntime { @@ -616,7 +602,5 @@ sp_api::mock_impl_runtime_apis! { fn get_message_to_sign_by_receiving_account(project_id: ProjectId, polimec_account: AccountId) -> Option { PolimecFunding::get_message_to_sign_by_receiving_account(project_id, polimec_account) } - - } } diff --git a/pallets/funding/src/runtime_api.rs b/pallets/funding/src/runtime_api.rs index c3941b166..10e06b45b 100644 --- a/pallets/funding/src/runtime_api.rs +++ b/pallets/funding/src/runtime_api.rs @@ -3,21 +3,12 @@ use crate::{traits::BondingRequirementCalculation, *}; use alloc::{collections::BTreeMap, string::String}; use frame_support::traits::fungibles::{Inspect, InspectEnumerable}; use itertools::Itertools; -use parity_scale_codec::{Decode, Encode}; use polimec_common::{assets::AcceptedFundingAsset, credentials::InvestorType, ProvideAssetPrice, USD_DECIMALS}; -use scale_info::TypeInfo; use sp_core::Get; use sp_runtime::traits::Zero; -#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, TypeInfo)] -pub struct ProjectParticipationIds { - account: AccountIdOf, - evaluation_ids: Vec, - bid_ids: Vec, - contribution_ids: Vec, -} sp_api::decl_runtime_apis! { - #[api_version(1)] + #[api_version(2)] pub trait Leaderboards { /// Get the top evaluations made for a project by the amount of PLMC bonded fn top_evaluations(project_id: ProjectId, amount: u32) -> Vec>; @@ -25,9 +16,6 @@ sp_api::decl_runtime_apis! { /// Get the top bids for a project by the amount of CTs bought. fn top_bids(project_id: ProjectId, amount: u32) -> Vec>; - /// Get the top contributions for a project by the amount of CTs bought. - fn top_contributions(project_id: ProjectId, amount: u32) -> Vec>; - /// Get the top projects by the absolute USD value raised fn top_projects_by_usd_raised(amount: u32) -> Vec<(ProjectId, ProjectMetadataOf, ProjectDetailsOf)>; @@ -39,9 +27,6 @@ sp_api::decl_runtime_apis! { pub trait UserInformation { /// Get all the contribution token balances for the participated projects fn contribution_tokens(account: AccountIdOf) -> Vec<(ProjectId, Balance)>; - - /// Get all the project participations made by a single DID. - fn all_project_participations_by_did(project_id: ProjectId, did: Did) -> Vec>; } #[api_version(1)] @@ -75,7 +60,6 @@ sp_api::decl_runtime_apis! { /// Gets the hex encoded bytes of the message needed to be signed by the receiving account to participate in the project. /// The message will first be prefixed with a blockchain-dependent string, then hashed, and then signed. fn get_message_to_sign_by_receiving_account(project_id: ProjectId, polimec_account: AccountIdOf) -> Option; - } } @@ -89,14 +73,11 @@ impl Pallet { pub fn top_bids(project_id: ProjectId, amount: u32) -> Vec> { Bids::::iter_prefix_values((project_id,)) - .sorted_by(|a, b| b.final_ct_amount().cmp(&a.final_ct_amount())) - .take(amount as usize) - .collect_vec() - } - - pub fn top_contributions(project_id: ProjectId, amount: u32) -> Vec> { - Contributions::::iter_prefix_values((project_id,)) - .sorted_by(|a, b| b.ct_amount.cmp(&a.ct_amount)) + .sorted_by(|a, b| { + let usd_ticket_a = a.original_ct_usd_price.saturating_mul_int(a.original_ct_amount); + let usd_ticket_b = b.original_ct_usd_price.saturating_mul_int(b.original_ct_amount); + usd_ticket_b.cmp(&usd_ticket_a) + }) .take(amount as usize) .collect_vec() } @@ -177,7 +158,7 @@ impl Pallet { let usd_spent = bucket_price.saturating_mul_int(ct_to_buy).max(One::one()); usd_to_spend = usd_to_spend.saturating_sub(usd_spent); - current_bucket.update(ct_to_buy) + current_bucket.update(ct_to_buy); } } @@ -225,7 +206,7 @@ impl Pallet { let usd_spent = bucket_price.saturating_mul_int(ct_to_buy).max(One::one()); usd_to_spend = usd_to_spend.saturating_sub(usd_spent); - current_bucket.update(ct_to_buy) + current_bucket.update(ct_to_buy); } } @@ -336,47 +317,6 @@ impl Pallet { Pallet::::get_substrate_message_to_sign(polimec_account, project_id) } - 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) - .map(|((account_id, evaluation_id), _evaluation)| (account_id, evaluation_id)) - .collect_vec(); - - let bids = Bids::::iter_prefix((project_id,)) - .filter(|((_account_id, _bid_id), bid)| bid.did == did) - .map(|((account_id, bid_id), _bid)| (account_id, bid_id)) - .collect_vec(); - - let contributions = Contributions::::iter_prefix((project_id,)) - .filter(|((_account_id, _contribution_id), contribution)| contribution.did == did) - .map(|((account_id, contribution_id), _contribution)| (account_id, contribution_id)) - .collect_vec(); - - #[allow(clippy::type_complexity)] - let mut map: BTreeMap, (Vec, Vec, Vec)> = BTreeMap::new(); - - for (account_id, evaluation_id) in evaluations { - map.entry(account_id).or_insert_with(|| (Vec::new(), Vec::new(), Vec::new())).0.push(evaluation_id); - } - - for (account_id, bid_id) in bids { - map.entry(account_id).or_insert_with(|| (Vec::new(), Vec::new(), Vec::new())).1.push(bid_id); - } - - for (account_id, contribution_id) in contributions { - map.entry(account_id).or_insert_with(|| (Vec::new(), Vec::new(), Vec::new())).2.push(contribution_id); - } - - map.into_iter() - .map(|(account, (evaluation_ids, bid_ids, contribution_ids))| ProjectParticipationIds { - account, - evaluation_ids, - bid_ids, - contribution_ids, - }) - .collect() - } - pub fn usd_target_percent_reached(project_id: ProjectId) -> FixedU128 { let project_details = ProjectsDetails::::get(project_id).expect("Project not found"); let funding_reached = project_details.funding_amount_reached_usd; diff --git a/pallets/funding/src/storage_migrations.rs b/pallets/funding/src/storage_migrations.rs index 94b27ab2a..1c060b9ef 100644 --- a/pallets/funding/src/storage_migrations.rs +++ b/pallets/funding/src/storage_migrations.rs @@ -1,145 +1,7 @@ //! A module that is responsible for migration of storage. -use crate::{ - AccountIdOf, BiddingTicketSizes, Config, CurrencyMetadata, FixedPointNumber, ParticipantsAccountType, PriceOf, - ProjectMetadataOf, StringLimitOf, -}; -use core::marker::PhantomData; -use frame_support::traits::{StorageVersion, UncheckedOnRuntimeUpgrade}; -use polimec_common::{assets::AcceptedFundingAsset, credentials::Cid}; -use scale_info::TypeInfo; -use serde::{Deserialize, Serialize}; -use sp_core::{ConstU32, Decode, Encode, Get, MaxEncodedLen, RuntimeDebug}; -use sp_runtime::{BoundedVec, Percent}; +use frame_support::traits::StorageVersion; extern crate alloc; -use alloc::vec::Vec; -use polimec_common::migration_types::{MigrationInfo, ParticipationType}; -use xcm::v4::Location; /// The current storage version -pub const STORAGE_VERSION: StorageVersion = StorageVersion::new(6); +pub const STORAGE_VERSION: StorageVersion = StorageVersion::new(7); pub const LOG: &str = "runtime::funding::migration"; - -pub mod v5_storage_items { - - use super::*; - #[derive(Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug, MaxEncodedLen, TypeInfo, Serialize, Deserialize)] - pub struct OldProjectMetadata { - /// Token Metadata - pub token_information: CurrencyMetadata, - /// Mainnet Token Max Supply - pub mainnet_token_max_supply: Balance, - /// Total allocation of Contribution Tokens available for the Funding Round. - pub total_allocation_size: Balance, - /// Percentage of the total allocation of Contribution Tokens available for the Auction Round - pub auction_round_allocation_percentage: Percent, - /// The minimum price per token in USD, decimal-aware. See [`calculate_decimals_aware_price()`](crate::traits::ProvideAssetPrice::calculate_decimals_aware_price) for more information. - pub minimum_price: Price, - /// Maximum and minimum ticket sizes for auction round - pub bidding_ticket_sizes: BiddingTicketSizes, - /// Participation currencies (e.g stablecoin, DOT, KSM) - /// e.g. https://github.com/paritytech/substrate/blob/427fd09bcb193c1e79dec85b1e207c718b686c35/frame/uniques/src/types.rs#L110 - /// For now is easier to handle the case where only just one Currency is accepted - pub participation_currencies: - BoundedVec>, - pub funding_destination_account: AccountId, - /// Additional metadata - pub policy_ipfs_cid: Option, - } - - #[derive(Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug, TypeInfo, MaxEncodedLen)] - pub struct OldMigrationOrigin { - pub user: Location, - pub id: u32, - pub participation_type: ParticipationType, - } - impl PartialOrd for OldMigrationOrigin { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } - } - impl Ord for OldMigrationOrigin { - fn cmp(&self, other: &Self) -> core::cmp::Ordering { - if self.participation_type == other.participation_type { - self.id.cmp(&other.id) - } else { - self.participation_type.cmp(&other.participation_type) - } - } - } - #[derive(Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug, TypeInfo, MaxEncodedLen)] - pub struct OldMigration { - pub origin: OldMigrationOrigin, - pub info: MigrationInfo, - } -} - -pub mod v6 { - use super::*; - use crate::{storage_migrations::v5_storage_items::OldMigration, MaxParticipationsPerUser}; - use polimec_common::migration_types::{Migration, MigrationOrigin, MigrationStatus}; - - type OldProjectMetadataOf = super::v5_storage_items::OldProjectMetadata< - BoundedVec>, - ::Balance, - PriceOf, - AccountIdOf, - Cid, - >; - - pub struct UncheckedMigrationToV6(PhantomData); - impl UncheckedOnRuntimeUpgrade for UncheckedMigrationToV6 { - fn on_runtime_upgrade() -> frame_support::weights::Weight { - let mut items = 0; - log::info!("Starting migration to V5"); - let translate_project_details = |_key, item: OldProjectMetadataOf| -> Option> { - items += 1; - - Some(ProjectMetadataOf:: { - token_information: item.token_information, - mainnet_token_max_supply: item.mainnet_token_max_supply, - total_allocation_size: item.total_allocation_size, - minimum_price: item.minimum_price, - bidding_ticket_sizes: item.bidding_ticket_sizes, - participation_currencies: item.participation_currencies, - funding_destination_account: item.funding_destination_account, - policy_ipfs_cid: item.policy_ipfs_cid, - participants_account_type: ParticipantsAccountType::Polkadot, - }) - }; - crate::ProjectsMetadata::::translate(translate_project_details); - - let translate_migration = - |_keys, - (status, migrations): (MigrationStatus, BoundedVec>)| - -> Option<(MigrationStatus, BoundedVec>)> { - let old_migrations = migrations.to_vec(); - let mut new_migrations = Vec::new(); - - for mut old_migration in old_migrations { - items += 1; - let origin_junction = old_migration.origin.user.interior.take_first().unwrap(); - let new_origin = MigrationOrigin { - user: origin_junction, - id: old_migration.origin.id, - participation_type: old_migration.origin.participation_type, - }; - new_migrations.push(Migration { origin: new_origin, info: old_migration.info }); - } - let new_migrations = new_migrations.try_into().ok()?; - Some((status, new_migrations)) - }; - crate::UserMigrations::::translate(translate_migration); - - log::info!("Migration to V5 completed. Migrated {} items", items); - T::DbWeight::get().reads_writes(items, items) - } - } - - pub type MigrationToV6 = frame_support::migrations::VersionedMigration< - 5, - 6, - UncheckedMigrationToV6, - crate::Pallet, - ::DbWeight, - >; -} diff --git a/pallets/funding/src/tests/1_application.rs b/pallets/funding/src/tests/1_application.rs index 51c73e2f4..3992d6e10 100644 --- a/pallets/funding/src/tests/1_application.rs +++ b/pallets/funding/src/tests/1_application.rs @@ -133,7 +133,7 @@ mod create_project_extrinsic { let failing_bids = vec![(BIDDER_1, Professional, 1000 * CT_UNIT).into(), (BIDDER_2, Retail, 1000 * CT_UNIT).into()]; let successful_evaluations = inst.generate_successful_evaluations(project_metadata.clone(), 5); - let successful_bids = inst.generate_bids_from_total_ct_percent(project_metadata.clone(), 90, 5); + let successful_bids = inst.generate_bids_from_total_ct_percent(project_metadata.clone(), 90, 10); let accounts = vec![ vec![ISSUER_1], @@ -914,7 +914,7 @@ mod edit_project_extrinsic { bidding_ticket_sizes: BiddingTicketSizes { professional: TicketSize::new(10_000 * USD_UNIT, Some(20_000 * USD_UNIT)), institutional: TicketSize::new(20_000 * USD_UNIT, Some(30_000 * USD_UNIT)), - retail: TicketSize::new(10 * USD_UNIT, None), + retail: TicketSize::new(100 * USD_UNIT, None), phantom: Default::default(), }, participation_currencies: vec![AcceptedFundingAsset::USDT, AcceptedFundingAsset::USDC] diff --git a/pallets/funding/src/tests/2_evaluation.rs b/pallets/funding/src/tests/2_evaluation.rs index 561a39f2e..a8cdcb155 100644 --- a/pallets/funding/src/tests/2_evaluation.rs +++ b/pallets/funding/src/tests/2_evaluation.rs @@ -831,63 +831,6 @@ mod evaluate_extrinsic { assert_err!(dispatch_error, TokenError::FundsUnavailable) } - #[test] - fn cannot_evaluate_more_than_project_limit() { - let mut inst = MockInstantiator::new(Some(RefCell::new(new_test_ext()))); - let project_metadata = default_project_metadata(ISSUER_1); - let evaluations = (0u32..::MaxEvaluationsPerProject::get()) - .map(|i| EvaluationParams::::from((i as u64 + 420, 100u128 * CT_UNIT))) - .collect_vec(); - let failing_evaluation = EvaluationParams::from((EVALUATOR_1, 1000 * CT_UNIT)); - - let project_id = inst.create_evaluating_project(project_metadata.clone(), ISSUER_1, None); - - let plmc_for_evaluating = inst.calculate_evaluation_plmc_spent(evaluations.clone()); - - inst.mint_plmc_ed_if_required(plmc_for_evaluating.accounts()); - inst.mint_plmc_to(plmc_for_evaluating.clone()); - - inst.evaluate_for_users(project_id, evaluations.clone()).unwrap(); - - let plmc_for_failing_evaluating = inst.calculate_evaluation_plmc_spent(vec![failing_evaluation.clone()]); - - inst.mint_plmc_to(plmc_for_failing_evaluating.clone()); - - assert_err!( - inst.evaluate_for_users(project_id, vec![failing_evaluation]), - Error::::TooManyProjectParticipations - ); - } - - #[test] - fn cannot_evaluate_more_than_user_limit() { - let mut inst = MockInstantiator::new(Some(RefCell::new(new_test_ext()))); - let project_metadata = default_project_metadata(ISSUER_1); - let evaluations = (0u32..::MaxEvaluationsPerUser::get()) - .map(|_| EvaluationParams::::from((EVALUATOR_1, 100u128 * USD_UNIT))) - .collect_vec(); - let failing_evaluation = EvaluationParams::from((EVALUATOR_1, 100 * USD_UNIT)); - - let project_id = inst.create_evaluating_project(project_metadata.clone(), ISSUER_1, None); - - let plmc_for_evaluating = inst.calculate_evaluation_plmc_spent(evaluations.clone()); - let plmc_existential_deposits = evaluations.accounts().existential_deposits(); - - inst.mint_plmc_to(plmc_for_evaluating.clone()); - inst.mint_plmc_to(plmc_existential_deposits.clone()); - - inst.evaluate_for_users(project_id, evaluations.clone()).unwrap(); - - let plmc_for_failing_evaluating = inst.calculate_evaluation_plmc_spent(vec![failing_evaluation.clone()]); - - inst.mint_plmc_to(plmc_for_failing_evaluating.clone()); - - assert_err!( - inst.evaluate_for_users(project_id, vec![failing_evaluation]), - Error::::TooManyUserParticipations - ); - } - #[test] fn cannot_use_balance_on_hold() { let mut inst = MockInstantiator::new(Some(RefCell::new(new_test_ext()))); diff --git a/pallets/funding/src/tests/3_auction.rs b/pallets/funding/src/tests/3_auction.rs index 4db22179f..ffd551865 100644 --- a/pallets/funding/src/tests/3_auction.rs +++ b/pallets/funding/src/tests/3_auction.rs @@ -1,4 +1,5 @@ use super::*; +use crate::ParticipationMode::*; use frame_support::traits::{fungible::InspectFreeze, fungibles::metadata::Inspect}; use polimec_common::assets::{AcceptedFundingAsset, AcceptedFundingAsset::USDT}; use sp_core::bounded_vec; @@ -19,7 +20,7 @@ mod round_flow { let mut inst = MockInstantiator::new(Some(RefCell::new(new_test_ext()))); let project_metadata = default_project_metadata(ISSUER_1); let evaluations = inst.generate_successful_evaluations(project_metadata.clone(), 5); - let bids = inst.generate_bids_from_total_ct_percent(project_metadata.clone(), 60, 5); + let bids = inst.generate_bids_from_total_ct_percent(project_metadata.clone(), 60, 10); let _project_id = inst.create_finished_project(project_metadata, ISSUER_1, None, evaluations, bids); } @@ -31,7 +32,7 @@ mod round_flow { let project3 = default_project_metadata(ISSUER_3); let project4 = default_project_metadata(ISSUER_4); let evaluations = inst.generate_successful_evaluations(project1.clone(), 5); - let bids = inst.generate_bids_from_total_ct_percent(project1.clone(), 60, 5); + let bids = inst.generate_bids_from_total_ct_percent(project1.clone(), 60, 10); inst.create_finished_project(project1, ISSUER_1, None, evaluations.clone(), bids.clone()); inst.create_finished_project(project2, ISSUER_2, None, evaluations.clone(), bids.clone()); @@ -39,41 +40,6 @@ mod round_flow { inst.create_finished_project(project4, ISSUER_4, None, evaluations, bids); } - #[test] - fn auction_gets_percentage_of_ct_total_allocation() { - let mut inst = MockInstantiator::new(Some(RefCell::new(new_test_ext()))); - let project_metadata = default_project_metadata(ISSUER_1); - let evaluations = inst.generate_successful_evaluations(project_metadata.clone(), 5); - let total_allocation = project_metadata.total_allocation_size; - let auction_allocation = total_allocation; - - let bids = vec![(BIDDER_1, Retail, auction_allocation).into()]; - let project_id = - inst.create_finished_project(project_metadata.clone(), ISSUER_1, None, evaluations.clone(), bids); - let mut bid_infos = Bids::::iter_prefix_values((project_id,)); - let bid_info = inst.execute(|| bid_infos.next().unwrap()); - assert!(inst.execute(|| bid_infos.next().is_none())); - assert_eq!(bid_info.original_ct_amount, auction_allocation); - - let project_metadata = default_project_metadata(ISSUER_2); - let bids = - vec![(BIDDER_1, Retail, auction_allocation).into(), (BIDDER_1, Institutional, 1000 * CT_UNIT).into()]; - let project_id = - inst.create_finished_project(project_metadata.clone(), ISSUER_2, None, evaluations.clone(), bids); - let project_details = inst.get_project_details(project_id); - - let bid_info_1 = inst.execute(|| Bids::::get((project_id, BIDDER_1, 1)).unwrap()); - let bid_info_2 = inst.execute(|| Bids::::get((project_id, BIDDER_1, 2)).unwrap()); - assert!(inst.execute(|| bid_infos.next().is_none())); - assert_eq!( - bid_info_1.status, - BidStatus::PartiallyAccepted(auction_allocation - 1000 * CT_UNIT), - "Should not be able to buy more than auction allocation" - ); - assert_eq!(bid_info_2.status, BidStatus::Accepted, "Should outbid the previous bid"); - assert_eq!(project_details.remaining_contribution_tokens, total_allocation - auction_allocation); - } - #[test] fn no_bids_made() { let mut inst = MockInstantiator::new(Some(RefCell::new(new_test_ext()))); @@ -229,61 +195,149 @@ mod round_flow { } #[test] - fn all_bids_but_one_have_price_higher_than_wap() { + fn auction_oversubscription() { let mut inst = MockInstantiator::new(Some(RefCell::new(new_test_ext()))); - let total_allocation = 10_000_000 * CT_UNIT; - let min_bid_ct = 500 * CT_UNIT; // 5k USD at 10USD/CT - let max_bids_per_project: u32 = ::MaxBidsPerProject::get(); - let big_bid: BidParams = (BIDDER_1, Institutional, total_allocation).into(); - let small_bids: Vec> = - (0..max_bids_per_project - 1).map(|i| (i as u64 + BIDDER_1, Retail, min_bid_ct).into()).collect(); - let all_bids = vec![vec![big_bid.clone()], small_bids.clone()].into_iter().flatten().collect_vec(); - - let mut project_metadata = default_project_metadata(ISSUER_1); - project_metadata.mainnet_token_max_supply = total_allocation; - project_metadata.total_allocation_size = total_allocation; + let project_metadata = default_project_metadata(ISSUER_1); + let bucket = Pallet::::create_bucket_from_metadata(&project_metadata).unwrap(); + let bids = inst.generate_bids_that_take_price_to( + project_metadata.clone(), + project_metadata.minimum_price + bucket.delta_price * FixedU128::from_float(3.0), + ); let project_id = inst.create_finished_project( project_metadata.clone(), ISSUER_1, None, inst.generate_successful_evaluations(project_metadata.clone(), 5), - all_bids, + bids, ); let wap = inst.get_project_details(project_id).weighted_average_price.unwrap(); + assert!(wap > project_metadata.minimum_price); + } - let all_bids = inst.execute(|| Bids::::iter_prefix_values((project_id,)).collect_vec()); + #[test] + fn on_idle_clears_oversubscribed_bids() { + let mut inst = MockInstantiator::new(Some(RefCell::new(new_test_ext()))); + let project_metadata = default_project_metadata(ISSUER_1); + let evaluations = inst.generate_successful_evaluations(project_metadata.clone(), 5); + let bids = inst.generate_bids_from_total_ct_percent(project_metadata.clone(), 100, 10); + + let project_id = inst.create_auctioning_project(project_metadata.clone(), ISSUER_1, None, evaluations); + inst.mint_necessary_tokens_for_bids(project_id, bids.clone()); + inst.bid_for_users(project_id, bids.clone()).unwrap(); - let higher_than_wap_bids = all_bids.iter().filter(|bid| bid.original_ct_usd_price > wap).collect_vec(); - assert_eq!(higher_than_wap_bids.len(), (max_bids_per_project - 1u32) as usize); + // Check full rejection of one bid by another + let last_bid_amount = bids[9].amount; + let oversubscribed_bid = + BidParams::::from((BIDDER_1, Retail, last_bid_amount, Classic(1u8), USDT)); + inst.mint_necessary_tokens_for_bids(project_id, vec![oversubscribed_bid.clone()]); + inst.bid_for_users(project_id, vec![oversubscribed_bid.clone()]).unwrap(); + assert_eq!(inst.execute(|| CTAmountOversubscribed::::get(project_id)), last_bid_amount); + inst.advance_time(1); + let rejected_bid = inst.execute(|| Bids::::get((project_id, 9)).unwrap()); + assert_eq!(inst.execute(|| CTAmountOversubscribed::::get(project_id)), Zero::zero()); + assert_eq!(rejected_bid.status, BidStatus::Rejected); + let yet_unknown_bid = inst.execute(|| Bids::::get((project_id, 8)).unwrap()); + assert_eq!(yet_unknown_bid.status, BidStatus::YetUnknown); + + // Check multiple bid rejections by one bid + let multiple_bids_amount = bids[8].amount + bids[7].amount + bids[6].amount; + let multiple_bids = + BidParams::::from((BIDDER_1, Retail, multiple_bids_amount, Classic(1u8), USDT)); + inst.mint_necessary_tokens_for_bids(project_id, vec![multiple_bids.clone()]); + inst.bid_for_users(project_id, vec![multiple_bids.clone()]).unwrap(); + assert_eq!(inst.execute(|| CTAmountOversubscribed::::get(project_id)), multiple_bids_amount); + inst.advance_time(1); + let rejected_bid_1 = inst.execute(|| Bids::::get((project_id, 8)).unwrap()); + let rejected_bid_2 = inst.execute(|| Bids::::get((project_id, 7)).unwrap()); + let rejected_bid_3 = inst.execute(|| Bids::::get((project_id, 6)).unwrap()); + assert_eq!(inst.execute(|| CTAmountOversubscribed::::get(project_id)), Zero::zero()); + assert_eq!(rejected_bid_1.status, BidStatus::Rejected); + assert_eq!(rejected_bid_2.status, BidStatus::Rejected); + assert_eq!(rejected_bid_3.status, BidStatus::Rejected); + let yet_unknown_bid = inst.execute(|| Bids::::get((project_id, 5)).unwrap()); + assert_eq!(yet_unknown_bid.status, BidStatus::YetUnknown); + + // Check partial rejection of one bid by another + let partial_bid_amount = last_bid_amount / 2; + let partial_bid = + BidParams::::from((BIDDER_1, Retail, partial_bid_amount, Classic(1u8), USDT)); + inst.mint_necessary_tokens_for_bids(project_id, vec![partial_bid.clone()]); + inst.bid_for_users(project_id, vec![partial_bid.clone()]).unwrap(); + assert_eq!(inst.execute(|| CTAmountOversubscribed::::get(project_id)), partial_bid_amount); + inst.advance_time(1); + let rejected_bid = inst.execute(|| Bids::::get((project_id, 5)).unwrap()); + assert_eq!(inst.execute(|| CTAmountOversubscribed::::get(project_id)), Zero::zero()); + assert_eq!(rejected_bid.status, BidStatus::PartiallyAccepted(last_bid_amount - partial_bid_amount)); + let yet_unknown_bid = inst.execute(|| Bids::::get((project_id, 4)).unwrap()); + assert_eq!(yet_unknown_bid.status, BidStatus::YetUnknown); } #[test] - fn auction_oversubscription() { + fn on_idle_clears_multiple_oversubscribed_projects() { let mut inst = MockInstantiator::new(Some(RefCell::new(new_test_ext()))); - let project_metadata = default_project_metadata(ISSUER_1); - let auction_allocation = project_metadata.total_allocation_size; - let bucket_size = Percent::from_percent(10) * auction_allocation; - let bids = vec![ - (BIDDER_1, auction_allocation).into(), - (BIDDER_2, bucket_size).into(), - (BIDDER_3, bucket_size).into(), - (BIDDER_4, bucket_size).into(), - (BIDDER_5, bucket_size).into(), - (BIDDER_6, bucket_size).into(), - ]; - let project_id = inst.create_finished_project( - project_metadata.clone(), - ISSUER_1, - None, - inst.generate_successful_evaluations(project_metadata.clone(), 5), - bids, - ); + // Create two projects with different metadata + let project_metadata_1 = default_project_metadata(ISSUER_1); + let project_metadata_2 = default_project_metadata(ISSUER_2); - let wap = inst.get_project_details(project_id).weighted_average_price.unwrap(); - assert!(wap > project_metadata.minimum_price); + // Generate evaluations and bids for both projects + let evaluations_1 = inst.generate_successful_evaluations(project_metadata_1.clone(), 5); + let evaluations_2 = inst.generate_successful_evaluations(project_metadata_2.clone(), 5); + let bids_1 = inst.generate_bids_from_total_ct_percent(project_metadata_1.clone(), 100, 5); + let bids_2 = inst.generate_bids_from_total_ct_percent(project_metadata_2.clone(), 100, 5); + + // Create two projects in auctioning state + let project_id_1 = + inst.create_auctioning_project(project_metadata_1.clone(), ISSUER_1, None, evaluations_1); + let project_id_2 = + inst.create_auctioning_project(project_metadata_2.clone(), ISSUER_2, None, evaluations_2); + + // Place initial bids for both projects + inst.mint_necessary_tokens_for_bids(project_id_1, bids_1.clone()); + inst.mint_necessary_tokens_for_bids(project_id_2, bids_2.clone()); + inst.bid_for_users(project_id_1, bids_1.clone()).unwrap(); + inst.bid_for_users(project_id_2, bids_2.clone()).unwrap(); + + // Create oversubscribed bids for both projects + let oversubscribed_bid_1 = + BidParams::::from((BIDDER_1, Retail, bids_1[4].amount, Classic(1u8), USDT)); + let oversubscribed_bid_2 = + BidParams::::from((BIDDER_2, Retail, bids_2[4].amount, Classic(1u8), USDT)); + + // Place oversubscribed bids + inst.mint_necessary_tokens_for_bids(project_id_1, vec![oversubscribed_bid_1.clone()]); + inst.mint_necessary_tokens_for_bids(project_id_2, vec![oversubscribed_bid_2.clone()]); + inst.bid_for_users(project_id_1, vec![oversubscribed_bid_1.clone()]).unwrap(); + inst.bid_for_users(project_id_2, vec![oversubscribed_bid_2.clone()]).unwrap(); + + // Verify both projects are oversubscribed + assert_eq!(inst.execute(|| CTAmountOversubscribed::::get(project_id_1)), bids_1[4].amount); + assert_eq!(inst.execute(|| CTAmountOversubscribed::::get(project_id_2)), bids_2[4].amount); + + // Advance time to trigger on_idle + inst.advance_time(1); + + // Verify oversubscribed amounts are cleared for both projects + assert_eq!(inst.execute(|| CTAmountOversubscribed::::get(project_id_1)), Zero::zero()); + assert_eq!(inst.execute(|| CTAmountOversubscribed::::get(project_id_2)), Zero::zero()); + + // Verify bid statuses for both projects + let rejected_bid_1 = inst.execute(|| Bids::::get((project_id_1, 4)).unwrap()); + let rejected_bid_2 = inst.execute(|| Bids::::get((project_id_2, 9)).unwrap()); + assert_eq!(rejected_bid_1.status, BidStatus::Rejected); + assert_eq!(rejected_bid_2.status, BidStatus::Rejected); + + let yet_unknown_bid_1 = inst.execute(|| Bids::::get((project_id_1, 3)).unwrap()); + let yet_unknown_bid_2 = inst.execute(|| Bids::::get((project_id_2, 8)).unwrap()); + assert_eq!(yet_unknown_bid_1.status, BidStatus::YetUnknown); + assert_eq!(yet_unknown_bid_2.status, BidStatus::YetUnknown); + + inst.go_to_next_state(project_id_1); + inst.go_to_next_state(project_id_2); + + assert!(inst.execute(|| ProjectsInAuctionRound::::get()).to_vec().is_empty()) } } } @@ -295,7 +349,7 @@ mod bid_extrinsic { #[cfg(test)] mod success { use super::*; - use frame_support::pallet_prelude::DispatchResultWithPostInfo; + use frame_support::dispatch::DispatchResult; #[test] fn evaluation_bond_counts_towards_bid() { @@ -350,8 +404,7 @@ mod bid_extrinsic { assert_eq!(evaluation_items.len(), 1); assert_eq!(evaluation_items[0].current_plmc_bond, already_bonded_plmc - usable_evaluation_plmc); - let bid_items = - inst.execute(|| Bids::::iter_prefix_values((project_id, evaluator_bidder)).collect_vec()); + let bid_items = inst.execute(|| Bids::::iter_prefix_values((project_id,)).collect_vec()); assert_eq!(bid_items.len(), 1); assert_eq!(bid_items[0].plmc_bond, necessary_plmc_for_bid); @@ -472,7 +525,7 @@ mod bid_extrinsic { bidder: AccountIdOf, investor_type: InvestorType, u8_multiplier: u8, - ) -> DispatchResultWithPostInfo { + ) -> DispatchResult { let project_policy = inst.get_project_metadata(project_id).policy_ipfs_cid.unwrap(); let jwt = get_mock_jwt_with_cid(bidder, investor_type, generate_did_from_account(BIDDER_1), project_policy); let amount = 1000 * CT_UNIT; @@ -702,7 +755,7 @@ mod bid_extrinsic { assert_eq!(inst.go_to_next_state(project_id), ProjectStatus::SettlementStarted(FundingOutcome::Failure)); inst.execute(|| { - PolimecFunding::settle_bid(RuntimeOrigin::signed(BIDDER_4), project_id, BIDDER_4, 0).unwrap(); + PolimecFunding::settle_bid(RuntimeOrigin::signed(BIDDER_4), project_id, 0).unwrap(); }); let free_balance = inst.get_free_plmc_balance_for(BIDDER_4); @@ -718,7 +771,16 @@ mod bid_extrinsic { fn can_bid_with_frozen_tokens_funding_success() { let mut inst = MockInstantiator::new(Some(RefCell::new(new_test_ext()))); let issuer = ISSUER_1; - let project_metadata = default_project_metadata(issuer); + let mut project_metadata = default_project_metadata(issuer); + let base_price = PriceOf::::from_float(1.0); + let decimal_aware_price = ::PriceProvider::calculate_decimals_aware_price( + base_price, + USD_DECIMALS, + CT_DECIMALS, + ) + .unwrap(); + project_metadata.minimum_price = decimal_aware_price; + let evaluations = inst.generate_successful_evaluations(project_metadata.clone(), 5); let project_id = inst.create_auctioning_project(project_metadata.clone(), issuer, None, evaluations); @@ -786,7 +848,7 @@ mod bid_extrinsic { assert_eq!(frozen_balance, frozen_amount); inst.execute(|| { - PolimecFunding::settle_bid(RuntimeOrigin::signed(BIDDER_4), project_id, BIDDER_4, 0).unwrap(); + PolimecFunding::settle_bid(RuntimeOrigin::signed(BIDDER_4), project_id, 0).unwrap(); }); let free_balance = inst.get_free_plmc_balance_for(BIDDER_4); @@ -856,17 +918,17 @@ mod bid_extrinsic { USDT_PARTICIPATION, ) }); + // USDT has the same decimals and price as our baseline USD let expected_plmc_bond = >::calculate_plmc_bond(USDT_PARTICIPATION, otm_multiplier).unwrap(); - let otm_escrow_account = - ::RootId::get().into_sub_account_truncating(project_id); + let otm_escrow_account = pallet_proxy_bonding::Pallet::::get_bonding_account(project_id); let otm_treasury_account = ::Treasury::get(); let otm_fee_recipient_account = ::FeeRecipient::get(); let funding_project_escrow = PolimecFunding::fund_account_id(project_id); - assert!(funding_project_escrow != otm_escrow_account); + assert_ne!(funding_project_escrow, otm_escrow_account); let pre_participation_treasury_free_plmc = inst.get_free_plmc_balance_for(otm_treasury_account); let pre_participation_otm_escrow_held_plmc = @@ -926,6 +988,39 @@ mod bid_extrinsic { Perquintill::from_float(0.999) ); + let post_participation_treasury_free_plmc = inst.get_free_plmc_balance_for(otm_treasury_account); + let post_participation_otm_escrow_held_plmc = + inst.get_reserved_plmc_balance_for(otm_escrow_account, HoldReason::Participation.into()); + let post_participation_otm_escrow_usdt = + inst.get_free_funding_asset_balance_for(usdt_id.clone(), otm_escrow_account); + let post_participation_otm_fee_recipient_usdt = + inst.get_free_funding_asset_balance_for(usdt_id.clone(), otm_fee_recipient_account); + let post_participation_buyer_usdt = inst.get_free_funding_asset_balance_for(usdt_id.clone(), BIDDER_1); + + assert_eq!( + post_participation_treasury_free_plmc, + pre_participation_treasury_free_plmc - expected_plmc_bond - inst.get_ed() + ); + assert_eq!( + post_participation_otm_escrow_held_plmc, + pre_participation_otm_escrow_held_plmc + expected_plmc_bond + ); + assert_close_enough!( + post_participation_otm_escrow_usdt, + pre_participation_otm_escrow_usdt + otm_usdt_fee, + Perquintill::from_float(0.999) + ); + assert_close_enough!( + post_participation_otm_fee_recipient_usdt, + pre_participation_otm_fee_recipient_usdt, + Perquintill::from_float(0.999) + ); + assert_close_enough!( + post_participation_buyer_usdt, + pre_participation_buyer_usdt - USDT_PARTICIPATION - otm_usdt_fee, + Perquintill::from_float(0.999) + ); + assert_eq!(inst.go_to_next_state(project_id), ProjectStatus::FundingSuccessful); assert_eq!(inst.go_to_next_state(project_id), ProjectStatus::SettlementStarted(FundingOutcome::Success)); inst.settle_project(project_id, true); @@ -1335,222 +1430,6 @@ mod bid_extrinsic { }); } - #[test] - fn cannot_bid_more_than_project_limit_count() { - let mut inst = MockInstantiator::new(Some(RefCell::new(new_test_ext()))); - let mut project_metadata = default_project_metadata(ISSUER_1); - project_metadata.mainnet_token_max_supply = 1_000_000_000 * CT_UNIT; - project_metadata.total_allocation_size = 100_000_000 * CT_UNIT; - - let evaluations = inst.generate_successful_evaluations(project_metadata.clone(), 5); - let max_bids_per_project: u32 = ::MaxBidsPerProject::get(); - let bids = (0u32..max_bids_per_project - 1).map(|i| (i as u64 + 420, 5000 * CT_UNIT).into()).collect_vec(); - - let project_id = inst.create_auctioning_project(project_metadata.clone(), ISSUER_1, None, evaluations); - - let plmc_for_bidding = - inst.calculate_auction_plmc_charged_with_given_price(&bids.clone(), project_metadata.minimum_price); - let usdt_for_bidding = inst.calculate_auction_funding_asset_charged_with_given_price( - &bids.clone(), - project_metadata.minimum_price, - ); - - inst.mint_plmc_ed_if_required(bids.accounts()); - inst.mint_plmc_to(plmc_for_bidding.clone()); - - inst.mint_funding_asset_ed_if_required(bids.to_account_asset_map()); - inst.mint_funding_asset_to(usdt_for_bidding.clone()); - - inst.bid_for_users(project_id, bids.clone()).unwrap(); - - let current_bucket = inst.execute(|| Buckets::::get(project_id)).unwrap(); - let remaining_ct = current_bucket.amount_left; - - // This bid should be split in 2, but the second one should fail, making the whole extrinsic fail and roll back storage - let failing_bid = BidParams::::from(( - BIDDER_1, - Retail, - remaining_ct + 5000 * CT_UNIT, - ParticipationMode::Classic(1u8), - AcceptedFundingAsset::USDT, - )); - let plmc_for_failing_bid = inst.calculate_auction_plmc_charged_from_all_bids_made_or_with_bucket( - &vec![failing_bid.clone()], - project_metadata.clone(), - Some(current_bucket), - ); - - let usdt_for_bidding = inst.calculate_auction_funding_asset_charged_from_all_bids_made_or_with_bucket( - &vec![failing_bid.clone()], - project_metadata.clone(), - Some(current_bucket), - ); - - inst.mint_plmc_to(plmc_for_failing_bid.clone()); - inst.mint_funding_asset_to(usdt_for_bidding.clone()); - - inst.execute(|| { - assert_noop!( - PolimecFunding::bid( - RuntimeOrigin::signed(failing_bid.bidder), - get_mock_jwt_with_cid( - failing_bid.bidder, - InvestorType::Professional, - generate_did_from_account(failing_bid.bidder), - project_metadata.clone().policy_ipfs_cid.unwrap() - ), - project_id, - failing_bid.amount, - failing_bid.mode, - failing_bid.asset - ), - Error::::TooManyProjectParticipations - ); - }); - - // Now we test that after reaching the limit, just one bid is also not allowed - inst.execute(|| { - assert_ok!(PolimecFunding::bid( - RuntimeOrigin::signed(failing_bid.bidder), - get_mock_jwt_with_cid( - failing_bid.bidder, - InvestorType::Professional, - generate_did_from_account(failing_bid.bidder), - project_metadata.clone().policy_ipfs_cid.unwrap() - ), - project_id, - remaining_ct, - failing_bid.mode, - failing_bid.asset - )); - }); - inst.execute(|| { - assert_noop!( - PolimecFunding::bid( - RuntimeOrigin::signed(failing_bid.bidder), - get_mock_jwt_with_cid( - failing_bid.bidder, - InvestorType::Professional, - generate_did_from_account(failing_bid.bidder), - project_metadata.clone().policy_ipfs_cid.unwrap() - ), - project_id, - 5000 * CT_UNIT, - failing_bid.mode, - failing_bid.asset - ), - Error::::TooManyProjectParticipations - ); - }); - } - - #[test] - fn cannot_bid_more_than_user_limit_count() { - let mut inst = MockInstantiator::new(Some(RefCell::new(new_test_ext()))); - let mut project_metadata = default_project_metadata(ISSUER_1); - project_metadata.mainnet_token_max_supply = 1_000_000_000 * CT_UNIT; - project_metadata.total_allocation_size = 100_000_000 * CT_UNIT; - - let evaluations = inst.generate_successful_evaluations(project_metadata.clone(), 5); - let max_bids_per_user: u32 = ::MaxBidsPerUser::get(); - let bids = (0u32..max_bids_per_user - 1u32).map(|_| (BIDDER_1, 5000 * CT_UNIT).into()).collect_vec(); - - let project_id = inst.create_auctioning_project(project_metadata.clone(), ISSUER_1, None, evaluations); - - let plmc_for_bidding = - inst.calculate_auction_plmc_charged_with_given_price(&bids.clone(), project_metadata.minimum_price); - let usdt_for_bidding = inst.calculate_auction_funding_asset_charged_with_given_price( - &bids.clone(), - project_metadata.minimum_price, - ); - - inst.mint_plmc_ed_if_required(bids.accounts()); - inst.mint_plmc_to(plmc_for_bidding.clone()); - - inst.mint_funding_asset_ed_if_required(bids.to_account_asset_map()); - inst.mint_funding_asset_to(usdt_for_bidding.clone()); - - inst.bid_for_users(project_id, bids.clone()).unwrap(); - - let current_bucket = inst.execute(|| Buckets::::get(project_id)).unwrap(); - let remaining_ct = current_bucket.amount_left; - - // This bid should be split in 2, but the second one should fail, making the whole extrinsic fail and roll back storage - let failing_bid = BidParams::::from(( - BIDDER_1, - Retail, - remaining_ct + 5000 * CT_UNIT, - ParticipationMode::Classic(1u8), - AcceptedFundingAsset::USDT, - )); - let plmc_for_failing_bid = inst.calculate_auction_plmc_charged_from_all_bids_made_or_with_bucket( - &vec![failing_bid.clone()], - project_metadata.clone(), - Some(current_bucket), - ); - let usdt_for_bidding = inst.calculate_auction_funding_asset_charged_from_all_bids_made_or_with_bucket( - &vec![failing_bid.clone()], - project_metadata.clone(), - Some(current_bucket), - ); - inst.mint_plmc_to(plmc_for_failing_bid.clone()); - inst.mint_funding_asset_to(usdt_for_bidding.clone()); - - inst.execute(|| { - assert_noop!( - PolimecFunding::bid( - RuntimeOrigin::signed(failing_bid.bidder), - get_mock_jwt_with_cid( - failing_bid.bidder, - InvestorType::Professional, - generate_did_from_account(failing_bid.bidder), - project_metadata.clone().policy_ipfs_cid.unwrap() - ), - project_id, - failing_bid.amount, - failing_bid.mode, - failing_bid.asset - ), - Error::::TooManyUserParticipations - ); - }); - - // Now we test that after reaching the limit, just one bid is also not allowed - inst.execute(|| { - assert_ok!(PolimecFunding::bid( - RuntimeOrigin::signed(failing_bid.bidder), - get_mock_jwt_with_cid( - failing_bid.bidder, - InvestorType::Professional, - generate_did_from_account(failing_bid.bidder), - project_metadata.clone().policy_ipfs_cid.unwrap() - ), - project_id, - remaining_ct, - failing_bid.mode, - failing_bid.asset - )); - }); - inst.execute(|| { - assert_noop!( - PolimecFunding::bid( - RuntimeOrigin::signed(failing_bid.bidder), - get_mock_jwt_with_cid( - failing_bid.bidder, - InvestorType::Professional, - generate_did_from_account(failing_bid.bidder), - project_metadata.clone().policy_ipfs_cid.unwrap() - ), - project_id, - 5000 * CT_UNIT, - failing_bid.mode, - failing_bid.asset - ), - Error::::TooManyUserParticipations - ); - }); - } - #[test] fn per_credential_type_ticket_size_minimums() { let mut inst = MockInstantiator::new(Some(RefCell::new(new_test_ext()))); @@ -2080,7 +1959,7 @@ mod end_auction_extrinsic { AcceptedFundingAsset::USDC, )); let bid_3 = BidParams::from(( - BIDDER_1, + BIDDER_5, Professional, 10_000 * CT_UNIT, ParticipationMode::Classic(5u8), @@ -2101,8 +1980,8 @@ mod end_auction_extrinsic { AcceptedFundingAsset::DOT, )); // post bucketing, the bids look like this: - // (BIDDER_1, 5k) - (BIDDER_2, 40k) - (BIDDER_1, 5k) - (BIDDER_1, 5k) - (BIDDER_3 - 5k) - (BIDDER_3 - 1k) - (BIDDER_4 - 2k) - // | -------------------- 10USD ----------------------|---- 11 USD ---|---- 12 USD ----|----------- 13 USD -------------| + // (BIDDER_1, 5k) - (BIDDER_2, 40k) - (BIDDER_5, 5k) - (BIDDER_5, 5k) - (BIDDER_3 - 5k) - (BIDDER_3 - 1k) - (BIDDER_4 - 2k) + // | -------------------- 10 USD ----------------------|---- 11 USD ---|---- 12 USD ----|----------- 13 USD -------------| // post wap ~ 1.0557252: // (Accepted, 5k) - (Partially, 32k) - (Rejected, 5k) - (Accepted, 5k) - (Accepted - 5k) - (Accepted - 1k) - (Accepted - 2k) @@ -2140,11 +2019,25 @@ mod end_auction_extrinsic { assert!(matches!(inst.go_to_next_state(project_id), ProjectStatus::FundingSuccessful)); + let bidder_5_rejected_bid = inst.execute(|| Bids::::get((project_id, 2)).unwrap()); + let _bidder_5_accepted_bid = inst.execute(|| Bids::::get((project_id, 3)).unwrap()); + let bidder_5_plmc_pre_balance = inst.get_free_plmc_balance_for(bidder_5_rejected_bid.bidder); + let bidder_5_funding_asset_pre_balance = inst.get_free_funding_asset_balance_for( + bidder_5_rejected_bid.funding_asset.id(), + bidder_5_rejected_bid.bidder, + ); + + assert!(matches!( + inst.go_to_next_state(project_id), + ProjectStatus::SettlementStarted(FundingOutcome::Success) + )); + inst.settle_project(project_id, true); + let wap = inst.get_project_details(project_id).weighted_average_price.unwrap(); let returned_auction_plmc = inst.calculate_auction_plmc_returned_from_all_bids_made(&bids, project_metadata.clone(), wap); let returned_funding_assets = - inst.calculate_auction_funding_asset_returned_from_all_bids_made(&bids, project_metadata, wap); + inst.calculate_auction_funding_asset_returned_from_all_bids_made(&bids, project_metadata.clone(), wap); let expected_free_plmc = inst .generic_map_operation(vec![returned_auction_plmc.clone(), prev_plmc_balances], MergeOperation::Add); @@ -2152,47 +2045,36 @@ mod end_auction_extrinsic { vec![returned_funding_assets.clone(), prev_funding_asset_balances], MergeOperation::Add, ); - let expected_reserved_plmc = - inst.generic_map_operation(vec![plmc_amounts.clone(), returned_auction_plmc], MergeOperation::Subtract); + let expected_reserved_plmc = inst.generic_map_operation( + vec![plmc_amounts.clone(), returned_auction_plmc.clone()], + MergeOperation::Subtract, + ); let expected_final_funding_spent = inst.generic_map_operation( - vec![funding_asset_amounts.clone(), returned_funding_assets], + vec![funding_asset_amounts.clone(), returned_funding_assets.clone()], MergeOperation::Subtract, ); let expected_issuer_funding = inst.sum_funding_asset_mappings(vec![expected_final_funding_spent]); // Assertions about rejected bid - let rejected_bid = inst.execute(|| Bids::::get((project_id, BIDDER_1, 2)).unwrap()); - assert_eq!(rejected_bid.status, BidStatus::Rejected); - let bidder_plmc_pre_balance = inst.get_free_plmc_balance_for(rejected_bid.bidder); - let bidder_funding_asset_pre_balance = - inst.get_free_funding_asset_balance_for(rejected_bid.funding_asset.id(), rejected_bid.bidder); - inst.execute(|| { - PolimecFunding::settle_bid( - RuntimeOrigin::signed(rejected_bid.bidder), - project_id, - rejected_bid.bidder, - 2, - ) - }) - .unwrap(); - let bidder_plmc_post_balance = inst.get_free_plmc_balance_for(rejected_bid.bidder); - let bidder_funding_asset_post_balance = - inst.get_free_funding_asset_balance_for(rejected_bid.funding_asset.id(), rejected_bid.bidder); - assert!(inst.execute(|| Bids::::get((project_id, BIDDER_1, 2))).is_none()); - assert_eq!(bidder_plmc_post_balance, bidder_plmc_pre_balance + rejected_bid.plmc_bond); - assert_eq!( - bidder_funding_asset_post_balance, - bidder_funding_asset_pre_balance + rejected_bid.funding_asset_amount_locked + let bidder_5_plmc_post_balance = inst.get_free_plmc_balance_for(bidder_5_rejected_bid.bidder); + let bidder_5_funding_asset_post_balance = inst.get_free_funding_asset_balance_for( + bidder_5_rejected_bid.funding_asset.id(), + bidder_5_rejected_bid.bidder, ); - // Any refunds on bids that were accepted/partially accepted will be done at the settlement once funding finishes + // Bidder 5's accepted bid should have some refunds due to paying the wap in the end instead of the bucket price. + // Bidder 5's rejected bid should have a full refund + let bidder_5_returned_plmc = + returned_auction_plmc.iter().find(|x| x.account == BIDDER_5).unwrap().plmc_amount; + let bidder_5_returned_funding_asset = + returned_funding_assets.iter().find(|x| x.account == BIDDER_5).unwrap().asset_amount; + + assert!(inst.execute(|| Bids::::get((project_id, 2))).is_none()); + assert_eq!(bidder_5_plmc_post_balance, bidder_5_plmc_pre_balance + bidder_5_returned_plmc); assert_eq!( - inst.execute(|| Bids::::get((project_id, BIDDER_2, 1)).unwrap()).status, - BidStatus::PartiallyAccepted(32_000 * CT_UNIT) + bidder_5_funding_asset_post_balance, + bidder_5_funding_asset_pre_balance + bidder_5_returned_funding_asset ); - assert_eq!(inst.go_to_next_state(project_id), ProjectStatus::SettlementStarted(FundingOutcome::Success)); - - inst.settle_project(project_id, true); inst.do_free_plmc_assertions(expected_free_plmc); inst.do_reserved_plmc_assertions(expected_reserved_plmc, HoldReason::Participation.into()); @@ -2285,5 +2167,112 @@ mod end_auction_extrinsic { Perquintill::from_float(0.99) ); } + + #[test] + fn oversubscribed_bid_can_get_refund_and_bid_again_in_auction_round() { + let mut inst = MockInstantiator::new(Some(RefCell::new(new_test_ext()))); + let project_metadata = default_project_metadata(ISSUER_1); + let evaluations = inst.generate_successful_evaluations(project_metadata.clone(), 5); + let bids = inst.generate_bids_from_total_ct_percent(project_metadata.clone(), 100, 10); + + let project_id = inst.create_auctioning_project(project_metadata.clone(), ISSUER_1, None, evaluations); + + inst.mint_necessary_tokens_for_bids(project_id, bids.clone()); + inst.bid_for_users(project_id, bids.clone()).unwrap(); + + // First bid is OTM + let first_bid_to_refund = bids[9].clone(); + // Second is classic + let second_bid_to_refund = bids[8].clone(); + + let oversubscribing_bids = vec![ + ( + BIDDER_1, + Retail, + first_bid_to_refund.amount + 200 * CT_UNIT, + ParticipationMode::Classic(1), + AcceptedFundingAsset::USDT, + ) + .into(), + ( + BIDDER_2, + Retail, + second_bid_to_refund.amount, + ParticipationMode::Classic(1), + AcceptedFundingAsset::USDT, + ) + .into(), + ]; + + inst.mint_necessary_tokens_for_bids(project_id, oversubscribing_bids.clone()); + inst.bid_for_users(project_id, vec![oversubscribing_bids[0].clone()]).unwrap(); + + inst.process_oversubscribed_bids(project_id); + + let pre_first_refund_bidder_plmc_balance = inst.get_free_plmc_balance_for(first_bid_to_refund.bidder); + let pre_first_refund_bidder_funding_asset_balance = + inst.get_free_funding_asset_balance_for(first_bid_to_refund.asset.id(), first_bid_to_refund.bidder); + + let first_bid = inst.execute(|| Bids::::get((project_id, 9)).unwrap()); + assert!(matches!(first_bid.status, BidStatus::Rejected)); + let second_bid = inst.execute(|| Bids::::get((project_id, 8)).unwrap()); + assert!(matches!(second_bid.status, BidStatus::PartiallyAccepted(_))); + + inst.execute(|| { + assert_ok!(Pallet::::do_settle_bid(project_id, 9)); + assert_noop!( + Pallet::::do_settle_bid(project_id, 8), + Error::::SettlementNotStarted + ); + }); + + let post_first_refund_bidder_plmc_balance = inst.get_free_plmc_balance_for(first_bid_to_refund.bidder); + let post_first_refund_bidder_funding_asset_balance = + inst.get_free_funding_asset_balance_for(first_bid_to_refund.asset.id(), first_bid_to_refund.bidder); + + // OTM bid doesnt give PLMC refund to bidder + assert_eq!(post_first_refund_bidder_plmc_balance, pre_first_refund_bidder_plmc_balance); + let mut funding_asset_refund = first_bid.funding_asset_amount_locked; + let usd_ticket = first_bid.original_ct_usd_price.saturating_mul_int(first_bid.original_ct_amount); + inst.add_otm_fee_to(&mut funding_asset_refund, usd_ticket, first_bid_to_refund.asset); + assert_eq!( + post_first_refund_bidder_funding_asset_balance, + pre_first_refund_bidder_funding_asset_balance + funding_asset_refund + ); + + inst.bid_for_users(project_id, vec![oversubscribing_bids[1].clone()]).unwrap(); + + inst.process_oversubscribed_bids(project_id); + let pre_second_refund_bidder_plmc_balance = inst.get_free_plmc_balance_for(second_bid_to_refund.bidder); + let pre_second_refund_bidder_funding_asset_balance = + inst.get_free_funding_asset_balance_for(second_bid_to_refund.asset.id(), second_bid_to_refund.bidder); + + let second_bid = inst.execute(|| Bids::::get((project_id, 8)).unwrap()); + assert!(matches!(second_bid.status, BidStatus::Rejected)); + let third_bid = inst.execute(|| Bids::::get((project_id, 7)).unwrap()); + assert!(matches!(third_bid.status, BidStatus::PartiallyAccepted(_))); + + inst.execute(|| { + assert_ok!(Pallet::::do_settle_bid(project_id, 8)); + assert_noop!( + Pallet::::do_settle_bid(project_id, 7), + Error::::SettlementNotStarted + ); + }); + + let post_second_refund_bidder_plmc_balance = inst.get_free_plmc_balance_for(second_bid_to_refund.bidder); + let post_second_refund_bidder_funding_asset_balance = + inst.get_free_funding_asset_balance_for(second_bid_to_refund.asset.id(), second_bid_to_refund.bidder); + + // Classic bid + assert_eq!( + post_second_refund_bidder_plmc_balance, + pre_second_refund_bidder_plmc_balance + second_bid.plmc_bond + ); + assert_eq!( + post_second_refund_bidder_funding_asset_balance, + pre_second_refund_bidder_funding_asset_balance + second_bid.funding_asset_amount_locked + ); + } } } diff --git a/pallets/funding/src/tests/5_funding_end.rs b/pallets/funding/src/tests/5_funding_end.rs index d7a90f4b3..4547f37c7 100644 --- a/pallets/funding/src/tests/5_funding_end.rs +++ b/pallets/funding/src/tests/5_funding_end.rs @@ -1,43 +1,6 @@ use super::*; use sp_runtime::PerThing; -#[cfg(test)] -mod round_flow { - use super::*; - - #[cfg(test)] - mod success { - use super::*; - - #[test] - fn auction_oversubscription() { - let mut inst = MockInstantiator::new(Some(RefCell::new(new_test_ext()))); - let project_metadata = default_project_metadata(ISSUER_1); - let auction_allocation = project_metadata.total_allocation_size; - let bucket_size = Percent::from_percent(10) * auction_allocation; - let bids = vec![ - (BIDDER_1, auction_allocation).into(), - (BIDDER_2, bucket_size).into(), - (BIDDER_3, bucket_size).into(), - (BIDDER_4, bucket_size).into(), - (BIDDER_5, bucket_size).into(), - (BIDDER_6, bucket_size).into(), - ]; - - let project_id = inst.create_finished_project( - project_metadata.clone(), - ISSUER_1, - None, - inst.generate_successful_evaluations(project_metadata.clone(), 5), - bids, - ); - - let wap = inst.get_project_details(project_id).weighted_average_price.unwrap(); - assert!(wap > project_metadata.minimum_price); - } - } -} - #[cfg(test)] mod end_funding_extrinsic { use super::*; @@ -49,8 +12,6 @@ mod end_funding_extrinsic { #[test] fn evaluator_reward_is_correct() { let (mut inst, project_id) = create_project_with_funding_percentage(95, true); - let project_details = inst.get_project_details(project_id); - let project_metadata = inst.get_project_metadata(project_id); assert_eq!( inst.get_project_details(project_id).status, ProjectStatus::SettlementStarted(FundingOutcome::Success) @@ -68,8 +29,7 @@ mod end_funding_extrinsic { let total_fee = Perquintill::from_rational(fee_1 + fee_2 + fee_3, USD_REACHED); - let total_ct_fee = - total_fee * (project_metadata.total_allocation_size - project_details.remaining_contribution_tokens); + let total_ct_fee = total_fee * 950_000 * CT_UNIT; let total_evaluator_reward = Perquintill::from_percent(30) * total_ct_fee; @@ -86,9 +46,32 @@ mod end_funding_extrinsic { early_evaluator_total_bonded_usd: EARLY_EVALUATOR_TOTAL_USD_BONDED, normal_evaluator_total_bonded_usd: NORMAL_EVALUATOR_TOTAL_USD_BONDED, }; - assert_eq!( - inst.get_project_details(project_id).evaluation_round_info.evaluators_outcome, - Some(EvaluatorsOutcome::Rewarded(expected_reward_info)) + + let EvaluatorsOutcome::Rewarded(stored_reward_info) = + inst.get_project_details(project_id).evaluation_round_info.evaluators_outcome.unwrap() + else { + panic!("Unexpected Evaluator Outcome") + }; + + assert_close_enough!( + stored_reward_info.early_evaluator_reward_pot, + expected_reward_info.early_evaluator_reward_pot, + Perquintill::from_float(0.999) + ); + assert_close_enough!( + stored_reward_info.normal_evaluator_reward_pot, + expected_reward_info.normal_evaluator_reward_pot, + Perquintill::from_float(0.999) + ); + assert_close_enough!( + stored_reward_info.early_evaluator_total_bonded_usd, + expected_reward_info.early_evaluator_total_bonded_usd, + Perquintill::from_float(0.999) + ); + assert_close_enough!( + stored_reward_info.normal_evaluator_total_bonded_usd, + expected_reward_info.normal_evaluator_total_bonded_usd, + Perquintill::from_float(0.999) ); } diff --git a/pallets/funding/src/tests/6_settlement.rs b/pallets/funding/src/tests/6_settlement.rs index 871f3c192..06744f9ae 100644 --- a/pallets/funding/src/tests/6_settlement.rs +++ b/pallets/funding/src/tests/6_settlement.rs @@ -15,8 +15,8 @@ mod round_flow { let percentage = 100u8; let (mut inst, project_id) = create_project_with_funding_percentage(percentage, true); let evaluations = inst.get_evaluations(project_id); - let bids = inst.get_bids(project_id); + let bids = inst.get_bids(project_id); inst.settle_project(project_id, true); inst.assert_total_funding_paid_out(project_id, bids.clone()); @@ -41,6 +41,15 @@ mod round_flow { fn ethereum_project_can_be_settled() { let mut inst = MockInstantiator::new(Some(RefCell::new(new_test_ext()))); let mut project_metadata = default_project_metadata(ISSUER_1); + let base_price = PriceOf::::from_float(1.0); + let decimal_aware_price = ::PriceProvider::calculate_decimals_aware_price( + base_price, + USD_DECIMALS, + CT_DECIMALS, + ) + .unwrap(); + project_metadata.minimum_price = decimal_aware_price; + project_metadata.participants_account_type = ParticipantsAccountType::Ethereum; let evaluations = vec![ @@ -102,7 +111,15 @@ mod round_flow { #[test] fn polkadot_project_with_different_receiving_accounts_can_be_settled() { let mut inst = MockInstantiator::new(Some(RefCell::new(new_test_ext()))); - let project_metadata = default_project_metadata(ISSUER_1); + let mut project_metadata = default_project_metadata(ISSUER_1); + let base_price = PriceOf::::from_float(1.0); + let decimal_aware_price = ::PriceProvider::calculate_decimals_aware_price( + base_price, + USD_DECIMALS, + CT_DECIMALS, + ) + .unwrap(); + project_metadata.minimum_price = decimal_aware_price; let evaluations = vec![ EvaluationParams::from((EVALUATOR_1, 500_000 * USD_UNIT, polkadot_junction!(EVALUATOR_1 + 420))), @@ -240,7 +257,6 @@ mod settle_evaluation_extrinsic { let mut inst = MockInstantiator::new(Some(RefCell::new(new_test_ext()))); let mut project_metadata = default_project_metadata(ISSUER_1); project_metadata.total_allocation_size = 1_000_000 * CT_UNIT; - let project_id = inst.create_finished_project( project_metadata.clone(), ISSUER_1, @@ -250,7 +266,7 @@ mod settle_evaluation_extrinsic { EvaluationParams::from((EVALUATOR_2, 250_000 * USD_UNIT)), EvaluationParams::from((EVALUATOR_3, 320_000 * USD_UNIT)), ], - inst.generate_bids_from_total_ct_percent(project_metadata.clone(), 100, 5), + inst.generate_bids_from_total_ct_percent(project_metadata.clone(), 100, 30), ); assert_eq!(inst.go_to_next_state(project_id), ProjectStatus::SettlementStarted(FundingOutcome::Success)); @@ -472,6 +488,14 @@ mod settle_bid_extrinsic { let usdt_ed = inst.get_funding_asset_ed(AcceptedFundingAsset::USDT.id()); let dot_ed = inst.get_funding_asset_ed(AcceptedFundingAsset::DOT.id()); let mut project_metadata = default_project_metadata(ISSUER_1); + let base_price = PriceOf::::from_float(1.0); + let decimal_aware_price = ::PriceProvider::calculate_decimals_aware_price( + base_price, + USD_DECIMALS, + CT_DECIMALS, + ) + .unwrap(); + project_metadata.minimum_price = decimal_aware_price; project_metadata.participation_currencies = bounded_vec![AcceptedFundingAsset::USDT, AcceptedFundingAsset::DOT]; let auction_allocation = project_metadata.total_allocation_size; @@ -496,8 +520,7 @@ mod settle_bid_extrinsic { let wap = inst.get_project_details(project_id).weighted_average_price.unwrap(); // Partial amount bid assertions - let partial_amount_bid_stored = - inst.execute(|| Bids::::get((project_id, BIDDER_1, 0)).unwrap()); + let partial_amount_bid_stored = inst.execute(|| Bids::::get((project_id, 0)).unwrap()); let mut final_partial_amount_bid_params = partial_amount_bid_params.clone(); final_partial_amount_bid_params.amount = auction_allocation - 2000 * CT_UNIT; let expected_final_plmc_bonded = inst.calculate_auction_plmc_charged_with_given_price( @@ -519,14 +542,22 @@ mod settle_bid_extrinsic { project_metadata.funding_destination_account, ); - inst.execute(|| { - assert_ok!(PolimecFunding::settle_bid(RuntimeOrigin::signed(BIDDER_1), project_id, BIDDER_1, 0)); - }); + let lower_price_bid_stored = inst.execute(|| Bids::::get((project_id, 1)).unwrap()); + let pre_issuer_dot_balance = inst.get_free_funding_asset_balance_for( + AcceptedFundingAsset::DOT.id(), + project_metadata.funding_destination_account, + ); + + inst.settle_project(project_id, true); let post_issuer_usdt_balance = inst.get_free_funding_asset_balance_for( AcceptedFundingAsset::USDT.id(), project_metadata.funding_destination_account, ); + let post_issuer_dot_balance = inst.get_free_funding_asset_balance_for( + AcceptedFundingAsset::DOT.id(), + project_metadata.funding_destination_account, + ); inst.assert_funding_asset_free_balance( BIDDER_1, @@ -557,7 +588,6 @@ mod settle_bid_extrinsic { inst.assert_plmc_free_balance(BIDDER_1, expected_plmc_refund + expected_final_plmc_bonded + ed); // Price > wap bid assertions - let lower_price_bid_stored = inst.execute(|| Bids::::get((project_id, BIDDER_2, 1)).unwrap()); let expected_final_plmc_bonded = inst .calculate_auction_plmc_charged_with_given_price(&vec![lower_price_bid_params.clone()], wap)[0] .plmc_amount; @@ -567,20 +597,6 @@ mod settle_bid_extrinsic { let expected_plmc_refund = lower_price_bid_stored.plmc_bond - expected_final_plmc_bonded; let expected_dot_refund = lower_price_bid_stored.funding_asset_amount_locked - expected_final_dot_paid; - let pre_issuer_dot_balance = inst.get_free_funding_asset_balance_for( - AcceptedFundingAsset::DOT.id(), - project_metadata.funding_destination_account, - ); - - inst.execute(|| { - assert_ok!(PolimecFunding::settle_bid(RuntimeOrigin::signed(BIDDER_1), project_id, BIDDER_2, 1)); - }); - - let post_issuer_dot_balance = inst.get_free_funding_asset_balance_for( - AcceptedFundingAsset::DOT.id(), - project_metadata.funding_destination_account, - ); - inst.assert_funding_asset_free_balance( BIDDER_2, AcceptedFundingAsset::DOT.id(), @@ -628,6 +644,14 @@ mod settle_bid_extrinsic { let usdt_ed = inst.get_funding_asset_ed(AcceptedFundingAsset::USDT.id()); let mut project_metadata = default_project_metadata(ISSUER_1); + let base_price = PriceOf::::from_float(1.0); + let decimal_aware_price = ::PriceProvider::calculate_decimals_aware_price( + base_price, + USD_DECIMALS, + CT_DECIMALS, + ) + .unwrap(); + project_metadata.minimum_price = decimal_aware_price; project_metadata.participation_currencies = bounded_vec![AcceptedFundingAsset::USDT, AcceptedFundingAsset::DOT]; let auction_allocation = project_metadata.total_allocation_size; @@ -648,7 +672,7 @@ mod settle_bid_extrinsic { ); assert_eq!(inst.go_to_next_state(project_id), ProjectStatus::SettlementStarted(FundingOutcome::Success)); - let no_refund_bid_stored = inst.execute(|| Bids::::get((project_id, BIDDER_1, 0)).unwrap()); + let no_refund_bid_stored = inst.execute(|| Bids::::get((project_id, 0)).unwrap()); let pre_issuer_usdc_balance = inst.get_free_funding_asset_balance_for( AcceptedFundingAsset::USDT.id(), @@ -656,7 +680,7 @@ mod settle_bid_extrinsic { ); inst.execute(|| { - assert_ok!(PolimecFunding::settle_bid(RuntimeOrigin::signed(BIDDER_1), project_id, BIDDER_1, 0)); + assert_ok!(PolimecFunding::settle_bid(RuntimeOrigin::signed(BIDDER_1), project_id, 0)); }); let post_issuer_usdc_balance = inst.get_free_funding_asset_balance_for( @@ -726,7 +750,7 @@ mod settle_bid_extrinsic { assert_eq!(inst.go_to_next_state(project_id), ProjectStatus::SettlementStarted(FundingOutcome::Failure)); // Partial amount bid assertions - let no_refund_bid_stored = inst.execute(|| Bids::::get((project_id, BIDDER_1, 0)).unwrap()); + let no_refund_bid_stored = inst.execute(|| Bids::::get((project_id, 0)).unwrap()); let pre_issuer_usdc_balance = inst.get_free_funding_asset_balance_for( AcceptedFundingAsset::USDT.id(), @@ -734,7 +758,7 @@ mod settle_bid_extrinsic { ); inst.execute(|| { - assert_ok!(PolimecFunding::settle_bid(RuntimeOrigin::signed(BIDDER_1), project_id, BIDDER_1, 0)); + assert_ok!(PolimecFunding::settle_bid(RuntimeOrigin::signed(BIDDER_1), project_id, 0)); }); let post_issuer_usdc_balance = inst.get_free_funding_asset_balance_for( @@ -767,6 +791,14 @@ mod settle_bid_extrinsic { let ed = inst.get_ed(); let usdt_ed = inst.get_funding_asset_ed(AcceptedFundingAsset::USDT.id()); let mut project_metadata = default_project_metadata(ISSUER_1); + let base_price = PriceOf::::from_float(0.5); + let decimal_aware_price = ::PriceProvider::calculate_decimals_aware_price( + base_price, + USD_DECIMALS, + CT_DECIMALS, + ) + .unwrap(); + project_metadata.minimum_price = decimal_aware_price; project_metadata.participation_currencies = bounded_vec![AcceptedFundingAsset::USDT, AcceptedFundingAsset::DOT]; let auction_allocation = project_metadata.total_allocation_size; @@ -790,17 +822,14 @@ mod settle_bid_extrinsic { let project_id = inst.create_finished_project(project_metadata.clone(), ISSUER_1, None, evaluations, bids); assert_eq!(inst.go_to_next_state(project_id), ProjectStatus::SettlementStarted(FundingOutcome::Success)); - let rejected_bid_stored = inst.execute(|| Bids::::get((project_id, BIDDER_1, 0)).unwrap()); - assert_eq!(rejected_bid_stored.status, BidStatus::Rejected); + let rejected_bid_stored = inst.execute(|| Bids::::get((project_id, 0)).unwrap()); let pre_issuer_usdt_balance = inst.get_free_funding_asset_balance_for( AcceptedFundingAsset::USDT.id(), project_metadata.funding_destination_account, ); - inst.execute(|| { - assert_ok!(PolimecFunding::settle_bid(RuntimeOrigin::signed(BIDDER_1), project_id, BIDDER_1, 0)); - }); + inst.settle_project(project_id, true); let post_issuer_usdt_balance = inst.get_free_funding_asset_balance_for( AcceptedFundingAsset::USDT.id(), @@ -835,23 +864,16 @@ mod settle_bid_extrinsic { fn cannot_settle_twice() { let percentage = 100u8; let (mut inst, project_id) = create_project_with_funding_percentage(percentage, true); - let first_bid = inst.get_bids(project_id).into_iter().next().unwrap(); inst.execute(|| { let bidder = first_bid.bidder; assert_ok!(crate::Pallet::::settle_bid( RuntimeOrigin::signed(bidder), project_id, - bidder, first_bid.id )); assert_noop!( - crate::Pallet::::settle_bid( - RuntimeOrigin::signed(bidder), - project_id, - bidder, - first_bid.id - ), + crate::Pallet::::settle_bid(RuntimeOrigin::signed(bidder), project_id, first_bid.id), Error::::ParticipationNotFound ); }); @@ -861,17 +883,11 @@ mod settle_bid_extrinsic { fn cannot_be_called_before_settlement_started() { let percentage = 100u8; let (mut inst, project_id) = create_project_with_funding_percentage(percentage, false); - let first_bid = inst.get_bids(project_id).into_iter().next().unwrap(); let bidder = first_bid.bidder; inst.execute(|| { assert_noop!( - crate::Pallet::::settle_bid( - RuntimeOrigin::signed(bidder), - project_id, - bidder, - first_bid.id - ), + crate::Pallet::::settle_bid(RuntimeOrigin::signed(bidder), project_id, first_bid.id), Error::::SettlementNotStarted ); }); diff --git a/pallets/funding/src/tests/7_ct_migration.rs b/pallets/funding/src/tests/7_ct_migration.rs index c98bdb39e..ea39766b6 100644 --- a/pallets/funding/src/tests/7_ct_migration.rs +++ b/pallets/funding/src/tests/7_ct_migration.rs @@ -11,7 +11,7 @@ mod pallet_migration { let mut inst = MockInstantiator::new(Some(RefCell::new(new_test_ext()))); let project_metadata = default_project_metadata(ISSUER_1); let evaluations = inst.generate_successful_evaluations(project_metadata.clone(), 5); - let bids = inst.generate_bids_from_total_ct_percent(project_metadata.clone(), 90, 5); + let bids = inst.generate_bids_from_total_ct_percent(project_metadata.clone(), 90, 10); let project_id = inst.create_finished_project(project_metadata.clone(), ISSUER_1, None, evaluations, bids); assert_eq!(inst.go_to_next_state(project_id), ProjectStatus::SettlementStarted(FundingOutcome::Success)); inst.settle_project(project_id, true); @@ -49,12 +49,12 @@ mod pallet_migration { })) ); assert_eq!(project_details.status, ProjectStatus::CTMigrationStarted); - assert_eq!(inst.execute(|| UnmigratedCounter::::get(project_id)), 10); + assert_eq!(inst.execute(|| UnmigratedCounter::::get(project_id)), 15); } fn create_pallet_migration_project(mut inst: MockInstantiator) -> (ProjectId, MockInstantiator) { let evaluations = inst.generate_successful_evaluations(default_project_metadata(ISSUER_1), 5); - let bids = inst.generate_bids_from_total_ct_percent(default_project_metadata(ISSUER_1), 90, 5); + let bids = inst.generate_bids_from_total_ct_percent(default_project_metadata(ISSUER_1), 90, 10); let project_id = inst.create_finished_project(default_project_metadata(ISSUER_1), ISSUER_1, None, evaluations, bids); assert_eq!(inst.go_to_next_state(project_id), ProjectStatus::SettlementStarted(FundingOutcome::Success)); @@ -174,7 +174,7 @@ mod offchain_migration { fn start_offchain_migration() { let mut inst = MockInstantiator::new(Some(RefCell::new(new_test_ext()))); let evaluations = inst.generate_successful_evaluations(default_project_metadata(ISSUER_1), 5); - let bids = inst.generate_bids_from_total_ct_percent(default_project_metadata(ISSUER_1), 90, 5); + let bids = inst.generate_bids_from_total_ct_percent(default_project_metadata(ISSUER_1), 90, 10); // Create migrations for 2 projects, to check the `remaining_participants` is unaffected by other projects let project_id = inst.create_finished_project( default_project_metadata(ISSUER_1), @@ -201,14 +201,14 @@ mod offchain_migration { }); let project_details = inst.get_project_details(project_id); - assert_eq!(inst.execute(|| UnmigratedCounter::::get(project_id)), 10); + assert_eq!(inst.execute(|| UnmigratedCounter::::get(project_id)), 15); assert_eq!(project_details.status, ProjectStatus::CTMigrationStarted); } fn create_offchain_migration_project(mut inst: MockInstantiator) -> (ProjectId, MockInstantiator) { let project_metadata = default_project_metadata(ISSUER_1); let evaluations = inst.generate_successful_evaluations(project_metadata.clone(), 5); - let bids = inst.generate_bids_from_total_ct_percent(project_metadata.clone(), 90, 5); + let bids = inst.generate_bids_from_total_ct_percent(project_metadata.clone(), 90, 10); let project_id = inst.create_finished_project(project_metadata, ISSUER_1, None, evaluations, bids); assert_eq!(inst.go_to_next_state(project_id), ProjectStatus::SettlementStarted(FundingOutcome::Success)); inst.settle_project(project_id, true); diff --git a/pallets/funding/src/tests/misc.rs b/pallets/funding/src/tests/misc.rs index 373e27d9f..98be42dc0 100644 --- a/pallets/funding/src/tests/misc.rs +++ b/pallets/funding/src/tests/misc.rs @@ -307,6 +307,35 @@ mod inner_functions { let multiplier_25_duration = multiplier_25.calculate_vesting_duration::(); assert_eq!(multiplier_25_duration, FixedU128::from_rational(52008, 1000).saturating_mul_int((DAYS * 7) as u64)); } + + #[test] + pub fn calculate_usd_sold_from_bucket() { + let project_metadata = default_project_metadata(ISSUER_1); + + let mut bucket = Pallet::::create_bucket_from_metadata(&project_metadata).unwrap(); + bucket.update(10_000 * CT_UNIT); + + // We bought 10k CTs at a price of 10USD, meaning we should get 100k USD + let usd_sold = + Pallet::::calculate_usd_sold_from_bucket(bucket, project_metadata.total_allocation_size); + assert_eq!(usd_sold, 100_000 * USD_UNIT); + + // This bucket has 2 buckets sold out on top of the first one + let mut bucket = Pallet::::create_bucket_from_metadata(&project_metadata).unwrap(); + bucket.update(project_metadata.total_allocation_size); + bucket.update(50_000 * CT_UNIT); + bucket.update(50_000 * CT_UNIT); + let wap = bucket.calculate_wap(project_metadata.total_allocation_size); + let usd_raised_first_bucket = project_metadata.minimum_price.saturating_mul_int(400_000 * CT_UNIT); + let usd_raised_second_bucket = wap.saturating_mul_int(50_000 * CT_UNIT); + let usd_raised_third_bucket = wap.saturating_mul_int(50_000 * CT_UNIT); + let total_expected_rasied = usd_raised_first_bucket + usd_raised_second_bucket + usd_raised_third_bucket; + + // We bought 10k CTs at a price of 10USD, meaning we should get 100k USD + let usd_sold = + Pallet::::calculate_usd_sold_from_bucket(bucket, project_metadata.total_allocation_size); + assert_eq!(usd_sold, total_expected_rasied); + } } #[test] @@ -317,8 +346,8 @@ fn project_state_transition_event() { project_metadata.clone(), ISSUER_1, None, - inst.generate_successful_evaluations(project_metadata.clone(), 5), - inst.generate_bids_from_total_ct_percent(project_metadata.clone(), 80, 5), + inst.generate_successful_evaluations(project_metadata.clone(), 10), + inst.generate_bids_from_total_ct_percent(project_metadata.clone(), 90, 30), true, ); diff --git a/pallets/funding/src/tests/mod.rs b/pallets/funding/src/tests/mod.rs index 5a1402bf1..a85fe23e7 100644 --- a/pallets/funding/src/tests/mod.rs +++ b/pallets/funding/src/tests/mod.rs @@ -5,10 +5,7 @@ use crate::{ use defaults::*; use frame_support::{ assert_err, assert_noop, assert_ok, - traits::{ - fungible::{MutateFreeze, MutateHold}, - Get, - }, + traits::fungible::{MutateFreeze, MutateHold}, }; use itertools::Itertools; use parachains_common::DAYS; @@ -108,9 +105,9 @@ pub mod defaults { total_allocation_size: 500_000 * CT_UNIT, minimum_price: decimal_aware_price, bidding_ticket_sizes: BiddingTicketSizes { - professional: TicketSize::new(10 * USD_UNIT, None), - institutional: TicketSize::new(10 * USD_UNIT, None), - retail: TicketSize::new(10 * USD_UNIT, None), + professional: TicketSize::new(100 * USD_UNIT, None), + institutional: TicketSize::new(100 * USD_UNIT, None), + retail: TicketSize::new(100 * USD_UNIT, None), phantom: Default::default(), }, participation_currencies: vec![USDT, USDC, DOT, WETH].try_into().unwrap(), @@ -157,7 +154,7 @@ pub mod defaults { // Used only to generate values, not for chain interactions let inst = MockInstantiator::new(None); let project_metadata = default_project_metadata(ISSUER_1); - inst.generate_bids_from_total_ct_percent(project_metadata, percent, 5) + inst.generate_bids_from_total_ct_percent(project_metadata, percent, 10) } } @@ -166,7 +163,7 @@ pub fn create_project_with_funding_percentage(percentage: u8, start_settlement: let mut project_metadata = default_project_metadata(ISSUER_1); project_metadata.total_allocation_size = 1_000_000 * CT_UNIT; let evaluations = inst.generate_successful_evaluations(project_metadata.clone(), 5); - let bids = inst.generate_bids_from_total_ct_percent(project_metadata.clone(), percentage, 5); + let bids = inst.generate_bids_from_total_ct_percent(project_metadata.clone(), percentage, 30); let project_id = inst.create_finished_project(project_metadata, ISSUER_1, None, evaluations, bids); @@ -174,12 +171,6 @@ pub fn create_project_with_funding_percentage(percentage: u8, start_settlement: assert!(matches!(inst.go_to_next_state(project_id), ProjectStatus::SettlementStarted(_))); } - // Sanity check - let project_details = inst.get_project_details(project_id); - let percent_reached = - Perquintill::from_rational(project_details.funding_amount_reached_usd, project_details.fundraising_target_usd); - assert_eq!(percent_reached, Perquintill::from_percent(percentage as u64)); - (inst, project_id) } diff --git a/pallets/funding/src/tests/runtime_api.rs b/pallets/funding/src/tests/runtime_api.rs index 330331304..0f6657420 100644 --- a/pallets/funding/src/tests/runtime_api.rs +++ b/pallets/funding/src/tests/runtime_api.rs @@ -51,12 +51,12 @@ fn top_bids() { ]; let project_metadata = default_project_metadata(ISSUER_1); let evaluations = inst.generate_successful_evaluations(project_metadata.clone(), 5); - let project_id = inst.create_finished_project(project_metadata, ISSUER_1, None, evaluations, bids); + let project_id = inst.create_finished_project(project_metadata.clone(), ISSUER_1, None, evaluations, bids); inst.execute(|| { let block_hash = System::block_hash(System::block_number()); let top_1 = TestRuntime::top_bids(&TestRuntime, block_hash, project_id, 1).unwrap(); - let bidder_4_evaluation = Bids::::get((project_id, BIDDER_4, 3)).unwrap(); + let bidder_4_evaluation = Bids::::get((project_id, 3)).unwrap(); assert!(top_1.len() == 1 && top_1[0] == bidder_4_evaluation); let top_4_bidders = TestRuntime::top_bids(&TestRuntime, block_hash, project_id, 4) @@ -176,7 +176,7 @@ fn contribution_tokens() { let bids_with_bob_1 = inst.generate_bids_from_total_ct_amount(1, bob_amount_1); let bob = bids_with_bob_1[0].bidder; - let bob_amount_2 = 500_000 * CT_UNIT; + let bob_amount_2 = 490_000 * CT_UNIT; let bids_with_bob_2 = inst.generate_bids_from_total_ct_amount(1, bob_amount_2); let bob_amount_3 = 300_020 * CT_UNIT; @@ -185,34 +185,28 @@ fn contribution_tokens() { let bob_amount_4 = 250_100 * CT_UNIT; let bids_with_bob_4 = inst.generate_bids_from_total_ct_amount(1, bob_amount_4); - let project_metadata = default_project_metadata(ISSUER_1); - let evaluations = inst.generate_successful_evaluations(project_metadata.clone(), 5); + let get_project = |issuer| { + let mut project_metadata = default_project_metadata(issuer); + let base_price = PriceOf::::from_float(1.0); + let decimal_aware_price = ::PriceProvider::calculate_decimals_aware_price( + base_price, + USD_DECIMALS, + CT_DECIMALS, + ) + .unwrap(); + project_metadata.minimum_price = decimal_aware_price; + project_metadata + }; + + let evaluations = inst.generate_successful_evaluations(get_project(ISSUER_1), 5); let project_id_1 = - inst.create_settled_project(project_metadata, ISSUER_1, None, evaluations.clone(), bids_with_bob_1, true); - let project_id_2 = inst.create_settled_project( - default_project_metadata(ISSUER_2), - ISSUER_2, - None, - evaluations.clone(), - bids_with_bob_2, - true, - ); - let project_id_3 = inst.create_settled_project( - default_project_metadata(ISSUER_3), - ISSUER_3, - None, - evaluations.clone(), - bids_with_bob_3, - true, - ); - let project_id_4 = inst.create_settled_project( - default_project_metadata(ISSUER_4), - ISSUER_4, - None, - evaluations.clone(), - bids_with_bob_4, - true, - ); + inst.create_settled_project(get_project(ISSUER_1), ISSUER_1, None, evaluations.clone(), bids_with_bob_1, true); + let project_id_2 = + inst.create_settled_project(get_project(ISSUER_2), ISSUER_2, None, evaluations.clone(), bids_with_bob_2, true); + let project_id_3 = + inst.create_settled_project(get_project(ISSUER_3), ISSUER_3, None, evaluations.clone(), bids_with_bob_3, true); + let project_id_4 = + inst.create_settled_project(get_project(ISSUER_4), ISSUER_4, None, evaluations.clone(), bids_with_bob_4, true); let expected_items = vec![ (project_id_2, bob_amount_2), @@ -271,8 +265,7 @@ fn funding_asset_to_ct_amount_classic() { let decimal_aware_price = PriceProviderOf::::calculate_decimals_aware_price(new_price, USD_DECIMALS, CT_DECIMALS).unwrap(); - let bids = - inst.generate_bids_that_take_price_to(project_metadata_2.clone(), decimal_aware_price, 420, |acc| acc + 1); + let bids = inst.generate_bids_that_take_price_to(project_metadata_2.clone(), decimal_aware_price); let project_id_2 = inst.create_finished_project(project_metadata_2.clone(), ISSUER_2, None, evaluations.clone(), bids); // Sanity check @@ -303,13 +296,7 @@ fn funding_asset_to_ct_amount_classic() { // Price should be at 16 USD/CT bucket.current_price = bucket.initial_price + bucket.delta_price * FixedU128::from_float(6.0f64); bucket.amount_left = bucket.delta_amount; - let bids = inst.generate_bids_from_bucket( - project_metadata_3.clone(), - bucket, - 420, - |acc| acc + 1, - AcceptedFundingAsset::USDT, - ); + let bids = inst.generate_bids_from_bucket(project_metadata_3.clone(), bucket, AcceptedFundingAsset::USDT); let necessary_plmc = inst.calculate_auction_plmc_charged_from_all_bids_made_or_with_bucket(&bids, project_metadata_3.clone(), None); let necessary_usdt = inst.calculate_auction_funding_asset_charged_from_all_bids_made_or_with_bucket( @@ -413,8 +400,7 @@ fn funding_asset_to_ct_amount_otm() { let decimal_aware_price = PriceProviderOf::::calculate_decimals_aware_price(new_price, USD_DECIMALS, CT_DECIMALS).unwrap(); - let bids = - inst.generate_bids_that_take_price_to(project_metadata_2.clone(), decimal_aware_price, 420, |acc| acc + 1); + let bids = inst.generate_bids_that_take_price_to(project_metadata_2.clone(), decimal_aware_price); let project_id_2 = inst.create_finished_project(project_metadata_2.clone(), ISSUER_2, None, evaluations.clone(), bids); // Sanity check @@ -446,13 +432,7 @@ fn funding_asset_to_ct_amount_otm() { // Price should be at 16 USD/CT bucket.current_price = bucket.initial_price + bucket.delta_price * FixedU128::from_float(6.0f64); bucket.amount_left = bucket.delta_amount; - let bids = inst.generate_bids_from_bucket( - project_metadata_3.clone(), - bucket, - 420, - |acc| acc + 1, - AcceptedFundingAsset::USDT, - ); + let bids = inst.generate_bids_from_bucket(project_metadata_3.clone(), bucket, AcceptedFundingAsset::USDT); let necessary_plmc = inst.calculate_auction_plmc_charged_from_all_bids_made_or_with_bucket(&bids, project_metadata_3.clone(), None); let necessary_usdt = inst.calculate_auction_funding_asset_charged_from_all_bids_made_or_with_bucket( @@ -537,11 +517,7 @@ fn get_message_to_sign_by_receiving_account() { #[test] fn get_next_vesting_schedule_merge_candidates() { let mut inst = MockInstantiator::new(Some(RefCell::new(new_test_ext()))); - let evaluations = vec![ - EvaluationParams::from((EVALUATOR_1, 500_000 * USD_UNIT)), - EvaluationParams::from((EVALUATOR_2, 250_000 * USD_UNIT)), - EvaluationParams::from((BIDDER_1, 320_000 * USD_UNIT)), - ]; + let evaluations = vec![EvaluationParams::from((EVALUATOR_1, 500_000 * USD_UNIT))]; let bids = vec![ BidParams::from(( BIDDER_1, @@ -572,16 +548,17 @@ fn get_next_vesting_schedule_merge_candidates() { AcceptedFundingAsset::USDT, )), ]; + let mut project_metadata = default_project_metadata(ISSUER_1); + let base_price = PriceOf::::from_float(1.0); + let decimal_aware_price = + ::PriceProvider::calculate_decimals_aware_price(base_price, USD_DECIMALS, CT_DECIMALS) + .unwrap(); + project_metadata.minimum_price = decimal_aware_price; - let project_id = inst.create_finished_project( - default_project_metadata(ISSUER_1), - ISSUER_1, - None, - evaluations.clone(), - bids.clone(), - ); - assert_eq!(ProjectStatus::SettlementStarted(FundingOutcome::Success), inst.go_to_next_state(project_id)); - inst.settle_project(project_id, true); + let _project_id = + inst.create_settled_project(project_metadata, ISSUER_1, None, evaluations.clone(), bids.clone(), true); + + let events = inst.execute(|| System::events().into_iter().collect::>()); let hold_reason: mock::RuntimeHoldReason = HoldReason::Participation.into(); let bidder_1_schedules = @@ -595,12 +572,11 @@ fn get_next_vesting_schedule_merge_candidates() { block_hash, BIDDER_1, HoldReason::Participation.into(), - // around 4 weeks of blocks - 210_000, + 300_000, ) .unwrap() .unwrap(); - assert_eq!((idx_1, idx_2), (1, 2)); + assert_eq!((idx_1, idx_2), (0, 1)); // 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(); @@ -610,8 +586,7 @@ fn get_next_vesting_schedule_merge_candidates() { block_hash, BIDDER_1, HoldReason::Participation.into(), - // around 4 weeks of blocks - 210_000, + 300_000, ) .unwrap() .unwrap(); @@ -623,12 +598,17 @@ fn get_next_vesting_schedule_merge_candidates() { fn calculate_otm_fee() { let mut inst = MockInstantiator::new(Some(RefCell::new(new_test_ext()))); let mut project_metadata = default_project_metadata(ISSUER_1); + let base_price = PriceOf::::from_float(1.0); + let decimal_aware_price = + ::PriceProvider::calculate_decimals_aware_price(base_price, USD_DECIMALS, CT_DECIMALS) + .unwrap(); + project_metadata.minimum_price = decimal_aware_price; project_metadata.participation_currencies = bounded_vec![AcceptedFundingAsset::DOT]; let dot_id = AcceptedFundingAsset::DOT.id(); let dot_decimals = inst.execute(|| ForeignAssets::decimals(dot_id.clone())); let dot_unit = 10u128.pow(dot_decimals as u32); - let dot_ticket = 10_000 * dot_unit; + let dot_ticket = 1000 * dot_unit; let dot_ed = inst.get_funding_asset_ed(dot_id.clone()); let block_hash = inst.execute(|| System::block_hash(System::block_number())); @@ -779,7 +759,13 @@ fn all_project_participations_by_did() { let mut inst = MockInstantiator::new(Some(RefCell::new(new_test_ext()))); let did_user = generate_did_from_account(420); - let project_metadata = default_project_metadata(ISSUER_1); + let mut project_metadata = default_project_metadata(ISSUER_1); + let base_price = PriceOf::::from_float(1.0); + let decimal_aware_price = + ::PriceProvider::calculate_decimals_aware_price(base_price, USD_DECIMALS, CT_DECIMALS) + .unwrap(); + project_metadata.minimum_price = decimal_aware_price; + let cid = project_metadata.clone().policy_ipfs_cid.unwrap(); let project_id = inst.create_evaluating_project(project_metadata.clone(), ISSUER_1, None); @@ -897,7 +883,7 @@ fn projects_by_did() { let did_user = generate_did_from_account(420); let evaluations = inst.generate_successful_evaluations(default_project_metadata(ISSUER_1), 5); - let bids = inst.generate_bids_from_total_ct_percent(default_project_metadata(ISSUER_1), 80, 7); + let bids = inst.generate_bids_from_total_ct_percent(default_project_metadata(ISSUER_1), 80, 10); let project_id_1 = inst.create_settled_project( default_project_metadata(ISSUER_1), ISSUER_1, diff --git a/pallets/funding/src/types.rs b/pallets/funding/src/types.rs index ef7ba8c7f..e70bcfd67 100644 --- a/pallets/funding/src/types.rs +++ b/pallets/funding/src/types.rs @@ -42,6 +42,7 @@ pub mod config { #[allow(clippy::wildcard_imports)] use super::*; use crate::Balance; + use sp_core::parameter_types; use xcm::v4::Location; @@ -342,7 +343,9 @@ pub mod storage { #[derive(Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug, MaxEncodedLen, TypeInfo)] pub struct BidInfo { + // Check pub id: u32, + // Check pub project_id: ProjectId, pub bidder: AccountId, pub did: Did, @@ -357,17 +360,6 @@ pub mod storage { pub when: BlockNumber, pub receiving_account: Junction, } - impl - BidInfo - { - pub fn final_ct_amount(&self) -> Balance { - match self.status { - BidStatus::Accepted => self.original_ct_amount, - BidStatus::PartiallyAccepted(amount) => amount, - _ => Zero::zero(), - } - } - } impl Ord for BidInfo @@ -432,12 +424,13 @@ pub mod storage { Self { amount_left, current_price: initial_price, initial_price, delta_price, delta_amount } } - /// Update the bucket - pub fn update(&mut self, removed_amount: Balance) { + /// Update the bucket, return the new price. + pub fn update(&mut self, removed_amount: Balance) -> Price { self.amount_left.saturating_reduce(removed_amount); if self.amount_left.is_zero() { self.next(); } + self.current_price } /// Updates the bucket to represent the next one in the sequence. This involves: @@ -654,6 +647,8 @@ pub mod inner { /// The bid is rejected because the ct tokens ran out Rejected, /// The bid is partially accepted as there were not enough tokens to fill the full bid + /// First item is how many contribution tokens are still accepted, + /// Second item is how many contribution tokens were refunded to the bidder (i.e. PLMC and Funding Asset released) PartiallyAccepted(Balance), } @@ -804,9 +799,8 @@ pub mod extrinsic { pub now: BlockNumberFor, pub did: Did, pub metadata_ticket_size_bounds: TicketSize, - pub total_bids_by_bidder: u32, - pub total_bids_for_project: u32, pub receiving_account: Junction, + pub auction_oversubscribed: bool, } pub struct DoContributeParams { diff --git a/pallets/funding/src/weights.rs b/pallets/funding/src/weights.rs index 7dbe35dd3..914c40062 100644 --- a/pallets/funding/src/weights.rs +++ b/pallets/funding/src/weights.rs @@ -59,7 +59,8 @@ pub trait WeightInfo { fn start_evaluation() -> Weight; fn evaluate(x: u32, ) -> Weight; fn end_evaluation_failure() -> Weight; - fn bid(x: u32, y: u32, ) -> Weight; + fn bid(x: u32, ) -> Weight; + fn process_next_oversubscribed_bid() -> Weight; fn end_auction(x: u32, y: u32, ) -> Weight; fn contribute(x: u32, ) -> Weight; fn end_funding_project_successful() -> Weight; @@ -204,48 +205,68 @@ impl WeightInfo for SubstrateWeight { /// Storage: `Timestamp::Now` (r:1 w:0) /// Proof: `Timestamp::Now` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) /// Storage: `Funding::ProjectsMetadata` (r:1 w:0) - /// Proof: `Funding::ProjectsMetadata` (`max_values`: None, `max_size`: Some(502), added: 2977, mode: `MaxEncodedLen`) + /// Proof: `Funding::ProjectsMetadata` (`max_values`: None, `max_size`: Some(437), added: 2912, mode: `MaxEncodedLen`) /// Storage: `Funding::ProjectsDetails` (r:1 w:0) - /// Proof: `Funding::ProjectsDetails` (`max_values`: None, `max_size`: Some(342), added: 2817, mode: `MaxEncodedLen`) + /// Proof: `Funding::ProjectsDetails` (`max_values`: None, `max_size`: Some(347), added: 2822, mode: `MaxEncodedLen`) /// Storage: `Funding::Buckets` (r:1 w:1) /// Proof: `Funding::Buckets` (`max_values`: None, `max_size`: Some(100), added: 2575, mode: `MaxEncodedLen`) - /// Storage: `Funding::BidCounts` (r:1 w:1) - /// Proof: `Funding::BidCounts` (`max_values`: None, `max_size`: Some(24), added: 2499, mode: `MaxEncodedLen`) - /// Storage: `Funding::Bids` (r:7 w:10) - /// Proof: `Funding::Bids` (`max_values`: None, `max_size`: Some(273), added: 2748, mode: `MaxEncodedLen`) /// Storage: `Funding::NextBidId` (r:1 w:1) /// Proof: `Funding::NextBidId` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `Funding::BidBucketBounds` (r:10 w:10) + /// Proof: `Funding::BidBucketBounds` (`max_values`: None, `max_size`: Some(60), added: 2535, mode: `MaxEncodedLen`) /// Storage: `Funding::AuctionBoughtUSD` (r:1 w:1) - /// Proof: `Funding::AuctionBoughtUSD` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `MaxEncodedLen`) + /// Proof: `Funding::AuctionBoughtUSD` (`max_values`: None, `max_size`: Some(118), added: 2593, mode: `MaxEncodedLen`) /// Storage: `Oracle::Values` (r:2 w:0) - /// Proof: `Oracle::Values` (`max_values`: None, `max_size`: Some(36), added: 2511, mode: `MaxEncodedLen`) + /// Proof: `Oracle::Values` (`max_values`: None, `max_size`: Some(634), added: 3109, mode: `MaxEncodedLen`) /// Storage: `ForeignAssets::Metadata` (r:1 w:0) - /// Proof: `ForeignAssets::Metadata` (`max_values`: None, `max_size`: Some(140), added: 2615, mode: `MaxEncodedLen`) + /// Proof: `ForeignAssets::Metadata` (`max_values`: None, `max_size`: Some(738), added: 3213, mode: `MaxEncodedLen`) /// Storage: `Funding::Evaluations` (r:1 w:0) - /// Proof: `Funding::Evaluations` (`max_values`: None, `max_size`: Some(254), added: 2729, mode: `MaxEncodedLen`) + /// Proof: `Funding::Evaluations` (`max_values`: None, `max_size`: Some(337), added: 2812, mode: `MaxEncodedLen`) /// Storage: `Balances::Holds` (r:1 w:1) - /// Proof: `Balances::Holds` (`max_values`: None, `max_size`: Some(1149), added: 3624, mode: `MaxEncodedLen`) + /// Proof: `Balances::Holds` (`max_values`: None, `max_size`: Some(175), added: 2650, mode: `MaxEncodedLen`) /// Storage: `ForeignAssets::Asset` (r:1 w:1) - /// Proof: `ForeignAssets::Asset` (`max_values`: None, `max_size`: Some(210), added: 2685, mode: `MaxEncodedLen`) + /// Proof: `ForeignAssets::Asset` (`max_values`: None, `max_size`: Some(808), added: 3283, mode: `MaxEncodedLen`) /// Storage: `ForeignAssets::Account` (r:2 w:2) - /// Proof: `ForeignAssets::Account` (`max_values`: None, `max_size`: Some(134), added: 2609, mode: `MaxEncodedLen`) - /// The range of component `x` is `[0, 6]`. - /// The range of component `y` is `[0, 10]`. - fn bid(x: u32, y: u32, ) -> Weight { + /// Proof: `ForeignAssets::Account` (`max_values`: None, `max_size`: Some(732), added: 3207, mode: `MaxEncodedLen`) + /// Storage: `Funding::CTAmountOversubscribed` (r:1 w:1) + /// Proof: `Funding::CTAmountOversubscribed` (`max_values`: None, `max_size`: Some(36), added: 2511, mode: `MaxEncodedLen`) + /// Storage: `Funding::Bids` (r:0 w:10) + /// Proof: `Funding::Bids` (`max_values`: None, `max_size`: Some(309), added: 2784, mode: `MaxEncodedLen`) + /// The range of component `x` is `[1, 10]`. + fn bid(x: u32, ) -> Weight { // Proof Size summary in bytes: - // Measured: `2870 + x * (192 ±0)` - // Estimated: `6208 + x * (2748 ±0)` - // Minimum execution time: 279_441_000 picoseconds. - Weight::from_parts(215_188_573, 6208) - // Standard Error: 264_218 - .saturating_add(Weight::from_parts(3_406_167, 0).saturating_mul(x.into())) - // Standard Error: 165_665 - .saturating_add(Weight::from_parts(71_144_235, 0).saturating_mul(y.into())) - .saturating_add(T::DbWeight::get().reads(16_u64)) + // Measured: `2890` + // Estimated: `7404 + x * (2535 ±0)` + // Minimum execution time: 184_000_000 picoseconds. + Weight::from_parts(139_444_380, 7404) + // Standard Error: 76_200 + .saturating_add(Weight::from_parts(59_216_258, 0).saturating_mul(x.into())) + .saturating_add(T::DbWeight::get().reads(15_u64)) .saturating_add(T::DbWeight::get().reads((1_u64).saturating_mul(x.into()))) .saturating_add(T::DbWeight::get().writes(8_u64)) - .saturating_add(T::DbWeight::get().writes((1_u64).saturating_mul(y.into()))) - .saturating_add(Weight::from_parts(0, 2748).saturating_mul(x.into())) + .saturating_add(T::DbWeight::get().writes((2_u64).saturating_mul(x.into()))) + .saturating_add(Weight::from_parts(0, 2535).saturating_mul(x.into())) + } + /// Storage: `Funding::ProjectsMetadata` (r:1 w:0) + /// Proof: `Funding::ProjectsMetadata` (`max_values`: None, `max_size`: Some(437), added: 2912, mode: `MaxEncodedLen`) + /// Storage: `Funding::Buckets` (r:1 w:0) + /// Proof: `Funding::Buckets` (`max_values`: None, `max_size`: Some(100), added: 2575, mode: `MaxEncodedLen`) + /// Storage: `Funding::OutbidBidsCutoff` (r:1 w:1) + /// Proof: `Funding::OutbidBidsCutoff` (`max_values`: None, `max_size`: Some(40), added: 2515, mode: `MaxEncodedLen`) + /// Storage: `Funding::CTAmountOversubscribed` (r:1 w:1) + /// Proof: `Funding::CTAmountOversubscribed` (`max_values`: None, `max_size`: Some(36), added: 2511, mode: `MaxEncodedLen`) + /// Storage: `Funding::Bids` (r:2 w:1) + /// Proof: `Funding::Bids` (`max_values`: None, `max_size`: Some(309), added: 2784, mode: `MaxEncodedLen`) + /// Storage: `Funding::BidBucketBounds` (r:1 w:0) + /// Proof: `Funding::BidBucketBounds` (`max_values`: None, `max_size`: Some(60), added: 2535, mode: `MaxEncodedLen`) + fn process_next_oversubscribed_bid() -> Weight { + // Proof Size summary in bytes: + // Measured: `1630` + // Estimated: `6558` + // Minimum execution time: 29_000_000 picoseconds. + Weight::from_parts(30_000_000, 6558) + .saturating_add(T::DbWeight::get().reads(7_u64)) + .saturating_add(T::DbWeight::get().writes(3_u64)) } /// Storage: `Funding::ProjectsMetadata` (r:1 w:0) /// Proof: `Funding::ProjectsMetadata` (`max_values`: None, `max_size`: Some(502), added: 2977, mode: `MaxEncodedLen`) @@ -760,48 +781,68 @@ impl WeightInfo for () { /// Storage: `Timestamp::Now` (r:1 w:0) /// Proof: `Timestamp::Now` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) /// Storage: `Funding::ProjectsMetadata` (r:1 w:0) - /// Proof: `Funding::ProjectsMetadata` (`max_values`: None, `max_size`: Some(502), added: 2977, mode: `MaxEncodedLen`) + /// Proof: `Funding::ProjectsMetadata` (`max_values`: None, `max_size`: Some(437), added: 2912, mode: `MaxEncodedLen`) /// Storage: `Funding::ProjectsDetails` (r:1 w:0) - /// Proof: `Funding::ProjectsDetails` (`max_values`: None, `max_size`: Some(342), added: 2817, mode: `MaxEncodedLen`) + /// Proof: `Funding::ProjectsDetails` (`max_values`: None, `max_size`: Some(347), added: 2822, mode: `MaxEncodedLen`) /// Storage: `Funding::Buckets` (r:1 w:1) /// Proof: `Funding::Buckets` (`max_values`: None, `max_size`: Some(100), added: 2575, mode: `MaxEncodedLen`) - /// Storage: `Funding::BidCounts` (r:1 w:1) - /// Proof: `Funding::BidCounts` (`max_values`: None, `max_size`: Some(24), added: 2499, mode: `MaxEncodedLen`) - /// Storage: `Funding::Bids` (r:7 w:10) - /// Proof: `Funding::Bids` (`max_values`: None, `max_size`: Some(273), added: 2748, mode: `MaxEncodedLen`) /// Storage: `Funding::NextBidId` (r:1 w:1) /// Proof: `Funding::NextBidId` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `Funding::BidBucketBounds` (r:10 w:10) + /// Proof: `Funding::BidBucketBounds` (`max_values`: None, `max_size`: Some(60), added: 2535, mode: `MaxEncodedLen`) /// Storage: `Funding::AuctionBoughtUSD` (r:1 w:1) - /// Proof: `Funding::AuctionBoughtUSD` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `MaxEncodedLen`) + /// Proof: `Funding::AuctionBoughtUSD` (`max_values`: None, `max_size`: Some(118), added: 2593, mode: `MaxEncodedLen`) /// Storage: `Oracle::Values` (r:2 w:0) - /// Proof: `Oracle::Values` (`max_values`: None, `max_size`: Some(36), added: 2511, mode: `MaxEncodedLen`) + /// Proof: `Oracle::Values` (`max_values`: None, `max_size`: Some(634), added: 3109, mode: `MaxEncodedLen`) /// Storage: `ForeignAssets::Metadata` (r:1 w:0) - /// Proof: `ForeignAssets::Metadata` (`max_values`: None, `max_size`: Some(140), added: 2615, mode: `MaxEncodedLen`) + /// Proof: `ForeignAssets::Metadata` (`max_values`: None, `max_size`: Some(738), added: 3213, mode: `MaxEncodedLen`) /// Storage: `Funding::Evaluations` (r:1 w:0) - /// Proof: `Funding::Evaluations` (`max_values`: None, `max_size`: Some(254), added: 2729, mode: `MaxEncodedLen`) + /// Proof: `Funding::Evaluations` (`max_values`: None, `max_size`: Some(337), added: 2812, mode: `MaxEncodedLen`) /// Storage: `Balances::Holds` (r:1 w:1) - /// Proof: `Balances::Holds` (`max_values`: None, `max_size`: Some(1149), added: 3624, mode: `MaxEncodedLen`) + /// Proof: `Balances::Holds` (`max_values`: None, `max_size`: Some(175), added: 2650, mode: `MaxEncodedLen`) /// Storage: `ForeignAssets::Asset` (r:1 w:1) - /// Proof: `ForeignAssets::Asset` (`max_values`: None, `max_size`: Some(210), added: 2685, mode: `MaxEncodedLen`) + /// Proof: `ForeignAssets::Asset` (`max_values`: None, `max_size`: Some(808), added: 3283, mode: `MaxEncodedLen`) /// Storage: `ForeignAssets::Account` (r:2 w:2) - /// Proof: `ForeignAssets::Account` (`max_values`: None, `max_size`: Some(134), added: 2609, mode: `MaxEncodedLen`) - /// The range of component `x` is `[0, 6]`. - /// The range of component `y` is `[0, 10]`. - fn bid(x: u32, y: u32, ) -> Weight { + /// Proof: `ForeignAssets::Account` (`max_values`: None, `max_size`: Some(732), added: 3207, mode: `MaxEncodedLen`) + /// Storage: `Funding::CTAmountOversubscribed` (r:1 w:1) + /// Proof: `Funding::CTAmountOversubscribed` (`max_values`: None, `max_size`: Some(36), added: 2511, mode: `MaxEncodedLen`) + /// Storage: `Funding::Bids` (r:0 w:10) + /// Proof: `Funding::Bids` (`max_values`: None, `max_size`: Some(309), added: 2784, mode: `MaxEncodedLen`) + /// The range of component `x` is `[1, 10]`. + fn bid(x: u32, ) -> Weight { // Proof Size summary in bytes: - // Measured: `2870 + x * (192 ±0)` - // Estimated: `6208 + x * (2748 ±0)` - // Minimum execution time: 279_441_000 picoseconds. - Weight::from_parts(215_188_573, 6208) - // Standard Error: 264_218 - .saturating_add(Weight::from_parts(3_406_167, 0).saturating_mul(x.into())) - // Standard Error: 165_665 - .saturating_add(Weight::from_parts(71_144_235, 0).saturating_mul(y.into())) - .saturating_add(RocksDbWeight::get().reads(16_u64)) + // Measured: `2890` + // Estimated: `7404 + x * (2535 ±0)` + // Minimum execution time: 184_000_000 picoseconds. + Weight::from_parts(139_444_380, 7404) + // Standard Error: 76_200 + .saturating_add(Weight::from_parts(59_216_258, 0).saturating_mul(x.into())) + .saturating_add(RocksDbWeight::get().reads(15_u64)) .saturating_add(RocksDbWeight::get().reads((1_u64).saturating_mul(x.into()))) .saturating_add(RocksDbWeight::get().writes(8_u64)) - .saturating_add(RocksDbWeight::get().writes((1_u64).saturating_mul(y.into()))) - .saturating_add(Weight::from_parts(0, 2748).saturating_mul(x.into())) + .saturating_add(RocksDbWeight::get().writes((2_u64).saturating_mul(x.into()))) + .saturating_add(Weight::from_parts(0, 2535).saturating_mul(x.into())) + } + /// Storage: `Funding::ProjectsMetadata` (r:1 w:0) + /// Proof: `Funding::ProjectsMetadata` (`max_values`: None, `max_size`: Some(437), added: 2912, mode: `MaxEncodedLen`) + /// Storage: `Funding::Buckets` (r:1 w:0) + /// Proof: `Funding::Buckets` (`max_values`: None, `max_size`: Some(100), added: 2575, mode: `MaxEncodedLen`) + /// Storage: `Funding::OutbidBidsCutoff` (r:1 w:1) + /// Proof: `Funding::OutbidBidsCutoff` (`max_values`: None, `max_size`: Some(40), added: 2515, mode: `MaxEncodedLen`) + /// Storage: `Funding::CTAmountOversubscribed` (r:1 w:1) + /// Proof: `Funding::CTAmountOversubscribed` (`max_values`: None, `max_size`: Some(36), added: 2511, mode: `MaxEncodedLen`) + /// Storage: `Funding::Bids` (r:2 w:1) + /// Proof: `Funding::Bids` (`max_values`: None, `max_size`: Some(309), added: 2784, mode: `MaxEncodedLen`) + /// Storage: `Funding::BidBucketBounds` (r:1 w:0) + /// Proof: `Funding::BidBucketBounds` (`max_values`: None, `max_size`: Some(60), added: 2535, mode: `MaxEncodedLen`) + fn process_next_oversubscribed_bid() -> Weight { + // Proof Size summary in bytes: + // Measured: `1630` + // Estimated: `6558` + // Minimum execution time: 29_000_000 picoseconds. + Weight::from_parts(30_000_000, 6558) + .saturating_add(RocksDbWeight::get().reads(7_u64)) + .saturating_add(RocksDbWeight::get().writes(3_u64)) } /// Storage: `Funding::ProjectsMetadata` (r:1 w:0) /// Proof: `Funding::ProjectsMetadata` (`max_values`: None, `max_size`: Some(502), added: 2977, mode: `MaxEncodedLen`) diff --git a/pallets/oracle-ocw/src/tests.rs b/pallets/oracle-ocw/src/tests.rs index 21cd2122b..cf90470c5 100644 --- a/pallets/oracle-ocw/src/tests.rs +++ b/pallets/oracle-ocw/src/tests.rs @@ -38,7 +38,6 @@ fn call_offchain_worker() { match tx.call { RuntimeCall::Oracle(orml_oracle::Call::feed_values { values }) => { - dbg!(&values); for (asset, price) in values { match asset { 10 => assert_close_enough(price, FixedU128::from_float(6.138485575453039783)), diff --git a/runtimes/polimec/src/lib.rs b/runtimes/polimec/src/lib.rs index 220905bb1..d446ff16c 100644 --- a/runtimes/polimec/src/lib.rs +++ b/runtimes/polimec/src/lib.rs @@ -43,8 +43,8 @@ use frame_system::{EnsureRoot, EnsureRootWithSuccess, EnsureSigned, EnsureSigned use pallet_aura::Authorities; use pallet_democracy::GetElectorate; use pallet_funding::{ - runtime_api::ProjectParticipationIds, BidInfoOf, ContributionInfoOf, DaysToBlocks, EvaluationInfoOf, - HereLocationGetter, PriceProviderOf, ProjectDetailsOf, ProjectId, ProjectMetadataOf, + BidInfoOf, DaysToBlocks, EvaluationInfoOf, HereLocationGetter, PriceProviderOf, ProjectDetailsOf, ProjectId, + ProjectMetadataOf, }; use parachains_common::{ impls::AssetsToBlockAuthor, @@ -170,7 +170,7 @@ pub type Migrations = migrations::Unreleased; /// The runtime migrations per release. #[allow(missing_docs)] pub mod migrations { - use crate::{parameter_types, Runtime}; + use crate::parameter_types; parameter_types! { pub const RandomPalletName: &'static str = "Random"; @@ -178,10 +178,7 @@ pub mod migrations { /// Unreleased migrations. Add new ones here: #[allow(unused_parens)] - pub type Unreleased = ( - super::custom_migrations::asset_id_migration::FromOldAssetIdMigration, - pallet_funding::storage_migrations::v6::MigrationToV6, - ); + pub type Unreleased = (); } /// Executive: handles dispatch to the various modules. @@ -1076,12 +1073,7 @@ impl pallet_funding::Config for Runtime { type FundingCurrency = ForeignAssets; type FundingSuccessThreshold = FundingSuccessThreshold; type InvestorOrigin = EnsureInvestor; - type MaxBidsPerProject = ConstU32<512>; - type MaxBidsPerUser = ConstU32<16>; type MaxCapacityThresholds = MaxCapacityThresholds; - type MaxContributionsPerUser = ConstU32<16>; - type MaxEvaluationsPerProject = ConstU32<512>; - type MaxEvaluationsPerUser = ConstU32<16>; type MaxMessageSizeThresholds = MaxMessageSizeThresholds; type MinUsdPerEvaluation = MinUsdPerEvaluation; type Multiplier = pallet_funding::types::Multiplier; @@ -1510,10 +1502,6 @@ impl_runtime_apis! { Funding::top_bids(project_id, amount) } - fn top_contributions(project_id: ProjectId, amount: u32) -> Vec> { - Funding::top_contributions(project_id, amount) - } - fn top_projects_by_usd_raised(amount: u32) -> Vec<(ProjectId, ProjectMetadataOf, ProjectDetailsOf)> { Funding::top_projects_by_usd_raised(amount) } @@ -1527,10 +1515,6 @@ impl_runtime_apis! { fn contribution_tokens(account: AccountId) -> Vec<(ProjectId, Balance)> { Funding::contribution_tokens(account) } - - fn all_project_participations_by_did(project_id: ProjectId, did: Did) -> Vec> { - Funding::all_project_participations_by_did(project_id, did) - } } impl pallet_funding::runtime_api::ProjectInformation for Runtime { diff --git a/runtimes/polimec/src/weights/pallet_funding.rs b/runtimes/polimec/src/weights/pallet_funding.rs index 493c4933d..2f4354e13 100644 --- a/runtimes/polimec/src/weights/pallet_funding.rs +++ b/runtimes/polimec/src/weights/pallet_funding.rs @@ -156,49 +156,68 @@ impl pallet_funding::WeightInfo for WeightInfo { /// Storage: `Timestamp::Now` (r:1 w:0) /// Proof: `Timestamp::Now` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) /// Storage: `Funding::ProjectsMetadata` (r:1 w:0) - /// Proof: `Funding::ProjectsMetadata` (`max_values`: None, `max_size`: Some(502), added: 2977, mode: `MaxEncodedLen`) + /// Proof: `Funding::ProjectsMetadata` (`max_values`: None, `max_size`: Some(437), added: 2912, mode: `MaxEncodedLen`) /// Storage: `Funding::ProjectsDetails` (r:1 w:0) - /// Proof: `Funding::ProjectsDetails` (`max_values`: None, `max_size`: Some(342), added: 2817, mode: `MaxEncodedLen`) + /// Proof: `Funding::ProjectsDetails` (`max_values`: None, `max_size`: Some(347), added: 2822, mode: `MaxEncodedLen`) /// Storage: `Funding::Buckets` (r:1 w:1) /// Proof: `Funding::Buckets` (`max_values`: None, `max_size`: Some(100), added: 2575, mode: `MaxEncodedLen`) - /// Storage: `Funding::BidCounts` (r:1 w:1) - /// Proof: `Funding::BidCounts` (`max_values`: None, `max_size`: Some(24), added: 2499, mode: `MaxEncodedLen`) - /// Storage: `Funding::Bids` (r:7 w:10) - /// Proof: `Funding::Bids` (`max_values`: None, `max_size`: Some(273), added: 2748, mode: `MaxEncodedLen`) /// Storage: `Funding::NextBidId` (r:1 w:1) /// Proof: `Funding::NextBidId` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `Funding::BidBucketBounds` (r:10 w:10) + /// Proof: `Funding::BidBucketBounds` (`max_values`: None, `max_size`: Some(60), added: 2535, mode: `MaxEncodedLen`) /// Storage: `Funding::AuctionBoughtUSD` (r:1 w:1) - /// Proof: `Funding::AuctionBoughtUSD` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `MaxEncodedLen`) + /// Proof: `Funding::AuctionBoughtUSD` (`max_values`: None, `max_size`: Some(118), added: 2593, mode: `MaxEncodedLen`) /// Storage: `Oracle::Values` (r:2 w:0) - /// Proof: `Oracle::Values` (`max_values`: None, `max_size`: Some(36), added: 2511, mode: `MaxEncodedLen`) + /// Proof: `Oracle::Values` (`max_values`: None, `max_size`: Some(634), added: 3109, mode: `MaxEncodedLen`) /// Storage: `ForeignAssets::Metadata` (r:1 w:0) - /// Proof: `ForeignAssets::Metadata` (`max_values`: None, `max_size`: Some(140), added: 2615, mode: `MaxEncodedLen`) + /// Proof: `ForeignAssets::Metadata` (`max_values`: None, `max_size`: Some(738), added: 3213, mode: `MaxEncodedLen`) /// Storage: `Funding::Evaluations` (r:1 w:0) - /// Proof: `Funding::Evaluations` (`max_values`: None, `max_size`: Some(254), added: 2729, mode: `MaxEncodedLen`) + /// Proof: `Funding::Evaluations` (`max_values`: None, `max_size`: Some(337), added: 2812, mode: `MaxEncodedLen`) /// Storage: `Balances::Holds` (r:1 w:1) /// Proof: `Balances::Holds` (`max_values`: None, `max_size`: Some(175), added: 2650, mode: `MaxEncodedLen`) /// Storage: `ForeignAssets::Asset` (r:1 w:1) - /// Proof: `ForeignAssets::Asset` (`max_values`: None, `max_size`: Some(210), added: 2685, mode: `MaxEncodedLen`) + /// Proof: `ForeignAssets::Asset` (`max_values`: None, `max_size`: Some(808), added: 3283, mode: `MaxEncodedLen`) /// Storage: `ForeignAssets::Account` (r:2 w:2) - /// Proof: `ForeignAssets::Account` (`max_values`: None, `max_size`: Some(134), added: 2609, mode: `MaxEncodedLen`) - /// The range of component `x` is `[0, 6]`. - /// The range of component `y` is `[0, 10]`. - fn bid(x: u32, y: u32, ) -> Weight { + /// Proof: `ForeignAssets::Account` (`max_values`: None, `max_size`: Some(732), added: 3207, mode: `MaxEncodedLen`) + /// Storage: `Funding::CTAmountOversubscribed` (r:1 w:1) + /// Proof: `Funding::CTAmountOversubscribed` (`max_values`: None, `max_size`: Some(36), added: 2511, mode: `MaxEncodedLen`) + /// Storage: `Funding::Bids` (r:0 w:10) + /// Proof: `Funding::Bids` (`max_values`: None, `max_size`: Some(309), added: 2784, mode: `MaxEncodedLen`) + /// The range of component `x` is `[1, 10]`. + fn bid(x: u32, ) -> Weight { // Proof Size summary in bytes: - // Measured: `2866 + x * (192 ±0)` - // Estimated: `6208 + x * (2748 ±0)` - // Minimum execution time: 274_581_000 picoseconds. - Weight::from_parts(211_541_829, 0) - .saturating_add(Weight::from_parts(0, 6208)) - // Standard Error: 261_236 - .saturating_add(Weight::from_parts(3_389_028, 0).saturating_mul(x.into())) - // Standard Error: 163_796 - .saturating_add(Weight::from_parts(68_717_795, 0).saturating_mul(y.into())) - .saturating_add(T::DbWeight::get().reads(16)) + // Measured: `2890` + // Estimated: `7404 + x * (2535 ±0)` + // Minimum execution time: 184_000_000 picoseconds. + Weight::from_parts(139_444_380, 7404) + // Standard Error: 76_200 + .saturating_add(Weight::from_parts(59_216_258, 0).saturating_mul(x.into())) + .saturating_add(T::DbWeight::get().reads(15_u64)) .saturating_add(T::DbWeight::get().reads((1_u64).saturating_mul(x.into()))) - .saturating_add(T::DbWeight::get().writes(8)) - .saturating_add(T::DbWeight::get().writes((1_u64).saturating_mul(y.into()))) - .saturating_add(Weight::from_parts(0, 2748).saturating_mul(x.into())) + .saturating_add(T::DbWeight::get().writes(8_u64)) + .saturating_add(T::DbWeight::get().writes((2_u64).saturating_mul(x.into()))) + .saturating_add(Weight::from_parts(0, 2535).saturating_mul(x.into())) + } + /// Storage: `Funding::ProjectsMetadata` (r:1 w:0) + /// Proof: `Funding::ProjectsMetadata` (`max_values`: None, `max_size`: Some(437), added: 2912, mode: `MaxEncodedLen`) + /// Storage: `Funding::Buckets` (r:1 w:0) + /// Proof: `Funding::Buckets` (`max_values`: None, `max_size`: Some(100), added: 2575, mode: `MaxEncodedLen`) + /// Storage: `Funding::OutbidBidsCutoff` (r:1 w:1) + /// Proof: `Funding::OutbidBidsCutoff` (`max_values`: None, `max_size`: Some(40), added: 2515, mode: `MaxEncodedLen`) + /// Storage: `Funding::CTAmountOversubscribed` (r:1 w:1) + /// Proof: `Funding::CTAmountOversubscribed` (`max_values`: None, `max_size`: Some(36), added: 2511, mode: `MaxEncodedLen`) + /// Storage: `Funding::Bids` (r:2 w:1) + /// Proof: `Funding::Bids` (`max_values`: None, `max_size`: Some(309), added: 2784, mode: `MaxEncodedLen`) + /// Storage: `Funding::BidBucketBounds` (r:1 w:0) + /// Proof: `Funding::BidBucketBounds` (`max_values`: None, `max_size`: Some(60), added: 2535, mode: `MaxEncodedLen`) + fn process_next_oversubscribed_bid() -> Weight { + // Proof Size summary in bytes: + // Measured: `1630` + // Estimated: `6558` + // Minimum execution time: 29_000_000 picoseconds. + Weight::from_parts(30_000_000, 6558) + .saturating_add(T::DbWeight::get().reads(7_u64)) + .saturating_add(T::DbWeight::get().writes(3_u64)) } /// Storage: `Funding::ProjectsMetadata` (r:1 w:0) /// Proof: `Funding::ProjectsMetadata` (`max_values`: None, `max_size`: Some(502), added: 2977, mode: `MaxEncodedLen`)