Coverage Report

Created: 2025-09-12 14:04

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/home/runner/work/torus-substrate/torus-substrate/pallets/governance/src/proposal.rs
Line
Count
Source
1
use codec::{Decode, Encode, MaxEncodedLen};
2
use pallet_torus0::namespace::NamespacePricingConfig;
3
use 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
        BoundedBTreeMap, DispatchError, FixedPointNumber, FixedU128, Percent, traits::Saturating,
12
    },
13
    sp_std::{collections::btree_set::BTreeSet, vec::Vec},
14
    sp_tracing::error,
15
};
16
17
use crate::{
18
    AccountIdOf, BalanceOf, BoundedBTreeSet, BoundedVec, DaoTreasuryAddress, DebugNoBound, Error,
19
    GlobalGovernanceConfig, GovernanceConfiguration, NotDelegatingVotingPower, Proposals, TypeInfo,
20
    UnrewardedProposals, frame::traits::ExistenceRequirement,
21
};
22
23
pub 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))]
28
pub 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
40
impl<T: crate::Config> Proposal<T> {
41
    /// Whether the proposal is still active.
42
    #[must_use]
43
3.92k
    pub fn is_active(&self) -> bool {
44
3.92k
        
matches!1.08k
(self.status, ProposalStatus::Open { .. })
45
3.92k
    }
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
2.82k
    pub fn execution_block(&self) -> BlockNumberFor<T> {
52
2.82k
        match self.data {
53
2.81k
            ProposalData::Emission { .. } => self.creation_block.saturating_add(
54
2.81k
                U256::from(21_600)
55
2.81k
                    .try_into()
56
2.81k
                    .ok()
57
2.81k
                    .expect("this is a safe conversion"),
58
            ),
59
12
            _ => self.expiration_block,
60
        }
61
2.82k
    }
62
63
    /// Marks a proposal as accepted and executes it.
64
14
    pub fn accept(
65
14
        mut self,
66
14
        block: BlockNumberFor<T>,
67
14
        stake_for: BalanceOf<T>,
68
14
        stake_against: BalanceOf<T>,
69
14
    ) -> DispatchResult {
70
14
        ensure!(self.is_active(), 
crate::Error::<T>::ProposalIsFinished0
);
71
72
14
        self.status = ProposalStatus::Accepted {
73
14
            block,
74
14
            stake_for,
75
14
            stake_against,
76
14
        };
77
78
14
        Proposals::<T>::insert(self.id, &self);
79
14
        crate::Pallet::<T>::deposit_event(crate::Event::ProposalAccepted(self.id));
80
81
14
        self.execute_proposal()
?0
;
82
83
14
        Ok(())
84
14
    }
85
86
    /// Executes the changes.
87
14
    fn execute_proposal(self) -> DispatchResult {
88
        // Proposal fee is given back to the proposer.
89
14
        let _ = <T as crate::Config>::Currency::transfer(
90
14
            &crate::DaoTreasuryAddress::<T>::get(),
91
14
            &self.proposer,
92
14
            self.proposal_cost,
93
14
            ExistenceRequirement::AllowDeath,
94
14
        );
95
96
14
        match self.data {
97
2
            ProposalData::GlobalParams(data) => {
98
                let GlobalParamsData {
99
2
                    min_name_length,
100
2
                    max_name_length,
101
2
                    min_weight_control_fee,
102
2
                    min_staking_fee,
103
2
                    dividends_participation_weight,
104
2
                    namespace_pricing_config,
105
2
                    proposal_cost,
106
2
                } = data;
107
108
2
                pallet_torus0::MinNameLength::<T>::set(min_name_length);
109
2
                pallet_torus0::MaxNameLength::<T>::set(max_name_length);
110
2
                pallet_torus0::DividendsParticipationWeight::<T>::set(
111
2
                    dividends_participation_weight,
112
                );
113
2
                pallet_torus0::FeeConstraints::<T>::mutate(|constraints| {
114
2
                    constraints.min_weight_control_fee =
115
2
                        Percent::from_percent(min_weight_control_fee);
116
2
                    constraints.min_staking_fee = Percent::from_percent(min_staking_fee);
117
2
                });
118
2
                pallet_torus0::NamespacePricingConfig::<T>::set(namespace_pricing_config);
119
2
                crate::GlobalGovernanceConfig::<T>::mutate(|config| {
120
2
                    config.proposal_cost = proposal_cost;
121
2
                });
122
            }
123
124
2
            ProposalData::TransferDaoTreasury { account, amount } => {
125
2
                <T as crate::Config>::Currency::transfer(
126
2
                    &DaoTreasuryAddress::<T>::get(),
127
2
                    &account,
128
2
                    amount,
129
2
                    ExistenceRequirement::AllowDeath,
130
                )
131
2
                .map_err(|_| crate::Error::<T>::InternalError)
?0
;
132
            }
133
134
            ProposalData::Emission {
135
4
                recycling_percentage,
136
4
                treasury_percentage,
137
4
                incentives_ratio,
138
4
            } => {
139
4
                pallet_emission0::EmissionRecyclingPercentage::<T>::set(recycling_percentage);
140
4
                crate::TreasuryEmissionFee::<T>::set(treasury_percentage);
141
4
                pallet_emission0::IncentivesRatio::<T>::set(incentives_ratio);
142
4
            }
143
144
6
            ProposalData::GlobalCustom => {}
145
        }
146
147
14
        Ok(())
148
14
    }
149
150
    /// Marks a proposal as refused.
151
2
    pub fn refuse(
152
2
        mut self,
153
2
        block: BlockNumberFor<T>,
154
2
        stake_for: BalanceOf<T>,
155
2
        stake_against: BalanceOf<T>,
156
2
    ) -> DispatchResult {
157
2
        ensure!(self.is_active(), 
crate::Error::<T>::ProposalIsFinished0
);
158
159
2
        self.status = ProposalStatus::Refused {
160
2
            block,
161
2
            stake_for,
162
2
            stake_against,
163
2
        };
164
165
2
        Proposals::<T>::insert(self.id, &self);
166
2
        crate::Pallet::<T>::deposit_event(crate::Event::ProposalRefused(self.id));
167
168
2
        Ok(())
169
2
    }
170
171
    /// Marks a proposal as expired.
172
4
    pub fn expire(mut self, block_number: BlockNumberFor<T>) -> DispatchResult {
173
4
        ensure!(self.is_active(), 
crate::Error::<T>::ProposalIsFinished0
);
174
4
        ensure!(
175
4
            block_number >= self.expiration_block,
176
0
            crate::Error::<T>::InvalidProposalFinalizationParameters
177
        );
178
179
4
        self.status = ProposalStatus::Expired;
180
181
4
        Proposals::<T>::insert(self.id, &self);
182
4
        crate::Pallet::<T>::deposit_event(crate::Event::ProposalExpired(self.id));
183
184
4
        Ok(())
185
4
    }
186
}
187
188
#[derive(Clone, DebugNoBound, TypeInfo, Decode, Encode, MaxEncodedLen, PartialEq, Eq)]
189
#[scale_info(skip_type_params(T))]
190
pub 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))]
231
pub 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
241
impl<T: crate::Config> GlobalParamsData<T> {
242
20
    pub fn validate(&self) -> DispatchResult {
243
20
        ensure!(
244
20
            self.min_name_length > 1,
245
4
            crate::Error::<T>::InvalidMinNameLength
246
        );
247
248
16
        ensure!(
249
16
            (self.max_name_length as u32) < T::MaxAgentNameLengthConstraint::get(),
250
2
            crate::Error::<T>::InvalidMaxNameLength
251
        );
252
253
14
        ensure!(
254
14
            self.min_weight_control_fee <= 100,
255
2
            crate::Error::<T>::InvalidMinWeightControlFee
256
        );
257
258
12
        ensure!(
259
12
            self.min_staking_fee <= 100,
260
2
            crate::Error::<T>::InvalidMinStakingFee
261
        );
262
263
10
        ensure!(
264
10
            self.proposal_cost <= 50_000_000_000_000_000_000_000,
265
2
            crate::Error::<T>::InvalidProposalCost
266
        );
267
268
8
        Ok(())
269
20
    }
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))]
275
pub 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
299
impl<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
1.10k
    pub fn required_stake(&self) -> Percent {
304
1.10k
        match self {
305
1.09k
            Self::Emission { .. } => Percent::from_parts(10),
306
10
            
Self::GlobalCustom8
| Self::TransferDaoTreasury { .. } => Percent::from_parts(50),
307
2
            Self::GlobalParams { .. } => Percent::from_parts(40),
308
        }
309
1.10k
    }
