Skip to content

Commit

Permalink
✨ Optimize wap calculation (#299)
Browse files Browse the repository at this point in the history
## What?
- Spread out the WAP on_initialize calculation into 2 blocks

## Why?
- Because the ref time was too close to the block limit

## How?
- Create a do_end_auction_closing function that does the deciding of which bids are successful, and refunds/deletes failed ones
- do_start_community now does the WAP calculation only

## Testing?
- Run normal tests, all logic should be maintained
  • Loading branch information
JuaniRios authored May 29, 2024
1 parent 720e9c6 commit 184b645
Show file tree
Hide file tree
Showing 15 changed files with 479 additions and 268 deletions.
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 {
// * 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

0 comments on commit 184b645

Please sign in to comment.