multiversx_sc_modules/governance/
mod.rs1multiversx_sc::imports!();
2
3pub mod governance_configurable;
4pub mod governance_events;
5pub mod governance_proposal;
6
7use governance_proposal::*;
8
9const MAX_GAS_LIMIT_PER_BLOCK: u64 = 600_000_000;
10const MIN_AMOUNT_PER_DEPOSIT: u64 = 1;
11pub const ALREADY_VOTED_ERR_MSG: &[u8] = b"Already voted for this proposal";
12pub const MIN_FEES_REACHED: &[u8] = b"Propose already reached min threshold for fees";
13pub const MIN_AMOUNT_NOT_REACHED: &[u8] = b"Minimum amount not reached";
14
15#[multiversx_sc::module]
16pub trait GovernanceModule:
17 governance_configurable::GovernanceConfigurablePropertiesModule
18 + governance_events::GovernanceEventsModule
19{
20 #[payable("*")]
26 #[endpoint(depositTokensForProposal)]
27 fn deposit_tokens_for_proposal(&self, proposal_id: ProposalId) {
28 self.require_caller_not_self();
29 self.require_valid_proposal_id(proposal_id);
30 require!(
31 self.get_proposal_status(proposal_id) == GovernanceProposalStatus::WaitingForFees,
32 "Proposal has to be executed or canceled first"
33 );
34 require!(
35 !self.proposal_reached_min_fees(proposal_id),
36 MIN_FEES_REACHED
37 );
38 let additional_fee = self.require_payment_token_governance_token();
39 require!(
40 additional_fee.amount >= MIN_AMOUNT_PER_DEPOSIT,
41 MIN_AMOUNT_NOT_REACHED
42 );
43
44 let caller = self.blockchain().get_caller();
45 let mut proposal = self.proposals().get(proposal_id);
46 proposal.fees.entries.push(FeeEntry {
47 depositor_addr: caller.clone(),
48 tokens: additional_fee.clone(),
49 });
50 proposal.fees.total_amount += additional_fee.amount.clone();
51 self.proposals().set(proposal_id, &proposal);
52
53 self.user_deposit_event(&caller, proposal_id, &additional_fee);
54 }
55
56 #[endpoint(withdrawGovernanceTokens)]
58 fn claim_deposited_tokens(&self, proposal_id: usize) {
59 self.require_caller_not_self();
60 self.require_valid_proposal_id(proposal_id);
61 require!(
62 self.get_proposal_status(proposal_id) == GovernanceProposalStatus::WaitingForFees,
63 "Cannot claim deposited tokens anymore; Proposal is not in WatingForFees state"
64 );
65
66 require!(
67 !self.proposal_reached_min_fees(proposal_id),
68 MIN_FEES_REACHED
69 );
70
71 let caller = self.blockchain().get_caller();
72 let mut proposal = self.proposals().get(proposal_id);
73 let mut fees_to_send = ManagedVec::<Self::Api, FeeEntry<Self::Api>>::new();
74 let mut i = 0;
75 while i < proposal.fees.entries.len() {
76 if proposal.fees.entries.get(i).depositor_addr == caller {
77 fees_to_send.push(proposal.fees.entries.get(i).clone());
78 proposal.fees.entries.remove(i);
79 } else {
80 i += 1;
81 }
82 }
83
84 for fee_entry in fees_to_send.iter() {
85 let payment = fee_entry.tokens.clone();
86
87 self.tx()
88 .to(&fee_entry.depositor_addr)
89 .single_esdt(
90 &payment.token_identifier,
91 payment.token_nonce,
92 &payment.amount,
93 )
94 .transfer();
95 self.user_claim_event(&caller, proposal_id, &fee_entry.tokens);
96 }
97 }
98
99 #[payable("*")]
111 #[endpoint]
112 fn propose(
113 &self,
114 description: ManagedBuffer,
115 actions: MultiValueEncoded<GovernanceActionAsMultiArg<Self::Api>>,
116 ) -> usize {
117 self.require_caller_not_self();
118
119 let payment = self.require_payment_token_governance_token();
120
121 require!(
122 payment.amount >= self.min_token_balance_for_proposing().get(),
123 "Not enough tokens for proposing action"
124 );
125 require!(!actions.is_empty(), "Proposal has no actions");
126 require!(
127 actions.len() <= MAX_GOVERNANCE_PROPOSAL_ACTIONS,
128 "Exceeded max actions per proposal"
129 );
130
131 let mut gov_actions = ArrayVec::new();
132 for action in actions {
133 let (gas_limit, dest_address, function_name, arguments) = action.into_tuple();
134 let gov_action = GovernanceAction {
135 gas_limit,
136 dest_address,
137 function_name,
138 arguments,
139 };
140
141 require!(
142 gas_limit < MAX_GAS_LIMIT_PER_BLOCK,
143 "A single action cannot use more than the max gas limit per block"
144 );
145
146 gov_actions.push(gov_action);
147 }
148
149 require!(
150 self.total_gas_needed(&gov_actions) < MAX_GAS_LIMIT_PER_BLOCK,
151 "Actions require too much gas to be executed"
152 );
153
154 let proposer = self.blockchain().get_caller();
155 let fees_entries = ManagedVec::from_single_item(FeeEntry {
156 depositor_addr: proposer.clone(),
157 tokens: payment.clone(),
158 });
159
160 let proposal = GovernanceProposal {
161 proposer: proposer.clone(),
162 description,
163 actions: gov_actions,
164 fees: ProposalFees {
165 total_amount: payment.amount,
166 entries: fees_entries,
167 },
168 };
169
170 let proposal_id = self.proposals().push(&proposal);
171 self.proposal_votes(proposal_id).set(ProposalVotes::new());
172
173 let current_block = self.blockchain().get_block_nonce();
174 self.proposal_start_block(proposal_id).set(current_block);
175
176 self.proposal_created_event(proposal_id, &proposer, current_block, &proposal);
177
178 proposal_id
179 }
180
181 #[payable("*")]
184 #[endpoint]
185 fn vote(&self, proposal_id: usize, vote: VoteType) {
186 self.require_caller_not_self();
187
188 let payment = self.require_payment_token_governance_token();
189 self.require_valid_proposal_id(proposal_id);
190 require!(
191 self.get_proposal_status(proposal_id) == GovernanceProposalStatus::Active,
192 "Proposal is not active"
193 );
194
195 let voter = self.blockchain().get_caller();
196 let new_user = self.user_voted_proposals(&voter).insert(proposal_id);
197 require!(new_user, ALREADY_VOTED_ERR_MSG);
198
199 match vote {
200 VoteType::UpVote => {
201 self.proposal_votes(proposal_id).update(|total_votes| {
202 total_votes.up_votes += &payment.amount.clone();
203 });
204 self.up_vote_cast_event(&voter, proposal_id, &payment.amount);
205 },
206 VoteType::DownVote => {
207 self.proposal_votes(proposal_id).update(|total_votes| {
208 total_votes.down_votes += &payment.amount.clone();
209 });
210 self.down_vote_cast_event(&voter, proposal_id, &payment.amount);
211 },
212 VoteType::DownVetoVote => {
213 self.proposal_votes(proposal_id).update(|total_votes| {
214 total_votes.down_veto_votes += &payment.amount.clone();
215 });
216 self.down_veto_vote_cast_event(&voter, proposal_id, &payment.amount);
217 },
218 VoteType::AbstainVote => {
219 self.proposal_votes(proposal_id).update(|total_votes| {
220 total_votes.abstain_votes += &payment.amount.clone();
221 });
222 self.abstain_vote_cast_event(&voter, proposal_id, &payment.amount);
223 },
224 }
225 }
226
227 #[endpoint]
232 fn queue(&self, proposal_id: usize) {
233 self.require_caller_not_self();
234
235 require!(
236 self.get_proposal_status(proposal_id) == GovernanceProposalStatus::Succeeded,
237 "Can only queue succeeded proposals"
238 );
239
240 let current_block = self.blockchain().get_block_nonce();
241 self.proposal_queue_block(proposal_id).set(current_block);
242
243 self.proposal_queued_event(proposal_id, current_block);
244 }
245
246 #[endpoint]
250 fn execute(&self, proposal_id: usize) {
251 self.require_caller_not_self();
252
253 require!(
254 self.get_proposal_status(proposal_id) == GovernanceProposalStatus::Queued,
255 "Can only execute queued proposals"
256 );
257
258 let current_block = self.blockchain().get_block_nonce();
259 let lock_blocks = self.lock_time_after_voting_ends_in_blocks().get();
260
261 let lock_start = self.proposal_queue_block(proposal_id).get();
262 let lock_end = lock_start + lock_blocks;
263
264 require!(
265 current_block >= lock_end,
266 "Proposal is in timelock status. Try again later"
267 );
268
269 let proposal = self.proposals().get(proposal_id);
270 let total_gas_needed = self.total_gas_needed(&proposal.actions);
271 let gas_left = self.blockchain().get_gas_left();
272
273 require!(
274 gas_left > total_gas_needed,
275 "Not enough gas to execute all proposals"
276 );
277
278 self.clear_proposal(proposal_id);
279
280 for action in proposal.actions {
281 self.tx()
282 .to(&action.dest_address)
283 .raw_call(action.function_name)
284 .gas(action.gas_limit)
285 .arguments_raw(action.arguments.into())
286 .transfer_execute()
287 }
288
289 self.proposal_executed_event(proposal_id);
290 }
291
292 #[endpoint]
296 fn cancel(&self, proposal_id: usize) {
297 self.require_caller_not_self();
298
299 match self.get_proposal_status(proposal_id) {
300 GovernanceProposalStatus::None => {
301 sc_panic!("Proposal does not exist");
302 },
303 GovernanceProposalStatus::Pending => {
304 let proposal = self.proposals().get(proposal_id);
305 let caller = self.blockchain().get_caller();
306
307 require!(
308 caller == proposal.proposer,
309 "Only original proposer may cancel a pending proposal"
310 );
311 },
312 GovernanceProposalStatus::Defeated => {},
313 GovernanceProposalStatus::WaitingForFees => {
314 self.refund_payments(proposal_id);
315 },
316 _ => {
317 sc_panic!("Action may not be cancelled");
318 },
319 }
320
321 self.clear_proposal(proposal_id);
322 self.proposal_canceled_event(proposal_id);
323 }
324
325 #[view(getProposalStatus)]
328 fn get_proposal_status(&self, proposal_id: usize) -> GovernanceProposalStatus {
329 if !self.proposal_exists(proposal_id) {
330 return GovernanceProposalStatus::None;
331 }
332
333 let queue_block = self.proposal_queue_block(proposal_id).get();
334 if queue_block > 0 {
335 return GovernanceProposalStatus::Queued;
336 }
337
338 let current_block = self.blockchain().get_block_nonce();
339 let proposal_block = self.proposal_start_block(proposal_id).get();
340 let voting_delay = self.voting_delay_in_blocks().get();
341 let voting_period = self.voting_period_in_blocks().get();
342
343 let voting_start = proposal_block + voting_delay;
344 let voting_end = voting_start + voting_period;
345
346 if current_block < voting_start {
347 return GovernanceProposalStatus::Pending;
348 }
349 if current_block >= voting_start && current_block < voting_end {
350 return GovernanceProposalStatus::Active;
351 }
352
353 if self.quorum_and_vote_reached(proposal_id) {
354 GovernanceProposalStatus::Succeeded
355 } else {
356 GovernanceProposalStatus::Defeated
357 }
358 }
359
360 fn quorum_and_vote_reached(&self, proposal_id: ProposalId) -> bool {
361 let proposal_votes = self.proposal_votes(proposal_id).get();
362 let total_votes = proposal_votes.get_total_votes();
363 let total_up_votes = proposal_votes.up_votes;
364 let total_down_votes = proposal_votes.down_votes;
365 let total_down_veto_votes = proposal_votes.down_veto_votes;
366 let third_total_votes = &total_votes / 3u64;
367 let quorum = self.quorum().get();
368
369 sc_print!("Total votes = {} quorum = {}", total_votes, quorum);
370 if total_down_veto_votes > third_total_votes {
371 false
372 } else {
373 total_votes >= quorum && total_up_votes > (total_down_votes + total_down_veto_votes)
374 }
375 }
376
377 #[view(getProposer)]
378 fn get_proposer(&self, proposal_id: usize) -> OptionalValue<ManagedAddress> {
379 if !self.proposal_exists(proposal_id) {
380 OptionalValue::None
381 } else {
382 OptionalValue::Some(self.proposals().get(proposal_id).proposer)
383 }
384 }
385
386 #[view(getProposalDescription)]
387 fn get_proposal_description(&self, proposal_id: usize) -> OptionalValue<ManagedBuffer> {
388 if !self.proposal_exists(proposal_id) {
389 OptionalValue::None
390 } else {
391 OptionalValue::Some(self.proposals().get(proposal_id).description)
392 }
393 }
394
395 #[view(getProposalActions)]
396 fn get_proposal_actions(
397 &self,
398 proposal_id: usize,
399 ) -> MultiValueEncoded<GovernanceActionAsMultiArg<Self::Api>> {
400 if !self.proposal_exists(proposal_id) {
401 return MultiValueEncoded::new();
402 }
403
404 let actions = self.proposals().get(proposal_id).actions;
405 let mut actions_as_multiarg = MultiValueEncoded::new();
406
407 for action in actions {
408 actions_as_multiarg.push(action.into_multiarg());
409 }
410
411 actions_as_multiarg
412 }
413
414 fn refund_payments(&self, proposal_id: ProposalId) {
417 let payments = self.proposals().get(proposal_id).fees;
418
419 for fee_entry in payments.entries.iter() {
420 let payment = &fee_entry.tokens;
421 self.tx()
422 .to(&fee_entry.depositor_addr)
423 .single_esdt(
424 &payment.token_identifier,
425 payment.token_nonce,
426 &payment.amount,
427 )
428 .transfer();
429 }
430 }
431
432 fn require_payment_token_governance_token(&self) -> EsdtTokenPayment {
433 let payment = self.call_value().single_esdt();
434 require!(
435 payment.token_identifier == self.governance_token_id().get(),
436 "Only Governance token accepted as payment"
437 );
438 payment.clone()
439 }
440
441 fn require_valid_proposal_id(&self, proposal_id: usize) {
442 require!(
443 self.is_valid_proposal_id(proposal_id),
444 "Invalid proposal ID"
445 );
446 }
447
448 fn require_caller_not_self(&self) {
449 let caller = self.blockchain().get_caller();
450 let sc_address = self.blockchain().get_sc_address();
451
452 require!(
453 caller != sc_address,
454 "Cannot call this endpoint through proposed action"
455 );
456 }
457
458 fn is_valid_proposal_id(&self, proposal_id: usize) -> bool {
459 proposal_id >= 1 && proposal_id <= self.proposals().len()
460 }
461
462 fn proposal_reached_min_fees(&self, proposal_id: ProposalId) -> bool {
463 let accumulated_fees = self.proposals().get(proposal_id).fees.total_amount;
464 let min_fees = self.min_fee_for_propose().get();
465 accumulated_fees >= min_fees
466 }
467
468 fn proposal_exists(&self, proposal_id: usize) -> bool {
469 self.is_valid_proposal_id(proposal_id) && !self.proposals().item_is_empty(proposal_id)
470 }
471
472 fn total_gas_needed(
473 &self,
474 actions: &ArrayVec<GovernanceAction<Self::Api>, MAX_GOVERNANCE_PROPOSAL_ACTIONS>,
475 ) -> u64 {
476 let mut total = 0;
477 for action in actions {
478 total += action.gas_limit;
479 }
480
481 total
482 }
483
484 fn clear_proposal(&self, proposal_id: usize) {
487 self.proposals().clear_entry(proposal_id);
488 self.proposal_start_block(proposal_id).clear();
489 self.proposal_queue_block(proposal_id).clear();
490
491 self.total_votes(proposal_id).clear();
492 self.total_downvotes(proposal_id).clear();
493 }
494
495 #[storage_mapper("governance:proposals")]
498 fn proposals(&self) -> VecMapper<GovernanceProposal<Self::Api>>;
499
500 #[storage_mapper("governance:proposalStartBlock")]
502 fn proposal_start_block(&self, proposal_id: usize) -> SingleValueMapper<u64>;
503
504 #[storage_mapper("governance:proposalQueueBlock")]
505 fn proposal_queue_block(&self, proposal_id: usize) -> SingleValueMapper<u64>;
506
507 #[storage_mapper("governance:userVotedProposals")]
508 fn user_voted_proposals(&self, user: &ManagedAddress) -> UnorderedSetMapper<ProposalId>;
509
510 #[view(getProposalVotes)]
511 #[storage_mapper("proposalVotes")]
512 fn proposal_votes(
513 &self,
514 proposal_id: ProposalId,
515 ) -> SingleValueMapper<ProposalVotes<Self::Api>>;
516
517 #[view(getTotalVotes)]
518 #[storage_mapper("governance:totalVotes")]
519 fn total_votes(&self, proposal_id: usize) -> SingleValueMapper<BigUint>;
520
521 #[view(getTotalDownvotes)]
522 #[storage_mapper("governance:totalDownvotes")]
523 fn total_downvotes(&self, proposal_id: usize) -> SingleValueMapper<BigUint>;
524}