From 4a7fde55a9170b98d45ffe1f6b19b6a2f0b07d39 Mon Sep 17 00:00:00 2001 From: Juan Ignacio Rios Date: Tue, 27 Jun 2023 15:49:58 +0200 Subject: [PATCH 01/27] feat(214): evaluator rewards test skeleton --- polimec-skeleton/pallets/funding/src/tests.rs | 54 ++++++++++++++++--- 1 file changed, 48 insertions(+), 6 deletions(-) diff --git a/polimec-skeleton/pallets/funding/src/tests.rs b/polimec-skeleton/pallets/funding/src/tests.rs index 7a29701c4..9d9cb04aa 100644 --- a/polimec-skeleton/pallets/funding/src/tests.rs +++ b/polimec-skeleton/pallets/funding/src/tests.rs @@ -1092,6 +1092,11 @@ mod defaults { let bounded_symbol = BoundedVec::try_from("CTEST".as_bytes().to_vec()).unwrap(); let metadata_hash = hashed(format!("{}-{}", METADATA, nonce)); ProjectMetadata { + token_information: CurrencyMetadata { + name: bounded_name, + symbol: bounded_symbol, + decimals: ASSET_DECIMALS, + }, total_allocation_size: 1_000_000_0_000_000_000, minimum_price: PriceOf::::from_float(1.0), ticket_size: TicketSize { @@ -1103,14 +1108,9 @@ mod defaults { maximum: None, }, funding_thresholds: Default::default(), + offchain_information_hash: Some(metadata_hash), conversion_rate: 0, participation_currencies: AcceptedFundingAsset::USDT, - offchain_information_hash: Some(metadata_hash), - token_information: CurrencyMetadata { - name: bounded_name, - symbol: bounded_symbol, - decimals: ASSET_DECIMALS, - }, } } @@ -1619,6 +1619,48 @@ mod evaluation_round_success { AuctioningProject::new_with(&test_env, project3, issuer, evaluations.clone()); AuctioningProject::new_with(&test_env, project4, issuer, evaluations); } + + #[test] + fn rewards_are_paid() { + const TARGET_FUNDING_AMOUNT_USD: BalanceOf = 1_000_000 * US_DOLLAR; + + + let test_env = TestEnvironment::new(); + let issuer = ISSUER; + let project = ProjectMetadataOf:: { + token_information: CurrencyMetadata { + name: "Test Token".as_bytes().to_vec().try_into().unwrap(), + symbol: "TT".as_bytes().to_vec().try_into().unwrap(), + decimals: 10, + }, + total_allocation_size: 1_000_000 * ASSET_UNIT, + minimum_price: 1u128.into(), + ticket_size: TicketSize::> { minimum: Some(1), maximum: None }, + participants_size: ParticipantsSize { + minimum: Some(2), + maximum: None, + }, + funding_thresholds: Default::default(), + conversion_rate: 0, + participation_currencies: Default::default(), + offchain_information_hash: Some(hashed(METADATA)), + }; + let evaluations = default_evaluations(); + let bids = default_bids(); + let community_contributions = default_community_buys(); + let remainder_contributions = default_remainder_buys(); + + let finished_project = FinishedProject::new_with( + &test_env, + project, + issuer, + evaluations, + bids, + community_contributions, + remainder_contributions, + ); + + } } #[cfg(test)] From 4e7629d2d16c4d26e84ad4feb6190e1289085de1 Mon Sep 17 00:00:00 2001 From: Juan Ignacio Rios Date: Tue, 27 Jun 2023 16:55:59 +0200 Subject: [PATCH 02/27] feat(214): 2 new fields on `ProjectMetadata`: - mainnet_token_max_supply - funding_destination_account --- polimec-skeleton/pallets/funding/src/lib.rs | 2 +- polimec-skeleton/pallets/funding/src/tests.rs | 6 +++++- polimec-skeleton/pallets/funding/src/types.rs | 9 ++++++--- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/polimec-skeleton/pallets/funding/src/lib.rs b/polimec-skeleton/pallets/funding/src/lib.rs index edc43470d..d20dc0456 100644 --- a/polimec-skeleton/pallets/funding/src/lib.rs +++ b/polimec-skeleton/pallets/funding/src/lib.rs @@ -227,7 +227,7 @@ pub type HashOf = ::Hash; pub type AssetIdOf = <::FundingCurrency as fungibles::Inspect<::AccountId>>::AssetId; -pub type ProjectMetadataOf = ProjectMetadata>, BalanceOf, PriceOf, HashOf>; +pub type ProjectMetadataOf = ProjectMetadata>, BalanceOf, PriceOf, AccountIdOf, HashOf>; pub type ProjectDetailsOf = ProjectDetails, BlockNumberOf, PriceOf, BalanceOf>; pub type VestingOf = Vesting, BalanceOf>; pub type EvaluationInfoOf = diff --git a/polimec-skeleton/pallets/funding/src/tests.rs b/polimec-skeleton/pallets/funding/src/tests.rs index 9d9cb04aa..e9b3b6574 100644 --- a/polimec-skeleton/pallets/funding/src/tests.rs +++ b/polimec-skeleton/pallets/funding/src/tests.rs @@ -1097,6 +1097,7 @@ mod defaults { symbol: bounded_symbol, decimals: ASSET_DECIMALS, }, + mainnet_token_max_supply: 8_000_000_0_000_000_000, total_allocation_size: 1_000_000_0_000_000_000, minimum_price: PriceOf::::from_float(1.0), ticket_size: TicketSize { @@ -1108,9 +1109,10 @@ mod defaults { maximum: None, }, funding_thresholds: Default::default(), - offchain_information_hash: Some(metadata_hash), conversion_rate: 0, participation_currencies: AcceptedFundingAsset::USDT, + funding_destination_account: ISSUER, + offchain_information_hash: Some(metadata_hash), } } @@ -1633,6 +1635,7 @@ mod evaluation_round_success { symbol: "TT".as_bytes().to_vec().try_into().unwrap(), decimals: 10, }, + mainnet_token_max_supply: 10_000_000 * ASSET_UNIT, total_allocation_size: 1_000_000 * ASSET_UNIT, minimum_price: 1u128.into(), ticket_size: TicketSize::> { minimum: Some(1), maximum: None }, @@ -1643,6 +1646,7 @@ mod evaluation_round_success { funding_thresholds: Default::default(), conversion_rate: 0, participation_currencies: Default::default(), + funding_destination_account: ISSUER, offchain_information_hash: Some(hashed(METADATA)), }; let evaluations = default_evaluations(); diff --git a/polimec-skeleton/pallets/funding/src/types.rs b/polimec-skeleton/pallets/funding/src/types.rs index dd286cf4a..acb754b7c 100644 --- a/polimec-skeleton/pallets/funding/src/types.rs +++ b/polimec-skeleton/pallets/funding/src/types.rs @@ -76,9 +76,11 @@ pub mod storage_types { use super::*; #[derive(Default, Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug, MaxEncodedLen, TypeInfo)] - pub struct ProjectMetadata { + pub struct ProjectMetadata { /// Token Metadata pub token_information: CurrencyMetadata, + /// Mainnet Token Max Supply + pub mainnet_token_max_supply: Balance, /// Total allocation of Contribution Tokens available for the Funding Round pub total_allocation_size: Balance, /// Minimum price per Contribution Token @@ -95,11 +97,12 @@ pub mod storage_types { /// e.g. https://github.com/paritytech/substrate/blob/427fd09bcb193c1e79dec85b1e207c718b686c35/frame/uniques/src/types.rs#L110 /// For now is easier to handle the case where only just one Currency is accepted pub participation_currencies: AcceptedFundingAsset, + pub funding_destination_account: AccountId, /// Additional metadata pub offchain_information_hash: Option, } - impl - ProjectMetadata + impl + ProjectMetadata { // TODO: PLMC-162. Perform a REAL validity check pub fn validity_check(&self) -> Result<(), ValidityError> { From 955ebeae53a2e78268219100d2d03b77e67c4185 Mon Sep 17 00:00:00 2001 From: Juan Ignacio Rios Date: Tue, 27 Jun 2023 18:03:55 +0200 Subject: [PATCH 03/27] feat(214): Finished project now accepts empty remainder buys, and so ends the funding immediately. Thought out values added to rewards function on FinishedProject instantiation based on the knowledge hub example. --- polimec-skeleton/pallets/funding/src/tests.rs | 89 ++++++++++++++++--- 1 file changed, 79 insertions(+), 10 deletions(-) diff --git a/polimec-skeleton/pallets/funding/src/tests.rs b/polimec-skeleton/pallets/funding/src/tests.rs index e9b3b6574..0dac07bd4 100644 --- a/polimec-skeleton/pallets/funding/src/tests.rs +++ b/polimec-skeleton/pallets/funding/src/tests.rs @@ -1003,6 +1003,10 @@ impl<'a> FinishedProject<'a> { community_contributions.clone(), ); + if remainder_contributions.is_empty() { + return remainder_funding_project.end_funding(); + } + let project_id = remainder_funding_project.get_project_id(); let ct_price = remainder_funding_project .get_project_details() @@ -1180,6 +1184,7 @@ mod defaults { pub mod helper_functions { use super::*; use std::collections::BTreeMap; + use sp_arithmetic::traits::Zero; pub fn get_ed() -> BalanceOf { ::ExistentialDeposit::get() @@ -1424,6 +1429,23 @@ pub mod helper_functions { } output } + + pub fn calculate_price_from_test_bids(bids: TestBids) -> PriceOf{ + // temp variable to store the total value of the bids (i.e price * amount) + let mut bid_usd_value_sum = BalanceOf::::zero(); + + for bid in bids.iter() { + let ticket_size = bid.price.checked_mul_int(bid.amount).unwrap(); + bid_usd_value_sum.saturating_accrue(ticket_size); + } + + bids.into_iter().map(|bid| { + let bid_weight = as FixedPointNumber>::saturating_from_rational( + bid.price.saturating_mul_int(bid.amount), bid_usd_value_sum + ); + bid.price * bid_weight + }).reduce(|a, b| a.saturating_add(b)).unwrap() + } } #[cfg(test)] @@ -1594,6 +1616,7 @@ mod creation_round_failure { #[cfg(test)] mod evaluation_round_success { + use sp_arithmetic::Percent; use super::*; #[test] @@ -1623,12 +1646,49 @@ mod evaluation_round_success { } #[test] - fn rewards_are_paid() { - const TARGET_FUNDING_AMOUNT_USD: BalanceOf = 1_000_000 * US_DOLLAR; - + fn rewards_are_paid_full_funding() { + const TARGET_FUNDING_AMOUNT_USD: BalanceOf = 2_000_000 * US_DOLLAR; + const EVALUATION_AMOUNT_1: BalanceOf = 100_000 * US_DOLLAR; + const EVALUATION_AMOUNT_2: BalanceOf = 230_000 * US_DOLLAR; + const EVALUATION_AMOUNT_3: BalanceOf = 76_000 * US_DOLLAR; let test_env = TestEnvironment::new(); - let issuer = ISSUER; + + let remainder_to_full_funding = TARGET_FUNDING_AMOUNT_USD - EVALUATION_AMOUNT_1 - EVALUATION_AMOUNT_2 - EVALUATION_AMOUNT_3; + let remaining_funding_weights = [25u8, 30u8, 31u8, 14u8]; + assert_eq!(remaining_funding_weights.iter().sum::(), 100u8, "remaining_funding_weights must sum up to 100%"); + let remaining_funding_weights = remaining_funding_weights.into_iter().map(|x| Percent::from_percent(x)).collect::>(); + + let bidder_1_ct_price = PriceOf::::from_float(4.2f64); + let bidder_2_ct_price = PriceOf::::from_float(2.3f64); + + let bidder_1_ct_amount = bidder_1_ct_price.reciprocal().unwrap().checked_mul_int(remaining_funding_weights[0] * remainder_to_full_funding).unwrap(); + let bidder_2_ct_amount = bidder_2_ct_price.reciprocal().unwrap().checked_mul_int(remaining_funding_weights[1] * remainder_to_full_funding).unwrap(); + + let final_ct_price = calculate_price_from_test_bids(vec![ + TestBid::new(BIDDER_1, bidder_1_ct_amount, bidder_1_ct_price, None, AcceptedFundingAsset::USDT), + TestBid::new(BIDDER_2, bidder_2_ct_amount, bidder_2_ct_price, None, AcceptedFundingAsset::USDT), + ]); + + let buyer_1_ct_amount = final_ct_price.reciprocal().unwrap().checked_mul_int(remaining_funding_weights[2] * remainder_to_full_funding).unwrap(); + let buyer_2_ct_amount = final_ct_price.reciprocal().unwrap().checked_mul_int(remaining_funding_weights[3] * remainder_to_full_funding).unwrap(); + + let evaluations: UserToPLMCBalance = vec![ + (EVALUATOR_1, EVALUATION_AMOUNT_1), + (EVALUATOR_2, EVALUATION_AMOUNT_2), + (EVALUATOR_3, EVALUATION_AMOUNT_3), + ]; + + let bids: TestBids = vec![ + TestBid::new(BIDDER_1, bidder_1_ct_amount, bidder_1_ct_price, None, AcceptedFundingAsset::USDT), + TestBid::new(BIDDER_2, bidder_2_ct_amount, bidder_2_ct_price, None, AcceptedFundingAsset::USDT), + ]; + + let community_contributions = vec![ + TestContribution::new(BUYER_1, buyer_1_ct_amount, None, AcceptedFundingAsset::USDT), + TestContribution::new(BUYER_2, buyer_2_ct_amount, None, AcceptedFundingAsset::USDT), + ]; + let project = ProjectMetadataOf:: { token_information: CurrencyMetadata { name: "Test Token".as_bytes().to_vec().try_into().unwrap(), @@ -1649,19 +1709,15 @@ mod evaluation_round_success { funding_destination_account: ISSUER, offchain_information_hash: Some(hashed(METADATA)), }; - let evaluations = default_evaluations(); - let bids = default_bids(); - let community_contributions = default_community_buys(); - let remainder_contributions = default_remainder_buys(); let finished_project = FinishedProject::new_with( &test_env, project, - issuer, + ISSUER, evaluations, bids, community_contributions, - remainder_contributions, + vec![], ); } @@ -3462,6 +3518,19 @@ mod test_helper_functions { ); assert_eq!(result, expected_plmc_spent); } + + #[test] + fn test_calculate_price_from_test_bids() { + let bids = vec![ + TestBid::new(100, 10_000_0_000_000_000, 15.into(), None, AcceptedFundingAsset::USDT), + TestBid::new(200, 20_000_0_000_000_000, 20.into(), None, AcceptedFundingAsset::USDT), + TestBid::new(300, 20_000_0_000_000_000, 10.into(), None, AcceptedFundingAsset::USDT), + ]; + let price = calculate_price_from_test_bids(bids); + let price_in_10_decimals = price.checked_mul_int(1_0_000_000_000_u128).unwrap(); + + assert_eq!(price_in_10_decimals, 16_3_333_333_333_u128.into()); + } } #[cfg(test)] From caa1b53651721b2bdbb9909e97dbdd05cf0a7f54 Mon Sep 17 00:00:00 2001 From: Juan Ignacio RIos Date: Mon, 3 Jul 2023 12:53:56 +0200 Subject: [PATCH 04/27] feat(214): evaluator rewards full test written. Actual implementation of the protocol pending --- polimec-skeleton/pallets/funding/src/lib.rs | 4 +- polimec-skeleton/pallets/funding/src/tests.rs | 119 +++++++++++++++--- 2 files changed, 102 insertions(+), 21 deletions(-) diff --git a/polimec-skeleton/pallets/funding/src/lib.rs b/polimec-skeleton/pallets/funding/src/lib.rs index d20dc0456..6a585139f 100644 --- a/polimec-skeleton/pallets/funding/src/lib.rs +++ b/polimec-skeleton/pallets/funding/src/lib.rs @@ -714,10 +714,10 @@ pub mod pallet { /// Bond PLMC for a project in the evaluation stage #[pallet::weight(T::WeightInfo::bond())] pub fn bond_evaluation( - origin: OriginFor, project_id: T::ProjectIdentifier, #[pallet::compact] amount: BalanceOf, + origin: OriginFor, project_id: T::ProjectIdentifier, #[pallet::compact] usd_amount: BalanceOf, ) -> DispatchResult { let evaluator = ensure_signed(origin)?; - Self::do_evaluate(evaluator, project_id, amount) + Self::do_evaluate(evaluator, project_id, usd_amount) } /// Release the bonded PLMC for an evaluator if the project assigned to it is in the EvaluationFailed phase diff --git a/polimec-skeleton/pallets/funding/src/tests.rs b/polimec-skeleton/pallets/funding/src/tests.rs index 0dac07bd4..271e7cb5f 100644 --- a/polimec-skeleton/pallets/funding/src/tests.rs +++ b/polimec-skeleton/pallets/funding/src/tests.rs @@ -1616,7 +1616,7 @@ mod creation_round_failure { #[cfg(test)] mod evaluation_round_success { - use sp_arithmetic::Percent; + use sp_arithmetic::{Perbill, Percent}; use super::*; #[test] @@ -1648,36 +1648,43 @@ mod evaluation_round_success { #[test] fn rewards_are_paid_full_funding() { const TARGET_FUNDING_AMOUNT_USD: BalanceOf = 2_000_000 * US_DOLLAR; - const EVALUATION_AMOUNT_1: BalanceOf = 100_000 * US_DOLLAR; - const EVALUATION_AMOUNT_2: BalanceOf = 230_000 * US_DOLLAR; - const EVALUATION_AMOUNT_3: BalanceOf = 76_000 * US_DOLLAR; + let evaluator_1_usd_amount: BalanceOf = Percent::from_percent(8u8) * TARGET_FUNDING_AMOUNT_USD; // Full early evaluator reward + let evaluator_2_usd_amount: BalanceOf = Percent::from_percent(3u8) * TARGET_FUNDING_AMOUNT_USD; // Partial early evaluator reward + let evaluator_3_usd_amount: BalanceOf = Percent::from_percent(4u8) * TARGET_FUNDING_AMOUNT_USD; // No early evaluator reward + + let funding_weights = [25, 30, 31, 14]; + assert_eq!(funding_weights.iter().sum::(), 100, "remaining_funding_weights must sum up to 100%"); + let funding_weights = funding_weights.into_iter().map(|x| Percent::from_percent(x)).collect::>(); + + let bidder_1_usd_amount: BalanceOf = funding_weights[0] * TARGET_FUNDING_AMOUNT_USD; + let bidder_2_usd_amount: BalanceOf = funding_weights[1] * TARGET_FUNDING_AMOUNT_USD; + + let buyer_1_usd_amount: BalanceOf = funding_weights[2] * TARGET_FUNDING_AMOUNT_USD; + let buyer_2_usd_amount: BalanceOf = funding_weights[3] * TARGET_FUNDING_AMOUNT_USD; let test_env = TestEnvironment::new(); - let remainder_to_full_funding = TARGET_FUNDING_AMOUNT_USD - EVALUATION_AMOUNT_1 - EVALUATION_AMOUNT_2 - EVALUATION_AMOUNT_3; - let remaining_funding_weights = [25u8, 30u8, 31u8, 14u8]; - assert_eq!(remaining_funding_weights.iter().sum::(), 100u8, "remaining_funding_weights must sum up to 100%"); - let remaining_funding_weights = remaining_funding_weights.into_iter().map(|x| Percent::from_percent(x)).collect::>(); + let plmc_price = test_env.in_ext(|| ::PriceProvider::get_price(PLMC_STATEMINT_ID)).unwrap(); + + let evaluations: UserToUSDBalance = vec![ + (EVALUATOR_1, evaluator_1_usd_amount), + (EVALUATOR_2, evaluator_2_usd_amount), + (EVALUATOR_3, evaluator_3_usd_amount), + ]; let bidder_1_ct_price = PriceOf::::from_float(4.2f64); let bidder_2_ct_price = PriceOf::::from_float(2.3f64); - let bidder_1_ct_amount = bidder_1_ct_price.reciprocal().unwrap().checked_mul_int(remaining_funding_weights[0] * remainder_to_full_funding).unwrap(); - let bidder_2_ct_amount = bidder_2_ct_price.reciprocal().unwrap().checked_mul_int(remaining_funding_weights[1] * remainder_to_full_funding).unwrap(); + let bidder_1_ct_amount = bidder_1_ct_price.reciprocal().unwrap().checked_mul_int(bidder_1_usd_amount).unwrap(); + let bidder_2_ct_amount = bidder_2_ct_price.reciprocal().unwrap().checked_mul_int(bidder_2_usd_amount).unwrap(); let final_ct_price = calculate_price_from_test_bids(vec![ TestBid::new(BIDDER_1, bidder_1_ct_amount, bidder_1_ct_price, None, AcceptedFundingAsset::USDT), TestBid::new(BIDDER_2, bidder_2_ct_amount, bidder_2_ct_price, None, AcceptedFundingAsset::USDT), ]); - let buyer_1_ct_amount = final_ct_price.reciprocal().unwrap().checked_mul_int(remaining_funding_weights[2] * remainder_to_full_funding).unwrap(); - let buyer_2_ct_amount = final_ct_price.reciprocal().unwrap().checked_mul_int(remaining_funding_weights[3] * remainder_to_full_funding).unwrap(); - - let evaluations: UserToPLMCBalance = vec![ - (EVALUATOR_1, EVALUATION_AMOUNT_1), - (EVALUATOR_2, EVALUATION_AMOUNT_2), - (EVALUATOR_3, EVALUATION_AMOUNT_3), - ]; + let buyer_1_ct_amount = final_ct_price.reciprocal().unwrap().checked_mul_int(buyer_1_usd_amount).unwrap(); + let buyer_2_ct_amount = final_ct_price.reciprocal().unwrap().checked_mul_int(buyer_2_usd_amount).unwrap(); let bids: TestBids = vec![ TestBid::new(BIDDER_1, bidder_1_ct_amount, bidder_1_ct_price, None, AcceptedFundingAsset::USDT), @@ -1717,9 +1724,83 @@ mod evaluation_round_success { evaluations, bids, community_contributions, - vec![], + vec![] ); + let project_id = finished_project.get_project_id(); + + let mut remaining_for_fee = TARGET_FUNDING_AMOUNT_USD; + let amount_for_10_percent = { + let sub = remaining_for_fee.checked_sub(1_000_000 * US_DOLLAR); + if let Some(sub) = sub { + remaining_for_fee = sub; + 1_000_000 * US_DOLLAR + } else { + let temp = remaining_for_fee; + remaining_for_fee = 0; + temp + } + }; + + let amount_for_8_percent = { + let sub = remaining_for_fee.checked_sub(5_000_000 * US_DOLLAR); + if let Some(sub) = sub { + remaining_for_fee = sub; + 5_000_000 * US_DOLLAR + } else { + let temp = remaining_for_fee; + remaining_for_fee = 0; + temp + } + }; + + let amount_for_6_percent = remaining_for_fee; + + let total_fee = Percent::from_percent(10u8) * amount_for_10_percent + Percent::from_percent(8u8) * amount_for_8_percent + Percent::from_percent(6u8) * amount_for_6_percent; + + // "Y" variable is 1, since the full funding amount was reached, which means the full 30% of the fee goes to evaluators + let evaluator_rewards_usd = Percent::from_percent(30) * total_fee; + let total_evaluation_locked = evaluator_1_usd_amount + evaluator_2_usd_amount + evaluator_3_usd_amount; + let early_evaluator_locked = Percent::from_percent(10) * TARGET_FUNDING_AMOUNT_USD; + + let eval_1_all_evaluator_reward_weight = Perbill::from_rational(evaluator_1_usd_amount, total_evaluation_locked); + let eval_2_all_evaluator_reward_weight = Perbill::from_rational(evaluator_2_usd_amount, total_evaluation_locked); + let eval_3_all_evaluator_reward_weight = Perbill::from_rational(evaluator_3_usd_amount, total_evaluation_locked); + + let eval_1_early_evaluator_reward_weight = Perbill::from_rational(evaluator_1_usd_amount, early_evaluator_locked); + let eval_2_early_evaluator_reward_weight = Perbill::from_rational(Perbill::from_rational(2u32, 3u32) * evaluator_2_usd_amount, early_evaluator_locked); + let eval_3_early_evaluator_reward_weight = Perbill::from_percent(0); + + let all_evaluator_rewards_pot = Percent::from_percent(80) * evaluator_rewards_usd; + let early_evaluator_rewards_pot = Percent::from_percent(20) * evaluator_rewards_usd; + + let evaluator_1_all_evaluator_reward = eval_1_all_evaluator_reward_weight * all_evaluator_rewards_pot; + let evaluator_2_all_evaluator_reward = eval_2_all_evaluator_reward_weight * all_evaluator_rewards_pot; + let evaluator_3_all_evaluator_reward = eval_3_all_evaluator_reward_weight * all_evaluator_rewards_pot; + + let evaluator_1_early_evaluator_reward = eval_1_early_evaluator_reward_weight * early_evaluator_rewards_pot; + let evaluator_2_early_evaluator_reward = eval_2_early_evaluator_reward_weight * early_evaluator_rewards_pot; + let evaluator_3_early_evaluator_reward = eval_3_early_evaluator_reward_weight * early_evaluator_rewards_pot; + + + + let actual_reward_balances = test_env.in_ext(|| { + vec![ + (EVALUATOR_1, ::ContributionTokenCurrency::balance(project_id, EVALUATOR_1)), + (EVALUATOR_2, ::ContributionTokenCurrency::balance(project_id, EVALUATOR_2)), + (EVALUATOR_3, ::ContributionTokenCurrency::balance(project_id, EVALUATOR_3)), + ] + }); + let expected_reward_balances = vec![ + (EVALUATOR_1, evaluator_1_all_evaluator_reward + evaluator_1_early_evaluator_reward), + (EVALUATOR_2, evaluator_2_all_evaluator_reward + evaluator_2_early_evaluator_reward), + (EVALUATOR_3, evaluator_2_all_evaluator_reward + evaluator_2_early_evaluator_reward), + ]; + assert_eq!(actual_reward_balances, expected_reward_balances); + + + + } } From fb34cc83a8103f04ae962011a866606aec62bd91 Mon Sep 17 00:00:00 2001 From: Juan Ignacio RIos Date: Mon, 3 Jul 2023 17:06:24 +0200 Subject: [PATCH 05/27] feat(214): bids at higher average price are taken at the average. Test not passing yet due to tiny decimal inconsistency --- polimec-skeleton/Cargo.lock | 28 +- polimec-skeleton/pallets/funding/Cargo.toml | 2 + .../pallets/funding/src/benchmarking.rs | 4 +- .../pallets/funding/src/functions.rs | 318 ++++++++++++++---- polimec-skeleton/pallets/funding/src/lib.rs | 9 +- polimec-skeleton/pallets/funding/src/mock.rs | 11 + polimec-skeleton/pallets/funding/src/tests.rs | 299 ++++++++++++---- polimec-skeleton/pallets/funding/src/types.rs | 25 +- 8 files changed, 554 insertions(+), 142 deletions(-) diff --git a/polimec-skeleton/Cargo.lock b/polimec-skeleton/Cargo.lock index ba8c24a02..c4a10060a 100644 --- a/polimec-skeleton/Cargo.lock +++ b/polimec-skeleton/Cargo.lock @@ -1255,7 +1255,7 @@ dependencies = [ "cranelift-codegen", "cranelift-entity", "cranelift-frontend", - "itertools", + "itertools 0.10.5", "log", "smallvec", "wasmparser", @@ -2800,7 +2800,7 @@ dependencies = [ "frame-system", "gethostname", "handlebars", - "itertools", + "itertools 0.10.5", "lazy_static", "linked-hash-map", "log", @@ -2949,7 +2949,7 @@ dependencies = [ "cfg-expr", "derive-syn-parse", "frame-support-procedural-tools", - "itertools", + "itertools 0.10.5", "proc-macro-warning", "proc-macro2", "quote", @@ -3806,6 +3806,15 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.6" @@ -5456,7 +5465,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2871aadd82a2c216ee68a69837a526dfe788ecbe74c4c5038a6acdbff6653066" dependencies = [ "expander 0.0.6", - "itertools", + "itertools 0.10.5", "petgraph", "proc-macro-crate", "proc-macro2", @@ -5890,6 +5899,7 @@ dependencies = [ "frame-benchmarking", "frame-support", "frame-system", + "itertools 0.11.0", "pallet-assets", "pallet-balances", "pallet-credentials", @@ -7717,7 +7727,7 @@ dependencies = [ "fatality", "futures", "futures-channel", - "itertools", + "itertools 0.10.5", "kvdb", "lru 0.9.0", "parity-db", @@ -8272,7 +8282,7 @@ checksum = "59230a63c37f3e18569bdb90e4a89cbf5bf8b06fea0b84e65ea10cc4df47addd" dependencies = [ "difflib", "float-cmp", - "itertools", + "itertools 0.10.5", "normalize-line-endings", "predicates-core", "regex", @@ -8442,7 +8452,7 @@ checksum = "119533552c9a7ffacc21e099c24a0ac8bb19c2a2a3f363de84cd9b844feab270" dependencies = [ "bytes", "heck", - "itertools", + "itertools 0.10.5", "lazy_static", "log", "multimap", @@ -8476,7 +8486,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5d2d8d10f3c6ded6da8b05b5fb3b8a5082514344d56c9f871412d29b4e075b4" dependencies = [ "anyhow", - "itertools", + "itertools 0.10.5", "proc-macro2", "quote", "syn 1.0.109", @@ -8729,7 +8739,7 @@ checksum = "3bd8f48b2066e9f69ab192797d66da804d1935bf22763204ed3675740cb0f221" dependencies = [ "derive_more", "fs-err", - "itertools", + "itertools 0.10.5", "static_init 0.5.2", "thiserror", ] diff --git a/polimec-skeleton/pallets/funding/Cargo.toml b/polimec-skeleton/pallets/funding/Cargo.toml index 6bbaabf88..eeb314891 100644 --- a/polimec-skeleton/pallets/funding/Cargo.toml +++ b/polimec-skeleton/pallets/funding/Cargo.toml @@ -17,6 +17,7 @@ parity-scale-codec = { version = "3.0.0", features = [ scale-info = { version = "2.5.0", default-features = false, features = [ "derive", ] } +itertools = { version = "0.11.0", default-features = false } # Substrate dependencies frame-benchmarking = { git = "https://github.com/paritytech/substrate", default-features = false, optional = true, branch = "polkadot-v0.9.42" } @@ -40,6 +41,7 @@ pallet-credentials = { path = "../credentials", default-features = false } [features] default = ["std"] std = [ + "itertools/use_std", "parity-scale-codec/std", "scale-info/std", "sp-std/std", diff --git a/polimec-skeleton/pallets/funding/src/benchmarking.rs b/polimec-skeleton/pallets/funding/src/benchmarking.rs index c105ac662..7757afd77 100644 --- a/polimec-skeleton/pallets/funding/src/benchmarking.rs +++ b/polimec-skeleton/pallets/funding/src/benchmarking.rs @@ -192,8 +192,8 @@ benchmarks! { verify { let project_auctions = Bids::::get(project_id, bidder_1); assert_eq!(project_auctions.len(), 1); - assert_eq!(project_auctions[0].ct_amount, 10_000_u64.into()); - assert_eq!(project_auctions[0].ct_usd_price, T::Price::saturating_from_integer(15)); + assert_eq!(project_auctions[0].original_ct_amount, 10_000_u64.into()); + assert_eq!(project_auctions[0].original_ct_usd_price, T::Price::saturating_from_integer(15)); let events = PolimecSystem::::events(); assert!(events.iter().any(|r| { let expected_event: ::RuntimeEvent = Event::::Bid { diff --git a/polimec-skeleton/pallets/funding/src/functions.rs b/polimec-skeleton/pallets/funding/src/functions.rs index b291a13f0..7e3b8ffc6 100644 --- a/polimec-skeleton/pallets/funding/src/functions.rs +++ b/polimec-skeleton/pallets/funding/src/functions.rs @@ -32,11 +32,17 @@ use frame_support::{ Get, }, }; +use sp_arithmetic::Perbill; -use sp_arithmetic::traits::Zero; +use sp_arithmetic::traits::{CheckedSub, Zero}; use sp_runtime::Percent; use sp_std::prelude::*; +use itertools::{Itertools}; + +pub const US_DOLLAR: u128 = 1_0_000_000_000; +pub const US_CENT: u128 = 0_0_100_000_000; + // Round transition functions impl Pallet { /// Called by user extrinsic @@ -100,6 +106,7 @@ impl Pallet { remainder: BlockNumberPair::new(None, None), }, remaining_contribution_tokens: initial_metadata.total_allocation_size, + funding_amount_reached: BalanceOf::::zero(), }; let project_metadata = initial_metadata; @@ -729,9 +736,11 @@ impl Pallet { let project_details = ProjectsDetails::::get(project_id).ok_or(Error::::ProjectInfoNotFound)?; let now = >::block_number(); let evaluation_id = Self::next_evaluation_id(); - let mut existing_evaluations = Evaluations::::get(project_id, evaluator.clone()); - + let mut caller_existing_evaluations = Evaluations::::get(project_id, evaluator.clone()); let plmc_usd_price = T::PriceProvider::get_price(PLMC_STATEMINT_ID).ok_or(Error::::PLMCPriceNotAvailable)?; + let early_evaluation_reward_threshold_usd = + T::EarlyEvaluationThreshold::get() * project_details.fundraising_target; + let all_existing_evaluations = Evaluations::::iter_prefix(project_id); // * Validity Checks * ensure!( @@ -750,26 +759,48 @@ impl Pallet { .checked_mul_int(usd_amount) .ok_or(Error::::BadMath)?; + let previous_total_evaluation_bonded_usd = all_existing_evaluations + .map(|(evaluator, evaluations)| { + evaluations.iter().fold(BalanceOf::::zero(), |acc, evaluation| { + acc.saturating_add(evaluation.early_usd_amount) + .saturating_add(evaluation.late_usd_amount) + }) + }) + .fold(BalanceOf::::zero(), |acc, evaluation| acc.saturating_add(evaluation)); + + let remaining_bond_to_reach_threshold = early_evaluation_reward_threshold_usd + .checked_sub(&previous_total_evaluation_bonded_usd) + .unwrap_or(BalanceOf::::zero()); + + let early_usd_amount = if usd_amount <= remaining_bond_to_reach_threshold { + usd_amount + } else { + remaining_bond_to_reach_threshold + }; + + let late_usd_amount = usd_amount.checked_sub(&early_usd_amount).ok_or(Error::::BadMath)?; + let new_evaluation = EvaluationInfoOf:: { id: evaluation_id, project_id, evaluator: evaluator.clone(), plmc_bond, - usd_amount, + early_usd_amount, + late_usd_amount, when: now, }; // * Update Storage * // TODO: PLMC-144. Unlock the PLMC when it's the right time - match existing_evaluations.try_push(new_evaluation.clone()) { + match caller_existing_evaluations.try_push(new_evaluation.clone()) { Ok(_) => { T::NativeCurrency::hold(&LockType::Evaluation(project_id), &evaluator, plmc_bond) .map_err(|_| Error::::InsufficientBalance)?; } Err(_) => { // Evaluations are stored in descending order. If the evaluation vector for the user is full, we drop the lowest/last bond - let lowest_evaluation = existing_evaluations.swap_remove(existing_evaluations.len() - 1); + let lowest_evaluation = caller_existing_evaluations.swap_remove(caller_existing_evaluations.len() - 1); ensure!( lowest_evaluation.plmc_bond < plmc_bond, @@ -788,15 +819,15 @@ impl Pallet { .map_err(|_| Error::::InsufficientBalance)?; // This should never fail since we just removed an element from the vector - existing_evaluations + caller_existing_evaluations .try_push(new_evaluation) .map_err(|_| Error::::ImpossibleState)?; } }; - existing_evaluations.sort_by_key(|bond| Reverse(bond.plmc_bond)); + caller_existing_evaluations.sort_by_key(|bond| Reverse(bond.plmc_bond)); - Evaluations::::set(project_id, evaluator.clone(), existing_evaluations); + Evaluations::::set(project_id, evaluator.clone(), caller_existing_evaluations); NextEvaluationId::::set(evaluation_id.saturating_add(One::one())); // * Emit events * @@ -919,10 +950,13 @@ impl Pallet { project_id, bidder: bidder.clone(), status: BidStatus::YetUnknown, - ct_amount, - ct_usd_price, + original_ct_amount: ct_amount, + original_ct_usd_price: ct_usd_price, + final_ct_amount: ct_amount, + final_ct_usd_price: ct_usd_price, funding_asset, - funding_asset_amount: required_funding_asset_transfer, + funding_asset_amount_locked: required_funding_asset_transfer, + multiplier, plmc_bond: required_plmc_bond, funded: false, plmc_vesting_period, @@ -947,10 +981,7 @@ impl Pallet { .ok_or(Error::::ImpossibleState)? .plmc_bond; - ensure!( - new_bid.plmc_bond > lowest_plmc_bond, - Error::::BidTooLow - ); + ensure!(new_bid.plmc_bond > lowest_plmc_bond, Error::::BidTooLow); Self::release_last_funding_item_in_vec( &bidder, @@ -958,7 +989,7 @@ impl Pallet { asset_id, &mut existing_bids, |x| x.plmc_bond, - |x| x.funding_asset_amount, + |x| x.funding_asset_amount_locked, )?; Self::try_plmc_participation_lock(&bidder, project_id, required_plmc_bond)?; @@ -1133,7 +1164,8 @@ impl Pallet { NextContributionId::::set(contribution_id.saturating_add(One::one())); ProjectsDetails::::mutate(project_id, |maybe_project| { if let Some(project) = maybe_project { - project.remaining_contribution_tokens = remaining_cts_after_purchase + project.remaining_contribution_tokens = remaining_cts_after_purchase; + project.funding_amount_reached = project.funding_amount_reached.saturating_add(ticket_size); } }); @@ -1534,61 +1566,87 @@ impl Pallet { let mut bid_token_amount_sum = BalanceOf::::zero(); // temp variable to store the total value of the bids (i.e price * amount) let mut bid_usd_value_sum = BalanceOf::::zero(); - + let project_account = Self::fund_account_id(project_id); + let plmc_price = T::PriceProvider::get_price(PLMC_STATEMINT_ID).ok_or(Error::::PLMCPriceNotAvailable)?; // sort bids by price bids.sort(); // accept only bids that were made before `end_block` i.e end of candle auction - let bids = bids + let bids: Result, DispatchError> = bids .into_iter() .map(|mut bid| { if bid.when > end_block { bid.status = BidStatus::Rejected(RejectionReason::AfterCandleEnd); // TODO: PLMC-147. Unlock funds. We can do this inside the "on_idle" hook, and change the `status` of the `Bid` to "Unreserved" - return bid; + return Ok(bid); } let buyable_amount = total_allocation_size.saturating_sub(bid_token_amount_sum); if buyable_amount == 0_u32.into() { bid.status = BidStatus::Rejected(RejectionReason::NoTokensLeft); - } else if bid.ct_amount <= buyable_amount { - let maybe_ticket_size = bid.ct_usd_price.checked_mul_int(bid.ct_amount); + } else if bid.original_ct_amount <= buyable_amount { + let maybe_ticket_size = bid.original_ct_usd_price.checked_mul_int(bid.original_ct_amount); if let Some(ticket_size) = maybe_ticket_size { - bid_token_amount_sum.saturating_accrue(bid.ct_amount); + bid_token_amount_sum.saturating_accrue(bid.original_ct_amount); bid_usd_value_sum.saturating_accrue(ticket_size); bid.status = BidStatus::Accepted; } else { bid.status = BidStatus::Rejected(RejectionReason::BadMath); - return bid; + return Ok(bid); } } else { - let maybe_ticket_size = bid.ct_usd_price.checked_mul_int(buyable_amount); + let maybe_ticket_size = bid.original_ct_usd_price.checked_mul_int(buyable_amount); if let Some(ticket_size) = maybe_ticket_size { bid_usd_value_sum.saturating_accrue(ticket_size); bid_token_amount_sum.saturating_accrue(buyable_amount); - bid.status = BidStatus::PartiallyAccepted(buyable_amount, RejectionReason::NoTokensLeft) + bid.status = BidStatus::PartiallyAccepted(buyable_amount, RejectionReason::NoTokensLeft); + bid.final_ct_amount = buyable_amount; + + let funding_asset_price = T::PriceProvider::get_price(bid.funding_asset.to_statemint_id()) + .ok_or(Error::::PriceNotFound)?; + let funding_asset_amount_needed = funding_asset_price.reciprocal().ok_or(Error::::BadMath)? + .checked_mul_int(ticket_size).ok_or(Error::::BadMath)?; + T::FundingCurrency::transfer( + bid.funding_asset.to_statemint_id(), + &project_account, + &bid.bidder, + bid.funding_asset_amount_locked.saturating_sub(funding_asset_amount_needed), + Preservation::Preserve + )?; + + let usd_bond_needed = bid.multiplier.calculate_bonding_requirement(ticket_size) + .map_err(|_| Error::::BadMath)?; + let plmc_bond_needed = plmc_price.reciprocal().ok_or(Error::::BadMath)? + .checked_mul_int(usd_bond_needed).ok_or(Error::::BadMath)?; + T::NativeCurrency::release(&LockType::Participation(project_id), &bid.bidder, bid.plmc_bond.saturating_sub(plmc_bond_needed), Precision::Exact)?; + + bid.funding_asset_amount_locked = funding_asset_amount_needed; + bid.plmc_bond = plmc_bond_needed; + } else { bid.status = BidStatus::Rejected(RejectionReason::BadMath); - return bid; + bid.final_ct_amount = 0_u32.into(); + bid.final_ct_usd_price = PriceOf::::zero(); + + T::FundingCurrency::transfer( + bid.funding_asset.to_statemint_id(), + &project_account, + &bid.bidder, + bid.funding_asset_amount_locked, + Preservation::Preserve + )?; + T::NativeCurrency::release(&LockType::Participation(project_id), &bid.bidder, bid.plmc_bond, Precision::Exact)?; + bid.funding_asset_amount_locked = BalanceOf::::zero(); + bid.plmc_bond = BalanceOf::::zero(); + + return Ok(bid); } // TODO: PLMC-147. Refund remaining amount } - bid + Ok(bid) }) - .collect::>>(); - - // Update the bid in the storage - for bid in bids.iter() { - Bids::::mutate(project_id, bid.bidder.clone(), |bids| -> Result<(), DispatchError> { - let bid_index = bids - .clone() - .into_iter() - .position(|b| b.id == bid.id) - .ok_or(Error::::ImpossibleState)?; - bids[bid_index] = bid.clone(); - Ok(()) - })?; - } + .collect(); + let bids = bids?; // Calculate the weighted price of the token for the next funding rounds, using winning bids. // for example: if there are 3 winning bids, @@ -1608,29 +1666,75 @@ impl Pallet { // lastly, sum all the weighted prices to get the final weighted price for the next funding round // 3 + 10.6 + 2.6 = 16.333... - let weighted_token_price = bids - // TODO: PLMC-150. collecting due to previous mut borrow, find a way to not collect and borrow bid on filter_map - .into_iter() - .filter_map(|bid| match bid.status { - BidStatus::Accepted => { + let weighted_token_price: PriceOf = bids + // TODO: PLMC-150. collecting due to previous mut borrow, find a way to not collect and borrow bid on filter_map + .iter() + .filter_map(|bid| match bid.status { + BidStatus::Accepted => { let bid_weight = ::saturating_from_rational( - bid.ct_usd_price.saturating_mul_int(bid.ct_amount), bid_usd_value_sum + bid.original_ct_usd_price.saturating_mul_int(bid.original_ct_amount), bid_usd_value_sum ); - let weighted_price = bid.ct_usd_price * bid_weight; - Some(weighted_price) - }, + let weighted_price = bid.original_ct_usd_price * bid_weight; + Some(weighted_price) + }, - BidStatus::PartiallyAccepted(amount, _) => { + BidStatus::PartiallyAccepted(amount, _) => { let bid_weight = ::saturating_from_rational( - bid.ct_usd_price.saturating_mul_int(amount), bid_usd_value_sum + bid.original_ct_usd_price.saturating_mul_int(amount), bid_usd_value_sum ); - Some(bid.ct_usd_price.saturating_mul(bid_weight)) - }, + Some(bid.original_ct_usd_price.saturating_mul(bid_weight)) + }, + + _ => None, + }) + .reduce(|a, b| a.saturating_add(b)) + .ok_or(Error::::NoBidsFound)?; - _ => None, - }) - .reduce(|a, b| a.saturating_add(b)) - .ok_or(Error::::NoBidsFound)?; + + // Update the bid in the storage + for bid in bids.into_iter() { + Bids::::mutate(project_id, bid.bidder.clone(), |bids| -> Result<(), DispatchError> { + let bid_index = bids + .clone() + .into_iter() + .position(|b| b.id == bid.id) + .ok_or(Error::::ImpossibleState)?; + let mut final_bid = bid; + + if final_bid.final_ct_usd_price > weighted_token_price { + final_bid.final_ct_usd_price = weighted_token_price; + let new_ticket_size = weighted_token_price.checked_mul_int(final_bid.final_ct_amount).ok_or(Error::::BadMath)?; + + let funding_asset_price = T::PriceProvider::get_price(final_bid.funding_asset.to_statemint_id()) + .ok_or(Error::::PriceNotFound)?; + let funding_asset_amount_needed = funding_asset_price.reciprocal().ok_or(Error::::BadMath)? + .checked_mul_int(new_ticket_size).ok_or(Error::::BadMath)?; + T::FundingCurrency::transfer( + final_bid.funding_asset.to_statemint_id(), + &project_account, + &final_bid.bidder, + final_bid.funding_asset_amount_locked.saturating_sub(funding_asset_amount_needed), + Preservation::Preserve + )?; + final_bid.funding_asset_amount_locked = funding_asset_amount_needed; + + let usd_bond_needed = final_bid.multiplier.calculate_bonding_requirement(new_ticket_size) + .map_err(|_| Error::::BadMath)?; + let plmc_bond_needed = plmc_price.reciprocal().ok_or(Error::::BadMath)? + .checked_mul_int(usd_bond_needed).ok_or(Error::::BadMath)?; + T::NativeCurrency::release( + &LockType::Participation(project_id), + &final_bid.bidder, + final_bid.plmc_bond.saturating_sub(plmc_bond_needed), + Precision::Exact + )?; + final_bid.plmc_bond = plmc_bond_needed; + } + + bids[bid_index] = final_bid; + Ok(()) + })?; + } // Update storage ProjectsDetails::::mutate(project_id, |maybe_info| -> Result<(), DispatchError> { @@ -1638,6 +1742,7 @@ impl Pallet { info.weighted_average_price = Some(weighted_token_price); info.remaining_contribution_tokens = info.remaining_contribution_tokens.saturating_sub(bid_token_amount_sum); + info.funding_amount_reached = info.funding_amount_reached.saturating_add(bid_usd_value_sum); Ok(()) } else { Err(Error::::ProjectNotFound.into()) @@ -1740,4 +1845,97 @@ impl Pallet { Ok(()) } + + pub fn calculate_fees(project_id: T::ProjectIdentifier) -> Result, DispatchError> { + let funding_reached = ProjectsDetails::::get(project_id) + .ok_or(Error::::ProjectNotFound)? + .funding_amount_reached; + let mut remaining_for_fee = funding_reached; + + Ok(T::FeeBrackets::get() + .into_iter() + .map(|(fee, limit)| { + let try_operation = remaining_for_fee.checked_sub(&limit); + if let Some(remaining_amount) = try_operation { + remaining_for_fee = remaining_amount; + fee * limit + } else { + let temp = remaining_for_fee; + remaining_for_fee = BalanceOf::::zero(); + fee * temp + } + }) + .fold(BalanceOf::::zero(), |acc, fee| acc.saturating_add(fee))) + } + + pub fn get_evaluator_ct_rewards( + project_id: T::ProjectIdentifier, + ) -> Result, DispatchError> { + let project_details = ProjectsDetails::::get(project_id).ok_or(Error::::ProjectNotFound)?; + let evaluation_usd_amounts = Evaluations::::iter_prefix(project_id) + .map(|(evaluator, evaluations)| { + ( + evaluator, + evaluations.into_iter().fold( + (BalanceOf::::zero(), BalanceOf::::zero()), + |acc, evaluation| { + ( + acc.0.saturating_add(evaluation.early_usd_amount), + acc.1.saturating_add(evaluation.late_usd_amount), + ) + }, + ), + ) + }) + .collect::>(); + let ct_price = project_details + .weighted_average_price + .ok_or(Error::::ImpossibleState)?; + let target_funding = project_details.fundraising_target; + let funding_reached = project_details.funding_amount_reached; + + // This is the "Y" variable from the knowledge hub + let percentage_of_target_funding = Percent::from_rational(funding_reached, target_funding); + + let fees = Self::calculate_fees(project_id)?; + let evaluator_fees = percentage_of_target_funding * (Percent::from_percent(30) * fees); + + let early_evaluator_rewards = Percent::from_percent(20) * evaluator_fees; + let all_evaluator_rewards = Percent::from_percent(80) * evaluator_fees; + + let early_evaluator_total_locked = evaluation_usd_amounts + .iter() + .fold(BalanceOf::::zero(), |acc, (_, (early, _))| { + acc.saturating_add(*early) + }); + let late_evaluator_total_locked = evaluation_usd_amounts + .iter() + .fold(BalanceOf::::zero(), |acc, (_, (_, late))| acc.saturating_add(*late)); + let all_evaluator_total_locked = early_evaluator_total_locked.saturating_add(late_evaluator_total_locked); + + let evaluator_usd_rewards = evaluation_usd_amounts + .into_iter() + .map(|(evaluator, (early, late))| { + let early_evaluator_weight = Percent::from_rational(early, early_evaluator_total_locked); + let all_evaluator_weight = Percent::from_rational(early + late, all_evaluator_total_locked); + + let early_reward = early_evaluator_weight * early_evaluator_rewards; + let all_reward = all_evaluator_weight * all_evaluator_rewards; + + (evaluator, early_reward.saturating_add(all_reward)) + }) + .collect::>(); + let ct_price_reciprocal = ct_price.reciprocal().ok_or(Error::::BadMath)?; + + evaluator_usd_rewards + .iter() + .map(|(evaluator, usd_reward)| { + if let Some(reward) = ct_price_reciprocal.checked_mul_int(*usd_reward) { + Ok((evaluator.clone(), reward)) + } else { + Err(Error::::BadMath.into()) + } + }) + .collect() + } } diff --git a/polimec-skeleton/pallets/funding/src/lib.rs b/polimec-skeleton/pallets/funding/src/lib.rs index 6a585139f..3d50ea0d3 100644 --- a/polimec-skeleton/pallets/funding/src/lib.rs +++ b/polimec-skeleton/pallets/funding/src/lib.rs @@ -227,7 +227,8 @@ pub type HashOf = ::Hash; pub type AssetIdOf = <::FundingCurrency as fungibles::Inspect<::AccountId>>::AssetId; -pub type ProjectMetadataOf = ProjectMetadata>, BalanceOf, PriceOf, AccountIdOf, HashOf>; +pub type ProjectMetadataOf = + ProjectMetadata>, BalanceOf, PriceOf, AccountIdOf, HashOf>; pub type ProjectDetailsOf = ProjectDetails, BlockNumberOf, PriceOf, BalanceOf>; pub type VestingOf = Vesting, BalanceOf>; pub type EvaluationInfoOf = @@ -241,6 +242,7 @@ pub type BidInfoOf = BidInfo< BlockNumberOf, VestingOf, VestingOf, + MultiplierOf, >; pub type ContributionInfoOf = ContributionInfo, ProjectIdOf, AccountIdOf, BalanceOf, VestingOf, VestingOf>; @@ -256,6 +258,7 @@ pub mod pallet { use frame_support::pallet_prelude::*; use frame_system::pallet_prelude::*; use local_macros::*; + use sp_arithmetic::Percent; #[pallet::pallet] pub struct Pallet(_); @@ -371,6 +374,10 @@ pub mod pallet { /// Weight information for extrinsics in this pallet. type WeightInfo: WeightInfo; + + type FeeBrackets: Get>; + + type EarlyEvaluationThreshold: Get; } #[pallet::storage] diff --git a/polimec-skeleton/pallets/funding/src/mock.rs b/polimec-skeleton/pallets/funding/src/mock.rs index adb149460..f6f85516f 100644 --- a/polimec-skeleton/pallets/funding/src/mock.rs +++ b/polimec-skeleton/pallets/funding/src/mock.rs @@ -29,6 +29,7 @@ use frame_support::{ }; use frame_system as system; use frame_system::EnsureRoot; +use sp_arithmetic::Percent; use sp_core::H256; use sp_runtime::{ testing::Header, @@ -50,6 +51,8 @@ pub const MILLI_PLMC: Balance = 10u128.pow(7); pub const MICRO_PLMC: Balance = 10u128.pow(4); pub const EXISTENTIAL_DEPOSIT: Balance = 10 * MILLI_PLMC; +const US_DOLLAR: u128 = 1_0_000_000_000u128; + // Configure a mock runtime to test the pallet. frame_support::construct_runtime!( pub enum TestRuntime where @@ -215,6 +218,12 @@ parameter_types! { (1984u32, FixedU128::from_float(0.95f64)), // USDT (2069u32, FixedU128::from_float(8.4f64)), // PLMC ]); + pub FeeBrackets: Vec<(Percent, Balance)> = vec![ + (Percent::from_percent(10), 1_000_000 * US_DOLLAR), + (Percent::from_percent(8), 5_000_000 * US_DOLLAR), + (Percent::from_percent(6), u128::MAX), // Making it max signifies the last bracket + ]; + pub EarlyEvaluationThreshold: Percent = Percent::from_percent(10); } impl pallet_funding::Config for TestRuntime { @@ -248,6 +257,8 @@ impl pallet_funding::Config for TestRuntime { #[cfg(feature = "runtime-benchmarks")] type BenchmarkHelper = (); type WeightInfo = (); + type FeeBrackets = FeeBrackets; + type EarlyEvaluationThreshold = EarlyEvaluationThreshold; } // Build genesis storage according to the mock runtime. diff --git a/polimec-skeleton/pallets/funding/src/tests.rs b/polimec-skeleton/pallets/funding/src/tests.rs index 271e7cb5f..79b7a1661 100644 --- a/polimec-skeleton/pallets/funding/src/tests.rs +++ b/polimec-skeleton/pallets/funding/src/tests.rs @@ -42,6 +42,7 @@ use crate::traits::BondingRequirementCalculation; use sp_runtime::DispatchError; use std::cell::RefCell; use std::iter::zip; +use sp_arithmetic::traits::Zero; type ProjectIdOf = ::ProjectIdentifier; type UserToPLMCBalance = Vec<(AccountId, BalanceOf)>; @@ -158,10 +159,10 @@ impl BidInfoFilterOf { if self.bidder.is_some() && self.bidder.unwrap() != bid.bidder { return false; } - if self.ct_amount.is_some() && self.ct_amount.unwrap() != bid.ct_amount { + if self.ct_amount.is_some() && self.ct_amount.unwrap() != bid.original_ct_amount { return false; } - if self.ct_usd_price.is_some() && self.ct_usd_price.unwrap() != bid.ct_usd_price { + if self.ct_usd_price.is_some() && self.ct_usd_price.unwrap() != bid.original_ct_usd_price { return false; } if self.funded.is_some() && self.funded.unwrap() != bid.funded { @@ -178,7 +179,7 @@ impl BidInfoFilterOf { return false; } if self.funding_asset_amount.is_some() - && self.funding_asset_amount.as_ref().unwrap() != &bid.funding_asset_amount + && self.funding_asset_amount.as_ref().unwrap() != &bid.funding_asset_amount_locked { return false; } @@ -187,14 +188,21 @@ impl BidInfoFilterOf { } } -const ISSUER: AccountId = 1; -const EVALUATOR_1: AccountId = 2; -const EVALUATOR_2: AccountId = 3; -const EVALUATOR_3: AccountId = 4; -const BIDDER_1: AccountId = 5; -const BIDDER_2: AccountId = 6; -const BUYER_1: AccountId = 7; -const BUYER_2: AccountId = 8; +const ISSUER: AccountId = 10; +const EVALUATOR_1: AccountId = 20; +const EVALUATOR_2: AccountId = 21; +const EVALUATOR_3: AccountId = 22; +const BIDDER_1: AccountId = 30; +const BIDDER_2: AccountId = 31; +const BIDDER_3: AccountId = 32; +const BIDDER_4: AccountId = 33; +const BUYER_1: AccountId = 40; +const BUYER_2: AccountId = 41; +const BUYER_3: AccountId = 42; +const BUYER_4: AccountId = 43; +const BUYER_5: AccountId = 44; +const BUYER_6: AccountId = 45; +const BUYER_7: AccountId = 46; const ASSET_DECIMALS: u8 = 10; const ASSET_UNIT: u128 = 10_u128.pow(ASSET_DECIMALS as u32); @@ -431,7 +439,7 @@ impl TestEnvironment { let contribution_total: ::Balance = Bids::::get(project_id, user.clone()) .iter() - .map(|c| c.funding_asset_amount) + .map(|c| c.funding_asset_amount_locked) .sum(); assert_eq!( contribution_total, expected_amount, @@ -508,6 +516,7 @@ impl<'a> CreatedProject<'a> { .checked_mul_int(expected_metadata.total_allocation_size) .unwrap(), remaining_contribution_tokens: expected_metadata.total_allocation_size, + funding_amount_reached: BalanceOf::::zero(), }; assert_eq!(metadata, expected_metadata); assert_eq!(details, expected_details); @@ -1183,8 +1192,8 @@ mod defaults { pub mod helper_functions { use super::*; - use std::collections::BTreeMap; use sp_arithmetic::traits::Zero; + use std::collections::BTreeMap; pub fn get_ed() -> BalanceOf { ::ExistentialDeposit::get() @@ -1430,7 +1439,7 @@ pub mod helper_functions { output } - pub fn calculate_price_from_test_bids(bids: TestBids) -> PriceOf{ + pub fn calculate_price_from_test_bids(bids: TestBids) -> PriceOf { // temp variable to store the total value of the bids (i.e price * amount) let mut bid_usd_value_sum = BalanceOf::::zero(); @@ -1439,12 +1448,16 @@ pub mod helper_functions { bid_usd_value_sum.saturating_accrue(ticket_size); } - bids.into_iter().map(|bid| { - let bid_weight = as FixedPointNumber>::saturating_from_rational( - bid.price.saturating_mul_int(bid.amount), bid_usd_value_sum - ); - bid.price * bid_weight - }).reduce(|a, b| a.saturating_add(b)).unwrap() + bids.into_iter() + .map(|bid| { + let bid_weight = as FixedPointNumber>::saturating_from_rational( + bid.price.saturating_mul_int(bid.amount), + bid_usd_value_sum, + ); + bid.price * bid_weight + }) + .reduce(|a, b| a.saturating_add(b)) + .unwrap() } } @@ -1616,8 +1629,8 @@ mod creation_round_failure { #[cfg(test)] mod evaluation_round_success { - use sp_arithmetic::{Perbill, Percent}; use super::*; + use sp_arithmetic::{Perbill, Percent}; #[test] fn evaluation_round_completed() { @@ -1647,14 +1660,22 @@ mod evaluation_round_success { #[test] fn rewards_are_paid_full_funding() { - const TARGET_FUNDING_AMOUNT_USD: BalanceOf = 2_000_000 * US_DOLLAR; - let evaluator_1_usd_amount: BalanceOf = Percent::from_percent(8u8) * TARGET_FUNDING_AMOUNT_USD; // Full early evaluator reward - let evaluator_2_usd_amount: BalanceOf = Percent::from_percent(3u8) * TARGET_FUNDING_AMOUNT_USD; // Partial early evaluator reward - let evaluator_3_usd_amount: BalanceOf = Percent::from_percent(4u8) * TARGET_FUNDING_AMOUNT_USD; // No early evaluator reward + // numbers taken fron knowledge hub + const TARGET_FUNDING_AMOUNT_USD: BalanceOf = 1_000_000 * US_DOLLAR; + let evaluator_1_usd_amount: BalanceOf = 75_000 * US_DOLLAR; // Full early evaluator reward + let evaluator_2_usd_amount: BalanceOf = 65_000 * US_DOLLAR; // Partial early evaluator reward + let evaluator_3_usd_amount: BalanceOf = 60_000 * US_DOLLAR; // No early evaluator reward let funding_weights = [25, 30, 31, 14]; - assert_eq!(funding_weights.iter().sum::(), 100, "remaining_funding_weights must sum up to 100%"); - let funding_weights = funding_weights.into_iter().map(|x| Percent::from_percent(x)).collect::>(); + assert_eq!( + funding_weights.iter().sum::(), + 100, + "remaining_funding_weights must sum up to 100%" + ); + let funding_weights = funding_weights + .into_iter() + .map(|x| Percent::from_percent(x)) + .collect::>(); let bidder_1_usd_amount: BalanceOf = funding_weights[0] * TARGET_FUNDING_AMOUNT_USD; let bidder_2_usd_amount: BalanceOf = funding_weights[1] * TARGET_FUNDING_AMOUNT_USD; @@ -1664,7 +1685,9 @@ mod evaluation_round_success { let test_env = TestEnvironment::new(); - let plmc_price = test_env.in_ext(|| ::PriceProvider::get_price(PLMC_STATEMINT_ID)).unwrap(); + let plmc_price = test_env + .in_ext(|| ::PriceProvider::get_price(PLMC_STATEMINT_ID)) + .unwrap(); let evaluations: UserToUSDBalance = vec![ (EVALUATOR_1, evaluator_1_usd_amount), @@ -1675,20 +1698,60 @@ mod evaluation_round_success { let bidder_1_ct_price = PriceOf::::from_float(4.2f64); let bidder_2_ct_price = PriceOf::::from_float(2.3f64); - let bidder_1_ct_amount = bidder_1_ct_price.reciprocal().unwrap().checked_mul_int(bidder_1_usd_amount).unwrap(); - let bidder_2_ct_amount = bidder_2_ct_price.reciprocal().unwrap().checked_mul_int(bidder_2_usd_amount).unwrap(); + let bidder_1_ct_amount = bidder_1_ct_price + .reciprocal() + .unwrap() + .checked_mul_int(bidder_1_usd_amount) + .unwrap(); + let bidder_2_ct_amount = bidder_2_ct_price + .reciprocal() + .unwrap() + .checked_mul_int(bidder_2_usd_amount) + .unwrap(); let final_ct_price = calculate_price_from_test_bids(vec![ - TestBid::new(BIDDER_1, bidder_1_ct_amount, bidder_1_ct_price, None, AcceptedFundingAsset::USDT), - TestBid::new(BIDDER_2, bidder_2_ct_amount, bidder_2_ct_price, None, AcceptedFundingAsset::USDT), + TestBid::new( + BIDDER_1, + bidder_1_ct_amount, + bidder_1_ct_price, + None, + AcceptedFundingAsset::USDT, + ), + TestBid::new( + BIDDER_2, + bidder_2_ct_amount, + bidder_2_ct_price, + None, + AcceptedFundingAsset::USDT, + ), ]); - let buyer_1_ct_amount = final_ct_price.reciprocal().unwrap().checked_mul_int(buyer_1_usd_amount).unwrap(); - let buyer_2_ct_amount = final_ct_price.reciprocal().unwrap().checked_mul_int(buyer_2_usd_amount).unwrap(); + let buyer_1_ct_amount = final_ct_price + .reciprocal() + .unwrap() + .checked_mul_int(buyer_1_usd_amount) + .unwrap(); + let buyer_2_ct_amount = final_ct_price + .reciprocal() + .unwrap() + .checked_mul_int(buyer_2_usd_amount) + .unwrap(); let bids: TestBids = vec![ - TestBid::new(BIDDER_1, bidder_1_ct_amount, bidder_1_ct_price, None, AcceptedFundingAsset::USDT), - TestBid::new(BIDDER_2, bidder_2_ct_amount, bidder_2_ct_price, None, AcceptedFundingAsset::USDT), + TestBid::new( + BIDDER_1, + bidder_1_ct_amount, + bidder_1_ct_price, + None, + AcceptedFundingAsset::USDT, + ), + TestBid::new( + BIDDER_2, + bidder_2_ct_amount, + bidder_2_ct_price, + None, + AcceptedFundingAsset::USDT, + ), ]; let community_contributions = vec![ @@ -1705,7 +1768,10 @@ mod evaluation_round_success { mainnet_token_max_supply: 10_000_000 * ASSET_UNIT, total_allocation_size: 1_000_000 * ASSET_UNIT, minimum_price: 1u128.into(), - ticket_size: TicketSize::> { minimum: Some(1), maximum: None }, + ticket_size: TicketSize::> { + minimum: Some(1), + maximum: None, + }, participants_size: ParticipantsSize { minimum: Some(2), maximum: None, @@ -1724,12 +1790,13 @@ mod evaluation_round_success { evaluations, bids, community_contributions, - vec![] + vec![], ); let project_id = finished_project.get_project_id(); let mut remaining_for_fee = TARGET_FUNDING_AMOUNT_USD; + let amount_for_10_percent = { let sub = remaining_for_fee.checked_sub(1_000_000 * US_DOLLAR); if let Some(sub) = sub { @@ -1756,19 +1823,28 @@ mod evaluation_round_success { let amount_for_6_percent = remaining_for_fee; - let total_fee = Percent::from_percent(10u8) * amount_for_10_percent + Percent::from_percent(8u8) * amount_for_8_percent + Percent::from_percent(6u8) * amount_for_6_percent; + let total_fee = Percent::from_percent(10u8) * amount_for_10_percent + + Percent::from_percent(8u8) * amount_for_8_percent + + Percent::from_percent(6u8) * amount_for_6_percent; // "Y" variable is 1, since the full funding amount was reached, which means the full 30% of the fee goes to evaluators let evaluator_rewards_usd = Percent::from_percent(30) * total_fee; let total_evaluation_locked = evaluator_1_usd_amount + evaluator_2_usd_amount + evaluator_3_usd_amount; let early_evaluator_locked = Percent::from_percent(10) * TARGET_FUNDING_AMOUNT_USD; - let eval_1_all_evaluator_reward_weight = Perbill::from_rational(evaluator_1_usd_amount, total_evaluation_locked); - let eval_2_all_evaluator_reward_weight = Perbill::from_rational(evaluator_2_usd_amount, total_evaluation_locked); - let eval_3_all_evaluator_reward_weight = Perbill::from_rational(evaluator_3_usd_amount, total_evaluation_locked); - - let eval_1_early_evaluator_reward_weight = Perbill::from_rational(evaluator_1_usd_amount, early_evaluator_locked); - let eval_2_early_evaluator_reward_weight = Perbill::from_rational(Perbill::from_rational(2u32, 3u32) * evaluator_2_usd_amount, early_evaluator_locked); + let eval_1_all_evaluator_reward_weight = + Perbill::from_rational(evaluator_1_usd_amount, total_evaluation_locked); + let eval_2_all_evaluator_reward_weight = + Perbill::from_rational(evaluator_2_usd_amount, total_evaluation_locked); + let eval_3_all_evaluator_reward_weight = + Perbill::from_rational(evaluator_3_usd_amount, total_evaluation_locked); + + let eval_1_early_evaluator_reward_weight = + Perbill::from_rational(evaluator_1_usd_amount, early_evaluator_locked); + let eval_2_early_evaluator_reward_weight = Perbill::from_rational( + Perbill::from_rational(2u32, 3u32) * evaluator_2_usd_amount, + early_evaluator_locked, + ); let eval_3_early_evaluator_reward_weight = Perbill::from_percent(0); let all_evaluator_rewards_pot = Percent::from_percent(80) * evaluator_rewards_usd; @@ -1782,25 +1858,28 @@ mod evaluation_round_success { let evaluator_2_early_evaluator_reward = eval_2_early_evaluator_reward_weight * early_evaluator_rewards_pot; let evaluator_3_early_evaluator_reward = eval_3_early_evaluator_reward_weight * early_evaluator_rewards_pot; - - let actual_reward_balances = test_env.in_ext(|| { vec![ - (EVALUATOR_1, ::ContributionTokenCurrency::balance(project_id, EVALUATOR_1)), - (EVALUATOR_2, ::ContributionTokenCurrency::balance(project_id, EVALUATOR_2)), - (EVALUATOR_3, ::ContributionTokenCurrency::balance(project_id, EVALUATOR_3)), + ( + EVALUATOR_1, + ::ContributionTokenCurrency::balance(project_id, EVALUATOR_1), + ), + ( + EVALUATOR_2, + ::ContributionTokenCurrency::balance(project_id, EVALUATOR_2), + ), + ( + EVALUATOR_3, + ::ContributionTokenCurrency::balance(project_id, EVALUATOR_3), + ), ] }); let expected_reward_balances = vec![ - (EVALUATOR_1, evaluator_1_all_evaluator_reward + evaluator_1_early_evaluator_reward), - (EVALUATOR_2, evaluator_2_all_evaluator_reward + evaluator_2_early_evaluator_reward), - (EVALUATOR_3, evaluator_2_all_evaluator_reward + evaluator_2_early_evaluator_reward), + (EVALUATOR_1, 1_236_9_500_000_000), + (EVALUATOR_2, 852_8_100_000_000), + (EVALUATOR_3, 660_2_400_000_000), ]; assert_eq!(actual_reward_balances, expected_reward_balances); - - - - } } @@ -2016,7 +2095,7 @@ mod auction_round_success { } #[test] - fn price_calculation() { + fn price_calculation_1() { // 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 @@ -2070,6 +2149,43 @@ mod auction_round_success { assert_eq!(price_in_12_decimals, 16_333_333_333_333_u128); } + #[test] + fn price_calculation_2() { + // From the knowledge hub + let test_env = TestEnvironment::new(); + let mut project_metadata = default_project(test_env.get_new_nonce()); + let auctioning_project = + AuctioningProject::new_with(&test_env, project_metadata, ISSUER, default_evaluations()); + let bids = vec![ + TestBid::new(BIDDER_1, 10_000 * ASSET_UNIT, 15.into(), None, AcceptedFundingAsset::USDT), + TestBid::new(BIDDER_2, 20_000 * ASSET_UNIT, 20.into(), None, AcceptedFundingAsset::USDT), + TestBid::new(BIDDER_3, 20_000 * ASSET_UNIT, 16.into(), None, AcceptedFundingAsset::USDT), + ]; + + let statemint_funding = calculate_auction_funding_asset_spent(bids.clone()); + let plmc_funding = calculate_auction_plmc_spent(bids.clone()); + let ed_funding = plmc_funding + .clone() + .into_iter() + .map(|(account, _amount)| (account, get_ed())) + .collect::(); + + test_env.mint_plmc_to(ed_funding); + test_env.mint_plmc_to(plmc_funding); + test_env.mint_statemint_asset_to(statemint_funding); + + auctioning_project.bid_for_users(bids).unwrap(); + + let community_funding_project = auctioning_project.start_community_funding(); + let token_price = community_funding_project + .get_project_details() + .weighted_average_price + .unwrap(); + + let price_in_10_decimals = token_price.checked_mul_int(1_0_000_000_000_u128).unwrap(); + assert_eq!(price_in_10_decimals, 17_6_666_666_666); + } + #[test] fn only_candle_bids_before_random_block_get_included() { let test_env = TestEnvironment::new(); @@ -2292,6 +2408,25 @@ mod auction_round_success { let _community_funding_project = CommunityFundingProject::new_with(&test_env, project, issuer, evaluations, bids); } + + #[test] + fn bids_at_higher_price_than_weighted_average_use_average() { + let test_env = TestEnvironment::new(); + let issuer = ISSUER; + let mut project = default_project(test_env.get_new_nonce()); + let evaluations = default_evaluations(); + let bids: TestBids = vec![ + TestBid::new(BIDDER_1, 10_000 * ASSET_UNIT, 15.into(), None, AcceptedFundingAsset::USDT), + TestBid::new(BIDDER_2, 20_000 * ASSET_UNIT, 20.into(), None, AcceptedFundingAsset::USDT), + TestBid::new(BIDDER_4, 20_000 * ASSET_UNIT, 16.into(), None, AcceptedFundingAsset::USDT), + ]; + + let community_funding_project = + CommunityFundingProject::new_with(&test_env, project, issuer, evaluations, bids); + let project_id = community_funding_project.project_id; + let bidder_2_bid = test_env.in_ext(|| Bids::::get(project_id, BIDDER_2))[0]; + assert_eq!(bidder_2_bid.final_ct_usd_price, PriceOf::::from_rational(88, 5)); + } } #[cfg(test)] @@ -2406,10 +2541,10 @@ mod auction_round_failure { test_env.ext_env.borrow_mut().execute_with(|| { let stored_bids = FundingModule::bids(project_id, DAVE); assert_eq!(stored_bids.len(), 4); - assert_eq!(stored_bids[0].ct_usd_price, 5_u128.into()); - assert_eq!(stored_bids[1].ct_usd_price, 8_u128.into()); - assert_eq!(stored_bids[2].ct_usd_price, 5_u128.into()); - assert_eq!(stored_bids[3].ct_usd_price, 2_u128.into()); + assert_eq!(stored_bids[0].original_ct_usd_price, 5_u128.into()); + assert_eq!(stored_bids[1].original_ct_usd_price, 8_u128.into()); + assert_eq!(stored_bids[2].original_ct_usd_price, 5_u128.into()); + assert_eq!(stored_bids[3].original_ct_usd_price, 2_u128.into()); }); } @@ -3645,6 +3780,46 @@ mod misc_features { } #[test] + fn get_evaluator_ct_rewards_works() { + let test_env = TestEnvironment::new(); + + // all values taken from the knowledge hub + let evaluations: UserToUSDBalance = vec![ + (EVALUATOR_1, 75_000 * US_DOLLAR), + (EVALUATOR_2, 65_000 * US_DOLLAR), + (EVALUATOR_3, 60_000 * US_DOLLAR), + ]; + + let bids: TestBids = vec![ + TestBid::new(BIDDER_1, 10_000 * ASSET_UNIT, 15.into(), None, AcceptedFundingAsset::USDT), + TestBid::new(BIDDER_2, 20_000 * ASSET_UNIT, 20.into(), None, AcceptedFundingAsset::USDT), + TestBid::new(BIDDER_4, 20_000 * ASSET_UNIT, 16.into(), None, AcceptedFundingAsset::USDT), + ]; + + let contributions: TestContributions = vec![ + TestContribution::new(BUYER_1, 4_000 * ASSET_UNIT, None, AcceptedFundingAsset::USDT), + TestContribution::new(BUYER_2, 2_000 * ASSET_UNIT, None, AcceptedFundingAsset::USDT), + TestContribution::new(BUYER_3, 2_000 * ASSET_UNIT, None, AcceptedFundingAsset::USDT), + TestContribution::new(BUYER_4, 5_000 * ASSET_UNIT, None, AcceptedFundingAsset::USDT), + TestContribution::new(BUYER_5, 30_000 * ASSET_UNIT, None, AcceptedFundingAsset::USDT), + TestContribution::new(BUYER_6, 5_000 * ASSET_UNIT, None, AcceptedFundingAsset::USDT), + TestContribution::new(BUYER_7, 2_000 * ASSET_UNIT, None, AcceptedFundingAsset::USDT), + ]; + + let project = + RemainderFundingProject::new_with(&test_env, default_project(0), ISSUER, evaluations, bids, contributions); + let details = project.get_project_details(); + let mut ct_evaluation_rewards = test_env.in_ext(|| FundingModule::get_evaluator_ct_rewards(project.get_project_id()).unwrap()); + ct_evaluation_rewards.sort_by_key(|item| item.0); + let expected_ct_rewards = vec![ + (EVALUATOR_1, 1_236_9_500_000_000), + (EVALUATOR_2, 852_8_100_000_000), + (EVALUATOR_3, 660_2_400_000_000), + ]; + + assert_eq!(ct_evaluation_rewards, expected_ct_rewards); + } + fn sandbox() { // let plmc_price_in_usd = 8_5_000_000_000_u128; // let token_amount= FixedU128::from_float(12.5); diff --git a/polimec-skeleton/pallets/funding/src/types.rs b/polimec-skeleton/pallets/funding/src/types.rs index acb754b7c..2a64d0a4e 100644 --- a/polimec-skeleton/pallets/funding/src/types.rs +++ b/polimec-skeleton/pallets/funding/src/types.rs @@ -130,6 +130,8 @@ pub mod storage_types { pub fundraising_target: Balance, /// The amount of Contribution Tokens that have not yet been sold pub remaining_contribution_tokens: Balance, + /// Funding reached amount in USD equivalent + pub funding_amount_reached: Balance, } /// Tells on_initialize what to do with the project @@ -149,7 +151,8 @@ pub mod storage_types { pub project_id: ProjectId, pub evaluator: AccountId, pub plmc_bond: Balance, - pub usd_amount: Balance, + pub early_usd_amount: Balance, + pub late_usd_amount: Balance, pub when: BlockNumber, } @@ -163,16 +166,20 @@ pub mod storage_types { BlockNumber, PlmcVesting, CTVesting, + Multiplier > { pub id: Id, pub project_id: ProjectId, pub bidder: AccountId, pub status: BidStatus, #[codec(compact)] - pub ct_amount: Balance, - pub ct_usd_price: Price, + pub original_ct_amount: Balance, + pub original_ct_usd_price: Price, + pub final_ct_amount: Balance, + pub final_ct_usd_price: Price, pub funding_asset: AcceptedFundingAsset, - pub funding_asset_amount: Balance, + pub funding_asset_amount_locked: Balance, + pub multiplier: Multiplier, pub plmc_bond: Balance, // TODO: PLMC-159. Not used yet, but will be used to check if the bid is funded after XCM is implemented pub funded: bool, @@ -190,11 +197,12 @@ pub mod storage_types { BlockNumber: Eq, PlmcVesting: Eq, CTVesting: Eq, - > Ord for BidInfo + Multiplier: Eq, + > Ord for BidInfo { fn cmp(&self, other: &Self) -> sp_std::cmp::Ordering { - let self_ticket_size = self.ct_usd_price.saturating_mul_int(self.ct_amount); - let other_ticket_size = other.ct_usd_price.saturating_mul_int(other.ct_amount); + let self_ticket_size = self.original_ct_usd_price.saturating_mul_int(self.original_ct_amount); + let other_ticket_size = other.original_ct_usd_price.saturating_mul_int(other.original_ct_amount); self_ticket_size.cmp(&other_ticket_size) } } @@ -208,7 +216,8 @@ pub mod storage_types { BlockNumber: Eq, PlmcVesting: Eq, CTVesting: Eq, - > PartialOrd for BidInfo + Multiplier: Eq, + > PartialOrd for BidInfo { fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) From a0e74aa5924f2f83af063e89e782a8046f7ec261 Mon Sep 17 00:00:00 2001 From: Juan Ignacio RIos Date: Tue, 4 Jul 2023 11:36:45 +0200 Subject: [PATCH 06/27] wip(214): tests failing --- .../pallets/funding/src/functions.rs | 20 +++++++++++++++---- polimec-skeleton/pallets/funding/src/lib.rs | 2 ++ polimec-skeleton/pallets/funding/src/mock.rs | 2 +- polimec-skeleton/pallets/funding/src/tests.rs | 18 +++++++++++++++-- polimec-skeleton/runtime/src/lib.rs | 18 +++++++++++------ 5 files changed, 47 insertions(+), 13 deletions(-) diff --git a/polimec-skeleton/pallets/funding/src/functions.rs b/polimec-skeleton/pallets/funding/src/functions.rs index 7e3b8ffc6..9419f968e 100644 --- a/polimec-skeleton/pallets/funding/src/functions.rs +++ b/polimec-skeleton/pallets/funding/src/functions.rs @@ -32,6 +32,7 @@ use frame_support::{ Get, }, }; +use frame_support::traits::fungibles::Inspect; use sp_arithmetic::Perbill; use sp_arithmetic::traits::{CheckedSub, Zero}; @@ -1702,6 +1703,7 @@ impl Pallet { let mut final_bid = bid; if final_bid.final_ct_usd_price > weighted_token_price { + final_bid.final_ct_usd_price = weighted_token_price; let new_ticket_size = weighted_token_price.checked_mul_int(final_bid.final_ct_amount).ok_or(Error::::BadMath)?; @@ -1709,25 +1711,35 @@ impl Pallet { .ok_or(Error::::PriceNotFound)?; let funding_asset_amount_needed = funding_asset_price.reciprocal().ok_or(Error::::BadMath)? .checked_mul_int(new_ticket_size).ok_or(Error::::BadMath)?; - T::FundingCurrency::transfer( + + let try_transfer = T::FundingCurrency::transfer( final_bid.funding_asset.to_statemint_id(), &project_account, &final_bid.bidder, final_bid.funding_asset_amount_locked.saturating_sub(funding_asset_amount_needed), Preservation::Preserve - )?; + ); + if let Err(e) = try_transfer { + Self::deposit_event(Event::::TransferError { error: e }); + } + final_bid.funding_asset_amount_locked = funding_asset_amount_needed; let usd_bond_needed = final_bid.multiplier.calculate_bonding_requirement(new_ticket_size) .map_err(|_| Error::::BadMath)?; let plmc_bond_needed = plmc_price.reciprocal().ok_or(Error::::BadMath)? .checked_mul_int(usd_bond_needed).ok_or(Error::::BadMath)?; - T::NativeCurrency::release( + + let try_release = T::NativeCurrency::release( &LockType::Participation(project_id), &final_bid.bidder, final_bid.plmc_bond.saturating_sub(plmc_bond_needed), Precision::Exact - )?; + ); + if let Err(e) = try_release { + Self::deposit_event(Event::::TransferError { error: e }); + } + final_bid.plmc_bond = plmc_bond_needed; } diff --git a/polimec-skeleton/pallets/funding/src/lib.rs b/polimec-skeleton/pallets/funding/src/lib.rs index 3d50ea0d3..ce7074cc5 100644 --- a/polimec-skeleton/pallets/funding/src/lib.rs +++ b/polimec-skeleton/pallets/funding/src/lib.rs @@ -547,6 +547,8 @@ pub mod pallet { contributor: AccountIdOf, amount: BalanceOf, }, + /// A transfer of tokens failed, but because it was done inside on_initialize it cannot be solved. + TransferError { error: DispatchError }, } #[pallet::error] diff --git a/polimec-skeleton/pallets/funding/src/mock.rs b/polimec-skeleton/pallets/funding/src/mock.rs index f6f85516f..7392a534c 100644 --- a/polimec-skeleton/pallets/funding/src/mock.rs +++ b/polimec-skeleton/pallets/funding/src/mock.rs @@ -286,7 +286,7 @@ pub fn new_test_ext() -> sp_io::TestExternalities { AcceptedFundingAsset::USDT.to_statemint_id(), ::PalletId::get().into_account_truncating(), false, - 10, + 1, )], metadata: vec![], accounts: vec![], diff --git a/polimec-skeleton/pallets/funding/src/tests.rs b/polimec-skeleton/pallets/funding/src/tests.rs index 79b7a1661..e2f482e8b 100644 --- a/polimec-skeleton/pallets/funding/src/tests.rs +++ b/polimec-skeleton/pallets/funding/src/tests.rs @@ -401,6 +401,7 @@ impl TestEnvironment { >::on_idle(System::block_number(), Weight::MAX); System::set_block_number(System::block_number() + 1); >::on_initialize(System::block_number()); + panic_if_on_initialize_failed(System::events()); } }); } @@ -1194,6 +1195,7 @@ pub mod helper_functions { use super::*; use sp_arithmetic::traits::Zero; use std::collections::BTreeMap; + use sp_core::H256; pub fn get_ed() -> BalanceOf { ::ExistentialDeposit::get() @@ -1459,6 +1461,16 @@ pub mod helper_functions { .reduce(|a, b| a.saturating_add(b)) .unwrap() } + + pub fn panic_if_on_initialize_failed(events: Vec>){ + let last_event = events.into_iter().last().expect("No events found for this action."); + match last_event { + frame_system::EventRecord { event: RuntimeEvent::FundingModule(Event::TransitionError { project_id, error }), .. } => { + panic!("Project {} transition failed in on_initialize: {:?}", project_id, error); + } + _ => {} + } + } } #[cfg(test)] @@ -2187,7 +2199,7 @@ mod auction_round_success { } #[test] - fn only_candle_bids_before_random_block_get_included() { + fn conly_candle_bids_before_random_block_get_included() { let test_env = TestEnvironment::new(); let issuer = ISSUER; let project = default_project(test_env.get_new_nonce()); @@ -2255,6 +2267,8 @@ mod auction_round_success { } test_env.advance_time(candle_end_block - test_env.current_block() + 1); + let details = auctioning_project.get_project_details(); + let now = test_env.current_block(); let random_end = auctioning_project .get_project_details() .phase_transition_points @@ -2425,7 +2439,7 @@ mod auction_round_success { CommunityFundingProject::new_with(&test_env, project, issuer, evaluations, bids); let project_id = community_funding_project.project_id; let bidder_2_bid = test_env.in_ext(|| Bids::::get(project_id, BIDDER_2))[0]; - assert_eq!(bidder_2_bid.final_ct_usd_price, PriceOf::::from_rational(88, 5)); + assert_eq!(bidder_2_bid.final_ct_usd_price.checked_mul_int(US_DOLLAR).unwrap(), 17_6_666_666_666); } } diff --git a/polimec-skeleton/runtime/src/lib.rs b/polimec-skeleton/runtime/src/lib.rs index 2bec43029..cffe29730 100644 --- a/polimec-skeleton/runtime/src/lib.rs +++ b/polimec-skeleton/runtime/src/lib.rs @@ -27,12 +27,7 @@ use smallvec::smallvec; use sp_api::impl_runtime_apis; pub use sp_consensus_aura::sr25519::AuthorityId as AuraId; use sp_core::{crypto::KeyTypeId, OpaqueMetadata}; -use sp_runtime::{ - create_runtime_str, generic, impl_opaque_keys, - traits::{AccountIdLookup, BlakeTwo256, Block as BlockT}, - transaction_validity::{TransactionSource, TransactionValidity}, - ApplyExtrinsicResult, FixedU128, -}; +use sp_runtime::{create_runtime_str, generic, impl_opaque_keys, traits::{AccountIdLookup, BlakeTwo256, Block as BlockT}, transaction_validity::{TransactionSource, TransactionValidity}, ApplyExtrinsicResult, FixedU128, Percent}; pub use sp_runtime::{MultiAddress, Perbill, Permill}; use sp_std::collections::btree_map::BTreeMap; use xcm_config::{RelayLocation, XcmConfig, XcmOriginToTransactDispatchOrigin}; @@ -161,6 +156,9 @@ pub const VERSION: RuntimeVersion = RuntimeVersion { state_version: 1, }; +const US_DOLLAR: u128 = 1_0_000_000_000u128; + + /// This determines the average expected block time that we are targeting. /// Blocks will be produced at a minimum duration defined by `SLOT_DURATION`. /// `SLOT_DURATION` is picked up by `pallet_timestamp` which is in turn picked @@ -560,6 +558,12 @@ parameter_types! { (1984u32, FixedU128::from_rational(95, 100)), // USDT (2069u32, FixedU128::from_rational(840, 100)), // PLMC ]); + pub FeeBrackets: Vec<(Percent, Balance)> = vec![ + (Percent::from_percent(10), 1_000_000 * US_DOLLAR), + (Percent::from_percent(8), 5_000_000 * US_DOLLAR), + (Percent::from_percent(6), u128::MAX), // Making it max signifies the last bracket + ]; + pub EarlyEvaluationThreshold: Percent = Percent::from_percent(10); } impl pallet_funding::Config for Runtime { @@ -593,6 +597,8 @@ impl pallet_funding::Config for Runtime { #[cfg(feature = "runtime-benchmarks")] type BenchmarkHelper = (); type WeightInfo = (); + type FeeBrackets = FeeBrackets; + type EarlyEvaluationThreshold = EarlyEvaluationThreshold; } impl pallet_credentials::Config for Runtime { From 4384b1efe7ce8f0a736ef426a0ed5c9199b8061e Mon Sep 17 00:00:00 2001 From: Juan Ignacio RIos Date: Wed, 5 Jul 2023 11:10:13 +0200 Subject: [PATCH 07/27] wip(214) --- .../pallets/funding/src/functions.rs | 20 +++-- polimec-skeleton/pallets/funding/src/lib.rs | 8 +- polimec-skeleton/pallets/funding/src/mock.rs | 2 +- polimec-skeleton/pallets/funding/src/tests.rs | 80 ++++++++++++------- polimec-skeleton/pallets/funding/src/types.rs | 3 +- 5 files changed, 74 insertions(+), 39 deletions(-) diff --git a/polimec-skeleton/pallets/funding/src/functions.rs b/polimec-skeleton/pallets/funding/src/functions.rs index 9419f968e..3ed053592 100644 --- a/polimec-skeleton/pallets/funding/src/functions.rs +++ b/polimec-skeleton/pallets/funding/src/functions.rs @@ -499,7 +499,15 @@ impl Pallet { let community_end_block = now + T::CommunityFundingDuration::get(); // * Update Storage * - Self::calculate_weighted_average_price(project_id, end_block, project_details.fundraising_target)?; + let calculation_result = Self::calculate_weighted_average_price(project_id, end_block, project_details.fundraising_target); + match calculation_result { + Err(pallet_error) if pallet_error == Error::::NoBidsFound.into() => { + let x = 10; + Ok(()) + }, + e @ Err(_) => {e}, + _ => {Ok(())} + }?; // Get info again after updating it with new price. let mut project_details = ProjectsDetails::::get(project_id).ok_or(Error::::ProjectInfoNotFound)?; project_details.phase_transition_points.random_candle_ending = Some(end_block); @@ -622,11 +630,11 @@ impl Pallet { if let Some(end_block) = remainder_end_block { ensure!(now > end_block, Error::::TooEarlyForFundingEnd); } else { - ensure!(remaining_cts == 0u32.into(), Error::::TooEarlyForFundingEnd); + ensure!(remaining_cts == 0u32.into() || project_details.status == ProjectStatus::EvaluationFailed, Error::::TooEarlyForFundingEnd); } // * Calculate new variables * - project_details.status = ProjectStatus::FundingEnded; + project_details.status = ProjectStatus::FundingSuccessful; ProjectsDetails::::insert(project_id, project_details.clone()); // * Update Storage * @@ -671,7 +679,7 @@ impl Pallet { // * Validity checks * ensure!( - project_details.status == ProjectStatus::FundingEnded, + project_details.status == ProjectStatus::FundingSuccessful, Error::::ProjectNotInFundingEndedRound ); @@ -1337,7 +1345,7 @@ impl Pallet { // Error::::NotAuthorized // ); ensure!( - project_details.status == ProjectStatus::FundingEnded, + project_details.status == ProjectStatus::FundingSuccessful, Error::::CannotClaimYet ); // TODO: PLMC-160. Check the flow of the final_price if the final price discovery during the Auction Round fails @@ -1421,7 +1429,7 @@ impl Pallet { // Error::::NotAuthorized // ); ensure!( - project_details.status == ProjectStatus::FundingEnded, + project_details.status == ProjectStatus::FundingSuccessful, Error::::CannotClaimYet ); // TODO: PLMC-160. Check the flow of the final_price if the final price discovery during the Auction Round fails diff --git a/polimec-skeleton/pallets/funding/src/lib.rs b/polimec-skeleton/pallets/funding/src/lib.rs index ce7074cc5..559636292 100644 --- a/polimec-skeleton/pallets/funding/src/lib.rs +++ b/polimec-skeleton/pallets/funding/src/lib.rs @@ -54,10 +54,10 @@ //! | Community Funding Start | After the [`Config::CandleAuctionDuration`] has passed, the auction automatically. A final token price for the next rounds is calculated based on the accepted bids. | [`CommunityRound`](ProjectStatus::CommunityRound) | //! | Funding Submissions | Retail investors can call the [`contribute()`](Pallet::contribute) extrinsic to buy tokens at the set price. | [`CommunityRound`](ProjectStatus::CommunityRound) | //! | Remainder Funding Start | After the [`Config::CommunityFundingDuration`] has passed, the project is now open to token purchases from any user type | [`RemainderRound`](ProjectStatus::RemainderRound) | -//! | Funding End | If all tokens were sold, or after the [`Config::RemainderFundingDuration`] has passed, the project automatically ends, and it is calculated if it reached its desired funding or not. | [`FundingEnded`](ProjectStatus::FundingEnded) | -//! | Evaluator Rewards | If the funding was successful, evaluators can claim their contribution token rewards with the [`TBD`]() extrinsic. If it failed, evaluators can either call the [`failed_evaluation_unbond_for()`](Pallet::failed_evaluation_unbond_for) extrinsic, or wait for the [`on_idle()`](Pallet::on_initialize) function, to return their funds | [`FundingEnded`](ProjectStatus::FundingEnded) | -//! | Bidder Rewards | If the funding was successful, bidders will call [`vested_contribution_token_bid_mint_for()`](Pallet::vested_contribution_token_bid_mint_for) to mint the contribution tokens they are owed, and [`vested_plmc_bid_unbond_for()`](Pallet::vested_plmc_bid_unbond_for) to unbond their PLMC, based on their current vesting schedule. | [`FundingEnded`](ProjectStatus::FundingEnded) | -//! | Buyer Rewards | If the funding was successful, users who bought tokens on the Community or Remainder round, can call [`vested_contribution_token_purchase_mint_for()`](Pallet::vested_contribution_token_purchase_mint_for) to mint the contribution tokens they are owed, and [`vested_plmc_purchase_unbond_for()`](Pallet::vested_plmc_purchase_unbond_for) to unbond their PLMC, based on their current vesting schedule | [`FundingEnded`](ProjectStatus::FundingEnded) | +//! | Funding End | If all tokens were sold, or after the [`Config::RemainderFundingDuration`] has passed, the project automatically ends, and it is calculated if it reached its desired funding or not. | [`FundingEnded`](ProjectStatus::FundingSuccessful) | +//! | Evaluator Rewards | If the funding was successful, evaluators can claim their contribution token rewards with the [`TBD`]() extrinsic. If it failed, evaluators can either call the [`failed_evaluation_unbond_for()`](Pallet::failed_evaluation_unbond_for) extrinsic, or wait for the [`on_idle()`](Pallet::on_initialize) function, to return their funds | [`FundingEnded`](ProjectStatus::FundingSuccessful) | +//! | Bidder Rewards | If the funding was successful, bidders will call [`vested_contribution_token_bid_mint_for()`](Pallet::vested_contribution_token_bid_mint_for) to mint the contribution tokens they are owed, and [`vested_plmc_bid_unbond_for()`](Pallet::vested_plmc_bid_unbond_for) to unbond their PLMC, based on their current vesting schedule. | [`FundingEnded`](ProjectStatus::FundingSuccessful) | +//! | Buyer Rewards | If the funding was successful, users who bought tokens on the Community or Remainder round, can call [`vested_contribution_token_purchase_mint_for()`](Pallet::vested_contribution_token_purchase_mint_for) to mint the contribution tokens they are owed, and [`vested_plmc_purchase_unbond_for()`](Pallet::vested_plmc_purchase_unbond_for) to unbond their PLMC, based on their current vesting schedule | [`FundingEnded`](ProjectStatus::FundingSuccessful) | //! //! ## Interface //! All users who wish to participate need to have a valid credential, given to them on the KILT parachain, by a KYC/AML provider. diff --git a/polimec-skeleton/pallets/funding/src/mock.rs b/polimec-skeleton/pallets/funding/src/mock.rs index 7392a534c..f6f85516f 100644 --- a/polimec-skeleton/pallets/funding/src/mock.rs +++ b/polimec-skeleton/pallets/funding/src/mock.rs @@ -286,7 +286,7 @@ pub fn new_test_ext() -> sp_io::TestExternalities { AcceptedFundingAsset::USDT.to_statemint_id(), ::PalletId::get().into_account_truncating(), false, - 1, + 10, )], metadata: vec![], accounts: vec![], diff --git a/polimec-skeleton/pallets/funding/src/tests.rs b/polimec-skeleton/pallets/funding/src/tests.rs index e2f482e8b..5138cf084 100644 --- a/polimec-skeleton/pallets/funding/src/tests.rs +++ b/polimec-skeleton/pallets/funding/src/tests.rs @@ -394,16 +394,21 @@ impl TestEnvironment { fn current_block(&self) -> BlockNumber { self.ext_env.borrow_mut().execute_with(|| System::block_number()) } - fn advance_time(&self, amount: BlockNumber) { + fn advance_time(&self, amount: BlockNumber) -> Result<(), DispatchError>{ self.ext_env.borrow_mut().execute_with(|| { for _block in 0..amount { >::on_finalize(System::block_number()); >::on_idle(System::block_number(), Weight::MAX); System::set_block_number(System::block_number() + 1); + let pre_events = System::events(); >::on_initialize(System::block_number()); - panic_if_on_initialize_failed(System::events()); + let post_events = System::events(); + if post_events.len() > pre_events.len() { + err_if_on_initialize_failed(System::events())?; + } } - }); + Ok(()) + }) } fn do_free_plmc_assertions(&self, correct_funds: UserToPLMCBalance) { for (user, balance) in correct_funds { @@ -600,7 +605,7 @@ impl<'a> EvaluatingProject<'a> { let evaluation_end = project_details.phase_transition_points.evaluation.end().unwrap(); let auction_start = evaluation_end.saturating_add(2); let blocks_to_start = auction_start.saturating_sub(self.test_env.current_block()); - self.test_env.advance_time(blocks_to_start); + self.test_env.advance_time(blocks_to_start).unwrap(); }; assert_eq!( @@ -706,7 +711,7 @@ impl<'a> AuctioningProject<'a> { let candle_start = english_end + 2; self.test_env - .advance_time(candle_start.saturating_sub(self.test_env.current_block())); + .advance_time(candle_start.saturating_sub(self.test_env.current_block())).unwrap(); let candle_end = self .get_project_details() .phase_transition_points @@ -717,7 +722,7 @@ impl<'a> AuctioningProject<'a> { let community_start = candle_end + 2; self.test_env - .advance_time(community_start.saturating_sub(self.test_env.current_block())); + .advance_time(community_start.saturating_sub(self.test_env.current_block())).unwrap(); assert_eq!(self.get_project_details().status, ProjectStatus::CommunityRound); @@ -858,7 +863,7 @@ impl<'a> CommunityFundingProject<'a> { .expect("Community funding end point should exist"); let remainder_start = community_funding_end + 1; self.test_env - .advance_time(remainder_start.saturating_sub(self.test_env.current_block())); + .advance_time(remainder_start.saturating_sub(self.test_env.current_block())).unwrap(); assert_eq!(self.get_project_details().status, ProjectStatus::RemainderRound); RemainderFundingProject { test_env: self.test_env, @@ -970,8 +975,8 @@ impl<'a> RemainderFundingProject<'a> { .expect("Should have remainder end"); let finish_block = remainder_funding_end + 1; self.test_env - .advance_time(finish_block.saturating_sub(self.test_env.current_block())); - assert_eq!(self.get_project_details().status, ProjectStatus::FundingEnded); + .advance_time(finish_block.saturating_sub(self.test_env.current_block())).unwrap(); + assert_eq!(self.get_project_details().status, ProjectStatus::FundingSuccessful); FinishedProject { test_env: self.test_env, @@ -1471,6 +1476,16 @@ pub mod helper_functions { _ => {} } } + + pub fn err_if_on_initialize_failed(events: Vec>) -> Result<(), DispatchError>{ + let last_event = events.into_iter().last().expect("No events found for this action."); + match last_event { + frame_system::EventRecord { event: RuntimeEvent::FundingModule(Event::TransitionError { project_id, error }), .. } => { + Err(error) + } + _ => {Ok(())} + } + } } #[cfg(test)] @@ -1934,7 +1949,7 @@ mod evaluation_round_failure { test_env.do_free_plmc_assertions(plmc_existential_deposits); test_env.do_reserved_plmc_assertions(plmc_eval_deposits, LockType::Evaluation(project_id)); - test_env.advance_time(evaluation_end - now + 1); + test_env.advance_time(evaluation_end - now + 1).unwrap(); assert_eq!( evaluating_project.get_project_details().status, @@ -1942,7 +1957,7 @@ mod evaluation_round_failure { ); // Check that on_idle has unlocked the failed bonds - test_env.advance_time(10); + test_env.advance_time(10).unwrap(); test_env.do_free_plmc_assertions(expected_evaluator_balances); } @@ -2199,7 +2214,7 @@ mod auction_round_success { } #[test] - fn conly_candle_bids_before_random_block_get_included() { + fn only_candle_bids_before_random_block_get_included() { let test_env = TestEnvironment::new(); let issuer = ISSUER; let project = default_project(test_env.get_new_nonce()); @@ -2213,7 +2228,7 @@ mod auction_round_success { .expect("Auction start point should exist"); // The block following the end of the english auction, is used to transition the project into candle auction. // We move past that transition, into the start of the candle auction. - test_env.advance_time(english_end_block - test_env.current_block() + 1); + test_env.advance_time(english_end_block - test_env.current_block() + 1).unwrap(); assert_eq!( auctioning_project.get_project_details().status, ProjectStatus::AuctionRound(AuctionPhase::Candle) @@ -2263,9 +2278,9 @@ mod auction_round_success { bids_made.push(bids[0]); bidding_account += 1; - test_env.advance_time(1); + test_env.advance_time(1).unwrap(); } - test_env.advance_time(candle_end_block - test_env.current_block() + 1); + test_env.advance_time(candle_end_block - test_env.current_block() + 1).unwrap(); let details = auctioning_project.get_project_details(); let now = test_env.current_block(); @@ -2333,12 +2348,12 @@ mod auction_round_success { test_env.mint_plmc_to(required_plmc); test_env.mint_plmc_to(ed_plmc); project.bond_for_users(evaluations).unwrap(); - test_env.advance_time(::EvaluationDuration::get() + 1); + test_env.advance_time(::EvaluationDuration::get() + 1).unwrap(); assert_eq!( project.get_project_details().status, ProjectStatus::AuctionInitializePeriod ); - test_env.advance_time(::AuctionInitializePeriodDuration::get() + 2); + test_env.advance_time(::AuctionInitializePeriodDuration::get() + 2).unwrap(); assert_eq!( project.get_project_details().status, ProjectStatus::AuctionRound(AuctionPhase::English) @@ -2359,12 +2374,12 @@ mod auction_round_success { test_env.mint_plmc_to(required_plmc); test_env.mint_plmc_to(ed_plmc); project.bond_for_users(evaluations).unwrap(); - test_env.advance_time(::EvaluationDuration::get() + 1); + test_env.advance_time(::EvaluationDuration::get() + 1).unwrap(); assert_eq!( project.get_project_details().status, ProjectStatus::AuctionInitializePeriod ); - test_env.advance_time(1); + test_env.advance_time(1).unwrap(); test_env .in_ext(|| FundingModule::start_auction(RuntimeOrigin::signed(ISSUER), project.get_project_id())) @@ -2389,12 +2404,12 @@ mod auction_round_success { test_env.mint_plmc_to(required_plmc); test_env.mint_plmc_to(ed_plmc); project.bond_for_users(evaluations).unwrap(); - test_env.advance_time(::EvaluationDuration::get() + 1); + test_env.advance_time(::EvaluationDuration::get() + 1).unwrap(); assert_eq!( project.get_project_details().status, ProjectStatus::AuctionInitializePeriod ); - test_env.advance_time(1); + test_env.advance_time(1).unwrap(); for account in 6000..6010 { test_env.in_ext(|| { @@ -2575,6 +2590,17 @@ mod auction_round_failure { let outcome = auctioning_project.bid_for_users(bids); frame_support::assert_err!(outcome, Error::::FundingAssetNotAccepted); } + + #[test] + fn no_bids_made() { + let test_env = TestEnvironment::new(); + let issuer = ISSUER; + let project = default_project(test_env.get_new_nonce()); + let evaluations = default_evaluations(); + let bids: TestBids = vec![]; + let _community_funding_project = + CommunityFundingProject::new_with(&test_env, project, issuer, evaluations, bids); + } } #[cfg(test)] @@ -2667,7 +2693,7 @@ mod community_round_success { community_funding_project .buy_for_retail_users(vec![contributions[0]]) .expect("The Buyer should be able to buy multiple times"); - test_env.advance_time((1 * HOURS) as BlockNumber); + test_env.advance_time((1 * HOURS) as BlockNumber).unwrap(); community_funding_project .buy_for_retail_users(vec![contributions[1]]) @@ -2728,7 +2754,7 @@ mod community_round_success { community_funding_project .buy_for_retail_users(contributions) .expect("The Buyer should be able to buy the exact amount of remaining CTs"); - test_env.advance_time(2u64); + test_env.advance_time(2u64).unwrap(); // Check remaining CTs is 0 assert_eq!( community_funding_project @@ -2741,7 +2767,7 @@ mod community_round_success { // Check project is in FundingEnded state assert_eq!( community_funding_project.get_project_details().status, - ProjectStatus::FundingEnded + ProjectStatus::FundingSuccessful ); test_env.do_free_plmc_assertions(vec![plmc_fundings[1].clone()]); @@ -2793,7 +2819,7 @@ mod community_round_success { community_funding_project .buy_for_retail_users(contributions) .expect("The Buyer should be able to buy the exact amount of remaining CTs"); - test_env.advance_time(2u64); + test_env.advance_time(2u64).unwrap(); // Check remaining CTs is 0 assert_eq!( @@ -2807,7 +2833,7 @@ mod community_round_success { // Check project is in FundingEnded state assert_eq!( community_funding_project.get_project_details().status, - ProjectStatus::FundingEnded + ProjectStatus::FundingSuccessful ); let reserved_plmc = plmc_fundings.swap_remove(0).1; @@ -3777,7 +3803,7 @@ mod misc_features { FundingModule::add_to_update_store(now + 20u64, (&69u32, RemainderFundingStart)); FundingModule::add_to_update_store(now + 5u64, (&404u32, RemainderFundingStart)); }); - test_env.advance_time(2u64); + test_env.advance_time(2u64).unwrap(); test_env.ext_env.borrow_mut().execute_with(|| { let stored = ProjectsToUpdate::::iter_values().collect::>(); assert_eq!(stored.len(), 3, "There should be 3 blocks scheduled for updating"); diff --git a/polimec-skeleton/pallets/funding/src/types.rs b/polimec-skeleton/pallets/funding/src/types.rs index 2a64d0a4e..ab998dffe 100644 --- a/polimec-skeleton/pallets/funding/src/types.rs +++ b/polimec-skeleton/pallets/funding/src/types.rs @@ -339,7 +339,8 @@ pub mod inner_types { AuctionRound(AuctionPhase), CommunityRound, RemainderRound, - FundingEnded, + FundingSuccessful, + FundingFailed, ReadyToLaunch, } From aebd8d6d6bb004d54876963568e494df7aa905c2 Mon Sep 17 00:00:00 2001 From: Juan Ignacio Rios Date: Wed, 5 Jul 2023 14:11:20 +0200 Subject: [PATCH 08/27] feat(214): cleanup field to keep track of success or failure remaining operations inside on_idle --- .../pallets/funding/src/functions.rs | 209 ++++++++++++------ polimec-skeleton/pallets/funding/src/lib.rs | 4 +- polimec-skeleton/pallets/funding/src/tests.rs | 163 +++++++++++--- polimec-skeleton/pallets/funding/src/types.rs | 85 ++++++- polimec-skeleton/runtime/src/lib.rs | 8 +- 5 files changed, 363 insertions(+), 106 deletions(-) diff --git a/polimec-skeleton/pallets/funding/src/functions.rs b/polimec-skeleton/pallets/funding/src/functions.rs index 3ed053592..f5cbbf054 100644 --- a/polimec-skeleton/pallets/funding/src/functions.rs +++ b/polimec-skeleton/pallets/funding/src/functions.rs @@ -22,6 +22,7 @@ use super::*; use crate::traits::{BondingRequirementCalculation, ProvideStatemintPrice}; use frame_support::traits::fungible::InspectHold; +use frame_support::traits::fungibles::Inspect; use frame_support::traits::tokens::{Precision, Preservation}; use frame_support::{ ensure, @@ -32,14 +33,13 @@ use frame_support::{ Get, }, }; -use frame_support::traits::fungibles::Inspect; use sp_arithmetic::Perbill; use sp_arithmetic::traits::{CheckedSub, Zero}; use sp_runtime::Percent; use sp_std::prelude::*; -use itertools::{Itertools}; +use itertools::Itertools; pub const US_DOLLAR: u128 = 1_0_000_000_000; pub const US_CENT: u128 = 0_0_100_000_000; @@ -499,33 +499,44 @@ impl Pallet { let community_end_block = now + T::CommunityFundingDuration::get(); // * Update Storage * - let calculation_result = Self::calculate_weighted_average_price(project_id, end_block, project_details.fundraising_target); - match calculation_result { + let calculation_result = + Self::calculate_weighted_average_price(project_id, end_block, project_details.fundraising_target); + let mut project_details = ProjectsDetails::::get(project_id).ok_or(Error::::ProjectInfoNotFound)?; + match calculation_result { Err(pallet_error) if pallet_error == Error::::NoBidsFound.into() => { - let x = 10; + project_details.status = ProjectStatus::AuctionFailed; + ProjectsDetails::::insert(project_id, project_details); + Self::add_to_update_store( + >::block_number() + 1u32.into(), + (&project_id, UpdateType::FundingEnd), + ); + + // * Emit events * + Self::deposit_event(Event::::AuctionFailed { project_id }); + Ok(()) - }, - e @ Err(_) => {e}, - _ => {Ok(())} - }?; - // Get info again after updating it with new price. - let mut project_details = ProjectsDetails::::get(project_id).ok_or(Error::::ProjectInfoNotFound)?; - project_details.phase_transition_points.random_candle_ending = Some(end_block); - project_details - .phase_transition_points - .community - .update(Some(community_start_block), Some(community_end_block.clone())); - project_details.status = ProjectStatus::CommunityRound; - ProjectsDetails::::insert(project_id, project_details); - // Schedule for automatic transition by `on_initialize` - Self::add_to_update_store( - community_end_block + 1u32.into(), - (&project_id, UpdateType::RemainderFundingStart), - ); + } + e @ Err(_) => e, + Some(()) => { + // Get info again after updating it with new price. + project_details.phase_transition_points.random_candle_ending = Some(end_block); + project_details + .phase_transition_points + .community + .update(Some(community_start_block), Some(community_end_block.clone())); + project_details.status = ProjectStatus::CommunityRound; + ProjectsDetails::::insert(project_id, project_details); + Self::add_to_update_store( + community_end_block + 1u32.into(), + (&project_id, UpdateType::RemainderFundingStart), + ); - // * Emit events * - Self::deposit_event(Event::::CommunityFundingStarted { project_id }); - Ok(()) + // * Emit events * + Self::deposit_event(Event::::CommunityFundingStarted { project_id }); + + Ok(()) + } + } } /// Called automatically by on_initialize @@ -630,30 +641,66 @@ impl Pallet { if let Some(end_block) = remainder_end_block { ensure!(now > end_block, Error::::TooEarlyForFundingEnd); } else { - ensure!(remaining_cts == 0u32.into() || project_details.status == ProjectStatus::EvaluationFailed, Error::::TooEarlyForFundingEnd); + ensure!( + remaining_cts == 0u32.into() + || project_details.status == ProjectStatus::EvaluationFailed + || project_details.status == ProjectStatus::AuctionFailed, + Error::::TooEarlyForFundingEnd + ); } // * Calculate new variables * - project_details.status = ProjectStatus::FundingSuccessful; - ProjectsDetails::::insert(project_id, project_details.clone()); + let funding_target = project_metadata + .minimum_price + .checked_mul_int(project_metadata.minimum_price) + .ok_or(Error::::BadMath)?; + let funding_reached = project_details.funding_amount_reached; + let funding_is_successful = !(project_details.status == ProjectStatus::EvaluationFailed + || project_details.status == ProjectStatus::AuctionFailed + || funding_reached < funding_target); + + if funding_is_successful { + project_details.status = ProjectStatus::FundingSuccessful; + project_details.cleanup = ProjectCleanup::Ready(RemainingOperations::Success(SuccessRemainingOperations::default())); + + // * Update Storage * + ProjectsDetails::::insert(project_id, project_details.clone()); + T::ContributionTokenCurrency::create(project_id, project_details.issuer.clone(), false, 1_u32.into()) + .map_err(|_| Error::::AssetCreationFailed)?; + T::ContributionTokenCurrency::set( + project_id, + &project_details.issuer, + token_information.name.into(), + token_information.symbol.into(), + token_information.decimals, + ) + .map_err(|_| Error::::AssetMetadataUpdateFailed)?; - // * Update Storage * - // Create the "Contribution Token" as an asset using the pallet_assets and set its metadata - T::ContributionTokenCurrency::create(project_id, project_details.issuer.clone(), false, 1_u32.into()) - .map_err(|_| Error::::AssetCreationFailed)?; - // Update the CT metadata - T::ContributionTokenCurrency::set( - project_id, - &project_details.issuer, - token_information.name.into(), - token_information.symbol.into(), - token_information.decimals, - ) - .map_err(|_| Error::::AssetMetadataUpdateFailed)?; + // * Emit events * + let success_reason = match remaining_cts { + 0u32 => SuccessReason::SoldOut, + _ => SuccessReason::ReachedTarget, + }; + Self::deposit_event(Event::::FundingEnded { project_id, outcome: FundingOutcome::Success(success_reason) }); + Ok(()) - // * Emit events * - Self::deposit_event(Event::FundingEnded { project_id }); - Ok(()) + } else { + project_details.status = ProjectStatus::FundingFailed; + project_details.cleanup = ProjectCleanup::Ready(RemainingOperations::Failure(FailureRemainingOperations::default())); + + // * Update Storage * + ProjectsDetails::::insert(project_id, project_details.clone()); + + // * Emit events * + let failure_reason = match project_details.status { + ProjectStatus::AuctionFailed => FailureReason::AuctionFailed, + ProjectStatus::EvaluationFailed => FailureReason::EvaluationFailed, + _ if funding_reached < funding_target => FailureReason::TargetNotReached, + _ => FailureReason::Unknown, + }; + Self::deposit_event(Event::::FundingEnded { project_id, outcome: FundingOutcome::Failure(failure_reason) }); + Ok(()) + } } /// Called manually by a user extrinsic @@ -1611,25 +1658,38 @@ impl Pallet { let funding_asset_price = T::PriceProvider::get_price(bid.funding_asset.to_statemint_id()) .ok_or(Error::::PriceNotFound)?; - let funding_asset_amount_needed = funding_asset_price.reciprocal().ok_or(Error::::BadMath)? - .checked_mul_int(ticket_size).ok_or(Error::::BadMath)?; + let funding_asset_amount_needed = funding_asset_price + .reciprocal() + .ok_or(Error::::BadMath)? + .checked_mul_int(ticket_size) + .ok_or(Error::::BadMath)?; T::FundingCurrency::transfer( bid.funding_asset.to_statemint_id(), &project_account, &bid.bidder, - bid.funding_asset_amount_locked.saturating_sub(funding_asset_amount_needed), - Preservation::Preserve + bid.funding_asset_amount_locked + .saturating_sub(funding_asset_amount_needed), + Preservation::Preserve, )?; - let usd_bond_needed = bid.multiplier.calculate_bonding_requirement(ticket_size) + let usd_bond_needed = bid + .multiplier + .calculate_bonding_requirement(ticket_size) .map_err(|_| Error::::BadMath)?; - let plmc_bond_needed = plmc_price.reciprocal().ok_or(Error::::BadMath)? - .checked_mul_int(usd_bond_needed).ok_or(Error::::BadMath)?; - T::NativeCurrency::release(&LockType::Participation(project_id), &bid.bidder, bid.plmc_bond.saturating_sub(plmc_bond_needed), Precision::Exact)?; + let plmc_bond_needed = plmc_price + .reciprocal() + .ok_or(Error::::BadMath)? + .checked_mul_int(usd_bond_needed) + .ok_or(Error::::BadMath)?; + T::NativeCurrency::release( + &LockType::Participation(project_id), + &bid.bidder, + bid.plmc_bond.saturating_sub(plmc_bond_needed), + Precision::Exact, + )?; bid.funding_asset_amount_locked = funding_asset_amount_needed; bid.plmc_bond = plmc_bond_needed; - } else { bid.status = BidStatus::Rejected(RejectionReason::BadMath); bid.final_ct_amount = 0_u32.into(); @@ -1640,9 +1700,14 @@ impl Pallet { &project_account, &bid.bidder, bid.funding_asset_amount_locked, - Preservation::Preserve + Preservation::Preserve, + )?; + T::NativeCurrency::release( + &LockType::Participation(project_id), + &bid.bidder, + bid.plmc_bond, + Precision::Exact, )?; - T::NativeCurrency::release(&LockType::Participation(project_id), &bid.bidder, bid.plmc_bond, Precision::Exact)?; bid.funding_asset_amount_locked = BalanceOf::::zero(); bid.plmc_bond = BalanceOf::::zero(); @@ -1699,7 +1764,6 @@ impl Pallet { .reduce(|a, b| a.saturating_add(b)) .ok_or(Error::::NoBidsFound)?; - // Update the bid in the storage for bid in bids.into_iter() { Bids::::mutate(project_id, bid.bidder.clone(), |bids| -> Result<(), DispatchError> { @@ -1711,21 +1775,27 @@ impl Pallet { let mut final_bid = bid; if final_bid.final_ct_usd_price > weighted_token_price { - final_bid.final_ct_usd_price = weighted_token_price; - let new_ticket_size = weighted_token_price.checked_mul_int(final_bid.final_ct_amount).ok_or(Error::::BadMath)?; + let new_ticket_size = weighted_token_price + .checked_mul_int(final_bid.final_ct_amount) + .ok_or(Error::::BadMath)?; let funding_asset_price = T::PriceProvider::get_price(final_bid.funding_asset.to_statemint_id()) .ok_or(Error::::PriceNotFound)?; - let funding_asset_amount_needed = funding_asset_price.reciprocal().ok_or(Error::::BadMath)? - .checked_mul_int(new_ticket_size).ok_or(Error::::BadMath)?; + let funding_asset_amount_needed = funding_asset_price + .reciprocal() + .ok_or(Error::::BadMath)? + .checked_mul_int(new_ticket_size) + .ok_or(Error::::BadMath)?; let try_transfer = T::FundingCurrency::transfer( final_bid.funding_asset.to_statemint_id(), &project_account, &final_bid.bidder, - final_bid.funding_asset_amount_locked.saturating_sub(funding_asset_amount_needed), - Preservation::Preserve + final_bid + .funding_asset_amount_locked + .saturating_sub(funding_asset_amount_needed), + Preservation::Preserve, ); if let Err(e) = try_transfer { Self::deposit_event(Event::::TransferError { error: e }); @@ -1733,16 +1803,21 @@ impl Pallet { final_bid.funding_asset_amount_locked = funding_asset_amount_needed; - let usd_bond_needed = final_bid.multiplier.calculate_bonding_requirement(new_ticket_size) + let usd_bond_needed = final_bid + .multiplier + .calculate_bonding_requirement(new_ticket_size) .map_err(|_| Error::::BadMath)?; - let plmc_bond_needed = plmc_price.reciprocal().ok_or(Error::::BadMath)? - .checked_mul_int(usd_bond_needed).ok_or(Error::::BadMath)?; + let plmc_bond_needed = plmc_price + .reciprocal() + .ok_or(Error::::BadMath)? + .checked_mul_int(usd_bond_needed) + .ok_or(Error::::BadMath)?; let try_release = T::NativeCurrency::release( &LockType::Participation(project_id), &final_bid.bidder, final_bid.plmc_bond.saturating_sub(plmc_bond_needed), - Precision::Exact + Precision::Exact, ); if let Err(e) = try_release { Self::deposit_event(Event::::TransferError { error: e }); diff --git a/polimec-skeleton/pallets/funding/src/lib.rs b/polimec-skeleton/pallets/funding/src/lib.rs index 559636292..209528ae8 100644 --- a/polimec-skeleton/pallets/funding/src/lib.rs +++ b/polimec-skeleton/pallets/funding/src/lib.rs @@ -499,7 +499,7 @@ pub mod pallet { when: T::BlockNumber, }, /// The auction round of a project ended. - AuctionEnded { project_id: T::ProjectIdentifier }, + AuctionFailed { project_id: T::ProjectIdentifier }, /// A `bonder` bonded an `amount` of PLMC for `project_id`. FundsBonded { project_id: T::ProjectIdentifier, @@ -532,7 +532,7 @@ pub mod pallet { /// A project is now in the remainder funding round RemainderFundingStarted { project_id: T::ProjectIdentifier }, /// A project has now finished funding - FundingEnded { project_id: T::ProjectIdentifier }, + FundingEnded { project_id: T::ProjectIdentifier, outcome: FundingOutcome}, /// Something was not properly initialized. Most likely due to dev error manually calling do_* functions or updating storage TransitionError { project_id: T::ProjectIdentifier, diff --git a/polimec-skeleton/pallets/funding/src/tests.rs b/polimec-skeleton/pallets/funding/src/tests.rs index 5138cf084..0bee90cf4 100644 --- a/polimec-skeleton/pallets/funding/src/tests.rs +++ b/polimec-skeleton/pallets/funding/src/tests.rs @@ -39,10 +39,10 @@ use frame_support::{ use helper_functions::*; use crate::traits::BondingRequirementCalculation; +use sp_arithmetic::traits::Zero; use sp_runtime::DispatchError; use std::cell::RefCell; use std::iter::zip; -use sp_arithmetic::traits::Zero; type ProjectIdOf = ::ProjectIdentifier; type UserToPLMCBalance = Vec<(AccountId, BalanceOf)>; @@ -394,7 +394,7 @@ impl TestEnvironment { fn current_block(&self) -> BlockNumber { self.ext_env.borrow_mut().execute_with(|| System::block_number()) } - fn advance_time(&self, amount: BlockNumber) -> Result<(), DispatchError>{ + fn advance_time(&self, amount: BlockNumber) -> Result<(), DispatchError> { self.ext_env.borrow_mut().execute_with(|| { for _block in 0..amount { >::on_finalize(System::block_number()); @@ -711,7 +711,8 @@ impl<'a> AuctioningProject<'a> { let candle_start = english_end + 2; self.test_env - .advance_time(candle_start.saturating_sub(self.test_env.current_block())).unwrap(); + .advance_time(candle_start.saturating_sub(self.test_env.current_block())) + .unwrap(); let candle_end = self .get_project_details() .phase_transition_points @@ -722,7 +723,8 @@ impl<'a> AuctioningProject<'a> { let community_start = candle_end + 2; self.test_env - .advance_time(community_start.saturating_sub(self.test_env.current_block())).unwrap(); + .advance_time(community_start.saturating_sub(self.test_env.current_block())) + .unwrap(); assert_eq!(self.get_project_details().status, ProjectStatus::CommunityRound); @@ -759,6 +761,11 @@ impl<'a> CommunityFundingProject<'a> { let auctioning_project = AuctioningProject::new_with(test_env, project_metadata, issuer, evaluations.clone()); let project_id = auctioning_project.get_project_id(); + + if bids.is_empty() { + panic!("Cannot start community funding without bids") + } + let bidders = bids .iter() .map(|b| b.bidder.clone()) @@ -863,7 +870,8 @@ impl<'a> CommunityFundingProject<'a> { .expect("Community funding end point should exist"); let remainder_start = community_funding_end + 1; self.test_env - .advance_time(remainder_start.saturating_sub(self.test_env.current_block())).unwrap(); + .advance_time(remainder_start.saturating_sub(self.test_env.current_block())) + .unwrap(); assert_eq!(self.get_project_details().status, ProjectStatus::RemainderRound); RemainderFundingProject { test_env: self.test_env, @@ -975,7 +983,8 @@ impl<'a> RemainderFundingProject<'a> { .expect("Should have remainder end"); let finish_block = remainder_funding_end + 1; self.test_env - .advance_time(finish_block.saturating_sub(self.test_env.current_block())).unwrap(); + .advance_time(finish_block.saturating_sub(self.test_env.current_block())) + .unwrap(); assert_eq!(self.get_project_details().status, ProjectStatus::FundingSuccessful); FinishedProject { @@ -1199,8 +1208,8 @@ mod defaults { pub mod helper_functions { use super::*; use sp_arithmetic::traits::Zero; - use std::collections::BTreeMap; use sp_core::H256; + use std::collections::BTreeMap; pub fn get_ed() -> BalanceOf { ::ExistentialDeposit::get() @@ -1467,23 +1476,29 @@ pub mod helper_functions { .unwrap() } - pub fn panic_if_on_initialize_failed(events: Vec>){ + pub fn panic_if_on_initialize_failed(events: Vec>) { let last_event = events.into_iter().last().expect("No events found for this action."); match last_event { - frame_system::EventRecord { event: RuntimeEvent::FundingModule(Event::TransitionError { project_id, error }), .. } => { + frame_system::EventRecord { + event: RuntimeEvent::FundingModule(Event::TransitionError { project_id, error }), + .. + } => { panic!("Project {} transition failed in on_initialize: {:?}", project_id, error); } _ => {} } } - pub fn err_if_on_initialize_failed(events: Vec>) -> Result<(), DispatchError>{ + pub fn err_if_on_initialize_failed( + events: Vec>, + ) -> Result<(), DispatchError> { let last_event = events.into_iter().last().expect("No events found for this action."); match last_event { - frame_system::EventRecord { event: RuntimeEvent::FundingModule(Event::TransitionError { project_id, error }), .. } => { - Err(error) - } - _ => {Ok(())} + frame_system::EventRecord { + event: RuntimeEvent::FundingModule(Event::TransitionError { project_id, error }), + .. + } => Err(error), + _ => Ok(()), } } } @@ -2184,9 +2199,27 @@ mod auction_round_success { let auctioning_project = AuctioningProject::new_with(&test_env, project_metadata, ISSUER, default_evaluations()); let bids = vec![ - TestBid::new(BIDDER_1, 10_000 * ASSET_UNIT, 15.into(), None, AcceptedFundingAsset::USDT), - TestBid::new(BIDDER_2, 20_000 * ASSET_UNIT, 20.into(), None, AcceptedFundingAsset::USDT), - TestBid::new(BIDDER_3, 20_000 * ASSET_UNIT, 16.into(), None, AcceptedFundingAsset::USDT), + TestBid::new( + BIDDER_1, + 10_000 * ASSET_UNIT, + 15.into(), + None, + AcceptedFundingAsset::USDT, + ), + TestBid::new( + BIDDER_2, + 20_000 * ASSET_UNIT, + 20.into(), + None, + AcceptedFundingAsset::USDT, + ), + TestBid::new( + BIDDER_3, + 20_000 * ASSET_UNIT, + 16.into(), + None, + AcceptedFundingAsset::USDT, + ), ]; let statemint_funding = calculate_auction_funding_asset_spent(bids.clone()); @@ -2228,7 +2261,9 @@ mod auction_round_success { .expect("Auction start point should exist"); // The block following the end of the english auction, is used to transition the project into candle auction. // We move past that transition, into the start of the candle auction. - test_env.advance_time(english_end_block - test_env.current_block() + 1).unwrap(); + test_env + .advance_time(english_end_block - test_env.current_block() + 1) + .unwrap(); assert_eq!( auctioning_project.get_project_details().status, ProjectStatus::AuctionRound(AuctionPhase::Candle) @@ -2280,7 +2315,9 @@ mod auction_round_success { test_env.advance_time(1).unwrap(); } - test_env.advance_time(candle_end_block - test_env.current_block() + 1).unwrap(); + test_env + .advance_time(candle_end_block - test_env.current_block() + 1) + .unwrap(); let details = auctioning_project.get_project_details(); let now = test_env.current_block(); @@ -2348,12 +2385,16 @@ mod auction_round_success { test_env.mint_plmc_to(required_plmc); test_env.mint_plmc_to(ed_plmc); project.bond_for_users(evaluations).unwrap(); - test_env.advance_time(::EvaluationDuration::get() + 1).unwrap(); + test_env + .advance_time(::EvaluationDuration::get() + 1) + .unwrap(); assert_eq!( project.get_project_details().status, ProjectStatus::AuctionInitializePeriod ); - test_env.advance_time(::AuctionInitializePeriodDuration::get() + 2).unwrap(); + test_env + .advance_time(::AuctionInitializePeriodDuration::get() + 2) + .unwrap(); assert_eq!( project.get_project_details().status, ProjectStatus::AuctionRound(AuctionPhase::English) @@ -2374,7 +2415,9 @@ mod auction_round_success { test_env.mint_plmc_to(required_plmc); test_env.mint_plmc_to(ed_plmc); project.bond_for_users(evaluations).unwrap(); - test_env.advance_time(::EvaluationDuration::get() + 1).unwrap(); + test_env + .advance_time(::EvaluationDuration::get() + 1) + .unwrap(); assert_eq!( project.get_project_details().status, ProjectStatus::AuctionInitializePeriod @@ -2404,7 +2447,9 @@ mod auction_round_success { test_env.mint_plmc_to(required_plmc); test_env.mint_plmc_to(ed_plmc); project.bond_for_users(evaluations).unwrap(); - test_env.advance_time(::EvaluationDuration::get() + 1).unwrap(); + test_env + .advance_time(::EvaluationDuration::get() + 1) + .unwrap(); assert_eq!( project.get_project_details().status, ProjectStatus::AuctionInitializePeriod @@ -2445,16 +2490,37 @@ mod auction_round_success { let mut project = default_project(test_env.get_new_nonce()); let evaluations = default_evaluations(); let bids: TestBids = vec![ - TestBid::new(BIDDER_1, 10_000 * ASSET_UNIT, 15.into(), None, AcceptedFundingAsset::USDT), - TestBid::new(BIDDER_2, 20_000 * ASSET_UNIT, 20.into(), None, AcceptedFundingAsset::USDT), - TestBid::new(BIDDER_4, 20_000 * ASSET_UNIT, 16.into(), None, AcceptedFundingAsset::USDT), + TestBid::new( + BIDDER_1, + 10_000 * ASSET_UNIT, + 15.into(), + None, + AcceptedFundingAsset::USDT, + ), + TestBid::new( + BIDDER_2, + 20_000 * ASSET_UNIT, + 20.into(), + None, + AcceptedFundingAsset::USDT, + ), + TestBid::new( + BIDDER_4, + 20_000 * ASSET_UNIT, + 16.into(), + None, + AcceptedFundingAsset::USDT, + ), ]; let community_funding_project = CommunityFundingProject::new_with(&test_env, project, issuer, evaluations, bids); let project_id = community_funding_project.project_id; let bidder_2_bid = test_env.in_ext(|| Bids::::get(project_id, BIDDER_2))[0]; - assert_eq!(bidder_2_bid.final_ct_usd_price.checked_mul_int(US_DOLLAR).unwrap(), 17_6_666_666_666); + assert_eq!( + bidder_2_bid.final_ct_usd_price.checked_mul_int(US_DOLLAR).unwrap(), + 17_6_666_666_666 + ); } } @@ -2598,8 +2664,20 @@ mod auction_round_failure { let project = default_project(test_env.get_new_nonce()); let evaluations = default_evaluations(); let bids: TestBids = vec![]; - let _community_funding_project = - CommunityFundingProject::new_with(&test_env, project, issuer, evaluations, bids); + let bidding_project = AuctioningProject::new_with(&test_env, project, issuer, evaluations); + + let details = bidding_project.get_project_details(); + let english_end = details.phase_transition_points.english_auction.end().unwrap(); + let now = test_env.current_block(); + test_env.advance_time(english_end - now + 2).unwrap(); + + let details = bidding_project.get_project_details(); + let candle_end = details.phase_transition_points.candle_auction.end().unwrap(); + let now = test_env.current_block(); + test_env.advance_time(candle_end - now + 2).unwrap(); + + let details = bidding_project.get_project_details(); + assert_eq!(details.status, ProjectStatus::FundingFailed); } } @@ -3831,9 +3909,27 @@ mod misc_features { ]; let bids: TestBids = vec![ - TestBid::new(BIDDER_1, 10_000 * ASSET_UNIT, 15.into(), None, AcceptedFundingAsset::USDT), - TestBid::new(BIDDER_2, 20_000 * ASSET_UNIT, 20.into(), None, AcceptedFundingAsset::USDT), - TestBid::new(BIDDER_4, 20_000 * ASSET_UNIT, 16.into(), None, AcceptedFundingAsset::USDT), + TestBid::new( + BIDDER_1, + 10_000 * ASSET_UNIT, + 15.into(), + None, + AcceptedFundingAsset::USDT, + ), + TestBid::new( + BIDDER_2, + 20_000 * ASSET_UNIT, + 20.into(), + None, + AcceptedFundingAsset::USDT, + ), + TestBid::new( + BIDDER_4, + 20_000 * ASSET_UNIT, + 16.into(), + None, + AcceptedFundingAsset::USDT, + ), ]; let contributions: TestContributions = vec![ @@ -3849,7 +3945,8 @@ mod misc_features { let project = RemainderFundingProject::new_with(&test_env, default_project(0), ISSUER, evaluations, bids, contributions); let details = project.get_project_details(); - let mut ct_evaluation_rewards = test_env.in_ext(|| FundingModule::get_evaluator_ct_rewards(project.get_project_id()).unwrap()); + let mut ct_evaluation_rewards = + test_env.in_ext(|| FundingModule::get_evaluator_ct_rewards(project.get_project_id()).unwrap()); ct_evaluation_rewards.sort_by_key(|item| item.0); let expected_ct_rewards = vec![ (EVALUATOR_1, 1_236_9_500_000_000), diff --git a/polimec-skeleton/pallets/funding/src/types.rs b/polimec-skeleton/pallets/funding/src/types.rs index ab998dffe..64b27a071 100644 --- a/polimec-skeleton/pallets/funding/src/types.rs +++ b/polimec-skeleton/pallets/funding/src/types.rs @@ -132,6 +132,8 @@ pub mod storage_types { pub remaining_contribution_tokens: Balance, /// Funding reached amount in USD equivalent pub funding_amount_reached: Balance, + /// Cleanup operations remaining + pub cleanup: ProjectCleanup, } /// Tells on_initialize what to do with the project @@ -166,7 +168,7 @@ pub mod storage_types { BlockNumber, PlmcVesting, CTVesting, - Multiplier + Multiplier, > { pub id: Id, pub project_id: ProjectId, @@ -334,9 +336,10 @@ pub mod inner_types { #[default] Application, EvaluationRound, - AuctionInitializePeriod, EvaluationFailed, + AuctionInitializePeriod, AuctionRound(AuctionPhase), + AuctionFailed, CommunityRound, RemainderRound, FundingSuccessful, @@ -457,4 +460,82 @@ pub mod inner_types { } } } + + #[derive(Default, Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug, TypeInfo, MaxEncodedLen)] + pub enum FundingOutcome { + Success(SuccessReason), + Failure(FailureReason), + } + + #[derive(Default, Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug, TypeInfo, MaxEncodedLen)] + pub enum SuccessReason { + SoldOut, + ReachedTarget, + } + + #[derive(Default, Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug, TypeInfo, MaxEncodedLen)] + pub enum FailureReason { + EvaluationFailed, + AuctionFailed, + TargetNotReached, + Unknown + } + + #[derive(Default, Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug, TypeInfo, MaxEncodedLen)] + pub enum ProjectCleanup { + #[default] + NotReady, + Ready(RemainingOperations), + Finished + } + + #[derive(Default, Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug, TypeInfo, MaxEncodedLen)] + pub enum RemainingOperations { + Success(SuccessRemainingOperations), + Failure(FailureRemainingOperations), + } + + #[derive(Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug, TypeInfo, MaxEncodedLen)] + pub struct SuccessRemainingOperations { + pub evaluation_unbonding: bool, + pub bidder_plmc_vesting: bool, + pub bidder_ct_mint: bool, + pub contributor_plmc_vesting: bool, + pub contributor_ct_mint: bool, + pub bids_funding_to_issuer_transfer: bool, + pub contributions_funding_to_issuer_transfer: bool, + } + impl Default for SuccessRemainingOperations { + fn default() -> Self { + Self { + evaluation_unbonding: true, + bidder_plmc_vesting: true, + bidder_ct_mint: true, + contributor_plmc_vesting: true, + contributor_ct_mint: true, + bids_funding_to_issuer_transfer: true, + contributions_funding_to_issuer_transfer: true, + } + } + } + + #[derive(Default, Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug, TypeInfo, MaxEncodedLen)] + pub struct FailureRemainingOperations { + pub evaluation_unbonding: bool, + pub bidder_plmc_unbonding: bool, + pub contributor_plmc_unbonding: bool, + pub bids_funding_to_bidder_return: bool, + pub contributions_funding_to_contributor_return: bool, + } + impl Default for FailureRemainingOperations { + fn default() -> Self { + Self { + evaluation_unbonding: true, + bidder_plmc_unbonding: true, + contributor_plmc_unbonding: true, + bids_funding_to_bidder_return: true, + contributions_funding_to_contributor_return: true, + } + } + } } diff --git a/polimec-skeleton/runtime/src/lib.rs b/polimec-skeleton/runtime/src/lib.rs index cffe29730..e764445a1 100644 --- a/polimec-skeleton/runtime/src/lib.rs +++ b/polimec-skeleton/runtime/src/lib.rs @@ -27,7 +27,12 @@ use smallvec::smallvec; use sp_api::impl_runtime_apis; pub use sp_consensus_aura::sr25519::AuthorityId as AuraId; use sp_core::{crypto::KeyTypeId, OpaqueMetadata}; -use sp_runtime::{create_runtime_str, generic, impl_opaque_keys, traits::{AccountIdLookup, BlakeTwo256, Block as BlockT}, transaction_validity::{TransactionSource, TransactionValidity}, ApplyExtrinsicResult, FixedU128, Percent}; +use sp_runtime::{ + create_runtime_str, generic, impl_opaque_keys, + traits::{AccountIdLookup, BlakeTwo256, Block as BlockT}, + transaction_validity::{TransactionSource, TransactionValidity}, + ApplyExtrinsicResult, FixedU128, Percent, +}; pub use sp_runtime::{MultiAddress, Perbill, Permill}; use sp_std::collections::btree_map::BTreeMap; use xcm_config::{RelayLocation, XcmConfig, XcmOriginToTransactDispatchOrigin}; @@ -158,7 +163,6 @@ pub const VERSION: RuntimeVersion = RuntimeVersion { const US_DOLLAR: u128 = 1_0_000_000_000u128; - /// This determines the average expected block time that we are targeting. /// Blocks will be produced at a minimum duration defined by `SLOT_DURATION`. /// `SLOT_DURATION` is picked up by `pallet_timestamp` which is in turn picked From 8c17c9e3cff9f45b7e70d1b402497f49d3a36d27 Mon Sep 17 00:00:00 2001 From: Juan Ignacio Rios Date: Wed, 5 Jul 2023 16:49:00 +0200 Subject: [PATCH 09/27] wip(214): evaluation unbond and automatic cleanup function --- .../pallets/funding/src/functions.rs | 43 ++++++----- polimec-skeleton/pallets/funding/src/impls.rs | 49 +++++++++++++ polimec-skeleton/pallets/funding/src/lib.rs | 72 +++++++++---------- polimec-skeleton/pallets/funding/src/types.rs | 6 +- .../pallets/funding/src/weights.rs | 6 ++ 5 files changed, 114 insertions(+), 62 deletions(-) create mode 100644 polimec-skeleton/pallets/funding/src/impls.rs diff --git a/polimec-skeleton/pallets/funding/src/functions.rs b/polimec-skeleton/pallets/funding/src/functions.rs index f5cbbf054..fe9735506 100644 --- a/polimec-skeleton/pallets/funding/src/functions.rs +++ b/polimec-skeleton/pallets/funding/src/functions.rs @@ -108,6 +108,7 @@ impl Pallet { }, remaining_contribution_tokens: initial_metadata.total_allocation_size, funding_amount_reached: BalanceOf::::zero(), + cleanup: ProjectCleanup::NotReady, }; let project_metadata = initial_metadata; @@ -241,7 +242,7 @@ impl Pallet { let initial_balance: BalanceOf = 0u32.into(); let total_amount_bonded = Evaluations::::iter_prefix(project_id).fold(initial_balance, |total, (_evaluator, bonds)| { - let user_total_plmc_bond = bonds.iter().fold(total, |acc, bond| acc.saturating_add(bond.plmc_bond)); + let user_total_plmc_bond = bonds.iter().fold(total, |acc, bond| acc.saturating_add(bond.original_plmc_bond)); total.saturating_add(user_total_plmc_bond) }); // TODO: PLMC-142. 10% is hardcoded, check if we want to configure it a runtime as explained here: @@ -840,7 +841,7 @@ impl Pallet { id: evaluation_id, project_id, evaluator: evaluator.clone(), - plmc_bond, + original_plmc_bond: plmc_bond, early_usd_amount, late_usd_amount, when: now, @@ -859,14 +860,14 @@ impl Pallet { let lowest_evaluation = caller_existing_evaluations.swap_remove(caller_existing_evaluations.len() - 1); ensure!( - lowest_evaluation.plmc_bond < plmc_bond, + lowest_evaluation.original_plmc_bond < plmc_bond, Error::::EvaluationBondTooLow ); T::NativeCurrency::release( &LockType::Evaluation(project_id), &lowest_evaluation.evaluator, - lowest_evaluation.plmc_bond, + lowest_evaluation.original_plmc_bond, Precision::Exact, ) .map_err(|_| Error::::InsufficientBalance)?; @@ -881,7 +882,7 @@ impl Pallet { } }; - caller_existing_evaluations.sort_by_key(|bond| Reverse(bond.plmc_bond)); + caller_existing_evaluations.sort_by_key(|bond| Reverse(bond.original_plmc_bond)); Evaluations::::set(project_id, evaluator.clone(), caller_existing_evaluations); NextEvaluationId::::set(evaluation_id.saturating_add(One::one())); @@ -896,40 +897,36 @@ impl Pallet { Ok(()) } - pub fn do_failed_evaluation_unbond_for( - bond_id: T::StorageItemId, project_id: T::ProjectIdentifier, evaluator: AccountIdOf, - releaser: AccountIdOf, - ) -> Result<(), DispatchError> { + pub fn do_evaluation_unbond_for(project_id: T::ProjectIdentifier, evaluator: AccountIdOf, releaser: AccountIdOf, evaluation_id: T::StorageItemId) -> Result<(), DispatchError> { // * Get variables * let project_details = ProjectsDetails::::get(project_id).ok_or(Error::::ProjectInfoNotFound)?; - let mut user_bonds = Evaluations::::get(project_id, evaluator.clone()); + let mut user_evaluations = Evaluations::::get(project_id, evaluator.clone()); + let evaluation_position = user_evaluations + .iter() + .position(|evaluation| evaluation.id == evaluation_id) + .ok_or(Error::::EvaluationNotFound)?; + let released_evaluation = user_evaluations.swap_remove(evaluation_position); // * Validity checks * ensure!( - project_details.status == ProjectStatus::EvaluationFailed, - Error::::EvaluationNotFailed + project_details.status == ProjectStatus::FundingSuccessful && released_evaluation.rewarded_or_slashed == true + || project_details.status == ProjectStatus::FundingFailed && released_evaluation.rewarded_or_slashed == true, + Error::::NotAllowed ); - // * Calculate new variables * - let evaluation_pos = user_bonds - .iter() - .position(|bond| bond.id == bond_id) - .ok_or(Error::::BondNotFound)?; - let evaluation = user_bonds.swap_remove(evaluation_pos); - // * Update Storage * T::NativeCurrency::release( &LockType::Evaluation(project_id), - &evaluation.evaluator.clone(), - evaluation.plmc_bond, + &evaluator, + released_evaluation.current_plmc_bond, Precision::Exact, )?; - Evaluations::::set(project_id, evaluator.clone(), user_bonds); + Evaluations::::set(project_id, evaluator.clone(), user_evaluations); // * Emit events * Self::deposit_event(Event::::BondReleased { project_id, - amount: evaluation.plmc_bond, + amount: released_evaluation.current_plmc_bond, bonder: evaluator, releaser, }); diff --git a/polimec-skeleton/pallets/funding/src/impls.rs b/polimec-skeleton/pallets/funding/src/impls.rs new file mode 100644 index 000000000..3872bda4c --- /dev/null +++ b/polimec-skeleton/pallets/funding/src/impls.rs @@ -0,0 +1,49 @@ +use frame_support::weights::Weight; +use sp_runtime::DispatchError; +use crate::{Evaluations, FailureRemainingOperations, RemainingOperations, SuccessRemainingOperations}; + +impl RemainingOperations { + pub fn do_one_operation(&mut self) -> Result { + match self { + RemainingOperations::None => Err(()), + RemainingOperations::Success(ops) => Ok(Weight::from_parts(100_000u64, 100_000_000u64)), + RemainingOperations::Failure(ops) => Ok(Weight::from_parts(100_000u64, 100_000_000u64)) + } + } +} + +fn unbond_evaluators(project_id: T::ProjectIdentifier, max_weight: Weight) -> Result { + // Unbond the plmc from failed evaluation projects + let evaluations = Evaluations::::iter_prefix_values(project_id) + .map(|(_evaluator, evaluations)| evaluations) + .flatten() + .collect::>(); + + let mut used_weight = Weight::zero(); + + let unbond_results = evaluations + // Retrieve as many as possible for the given weight + .take_while(|_bond| { + let new_used_weight = used_weight.saturating_add(T::WeightInfo::evaluation_unbond_for()); + if new_used_weight <= max_weight { + used_weight = new_used_weight; + true + } else { + false + } + }) + // Unbond the plmc + .map(|bond| + Self::do_evaluation_unbond_for() + + .collect::>(); + + + + // Make sure no unbonding failed + for result in unbond_results { + if let Err(e) = result { + Self::deposit_event(Event::::FailedEvaluationUnbondFailed { error: e }); + } + } +} \ No newline at end of file diff --git a/polimec-skeleton/pallets/funding/src/lib.rs b/polimec-skeleton/pallets/funding/src/lib.rs index 209528ae8..a81c43ae1 100644 --- a/polimec-skeleton/pallets/funding/src/lib.rs +++ b/polimec-skeleton/pallets/funding/src/lib.rs @@ -195,6 +195,7 @@ pub mod tests; #[cfg(feature = "runtime-benchmarks")] mod benchmarking; pub mod traits; +mod impls; #[allow(unused_imports)] use polimec_traits::{MemberRole, PolimecMembers}; @@ -663,6 +664,8 @@ pub mod pallet { EvaluationBondTooLow, /// Bond is bigger than the limit set by issuer EvaluationBondTooHigh, + /// Tried to do an operation on an evaluation that does not exist + EvaluationNotFound } #[pallet::call] @@ -854,48 +857,41 @@ pub mod pallet { fn on_idle(_now: T::BlockNumber, max_weight: Weight) -> Weight { let pallet_account: AccountIdOf = ::PalletId::get().into_account_truncating(); - let mut remaining_weight = max_weight; - // Unbond the plmc from failed evaluation projects - let unbond_results = ProjectsDetails::::iter() - // Retrieve failed evaluation projects - .filter_map(|(project_id, info)| { - if let ProjectStatus::EvaluationFailed = info.status { - Some(project_id) - } else { - None - } - }) - // Get a flat list of bonds - .flat_map(|project_id| { - // get all the bonds for projects with a failed evaluation phase - Evaluations::::iter_prefix(project_id) - .flat_map(|(_bonder, bonds)| bonds) - .collect::>() - }) - // Retrieve as many as possible for the given weight - .take_while(|_bond| { - if let Some(new_weight) = - remaining_weight.checked_sub(&T::WeightInfo::failed_evaluation_unbond_for()) - { - remaining_weight = new_weight; - true - } else { - false + let projects_needing_cleanup = ProjectsDetails::::iter() + .filter_map(|(project_id, info)| { + match info.cleanup { + ProjectCleanup::Ready(remaining_operations) if remaining_operations != RemainingOperations::None => { + Some((project_id, remaining_operations)) + } + _ => None } - }) - // Unbond the plmc - .map(|bond| - Self::do_failed_evaluation_unbond_for( - bond.id, bond.project_id, bond.evaluator, pallet_account.clone())) - .collect::>(); - - // Make sure no unbonding failed - for result in unbond_results { - if let Err(e) = result { - Self::deposit_event(Event::::FailedEvaluationUnbondFailed { error: e }); + }) + .collect::>(); + + let projects_amount = projects_needing_cleanup.len() as u64; + let mut max_weight_per_project = remaining_weight.saturating_div(projects_amount); + + for (i, (project_id, mut remaining_ops)) in projects_needing_cleanup.into_iter().enumerate() { + let mut consumed_weight = T::WeightInfo::insert_cleaned_project(); + while consumed_weight < max_weight_per_project { + if let Some(weight) = remaining_ops.do_one_operation() { + consumed_weight += weight + } else { + break + } } + let mut details = ProjectsDetails::::get(project_id); + if let RemainingOperations::None = remaining_ops { + details.cleanup = ProjectCleanup::Finished; + } else { + details.cleanup = ProjectCleanup::Ready(remaining_ops); + } + + ProjectsDetails::::insert(project_id, details); + remaining_weight = remaining_weight.saturating_sub(consumed_weight); + max_weight_per_project = remaining_weight.saturating_div((projects_amount - i - 1)); } // // TODO: PLMC-127. Set a proper weight diff --git a/polimec-skeleton/pallets/funding/src/types.rs b/polimec-skeleton/pallets/funding/src/types.rs index 64b27a071..009734161 100644 --- a/polimec-skeleton/pallets/funding/src/types.rs +++ b/polimec-skeleton/pallets/funding/src/types.rs @@ -152,10 +152,13 @@ pub mod storage_types { pub id: Id, pub project_id: ProjectId, pub evaluator: AccountId, - pub plmc_bond: Balance, + pub original_plmc_bond: Balance, + // An evaluation bond can be converted to participation bond + pub current_plmc_bond: Balance, pub early_usd_amount: Balance, pub late_usd_amount: Balance, pub when: BlockNumber, + pub rewarded_or_slashed: bool, } #[derive(Clone, Copy, Encode, Decode, Eq, PartialEq, RuntimeDebug, MaxEncodedLen, TypeInfo)] @@ -493,6 +496,7 @@ pub mod inner_types { pub enum RemainingOperations { Success(SuccessRemainingOperations), Failure(FailureRemainingOperations), + None } #[derive(Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug, TypeInfo, MaxEncodedLen)] diff --git a/polimec-skeleton/pallets/funding/src/weights.rs b/polimec-skeleton/pallets/funding/src/weights.rs index 96c642781..a593e8f8b 100644 --- a/polimec-skeleton/pallets/funding/src/weights.rs +++ b/polimec-skeleton/pallets/funding/src/weights.rs @@ -59,6 +59,9 @@ pub trait WeightInfo { fn claim_contribution_tokens() -> Weight; fn on_initialize() -> Weight; fn failed_evaluation_unbond_for() -> Weight; + fn insert_cleaned_projects(amount: u64) -> Weight; + // used in on_idle to deduct the weight required to update the cleaned_project + fn insert_cleaned_project() -> Weight; } /// Weights for pallet_funding using the Substrate node and recommended hardware. @@ -342,4 +345,7 @@ impl WeightInfo for () { fn failed_evaluation_unbond_for() -> Weight { Weight::from_parts(1_000_000, 0) } + + // used in on_idle to deduct the weight required to update the cleaned_project + fn insert_cleaned_project() -> Weight{ Weight::from_parts(1_000_000, 0) } } \ No newline at end of file From a7bec119caad26aa948672cf9fce15aef92f40c4 Mon Sep 17 00:00:00 2001 From: Juan Ignacio RIos Date: Thu, 6 Jul 2023 16:41:33 +0200 Subject: [PATCH 10/27] wip(214): evaluation reward and unbonding generic impl --- .../pallets/funding/src/functions.rs | 68 +++-- polimec-skeleton/pallets/funding/src/impls.rs | 260 ++++++++++++++---- polimec-skeleton/pallets/funding/src/lib.rs | 59 ++-- polimec-skeleton/pallets/funding/src/tests.rs | 1 + .../pallets/funding/src/traits.rs | 7 + polimec-skeleton/pallets/funding/src/types.rs | 21 +- .../pallets/funding/src/weights.rs | 17 +- 7 files changed, 326 insertions(+), 107 deletions(-) diff --git a/polimec-skeleton/pallets/funding/src/functions.rs b/polimec-skeleton/pallets/funding/src/functions.rs index fe9735506..681cab2d9 100644 --- a/polimec-skeleton/pallets/funding/src/functions.rs +++ b/polimec-skeleton/pallets/funding/src/functions.rs @@ -242,7 +242,9 @@ impl Pallet { let initial_balance: BalanceOf = 0u32.into(); let total_amount_bonded = Evaluations::::iter_prefix(project_id).fold(initial_balance, |total, (_evaluator, bonds)| { - let user_total_plmc_bond = bonds.iter().fold(total, |acc, bond| acc.saturating_add(bond.original_plmc_bond)); + let user_total_plmc_bond = bonds + .iter() + .fold(total, |acc, bond| acc.saturating_add(bond.original_plmc_bond)); total.saturating_add(user_total_plmc_bond) }); // TODO: PLMC-142. 10% is hardcoded, check if we want to configure it a runtime as explained here: @@ -292,9 +294,8 @@ impl Pallet { } else { // * Update storage * project_details.status = ProjectStatus::EvaluationFailed; + project_details.cleanup = ProjectCleanup::Ready(RemainingOperations::Failure(Default::default())); ProjectsDetails::::insert(project_id, project_details); - // Schedule project for processing in on_initialize - Self::add_to_update_store(now + 1u32.into(), (&project_id, UpdateType::FundingEnd)); // * Emit events * Self::deposit_event(Event::::EvaluationFailed { project_id }); @@ -505,7 +506,7 @@ impl Pallet { let mut project_details = ProjectsDetails::::get(project_id).ok_or(Error::::ProjectInfoNotFound)?; match calculation_result { Err(pallet_error) if pallet_error == Error::::NoBidsFound.into() => { - project_details.status = ProjectStatus::AuctionFailed; + project_details.status = ProjectStatus::FundingFailed; ProjectsDetails::::insert(project_id, project_details); Self::add_to_update_store( >::block_number() + 1u32.into(), @@ -518,7 +519,7 @@ impl Pallet { Ok(()) } e @ Err(_) => e, - Some(()) => { + Ok(()) => { // Get info again after updating it with new price. project_details.phase_transition_points.random_candle_ending = Some(end_block); project_details @@ -643,9 +644,7 @@ impl Pallet { ensure!(now > end_block, Error::::TooEarlyForFundingEnd); } else { ensure!( - remaining_cts == 0u32.into() - || project_details.status == ProjectStatus::EvaluationFailed - || project_details.status == ProjectStatus::AuctionFailed, + remaining_cts == 0u32.into() || project_details.status == ProjectStatus::FundingFailed, Error::::TooEarlyForFundingEnd ); } @@ -653,16 +652,16 @@ impl Pallet { // * Calculate new variables * let funding_target = project_metadata .minimum_price - .checked_mul_int(project_metadata.minimum_price) + .checked_mul_int(project_metadata.total_allocation_size) .ok_or(Error::::BadMath)?; let funding_reached = project_details.funding_amount_reached; - let funding_is_successful = !(project_details.status == ProjectStatus::EvaluationFailed - || project_details.status == ProjectStatus::AuctionFailed - || funding_reached < funding_target); + let funding_is_successful = + !(project_details.status == ProjectStatus::FundingFailed || funding_reached < funding_target); if funding_is_successful { project_details.status = ProjectStatus::FundingSuccessful; - project_details.cleanup = ProjectCleanup::Ready(RemainingOperations::Success(SuccessRemainingOperations::default())); + project_details.cleanup = + ProjectCleanup::Ready(RemainingOperations::Success(SuccessRemainingOperations::default())); // * Update Storage * ProjectsDetails::::insert(project_id, project_details.clone()); @@ -679,27 +678,28 @@ impl Pallet { // * Emit events * let success_reason = match remaining_cts { - 0u32 => SuccessReason::SoldOut, + x if x == 0u32.into() => SuccessReason::SoldOut, _ => SuccessReason::ReachedTarget, }; - Self::deposit_event(Event::::FundingEnded { project_id, outcome: FundingOutcome::Success(success_reason) }); + Self::deposit_event(Event::::FundingEnded { + project_id, + outcome: FundingOutcome::Success(success_reason), + }); Ok(()) - } else { project_details.status = ProjectStatus::FundingFailed; - project_details.cleanup = ProjectCleanup::Ready(RemainingOperations::Failure(FailureRemainingOperations::default())); + project_details.cleanup = + ProjectCleanup::Ready(RemainingOperations::Failure(FailureRemainingOperations::default())); // * Update Storage * ProjectsDetails::::insert(project_id, project_details.clone()); // * Emit events * - let failure_reason = match project_details.status { - ProjectStatus::AuctionFailed => FailureReason::AuctionFailed, - ProjectStatus::EvaluationFailed => FailureReason::EvaluationFailed, - _ if funding_reached < funding_target => FailureReason::TargetNotReached, - _ => FailureReason::Unknown, - }; - Self::deposit_event(Event::::FundingEnded { project_id, outcome: FundingOutcome::Failure(failure_reason) }); + let failure_reason = FailureReason::TargetNotReached; + Self::deposit_event(Event::::FundingEnded { + project_id, + outcome: FundingOutcome::Failure(failure_reason), + }); Ok(()) } } @@ -842,9 +842,11 @@ impl Pallet { project_id, evaluator: evaluator.clone(), original_plmc_bond: plmc_bond, + current_plmc_bond: plmc_bond, early_usd_amount, late_usd_amount, when: now, + rewarded_or_slashed: false, }; // * Update Storage * @@ -897,7 +899,10 @@ impl Pallet { Ok(()) } - pub fn do_evaluation_unbond_for(project_id: T::ProjectIdentifier, evaluator: AccountIdOf, releaser: AccountIdOf, evaluation_id: T::StorageItemId) -> Result<(), DispatchError> { + pub fn do_evaluation_unbond_for( + releaser: AccountIdOf, project_id: T::ProjectIdentifier, evaluator: AccountIdOf, + evaluation_id: T::StorageItemId, + ) -> Result<(), DispatchError> { // * Get variables * let project_details = ProjectsDetails::::get(project_id).ok_or(Error::::ProjectInfoNotFound)?; let mut user_evaluations = Evaluations::::get(project_id, evaluator.clone()); @@ -909,8 +914,10 @@ impl Pallet { // * Validity checks * ensure!( - project_details.status == ProjectStatus::FundingSuccessful && released_evaluation.rewarded_or_slashed == true - || project_details.status == ProjectStatus::FundingFailed && released_evaluation.rewarded_or_slashed == true, + project_details.status == ProjectStatus::FundingSuccessful + && released_evaluation.rewarded_or_slashed == true + || project_details.status == ProjectStatus::FundingFailed + && released_evaluation.rewarded_or_slashed == true, Error::::NotAllowed ); @@ -1525,6 +1532,13 @@ impl Pallet { Ok(()) } + + pub fn do_evaluation_reward_or_slash( + caller: AccountIdOf, project_id: T::ProjectIdentifier, evaluator: AccountIdOf, + evaluation_id: StorageItemIdOf, + ) -> Result<(), DispatchError> { + Ok(()) + } } // Helper functions diff --git a/polimec-skeleton/pallets/funding/src/impls.rs b/polimec-skeleton/pallets/funding/src/impls.rs index 3872bda4c..a5b218297 100644 --- a/polimec-skeleton/pallets/funding/src/impls.rs +++ b/polimec-skeleton/pallets/funding/src/impls.rs @@ -1,49 +1,217 @@ +use crate::traits::DoRemainingOperation; +use crate::{Config, EvaluationInfoOf, Evaluations, Event, FailureRemainingOperations, Pallet, RemainingOperations, SuccessRemainingOperations, WeightInfo}; +use frame_support::traits::Get; use frame_support::weights::Weight; -use sp_runtime::DispatchError; -use crate::{Evaluations, FailureRemainingOperations, RemainingOperations, SuccessRemainingOperations}; - -impl RemainingOperations { - pub fn do_one_operation(&mut self) -> Result { - match self { - RemainingOperations::None => Err(()), - RemainingOperations::Success(ops) => Ok(Weight::from_parts(100_000u64, 100_000_000u64)), - RemainingOperations::Failure(ops) => Ok(Weight::from_parts(100_000u64, 100_000_000u64)) - } - } +use sp_runtime::traits::AccountIdConversion; +use sp_std::prelude::*; + +impl DoRemainingOperation for RemainingOperations { + fn is_done(&self) -> bool { + matches!(self, RemainingOperations::None) + } + fn do_one_operation(&mut self, project_id: T::ProjectIdentifier) -> Result { + match self { + RemainingOperations::None => Err(()), + RemainingOperations::Success(ops) => { + let weight = ops.do_one_operation::(project_id); + if ops.is_done() { + *self = RemainingOperations::None; + } + weight + } + RemainingOperations::Failure(ops) => { + let weight = ops.do_one_operation::(project_id); + if ops.is_done() { + *self = RemainingOperations::None; + } + weight + } + } + } +} + +impl DoRemainingOperation for FailureRemainingOperations { + fn is_done(&self) -> bool { + !self.evaluation_unbonding + && !self.bidder_plmc_unbonding + && !self.contributor_plmc_unbonding + && !self.bids_funding_to_bidder_return + && !self.contributions_funding_to_contributor_return + } + fn do_one_operation(&mut self, project_id: T::ProjectIdentifier) -> Result { + if self.evaluation_reward_or_slash { + + + } else if self.evaluation_unbonding { + let evaluations = Evaluations::::iter_prefix_values(project_id) + .flatten() + .collect::>>(); + + let evaluation = evaluations + .iter() + .find(|evaluation| evaluation.rewarded_or_slashed == true) + .ok_or(())?; + Pallet::::do_evaluation_unbond_for( + T::PalletId::get().into_account_truncating(), + evaluation.project_id, + evaluation.evaluator.clone(), + evaluation.id, + ) + .map_err(|_| ())?; + + if evaluations.len() == 1 { + self.evaluation_unbonding = false; + } + + Ok(T::WeightInfo::evaluation_unbond_for()) + } else if self.bidder_plmc_unbonding { + todo!(); + } else if self.contributor_plmc_unbonding { + todo!(); + } else if self.bids_funding_to_bidder_return { + todo!(); + } else if self.contributions_funding_to_contributor_return { + todo!(); + } else { + todo!(); + } + } } -fn unbond_evaluators(project_id: T::ProjectIdentifier, max_weight: Weight) -> Result { - // Unbond the plmc from failed evaluation projects - let evaluations = Evaluations::::iter_prefix_values(project_id) - .map(|(_evaluator, evaluations)| evaluations) - .flatten() - .collect::>(); - - let mut used_weight = Weight::zero(); - - let unbond_results = evaluations - // Retrieve as many as possible for the given weight - .take_while(|_bond| { - let new_used_weight = used_weight.saturating_add(T::WeightInfo::evaluation_unbond_for()); - if new_used_weight <= max_weight { - used_weight = new_used_weight; - true - } else { - false - } - }) - // Unbond the plmc - .map(|bond| - Self::do_evaluation_unbond_for() - - .collect::>(); - - - - // Make sure no unbonding failed - for result in unbond_results { - if let Err(e) = result { - Self::deposit_event(Event::::FailedEvaluationUnbondFailed { error: e }); - } - } -} \ No newline at end of file +impl DoRemainingOperation for SuccessRemainingOperations { + fn is_done(&self) -> bool { + !self.evaluation_unbonding + && !self.bidder_plmc_vesting + && !self.bidder_ct_mint + && !self.contributor_plmc_vesting + && !self.contributor_ct_mint + && !self.bids_funding_to_issuer_transfer + && !self.contributions_funding_to_issuer_transfer + } + fn do_one_operation(&mut self, project_id: T::ProjectIdentifier) -> Result { + if self.evaluation_reward_or_slash { + reward_or_slash_one_evaluation::(project_id).or_else(|_| { + self.evaluation_reward_or_slash = false; + Ok(Weight::zero()) + }) + } else if self.evaluation_unbonding { + todo!(); + } else if self.bidder_plmc_vesting { + todo!(); + } else if self.bidder_ct_mint { + todo!(); + } else if self.contributor_plmc_vesting { + todo!(); + } else if self.contributor_ct_mint { + todo!(); + } else if self.bids_funding_to_issuer_transfer { + todo!(); + } else if self.contributions_funding_to_issuer_transfer { + todo!(); + } else { + todo!(); + } + } +} + +enum OperationsLeft { + Some(u64), + None, +} + +fn reward_or_slash_one_evaluation(project_id: T::ProjectIdentifier) -> Result { + let mut user_evaluations = Evaluations::::iter_prefix_values(project_id) + .find(|evaluations| evaluations.iter().any(|e| !e.rewarded_or_slashed)).ok_or(())?; + + let mut evaluation = user_evaluations + .iter_mut() + .find(|evaluation| !evaluation.rewarded_or_slashed) + .expect("user_evaluations can only be Some if an item here is found; qed"); + + Pallet::::do_evaluation_reward_or_slash( + T::PalletId::get().into_account_truncating(), + evaluation.project_id, + evaluation.evaluator.clone(), + evaluation.id, + ) + .map_err(|_| ())?; + + evaluation.rewarded_or_slashed = true; + + Evaluations::::insert(project_id, evaluation.evaluator.clone(), user_evaluations); + + Ok(Weight::zero()) +} + +fn unbond_one_evaluation(project_id: T::ProjectIdentifier) -> Result { + let mut user_evaluations = Evaluations::::iter_prefix_values(project_id) + .find(|evaluations| evaluations.iter().any(|e| e.rewarded_or_slashed)).ok_or(())?; + + let mut evaluation = user_evaluations + .iter_mut() + .find(|evaluation| evaluation.rewarded_or_slashed) + .expect("user_evaluations can only be Some if an item here is found; qed"); + + Pallet::::do_evaluation_unbond_for( + T::PalletId::get().into_account_truncating(), + evaluation.project_id, + evaluation.evaluator.clone(), + evaluation.id, + ) + .map_err(|_| ())?; + + Evaluations::::insert(project_id, evaluation.evaluator.clone(), user_evaluations); + + Ok(Weight::zero()) +} + +fn unbond_evaluators( + project_id: T::ProjectIdentifier, max_weight: Weight, +) -> (Weight, OperationsLeft) { + let evaluations = Evaluations::::iter_prefix_values(project_id) + .flatten() + .collect::>>(); + + let mut used_weight = Weight::zero(); + + let unbond_results = evaluations + .iter() + .take_while(|_evaluation| { + let new_used_weight = used_weight.saturating_add(T::WeightInfo::evaluation_unbond_for()); + if new_used_weight.any_gt(max_weight) { + false + } else { + used_weight = new_used_weight; + true + } + }) + .map(|evaluation| { + Pallet::::do_evaluation_unbond_for( + T::PalletId::get().into_account_truncating(), + evaluation.project_id, + evaluation.evaluator.clone(), + evaluation.id, + ) + }) + .collect::>(); + + let successful_results = unbond_results + .into_iter() + .filter(|result| { + if let Err(e) = result { + Pallet::::deposit_event(Event::EvaluationUnbondFailed { error: *e }); + false + } else { + true + } + }) + .collect::>(); + + let operations_left = if successful_results.len() == evaluations.len() { + OperationsLeft::None + } else { + OperationsLeft::Some(evaluations.len().saturating_sub(successful_results.len()) as u64) + }; + + (used_weight, operations_left) +} diff --git a/polimec-skeleton/pallets/funding/src/lib.rs b/polimec-skeleton/pallets/funding/src/lib.rs index a81c43ae1..cd10ffd0d 100644 --- a/polimec-skeleton/pallets/funding/src/lib.rs +++ b/polimec-skeleton/pallets/funding/src/lib.rs @@ -194,8 +194,8 @@ pub mod tests; #[cfg(feature = "runtime-benchmarks")] mod benchmarking; -pub mod traits; mod impls; +pub mod traits; #[allow(unused_imports)] use polimec_traits::{MemberRole, PolimecMembers}; @@ -255,7 +255,7 @@ const PLMC_STATEMINT_ID: u32 = 2069; #[frame_support::pallet(dev_mode)] pub mod pallet { use super::*; - use crate::traits::{BondingRequirementCalculation, ProvideStatemintPrice}; + use crate::traits::{BondingRequirementCalculation, DoRemainingOperation, ProvideStatemintPrice}; use frame_support::pallet_prelude::*; use frame_system::pallet_prelude::*; use local_macros::*; @@ -533,14 +533,17 @@ pub mod pallet { /// A project is now in the remainder funding round RemainderFundingStarted { project_id: T::ProjectIdentifier }, /// A project has now finished funding - FundingEnded { project_id: T::ProjectIdentifier, outcome: FundingOutcome}, + FundingEnded { + project_id: T::ProjectIdentifier, + outcome: FundingOutcome, + }, /// Something was not properly initialized. Most likely due to dev error manually calling do_* functions or updating storage TransitionError { project_id: T::ProjectIdentifier, error: DispatchError, }, /// Something terribly wrong happened where the bond could not be unbonded. Most likely a programming error - FailedEvaluationUnbondFailed { error: DispatchError }, + EvaluationUnbondFailed { error: DispatchError }, /// Contribution tokens were minted to a user ContributionTokenMinted { caller: AccountIdOf, @@ -665,7 +668,7 @@ pub mod pallet { /// Bond is bigger than the limit set by issuer EvaluationBondTooHigh, /// Tried to do an operation on an evaluation that does not exist - EvaluationNotFound + EvaluationNotFound, } #[pallet::call] @@ -732,14 +735,14 @@ pub mod pallet { Self::do_evaluate(evaluator, project_id, usd_amount) } - /// Release the bonded PLMC for an evaluator if the project assigned to it is in the EvaluationFailed phase - #[pallet::weight(T::WeightInfo::failed_evaluation_unbond_for())] - pub fn failed_evaluation_unbond_for( + /// Release evaluation-bonded PLMC when a project finishes its funding round. + #[pallet::weight(T::WeightInfo::evaluation_unbond_for())] + pub fn evaluation_unbond_for( origin: OriginFor, bond_id: T::StorageItemId, project_id: T::ProjectIdentifier, evaluator: AccountIdOf, ) -> DispatchResult { let releaser = ensure_signed(origin)?; - Self::do_failed_evaluation_unbond_for(bond_id, project_id, evaluator, releaser) + Self::do_evaluation_unbond_for(releaser, project_id, evaluator, bond_id) } /// Bid for a project in the Auction round @@ -860,29 +863,39 @@ pub mod pallet { let mut remaining_weight = max_weight; let projects_needing_cleanup = ProjectsDetails::::iter() - .filter_map(|(project_id, info)| { - match info.cleanup { - ProjectCleanup::Ready(remaining_operations) if remaining_operations != RemainingOperations::None => { - Some((project_id, remaining_operations)) - } - _ => None - } + .filter_map(|(project_id, info)| match info.cleanup { + ProjectCleanup::Ready(remaining_operations) + if remaining_operations != RemainingOperations::None => + { + Some((project_id, remaining_operations)) + } + _ => None, }) .collect::>(); let projects_amount = projects_needing_cleanup.len() as u64; + if projects_amount == 0 { + return max_weight; + } + let mut max_weight_per_project = remaining_weight.saturating_div(projects_amount); - for (i, (project_id, mut remaining_ops)) in projects_needing_cleanup.into_iter().enumerate() { + for (remaining_projects, (project_id, mut remaining_ops)) in + projects_needing_cleanup.into_iter().enumerate().rev() + { let mut consumed_weight = T::WeightInfo::insert_cleaned_project(); - while consumed_weight < max_weight_per_project { - if let Some(weight) = remaining_ops.do_one_operation() { + while !consumed_weight.any_gt(max_weight_per_project) { + if let Ok(weight) = remaining_ops.do_one_operation::(project_id) { consumed_weight += weight } else { - break + break; } } - let mut details = ProjectsDetails::::get(project_id); + let mut details = if let Some(d) = ProjectsDetails::::get(project_id) { + d + } else { + continue; + }; if let RemainingOperations::None = remaining_ops { details.cleanup = ProjectCleanup::Finished; } else { @@ -891,7 +904,9 @@ pub mod pallet { ProjectsDetails::::insert(project_id, details); remaining_weight = remaining_weight.saturating_sub(consumed_weight); - max_weight_per_project = remaining_weight.saturating_div((projects_amount - i - 1)); + if remaining_projects > 0 { + max_weight_per_project = remaining_weight.saturating_div(remaining_projects as u64); + } } // // TODO: PLMC-127. Set a proper weight diff --git a/polimec-skeleton/pallets/funding/src/tests.rs b/polimec-skeleton/pallets/funding/src/tests.rs index 0bee90cf4..eb68531aa 100644 --- a/polimec-skeleton/pallets/funding/src/tests.rs +++ b/polimec-skeleton/pallets/funding/src/tests.rs @@ -523,6 +523,7 @@ impl<'a> CreatedProject<'a> { .unwrap(), remaining_contribution_tokens: expected_metadata.total_allocation_size, funding_amount_reached: BalanceOf::::zero(), + cleanup: ProjectCleanup::NotReady, }; assert_eq!(metadata, expected_metadata); assert_eq!(details, expected_details); diff --git a/polimec-skeleton/pallets/funding/src/traits.rs b/polimec-skeleton/pallets/funding/src/traits.rs index 844678416..28b463c5b 100644 --- a/polimec-skeleton/pallets/funding/src/traits.rs +++ b/polimec-skeleton/pallets/funding/src/traits.rs @@ -1,4 +1,5 @@ use crate::{BalanceOf, Config}; +use frame_support::weights::Weight; use sp_arithmetic::FixedPointNumber; pub trait BondingRequirementCalculation { @@ -10,3 +11,9 @@ pub trait ProvideStatemintPrice { type Price: FixedPointNumber; fn get_price(asset_id: Self::AssetId) -> Option; } + +pub trait DoRemainingOperation { + fn is_done(&self) -> bool; + + fn do_one_operation(&mut self, project_id: T::ProjectIdentifier) -> Result; +} diff --git a/polimec-skeleton/pallets/funding/src/types.rs b/polimec-skeleton/pallets/funding/src/types.rs index 009734161..353fec601 100644 --- a/polimec-skeleton/pallets/funding/src/types.rs +++ b/polimec-skeleton/pallets/funding/src/types.rs @@ -342,7 +342,6 @@ pub mod inner_types { EvaluationFailed, AuctionInitializePeriod, AuctionRound(AuctionPhase), - AuctionFailed, CommunityRound, RemainderRound, FundingSuccessful, @@ -464,24 +463,24 @@ pub mod inner_types { } } - #[derive(Default, Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug, TypeInfo, MaxEncodedLen)] + #[derive(Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug, TypeInfo, MaxEncodedLen)] pub enum FundingOutcome { Success(SuccessReason), Failure(FailureReason), } - #[derive(Default, Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug, TypeInfo, MaxEncodedLen)] + #[derive(Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug, TypeInfo, MaxEncodedLen)] pub enum SuccessReason { SoldOut, ReachedTarget, } - #[derive(Default, Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug, TypeInfo, MaxEncodedLen)] + #[derive(Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug, TypeInfo, MaxEncodedLen)] pub enum FailureReason { EvaluationFailed, AuctionFailed, TargetNotReached, - Unknown + Unknown, } #[derive(Default, Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug, TypeInfo, MaxEncodedLen)] @@ -489,18 +488,19 @@ pub mod inner_types { #[default] NotReady, Ready(RemainingOperations), - Finished + Finished, } - #[derive(Default, Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug, TypeInfo, MaxEncodedLen)] + #[derive(Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug, TypeInfo, MaxEncodedLen)] pub enum RemainingOperations { Success(SuccessRemainingOperations), Failure(FailureRemainingOperations), - None + None, } #[derive(Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug, TypeInfo, MaxEncodedLen)] pub struct SuccessRemainingOperations { + pub evaluation_reward_or_slash: bool, pub evaluation_unbonding: bool, pub bidder_plmc_vesting: bool, pub bidder_ct_mint: bool, @@ -512,6 +512,7 @@ pub mod inner_types { impl Default for SuccessRemainingOperations { fn default() -> Self { Self { + evaluation_reward_or_slash: true, evaluation_unbonding: true, bidder_plmc_vesting: true, bidder_ct_mint: true, @@ -523,8 +524,9 @@ pub mod inner_types { } } - #[derive(Default, Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug, TypeInfo, MaxEncodedLen)] + #[derive(Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug, TypeInfo, MaxEncodedLen)] pub struct FailureRemainingOperations { + pub evaluation_reward_or_slash: bool, pub evaluation_unbonding: bool, pub bidder_plmc_unbonding: bool, pub contributor_plmc_unbonding: bool, @@ -534,6 +536,7 @@ pub mod inner_types { impl Default for FailureRemainingOperations { fn default() -> Self { Self { + evaluation_reward_or_slash: true, evaluation_unbonding: true, bidder_plmc_unbonding: true, contributor_plmc_unbonding: true, diff --git a/polimec-skeleton/pallets/funding/src/weights.rs b/polimec-skeleton/pallets/funding/src/weights.rs index a593e8f8b..2815c9451 100644 --- a/polimec-skeleton/pallets/funding/src/weights.rs +++ b/polimec-skeleton/pallets/funding/src/weights.rs @@ -59,9 +59,9 @@ pub trait WeightInfo { fn claim_contribution_tokens() -> Weight; fn on_initialize() -> Weight; fn failed_evaluation_unbond_for() -> Weight; - fn insert_cleaned_projects(amount: u64) -> Weight; // used in on_idle to deduct the weight required to update the cleaned_project fn insert_cleaned_project() -> Weight; + fn evaluation_unbond_for() -> Weight; } /// Weights for pallet_funding using the Substrate node and recommended hardware. @@ -204,8 +204,15 @@ impl WeightInfo for SubstrateWeight { fn failed_evaluation_unbond_for() -> Weight { Weight::from_parts(1_000_000, 0) } -} + fn insert_cleaned_project() -> Weight { + Weight::from_parts(1_000_000, 0) + } + + fn evaluation_unbond_for() -> Weight { + Weight::from_parts(1_000_000, 0) + } +} // For backwards compatibility and tests impl WeightInfo for () { // Storage: PolimecFunding Images (r:0 w:1) @@ -347,5 +354,9 @@ impl WeightInfo for () { } // used in on_idle to deduct the weight required to update the cleaned_project - fn insert_cleaned_project() -> Weight{ Weight::from_parts(1_000_000, 0) } + fn insert_cleaned_project() -> Weight { Weight::from_parts(1_000_000, 0) } + + fn evaluation_unbond_for() -> Weight { + Weight::from_parts(1_000_000, 0) + } } \ No newline at end of file From b200673d9a56326a906301b1cbba9573bcbd785e Mon Sep 17 00:00:00 2001 From: Juan Ignacio Rios Date: Fri, 7 Jul 2023 13:31:36 +0200 Subject: [PATCH 11/27] wip(214) --- .../pallets/funding/src/functions.rs | 9 +- polimec-skeleton/pallets/funding/src/impls.rs | 104 +++++++++++------- 2 files changed, 67 insertions(+), 46 deletions(-) diff --git a/polimec-skeleton/pallets/funding/src/functions.rs b/polimec-skeleton/pallets/funding/src/functions.rs index 681cab2d9..2aa00c829 100644 --- a/polimec-skeleton/pallets/funding/src/functions.rs +++ b/polimec-skeleton/pallets/funding/src/functions.rs @@ -914,10 +914,11 @@ impl Pallet { // * Validity checks * ensure!( - project_details.status == ProjectStatus::FundingSuccessful - && released_evaluation.rewarded_or_slashed == true - || project_details.status == ProjectStatus::FundingFailed - && released_evaluation.rewarded_or_slashed == true, + released_evaluation.rewarded_or_slashed == true + && matches!( + project_details.status, + ProjectStatus::EvaluationFailed | ProjectStatus::FundingFailed | ProjectStatus::FundingSuccessful + ), Error::::NotAllowed ); diff --git a/polimec-skeleton/pallets/funding/src/impls.rs b/polimec-skeleton/pallets/funding/src/impls.rs index a5b218297..9b95835ff 100644 --- a/polimec-skeleton/pallets/funding/src/impls.rs +++ b/polimec-skeleton/pallets/funding/src/impls.rs @@ -1,5 +1,8 @@ use crate::traits::DoRemainingOperation; -use crate::{Config, EvaluationInfoOf, Evaluations, Event, FailureRemainingOperations, Pallet, RemainingOperations, SuccessRemainingOperations, WeightInfo}; +use crate::{ + Config, EvaluationInfoOf, Evaluations, Event, FailureRemainingOperations, Pallet, RemainingOperations, + SuccessRemainingOperations, WeightInfo, +}; use frame_support::traits::Get; use frame_support::weights::Weight; use sp_runtime::traits::AccountIdConversion; @@ -40,40 +43,34 @@ impl DoRemainingOperation for FailureRemainingOperations { } fn do_one_operation(&mut self, project_id: T::ProjectIdentifier) -> Result { if self.evaluation_reward_or_slash { - - + reward_or_slash_one_evaluation::(project_id).or_else(|_| { + self.evaluation_reward_or_slash = false; + Ok(Weight::zero()) + }) } else if self.evaluation_unbonding { - let evaluations = Evaluations::::iter_prefix_values(project_id) - .flatten() - .collect::>>(); - - let evaluation = evaluations - .iter() - .find(|evaluation| evaluation.rewarded_or_slashed == true) - .ok_or(())?; - Pallet::::do_evaluation_unbond_for( - T::PalletId::get().into_account_truncating(), - evaluation.project_id, - evaluation.evaluator.clone(), - evaluation.id, - ) - .map_err(|_| ())?; - - if evaluations.len() == 1 { + unbond_one_evaluation::(project_id).or_else(|_| { self.evaluation_unbonding = false; - } - - Ok(T::WeightInfo::evaluation_unbond_for()) + Ok(Weight::zero()) + }) } else if self.bidder_plmc_unbonding { - todo!(); + // todo!(); + self.bidder_plmc_unbonding = false; + Ok(Weight::zero()) } else if self.contributor_plmc_unbonding { - todo!(); + // todo!(); + self.contributor_plmc_unbonding = false; + Ok(Weight::zero()) } else if self.bids_funding_to_bidder_return { - todo!(); + // todo!(); + self.bids_funding_to_bidder_return = false; + Ok(Weight::zero()) } else if self.contributions_funding_to_contributor_return { - todo!(); + // todo!(); + self.contributions_funding_to_contributor_return = false; + Ok(Weight::zero()) } else { - todo!(); + // todo!(); + Ok(Weight::zero()) } } } @@ -94,22 +91,45 @@ impl DoRemainingOperation for SuccessRemainingOperations { self.evaluation_reward_or_slash = false; Ok(Weight::zero()) }) + } else if self.evaluation_unbonding { - todo!(); + unbond_one_evaluation::(project_id).or_else(|_| { + self.evaluation_unbonding = false; + Ok(Weight::zero()) + }) } else if self.bidder_plmc_vesting { - todo!(); + // todo!(); + self.bidder_plmc_vesting = false; + Ok(Weight::zero()) + } else if self.bidder_ct_mint { - todo!(); + // todo!(); + self.bidder_ct_mint = false; + Ok(Weight::zero()) + } else if self.contributor_plmc_vesting { - todo!(); + // todo!(); + self.contributor_plmc_vesting = false; + Ok(Weight::zero()) + } else if self.contributor_ct_mint { - todo!(); + // todo!(); + self.contributor_ct_mint = false; + Ok(Weight::zero()) + } else if self.bids_funding_to_issuer_transfer { - todo!(); + // todo!(); + self.bids_funding_to_issuer_transfer = false; + Ok(Weight::zero()) + } else if self.contributions_funding_to_issuer_transfer { - todo!(); + // todo!(); + self.contributions_funding_to_issuer_transfer = false; + Ok(Weight::zero()) + } else { - todo!(); + // todo!(); + Ok(Weight::zero()) } } } @@ -121,7 +141,8 @@ enum OperationsLeft { fn reward_or_slash_one_evaluation(project_id: T::ProjectIdentifier) -> Result { let mut user_evaluations = Evaluations::::iter_prefix_values(project_id) - .find(|evaluations| evaluations.iter().any(|e| !e.rewarded_or_slashed)).ok_or(())?; + .find(|evaluations| evaluations.iter().any(|e| !e.rewarded_or_slashed)) + .ok_or(())?; let mut evaluation = user_evaluations .iter_mut() @@ -134,7 +155,7 @@ fn reward_or_slash_one_evaluation(project_id: T::ProjectIdentifier) - evaluation.evaluator.clone(), evaluation.id, ) - .map_err(|_| ())?; + .map_err(|_| ())?; evaluation.rewarded_or_slashed = true; @@ -145,7 +166,8 @@ fn reward_or_slash_one_evaluation(project_id: T::ProjectIdentifier) - fn unbond_one_evaluation(project_id: T::ProjectIdentifier) -> Result { let mut user_evaluations = Evaluations::::iter_prefix_values(project_id) - .find(|evaluations| evaluations.iter().any(|e| e.rewarded_or_slashed)).ok_or(())?; + .find(|evaluations| evaluations.iter().any(|e| e.rewarded_or_slashed)) + .ok_or(())?; let mut evaluation = user_evaluations .iter_mut() @@ -158,9 +180,7 @@ fn unbond_one_evaluation(project_id: T::ProjectIdentifier) -> evaluation.evaluator.clone(), evaluation.id, ) - .map_err(|_| ())?; - - Evaluations::::insert(project_id, evaluation.evaluator.clone(), user_evaluations); + .map_err(|_| ())?; Ok(Weight::zero()) } From c6ba18eb901db1448750bb694263315fb86f4dcf Mon Sep 17 00:00:00 2001 From: Juan Ignacio Rios Date: Tue, 11 Jul 2023 15:29:25 +0200 Subject: [PATCH 12/27] feat(214): cleanup state machine basic implementation --- .../pallets/funding/src/functions.rs | 51 +- polimec-skeleton/pallets/funding/src/impls.rs | 729 ++++++++++++++---- polimec-skeleton/pallets/funding/src/lib.rs | 65 +- polimec-skeleton/pallets/funding/src/types.rs | 78 +- 4 files changed, 720 insertions(+), 203 deletions(-) diff --git a/polimec-skeleton/pallets/funding/src/functions.rs b/polimec-skeleton/pallets/funding/src/functions.rs index 2aa00c829..eb6efce04 100644 --- a/polimec-skeleton/pallets/funding/src/functions.rs +++ b/polimec-skeleton/pallets/funding/src/functions.rs @@ -294,7 +294,7 @@ impl Pallet { } else { // * Update storage * project_details.status = ProjectStatus::EvaluationFailed; - project_details.cleanup = ProjectCleanup::Ready(RemainingOperations::Failure(Default::default())); + project_details.cleanup = ProjectCleanup::Ready(ProjectFinalizer::Failure(Default::default())); ProjectsDetails::::insert(project_id, project_details); // * Emit events * @@ -660,8 +660,7 @@ impl Pallet { if funding_is_successful { project_details.status = ProjectStatus::FundingSuccessful; - project_details.cleanup = - ProjectCleanup::Ready(RemainingOperations::Success(SuccessRemainingOperations::default())); + project_details.cleanup = ProjectCleanup::Ready(ProjectFinalizer::Success(Default::default())); // * Update Storage * ProjectsDetails::::insert(project_id, project_details.clone()); @@ -688,8 +687,7 @@ impl Pallet { Ok(()) } else { project_details.status = ProjectStatus::FundingFailed; - project_details.cleanup = - ProjectCleanup::Ready(RemainingOperations::Failure(FailureRemainingOperations::default())); + project_details.cleanup = ProjectCleanup::Ready(ProjectFinalizer::Failure(Default::default())); // * Update Storage * ProjectsDetails::::insert(project_id, project_details.clone()); @@ -1023,6 +1021,7 @@ impl Pallet { plmc_vesting_period, ct_vesting_period, when: now, + funds_released: false, }; // * Update storage * @@ -1177,6 +1176,7 @@ impl Pallet { plmc_bond: required_plmc_bond, plmc_vesting_period, ct_vesting_period, + funds_released: false, }; // * Update storage * @@ -1540,6 +1540,47 @@ impl Pallet { ) -> Result<(), DispatchError> { Ok(()) } + + pub fn do_release_bid_funds_for( + caller: AccountIdOf, project_id: T::ProjectIdentifier, bidder: AccountIdOf, bid_id: StorageItemIdOf, + ) -> Result<(), DispatchError> { + Ok(()) + } + + pub fn do_bid_unbond_for( + caller: AccountIdOf, project_id: T::ProjectIdentifier, bidder: AccountIdOf, bid_id: StorageItemIdOf, + ) -> Result<(), DispatchError> { + Ok(()) + } + + pub fn do_release_contribution_funds_for( + caller: AccountIdOf, project_id: T::ProjectIdentifier, contributor: AccountIdOf, + contribution_id: StorageItemIdOf, + ) -> Result<(), DispatchError> { + Ok(()) + } + + pub fn do_contribution_unbond_for( + caller: AccountIdOf, project_id: T::ProjectIdentifier, contributor: AccountIdOf, + contribution_id: StorageItemIdOf, + ) -> Result<(), DispatchError> { + Ok(()) + } + + pub fn do_payout_contribution_funds_for( + caller: AccountIdOf, project_id: T::ProjectIdentifier, contributor: AccountIdOf, + contribution_id: StorageItemIdOf, + ) -> Result<(), DispatchError> { + Ok(()) + } + + pub fn do_payout_bid_funds_for( + caller: AccountIdOf, project_id: T::ProjectIdentifier, bidder: AccountIdOf, + bid_id: StorageItemIdOf, + ) -> Result<(), DispatchError> { + Ok(()) + } + } // Helper functions diff --git a/polimec-skeleton/pallets/funding/src/impls.rs b/polimec-skeleton/pallets/funding/src/impls.rs index 9b95835ff..399b714b4 100644 --- a/polimec-skeleton/pallets/funding/src/impls.rs +++ b/polimec-skeleton/pallets/funding/src/impls.rs @@ -1,31 +1,31 @@ use crate::traits::DoRemainingOperation; use crate::{ - Config, EvaluationInfoOf, Evaluations, Event, FailureRemainingOperations, Pallet, RemainingOperations, - SuccessRemainingOperations, WeightInfo, + Bids, Config, Contributions, EvaluationInfoOf, Evaluations, Event, FailureFinalizer, Pallet, ProjectFinalizer, + SuccessFinalizer, WeightInfo, }; use frame_support::traits::Get; use frame_support::weights::Weight; use sp_runtime::traits::AccountIdConversion; use sp_std::prelude::*; -impl DoRemainingOperation for RemainingOperations { +impl DoRemainingOperation for ProjectFinalizer { fn is_done(&self) -> bool { - matches!(self, RemainingOperations::None) + matches!(self, ProjectFinalizer::None) } fn do_one_operation(&mut self, project_id: T::ProjectIdentifier) -> Result { match self { - RemainingOperations::None => Err(()), - RemainingOperations::Success(ops) => { + ProjectFinalizer::None => Err(()), + ProjectFinalizer::Success(ops) => { let weight = ops.do_one_operation::(project_id); if ops.is_done() { - *self = RemainingOperations::None; + *self = ProjectFinalizer::None; } weight } - RemainingOperations::Failure(ops) => { + ProjectFinalizer::Failure(ops) => { let weight = ops.do_one_operation::(project_id); if ops.is_done() { - *self = RemainingOperations::None; + *self = ProjectFinalizer::None; } weight } @@ -33,103 +33,192 @@ impl DoRemainingOperation for RemainingOperations { } } -impl DoRemainingOperation for FailureRemainingOperations { +impl DoRemainingOperation for SuccessFinalizer { fn is_done(&self) -> bool { - !self.evaluation_unbonding - && !self.bidder_plmc_unbonding - && !self.contributor_plmc_unbonding - && !self.bids_funding_to_bidder_return - && !self.contributions_funding_to_contributor_return + matches!(self, SuccessFinalizer::Finished) } - fn do_one_operation(&mut self, project_id: T::ProjectIdentifier) -> Result { - if self.evaluation_reward_or_slash { - reward_or_slash_one_evaluation::(project_id).or_else(|_| { - self.evaluation_reward_or_slash = false; - Ok(Weight::zero()) - }) - } else if self.evaluation_unbonding { - unbond_one_evaluation::(project_id).or_else(|_| { - self.evaluation_unbonding = false; + + fn do_one_operation(&mut self, project_id: T::ProjectIdentifier) -> Result { + match self { + SuccessFinalizer::Initialized => { + *self = + SuccessFinalizer::EvaluationRewardOrSlash(remaining_evaluators_to_reward_or_slash::(project_id)); Ok(Weight::zero()) - }) - } else if self.bidder_plmc_unbonding { - // todo!(); - self.bidder_plmc_unbonding = false; - Ok(Weight::zero()) - } else if self.contributor_plmc_unbonding { - // todo!(); - self.contributor_plmc_unbonding = false; - Ok(Weight::zero()) - } else if self.bids_funding_to_bidder_return { - // todo!(); - self.bids_funding_to_bidder_return = false; - Ok(Weight::zero()) - } else if self.contributions_funding_to_contributor_return { - // todo!(); - self.contributions_funding_to_contributor_return = false; - Ok(Weight::zero()) - } else { - // todo!(); - Ok(Weight::zero()) + } + SuccessFinalizer::EvaluationRewardOrSlash(remaining) => { + if *remaining == 0 { + *self = SuccessFinalizer::EvaluationUnbonding(remaining_evaluations::(project_id)); + Ok(Weight::zero()) + } else { + let (consumed_weight, remaining_evaluations) = reward_or_slash_one_evaluation::(project_id); + *self = SuccessFinalizer::EvaluationRewardOrSlash(remaining_evaluations); + Ok(consumed_weight) + } + } + SuccessFinalizer::EvaluationUnbonding(remaining) => { + if *remaining == 0 { + *self = SuccessFinalizer::BidPLMCVesting(remaining_bids_without_plmc_vesting::(project_id)); + Ok(Weight::zero()) + } else { + let (consumed_weight, remaining_evaluations) = unbond_one_evaluation::(project_id); + *self = SuccessFinalizer::EvaluationUnbonding(remaining_evaluations); + Ok(consumed_weight) + } + } + SuccessFinalizer::BidPLMCVesting(remaining) => { + if *remaining == 0 { + *self = SuccessFinalizer::BidCTMint(remaining_bids_without_ct_minted::(project_id)); + Ok(Weight::zero()) + } else { + let (consumed_weight, remaining_bids) = start_bid_plmc_vesting_schedule::(project_id); + *self = SuccessFinalizer::BidPLMCVesting(remaining_bids); + Ok(consumed_weight) + } + } + SuccessFinalizer::BidCTMint(remaining) => { + if *remaining == 0 { + *self = SuccessFinalizer::ContributionPLMCVesting( + remaining_contributions_without_plmc_vesting::(project_id), + ); + Ok(Weight::zero()) + } else { + let (consumed_weight, remaining_bids) = mint_ct_for_one_bid::(project_id); + *self = SuccessFinalizer::BidCTMint(remaining_bids); + Ok(consumed_weight) + } + } + SuccessFinalizer::ContributionPLMCVesting(remaining) => { + if *remaining == 0 { + *self = SuccessFinalizer::ContributionCTMint(remaining_contributions_without_ct_minted::( + project_id, + )); + Ok(Weight::zero()) + } else { + let (consumed_weight, remaining_contributions) = + start_contribution_plmc_vesting_schedule::(project_id); + *self = SuccessFinalizer::ContributionPLMCVesting(remaining_contributions); + Ok(consumed_weight) + } + } + SuccessFinalizer::ContributionCTMint(remaining) => { + if *remaining == 0 { + *self = SuccessFinalizer::BidFundingPayout(remaining_bids_without_issuer_payout::(project_id)); + Ok(Weight::zero()) + } else { + let (consumed_weight, remaining_contributions) = mint_ct_for_one_contribution::(project_id); + *self = SuccessFinalizer::ContributionCTMint(remaining_contributions); + Ok(consumed_weight) + } + } + SuccessFinalizer::BidFundingPayout(remaining) => { + if *remaining == 0 { + *self = SuccessFinalizer::ContributionFundingPayout( + remaining_contributions_without_issuer_payout::(project_id), + ); + Ok(Weight::zero()) + } else { + let (consumed_weight, remaining_contributions) = issuer_funding_payout_one_bid::(project_id); + *self = SuccessFinalizer::BidFundingPayout(remaining_contributions); + Ok(consumed_weight) + } + } + SuccessFinalizer::ContributionFundingPayout(remaining) => { + if *remaining == 0 { + *self = SuccessFinalizer::Finished; + Ok(Weight::zero()) + } else { + let (consumed_weight, remaining_contributions) = + issuer_funding_payout_one_contribution::(project_id); + *self = SuccessFinalizer::ContributionFundingPayout(remaining_contributions); + Ok(consumed_weight) + } + } + SuccessFinalizer::Finished => {Err(())} } } } -impl DoRemainingOperation for SuccessRemainingOperations { +impl DoRemainingOperation for FailureFinalizer { fn is_done(&self) -> bool { - !self.evaluation_unbonding - && !self.bidder_plmc_vesting - && !self.bidder_ct_mint - && !self.contributor_plmc_vesting - && !self.contributor_ct_mint - && !self.bids_funding_to_issuer_transfer - && !self.contributions_funding_to_issuer_transfer + matches!(self, FailureFinalizer::Finished) } - fn do_one_operation(&mut self, project_id: T::ProjectIdentifier) -> Result { - if self.evaluation_reward_or_slash { - reward_or_slash_one_evaluation::(project_id).or_else(|_| { - self.evaluation_reward_or_slash = false; + fn do_one_operation(&mut self, project_id: T::ProjectIdentifier) -> Result { + match self { + FailureFinalizer::Initialized => { + *self = + FailureFinalizer::EvaluationRewardOrSlash(remaining_evaluators_to_reward_or_slash::(project_id)); Ok(Weight::zero()) - }) + } - } else if self.evaluation_unbonding { - unbond_one_evaluation::(project_id).or_else(|_| { - self.evaluation_unbonding = false; - Ok(Weight::zero()) - }) - } else if self.bidder_plmc_vesting { - // todo!(); - self.bidder_plmc_vesting = false; - Ok(Weight::zero()) - - } else if self.bidder_ct_mint { - // todo!(); - self.bidder_ct_mint = false; - Ok(Weight::zero()) - - } else if self.contributor_plmc_vesting { - // todo!(); - self.contributor_plmc_vesting = false; - Ok(Weight::zero()) - - } else if self.contributor_ct_mint { - // todo!(); - self.contributor_ct_mint = false; - Ok(Weight::zero()) - - } else if self.bids_funding_to_issuer_transfer { - // todo!(); - self.bids_funding_to_issuer_transfer = false; - Ok(Weight::zero()) - - } else if self.contributions_funding_to_issuer_transfer { - // todo!(); - self.contributions_funding_to_issuer_transfer = false; - Ok(Weight::zero()) - - } else { - // todo!(); - Ok(Weight::zero()) + FailureFinalizer::EvaluationRewardOrSlash(remaining) => { + if *remaining == 0 { + *self = FailureFinalizer::EvaluationUnbonding(remaining_evaluations::(project_id)); + Ok(Weight::zero()) + } else { + let (consumed_weight, remaining_evaluators) = reward_or_slash_one_evaluation::(project_id); + *self = FailureFinalizer::EvaluationRewardOrSlash(remaining_evaluators); + Ok(consumed_weight) + } + } + + FailureFinalizer::EvaluationUnbonding(remaining) => { + if *remaining == 0 { + *self = FailureFinalizer::BidFundingRelease(remaining_bids_to_release_funds::(project_id)); + Ok(Weight::zero()) + } else { + let (consumed_weight, remaining_evaluators) = unbond_one_evaluation::(project_id); + *self = FailureFinalizer::EvaluationUnbonding(remaining_evaluators); + Ok(consumed_weight) + } + } + + FailureFinalizer::BidFundingRelease(remaining) => { + if *remaining == 0 { + *self = FailureFinalizer::BidUnbonding(remaining_bids::(project_id)); + Ok(Weight::zero()) + } else { + let (consumed_weight, remaining_bids) = release_funds_one_bid::(project_id); + *self = FailureFinalizer::BidFundingRelease(remaining_bids); + Ok(consumed_weight) + } + } + + FailureFinalizer::BidUnbonding(remaining) => { + if *remaining == 0 { + *self = FailureFinalizer::ContributionFundingRelease( + remaining_contributions_to_release_funds::(project_id), + ); + Ok(Weight::zero()) + } else { + let (consumed_weight, remaining_bids) = unbond_one_bid::(project_id); + *self = FailureFinalizer::BidUnbonding(remaining_bids); + Ok(consumed_weight) + } + } + + FailureFinalizer::ContributionFundingRelease(remaining) => { + if *remaining == 0 { + *self = FailureFinalizer::ContributionUnbonding(remaining_contributions::(project_id)); + Ok(Weight::zero()) + } else { + let (consumed_weight, remaining_contributions) = release_funds_one_contribution::(project_id); + *self = FailureFinalizer::ContributionFundingRelease(remaining_contributions); + Ok(consumed_weight) + } + } + + FailureFinalizer::ContributionUnbonding(remaining) => { + if *remaining == 0 { + *self = FailureFinalizer::Finished; + Ok(Weight::zero()) + } else { + let (consumed_weight, remaining_contributions) = unbond_one_contribution::(project_id); + *self = FailureFinalizer::ContributionUnbonding(remaining_contributions); + Ok(consumed_weight) + } + } + + FailureFinalizer::Finished => Err(()), } } } @@ -139,55 +228,422 @@ enum OperationsLeft { None, } -fn reward_or_slash_one_evaluation(project_id: T::ProjectIdentifier) -> Result { - let mut user_evaluations = Evaluations::::iter_prefix_values(project_id) - .find(|evaluations| evaluations.iter().any(|e| !e.rewarded_or_slashed)) - .ok_or(())?; +fn remaining_evaluators_to_reward_or_slash(project_id: T::ProjectIdentifier) -> u64 { + Evaluations::::iter_prefix_values(project_id) + .flatten() + .filter(|evaluation| !evaluation.rewarded_or_slashed) + .count() as u64 +} + +fn remaining_evaluations(project_id: T::ProjectIdentifier) -> u64 { + Evaluations::::iter_prefix_values(project_id).flatten().count() as u64 +} + +fn remaining_bids_to_release_funds(project_id: T::ProjectIdentifier) -> u64 { + Bids::::iter_prefix_values(project_id) + .flatten() + .filter(|bid| !bid.funds_released) + .count() as u64 +} + +fn remaining_bids(project_id: T::ProjectIdentifier) -> u64 { + Bids::::iter_prefix_values(project_id).flatten().count() as u64 +} + +fn remaining_contributions_to_release_funds(project_id: T::ProjectIdentifier) -> u64 { + Contributions::::iter_prefix_values(project_id) + .flatten() + .filter(|contribution| !contribution.funds_released) + .count() as u64 +} + +fn remaining_contributions(project_id: T::ProjectIdentifier) -> u64 { + Contributions::::iter_prefix_values(project_id).flatten().count() as u64 +} + +fn remaining_bids_without_plmc_vesting(project_id: T::ProjectIdentifier) -> u64 { + // TODO: current vesting implementation starts the schedule on bid creation. We should later on use pallet_vesting + // and add a check in the bid struct for initializing the vesting schedule + 0u64 +} + +fn remaining_bids_without_ct_minted(project_id: T::ProjectIdentifier) -> u64 { + // TODO: currently we vest the contribution tokens. We should change this to a direct mint. + 0u64 +} + +fn remaining_contributions_without_plmc_vesting(project_id: T::ProjectIdentifier) -> u64 { + // TODO: current vesting implementation starts the schedule on contribution creation. We should later on use pallet_vesting + // and add a check in the contribution struct for initializing the vesting schedule + 0u64 +} + +fn remaining_contributions_without_ct_minted(project_id: T::ProjectIdentifier) -> u64 { + // TODO: currently we vest the contribution tokens. We should change this to a direct mint. + 0u64 +} + +fn remaining_bids_without_issuer_payout(project_id: T::ProjectIdentifier) -> u64 { + Bids::::iter_prefix_values(project_id) + .flatten() + .filter(|bid| !bid.funds_released) + .count() as u64 +} + +fn remaining_contributions_without_issuer_payout(project_id: T::ProjectIdentifier) -> u64 { + Contributions::::iter_prefix_values(project_id) + .flatten() + .filter(|bid| !bid.funds_released) + .count() as u64 +} + +fn reward_or_slash_one_evaluation(project_id: T::ProjectIdentifier) -> (Weight, u64) { + let project_evaluations: Vec<_> = Evaluations::::iter_prefix_values(project_id).collect(); + let remaining_evaluations = project_evaluations + .iter() + .flatten() + .filter(|evaluation| !evaluation.rewarded_or_slashed) + .count() as u64; + + let maybe_user_evaluations = project_evaluations + .into_iter() + .find(|evaluations| evaluations.iter().any(|evaluation| !evaluation.rewarded_or_slashed)); + + if let Some(mut user_evaluations) = maybe_user_evaluations { + let mut evaluation = user_evaluations + .iter_mut() + .find(|evaluation| !evaluation.rewarded_or_slashed) + .expect("user_evaluations can only exist if an item here is found; qed"); + + match Pallet::::do_evaluation_reward_or_slash( + T::PalletId::get().into_account_truncating(), + evaluation.project_id, + evaluation.evaluator.clone(), + evaluation.id, + ) { + Ok(_) => (), + Err(e) => Pallet::::deposit_event(Event::EvaluationRewardOrSlashFailed { + project_id: evaluation.project_id, + evaluator: evaluation.evaluator.clone(), + id: evaluation.id, + error: e, + }), + }; + + // if the evaluation outcome failed, we still want to flag it as rewarded or slashed. Otherwise the automatic + // transition will get stuck. + evaluation.rewarded_or_slashed = true; + + Evaluations::::insert(project_id, evaluation.evaluator.clone(), user_evaluations); + + (Weight::zero(), remaining_evaluations.saturating_sub(1u64)) + } else { + (Weight::zero(), 0u64) + } +} + +fn unbond_one_evaluation(project_id: T::ProjectIdentifier) -> (Weight, u64) { + let project_evaluations: Vec<_> = Evaluations::::iter_prefix_values(project_id).collect(); + let evaluation_count = project_evaluations.iter().flatten().count() as u64; + + let mut maybe_user_evaluations = project_evaluations + .into_iter() + .find(|evaluations| evaluations.iter().any(|e| e.rewarded_or_slashed)); + + if let Some(mut user_evaluations) = maybe_user_evaluations { + let mut evaluation = user_evaluations + .iter_mut() + .find(|evaluation| evaluation.rewarded_or_slashed) + .expect("user_evaluations can only exist if an item here is found; qed"); + + match Pallet::::do_evaluation_unbond_for( + T::PalletId::get().into_account_truncating(), + evaluation.project_id, + evaluation.evaluator.clone(), + evaluation.id, + ) { + Ok(_) => (), + Err(e) => Pallet::::deposit_event(Event::EvaluationUnbondFailed { + project_id: evaluation.project_id, + evaluator: evaluation.evaluator.clone(), + id: evaluation.id, + error: e, + }), + }; + (Weight::zero(), evaluation_count.saturating_sub(1u64)) + } else { + (Weight::zero(), 0u64) + } +} + +fn release_funds_one_bid(project_id: T::ProjectIdentifier) -> (Weight, u64) { + let project_bids: Vec<_> = Bids::::iter_prefix_values(project_id).collect(); + let remaining_bids = project_bids.iter().flatten().filter(|bid| !bid.funds_released).count() as u64; + let maybe_user_bids = project_bids + .into_iter() + .find(|bids| bids.iter().any(|bid| !bid.funds_released)); + + if let Some(mut user_bids) = maybe_user_bids { + let mut bid = user_bids + .iter_mut() + .find(|bid| !bid.funds_released) + .expect("user_bids can only exist if an item here is found; qed"); + + match Pallet::::do_release_bid_funds_for( + T::PalletId::get().into_account_truncating(), + bid.project_id, + bid.bidder.clone(), + bid.id, + ) { + Ok(_) => (), + Err(e) => Pallet::::deposit_event(Event::ReleaseBidFundsFailed { + project_id: bid.project_id, + bidder: bid.bidder.clone(), + id: bid.id, + error: e, + }), + }; + + bid.funds_released = true; + + Bids::::insert(project_id, bid.bidder.clone(), user_bids); + + (Weight::zero(), remaining_bids.saturating_sub(1u64)) + } else { + (Weight::zero(), 0u64) + } +} + +fn unbond_one_bid(project_id: T::ProjectIdentifier) -> (Weight, u64) { + let project_bids: Vec<_> = Bids::::iter_prefix_values(project_id).collect(); + // let bids_count = project_bids.iter().flatten().count() as u64; + // remove when do_bid_unbond_for is correctly implemented + let bids_count = 0u64; + + let mut maybe_user_bids = project_bids + .into_iter() + .find(|bids| bids.iter().any(|e| e.funds_released)); + + if let Some(mut user_bids) = maybe_user_bids { + let mut bid = user_bids + .iter_mut() + .find(|bid| bid.funds_released) + .expect("user_evaluations can only exist if an item here is found; qed"); + + match Pallet::::do_bid_unbond_for( + T::PalletId::get().into_account_truncating(), + bid.project_id, + bid.bidder.clone(), + bid.id, + ) { + Ok(_) => (), + Err(e) => Pallet::::deposit_event(Event::BidUnbondFailed { + project_id: bid.project_id, + bidder: bid.bidder.clone(), + id: bid.id, + error: e, + }), + }; + (Weight::zero(), bids_count.saturating_sub(1u64)) + } else { + (Weight::zero(), 0u64) + } +} + +fn release_funds_one_contribution(project_id: T::ProjectIdentifier) -> (Weight, u64) { + let project_contributions: Vec<_> = Contributions::::iter_prefix_values(project_id).collect(); + // let remaining_contributions = project_contributions + // .iter() + // .flatten() + // .filter(|contribution| !contribution.funds_released) + // .count() as u64; + // remove when do_release_contribution_funds_for is correctly implemented + let remaining_contributions = 0u64; + let maybe_user_contributions = project_contributions + .into_iter() + .find(|contributions| contributions.iter().any(|contribution| !contribution.funds_released)); + + if let Some(mut user_contributions) = maybe_user_contributions { + let mut contribution = user_contributions + .iter_mut() + .find(|contribution| !contribution.funds_released) + .expect("user_contributions can only exist if an item here is found; qed"); + + match Pallet::::do_release_contribution_funds_for( + T::PalletId::get().into_account_truncating(), + contribution.project_id, + contribution.contributor.clone(), + contribution.id, + ) { + Ok(_) => (), + Err(e) => Pallet::::deposit_event(Event::ReleaseContributionFundsFailed { + project_id: contribution.project_id, + contributor: contribution.contributor.clone(), + id: contribution.id, + error: e, + }), + }; + + contribution.funds_released = true; + + Contributions::::insert(project_id, contribution.contributor.clone(), user_contributions); + + (Weight::zero(), remaining_contributions.saturating_sub(1u64)) + } else { + (Weight::zero(), 0u64) + } +} + +fn unbond_one_contribution(project_id: T::ProjectIdentifier) -> (Weight, u64) { + let project_contributions: Vec<_> = Contributions::::iter_prefix_values(project_id).collect(); - let mut evaluation = user_evaluations - .iter_mut() - .find(|evaluation| !evaluation.rewarded_or_slashed) - .expect("user_evaluations can only be Some if an item here is found; qed"); + // let contributions_count = project_contributions.iter().flatten().count() as u64; + let contributions_count = 0u64; - Pallet::::do_evaluation_reward_or_slash( - T::PalletId::get().into_account_truncating(), - evaluation.project_id, - evaluation.evaluator.clone(), - evaluation.id, - ) - .map_err(|_| ())?; - evaluation.rewarded_or_slashed = true; + let mut maybe_user_contributions = project_contributions + .into_iter() + .find(|contributions| contributions.iter().any(|e| e.funds_released)); - Evaluations::::insert(project_id, evaluation.evaluator.clone(), user_evaluations); + if let Some(mut user_contributions) = maybe_user_contributions { + let mut contribution = user_contributions + .iter_mut() + .find(|contribution| contribution.funds_released) + .expect("user_evaluations can only exist if an item here is found; qed"); - Ok(Weight::zero()) + match Pallet::::do_contribution_unbond_for( + T::PalletId::get().into_account_truncating(), + contribution.project_id, + contribution.contributor.clone(), + contribution.id, + ) { + Ok(_) => (), + Err(e) => Pallet::::deposit_event(Event::ContributionUnbondFailed { + project_id: contribution.project_id, + contributor: contribution.contributor.clone(), + id: contribution.id, + error: e, + }), + }; + (Weight::zero(), contributions_count.saturating_sub(1u64)) + } else { + (Weight::zero(), 0u64) + } } -fn unbond_one_evaluation(project_id: T::ProjectIdentifier) -> Result { - let mut user_evaluations = Evaluations::::iter_prefix_values(project_id) - .find(|evaluations| evaluations.iter().any(|e| e.rewarded_or_slashed)) - .ok_or(())?; +fn start_bid_plmc_vesting_schedule(project_id: T::ProjectIdentifier) -> (Weight, u64) { + // TODO: change when new vesting schedule is implemented + (Weight::zero(), 0u64) +} - let mut evaluation = user_evaluations - .iter_mut() - .find(|evaluation| evaluation.rewarded_or_slashed) - .expect("user_evaluations can only be Some if an item here is found; qed"); +fn start_contribution_plmc_vesting_schedule(project_id: T::ProjectIdentifier) -> (Weight, u64) { + // TODO: change when new vesting schedule is implemented + (Weight::zero(), 0u64) +} - Pallet::::do_evaluation_unbond_for( - T::PalletId::get().into_account_truncating(), - evaluation.project_id, - evaluation.evaluator.clone(), - evaluation.id, - ) - .map_err(|_| ())?; +fn mint_ct_for_one_bid(project_id: T::ProjectIdentifier) -> (Weight, u64) { + // TODO: Change when new vesting schedule is implemented + (Weight::zero(), 0u64) +} - Ok(Weight::zero()) +fn mint_ct_for_one_contribution(project_id: T::ProjectIdentifier) -> (Weight, u64) { + // TODO: Change when new vesting schedule is implemented + (Weight::zero(), 0u64) } -fn unbond_evaluators( - project_id: T::ProjectIdentifier, max_weight: Weight, -) -> (Weight, OperationsLeft) { +fn issuer_funding_payout_one_bid(project_id: T::ProjectIdentifier) -> (Weight, u64) { + let project_bids: Vec<_> = Bids::::iter_prefix_values(project_id).collect(); + + // let remaining_bids = project_bids + // .iter() + // .flatten() + // .filter(|bid| !bid.funds_released) + // .count() as u64; + let remaining_bids = 0u64; + + let maybe_user_bids = project_bids + .into_iter() + .find(|bids| bids.iter().any(|bid| !bid.funds_released)); + + if let Some(mut user_bids) = maybe_user_bids { + let mut bid = user_bids + .iter_mut() + .find(|bid| !bid.funds_released) + .expect("user_bids can only exist if an item here is found; qed"); + + match Pallet::::do_payout_bid_funds_for( + T::PalletId::get().into_account_truncating(), + bid.project_id, + bid.bidder.clone(), + bid.id, + ) { + Ok(_) => (), + Err(e) => Pallet::::deposit_event(Event::PayoutContributionFundsFailed { + project_id: bid.project_id, + contributor: bid.bidder.clone(), + id: bid.id, + error: e, + }), + }; + + bid.funds_released = true; + + Bids::::insert(project_id, bid.bidder.clone(), user_bids); + + (Weight::zero(), remaining_bids.saturating_sub(1u64)) + } else { + (Weight::zero(), 0u64) + } +} + +fn issuer_funding_payout_one_contribution(project_id: T::ProjectIdentifier) -> (Weight, u64) { + let project_contributions: Vec<_> = Contributions::::iter_prefix_values(project_id).collect(); + + // let remaining_contributions = project_contributions + // .iter() + // .flatten() + // .filter(|contribution| !contribution.funds_released) + // .count() as u64; + let remaining_contributions = 0u64; + + let maybe_user_contributions = project_contributions + .into_iter() + .find(|contributions| contributions.iter().any(|contribution| !contribution.funds_released)); + + if let Some(mut user_contributions) = maybe_user_contributions { + let mut contribution = user_contributions + .iter_mut() + .find(|contribution| !contribution.funds_released) + .expect("user_contributions can only exist if an item here is found; qed"); + + match Pallet::::do_payout_contribution_funds_for( + T::PalletId::get().into_account_truncating(), + contribution.project_id, + contribution.contributor.clone(), + contribution.id, + ) { + Ok(_) => (), + Err(e) => Pallet::::deposit_event(Event::PayoutContributionFundsFailed { + project_id: contribution.project_id, + contributor: contribution.contributor.clone(), + id: contribution.id, + error: e, + }), + }; + + contribution.funds_released = true; + + Contributions::::insert(project_id, contribution.contributor.clone(), user_contributions); + + (Weight::zero(), remaining_contributions.saturating_sub(1u64)) + } else { + (Weight::zero(), 0u64) + } +} + +// might come in handy later +#[allow(unused)] +fn unbond_evaluators(project_id: T::ProjectIdentifier, max_weight: Weight) -> (Weight, OperationsLeft) { let evaluations = Evaluations::::iter_prefix_values(project_id) .flatten() .collect::>>(); @@ -217,14 +673,7 @@ fn unbond_evaluators( let successful_results = unbond_results .into_iter() - .filter(|result| { - if let Err(e) = result { - Pallet::::deposit_event(Event::EvaluationUnbondFailed { error: *e }); - false - } else { - true - } - }) + .filter(|result| if let Err(e) = result { false } else { true }) .collect::>(); let operations_left = if successful_results.len() == evaluations.len() { diff --git a/polimec-skeleton/pallets/funding/src/lib.rs b/polimec-skeleton/pallets/funding/src/lib.rs index cd10ffd0d..cdcae080e 100644 --- a/polimec-skeleton/pallets/funding/src/lib.rs +++ b/polimec-skeleton/pallets/funding/src/lib.rs @@ -543,7 +543,12 @@ pub mod pallet { error: DispatchError, }, /// Something terribly wrong happened where the bond could not be unbonded. Most likely a programming error - EvaluationUnbondFailed { error: DispatchError }, + EvaluationUnbondFailed { + project_id: ProjectIdOf, + evaluator: AccountIdOf, + id: StorageItemIdOf, + error: DispatchError, + }, /// Contribution tokens were minted to a user ContributionTokenMinted { caller: AccountIdOf, @@ -553,6 +558,48 @@ pub mod pallet { }, /// A transfer of tokens failed, but because it was done inside on_initialize it cannot be solved. TransferError { error: DispatchError }, + EvaluationRewardOrSlashFailed { + project_id: ProjectIdOf, + evaluator: AccountIdOf, + id: StorageItemIdOf, + error: DispatchError, + }, + ReleaseBidFundsFailed { + project_id: ProjectIdOf, + bidder: AccountIdOf, + id: StorageItemIdOf, + error: DispatchError, + }, + BidUnbondFailed { + project_id: ProjectIdOf, + bidder: AccountIdOf, + id: StorageItemIdOf, + error: DispatchError, + }, + ReleaseContributionFundsFailed { + project_id: ProjectIdOf, + contributor: AccountIdOf, + id: StorageItemIdOf, + error: DispatchError, + }, + ContributionUnbondFailed { + project_id: ProjectIdOf, + contributor: AccountIdOf, + id: StorageItemIdOf, + error: DispatchError, + }, + PayoutContributionFundsFailed { + project_id: ProjectIdOf, + contributor: AccountIdOf, + id: StorageItemIdOf, + error: DispatchError, + }, + PayoutBidFundsFailed { + project_id: ProjectIdOf, + bidder: AccountIdOf, + id: StorageItemIdOf, + error: DispatchError, + }, } #[pallet::error] @@ -864,10 +911,8 @@ pub mod pallet { let projects_needing_cleanup = ProjectsDetails::::iter() .filter_map(|(project_id, info)| match info.cleanup { - ProjectCleanup::Ready(remaining_operations) - if remaining_operations != RemainingOperations::None => - { - Some((project_id, remaining_operations)) + ProjectCleanup::Ready(project_finalizer) if project_finalizer != ProjectFinalizer::None => { + Some((project_id, project_finalizer)) } _ => None, }) @@ -880,12 +925,12 @@ pub mod pallet { let mut max_weight_per_project = remaining_weight.saturating_div(projects_amount); - for (remaining_projects, (project_id, mut remaining_ops)) in + for (remaining_projects, (project_id, mut project_finalizer)) in projects_needing_cleanup.into_iter().enumerate().rev() { let mut consumed_weight = T::WeightInfo::insert_cleaned_project(); while !consumed_weight.any_gt(max_weight_per_project) { - if let Ok(weight) = remaining_ops.do_one_operation::(project_id) { + if let Ok(weight) = project_finalizer.do_one_operation::(project_id) { consumed_weight += weight } else { break; @@ -896,10 +941,10 @@ pub mod pallet { } else { continue; }; - if let RemainingOperations::None = remaining_ops { + if let ProjectFinalizer::None = project_finalizer { details.cleanup = ProjectCleanup::Finished; } else { - details.cleanup = ProjectCleanup::Ready(remaining_ops); + details.cleanup = ProjectCleanup::Ready(project_finalizer); } ProjectsDetails::::insert(project_id, details); @@ -909,7 +954,7 @@ pub mod pallet { } } - // // TODO: PLMC-127. Set a proper weight + // TODO: PLMC-127. Set a proper weight max_weight.saturating_sub(remaining_weight) } } diff --git a/polimec-skeleton/pallets/funding/src/types.rs b/polimec-skeleton/pallets/funding/src/types.rs index 353fec601..6d1b79347 100644 --- a/polimec-skeleton/pallets/funding/src/types.rs +++ b/polimec-skeleton/pallets/funding/src/types.rs @@ -191,6 +191,7 @@ pub mod storage_types { pub plmc_vesting_period: PlmcVesting, pub ct_vesting_period: CTVesting, pub when: BlockNumber, + pub funds_released: bool, } impl< @@ -241,6 +242,7 @@ pub mod storage_types { pub plmc_bond: Balance, pub plmc_vesting_period: PLMCVesting, pub ct_vesting_period: CTVesting, + pub funds_released: bool, } } @@ -487,62 +489,42 @@ pub mod inner_types { pub enum ProjectCleanup { #[default] NotReady, - Ready(RemainingOperations), + Ready(ProjectFinalizer), Finished, } #[derive(Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug, TypeInfo, MaxEncodedLen)] - pub enum RemainingOperations { - Success(SuccessRemainingOperations), - Failure(FailureRemainingOperations), + pub enum ProjectFinalizer { + Success(SuccessFinalizer), + Failure(FailureFinalizer), None, } - #[derive(Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug, TypeInfo, MaxEncodedLen)] - pub struct SuccessRemainingOperations { - pub evaluation_reward_or_slash: bool, - pub evaluation_unbonding: bool, - pub bidder_plmc_vesting: bool, - pub bidder_ct_mint: bool, - pub contributor_plmc_vesting: bool, - pub contributor_ct_mint: bool, - pub bids_funding_to_issuer_transfer: bool, - pub contributions_funding_to_issuer_transfer: bool, - } - impl Default for SuccessRemainingOperations { - fn default() -> Self { - Self { - evaluation_reward_or_slash: true, - evaluation_unbonding: true, - bidder_plmc_vesting: true, - bidder_ct_mint: true, - contributor_plmc_vesting: true, - contributor_ct_mint: true, - bids_funding_to_issuer_transfer: true, - contributions_funding_to_issuer_transfer: true, - } - } + #[derive(Default, Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug, TypeInfo, MaxEncodedLen)] + pub enum SuccessFinalizer { + #[default] + Initialized, + EvaluationRewardOrSlash(u64), + EvaluationUnbonding(u64), + BidPLMCVesting(u64), + BidCTMint(u64), + ContributionPLMCVesting(u64), + ContributionCTMint(u64), + BidFundingPayout(u64), + ContributionFundingPayout(u64), + Finished, } - #[derive(Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug, TypeInfo, MaxEncodedLen)] - pub struct FailureRemainingOperations { - pub evaluation_reward_or_slash: bool, - pub evaluation_unbonding: bool, - pub bidder_plmc_unbonding: bool, - pub contributor_plmc_unbonding: bool, - pub bids_funding_to_bidder_return: bool, - pub contributions_funding_to_contributor_return: bool, - } - impl Default for FailureRemainingOperations { - fn default() -> Self { - Self { - evaluation_reward_or_slash: true, - evaluation_unbonding: true, - bidder_plmc_unbonding: true, - contributor_plmc_unbonding: true, - bids_funding_to_bidder_return: true, - contributions_funding_to_contributor_return: true, - } - } + #[derive(Default, Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug, TypeInfo, MaxEncodedLen)] + pub enum FailureFinalizer { + #[default] + Initialized, + EvaluationRewardOrSlash(u64), + EvaluationUnbonding(u64), + BidFundingRelease(u64), + BidUnbonding(u64), + ContributionFundingRelease(u64), + ContributionUnbonding(u64), + Finished, } } From 8f04bbf5ecc9e0a405d630e1f020002d644a24c3 Mon Sep 17 00:00:00 2001 From: Juan Ignacio Rios Date: Tue, 11 Jul 2023 17:20:25 +0200 Subject: [PATCH 13/27] wip --- .../pallets/funding/src/functions.rs | 86 +++++++++---------- polimec-skeleton/pallets/funding/src/tests.rs | 1 + 2 files changed, 44 insertions(+), 43 deletions(-) diff --git a/polimec-skeleton/pallets/funding/src/functions.rs b/polimec-skeleton/pallets/funding/src/functions.rs index eb6efce04..41e33a0e1 100644 --- a/polimec-skeleton/pallets/funding/src/functions.rs +++ b/polimec-skeleton/pallets/funding/src/functions.rs @@ -897,49 +897,6 @@ impl Pallet { Ok(()) } - pub fn do_evaluation_unbond_for( - releaser: AccountIdOf, project_id: T::ProjectIdentifier, evaluator: AccountIdOf, - evaluation_id: T::StorageItemId, - ) -> Result<(), DispatchError> { - // * Get variables * - let project_details = ProjectsDetails::::get(project_id).ok_or(Error::::ProjectInfoNotFound)?; - let mut user_evaluations = Evaluations::::get(project_id, evaluator.clone()); - let evaluation_position = user_evaluations - .iter() - .position(|evaluation| evaluation.id == evaluation_id) - .ok_or(Error::::EvaluationNotFound)?; - let released_evaluation = user_evaluations.swap_remove(evaluation_position); - - // * Validity checks * - ensure!( - released_evaluation.rewarded_or_slashed == true - && matches!( - project_details.status, - ProjectStatus::EvaluationFailed | ProjectStatus::FundingFailed | ProjectStatus::FundingSuccessful - ), - Error::::NotAllowed - ); - - // * Update Storage * - T::NativeCurrency::release( - &LockType::Evaluation(project_id), - &evaluator, - released_evaluation.current_plmc_bond, - Precision::Exact, - )?; - Evaluations::::set(project_id, evaluator.clone(), user_evaluations); - - // * Emit events * - Self::deposit_event(Event::::BondReleased { - project_id, - amount: released_evaluation.current_plmc_bond, - bonder: evaluator, - releaser, - }); - - Ok(()) - } - /// Bid for a project in the bidding stage /// /// # Arguments @@ -1534,6 +1491,49 @@ impl Pallet { Ok(()) } + pub fn do_evaluation_unbond_for( + releaser: AccountIdOf, project_id: T::ProjectIdentifier, evaluator: AccountIdOf, + evaluation_id: T::StorageItemId, + ) -> Result<(), DispatchError> { + // * Get variables * + let project_details = ProjectsDetails::::get(project_id).ok_or(Error::::ProjectInfoNotFound)?; + let mut user_evaluations = Evaluations::::get(project_id, evaluator.clone()); + let evaluation_position = user_evaluations + .iter() + .position(|evaluation| evaluation.id == evaluation_id) + .ok_or(Error::::EvaluationNotFound)?; + let released_evaluation = user_evaluations.swap_remove(evaluation_position); + + // * Validity checks * + ensure!( + released_evaluation.rewarded_or_slashed == true + && matches!( + project_details.status, + ProjectStatus::EvaluationFailed | ProjectStatus::FundingFailed | ProjectStatus::FundingSuccessful + ), + Error::::NotAllowed + ); + + // * Update Storage * + T::NativeCurrency::release( + &LockType::Evaluation(project_id), + &evaluator, + released_evaluation.current_plmc_bond, + Precision::Exact, + )?; + Evaluations::::set(project_id, evaluator.clone(), user_evaluations); + + // * Emit events * + Self::deposit_event(Event::::BondReleased { + project_id, + amount: released_evaluation.current_plmc_bond, + bonder: evaluator, + releaser, + }); + + Ok(()) + } + pub fn do_evaluation_reward_or_slash( caller: AccountIdOf, project_id: T::ProjectIdentifier, evaluator: AccountIdOf, evaluation_id: StorageItemIdOf, diff --git a/polimec-skeleton/pallets/funding/src/tests.rs b/polimec-skeleton/pallets/funding/src/tests.rs index eb68531aa..bf00faca8 100644 --- a/polimec-skeleton/pallets/funding/src/tests.rs +++ b/polimec-skeleton/pallets/funding/src/tests.rs @@ -2523,6 +2523,7 @@ mod auction_round_success { 17_6_666_666_666 ); } + } #[cfg(test)] From 6950a98add89ae14ee20ae2c93d3735265428410 Mon Sep 17 00:00:00 2001 From: Juan Ignacio Rios Date: Wed, 12 Jul 2023 19:01:56 +0200 Subject: [PATCH 14/27] feat(214): Fixed most tests. Unbonding tests are now failing due to new implementation pending. Logic for reward calculation is now correct. But the extrinsic implementation is still pending... --- .../pallets/funding/src/functions.rs | 29 +-- polimec-skeleton/pallets/funding/src/impls.rs | 3 +- polimec-skeleton/pallets/funding/src/tests.rs | 173 +++++++++++++++--- 3 files changed, 163 insertions(+), 42 deletions(-) diff --git a/polimec-skeleton/pallets/funding/src/functions.rs b/polimec-skeleton/pallets/funding/src/functions.rs index 41e33a0e1..307421658 100644 --- a/polimec-skeleton/pallets/funding/src/functions.rs +++ b/polimec-skeleton/pallets/funding/src/functions.rs @@ -250,7 +250,7 @@ impl Pallet { // TODO: PLMC-142. 10% is hardcoded, check if we want to configure it a runtime as explained here: // https://substrate.stackexchange.com/questions/2784/how-to-get-a-percent-portion-of-a-balance: // TODO: PLMC-143. Check if it's safe to use * here - let evaluation_target_usd = Percent::from_percent(10) * fundraising_target_usd; + let evaluation_target_usd = Perbill::from_percent(10) * fundraising_target_usd; let evaluation_target_plmc = current_plmc_price .reciprocal() .ok_or(Error::::BadMath)? @@ -1187,8 +1187,11 @@ impl Pallet { } }); + + // If no CTs remain, end the funding phase if remaining_cts_after_purchase == 0u32.into() { + Self::remove_from_update_store(&project_id)?; Self::add_to_update_store(now + 1u32.into(), (&project_id, UpdateType::FundingEnd)); } @@ -1575,12 +1578,10 @@ impl Pallet { } pub fn do_payout_bid_funds_for( - caller: AccountIdOf, project_id: T::ProjectIdentifier, bidder: AccountIdOf, - bid_id: StorageItemIdOf, + caller: AccountIdOf, project_id: T::ProjectIdentifier, bidder: AccountIdOf, bid_id: StorageItemIdOf, ) -> Result<(), DispatchError> { Ok(()) } - } // Helper functions @@ -1817,6 +1818,7 @@ impl Pallet { .reduce(|a, b| a.saturating_add(b)) .ok_or(Error::::NoBidsFound)?; + let mut final_total_funding_reached_by_bids = BalanceOf::::zero(); // Update the bid in the storage for bid in bids.into_iter() { Bids::::mutate(project_id, bid.bidder.clone(), |bids| -> Result<(), DispatchError> { @@ -1878,7 +1880,10 @@ impl Pallet { final_bid.plmc_bond = plmc_bond_needed; } - + let final_ticket_size = final_bid.final_ct_usd_price + .checked_mul_int(final_bid.final_ct_amount) + .ok_or(Error::::BadMath)?; + final_total_funding_reached_by_bids += final_ticket_size; bids[bid_index] = final_bid; Ok(()) })?; @@ -1890,7 +1895,7 @@ impl Pallet { info.weighted_average_price = Some(weighted_token_price); info.remaining_contribution_tokens = info.remaining_contribution_tokens.saturating_sub(bid_token_amount_sum); - info.funding_amount_reached = info.funding_amount_reached.saturating_add(bid_usd_value_sum); + info.funding_amount_reached = info.funding_amount_reached.saturating_add(final_total_funding_reached_by_bids); Ok(()) } else { Err(Error::::ProjectNotFound.into()) @@ -2043,13 +2048,13 @@ impl Pallet { let funding_reached = project_details.funding_amount_reached; // This is the "Y" variable from the knowledge hub - let percentage_of_target_funding = Percent::from_rational(funding_reached, target_funding); + let percentage_of_target_funding = Perbill::from_rational(funding_reached, target_funding); let fees = Self::calculate_fees(project_id)?; - let evaluator_fees = percentage_of_target_funding * (Percent::from_percent(30) * fees); + let evaluator_fees = percentage_of_target_funding * (Perbill::from_percent(30) * fees); - let early_evaluator_rewards = Percent::from_percent(20) * evaluator_fees; - let all_evaluator_rewards = Percent::from_percent(80) * evaluator_fees; + let early_evaluator_rewards = Perbill::from_percent(20) * evaluator_fees; + let all_evaluator_rewards = Perbill::from_percent(80) * evaluator_fees; let early_evaluator_total_locked = evaluation_usd_amounts .iter() @@ -2064,8 +2069,8 @@ impl Pallet { let evaluator_usd_rewards = evaluation_usd_amounts .into_iter() .map(|(evaluator, (early, late))| { - let early_evaluator_weight = Percent::from_rational(early, early_evaluator_total_locked); - let all_evaluator_weight = Percent::from_rational(early + late, all_evaluator_total_locked); + let early_evaluator_weight = Perbill::from_rational(early, early_evaluator_total_locked); + let all_evaluator_weight = Perbill::from_rational(early + late, all_evaluator_total_locked); let early_reward = early_evaluator_weight * early_evaluator_rewards; let all_reward = all_evaluator_weight * all_evaluator_rewards; diff --git a/polimec-skeleton/pallets/funding/src/impls.rs b/polimec-skeleton/pallets/funding/src/impls.rs index 399b714b4..94d82fd04 100644 --- a/polimec-skeleton/pallets/funding/src/impls.rs +++ b/polimec-skeleton/pallets/funding/src/impls.rs @@ -133,7 +133,7 @@ impl DoRemainingOperation for SuccessFinalizer { Ok(consumed_weight) } } - SuccessFinalizer::Finished => {Err(())} + SuccessFinalizer::Finished => Err(()), } } } @@ -500,7 +500,6 @@ fn unbond_one_contribution(project_id: T::ProjectIdentifier) -> (Weig // let contributions_count = project_contributions.iter().flatten().count() as u64; let contributions_count = 0u64; - let mut maybe_user_contributions = project_contributions .into_iter() .find(|contributions| contributions.iter().any(|e| e.funds_released)); diff --git a/polimec-skeleton/pallets/funding/src/tests.rs b/polimec-skeleton/pallets/funding/src/tests.rs index bf00faca8..c2cdafe67 100644 --- a/polimec-skeleton/pallets/funding/src/tests.rs +++ b/polimec-skeleton/pallets/funding/src/tests.rs @@ -249,6 +249,16 @@ trait ProjectInstance { fn in_ext(&self, execute: impl FnOnce() -> R) -> R { self.get_test_environment().ext_env.borrow_mut().execute_with(execute) } + fn get_update_pair(&self) -> (BlockNumber, UpdateType) { + self.in_ext(|| { + ProjectsToUpdate::::iter().find_map(|(block, update_vec)| { + update_vec + .iter() + .find(|(project_id, update)| *project_id == self.get_project_id()) + .map(|(project_id, update)| (block, update.clone())) + }).unwrap() + }) + } } // Initial instance of a test @@ -880,6 +890,25 @@ impl<'a> CommunityFundingProject<'a> { project_id: self.project_id, } } + + fn finish_funding(self) -> FinishedProject<'a> { + let test_env = self.get_test_environment(); + let (update_block, _) = self.get_update_pair(); + test_env.advance_time(update_block.saturating_sub(test_env.current_block())).unwrap(); + if self.get_project_details().status == ProjectStatus::RemainderRound { + let (end_block, _) = self.get_update_pair(); + self.test_env + .advance_time(end_block.saturating_sub(self.test_env.current_block())) + .unwrap(); + } + let project_details = self.get_project_details(); + assert!(matches!(project_details.status, ProjectStatus::FundingSuccessful) || matches!(project_details.status, ProjectStatus::FundingFailed), "Project should be in Finished status"); + FinishedProject { + test_env: self.test_env, + issuer: self.issuer, + project_id: self.project_id, + } + } } #[derive(Debug)] @@ -935,7 +964,7 @@ impl<'a> RemainderFundingProject<'a> { test_env.get_free_statemint_asset_balances_for(asset_id, contributors.clone()); let plmc_evaluation_deposits = calculate_evaluation_plmc_spent(evaluations.clone()); - let plmc_bid_deposits = calculate_auction_plmc_spent(bids.clone()); + let plmc_bid_deposits = calculate_auction_plmc_spent_after_price_calculation(bids.clone(), ct_price); let plmc_contribution_deposits = calculate_contributed_plmc_spent(contributions.clone(), ct_price); let necessary_plmc_mint = @@ -974,6 +1003,9 @@ impl<'a> RemainderFundingProject<'a> { community_funding_project.start_remainder_funding() } + // 227_683_3_333_333_330 + // 1_000_000_0_000_000_000 + fn end_funding(&self) -> FinishedProject<'a> { assert_eq!(self.get_project_details().status, ProjectStatus::RemainderRound); let remainder_funding_end = self @@ -1047,7 +1079,7 @@ impl<'a> FinishedProject<'a> { test_env.get_free_statemint_asset_balances_for(asset_id, contributors.clone()); let plmc_evaluation_deposits = calculate_evaluation_plmc_spent(evaluations.clone()); - let plmc_bid_deposits = calculate_auction_plmc_spent(bids.clone()); + let plmc_bid_deposits = calculate_auction_plmc_spent_after_price_calculation(bids.clone(), ct_price); let plmc_community_contribution_deposits = calculate_contributed_plmc_spent(community_contributions.clone(), ct_price); let plmc_remainder_contribution_deposits = @@ -1175,14 +1207,14 @@ mod defaults { vec![ TestBid::new( BIDDER_1, - 3000 * ASSET_UNIT, - 50_u128.into(), + 50000 * ASSET_UNIT, + 18_u128.into(), None, AcceptedFundingAsset::USDT, ), TestBid::new( BIDDER_2, - 5000 * ASSET_UNIT, + 40000 * ASSET_UNIT, 15_u128.into(), None, AcceptedFundingAsset::USDT, @@ -1192,16 +1224,17 @@ mod defaults { pub fn default_community_buys() -> TestContributions { vec![ - TestContribution::new(BUYER_1, 10 * ASSET_UNIT, None, AcceptedFundingAsset::USDT), - TestContribution::new(BUYER_2, 20 * ASSET_UNIT, None, AcceptedFundingAsset::USDT), + TestContribution::new(BUYER_1, 100 * ASSET_UNIT, None, AcceptedFundingAsset::USDT), + TestContribution::new(BUYER_2, 200 * ASSET_UNIT, None, AcceptedFundingAsset::USDT), + TestContribution::new(BUYER_3, 2000 * ASSET_UNIT, None, AcceptedFundingAsset::USDT), ] } pub fn default_remainder_buys() -> TestContributions { vec![ - TestContribution::new(EVALUATOR_2, 30 * ASSET_UNIT, None, AcceptedFundingAsset::USDT), - TestContribution::new(BUYER_2, 6 * ASSET_UNIT, None, AcceptedFundingAsset::USDT), - TestContribution::new(BIDDER_1, 4 * ASSET_UNIT, None, AcceptedFundingAsset::USDT), + TestContribution::new(EVALUATOR_2, 300 * ASSET_UNIT, None, AcceptedFundingAsset::USDT), + TestContribution::new(BUYER_2, 600 * ASSET_UNIT, None, AcceptedFundingAsset::USDT), + TestContribution::new(BIDDER_1, 4000 * ASSET_UNIT, None, AcceptedFundingAsset::USDT), ] } } @@ -1243,6 +1276,27 @@ pub mod helper_functions { output } + // This differs from `calculate_auction_plmc_spent` in that it recalculates bids over the average price as using that price. + pub fn calculate_auction_plmc_spent_after_price_calculation( + bids: TestBids, price: PriceOf, + ) -> UserToPLMCBalance { + let plmc_price = PriceMap::get().get(&PLMC_STATEMINT_ID).unwrap().clone(); + let mut output = UserToPLMCBalance::new(); + for bid in bids { + let final_price = if bid.price < price { bid.price } else { price }; + + let usd_ticket_size = final_price.saturating_mul_int(bid.amount); + let usd_bond = bid + .multiplier + .unwrap_or_default() + .calculate_bonding_requirement(usd_ticket_size) + .unwrap(); + let plmc_bond = plmc_price.reciprocal().unwrap().saturating_mul_int(usd_bond); + output.push((bid.bidder, plmc_bond)); + } + output + } + pub fn calculate_auction_funding_asset_spent(bids: TestBids) -> UserToStatemintAsset { let mut output = UserToStatemintAsset::new(); for bid in bids { @@ -2278,8 +2332,13 @@ mod auction_round_success { .expect("Candle auction end point should exist"); let mut bidding_account = 1000; - // Imitate the first default bid - let bid_info = default_bids()[0]; + let bid_info = TestBid::new( + 0, + 50u128, + PriceOf::::from_float(15f64), + None, + AcceptedFundingAsset::USDT, + ); let plmc_necessary_funding = calculate_auction_plmc_spent(vec![bid_info.clone()])[0].1; let statemint_asset_necessary_funding = calculate_auction_funding_asset_spent(vec![bid_info.clone()])[0].1; @@ -2523,7 +2582,6 @@ mod auction_round_success { 17_6_666_666_666 ); } - } #[cfg(test)] @@ -3228,10 +3286,9 @@ mod community_round_success { )); } - let expected_price = FixedU128::from_float(38.3333333333f64); + let expected_price = calculate_price_from_test_bids(bids.clone()); let fill_necessary_plmc = calculate_contributed_plmc_spent(fill_contributions.clone(), expected_price); - let fill_necessary_usdt_for_bids = - calculate_contributed_funding_asset_spent(fill_contributions.clone(), expected_price); + let fill_necessary_usdt = calculate_contributed_funding_asset_spent(fill_contributions.clone(), expected_price); let overflow_necessary_plmc = calculate_contributed_plmc_spent(vec![overflow_contribution], expected_price); let overflow_necessary_usdt = @@ -3251,7 +3308,7 @@ mod community_round_success { let project_id = community_funding_project.get_project_id(); test_env.mint_plmc_to(vec![(evaluator_contributor, FUNDED_DELTA_PLMC)]); - test_env.mint_statemint_asset_to(fill_necessary_usdt_for_bids); + test_env.mint_statemint_asset_to(fill_necessary_usdt); test_env.mint_statemint_asset_to(overflow_necessary_usdt); community_funding_project @@ -3371,7 +3428,7 @@ mod remainder_round_success { )); } - let expected_price = FixedU128::from_float(38.3333333333f64); + let expected_price = calculate_price_from_test_bids(bids.clone()); let fill_necessary_plmc = calculate_contributed_plmc_spent(fill_contributions.clone(), expected_price); let fill_necessary_usdt_for_bids = calculate_contributed_funding_asset_spent(fill_contributions.clone(), expected_price); @@ -3582,12 +3639,12 @@ mod bids_vesting { let project_id = finished_project.project_id; let ct_price = finished_project.get_project_details().weighted_average_price.unwrap(); - let plmc_bid_deposits = calculate_auction_plmc_spent(bids.clone()); + let plmc_bid_deposits = calculate_auction_plmc_spent_after_price_calculation(bids.clone(), ct_price); let plmc_community_contribution_deposits = calculate_contributed_plmc_spent(community_contributions.clone(), ct_price); let plmc_remainder_contribution_deposits = calculate_contributed_plmc_spent(remainder_contributions.clone(), ct_price); - let total_plmc_participation_locked = merge_add_mappings_by_user(vec![ + let mut total_plmc_participation_locked = merge_add_mappings_by_user(vec![ plmc_bid_deposits.clone(), plmc_community_contribution_deposits, plmc_remainder_contribution_deposits.clone(), @@ -3871,8 +3928,10 @@ mod test_helper_functions { #[cfg(test)] mod misc_features { + use sp_arithmetic::Perbill; use super::*; use crate::UpdateType::{CommunityFundingStart, RemainderFundingStart}; + use testing_macros::*; #[test] fn remove_from_update_store_works() { @@ -3903,6 +3962,33 @@ mod misc_features { fn get_evaluator_ct_rewards_works() { let test_env = TestEnvironment::new(); + let bounded_name = BoundedVec::try_from("Contribution Token TEST".as_bytes().to_vec()).unwrap(); + let bounded_symbol = BoundedVec::try_from("CTEST".as_bytes().to_vec()).unwrap(); + let metadata_hash = hashed(format!("{}-{}", METADATA, 420)); + let project_metadata = ProjectMetadataOf:: { + token_information: CurrencyMetadata { + name: bounded_name, + symbol: bounded_symbol, + decimals: ASSET_DECIMALS, + }, + mainnet_token_max_supply: 8_000_000_0_000_000_000, + total_allocation_size: 100_000_0_000_000_000, + minimum_price: PriceOf::::from_float(10.0), + ticket_size: TicketSize { + minimum: Some(1), + maximum: None, + }, + participants_size: ParticipantsSize { + minimum: Some(2), + maximum: None, + }, + funding_thresholds: Default::default(), + conversion_rate: 0, + participation_currencies: AcceptedFundingAsset::USDT, + funding_destination_account: ISSUER, + offchain_information_hash: Some(metadata_hash), + }; + // all values taken from the knowledge hub let evaluations: UserToUSDBalance = vec![ (EVALUATOR_1, 75_000 * US_DOLLAR), @@ -3944,19 +4030,38 @@ mod misc_features { TestContribution::new(BUYER_7, 2_000 * ASSET_UNIT, None, AcceptedFundingAsset::USDT), ]; - let project = - RemainderFundingProject::new_with(&test_env, default_project(0), ISSUER, evaluations, bids, contributions); - let details = project.get_project_details(); + let community_funding_project = CommunityFundingProject::new_with( + &test_env, + project_metadata, + ISSUER, + evaluations, + bids, + ); + let details = community_funding_project.get_project_details(); + let ct_price = details.weighted_average_price.unwrap(); + let mut plmc_deposits = calculate_contributed_plmc_spent(contributions.clone(), ct_price); + plmc_deposits = plmc_deposits.into_iter().map(|(account, balance)| (account, balance + get_ed())).collect(); + let funding_deposits = calculate_contributed_funding_asset_spent(contributions.clone(), ct_price); + + test_env.mint_plmc_to(plmc_deposits); + test_env.mint_statemint_asset_to(funding_deposits); + + community_funding_project.buy_for_retail_users(contributions).unwrap(); + let finished_project = community_funding_project.finish_funding(); + let details = finished_project.get_project_details(); let mut ct_evaluation_rewards = - test_env.in_ext(|| FundingModule::get_evaluator_ct_rewards(project.get_project_id()).unwrap()); + test_env.in_ext(|| FundingModule::get_evaluator_ct_rewards(finished_project.get_project_id()).unwrap()); ct_evaluation_rewards.sort_by_key(|item| item.0); let expected_ct_rewards = vec![ - (EVALUATOR_1, 1_236_9_500_000_000), - (EVALUATOR_2, 852_8_100_000_000), - (EVALUATOR_3, 660_2_400_000_000), + (EVALUATOR_1, 1_196_1_509_434_007), + (EVALUATOR_2, 824_0_150_943_427), + (EVALUATOR_3, 637_9_471_698_137), ]; - assert_eq!(ct_evaluation_rewards, expected_ct_rewards); + for (real, desired) in zip(ct_evaluation_rewards.iter(), expected_ct_rewards.iter()) { + assert_eq!(real.0, desired.0, "bad accounts order"); + assert_close_enough!(real.1, desired.1, Perbill::from_parts(1u32)); + } } fn sandbox() { @@ -3973,3 +4078,15 @@ mod misc_features { // 29_4_117_647_058 } } + +mod testing_macros { + #[allow(unused_macros)] + macro_rules! assert_close_enough { + ($real:expr, $desired:expr, $min_approximation:expr) => { + let real_approximation = Perbill::from_rational_approximation($real, $desired); + assert!(real_approximation >= $min_approximation); + }; + } + pub(crate) use assert_close_enough; + +} From aee20325018c33b9a8d6ba46991cdc839b83d562 Mon Sep 17 00:00:00 2001 From: Juan Ignacio Rios Date: Thu, 13 Jul 2023 12:06:40 +0200 Subject: [PATCH 15/27] feat(214): fix failing funding of reward test --- polimec-skeleton/pallets/funding/src/tests.rs | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/polimec-skeleton/pallets/funding/src/tests.rs b/polimec-skeleton/pallets/funding/src/tests.rs index c2cdafe67..4904da578 100644 --- a/polimec-skeleton/pallets/funding/src/tests.rs +++ b/polimec-skeleton/pallets/funding/src/tests.rs @@ -1757,18 +1757,15 @@ mod evaluation_round_success { #[test] fn rewards_are_paid_full_funding() { - // numbers taken fron knowledge hub + // numbers taken from knowledge hub const TARGET_FUNDING_AMOUNT_USD: BalanceOf = 1_000_000 * US_DOLLAR; let evaluator_1_usd_amount: BalanceOf = 75_000 * US_DOLLAR; // Full early evaluator reward let evaluator_2_usd_amount: BalanceOf = 65_000 * US_DOLLAR; // Partial early evaluator reward let evaluator_3_usd_amount: BalanceOf = 60_000 * US_DOLLAR; // No early evaluator reward - let funding_weights = [25, 30, 31, 14]; - assert_eq!( - funding_weights.iter().sum::(), - 100, - "remaining_funding_weights must sum up to 100%" - ); + // 105% funding because price for bids is always a couple points per billion lower than expected + let funding_weights = [25, 30, 35, 15]; + let funding_weights = funding_weights .into_iter() .map(|x| Percent::from_percent(x)) @@ -1792,8 +1789,8 @@ mod evaluation_round_success { (EVALUATOR_3, evaluator_3_usd_amount), ]; - let bidder_1_ct_price = PriceOf::::from_float(4.2f64); - let bidder_2_ct_price = PriceOf::::from_float(2.3f64); + let bidder_1_ct_price = PriceOf::::from_float(14f64); + let bidder_2_ct_price = PriceOf::::from_float(14f64); let bidder_1_ct_amount = bidder_1_ct_price .reciprocal() From a16dcbc5c4008383a2b8bbf49095974bbf53a85a Mon Sep 17 00:00:00 2001 From: Juan Ignacio Rios Date: Thu, 13 Jul 2023 12:22:46 +0200 Subject: [PATCH 16/27] feat(214): fix failing funding of reward test --- .../pallets/funding/src/functions.rs | 9 ++-- polimec-skeleton/pallets/funding/src/tests.rs | 41 +++++++++++-------- 2 files changed, 28 insertions(+), 22 deletions(-) diff --git a/polimec-skeleton/pallets/funding/src/functions.rs b/polimec-skeleton/pallets/funding/src/functions.rs index 307421658..8f8ebc935 100644 --- a/polimec-skeleton/pallets/funding/src/functions.rs +++ b/polimec-skeleton/pallets/funding/src/functions.rs @@ -1187,8 +1187,6 @@ impl Pallet { } }); - - // If no CTs remain, end the funding phase if remaining_cts_after_purchase == 0u32.into() { Self::remove_from_update_store(&project_id)?; @@ -1880,7 +1878,8 @@ impl Pallet { final_bid.plmc_bond = plmc_bond_needed; } - let final_ticket_size = final_bid.final_ct_usd_price + let final_ticket_size = final_bid + .final_ct_usd_price .checked_mul_int(final_bid.final_ct_amount) .ok_or(Error::::BadMath)?; final_total_funding_reached_by_bids += final_ticket_size; @@ -1895,7 +1894,9 @@ impl Pallet { info.weighted_average_price = Some(weighted_token_price); info.remaining_contribution_tokens = info.remaining_contribution_tokens.saturating_sub(bid_token_amount_sum); - info.funding_amount_reached = info.funding_amount_reached.saturating_add(final_total_funding_reached_by_bids); + info.funding_amount_reached = info + .funding_amount_reached + .saturating_add(final_total_funding_reached_by_bids); Ok(()) } else { Err(Error::::ProjectNotFound.into()) diff --git a/polimec-skeleton/pallets/funding/src/tests.rs b/polimec-skeleton/pallets/funding/src/tests.rs index 4904da578..3f150c385 100644 --- a/polimec-skeleton/pallets/funding/src/tests.rs +++ b/polimec-skeleton/pallets/funding/src/tests.rs @@ -251,12 +251,14 @@ trait ProjectInstance { } fn get_update_pair(&self) -> (BlockNumber, UpdateType) { self.in_ext(|| { - ProjectsToUpdate::::iter().find_map(|(block, update_vec)| { - update_vec - .iter() - .find(|(project_id, update)| *project_id == self.get_project_id()) - .map(|(project_id, update)| (block, update.clone())) - }).unwrap() + ProjectsToUpdate::::iter() + .find_map(|(block, update_vec)| { + update_vec + .iter() + .find(|(project_id, update)| *project_id == self.get_project_id()) + .map(|(project_id, update)| (block, update.clone())) + }) + .unwrap() }) } } @@ -894,7 +896,9 @@ impl<'a> CommunityFundingProject<'a> { fn finish_funding(self) -> FinishedProject<'a> { let test_env = self.get_test_environment(); let (update_block, _) = self.get_update_pair(); - test_env.advance_time(update_block.saturating_sub(test_env.current_block())).unwrap(); + test_env + .advance_time(update_block.saturating_sub(test_env.current_block())) + .unwrap(); if self.get_project_details().status == ProjectStatus::RemainderRound { let (end_block, _) = self.get_update_pair(); self.test_env @@ -902,7 +906,11 @@ impl<'a> CommunityFundingProject<'a> { .unwrap(); } let project_details = self.get_project_details(); - assert!(matches!(project_details.status, ProjectStatus::FundingSuccessful) || matches!(project_details.status, ProjectStatus::FundingFailed), "Project should be in Finished status"); + assert!( + matches!(project_details.status, ProjectStatus::FundingSuccessful) + || matches!(project_details.status, ProjectStatus::FundingFailed), + "Project should be in Finished status" + ); FinishedProject { test_env: self.test_env, issuer: self.issuer, @@ -3925,9 +3933,9 @@ mod test_helper_functions { #[cfg(test)] mod misc_features { - use sp_arithmetic::Perbill; use super::*; use crate::UpdateType::{CommunityFundingStart, RemainderFundingStart}; + use sp_arithmetic::Perbill; use testing_macros::*; #[test] @@ -4027,17 +4035,15 @@ mod misc_features { TestContribution::new(BUYER_7, 2_000 * ASSET_UNIT, None, AcceptedFundingAsset::USDT), ]; - let community_funding_project = CommunityFundingProject::new_with( - &test_env, - project_metadata, - ISSUER, - evaluations, - bids, - ); + let community_funding_project = + CommunityFundingProject::new_with(&test_env, project_metadata, ISSUER, evaluations, bids); let details = community_funding_project.get_project_details(); let ct_price = details.weighted_average_price.unwrap(); let mut plmc_deposits = calculate_contributed_plmc_spent(contributions.clone(), ct_price); - plmc_deposits = plmc_deposits.into_iter().map(|(account, balance)| (account, balance + get_ed())).collect(); + plmc_deposits = plmc_deposits + .into_iter() + .map(|(account, balance)| (account, balance + get_ed())) + .collect(); let funding_deposits = calculate_contributed_funding_asset_spent(contributions.clone(), ct_price); test_env.mint_plmc_to(plmc_deposits); @@ -4085,5 +4091,4 @@ mod testing_macros { }; } pub(crate) use assert_close_enough; - } From 58705c5674a862c12012d9c95a7ed1a50742f02d Mon Sep 17 00:00:00 2001 From: Juan Ignacio RIos Date: Thu, 13 Jul 2023 18:16:57 +0200 Subject: [PATCH 17/27] chore(214): remove warnings --- .../pallets/funding/src/functions.rs | 211 ++++++++-- polimec-skeleton/pallets/funding/src/impls.rs | 101 ++--- polimec-skeleton/pallets/funding/src/lib.rs | 16 +- polimec-skeleton/pallets/funding/src/tests.rs | 386 ++++-------------- .../pallets/funding/src/traits.rs | 3 +- polimec-skeleton/pallets/funding/src/types.rs | 20 +- 6 files changed, 338 insertions(+), 399 deletions(-) diff --git a/polimec-skeleton/pallets/funding/src/functions.rs b/polimec-skeleton/pallets/funding/src/functions.rs index 8f8ebc935..36a6cad97 100644 --- a/polimec-skeleton/pallets/funding/src/functions.rs +++ b/polimec-skeleton/pallets/funding/src/functions.rs @@ -22,7 +22,6 @@ use super::*; use crate::traits::{BondingRequirementCalculation, ProvideStatemintPrice}; use frame_support::traits::fungible::InspectHold; -use frame_support::traits::fungibles::Inspect; use frame_support::traits::tokens::{Precision, Preservation}; use frame_support::{ ensure, @@ -36,14 +35,8 @@ use frame_support::{ use sp_arithmetic::Perbill; use sp_arithmetic::traits::{CheckedSub, Zero}; -use sp_runtime::Percent; use sp_std::prelude::*; -use itertools::Itertools; - -pub const US_DOLLAR: u128 = 1_0_000_000_000; -pub const US_CENT: u128 = 0_0_100_000_000; - // Round transition functions impl Pallet { /// Called by user extrinsic @@ -109,6 +102,7 @@ impl Pallet { remaining_contribution_tokens: initial_metadata.total_allocation_size, funding_amount_reached: BalanceOf::::zero(), cleanup: ProjectCleanup::NotReady, + evaluation_reward_or_slash_info: None, }; let project_metadata = initial_metadata; @@ -657,10 +651,11 @@ impl Pallet { let funding_reached = project_details.funding_amount_reached; let funding_is_successful = !(project_details.status == ProjectStatus::FundingFailed || funding_reached < funding_target); - + let evaluation_reward_or_slash_info = Self::generate_evaluation_reward_or_slash_info(project_id)?; if funding_is_successful { project_details.status = ProjectStatus::FundingSuccessful; project_details.cleanup = ProjectCleanup::Ready(ProjectFinalizer::Success(Default::default())); + project_details.evaluation_reward_or_slash_info = Some(evaluation_reward_or_slash_info); // * Update Storage * ProjectsDetails::::insert(project_id, project_details.clone()); @@ -815,7 +810,7 @@ impl Pallet { .ok_or(Error::::BadMath)?; let previous_total_evaluation_bonded_usd = all_existing_evaluations - .map(|(evaluator, evaluations)| { + .map(|(_evaluator, evaluations)| { evaluations.iter().fold(BalanceOf::::zero(), |acc, evaluation| { acc.saturating_add(evaluation.early_usd_amount) .saturating_add(evaluation.late_usd_amount) @@ -1535,48 +1530,103 @@ impl Pallet { Ok(()) } - pub fn do_evaluation_reward_or_slash( + pub fn do_evaluation_reward( caller: AccountIdOf, project_id: T::ProjectIdentifier, evaluator: AccountIdOf, evaluation_id: StorageItemIdOf, ) -> Result<(), DispatchError> { + // * Get variables * + let project_details = ProjectsDetails::::get(project_id).ok_or(Error::::ProjectInfoNotFound)?; + let ct_price = project_details + .weighted_average_price + .ok_or(Error::::ImpossibleState)?; + let reward_info = + if let Some(EvaluationRewardOrSlashInfo::Rewards(info)) = project_details.evaluation_reward_or_slash_info { + info + } else { + return Err(Error::::NotAllowed.into()); + }; + let mut user_evaluations = Evaluations::::get(project_id, evaluator.clone()); + let evaluation = user_evaluations + .iter_mut() + .find(|evaluation| evaluation.id == evaluation_id) + .ok_or(Error::::EvaluationNotFound)?; + + // * Validity checks * + ensure!( + evaluation.rewarded_or_slashed == false + && matches!(project_details.status, ProjectStatus::FundingSuccessful), + Error::::NotAllowed + ); + + // * Calculate variables * + let early_reward_weight = Perbill::from_rational( + evaluation.early_usd_amount, + reward_info.early_evaluator_total_bonded_usd, + ); + let normal_reward_weight = Perbill::from_rational( + evaluation.late_usd_amount.saturating_add(evaluation.early_usd_amount), + reward_info.normal_evaluator_total_bonded_usd, + ); + let total_reward_amount_usd = early_reward_weight * reward_info.early_evaluator_reward_pot_usd + + normal_reward_weight * reward_info.normal_evaluator_reward_pot_usd; + let reward_amount_ct: BalanceOf = ct_price + .reciprocal() + .ok_or(Error::::BadMath)? + .checked_mul_int(total_reward_amount_usd) + .ok_or(Error::::BadMath)?; + + // * Update storage * + T::ContributionTokenCurrency::mint_into(project_id, &evaluation.evaluator, reward_amount_ct)?; + evaluation.rewarded_or_slashed = true; + Evaluations::::set(project_id, evaluator.clone(), user_evaluations); + + // * Emit events * + Self::deposit_event(Event::::EvaluationRewarded { + project_id, + evaluator: evaluator.clone(), + id: evaluation_id, + amount: reward_amount_ct, + caller, + }); + Ok(()) } pub fn do_release_bid_funds_for( - caller: AccountIdOf, project_id: T::ProjectIdentifier, bidder: AccountIdOf, bid_id: StorageItemIdOf, + _caller: AccountIdOf, _project_id: T::ProjectIdentifier, _bidder: AccountIdOf, _bid_id: StorageItemIdOf, ) -> Result<(), DispatchError> { Ok(()) } pub fn do_bid_unbond_for( - caller: AccountIdOf, project_id: T::ProjectIdentifier, bidder: AccountIdOf, bid_id: StorageItemIdOf, + _caller: AccountIdOf, _project_id: T::ProjectIdentifier, _bidder: AccountIdOf, _bid_id: StorageItemIdOf, ) -> Result<(), DispatchError> { Ok(()) } pub fn do_release_contribution_funds_for( - caller: AccountIdOf, project_id: T::ProjectIdentifier, contributor: AccountIdOf, - contribution_id: StorageItemIdOf, + _caller: AccountIdOf, _project_id: T::ProjectIdentifier, _contributor: AccountIdOf, + _contribution_id: StorageItemIdOf, ) -> Result<(), DispatchError> { Ok(()) } pub fn do_contribution_unbond_for( - caller: AccountIdOf, project_id: T::ProjectIdentifier, contributor: AccountIdOf, - contribution_id: StorageItemIdOf, + _caller: AccountIdOf, _project_id: T::ProjectIdentifier, _contributor: AccountIdOf, + _contribution_id: StorageItemIdOf, ) -> Result<(), DispatchError> { Ok(()) } pub fn do_payout_contribution_funds_for( - caller: AccountIdOf, project_id: T::ProjectIdentifier, contributor: AccountIdOf, - contribution_id: StorageItemIdOf, + _caller: AccountIdOf, _project_id: T::ProjectIdentifier, _contributor: AccountIdOf, + _contribution_id: StorageItemIdOf, ) -> Result<(), DispatchError> { Ok(()) } pub fn do_payout_bid_funds_for( - caller: AccountIdOf, project_id: T::ProjectIdentifier, bidder: AccountIdOf, bid_id: StorageItemIdOf, + _caller: AccountIdOf, _project_id: T::ProjectIdentifier, _bidder: AccountIdOf, _bid_id: StorageItemIdOf, ) -> Result<(), DispatchError> { Ok(()) } @@ -2025,7 +2075,12 @@ impl Pallet { pub fn get_evaluator_ct_rewards( project_id: T::ProjectIdentifier, ) -> Result, DispatchError> { - let project_details = ProjectsDetails::::get(project_id).ok_or(Error::::ProjectNotFound)?; + let (early_evaluator_rewards, all_evaluator_rewards, early_evaluator_total_locked, all_evaluator_total_locked) = + Self::get_evaluator_rewards_info(project_id)?; + let ct_price = ProjectsDetails::::get(project_id) + .ok_or(Error::::ProjectNotFound)? + .weighted_average_price + .ok_or(Error::::ImpossibleState)?; let evaluation_usd_amounts = Evaluations::::iter_prefix(project_id) .map(|(evaluator, evaluations)| { ( @@ -2042,31 +2097,6 @@ impl Pallet { ) }) .collect::>(); - let ct_price = project_details - .weighted_average_price - .ok_or(Error::::ImpossibleState)?; - let target_funding = project_details.fundraising_target; - let funding_reached = project_details.funding_amount_reached; - - // This is the "Y" variable from the knowledge hub - let percentage_of_target_funding = Perbill::from_rational(funding_reached, target_funding); - - let fees = Self::calculate_fees(project_id)?; - let evaluator_fees = percentage_of_target_funding * (Perbill::from_percent(30) * fees); - - let early_evaluator_rewards = Perbill::from_percent(20) * evaluator_fees; - let all_evaluator_rewards = Perbill::from_percent(80) * evaluator_fees; - - let early_evaluator_total_locked = evaluation_usd_amounts - .iter() - .fold(BalanceOf::::zero(), |acc, (_, (early, _))| { - acc.saturating_add(*early) - }); - let late_evaluator_total_locked = evaluation_usd_amounts - .iter() - .fold(BalanceOf::::zero(), |acc, (_, (_, late))| acc.saturating_add(*late)); - let all_evaluator_total_locked = early_evaluator_total_locked.saturating_add(late_evaluator_total_locked); - let evaluator_usd_rewards = evaluation_usd_amounts .into_iter() .map(|(evaluator, (early, late))| { @@ -2092,4 +2122,95 @@ impl Pallet { }) .collect() } + + pub fn get_evaluator_rewards_info( + project_id: ::ProjectIdentifier, + ) -> Result< + ( + ::Balance, + ::Balance, + ::Balance, + ::Balance, + ), + DispatchError, + > { + let project_details = ProjectsDetails::::get(project_id).ok_or(Error::::ProjectNotFound)?; + let evaluation_usd_amounts = Evaluations::::iter_prefix(project_id) + .map(|(evaluator, evaluations)| { + ( + evaluator, + evaluations.into_iter().fold( + (BalanceOf::::zero(), BalanceOf::::zero()), + |acc, evaluation| { + ( + acc.0.saturating_add(evaluation.early_usd_amount), + acc.1.saturating_add(evaluation.late_usd_amount), + ) + }, + ), + ) + }) + .collect::>(); + let target_funding = project_details.fundraising_target; + let funding_reached = project_details.funding_amount_reached; + + // This is the "Y" variable from the knowledge hub + let percentage_of_target_funding = Perbill::from_rational(funding_reached, target_funding); + + let fees = Self::calculate_fees(project_id)?; + let evaluator_fees = percentage_of_target_funding * (Perbill::from_percent(30) * fees); + + let early_evaluator_rewards = Perbill::from_percent(20) * evaluator_fees; + let all_evaluator_rewards = Perbill::from_percent(80) * evaluator_fees; + + let early_evaluator_total_locked = evaluation_usd_amounts + .iter() + .fold(BalanceOf::::zero(), |acc, (_, (early, _))| { + acc.saturating_add(*early) + }); + let late_evaluator_total_locked = evaluation_usd_amounts + .iter() + .fold(BalanceOf::::zero(), |acc, (_, (_, late))| acc.saturating_add(*late)); + let all_evaluator_total_locked = early_evaluator_total_locked.saturating_add(late_evaluator_total_locked); + Ok(( + early_evaluator_rewards, + all_evaluator_rewards, + early_evaluator_total_locked, + all_evaluator_total_locked, + )) + } + + pub fn generate_evaluation_reward_or_slash_info( + project_id: T::ProjectIdentifier, + ) -> Result, DispatchError> { + let project_details = ProjectsDetails::::get(project_id).ok_or(Error::::ProjectNotFound)?; + let funding_target = project_details.fundraising_target; + let funding_reached = project_details.funding_amount_reached; + let funding_ratio = Perbill::from_rational(funding_reached, funding_target); + + // Project Automatically rejected, evaluators slashed + if funding_ratio <= Perbill::from_percent(33) { + todo!() + // Project Manually accepted, evaluators slashed + } else if funding_ratio < Perbill::from_percent(75) { + todo!() + // Project Manually accepted, evaluators unaffected + } else if funding_ratio < Perbill::from_percent(90) { + todo!() + // Project Automatically accepted, evaluators rewarded + } else { + let ( + early_evaluator_reward_pot_usd, + normal_evaluator_reward_pot_usd, + early_evaluator_total_bonded_usd, + normal_evaluator_total_bonded_usd, + ) = Self::get_evaluator_rewards_info(project_id)?; + Ok(EvaluationRewardOrSlashInfo::Rewards(RewardInfo { + early_evaluator_reward_pot_usd, + normal_evaluator_reward_pot_usd, + early_evaluator_total_bonded_usd, + normal_evaluator_total_bonded_usd, + })) + } + } } diff --git a/polimec-skeleton/pallets/funding/src/impls.rs b/polimec-skeleton/pallets/funding/src/impls.rs index 94d82fd04..2c190d6c0 100644 --- a/polimec-skeleton/pallets/funding/src/impls.rs +++ b/polimec-skeleton/pallets/funding/src/impls.rs @@ -1,33 +1,31 @@ -use crate::traits::DoRemainingOperation; -use crate::{ - Bids, Config, Contributions, EvaluationInfoOf, Evaluations, Event, FailureFinalizer, Pallet, ProjectFinalizer, - SuccessFinalizer, WeightInfo, -}; +use crate::*; use frame_support::traits::Get; use frame_support::weights::Weight; +use sp_runtime::DispatchError; use sp_runtime::traits::AccountIdConversion; use sp_std::prelude::*; +use crate::traits::DoRemainingOperation; impl DoRemainingOperation for ProjectFinalizer { fn is_done(&self) -> bool { matches!(self, ProjectFinalizer::None) } - fn do_one_operation(&mut self, project_id: T::ProjectIdentifier) -> Result { + fn do_one_operation(&mut self, project_id: T::ProjectIdentifier) -> Result { match self { - ProjectFinalizer::None => Err(()), + ProjectFinalizer::None => Err(Error::::NoFinalizerSet.into()), ProjectFinalizer::Success(ops) => { - let weight = ops.do_one_operation::(project_id); + let weight = ops.do_one_operation::(project_id)?; if ops.is_done() { *self = ProjectFinalizer::None; } - weight + Ok(weight) } ProjectFinalizer::Failure(ops) => { - let weight = ops.do_one_operation::(project_id); + let weight = ops.do_one_operation::(project_id)?; if ops.is_done() { *self = ProjectFinalizer::None; } - weight + Ok(weight) } } } @@ -38,7 +36,7 @@ impl DoRemainingOperation for SuccessFinalizer { matches!(self, SuccessFinalizer::Finished) } - fn do_one_operation(&mut self, project_id: T::ProjectIdentifier) -> Result { + fn do_one_operation(&mut self, project_id: T::ProjectIdentifier) -> Result { match self { SuccessFinalizer::Initialized => { *self = @@ -50,7 +48,7 @@ impl DoRemainingOperation for SuccessFinalizer { *self = SuccessFinalizer::EvaluationUnbonding(remaining_evaluations::(project_id)); Ok(Weight::zero()) } else { - let (consumed_weight, remaining_evaluations) = reward_or_slash_one_evaluation::(project_id); + let (consumed_weight, remaining_evaluations) = reward_or_slash_one_evaluation::(project_id)?; *self = SuccessFinalizer::EvaluationRewardOrSlash(remaining_evaluations); Ok(consumed_weight) } @@ -133,7 +131,7 @@ impl DoRemainingOperation for SuccessFinalizer { Ok(consumed_weight) } } - SuccessFinalizer::Finished => Err(()), + SuccessFinalizer::Finished => Err(Error::::FinalizerFinished.into()), } } } @@ -142,7 +140,7 @@ impl DoRemainingOperation for FailureFinalizer { fn is_done(&self) -> bool { matches!(self, FailureFinalizer::Finished) } - fn do_one_operation(&mut self, project_id: T::ProjectIdentifier) -> Result { + fn do_one_operation(&mut self, project_id: T::ProjectIdentifier) -> Result { match self { FailureFinalizer::Initialized => { *self = @@ -155,7 +153,7 @@ impl DoRemainingOperation for FailureFinalizer { *self = FailureFinalizer::EvaluationUnbonding(remaining_evaluations::(project_id)); Ok(Weight::zero()) } else { - let (consumed_weight, remaining_evaluators) = reward_or_slash_one_evaluation::(project_id); + let (consumed_weight, remaining_evaluators) = reward_or_slash_one_evaluation::(project_id)?; *self = FailureFinalizer::EvaluationRewardOrSlash(remaining_evaluators); Ok(consumed_weight) } @@ -218,7 +216,7 @@ impl DoRemainingOperation for FailureFinalizer { } } - FailureFinalizer::Finished => Err(()), + FailureFinalizer::Finished => Err(Error::::FinalizerFinished.into()), } } } @@ -261,24 +259,24 @@ fn remaining_contributions(project_id: T::ProjectIdentifier) -> u64 { Contributions::::iter_prefix_values(project_id).flatten().count() as u64 } -fn remaining_bids_without_plmc_vesting(project_id: T::ProjectIdentifier) -> u64 { +fn remaining_bids_without_plmc_vesting(_project_id: T::ProjectIdentifier) -> u64 { // TODO: current vesting implementation starts the schedule on bid creation. We should later on use pallet_vesting // and add a check in the bid struct for initializing the vesting schedule 0u64 } -fn remaining_bids_without_ct_minted(project_id: T::ProjectIdentifier) -> u64 { +fn remaining_bids_without_ct_minted(_project_id: T::ProjectIdentifier) -> u64 { // TODO: currently we vest the contribution tokens. We should change this to a direct mint. 0u64 } -fn remaining_contributions_without_plmc_vesting(project_id: T::ProjectIdentifier) -> u64 { +fn remaining_contributions_without_plmc_vesting(_project_id: T::ProjectIdentifier) -> u64 { // TODO: current vesting implementation starts the schedule on contribution creation. We should later on use pallet_vesting // and add a check in the contribution struct for initializing the vesting schedule 0u64 } -fn remaining_contributions_without_ct_minted(project_id: T::ProjectIdentifier) -> u64 { +fn remaining_contributions_without_ct_minted(_project_id: T::ProjectIdentifier) -> u64 { // TODO: currently we vest the contribution tokens. We should change this to a direct mint. 0u64 } @@ -297,7 +295,8 @@ fn remaining_contributions_without_issuer_payout(project_id: T::Proje .count() as u64 } -fn reward_or_slash_one_evaluation(project_id: T::ProjectIdentifier) -> (Weight, u64) { +fn reward_or_slash_one_evaluation(project_id: T::ProjectIdentifier) -> Result<(Weight, u64), DispatchError> { + let project_details = ProjectsDetails::::get(project_id).ok_or(Error::::ProjectNotFound)?; let project_evaluations: Vec<_> = Evaluations::::iter_prefix_values(project_id).collect(); let remaining_evaluations = project_evaluations .iter() @@ -315,30 +314,34 @@ fn reward_or_slash_one_evaluation(project_id: T::ProjectIdentifier) - .find(|evaluation| !evaluation.rewarded_or_slashed) .expect("user_evaluations can only exist if an item here is found; qed"); - match Pallet::::do_evaluation_reward_or_slash( - T::PalletId::get().into_account_truncating(), - evaluation.project_id, - evaluation.evaluator.clone(), - evaluation.id, - ) { - Ok(_) => (), - Err(e) => Pallet::::deposit_event(Event::EvaluationRewardOrSlashFailed { - project_id: evaluation.project_id, - evaluator: evaluation.evaluator.clone(), - id: evaluation.id, - error: e, - }), - }; + match project_details.evaluation_reward_or_slash_info { + Some(EvaluationRewardOrSlashInfo::Rewards(_)) => { + match Pallet::::do_evaluation_reward( + T::PalletId::get().into_account_truncating(), + evaluation.project_id, + evaluation.evaluator.clone(), + evaluation.id, + ) { + Ok(_) => (), + Err(e) => Pallet::::deposit_event(Event::EvaluationRewardOrSlashFailed { + project_id: evaluation.project_id, + evaluator: evaluation.evaluator.clone(), + id: evaluation.id, + error: e, + }), + }; + } + _ => (), + } // if the evaluation outcome failed, we still want to flag it as rewarded or slashed. Otherwise the automatic // transition will get stuck. evaluation.rewarded_or_slashed = true; - Evaluations::::insert(project_id, evaluation.evaluator.clone(), user_evaluations); - (Weight::zero(), remaining_evaluations.saturating_sub(1u64)) + Ok((Weight::zero(), remaining_evaluations.saturating_sub(1u64))) } else { - (Weight::zero(), 0u64) + Ok((Weight::zero(), 0u64)) } } @@ -346,12 +349,12 @@ fn unbond_one_evaluation(project_id: T::ProjectIdentifier) -> let project_evaluations: Vec<_> = Evaluations::::iter_prefix_values(project_id).collect(); let evaluation_count = project_evaluations.iter().flatten().count() as u64; - let mut maybe_user_evaluations = project_evaluations + let maybe_user_evaluations = project_evaluations .into_iter() .find(|evaluations| evaluations.iter().any(|e| e.rewarded_or_slashed)); if let Some(mut user_evaluations) = maybe_user_evaluations { - let mut evaluation = user_evaluations + let evaluation = user_evaluations .iter_mut() .find(|evaluation| evaluation.rewarded_or_slashed) .expect("user_evaluations can only exist if an item here is found; qed"); @@ -420,12 +423,12 @@ fn unbond_one_bid(project_id: T::ProjectIdentifier) -> (Weight, u64) // remove when do_bid_unbond_for is correctly implemented let bids_count = 0u64; - let mut maybe_user_bids = project_bids + let maybe_user_bids = project_bids .into_iter() .find(|bids| bids.iter().any(|e| e.funds_released)); if let Some(mut user_bids) = maybe_user_bids { - let mut bid = user_bids + let bid = user_bids .iter_mut() .find(|bid| bid.funds_released) .expect("user_evaluations can only exist if an item here is found; qed"); @@ -500,12 +503,12 @@ fn unbond_one_contribution(project_id: T::ProjectIdentifier) -> (Weig // let contributions_count = project_contributions.iter().flatten().count() as u64; let contributions_count = 0u64; - let mut maybe_user_contributions = project_contributions + let maybe_user_contributions = project_contributions .into_iter() .find(|contributions| contributions.iter().any(|e| e.funds_released)); if let Some(mut user_contributions) = maybe_user_contributions { - let mut contribution = user_contributions + let contribution = user_contributions .iter_mut() .find(|contribution| contribution.funds_released) .expect("user_evaluations can only exist if an item here is found; qed"); @@ -530,22 +533,22 @@ fn unbond_one_contribution(project_id: T::ProjectIdentifier) -> (Weig } } -fn start_bid_plmc_vesting_schedule(project_id: T::ProjectIdentifier) -> (Weight, u64) { +fn start_bid_plmc_vesting_schedule(_project_id: T::ProjectIdentifier) -> (Weight, u64) { // TODO: change when new vesting schedule is implemented (Weight::zero(), 0u64) } -fn start_contribution_plmc_vesting_schedule(project_id: T::ProjectIdentifier) -> (Weight, u64) { +fn start_contribution_plmc_vesting_schedule(_project_id: T::ProjectIdentifier) -> (Weight, u64) { // TODO: change when new vesting schedule is implemented (Weight::zero(), 0u64) } -fn mint_ct_for_one_bid(project_id: T::ProjectIdentifier) -> (Weight, u64) { +fn mint_ct_for_one_bid(_project_id: T::ProjectIdentifier) -> (Weight, u64) { // TODO: Change when new vesting schedule is implemented (Weight::zero(), 0u64) } -fn mint_ct_for_one_contribution(project_id: T::ProjectIdentifier) -> (Weight, u64) { +fn mint_ct_for_one_contribution(_project_id: T::ProjectIdentifier) -> (Weight, u64) { // TODO: Change when new vesting schedule is implemented (Weight::zero(), 0u64) } diff --git a/polimec-skeleton/pallets/funding/src/lib.rs b/polimec-skeleton/pallets/funding/src/lib.rs index cdcae080e..eefeafb71 100644 --- a/polimec-skeleton/pallets/funding/src/lib.rs +++ b/polimec-skeleton/pallets/funding/src/lib.rs @@ -230,7 +230,9 @@ pub type AssetIdOf = pub type ProjectMetadataOf = ProjectMetadata>, BalanceOf, PriceOf, AccountIdOf, HashOf>; -pub type ProjectDetailsOf = ProjectDetails, BlockNumberOf, PriceOf, BalanceOf>; +pub type ProjectDetailsOf = + ProjectDetails, BlockNumberOf, PriceOf, BalanceOf, EvaluationRewardOrSlashInfoOf>; +pub type EvaluationRewardOrSlashInfoOf = EvaluationRewardOrSlashInfo, BalanceOf>; pub type VestingOf = Vesting, BalanceOf>; pub type EvaluationInfoOf = EvaluationInfo, ProjectIdOf, AccountIdOf, BalanceOf, BlockNumberOf>; @@ -600,6 +602,13 @@ pub mod pallet { id: StorageItemIdOf, error: DispatchError, }, + EvaluationRewarded { + project_id: ProjectIdOf, + evaluator: AccountIdOf, + id: StorageItemIdOf, + amount: BalanceOf, + caller: AccountIdOf, + }, } #[pallet::error] @@ -716,6 +725,10 @@ pub mod pallet { EvaluationBondTooHigh, /// Tried to do an operation on an evaluation that does not exist EvaluationNotFound, + /// Tried to do an operation on a finalizer that is not yet set + NoFinalizerSet, + /// Tried to do an operation on a finalizer that already finished + FinalizerFinished, } #[pallet::call] @@ -906,7 +919,6 @@ pub mod pallet { } fn on_idle(_now: T::BlockNumber, max_weight: Weight) -> Weight { - let pallet_account: AccountIdOf = ::PalletId::get().into_account_truncating(); let mut remaining_weight = max_weight; let projects_needing_cleanup = ProjectsDetails::::iter() diff --git a/polimec-skeleton/pallets/funding/src/tests.rs b/polimec-skeleton/pallets/funding/src/tests.rs index 3f150c385..377bffd6c 100644 --- a/polimec-skeleton/pallets/funding/src/tests.rs +++ b/polimec-skeleton/pallets/funding/src/tests.rs @@ -255,8 +255,8 @@ trait ProjectInstance { .find_map(|(block, update_vec)| { update_vec .iter() - .find(|(project_id, update)| *project_id == self.get_project_id()) - .map(|(project_id, update)| (block, update.clone())) + .find(|(project_id, _update)| *project_id == self.get_project_id()) + .map(|(_project_id, update)| (block, update.clone())) }) .unwrap() }) @@ -536,6 +536,7 @@ impl<'a> CreatedProject<'a> { remaining_contribution_tokens: expected_metadata.total_allocation_size, funding_amount_reached: BalanceOf::::zero(), cleanup: ProjectCleanup::NotReady, + evaluation_reward_or_slash_info: None, }; assert_eq!(metadata, expected_metadata); assert_eq!(details, expected_details); @@ -1558,7 +1559,7 @@ pub mod helper_functions { let last_event = events.into_iter().last().expect("No events found for this action."); match last_event { frame_system::EventRecord { - event: RuntimeEvent::FundingModule(Event::TransitionError { project_id, error }), + event: RuntimeEvent::FundingModule(Event::TransitionError { project_id: _, error }), .. } => Err(error), _ => Ok(()), @@ -1735,7 +1736,8 @@ mod creation_round_failure { #[cfg(test)] mod evaluation_round_success { use super::*; - use sp_arithmetic::{Perbill, Percent}; + use sp_arithmetic::{Perbill}; + use testing_macros::assert_close_enough; #[test] fn evaluation_round_completed() { @@ -1765,201 +1767,94 @@ mod evaluation_round_success { #[test] fn rewards_are_paid_full_funding() { - // numbers taken from knowledge hub - const TARGET_FUNDING_AMOUNT_USD: BalanceOf = 1_000_000 * US_DOLLAR; - let evaluator_1_usd_amount: BalanceOf = 75_000 * US_DOLLAR; // Full early evaluator reward - let evaluator_2_usd_amount: BalanceOf = 65_000 * US_DOLLAR; // Partial early evaluator reward - let evaluator_3_usd_amount: BalanceOf = 60_000 * US_DOLLAR; // No early evaluator reward - - // 105% funding because price for bids is always a couple points per billion lower than expected - let funding_weights = [25, 30, 35, 15]; - - let funding_weights = funding_weights - .into_iter() - .map(|x| Percent::from_percent(x)) - .collect::>(); - - let bidder_1_usd_amount: BalanceOf = funding_weights[0] * TARGET_FUNDING_AMOUNT_USD; - let bidder_2_usd_amount: BalanceOf = funding_weights[1] * TARGET_FUNDING_AMOUNT_USD; - - let buyer_1_usd_amount: BalanceOf = funding_weights[2] * TARGET_FUNDING_AMOUNT_USD; - let buyer_2_usd_amount: BalanceOf = funding_weights[3] * TARGET_FUNDING_AMOUNT_USD; - let test_env = TestEnvironment::new(); - let plmc_price = test_env - .in_ext(|| ::PriceProvider::get_price(PLMC_STATEMINT_ID)) - .unwrap(); + let bounded_name = BoundedVec::try_from("Contribution Token TEST".as_bytes().to_vec()).unwrap(); + let bounded_symbol = BoundedVec::try_from("CTEST".as_bytes().to_vec()).unwrap(); + let metadata_hash = hashed(format!("{}-{}", METADATA, 420)); + let project_metadata = ProjectMetadataOf:: { + token_information: CurrencyMetadata { + name: bounded_name, + symbol: bounded_symbol, + decimals: ASSET_DECIMALS, + }, + mainnet_token_max_supply: 8_000_000_0_000_000_000, + total_allocation_size: 100_000_0_000_000_000, + minimum_price: PriceOf::::from_float(10.0), + ticket_size: TicketSize { + minimum: Some(1), + maximum: None, + }, + participants_size: ParticipantsSize { + minimum: Some(2), + maximum: None, + }, + funding_thresholds: Default::default(), + conversion_rate: 0, + participation_currencies: AcceptedFundingAsset::USDT, + funding_destination_account: ISSUER, + offchain_information_hash: Some(metadata_hash), + }; + // all values taken from the knowledge hub let evaluations: UserToUSDBalance = vec![ - (EVALUATOR_1, evaluator_1_usd_amount), - (EVALUATOR_2, evaluator_2_usd_amount), - (EVALUATOR_3, evaluator_3_usd_amount), + (EVALUATOR_1, 75_000 * US_DOLLAR), + (EVALUATOR_2, 65_000 * US_DOLLAR), + (EVALUATOR_3, 60_000 * US_DOLLAR), ]; - let bidder_1_ct_price = PriceOf::::from_float(14f64); - let bidder_2_ct_price = PriceOf::::from_float(14f64); - - let bidder_1_ct_amount = bidder_1_ct_price - .reciprocal() - .unwrap() - .checked_mul_int(bidder_1_usd_amount) - .unwrap(); - let bidder_2_ct_amount = bidder_2_ct_price - .reciprocal() - .unwrap() - .checked_mul_int(bidder_2_usd_amount) - .unwrap(); - - let final_ct_price = calculate_price_from_test_bids(vec![ + let bids: TestBids = vec![ TestBid::new( BIDDER_1, - bidder_1_ct_amount, - bidder_1_ct_price, + 10_000 * ASSET_UNIT, + 15.into(), None, AcceptedFundingAsset::USDT, ), TestBid::new( BIDDER_2, - bidder_2_ct_amount, - bidder_2_ct_price, - None, - AcceptedFundingAsset::USDT, - ), - ]); - - let buyer_1_ct_amount = final_ct_price - .reciprocal() - .unwrap() - .checked_mul_int(buyer_1_usd_amount) - .unwrap(); - let buyer_2_ct_amount = final_ct_price - .reciprocal() - .unwrap() - .checked_mul_int(buyer_2_usd_amount) - .unwrap(); - - let bids: TestBids = vec![ - TestBid::new( - BIDDER_1, - bidder_1_ct_amount, - bidder_1_ct_price, + 20_000 * ASSET_UNIT, + 20.into(), None, AcceptedFundingAsset::USDT, ), TestBid::new( - BIDDER_2, - bidder_2_ct_amount, - bidder_2_ct_price, + BIDDER_4, + 20_000 * ASSET_UNIT, + 16.into(), None, AcceptedFundingAsset::USDT, ), ]; - let community_contributions = vec![ - TestContribution::new(BUYER_1, buyer_1_ct_amount, None, AcceptedFundingAsset::USDT), - TestContribution::new(BUYER_2, buyer_2_ct_amount, None, AcceptedFundingAsset::USDT), + let contributions: TestContributions = vec![ + TestContribution::new(BUYER_1, 4_000 * ASSET_UNIT, None, AcceptedFundingAsset::USDT), + TestContribution::new(BUYER_2, 2_000 * ASSET_UNIT, None, AcceptedFundingAsset::USDT), + TestContribution::new(BUYER_3, 2_000 * ASSET_UNIT, None, AcceptedFundingAsset::USDT), + TestContribution::new(BUYER_4, 5_000 * ASSET_UNIT, None, AcceptedFundingAsset::USDT), + TestContribution::new(BUYER_5, 30_000 * ASSET_UNIT, None, AcceptedFundingAsset::USDT), + TestContribution::new(BUYER_6, 5_000 * ASSET_UNIT, None, AcceptedFundingAsset::USDT), + TestContribution::new(BUYER_7, 2_000 * ASSET_UNIT, None, AcceptedFundingAsset::USDT), ]; - let project = ProjectMetadataOf:: { - token_information: CurrencyMetadata { - name: "Test Token".as_bytes().to_vec().try_into().unwrap(), - symbol: "TT".as_bytes().to_vec().try_into().unwrap(), - decimals: 10, - }, - mainnet_token_max_supply: 10_000_000 * ASSET_UNIT, - total_allocation_size: 1_000_000 * ASSET_UNIT, - minimum_price: 1u128.into(), - ticket_size: TicketSize::> { - minimum: Some(1), - maximum: None, - }, - participants_size: ParticipantsSize { - minimum: Some(2), - maximum: None, - }, - funding_thresholds: Default::default(), - conversion_rate: 0, - participation_currencies: Default::default(), - funding_destination_account: ISSUER, - offchain_information_hash: Some(hashed(METADATA)), - }; - - let finished_project = FinishedProject::new_with( - &test_env, - project, - ISSUER, - evaluations, - bids, - community_contributions, - vec![], - ); - - let project_id = finished_project.get_project_id(); - - let mut remaining_for_fee = TARGET_FUNDING_AMOUNT_USD; - - let amount_for_10_percent = { - let sub = remaining_for_fee.checked_sub(1_000_000 * US_DOLLAR); - if let Some(sub) = sub { - remaining_for_fee = sub; - 1_000_000 * US_DOLLAR - } else { - let temp = remaining_for_fee; - remaining_for_fee = 0; - temp - } - }; - - let amount_for_8_percent = { - let sub = remaining_for_fee.checked_sub(5_000_000 * US_DOLLAR); - if let Some(sub) = sub { - remaining_for_fee = sub; - 5_000_000 * US_DOLLAR - } else { - let temp = remaining_for_fee; - remaining_for_fee = 0; - temp - } - }; - - let amount_for_6_percent = remaining_for_fee; - - let total_fee = Percent::from_percent(10u8) * amount_for_10_percent - + Percent::from_percent(8u8) * amount_for_8_percent - + Percent::from_percent(6u8) * amount_for_6_percent; - - // "Y" variable is 1, since the full funding amount was reached, which means the full 30% of the fee goes to evaluators - let evaluator_rewards_usd = Percent::from_percent(30) * total_fee; - let total_evaluation_locked = evaluator_1_usd_amount + evaluator_2_usd_amount + evaluator_3_usd_amount; - let early_evaluator_locked = Percent::from_percent(10) * TARGET_FUNDING_AMOUNT_USD; - - let eval_1_all_evaluator_reward_weight = - Perbill::from_rational(evaluator_1_usd_amount, total_evaluation_locked); - let eval_2_all_evaluator_reward_weight = - Perbill::from_rational(evaluator_2_usd_amount, total_evaluation_locked); - let eval_3_all_evaluator_reward_weight = - Perbill::from_rational(evaluator_3_usd_amount, total_evaluation_locked); - - let eval_1_early_evaluator_reward_weight = - Perbill::from_rational(evaluator_1_usd_amount, early_evaluator_locked); - let eval_2_early_evaluator_reward_weight = Perbill::from_rational( - Perbill::from_rational(2u32, 3u32) * evaluator_2_usd_amount, - early_evaluator_locked, - ); - let eval_3_early_evaluator_reward_weight = Perbill::from_percent(0); - - let all_evaluator_rewards_pot = Percent::from_percent(80) * evaluator_rewards_usd; - let early_evaluator_rewards_pot = Percent::from_percent(20) * evaluator_rewards_usd; - - let evaluator_1_all_evaluator_reward = eval_1_all_evaluator_reward_weight * all_evaluator_rewards_pot; - let evaluator_2_all_evaluator_reward = eval_2_all_evaluator_reward_weight * all_evaluator_rewards_pot; - let evaluator_3_all_evaluator_reward = eval_3_all_evaluator_reward_weight * all_evaluator_rewards_pot; + let community_funding_project = + CommunityFundingProject::new_with(&test_env, project_metadata, ISSUER, evaluations, bids); + let details = community_funding_project.get_project_details(); + let ct_price = details.weighted_average_price.unwrap(); + let mut plmc_deposits = calculate_contributed_plmc_spent(contributions.clone(), ct_price); + plmc_deposits = plmc_deposits + .into_iter() + .map(|(account, balance)| (account, balance + get_ed())) + .collect(); + let funding_deposits = calculate_contributed_funding_asset_spent(contributions.clone(), ct_price); - let evaluator_1_early_evaluator_reward = eval_1_early_evaluator_reward_weight * early_evaluator_rewards_pot; - let evaluator_2_early_evaluator_reward = eval_2_early_evaluator_reward_weight * early_evaluator_rewards_pot; - let evaluator_3_early_evaluator_reward = eval_3_early_evaluator_reward_weight * early_evaluator_rewards_pot; + test_env.mint_plmc_to(plmc_deposits); + test_env.mint_statemint_asset_to(funding_deposits); + community_funding_project.buy_for_retail_users(contributions).unwrap(); + let finished_project = community_funding_project.finish_funding(); + test_env.advance_time(10).unwrap(); + let project_id = finished_project.project_id; let actual_reward_balances = test_env.in_ext(|| { vec![ ( @@ -1976,13 +1871,18 @@ mod evaluation_round_success { ), ] }); - let expected_reward_balances = vec![ - (EVALUATOR_1, 1_236_9_500_000_000), - (EVALUATOR_2, 852_8_100_000_000), - (EVALUATOR_3, 660_2_400_000_000), + let expected_ct_rewards = vec![ + (EVALUATOR_1, 1_196_1_509_434_007), + (EVALUATOR_2, 824_0_150_943_427), + (EVALUATOR_3, 637_9_471_698_137), ]; - assert_eq!(actual_reward_balances, expected_reward_balances); + + for (real, desired) in zip(actual_reward_balances.iter(), expected_ct_rewards.iter()) { + assert_eq!(real.0, desired.0, "bad accounts order"); + assert_close_enough!(real.1, desired.1, Perbill::from_parts(1u32)); + } } + } #[cfg(test)] @@ -2255,7 +2155,7 @@ mod auction_round_success { fn price_calculation_2() { // From the knowledge hub let test_env = TestEnvironment::new(); - let mut project_metadata = default_project(test_env.get_new_nonce()); + let project_metadata = default_project(test_env.get_new_nonce()); let auctioning_project = AuctioningProject::new_with(&test_env, project_metadata, ISSUER, default_evaluations()); let bids = vec![ @@ -2384,8 +2284,6 @@ mod auction_round_success { .advance_time(candle_end_block - test_env.current_block() + 1) .unwrap(); - let details = auctioning_project.get_project_details(); - let now = test_env.current_block(); let random_end = auctioning_project .get_project_details() .phase_transition_points @@ -2552,7 +2450,7 @@ mod auction_round_success { fn bids_at_higher_price_than_weighted_average_use_average() { let test_env = TestEnvironment::new(); let issuer = ISSUER; - let mut project = default_project(test_env.get_new_nonce()); + let project = default_project(test_env.get_new_nonce()); let evaluations = default_evaluations(); let bids: TestBids = vec![ TestBid::new( @@ -2728,7 +2626,6 @@ mod auction_round_failure { let issuer = ISSUER; let project = default_project(test_env.get_new_nonce()); let evaluations = default_evaluations(); - let bids: TestBids = vec![]; let bidding_project = AuctioningProject::new_with(&test_env, project, issuer, evaluations); let details = bidding_project.get_project_details(); @@ -3649,7 +3546,7 @@ mod bids_vesting { calculate_contributed_plmc_spent(community_contributions.clone(), ct_price); let plmc_remainder_contribution_deposits = calculate_contributed_plmc_spent(remainder_contributions.clone(), ct_price); - let mut total_plmc_participation_locked = merge_add_mappings_by_user(vec![ + let total_plmc_participation_locked = merge_add_mappings_by_user(vec![ plmc_bid_deposits.clone(), plmc_community_contribution_deposits, plmc_remainder_contribution_deposits.clone(), @@ -3935,8 +3832,7 @@ mod test_helper_functions { mod misc_features { use super::*; use crate::UpdateType::{CommunityFundingStart, RemainderFundingStart}; - use sp_arithmetic::Perbill; - use testing_macros::*; + #[test] fn remove_from_update_store_works() { @@ -3963,122 +3859,10 @@ mod misc_features { }); } - #[test] - fn get_evaluator_ct_rewards_works() { - let test_env = TestEnvironment::new(); - - let bounded_name = BoundedVec::try_from("Contribution Token TEST".as_bytes().to_vec()).unwrap(); - let bounded_symbol = BoundedVec::try_from("CTEST".as_bytes().to_vec()).unwrap(); - let metadata_hash = hashed(format!("{}-{}", METADATA, 420)); - let project_metadata = ProjectMetadataOf:: { - token_information: CurrencyMetadata { - name: bounded_name, - symbol: bounded_symbol, - decimals: ASSET_DECIMALS, - }, - mainnet_token_max_supply: 8_000_000_0_000_000_000, - total_allocation_size: 100_000_0_000_000_000, - minimum_price: PriceOf::::from_float(10.0), - ticket_size: TicketSize { - minimum: Some(1), - maximum: None, - }, - participants_size: ParticipantsSize { - minimum: Some(2), - maximum: None, - }, - funding_thresholds: Default::default(), - conversion_rate: 0, - participation_currencies: AcceptedFundingAsset::USDT, - funding_destination_account: ISSUER, - offchain_information_hash: Some(metadata_hash), - }; - - // all values taken from the knowledge hub - let evaluations: UserToUSDBalance = vec![ - (EVALUATOR_1, 75_000 * US_DOLLAR), - (EVALUATOR_2, 65_000 * US_DOLLAR), - (EVALUATOR_3, 60_000 * US_DOLLAR), - ]; - - let bids: TestBids = vec![ - TestBid::new( - BIDDER_1, - 10_000 * ASSET_UNIT, - 15.into(), - None, - AcceptedFundingAsset::USDT, - ), - TestBid::new( - BIDDER_2, - 20_000 * ASSET_UNIT, - 20.into(), - None, - AcceptedFundingAsset::USDT, - ), - TestBid::new( - BIDDER_4, - 20_000 * ASSET_UNIT, - 16.into(), - None, - AcceptedFundingAsset::USDT, - ), - ]; - - let contributions: TestContributions = vec![ - TestContribution::new(BUYER_1, 4_000 * ASSET_UNIT, None, AcceptedFundingAsset::USDT), - TestContribution::new(BUYER_2, 2_000 * ASSET_UNIT, None, AcceptedFundingAsset::USDT), - TestContribution::new(BUYER_3, 2_000 * ASSET_UNIT, None, AcceptedFundingAsset::USDT), - TestContribution::new(BUYER_4, 5_000 * ASSET_UNIT, None, AcceptedFundingAsset::USDT), - TestContribution::new(BUYER_5, 30_000 * ASSET_UNIT, None, AcceptedFundingAsset::USDT), - TestContribution::new(BUYER_6, 5_000 * ASSET_UNIT, None, AcceptedFundingAsset::USDT), - TestContribution::new(BUYER_7, 2_000 * ASSET_UNIT, None, AcceptedFundingAsset::USDT), - ]; - - let community_funding_project = - CommunityFundingProject::new_with(&test_env, project_metadata, ISSUER, evaluations, bids); - let details = community_funding_project.get_project_details(); - let ct_price = details.weighted_average_price.unwrap(); - let mut plmc_deposits = calculate_contributed_plmc_spent(contributions.clone(), ct_price); - plmc_deposits = plmc_deposits - .into_iter() - .map(|(account, balance)| (account, balance + get_ed())) - .collect(); - let funding_deposits = calculate_contributed_funding_asset_spent(contributions.clone(), ct_price); - - test_env.mint_plmc_to(plmc_deposits); - test_env.mint_statemint_asset_to(funding_deposits); - - community_funding_project.buy_for_retail_users(contributions).unwrap(); - let finished_project = community_funding_project.finish_funding(); - let details = finished_project.get_project_details(); - let mut ct_evaluation_rewards = - test_env.in_ext(|| FundingModule::get_evaluator_ct_rewards(finished_project.get_project_id()).unwrap()); - ct_evaluation_rewards.sort_by_key(|item| item.0); - let expected_ct_rewards = vec![ - (EVALUATOR_1, 1_196_1_509_434_007), - (EVALUATOR_2, 824_0_150_943_427), - (EVALUATOR_3, 637_9_471_698_137), - ]; - - for (real, desired) in zip(ct_evaluation_rewards.iter(), expected_ct_rewards.iter()) { - assert_eq!(real.0, desired.0, "bad accounts order"); - assert_close_enough!(real.1, desired.1, Perbill::from_parts(1u32)); - } - } + #[allow(dead_code)] fn sandbox() { - // let plmc_price_in_usd = 8_5_000_000_000_u128; - // let token_amount= FixedU128::from_float(12.5); - // let ticket_size: u128 = token_amount.checked_mul_int(plmc_price_in_usd).unwrap(); - // - // let ticket_size = 250_0_000_000_000_u128; - // let rate = FixedU128::from_float(8.5f64); - // let inv_rate = rate.reciprocal().unwrap(); - // let amount = inv_rate.checked_mul_int(ticket_size).unwrap(); - // let a = FixedU128::from - // let x = "x"; - // 29_4_117_647_058 + assert!(true) } } @@ -4086,7 +3870,7 @@ mod testing_macros { #[allow(unused_macros)] macro_rules! assert_close_enough { ($real:expr, $desired:expr, $min_approximation:expr) => { - let real_approximation = Perbill::from_rational_approximation($real, $desired); + let real_approximation = Perbill::from_rational($real, $desired); assert!(real_approximation >= $min_approximation); }; } diff --git a/polimec-skeleton/pallets/funding/src/traits.rs b/polimec-skeleton/pallets/funding/src/traits.rs index 28b463c5b..95071a346 100644 --- a/polimec-skeleton/pallets/funding/src/traits.rs +++ b/polimec-skeleton/pallets/funding/src/traits.rs @@ -1,6 +1,7 @@ use crate::{BalanceOf, Config}; use frame_support::weights::Weight; use sp_arithmetic::FixedPointNumber; +use sp_runtime::DispatchError; pub trait BondingRequirementCalculation { fn calculate_bonding_requirement(&self, ticket_size: BalanceOf) -> Result, ()>; @@ -15,5 +16,5 @@ pub trait ProvideStatemintPrice { pub trait DoRemainingOperation { fn is_done(&self) -> bool; - fn do_one_operation(&mut self, project_id: T::ProjectIdentifier) -> Result; + fn do_one_operation(&mut self, project_id: T::ProjectIdentifier) -> Result; } diff --git a/polimec-skeleton/pallets/funding/src/types.rs b/polimec-skeleton/pallets/funding/src/types.rs index 6d1b79347..150bb0929 100644 --- a/polimec-skeleton/pallets/funding/src/types.rs +++ b/polimec-skeleton/pallets/funding/src/types.rs @@ -26,6 +26,7 @@ use sp_arithmetic::{FixedPointNumber, FixedPointOperand}; use sp_runtime::traits::CheckedDiv; use sp_std::cmp::Eq; use sp_std::collections::btree_map::*; +use sp_std::prelude::*; pub use config_types::*; pub use inner_types::*; @@ -116,7 +117,7 @@ pub mod storage_types { } #[derive(Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug, MaxEncodedLen, TypeInfo)] - pub struct ProjectDetails { + pub struct ProjectDetails { pub issuer: AccountId, /// Whether the project is frozen, so no `metadata` changes are allowed. pub is_frozen: bool, @@ -134,6 +135,8 @@ pub mod storage_types { pub funding_amount_reached: Balance, /// Cleanup operations remaining pub cleanup: ProjectCleanup, + /// Evaluator rewards/penalties + pub evaluation_reward_or_slash_info: Option, } /// Tells on_initialize what to do with the project @@ -527,4 +530,19 @@ pub mod inner_types { ContributionUnbonding(u64), Finished, } + + #[derive(Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug, TypeInfo, MaxEncodedLen)] + pub enum EvaluationRewardOrSlashInfo { + Rewards(RewardInfo), + Slashes(Vec<(AccountId, Balance)>), + Unchanged, + } + + #[derive(Default, Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug, TypeInfo, MaxEncodedLen)] + pub struct RewardInfo { + pub early_evaluator_reward_pot_usd: Balance, + pub normal_evaluator_reward_pot_usd: Balance, + pub early_evaluator_total_bonded_usd: Balance, + pub normal_evaluator_total_bonded_usd: Balance, + } } From 9c24c8bc43931ca9e6a47f3e6cae51dc8795bddb Mon Sep 17 00:00:00 2001 From: Juan Ignacio RIos Date: Thu, 13 Jul 2023 18:48:42 +0200 Subject: [PATCH 18/27] chore(214): cargo fmt --- polimec-skeleton/pallets/funding/src/functions.rs | 9 ++++++--- polimec-skeleton/pallets/funding/src/impls.rs | 8 +++++--- polimec-skeleton/pallets/funding/src/tests.rs | 5 +---- polimec-skeleton/pallets/funding/src/traits.rs | 3 ++- 4 files changed, 14 insertions(+), 11 deletions(-) diff --git a/polimec-skeleton/pallets/funding/src/functions.rs b/polimec-skeleton/pallets/funding/src/functions.rs index 36a6cad97..91cc07a1a 100644 --- a/polimec-skeleton/pallets/funding/src/functions.rs +++ b/polimec-skeleton/pallets/funding/src/functions.rs @@ -1593,13 +1593,15 @@ impl Pallet { } pub fn do_release_bid_funds_for( - _caller: AccountIdOf, _project_id: T::ProjectIdentifier, _bidder: AccountIdOf, _bid_id: StorageItemIdOf, + _caller: AccountIdOf, _project_id: T::ProjectIdentifier, _bidder: AccountIdOf, + _bid_id: StorageItemIdOf, ) -> Result<(), DispatchError> { Ok(()) } pub fn do_bid_unbond_for( - _caller: AccountIdOf, _project_id: T::ProjectIdentifier, _bidder: AccountIdOf, _bid_id: StorageItemIdOf, + _caller: AccountIdOf, _project_id: T::ProjectIdentifier, _bidder: AccountIdOf, + _bid_id: StorageItemIdOf, ) -> Result<(), DispatchError> { Ok(()) } @@ -1626,7 +1628,8 @@ impl Pallet { } pub fn do_payout_bid_funds_for( - _caller: AccountIdOf, _project_id: T::ProjectIdentifier, _bidder: AccountIdOf, _bid_id: StorageItemIdOf, + _caller: AccountIdOf, _project_id: T::ProjectIdentifier, _bidder: AccountIdOf, + _bid_id: StorageItemIdOf, ) -> Result<(), DispatchError> { Ok(()) } diff --git a/polimec-skeleton/pallets/funding/src/impls.rs b/polimec-skeleton/pallets/funding/src/impls.rs index 2c190d6c0..1426737ec 100644 --- a/polimec-skeleton/pallets/funding/src/impls.rs +++ b/polimec-skeleton/pallets/funding/src/impls.rs @@ -1,16 +1,18 @@ +use crate::traits::DoRemainingOperation; use crate::*; use frame_support::traits::Get; use frame_support::weights::Weight; -use sp_runtime::DispatchError; use sp_runtime::traits::AccountIdConversion; +use sp_runtime::DispatchError; use sp_std::prelude::*; -use crate::traits::DoRemainingOperation; impl DoRemainingOperation for ProjectFinalizer { fn is_done(&self) -> bool { matches!(self, ProjectFinalizer::None) } - fn do_one_operation(&mut self, project_id: T::ProjectIdentifier) -> Result { + fn do_one_operation( + &mut self, project_id: T::ProjectIdentifier, + ) -> Result { match self { ProjectFinalizer::None => Err(Error::::NoFinalizerSet.into()), ProjectFinalizer::Success(ops) => { diff --git a/polimec-skeleton/pallets/funding/src/tests.rs b/polimec-skeleton/pallets/funding/src/tests.rs index 377bffd6c..f7ed711f2 100644 --- a/polimec-skeleton/pallets/funding/src/tests.rs +++ b/polimec-skeleton/pallets/funding/src/tests.rs @@ -1736,7 +1736,7 @@ mod creation_round_failure { #[cfg(test)] mod evaluation_round_success { use super::*; - use sp_arithmetic::{Perbill}; + use sp_arithmetic::Perbill; use testing_macros::assert_close_enough; #[test] @@ -1882,7 +1882,6 @@ mod evaluation_round_success { assert_close_enough!(real.1, desired.1, Perbill::from_parts(1u32)); } } - } #[cfg(test)] @@ -3833,7 +3832,6 @@ mod misc_features { use super::*; use crate::UpdateType::{CommunityFundingStart, RemainderFundingStart}; - #[test] fn remove_from_update_store_works() { let test_env = TestEnvironment::new(); @@ -3859,7 +3857,6 @@ mod misc_features { }); } - #[allow(dead_code)] fn sandbox() { assert!(true) diff --git a/polimec-skeleton/pallets/funding/src/traits.rs b/polimec-skeleton/pallets/funding/src/traits.rs index 95071a346..51a845b3b 100644 --- a/polimec-skeleton/pallets/funding/src/traits.rs +++ b/polimec-skeleton/pallets/funding/src/traits.rs @@ -16,5 +16,6 @@ pub trait ProvideStatemintPrice { pub trait DoRemainingOperation { fn is_done(&self) -> bool; - fn do_one_operation(&mut self, project_id: T::ProjectIdentifier) -> Result; + fn do_one_operation(&mut self, project_id: T::ProjectIdentifier) + -> Result; } From 57bc62d2d54e590712cea6c74a27c573bc601406 Mon Sep 17 00:00:00 2001 From: Juan Ignacio RIos Date: Fri, 14 Jul 2023 18:08:13 +0200 Subject: [PATCH 19/27] wip: save --- polimec-skeleton/pallets/funding/src/functions.rs | 14 +++++--------- polimec-skeleton/pallets/funding/src/lib.rs | 1 - 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/polimec-skeleton/pallets/funding/src/functions.rs b/polimec-skeleton/pallets/funding/src/functions.rs index 91cc07a1a..e764fd3fe 100644 --- a/polimec-skeleton/pallets/funding/src/functions.rs +++ b/polimec-skeleton/pallets/funding/src/functions.rs @@ -241,9 +241,7 @@ impl Pallet { .fold(total, |acc, bond| acc.saturating_add(bond.original_plmc_bond)); total.saturating_add(user_total_plmc_bond) }); - // TODO: PLMC-142. 10% is hardcoded, check if we want to configure it a runtime as explained here: - // https://substrate.stackexchange.com/questions/2784/how-to-get-a-percent-portion-of-a-balance: - // TODO: PLMC-143. Check if it's safe to use * here + let evaluation_target_usd = Perbill::from_percent(10) * fundraising_target_usd; let evaluation_target_plmc = current_plmc_price .reciprocal() @@ -288,7 +286,7 @@ impl Pallet { } else { // * Update storage * project_details.status = ProjectStatus::EvaluationFailed; - project_details.cleanup = ProjectCleanup::Ready(ProjectFinalizer::Failure(Default::default())); + project_details.cleanup = ProjectCleanup::Ready(ProjectFinalizer::Failure(FailureFinalizer::Initialized)); ProjectsDetails::::insert(project_id, project_details); // * Emit events * @@ -654,7 +652,7 @@ impl Pallet { let evaluation_reward_or_slash_info = Self::generate_evaluation_reward_or_slash_info(project_id)?; if funding_is_successful { project_details.status = ProjectStatus::FundingSuccessful; - project_details.cleanup = ProjectCleanup::Ready(ProjectFinalizer::Success(Default::default())); + project_details.cleanup = ProjectCleanup::Ready(ProjectFinalizer::Success(SuccessFinalizer::Initialized)); project_details.evaluation_reward_or_slash_info = Some(evaluation_reward_or_slash_info); // * Update Storage * @@ -819,8 +817,7 @@ impl Pallet { .fold(BalanceOf::::zero(), |acc, evaluation| acc.saturating_add(evaluation)); let remaining_bond_to_reach_threshold = early_evaluation_reward_threshold_usd - .checked_sub(&previous_total_evaluation_bonded_usd) - .unwrap_or(BalanceOf::::zero()); + .saturating_sub(&previous_total_evaluation_bonded_usd) let early_usd_amount = if usd_amount <= remaining_bond_to_reach_threshold { usd_amount @@ -1567,8 +1564,7 @@ impl Pallet { evaluation.late_usd_amount.saturating_add(evaluation.early_usd_amount), reward_info.normal_evaluator_total_bonded_usd, ); - let total_reward_amount_usd = early_reward_weight * reward_info.early_evaluator_reward_pot_usd - + normal_reward_weight * reward_info.normal_evaluator_reward_pot_usd; + let total_reward_amount_usd = (early_reward_weight * reward_info.early_evaluator_reward_pot_usd).saturating_add(normal_reward_weight * reward_info.normal_evaluator_reward_pot_usd); let reward_amount_ct: BalanceOf = ct_price .reciprocal() .ok_or(Error::::BadMath)? diff --git a/polimec-skeleton/pallets/funding/src/lib.rs b/polimec-skeleton/pallets/funding/src/lib.rs index eefeafb71..f3f5896e8 100644 --- a/polimec-skeleton/pallets/funding/src/lib.rs +++ b/polimec-skeleton/pallets/funding/src/lib.rs @@ -966,7 +966,6 @@ pub mod pallet { } } - // TODO: PLMC-127. Set a proper weight max_weight.saturating_sub(remaining_weight) } } From 2bdd3c25439ca3ed778ab441d071cb94a5dcdc7a Mon Sep 17 00:00:00 2001 From: Juan Ignacio RIos Date: Tue, 18 Jul 2023 16:18:11 +0200 Subject: [PATCH 20/27] feat(214): ProjectDetails changed. Evaluation total bond now stored in it. --- .../pallets/funding/src/functions.rs | 37 ++++++++++--------- polimec-skeleton/pallets/funding/src/impls.rs | 4 +- polimec-skeleton/pallets/funding/src/lib.rs | 5 ++- polimec-skeleton/pallets/funding/src/tests.rs | 6 ++- polimec-skeleton/pallets/funding/src/types.rs | 19 +++++++--- 5 files changed, 42 insertions(+), 29 deletions(-) diff --git a/polimec-skeleton/pallets/funding/src/functions.rs b/polimec-skeleton/pallets/funding/src/functions.rs index e764fd3fe..883e4dc35 100644 --- a/polimec-skeleton/pallets/funding/src/functions.rs +++ b/polimec-skeleton/pallets/funding/src/functions.rs @@ -102,7 +102,11 @@ impl Pallet { remaining_contribution_tokens: initial_metadata.total_allocation_size, funding_amount_reached: BalanceOf::::zero(), cleanup: ProjectCleanup::NotReady, - evaluation_reward_or_slash_info: None, + evaluation_round_info: EvaluationRoundInfoOf:: { + total_bonded_usd: Zero::zero(), + total_bonded_plmc: Zero::zero(), + evaluators_outcome: EvaluatorsOutcome::Unchanged, + }, }; let project_metadata = initial_metadata; @@ -649,11 +653,11 @@ impl Pallet { let funding_reached = project_details.funding_amount_reached; let funding_is_successful = !(project_details.status == ProjectStatus::FundingFailed || funding_reached < funding_target); - let evaluation_reward_or_slash_info = Self::generate_evaluation_reward_or_slash_info(project_id)?; + let evaluators_outcome = Self::generate_evaluators_outcome(project_id)?; + project_details.evaluation_round_info.evaluators_outcome = evaluators_outcome; if funding_is_successful { project_details.status = ProjectStatus::FundingSuccessful; project_details.cleanup = ProjectCleanup::Ready(ProjectFinalizer::Success(SuccessFinalizer::Initialized)); - project_details.evaluation_reward_or_slash_info = Some(evaluation_reward_or_slash_info); // * Update Storage * ProjectsDetails::::insert(project_id, project_details.clone()); @@ -781,14 +785,14 @@ impl Pallet { evaluator: AccountIdOf, project_id: T::ProjectIdentifier, usd_amount: BalanceOf, ) -> Result<(), DispatchError> { // * Get variables * - let project_details = ProjectsDetails::::get(project_id).ok_or(Error::::ProjectInfoNotFound)?; + let mut project_details = ProjectsDetails::::get(project_id).ok_or(Error::::ProjectInfoNotFound)?; let now = >::block_number(); let evaluation_id = Self::next_evaluation_id(); let mut caller_existing_evaluations = Evaluations::::get(project_id, evaluator.clone()); let plmc_usd_price = T::PriceProvider::get_price(PLMC_STATEMINT_ID).ok_or(Error::::PLMCPriceNotAvailable)?; let early_evaluation_reward_threshold_usd = T::EarlyEvaluationThreshold::get() * project_details.fundraising_target; - let all_existing_evaluations = Evaluations::::iter_prefix(project_id); + let evaluation_round_info = &mut project_details.evaluation_round_info; // * Validity Checks * ensure!( @@ -807,17 +811,10 @@ impl Pallet { .checked_mul_int(usd_amount) .ok_or(Error::::BadMath)?; - let previous_total_evaluation_bonded_usd = all_existing_evaluations - .map(|(_evaluator, evaluations)| { - evaluations.iter().fold(BalanceOf::::zero(), |acc, evaluation| { - acc.saturating_add(evaluation.early_usd_amount) - .saturating_add(evaluation.late_usd_amount) - }) - }) - .fold(BalanceOf::::zero(), |acc, evaluation| acc.saturating_add(evaluation)); + let previous_total_evaluation_bonded_usd = evaluation_round_info.total_bonded_usd; let remaining_bond_to_reach_threshold = early_evaluation_reward_threshold_usd - .saturating_sub(&previous_total_evaluation_bonded_usd) + .saturating_sub(previous_total_evaluation_bonded_usd); let early_usd_amount = if usd_amount <= remaining_bond_to_reach_threshold { usd_amount @@ -878,6 +875,10 @@ impl Pallet { Evaluations::::set(project_id, evaluator.clone(), caller_existing_evaluations); NextEvaluationId::::set(evaluation_id.saturating_add(One::one())); + evaluation_round_info.total_bonded_usd += usd_amount; + evaluation_round_info.total_bonded_plmc += plmc_bond; + ProjectsDetails::::insert(project_id, project_details); + // * Emit events * Self::deposit_event(Event::::FundsBonded { @@ -1537,7 +1538,7 @@ impl Pallet { .weighted_average_price .ok_or(Error::::ImpossibleState)?; let reward_info = - if let Some(EvaluationRewardOrSlashInfo::Rewards(info)) = project_details.evaluation_reward_or_slash_info { + if let EvaluatorsOutcome::Rewarded(info) = project_details.evaluation_round_info.evaluators_outcome { info } else { return Err(Error::::NotAllowed.into()); @@ -2179,9 +2180,9 @@ impl Pallet { )) } - pub fn generate_evaluation_reward_or_slash_info( + pub fn generate_evaluators_outcome( project_id: T::ProjectIdentifier, - ) -> Result, DispatchError> { + ) -> Result, DispatchError> { let project_details = ProjectsDetails::::get(project_id).ok_or(Error::::ProjectNotFound)?; let funding_target = project_details.fundraising_target; let funding_reached = project_details.funding_amount_reached; @@ -2204,7 +2205,7 @@ impl Pallet { early_evaluator_total_bonded_usd, normal_evaluator_total_bonded_usd, ) = Self::get_evaluator_rewards_info(project_id)?; - Ok(EvaluationRewardOrSlashInfo::Rewards(RewardInfo { + Ok(EvaluatorsOutcome::Rewarded(RewardInfo { early_evaluator_reward_pot_usd, normal_evaluator_reward_pot_usd, early_evaluator_total_bonded_usd, diff --git a/polimec-skeleton/pallets/funding/src/impls.rs b/polimec-skeleton/pallets/funding/src/impls.rs index 1426737ec..4c67c102a 100644 --- a/polimec-skeleton/pallets/funding/src/impls.rs +++ b/polimec-skeleton/pallets/funding/src/impls.rs @@ -316,8 +316,8 @@ fn reward_or_slash_one_evaluation(project_id: T::ProjectIdentifier) - .find(|evaluation| !evaluation.rewarded_or_slashed) .expect("user_evaluations can only exist if an item here is found; qed"); - match project_details.evaluation_reward_or_slash_info { - Some(EvaluationRewardOrSlashInfo::Rewards(_)) => { + match project_details.evaluation_round_info.evaluators_outcome { + EvaluatorsOutcome::Rewarded(_) => { match Pallet::::do_evaluation_reward( T::PalletId::get().into_account_truncating(), evaluation.project_id, diff --git a/polimec-skeleton/pallets/funding/src/lib.rs b/polimec-skeleton/pallets/funding/src/lib.rs index f3f5896e8..7f743055c 100644 --- a/polimec-skeleton/pallets/funding/src/lib.rs +++ b/polimec-skeleton/pallets/funding/src/lib.rs @@ -228,11 +228,12 @@ pub type HashOf = ::Hash; pub type AssetIdOf = <::FundingCurrency as fungibles::Inspect<::AccountId>>::AssetId; +pub type EvaluatorsOutcomeOf = EvaluatorsOutcome, BalanceOf>; pub type ProjectMetadataOf = ProjectMetadata>, BalanceOf, PriceOf, AccountIdOf, HashOf>; pub type ProjectDetailsOf = - ProjectDetails, BlockNumberOf, PriceOf, BalanceOf, EvaluationRewardOrSlashInfoOf>; -pub type EvaluationRewardOrSlashInfoOf = EvaluationRewardOrSlashInfo, BalanceOf>; + ProjectDetails, BlockNumberOf, PriceOf, BalanceOf, EvaluationRoundInfoOf>; +pub type EvaluationRoundInfoOf = EvaluationRoundInfo, BalanceOf>; pub type VestingOf = Vesting, BalanceOf>; pub type EvaluationInfoOf = EvaluationInfo, ProjectIdOf, AccountIdOf, BalanceOf, BlockNumberOf>; diff --git a/polimec-skeleton/pallets/funding/src/tests.rs b/polimec-skeleton/pallets/funding/src/tests.rs index f7ed711f2..958106867 100644 --- a/polimec-skeleton/pallets/funding/src/tests.rs +++ b/polimec-skeleton/pallets/funding/src/tests.rs @@ -536,7 +536,11 @@ impl<'a> CreatedProject<'a> { remaining_contribution_tokens: expected_metadata.total_allocation_size, funding_amount_reached: BalanceOf::::zero(), cleanup: ProjectCleanup::NotReady, - evaluation_reward_or_slash_info: None, + evaluation_round_info: EvaluationRoundInfoOf:: { + total_bonded_usd: Zero::zero(), + total_bonded_plmc: Zero::zero(), + evaluators_outcome: EvaluatorsOutcome::Unchanged, + }, }; assert_eq!(metadata, expected_metadata); assert_eq!(details, expected_details); diff --git a/polimec-skeleton/pallets/funding/src/types.rs b/polimec-skeleton/pallets/funding/src/types.rs index 150bb0929..dca00f5ad 100644 --- a/polimec-skeleton/pallets/funding/src/types.rs +++ b/polimec-skeleton/pallets/funding/src/types.rs @@ -117,7 +117,7 @@ pub mod storage_types { } #[derive(Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug, MaxEncodedLen, TypeInfo)] - pub struct ProjectDetails { + pub struct ProjectDetails { pub issuer: AccountId, /// Whether the project is frozen, so no `metadata` changes are allowed. pub is_frozen: bool, @@ -135,8 +135,8 @@ pub mod storage_types { pub funding_amount_reached: Balance, /// Cleanup operations remaining pub cleanup: ProjectCleanup, - /// Evaluator rewards/penalties - pub evaluation_reward_or_slash_info: Option, + /// Information about the total amount bonded, and the outcome in regards to reward/slash/nothing + pub evaluation_round_info: EvaluationRoundInfo, } /// Tells on_initialize what to do with the project @@ -532,10 +532,17 @@ pub mod inner_types { } #[derive(Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug, TypeInfo, MaxEncodedLen)] - pub enum EvaluationRewardOrSlashInfo { - Rewards(RewardInfo), - Slashes(Vec<(AccountId, Balance)>), + pub struct EvaluationRoundInfo { + pub total_bonded_usd: Balance, + pub total_bonded_plmc: Balance, + pub evaluators_outcome: EvaluatorsOutcome + } + + #[derive(Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug, TypeInfo, MaxEncodedLen)] + pub enum EvaluatorsOutcome { Unchanged, + Rewarded(RewardInfo), + Slashed(Vec<(AccountId, Balance)>), } #[derive(Default, Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug, TypeInfo, MaxEncodedLen)] From c3ad9e18bfff904b7a7e886d565f8a8efd4f62a5 Mon Sep 17 00:00:00 2001 From: Juan Ignacio RIos Date: Wed, 19 Jul 2023 11:46:47 +0200 Subject: [PATCH 21/27] feat(214): bid refunds on failure due to candle end and ct soldout implemented and tested --- .../pallets/funding/src/functions.rs | 101 ++++++++--- polimec-skeleton/pallets/funding/src/tests.rs | 171 +++++++++++++++++- polimec-skeleton/pallets/funding/src/types.rs | 13 +- 3 files changed, 252 insertions(+), 33 deletions(-) diff --git a/polimec-skeleton/pallets/funding/src/functions.rs b/polimec-skeleton/pallets/funding/src/functions.rs index 883e4dc35..94836b6fd 100644 --- a/polimec-skeleton/pallets/funding/src/functions.rs +++ b/polimec-skeleton/pallets/funding/src/functions.rs @@ -32,7 +32,8 @@ use frame_support::{ Get, }, }; -use sp_arithmetic::Perbill; +use itertools::Itertools; +use sp_arithmetic::Perquintill; use sp_arithmetic::traits::{CheckedSub, Zero}; use sp_std::prelude::*; @@ -246,7 +247,7 @@ impl Pallet { total.saturating_add(user_total_plmc_bond) }); - let evaluation_target_usd = Perbill::from_percent(10) * fundraising_target_usd; + let evaluation_target_usd = Perquintill::from_percent(10) * fundraising_target_usd; let evaluation_target_plmc = current_plmc_price .reciprocal() .ok_or(Error::::BadMath)? @@ -813,8 +814,8 @@ impl Pallet { let previous_total_evaluation_bonded_usd = evaluation_round_info.total_bonded_usd; - let remaining_bond_to_reach_threshold = early_evaluation_reward_threshold_usd - .saturating_sub(previous_total_evaluation_bonded_usd); + let remaining_bond_to_reach_threshold = + early_evaluation_reward_threshold_usd.saturating_sub(previous_total_evaluation_bonded_usd); let early_usd_amount = if usd_amount <= remaining_bond_to_reach_threshold { usd_amount @@ -879,7 +880,6 @@ impl Pallet { evaluation_round_info.total_bonded_plmc += plmc_bond; ProjectsDetails::::insert(project_id, project_details); - // * Emit events * Self::deposit_event(Event::::FundsBonded { project_id, @@ -1538,7 +1538,7 @@ impl Pallet { .weighted_average_price .ok_or(Error::::ImpossibleState)?; let reward_info = - if let EvaluatorsOutcome::Rewarded(info) = project_details.evaluation_round_info.evaluators_outcome { + if let EvaluatorsOutcome::Rewarded(info) = project_details.evaluation_round_info.evaluators_outcome { info } else { return Err(Error::::NotAllowed.into()); @@ -1557,15 +1557,16 @@ impl Pallet { ); // * Calculate variables * - let early_reward_weight = Perbill::from_rational( + let early_reward_weight = Perquintill::from_rational( evaluation.early_usd_amount, reward_info.early_evaluator_total_bonded_usd, ); - let normal_reward_weight = Perbill::from_rational( + let normal_reward_weight = Perquintill::from_rational( evaluation.late_usd_amount.saturating_add(evaluation.early_usd_amount), reward_info.normal_evaluator_total_bonded_usd, ); - let total_reward_amount_usd = (early_reward_weight * reward_info.early_evaluator_reward_pot_usd).saturating_add(normal_reward_weight * reward_info.normal_evaluator_reward_pot_usd); + let total_reward_amount_usd = (early_reward_weight * reward_info.early_evaluator_reward_pot_usd) + .saturating_add(normal_reward_weight * reward_info.normal_evaluator_reward_pot_usd); let reward_amount_ct: BalanceOf = ct_price .reciprocal() .ok_or(Error::::BadMath)? @@ -1726,8 +1727,9 @@ impl Pallet { let mut bid_usd_value_sum = BalanceOf::::zero(); let project_account = Self::fund_account_id(project_id); let plmc_price = T::PriceProvider::get_price(PLMC_STATEMINT_ID).ok_or(Error::::PLMCPriceNotAvailable)?; - // sort bids by price + // sort bids by price, and equal prices sorted by block number bids.sort(); + bids.reverse(); // accept only bids that were made before `end_block` i.e end of candle auction let bids: Result, DispatchError> = bids .into_iter() @@ -1735,11 +1737,49 @@ impl Pallet { if bid.when > end_block { bid.status = BidStatus::Rejected(RejectionReason::AfterCandleEnd); // TODO: PLMC-147. Unlock funds. We can do this inside the "on_idle" hook, and change the `status` of the `Bid` to "Unreserved" + bid.final_ct_amount = 0_u32.into(); + bid.final_ct_usd_price = PriceOf::::zero(); + + T::FundingCurrency::transfer( + bid.funding_asset.to_statemint_id(), + &project_account, + &bid.bidder, + bid.funding_asset_amount_locked, + Preservation::Preserve, + )?; + T::NativeCurrency::release( + &LockType::Participation(project_id), + &bid.bidder, + bid.plmc_bond, + Precision::Exact, + )?; + bid.funding_asset_amount_locked = BalanceOf::::zero(); + bid.plmc_bond = BalanceOf::::zero(); + return Ok(bid); } let buyable_amount = total_allocation_size.saturating_sub(bid_token_amount_sum); if buyable_amount == 0_u32.into() { bid.status = BidStatus::Rejected(RejectionReason::NoTokensLeft); + bid.final_ct_amount = 0_u32.into(); + bid.final_ct_usd_price = PriceOf::::zero(); + + T::FundingCurrency::transfer( + bid.funding_asset.to_statemint_id(), + &project_account, + &bid.bidder, + bid.funding_asset_amount_locked, + Preservation::Preserve, + )?; + T::NativeCurrency::release( + &LockType::Participation(project_id), + &bid.bidder, + bid.plmc_bond, + Precision::Exact, + )?; + bid.funding_asset_amount_locked = BalanceOf::::zero(); + bid.plmc_bond = BalanceOf::::zero(); + return Ok(bid); } else if bid.original_ct_amount <= buyable_amount { let maybe_ticket_size = bid.original_ct_usd_price.checked_mul_int(bid.original_ct_amount); if let Some(ticket_size) = maybe_ticket_size { @@ -1748,6 +1788,25 @@ impl Pallet { bid.status = BidStatus::Accepted; } else { bid.status = BidStatus::Rejected(RejectionReason::BadMath); + + bid.final_ct_amount = 0_u32.into(); + bid.final_ct_usd_price = PriceOf::::zero(); + + T::FundingCurrency::transfer( + bid.funding_asset.to_statemint_id(), + &project_account, + &bid.bidder, + bid.funding_asset_amount_locked, + Preservation::Preserve, + )?; + T::NativeCurrency::release( + &LockType::Participation(project_id), + &bid.bidder, + bid.plmc_bond, + Precision::Exact, + )?; + bid.funding_asset_amount_locked = BalanceOf::::zero(); + bid.plmc_bond = BalanceOf::::zero(); return Ok(bid); } } else { @@ -1815,8 +1874,6 @@ impl Pallet { return Ok(bid); } - - // TODO: PLMC-147. Refund remaining amount } Ok(bid) @@ -2100,8 +2157,8 @@ impl Pallet { let evaluator_usd_rewards = evaluation_usd_amounts .into_iter() .map(|(evaluator, (early, late))| { - let early_evaluator_weight = Perbill::from_rational(early, early_evaluator_total_locked); - let all_evaluator_weight = Perbill::from_rational(early + late, all_evaluator_total_locked); + let early_evaluator_weight = Perquintill::from_rational(early, early_evaluator_total_locked); + let all_evaluator_weight = Perquintill::from_rational(early + late, all_evaluator_total_locked); let early_reward = early_evaluator_weight * early_evaluator_rewards; let all_reward = all_evaluator_weight * all_evaluator_rewards; @@ -2155,13 +2212,13 @@ impl Pallet { let funding_reached = project_details.funding_amount_reached; // This is the "Y" variable from the knowledge hub - let percentage_of_target_funding = Perbill::from_rational(funding_reached, target_funding); + let percentage_of_target_funding = Perquintill::from_rational(funding_reached, target_funding); let fees = Self::calculate_fees(project_id)?; - let evaluator_fees = percentage_of_target_funding * (Perbill::from_percent(30) * fees); + let evaluator_fees = percentage_of_target_funding * (Perquintill::from_percent(30) * fees); - let early_evaluator_rewards = Perbill::from_percent(20) * evaluator_fees; - let all_evaluator_rewards = Perbill::from_percent(80) * evaluator_fees; + let early_evaluator_rewards = Perquintill::from_percent(20) * evaluator_fees; + let all_evaluator_rewards = Perquintill::from_percent(80) * evaluator_fees; let early_evaluator_total_locked = evaluation_usd_amounts .iter() @@ -2186,16 +2243,16 @@ impl Pallet { let project_details = ProjectsDetails::::get(project_id).ok_or(Error::::ProjectNotFound)?; let funding_target = project_details.fundraising_target; let funding_reached = project_details.funding_amount_reached; - let funding_ratio = Perbill::from_rational(funding_reached, funding_target); + let funding_ratio = Perquintill::from_rational(funding_reached, funding_target); // Project Automatically rejected, evaluators slashed - if funding_ratio <= Perbill::from_percent(33) { + if funding_ratio <= Perquintill::from_percent(33) { todo!() // Project Manually accepted, evaluators slashed - } else if funding_ratio < Perbill::from_percent(75) { + } else if funding_ratio < Perquintill::from_percent(75) { todo!() // Project Manually accepted, evaluators unaffected - } else if funding_ratio < Perbill::from_percent(90) { + } else if funding_ratio < Perquintill::from_percent(90) { todo!() // Project Automatically accepted, evaluators rewarded } else { diff --git a/polimec-skeleton/pallets/funding/src/tests.rs b/polimec-skeleton/pallets/funding/src/tests.rs index 958106867..1148bf22e 100644 --- a/polimec-skeleton/pallets/funding/src/tests.rs +++ b/polimec-skeleton/pallets/funding/src/tests.rs @@ -1740,7 +1740,7 @@ mod creation_round_failure { #[cfg(test)] mod evaluation_round_success { use super::*; - use sp_arithmetic::Perbill; + use sp_arithmetic::Perquintill; use testing_macros::assert_close_enough; #[test] @@ -1883,7 +1883,8 @@ mod evaluation_round_success { for (real, desired) in zip(actual_reward_balances.iter(), expected_ct_rewards.iter()) { assert_eq!(real.0, desired.0, "bad accounts order"); - assert_close_enough!(real.1, desired.1, Perbill::from_parts(1u32)); + // 0.01 parts of a Perbill + assert_close_enough!(real.1, desired.1, Perquintill::from_parts(10_000_000u64)); } } } @@ -2644,6 +2645,164 @@ mod auction_round_failure { let details = bidding_project.get_project_details(); assert_eq!(details.status, ProjectStatus::FundingFailed); } + + #[test] + fn after_ct_soldout_bid_gets_refunded() { + let test_env = TestEnvironment::new(); + let auctioning_project = + AuctioningProject::new_with(&test_env, default_project(0), ISSUER, default_evaluations()); + let metadata = auctioning_project.get_project_metadata(); + let max_cts_for_bids = metadata.total_allocation_size.clone(); + let project_id = auctioning_project.get_project_id(); + + let glutton_bid = TestBid::new( + BIDDER_1, + max_cts_for_bids, + 10_u128.into(), + None, + AcceptedFundingAsset::USDT, + ); + let rejected_bid = TestBid::new( + BIDDER_2, + 10_000 * ASSET_UNIT, + 5_u128.into(), + None, + AcceptedFundingAsset::USDT, + ); + + let mut plmc_fundings: UserToPLMCBalance = + calculate_auction_plmc_spent(vec![glutton_bid.clone(), rejected_bid.clone()]); + plmc_fundings.push((BIDDER_1, get_ed())); + plmc_fundings.push((BIDDER_2, get_ed())); + + let usdt_fundings = calculate_auction_funding_asset_spent(vec![glutton_bid.clone(), rejected_bid.clone()]); + + test_env.mint_plmc_to(plmc_fundings.clone()); + test_env.mint_statemint_asset_to(usdt_fundings.clone()); + + auctioning_project + .bid_for_users(vec![glutton_bid, rejected_bid]) + .expect("Bids should pass"); + + test_env.do_free_plmc_assertions(vec![(BIDDER_1, get_ed()), (BIDDER_2, get_ed())]); + test_env.do_reserved_plmc_assertions( + vec![(BIDDER_1, plmc_fundings[0].1), (BIDDER_2, plmc_fundings[1].1)], + LockType::Participation(project_id), + ); + test_env.do_bid_transferred_statemint_asset_assertions( + vec![ + ( + BIDDER_1, + usdt_fundings[0].1, + AcceptedFundingAsset::USDT.to_statemint_id(), + ), + ( + BIDDER_2, + usdt_fundings[1].1, + AcceptedFundingAsset::USDT.to_statemint_id(), + ), + ], + project_id, + ); + + let community_funding_project = auctioning_project.start_community_funding(); + let details = community_funding_project.get_project_details(); + + test_env.do_free_plmc_assertions(vec![(BIDDER_1, get_ed()), (BIDDER_2, plmc_fundings[1].1 + get_ed())]); + + test_env.do_reserved_plmc_assertions( + vec![(BIDDER_1, plmc_fundings[0].1), (BIDDER_2, 0)], + LockType::Participation(project_id), + ); + + test_env.do_bid_transferred_statemint_asset_assertions( + vec![ + ( + BIDDER_1, + usdt_fundings[0].1, + AcceptedFundingAsset::USDT.to_statemint_id(), + ), + (BIDDER_2, 0, AcceptedFundingAsset::USDT.to_statemint_id()), + ], + project_id, + ); + } + + #[test] + fn after_random_end_bid_gets_refunded() { + let test_env = TestEnvironment::new(); + let auctioning_project = + AuctioningProject::new_with(&test_env, default_project(0), ISSUER, default_evaluations()); + let project_id = auctioning_project.get_project_id(); + + let (bid_in, bid_out) = (default_bids()[0], default_bids()[1]); + + let mut plmc_fundings: UserToPLMCBalance = calculate_auction_plmc_spent(vec![bid_in.clone(), bid_out.clone()]); + plmc_fundings.push((BIDDER_1, get_ed())); + plmc_fundings.push((BIDDER_2, get_ed())); + + let usdt_fundings = calculate_auction_funding_asset_spent(vec![bid_in.clone(), bid_out.clone()]); + + test_env.mint_plmc_to(plmc_fundings.clone()); + test_env.mint_statemint_asset_to(usdt_fundings.clone()); + + auctioning_project + .bid_for_users(vec![bid_in]) + .expect("Bids should pass"); + + test_env.advance_time( + ::EnglishAuctionDuration::get() + + ::CandleAuctionDuration::get() + - 1, + ).unwrap(); + + auctioning_project + .bid_for_users(vec![bid_out]) + .expect("Bids should pass"); + + test_env.do_free_plmc_assertions(vec![(BIDDER_1, get_ed()), (BIDDER_2, get_ed())]); + test_env.do_reserved_plmc_assertions( + vec![(BIDDER_1, plmc_fundings[0].1), (BIDDER_2, plmc_fundings[1].1)], + LockType::Participation(project_id), + ); + test_env.do_bid_transferred_statemint_asset_assertions( + vec![ + ( + BIDDER_1, + usdt_fundings[0].1, + AcceptedFundingAsset::USDT.to_statemint_id(), + ), + ( + BIDDER_2, + usdt_fundings[1].1, + AcceptedFundingAsset::USDT.to_statemint_id(), + ), + ], + project_id, + ); + + let community_funding_project = auctioning_project.start_community_funding(); + let details = community_funding_project.get_project_details(); + + test_env.do_free_plmc_assertions(vec![(BIDDER_1, get_ed()), (BIDDER_2, plmc_fundings[1].1 + get_ed())]); + + test_env.do_reserved_plmc_assertions( + vec![(BIDDER_1, plmc_fundings[0].1), (BIDDER_2, 0)], + LockType::Participation(project_id), + ); + + test_env.do_bid_transferred_statemint_asset_assertions( + vec![ + ( + BIDDER_1, + usdt_fundings[0].1, + AcceptedFundingAsset::USDT.to_statemint_id(), + ), + (BIDDER_2, 0, AcceptedFundingAsset::USDT.to_statemint_id()), + ], + project_id, + ); + } } #[cfg(test)] @@ -3870,9 +4029,11 @@ mod misc_features { mod testing_macros { #[allow(unused_macros)] macro_rules! assert_close_enough { - ($real:expr, $desired:expr, $min_approximation:expr) => { - let real_approximation = Perbill::from_rational($real, $desired); - assert!(real_approximation >= $min_approximation); + ($real:expr, $desired:expr, $max_approximation:expr) => { + let real_parts = Perquintill::from_rational($real, $desired); + let one = Perquintill::from_percent(100u64); + let real_approximation = one - real_parts; + assert!(real_approximation <= $max_approximation); }; } pub(crate) use assert_close_enough; diff --git a/polimec-skeleton/pallets/funding/src/types.rs b/polimec-skeleton/pallets/funding/src/types.rs index dca00f5ad..25a8dcf24 100644 --- a/polimec-skeleton/pallets/funding/src/types.rs +++ b/polimec-skeleton/pallets/funding/src/types.rs @@ -203,16 +203,17 @@ pub mod storage_types { Balance: BalanceT + FixedPointOperand + Ord, Price: FixedPointNumber, AccountId: Eq, - BlockNumber: Eq, + BlockNumber: Eq + Ord, PlmcVesting: Eq, CTVesting: Eq, Multiplier: Eq, > Ord for BidInfo { fn cmp(&self, other: &Self) -> sp_std::cmp::Ordering { - let self_ticket_size = self.original_ct_usd_price.saturating_mul_int(self.original_ct_amount); - let other_ticket_size = other.original_ct_usd_price.saturating_mul_int(other.original_ct_amount); - self_ticket_size.cmp(&other_ticket_size) + match self.original_ct_usd_price.cmp(&other.original_ct_usd_price) { + sp_std::cmp::Ordering::Equal => Ord::cmp(&self.when, &other.when), + other => other, + } } } @@ -222,7 +223,7 @@ pub mod storage_types { Balance: BalanceT + FixedPointOperand, Price: FixedPointNumber, AccountId: Eq, - BlockNumber: Eq, + BlockNumber: Eq + Ord, PlmcVesting: Eq, CTVesting: Eq, Multiplier: Eq, @@ -535,7 +536,7 @@ pub mod inner_types { pub struct EvaluationRoundInfo { pub total_bonded_usd: Balance, pub total_bonded_plmc: Balance, - pub evaluators_outcome: EvaluatorsOutcome + pub evaluators_outcome: EvaluatorsOutcome, } #[derive(Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug, TypeInfo, MaxEncodedLen)] From 2c3e0c522eb6e3729dfc835c0b97e76423a62f1c Mon Sep 17 00:00:00 2001 From: Juan Ignacio RIos Date: Wed, 19 Jul 2023 16:23:34 +0200 Subject: [PATCH 22/27] feat(214): tried to add final price check on new_with for CommunityFundingProject but it is already implemented if the caller adds it in teh BidInfoFilter --- .../pallets/funding/src/functions.rs | 14 +- polimec-skeleton/pallets/funding/src/tests.rs | 367 +++++++++++++++--- 2 files changed, 323 insertions(+), 58 deletions(-) diff --git a/polimec-skeleton/pallets/funding/src/functions.rs b/polimec-skeleton/pallets/funding/src/functions.rs index 94836b6fd..edc18deae 100644 --- a/polimec-skeleton/pallets/funding/src/functions.rs +++ b/polimec-skeleton/pallets/funding/src/functions.rs @@ -637,14 +637,12 @@ impl Pallet { let remainder_end_block = project_details.phase_transition_points.remainder.end(); // * Validity checks * - if let Some(end_block) = remainder_end_block { - ensure!(now > end_block, Error::::TooEarlyForFundingEnd); - } else { - ensure!( - remaining_cts == 0u32.into() || project_details.status == ProjectStatus::FundingFailed, - Error::::TooEarlyForFundingEnd - ); - } + ensure!( + remaining_cts == 0u32.into() + || project_details.status == ProjectStatus::FundingFailed + || matches!(remainder_end_block, Some(end_block) if now > end_block), + Error::::TooEarlyForFundingEnd + ); // * Calculate new variables * let funding_target = project_metadata diff --git a/polimec-skeleton/pallets/funding/src/tests.rs b/polimec-skeleton/pallets/funding/src/tests.rs index 1148bf22e..b2b01e84d 100644 --- a/polimec-skeleton/pallets/funding/src/tests.rs +++ b/polimec-skeleton/pallets/funding/src/tests.rs @@ -100,19 +100,24 @@ impl TestContribution { pub type TestContributions = Vec; #[derive(Clone, PartialEq, Eq, Debug)] -pub struct BidInfoFilter { - pub bid_id: Option, - pub when: Option, - pub status: Option>, - pub project: Option, +pub struct BidInfoFilter { + pub id: Option, + pub project_id: Option, pub bidder: Option, - pub ct_amount: Option, - pub ct_usd_price: Option, + pub status: Option>, + pub original_ct_amount: Option, + pub original_ct_usd_price: Option, + pub final_ct_amount: Option, + pub final_ct_usd_price: Option, + pub funding_asset: Option, + pub funding_asset_amount_locked: Option, + pub multiplier: Option, + pub plmc_bond: Option, pub funded: Option, pub plmc_vesting_period: Option, pub ct_vesting_period: Option, - pub funding_asset: Option, - pub funding_asset_amount: Option, + pub when: Option, + pub funds_released: Option, } type BidInfoFilterOf = BidInfoFilter< ::StorageItemId, @@ -120,6 +125,7 @@ type BidInfoFilterOf = BidInfoFilter< BalanceOf, PriceOf, ::AccountId, + MultiplierOf, BlockNumberOf, VestingOf, VestingOf, @@ -127,60 +133,77 @@ type BidInfoFilterOf = BidInfoFilter< impl Default for BidInfoFilterOf { fn default() -> Self { BidInfoFilter { - bid_id: None, - when: None, - status: None, - project: None, + id: None, + project_id: None, bidder: None, - ct_amount: None, - ct_usd_price: None, + status: None, + original_ct_amount: None, + original_ct_usd_price: None, + final_ct_amount: None, + final_ct_usd_price: None, + funding_asset: None, + funding_asset_amount_locked: None, + multiplier: None, + plmc_bond: None, funded: None, plmc_vesting_period: None, ct_vesting_period: None, - funding_asset: None, - funding_asset_amount: None, + when: None, + funds_released: None, } } } impl BidInfoFilterOf { fn matches_bid(&self, bid: &BidInfoOf) -> bool { - if self.bid_id.is_some() && self.bid_id.unwrap() != bid.id { + if self.id.is_some() && self.id.unwrap() != bid.id { return false; } - if self.when.is_some() && self.when.unwrap() != bid.when { + if self.project_id.is_some() && self.project_id.unwrap() != bid.project_id { + return false; + } + if self.bidder.is_some() && self.bidder.unwrap() != bid.bidder { return false; } if self.status.is_some() && self.status.as_ref().unwrap() != &bid.status { return false; } - if self.project.is_some() && self.project.unwrap() != bid.project_id { + if self.original_ct_amount.is_some() && self.original_ct_amount.unwrap() != bid.original_ct_amount { return false; } - if self.bidder.is_some() && self.bidder.unwrap() != bid.bidder { + if self.original_ct_usd_price.is_some() && self.original_ct_usd_price.unwrap() != bid.original_ct_usd_price { + return false; + } + if self.final_ct_amount.is_some() && self.final_ct_amount.unwrap() != bid.final_ct_amount { + return false; + } + if self.final_ct_usd_price.is_some() && self.final_ct_usd_price.unwrap() != bid.final_ct_usd_price { + return false; + } + if self.funding_asset.is_some() && self.funding_asset.unwrap() != bid.funding_asset { return false; } - if self.ct_amount.is_some() && self.ct_amount.unwrap() != bid.original_ct_amount { + if self.funding_asset_amount_locked.is_some() && self.funding_asset_amount_locked.unwrap() != bid.funding_asset_amount_locked { return false; } - if self.ct_usd_price.is_some() && self.ct_usd_price.unwrap() != bid.original_ct_usd_price { + if self.multiplier.is_some() && self.multiplier.unwrap() != bid.multiplier { + return false; + } + if self.plmc_bond.is_some() && self.plmc_bond.unwrap() != bid.plmc_bond { return false; } if self.funded.is_some() && self.funded.unwrap() != bid.funded { return false; } - if self.plmc_vesting_period.is_some() && self.plmc_vesting_period.as_ref().unwrap() != &bid.plmc_vesting_period - { + if self.plmc_vesting_period.is_some() && self.plmc_vesting_period.unwrap() != bid.plmc_vesting_period { return false; } - if self.ct_vesting_period.is_some() && self.ct_vesting_period.as_ref().unwrap() != &bid.ct_vesting_period { + if self.ct_vesting_period.is_some() && self.ct_vesting_period.unwrap() != bid.ct_vesting_period { return false; } - if self.funding_asset.is_some() && self.funding_asset.as_ref().unwrap() != &bid.funding_asset { + if self.when.is_some() && self.when.unwrap() != bid.when { return false; } - if self.funding_asset_amount.is_some() - && self.funding_asset_amount.as_ref().unwrap() != &bid.funding_asset_amount_locked - { + if self.funds_released.is_some() && self.funds_released.unwrap() != bid.funds_released { return false; } @@ -325,6 +348,17 @@ impl TestEnvironment { balances }) } + fn get_all_reserved_plmc_balances(&self, reserve_type: LockType>) -> UserToPLMCBalance { + self.ext_env.borrow_mut().execute_with(|| { + let mut fundings = UserToPLMCBalance::new(); + let user_keys: Vec = frame_system::Account::::iter_keys().collect(); + for user in user_keys { + let funding = Balances::balance_on_hold(&reserve_type, &user); + fundings.push((user, funding)); + } + fundings + }) + } #[allow(dead_code)] fn get_all_free_statemint_asset_balances(&self, asset_id: AssetId) -> UserToStatemintAsset { self.ext_env.borrow_mut().execute_with(|| { @@ -338,6 +372,7 @@ impl TestEnvironment { balances }) } + fn get_free_plmc_balances_for(&self, user_keys: Vec) -> UserToPLMCBalance { self.ext_env.borrow_mut().execute_with(|| { let mut balances = UserToPLMCBalance::new(); @@ -349,6 +384,17 @@ impl TestEnvironment { balances }) } + fn get_reserved_plmc_balances_for(&self, user_keys: Vec, lock_type: LockType>) -> UserToPLMCBalance { + self.ext_env.borrow_mut().execute_with(|| { + let mut balances = UserToPLMCBalance::new(); + for user in user_keys { + let funding = Balances::balance_on_hold(&lock_type, &user); + balances.push((user, funding)); + } + balances.sort_by(|a, b| a.0.cmp(&b.0)); + balances + }) + } fn get_free_statemint_asset_balances_for( &self, asset_id: AssetId, user_keys: Vec, ) -> UserToStatemintAsset { @@ -377,18 +423,7 @@ impl TestEnvironment { }); } } - #[allow(dead_code)] - fn get_reserved_fundings(&self, reserve_type: LockType>) -> UserToPLMCBalance { - self.ext_env.borrow_mut().execute_with(|| { - let mut fundings = UserToPLMCBalance::new(); - let user_keys: Vec = frame_system::Account::::iter_keys().collect(); - for user in user_keys { - let funding = Balances::balance_on_hold(&reserve_type, &user); - fundings.push((user, funding)); - } - fundings - }) - } + fn mint_plmc_to(&self, mapping: UserToPLMCBalance) { self.ext_env.borrow_mut().execute_with(|| { for (account, amount) in mapping { @@ -866,8 +901,8 @@ impl<'a> CommunityFundingProject<'a> { "Weighted average price should exist" ); - for filter in bid_expectations { - assert!(flattened_bids.iter().any(|bid| filter.matches_bid(&bid))) + for mut filter in bid_expectations { + let _found_bid = flattened_bids.iter().find(|bid| filter.matches_bid(&bid)).unwrap(); } // Remaining CTs are updated @@ -1559,13 +1594,22 @@ pub mod helper_functions { pub fn err_if_on_initialize_failed( events: Vec>, - ) -> Result<(), DispatchError> { + ) -> Result<(), Error> { let last_event = events.into_iter().last().expect("No events found for this action."); match last_event { frame_system::EventRecord { event: RuntimeEvent::FundingModule(Event::TransitionError { project_id: _, error }), .. - } => Err(error), + } => { + match error { + DispatchError::Module(module_error) => { + let pallet_error: Error = Decode::decode(&mut &module_error.error[..]).unwrap(); + Err(pallet_error) + + } + _ => panic!("wrong conversion") + } + }, _ => Ok(()), } } @@ -1887,6 +1931,79 @@ mod evaluation_round_success { assert_close_enough!(real.1, desired.1, Perquintill::from_parts(10_000_000u64)); } } + + #[test] + fn plmc_unbonded_after_funding_success() { + let test_env = TestEnvironment::new(); + let evaluations = default_evaluations(); + let evaluators = evaluations.iter().map(|ev|ev.0.clone()).collect::>(); + + let remainder_funding_project = RemainderFundingProject::new_with( + &test_env, + default_project(test_env.get_new_nonce()), + ISSUER, + evaluations.clone(), + default_bids(), + default_community_buys(), + ); + let project_id = remainder_funding_project.get_project_id(); + let prev_reserved_plmc = test_env.get_reserved_plmc_balances_for(evaluators.clone(), LockType::Evaluation(project_id)); + + let prev_free_plmc = test_env.get_free_plmc_balances_for(evaluators.clone()); + + remainder_funding_project.end_funding(); + test_env.advance_time(10).unwrap(); + + let post_unbond_amounts: UserToPLMCBalance = prev_reserved_plmc.iter().map(|(evaluator, amount)| (*evaluator, Zero::zero())).collect(); + + let temp_actual_par_lock = test_env.get_all_reserved_plmc_balances(LockType::Participation(project_id)); + + test_env.do_reserved_plmc_assertions(post_unbond_amounts.clone(), LockType::Evaluation(project_id)); + test_env.do_reserved_plmc_assertions(post_unbond_amounts, LockType::Participation(project_id)); + + let post_free_plmc = test_env.get_free_plmc_balances_for(evaluators.clone()); + + let increased_amounts = merge_subtract_mappings_by_user(post_free_plmc, vec![prev_free_plmc]); + + assert_eq!(increased_amounts, calculate_evaluation_plmc_spent(evaluations)) + } + + #[test] + fn plmc_unbonded_after_funding_failure() { + let test_env = TestEnvironment::new(); + let evaluations = default_evaluations(); + let evaluators = evaluations.iter().map(|ev|ev.0.clone()).collect::>(); + + let remainder_funding_project = RemainderFundingProject::new_with( + &test_env, + default_project(test_env.get_new_nonce()), + ISSUER, + evaluations.clone(), + default_bids(), , + ); + let project_id = remainder_funding_project.get_project_id(); + let prev_reserved_plmc = test_env.get_reserved_plmc_balances_for(evaluators.clone(), LockType::Evaluation(project_id)); + + let prev_free_plmc = test_env.get_free_plmc_balances_for(evaluators.clone()); + + remainder_funding_project.end_funding(); + test_env.advance_time(10).unwrap(); + + let post_unbond_amounts: UserToPLMCBalance = prev_reserved_plmc.iter().map(|(evaluator, amount)| (*evaluator, Zero::zero())).collect(); + + let temp_actual_par_lock = test_env.get_all_reserved_plmc_balances(LockType::Participation(project_id)); + + test_env.do_reserved_plmc_assertions(post_unbond_amounts.clone(), LockType::Evaluation(project_id)); + test_env.do_reserved_plmc_assertions(post_unbond_amounts, LockType::Participation(project_id)); + + let post_free_plmc = test_env.get_free_plmc_balances_for(evaluators.clone()); + + let increased_amounts = merge_subtract_mappings_by_user(post_free_plmc, vec![prev_free_plmc]); + + assert_eq!(increased_amounts, calculate_evaluation_plmc_spent(evaluations)) + } + + } #[cfg(test)] @@ -2601,12 +2718,13 @@ mod auction_round_failure { auctioning_project.bid_for_users(bids).expect("Bids should pass"); test_env.ext_env.borrow_mut().execute_with(|| { - let stored_bids = FundingModule::bids(project_id, DAVE); + let mut stored_bids = FundingModule::bids(project_id, DAVE); assert_eq!(stored_bids.len(), 4); + stored_bids.sort(); assert_eq!(stored_bids[0].original_ct_usd_price, 5_u128.into()); - assert_eq!(stored_bids[1].original_ct_usd_price, 8_u128.into()); - assert_eq!(stored_bids[2].original_ct_usd_price, 5_u128.into()); - assert_eq!(stored_bids[3].original_ct_usd_price, 2_u128.into()); + assert_eq!(stored_bids[1].original_ct_usd_price, 5_u128.into()); + assert_eq!(stored_bids[2].original_ct_usd_price, 7_u128.into()); + assert_eq!(stored_bids[3].original_ct_usd_price, 8_u128.into()); }); } @@ -3543,6 +3661,155 @@ mod remainder_round_success { }); assert_eq!(evaluation_bond, 0); } + + #[test] + fn remainder_round_ends_on_all_ct_sold_exact() { + let test_env = TestEnvironment::new(); + let remainder_funding_project = RemainderFundingProject::new_with( + &test_env, + default_project(0), + ISSUER, + default_evaluations(), + default_bids(), + default_community_buys() + ); + const BOB: AccountId = 808; + + let remaining_ct = remainder_funding_project + .get_project_details() + .remaining_contribution_tokens; + let ct_price = remainder_funding_project + .get_project_details() + .weighted_average_price + .expect("CT Price should exist"); + let project_id = remainder_funding_project.get_project_id(); + + let contributions: TestContributions = vec![TestContribution::new( + BOB, + remaining_ct, + None, + AcceptedFundingAsset::USDT, + )]; + let mut plmc_fundings: UserToPLMCBalance = calculate_contributed_plmc_spent(contributions.clone(), ct_price); + plmc_fundings.push((BOB, get_ed())); + let statemint_asset_fundings: UserToStatemintAsset = + calculate_contributed_funding_asset_spent(contributions.clone(), ct_price); + + test_env.mint_plmc_to(plmc_fundings.clone()); + test_env.mint_statemint_asset_to(statemint_asset_fundings.clone()); + + // Buy remaining CTs + remainder_funding_project + .buy_for_any_user(contributions) + .expect("The Buyer should be able to buy the exact amount of remaining CTs"); + test_env.advance_time(2u64).unwrap(); + + // Check remaining CTs is 0 + assert_eq!( + remainder_funding_project + .get_project_details() + .remaining_contribution_tokens, + 0, + "There are still remaining CTs" + ); + + // Check project is in FundingEnded state + assert_eq!( + remainder_funding_project.get_project_details().status, + ProjectStatus::FundingSuccessful + ); + + test_env.do_free_plmc_assertions(vec![plmc_fundings[1].clone()]); + test_env.do_free_statemint_asset_assertions(vec![(BOB, 0_u128, AcceptedFundingAsset::USDT.to_statemint_id())]); + test_env.do_reserved_plmc_assertions(vec![plmc_fundings[0].clone()], LockType::Participation(project_id)); + test_env.do_contribution_transferred_statemint_asset_assertions( + statemint_asset_fundings, + remainder_funding_project.get_project_id(), + ); + } + + #[test] + fn remainder_round_ends_on_all_ct_sold_overbuy() { + let test_env = TestEnvironment::new(); + let remainder_funding_project = RemainderFundingProject::new_with( + &test_env, + default_project(0), + ISSUER, + default_evaluations(), + default_bids(), + default_community_buys() + ); + const BOB: AccountId = 808; + const OVERBUY_CT: BalanceOf = 40 * ASSET_UNIT; + + let remaining_ct = remainder_funding_project + .get_project_details() + .remaining_contribution_tokens; + + let ct_price = remainder_funding_project + .get_project_details() + .weighted_average_price + .expect("CT Price should exist"); + + let project_id = remainder_funding_project.get_project_id(); + + let contributions: TestContributions = vec![ + TestContribution::new(BOB, remaining_ct, None, AcceptedFundingAsset::USDT), + TestContribution::new(BOB, OVERBUY_CT, None, AcceptedFundingAsset::USDT), + ]; + let mut plmc_fundings: UserToPLMCBalance = calculate_contributed_plmc_spent(contributions.clone(), ct_price); + plmc_fundings.push((BOB, get_ed())); + let mut statemint_asset_fundings: UserToStatemintAsset = + calculate_contributed_funding_asset_spent(contributions.clone(), ct_price); + + test_env.mint_plmc_to(plmc_fundings.clone()); + test_env.mint_statemint_asset_to(statemint_asset_fundings.clone()); + + // Buy remaining CTs + remainder_funding_project + .buy_for_any_user(contributions) + .expect("The Buyer should be able to buy the exact amount of remaining CTs"); + test_env.advance_time(2u64).unwrap(); + + // Check remaining CTs is 0 + assert_eq!( + remainder_funding_project + .get_project_details() + .remaining_contribution_tokens, + 0, + "There are still remaining CTs" + ); + + // Check project is in FundingEnded state + assert_eq!( + remainder_funding_project.get_project_details().status, + ProjectStatus::FundingSuccessful + ); + + let reserved_plmc = plmc_fundings.swap_remove(0).1; + let remaining_plmc: Balance = plmc_fundings.iter().fold(0_u128, |acc, (_, amount)| acc + amount); + + let actual_funding_transferred = statemint_asset_fundings.swap_remove(0).1; + let remaining_statemint_assets: Balance = statemint_asset_fundings + .iter() + .fold(0_u128, |acc, (_, amount, _)| acc + amount); + + test_env.do_free_plmc_assertions(vec![(BOB, remaining_plmc)]); + test_env.do_free_statemint_asset_assertions(vec![( + BOB, + remaining_statemint_assets, + AcceptedFundingAsset::USDT.to_statemint_id(), + )]); + test_env.do_reserved_plmc_assertions(vec![(BOB, reserved_plmc)], LockType::Participation(project_id)); + test_env.do_contribution_transferred_statemint_asset_assertions( + vec![( + BOB, + actual_funding_transferred, + AcceptedFundingAsset::USDT.to_statemint_id(), + )], + remainder_funding_project.get_project_id(), + ); + } } #[cfg(test)] From b7d73ce8dd3e95e8a53fa944275137e30867508d Mon Sep 17 00:00:00 2001 From: Juan Ignacio RIos Date: Wed, 19 Jul 2023 17:03:06 +0200 Subject: [PATCH 23/27] fix(214): reflecting BidInfoFilter changes on tests --- polimec-skeleton/pallets/funding/src/tests.rs | 23 +++++++++++-------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/polimec-skeleton/pallets/funding/src/tests.rs b/polimec-skeleton/pallets/funding/src/tests.rs index b2b01e84d..b903447bd 100644 --- a/polimec-skeleton/pallets/funding/src/tests.rs +++ b/polimec-skeleton/pallets/funding/src/tests.rs @@ -846,8 +846,8 @@ impl<'a> CommunityFundingProject<'a> { let bid_expectations = bids .iter() .map(|bid| BidInfoFilter { - ct_amount: Some(bid.amount), - ct_usd_price: Some(bid.price), + original_ct_amount: Some(bid.amount), + original_ct_usd_price: Some(bid.price), ..Default::default() }) .collect::>(); @@ -1979,14 +1979,17 @@ mod evaluation_round_success { default_project(test_env.get_new_nonce()), ISSUER, evaluations.clone(), - default_bids(), , + vec![TestBid::new(BUYER_1, 1000 * ASSET_UNIT, 10u128.into(), None, AcceptedFundingAsset::USDT)], + vec![TestContribution::new(BUYER_1, 1000 * US_DOLLAR, None, AcceptedFundingAsset::USDT)], ); + let project_id = remainder_funding_project.get_project_id(); let prev_reserved_plmc = test_env.get_reserved_plmc_balances_for(evaluators.clone(), LockType::Evaluation(project_id)); let prev_free_plmc = test_env.get_free_plmc_balances_for(evaluators.clone()); - remainder_funding_project.end_funding(); + let finished_project = remainder_funding_project.end_funding(); + assert_eq!(finished_project.get_project_details().status, ProjectStatus::FundingFailed); test_env.advance_time(10).unwrap(); let post_unbond_amounts: UserToPLMCBalance = prev_reserved_plmc.iter().map(|(evaluator, amount)| (*evaluator, Zero::zero())).collect(); @@ -2423,10 +2426,10 @@ mod auction_round_success { let pid = auctioning_project.get_project_id(); let stored_bids = auctioning_project.in_ext(|| FundingModule::bids(pid, bid.bidder)); let desired_bid = BidInfoFilter { - project: Some(pid), + project_id: Some(pid), bidder: Some(bid.bidder), - ct_amount: Some(bid.amount), - ct_usd_price: Some(bid.price), + original_ct_amount: Some(bid.amount), + original_ct_usd_price: Some(bid.price), status: Some(BidStatus::Accepted), ..Default::default() }; @@ -2441,10 +2444,10 @@ mod auction_round_success { let pid = auctioning_project.get_project_id(); let stored_bids = auctioning_project.in_ext(|| FundingModule::bids(pid, bid.bidder)); let desired_bid = BidInfoFilter { - project: Some(pid), + project_id: Some(pid), bidder: Some(bid.bidder), - ct_amount: Some(bid.amount), - ct_usd_price: Some(bid.price), + original_ct_amount: Some(bid.amount), + original_ct_usd_price: Some(bid.price), status: Some(BidStatus::Rejected(RejectionReason::AfterCandleEnd)), ..Default::default() }; From 1b525aee2d218a52c5753f2ed3fa5590ca2c4175 Mon Sep 17 00:00:00 2001 From: Juan Ignacio RIos Date: Fri, 21 Jul 2023 10:32:41 +0200 Subject: [PATCH 24/27] chore(214): fix warnings --- .../pallets/funding/src/functions.rs | 5 ++--- polimec-skeleton/pallets/funding/src/tests.rs | 19 ++++++------------- 2 files changed, 8 insertions(+), 16 deletions(-) diff --git a/polimec-skeleton/pallets/funding/src/functions.rs b/polimec-skeleton/pallets/funding/src/functions.rs index edc18deae..45ecb29ba 100644 --- a/polimec-skeleton/pallets/funding/src/functions.rs +++ b/polimec-skeleton/pallets/funding/src/functions.rs @@ -32,7 +32,7 @@ use frame_support::{ Get, }, }; -use itertools::Itertools; + use sp_arithmetic::Perquintill; use sp_arithmetic::traits::{CheckedSub, Zero}; @@ -1726,8 +1726,7 @@ impl Pallet { let project_account = Self::fund_account_id(project_id); let plmc_price = T::PriceProvider::get_price(PLMC_STATEMINT_ID).ok_or(Error::::PLMCPriceNotAvailable)?; // sort bids by price, and equal prices sorted by block number - bids.sort(); - bids.reverse(); + bids.sort_by(|a, b| b.cmp(a)); // accept only bids that were made before `end_block` i.e end of candle auction let bids: Result, DispatchError> = bids .into_iter() diff --git a/polimec-skeleton/pallets/funding/src/tests.rs b/polimec-skeleton/pallets/funding/src/tests.rs index b903447bd..1aad61778 100644 --- a/polimec-skeleton/pallets/funding/src/tests.rs +++ b/polimec-skeleton/pallets/funding/src/tests.rs @@ -348,6 +348,7 @@ impl TestEnvironment { balances }) } + #[allow(dead_code)] fn get_all_reserved_plmc_balances(&self, reserve_type: LockType>) -> UserToPLMCBalance { self.ext_env.borrow_mut().execute_with(|| { let mut fundings = UserToPLMCBalance::new(); @@ -901,7 +902,7 @@ impl<'a> CommunityFundingProject<'a> { "Weighted average price should exist" ); - for mut filter in bid_expectations { + for filter in bid_expectations { let _found_bid = flattened_bids.iter().find(|bid| filter.matches_bid(&bid)).unwrap(); } @@ -1954,9 +1955,7 @@ mod evaluation_round_success { remainder_funding_project.end_funding(); test_env.advance_time(10).unwrap(); - let post_unbond_amounts: UserToPLMCBalance = prev_reserved_plmc.iter().map(|(evaluator, amount)| (*evaluator, Zero::zero())).collect(); - - let temp_actual_par_lock = test_env.get_all_reserved_plmc_balances(LockType::Participation(project_id)); + let post_unbond_amounts: UserToPLMCBalance = prev_reserved_plmc.iter().map(|(evaluator, _amount)| (*evaluator, Zero::zero())).collect(); test_env.do_reserved_plmc_assertions(post_unbond_amounts.clone(), LockType::Evaluation(project_id)); test_env.do_reserved_plmc_assertions(post_unbond_amounts, LockType::Participation(project_id)); @@ -1992,9 +1991,8 @@ mod evaluation_round_success { assert_eq!(finished_project.get_project_details().status, ProjectStatus::FundingFailed); test_env.advance_time(10).unwrap(); - let post_unbond_amounts: UserToPLMCBalance = prev_reserved_plmc.iter().map(|(evaluator, amount)| (*evaluator, Zero::zero())).collect(); + let post_unbond_amounts: UserToPLMCBalance = prev_reserved_plmc.iter().map(|(evaluator, _amount)| (*evaluator, Zero::zero())).collect(); - let temp_actual_par_lock = test_env.get_all_reserved_plmc_balances(LockType::Participation(project_id)); test_env.do_reserved_plmc_assertions(post_unbond_amounts.clone(), LockType::Evaluation(project_id)); test_env.do_reserved_plmc_assertions(post_unbond_amounts, LockType::Participation(project_id)); @@ -2826,9 +2824,6 @@ mod auction_round_failure { project_id, ); - let community_funding_project = auctioning_project.start_community_funding(); - let details = community_funding_project.get_project_details(); - test_env.do_free_plmc_assertions(vec![(BIDDER_1, get_ed()), (BIDDER_2, plmc_fundings[1].1 + get_ed())]); test_env.do_reserved_plmc_assertions( @@ -2902,9 +2897,6 @@ mod auction_round_failure { project_id, ); - let community_funding_project = auctioning_project.start_community_funding(); - let details = community_funding_project.get_project_details(); - test_env.do_free_plmc_assertions(vec![(BIDDER_1, get_ed()), (BIDDER_2, plmc_fundings[1].1 + get_ed())]); test_env.do_reserved_plmc_assertions( @@ -2928,6 +2920,7 @@ mod auction_round_failure { #[cfg(test)] mod community_round_success { + use frame_support::traits::fungible::Inspect; use super::*; pub const HOURS: BlockNumber = 300u64; @@ -3221,7 +3214,7 @@ mod community_round_success { // Check that the right amount of PLMC is bonded, and funding currency is transferred let contributor_post_buy_plmc_balance = - project.in_ext(|| ::NativeCurrency::free_balance(&CONTRIBUTOR)); + project.in_ext(|| ::NativeCurrency::balance(&CONTRIBUTOR)); let contributor_post_buy_statemint_asset_balance = project.in_ext(|| ::FundingCurrency::balance(USDT_STATEMINT_ID, &CONTRIBUTOR)); From 4de99461680cbfd1ecba183bb8e1ddf78b0efa61 Mon Sep 17 00:00:00 2001 From: Juan Ignacio RIos Date: Fri, 21 Jul 2023 10:38:57 +0200 Subject: [PATCH 25/27] fix(214): broken auction unbond test fixed --- polimec-skeleton/pallets/funding/src/tests.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/polimec-skeleton/pallets/funding/src/tests.rs b/polimec-skeleton/pallets/funding/src/tests.rs index 1aad61778..640a72d8e 100644 --- a/polimec-skeleton/pallets/funding/src/tests.rs +++ b/polimec-skeleton/pallets/funding/src/tests.rs @@ -2824,6 +2824,8 @@ mod auction_round_failure { project_id, ); + let _community_funding_project = auctioning_project.start_community_funding(); + test_env.do_free_plmc_assertions(vec![(BIDDER_1, get_ed()), (BIDDER_2, plmc_fundings[1].1 + get_ed())]); test_env.do_reserved_plmc_assertions( @@ -2897,6 +2899,8 @@ mod auction_round_failure { project_id, ); + let _community_funding_project = auctioning_project.start_community_funding(); + test_env.do_free_plmc_assertions(vec![(BIDDER_1, get_ed()), (BIDDER_2, plmc_fundings[1].1 + get_ed())]); test_env.do_reserved_plmc_assertions( From b5731b15cd227413396ba7bbe4a085fb8da96d0a Mon Sep 17 00:00:00 2001 From: Juan Ignacio RIos Date: Fri, 21 Jul 2023 10:39:46 +0200 Subject: [PATCH 26/27] chore(214): cargo fmt --- .../pallets/funding/src/functions.rs | 4 +- polimec-skeleton/pallets/funding/src/tests.rs | 94 +++++++++++++------ 2 files changed, 65 insertions(+), 33 deletions(-) diff --git a/polimec-skeleton/pallets/funding/src/functions.rs b/polimec-skeleton/pallets/funding/src/functions.rs index 45ecb29ba..fe8bd4c19 100644 --- a/polimec-skeleton/pallets/funding/src/functions.rs +++ b/polimec-skeleton/pallets/funding/src/functions.rs @@ -639,8 +639,8 @@ impl Pallet { // * Validity checks * ensure!( remaining_cts == 0u32.into() - || project_details.status == ProjectStatus::FundingFailed - || matches!(remainder_end_block, Some(end_block) if now > end_block), + || project_details.status == ProjectStatus::FundingFailed + || matches!(remainder_end_block, Some(end_block) if now > end_block), Error::::TooEarlyForFundingEnd ); diff --git a/polimec-skeleton/pallets/funding/src/tests.rs b/polimec-skeleton/pallets/funding/src/tests.rs index 640a72d8e..8dd7aacb9 100644 --- a/polimec-skeleton/pallets/funding/src/tests.rs +++ b/polimec-skeleton/pallets/funding/src/tests.rs @@ -100,7 +100,17 @@ impl TestContribution { pub type TestContributions = Vec; #[derive(Clone, PartialEq, Eq, Debug)] -pub struct BidInfoFilter { +pub struct BidInfoFilter< + Id, + ProjectId, + Balance: BalanceT, + Price, + AccountId, + Multiplier, + BlockNumber, + PlmcVesting, + CTVesting, +> { pub id: Option, pub project_id: Option, pub bidder: Option, @@ -182,7 +192,9 @@ impl BidInfoFilterOf { if self.funding_asset.is_some() && self.funding_asset.unwrap() != bid.funding_asset { return false; } - if self.funding_asset_amount_locked.is_some() && self.funding_asset_amount_locked.unwrap() != bid.funding_asset_amount_locked { + if self.funding_asset_amount_locked.is_some() + && self.funding_asset_amount_locked.unwrap() != bid.funding_asset_amount_locked + { return false; } if self.multiplier.is_some() && self.multiplier.unwrap() != bid.multiplier { @@ -385,7 +397,9 @@ impl TestEnvironment { balances }) } - fn get_reserved_plmc_balances_for(&self, user_keys: Vec, lock_type: LockType>) -> UserToPLMCBalance { + fn get_reserved_plmc_balances_for( + &self, user_keys: Vec, lock_type: LockType>, + ) -> UserToPLMCBalance { self.ext_env.borrow_mut().execute_with(|| { let mut balances = UserToPLMCBalance::new(); for user in user_keys { @@ -1601,15 +1615,12 @@ pub mod helper_functions { frame_system::EventRecord { event: RuntimeEvent::FundingModule(Event::TransitionError { project_id: _, error }), .. - } => { - match error { - DispatchError::Module(module_error) => { - let pallet_error: Error = Decode::decode(&mut &module_error.error[..]).unwrap(); - Err(pallet_error) - - } - _ => panic!("wrong conversion") + } => match error { + DispatchError::Module(module_error) => { + let pallet_error: Error = Decode::decode(&mut &module_error.error[..]).unwrap(); + Err(pallet_error) } + _ => panic!("wrong conversion"), }, _ => Ok(()), } @@ -1937,7 +1948,7 @@ mod evaluation_round_success { fn plmc_unbonded_after_funding_success() { let test_env = TestEnvironment::new(); let evaluations = default_evaluations(); - let evaluators = evaluations.iter().map(|ev|ev.0.clone()).collect::>(); + let evaluators = evaluations.iter().map(|ev| ev.0.clone()).collect::>(); let remainder_funding_project = RemainderFundingProject::new_with( &test_env, @@ -1948,14 +1959,18 @@ mod evaluation_round_success { default_community_buys(), ); let project_id = remainder_funding_project.get_project_id(); - let prev_reserved_plmc = test_env.get_reserved_plmc_balances_for(evaluators.clone(), LockType::Evaluation(project_id)); + let prev_reserved_plmc = + test_env.get_reserved_plmc_balances_for(evaluators.clone(), LockType::Evaluation(project_id)); let prev_free_plmc = test_env.get_free_plmc_balances_for(evaluators.clone()); remainder_funding_project.end_funding(); test_env.advance_time(10).unwrap(); - let post_unbond_amounts: UserToPLMCBalance = prev_reserved_plmc.iter().map(|(evaluator, _amount)| (*evaluator, Zero::zero())).collect(); + let post_unbond_amounts: UserToPLMCBalance = prev_reserved_plmc + .iter() + .map(|(evaluator, _amount)| (*evaluator, Zero::zero())) + .collect(); test_env.do_reserved_plmc_assertions(post_unbond_amounts.clone(), LockType::Evaluation(project_id)); test_env.do_reserved_plmc_assertions(post_unbond_amounts, LockType::Participation(project_id)); @@ -1971,28 +1986,45 @@ mod evaluation_round_success { fn plmc_unbonded_after_funding_failure() { let test_env = TestEnvironment::new(); let evaluations = default_evaluations(); - let evaluators = evaluations.iter().map(|ev|ev.0.clone()).collect::>(); + let evaluators = evaluations.iter().map(|ev| ev.0.clone()).collect::>(); let remainder_funding_project = RemainderFundingProject::new_with( &test_env, default_project(test_env.get_new_nonce()), ISSUER, evaluations.clone(), - vec![TestBid::new(BUYER_1, 1000 * ASSET_UNIT, 10u128.into(), None, AcceptedFundingAsset::USDT)], - vec![TestContribution::new(BUYER_1, 1000 * US_DOLLAR, None, AcceptedFundingAsset::USDT)], + vec![TestBid::new( + BUYER_1, + 1000 * ASSET_UNIT, + 10u128.into(), + None, + AcceptedFundingAsset::USDT, + )], + vec![TestContribution::new( + BUYER_1, + 1000 * US_DOLLAR, + None, + AcceptedFundingAsset::USDT, + )], ); let project_id = remainder_funding_project.get_project_id(); - let prev_reserved_plmc = test_env.get_reserved_plmc_balances_for(evaluators.clone(), LockType::Evaluation(project_id)); + let prev_reserved_plmc = + test_env.get_reserved_plmc_balances_for(evaluators.clone(), LockType::Evaluation(project_id)); let prev_free_plmc = test_env.get_free_plmc_balances_for(evaluators.clone()); let finished_project = remainder_funding_project.end_funding(); - assert_eq!(finished_project.get_project_details().status, ProjectStatus::FundingFailed); + assert_eq!( + finished_project.get_project_details().status, + ProjectStatus::FundingFailed + ); test_env.advance_time(10).unwrap(); - let post_unbond_amounts: UserToPLMCBalance = prev_reserved_plmc.iter().map(|(evaluator, _amount)| (*evaluator, Zero::zero())).collect(); - + let post_unbond_amounts: UserToPLMCBalance = prev_reserved_plmc + .iter() + .map(|(evaluator, _amount)| (*evaluator, Zero::zero())) + .collect(); test_env.do_reserved_plmc_assertions(post_unbond_amounts.clone(), LockType::Evaluation(project_id)); test_env.do_reserved_plmc_assertions(post_unbond_amounts, LockType::Participation(project_id)); @@ -2003,8 +2035,6 @@ mod evaluation_round_success { assert_eq!(increased_amounts, calculate_evaluation_plmc_spent(evaluations)) } - - } #[cfg(test)] @@ -2868,11 +2898,13 @@ mod auction_round_failure { .bid_for_users(vec![bid_in]) .expect("Bids should pass"); - test_env.advance_time( - ::EnglishAuctionDuration::get() - + ::CandleAuctionDuration::get() - - 1, - ).unwrap(); + test_env + .advance_time( + ::EnglishAuctionDuration::get() + + ::CandleAuctionDuration::get() + - 1, + ) + .unwrap(); auctioning_project .bid_for_users(vec![bid_out]) @@ -2924,8 +2956,8 @@ mod auction_round_failure { #[cfg(test)] mod community_round_success { - use frame_support::traits::fungible::Inspect; use super::*; + use frame_support::traits::fungible::Inspect; pub const HOURS: BlockNumber = 300u64; @@ -3671,7 +3703,7 @@ mod remainder_round_success { ISSUER, default_evaluations(), default_bids(), - default_community_buys() + default_community_buys(), ); const BOB: AccountId = 808; @@ -3737,7 +3769,7 @@ mod remainder_round_success { ISSUER, default_evaluations(), default_bids(), - default_community_buys() + default_community_buys(), ); const BOB: AccountId = 808; const OVERBUY_CT: BalanceOf = 40 * ASSET_UNIT; From a71195f7aceb67e7b4216098805d8ab32b65b0e9 Mon Sep 17 00:00:00 2001 From: Juan Ignacio RIos Date: Fri, 21 Jul 2023 10:42:15 +0200 Subject: [PATCH 27/27] chore(214): unsafe math fix --- polimec-skeleton/pallets/funding/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/polimec-skeleton/pallets/funding/src/lib.rs b/polimec-skeleton/pallets/funding/src/lib.rs index 7f743055c..9125c9cd1 100644 --- a/polimec-skeleton/pallets/funding/src/lib.rs +++ b/polimec-skeleton/pallets/funding/src/lib.rs @@ -944,7 +944,7 @@ pub mod pallet { let mut consumed_weight = T::WeightInfo::insert_cleaned_project(); while !consumed_weight.any_gt(max_weight_per_project) { if let Ok(weight) = project_finalizer.do_one_operation::(project_id) { - consumed_weight += weight + consumed_weight.saturating_accrue(weight); } else { break; }