310
}
311
312
#[derive(DebugNoBound, TypeInfo, Decode, Encode, MaxEncodedLen, PartialEq, Eq)]
313
#[scale_info(skip_type_params(T))]
314
pub 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)]
322
12
pub fn add_global_params_proposal<T: crate::Config>(
323
12
    proposer: AccountIdOf<T>,
324
12
    data: GlobalParamsData<T>,
325
12
    metadata: Vec<u8>,
326
12
) -> DispatchResult {
327
12
    data.validate()
?4
;
328
8
    let data = ProposalData::<T>::GlobalParams(data);
329
330
8
    add_proposal::<T>(proposer, data, metadata)
331
12
}
332
333
/// Create global custom proposal with metadata.
334
16
pub fn add_global_custom_proposal<T: crate::Config>(
335
16
    proposer: AccountIdOf<T>,
336
16
    metadata: Vec<u8>,
337
16
) -> DispatchResult {
338
16
    add_proposal(proposer, ProposalData::<T>::GlobalCustom, metadata)
339
16
}
340
341
/// Create a treasury transfer proposal with metadata.
342
4
pub fn add_dao_treasury_transfer_proposal<T: crate::Config>(
343
4
    proposer: AccountIdOf<T>,
344
4
    value: BalanceOf<T>,
345
4
    destination_key: AccountIdOf<T>,
346
4
    metadata: Vec<u8>,
347
4
) -> DispatchResult {
348
4
    let data = ProposalData::<T>::TransferDaoTreasury {
349
4
        account: destination_key,
350
4
        amount: value,
351
4
    };
352
353
4
    add_proposal::<T>(proposer, data, metadata)
354
4
}
355
356
/// Creates a new emissions proposal. Only valid if `recycling_percentage +
357
/// treasury_percentage <= u128::MAX`.
358
10
pub fn add_emission_proposal<T: crate::Config>(
359
10
    proposer: AccountIdOf<T>,
360
10
    recycling_percentage: Percent,
361
10
    treasury_percentage: Percent,
362
10
    incentives_ratio: Percent,
363
10
    metadata: Vec<u8>,
364
10
) -> DispatchResult {
365
10
    ensure!(
366
10
        recycling_percentage
367
10
            .checked_add(&treasury_percentage)
368
10
            .is_some(),
369
2
        crate::Error::<T>::InvalidEmissionProposalData
370
    );
371
372
8
    let data = ProposalData::<T>::Emission {
373
8
        recycling_percentage,
374
8
        treasury_percentage,
375
8
        incentives_ratio,
376
8
    };
377
378
8
    add_proposal::<T>(proposer, data, metadata)
379
10
}
380
381
/// Creates a new proposal and saves it. Internally used.
382
36
fn add_proposal<T: crate::Config>(
383
36
    proposer: AccountIdOf<T>,
384
36
    data: ProposalData<T>,
385
36
    metadata: Vec<u8>,
386
36
) -> DispatchResult {
387
36
    ensure!(
388
36
        !metadata.is_empty(),
389
2
        crate::Error::<T>::ProposalDataTooSmall
390
    );
391
34
    ensure!(
392
34
        metadata.len() <= 256,
393
2
        crate::Error::<T>::ProposalDataTooLarge
394
    );
395
396
32
    let config = GlobalGovernanceConfig::<T>::get();
397
398
32
    let cost = config.proposal_cost;
399
32
    <T as crate::Config>::Currency::transfer(
400
32
        &proposer,
401
32
        &crate::DaoTreasuryAddress::<T>::get(),
402
32
        cost,
403
32
        ExistenceRequirement::AllowDeath,
404
    )
405
32
    .map_err(|_| crate::Error::<T>::NotEnoughBalanceToApply)
?2
;
406
407
30
    let proposal_id: u64 = crate::Proposals::<T>::iter()
408
30
        .count()
409
30
        .try_into()
410
30
        .map_err(|_| crate::Error::<T>::InternalError)
?0
;
411
412
30
    let current_block = <polkadot_sdk::frame_system::Pallet<T>>::block_number();
413
414
30
    let proposal = Proposal::<T> {
415
30
        id: proposal_id,
416
30
        proposer,
417
30
        expiration_block: current_block.saturating_add(config.proposal_expiration),
418
30
        data,
419
30
        status: ProposalStatus::Open {
420
30
            votes_for: BoundedBTreeSet::new(),
421
30
            votes_against: BoundedBTreeSet::new(),
422
30
            stake_for: 0,
423
30
            stake_against: 0,
424
30
        },
425
30
        metadata: BoundedVec::truncate_from(metadata),
426
30
        proposal_cost: cost,
427
30
        creation_block: current_block,
428
30
    };
429
430
30
    crate::Proposals::<T>::insert(proposal_id, proposal);
431
432
30
    Ok(())
433
36
}
434
435
/// Every 100 blocks, iterates through all pending proposals and executes the
436
/// ones eligible.
437
477k
pub fn tick_proposals<T: crate::Config>(block_number: BlockNumberFor<T>) {
438
477k
    let block_number_u64: u64 = block_number
439
477k
        .try_into()
440
477k
        .ok()
441
477k
        .expect("blocknumber wont be greater than 2^64");
442
477k
    if block_number_u64 % 100 != 0 {
443
473k
        return;
444
4.77k
    }
445
446
4.77k
    let not_delegating = NotDelegatingVotingPower::<T>::get().into_inner();
447
448
4.77k
    let proposals = Proposals::<T>::iter().filter(|(_, p)| 
p3.90k
.
is_active3.90k
());
449
450
7.59k
    for (
id2.82k
,
proposal2.82k
) in proposals {
451
2.82k
        let res = with_storage_layer(|| tick_proposal(&not_delegating, block_number, proposal));
452
2.82k
        if let Err(
err0
) = res {
453
0
            error!("failed to tick proposal {id}: {err:?}, skipping...");
454
2.82k
        }
455
    }
456
477k
}
457
458
/// Returns the minimum amount of active stake needed for a proposal be executed
459
/// based on the given percentage.
460
1.10k
fn get_minimum_stake_to_execute_with_percentage<T: crate::Config>(
461
1.10k
    threshold: Percent,
462
1.10k
) -> BalanceOf<T> {
463
1.10k
    let stake = pallet_torus0::TotalStake::<T>::get();
464
1.10k
    threshold.mul_floor(stake)
465
1.10k
}
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.
469
2.82k
fn tick_proposal<T: crate::Config>(
470
2.82k
    not_delegating: &BTreeSet<T::AccountId>,
471
2.82k
    block_number: BlockNumberFor<T>,
472
2.82k
    mut proposal: Proposal<T>,
473
2.82k
) -> DispatchResult {
474
    let ProposalStatus::Open {
475
2.82k
        votes_for,
476
2.82k
        votes_against,
477
        ..
478
2.82k
    } = &proposal.status
479
    else {
480
0
        return Err(Error::<T>::ProposalIsFinished.into());
481
    };
482
483
2.82k
    let votes_for: Vec<(AccountIdOf<T>, BalanceOf<T>)> = votes_for
484
2.82k
        .iter()
485
2.82k
        .cloned()
486
2.82k
        .map(|id| {
487
2.82k
            let stake = calc_stake::<T>(not_delegating, &id);
488
2.82k
            (id, stake)
489
2.82k
        })
490
2.82k
        .collect();
491
2.82k
    let votes_against: Vec<(AccountIdOf<T>, BalanceOf<T>)> = votes_against
492
2.82k
        .iter()
493
2.82k
        .cloned()
494
2.82k
        .map(|id| 
{8
495
8
            let stake = calc_stake::<T>(not_delegating, &id);
496
8
            (id, stake)
497
8
        })
498
2.82k
        .collect();
499
500
2.82k
    let stake_for_sum: BalanceOf<T> = votes_for.iter().map(|(_, stake)| stake).sum();
501
2.82k
    let stake_against_sum: BalanceOf<T> = votes_against.iter().map(|(_, stake)| stake).sum();
502
503
2.82k
    if block_number < proposal.expiration_block {
504
        if let ProposalStatus::Open {
505
2.37k
            stake_for,
506
2.37k
            stake_against,
507
            ..
508
2.37k
        } = &mut proposal.status
509
2.37k
        {
510
2.37k
            *stake_for = stake_for_sum;
511
2.37k
            *stake_against = stake_against_sum;
512
2.37k
        
}0
513
2.37k
        Proposals::<T>::set(proposal.id, Some(proposal.clone()));
514
446
    }
515
516
2.82k
    if block_number < proposal.execution_block() {
517
1.72k
        return Ok(());
518
1.10k
    }
519
520
1.10k
    let total_stake = stake_for_sum.saturating_add(stake_against_sum);
521
1.10k
    let minimal_stake_to_execute =
522
1.10k
        get_minimum_stake_to_execute_with_percentage::<T>(proposal.data.required_stake());
523
524
1.10k
    if total_stake >= minimal_stake_to_execute {
525
16
        create_unrewarded_proposal::<T>(proposal.id, block_number, votes_for, votes_against);
526
16
        if stake_against_sum > stake_for_sum {
527
2
            proposal.refuse(block_number, stake_for_sum, stake_against_sum)
528
        } else {
529
14
            proposal.accept(block_number, stake_for_sum, stake_against_sum)
530
        }
531
1.08k
    } else if block_number >= proposal.expiration_block {
532
4
        create_unrewarded_proposal::<T>(proposal.id, block_number, votes_for, votes_against);
533
4
        proposal.expire(block_number)
534
    } else {
535
1.08k
        Ok(())
536
    }
537
2.82k
}
538
539
type 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].
543
20
fn create_unrewarded_proposal<T: crate::Config>(
544
20
    proposal_id: u64,
545
20
    block_number: BlockNumberFor<T>,
546
20
    votes_for: Vec<(AccountIdOf<T>, BalanceOf<T>)>,
547
20
    votes_against: Vec<(AccountIdOf<T>, BalanceOf<T>)>,
548
20
) {
549
20
    let mut reward_votes_for = BoundedBTreeMap::new();
550
40
    for (
key20
,
value20
) in votes_for {
551
20
        let _ = reward_votes_for.try_insert(key, value);
552
20
    }
553
554
20
    let mut reward_votes_against: AccountStakes<T> = BoundedBTreeMap::new();
555
28
    for (
key8
,
value8
) in votes_against {
556
8
        let _ = reward_votes_against.try_insert(key, value);
557
8
    }
558
559
20
    UnrewardedProposals::<T>::insert(
560
20
        proposal_id,
561
20
        UnrewardedProposal::<T> {
562
20
            block: block_number,
563
20
            votes_for: reward_votes_for,
564
20
            votes_against: reward_votes_against,
565
20
        },
566
    );
567
20
}
568
569
/// Calculates the stake for a voter. This function takes into account all
570
/// accounts delegating voting power to the voter.
571
#[inline]
572
2.83k
fn calc_stake<T: crate::Config>(
573
2.83k
    not_delegating: &BTreeSet<T::AccountId>,
574
2.83k
    voter: &T::AccountId,
575
2.83k
) -> BalanceOf<T> {
576
2.83k
    let own_stake: BalanceOf<T> = if !not_delegating.contains(voter) {
577
2.82k
        0
578
    } else {
579
2
        pallet_torus0::stake::sum_staking_to::<T>(voter)
580
    };
581
582
2.83k
    let delegated_stake = pallet_torus0::stake::get_staked_by_vector::<T>(voter)
583
2.83k
        .into_iter()
584
2.83k
        .
filter2.83k
(|(staker, _)| !not_delegating.contains(staker))
585
2.83k
        .map(|(_, stake)| stake)
586
2.83k
        .sum();
587
588
2.83k
    own_stake.saturating_add(delegated_stake)
589
2.83k
}
590
591
/// Processes the proposal reward queue and distributes rewards for all voters.
592
477k
pub fn tick_proposal_rewards<T: crate::Config>(block_number: BlockNumberFor<T>) {
593
477k
    let governance_config = crate::GlobalGovernanceConfig::<T>::get();
594
595
477k
    let block_number: u64 = block_number
596
477k
        .try_into()
597
477k
        .ok()
598
477k
        .expect("blocknumber wont be greater than 2^64");
599
477k
    let proposal_reward_interval: u64 = governance_config
600
477k
        .proposal_reward_interval
601
477k
        .try_into()
602
477k
        .ok()
603
477k
        .expect("blocknumber wont be greater than 2^64");
604
605
477k
    let reached_interval = block_number
606
477k
        .checked_rem(proposal_reward_interval)
607
477k
        .is_some_and(|r| r == 0);
608
477k
    if !reached_interval {
609
477k
        return;
610
4
    }
611
612
4
    let mut n = 0u16;
613
4
    let mut account_stakes: AccountStakes<T> = BoundedBTreeMap::new();
614
4
    let mut total_allocation = FixedU128::from_inner(0);
615
4
    for (proposal_id, unrewarded_proposal) in UnrewardedProposals::<T>::iter() {
616
4
        let proposal_block: u64 = unrewarded_proposal
617
4
            .block
618
4
            .try_into()
619
4
            .ok()
620
4
            .expect("blocknumber wont be greater than 2^64");
621
622
        // Just checking if it's in the chain interval
623
4
        if proposal_block < block_number.saturating_sub(proposal_reward_interval) {
624
0
            continue;
625
4
        }
626
627
4
        for (acc_id, stake) in unrewarded_proposal
628
4
            .votes_for
629
4
            .into_iter()
630
4
            .chain(unrewarded_proposal.votes_against.into_iter())
631
4
        {
632
4
            let curr_stake = *account_stakes.get(&acc_id).unwrap_or(&0u128);
633
4
            let _ = account_stakes.try_insert(acc_id, curr_stake.saturating_add(stake));
634
4
        }
635
636
4
        match get_reward_allocation::<T>(&governance_config, n) {
637
4
            Ok(allocation) => total_allocation = total_allocation.saturating_add(allocation),
638
0
            Err(err) => {
639
0
                error!("could not get reward allocation for proposal {proposal_id}: {err:?}");
640
0
                continue;
641
            }
642
        }
643
644
4
        UnrewardedProposals::<T>::remove(proposal_id);
645
4
        n = n.saturating_add(1);
646
    }
647
648
4
    distribute_proposal_rewards::<T>(
649
4
        account_stakes,
650
4
        total_allocation,
651
4
        governance_config.max_proposal_reward_treasury_allocation,
652
    );
653
477k
}
654
655
/// Calculates the total balance to be rewarded for a proposal.
656
6
pub fn get_reward_allocation<T: crate::Config>(
657
6
    governance_config: &GovernanceConfiguration<T>,
658
6
    n: u16,
659
6
) -> Result<FixedU128, DispatchError> {
660
6
    let treasury_address = DaoTreasuryAddress::<T>::get();
661
6
    let treasury_balance = <T as crate::Config>::Currency::free_balance(&treasury_address);
662
663
6
    let allocation_percentage = governance_config.proposal_reward_treasury_allocation;
664
6
    let max_allocation = governance_config.max_proposal_reward_treasury_allocation;
665
666
6
    let mut allocation = FixedU128::from_inner(
667
6
        allocation_percentage
668
6
            .mul_floor(treasury_balance)
669
6
            .min(max_allocation),
670
    );
671
672
6
    if n > 0 {
673
0
        let mut base = FixedU128::from_inner((1.5 * FixedU128::DIV as f64) as u128);
674
0
        let mut result = FixedU128::from_u32(1);
675
0
        let mut remaining = n;
676
677
0
        while remaining > 0 {
678
0
            if remaining % 2 == 1 {
679
0
                result = result.const_checked_mul(base).unwrap_or(result);
680
0
            }
681
0
            base = base.const_checked_mul(base).unwrap_or_default();
682
0
            remaining /= 2;
683
        }
684
685
0
        allocation = allocation.const_checked_div(result).unwrap_or(allocation);
686
6
    }
687
688
6
    Ok(allocation)
689
6
}
690
691
/// Distributes the proposal rewards in a quadratic formula to all voters.
692
4
fn distribute_proposal_rewards<T: crate::Config>(
693
4
    account_stakes: AccountStakes<T>,
694
4
    total_allocation: FixedU128,
695
4
    max_proposal_reward_treasury_allocation: BalanceOf<T>,
696
4
) {
697
    // This is just a sanity check, making sure we can never allocate more than the
698
    // max
699
4
    if total_allocation > FixedU128::from_inner(max_proposal_reward_treasury_allocation) {
700
0
        error!("total allocation exceeds max proposal reward treasury allocation");
701
0
        return;
702
4
    }
703
704
    use polkadot_sdk::frame_support::sp_runtime::traits::IntegerSquareRoot;
705
706
4
    let dao_treasury_address = DaoTreasuryAddress::<T>::get();
707
4
    let account_sqrt_stakes: Vec<_> = account_stakes
708
4
        .into_iter()
709
4
        .map(|(acc_id, stake)| (acc_id, stake.integer_sqrt()))
710
4
        .collect();
711
712
4
    let total_stake: BalanceOf<T> = account_sqrt_stakes.iter().map(|(_, stake)| *stake).sum();
713
4
    let total_stake = FixedU128::from_inner(total_stake);
714
715
4
    for (acc_id, stake) in account_sqrt_stakes.into_iter() {
716
4
        let percentage = FixedU128::from_inner(stake)
717
4
            .const_checked_div(total_stake)
718
4
            .unwrap_or_default();
719
720
4
        let reward = total_allocation
721
4
            .const_checked_mul(percentage)
722
4
            .unwrap_or_default()
723
4
            .into_inner();
724
725
        // Transfer the proposal reward to the accounts from treasury
726
4
        if let Err(
err0
) = <T as crate::Config>::Currency::transfer(
727
4
            &dao_treasury_address,
728
4
            &acc_id,
729
4
            reward,
730
4
            ExistenceRequirement::AllowDeath,
731
4
        ) {
732
0
            error!("could not transfer proposal reward: {err:?}")
733
4
        }
734
    }
735
4
}