From 6bc36f09fab3a8c2292ccbf79ae675132de5ef9c Mon Sep 17 00:00:00 2001 From: ndkazu Date: Sat, 11 Jan 2025 19:51:35 +0900 Subject: [PATCH] vote extrinsic --- substrate/frame/opf/src/functions.rs | 113 ++++++++++++++++++++- substrate/frame/opf/src/lib.rs | 146 ++++++++++++++++++--------- substrate/frame/opf/src/mock.rs | 10 +- substrate/frame/opf/src/tests.rs | 3 +- substrate/frame/opf/src/types.rs | 32 +++--- 5 files changed, 235 insertions(+), 69 deletions(-) diff --git a/substrate/frame/opf/src/functions.rs b/substrate/frame/opf/src/functions.rs index 863cddad927f..dccb2abc753c 100644 --- a/substrate/frame/opf/src/functions.rs +++ b/substrate/frame/opf/src/functions.rs @@ -18,6 +18,110 @@ pub use super::*; impl Pallet { + // Helper function for voting action. Existing votes are over-written, and Hold is adjusted + pub fn try_vote( + voter_id: VoterId, + project: ProjectId, + amount: BalanceOf, + is_fund: bool, + conviction: Democracy::Conviction, + ) -> DispatchResult { + if !ProjectFunds::::contains_key(&project) { + let bounded = BoundedVec::, ConstU32<2>>::try_from(vec![ + BalanceOf::::zero(), + BalanceOf::::zero(), + ]) + .expect("It works"); + ProjectFunds::::insert(&project, bounded); + } + + let projects = WhiteListedProjectAccounts::::get(project.clone()) + .ok_or(Error::::NoProjectAvailable); + let conviction_fund = amount.saturating_add( + amount.saturating_mul(>::from(conviction).into()), + ); + + // Create vote infos and store/adjust them + let round_number = NextVotingRoundNumber::::get().saturating_sub(1); + let mut round = VotingRounds::::get(round_number).ok_or(Error::::NoRoundFound)?; + if is_fund { + round.total_positive_votes_amount = + round.total_positive_votes_amount.saturating_add(conviction_fund); + } else { + round.total_negative_votes_amount = + round.total_negative_votes_amount.saturating_add(conviction_fund); + } + + VotingRounds::::mutate(round_number, |val| { + *val = Some(round.clone()); + }); + + let mut new_vote = VoteInfo { + amount, + round: round.clone(), + is_fund, + conviction, + funds_unlock_block: round.round_ending_block, + }; + + // Update Funds unlock block according to the selected conviction + new_vote.funds_unlock(); + if Votes::::contains_key(&project, &voter_id) { + let old_vote = Votes::::get(&project, &voter_id).ok_or(Error::::NoVoteData)?; + let old_amount = old_vote.amount; + let old_conviction = old_vote.conviction; + let old_conviction_amount = + old_amount.saturating_add(old_amount.saturating_mul( + >::from(old_conviction).into(), + )); + ProjectFunds::::mutate(&project, |val| { + let mut val0 = val.clone().into_inner(); + if is_fund { + val0[0] = val0[0 as usize] + .saturating_add(conviction_fund) + .saturating_sub(old_conviction_amount); + } else { + val0[1] = val0[1 as usize] + .saturating_add(conviction_fund) + .saturating_sub(old_conviction_amount); + } + *val = BoundedVec::, ConstU32<2>>::try_from(val0).expect("It works"); + }); + + Votes::::mutate(&project, &voter_id, |value| { + *value = Some(new_vote); + }); + + // Adjust locked amount + let total_hold = T::NativeBalance::total_balance_on_hold(&voter_id); + let new_hold = total_hold.saturating_sub(old_amount).saturating_add(amount); + T::NativeBalance::set_on_hold(&HoldReason::FundsReserved.into(), &voter_id, new_hold)?; + } else { + Votes::::insert(&project, &voter_id, new_vote); + ProjectFunds::::mutate(&project, |val| { + let mut val0 = val.clone().into_inner(); + if is_fund { + val0[0] = val0[0 as usize].saturating_add(conviction_fund); + } else { + val0[1] = val0[1 as usize].saturating_add(conviction_fund); + } + *val = BoundedVec::, ConstU32<2>>::try_from(val0).expect("It works"); + }); + // Lock the necessary amount + T::NativeBalance::hold(&HoldReason::FundsReserved.into(), &voter_id, amount)?; + } + /* + let ref_index = + ReferendumIndexLog::::get(&project).ok_or(Error::::NoProjectAvailable)?; + + let vote = Democracy::Vote { aye: is_fund, conviction }; + let account_vote = Democracy::AccountVote::Standard{ vote, balance: amount }; + + Democracy::Pallet::::vote(&voter_id, ref_index, vote)?;*/ + + Ok(()) + } + pub fn pot_account() -> AccountIdOf { // Get Pot account T::PotId::get().into_account_truncating() @@ -70,7 +174,9 @@ impl Pallet { // Reward calculation is executed within the Voting period pub fn calculate_rewards(total_reward: BalanceOf) -> DispatchResult { let projects: Vec> = WhiteListedProjectAccounts::::iter_keys().collect(); - if projects.is_empty() { return Ok(()) } + if projects.is_empty() { + return Ok(()) + } let round_number = NextVotingRoundNumber::::get().saturating_sub(1); let round = VotingRounds::::get(round_number).ok_or(Error::::NoRoundFound)?; if projects.clone().len() > 0 as usize { @@ -95,12 +201,16 @@ impl Pallet { let project_percentage = Percent::from_rational(project_reward, total_votes_amount); let final_amount = project_percentage * total_reward; + let infos = WhiteListedProjectAccounts::::get(&project_id) + .ok_or(Error::::NoProjectAvailable)?; + let ref_index = infos.index; // Send calculated reward for reward distribution let project_info = ProjectInfo { project_id: project_id.clone(), submission_block: when, amount: final_amount, + index: ref_index, }; // create a spend for project to be rewarded @@ -151,7 +261,6 @@ impl Pallet { // Conditions for reward distribution preparations are: // - We are at the end of voting_round period if now > round_ending_block { - // Clear ProjectFunds storage ProjectFunds::::drain(); // Emmit events diff --git a/substrate/frame/opf/src/lib.rs b/substrate/frame/opf/src/lib.rs index 9858033cb899..69f6d40f6013 100644 --- a/substrate/frame/opf/src/lib.rs +++ b/substrate/frame/opf/src/lib.rs @@ -41,20 +41,20 @@ pub mod pallet { pub trait Config: frame_system::Config + Democracy::Config { type RuntimeEvent: From> + IsType<::RuntimeEvent>; type RuntimeCall: Convert<::RuntimeCall, ::RuntimeCall> - + Parameter - + UnfilteredDispatchable::RuntimeOrigin> - + From> - + GetDispatchInfo; + + Parameter + + UnfilteredDispatchable::RuntimeOrigin> + + From> + + GetDispatchInfo; /// The admin origin that can list and un-list whitelisted projects. type AdminOrigin: EnsureOrigin; /// Type to access the Balances Pallet. type NativeBalance: fungible::Inspect - + fungible::Mutate - + fungible::hold::Inspect - + fungible::hold::Mutate - + fungible::freeze::Inspect - + fungible::freeze::Mutate; + + fungible::Mutate + + fungible::hold::Inspect + + fungible::hold::Mutate + + fungible::freeze::Inspect + + fungible::freeze::Mutate; type RuntimeHoldReason: From; /// Provider for the block number. @@ -116,7 +116,7 @@ pub mod pallet { /// List of Whitelisted Project registered #[pallet::storage] pub type WhiteListedProjectAccounts = - CountedStorageMap<_, Twox64Concat, ProjectId, ProjectInfo, OptionQuery>; + CountedStorageMap<_, Twox64Concat, ProjectId, ProjectInfo, OptionQuery>; /// Returns (positive_funds,negative_funds) of Whitelisted Project accounts #[pallet::storage] @@ -128,6 +128,18 @@ pub mod pallet { ValueQuery, >; + /// Returns Votes Infos against (project_id, voter_id) key + #[pallet::storage] + pub type Votes = StorageDoubleMap< + _, + Blake2_128Concat, + ProjectId, + Twox64Concat, + VoterId, + VoteInfo, + OptionQuery, + >; + #[pallet::event] #[pallet::generate_deposit(pub(super) fn deposit_event)] pub enum Event { @@ -224,7 +236,9 @@ pub mod pallet { /// Another project has already been submitted under the same project_id SubmittedProjectId, /// Project batch already submitted - BatchAlreadySubmitted + BatchAlreadySubmitted, + NoVoteData, + NotEnoughFunds, } #[pallet::hooks] @@ -236,7 +250,6 @@ pub mod pallet { #[pallet::call] impl Pallet { - /// OPF Projects registration /// /// ## Dispatch Origin @@ -256,20 +269,23 @@ pub mod pallet { /// ## Events /// Emits [`Event::::Projectslisted`]. #[pallet::call_index(1)] - pub fn register_projects_batch(origin: OriginFor, projects_id: Vec>) -> DispatchResult { + pub fn register_projects_batch( + origin: OriginFor, + projects_id: Vec>, + ) -> DispatchResult { //T::AdminOrigin::ensure_origin_or_root(origin.clone())?; let who = T::SubmitOrigin::ensure_origin(origin.clone())?; // Only 1 batch submission per round let mut round_index = NextVotingRoundNumber::::get(); - // No active round? - if round_index == 0 { - // Start the first voting round - let _round0 = VotingRoundInfo::::new(); - round_index = NextVotingRoundNumber::::get(); - } + // No active round? + if round_index == 0 { + // Start the first voting round + let _round0 = VotingRoundInfo::::new(); + round_index = NextVotingRoundNumber::::get(); + } - let current_round_index = round_index.saturating_sub(1); + let current_round_index = round_index.saturating_sub(1); let round_infos = VotingRounds::::get(current_round_index).expect("InvalidResult"); // Check no Project batch has been submitted yet @@ -280,21 +296,36 @@ pub mod pallet { let when = T::BlockNumberProvider::current_block_number(); if when >= round_ending_block { // Create a new round. - let _new_round = VotingRoundInfo::::new(); + let _new_round = VotingRoundInfo::::new(); } - for project_id in &projects_id{ + for project_id in &projects_id { ProjectInfo::::new(project_id.clone()); // Prepare the proposal call - let call0: ::RuntimeCall = crate::Call::::on_registration {project_id: project_id.clone()}.into(); + let call0: ::RuntimeCall = + crate::Call::::on_registration { project_id: project_id.clone() }.into(); let call = ::RuntimeCall::convert(call0); - let call_f = T::Preimages::bound(call).unwrap(); + let call_f = T::Preimages::bound(call)?; let threshold = Democracy::VoteThreshold::SimpleMajority; - Democracy::Pallet::::propose(origin.clone(), call_f.clone(), T::MinimumDeposit::get())?; - Democracy::Pallet::::internal_start_referendum(call_f,threshold, Zero::zero()); - + Democracy::Pallet::::propose( + origin.clone(), + call_f.clone(), + T::MinimumDeposit::get(), + )?; + let referendum_index = Democracy::Pallet::::internal_start_referendum( + call_f, + threshold, + Zero::zero(), + ); + let mut new_infos = WhiteListedProjectAccounts::::get(&project_id) + .ok_or(Error::::NoProjectAvailable)?; + new_infos.index = referendum_index; + + WhiteListedProjectAccounts::::mutate(project_id, |value| { + *value = Some(new_infos); + }); } - + Self::deposit_event(Event::Projectslisted { when, projects_id }); Ok(()) } @@ -333,7 +364,33 @@ pub mod pallet { #[pallet::call_index(3)] #[transactional] - pub fn vote(origin: OriginFor) -> DispatchResult { + pub fn vote( + origin: OriginFor, + project_id: ProjectId, + #[pallet::compact] amount: BalanceOf, + is_fund: bool, + conviction: Democracy::Conviction, + ) -> DispatchResult { + let voter = ensure_signed(origin.clone())?; + // Get current voting round & check if we are in voting period or not + Self::period_check()?; + // Check that voter has enough funds to vote + let voter_balance = T::NativeBalance::total_balance(&voter); + ensure!(voter_balance > amount, Error::::NotEnoughFunds); + + // Check the available un-holded balance + let voter_holds = T::NativeBalance::total_balance_on_hold(&voter); + let available_funds = voter_balance.saturating_sub(voter_holds); + ensure!(available_funds > amount, Error::::NotEnoughFunds); + + let infos = WhiteListedProjectAccounts::::get(&project_id) + .ok_or(Error::::NoProjectAvailable)?; + let ref_index = infos.index; + let vote = Democracy::Vote { aye: is_fund, conviction }; + // let account_vote = Democracy::AccountVote::Standard { vote, balance: amount }; + + // Democracy::Pallet::::vote(origin, ref_index, account_vote)?; + Ok(()) } @@ -370,23 +427,20 @@ pub mod pallet { let now = T::BlockNumberProvider::current_block_number(); let info = Spends::::get(&project_id).ok_or(Error::::InexistentSpend)?; if now >= info.expire { - Spends::::remove(&project_id); - Self::deposit_event(Event::ExpiredClaim { - expired_when: info.expire, - project_id, - }); - Ok(()) - } else if now < info.expire { - // transfer the funds - Self::spend(info.amount, project_id.clone())?; - Self::deposit_event(Event::RewardClaimed { - when: now, - amount: info.amount, - project_id: project_id.clone(), - }); - Self::unlist_project(project_id)?; - Ok(()) - } else { + Spends::::remove(&project_id); + Self::deposit_event(Event::ExpiredClaim { expired_when: info.expire, project_id }); + Ok(()) + } else if now < info.expire { + // transfer the funds + Self::spend(info.amount, project_id.clone())?; + Self::deposit_event(Event::RewardClaimed { + when: now, + amount: info.amount, + project_id: project_id.clone(), + }); + Self::unlist_project(project_id)?; + Ok(()) + } else { Err(DispatchError::Other("Not Claiming Period")) } } diff --git a/substrate/frame/opf/src/mock.rs b/substrate/frame/opf/src/mock.rs index e6bbb19cb443..1d10d9d741f1 100644 --- a/substrate/frame/opf/src/mock.rs +++ b/substrate/frame/opf/src/mock.rs @@ -17,14 +17,15 @@ //! Test environment for OPF pallet. use crate as pallet_opf; -use codec::{Decode, Encode, MaxEncodedLen}; use crate::Convert; +use codec::{Decode, Encode, MaxEncodedLen}; pub use frame_support::{ derive_impl, ord_parameter_types, pallet_prelude::TypeInfo, parameter_types, traits::{ - ConstU32, ConstU64, SortedMembers, EqualPrivilegeOnly, OnFinalize, OnInitialize, OriginTrait, VoteTally, + ConstU32, ConstU64, EqualPrivilegeOnly, OnFinalize, OnInitialize, OriginTrait, + SortedMembers, VoteTally, }, weights::Weight, PalletId, @@ -38,7 +39,6 @@ pub type Block = frame_system::mocking::MockBlock; pub type Balance = u64; pub type AccountId = u64; - // Configure a mock runtime to test the pallet. frame_support::construct_runtime!( pub struct Test { @@ -169,9 +169,9 @@ impl Convert for RuntimeCall { fn convert(call: RuntimeCall) -> RuntimeCall { let call_encoded: Vec = call.encode(); let ref_call_encoded = &call_encoded; - if let Ok(call_formatted) = RuntimeCall::decode(&mut &ref_call_encoded[..]){ + if let Ok(call_formatted) = RuntimeCall::decode(&mut &ref_call_encoded[..]) { call_formatted - } else{ + } else { call } } diff --git a/substrate/frame/opf/src/tests.rs b/substrate/frame/opf/src/tests.rs index 18a3b29baad0..8a2fd819f1b4 100644 --- a/substrate/frame/opf/src/tests.rs +++ b/substrate/frame/opf/src/tests.rs @@ -32,9 +32,8 @@ pub fn next_block() { ); } -pub fn project_list() -> Vec>{ +pub fn project_list() -> Vec> { vec![ALICE, BOB, DAVE] - } pub fn run_to_block(n: BlockNumberFor) { diff --git a/substrate/frame/opf/src/types.rs b/substrate/frame/opf/src/types.rs index ae6ac1581654..906288f48e42 100644 --- a/substrate/frame/opf/src/types.rs +++ b/substrate/frame/opf/src/types.rs @@ -20,12 +20,11 @@ pub use super::*; pub use frame_support::{ - pallet_prelude::*, - traits::UnfilteredDispatchable, dispatch::GetDispatchInfo, + pallet_prelude::*, traits::{ fungible, - fungible::{Inspect, Mutate, MutateHold}, + fungible::{Inspect, InspectHold, Mutate, MutateHold}, fungibles, schedule::{ v3::{Anon as ScheduleAnon, Named as ScheduleNamed}, @@ -33,14 +32,13 @@ pub use frame_support::{ }, tokens::{Precision, Preservation}, Bounded, DefensiveOption, EnsureOrigin, LockIdentifier, OriginTrait, QueryPreimage, - StorePreimage, + StorePreimage, UnfilteredDispatchable, }, transactional, weights::WeightMeter, PalletId, Serialize, }; pub use frame_system::{pallet_prelude::*, RawOrigin}; -pub use pallet_conviction_voting::Conviction; pub use scale_info::prelude::vec::Vec; pub use sp_runtime::{ traits::{ @@ -125,16 +123,19 @@ pub struct ProjectInfo { /// Amount to be locked & payed for this project pub amount: BalanceOf, + + /// Referendum Index + pub index: u32, } impl ProjectInfo { pub fn new(project_id: ProjectId) { let submission_block = T::BlockNumberProvider::current_block_number(); let amount = Zero::zero(); - let project_info = ProjectInfo { project_id: project_id.clone(), submission_block, amount }; + let project_info = + ProjectInfo { project_id: project_id.clone(), submission_block, amount, index: 0 }; WhiteListedProjectAccounts::::insert(project_id, project_info); } - } #[derive(Encode, Decode, Clone, PartialEq, MaxEncodedLen, RuntimeDebug, TypeInfo)] @@ -149,7 +150,7 @@ pub struct VoteInfo { /// Whether the vote is "fund" / "not fund" pub is_fund: bool, - pub conviction: Conviction, + pub conviction: Democracy::Conviction, pub funds_unlock_block: ProvidedBlockNumberFor, } @@ -157,7 +158,7 @@ pub struct VoteInfo { // If no conviction, user's funds are released at the end of the voting round impl VoteInfo { pub fn funds_unlock(&mut self) { - let conviction_coeff = >::from(self.conviction); + let conviction_coeff = >::from(self.conviction); let funds_unlock_block = self .round .round_ending_block @@ -173,7 +174,7 @@ impl Default for VoteInfo { let round = VotingRounds::::get(0).expect("Round 0 exists"); let amount = Zero::zero(); let is_fund = false; - let conviction = Conviction::None; + let conviction = Democracy::Conviction::None; let funds_unlock_block = round.round_ending_block; VoteInfo { amount, round, is_fund, conviction, funds_unlock_block } } @@ -195,10 +196,13 @@ impl VotingRoundInfo { pub fn new() -> Self { let round_starting_block = T::BlockNumberProvider::current_block_number(); let batch_submitted = false; - let round_ending_block = round_starting_block - .clone() - .saturating_add(::VotingPeriod::get()); - let round_number = NextVotingRoundNumber::::mutate(|n| { let res = *n; *n = n.saturating_add(1); res }); + let round_ending_block = + round_starting_block.clone().saturating_add(::VotingPeriod::get()); + let round_number = NextVotingRoundNumber::::mutate(|n| { + let res = *n; + *n = n.saturating_add(1); + res + }); let total_positive_votes_amount = BalanceOf::::zero(); let total_negative_votes_amount = BalanceOf::::zero();