Skip to content

Commit

Permalink
optimize wap calculation
Browse files Browse the repository at this point in the history
  • Loading branch information
JuaniRios committed May 17, 2024
1 parent c7a42f4 commit b046766
Show file tree
Hide file tree
Showing 13 changed files with 230 additions and 101 deletions.
2 changes: 1 addition & 1 deletion pallets/funding/src/benchmarking.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1984,7 +1984,7 @@ mod benchmarks {

#[block]
{
Pallet::<T>::do_auction_closing(project_id).unwrap();
Pallet::<T>::do_start_auction_closing(project_id).unwrap();
}
// * validity checks *
// Storage
Expand Down
1 change: 1 addition & 0 deletions pallets/funding/src/functions/1_application.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ impl<T: Config> Pallet<T> {
total_bonded_plmc: Zero::zero(),
evaluators_outcome: EvaluatorsOutcome::Unchanged,
},
auction_round_info: AuctionRoundInfoOf::<T> { is_oversubscribed: IsOversubscribed::No },
funding_end_block: None,
parachain_id: None,
migration_readiness_check: None,
Expand Down
65 changes: 62 additions & 3 deletions pallets/funding/src/functions/3_auction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ impl<T: Config> Pallet<T> {
/// 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<T>, project_id: ProjectId) -> DispatchResultWithPostInfo {
pub fn do_start_auction_opening(caller: AccountIdOf<T>, project_id: ProjectId) -> DispatchResultWithPostInfo {
// * Get variables *
let mut project_details = ProjectsDetails::<T>::get(project_id).ok_or(Error::<T>::ProjectDetailsNotFound)?;
let now = <frame_system::Pallet<T>>::block_number();
Expand Down Expand Up @@ -109,7 +109,7 @@ impl<T: Config> Pallet<T> {
/// 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::<T>::get(project_id).ok_or(Error::<T>::ProjectDetailsNotFound)?;
let now = <frame_system::Pallet<T>>::block_number();
Expand All @@ -134,7 +134,7 @@ impl<T: Config> Pallet<T> {
// 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::<T>::TooManyInsertionAttempts.into()),
Expand All @@ -149,6 +149,65 @@ impl<T: Config> Pallet<T> {
})
}

#[transactional]
pub fn do_end_auction_closing(project_id: ProjectId) -> DispatchResultWithPostInfo {
// * Get variables *
let mut project_details = ProjectsDetails::<T>::get(project_id).ok_or(Error::<T>::ProjectDetailsNotFound)?;
let project_metadata = ProjectsMetadata::<T>::get(project_id).ok_or(Error::<T>::ProjectMetadataNotFound)?;
let now = <frame_system::Pallet<T>>::block_number();
let auction_closing_start_block =
project_details.phase_transition_points.auction_closing.start().ok_or(Error::<T>::TransitionPointNotSet)?;
let auction_closing_end_block =
project_details.phase_transition_points.auction_closing.end().ok_or(Error::<T>::TransitionPointNotSet)?;

// * Validity checks *
ensure!(now > auction_closing_end_block, Error::<T>::TooEarlyForRound);
ensure!(project_details.status == ProjectStatus::AuctionClosing, Error::<T>::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.
project_details.phase_transition_points.random_closing_ending = Some(end_block);
ProjectsDetails::<T>::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::<T>::TooManyInsertionAttempts.into()),
};

// * Emit events *
Self::deposit_event(Event::<T>::ProjectPhaseTransition {
project_id,
phase: ProjectPhases::CalculatingWAP,
});

Ok(PostDispatchInfo {
// TODO: make new benchmark
actual_weight: Some(WeightInfoOf::<T>::start_community_funding(
insertion_iterations,
accepted_bids_count,
rejected_bids_count,
)),
pays_fee: Pays::Yes,
})
},
}
}

/// Bid for a project in the bidding stage.
///
/// # Arguments
Expand Down
19 changes: 11 additions & 8 deletions pallets/funding/src/functions/4_contribution.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,16 +42,17 @@ impl<T: Config> Pallet<T> {
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());
let auction_allocation_size =
project_metadata.auction_round_allocation_percentage * project_metadata.total_allocation_size;

// * 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 _ = Self::decide_winning_bids(project_id, end_block, auction_allocation_size)?;
let wap_result = Self::calculate_weighted_average_price(project_id);

let mut project_details = ProjectsDetails::<T>::get(project_id).ok_or(Error::<T>::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
Expand All @@ -75,10 +76,12 @@ impl<T: Config> Pallet<T> {
phase: ProjectPhases::CommunityFunding,
});

