From 184b6456aa90b1aafd77cde81572e35f316b659a Mon Sep 17 00:00:00 2001 From: Juan Ignacio Rios <54085674+JuaniRios@users.noreply.github.com> Date: Wed, 29 May 2024 14:26:00 +0200 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20Optimize=20wap=20calculation=20(#29?= =?UTF-8?q?9)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## What? - Spread out the WAP on_initialize calculation into 2 blocks ## Why? - Because the ref time was too close to the block limit ## How? - Create a do_end_auction_closing function that does the deciding of which bids are successful, and refunds/deletes failed ones - do_start_community now does the WAP calculation only ## Testing? - Run normal tests, all logic should be maintained --- pallets/funding/src/benchmarking.rs | 316 ++++++++++++------ .../funding/src/functions/1_application.rs | 1 + pallets/funding/src/functions/3_auction.rs | 80 ++++- .../funding/src/functions/4_contribution.rs | 23 +- pallets/funding/src/functions/misc.rs | 73 ++-- .../src/instantiator/async_features.rs | 69 ++-- .../src/instantiator/chain_interactions.rs | 17 +- pallets/funding/src/lib.rs | 28 +- pallets/funding/src/mock.rs | 16 +- pallets/funding/src/tests/2_evaluation.rs | 1 + pallets/funding/src/tests/3_auction.rs | 39 ++- pallets/funding/src/tests/4_community.rs | 3 +- pallets/funding/src/tests/5_remainder.rs | 61 ++-- pallets/funding/src/tests/misc.rs | 15 - pallets/funding/src/types.rs | 5 + 15 files changed, 479 insertions(+), 268 deletions(-) diff --git a/pallets/funding/src/benchmarking.rs b/pallets/funding/src/benchmarking.rs index 739b7bd82..eb4e17ef9 100644 --- a/pallets/funding/src/benchmarking.rs +++ b/pallets/funding/src/benchmarking.rs @@ -47,7 +47,7 @@ pub fn usdt_id() -> u32 { AcceptedFundingAsset::USDT.to_assethub_id() } -pub fn default_project(issuer: AccountIdOf) -> ProjectMetadataOf +pub fn default_project_metadata(issuer: AccountIdOf) -> ProjectMetadataOf where T::Price: From, T::Hash: From, @@ -105,7 +105,7 @@ where { let threshold = ::EvaluationSuccessThreshold::get(); let default_project_metadata: ProjectMetadataOf = - default_project::(account::>("issuer", 0, 0)); + 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; @@ -132,7 +132,7 @@ where ::Balance: From, T::Hash: From, { - let default_project_metadata = default_project::(account::>("issuer", 0, 0)); + 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.auction_round_allocation_percentage * default_project_metadata.total_allocation_size, ); @@ -155,7 +155,7 @@ where T::Hash: From, { let inst = BenchInstantiator::::new(None); - let default_project = default_project::(account::>("issuer", 0, 0)); + let default_project = default_project_metadata::(account::>("issuer", 0, 0)); let total_ct_for_bids = default_project.auction_round_allocation_percentage * 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( @@ -174,7 +174,7 @@ where T::Hash: From, { let inst = BenchInstantiator::::new(None); - let default_project_metadata = default_project::(account::>("issuer", 0, 0)); + let default_project_metadata = default_project_metadata::(account::>("issuer", 0, 0)); let funding_target = default_project_metadata.minimum_price.saturating_mul_int(default_project_metadata.total_allocation_size); @@ -200,7 +200,7 @@ where T::Hash: From, { let inst = BenchInstantiator::::new(None); - let default_project_metadata = default_project::(account::>("issuer", 0, 0)); + let default_project_metadata = default_project_metadata::(account::>("issuer", 0, 0)); let funding_target = default_project_metadata.minimum_price.saturating_mul_int(default_project_metadata.total_allocation_size); @@ -316,7 +316,7 @@ where let issuer = string_account::>(issuer_name, 0, 0); TestProjectParams:: { expected_state: state, - metadata: default_project::(issuer.clone()), + metadata: default_project_metadata::(issuer.clone()), issuer: issuer.clone(), evaluations: default_evaluations::(), bids: default_bids::(), @@ -382,7 +382,7 @@ mod benchmarks { let issuer = account::>("issuer", 0, 0); whitelist_account!(issuer); - let project_metadata = default_project::(issuer.clone()); + let project_metadata = default_project_metadata::(issuer.clone()); let metadata_deposit = T::ContributionTokenCurrency::calc_metadata_deposit( project_metadata.token_information.name.as_slice(), @@ -431,7 +431,7 @@ mod benchmarks { let issuer = account::>("issuer", 0, 0); whitelist_account!(issuer); - let project_metadata = default_project::(issuer.clone()); + let project_metadata = default_project_metadata::(issuer.clone()); let project_id = inst.create_new_project(project_metadata.clone(), issuer.clone()); let jwt = get_mock_jwt_with_cid( issuer.clone(), @@ -464,7 +464,7 @@ mod benchmarks { let issuer_funding = account::>("issuer_funding", 0, 0); whitelist_account!(issuer); - let project_metadata = default_project::(issuer.clone()); + let project_metadata = default_project_metadata::(issuer.clone()); let project_id = inst.create_new_project(project_metadata.clone(), issuer.clone()); let project_metadata = ProjectMetadataOf:: { @@ -566,7 +566,7 @@ mod benchmarks { let issuer = account::>("issuer", 0, 0); whitelist_account!(issuer); - let project_metadata = default_project::(issuer.clone()); + let project_metadata = default_project_metadata::(issuer.clone()); let project_id = inst.create_new_project(project_metadata.clone(), issuer.clone()); // start_evaluation fn will try to add an automatic transition 1 block after the last evaluation block @@ -619,7 +619,7 @@ mod benchmarks { let issuer = account::>("issuer", 0, 0); whitelist_account!(issuer); - let project_metadata = default_project::(issuer.clone()); + let project_metadata = default_project_metadata::(issuer.clone()); let project_id = inst.create_evaluating_project(project_metadata.clone(), issuer.clone()); let evaluations = default_evaluations(); @@ -679,7 +679,7 @@ mod benchmarks { let test_evaluator = account::>("evaluator", 0, 0); whitelist_account!(test_evaluator); - let project_metadata = default_project::(issuer.clone()); + let project_metadata = default_project_metadata::(issuer.clone()); let project_id = inst.create_evaluating_project(project_metadata.clone(), issuer); let existing_evaluation = UserToUSDBalance::new(test_evaluator.clone(), (200 * USD_UNIT).into()); @@ -790,7 +790,7 @@ mod benchmarks { let bidder = account::>("bidder", 0, 0); whitelist_account!(bidder); - let mut project_metadata = default_project::(issuer.clone()); + let mut project_metadata = default_project_metadata::(issuer.clone()); project_metadata.mainnet_token_max_supply = (100_000 * CT_UNIT).try_into().unwrap_or_else(|_| panic!("Failed to create BalanceOf")); project_metadata.total_allocation_size = @@ -1122,7 +1122,7 @@ mod benchmarks { let contributor = account::>("contributor", 0, 0); whitelist_account!(contributor); - let project_metadata = default_project::(issuer.clone()); + let project_metadata = default_project_metadata::(issuer.clone()); let project_id = inst.create_community_contributing_project( project_metadata.clone(), @@ -1412,7 +1412,7 @@ mod benchmarks { let issuer = account::>("issuer", 0, 0); whitelist_account!(issuer); - let project_metadata = default_project::(issuer.clone()); + let project_metadata = default_project_metadata::(issuer.clone()); let target_funding_amount: BalanceOf = project_metadata.minimum_price.saturating_mul_int(project_metadata.total_allocation_size); @@ -1484,7 +1484,7 @@ mod benchmarks { whitelist_account!(evaluator); let project_id = inst.create_finished_project( - default_project::(issuer.clone()), + default_project_metadata::(issuer.clone()), issuer, evaluations, default_bids::(), @@ -1546,7 +1546,7 @@ mod benchmarks { let evaluator = evaluations[0].account.clone(); whitelist_account!(evaluator); - let project_metadata = default_project::(issuer.clone()); + let project_metadata = default_project_metadata::(issuer.clone()); let target_funding_amount: BalanceOf = project_metadata.minimum_price.saturating_mul_int(project_metadata.total_allocation_size); @@ -1629,7 +1629,7 @@ mod benchmarks { whitelist_account!(bidder); let project_id = inst.create_finished_project( - default_project::(issuer.clone()), + default_project_metadata::(issuer.clone()), issuer, default_evaluations::(), bids, @@ -1672,7 +1672,7 @@ mod benchmarks { let issuer = account::>("issuer", 0, 0); let evaluations = default_evaluations::(); - let project_metadata = default_project::(issuer.clone()); + let project_metadata = default_project_metadata::(issuer.clone()); let target_funding_amount: BalanceOf = project_metadata.minimum_price.saturating_mul_int(project_metadata.total_allocation_size); @@ -1734,7 +1734,7 @@ mod benchmarks { whitelist_account!(contributor); let project_id = inst.create_finished_project( - default_project::(issuer.clone()), + default_project_metadata::(issuer.clone()), issuer, default_evaluations::(), default_bids::(), @@ -1788,7 +1788,7 @@ mod benchmarks { let issuer = account::>("issuer", 0, 0); let evaluations = default_evaluations::(); - let project_metadata = default_project::(issuer.clone()); + let project_metadata = default_project_metadata::(issuer.clone()); let target_funding_amount: BalanceOf = project_metadata.minimum_price.saturating_mul_int(project_metadata.total_allocation_size); @@ -1864,7 +1864,7 @@ mod benchmarks { let issuer = account::>("issuer", 0, 0); whitelist_account!(issuer); - let project_metadata = default_project::(issuer.clone()); + let project_metadata = default_project_metadata::(issuer.clone()); let project_id = inst.create_evaluating_project(project_metadata, issuer.clone()); let evaluations = default_evaluations(); @@ -1908,7 +1908,7 @@ mod benchmarks { let issuer = account::>("issuer", 0, 0); whitelist_account!(issuer); - let project_metadata = default_project::(issuer.clone()); + let project_metadata = default_project_metadata::(issuer.clone()); let project_id = inst.create_evaluating_project(project_metadata, issuer.clone()); let project_details = inst.get_project_details(project_id); @@ -1969,7 +1969,7 @@ mod benchmarks { let issuer = account::>("issuer", 0, 0); whitelist_account!(issuer); - let project_metadata = default_project::(issuer.clone()); + let project_metadata = default_project_metadata::(issuer.clone()); let project_id = inst.create_auctioning_project(project_metadata, issuer.clone(), default_evaluations()); let opening_end_block = @@ -1984,7 +1984,7 @@ mod benchmarks { #[block] { - Pallet::::do_auction_closing(project_id).unwrap(); + Pallet::::do_start_auction_closing(project_id).unwrap(); } // * validity checks * // Storage @@ -1997,16 +1997,14 @@ mod benchmarks { ); } - // do_community_funding - // Should be complex due to calling `calculate_weighted_average_price` #[benchmark] - fn start_community_funding( + fn end_auction_closing( // Insertion attempts in add_to_update_store. Total amount of storage items iterated through in `ProjectsToUpdate`. Leave one free to make the fn succeed x: Linear<1, { ::MaxProjectsToUpdateInsertionAttempts::get() - 1 }>, // Accepted Bids - y: Linear<0, { ::MaxBidsPerProject::get() / 2 }>, + y: Linear<1, { ::MaxBidsPerProject::get() / 2 }>, // Failed Bids - z: Linear<0, { ::MaxBidsPerProject::get() / 2 }>, + z: Linear<1, { ::MaxBidsPerProject::get() / 2 }>, ) { // * setup * let mut inst = BenchInstantiator::::new(None); @@ -2015,67 +2013,46 @@ mod benchmarks { let issuer = account::>("issuer", 0, 0); whitelist_account!(issuer); - let bounded_name = BoundedVec::try_from("Contribution Token TEST".as_bytes().to_vec()).unwrap(); - let bounded_symbol = BoundedVec::try_from("CTEST".as_bytes().to_vec()).unwrap(); - let metadata_hash = BoundedVec::try_from(IPFS_CID.as_bytes().to_vec()).unwrap(); - let project_metadata = ProjectMetadata { - token_information: CurrencyMetadata { name: bounded_name, symbol: bounded_symbol, decimals: CT_DECIMALS }, - mainnet_token_max_supply: BalanceOf::::try_from(1_000_000 * CT_UNIT) - .unwrap_or_else(|_| panic!("Failed to create BalanceOf")), - total_allocation_size: BalanceOf::::try_from(1_000_000 * CT_UNIT) - .unwrap_or_else(|_| panic!("Failed to create BalanceOf")), - auction_round_allocation_percentage: Percent::from_percent(50u8), - minimum_price: PriceProviderOf::::calculate_decimals_aware_price( - 10u128.into(), - USD_DECIMALS, - CT_DECIMALS, - ) - .unwrap(), - bidding_ticket_sizes: BiddingTicketSizes { - professional: TicketSize::new( - BalanceOf::::try_from(5000 * USD_UNIT).unwrap_or_else(|_| panic!("Failed to create BalanceOf")), - None, - ), - institutional: TicketSize::new( - BalanceOf::::try_from(5000 * USD_UNIT).unwrap_or_else(|_| panic!("Failed to create BalanceOf")), - None, - ), - phantom: Default::default(), - }, - contributing_ticket_sizes: ContributingTicketSizes { - retail: TicketSize::new( - BalanceOf::::try_from(USD_UNIT).unwrap_or_else(|_| panic!("Failed to create BalanceOf")), - None, - ), - professional: TicketSize::new( - BalanceOf::::try_from(USD_UNIT).unwrap_or_else(|_| panic!("Failed to create BalanceOf")), - None, - ), - institutional: TicketSize::new( - BalanceOf::::try_from(USD_UNIT).unwrap_or_else(|_| panic!("Failed to create BalanceOf")), - None, - ), - phantom: Default::default(), - }, - participation_currencies: vec![AcceptedFundingAsset::USDT].try_into().unwrap(), - funding_destination_account: issuer.clone(), - policy_ipfs_cid: Some(metadata_hash.into()), - }; - let project_id = - inst.create_auctioning_project(project_metadata.clone(), issuer.clone(), default_evaluations()); + let mut project_metadata = default_project_metadata::(issuer.clone()); + project_metadata.mainnet_token_max_supply = + BalanceOf::::try_from(10_000_000 * CT_UNIT).unwrap_or_else(|_| panic!("Failed to create BalanceOf")); + project_metadata.total_allocation_size = + BalanceOf::::try_from(10_000_000 * CT_UNIT).unwrap_or_else(|_| panic!("Failed to create BalanceOf")); + project_metadata.auction_round_allocation_percentage = Percent::from_percent(100u8); + + let project_id = inst.create_auctioning_project( + project_metadata.clone(), + issuer.clone(), + inst.generate_successful_evaluations( + project_metadata.clone(), + default_evaluators::(), + default_weights(), + ), + ); - let accepted_bids = (0..y) + let auction_allocation = + project_metadata.auction_round_allocation_percentage * project_metadata.total_allocation_size; + let min_bid_amount = 500u128; + let smaller_than_wap_accepted_bid = vec![BidParams::::new( + account::>("bidder", 0, 0), + auction_allocation, + 1u8, + AcceptedFundingAsset::USDT, + )]; + let higher_than_wap_accepted_bids = (1..y) .map(|i| { BidParams::::new( account::>("bidder", 0, i), - (500 * CT_UNIT).into(), + (min_bid_amount * CT_UNIT).into(), 1u8, AcceptedFundingAsset::USDT, ) }) .collect_vec(); + let accepted_bids = + vec![smaller_than_wap_accepted_bid, higher_than_wap_accepted_bids].into_iter().flatten().collect_vec(); let rejected_bids = (0..z) .map(|i| { BidParams::::new( @@ -2087,7 +2064,7 @@ mod benchmarks { }) .collect_vec(); - let all_bids = accepted_bids.iter().chain(rejected_bids.iter()).cloned().collect_vec(); + let all_bids = vec![accepted_bids.clone(), rejected_bids.clone()].into_iter().flatten().collect_vec(); let plmc_needed_for_bids = inst.calculate_auction_plmc_charged_from_all_bids_made_or_with_bucket( &all_bids, @@ -2112,36 +2089,166 @@ mod benchmarks { inst.jump_to_block(transition_block); let auction_closing_end_block = inst.get_project_details(project_id).phase_transition_points.auction_closing.end().unwrap(); - // we don't use advance time to avoid triggering on_initialize. This benchmark should only measure the fn - // weight and not the whole on_initialize call weight - frame_system::Pallet::::set_block_number(auction_closing_end_block + One::one()); + // Go to the last block of closing auction, to make bids fail + frame_system::Pallet::::set_block_number(auction_closing_end_block); + inst.bid_for_users(project_id, rejected_bids).unwrap(); + let now = inst.current_block(); + let transition_block = now + One::one(); + frame_system::Pallet::::set_block_number(transition_block); - let community_end_block = now + T::CommunityFundingDuration::get(); - - let insertion_block_number = community_end_block + One::one(); - fill_projects_to_update::(x, insertion_block_number); + fill_projects_to_update::(x, transition_block); #[block] { - Pallet::::do_start_community_funding(project_id).unwrap(); + Pallet::::do_end_auction_closing(project_id).unwrap(); } // * validity checks * // Storage let stored_details = ProjectsDetails::::get(project_id).unwrap(); - assert_eq!(stored_details.status, ProjectStatus::CommunityRound); + assert_eq!(stored_details.status, ProjectStatus::CalculatingWAP); assert!( stored_details.phase_transition_points.random_closing_ending.unwrap() < stored_details.phase_transition_points.auction_closing.end().unwrap() ); - let accepted_bids_count = - Bids::::iter_prefix_values((project_id,)).filter(|b| matches!(b.status, BidStatus::Accepted)).count(); + let accepted_bids_count = Bids::::iter_prefix_values((project_id,)) + .filter(|b| matches!(b.status, BidStatus::Accepted | BidStatus::PartiallyAccepted(..))) + .count(); let rejected_bids_count = Bids::::iter_prefix_values((project_id,)).filter(|b| matches!(b.status, BidStatus::Rejected(_))).count(); assert_eq!(rejected_bids_count, 0); assert_eq!(accepted_bids_count, y as usize); + // Events + frame_system::Pallet::::assert_last_event( + Event::::ProjectPhaseTransition { project_id, phase: ProjectPhases::CalculatingWAP }.into(), + ); + } + + // do_community_funding + // Should be complex due to calling `calculate_weighted_average_price` + #[benchmark] + fn start_community_funding( + // Insertion attempts in add_to_update_store. Total amount of storage items iterated through in `ProjectsToUpdate`. Leave one free to make the fn succeed + x: Linear<1, { ::MaxProjectsToUpdateInsertionAttempts::get() - 1 }>, + // Accepted Bids + y: Linear<1, { ::MaxBidsPerProject::get() / 2 }>, + // Rejected Bids + z: Linear<1, { ::MaxBidsPerProject::get() / 2 }>, + ) { + // * setup * + let mut inst = BenchInstantiator::::new(None); + // real benchmark starts at block 0, and we can't call `events()` at block 0 + inst.advance_time(1u32.into()).unwrap(); + + let issuer = account::>("issuer", 0, 0); + whitelist_account!(issuer); + let mut project_metadata = default_project_metadata::(issuer.clone()); + + project_metadata.mainnet_token_max_supply = + BalanceOf::::try_from(10_000_000 * CT_UNIT).unwrap_or_else(|_| panic!("Failed to create BalanceOf")); + project_metadata.total_allocation_size = + BalanceOf::::try_from(10_000_000 * CT_UNIT).unwrap_or_else(|_| panic!("Failed to create BalanceOf")); + project_metadata.auction_round_allocation_percentage = Percent::from_percent(100u8); + + let project_id = inst.create_auctioning_project( + project_metadata.clone(), + issuer.clone(), + inst.generate_successful_evaluations( + project_metadata.clone(), + default_evaluators::(), + default_weights(), + ), + ); + + let auction_allocation = + project_metadata.auction_round_allocation_percentage * project_metadata.total_allocation_size; + let min_bid_amount = 500u128; + let smaller_than_wap_accepted_bid = vec![BidParams::::new( + account::>("bidder", 0, 0), + auction_allocation, + 1u8, + AcceptedFundingAsset::USDT, + )]; + let higher_than_wap_accepted_bids = (1..y) + .map(|i| { + BidParams::::new( + account::>("bidder", 0, i), + (min_bid_amount * CT_UNIT).into(), + 1u8, + AcceptedFundingAsset::USDT, + ) + }) + .collect_vec(); + + let accepted_bids = + vec![smaller_than_wap_accepted_bid, higher_than_wap_accepted_bids].into_iter().flatten().collect_vec(); + + let rejected_bids = (0..z) + .map(|i| { + BidParams::::new( + account::>("bidder", 0, i), + (500 * CT_UNIT).into(), + 1u8, + AcceptedFundingAsset::USDT, + ) + }) + .collect_vec(); + + let all_bids = vec![accepted_bids.clone(), rejected_bids.clone()].into_iter().flatten().collect_vec(); + + let plmc_needed_for_bids = inst.calculate_auction_plmc_charged_from_all_bids_made_or_with_bucket( + &all_bids, + project_metadata.clone(), + None, + ); + let plmc_ed = all_bids.accounts().existential_deposits(); + let funding_asset_needed_for_bids = inst + .calculate_auction_funding_asset_charged_from_all_bids_made_or_with_bucket( + &all_bids, + project_metadata.clone(), + None, + ); + + inst.mint_plmc_to(plmc_needed_for_bids); + inst.mint_plmc_to(plmc_ed); + inst.mint_foreign_asset_to(funding_asset_needed_for_bids); + + inst.bid_for_users(project_id, accepted_bids).unwrap(); + + let transition_block = inst.get_update_block(project_id, &UpdateType::AuctionClosingStart).unwrap(); + inst.jump_to_block(transition_block); + let auction_closing_end_block = + inst.get_project_details(project_id).phase_transition_points.auction_closing.end().unwrap(); + // Go to the last block of closing auction, to make bids fail + frame_system::Pallet::::set_block_number(auction_closing_end_block); + inst.bid_for_users(project_id, rejected_bids).unwrap(); + + let transition_block = inst.get_update_block(project_id, &UpdateType::AuctionClosingEnd).unwrap(); + inst.jump_to_block(transition_block); + let transition_block = inst.get_update_block(project_id, &UpdateType::CommunityFundingStart).unwrap(); + // Block is at automatic transition, but it's not run with on_initialize, we do it manually + frame_system::Pallet::::set_block_number(transition_block); + + let now = inst.current_block(); + let community_end_block = now + T::CommunityFundingDuration::get() - One::one(); + + let insertion_block_number = community_end_block + One::one(); + fill_projects_to_update::(x, insertion_block_number); + + #[block] + { + Pallet::::do_start_community_funding(project_id).unwrap(); + } + + // * validity checks * + // Storage + let stored_details = ProjectsDetails::::get(project_id).unwrap(); + assert_eq!(stored_details.status, ProjectStatus::CommunityRound); + let accepted_bids_count = Bids::::iter_prefix_values((project_id,)).count(); + assert_eq!(accepted_bids_count, y as usize); + // Events frame_system::Pallet::::assert_last_event( Event::::ProjectPhaseTransition { project_id, phase: ProjectPhases::CommunityFunding }.into(), @@ -2163,7 +2270,7 @@ mod benchmarks { let issuer = account::>("issuer", 0, 0); whitelist_account!(issuer); - let project_metadata = default_project::(issuer.clone()); + let project_metadata = default_project_metadata::(issuer.clone()); let project_id = inst.create_community_contributing_project( project_metadata, issuer.clone(), @@ -2210,7 +2317,7 @@ mod benchmarks { let issuer = account::>("issuer", 0, 0); - let project_metadata = default_project::(issuer.clone()); + let project_metadata = default_project_metadata::(issuer.clone()); let target_funding_amount: BalanceOf = project_metadata.minimum_price.saturating_mul_int(project_metadata.total_allocation_size); @@ -2267,7 +2374,7 @@ mod benchmarks { let issuer = account::>("issuer", 0, 0); - let project_metadata = default_project::(issuer.clone()); + let project_metadata = default_project_metadata::(issuer.clone()); let target_funding_amount: BalanceOf = project_metadata.minimum_price.saturating_mul_int(project_metadata.total_allocation_size); @@ -2325,7 +2432,7 @@ mod benchmarks { let issuer = account::>("issuer", 0, 0); - let project_metadata = default_project::(issuer.clone()); + let project_metadata = default_project_metadata::(issuer.clone()); let target_funding_amount: BalanceOf = project_metadata.minimum_price.saturating_mul_int(project_metadata.total_allocation_size); @@ -2385,7 +2492,7 @@ mod benchmarks { let issuer = account::>("issuer", 0, 0); - let project_metadata = default_project::(issuer.clone()); + let project_metadata = default_project_metadata::(issuer.clone()); let target_funding_amount: BalanceOf = project_metadata.minimum_price.saturating_mul_int(project_metadata.total_allocation_size); @@ -2459,7 +2566,7 @@ mod benchmarks { let issuer = account::>("issuer", 0, 0); - let project_metadata = default_project::(issuer.clone()); + let project_metadata = default_project_metadata::(issuer.clone()); let target_funding_amount: BalanceOf = project_metadata.minimum_price.saturating_mul_int(project_metadata.total_allocation_size); let manual_outcome_threshold = Percent::from_percent(50); @@ -2508,7 +2615,7 @@ mod benchmarks { let issuer = account::>("issuer", 0, 0); - let project_metadata = default_project::(issuer.clone()); + let project_metadata = default_project_metadata::(issuer.clone()); let project_id = inst.create_finished_project( project_metadata, issuer.clone(), @@ -2538,7 +2645,7 @@ mod benchmarks { let issuer = account::>("issuer", 0, 0); - let project_metadata = default_project::(issuer.clone()); + let project_metadata = default_project_metadata::(issuer.clone()); let target_funding_amount: BalanceOf = project_metadata.minimum_price.saturating_mul_int(project_metadata.total_allocation_size); @@ -2711,6 +2818,13 @@ mod benchmarks { }); } + #[test] + fn bench_end_auction_closing() { + new_test_ext().execute_with(|| { + assert_ok!(PalletFunding::::test_end_auction_closing()); + }); + } + #[test] fn bench_start_community_funding() { new_test_ext().execute_with(|| { diff --git a/pallets/funding/src/functions/1_application.rs b/pallets/funding/src/functions/1_application.rs index 065f45a3b..2f6c855ba 100644 --- a/pallets/funding/src/functions/1_application.rs +++ b/pallets/funding/src/functions/1_application.rs @@ -44,6 +44,7 @@ impl Pallet { total_bonded_plmc: Zero::zero(), evaluators_outcome: EvaluatorsOutcome::Unchanged, }, + usd_bid_on_oversubscription: None, funding_end_block: None, parachain_id: None, migration_readiness_check: None, diff --git a/pallets/funding/src/functions/3_auction.rs b/pallets/funding/src/functions/3_auction.rs index dfce9a82f..b8dd237ca 100644 --- a/pallets/funding/src/functions/3_auction.rs +++ b/pallets/funding/src/functions/3_auction.rs @@ -4,7 +4,7 @@ impl Pallet { /// Called by user extrinsic /// Starts the auction round for a project. From the next block forward, any professional or /// institutional user can set bids for a token_amount/token_price pair. - /// Any bids from this point until the auction_closing starts, will be considered as valid. + /// Any bids from this point until the auction_closing starts will be considered as valid. /// /// # Arguments /// * `project_id` - The project identifier @@ -24,7 +24,7 @@ impl Pallet { /// Later on, `on_initialize` transitions the project into the closing auction round, by calling /// [`do_auction_closing`](Self::do_auction_closing). #[transactional] - pub fn do_auction_opening(caller: AccountIdOf, project_id: ProjectId) -> DispatchResultWithPostInfo { + pub fn do_start_auction_opening(caller: AccountIdOf, project_id: ProjectId) -> DispatchResultWithPostInfo { // * Get variables * let mut project_details = ProjectsDetails::::get(project_id).ok_or(Error::::ProjectDetailsNotFound)?; let now = >::block_number(); @@ -43,7 +43,7 @@ impl Pallet { ensure!(now >= auction_initialize_period_start_block, Error::::TooEarlyForRound); // If the auction is first manually started, the automatic transition fails here. This - // behaviour is intended, as it gracefully skips the automatic transition if the + // behavior is intended, as it gracefully skips the automatic transition if the // auction was started manually. ensure!(project_details.status == ProjectStatus::AuctionInitializePeriod, Error::::IncorrectRound); @@ -87,8 +87,8 @@ impl Pallet { /// Called automatically by on_initialize /// Starts the auction closing round for a project. - /// Any bids from this point until the auction closing round ends, are not guaranteed. Only bids - /// made before the random ending block between the auction closing start and end will be considered + /// Any bids from this point until the auction closing round ends are not guaranteed. + /// Only bids made before the random ending block between the auction closing start and end will be considered. /// /// # Arguments /// * `project_id` - The project identifier @@ -109,7 +109,7 @@ impl Pallet { /// Later on, `on_initialize` ends the auction closing round and starts the community round, /// by calling [`do_community_funding`](Self::do_start_community_funding). #[transactional] - pub fn do_auction_closing(project_id: ProjectId) -> DispatchResultWithPostInfo { + pub fn do_start_auction_closing(project_id: ProjectId) -> DispatchResultWithPostInfo { // * Get variables * let mut project_details = ProjectsDetails::::get(project_id).ok_or(Error::::ProjectDetailsNotFound)?; let now = >::block_number(); @@ -134,7 +134,7 @@ impl Pallet { // Schedule for automatic check by on_initialize. Success depending on enough funding reached let insertion_iterations = match Self::add_to_update_store( closing_end_block + 1u32.into(), - (&project_id, UpdateType::CommunityFundingStart), + (&project_id, UpdateType::AuctionClosingEnd), ) { Ok(iterations) => iterations, Err(_iterations) => return Err(Error::::TooManyInsertionAttempts.into()), @@ -149,6 +149,70 @@ impl Pallet { }) } + /// Decides which bids are accepted and which are rejected. + /// Deletes and refunds the rejected ones, and prepares the project for the WAP calculation the next block + #[transactional] + pub fn do_end_auction_closing(project_id: ProjectId) -> DispatchResultWithPostInfo { + // * Get variables * + let project_details = ProjectsDetails::::get(project_id).ok_or(Error::::ProjectDetailsNotFound)?; + let project_metadata = ProjectsMetadata::::get(project_id).ok_or(Error::::ProjectMetadataNotFound)?; + let now = >::block_number(); + let auction_closing_start_block = + project_details.phase_transition_points.auction_closing.start().ok_or(Error::::TransitionPointNotSet)?; + let auction_closing_end_block = + project_details.phase_transition_points.auction_closing.end().ok_or(Error::::TransitionPointNotSet)?; + + // * Validity checks * + ensure!(now > auction_closing_end_block, Error::::TooEarlyForRound); + ensure!(project_details.status == ProjectStatus::AuctionClosing, Error::::IncorrectRound); + + // * Calculate new variables * + let end_block = Self::select_random_block(auction_closing_start_block, auction_closing_end_block); + + // * Update Storage * + let calculation_result = Self::decide_winning_bids( + project_id, + end_block, + project_metadata.auction_round_allocation_percentage * project_metadata.total_allocation_size, + ); + + match calculation_result { + Err(e) => return Err(DispatchErrorWithPostInfo { post_info: ().into(), error: e }), + Ok((accepted_bids_count, rejected_bids_count)) => { + // Get info again after updating it with new price. + let mut project_details = + ProjectsDetails::::get(project_id).ok_or(Error::::ProjectDetailsNotFound)?; + project_details.phase_transition_points.random_closing_ending = Some(end_block); + project_details.status = ProjectStatus::CalculatingWAP; + ProjectsDetails::::insert(project_id, project_details); + + let insertion_iterations = match Self::add_to_update_store( + now + 1u32.into(), + (&project_id, UpdateType::CommunityFundingStart), + ) { + Ok(iterations) => iterations, + Err(_iterations) => return Err(Error::::TooManyInsertionAttempts.into()), + }; + + // * Emit events * + Self::deposit_event(Event::::ProjectPhaseTransition { + project_id, + phase: ProjectPhases::CalculatingWAP, + }); + + Ok(PostDispatchInfo { + // TODO: make new benchmark + actual_weight: Some(WeightInfoOf::::start_community_funding( + insertion_iterations, + accepted_bids_count, + rejected_bids_count, + )), + pays_fee: Pays::Yes, + }) + }, + } + } + /// Bid for a project in the bidding stage. /// /// # Arguments @@ -230,7 +294,7 @@ impl Pallet { ); ensure!(multiplier.into() <= max_multiplier && multiplier.into() > 0u8, Error::::ForbiddenMultiplier); - // Note: We limit the CT Amount to the auction allocation size, to avoid long running loops. + // Note: We limit the CT Amount to the auction allocation size, to avoid long-running loops. ensure!( ct_amount <= project_metadata.auction_round_allocation_percentage * project_metadata.total_allocation_size, Error::::TooHigh diff --git a/pallets/funding/src/functions/4_contribution.rs b/pallets/funding/src/functions/4_contribution.rs index 44e8d20ce..55d6ef4bb 100644 --- a/pallets/funding/src/functions/4_contribution.rs +++ b/pallets/funding/src/functions/4_contribution.rs @@ -27,33 +27,26 @@ impl Pallet { pub fn do_start_community_funding(project_id: ProjectId) -> DispatchResultWithPostInfo { // * Get variables * let project_details = ProjectsDetails::::get(project_id).ok_or(Error::::ProjectDetailsNotFound)?; - let project_metadata = ProjectsMetadata::::get(project_id).ok_or(Error::::ProjectMetadataNotFound)?; let now = >::block_number(); - let auction_closing_start_block = - project_details.phase_transition_points.auction_closing.start().ok_or(Error::::TransitionPointNotSet)?; let auction_closing_end_block = project_details.phase_transition_points.auction_closing.end().ok_or(Error::::TransitionPointNotSet)?; // * Validity checks * ensure!(now > auction_closing_end_block, Error::::TooEarlyForRound); - ensure!(project_details.status == ProjectStatus::AuctionClosing, Error::::IncorrectRound); + ensure!(project_details.status == ProjectStatus::CalculatingWAP, Error::::IncorrectRound); // * Calculate new variables * - let end_block = Self::select_random_block(auction_closing_start_block, auction_closing_end_block); let community_start_block = now; let community_end_block = now.saturating_add(T::CommunityFundingDuration::get()).saturating_sub(One::one()); + // * Update Storage * - let calculation_result = Self::calculate_weighted_average_price( - project_id, - end_block, - project_metadata.auction_round_allocation_percentage * project_metadata.total_allocation_size, - ); + let wap_result = Self::calculate_weighted_average_price(project_id); + let mut project_details = ProjectsDetails::::get(project_id).ok_or(Error::::ProjectDetailsNotFound)?; - match calculation_result { + match wap_result { Err(e) => return Err(DispatchErrorWithPostInfo { post_info: ().into(), error: e }), - Ok((accepted_bids_count, rejected_bids_count)) => { + Ok(winning_bids_count) => { // Get info again after updating it with new price. - project_details.phase_transition_points.random_closing_ending = Some(end_block); project_details .phase_transition_points .community @@ -75,10 +68,12 @@ impl Pallet { phase: ProjectPhases::CommunityFunding, }); + //TODO: address this + let rejected_bids_count = 0; Ok(PostDispatchInfo { actual_weight: Some(WeightInfoOf::::start_community_funding( insertion_iterations, - accepted_bids_count, + winning_bids_count, rejected_bids_count, )), pays_fee: Pays::Yes, diff --git a/pallets/funding/src/functions/misc.rs b/pallets/funding/src/functions/misc.rs index c1f42373d..d0a1b6e36 100644 --- a/pallets/funding/src/functions/misc.rs +++ b/pallets/funding/src/functions/misc.rs @@ -1,4 +1,5 @@ use super::*; +use itertools::Itertools; // Helper functions // ATTENTION: if this is called directly, it will not be transactional @@ -69,8 +70,7 @@ impl Pallet { Ok(VestingInfo { total_amount: bonded_amount, amount_per_block, duration }) } - /// Calculates the price (in USD) of contribution tokens for the Community and Remainder Rounds - pub fn calculate_weighted_average_price( + pub fn decide_winning_bids( project_id: ProjectId, end_block: BlockNumberFor, auction_allocation_size: BalanceOf, @@ -82,8 +82,6 @@ impl Pallet { // temp variable to store the total value of the bids (i.e price * amount = Cumulative Ticket Size) let mut bid_usd_value_sum = BalanceOf::::zero(); let project_account = Self::fund_account_id(project_id); - let plmc_price = T::PriceProvider::get_decimals_aware_price(PLMC_FOREIGN_ID, USD_DECIMALS, PLMC_DECIMALS) - .ok_or(Error::::PriceNotFound)?; let project_metadata = ProjectsMetadata::::get(project_id).ok_or(Error::::ProjectMetadataNotFound)?; let mut highest_accepted_price = project_metadata.minimum_price; @@ -122,21 +120,44 @@ impl Pallet { bid.final_ct_amount = buyable_amount; highest_accepted_price = highest_accepted_price.max(bid.original_ct_usd_price); } + Bids::::insert((project_id, &bid.bidder, &bid.id), &bid); bid }) .partition(|bid| matches!(bid.status, BidStatus::Accepted | BidStatus::PartiallyAccepted(..))); - // Weight calculation variables - let accepted_bids_count = accepted_bids.len() as u32; - let rejected_bids_count = rejected_bids.len() as u32; - // Refund rejected bids. We do it here, so we don't have to calculate all the project // prices and then fail to refund the bids. + let total_rejected_bids = rejected_bids.len() as u32; for bid in rejected_bids.into_iter() { Self::refund_bid(&bid, project_id, &project_account)?; Bids::::remove((project_id, &bid.bidder, &bid.id)); } + ProjectsDetails::::mutate(project_id, |maybe_info| -> DispatchResult { + if let Some(info) = maybe_info { + info.remaining_contribution_tokens.saturating_reduce(bid_token_amount_sum); + if highest_accepted_price > project_metadata.minimum_price { + info.usd_bid_on_oversubscription = Some(bid_usd_value_sum); + } + Ok(()) + } else { + Err(Error::::ProjectDetailsNotFound.into()) + } + })?; + + Ok((accepted_bids.len() as u32, total_rejected_bids)) + } + + /// Calculates the price (in USD) of contribution tokens for the Community and Remainder Rounds + pub fn calculate_weighted_average_price(project_id: ProjectId) -> Result { + let project_metadata = ProjectsMetadata::::get(project_id).ok_or(Error::::ProjectMetadataNotFound)?; + let project_details = ProjectsDetails::::get(project_id).ok_or(Error::::ProjectDetailsNotFound)?; + // Rejected bids were deleted in the previous block. + let accepted_bids = Bids::::iter_prefix_values((project_id,)).collect_vec(); + let project_account = Self::fund_account_id(project_id); + let plmc_price = T::PriceProvider::get_decimals_aware_price(PLMC_FOREIGN_ID, USD_DECIMALS, PLMC_DECIMALS) + .ok_or(Error::::PriceNotFound)?; + // Calculate the weighted price of the token for the next funding rounds, using winning bids. // for example: if there are 3 winning bids, // A: 10K tokens @ USD15 per token = 150K USD value @@ -155,31 +176,34 @@ impl Pallet { // lastly, sum all the weighted prices to get the final weighted price for the next funding round // 3 + 10.6 + 2.6 = 16.333... - let calc_weighted_price_fn = |bid: &BidInfoOf| -> PriceOf { - let ticket_size = bid.original_ct_usd_price.saturating_mul_int(bid.final_ct_amount); - let bid_weight = ::saturating_from_rational(ticket_size, bid_usd_value_sum); - let weighted_price = bid.original_ct_usd_price.saturating_mul(bid_weight); - weighted_price - }; - let mut weighted_token_price = if highest_accepted_price == project_metadata.minimum_price { - project_metadata.minimum_price - } else { + + // After reading from storage all accepted bids when calculating the weighted price of each bid, we store them here + let mut weighted_token_price = if let Some(total_usd_bid) = project_details.usd_bid_on_oversubscription { + let calc_weighted_price_fn = |bid: &BidInfoOf| -> PriceOf { + let ticket_size = bid.original_ct_usd_price.saturating_mul_int(bid.final_ct_amount); + let bid_weight = ::saturating_from_rational(ticket_size, total_usd_bid); + let weighted_price = bid.original_ct_usd_price.saturating_mul(bid_weight); + weighted_price + }; + accepted_bids .iter() .map(calc_weighted_price_fn) - .fold(Zero::zero(), |a: T::Price, b: T::Price| a.saturating_add(b)) + .fold(Zero::zero(), |a: PriceOf, b: PriceOf| a.saturating_add(b)) + } else { + project_metadata.minimum_price }; - // We are 99% sure that the price cannot be less than minimum if some accepted bids have higher price, but rounding + + // We are 99% sure that the price cannot be less than the minimum if some accepted bids have higher price, but rounding // errors are strange, so we keep this just in case. if weighted_token_price < project_metadata.minimum_price { weighted_token_price = project_metadata.minimum_price; - } + }; let mut final_total_funding_reached_by_bids = BalanceOf::::zero(); - // Update storage - // Update the bid in the storage - for mut bid in accepted_bids.into_iter() { + let total_accepted_bids = accepted_bids.len() as u32; + for mut bid in accepted_bids { if bid.final_ct_usd_price > weighted_token_price || matches!(bid.status, BidStatus::PartiallyAccepted(..)) { if bid.final_ct_usd_price > weighted_token_price { bid.final_ct_usd_price = weighted_token_price; @@ -247,7 +271,6 @@ impl Pallet { ProjectsDetails::::mutate(project_id, |maybe_info| -> DispatchResult { if let Some(info) = maybe_info { info.weighted_average_price = Some(weighted_token_price); - info.remaining_contribution_tokens.saturating_reduce(bid_token_amount_sum); info.funding_amount_reached_usd.saturating_accrue(final_total_funding_reached_by_bids); Ok(()) } else { @@ -255,7 +278,7 @@ impl Pallet { } })?; - Ok((accepted_bids_count, rejected_bids_count)) + Ok(total_accepted_bids) } /// Refund a bid because of `reason`. diff --git a/pallets/funding/src/instantiator/async_features.rs b/pallets/funding/src/instantiator/async_features.rs index 01844260e..7e1947479 100644 --- a/pallets/funding/src/instantiator/async_features.rs +++ b/pallets/funding/src/instantiator/async_features.rs @@ -203,7 +203,7 @@ pub async fn async_start_auction< assert_eq!(inst.get_project_details(project_id).status, ProjectStatus::AuctionInitializePeriod); - inst.execute(|| crate::Pallet::::do_auction_opening(caller.clone(), project_id).unwrap()); + inst.execute(|| crate::Pallet::::do_start_auction_opening(caller.clone(), project_id).unwrap()); assert_eq!(inst.get_project_details(project_id).status, ProjectStatus::AuctionOpening); @@ -280,35 +280,32 @@ pub async fn async_start_community_funding< project_id: ProjectId, ) -> Result<(), DispatchError> { let mut inst = instantiator.lock().await; + if let Some(update_block) = inst.get_update_block(project_id, &UpdateType::AuctionClosingStart) { + let notify = Arc::new(Notify::new()); + block_orchestrator.add_awaiting_project(update_block, notify.clone()).await; + drop(inst); + notify.notified().await; + } + let mut inst = instantiator.lock().await; + if let Some(update_block) = inst.get_update_block(project_id, &UpdateType::AuctionClosingEnd) { + let notify = Arc::new(Notify::new()); + block_orchestrator.add_awaiting_project(update_block, notify.clone()).await; + drop(inst); + notify.notified().await; + } + let mut inst = instantiator.lock().await; + if let Some(update_block) = inst.get_update_block(project_id, &UpdateType::CommunityFundingStart) { + let notify = Arc::new(Notify::new()); + block_orchestrator.add_awaiting_project(update_block, notify.clone()).await; + drop(inst); + notify.notified().await; + } + let mut inst = instantiator.lock().await; - let update_block = inst.get_update_block(project_id, &UpdateType::AuctionClosingStart).unwrap(); - let closing_start = update_block; - - let notify = Arc::new(Notify::new()); - - block_orchestrator.add_awaiting_project(closing_start, notify.clone()).await; - - // Wait for the notification that our desired block was reached to continue - - drop(inst); - - notify.notified().await; - - inst = instantiator.lock().await; - let update_block = inst.get_update_block(project_id, &UpdateType::CommunityFundingStart).unwrap(); - let community_start = update_block; - - let notify = Arc::new(Notify::new()); - - block_orchestrator.add_awaiting_project(community_start, notify.clone()).await; - - drop(inst); - - notify.notified().await; - - inst = instantiator.lock().await; - - assert_eq!(inst.get_project_details(project_id).status, ProjectStatus::CommunityRound); + ensure!( + inst.get_project_details(project_id).status == ProjectStatus::CommunityRound, + DispatchError::from("Auction failed") + ); Ok(()) } @@ -765,14 +762,16 @@ pub async fn async_create_project_at< ) -> ProjectId { let time_to_new_project: BlockNumberFor = Zero::zero(); let time_to_evaluation: BlockNumberFor = time_to_new_project + Zero::zero(); - // we immediately start the auction, so we dont wait for T::AuctionInitializePeriodDuration. + // we immediately start the auction, so we don't wait for T::AuctionInitializePeriodDuration. let time_to_auction: BlockNumberFor = time_to_evaluation + ::EvaluationDuration::get(); - let time_to_community: BlockNumberFor = - time_to_auction + ::AuctionOpeningDuration::get() + ::AuctionClosingDuration::get(); + let wap_calculation_duration: BlockNumberFor = 2u32.into(); + let time_to_community: BlockNumberFor = time_to_auction + + ::AuctionOpeningDuration::get() + + ::AuctionClosingDuration::get() + + wap_calculation_duration; let time_to_remainder: BlockNumberFor = time_to_community + ::CommunityFundingDuration::get(); - let time_to_finish: BlockNumberFor = time_to_remainder + - ::RemainderFundingDuration::get() + - ::SuccessToSettlementTime::get(); + let time_to_finish: BlockNumberFor = time_to_remainder + ::RemainderFundingDuration::get(); + let mut inst = mutex_inst.lock().await; let now = inst.current_block(); drop(inst); diff --git a/pallets/funding/src/instantiator/chain_interactions.rs b/pallets/funding/src/instantiator/chain_interactions.rs index d0a70f8ec..55e2fe2e8 100644 --- a/pallets/funding/src/instantiator/chain_interactions.rs +++ b/pallets/funding/src/instantiator/chain_interactions.rs @@ -292,6 +292,7 @@ impl< total_bonded_plmc: Zero::zero(), evaluators_outcome: EvaluatorsOutcome::Unchanged, }, + usd_bid_on_oversubscription: None, funding_end_block: None, parachain_id: None, migration_readiness_check: None, @@ -451,7 +452,7 @@ impl< assert_eq!(self.get_project_details(project_id).status, ProjectStatus::AuctionInitializePeriod); - self.execute(|| crate::Pallet::::do_auction_opening(caller, project_id).unwrap()); + self.execute(|| crate::Pallet::::do_start_auction_opening(caller, project_id).unwrap()); assert_eq!(self.get_project_details(project_id).status, ProjectStatus::AuctionOpening); @@ -515,14 +516,14 @@ impl< pub fn start_community_funding(&mut self, project_id: ProjectId) -> Result<(), DispatchError> { if let Some(update_block) = self.get_update_block(project_id, &UpdateType::AuctionClosingStart) { - self.execute(|| frame_system::Pallet::::set_block_number(update_block - One::one())); - self.advance_time(1u32.into()).unwrap(); + self.jump_to_block(update_block); + } + if let Some(update_block) = self.get_update_block(project_id, &UpdateType::AuctionClosingEnd) { + self.jump_to_block(update_block); + } + if let Some(update_block) = self.get_update_block(project_id, &UpdateType::CommunityFundingStart) { + self.jump_to_block(update_block); } - let Some(update_block) = self.get_update_block(project_id, &UpdateType::CommunityFundingStart) else { - unreachable!() - }; - self.execute(|| frame_system::Pallet::::set_block_number(update_block - One::one())); - self.advance_time(1u32.into()).unwrap(); ensure!( self.get_project_details(project_id).status == ProjectStatus::CommunityRound, diff --git a/pallets/funding/src/lib.rs b/pallets/funding/src/lib.rs index eb437ef25..ca864a4ce 100644 --- a/pallets/funding/src/lib.rs +++ b/pallets/funding/src/lib.rs @@ -139,7 +139,6 @@ use sp_std::{marker::PhantomData, prelude::*}; pub use types::*; use xcm::v3::{opaque::Instruction, prelude::*, SendXcm}; - #[cfg(test)] pub mod mock; pub mod storage_migrations; @@ -151,10 +150,10 @@ pub mod tests; #[cfg(feature = "runtime-benchmarks")] pub mod benchmarking; +mod functions; #[cfg(any(feature = "runtime-benchmarks", feature = "std"))] pub mod instantiator; pub mod traits; -mod functions; pub type AccountIdOf = ::AccountId; pub type ProjectId = u32; @@ -889,7 +888,7 @@ pub mod pallet { let (account, _did, investor_type, _cid) = T::InvestorOrigin::ensure_origin(origin, &jwt, T::VerifierPublicKey::get())?; ensure!(investor_type == InvestorType::Institutional, Error::::WrongInvestorType); - Self::do_auction_opening(account, project_id) + Self::do_start_auction_opening(account, project_id) } /// Bond PLMC for a project in the evaluation stage @@ -1173,16 +1172,19 @@ pub mod pallet { #[pallet::weight(WeightInfoOf::::start_auction_manually(::MaxProjectsToUpdateInsertionAttempts::get() - 1))] pub fn root_do_auction_opening(origin: OriginFor, project_id: ProjectId) -> DispatchResultWithPostInfo { ensure_root(origin)?; - Self::do_auction_opening(T::PalletId::get().into_account_truncating(), project_id) + Self::do_start_auction_opening(T::PalletId::get().into_account_truncating(), project_id) } #[pallet::call_index(29)] #[pallet::weight(WeightInfoOf::::start_auction_closing_phase( ::MaxProjectsToUpdateInsertionAttempts::get() - 1, ))] - pub fn root_do_auction_closing(origin: OriginFor, project_id: ProjectId) -> DispatchResultWithPostInfo { + pub fn root_do_start_auction_closing( + origin: OriginFor, + project_id: ProjectId, + ) -> DispatchResultWithPostInfo { ensure_root(origin)?; - Self::do_auction_closing(project_id) + Self::do_start_auction_closing(project_id) } #[pallet::call_index(30)] @@ -1290,7 +1292,8 @@ pub mod pallet { // AuctionInitializePeriod -> AuctionOpening // Only if it wasn't first handled by user extrinsic UpdateType::AuctionOpeningStart => { - let call = Self::do_auction_opening(T::PalletId::get().into_account_truncating(), project_id); + let call = + Self::do_start_auction_opening(T::PalletId::get().into_account_truncating(), project_id); let fallback_weight = Call::::root_do_auction_opening { project_id }.get_dispatch_info().weight; update_weight(&mut used_weight, call, fallback_weight); @@ -1298,9 +1301,16 @@ pub mod pallet { // AuctionOpening -> AuctionClosing UpdateType::AuctionClosingStart => { - let call = Self::do_auction_closing(project_id); + let call = Self::do_start_auction_closing(project_id); + let fallback_weight = + Call::::root_do_start_auction_closing { project_id }.get_dispatch_info().weight; + update_weight(&mut used_weight, call, fallback_weight); + }, + + UpdateType::AuctionClosingEnd => { + let call = Self::do_end_auction_closing(project_id); let fallback_weight = - Call::::root_do_auction_closing { project_id }.get_dispatch_info().weight; + Call::::root_do_start_auction_closing { project_id }.get_dispatch_info().weight; update_weight(&mut used_weight, call, fallback_weight); }, diff --git a/pallets/funding/src/mock.rs b/pallets/funding/src/mock.rs index 778dda116..22ef15f5c 100644 --- a/pallets/funding/src/mock.rs +++ b/pallets/funding/src/mock.rs @@ -290,14 +290,14 @@ pub const HOURS: BlockNumber = 300u64; // REMARK: In the production configuration we use DAYS instead of HOURS. parameter_types! { - pub const EvaluationDuration: BlockNumber = 3u64; - pub const AuctionInitializePeriodDuration: BlockNumber = 3u64; - pub const AuctionOpeningDuration: BlockNumber = 3u64; - pub const AuctionClosingDuration: BlockNumber = 3u64; - pub const CommunityRoundDuration: BlockNumber = 3u64; - pub const RemainderFundingDuration: BlockNumber = 3u64; - pub const ManualAcceptanceDuration: BlockNumber = 3u64; - pub const SuccessToSettlementTime: BlockNumber = 3u64; + pub const EvaluationDuration: BlockNumber = 10u64; + pub const AuctionInitializePeriodDuration: BlockNumber = 10u64; + pub const AuctionOpeningDuration: BlockNumber = 10u64; + pub const AuctionClosingDuration: BlockNumber = 10u64; + pub const CommunityRoundDuration: BlockNumber = 10u64; + pub const RemainderFundingDuration: BlockNumber = 10u64; + pub const ManualAcceptanceDuration: BlockNumber = 10u64; + pub const SuccessToSettlementTime: BlockNumber = 10u64; pub const FundingPalletId: PalletId = PalletId(*b"py/cfund"); pub FeeBrackets: Vec<(Percent, Balance)> = vec![ diff --git a/pallets/funding/src/tests/2_evaluation.rs b/pallets/funding/src/tests/2_evaluation.rs index 725e3fd79..0251d092b 100644 --- a/pallets/funding/src/tests/2_evaluation.rs +++ b/pallets/funding/src/tests/2_evaluation.rs @@ -325,6 +325,7 @@ mod start_evaluation_extrinsic { total_bonded_plmc: 0u128, evaluators_outcome: EvaluatorsOutcome::Unchanged, }, + usd_bid_on_oversubscription: None, funding_end_block: None, parachain_id: None, migration_readiness_check: None, diff --git a/pallets/funding/src/tests/3_auction.rs b/pallets/funding/src/tests/3_auction.rs index 6319bcf32..b0c279376 100644 --- a/pallets/funding/src/tests/3_auction.rs +++ b/pallets/funding/src/tests/3_auction.rs @@ -671,6 +671,37 @@ mod round_flow { hash_set_2.insert(amount); } } + + #[test] + fn all_bids_but_one_have_price_higher_than_wap() { + 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 big_bid: BidParams = (BIDDER_1, total_allocation).into(); + let small_bids: Vec> = + (0..1023u32).map(|i| (i + BIDDER_1, 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; + project_metadata.auction_round_allocation_percentage = Percent::from_percent(100); + + let project_id = inst.create_community_contributing_project( + project_metadata.clone(), + ISSUER_1, + inst.generate_successful_evaluations(project_metadata.clone(), default_evaluators(), default_weights()), + all_bids, + ); + + let wap = inst.get_project_details(project_id).weighted_average_price.unwrap(); + + let all_bids = inst.execute(|| Bids::::iter_prefix_values((project_id,)).collect_vec()); + + 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(), 1023); + } } #[cfg(test)] @@ -745,7 +776,7 @@ mod start_auction_extrinsic { inst.advance_time(::EvaluationDuration::get() + 1).unwrap(); assert_eq!(inst.get_project_details(project_id).status, ProjectStatus::AuctionInitializePeriod); inst.advance_time(1).unwrap(); - inst.execute(|| Pallet::::do_auction_opening(ISSUER_1, project_id)).unwrap(); + inst.execute(|| Pallet::::do_start_auction_opening(ISSUER_1, project_id)).unwrap(); assert_eq!(inst.get_project_details(project_id).status, ProjectStatus::AuctionOpening); } @@ -765,7 +796,7 @@ mod start_auction_extrinsic { for account in 6000..6010 { inst.execute(|| { - let response = Pallet::::do_auction_opening(account, project_id); + let response = Pallet::::do_start_auction_opening(account, project_id); assert_noop!(response, Error::::NotIssuer); }); } @@ -782,7 +813,7 @@ mod start_auction_extrinsic { let project_id = inst.create_evaluating_project(default_project_metadata(ISSUER_1), ISSUER_1); inst.execute(|| { assert_noop!( - PolimecFunding::do_auction_opening(ISSUER_1, project_id), + PolimecFunding::do_start_auction_opening(ISSUER_1, project_id), Error::::TransitionPointNotSet ); }); @@ -795,7 +826,7 @@ mod start_auction_extrinsic { inst.advance_time(::EvaluationDuration::get() + 1).unwrap(); inst.execute(|| { assert_noop!( - PolimecFunding::do_auction_opening(ISSUER_1, project_id), + PolimecFunding::do_start_auction_opening(ISSUER_1, project_id), Error::::TransitionPointNotSet ); }); diff --git a/pallets/funding/src/tests/4_community.rs b/pallets/funding/src/tests/4_community.rs index 5111d6f5b..e041482b7 100644 --- a/pallets/funding/src/tests/4_community.rs +++ b/pallets/funding/src/tests/4_community.rs @@ -765,8 +765,7 @@ mod community_contribute_extrinsic { ) .unwrap(); inst.bid_for_users(project_id, failing_bids_after_random_end).unwrap(); - inst.advance_time(2).unwrap(); - assert_eq!(inst.get_project_details(project_id).status, ProjectStatus::CommunityRound); + inst.start_community_funding(project_id).unwrap(); // Some low amount of plmc and usdt to cover a purchase of 10CTs. let plmc_mints = vec![ diff --git a/pallets/funding/src/tests/5_remainder.rs b/pallets/funding/src/tests/5_remainder.rs index 9cec02e60..f8dba1536 100644 --- a/pallets/funding/src/tests/5_remainder.rs +++ b/pallets/funding/src/tests/5_remainder.rs @@ -59,7 +59,7 @@ mod round_flow { }, ]; - let (project_ids, mut inst) = create_multiple_projects_at(inst, project_params); + let (_project_ids, mut inst) = create_multiple_projects_at(inst, project_params); assert_eq!(inst.get_project_details(0).status, ProjectStatus::FundingSuccessful); assert_eq!(inst.get_project_details(1).status, ProjectStatus::FundingSuccessful); @@ -890,8 +890,7 @@ mod remaining_contribute_extrinsic { inst.bid_for_users(project_id, successful_bids).unwrap(); inst.advance_time( ::AuctionOpeningDuration::get() + - ::AuctionClosingDuration::get() + - 1, + ::AuctionClosingDuration::get() ) .unwrap(); inst.bid_for_users(project_id, failing_bids_after_random_end).unwrap(); @@ -1466,48 +1465,32 @@ mod remaining_contribute_extrinsic { #[test] fn called_outside_remainder_round() { let mut inst = MockInstantiator::new(Some(RefCell::new(new_test_ext()))); - let created_project = inst.create_new_project(default_project_metadata(ISSUER_1), ISSUER_1); - let evaluating_project = inst.create_evaluating_project(default_project_metadata(ISSUER_2), ISSUER_2); - let auctioning_project = - inst.create_auctioning_project(default_project_metadata(ISSUER_3), ISSUER_3, default_evaluations()); - let community_project = inst.create_community_contributing_project( - default_project_metadata(ISSUER_4), + let project_metadata = default_project_metadata(ISSUER_1); + let project_id = inst.create_community_contributing_project( + project_metadata.clone(), ISSUER_4, default_evaluations(), default_bids(), ); - let finished_project = inst.create_finished_project( - default_project_metadata(ISSUER_5), - ISSUER_5, - default_evaluations(), - default_bids(), - default_community_buys(), - default_remainder_buys(), - ); - let projects = - vec![created_project, evaluating_project, auctioning_project, community_project, finished_project]; - for project in projects { - let project_policy = inst.get_project_metadata(project).policy_ipfs_cid.unwrap(); - inst.execute(|| { - assert_noop!( - PolimecFunding::remaining_contribute( - RuntimeOrigin::signed(BUYER_1), - get_mock_jwt_with_cid( - BUYER_1, - InvestorType::Retail, - generate_did_from_account(BUYER_1), - project_policy - ), - project, - 1000 * CT_UNIT, - 1u8.try_into().unwrap(), - AcceptedFundingAsset::USDT + inst.execute(|| { + assert_noop!( + PolimecFunding::remaining_contribute( + RuntimeOrigin::signed(BUYER_1), + get_mock_jwt_with_cid( + BUYER_1, + InvestorType::Retail, + generate_did_from_account(BUYER_1), + project_metadata.clone().policy_ipfs_cid.unwrap() ), - Error::::IncorrectRound - ); - }); - } + project_id, + 1000 * CT_UNIT, + 1u8.try_into().unwrap(), + AcceptedFundingAsset::USDT + ), + Error::::IncorrectRound + ); + }); } #[test] diff --git a/pallets/funding/src/tests/misc.rs b/pallets/funding/src/tests/misc.rs index f8263b7cf..e53643773 100644 --- a/pallets/funding/src/tests/misc.rs +++ b/pallets/funding/src/tests/misc.rs @@ -412,14 +412,6 @@ mod async_tests { ]; let (project_ids, mut inst) = create_multiple_projects_at(inst, project_params); - let now = inst.current_block(); - - dbg!(inst.get_project_details(project_ids[0]).status); - dbg!(inst.get_project_details(project_ids[1]).status); - dbg!(inst.get_project_details(project_ids[2]).status); - dbg!(inst.get_project_details(project_ids[3]).status); - dbg!(inst.get_project_details(project_ids[4]).status); - dbg!(inst.get_project_details(project_ids[5]).status); assert_eq!(inst.get_project_details(project_ids[0]).status, ProjectStatus::Application); assert_eq!(inst.get_project_details(project_ids[1]).status, ProjectStatus::EvaluationRound); @@ -549,13 +541,6 @@ mod async_tests { let ext = sp_io::TestExternalities::new(t); let mut inst = MockInstantiator::new(Some(RefCell::new(ext))); - dbg!(inst.get_project_details(0).status); - dbg!(inst.get_project_details(1).status); - dbg!(inst.get_project_details(2).status); - dbg!(inst.get_project_details(3).status); - dbg!(inst.get_project_details(4).status); - dbg!(inst.get_project_details(5).status); - assert_eq!(inst.get_project_details(0).status, ProjectStatus::FundingSuccessful); assert_eq!(inst.get_project_details(1).status, ProjectStatus::RemainderRound); assert_eq!(inst.get_project_details(2).status, ProjectStatus::CommunityRound); diff --git a/pallets/funding/src/types.rs b/pallets/funding/src/types.rs index bc95e8e33..4ca6fc8ae 100644 --- a/pallets/funding/src/types.rs +++ b/pallets/funding/src/types.rs @@ -322,6 +322,8 @@ pub mod storage_types { pub funding_amount_reached_usd: Balance, /// Information about the total amount bonded, and the outcome in regards to reward/slash/nothing pub evaluation_round_info: EvaluationRoundInfo, + /// If the auction was oversubscribed, how much USD was raised across all winning bids + pub usd_bid_on_oversubscription: Option, /// When the Funding Round ends pub funding_end_block: Option, /// ParaId of project @@ -337,6 +339,7 @@ pub mod storage_types { EvaluationEnd, AuctionOpeningStart, AuctionClosingStart, + AuctionClosingEnd, CommunityFundingStart, RemainderFundingStart, FundingEnd, @@ -639,6 +642,7 @@ pub mod inner_types { AuctionInitializePeriod, AuctionOpening, AuctionClosing, + CalculatingWAP, CommunityRound, RemainderRound, FundingFailed, @@ -745,6 +749,7 @@ pub mod inner_types { AuctionInitializePeriod, AuctionOpening, AuctionClosing, + CalculatingWAP, CommunityFunding, RemainderFunding, DecisionPeriod,