pallet_governance/
proposal.rs

1use codec::{Decode, Encode, MaxEncodedLen};
2use pallet_torus0::namespace::NamespacePricingConfig;
3use polkadot_sdk::{
4    frame_election_provider_support::Get,
5    frame_support::{
6        dispatch::DispatchResult, ensure, storage::with_storage_layer, traits::Currency,
7    },
8    polkadot_sdk_frame::{prelude::BlockNumberFor, traits::CheckedAdd},
9    sp_core::{ConstU32, U256},
10    sp_runtime::{
11        traits::Saturating, BoundedBTreeMap, DispatchError, FixedPointNumber, FixedU128, Percent,
12    },
13    sp_std::{collections::btree_set::BTreeSet, vec::Vec},
14    sp_tracing::error,
15};
16
17use crate::{
18    frame::traits::ExistenceRequirement, AccountIdOf, BalanceOf, BoundedBTreeSet, BoundedVec,
19    DaoTreasuryAddress, DebugNoBound, Error, GlobalGovernanceConfig, GovernanceConfiguration,
20    NotDelegatingVotingPower, Proposals, TypeInfo, UnrewardedProposals,
21};
22
23pub type ProposalId = u64;
24
25/// A network proposal created by the community. Core part of the DAO.
26#[derive(Clone, DebugNoBound, TypeInfo, Decode, Encode, MaxEncodedLen)]
27#[scale_info(skip_type_params(T))]
28pub struct Proposal<T: crate::Config> {
29    pub id: ProposalId,
30    pub proposer: AccountIdOf<T>,
31    pub expiration_block: BlockNumberFor<T>,
32    /// The actual data and type of the proposal.
33    pub data: ProposalData<T>,
34    pub status: ProposalStatus<T>,
35    pub metadata: BoundedVec<u8, ConstU32<256>>,
36    pub proposal_cost: BalanceOf<T>,
37    pub creation_block: BlockNumberFor<T>,
38}
39
40impl<T: crate::Config> Proposal<T> {
41    /// Whether the proposal is still active.
42    #[must_use]
43    pub fn is_active(&self) -> bool {
44        matches!(self.status, ProposalStatus::Open { .. })
45    }
46
47    /// Returns the block in which a proposal should be executed.
48    /// For emission proposals, that is the creation block + 21600 blocks
49    /// (roughly 2 days at 1 block every 8 seconds), as for the others, they
50    /// are only executed on the expiration block.
51    pub fn execution_block(&self) -> BlockNumberFor<T> {
52        match self.data {
53            ProposalData::Emission { .. } => self.creation_block.saturating_add(
54                U256::from(21_600)
55                    .try_into()
56                    .ok()
57                    .expect("this is a safe conversion"),
58            ),
59            _ => self.expiration_block,
60        }
61    }
62
63    /// Marks a proposal as accepted and executes it.
64    pub fn accept(
65        mut self,
66        block: BlockNumberFor<T>,
67        stake_for: BalanceOf<T>,
68        stake_against: BalanceOf<T>,
69    ) -> DispatchResult {
70        ensure!(self.is_active(), crate::Error::<T>::ProposalIsFinished);
71
72        self.status = ProposalStatus::Accepted {
73            block,
74            stake_for,
75            stake_against,
76        };
77
78        Proposals::<T>::insert(self.id, &self);
79        crate::Pallet::<T>::deposit_event(crate::Event::ProposalAccepted(self.id));
80
81        self.execute_proposal()?;
82
83        Ok(())
84    }
85
86    /// Executes the changes.
87    fn execute_proposal(self) -> DispatchResult {
88        // Proposal fee is given back to the proposer.
89        let _ = <T as crate::Config>::Currency::transfer(
90            &crate::DaoTreasuryAddress::<T>::get(),
91            &self.proposer,
92            self.proposal_cost,
93            ExistenceRequirement::AllowDeath,
94        );
95
96        match self.data {
97            ProposalData::GlobalParams(data) => {
98                let GlobalParamsData {
99                    min_name_length,
100                    max_name_length,
101                    min_weight_control_fee,
102                    min_staking_fee,
103                    dividends_participation_weight,
104                    namespace_pricing_config,
105                    proposal_cost,
106                } = data;
107
108                pallet_torus0::MinNameLength::<T>::set(min_name_length);
109                pallet_torus0::MaxNameLength::<T>::set(max_name_length);
110                pallet_torus0::DividendsParticipationWeight::<T>::set(
111                    dividends_participation_weight,
112                );
113                pallet_torus0::FeeConstraints::<T>::mutate(|constraints| {
114                    constraints.min_weight_control_fee =
115                        Percent::from_percent(min_weight_control_fee);
116                    constraints.min_staking_fee = Percent::from_percent(min_staking_fee);
117                });
118                pallet_torus0::NamespacePricingConfig::<T>::set(namespace_pricing_config);
119                crate::GlobalGovernanceConfig::<T>::mutate(|config| {
120                    config.proposal_cost = proposal_cost;
121                });
122            }
123
124            ProposalData::TransferDaoTreasury { account, amount } => {
125                <T as crate::Config>::Currency::transfer(
126                    &DaoTreasuryAddress::<T>::get(),
127                    &account,
128                    amount,
129                    ExistenceRequirement::AllowDeath,
130                )
131                .map_err(|_| crate::Error::<T>::InternalError)?;
132            }
133
134            ProposalData::Emission {
135                recycling_percentage,
136                treasury_percentage,
137                incentives_ratio,
138            } => {
139                pallet_emission0::EmissionRecyclingPercentage::<T>::set(recycling_percentage);
140                crate::TreasuryEmissionFee::<T>::set(treasury_percentage);
141                pallet_emission0::IncentivesRatio::<T>::set(incentives_ratio);
142            }
143
144            ProposalData::GlobalCustom => {}
145        }
146
147        Ok(())
148    }
149
150    /// Marks a proposal as refused.
151    pub fn refuse(
152        mut self,
153        block: BlockNumberFor<T>,
154        stake_for: BalanceOf<T>,
155        stake_against: BalanceOf<T>,
156    ) -> DispatchResult {
157        ensure!(self.is_active(), crate::Error::<T>::ProposalIsFinished);
158
159        self.status = ProposalStatus::Refused {
160            block,
161            stake_for,
162            stake_against,
163        };
164
165        Proposals::<T>::insert(self.id, &self);
166        crate::Pallet::<T>::deposit_event(crate::Event::ProposalRefused(self.id));
167
168        Ok(())
169    }
170
171    /// Marks a proposal as expired.
172    pub fn expire(mut self, block_number: BlockNumberFor<T>) -> DispatchResult {
173        ensure!(self.is_active(), crate::Error::<T>::ProposalIsFinished);
174        ensure!(
175            block_number >= self.expiration_block,
176            crate::Error::<T>::InvalidProposalFinalizationParameters
177        );
178
179        self.status = ProposalStatus::Expired;
180
181        Proposals::<T>::insert(self.id, &self);
182        crate::Pallet::<T>::deposit_event(crate::Event::ProposalExpired(self.id));
183
184        Ok(())
185    }
186}
187
188#[derive(Clone, DebugNoBound, TypeInfo, Decode, Encode, MaxEncodedLen, PartialEq, Eq)]
189#[scale_info(skip_type_params(T))]
190pub enum ProposalStatus<T: crate::Config> {
191    /// The proposal is active and being voted upon. The votes values only hold
192    /// accounts and not stake per key, because this is subtle to change
193    /// overtime. The stake values are there to help clients estimate the status
194    /// of the voting, they are updated every few blocks, but are not used in
195    /// the final calculation.
196    Open {
197        /// Accounts who have voted for this proposal to be accepted.
198        votes_for: BoundedBTreeSet<AccountIdOf<T>, ConstU32<{ u32::MAX }>>,
199        /// Accounts who have voted against this proposal being accepted.
200        votes_against: BoundedBTreeSet<AccountIdOf<T>, ConstU32<{ u32::MAX }>>,
201        /// A roughly estimation of the total stake voting for the proposal.
202        stake_for: BalanceOf<T>,
203        /// A roughly estimation of the total stake voting against the proposal.
204        stake_against: BalanceOf<T>,
205    },
206    /// Proposal was accepted.
207    Accepted {
208        block: BlockNumberFor<T>,
209        /// Total stake that voted for the proposal.
210        stake_for: BalanceOf<T>,
211        /// Total stake that voted against the proposal.
212        stake_against: BalanceOf<T>,
213    },
214    /// Proposal was refused.
215    Refused {
216        block: BlockNumberFor<T>,
217        /// Total stake that voted for the proposal.
218        stake_for: BalanceOf<T>,
219        /// Total stake that voted against the proposal.
220        stake_against: BalanceOf<T>,
221    },
222    /// Proposal expired without enough network participation.
223    Expired,
224}
225
226// TODO: add Agent URL max length
227/// Update the global parameters configuration, like, max and min name lengths,
228/// and other validations. All values are set within default storage values.
229#[derive(Clone, DebugNoBound, TypeInfo, Decode, Encode, MaxEncodedLen, PartialEq, Eq)]
230#[scale_info(skip_type_params(T))]
231pub struct GlobalParamsData<T: crate::Config> {
232    pub min_name_length: u16,
233    pub max_name_length: u16,
234    pub min_weight_control_fee: u8,
235    pub min_staking_fee: u8,
236    pub dividends_participation_weight: Percent,
237    pub namespace_pricing_config: NamespacePricingConfig<T>,
238    pub proposal_cost: BalanceOf<T>,
239}
240
241impl<T: crate::Config> GlobalParamsData<T> {
242    pub fn validate(&self) -> DispatchResult {
243        ensure!(
244            self.min_name_length > 1,
245            crate::Error::<T>::InvalidMinNameLength
246        );
247
248        ensure!(
249            (self.max_name_length as u32) < T::MaxAgentNameLengthConstraint::get(),
250            crate::Error::<T>::InvalidMaxNameLength
251        );
252
253        ensure!(
254            self.min_weight_control_fee <= 100,
255            crate::Error::<T>::InvalidMinWeightControlFee
256        );
257
258        ensure!(
259            self.min_staking_fee <= 100,
260            crate::Error::<T>::InvalidMinStakingFee
261        );
262
263        ensure!(
264            self.proposal_cost <= 50_000_000_000_000_000_000_000,
265            crate::Error::<T>::InvalidProposalCost
266        );
267
268        Ok(())
269    }
270}
271
272/// The proposal type and data.
273#[derive(Clone, DebugNoBound, TypeInfo, Decode, Encode, MaxEncodedLen, PartialEq, Eq)]
274#[scale_info(skip_type_params(T))]
275pub enum ProposalData<T: crate::Config> {
276    /// Applies changes to global parameters.
277    GlobalParams(GlobalParamsData<T>),
278    /// A custom proposal with not immediate impact in the chain. Can be used as
279    /// referendums regarding the future of the chain.
280    GlobalCustom,
281    /// Changes the emission rates for incentives, recycling and treasury.
282    Emission {
283        /// The amount of tokens per block to be recycled ("burned").
284        recycling_percentage: Percent,
285        /// The amount of tokens sent to the treasury AFTER recycling fee was
286        /// applied.
287        treasury_percentage: Percent,
288        /// This changes how incentives and dividends are distributed. 50% means
289        /// they are distributed equally.
290        incentives_ratio: Percent,
291    },
292    /// Transfers funds from the treasury account to the specified account.
293    TransferDaoTreasury {
294        account: AccountIdOf<T>,
295        amount: BalanceOf<T>,
296    },
297}
298
299impl<T: crate::Config> ProposalData<T> {
300    /// The percentage of total active stake participating in the proposal for
301    /// it to be processes (either approved or refused).
302    #[must_use]
303    pub fn required_stake(&self) -> Percent {
304        match self {
305            Self::Emission { .. } => Percent::from_parts(10),
306            Self::GlobalCustom | Self::TransferDaoTreasury { .. } => Percent::from_parts(50),
307            Self::GlobalParams { .. } => Percent::from_parts(40),
308        }
309    }
310}
311
312#[derive(DebugNoBound, TypeInfo, Decode, Encode, MaxEncodedLen, PartialEq, Eq)]
313#[scale_info(skip_type_params(T))]
314pub struct UnrewardedProposal<T: crate::Config> {
315    pub block: BlockNumberFor<T>,
316    pub votes_for: BoundedBTreeMap<AccountIdOf<T>, BalanceOf<T>, ConstU32<{ u32::MAX }>>,
317    pub votes_against: BoundedBTreeMap<AccountIdOf<T>, BalanceOf<T>, ConstU32<{ u32::MAX }>>,
318}
319
320/// Create global update parameters proposal with metadata.
321#[allow(clippy::too_many_arguments)]
322pub fn add_global_params_proposal<T: crate::Config>(
323    proposer: AccountIdOf<T>,
324    data: GlobalParamsData<T>,
325    metadata: Vec<u8>,
326) -> DispatchResult {
327    data.validate()?;
328    let data = ProposalData::<T>::GlobalParams(data);
329
330    add_proposal::<T>(proposer, data, metadata)
331}
332
333/// Create global custom proposal with metadata.
334pub fn add_global_custom_proposal<T: crate::Config>(
335    proposer: AccountIdOf<T>,
336    metadata: Vec<u8>,
337) -> DispatchResult {
338    add_proposal(proposer, ProposalData::<T>::GlobalCustom, metadata)
339}
340
341/// Create a treasury transfer proposal with metadata.
342pub fn add_dao_treasury_transfer_proposal<T: crate::Config>(
343    proposer: AccountIdOf<T>,
344    value: BalanceOf<T>,
345    destination_key: AccountIdOf<T>,
346    metadata: Vec<u8>,
347) -> DispatchResult {
348    let data = ProposalData::<T>::TransferDaoTreasury {
349        account: destination_key,
350        amount: value,
351    };
352
353    add_proposal::<T>(proposer, data, metadata)
354}
355
356/// Creates a new emissions proposal. Only valid if `recycling_percentage +
357/// treasury_percentage <= u128::MAX`.
358pub fn add_emission_proposal<T: crate::Config>(
359    proposer: AccountIdOf<T>,
360    recycling_percentage: Percent,
361    treasury_percentage: Percent,
362    incentives_ratio: Percent,
363    metadata: Vec<u8>,
364) -> DispatchResult {
365    ensure!(
366        recycling_percentage
367            .checked_add(&treasury_percentage)
368            .is_some(),
369        crate::Error::<T>::InvalidEmissionProposalData
370    );
371
372    let data = ProposalData::<T>::Emission {
373        recycling_percentage,
374        treasury_percentage,
375        incentives_ratio,
376    };
377
378    add_proposal::<T>(proposer, data, metadata)
379}
380
381/// Creates a new proposal and saves it. Internally used.
382fn add_proposal<T: crate::Config>(
383    proposer: AccountIdOf<T>,
384    data: ProposalData<T>,
385    metadata: Vec<u8>,
386) -> DispatchResult {
387    ensure!(
388        !metadata.is_empty(),
389        crate::Error::<T>::ProposalDataTooSmall
390    );
391    ensure!(
392        metadata.len() <= 256,
393        crate::Error::<T>::ProposalDataTooLarge
394    );
395
396    let config = GlobalGovernanceConfig::<T>::get();
397
398    let cost = config.proposal_cost;
399    <T as crate::Config>::Currency::transfer(
400        &proposer,
401        &crate::DaoTreasuryAddress::<T>::get(),
402        cost,
403        ExistenceRequirement::AllowDeath,
404    )
405    .map_err(|_| crate::Error::<T>::NotEnoughBalanceToApply)?;
406
407    let proposal_id: u64 = crate::Proposals::<T>::iter()
408        .count()
409        .try_into()
410        .map_err(|_| crate::Error::<T>::InternalError)?;
411
412    let current_block = <polkadot_sdk::frame_system::Pallet<T>>::block_number();
413
414    let proposal = Proposal::<T> {
415        id: proposal_id,
416        proposer,
417        expiration_block: current_block.saturating_add(config.proposal_expiration),
418        data,
419        status: ProposalStatus::Open {
420            votes_for: BoundedBTreeSet::new(),
421            votes_against: BoundedBTreeSet::new(),
422            stake_for: 0,
423            stake_against: 0,
424        },
425        metadata: BoundedVec::truncate_from(metadata),
426        proposal_cost: cost,
427        creation_block: current_block,
428    };
429
430    crate::Proposals::<T>::insert(proposal_id, proposal);
431
432    Ok(())
433}
434
435/// Every 100 blocks, iterates through all pending proposals and executes the
436/// ones eligible.
437pub fn tick_proposals<T: crate::Config>(block_number: BlockNumberFor<T>) {
438    let block_number_u64: u64 = block_number
439        .try_into()
440        .ok()
441        .expect("blocknumber wont be greater than 2^64");
442    if block_number_u64 % 100 != 0 {
443        return;
444    }
445
446    let not_delegating = NotDelegatingVotingPower::<T>::get().into_inner();
447
448    let proposals = Proposals::<T>::iter().filter(|(_, p)| p.is_active());
449
450    for (id, proposal) in proposals {
451        let res = with_storage_layer(|| tick_proposal(&not_delegating, block_number, proposal));
452        if let Err(err) = res {
453            error!("failed to tick proposal {id}: {err:?}, skipping...");
454        }
455    }
456}
457
458/// Returns the minimum amount of active stake needed for a proposal be executed
459/// based on the given percentage.
460fn get_minimum_stake_to_execute_with_percentage<T: crate::Config>(
461    threshold: Percent,
462) -> BalanceOf<T> {
463    let stake = pallet_torus0::TotalStake::<T>::get();
464    threshold.mul_floor(stake)
465}
466
467/// Sums all stakes for votes in favor and against. The biggest value wins and
468/// the proposal is processes and executed. expiration block.
469fn tick_proposal<T: crate::Config>(
470    not_delegating: &BTreeSet<T::AccountId>,
471    block_number: BlockNumberFor<T>,
472    mut proposal: Proposal<T>,
473) -> DispatchResult {
474    let ProposalStatus::Open {
475        votes_for,
476        votes_against,
477        ..
478    } = &proposal.status
479    else {
480        return Err(Error::<T>::ProposalIsFinished.into());
481    };
482
483    let votes_for: Vec<(AccountIdOf<T>, BalanceOf<T>)> = votes_for
484        .iter()
485        .cloned()
486        .map(|id| {
487            let stake = calc_stake::<T>(not_delegating, &id);
488            (id, stake)
489        })
490        .collect();
491    let votes_against: Vec<(AccountIdOf<T>, BalanceOf<T>)> = votes_against
492        .iter()
493        .cloned()
494        .map(|id| {
495            let stake = calc_stake::<T>(not_delegating, &id);
496            (id, stake)
497        })
498        .collect();
499
500    let stake_for_sum: BalanceOf<T> = votes_for.iter().map(|(_, stake)| stake).sum();
501    let stake_against_sum: BalanceOf<T> = votes_against.iter().map(|(_, stake)| stake).sum();
502
503    if block_number < proposal.expiration_block {
504        if let ProposalStatus::Open {
505            stake_for,
506            stake_against,
507            ..
508        } = &mut proposal.status
509        {
510            *stake_for = stake_for_sum;
511            *stake_against = stake_against_sum;
512        }
513        Proposals::<T>::set(proposal.id, Some(proposal.clone()));
514    }
515
516    if block_number < proposal.execution_block() {
517        return Ok(());
518    }
519
520    let total_stake = stake_for_sum.saturating_add(stake_against_sum);
521    let minimal_stake_to_execute =
522        get_minimum_stake_to_execute_with_percentage::<T>(proposal.data.required_stake());
523
524    if total_stake >= minimal_stake_to_execute {
525        create_unrewarded_proposal::<T>(proposal.id, block_number, votes_for, votes_against);
526        if stake_against_sum > stake_for_sum {
527            proposal.refuse(block_number, stake_for_sum, stake_against_sum)
528        } else {
529            proposal.accept(block_number, stake_for_sum, stake_against_sum)
530        }
531    } else if block_number >= proposal.expiration_block {
532        create_unrewarded_proposal::<T>(proposal.id, block_number, votes_for, votes_against);
533        proposal.expire(block_number)
534    } else {
535        Ok(())
536    }
537}
538
539type AccountStakes<T> = BoundedBTreeMap<AccountIdOf<T>, BalanceOf<T>, ConstU32<{ u32::MAX }>>;
540
541/// Put the proposal in the reward queue, which will be processed by
542/// [tick_proposal_rewards].
543fn create_unrewarded_proposal<T: crate::Config>(
544    proposal_id: u64,
545    block_number: BlockNumberFor<T>,
546    votes_for: Vec<(AccountIdOf<T>, BalanceOf<T>)>,
547    votes_against: Vec<(AccountIdOf<T>, BalanceOf<T>)>,
548) {
549    let mut reward_votes_for = BoundedBTreeMap::new();
550    for (key, value) in votes_for {
551        let _ = reward_votes_for.try_insert(key, value);
552    }
553
554    let mut reward_votes_against: AccountStakes<T> = BoundedBTreeMap::new();
555    for (key, value) in votes_against {
556        let _ = reward_votes_against.try_insert(key, value);
557    }
558
559    UnrewardedProposals::<T>::insert(
560        proposal_id,
561        UnrewardedProposal::<T> {
562            block: block_number,
563            votes_for: reward_votes_for,
564            votes_against: reward_votes_against,
565        },
566    );
567}
568
569/// Calculates the stake for a voter. This function takes into account all
570/// accounts delegating voting power to the voter.
571#[inline]
572fn calc_stake<T: crate::Config>(
573    not_delegating: &BTreeSet<T::AccountId>,
574    voter: &T::AccountId,
575) -> BalanceOf<T> {
576    let own_stake: BalanceOf<T> = if !not_delegating.contains(voter) {
577        0
578    } else {
579        pallet_torus0::stake::sum_staking_to::<T>(voter)
580    };
581
582    let delegated_stake = pallet_torus0::stake::get_staked_by_vector::<T>(voter)
583        .into_iter()
584        .filter(|(staker, _)| !not_delegating.contains(staker))
585        .map(|(_, stake)| stake)
586        .sum();
587
588    own_stake.saturating_add(delegated_stake)
589}
590
591/// Processes the proposal reward queue and distributes rewards for all voters.
592pub fn tick_proposal_rewards<T: crate::Config>(block_number: BlockNumberFor<T>) {
593    let governance_config = crate::GlobalGovernanceConfig::<T>::get();
594
595    let block_number: u64 = block_number
596        .try_into()
597        .ok()
598        .expect("blocknumber wont be greater than 2^64");
599    let proposal_reward_interval: u64 = governance_config
600        .proposal_reward_interval
601        .try_into()
602        .ok()
603        .expect("blocknumber wont be greater than 2^64");
604
605    let reached_interval = block_number
606        .checked_rem(proposal_reward_interval)
607        .is_some_and(|r| r == 0);
608    if !reached_interval {
609        return;
610    }
611
612    let mut n = 0u16;
613    let mut account_stakes: AccountStakes<T> = BoundedBTreeMap::new();
614    let mut total_allocation = FixedU128::from_inner(0);
615    for (proposal_id, unrewarded_proposal) in UnrewardedProposals::<T>::iter() {
616        let proposal_block: u64 = unrewarded_proposal
617            .block
618            .try_into()
619            .ok()
620            .expect("blocknumber wont be greater than 2^64");
621
622        // Just checking if it's in the chain interval
623        if proposal_block < block_number.saturating_sub(proposal_reward_interval) {
624            continue;
625        }
626
627        for (acc_id, stake) in unrewarded_proposal
628            .votes_for
629            .into_iter()
630            .chain(unrewarded_proposal.votes_against.into_iter())
631        {
632            let curr_stake = *account_stakes.get(&acc_id).unwrap_or(&0u128);
633            let _ = account_stakes.try_insert(acc_id, curr_stake.saturating_add(stake));
634        }
635
636        match get_reward_allocation::<T>(&governance_config, n) {
637            Ok(allocation) => total_allocation = total_allocation.saturating_add(allocation),
638            Err(err) => {
639                error!("could not get reward allocation for proposal {proposal_id}: {err:?}");
640                continue;
641            }
642        }
643
644        UnrewardedProposals::<T>::remove(proposal_id);
645        n = n.saturating_add(1);
646    }
647
648    distribute_proposal_rewards::<T>(
649        account_stakes,
650        total_allocation,
651        governance_config.max_proposal_reward_treasury_allocation,
652    );
653}
654
655/// Calculates the total balance to be rewarded for a proposal.
656pub fn get_reward_allocation<T: crate::Config>(
657    governance_config: &GovernanceConfiguration<T>,
658    n: u16,
659) -> Result<FixedU128, DispatchError> {
660    let treasury_address = DaoTreasuryAddress::<T>::get();
661    let treasury_balance = <T as crate::Config>::Currency::free_balance(&treasury_address);
662
663    let allocation_percentage = governance_config.proposal_reward_treasury_allocation;
664    let max_allocation = governance_config.max_proposal_reward_treasury_allocation;
665
666    let mut allocation = FixedU128::from_inner(
667        allocation_percentage
668            .mul_floor(treasury_balance)
669            .min(max_allocation),
670    );
671
672    if n > 0 {
673        let mut base = FixedU128::from_inner((1.5 * FixedU128::DIV as f64) as u128);
674        let mut result = FixedU128::from_u32(1);
675        let mut remaining = n;
676
677        while remaining > 0 {
678            if remaining % 2 == 1 {
679                result = result.const_checked_mul(base).unwrap_or(result);
680            }
681            base = base.const_checked_mul(base).unwrap_or_default();
682            remaining /= 2;
683        }
684
685        allocation = allocation.const_checked_div(result).unwrap_or(allocation);
686    }
687
688    Ok(allocation)
689}
690
691/// Distributes the proposal rewards in a quadratic formula to all voters.
692fn distribute_proposal_rewards<T: crate::Config>(
693    account_stakes: AccountStakes<T>,
694    total_allocation: FixedU128,
695    max_proposal_reward_treasury_allocation: BalanceOf<T>,
696) {
697    // This is just a sanity check, making sure we can never allocate more than the
698    // max
699    if total_allocation > FixedU128::from_inner(max_proposal_reward_treasury_allocation) {
700        error!("total allocation exceeds max proposal reward treasury allocation");
701        return;
702    }
703
704    use polkadot_sdk::frame_support::sp_runtime::traits::IntegerSquareRoot;
705
706    let dao_treasury_address = DaoTreasuryAddress::<T>::get();
707    let account_sqrt_stakes: Vec<_> = account_stakes
708        .into_iter()
709        .map(|(acc_id, stake)| (acc_id, stake.integer_sqrt()))
710        .collect();
711
712    let total_stake: BalanceOf<T> = account_sqrt_stakes.iter().map(|(_, stake)| *stake).sum();
713    let total_stake = FixedU128::from_inner(total_stake);
714
715    for (acc_id, stake) in account_sqrt_stakes.into_iter() {
716        let percentage = FixedU128::from_inner(stake)
717            .const_checked_div(total_stake)
718            .unwrap_or_default();
719
720        let reward = total_allocation
721            .const_checked_mul(percentage)
722            .unwrap_or_default()
723            .into_inner();
724
725        // Transfer the proposal reward to the accounts from treasury
726        if let Err(err) = <T as crate::Config>::Currency::transfer(
727            &dao_treasury_address,
728            &acc_id,
729            reward,
730            ExistenceRequirement::AllowDeath,
731        ) {
732            error!("could not transfer proposal reward: {err:?}")
733        }
734    }
735}