diff --git a/pallets/funding/src/mock.rs b/pallets/funding/src/mock.rs index 4f9643705..216ee7091 100644 --- a/pallets/funding/src/mock.rs +++ b/pallets/funding/src/mock.rs @@ -571,9 +571,14 @@ sp_api::mock_impl_runtime_apis! { } impl ExtrinsicHelpers for TestRuntime { - fn funding_asset_to_ct_amount(project_id: ProjectId, asset: AcceptedFundingAsset, asset_amount: Balance) -> Balance { - PolimecFunding::funding_asset_to_ct_amount(project_id, asset, asset_amount) + fn funding_asset_to_ct_amount_classic(project_id: ProjectId, funding_asset: AcceptedFundingAsset, funding_asset_amount: Balance) -> Balance { + PolimecFunding::funding_asset_to_ct_amount_classic(project_id, funding_asset, funding_asset_amount) } + + fn funding_asset_to_ct_amount_otm(project_id: ProjectId, funding_asset: AcceptedFundingAsset, funding_asset_amount: Balance) -> (Balance, Balance) { + PolimecFunding::funding_asset_to_ct_amount_otm(project_id, funding_asset, funding_asset_amount) + } + fn get_next_vesting_schedule_merge_candidates(account: AccountId, hold_reason: RuntimeHoldReason, end_max_delta: Balance) -> Option<(u32, u32)> { PolimecFunding::get_next_vesting_schedule_merge_candidates(account, hold_reason, end_max_delta) } @@ -581,6 +586,7 @@ sp_api::mock_impl_runtime_apis! { fn calculate_otm_fee(funding_asset: AcceptedFundingAsset, funding_asset_amount: Balance) -> Option { PolimecFunding::calculate_otm_fee(funding_asset, funding_asset_amount) } + fn get_funding_asset_min_max_amounts(project_id: ProjectId, did: Did, funding_asset: AcceptedFundingAsset, investor_type: InvestorType) -> Option<(Balance, Balance)> { PolimecFunding::get_funding_asset_min_max_amounts(project_id, did, funding_asset, investor_type) } diff --git a/pallets/funding/src/runtime_api.rs b/pallets/funding/src/runtime_api.rs index 9c1ecc354..1959f98e3 100644 --- a/pallets/funding/src/runtime_api.rs +++ b/pallets/funding/src/runtime_api.rs @@ -1,13 +1,16 @@ -use crate::traits::BondingRequirementCalculation; #[allow(clippy::wildcard_imports)] use crate::*; +use crate::{traits::BondingRequirementCalculation, HoldReason::Participation}; use alloc::collections::BTreeMap; use frame_support::traits::fungibles::{Inspect, InspectEnumerable}; use itertools::Itertools; use parity_scale_codec::{Decode, Encode}; use polimec_common::{credentials::InvestorType, ProvideAssetPrice, USD_DECIMALS}; use scale_info::TypeInfo; +use sp_arithmetic::Perquintill; +use sp_core::Get; use sp_runtime::traits::Zero; + #[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, TypeInfo)] pub struct ProjectParticipationIds { account: AccountIdOf, @@ -57,7 +60,10 @@ sp_api::decl_runtime_apis! { pub trait ExtrinsicHelpers { /// Get the current price of a contribution token (either current bucket in the auction, or WAP in contribution phase), /// and calculate the amount of tokens that can be bought with the given amount USDT/USDC/DOT. - fn funding_asset_to_ct_amount(project_id: ProjectId, asset: AcceptedFundingAsset, asset_amount: Balance) -> Balance; + fn funding_asset_to_ct_amount_classic(project_id: ProjectId, funding_asset: AcceptedFundingAsset, funding_asset_amount: Balance) -> Balance; + + /// Calculate how many CTs and what the OTM fee is for a given project and funding asset amount. + fn funding_asset_to_ct_amount_otm(project_id: ProjectId, funding_asset: AcceptedFundingAsset, funding_asset_amount: Balance) -> (Balance, Balance); /// Get the indexes of vesting schedules that are good candidates to be merged. /// Schedules that have not yet started are de-facto bad candidates. @@ -68,6 +74,7 @@ sp_api::decl_runtime_apis! { /// Gets the minimum and maximum amount of FundingAsset a user can input in the UI. fn get_funding_asset_min_max_amounts(project_id: ProjectId, did: Did, funding_asset: AcceptedFundingAsset, investor_type: InvestorType) -> Option<(Balance, Balance)>; + } } @@ -138,7 +145,7 @@ impl Pallet { .collect_vec() } - pub fn funding_asset_to_ct_amount( + pub fn funding_asset_to_ct_amount_classic( project_id: ProjectId, asset: AcceptedFundingAsset, asset_amount: Balance, @@ -176,6 +183,53 @@ impl Pallet { ct_amount } + pub fn funding_asset_to_ct_amount_otm( + project_id: ProjectId, + funding_asset: AcceptedFundingAsset, + total_funding_asset_amount: Balance, + ) -> (Balance, Balance) { + let project_details = ProjectsDetails::::get(project_id).expect("Project not found"); + let funding_asset_usd_price = + Pallet::::get_decimals_aware_funding_asset_price(&funding_asset).expect("Price not found"); + let otm_multiplier = ParticipationMode::OTM.multiplier(); + let otm_fee_percentage = ::FeePercentage::get() / otm_multiplier; + + let divisor = FixedU128::from_perbill(otm_fee_percentage) + FixedU128::one(); + let participating_funding_asset_amount = + divisor.reciprocal().unwrap().saturating_mul_int(total_funding_asset_amount); + let fee_funding_asset_amount = total_funding_asset_amount.saturating_sub(participating_funding_asset_amount); + + let participating_usd_ticket_size = + funding_asset_usd_price.saturating_mul_int(participating_funding_asset_amount); + + let mut ct_amount = Zero::zero(); + + // Contribution phase + if let Some(wap) = project_details.weighted_average_price { + ct_amount = wap.reciprocal().expect("Bad math").saturating_mul_int(participating_usd_ticket_size); + } + // Auction phase, we need to consider multiple buckets + else { + let mut usd_to_spend = participating_usd_ticket_size; + let mut current_bucket = Buckets::::get(project_id).expect("Bucket not found"); + while usd_to_spend > Zero::zero() { + let bucket_price = current_bucket.current_price; + + let ct_to_buy = bucket_price.reciprocal().expect("Bad math").saturating_mul_int(usd_to_spend); + let ct_to_buy = ct_to_buy.min(current_bucket.amount_left); + + ct_amount = ct_amount.saturating_add(ct_to_buy); + // if usd spent is 0, we will have an infinite loop + let usd_spent = bucket_price.saturating_mul_int(ct_to_buy).max(One::one()); + usd_to_spend = usd_to_spend.saturating_sub(usd_spent); + + current_bucket.update(ct_to_buy) + } + } + + (ct_amount, fee_funding_asset_amount) + } + pub fn get_next_vesting_schedule_merge_candidates( account_id: AccountIdOf, hold_reason: ::RuntimeHoldReason, diff --git a/pallets/funding/src/tests/3_auction.rs b/pallets/funding/src/tests/3_auction.rs index c3e06b2b0..154fa3d9d 100644 --- a/pallets/funding/src/tests/3_auction.rs +++ b/pallets/funding/src/tests/3_auction.rs @@ -903,7 +903,7 @@ mod bid_extrinsic { inst.mint_funding_asset_to(vec![required_usdt.clone()]); let ct_participation = inst.execute(|| { - >::funding_asset_to_ct_amount( + >::funding_asset_to_ct_amount_classic( project_id, AcceptedFundingAsset::USDT, USDT_PARTICIPATION, @@ -1057,7 +1057,7 @@ mod bid_extrinsic { inst.mint_funding_asset_to(vec![required_usdt.clone()]); let ct_participation = inst.execute(|| { - >::funding_asset_to_ct_amount( + >::funding_asset_to_ct_amount_classic( project_id, AcceptedFundingAsset::USDT, USDT_PARTICIPATION, diff --git a/pallets/funding/src/tests/4_contribution.rs b/pallets/funding/src/tests/4_contribution.rs index 7a49b944f..587c240d6 100644 --- a/pallets/funding/src/tests/4_contribution.rs +++ b/pallets/funding/src/tests/4_contribution.rs @@ -1067,7 +1067,7 @@ mod contribute_extrinsic { inst.mint_funding_asset_to(vec![required_usdt.clone()]); let ct_participation = inst.execute(|| { - >::funding_asset_to_ct_amount( + >::funding_asset_to_ct_amount_classic( project_id, AcceptedFundingAsset::USDT, USDT_PARTICIPATION, @@ -1221,7 +1221,7 @@ mod contribute_extrinsic { inst.mint_funding_asset_to(vec![required_usdt.clone()]); let ct_participation = inst.execute(|| { - >::funding_asset_to_ct_amount( + >::funding_asset_to_ct_amount_classic( project_id, AcceptedFundingAsset::USDT, USDT_PARTICIPATION, diff --git a/pallets/funding/src/tests/runtime_api.rs b/pallets/funding/src/tests/runtime_api.rs index cdd9528ca..053bd4fb4 100644 --- a/pallets/funding/src/tests/runtime_api.rs +++ b/pallets/funding/src/tests/runtime_api.rs @@ -347,7 +347,7 @@ fn funding_asset_to_ct_amount() { let expected_ct_amount_contribution = 9_315 * CT_UNIT; inst.execute(|| { let block_hash = System::block_hash(System::block_number()); - let ct_amount = TestRuntime::funding_asset_to_ct_amount( + let ct_amount = TestRuntime::funding_asset_to_ct_amount_classic( &TestRuntime, block_hash, project_id_1, @@ -381,7 +381,7 @@ fn funding_asset_to_ct_amount() { let expected_ct_amount_contribution = 5_714_720_000_000_000_000; inst.execute(|| { let block_hash = System::block_hash(System::block_number()); - let ct_amount = TestRuntime::funding_asset_to_ct_amount( + let ct_amount = TestRuntime::funding_asset_to_ct_amount_classic( &TestRuntime, block_hash, project_id_2, @@ -433,7 +433,7 @@ fn funding_asset_to_ct_amount() { inst.execute(|| { let block_hash = System::block_hash(System::block_number()); - let ct_amount = TestRuntime::funding_asset_to_ct_amount( + let ct_amount = TestRuntime::funding_asset_to_ct_amount_classic( &TestRuntime, block_hash, project_id_3, @@ -455,7 +455,7 @@ fn funding_asset_to_ct_amount() { inst.execute(|| { let block_hash = System::block_hash(System::block_number()); - let ct_amount = TestRuntime::funding_asset_to_ct_amount( + let ct_amount = TestRuntime::funding_asset_to_ct_amount_classic( &TestRuntime, block_hash, project_id_3, @@ -570,7 +570,7 @@ fn calculate_otm_fee() { let ct_amount = inst .execute(|| { - TestRuntime::funding_asset_to_ct_amount( + TestRuntime::funding_asset_to_ct_amount_classic( &TestRuntime, block_hash, project_id, @@ -752,7 +752,7 @@ fn get_funding_asset_min_max_amounts() { // This test requires the buyer to have contributed 4500 USD before calling the API let required_ct = inst .execute(|| { - TestRuntime::funding_asset_to_ct_amount( + TestRuntime::funding_asset_to_ct_amount_classic( &TestRuntime, block_hash, project_id, diff --git a/runtimes/polimec/src/lib.rs b/runtimes/polimec/src/lib.rs index c36a1ca5b..5ec98261f 100644 --- a/runtimes/polimec/src/lib.rs +++ b/runtimes/polimec/src/lib.rs @@ -1534,8 +1534,11 @@ impl_runtime_apis! { } impl pallet_funding::runtime_api::ExtrinsicHelpers for Runtime { - fn funding_asset_to_ct_amount(project_id: ProjectId, asset: AcceptedFundingAsset, asset_amount: Balance) -> Balance { - Funding::funding_asset_to_ct_amount(project_id, asset, asset_amount) + fn funding_asset_to_ct_amount_classic(project_id: ProjectId, asset: AcceptedFundingAsset, asset_amount: Balance) -> Balance { + Funding::funding_asset_to_ct_amount_classic(project_id, asset, asset_amount) + } + fn funding_asset_to_ct_amount_otm(project_id: ProjectId, asset: AcceptedFundingAsset, asset_amount: Balance) -> (Balance, Balance) { + Funding::funding_asset_to_ct_amount_otm(project_id, asset, asset_amount) } fn get_next_vesting_schedule_merge_candidates(account: AccountId, hold_reason: RuntimeHoldReason, end_max_delta: Balance) -> Option<(u32, u32)> { Funding::get_next_vesting_schedule_merge_candidates(account, hold_reason, end_max_delta)