Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

✨ Optimize wap calculation #299

Merged
merged 1 commit into from
May 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
316 changes: 215 additions & 101 deletions pallets/funding/src/benchmarking.rs

Large diffs are not rendered by default.

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,
},
usd_bid_on_oversubscription: None,
funding_end_block: None,
parachain_id: None,
migration_readiness_check: None,
Expand Down
80 changes: 72 additions & 8 deletions pallets/funding/src/functions/3_auction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ impl<T: Config> Pallet<T> {
/// 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
Expand All @@ -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 All @@ -43,7 +43,7 @@ impl<T: Config> Pallet<T> {

ensure!(now >= auction_initialize_period_start_block, Error::<T>::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::<T>::IncorrectRound);

Expand Down Expand Up @@ -87,8 +87,8 @@ impl<T: Config> Pallet<T> {

/// 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
Expand All @@ -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,70 @@ impl<T: Config> Pallet<T> {
})
}

/// 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 {
JuaniRios marked this conversation as resolved.
Show resolved Hide resolved
// * Get variables *
let 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.
let mut project_details =
ProjectsDetails::<T>::get(project_id).ok_or(Error::<T>::ProjectDetailsNotFound)?;
project_details.phase_transition_points.random_closing_ending = Some(end_block);
project_details.status = ProjectStatus::CalculatingWAP;
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 Expand Up @@ -230,7 +294,7 @@ impl<T: Config> Pallet<T> {
);
ensure!(multiplier.into() <= max_multiplier && multiplier.into() > 0u8, Error::<T>::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::<T>::TooHigh
Expand Down
23 changes: 9 additions & 14 deletions pallets/funding/src/functions/4_contribution.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,33 +27,26 @@ impl<T: Config> Pallet<T> {
pub fn do_start_community_funding(project_id: ProjectId) -> DispatchResultWithPostInfo {
// * Get variables *
let 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);
ensure!(project_details.status == ProjectStatus::CalculatingWAP, Error::<T>::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::<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
.phase_transition_points
.community
Expand All @@ -75,10 +68,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
73 changes: 48 additions & 25 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,44 @@ 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);
if highest_accepted_price > project_metadata.minimum_price {
info.usd_bid_on_oversubscription = Some(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)?;

// 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 +176,34 @@ 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 {

// 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<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: T::Price, b: T::Price| a.saturating_add(b))
.fold(Zero::zero(), |a: PriceOf<T>, b: PriceOf<T>| 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::<T>::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;
Expand Down Expand Up @@ -247,15 +271,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