numbat_wasm_module_governance/
lib.rs

1#![no_std]
2
3numbat_wasm::imports!();
4
5pub mod governance_configurable;
6
7pub mod governance_proposal;
8use governance_proposal::*;
9
10const MAX_GAS_LIMIT_PER_BLOCK: u64 = 1_500_000_000;
11
12#[numbat_wasm::module]
13pub trait GovernanceModule:
14    governance_configurable::GovernanceConfigurablePropertiesModule
15{
16    // endpoints
17
18    // Used to deposit tokens for "payable" actions
19    // There is no "withdraw" functionality
20    // Funds can only be retrived through an action
21    #[payable("*")]
22    #[endpoint(depositTokensForAction)]
23    fn deposit_tokens_for_action(
24        &self,
25        #[payment_token] payment_token: TokenIdentifier,
26        #[payment_nonce] payment_nonce: u64,
27        #[payment_amount] payment_amount: BigUint,
28    ) {
29        let caller = self.blockchain().get_caller();
30
31        self.user_deposit_event(&caller, &payment_token, payment_nonce, &payment_amount);
32    }
33
34    // Used to withdraw the tokens after the action was executed or cancelled
35    #[endpoint(withdrawGovernanceTokens)]
36    fn withdraw_governance_tokens(&self, proposal_id: usize) -> SCResult<()> {
37        self.require_valid_proposal_id(proposal_id)?;
38        require!(
39            self.get_proposal_status(proposal_id) == GovernanceProposalStatus::None,
40            "Proposal has to be executed or canceled first"
41        );
42
43        let caller = self.blockchain().get_caller();
44        let governance_token_id = self.governance_token_id().get();
45        let nr_votes_tokens = self
46            .votes(proposal_id)
47            .get(&caller)
48            .unwrap_or_else(|| self.types().big_uint_zero());
49        let nr_downvotes_tokens = self
50            .downvotes(proposal_id)
51            .get(&caller)
52            .unwrap_or_else(|| self.types().big_uint_zero());
53        let total_tokens = nr_votes_tokens + nr_downvotes_tokens;
54
55        if total_tokens > 0 {
56            self.votes(proposal_id).remove(&caller);
57            self.downvotes(proposal_id).remove(&caller);
58
59            self.send()
60                .direct(&caller, &governance_token_id, 0, &total_tokens, &[]);
61        }
62
63        Ok(())
64    }
65
66    #[payable("*")]
67    #[endpoint]
68    fn propose(
69        &self,
70        #[payment_amount] payment_amount: BigUint,
71        description: BoxedBytes,
72        #[var_args] actions: VarArgs<GovernanceActionAsMultiArg<Self::Api>>,
73    ) -> SCResult<usize> {
74        self.require_payment_token_governance_token()?;
75        require!(
76            payment_amount >= self.min_token_balance_for_proposing().get(),
77            "Not enough tokens for proposing action"
78        );
79        require!(!actions.is_empty(), "Proposal has no actions");
80        require!(
81            actions.len() <= self.max_actions_per_proposal().get(),
82            "Exceeded max actions per proposal"
83        );
84
85        let mut gov_actions = Vec::with_capacity(actions.len());
86        for action in actions.into_vec() {
87            let (gas_limit, dest_address, token_id, token_nonce, amount, function_name, arguments) =
88                action.into_tuple();
89            let gov_action = GovernanceAction {
90                gas_limit,
91                dest_address,
92                token_id,
93                token_nonce,
94                amount,
95                function_name,
96                arguments,
97            };
98
99            require!(
100                gas_limit < MAX_GAS_LIMIT_PER_BLOCK,
101                "A single action cannot use more than the max gas limit per block"
102            );
103
104            gov_actions.push(gov_action);
105        }
106
107        require!(
108            self.total_gas_needed(&gov_actions) < MAX_GAS_LIMIT_PER_BLOCK,
109            "Actions require too much gas to be executed"
110        );
111
112        let proposer = self.blockchain().get_caller();
113        let current_block = self.blockchain().get_block_nonce();
114        let proposal_id = self.proposals().len() + 1;
115
116        self.proposal_created_event(
117            proposal_id,
118            &proposer,
119            current_block,
120            &description,
121            &gov_actions,
122        );
123
124        let proposal = GovernanceProposal {
125            proposer: proposer.clone(),
126            description,
127            actions: gov_actions,
128        };
129        let _ = self.proposals().push(&proposal);
130
131        self.proposal_start_block(proposal_id).set(&current_block);
132
133        self.total_votes(proposal_id).set(&payment_amount);
134        self.votes(proposal_id).insert(proposer, payment_amount);
135
136        Ok(proposal_id)
137    }
138
139    #[payable("*")]
140    #[endpoint]
141    fn vote(&self, #[payment_amount] payment_amount: BigUint, proposal_id: usize) -> SCResult<()> {
142        self.require_payment_token_governance_token()?;
143        self.require_valid_proposal_id(proposal_id)?;
144        require!(
145            self.get_proposal_status(proposal_id) == GovernanceProposalStatus::Active,
146            "Proposal is not active"
147        );
148
149        let voter = self.blockchain().get_caller();
150
151        self.vote_cast_event(&voter, proposal_id, &payment_amount);
152
153        self.total_votes(proposal_id)
154            .update(|total_votes| *total_votes += &payment_amount);
155        self.votes(proposal_id)
156            .entry(voter)
157            .and_modify(|nr_votes| *nr_votes += &payment_amount)
158            .or_insert(payment_amount);
159
160        Ok(())
161    }
162
163    #[payable("*")]
164    #[endpoint]
165    fn downvote(
166        &self,
167        #[payment_amount] payment_amount: BigUint,
168        proposal_id: usize,
169    ) -> SCResult<()> {
170        self.require_payment_token_governance_token()?;
171        self.require_valid_proposal_id(proposal_id)?;
172        require!(
173            self.get_proposal_status(proposal_id) == GovernanceProposalStatus::Active,
174            "Proposal is not active"
175        );
176
177        let downvoter = self.blockchain().get_caller();
178
179        self.downvote_cast_event(&downvoter, proposal_id, &payment_amount);
180
181        self.total_downvotes(proposal_id)
182            .update(|total_downvotes| *total_downvotes += &payment_amount);
183        self.downvotes(proposal_id)
184            .entry(downvoter)
185            .and_modify(|nr_downvotes| *nr_downvotes += &payment_amount)
186            .or_insert(payment_amount);
187
188        Ok(())
189    }
190
191    #[endpoint]
192    fn queue(&self, proposal_id: usize) -> SCResult<()> {
193        require!(
194            self.get_proposal_status(proposal_id) == GovernanceProposalStatus::Succeeded,
195            "Can only queue succeeded proposals"
196        );
197
198        let current_block = self.blockchain().get_block_nonce();
199        self.proposal_queue_block(proposal_id).set(&current_block);
200
201        self.proposal_queued_event(proposal_id, current_block);
202
203        Ok(())
204    }
205
206    #[endpoint]
207    fn execute(&self, proposal_id: usize) -> SCResult<()> {
208        require!(
209            self.get_proposal_status(proposal_id) == GovernanceProposalStatus::Queued,
210            "Can only execute queued proposals"
211        );
212
213        let current_block = self.blockchain().get_block_nonce();
214        let lock_blocks = self.lock_time_after_voting_ends_in_blocks().get();
215
216        let lock_start = self.proposal_queue_block(proposal_id).get();
217        let lock_end = lock_start + lock_blocks;
218
219        require!(
220            current_block >= lock_end,
221            "Proposal is in timelock status. Try again later"
222        );
223
224        let proposal = self.proposals().get(proposal_id);
225        let total_gas_needed = self.total_gas_needed(&proposal.actions);
226        let gas_left = self.blockchain().get_gas_left();
227
228        require!(
229            gas_left > total_gas_needed,
230            "Not enough gas to execute all proposals"
231        );
232
233        for action in proposal.actions {
234            let mut contract_call = self
235                .send()
236                .contract_call::<()>(action.dest_address, action.function_name)
237                .with_gas_limit(action.gas_limit);
238
239            if action.amount > 0 {
240                contract_call = contract_call.add_token_transfer(
241                    action.token_id,
242                    action.token_nonce,
243                    action.amount,
244                );
245            }
246
247            for arg in action.arguments {
248                contract_call.push_argument_raw_bytes(arg.as_slice());
249            }
250
251            contract_call.transfer_execute();
252        }
253
254        self.clear_proposal(proposal_id);
255
256        self.proposal_executed_event(proposal_id);
257
258        Ok(())
259    }
260
261    #[endpoint]
262    fn cancel(&self, proposal_id: usize) -> SCResult<()> {
263        match self.get_proposal_status(proposal_id) {
264            GovernanceProposalStatus::None => {
265                return sc_error!("Proposal does not exist");
266            },
267            GovernanceProposalStatus::Defeated => {},
268            _ => {
269                let proposal = self.proposals().get(proposal_id);
270                let caller = self.blockchain().get_caller();
271
272                require!(
273                    caller == proposal.proposer,
274                    "Only original proposer may cancel a non-defeated proposal"
275                );
276            },
277        }
278
279        self.clear_proposal(proposal_id);
280
281        self.proposal_canceled_event(proposal_id);
282
283        Ok(())
284    }
285
286    // views
287
288    #[view(getProposalStatus)]
289    fn get_proposal_status(&self, proposal_id: usize) -> GovernanceProposalStatus {
290        if !self.proposal_exists(proposal_id) {
291            return GovernanceProposalStatus::None;
292        }
293
294        let queue_block = self.proposal_queue_block(proposal_id).get();
295        if queue_block > 0 {
296            return GovernanceProposalStatus::Queued;
297        }
298
299        let current_block = self.blockchain().get_block_nonce();
300        let proposal_block = self.proposal_start_block(proposal_id).get();
301        let voting_delay = self.voting_delay_in_blocks().get();
302        let voting_period = self.voting_period_in_blocks().get();
303
304        let voting_start = proposal_block + voting_delay;
305        let voting_end = voting_start + voting_period;
306
307        if current_block < voting_start {
308            return GovernanceProposalStatus::Pending;
309        }
310        if current_block >= voting_start && current_block < voting_end {
311            return GovernanceProposalStatus::Active;
312        }
313
314        let total_votes = self.total_votes(proposal_id).get();
315        let total_downvotes = self.total_downvotes(proposal_id).get();
316        let quorum = self.quorum().get();
317
318        if total_votes > total_downvotes && total_votes - total_downvotes >= quorum {
319            GovernanceProposalStatus::Succeeded
320        } else {
321            GovernanceProposalStatus::Defeated
322        }
323    }
324
325    #[view(getProposer)]
326    fn get_proposer(&self, proposal_id: usize) -> OptionalArg<ManagedAddress> {
327        if !self.proposal_exists(proposal_id) {
328            OptionalArg::None
329        } else {
330            OptionalArg::Some(self.proposals().get(proposal_id).proposer)
331        }
332    }
333
334    #[view(getProposalDescription)]
335    fn get_proposal_description(&self, proposal_id: usize) -> OptionalArg<BoxedBytes> {
336        if !self.proposal_exists(proposal_id) {
337            OptionalArg::None
338        } else {
339            OptionalArg::Some(self.proposals().get(proposal_id).description)
340        }
341    }
342
343    #[view(getProposalActions)]
344    fn get_proposal_actions(
345        &self,
346        proposal_id: usize,
347    ) -> MultiResultVec<GovernanceActionAsMultiArg<Self::Api>> {
348        if !self.proposal_exists(proposal_id) {
349            return Vec::new().into();
350        }
351
352        let actions = self.proposals().get(proposal_id).actions;
353        let mut actions_as_multiarg = Vec::with_capacity(actions.len());
354
355        for action in actions {
356            actions_as_multiarg.push(action.into_multiarg());
357        }
358
359        actions_as_multiarg.into()
360    }
361
362    // private
363
364    fn require_payment_token_governance_token(&self) -> SCResult<()> {
365        require!(
366            self.call_value().token() == self.governance_token_id().get(),
367            "Only Governance token accepted as payment"
368        );
369        Ok(())
370    }
371
372    fn require_valid_proposal_id(&self, proposal_id: usize) -> SCResult<()> {
373        require!(
374            self.is_valid_proposal_id(proposal_id),
375            "Invalid proposal ID"
376        );
377        Ok(())
378    }
379
380    fn is_valid_proposal_id(&self, proposal_id: usize) -> bool {
381        proposal_id >= 1 && proposal_id <= self.proposals().len()
382    }
383
384    fn proposal_exists(&self, proposal_id: usize) -> bool {
385        self.is_valid_proposal_id(proposal_id) && !self.proposals().item_is_empty(proposal_id)
386    }
387
388    fn total_gas_needed(&self, actions: &[GovernanceAction<Self::Api>]) -> u64 {
389        let mut total = 0;
390        for action in actions {
391            total += action.gas_limit;
392        }
393
394        total
395    }
396
397    /// specific votes/downvotes are not cleared,
398    /// as they're used for reclaim tokens logic and cleared one by one
399    fn clear_proposal(&self, proposal_id: usize) {
400        self.proposals().clear_entry(proposal_id);
401        self.proposal_start_block(proposal_id).clear();
402        self.proposal_queue_block(proposal_id).clear();
403
404        self.total_votes(proposal_id).clear();
405        self.total_downvotes(proposal_id).clear();
406    }
407
408    // events
409
410    #[event("proposalCreated")]
411    fn proposal_created_event(
412        &self,
413        #[indexed] proposal_id: usize,
414        #[indexed] proposer: &ManagedAddress,
415        #[indexed] start_block: u64,
416        #[indexed] description: &BoxedBytes,
417        actions: &[GovernanceAction<Self::Api>],
418    );
419
420    #[event("voteCast")]
421    fn vote_cast_event(
422        &self,
423        #[indexed] voter: &ManagedAddress,
424        #[indexed] proposal_id: usize,
425        nr_votes: &BigUint,
426    );
427
428    #[event("downvoteCast")]
429    fn downvote_cast_event(
430        &self,
431        #[indexed] downvoter: &ManagedAddress,
432        #[indexed] proposal_id: usize,
433        nr_downvotes: &BigUint,
434    );
435
436    #[event("proposalCanceled")]
437    fn proposal_canceled_event(&self, #[indexed] proposal_id: usize);
438
439    #[event("proposalQueued")]
440    fn proposal_queued_event(&self, #[indexed] proposal_id: usize, #[indexed] queued_block: u64);
441
442    #[event("proposalExecuted")]
443    fn proposal_executed_event(&self, #[indexed] proposal_id: usize);
444
445    #[event("userDeposit")]
446    fn user_deposit_event(
447        &self,
448        #[indexed] address: &ManagedAddress,
449        #[indexed] token_id: &TokenIdentifier,
450        #[indexed] token_nonce: u64,
451        amount: &BigUint,
452    );
453
454    // storage - general
455
456    #[storage_mapper("governance:proposals")]
457    fn proposals(&self) -> VecMapper<GovernanceProposal<Self::Api>>;
458
459    /// Not stored under "proposals", as that would require deserializing the whole struct
460    #[storage_mapper("governance:proposalStartBlock")]
461    fn proposal_start_block(&self, proposal_id: usize) -> SingleValueMapper<u64>;
462
463    #[storage_mapper("governance:proposalQueueBlock")]
464    fn proposal_queue_block(&self, proposal_id: usize) -> SingleValueMapper<u64>;
465
466    #[storage_mapper("governance:votes")]
467    fn votes(&self, proposal_id: usize) -> MapMapper<ManagedAddress, BigUint>;
468
469    #[storage_mapper("governance:downvotes")]
470    fn downvotes(&self, proposal_id: usize) -> MapMapper<ManagedAddress, BigUint>;
471
472    /// Could be calculated by iterating over the "votes" mapper, but that costs a lot of gas
473    #[view(getTotalVotes)]
474    #[storage_mapper("governance:totalVotes")]
475    fn total_votes(&self, proposal_id: usize) -> SingleValueMapper<BigUint>;
476
477    /// Could be calculated by iterating over the "downvotes" mapper, but that costs a lot of gas
478    #[view(getTotalDownvotes)]
479    #[storage_mapper("governance:totalDownvotes")]
480    fn total_downvotes(&self, proposal_id: usize) -> SingleValueMapper<BigUint>;
481}