From 8fcf2944b924192b99bc7b093a78733d24a25cd5 Mon Sep 17 00:00:00 2001 From: Juan Ignacio Rios <54085674+JuaniRios@users.noreply.github.com> Date: Tue, 11 Jun 2024 16:46:26 +0200 Subject: [PATCH] =?UTF-8?q?=E2=9C=85=20Test=20freeze=20restrictions=20on?= =?UTF-8?q?=20contributions=20(#308)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## What? - Test what happens when you contribute (community/remainder) with frozen balance, i.e. a hold on a freeze. ## Why? - To double-check our assumptions about these interactions ## How? - An contributor holds ED + an amount frozen. - It uses that frozen amount to contribute - The project fails / succeeds - If it fails, he gets the PLMC back, if it succeeds, he waits and then vests the full amount. - In the end, the user still has the freeze restriction ## Testing? `can_contribute_with_frozen_tokens_funding_success` and `can_contribute_with_frozen_tokens_funding_failed` --- pallets/funding/src/tests/2_evaluation.rs | 7 +- pallets/funding/src/tests/3_auction.rs | 1 - pallets/funding/src/tests/4_community.rs | 198 +++++++++++++++++- pallets/funding/src/tests/5_remainder.rs | 243 ++++++++++++++++++++++ runtimes/politest/src/lib.rs | 2 +- 5 files changed, 442 insertions(+), 9 deletions(-) diff --git a/pallets/funding/src/tests/2_evaluation.rs b/pallets/funding/src/tests/2_evaluation.rs index 10a824296..17b7f9c7f 100644 --- a/pallets/funding/src/tests/2_evaluation.rs +++ b/pallets/funding/src/tests/2_evaluation.rs @@ -484,12 +484,8 @@ mod evaluate_extrinsic { #[cfg(test)] mod success { use super::*; - use frame_support::traits::{ - fungible::{InspectFreeze, Mutate}, - tokens::Preservation, - }; + use frame_support::traits::fungible::InspectFreeze; use pallet_balances::AccountData; - use sp_runtime::DispatchError::Token; #[test] fn all_investor_types() { @@ -675,7 +671,6 @@ mod evaluate_extrinsic { let evaluation_held_balance = inst.get_reserved_plmc_balance_for(EVALUATOR_4, HoldReason::Evaluation(project_id).into()); let frozen_balance = inst.execute(|| mock::Balances::balance_frozen(&(), &EVALUATOR_4)); - let account_data = inst.execute(|| System::account(&EVALUATOR_4)).data; assert_eq!(free_balance, inst.get_ed()); assert_eq!(evaluation_held_balance, frozen_amount); diff --git a/pallets/funding/src/tests/3_auction.rs b/pallets/funding/src/tests/3_auction.rs index 183c9230d..a6b34b141 100644 --- a/pallets/funding/src/tests/3_auction.rs +++ b/pallets/funding/src/tests/3_auction.rs @@ -866,7 +866,6 @@ mod bid_extrinsic { mod success { use super::*; use frame_support::{dispatch::DispatchResultWithPostInfo, traits::fungible::InspectFreeze}; - use pallet_balances::AccountData; #[test] fn evaluation_bond_counts_towards_bid() { diff --git a/pallets/funding/src/tests/4_community.rs b/pallets/funding/src/tests/4_community.rs index e041482b7..0a34dda90 100644 --- a/pallets/funding/src/tests/4_community.rs +++ b/pallets/funding/src/tests/4_community.rs @@ -315,7 +315,8 @@ mod community_contribute_extrinsic { #[cfg(test)] mod success { use super::*; - use frame_support::dispatch::DispatchResultWithPostInfo; + use frame_support::{dispatch::DispatchResultWithPostInfo, traits::fungible::InspectFreeze}; + use polimec_common_test_utils::get_mock_jwt; #[test] fn evaluation_bond_counts_towards_contribution() { @@ -842,6 +843,201 @@ mod community_contribute_extrinsic { bid_should_succeed(BIDDER_6, InvestorType::Retail, BIDDER_6); bid_should_succeed(BUYER_6, InvestorType::Retail, BIDDER_6); } + + #[test] + fn can_contribute_with_frozen_tokens_funding_failed() { + let mut inst = MockInstantiator::new(Some(RefCell::new(new_test_ext()))); + let issuer = ISSUER_1; + let project_metadata = default_project_metadata(issuer); + let project_id = inst.create_community_contributing_project( + project_metadata.clone(), + issuer, + default_evaluations(), + vec![], + ); + let wap = inst.get_project_details(project_id).weighted_average_price.unwrap(); + + let contribution = ContributionParams::new(BUYER_4, 500 * CT_UNIT, 1u8, AcceptedFundingAsset::USDT); + let plmc_required = inst.calculate_contributed_plmc_spent(vec![contribution.clone()], wap); + let frozen_amount = plmc_required[0].plmc_amount; + let plmc_existential_deposits = plmc_required.accounts().existential_deposits(); + + inst.mint_plmc_to(plmc_existential_deposits); + inst.mint_plmc_to(plmc_required.clone()); + + inst.execute(|| { + mock::Balances::set_freeze(&(), &BUYER_4, plmc_required[0].plmc_amount).unwrap(); + }); + + let usdt_required = inst.calculate_contributed_funding_asset_spent(vec![contribution.clone()], wap); + inst.mint_foreign_asset_to(usdt_required); + + inst.execute(|| { + assert_noop!( + Balances::transfer_allow_death(RuntimeOrigin::signed(BUYER_4), ISSUER_1, frozen_amount,), + TokenError::Frozen + ); + }); + + inst.execute(|| { + assert_ok!(PolimecFunding::community_contribute( + RuntimeOrigin::signed(BUYER_4), + get_mock_jwt_with_cid( + BUYER_4, + InvestorType::Institutional, + generate_did_from_account(BUYER_4), + project_metadata.clone().policy_ipfs_cid.unwrap() + ), + project_id, + contribution.amount, + contribution.multiplier, + contribution.asset + )); + }); + + inst.start_remainder_or_end_funding(project_id).unwrap(); + inst.finish_funding(project_id).unwrap(); + + assert_eq!(inst.get_project_details(project_id).status, ProjectStatus::FundingFailed); + + let free_balance = inst.get_free_plmc_balance_for(BUYER_4); + let bid_held_balance = + inst.get_reserved_plmc_balance_for(BUYER_4, HoldReason::Participation(project_id).into()); + let frozen_balance = inst.execute(|| mock::Balances::balance_frozen(&(), &BUYER_4)); + + assert_eq!(free_balance, inst.get_ed()); + assert_eq!(bid_held_balance, frozen_amount); + assert_eq!(frozen_balance, frozen_amount); + + inst.execute(|| { + PolimecFunding::settle_failed_contribution(RuntimeOrigin::signed(BUYER_4), project_id, BUYER_4, 0) + .unwrap(); + }); + + let free_balance = inst.get_free_plmc_balance_for(BUYER_4); + let bid_held_balance = + inst.get_reserved_plmc_balance_for(BUYER_4, HoldReason::Evaluation(project_id).into()); + let frozen_balance = inst.execute(|| mock::Balances::balance_frozen(&(), &BUYER_4)); + + assert_eq!(free_balance, inst.get_ed() + frozen_amount); + assert_eq!(bid_held_balance, Zero::zero()); + assert_eq!(frozen_balance, frozen_amount); + } + + #[test] + fn can_contribute_with_frozen_tokens_funding_success() { + let mut inst = MockInstantiator::new(Some(RefCell::new(new_test_ext()))); + let issuer = ISSUER_1; + let project_metadata = default_project_metadata(issuer); + let project_id = inst.create_community_contributing_project( + project_metadata.clone(), + issuer, + default_evaluations(), + default_bids(), + ); + let wap = inst.get_project_details(project_id).weighted_average_price.unwrap(); + + let contribution = ContributionParams::new(BUYER_4, 500 * CT_UNIT, 5u8, AcceptedFundingAsset::USDT); + let plmc_required = inst.calculate_contributed_plmc_spent(vec![contribution.clone()], wap); + let frozen_amount = plmc_required[0].plmc_amount; + let plmc_existential_deposits = plmc_required.accounts().existential_deposits(); + + inst.mint_plmc_to(plmc_existential_deposits); + inst.mint_plmc_to(plmc_required.clone()); + + inst.execute(|| { + mock::Balances::set_freeze(&(), &BUYER_4, plmc_required[0].plmc_amount).unwrap(); + }); + + let usdt_required = inst.calculate_contributed_funding_asset_spent(vec![contribution.clone()], wap); + inst.mint_foreign_asset_to(usdt_required); + + inst.execute(|| { + assert_noop!( + Balances::transfer_allow_death(RuntimeOrigin::signed(BUYER_4), ISSUER_1, frozen_amount,), + TokenError::Frozen + ); + }); + + inst.execute(|| { + assert_ok!(PolimecFunding::community_contribute( + RuntimeOrigin::signed(BUYER_4), + get_mock_jwt_with_cid( + BUYER_4, + InvestorType::Institutional, + generate_did_from_account(BUYER_4), + project_metadata.clone().policy_ipfs_cid.unwrap() + ), + project_id, + contribution.amount, + contribution.multiplier, + contribution.asset + )); + }); + + inst.start_remainder_or_end_funding(project_id).unwrap(); + inst.finish_funding(project_id).unwrap(); + + assert_eq!(inst.get_project_details(project_id).status, ProjectStatus::AwaitingProjectDecision); + inst.execute(|| { + assert_ok!(PolimecFunding::decide_project_outcome( + RuntimeOrigin::signed(ISSUER_1), + get_mock_jwt(ISSUER_1, InvestorType::Institutional, generate_did_from_account(ISSUER_1)), + project_id, + FundingOutcomeDecision::AcceptFunding + )); + }); + let decision_block = inst + .get_update_block(project_id, &UpdateType::ProjectDecision(FundingOutcomeDecision::AcceptFunding)) + .unwrap(); + inst.jump_to_block(decision_block); + + let settlement_block = inst.get_update_block(project_id, &UpdateType::StartSettlement).unwrap(); + inst.jump_to_block(settlement_block); + + let free_balance = inst.get_free_plmc_balance_for(BUYER_4); + let bid_held_balance = + inst.get_reserved_plmc_balance_for(BUYER_4, HoldReason::Participation(project_id).into()); + let frozen_balance = inst.execute(|| mock::Balances::balance_frozen(&(), &BUYER_4)); + + assert_eq!(free_balance, inst.get_ed()); + assert_eq!(bid_held_balance, frozen_amount); + assert_eq!(frozen_balance, frozen_amount); + + inst.execute(|| { + PolimecFunding::settle_successful_contribution(RuntimeOrigin::signed(BUYER_4), project_id, BUYER_4, 0) + .unwrap(); + }); + + let free_balance = inst.get_free_plmc_balance_for(BUYER_4); + let bid_held_balance = + inst.get_reserved_plmc_balance_for(BUYER_4, HoldReason::Participation(project_id).into()); + let frozen_balance = inst.execute(|| mock::Balances::balance_frozen(&(), &BUYER_4)); + + assert_eq!(free_balance, inst.get_ed()); + assert_eq!(bid_held_balance, frozen_amount); + assert_eq!(frozen_balance, frozen_amount); + + let vest_duration = + MultiplierOf::::new(5u8).unwrap().calculate_vesting_duration::(); + let now = inst.current_block(); + inst.jump_to_block(now + vest_duration + 1u64); + inst.execute(|| { + assert_ok!(mock::LinearRelease::vest( + RuntimeOrigin::signed(BUYER_4), + HoldReason::Participation(project_id).into() + )); + }); + + let free_balance = inst.get_free_plmc_balance_for(BUYER_4); + let bid_held_balance = + inst.get_reserved_plmc_balance_for(BUYER_4, HoldReason::Participation(project_id).into()); + let frozen_balance = inst.execute(|| mock::Balances::balance_frozen(&(), &BUYER_4)); + + assert_eq!(free_balance, inst.get_ed() + frozen_amount); + assert_eq!(bid_held_balance, Zero::zero()); + assert_eq!(frozen_balance, frozen_amount); + } } #[cfg(test)] diff --git a/pallets/funding/src/tests/5_remainder.rs b/pallets/funding/src/tests/5_remainder.rs index a5d7f79c9..e6f953269 100644 --- a/pallets/funding/src/tests/5_remainder.rs +++ b/pallets/funding/src/tests/5_remainder.rs @@ -329,6 +329,8 @@ mod remaining_contribute_extrinsic { #[cfg(test)] mod success { use super::*; + use frame_support::traits::fungible::InspectFreeze; + use pallet_balances::AccountData; #[test] fn evaluation_bond_counts_towards_contribution() { @@ -974,6 +976,247 @@ mod remaining_contribute_extrinsic { bid_should_succeed(BIDDER_6, InvestorType::Retail, BIDDER_6); bid_should_succeed(BUYER_6, InvestorType::Retail, BIDDER_6); } + + #[test] + fn can_contribute_with_frozen_tokens_funding_failed() { + let mut inst = MockInstantiator::new(Some(RefCell::new(new_test_ext()))); + let issuer = ISSUER_1; + let project_metadata = default_project_metadata(issuer); + let project_id = inst.create_remainder_contributing_project( + project_metadata.clone(), + issuer, + default_evaluations(), + vec![], + vec![], + ); + let wap = inst.get_project_details(project_id).weighted_average_price.unwrap(); + + let contribution = ContributionParams::new(BUYER_4, 500 * CT_UNIT, 1u8, AcceptedFundingAsset::USDT); + let plmc_required = inst.calculate_contributed_plmc_spent(vec![contribution.clone()], wap); + let frozen_amount = plmc_required[0].plmc_amount; + let plmc_existential_deposits = plmc_required.accounts().existential_deposits(); + + inst.mint_plmc_to(plmc_existential_deposits); + inst.mint_plmc_to(plmc_required.clone()); + + inst.execute(|| { + mock::Balances::set_freeze(&(), &BUYER_4, plmc_required[0].plmc_amount).unwrap(); + }); + + let usdt_required = inst.calculate_contributed_funding_asset_spent(vec![contribution.clone()], wap); + inst.mint_foreign_asset_to(usdt_required); + + inst.execute(|| { + assert_noop!( + Balances::transfer_allow_death(RuntimeOrigin::signed(BUYER_4), ISSUER_1, frozen_amount,), + TokenError::Frozen + ); + }); + + inst.execute(|| { + assert_ok!(PolimecFunding::remaining_contribute( + RuntimeOrigin::signed(BUYER_4), + get_mock_jwt_with_cid( + BUYER_4, + InvestorType::Institutional, + generate_did_from_account(BUYER_4), + project_metadata.clone().policy_ipfs_cid.unwrap() + ), + project_id, + contribution.amount, + contribution.multiplier, + contribution.asset + )); + }); + + inst.finish_funding(project_id).unwrap(); + + assert_eq!(inst.get_project_details(project_id).status, ProjectStatus::FundingFailed); + + let free_balance = inst.get_free_plmc_balance_for(BUYER_4); + let bid_held_balance = + inst.get_reserved_plmc_balance_for(BUYER_4, HoldReason::Participation(project_id).into()); + let frozen_balance = inst.execute(|| mock::Balances::balance_frozen(&(), &BUYER_4)); + let account_data = inst.execute(|| System::account(&BUYER_4)).data; + + assert_eq!(free_balance, inst.get_ed()); + assert_eq!(bid_held_balance, frozen_amount); + assert_eq!(frozen_balance, frozen_amount); + let expected_account_data = AccountData { + free: inst.get_ed(), + reserved: frozen_amount, + frozen: frozen_amount, + flags: Default::default(), + }; + assert_eq!(account_data, expected_account_data); + + inst.execute(|| { + PolimecFunding::settle_failed_contribution(RuntimeOrigin::signed(BUYER_4), project_id, BUYER_4, 0) + .unwrap(); + }); + + let free_balance = inst.get_free_plmc_balance_for(BUYER_4); + let bid_held_balance = + inst.get_reserved_plmc_balance_for(BUYER_4, HoldReason::Evaluation(project_id).into()); + let frozen_balance = inst.execute(|| mock::Balances::balance_frozen(&(), &BUYER_4)); + let account_data = inst.execute(|| System::account(&BUYER_4)).data; + + assert_eq!(free_balance, inst.get_ed() + frozen_amount); + assert_eq!(bid_held_balance, Zero::zero()); + assert_eq!(frozen_balance, frozen_amount); + let expected_account_data = AccountData { + free: inst.get_ed() + frozen_amount, + reserved: Zero::zero(), + frozen: frozen_amount, + flags: Default::default(), + }; + assert_eq!(account_data, expected_account_data); + assert_eq!(account_data.frozen, account_data.free - inst.get_ed()); + } + + #[test] + fn can_contribute_with_frozen_tokens_funding_success() { + let mut inst = MockInstantiator::new(Some(RefCell::new(new_test_ext()))); + let issuer = ISSUER_1; + let project_metadata = default_project_metadata(issuer); + let project_id = inst.create_remainder_contributing_project( + project_metadata.clone(), + issuer, + default_evaluations(), + default_bids(), + vec![], + ); + let wap = inst.get_project_details(project_id).weighted_average_price.unwrap(); + + let contribution = ContributionParams::new(BUYER_4, 500 * CT_UNIT, 5u8, AcceptedFundingAsset::USDT); + let plmc_required = inst.calculate_contributed_plmc_spent(vec![contribution.clone()], wap); + let frozen_amount = plmc_required[0].plmc_amount; + let plmc_existential_deposits = plmc_required.accounts().existential_deposits(); + + inst.mint_plmc_to(plmc_existential_deposits); + inst.mint_plmc_to(plmc_required.clone()); + + inst.execute(|| { + mock::Balances::set_freeze(&(), &BUYER_4, plmc_required[0].plmc_amount).unwrap(); + }); + + let usdt_required = inst.calculate_contributed_funding_asset_spent(vec![contribution.clone()], wap); + inst.mint_foreign_asset_to(usdt_required); + + inst.execute(|| { + assert_noop!( + Balances::transfer_allow_death(RuntimeOrigin::signed(BUYER_4), ISSUER_1, frozen_amount,), + TokenError::Frozen + ); + }); + + inst.execute(|| { + assert_ok!(PolimecFunding::remaining_contribute( + RuntimeOrigin::signed(BUYER_4), + get_mock_jwt_with_cid( + BUYER_4, + InvestorType::Institutional, + generate_did_from_account(BUYER_4), + project_metadata.clone().policy_ipfs_cid.unwrap() + ), + project_id, + contribution.amount, + contribution.multiplier, + contribution.asset + )); + }); + + inst.finish_funding(project_id).unwrap(); + + assert_eq!(inst.get_project_details(project_id).status, ProjectStatus::AwaitingProjectDecision); + inst.execute(|| { + assert_ok!(PolimecFunding::decide_project_outcome( + RuntimeOrigin::signed(ISSUER_1), + get_mock_jwt_with_cid( + ISSUER_1, + InvestorType::Institutional, + generate_did_from_account(ISSUER_1), + project_metadata.policy_ipfs_cid.unwrap() + ), + project_id, + FundingOutcomeDecision::AcceptFunding + )); + }); + let decision_block = inst + .get_update_block(project_id, &UpdateType::ProjectDecision(FundingOutcomeDecision::AcceptFunding)) + .unwrap(); + inst.jump_to_block(decision_block); + + let settlement_block = inst.get_update_block(project_id, &UpdateType::StartSettlement).unwrap(); + inst.jump_to_block(settlement_block); + + let free_balance = inst.get_free_plmc_balance_for(BUYER_4); + let bid_held_balance = + inst.get_reserved_plmc_balance_for(BUYER_4, HoldReason::Participation(project_id).into()); + let frozen_balance = inst.execute(|| mock::Balances::balance_frozen(&(), &BUYER_4)); + let account_data = inst.execute(|| System::account(&BUYER_4)).data; + + assert_eq!(free_balance, inst.get_ed()); + assert_eq!(bid_held_balance, frozen_amount); + assert_eq!(frozen_balance, frozen_amount); + let expected_account_data = AccountData { + free: inst.get_ed(), + reserved: frozen_amount, + frozen: frozen_amount, + flags: Default::default(), + }; + assert_eq!(account_data, expected_account_data); + + inst.execute(|| { + PolimecFunding::settle_successful_contribution(RuntimeOrigin::signed(BUYER_4), project_id, BUYER_4, 0) + .unwrap(); + }); + + let free_balance = inst.get_free_plmc_balance_for(BUYER_4); + let bid_held_balance = + inst.get_reserved_plmc_balance_for(BUYER_4, HoldReason::Participation(project_id).into()); + let frozen_balance = inst.execute(|| mock::Balances::balance_frozen(&(), &BUYER_4)); + let account_data = inst.execute(|| System::account(&BUYER_4)).data; + + assert_eq!(free_balance, inst.get_ed()); + assert_eq!(bid_held_balance, frozen_amount); + assert_eq!(frozen_balance, frozen_amount); + let expected_account_data = AccountData { + free: inst.get_ed(), + reserved: frozen_amount, + frozen: frozen_amount, + flags: Default::default(), + }; + assert_eq!(account_data, expected_account_data); + + let vest_duration = + MultiplierOf::::new(5u8).unwrap().calculate_vesting_duration::(); + let now = inst.current_block(); + inst.jump_to_block(now + vest_duration + 1u64); + inst.execute(|| { + assert_ok!(mock::LinearRelease::vest( + RuntimeOrigin::signed(BUYER_4), + HoldReason::Participation(project_id).into() + )); + }); + + let free_balance = inst.get_free_plmc_balance_for(BUYER_4); + let bid_held_balance = + inst.get_reserved_plmc_balance_for(BUYER_4, HoldReason::Participation(project_id).into()); + let frozen_balance = inst.execute(|| mock::Balances::balance_frozen(&(), &BUYER_4)); + let account_data = inst.execute(|| System::account(&BUYER_4)).data; + + assert_eq!(free_balance, inst.get_ed() + frozen_amount); + assert_eq!(bid_held_balance, Zero::zero()); + assert_eq!(frozen_balance, frozen_amount); + let expected_account_data = AccountData { + free: inst.get_ed() + frozen_amount, + reserved: Zero::zero(), + frozen: frozen_amount, + flags: Default::default(), + }; + assert_eq!(account_data, expected_account_data); + } } #[cfg(test)] diff --git a/runtimes/politest/src/lib.rs b/runtimes/politest/src/lib.rs index 44b9cdf1d..d025336fc 100644 --- a/runtimes/politest/src/lib.rs +++ b/runtimes/politest/src/lib.rs @@ -169,7 +169,7 @@ pub mod migrations { use crate::Runtime; /// Unreleased migrations. Add new ones here: - pub type Unreleased = (pallet_funding::storage_migrations::v3::MigrationToV3); + pub type Unreleased = pallet_funding::storage_migrations::v3::MigrationToV3; } /// Executive: handles dispatch to the various modules.