numbat_wasm_module_governance/
lib.rs1#![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 #[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 #[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(¤t_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(¤t_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 #[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 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 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 #[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_mapper("governance:proposals")]
457 fn proposals(&self) -> VecMapper<GovernanceProposal<Self::Api>>;
458
459 #[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 #[view(getTotalVotes)]
474 #[storage_mapper("governance:totalVotes")]
475 fn total_votes(&self, proposal_id: usize) -> SingleValueMapper<BigUint>;
476
477 #[view(getTotalDownvotes)]
479 #[storage_mapper("governance:totalDownvotes")]
480 fn total_downvotes(&self, proposal_id: usize) -> SingleValueMapper<BigUint>;
481}