/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(¬_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 | } |