diff --git a/integration-tests/src/tests/e2e.rs b/integration-tests/src/tests/e2e.rs index f4d93ad74..b54fb85c8 100644 --- a/integration-tests/src/tests/e2e.rs +++ b/integration-tests/src/tests/e2e.rs @@ -158,24 +158,20 @@ fn pre_wap_bids() -> Vec<(u32, [u8; 32], ParticipationMode, InvestorType, u64, f ] } -fn wap() -> f64 { - 10.30346708 -} - #[allow(unused)] fn post_wap_bids() -> Vec<(u32, [u8; 32], ParticipationMode, InvestorType, u64, f64, AcceptedFundingAsset, f64, f64)> { // (bid_id, User, Participation mode, Investor type, CTs specified in extrinsic, CT Price, Participation Currency, Final participation currency ticket, PLMC bonded as a consequence) vec![ - (21, SAM, OTM, Professional, 2_000, 10.303467, USDT, 20_916.0382, 22_620.1253), - (22, SAM, OTM, Professional, 2_200, 10.303467, DOT, 4_947.8800, 24_882.1378), - (16, DAVE, OTM, Professional, 1_000, 10.303467, USDT, 10_458.0191, 11_310.0627), - (17, GERALT, Classic(15), Institutional, 500, 10.303467, USDT, 5_151.7335, 1_885.0104), - (18, GEORGE, Classic(20), Institutional, 1_900, 10.303467, USDT, 19_576.5875, 5_372.2798), - (19, GINO, Classic(25), Institutional, 600, 10.303467, USDT, 6_182.0802, 1_357.2075), - (20, STEVE, OTM, Professional, 1_000, 10.303467, USDT, 10_458.0191, 11_310.0627), - (0, BROCK, OTM, Professional, 700, 10.000000, USDC, 7_101.4493, 7_683.8639), - (1, BEN, OTM, Professional, 4_000, 10.000000, USDT, 40_600.0000, 43_907.7936), - (2, BILL, Classic(3), Professional, 3_000, 10.000000, USDC, 29_985.0075, 54_884.7420), + (21, SAM, OTM, Professional, 2_000, 12.0, USDT, 24000.0, 26344.6762), + (22, SAM, OTM, Professional, 2_200, 12.0, DOT, 5677.419355, 28979.1438), + (16, DAVE, OTM, Professional, 1_000, 11.0, USDT, 11000.0, 12074.6432), + (17, GERALT, Classic(15), Institutional, 500, 11.0, USDT, 5500.0, 2012.4405), + (18, GEORGE, Classic(20), Institutional, 1_900, 11.0, USDT, 20900.0, 5735.4555), + (19, GINO, Classic(25), Institutional, 600, 11.0, USDT, 6600.0, 1448.9572), + (20, STEVE, OTM, Professional, 1_000, 11.0, USDT, 11000.0, 12074.6432), + (0, BROCK, OTM, Professional, 700, 10.000000, USDC, 6996.501749, 7_683.8639), + (1, BEN, OTM, Professional, 4_000, 10.000000, USDT, 40_000.0, 43_907.7936), + (2, BILL, Classic(3), Professional, 3_000, 10.000000, USDC, 29985.0075, 54_884.7420), (3, BRAD, Classic(6), Professional, 700, 10.000000, USDT, 7_000.0000, 6_403.2199), (4, BROCK, Classic(9), Professional, 3_400, 10.000000, USDT, 34_000.0000, 20_734.2359), (5, BLAIR, Classic(8), Professional, 1_000, 10.000000, USDT, 10_000.0000, 6_860.5928), @@ -187,7 +183,7 @@ fn post_wap_bids() -> Vec<(u32, [u8; 32], ParticipationMode, InvestorType, u64, (11, BELLA, Classic(1), Professional, 800, 10.000000, USDT, 8_000.0000, 43_907.7936), (12, BRUCE, Classic(4), Institutional, 3_000, 10.000000, USDT, 30_000.0000, 41_163.5565), (13, BRENT, Classic(1), Institutional, 8_000, 10.000000, USDT, 80_000.0000, 439_077.9363), - (14, DOUG, OTM, Institutional, 100, 10.000000, USDT, 1_015.0000, 5_488.4742), + (14, DOUG, OTM, Institutional, 100, 10.000000, USDT, 1000.0, 5_488.4742), (15, DAVE, OTM, Professional, 0, 10.000000, USDT, 0.00, 0.00), ] } @@ -198,7 +194,7 @@ fn cts_minted() -> f64 { } fn usd_raised() -> f64 { - 502_791.8972 + 513_400.0000 } #[allow(unused)] @@ -209,7 +205,7 @@ fn ct_fees() -> (f64, f64, f64) { fn issuer_payouts() -> (f64, f64, f64) { // (USDT, USDC, DOT) - (430_124.27, 36_981.51, 7_670.46) + (437_000.00, 36_981.51, 8_473.12) } fn evaluator_reward_pots() -> (f64, f64, f64, f64) { @@ -245,9 +241,9 @@ fn final_payouts() -> Vec<([u8; 32], f64, f64)> { (DAVE, 1059.661749, 0.00), (MASON, 35.37757672, 0.00), (MIKE, 82.74302164, 0.00), - (GERALT, 500.0, 1_885.01), - (GEORGE, 1_900.0, 5_372.28), - (GINO, 600.0, 1_357.21), + (GERALT, 500.0, 2_012.44), + (GEORGE, 1_900.0, 5_735.46), + (GINO, 600.0, 1_448.96), (STEVE, 1017.159307, 0.0), (SAM, 4267.09505, 0.0), ] @@ -255,7 +251,7 @@ fn final_payouts() -> Vec<([u8; 32], f64, f64)> { fn otm_fee_recipient_balances() -> (f64, f64, f64) { // USDT, USDC, DOT - (1233.208025, 104.9475262, 73.12137929) + (1305.0, 104.9475262, 85.16129032) } fn otm_treasury_sub_account_plmc_held() -> f64 { @@ -462,18 +458,6 @@ fn e2e_test() { let project_details = inst.get_project_details(project_id); let project_metadata = inst.get_project_metadata(project_id); - let stored_wap = project_details.weighted_average_price.unwrap(); - let expected_wap = PriceProviderOf::::calculate_decimals_aware_price( - PriceOf::::from_float(wap()), - USD_DECIMALS, - CT_DECIMALS, - ) - .unwrap(); - assert_close_enough!( - stored_wap.saturating_mul_int(PLMC), - expected_wap.saturating_mul_int(PLMC), - Perquintill::from_float(0.9999) - ); let actual_cts_minted = polimec_runtime::ContributionTokens::total_issuance(project_id); let expected_cts_minted = FixedU128::from_float(cts_minted()).saturating_mul_int(CT_UNIT); @@ -544,6 +528,8 @@ fn e2e_test() { ); for (user, ct_rewarded, plmc_bonded) in final_payouts() { + let names = names(); + let user: PolimecAccountId = user.into(); let ct_rewarded = FixedU128::from_float(ct_rewarded).saturating_mul_int(CT_UNIT); let plmc_bonded = FixedU128::from_float(plmc_bonded).saturating_mul_int(PLMC); diff --git a/pallets/funding/src/benchmarking.rs b/pallets/funding/src/benchmarking.rs index 408187afd..e0fe69c39 100644 --- a/pallets/funding/src/benchmarking.rs +++ b/pallets/funding/src/benchmarking.rs @@ -771,8 +771,8 @@ mod benchmarks { } // We have 3 logic paths - // 1 - Accepted bid with no refunds (i.e. final price <= WAP, no partial acceptance) - // 2 - Accepted bid with refund (i.e. final price > WAP or partial acceptance) + // 1 - Accepted bid with no refunds + // 2 - Accepted bid with refund (i.e. partially accepted bid ) // 3 - Rejected bid (i.e. bid not accepted, everything refunded, no CT/migration) // Path 2 is the most expensive but not by far, so we only benchmark and charge for this weight #[benchmark] @@ -788,10 +788,15 @@ mod benchmarks { let project_metadata = default_project_metadata::(issuer.clone()); let increase = project_metadata.minimum_price * PriceOf::::saturating_from_rational(5, 10); - let target_wap = project_metadata.minimum_price + increase; + let target_price = project_metadata.minimum_price + increase; + + let mut new_bucket = Pallet::::create_bucket_from_metadata(&project_metadata).unwrap(); + new_bucket.current_price = target_price; + new_bucket.amount_left = new_bucket.delta_amount; + 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 bids = inst.generate_bids_from_bucket(project_metadata.clone(), new_bucket, USDT); let project_id = inst.create_finished_project(project_metadata.clone(), issuer, None, evaluations, bids.clone()); @@ -829,7 +834,6 @@ mod benchmarks { id: bid_to_settle.id, status: bid_to_settle.status, final_ct_amount: expected_ct_amount, - final_ct_usd_price: bid_to_settle.original_ct_usd_price, } .into(), ); diff --git a/pallets/funding/src/functions/1_application.rs b/pallets/funding/src/functions/1_application.rs index ae9d17a99..ca75f6b38 100644 --- a/pallets/funding/src/functions/1_application.rs +++ b/pallets/funding/src/functions/1_application.rs @@ -33,7 +33,6 @@ impl Pallet { issuer_account: issuer.clone(), issuer_did: did.clone(), is_frozen: false, - weighted_average_price: None, fundraising_target_usd: fundraising_target, status: ProjectStatus::Application, round_duration: BlockNumberPair::new(None, None), diff --git a/pallets/funding/src/functions/3_auction.rs b/pallets/funding/src/functions/3_auction.rs index 8694c60ec..dc3026057 100644 --- a/pallets/funding/src/functions/3_auction.rs +++ b/pallets/funding/src/functions/3_auction.rs @@ -97,7 +97,7 @@ impl Pallet { }; BidBucketBounds::::mutate(project_id, current_bucket.current_price, |maybe_indexes| { - if let Some((i, j)) = maybe_indexes { + if let Some((i, _j)) = maybe_indexes { *maybe_indexes = Some((*i, bid_id)); } else { *maybe_indexes = Some((bid_id, bid_id)); diff --git a/pallets/funding/src/functions/5_funding_end.rs b/pallets/funding/src/functions/5_funding_end.rs index 52902420b..8acc27e17 100644 --- a/pallets/funding/src/functions/5_funding_end.rs +++ b/pallets/funding/src/functions/5_funding_end.rs @@ -26,8 +26,6 @@ impl Pallet { ProjectsInAuctionRound::::put(WeakBoundedVec::force_from(project_ids, None)); let auction_allocation_size = project_metadata.total_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 = @@ -39,7 +37,7 @@ impl Pallet { DidWithActiveProjects::::set(issuer_did, None); - let usd_raised = Self::calculate_usd_sold_from_bucket(bucket.clone(), project_metadata.total_allocation_size); + let usd_raised = bucket.calculate_usd_raised(auction_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() }; diff --git a/pallets/funding/src/functions/6_settlement.rs b/pallets/funding/src/functions/6_settlement.rs index e9b31a64d..c325a7d95 100644 --- a/pallets/funding/src/functions/6_settlement.rs +++ b/pallets/funding/src/functions/6_settlement.rs @@ -159,7 +159,6 @@ impl Pallet { 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.unwrap_or(project_metadata.minimum_price); let mut bid = Bids::::get((project_id, bid_id)).ok_or(Error::::ParticipationNotFound)?; ensure!( @@ -173,8 +172,8 @@ impl Pallet { // 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 } = - Self::calculate_refund(&bid, funding_success, wap)?; + let BidRefund { final_ct_amount, refunded_plmc, refunded_funding_asset_amount } = + Self::calculate_refund(&bid, funding_success)?; Self::release_funding_asset(project_id, &bid.bidder, refunded_funding_asset_amount, bid.funding_asset)?; @@ -227,7 +226,6 @@ impl Pallet { id: bid.id, status: bid.status, final_ct_amount, - final_ct_usd_price, }); Ok(()) @@ -235,34 +233,32 @@ impl Pallet { /// Calculate the amount of funds the bidder should receive back based on the original bid /// amount and price compared to the final bid amount and price. - fn calculate_refund( - bid: &BidInfoOf, - funding_success: bool, - wap: PriceOf, - ) -> Result, DispatchError> { - let final_ct_usd_price = if bid.original_ct_usd_price > wap { wap } else { bid.original_ct_usd_price }; + fn calculate_refund(bid: &BidInfoOf, funding_success: bool) -> Result { let multiplier: MultiplierOf = bid.mode.multiplier().try_into().map_err(|_| Error::::BadMath)?; - if !funding_success || bid.status == BidStatus::Rejected { - return Ok(BidRefund:: { - final_ct_usd_price, + let ct_price = bid.original_ct_usd_price; + + match bid.status { + BidStatus::Accepted if funding_success => Ok(BidRefund { + final_ct_amount: bid.original_ct_amount, + refunded_plmc: Zero::zero(), + refunded_funding_asset_amount: Zero::zero(), + }), + BidStatus::PartiallyAccepted(accepted_amount) if funding_success => { + let new_ticket_size = ct_price.checked_mul_int(accepted_amount).ok_or(Error::::BadMath)?; + let new_plmc_bond = Self::calculate_plmc_bond(new_ticket_size, multiplier)?; + let new_funding_asset_amount = + Self::calculate_funding_asset_amount(new_ticket_size, bid.funding_asset)?; + let refunded_plmc = bid.plmc_bond.saturating_sub(new_plmc_bond); + let refunded_funding_asset_amount = + bid.funding_asset_amount_locked.saturating_sub(new_funding_asset_amount); + Ok(BidRefund { final_ct_amount: accepted_amount, refunded_plmc, refunded_funding_asset_amount }) + }, + _ => Ok(BidRefund { final_ct_amount: Zero::zero(), refunded_plmc: bid.plmc_bond, refunded_funding_asset_amount: bid.funding_asset_amount_locked, - }); + }), } - 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)?; - let new_funding_asset_amount = Self::calculate_funding_asset_amount(new_ticket_size, bid.funding_asset)?; - let refunded_plmc = bid.plmc_bond.saturating_sub(new_plmc_bond); - let refunded_funding_asset_amount = bid.funding_asset_amount_locked.saturating_sub(new_funding_asset_amount); - - Ok(BidRefund:: { final_ct_usd_price, final_ct_amount, refunded_plmc, refunded_funding_asset_amount }) } pub fn do_mark_project_as_settled(project_id: ProjectId) -> DispatchResult { diff --git a/pallets/funding/src/functions/misc.rs b/pallets/funding/src/functions/misc.rs index 4d21a0cf4..e92568c9a 100644 --- a/pallets/funding/src/functions/misc.rs +++ b/pallets/funding/src/functions/misc.rs @@ -456,44 +456,6 @@ 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 3040eb057..4b793b04f 100644 --- a/pallets/funding/src/instantiator/calculations.rs +++ b/pallets/funding/src/instantiator/calculations.rs @@ -1,6 +1,5 @@ use super::*; use crate::{MultiplierOf, ParticipationMode}; -use core::cmp::Ordering; use itertools::{izip, GroupBy}; #[allow(clippy::wildcard_imports)] use polimec_common::assets::AcceptedFundingAsset; @@ -10,7 +9,6 @@ use polimec_common::{ }; use sp_core::{blake2_256, ecdsa, hexdisplay::AsBytesRef, keccak_256, sr25519, Pair}; use sp_runtime::traits::TrailingZeroInput; -use sp_std::ops::Div; use InvestorType::{self, *}; impl< @@ -129,7 +127,6 @@ impl< // bids in the order they were made bids: &Vec>, project_metadata: ProjectMetadataOf, - weighted_average_price: PriceOf, ) -> Vec> { let mut output = Vec::new(); let charged_bids = self.get_actual_price_charged_for_bucketed_bids(bids, project_metadata.clone(), None); @@ -158,10 +155,7 @@ impl< let bought_cts = if remaining_cts < bid.amount { remaining_cts } else { bid.amount }; remaining_cts = remaining_cts.saturating_sub(bought_cts); - let final_price = - if weighted_average_price > price_charged { price_charged } else { weighted_average_price }; - - let actual_usd_ticket_size = final_price.saturating_mul_int(bought_cts); + let actual_usd_ticket_size = price_charged.saturating_mul_int(bought_cts); let mut actual_plmc_bond = Balance::zero(); if let ParticipationMode::Classic(multiplier) = bid.mode { self.add_required_plmc_to(&mut actual_plmc_bond, actual_usd_ticket_size, multiplier); @@ -180,15 +174,10 @@ impl< &mut self, bids: &Vec>, project_metadata: ProjectMetadataOf, - weighted_average_price: PriceOf, ) -> Vec> { let plmc_charged = self.calculate_auction_plmc_charged_from_all_bids_made_or_with_bucket(bids, project_metadata.clone(), None); - let plmc_returned = self.calculate_auction_plmc_returned_from_all_bids_made( - bids, - project_metadata.clone(), - weighted_average_price, - ); + let plmc_returned = self.calculate_auction_plmc_returned_from_all_bids_made(bids, project_metadata.clone()); plmc_charged.subtract_accounts(plmc_returned) } @@ -239,7 +228,6 @@ impl< // bids in the order they were made bids: &Vec>, project_metadata: ProjectMetadataOf, - weighted_average_price: PriceOf, ) -> Vec> { let mut output = Vec::new(); let charged_bids = self.get_actual_price_charged_for_bucketed_bids(bids, project_metadata.clone(), None); @@ -269,10 +257,7 @@ impl< let bought_cts = if remaining_cts < bid.amount { remaining_cts } else { bid.amount }; remaining_cts = remaining_cts.saturating_sub(bought_cts); - let final_price = - if weighted_average_price > price_charged { price_charged } else { weighted_average_price }; - - let actual_usd_ticket_size = final_price.saturating_mul_int(bought_cts); + let actual_usd_ticket_size = price_charged.saturating_mul_int(bought_cts); let mut actual_funding_asset_spent = Balance::zero(); self.add_required_funding_asset_to(&mut actual_funding_asset_spent, actual_usd_ticket_size, bid.asset); if bid.mode == ParticipationMode::OTM { @@ -292,18 +277,14 @@ impl< &mut self, bids: &Vec>, project_metadata: ProjectMetadataOf, - weighted_average_price: PriceOf, ) -> Vec> { let funding_asset_charged = self.calculate_auction_funding_asset_charged_from_all_bids_made_or_with_bucket( bids, project_metadata.clone(), None, ); - let funding_asset_returned = self.calculate_auction_funding_asset_returned_from_all_bids_made( - bids, - project_metadata.clone(), - weighted_average_price, - ); + let funding_asset_returned = + self.calculate_auction_funding_asset_returned_from_all_bids_made(bids, project_metadata.clone()); funding_asset_charged.subtract_accounts(funding_asset_returned) } @@ -577,75 +558,30 @@ impl< pub fn generate_bids_from_higher_usd_than_target( &mut self, project_metadata: ProjectMetadataOf, - usd_amount: Balance, + usd_target: Balance, ) -> Vec> { - let min_price = project_metadata.minimum_price; - let total_allocation_size = project_metadata.total_allocation_size; - let first_bucket_usd_amount = min_price.saturating_mul_int(total_allocation_size); - - // Initial setup - let target_wap_multiplicator = PriceOf::::saturating_from_rational(usd_amount, first_bucket_usd_amount); - let mut target_wap: PriceOf = min_price * target_wap_multiplicator; - let mut bucket = self.find_bucket_for_wap(project_metadata.clone(), target_wap); - - 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); - - // Get initial USD amount - let project_id = self.create_finished_project( - project_metadata.clone(), - self.account_from_u32(420, "blaze it"), - None, - evaluations.clone(), - bids.clone(), - ); - let mut usd_amount_raised = self.get_project_details(project_id).funding_amount_reached_usd; - - let mut step_divider = PriceOf::::saturating_from_rational(1, 1); - let mut previous_direction = usd_amount_raised < usd_amount; - let mut previous_wap = target_wap; - let mut loop_counter = 0; - - while usd_amount_raised != usd_amount { - loop_counter += 1; - if loop_counter > 100 { - return bids; - } - - let current_direction = usd_amount_raised < usd_amount; - - // If we changed direction, increase precision - if current_direction != previous_direction { - step_divider = step_divider * PriceOf::::saturating_from_rational(10, 1); - } - - let step_size = bucket.delta_price.div(step_divider); - target_wap = if current_direction { target_wap + step_size } else { target_wap - step_size }; - - // Check if WAP is the same as previous - if target_wap == previous_wap { - return bids; - } - 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); + let mut bucket = Pallet::::create_bucket_from_metadata(&project_metadata).unwrap(); + bucket.update(project_metadata.total_allocation_size); + + // Increase bucket price until we go past the target usd amount + let mut usd_raised = bucket.calculate_usd_raised(project_metadata.total_allocation_size); + while usd_raised < usd_target { + bucket.update(bucket.delta_amount); + usd_raised = bucket.calculate_usd_raised(project_metadata.total_allocation_size); + } - let project_id = self.create_finished_project( - project_metadata.clone(), - self.account_from_u32(420, "blaze it"), - None, - evaluations.clone(), - bids.clone(), - ); - usd_amount_raised = self.get_project_details(project_id).funding_amount_reached_usd; + // Go one bucket back + bucket.current_price = bucket.current_price.saturating_sub(bucket.delta_price); + bucket.amount_left = bucket.delta_amount; - previous_direction = current_direction; + // Start buying the min amount of tokens in this bucket until we reach or surpass the usd amount + while bucket.calculate_usd_raised(project_metadata.total_allocation_size) < usd_target { + let min_ticket = project_metadata.bidding_ticket_sizes.retail.usd_minimum_per_participation; + let ct_min_ticket = bucket.current_price.reciprocal().unwrap().saturating_mul_int(min_ticket); + bucket.update(ct_min_ticket); } - bids + self.generate_bids_from_bucket(project_metadata.clone(), bucket, AcceptedFundingAsset::USDT) } pub fn generate_bids_from_total_ct_percent( @@ -685,55 +621,6 @@ impl< early_evaluators_rewards.saturating_add(normal_evaluators_rewards) } - pub fn find_bucket_for_wap(&self, project_metadata: ProjectMetadataOf, target_wap: PriceOf) -> BucketOf { - let mut bucket = >::create_bucket_from_metadata(&project_metadata).unwrap(); - let auction_allocation = project_metadata.total_allocation_size; - - if target_wap == bucket.initial_price { - return bucket - } - - // Fill first bucket - bucket.update(bucket.delta_amount * 10u128); - - // Fill remaining buckets till we pass by the wap - loop { - let wap = bucket.calculate_wap(auction_allocation); - - if wap == target_wap { - return bucket - } - if wap < target_wap { - bucket.update(bucket.delta_amount); - } else { - break - } - } - - // Go back one bucket - bucket.amount_left = bucket.delta_amount; - bucket.current_price = bucket.current_price - bucket.delta_price; - - // Do a binary search on the amount to reach the desired wap - let mut lower_bound: Balance = Zero::zero(); - let mut upper_bound: Balance = bucket.delta_amount; - - while lower_bound <= upper_bound { - let mid_point = (lower_bound + upper_bound) / 2u128; - bucket.amount_left = mid_point; - let new_wap = bucket.calculate_wap(auction_allocation); - - // refactor as match - match new_wap.cmp(&target_wap) { - Ordering::Equal => return bucket, - Ordering::Less => upper_bound = mid_point - 1u128, - Ordering::Greater => lower_bound = mid_point + 1u128, - } - } - - bucket - } - // We assume a single bid can cover the whole first bucket. Make sure the ticket sizes allow this. pub fn generate_bids_from_bucket( &self, @@ -741,9 +628,11 @@ impl< bucket: BucketOf, funding_asset: AcceptedFundingAsset, ) -> Vec> { - if bucket.current_price == bucket.initial_price { - return vec![] - } + let mut new_bucket = Pallet::::create_bucket_from_metadata(&project_metadata).unwrap(); + assert_eq!(new_bucket.delta_amount, bucket.delta_amount, "Buckets must have the same delta amount"); + assert_eq!(new_bucket.delta_price, bucket.delta_price, "Buckets must have the same delta price"); + assert_eq!(new_bucket.initial_price, bucket.initial_price, "Buckets must have the same initial price"); + let auction_allocation = project_metadata.total_allocation_size; let mut starting_account = self.account_from_u32(0, "BIDDER"); @@ -761,53 +650,31 @@ impl< bid }; - let step_amounts = ((bucket.current_price - bucket.initial_price) / bucket.delta_price).saturating_mul_int(1u8); - let last_bid_amount = bucket.delta_amount - bucket.amount_left; - let mut bids = Vec::new(); - let first_bid = generate_bid(auction_allocation); - bids.push(first_bid); + if bucket.current_price > bucket.initial_price { + let allocation_bid = generate_bid(auction_allocation); + bids.push(allocation_bid); + new_bucket.update(auction_allocation); + } - for _i in 0u8..step_amounts - 1u8 { - let full_bucket_bid = generate_bid(bucket.delta_amount); - bids.push(full_bucket_bid); + while bucket.current_price > new_bucket.current_price { + let bucket_bid = generate_bid(bucket.delta_amount); + bids.push(bucket_bid); + new_bucket.update(bucket.delta_amount); } - // A CT amount can be so low that the PLMC required is less than the minimum mintable amount. We estimate all bids - // should be at least 1% of a bucket. - let min_bid_amount = Percent::from_percent(1) * bucket.delta_amount; - if last_bid_amount > min_bid_amount { + let last_bid_amount = bucket.delta_amount - bucket.amount_left; + let last_usd_amount = bucket.current_price.saturating_mul_int(last_bid_amount); + if last_usd_amount >= project_metadata.bidding_ticket_sizes.retail.usd_minimum_per_participation { let last_bid = generate_bid(last_bid_amount); bids.push(last_bid); + new_bucket.update(last_bid_amount); } - bids - } - - pub fn generate_bids_that_take_price_to( - &self, - project_metadata: ProjectMetadataOf, - desired_price: PriceOf, - ) -> Vec> { - let necessary_bucket = self.find_bucket_for_wap(project_metadata.clone(), desired_price); - self.generate_bids_from_bucket(project_metadata, necessary_bucket, AcceptedFundingAsset::USDT) - } + assert_eq!(new_bucket, bucket, "Buckets must match after generating bids"); - // Make sure the bids are in the order they were made - pub fn calculate_wap_from_all_bids_made( - &self, - project_metadata: &ProjectMetadataOf, - bids: &Vec>, - ) -> PriceOf { - let mut bucket = Pallet::::create_bucket_from_metadata(project_metadata).unwrap(); - - for bid in bids { - bucket.update(bid.amount); - } - - let auction_allocation = project_metadata.total_allocation_size; - bucket.calculate_wap(auction_allocation) + bids } pub fn remainder_round_block(&self) -> BlockNumberFor { diff --git a/pallets/funding/src/instantiator/chain_interactions.rs b/pallets/funding/src/instantiator/chain_interactions.rs index 82c76c9af..8d5fe2bf5 100644 --- a/pallets/funding/src/instantiator/chain_interactions.rs +++ b/pallets/funding/src/instantiator/chain_interactions.rs @@ -284,7 +284,6 @@ impl< issuer_account: self.get_issuer(project_id), issuer_did, is_frozen: false, - weighted_average_price: None, status: ProjectStatus::Application, round_duration: BlockNumberPair::new(None, None), fundraising_target_usd: expected_metadata @@ -334,7 +333,6 @@ impl< let project_metadata = self.get_project_metadata(project_id); let project_details = self.get_project_details(project_id); let project_bids = self.execute(|| Bids::::iter_prefix_values((project_id,)).collect::>()); - assert!(project_details.weighted_average_price.is_some(), "Weighted average price should exist"); for filter in bid_expectations { let _found_bid = project_bids.iter().find(|bid| filter.matches_bid(bid)).unwrap(); diff --git a/pallets/funding/src/instantiator/tests.rs b/pallets/funding/src/instantiator/tests.rs index f6091dac7..395742930 100644 --- a/pallets/funding/src/instantiator/tests.rs +++ b/pallets/funding/src/instantiator/tests.rs @@ -1,171 +1,12 @@ use crate::{ - instantiator::{UserToFundingAsset, UserToPLMCBalance}, - mock::{new_test_ext, TestRuntime, PLMC}, - tests::{ - defaults::{bounded_name, bounded_symbol, default_project_metadata, ipfs_hash}, - CT_DECIMALS, CT_UNIT, - }, + mock::{new_test_ext, TestRuntime}, + tests::{defaults::default_project_metadata, CT_DECIMALS, CT_UNIT}, *, }; use core::cell::RefCell; -use itertools::Itertools; use polimec_common::{assets::AcceptedFundingAsset, ProvideAssetPrice, USD_DECIMALS, USD_UNIT}; use sp_arithmetic::Perquintill; -#[test] -fn dry_run_wap() { - let mut inst = tests::MockInstantiator::new(Some(RefCell::new(new_test_ext()))); - - const ADAM: AccountIdOf = 60; - const TOM: AccountIdOf = 61; - const SOFIA: AccountIdOf = 62; - const FRED: AccountIdOf = 63; - const ANNA: AccountIdOf = 64; - const DAMIAN: AccountIdOf = 65; - - let accounts = [ADAM, TOM, SOFIA, FRED, ANNA, DAMIAN]; - - let bounded_name = bounded_name(); - let bounded_symbol = bounded_symbol(); - let metadata_hash = ipfs_hash(); - let normalized_price = PriceOf::::from_float(10.0); - let decimal_aware_price = - PriceProviderOf::::calculate_decimals_aware_price(normalized_price, USD_DECIMALS, CT_DECIMALS) - .unwrap(); - let project_metadata = ProjectMetadata { - token_information: CurrencyMetadata { name: bounded_name, symbol: bounded_symbol, decimals: CT_DECIMALS }, - mainnet_token_max_supply: 8_000_000 * CT_UNIT, - total_allocation_size: 100_000 * CT_UNIT, - minimum_price: decimal_aware_price, - bidding_ticket_sizes: BiddingTicketSizes { - professional: TicketSize::new(5000 * USD_UNIT, None), - institutional: TicketSize::new(5000 * USD_UNIT, None), - retail: TicketSize::new(100 * USD_UNIT, None), - phantom: Default::default(), - }, - participation_currencies: vec![AcceptedFundingAsset::USDT].try_into().unwrap(), - funding_destination_account: 0, - policy_ipfs_cid: Some(metadata_hash), - participants_account_type: ParticipantsAccountType::Polkadot, - }; - - // overfund with plmc - let plmc_fundings = - accounts.iter().map(|acc| UserToPLMCBalance { account: *acc, plmc_amount: PLMC * 1_000_000 }).collect_vec(); - let usdt_fundings = accounts - .iter() - .map(|acc| UserToFundingAsset { - account: *acc, - asset_amount: USD_UNIT * 1_000_000, - asset_id: AcceptedFundingAsset::USDT.id(), - }) - .collect_vec(); - inst.mint_plmc_to(plmc_fundings); - inst.mint_funding_asset_to(usdt_fundings); - - let evaluations = inst.generate_successful_evaluations(project_metadata.clone(), 5); - let project_id = inst.create_auctioning_project(project_metadata.clone(), 0, None, evaluations); - - let bids = vec![ - (ADAM, 10_000 * CT_UNIT).into(), - (TOM, 20_000 * CT_UNIT).into(), - (SOFIA, 20_000 * CT_UNIT).into(), - (FRED, 10_000 * CT_UNIT).into(), - (ANNA, 5_000 * CT_UNIT).into(), - (DAMIAN, 5_000 * CT_UNIT).into(), - ]; - - inst.bid_for_users(project_id, bids).unwrap(); - - 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(); - let bucket = inst.execute(|| Buckets::::get(project_id).unwrap()); - let dry_run_price = bucket.calculate_wap(project_metadata.total_allocation_size); - - assert_eq!(dry_run_price, wap); -} - -#[test] -fn find_bucket_for_wap() { - let mut inst = tests::MockInstantiator::new(Some(RefCell::new(new_test_ext()))); - - const ADAM: AccountIdOf = 60; - const TOM: AccountIdOf = 61; - const SOFIA: AccountIdOf = 62; - const FRED: AccountIdOf = 63; - const ANNA: AccountIdOf = 64; - const DAMIAN: AccountIdOf = 65; - - let accounts = [ADAM, TOM, SOFIA, FRED, ANNA, DAMIAN]; - - let bounded_name = bounded_name(); - let bounded_symbol = bounded_symbol(); - let metadata_hash = ipfs_hash(); - let normalized_price = PriceOf::::from_float(10.0); - let decimal_aware_price = - PriceProviderOf::::calculate_decimals_aware_price(normalized_price, USD_DECIMALS, CT_DECIMALS) - .unwrap(); - let project_metadata = ProjectMetadata { - token_information: CurrencyMetadata { name: bounded_name, symbol: bounded_symbol, decimals: CT_DECIMALS }, - mainnet_token_max_supply: 8_000_000 * CT_UNIT, - total_allocation_size: 50_000 * CT_UNIT, - minimum_price: decimal_aware_price, - bidding_ticket_sizes: BiddingTicketSizes { - professional: TicketSize::new(5000 * USD_UNIT, None), - institutional: TicketSize::new(5000 * USD_UNIT, None), - retail: TicketSize::new(100 * USD_UNIT, None), - phantom: Default::default(), - }, - participation_currencies: vec![AcceptedFundingAsset::USDT].try_into().unwrap(), - funding_destination_account: 0, - policy_ipfs_cid: Some(metadata_hash), - participants_account_type: ParticipantsAccountType::Polkadot, - }; - - // overfund with plmc - let plmc_fundings = - accounts.iter().map(|acc| UserToPLMCBalance { account: *acc, plmc_amount: PLMC * 1_000_000 }).collect_vec(); - let usdt_fundings = accounts - .iter() - .map(|acc| UserToFundingAsset { - account: *acc, - asset_amount: USD_UNIT * 1_000_000, - asset_id: AcceptedFundingAsset::USDT.id(), - }) - .collect_vec(); - inst.mint_plmc_to(plmc_fundings); - inst.mint_funding_asset_to(usdt_fundings); - - let evaluations = inst.generate_successful_evaluations(project_metadata.clone(), 5); - let project_id = inst.create_auctioning_project(project_metadata.clone(), 0, None, evaluations); - - let bids = vec![ - (ADAM, 10_000 * CT_UNIT).into(), - (TOM, 20_000 * CT_UNIT).into(), - (SOFIA, 20_000 * CT_UNIT).into(), - (FRED, 10_000 * CT_UNIT).into(), - (ANNA, 5_000 * CT_UNIT).into(), - (DAMIAN, 5_000 * CT_UNIT).into(), - ]; - - inst.bid_for_users(project_id, bids).unwrap(); - - assert!(matches!(inst.go_to_next_state(project_id), ProjectStatus::FundingSuccessful)); - - let project_details = inst.get_project_details(project_id); - let wap = project_details.weighted_average_price.unwrap(); - let bucket_stored = inst.execute(|| Buckets::::get(project_id).unwrap()); - - let bucket_found = inst.find_bucket_for_wap(project_metadata.clone(), wap); - assert_eq!(bucket_found, bucket_stored); - - let wap_found = bucket_found.calculate_wap(project_metadata.total_allocation_size); - assert_eq!(wap_found, wap); -} - #[test] fn generate_bids_from_bucket() { let mut inst = tests::MockInstantiator::new(Some(RefCell::new(new_test_ext()))); @@ -173,16 +14,18 @@ fn generate_bids_from_bucket() { // Has a min price of 10.0 let project_metadata = default_project_metadata(0); let desired_real_wap = FixedU128::from_float(20.0f64); - let desired_price_aware_wap = + let desired_bucket_price_aware = 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 mut necessary_bucket = Pallet::::create_bucket_from_metadata(&project_metadata).unwrap(); + necessary_bucket.current_price = desired_bucket_price_aware; + necessary_bucket.amount_left = necessary_bucket.delta_amount; + 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); - let wap = project_details.weighted_average_price.unwrap(); - assert_eq!(wap, desired_price_aware_wap); + let current_bucket = inst.execute(|| Buckets::::get(project_id).unwrap()); + assert_eq!(current_bucket.current_price, desired_bucket_price_aware); } #[test] diff --git a/pallets/funding/src/lib.rs b/pallets/funding/src/lib.rs index c0db0704d..f97a4465f 100644 --- a/pallets/funding/src/lib.rs +++ b/pallets/funding/src/lib.rs @@ -134,7 +134,7 @@ pub type AssetIdOf = pub type VestingInfoOf = VestingInfo>; pub type ProjectMetadataOf = ProjectMetadata>, PriceOf, AccountIdOf, Cid>; -pub type ProjectDetailsOf = ProjectDetails, Did, BlockNumberFor, PriceOf, EvaluationRoundInfo>; +pub type ProjectDetailsOf = ProjectDetails, Did, BlockNumberFor, EvaluationRoundInfo>; pub type EvaluationInfoOf = EvaluationInfo, BlockNumberFor>; pub type BidInfoOf = BidInfo, AccountIdOf, BlockNumberFor>; @@ -500,7 +500,6 @@ pub mod pallet { id: u32, status: BidStatus, final_ct_amount: Balance, - final_ct_usd_price: PriceOf, }, /// A contribution was settled. On Funding Success the PLMC has been unbonded/locked with a vesting schedule and the funding assets have been transferred to the issuer. /// If Funding Failed, the PLMC has been unbonded and the funds have been returned to the contributor. diff --git a/pallets/funding/src/runtime_api.rs b/pallets/funding/src/runtime_api.rs index 10e06b45b..66fb0e70f 100644 --- a/pallets/funding/src/runtime_api.rs +++ b/pallets/funding/src/runtime_api.rs @@ -132,34 +132,26 @@ impl Pallet { asset: AcceptedFundingAsset, asset_amount: Balance, ) -> Balance { - let project_details = ProjectsDetails::::get(project_id).expect("Project not found"); let funding_asset_usd_price = Pallet::::get_decimals_aware_funding_asset_price(&asset).expect("Price not found"); let usd_ticket_size = funding_asset_usd_price.saturating_mul_int(asset_amount); - let mut ct_amount = Zero::zero(); + let mut ct_amount = Balance::zero(); - // Contribution phase - if let Some(wap) = project_details.weighted_average_price { - ct_amount = wap.reciprocal().expect("Bad math").saturating_mul_int(usd_ticket_size); - } - // Auction phase, we need to consider multiple buckets - else { - let mut usd_to_spend = usd_ticket_size; - let mut current_bucket = Buckets::::get(project_id).expect("Bucket not found"); - while usd_to_spend > Zero::zero() { - let bucket_price = current_bucket.current_price; + let mut usd_to_spend = usd_ticket_size; + let mut current_bucket = Buckets::::get(project_id).expect("Bucket not found"); + while usd_to_spend > Zero::zero() { + let bucket_price = current_bucket.current_price; - let ct_to_buy = bucket_price.reciprocal().expect("Bad math").saturating_mul_int(usd_to_spend); - let ct_to_buy = ct_to_buy.min(current_bucket.amount_left); + let ct_to_buy = bucket_price.reciprocal().expect("Bad math").saturating_mul_int(usd_to_spend); + let ct_to_buy = ct_to_buy.min(current_bucket.amount_left); - ct_amount = ct_amount.saturating_add(ct_to_buy); - // if usd spent is 0, we will have an infinite loop - let usd_spent = bucket_price.saturating_mul_int(ct_to_buy).max(One::one()); - usd_to_spend = usd_to_spend.saturating_sub(usd_spent); + ct_amount = ct_amount.saturating_add(ct_to_buy); + // if usd spent is 0, we will have an infinite loop + 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); } ct_amount @@ -170,7 +162,6 @@ impl Pallet { funding_asset: AcceptedFundingAsset, total_funding_asset_amount: Balance, ) -> (Balance, Balance) { - let project_details = ProjectsDetails::::get(project_id).expect("Project not found"); let funding_asset_usd_price = Pallet::::get_decimals_aware_funding_asset_price(&funding_asset).expect("Price not found"); let otm_multiplier = ParticipationMode::OTM.multiplier(); @@ -185,29 +176,22 @@ impl Pallet { let participating_usd_ticket_size = funding_asset_usd_price.saturating_mul_int(participating_funding_asset_amount); - let mut ct_amount = Zero::zero(); + let mut ct_amount = Balance::zero(); - // Contribution phase - if let Some(wap) = project_details.weighted_average_price { - ct_amount = wap.reciprocal().expect("Bad math").saturating_mul_int(participating_usd_ticket_size); - } - // Auction phase, we need to consider multiple buckets - else { - let mut usd_to_spend = participating_usd_ticket_size; - let mut current_bucket = Buckets::::get(project_id).expect("Bucket not found"); - while usd_to_spend > Zero::zero() { - let bucket_price = current_bucket.current_price; + let mut usd_to_spend = participating_usd_ticket_size; + let mut current_bucket = Buckets::::get(project_id).expect("Bucket not found"); + while usd_to_spend > Zero::zero() { + let bucket_price = current_bucket.current_price; - let ct_to_buy = bucket_price.reciprocal().expect("Bad math").saturating_mul_int(usd_to_spend); - let ct_to_buy = ct_to_buy.min(current_bucket.amount_left); + let ct_to_buy = bucket_price.reciprocal().expect("Bad math").saturating_mul_int(usd_to_spend); + let ct_to_buy = ct_to_buy.min(current_bucket.amount_left); - ct_amount = ct_amount.saturating_add(ct_to_buy); - // if usd spent is 0, we will have an infinite loop - let usd_spent = bucket_price.saturating_mul_int(ct_to_buy).max(One::one()); - usd_to_spend = usd_to_spend.saturating_sub(usd_spent); + ct_amount = ct_amount.saturating_add(ct_to_buy); + // if usd spent is 0, we will have an infinite loop + 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); } (ct_amount, fee_funding_asset_amount) diff --git a/pallets/funding/src/tests/2_evaluation.rs b/pallets/funding/src/tests/2_evaluation.rs index a8cdcb155..f814d8f0b 100644 --- a/pallets/funding/src/tests/2_evaluation.rs +++ b/pallets/funding/src/tests/2_evaluation.rs @@ -286,7 +286,6 @@ mod start_evaluation_extrinsic { issuer_account: ISSUER_1, issuer_did, is_frozen: true, - weighted_average_price: None, status: ProjectStatus::EvaluationRound, round_duration: BlockNumberPair::new( Some(1), diff --git a/pallets/funding/src/tests/3_auction.rs b/pallets/funding/src/tests/3_auction.rs index ffd551865..fd21d19d3 100644 --- a/pallets/funding/src/tests/3_auction.rs +++ b/pallets/funding/src/tests/3_auction.rs @@ -1,6 +1,6 @@ use super::*; use crate::ParticipationMode::*; -use frame_support::traits::{fungible::InspectFreeze, fungibles::metadata::Inspect}; +use frame_support::traits::fungible::InspectFreeze; use polimec_common::assets::{AcceptedFundingAsset, AcceptedFundingAsset::USDT}; use sp_core::bounded_vec; use sp_runtime::traits::Convert; @@ -49,11 +49,6 @@ mod round_flow { let project_id = inst.create_auctioning_project(project_metadata.clone(), issuer, None, evaluations); assert!(matches!(inst.go_to_next_state(project_id), ProjectStatus::FundingFailed)); - - assert_eq!( - inst.get_project_details(project_id).weighted_average_price, - Some(project_metadata.minimum_price) - ); } #[test] @@ -198,22 +193,19 @@ mod round_flow { fn auction_oversubscription() { let mut inst = MockInstantiator::new(Some(RefCell::new(new_test_ext()))); 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 mut bucket = Pallet::::create_bucket_from_metadata(&project_metadata).unwrap(); + bucket.current_price = + bucket.initial_price + (bucket.delta_price.mul(PriceOf::::from_float(10.0))); + bucket.amount_left = bucket.delta_amount; + let bids = inst.generate_bids_from_bucket(project_metadata.clone(), bucket, USDT); - let project_id = inst.create_finished_project( + 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); } #[test] @@ -1829,100 +1821,7 @@ mod end_auction_extrinsic { mod success { use super::*; - #[test] - fn wap_is_accurate() { - // From the knowledge hub: https://hub.polimec.org/learn/calculation-example#auction-round-calculation-example - let mut inst = MockInstantiator::new(Some(RefCell::new(new_test_ext()))); - - const ADAM: AccountIdOf = 60; - const TOM: AccountIdOf = 61; - const SOFIA: AccountIdOf = 62; - const FRED: AccountIdOf = 63; - const ANNA: AccountIdOf = 64; - const DAMIAN: AccountIdOf = 65; - - let accounts = [ADAM, TOM, SOFIA, FRED, ANNA, DAMIAN]; - - let bounded_name = bounded_name(); - let bounded_symbol = bounded_symbol(); - let metadata_hash = ipfs_hash(); - let normalized_price = PriceOf::::from_float(10.0); - let decimal_aware_price = PriceProviderOf::::calculate_decimals_aware_price( - normalized_price, - USD_DECIMALS, - CT_DECIMALS, - ) - .unwrap(); - let project_metadata = ProjectMetadata { - token_information: CurrencyMetadata { - name: bounded_name, - symbol: bounded_symbol, - decimals: CT_DECIMALS, - }, - mainnet_token_max_supply: 8_000_000 * CT_UNIT, - total_allocation_size: 50_000 * CT_UNIT, - minimum_price: decimal_aware_price, - bidding_ticket_sizes: BiddingTicketSizes { - 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![AcceptedFundingAsset::USDT].try_into().unwrap(), - funding_destination_account: ISSUER_1, - policy_ipfs_cid: Some(metadata_hash), - participants_account_type: ParticipantsAccountType::Polkadot, - }; - - // overfund with plmc - let plmc_fundings = accounts - .iter() - .map(|acc| UserToPLMCBalance { account: *acc, plmc_amount: PLMC * 1_000_000 }) - .collect_vec(); - let usdt_fundings = accounts - .iter() - .map(|acc| UserToFundingAsset { - account: *acc, - asset_amount: USD_UNIT * 1_000_000, - asset_id: AcceptedFundingAsset::USDT.id(), - }) - .collect_vec(); - inst.mint_plmc_to(plmc_fundings); - inst.mint_funding_asset_to(usdt_fundings); - - let evaluations = inst.generate_successful_evaluations(project_metadata.clone(), 5); - let project_id = inst.create_auctioning_project(project_metadata.clone(), ISSUER_1, None, evaluations); - - let bids = vec![ - (ADAM, 10_000 * CT_UNIT).into(), - (TOM, 20_000 * CT_UNIT).into(), - (SOFIA, 20_000 * CT_UNIT).into(), - (FRED, 10_000 * CT_UNIT).into(), - (ANNA, 5_000 * CT_UNIT).into(), - (DAMIAN, 5_000 * CT_UNIT).into(), - ]; - - inst.bid_for_users(project_id, bids).unwrap(); - - let next_state = inst.go_to_next_state(project_id); - - assert!(matches!(next_state, ProjectStatus::FundingSuccessful)); - - let token_price = inst.get_project_details(project_id).weighted_average_price.unwrap(); - let normalized_wap = - PriceProviderOf::::convert_back_to_normal_price(token_price, USD_DECIMALS, CT_DECIMALS) - .unwrap(); - let desired_price = PriceOf::::from_float(11.1818f64); - - assert_close_enough!( - normalized_wap.saturating_mul_int(CT_UNIT), - desired_price.saturating_mul_int(CT_UNIT), - Perquintill::from_float(0.9999) - ); - } - // Partial acceptance at price <= wap (refund due to less CT bought) - // Full Acceptance at price > wap (refund due to final price lower than original price paid) // Rejection due to no more tokens left (full refund) #[test] fn bids_get_rejected_and_refunded() { @@ -1982,7 +1881,6 @@ mod end_auction_extrinsic { // post bucketing, the bids look like this: // (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) let bids = vec![bid_1, bid_2, bid_3, bid_4, bid_5]; @@ -2033,11 +1931,10 @@ mod end_auction_extrinsic { )); 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); + inst.calculate_auction_plmc_returned_from_all_bids_made(&bids, project_metadata.clone()); let returned_funding_assets = - inst.calculate_auction_funding_asset_returned_from_all_bids_made(&bids, project_metadata.clone(), wap); + inst.calculate_auction_funding_asset_returned_from_all_bids_made(&bids, project_metadata.clone()); let expected_free_plmc = inst .generic_map_operation(vec![returned_auction_plmc.clone(), prev_plmc_balances], MergeOperation::Add); @@ -2086,88 +1983,6 @@ mod end_auction_extrinsic { } } - #[test] - fn wap_from_different_funding_assets() { - // From the knowledge hub: https://hub.polimec.org/learn/calculation-example#auction-round-calculation-example - let mut inst = MockInstantiator::new(Some(RefCell::new(new_test_ext()))); - - const ADAM: AccountIdOf = 60; - const TOM: AccountIdOf = 61; - const SOFIA: AccountIdOf = 62; - const FRED: AccountIdOf = 63; - const ANNA: AccountIdOf = 64; - const DAMIAN: AccountIdOf = 65; - - let accounts = [ADAM, TOM, SOFIA, FRED, ANNA, DAMIAN]; - let mut project_metadata = default_project_metadata(ISSUER_1); - project_metadata.total_allocation_size = 50_000 * CT_UNIT; - project_metadata.participation_currencies = bounded_vec![ - AcceptedFundingAsset::USDT, - AcceptedFundingAsset::USDC, - AcceptedFundingAsset::DOT, - AcceptedFundingAsset::WETH - ]; - - // overfund with plmc - let plmc_fundings = accounts - .iter() - .map(|acc| UserToPLMCBalance { account: *acc, plmc_amount: PLMC * 1_000_000 }) - .collect_vec(); - - let fundings = [ - AcceptedFundingAsset::USDT, - AcceptedFundingAsset::USDC, - AcceptedFundingAsset::DOT, - AcceptedFundingAsset::WETH, - ]; - assert_eq!(fundings.len(), AcceptedFundingAsset::VARIANT_COUNT); - let mut fundings = fundings.into_iter().cycle(); - - let funding_asset_mints = accounts - .iter() - .map(|acc| { - let accepted_asset = fundings.next().unwrap(); - let asset_id = accepted_asset.id(); - let asset_decimals = - inst.execute(|| ::FundingCurrency::decimals(asset_id.clone())); - let asset_unit = 10u128.checked_pow(asset_decimals.into()).unwrap(); - UserToFundingAsset { account: *acc, asset_amount: asset_unit * 1_000_000, asset_id } - }) - .collect_vec(); - inst.mint_plmc_to(plmc_fundings); - inst.mint_funding_asset_to(funding_asset_mints); - - let evaluations = inst.generate_successful_evaluations(project_metadata.clone(), 5); - let project_id = inst.create_auctioning_project(project_metadata, ISSUER_1, None, evaluations); - - let bids = vec![ - (ADAM, Retail, 10_000 * CT_UNIT, ParticipationMode::Classic(1), AcceptedFundingAsset::USDT).into(), - (TOM, Professional, 20_000 * CT_UNIT, ParticipationMode::Classic(1), AcceptedFundingAsset::USDC).into(), - (SOFIA, Retail, 20_000 * CT_UNIT, ParticipationMode::Classic(1), AcceptedFundingAsset::DOT).into(), - (FRED, Institutional, 10_000 * CT_UNIT, ParticipationMode::Classic(1), AcceptedFundingAsset::WETH) - .into(), - (ANNA, Retail, 5_000 * CT_UNIT, ParticipationMode::Classic(1), AcceptedFundingAsset::USDT).into(), - (DAMIAN, Retail, 5_000 * CT_UNIT, ParticipationMode::Classic(1), AcceptedFundingAsset::USDC).into(), - ]; - - inst.bid_for_users(project_id, bids).unwrap(); - - assert!(matches!(inst.go_to_next_state(project_id), ProjectStatus::FundingSuccessful)); - - let token_price = inst.get_project_details(project_id).weighted_average_price.unwrap(); - let normalized_wap = - PriceProviderOf::::convert_back_to_normal_price(token_price, USD_DECIMALS, CT_DECIMALS) - .unwrap(); - - let desired_price = PriceOf::::from_float(11.1818f64); - - assert_close_enough!( - normalized_wap.saturating_mul_int(USD_UNIT), - desired_price.saturating_mul_int(USD_UNIT), - 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()))); diff --git a/pallets/funding/src/tests/6_settlement.rs b/pallets/funding/src/tests/6_settlement.rs index 06744f9ae..376801127 100644 --- a/pallets/funding/src/tests/6_settlement.rs +++ b/pallets/funding/src/tests/6_settlement.rs @@ -167,12 +167,12 @@ mod start_settlement_extrinsic { let (mut inst, project_id) = create_project_with_funding_percentage(40, false); let ct_treasury = ::ContributionTreasury::get(); let project_details = inst.get_project_details(project_id); + let project_metadata = inst.get_project_metadata(project_id); assert_eq!(project_details.funding_amount_reached_usd, 4_000_000 * USD_UNIT); let usd_fee = Percent::from_percent(10u8) * (1_000_000 * USD_UNIT) + Percent::from_percent(8u8) * (3_000_000 * USD_UNIT); - let ct_fee = - project_details.weighted_average_price.unwrap().reciprocal().unwrap().saturating_mul_int(usd_fee); + let ct_fee = project_metadata.minimum_price.reciprocal().unwrap().saturating_mul_int(usd_fee); // Liquidity Pools and Long Term Holder Bonus treasury allocation let treasury_allocation = Percent::from_percent(50) * ct_fee + Percent::from_percent(20) * ct_fee; @@ -486,7 +486,6 @@ mod settle_bid_extrinsic { let mut inst = MockInstantiator::new(Some(RefCell::new(new_test_ext()))); let ed = inst.get_ed(); 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( @@ -506,18 +505,17 @@ mod settle_bid_extrinsic { ParticipationMode::Classic(3u8), AcceptedFundingAsset::USDT, )); - let lower_price_bid_params = BidParams::from(( + let accepted_bid_params = BidParams::from(( BIDDER_2, Retail, 2000 * CT_UNIT, ParticipationMode::Classic(5u8), AcceptedFundingAsset::DOT, )); - let bids = vec![partial_amount_bid_params.clone(), lower_price_bid_params.clone()]; + let bids = vec![partial_amount_bid_params.clone(), accepted_bid_params.clone()]; let evaluations = inst.generate_successful_evaluations(project_metadata.clone(), 5); 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 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, 0)).unwrap()); @@ -542,22 +540,12 @@ mod settle_bid_extrinsic { project_metadata.funding_destination_account, ); - 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, @@ -586,55 +574,6 @@ mod settle_bid_extrinsic { inst.execute(|| LinearRelease::vest(RuntimeOrigin::signed(BIDDER_1), hold_reason).expect("Vesting failed")); inst.assert_plmc_free_balance(BIDDER_1, expected_plmc_refund + expected_final_plmc_bonded + ed); - - // Price > wap bid assertions - let expected_final_plmc_bonded = inst - .calculate_auction_plmc_charged_with_given_price(&vec![lower_price_bid_params.clone()], wap)[0] - .plmc_amount; - let expected_final_dot_paid = inst - .calculate_auction_funding_asset_charged_with_given_price(&vec![lower_price_bid_params.clone()], wap)[0] - .asset_amount; - 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; - - inst.assert_funding_asset_free_balance( - BIDDER_2, - AcceptedFundingAsset::DOT.id(), - expected_dot_refund + dot_ed, - ); - assert_eq!(post_issuer_dot_balance, pre_issuer_dot_balance + expected_final_dot_paid); - - inst.assert_plmc_free_balance(BIDDER_2, expected_plmc_refund + ed); - inst.assert_ct_balance(project_id, BIDDER_2, 2000 * CT_UNIT); - - inst.assert_migration( - project_id, - BIDDER_2, - 2000 * CT_UNIT, - 1, - ParticipationType::Bid, - polkadot_junction!(BIDDER_2), - true, - ); - - // Multiplier 5 should be unbonded no earlier than after 8.67 weeks (i.e. 436'867 blocks) - let multiplier: MultiplierOf = - lower_price_bid_params.mode.multiplier().try_into().ok().unwrap(); - let vesting_time = multiplier.calculate_vesting_duration::(); - - // Sanity check, 5 blocks should not be enough - inst.advance_time(5u64); - inst.execute(|| LinearRelease::vest(RuntimeOrigin::signed(BIDDER_2), hold_reason).expect("Vesting failed")); - assert_ne!( - inst.get_free_plmc_balance_for(BIDDER_2), - expected_plmc_refund + expected_final_plmc_bonded + ed - ); - - // After the vesting time, the full amount should be vested - let current_block = inst.current_block(); - inst.jump_to_block(current_block + vesting_time - 5u64); - inst.execute(|| LinearRelease::vest(RuntimeOrigin::signed(BIDDER_2), hold_reason).expect("Vesting failed")); - inst.assert_plmc_free_balance(BIDDER_2, expected_plmc_refund + expected_final_plmc_bonded + ed); } #[test] diff --git a/pallets/funding/src/tests/misc.rs b/pallets/funding/src/tests/misc.rs index 98be42dc0..7e90bd2d3 100644 --- a/pallets/funding/src/tests/misc.rs +++ b/pallets/funding/src/tests/misc.rs @@ -1,5 +1,4 @@ use super::*; -use polimec_common::assets::AcceptedFundingAsset; // check that functions created to facilitate testing return the expected results mod helper_functions { @@ -131,142 +130,6 @@ mod helper_functions { assert_close_enough!(expected, calculated, Perquintill::from_float(0.999)); } } - - #[test] - fn calculate_auction_plmc_returned() { - const CT_AMOUNT_1: u128 = 5000 * CT_UNIT; - const CT_AMOUNT_2: u128 = 40_000 * CT_UNIT; - const CT_AMOUNT_3: u128 = 10_000 * CT_UNIT; - const CT_AMOUNT_4: u128 = 6000 * CT_UNIT; - const CT_AMOUNT_5: u128 = 2000 * CT_UNIT; - - let bid_1 = BidParams::from(( - BIDDER_1, - Retail, - CT_AMOUNT_1, - ParticipationMode::Classic(1u8), - AcceptedFundingAsset::USDT, - )); - let bid_2 = BidParams::from(( - BIDDER_2, - Retail, - CT_AMOUNT_2, - ParticipationMode::Classic(1u8), - AcceptedFundingAsset::USDT, - )); - let bid_3 = BidParams::from(( - BIDDER_1, - Retail, - CT_AMOUNT_3, - ParticipationMode::Classic(1u8), - AcceptedFundingAsset::USDT, - )); - let bid_4 = BidParams::from(( - BIDDER_3, - Retail, - CT_AMOUNT_4, - ParticipationMode::Classic(1u8), - AcceptedFundingAsset::USDT, - )); - let bid_5 = BidParams::from(( - BIDDER_4, - Retail, - CT_AMOUNT_5, - ParticipationMode::Classic(1u8), - AcceptedFundingAsset::USDT, - )); - - // 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) - // | -------------------- 1USD ----------------------|---- 1.1 USD ---|---- 1.2 USD ----|----------- 1.3 USD -------------| - // post wap ~ 1.0557252: - // (Accepted, 5k) - (Partially, 32k) - (Rejected, 5k) - (Accepted, 5k) - (Accepted - 5k) - (Accepted - 1k) - (Accepted - 2k) - - const ORIGINAL_PLMC_CHARGED_BIDDER_1: f64 = 18_452.3809523790; - const ORIGINAL_PLMC_CHARGED_BIDDER_2: f64 = 47_619.0476190470; - const ORIGINAL_PLMC_CHARGED_BIDDER_3: f64 = 86_90.4761904760; - const ORIGINAL_PLMC_CHARGED_BIDDER_4: f64 = 30_95.2380952380; - - const FINAL_PLMC_CHARGED_BIDDER_1: f64 = 12_236.4594692840; - const FINAL_PLMC_CHARGED_BIDDER_2: f64 = 38_095.2380952380; - const FINAL_PLMC_CHARGED_BIDDER_3: f64 = 75_40.8942202840; - const FINAL_PLMC_CHARGED_BIDDER_4: f64 = 2_513.6314067610; - - let bids = vec![bid_1, bid_2, bid_3, bid_4, bid_5]; - - let mut inst = MockInstantiator::new(Some(RefCell::new(new_test_ext()))); - let project_metadata = ProjectMetadata { - token_information: default_token_information(), - mainnet_token_max_supply: 8_000_000 * CT_UNIT, - total_allocation_size: 50_000 * CT_UNIT, - minimum_price: PriceProviderOf::::calculate_decimals_aware_price( - PriceOf::::from_float(10.0), - USD_DECIMALS, - CT_DECIMALS, - ) - .unwrap(), - bidding_ticket_sizes: BiddingTicketSizes { - professional: TicketSize::new(5000 * USD_UNIT, None), - institutional: TicketSize::new(5000 * USD_UNIT, None), - retail: TicketSize::new(100 * USD_UNIT, None), - phantom: Default::default(), - }, - participation_currencies: vec![AcceptedFundingAsset::USDT].try_into().unwrap(), - funding_destination_account: ISSUER_1, - policy_ipfs_cid: Some(ipfs_hash()), - participants_account_type: ParticipantsAccountType::Polkadot, - }; - - let successful_evaluations = inst.generate_successful_evaluations(project_metadata.clone(), 5); - - let project_id = inst.create_finished_project( - project_metadata.clone(), - ISSUER_1, - None, - successful_evaluations, - bids.clone(), - ); - - let wap = inst.get_project_details(project_id).weighted_average_price.unwrap(); - - let expected_returns = vec![ - ORIGINAL_PLMC_CHARGED_BIDDER_1 - FINAL_PLMC_CHARGED_BIDDER_1, - ORIGINAL_PLMC_CHARGED_BIDDER_2 - FINAL_PLMC_CHARGED_BIDDER_2, - ORIGINAL_PLMC_CHARGED_BIDDER_3 - FINAL_PLMC_CHARGED_BIDDER_3, - ORIGINAL_PLMC_CHARGED_BIDDER_4 - FINAL_PLMC_CHARGED_BIDDER_4, - ]; - - let mut returned_plmc_mappings = - inst.calculate_auction_plmc_returned_from_all_bids_made(&bids, project_metadata.clone(), wap); - returned_plmc_mappings.sort_by(|b1, b2| b1.account.cmp(&b2.account)); - - let returned_plmc_balances = returned_plmc_mappings.into_iter().map(|map| map.plmc_amount).collect_vec(); - - for (expected_return, returned_balance) in zip(expected_returns, returned_plmc_balances) { - let expected_value = FixedU128::from_float(expected_return).checked_mul_int(PLMC).unwrap(); - - assert_close_enough!(expected_value, returned_balance, Perquintill::from_float(0.99)); - } - } - - #[test] - fn bucket_wap_calculation() { - let initial_price = FixedU128::from_float(10.0); - let mut bucket = Bucket::new(100u128, initial_price, FixedU128::from_float(1.0), 10u128); - let wap = bucket.calculate_wap(100u128); - assert!(wap == initial_price); - - // Initial token amount: 100 - // Simulate total bidding amount of 128 - bucket.update(100u128); - bucket.update(10u128); - bucket.update(10u128); - bucket.update(8u128); - let wap = bucket.calculate_wap(100u128); - let expected = FixedU128::from_float(10.628); - let diff = if wap > expected { wap - expected } else { expected - wap }; - assert!(diff <= FixedU128::from_float(0.001)); - } } // logic of small functions that extrinsics use to process data or interact with storage @@ -316,24 +179,25 @@ mod inner_functions { 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); + let usd_sold = bucket.calculate_usd_raised(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(); + + let usd_raised_first_bucket = bucket.current_price.saturating_mul_int(400_000 * CT_UNIT); bucket.update(project_metadata.total_allocation_size); + + let usd_raised_second_bucket = bucket.current_price.saturating_mul_int(50_000 * CT_UNIT); bucket.update(50_000 * CT_UNIT); + + let usd_raised_third_bucket = bucket.current_price.saturating_mul_int(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); + let usd_sold = bucket.calculate_usd_raised(project_metadata.total_allocation_size); assert_eq!(usd_sold, total_expected_rasied); } } diff --git a/pallets/funding/src/tests/runtime_api.rs b/pallets/funding/src/tests/runtime_api.rs index 0f6657420..30c228219 100644 --- a/pallets/funding/src/tests/runtime_api.rs +++ b/pallets/funding/src/tests/runtime_api.rs @@ -226,69 +226,9 @@ fn contribution_tokens() { fn funding_asset_to_ct_amount_classic() { let mut inst = MockInstantiator::new(Some(RefCell::new(new_test_ext()))); - // We want to use a funding asset that is not equal to 1 USD - // Sanity check - assert_eq!( - PriceProviderOf::::get_price(AcceptedFundingAsset::DOT.id()).unwrap(), - PriceOf::::from_float(69.0f64) - ); - - let dot_amount: u128 = 1350_0_000_000_000; - // USD Ticket = 93_150 USD - - // Easy case, wap is already calculated, we want to know how many tokens at wap we can buy with `x` USDT - let project_metadata_1 = default_project_metadata(ISSUER_1); - let evaluations = inst.generate_successful_evaluations(project_metadata_1.clone(), 5); - let project_id_1 = - inst.create_finished_project(project_metadata_1.clone(), ISSUER_1, None, evaluations.clone(), vec![]); - let wap = project_metadata_1.minimum_price; - assert_eq!(inst.get_project_details(project_id_1).weighted_average_price.unwrap(), wap); - - // Price of ct is min price = 10 USD/CT - let expected_ct_amount_contribution = 9_315 * CT_UNIT; - inst.execute(|| { - let block_hash = System::block_hash(System::block_number()); - let ct_amount = TestRuntime::funding_asset_to_ct_amount_classic( - &TestRuntime, - block_hash, - project_id_1, - AcceptedFundingAsset::DOT, - dot_amount, - ) - .unwrap(); - assert_eq!(ct_amount, expected_ct_amount_contribution); - }); - - // Medium case, contribution at a wap that is not the minimum price. - let project_metadata_2 = default_project_metadata(ISSUER_2); - let new_price = PriceOf::::from_float(16.3f64); - 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); - let project_id_2 = - inst.create_finished_project(project_metadata_2.clone(), ISSUER_2, None, evaluations.clone(), bids); - // Sanity check - let project_details = inst.get_project_details(project_id_2); - assert_eq!(project_details.weighted_average_price.unwrap(), decimal_aware_price); - - // 5'714.72... rounded down - let expected_ct_amount_contribution = 5_714_720_000_000_000_000; - inst.execute(|| { - let block_hash = System::block_hash(System::block_number()); - let ct_amount = TestRuntime::funding_asset_to_ct_amount_classic( - &TestRuntime, - block_hash, - project_id_2, - AcceptedFundingAsset::DOT, - dot_amount, - ) - .unwrap(); - assert_close_enough!(ct_amount, expected_ct_amount_contribution, Perquintill::from_float(0.999f64)); - }); - // Medium case, a bid goes over part of a bucket (bucket after the first one) let project_metadata_3 = default_project_metadata(ISSUER_3); + let evaluations = inst.generate_successful_evaluations(project_metadata_3.clone(), 5); let project_id_3 = inst.create_auctioning_project(project_metadata_3.clone(), ISSUER_3, None, evaluations.clone()); let mut bucket = inst.execute(|| Buckets::::get(project_id_3)).unwrap(); @@ -297,15 +237,7 @@ fn funding_asset_to_ct_amount_classic() { 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, 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( - &bids, - project_metadata_3.clone(), - None, - ); - inst.mint_plmc_to(necessary_plmc); - inst.mint_funding_asset_to(necessary_usdt); + inst.mint_necessary_tokens_for_bids(project_id_3, bids.clone()); inst.bid_for_users(project_id_3, bids).unwrap(); // Sanity check @@ -366,65 +298,9 @@ fn funding_asset_to_ct_amount_otm() { PriceOf::::from_float(69.0f64) ); - let dot_participation_amount: u128 = 1350_0_000_000_000; - // USD Ticket = 93_150 USD - let dot_fee_amount = FixedU128::from_float(0.015f64).saturating_mul_int(dot_participation_amount); - - // Easy case, wap is already calculated, we want to know how many tokens at wap we can buy with `x` USDT - let project_metadata_1 = default_project_metadata(ISSUER_1); - let evaluations = inst.generate_successful_evaluations(project_metadata_1.clone(), 5); - let project_id_1 = - inst.create_finished_project(project_metadata_1.clone(), ISSUER_1, None, evaluations.clone(), vec![]); - let wap = project_metadata_1.minimum_price; - assert_eq!(inst.get_project_details(project_id_1).weighted_average_price.unwrap(), wap); - - // Price of ct is min price = 10 USD/CT - let expected_ct_amount_contribution = 9_315 * CT_UNIT; - inst.execute(|| { - let block_hash = System::block_hash(System::block_number()); - let (ct_amount, fee_amount) = TestRuntime::funding_asset_to_ct_amount_otm( - &TestRuntime, - block_hash, - project_id_1, - AcceptedFundingAsset::DOT, - dot_participation_amount + dot_fee_amount, - ) - .unwrap(); - assert_close_enough!(ct_amount, expected_ct_amount_contribution, Perquintill::from_float(0.9999)); - assert_close_enough!(fee_amount, dot_fee_amount, Perquintill::from_float(0.9999)); - }); - - // Medium case, contribution at a wap that is not the minimum price. - let project_metadata_2 = default_project_metadata(ISSUER_2); - let new_price = PriceOf::::from_float(16.3f64); - 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); - let project_id_2 = - inst.create_finished_project(project_metadata_2.clone(), ISSUER_2, None, evaluations.clone(), bids); - // Sanity check - let project_details = inst.get_project_details(project_id_2); - assert_eq!(project_details.weighted_average_price.unwrap(), decimal_aware_price); - - // 5'714.72... rounded down - let expected_ct_amount_contribution = 5_714_720_000_000_000_000; - inst.execute(|| { - let block_hash = System::block_hash(System::block_number()); - let (ct_amount, fee_amount) = TestRuntime::funding_asset_to_ct_amount_otm( - &TestRuntime, - block_hash, - project_id_2, - AcceptedFundingAsset::DOT, - dot_participation_amount + dot_fee_amount, - ) - .unwrap(); - assert_close_enough!(ct_amount, expected_ct_amount_contribution, Perquintill::from_float(0.9999f64)); - assert_close_enough!(fee_amount, dot_fee_amount, Perquintill::from_float(0.9999f64)); - }); - // Medium case, a bid goes over part of a bucket (bucket after the first one) let project_metadata_3 = default_project_metadata(ISSUER_3); + let evaluations = inst.generate_successful_evaluations(project_metadata_3.clone(), 5); let project_id_3 = inst.create_auctioning_project(project_metadata_3.clone(), ISSUER_3, None, evaluations.clone()); let mut bucket = inst.execute(|| Buckets::::get(project_id_3)).unwrap(); @@ -433,15 +309,7 @@ fn funding_asset_to_ct_amount_otm() { 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, 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( - &bids, - project_metadata_3.clone(), - None, - ); - inst.mint_plmc_to(necessary_plmc); - inst.mint_funding_asset_to(necessary_usdt); + inst.mint_necessary_tokens_for_bids(project_id_3, bids.clone()); inst.bid_for_users(project_id_3, bids).unwrap(); // Sanity check @@ -558,8 +426,6 @@ fn get_next_vesting_schedule_merge_candidates() { 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 = inst.execute(|| pallet_linear_release::Vesting::::get(BIDDER_1, hold_reason).unwrap().to_vec()); diff --git a/pallets/funding/src/types.rs b/pallets/funding/src/types.rs index e70bcfd67..abb0a1562 100644 --- a/pallets/funding/src/types.rs +++ b/pallets/funding/src/types.rs @@ -287,13 +287,11 @@ pub mod storage { } #[derive(Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug, MaxEncodedLen, TypeInfo)] - pub struct ProjectDetails { + pub struct ProjectDetails { pub issuer_account: AccountId, pub issuer_did: Did, /// Whether the project is frozen, so no `metadata` changes are allowed. pub is_frozen: bool, - /// The price in USD per token decided after the Auction Round - pub weighted_average_price: Option, /// The current status of the project pub status: ProjectStatus, /// When the different project phases start and end @@ -441,31 +439,42 @@ pub mod storage { self.current_price = self.current_price.saturating_add(self.delta_price); } - pub fn calculate_wap(self, mut total_amount: Balance) -> Price { - // First bucket is not empty so wap is the same as the initial price - if self.current_price == self.initial_price { - return self.current_price; + pub fn calculate_usd_raised(self, allocation_size: Balance) -> Balance { + let mut usd_raised = Balance::zero(); + let mut cts_left = allocation_size; + let mut calculation_bucket = self.clone(); + + // If current bucket is the first bucket, then its not oversubscribed and we just return the price * tokens bought + if calculation_bucket.current_price == calculation_bucket.initial_price { + let amount_bought = allocation_size.saturating_sub(calculation_bucket.amount_left); + return calculation_bucket.current_price.saturating_mul_int(amount_bought); } - let mut amount: Balance = self.delta_amount.saturating_sub(self.amount_left); - let mut price: Price = self.current_price; - let mut bucket_sizes: Vec<(Balance, Price)> = Vec::new(); - while price > self.initial_price && total_amount > Balance::zero() { - total_amount.saturating_reduce(amount); - bucket_sizes.push((price.saturating_mul_int(amount), price)); - price = price.saturating_sub(self.delta_price); - amount = self.delta_amount; + // If the current bucket is at a higher price, then auction is oversubscribed + // We first calculate the amount bought by checking the amount remaining in the bucket. + // Then we go down in buckets assuming the full amount was bought in each lower bucket. + else { + let amount_bought = calculation_bucket.delta_amount.saturating_sub(calculation_bucket.amount_left); + cts_left.saturating_reduce(amount_bought); + usd_raised = + usd_raised.saturating_add(calculation_bucket.current_price.saturating_mul_int(amount_bought)); + calculation_bucket.current_price.saturating_reduce(calculation_bucket.delta_price); } - if total_amount > Balance::zero() { - bucket_sizes.push((self.initial_price.saturating_mul_int(total_amount), self.initial_price)); - } + while cts_left > 0 { + let amount = if calculation_bucket.current_price == calculation_bucket.initial_price { + cts_left + } else { + cts_left.min(calculation_bucket.delta_amount) + }; - let sum = bucket_sizes.iter().map(|x| x.0).fold(Balance::zero(), |acc, x| acc.saturating_add(x)); + usd_raised = usd_raised.saturating_add(calculation_bucket.current_price.saturating_mul_int(amount)); + cts_left = cts_left.saturating_sub(amount); + + calculation_bucket.current_price = + calculation_bucket.current_price.saturating_sub(calculation_bucket.delta_price); + } - bucket_sizes - .into_iter() - .map(|x| ::saturating_from_rational(x.0, sum).saturating_mul(x.1)) - .fold(Price::zero(), |acc: Price, p: Price| acc.saturating_add(p)) + usd_raised } } } @@ -828,8 +837,7 @@ pub mod extrinsic { pub receiving_account: Junction, } - pub struct BidRefund { - pub final_ct_usd_price: PriceOf, + pub struct BidRefund { pub final_ct_amount: Balance, pub refunded_plmc: Balance, pub refunded_funding_asset_amount: Balance, diff --git a/pallets/oracle-ocw/src/tests.rs b/pallets/oracle-ocw/src/tests.rs index cf90470c5..e9252a6cd 100644 --- a/pallets/oracle-ocw/src/tests.rs +++ b/pallets/oracle-ocw/src/tests.rs @@ -37,7 +37,7 @@ fn call_offchain_worker() { assert_eq!(tx.signature.unwrap().0, 0); match tx.call { - RuntimeCall::Oracle(orml_oracle::Call::feed_values { values }) => { + RuntimeCall::Oracle(orml_oracle::Call::feed_values { values }) => for (asset, price) in values { match asset { 10 => assert_close_enough(price, FixedU128::from_float(6.138485575453039783)), @@ -47,8 +47,7 @@ fn call_offchain_worker() { 10_000 => assert_close_enough(price, FixedU128::from_float(3611.253612654460630264)), _ => panic!("Unexpected asset"), } - } - }, + }, _ => panic!("Unexpected call"), } });