//TODO: address this
let rejected_bids_count = 0;
Ok(PostDispatchInfo {
actual_weight: Some(WeightInfoOf::<T>::start_community_funding(
insertion_iterations,
accepted_bids_count,
winning_bids_count,
rejected_bids_count,
)),
pays_fee: Pays::Yes,
Expand Down
88 changes: 60 additions & 28 deletions pallets/funding/src/functions/misc.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use super::*;
use itertools::Itertools;

// Helper functions
// ATTENTION: if this is called directly, it will not be transactional
Expand Down Expand Up @@ -69,8 +70,7 @@ impl<T: Config> Pallet<T> {
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<T>,
auction_allocation_size: BalanceOf<T>,
Expand All @@ -82,8 +82,6 @@ impl<T: Config> Pallet<T> {
// temp variable to store the total value of the bids (i.e price * amount = Cumulative Ticket Size)
let mut bid_usd_value_sum = BalanceOf::<T>::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::<T>::PriceNotFound)?;

let project_metadata = ProjectsMetadata::<T>::get(project_id).ok_or(Error::<T>::ProjectMetadataNotFound)?;
let mut highest_accepted_price = project_metadata.minimum_price;
Expand Down Expand Up @@ -122,21 +120,49 @@ impl<T: Config> Pallet<T> {
bid.final_ct_amount = buyable_amount;
highest_accepted_price = highest_accepted_price.max(bid.original_ct_usd_price);
}
Bids::<T>::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::<T>::remove((project_id, &bid.bidder, &bid.id));
}

ProjectsDetails::<T>::mutate(project_id, |maybe_info| -> DispatchResult {
if let Some(info) = maybe_info {
info.remaining_contribution_tokens.saturating_reduce(bid_token_amount_sum);
info.auction_round_info = AuctionRoundInfo {
is_oversubscribed: if highest_accepted_price == project_metadata.minimum_price {
IsOversubscribed::No
} else {
IsOversubscribed::Yes { total_usd_bid: bid_usd_value_sum }
},
};
Ok(())
} else {
Err(Error::<T>::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<u32, DispatchError> {
let project_metadata = ProjectsMetadata::<T>::get(project_id).ok_or(Error::<T>::ProjectMetadataNotFound)?;
let project_details = ProjectsDetails::<T>::get(project_id).ok_or(Error::<T>::ProjectDetailsNotFound)?;
// Rejected bids were deleted in the previous block.
let accepted_bids = Bids::<T>::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::<T>::PriceNotFound)?;
let is_oversubscribed = project_details.auction_round_info.is_oversubscribed;

// 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
Expand All @@ -155,31 +181,38 @@ impl<T: Config> Pallet<T> {

// 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<T>| -> PriceOf<T> {
let ticket_size = bid.original_ct_usd_price.saturating_mul_int(bid.final_ct_amount);
let bid_weight = <T::Price as FixedPointNumber>::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 {
accepted_bids
.iter()
.map(calc_weighted_price_fn)
.fold(Zero::zero(), |a: T::Price, b: T::Price| a.saturating_add(b))

// After reading from storage all accepted bids when calculating the weighted price of each bid, we store them here
let mut weighted_token_price = match is_oversubscribed {
IsOversubscribed::No => project_metadata.minimum_price,
IsOversubscribed::Yes { total_usd_bid } => {
let calc_weighted_price_fn = |bid: &BidInfoOf<T>| -> PriceOf<T> {
let ticket_size = bid.original_ct_usd_price.saturating_mul_int(bid.final_ct_amount);
let bid_weight =
<T::Price as FixedPointNumber>::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: PriceOf<T>, b: PriceOf<T>| a.saturating_add(b))
},
};
// 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::<T>::zero();

// Update storage
// Update the bid in the storage
for mut bid in accepted_bids.into_iter() {
let mut total_accepted_bids = 0u32;
for mut bid in accepted_bids {
total_accepted_bids += 1;

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;
Expand Down Expand Up @@ -247,15 +280,14 @@ impl<T: Config> Pallet<T> {
ProjectsDetails::<T>::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 {
Err(Error::<T>::ProjectDetailsNotFound.into())
}
})?;

Ok((accepted_bids_count, rejected_bids_count))
Ok(total_accepted_bids)
}

/// Refund a bid because of `reason`.
Expand Down
Loading

0 comments on commit b046766

Please sign in to comment.