multiversx_sc_modules/governance/
mod.rs

1multiversx_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    // endpoints
21
22    /// Used to deposit tokens for "payable" actions.
23    /// Funds will be returned if the proposal is defeated.
24    /// To keep the logic simple, all tokens have to be deposited at once
25    #[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    // Used to withdraw the tokens after the action was executed or cancelled
57    #[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    /// Propose a list of actions.
100    /// A maximum of MAX_GOVERNANCE_PROPOSAL_ACTIONS can be proposed at a time.
101    ///
102    /// An action has the following format:
103    ///     - gas limit for action execution
104    ///     - destination address
105    ///     - a vector of ESDT transfers, in the form of ManagedVec<EsdTokenPayment>
106    ///     - endpoint to be called on the destination
107    ///     - a vector of arguments for the endpoint, in the form of ManagedVec<ManagedBuffer>
108    ///
109    /// Returns the ID of the newly created proposal.
110    #[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    /// Vote on a proposal by depositing any amount of governance tokens
182    /// These tokens will be locked until the proposal is executed or cancelled.
183    #[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    /// Queue a proposal for execution.
228    /// This can be done only if the proposal has reached the quorum.
229    /// A proposal is considered successful and ready for queing if
230    /// total_votes - total_downvotes >= quorum
231    #[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    /// Execute a previously queued proposal.
247    /// This will clear the proposal and unlock the governance tokens.
248    /// Said tokens can then be withdrawn and used to vote/downvote other proposals.
249    #[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    /// Cancel a proposed action. This can be done:
293    /// - by the proposer, at any time
294    /// - by anyone, if the proposal was defeated
295    #[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    // views
326
327    #[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    // private
415
416    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    /// specific votes/downvotes are not cleared,
485    /// as they're used for reclaim tokens logic and cleared one by one
486    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 - general
496
497    #[storage_mapper("governance:proposals")]
498    fn proposals(&self) -> VecMapper<GovernanceProposal<Self::Api>>;
499
500    /// Not stored under "proposals", as that would require deserializing the whole struct
501    #[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}