spl_governance/state/
proposal.rs

1//! Proposal  Account
2
3use {
4    crate::{
5        addins::max_voter_weight::{
6            assert_is_valid_max_voter_weight,
7            get_max_voter_weight_record_data_for_realm_and_governing_token_mint,
8        },
9        error::GovernanceError,
10        state::{
11            enums::{
12                GovernanceAccountType, InstructionExecutionFlags, MintMaxVoterWeightSource,
13                ProposalState, TransactionExecutionStatus, VoteThreshold, VoteTipping,
14            },
15            governance::GovernanceConfig,
16            legacy::ProposalV1,
17            proposal_transaction::ProposalTransactionV2,
18            realm::RealmV2,
19            realm_config::RealmConfigAccount,
20            vote_record::{Vote, VoteKind},
21        },
22        tools::spl_token::get_spl_token_mint_supply,
23        PROGRAM_AUTHORITY_SEED,
24    },
25    borsh::{maybestd::io::Write, BorshDeserialize, BorshSchema, BorshSerialize},
26    solana_program::{
27        account_info::{next_account_info, AccountInfo},
28        clock::{Slot, UnixTimestamp},
29        program_error::ProgramError,
30        program_pack::IsInitialized,
31        pubkey::Pubkey,
32    },
33    spl_governance_tools::account::{get_account_data, get_account_type, AccountMaxSize},
34    std::{cmp::Ordering, slice::Iter},
35};
36
37/// Proposal option vote result
38#[derive(Clone, Debug, PartialEq, Eq, BorshDeserialize, BorshSerialize, BorshSchema)]
39pub enum OptionVoteResult {
40    /// Vote on the option is not resolved yet
41    None,
42
43    /// Vote on the option is completed and the option passed
44    Succeeded,
45
46    /// Vote on the option is completed and the option was defeated
47    Defeated,
48}
49
50/// Proposal Option
51#[derive(Clone, Debug, PartialEq, Eq, BorshDeserialize, BorshSerialize, BorshSchema)]
52pub struct ProposalOption {
53    /// Option label
54    pub label: String,
55
56    /// Vote weight for the option
57    pub vote_weight: u64,
58
59    /// Vote result for the option
60    pub vote_result: OptionVoteResult,
61
62    /// The number of the transactions already executed
63    pub transactions_executed_count: u16,
64
65    /// The number of transactions included in the option
66    pub transactions_count: u16,
67
68    /// The index of the the next transaction to be added
69    pub transactions_next_index: u16,
70}
71
72/// Proposal vote type
73#[derive(Clone, Debug, PartialEq, Eq, BorshDeserialize, BorshSerialize, BorshSchema)]
74pub enum VoteType {
75    /// Single choice vote with mutually exclusive choices
76    /// In the SingeChoice mode there can ever be a single winner
77    /// If multiple options score the same highest vote then the Proposal is
78    /// not resolved and considered as Failed.
79    /// Note: Yes/No vote is a single choice (Yes) vote with the deny
80    /// option (No)
81    SingleChoice,
82
83    /// Multiple options can be selected with up to max_voter_options per voter
84    /// and with up to max_winning_options of successful options
85    /// Ex. voters are given 5 options, can choose up to 3 (max_voter_options)
86    /// and only 1 (max_winning_options) option can win and be executed
87    MultiChoice {
88        /// Type of MultiChoice
89        #[allow(dead_code)]
90        choice_type: MultiChoiceType,
91
92        /// The min number of options a voter must choose
93        ///
94        /// Note: In the current version the limit is not supported and not
95        /// enforced and must always be set to 1
96        #[allow(dead_code)]
97        min_voter_options: u8,
98
99        /// The max number of options a voter can choose
100        ///
101        /// Note: In the current version the limit is not supported and not
102        /// enforced and must always be set to the number of available
103        /// options
104        #[allow(dead_code)]
105        max_voter_options: u8,
106
107        /// The max number of wining options
108        /// For executable proposals it limits how many options can be executed
109        /// for a Proposal
110        ///
111        /// Note: In the current version the limit is not supported and not
112        /// enforced and must always be set to the number of available
113        /// options
114        #[allow(dead_code)]
115        max_winning_options: u8,
116    },
117}
118
119/// Type of MultiChoice.
120#[derive(Clone, Debug, PartialEq, Eq, BorshDeserialize, BorshSerialize, BorshSchema)]
121pub enum MultiChoiceType {
122    /// Multiple options can be approved with full weight allocated to each
123    /// approved option
124    FullWeight,
125
126    /// Multiple options can be approved with weight allocated proportionally
127    /// to the percentage of the total weight.
128    /// The full weight has to be voted among the approved options, i.e.,
129    /// 100% of the weight has to be allocated
130    Weighted,
131}
132
133/// Governance Proposal
134#[derive(Clone, Debug, PartialEq, Eq, BorshDeserialize, BorshSerialize, BorshSchema)]
135pub struct ProposalV2 {
136    /// Governance account type
137    pub account_type: GovernanceAccountType,
138
139    /// Governance account the Proposal belongs to
140    pub governance: Pubkey,
141
142    /// Indicates which Governing Token is used to vote on the Proposal
143    /// Whether the general Community token owners or the Council tokens owners
144    /// vote on this Proposal
145    pub governing_token_mint: Pubkey,
146
147    /// Current proposal state
148    pub state: ProposalState,
149
150    // TODO: add state_at timestamp to have single field to filter recent proposals in the UI
151    /// The TokenOwnerRecord representing the user who created and owns this
152    /// Proposal
153    pub token_owner_record: Pubkey,
154
155    /// The number of signatories assigned to the Proposal
156    pub signatories_count: u8,
157
158    /// The number of signatories who already signed
159    pub signatories_signed_off_count: u8,
160
161    /// Vote type
162    pub vote_type: VoteType,
163
164    /// Proposal options
165    pub options: Vec<ProposalOption>,
166
167    /// The total weight of the Proposal rejection votes
168    /// If the proposal has no deny option then the weight is None
169    ///
170    /// Only proposals with the deny option can have executable instructions
171    /// attached to them Without the deny option a proposal is only non
172    /// executable survey
173    ///
174    /// The deny options is also used for off-chain and/or manually executable
175    /// proposal to make them binding as opposed to survey only proposals
176    pub deny_vote_weight: Option<u64>,
177
178    /// Reserved space for future versions
179    /// This field is a leftover from unused veto_vote_weight: Option<u64>
180    pub reserved1: u8,
181
182    /// The total weight of  votes
183    /// Note: Abstain is not supported in the current version
184    pub abstain_vote_weight: Option<u64>,
185
186    /// Optional start time if the Proposal should not enter voting state
187    /// immediately after being signed off Note: start_at is not supported
188    /// in the current version
189    pub start_voting_at: Option<UnixTimestamp>,
190
191    /// When the Proposal was created and entered Draft state
192    pub draft_at: UnixTimestamp,
193
194    /// When Signatories started signing off the Proposal
195    pub signing_off_at: Option<UnixTimestamp>,
196
197    /// When the Proposal began voting as UnixTimestamp
198    pub voting_at: Option<UnixTimestamp>,
199
200    /// When the Proposal began voting as Slot
201    /// Note: The slot is not currently used but the exact slot is going to be
202    /// required to support snapshot based vote weights
203    pub voting_at_slot: Option<Slot>,
204
205    /// When the Proposal ended voting and entered either Succeeded or Defeated
206    pub voting_completed_at: Option<UnixTimestamp>,
207
208    /// When the Proposal entered Executing state
209    pub executing_at: Option<UnixTimestamp>,
210
211    /// When the Proposal entered final state Completed or Cancelled and was
212    /// closed
213    pub closed_at: Option<UnixTimestamp>,
214
215    /// Instruction execution flag for ordered and transactional instructions
216    /// Note: This field is not used in the current version
217    pub execution_flags: InstructionExecutionFlags,
218
219    /// The max vote weight for the Governing Token mint at the time Proposal
220    /// was decided.
221    /// It's used to show correct vote results for historical proposals in
222    /// cases when the mint supply or max weight source changed after vote was
223    /// completed.
224    pub max_vote_weight: Option<u64>,
225
226    /// Max voting time for the proposal if different from parent Governance
227    /// (only higher value possible).
228    /// Note: This field is not used in the current version
229    pub max_voting_time: Option<u32>,
230
231    /// The vote threshold at the time Proposal was decided
232    /// It's used to show correct vote results for historical proposals in cases
233    /// when the threshold was changed for governance config after vote was
234    /// completed.
235    /// TODO: Use this field to override the threshold from parent Governance
236    /// (only higher value possible)
237    pub vote_threshold: Option<VoteThreshold>,
238
239    /// Reserved space for future versions
240    pub reserved: [u8; 64],
241
242    /// Proposal name
243    pub name: String,
244
245    /// Link to proposal's description
246    pub description_link: String,
247
248    /// The total weight of Veto votes
249    pub veto_vote_weight: u64,
250}
251
252impl AccountMaxSize for ProposalV2 {
253    fn get_max_size(&self) -> Option<usize> {
254        let options_size: usize = self.options.iter().map(|o| o.label.len() + 19).sum();
255        Some(self.name.len() + self.description_link.len() + options_size + 297)
256    }
257}
258
259impl IsInitialized for ProposalV2 {
260    fn is_initialized(&self) -> bool {
261        self.account_type == GovernanceAccountType::ProposalV2
262    }
263}
264
265impl ProposalV2 {
266    /// Checks if Signatories can be edited (added or removed) for the Proposal
267    /// in the given state
268    pub fn assert_can_edit_signatories(&self) -> Result<(), ProgramError> {
269        self.assert_is_draft_state()
270            .map_err(|_| GovernanceError::InvalidStateCannotEditSignatories.into())
271    }
272
273    /// Checks if Proposal can be singed off
274    pub fn assert_can_sign_off(&self) -> Result<(), ProgramError> {
275        match self.state {
276            ProposalState::Draft | ProposalState::SigningOff => Ok(()),
277            ProposalState::Executing
278            | ProposalState::ExecutingWithErrors
279            | ProposalState::Completed
280            | ProposalState::Cancelled
281            | ProposalState::Voting
282            | ProposalState::Succeeded
283            | ProposalState::Defeated
284            | ProposalState::Vetoed => Err(GovernanceError::InvalidStateCannotSignOff.into()),
285        }
286    }
287
288    /// Checks the Proposal is in Voting state
289    fn assert_is_voting_state(&self) -> Result<(), ProgramError> {
290        if self.state != ProposalState::Voting {
291            return Err(GovernanceError::InvalidProposalState.into());
292        }
293
294        Ok(())
295    }
296
297    /// Checks the Proposal is in Draft state
298    fn assert_is_draft_state(&self) -> Result<(), ProgramError> {
299        if self.state != ProposalState::Draft {
300            return Err(GovernanceError::InvalidProposalState.into());
301        }
302
303        Ok(())
304    }
305
306    /// Checks the Proposal was finalized (no more state transition will happen)
307    pub fn assert_is_final_state(&self) -> Result<(), ProgramError> {
308        match self.state {
309            ProposalState::Completed
310            | ProposalState::Cancelled
311            | ProposalState::Defeated
312            | ProposalState::Vetoed => Ok(()),
313            ProposalState::Executing
314            | ProposalState::ExecutingWithErrors
315            | ProposalState::SigningOff
316            | ProposalState::Voting
317            | ProposalState::Draft
318            | ProposalState::Succeeded => Err(GovernanceError::InvalidStateNotFinal.into()),
319        }
320    }
321
322    /// Checks if Proposal can be voted on
323    pub fn assert_can_cast_vote(
324        &self,
325        config: &GovernanceConfig,
326        vote: &Vote,
327        current_unix_timestamp: UnixTimestamp,
328    ) -> Result<(), ProgramError> {
329        self.assert_is_voting_state()
330            .map_err(|_| GovernanceError::InvalidStateCannotVote)?;
331
332        // Check if we are still within the configured max voting time period
333        if self.has_voting_max_time_ended(config, current_unix_timestamp) {
334            return Err(GovernanceError::ProposalVotingTimeExpired.into());
335        }
336
337        match vote {
338            Vote::Approve(_) | Vote::Abstain => {
339                // Once the base voting time passes and we are in the voting cool off time
340                // approving votes are no longer accepted Abstain is considered
341                // as positive vote because when attendance quorum is used it can tip the scales
342                if self.has_voting_base_time_ended(config, current_unix_timestamp) {
343                    Err(GovernanceError::VoteNotAllowedInCoolOffTime.into())
344                } else {
345                    Ok(())
346                }
347            }
348            // Within voting cool off time only counter votes are allowed
349            Vote::Deny | Vote::Veto => Ok(()),
350        }
351    }
352
353    /// Checks if proposal has concluded so that security deposit is no longer
354    /// needed
355    pub fn assert_can_refund_proposal_deposit(&self) -> Result<(), ProgramError> {
356        match self.state {
357            ProposalState::Succeeded
358            | ProposalState::Executing
359            | ProposalState::Completed
360            | ProposalState::Cancelled
361            | ProposalState::Defeated
362            | ProposalState::ExecutingWithErrors
363            | ProposalState::Vetoed => Ok(()),
364            ProposalState::Draft | ProposalState::SigningOff | ProposalState::Voting => {
365                Err(GovernanceError::CannotRefundProposalDeposit.into())
366            }
367        }
368    }
369
370    /// Expected base vote end time determined by the configured
371    /// base_voting_time and actual voting start time
372    pub fn voting_base_time_end(&self, config: &GovernanceConfig) -> UnixTimestamp {
373        self.voting_at
374            .unwrap()
375            .checked_add(config.voting_base_time as i64)
376            .unwrap()
377    }
378
379    /// Checks whether the base voting time has ended for the proposal
380    pub fn has_voting_base_time_ended(
381        &self,
382        config: &GovernanceConfig,
383        current_unix_timestamp: UnixTimestamp,
384    ) -> bool {
385        // Check if we passed the configured base vote end time
386        self.voting_base_time_end(config) < current_unix_timestamp
387    }
388
389    /// Expected max vote end time determined by the configured
390    /// base_voting_time, optional voting_cool_off_time and actual voting start
391    /// time
392    pub fn voting_max_time_end(&self, config: &GovernanceConfig) -> UnixTimestamp {
393        self.voting_base_time_end(config)
394            .checked_add(config.voting_cool_off_time as i64)
395            .unwrap()
396    }
397
398    /// Checks whether the max voting time has ended for the proposal
399    pub fn has_voting_max_time_ended(
400        &self,
401        config: &GovernanceConfig,
402        current_unix_timestamp: UnixTimestamp,
403    ) -> bool {
404        // Check if we passed the max vote end time
405        self.voting_max_time_end(config) < current_unix_timestamp
406    }
407
408    /// Checks if Proposal can be finalized
409    pub fn assert_can_finalize_vote(
410        &self,
411        config: &GovernanceConfig,
412        current_unix_timestamp: UnixTimestamp,
413    ) -> Result<(), ProgramError> {
414        self.assert_is_voting_state()
415            .map_err(|_| GovernanceError::InvalidStateCannotFinalize)?;
416
417        // We can only finalize the vote after the configured max_voting_time has
418        // expired and vote time ended
419        if !self.has_voting_max_time_ended(config, current_unix_timestamp) {
420            return Err(GovernanceError::CannotFinalizeVotingInProgress.into());
421        }
422
423        Ok(())
424    }
425
426    /// Finalizes vote by moving it to final state Succeeded or Defeated if
427    /// max_voting_time has passed If Proposal is still within
428    /// max_voting_time period then error is returned
429    pub fn finalize_vote(
430        &mut self,
431        max_voter_weight: u64,
432        config: &GovernanceConfig,
433        current_unix_timestamp: UnixTimestamp,
434        vote_threshold: &VoteThreshold,
435    ) -> Result<(), ProgramError> {
436        self.assert_can_finalize_vote(config, current_unix_timestamp)?;
437
438        self.state = self.resolve_final_vote_state(max_voter_weight, vote_threshold)?;
439        self.voting_completed_at = Some(self.voting_max_time_end(config));
440
441        // Capture vote params to correctly display historical results
442        self.max_vote_weight = Some(max_voter_weight);
443        self.vote_threshold = Some(vote_threshold.clone());
444
445        Ok(())
446    }
447
448    /// Resolves final proposal state after vote ends
449    /// It inspects all proposals options and resolves their final vote results
450    fn resolve_final_vote_state(
451        &mut self,
452        max_vote_weight: u64,
453        vote_threshold: &VoteThreshold,
454    ) -> Result<ProposalState, ProgramError> {
455        // Get the min vote weight required for options to pass
456        let min_vote_threshold_weight =
457            get_min_vote_threshold_weight(vote_threshold, max_vote_weight).unwrap();
458
459        // If the proposal has a reject option then any other option must beat it
460        // regardless of the configured min_vote_threshold_weight
461        let deny_vote_weight = self.deny_vote_weight.unwrap_or(0);
462
463        let mut best_succeeded_option_weight = 0;
464        let mut best_succeeded_option_count = 0u16;
465
466        for option in self.options.iter_mut() {
467            // Any positive vote (Yes) must be equal or above the required
468            // min_vote_threshold_weight and higher than the reject option vote (No)
469            // The same number of positive (Yes) and rejecting (No) votes is a tie and
470            // resolved as Defeated In other words  +1 vote as a tie breaker is
471            // required to succeed for the positive option vote
472            if option.vote_weight >= min_vote_threshold_weight
473                && option.vote_weight > deny_vote_weight
474            {
475                option.vote_result = OptionVoteResult::Succeeded;
476
477                match option.vote_weight.cmp(&best_succeeded_option_weight) {
478                    Ordering::Greater => {
479                        best_succeeded_option_weight = option.vote_weight;
480                        best_succeeded_option_count = 1;
481                    }
482                    Ordering::Equal => {
483                        best_succeeded_option_count =
484                            best_succeeded_option_count.checked_add(1).unwrap()
485                    }
486                    Ordering::Less => {}
487                }
488            } else {
489                option.vote_result = OptionVoteResult::Defeated;
490            }
491        }
492
493        let mut final_state = if best_succeeded_option_count == 0 {
494            // If none of the individual options succeeded then the proposal as a whole is
495            // defeated
496            ProposalState::Defeated
497        } else {
498            match &self.vote_type {
499                VoteType::SingleChoice => {
500                    let proposal_state = if best_succeeded_option_count > 1 {
501                        // If there is more than one winning option then the single choice proposal
502                        // is considered as defeated
503                        best_succeeded_option_weight = u64::MAX; // no winning option
504                        ProposalState::Defeated
505                    } else {
506                        ProposalState::Succeeded
507                    };
508
509                    // Coerce options vote results based on the winning score
510                    // (best_succeeded_vote_weight)
511                    for option in self.options.iter_mut() {
512                        option.vote_result = if option.vote_weight == best_succeeded_option_weight {
513                            OptionVoteResult::Succeeded
514                        } else {
515                            OptionVoteResult::Defeated
516                        };
517                    }
518
519                    proposal_state
520                }
521                VoteType::MultiChoice {
522                    choice_type: _,
523                    max_voter_options: _,
524                    max_winning_options: _,
525                    min_voter_options: _,
526                } => {
527                    // If any option succeeded for multi choice then the proposal as a whole
528                    // succeeded as well
529                    ProposalState::Succeeded
530                }
531            }
532        };
533
534        // None executable proposal is just a survey and is considered Completed once
535        // the vote ends and no more actions are available There is no overall
536        // Success or Failure status for the Proposal however individual options still
537        // have their own status
538        //
539        // Note: An off-chain/manually executable Proposal has no instructions but it
540        // still must have the deny vote enabled to be binding In such a case,
541        // if successful, the Proposal vote ends in Succeeded state and it must be
542        // manually transitioned to Completed state by the Proposal owner once
543        // the external actions are executed
544        if self.deny_vote_weight.is_none() {
545            final_state = ProposalState::Completed;
546        }
547
548        Ok(final_state)
549    }
550
551    /// Calculates max voter weight for given mint supply and realm config
552    fn get_max_voter_weight_from_mint_supply(
553        &mut self,
554        realm_data: &RealmV2,
555        governing_token_mint: &Pubkey,
556        governing_token_mint_supply: u64,
557        vote_kind: &VoteKind,
558    ) -> Result<u64, ProgramError> {
559        // max vote weight fraction is only used for community mint
560        if Some(*governing_token_mint) == realm_data.config.council_mint {
561            return Ok(governing_token_mint_supply);
562        }
563
564        let max_voter_weight = match realm_data.config.community_mint_max_voter_weight_source {
565            MintMaxVoterWeightSource::SupplyFraction(fraction) => {
566                if fraction == MintMaxVoterWeightSource::SUPPLY_FRACTION_BASE {
567                    return Ok(governing_token_mint_supply);
568                }
569
570                (governing_token_mint_supply as u128)
571                    .checked_mul(fraction as u128)
572                    .unwrap()
573                    .checked_div(MintMaxVoterWeightSource::SUPPLY_FRACTION_BASE as u128)
574                    .unwrap() as u64
575            }
576            MintMaxVoterWeightSource::Absolute(value) => value,
577        };
578
579        // When the fraction or absolute value is used it's possible we can go over the
580        // calculated max_vote_weight and we have to adjust it in case more
581        // votes have been cast
582        Ok(self.coerce_max_voter_weight(max_voter_weight, vote_kind))
583    }
584
585    /// Adjusts max voter weight to ensure it's not lower than total cast votes
586    fn coerce_max_voter_weight(&self, max_voter_weight: u64, vote_kind: &VoteKind) -> u64 {
587        let total_vote_weight = match vote_kind {
588            VoteKind::Electorate => {
589                let deny_vote_weight = self.deny_vote_weight.unwrap_or(0);
590
591                let max_option_vote_weight =
592                    self.options.iter().map(|o| o.vote_weight).max().unwrap();
593
594                max_option_vote_weight
595                    .checked_add(deny_vote_weight)
596                    .unwrap()
597            }
598            VoteKind::Veto => self.veto_vote_weight,
599        };
600
601        max_voter_weight.max(total_vote_weight)
602    }
603
604    /// Resolves max voter weight using either 1) voting governing_token_mint
605    /// supply or 2) max voter weight if configured for the token mint
606    #[allow(clippy::too_many_arguments)]
607    pub fn resolve_max_voter_weight(
608        &mut self,
609        account_info_iter: &mut Iter<AccountInfo>,
610        realm: &Pubkey,
611        realm_data: &RealmV2,
612        realm_config_data: &RealmConfigAccount,
613        vote_governing_token_mint_info: &AccountInfo,
614        vote_kind: &VoteKind,
615    ) -> Result<u64, ProgramError> {
616        // if the Realm is configured to use max voter weight for the given voting
617        // governing_token_mint then use the externally provided max_voter_weight
618        // instead of the supply based max
619        if let Some(max_voter_weight_addin) = realm_config_data
620            .get_token_config(realm_data, vote_governing_token_mint_info.key)?
621            .max_voter_weight_addin
622        {
623            let max_voter_weight_record_info = next_account_info(account_info_iter)?;
624
625            let max_voter_weight_record_data =
626                get_max_voter_weight_record_data_for_realm_and_governing_token_mint(
627                    &max_voter_weight_addin,
628                    max_voter_weight_record_info,
629                    realm,
630                    vote_governing_token_mint_info.key,
631                )?;
632
633            assert_is_valid_max_voter_weight(&max_voter_weight_record_data)?;
634
635            // When the max voter weight addin is used it's possible it can be inaccurate
636            // and we can have more votes then the max provided by the addin and
637            // we have to adjust it to whatever result is higher
638            return Ok(self.coerce_max_voter_weight(
639                max_voter_weight_record_data.max_voter_weight,
640                vote_kind,
641            ));
642        }
643
644        let vote_governing_token_mint_supply =
645            get_spl_token_mint_supply(vote_governing_token_mint_info)?;
646
647        let max_voter_weight = self.get_max_voter_weight_from_mint_supply(
648            realm_data,
649            vote_governing_token_mint_info.key,
650            vote_governing_token_mint_supply,
651            vote_kind,
652        )?;
653
654        Ok(max_voter_weight)
655    }
656
657    /// Checks if vote can be tipped and automatically transitioned to Succeeded
658    /// or Defeated state If the conditions are met the state is updated
659    /// accordingly
660    pub fn try_tip_vote(
661        &mut self,
662        max_voter_weight: u64,
663        vote_tipping: &VoteTipping,
664        current_unix_timestamp: UnixTimestamp,
665        vote_threshold: &VoteThreshold,
666        vote_kind: &VoteKind,
667    ) -> Result<bool, ProgramError> {
668        if let Some(tipped_state) = self.try_get_tipped_vote_state(
669            max_voter_weight,
670            vote_tipping,
671            vote_threshold,
672            vote_kind,
673        ) {
674            self.state = tipped_state;
675            self.voting_completed_at = Some(current_unix_timestamp);
676
677            // Capture vote params to correctly display historical results
678            // Note: For Veto vote the captured params are from the Veto config
679            self.max_vote_weight = Some(max_voter_weight);
680            self.vote_threshold = Some(vote_threshold.clone());
681
682            Ok(true)
683        } else {
684            Ok(false)
685        }
686    }
687
688    /// Checks if vote can be tipped and automatically transitioned to
689    /// Succeeded, Defeated or Vetoed state.
690    /// If yes then Some(ProposalState) is returned and None otherwise
691    pub fn try_get_tipped_vote_state(
692        &mut self,
693        max_voter_weight: u64,
694        vote_tipping: &VoteTipping,
695        vote_threshold: &VoteThreshold,
696        vote_kind: &VoteKind,
697    ) -> Option<ProposalState> {
698        let min_vote_threshold_weight =
699            get_min_vote_threshold_weight(vote_threshold, max_voter_weight).unwrap();
700
701        match vote_kind {
702            VoteKind::Electorate => self.try_get_tipped_electorate_vote_state(
703                max_voter_weight,
704                vote_tipping,
705                min_vote_threshold_weight,
706            ),
707            VoteKind::Veto => self.try_get_tipped_veto_vote_state(min_vote_threshold_weight),
708        }
709    }
710
711    /// Checks if Electorate vote can be tipped and automatically transitioned
712    /// to Succeeded or Defeated state.
713    /// If yes then Some(ProposalState) is returned and None otherwise
714    fn try_get_tipped_electorate_vote_state(
715        &mut self,
716        max_voter_weight: u64,
717        vote_tipping: &VoteTipping,
718        min_vote_threshold_weight: u64,
719    ) -> Option<ProposalState> {
720        // Vote tipping is currently supported for SingleChoice votes with
721        // single Yes and No (rejection) options only.
722        // Note: Tipping for multiple options (single choice and multiple
723        // choices) should be possible but it requires a great deal of
724        // considerations and I decided to fight it another day
725        if self.vote_type != VoteType::SingleChoice
726            // Tipping should not be allowed for opinion only proposals (surveys
727            // without rejection) to allow everybody's voice to be heard
728            || self.deny_vote_weight.is_none()
729            || self.options.len() != 1
730        {
731            return None;
732        };
733
734        let yes_option = &mut self.options[0];
735
736        let yes_vote_weight = yes_option.vote_weight;
737        let deny_vote_weight = self.deny_vote_weight.unwrap();
738
739        match vote_tipping {
740            VoteTipping::Disabled => {}
741            VoteTipping::Strict => {
742                if yes_vote_weight >= min_vote_threshold_weight
743                    && yes_vote_weight > (max_voter_weight.saturating_sub(yes_vote_weight))
744                {
745                    yes_option.vote_result = OptionVoteResult::Succeeded;
746                    return Some(ProposalState::Succeeded);
747                }
748            }
749            VoteTipping::Early => {
750                if yes_vote_weight >= min_vote_threshold_weight
751                    && yes_vote_weight > deny_vote_weight
752                {
753                    yes_option.vote_result = OptionVoteResult::Succeeded;
754                    return Some(ProposalState::Succeeded);
755                }
756            }
757        }
758
759        // If vote tipping isn't disabled entirely, allow a vote to complete as
760        // "defeated" if there is no possible way of reaching majority or the
761        // min_vote_threshold_weight for another option. This tipping is always
762        // strict, there's no equivalent to "early" tipping for deny votes.
763        if *vote_tipping != VoteTipping::Disabled
764            && (deny_vote_weight > (max_voter_weight.saturating_sub(min_vote_threshold_weight))
765                || deny_vote_weight >= (max_voter_weight.saturating_sub(deny_vote_weight)))
766        {
767            yes_option.vote_result = OptionVoteResult::Defeated;
768            return Some(ProposalState::Defeated);
769        }
770
771        None
772    }
773
774    /// Checks if vote can be tipped and transitioned to Vetoed state
775    /// If yes then Some(ProposalState::Vetoed) is returned and None otherwise
776    fn try_get_tipped_veto_vote_state(
777        &mut self,
778        min_vote_threshold_weight: u64,
779    ) -> Option<ProposalState> {
780        // Veto vote tips as soon as the required threshold is reached
781        // It's irrespectively of vote_tipping config because the outcome of the
782        // Proposal can't change any longer after being vetoed
783        if self.veto_vote_weight >= min_vote_threshold_weight {
784            // Note: Since we don't tip multi option votes all options vote_result would
785            // remain as None
786            Some(ProposalState::Vetoed)
787        } else {
788            None
789        }
790    }
791
792    /// Checks if Proposal can be canceled in the given state
793    pub fn assert_can_cancel(
794        &self,
795        config: &GovernanceConfig,
796        current_unix_timestamp: UnixTimestamp,
797    ) -> Result<(), ProgramError> {
798        match self.state {
799            ProposalState::Draft | ProposalState::SigningOff => Ok(()),
800            ProposalState::Voting => {
801                // Note: If there is no tipping point the proposal can be still in Voting state
802                // but already past the configured max_voting_time In that case
803                // we treat the proposal as finalized and it's no longer allowed to be canceled
804                if self.has_voting_max_time_ended(config, current_unix_timestamp) {
805                    return Err(GovernanceError::ProposalVotingTimeExpired.into());
806                }
807                Ok(())
808            }
809            ProposalState::Executing
810            | ProposalState::ExecutingWithErrors
811            | ProposalState::Completed
812            | ProposalState::Cancelled
813            | ProposalState::Succeeded
814            | ProposalState::Defeated
815            | ProposalState::Vetoed => {
816                Err(GovernanceError::InvalidStateCannotCancelProposal.into())
817            }
818        }
819    }
820
821    /// Checks if Instructions can be edited (inserted or removed) for the
822    /// Proposal in the given state It also asserts whether the Proposal is
823    /// executable (has the reject option)
824    pub fn assert_can_edit_instructions(&self) -> Result<(), ProgramError> {
825        if self.assert_is_draft_state().is_err() {
826            return Err(GovernanceError::InvalidStateCannotEditTransactions.into());
827        }
828
829        // For security purposes only proposals with the reject option can have
830        // executable instructions
831        if self.deny_vote_weight.is_none() {
832            return Err(GovernanceError::ProposalIsNotExecutable.into());
833        }
834
835        Ok(())
836    }
837
838    /// Checks if Instructions can be executed for the Proposal in the given
839    /// state
840    pub fn assert_can_execute_transaction(
841        &self,
842        proposal_transaction_data: &ProposalTransactionV2,
843        current_unix_timestamp: UnixTimestamp,
844    ) -> Result<(), ProgramError> {
845        match self.state {
846            ProposalState::Succeeded
847            | ProposalState::Executing
848            | ProposalState::ExecutingWithErrors => {}
849            ProposalState::Draft
850            | ProposalState::SigningOff
851            | ProposalState::Completed
852            | ProposalState::Voting
853            | ProposalState::Cancelled
854            | ProposalState::Defeated
855            | ProposalState::Vetoed => {
856                return Err(GovernanceError::InvalidStateCannotExecuteTransaction.into())
857            }
858        }
859
860        if self.options[proposal_transaction_data.option_index as usize].vote_result
861            != OptionVoteResult::Succeeded
862        {
863            return Err(GovernanceError::CannotExecuteDefeatedOption.into());
864        }
865
866        if self
867            .voting_completed_at
868            .unwrap()
869            .checked_add(proposal_transaction_data.hold_up_time as i64)
870            .unwrap()
871            >= current_unix_timestamp
872        {
873            return Err(GovernanceError::CannotExecuteTransactionWithinHoldUpTime.into());
874        }
875
876        if proposal_transaction_data.executed_at.is_some() {
877            return Err(GovernanceError::TransactionAlreadyExecuted.into());
878        }
879
880        Ok(())
881    }
882
883    /// Checks if the instruction can be flagged with error for the Proposal in
884    /// the given state
885    pub fn assert_can_flag_transaction_error(
886        &self,
887        proposal_transaction_data: &ProposalTransactionV2,
888        current_unix_timestamp: UnixTimestamp,
889    ) -> Result<(), ProgramError> {
890        // Instruction can be flagged for error only when it's eligible for execution
891        self.assert_can_execute_transaction(proposal_transaction_data, current_unix_timestamp)?;
892
893        if proposal_transaction_data.execution_status == TransactionExecutionStatus::Error {
894            return Err(GovernanceError::TransactionAlreadyFlaggedWithError.into());
895        }
896
897        Ok(())
898    }
899
900    /// Checks if Proposal with off-chain/manual actions can be transitioned to
901    /// Completed
902    pub fn assert_can_complete(&self) -> Result<(), ProgramError> {
903        // Proposal vote must be successful
904        if self.state != ProposalState::Succeeded {
905            return Err(GovernanceError::InvalidStateToCompleteProposal.into());
906        }
907
908        // There must be no on-chain executable actions
909        if self.options.iter().any(|o| o.transactions_count != 0) {
910            return Err(GovernanceError::InvalidStateToCompleteProposal.into());
911        }
912
913        Ok(())
914    }
915
916    /// Asserts the given vote is valid for the proposal
917    pub fn assert_valid_vote(&self, vote: &Vote) -> Result<(), ProgramError> {
918        match vote {
919            Vote::Approve(choices) => {
920                if self.options.len() != choices.len() {
921                    return Err(GovernanceError::InvalidNumberOfVoteChoices.into());
922                }
923
924                let mut choice_count = 0u16;
925                let mut total_choice_weight_percentage = 0u8;
926
927                for choice in choices {
928                    if choice.rank > 0 {
929                        return Err(GovernanceError::RankedVoteIsNotSupported.into());
930                    }
931
932                    if choice.weight_percentage > 0 {
933                        choice_count = choice_count.checked_add(1).unwrap();
934
935                        match self.vote_type {
936                            VoteType::MultiChoice {
937                                choice_type: MultiChoiceType::Weighted,
938                                min_voter_options: _,
939                                max_voter_options: _,
940                                max_winning_options: _,
941                            } => {
942                                // Calculate the total percentage for all choices for weighted
943                                // choice vote. The total must add up
944                                // to exactly 100%
945                                total_choice_weight_percentage = total_choice_weight_percentage
946                                    .checked_add(choice.weight_percentage)
947                                    .ok_or(GovernanceError::TotalVoteWeightMustBe100Percent)?;
948                            }
949                            _ => {
950                                if choice.weight_percentage != 100 {
951                                    return Err(
952                                        GovernanceError::ChoiceWeightMustBe100Percent.into()
953                                    );
954                                }
955                            }
956                        }
957                    }
958                }
959
960                match self.vote_type {
961                    VoteType::SingleChoice => {
962                        if choice_count != 1 {
963                            return Err(GovernanceError::SingleChoiceOnlyIsAllowed.into());
964                        }
965                    }
966                    VoteType::MultiChoice {
967                        choice_type: MultiChoiceType::FullWeight,
968                        min_voter_options: _,
969                        max_voter_options: _,
970                        max_winning_options: _,
971                    } => {
972                        if choice_count == 0 {
973                            return Err(GovernanceError::AtLeastSingleChoiceIsRequired.into());
974                        }
975                    }
976                    VoteType::MultiChoice {
977                        choice_type: MultiChoiceType::Weighted,
978                        min_voter_options: _,
979                        max_voter_options: _,
980                        max_winning_options: _,
981                    } => {
982                        if choice_count == 0 {
983                            return Err(GovernanceError::AtLeastSingleChoiceIsRequired.into());
984                        }
985                        if total_choice_weight_percentage != 100 {
986                            return Err(GovernanceError::TotalVoteWeightMustBe100Percent.into());
987                        }
988                    }
989                }
990            }
991            Vote::Deny => {
992                if self.deny_vote_weight.is_none() {
993                    return Err(GovernanceError::DenyVoteIsNotAllowed.into());
994                }
995            }
996            Vote::Abstain => {
997                return Err(GovernanceError::NotSupportedVoteType.into());
998            }
999            Vote::Veto => {}
1000        }
1001
1002        Ok(())
1003    }
1004
1005    /// Serializes account into the target buffer
1006    pub fn serialize<W: Write>(self, writer: W) -> Result<(), ProgramError> {
1007        if self.account_type == GovernanceAccountType::ProposalV2 {
1008            borsh::to_writer(writer, &self)?
1009        } else if self.account_type == GovernanceAccountType::ProposalV1 {
1010            // V1 account can't be resized and we have to translate it back to the original
1011            // format
1012
1013            if self.abstain_vote_weight.is_some() {
1014                panic!("ProposalV1 doesn't support Abstain vote")
1015            }
1016
1017            if self.veto_vote_weight > 0 {
1018                panic!("ProposalV1 doesn't support Veto vote")
1019            }
1020
1021            if self.start_voting_at.is_some() {
1022                panic!("ProposalV1 doesn't support start time")
1023            }
1024
1025            if self.max_voting_time.is_some() {
1026                panic!("ProposalV1 doesn't support max voting time")
1027            }
1028
1029            if self.options.len() != 1 {
1030                panic!("ProposalV1 doesn't support multiple options")
1031            }
1032
1033            let proposal_data_v1 = ProposalV1 {
1034                account_type: self.account_type,
1035                governance: self.governance,
1036                governing_token_mint: self.governing_token_mint,
1037                state: self.state,
1038                token_owner_record: self.token_owner_record,
1039                signatories_count: self.signatories_count,
1040                signatories_signed_off_count: self.signatories_signed_off_count,
1041                yes_votes_count: self.options[0].vote_weight,
1042                no_votes_count: self.deny_vote_weight.unwrap(),
1043                instructions_executed_count: self.options[0].transactions_executed_count,
1044                instructions_count: self.options[0].transactions_count,
1045                instructions_next_index: self.options[0].transactions_next_index,
1046                draft_at: self.draft_at,
1047                signing_off_at: self.signing_off_at,
1048                voting_at: self.voting_at,
1049                voting_at_slot: self.voting_at_slot,
1050                voting_completed_at: self.voting_completed_at,
1051                executing_at: self.executing_at,
1052                closed_at: self.closed_at,
1053                execution_flags: self.execution_flags,
1054                max_vote_weight: self.max_vote_weight,
1055                vote_threshold: self.vote_threshold,
1056                name: self.name,
1057                description_link: self.description_link,
1058            };
1059
1060            borsh::to_writer(writer, &proposal_data_v1)?
1061        }
1062
1063        Ok(())
1064    }
1065}
1066
1067/// Converts given vote threshold (ex. in percentages) to absolute vote weight
1068/// and returns the min weight required for a proposal option to pass
1069fn get_min_vote_threshold_weight(
1070    vote_threshold: &VoteThreshold,
1071    max_voter_weight: u64,
1072) -> Result<u64, ProgramError> {
1073    let yes_vote_threshold_percentage = match vote_threshold {
1074        VoteThreshold::YesVotePercentage(yes_vote_threshold_percentage) => {
1075            *yes_vote_threshold_percentage
1076        }
1077        _ => {
1078            return Err(GovernanceError::VoteThresholdTypeNotSupported.into());
1079        }
1080    };
1081
1082    let numerator = (yes_vote_threshold_percentage as u128)
1083        .checked_mul(max_voter_weight as u128)
1084        .unwrap();
1085
1086    let mut yes_vote_threshold = numerator.checked_div(100).unwrap();
1087
1088    if yes_vote_threshold.checked_mul(100).unwrap() < numerator {
1089        yes_vote_threshold = yes_vote_threshold.checked_add(1).unwrap();
1090    }
1091
1092    Ok(yes_vote_threshold as u64)
1093}
1094
1095/// Deserializes Proposal account and checks owner program
1096pub fn get_proposal_data(
1097    program_id: &Pubkey,
1098    proposal_info: &AccountInfo,
1099) -> Result<ProposalV2, ProgramError> {
1100    let account_type: GovernanceAccountType = get_account_type(program_id, proposal_info)?;
1101
1102    // If the account is V1 version then translate to V2
1103    if account_type == GovernanceAccountType::ProposalV1 {
1104        let proposal_data_v1 = get_account_data::<ProposalV1>(program_id, proposal_info)?;
1105
1106        let vote_result = match proposal_data_v1.state {
1107            ProposalState::Draft
1108            | ProposalState::SigningOff
1109            | ProposalState::Voting
1110            | ProposalState::Cancelled => OptionVoteResult::None,
1111            ProposalState::Succeeded
1112            | ProposalState::Executing
1113            | ProposalState::ExecutingWithErrors
1114            | ProposalState::Completed => OptionVoteResult::Succeeded,
1115            ProposalState::Vetoed | ProposalState::Defeated => OptionVoteResult::None,
1116        };
1117
1118        return Ok(ProposalV2 {
1119            account_type,
1120            governance: proposal_data_v1.governance,
1121            governing_token_mint: proposal_data_v1.governing_token_mint,
1122            state: proposal_data_v1.state,
1123            token_owner_record: proposal_data_v1.token_owner_record,
1124            signatories_count: proposal_data_v1.signatories_count,
1125            signatories_signed_off_count: proposal_data_v1.signatories_signed_off_count,
1126            vote_type: VoteType::SingleChoice,
1127            options: vec![ProposalOption {
1128                label: "Yes".to_string(),
1129                vote_weight: proposal_data_v1.yes_votes_count,
1130                vote_result,
1131                transactions_executed_count: proposal_data_v1.instructions_executed_count,
1132                transactions_count: proposal_data_v1.instructions_count,
1133                transactions_next_index: proposal_data_v1.instructions_next_index,
1134            }],
1135            deny_vote_weight: Some(proposal_data_v1.no_votes_count),
1136            veto_vote_weight: 0,
1137            abstain_vote_weight: None,
1138            start_voting_at: None,
1139            draft_at: proposal_data_v1.draft_at,
1140            signing_off_at: proposal_data_v1.signing_off_at,
1141            voting_at: proposal_data_v1.voting_at,
1142            voting_at_slot: proposal_data_v1.voting_at_slot,
1143            voting_completed_at: proposal_data_v1.voting_completed_at,
1144            executing_at: proposal_data_v1.executing_at,
1145            closed_at: proposal_data_v1.closed_at,
1146            execution_flags: proposal_data_v1.execution_flags,
1147            max_vote_weight: proposal_data_v1.max_vote_weight,
1148            max_voting_time: None,
1149            vote_threshold: proposal_data_v1.vote_threshold,
1150            name: proposal_data_v1.name,
1151            description_link: proposal_data_v1.description_link,
1152            reserved: [0; 64],
1153            reserved1: 0,
1154        });
1155    }
1156
1157    get_account_data::<ProposalV2>(program_id, proposal_info)
1158}
1159
1160/// Deserializes Proposal and validates it belongs to the given Governance and
1161/// governing_token_mint
1162pub fn get_proposal_data_for_governance_and_governing_mint(
1163    program_id: &Pubkey,
1164    proposal_info: &AccountInfo,
1165    governance: &Pubkey,
1166    governing_token_mint: &Pubkey,
1167) -> Result<ProposalV2, ProgramError> {
1168    let proposal_data = get_proposal_data_for_governance(program_id, proposal_info, governance)?;
1169
1170    if proposal_data.governing_token_mint != *governing_token_mint {
1171        return Err(GovernanceError::InvalidGoverningMintForProposal.into());
1172    }
1173
1174    Ok(proposal_data)
1175}
1176
1177/// Deserializes Proposal and validates it belongs to the given Governance
1178pub fn get_proposal_data_for_governance(
1179    program_id: &Pubkey,
1180    proposal_info: &AccountInfo,
1181    governance: &Pubkey,
1182) -> Result<ProposalV2, ProgramError> {
1183    let proposal_data = get_proposal_data(program_id, proposal_info)?;
1184
1185    if proposal_data.governance != *governance {
1186        return Err(GovernanceError::InvalidGovernanceForProposal.into());
1187    }
1188
1189    Ok(proposal_data)
1190}
1191
1192/// Returns Proposal PDA seeds
1193pub fn get_proposal_address_seeds<'a>(
1194    governance: &'a Pubkey,
1195    governing_token_mint: &'a Pubkey,
1196    proposal_seed: &'a Pubkey,
1197) -> [&'a [u8]; 4] {
1198    [
1199        PROGRAM_AUTHORITY_SEED,
1200        governance.as_ref(),
1201        governing_token_mint.as_ref(),
1202        proposal_seed.as_ref(),
1203    ]
1204}
1205
1206/// Returns Proposal PDA address
1207pub fn get_proposal_address<'a>(
1208    program_id: &Pubkey,
1209    governance: &'a Pubkey,
1210    governing_token_mint: &'a Pubkey,
1211    proposal_seed: &'a Pubkey,
1212) -> Pubkey {
1213    Pubkey::find_program_address(
1214        &get_proposal_address_seeds(governance, governing_token_mint, proposal_seed),
1215        program_id,
1216    )
1217    .0
1218}
1219
1220/// Assert options to create proposal are valid for the Proposal vote_type
1221pub fn assert_valid_proposal_options(
1222    options: &[String],
1223    vote_type: &VoteType,
1224) -> Result<(), ProgramError> {
1225    if options.is_empty() || options.len() > 10 {
1226        return Err(GovernanceError::InvalidProposalOptions.into());
1227    }
1228
1229    if let VoteType::MultiChoice {
1230        choice_type: _,
1231        min_voter_options,
1232        max_voter_options,
1233        max_winning_options,
1234    } = vote_type
1235    {
1236        if options.len() == 1
1237            || *max_voter_options as usize != options.len()
1238            || *max_winning_options as usize != options.len()
1239            || *min_voter_options != 1
1240        {
1241            return Err(GovernanceError::InvalidMultiChoiceProposalParameters.into());
1242        }
1243    }
1244
1245    // TODO: Check for duplicated option labels
1246    // The options are identified by index so it's ok for now
1247
1248    if options.iter().any(|o| o.is_empty()) {
1249        return Err(GovernanceError::InvalidProposalOptions.into());
1250    }
1251
1252    Ok(())
1253}
1254
1255#[cfg(test)]
1256mod test {
1257    use {
1258        super::*,
1259        crate::state::{
1260            enums::{MintMaxVoterWeightSource, VoteThreshold},
1261            legacy::ProposalV1,
1262            realm::RealmConfig,
1263            vote_record::VoteChoice,
1264        },
1265        proptest::prelude::*,
1266        solana_program::clock::Epoch,
1267    };
1268
1269    fn create_test_proposal() -> ProposalV2 {
1270        ProposalV2 {
1271            account_type: GovernanceAccountType::TokenOwnerRecordV2,
1272            governance: Pubkey::new_unique(),
1273            governing_token_mint: Pubkey::new_unique(),
1274            max_vote_weight: Some(10),
1275            state: ProposalState::Draft,
1276            token_owner_record: Pubkey::new_unique(),
1277            signatories_count: 10,
1278            signatories_signed_off_count: 5,
1279            description_link: "This is my description".to_string(),
1280            name: "This is my name".to_string(),
1281
1282            start_voting_at: Some(0),
1283            draft_at: 10,
1284            signing_off_at: Some(10),
1285
1286            voting_at: Some(10),
1287            voting_at_slot: Some(500),
1288
1289            voting_completed_at: Some(10),
1290            executing_at: Some(10),
1291            closed_at: Some(10),
1292
1293            vote_type: VoteType::SingleChoice,
1294            options: vec![ProposalOption {
1295                label: "yes".to_string(),
1296                vote_weight: 0,
1297                vote_result: OptionVoteResult::None,
1298                transactions_executed_count: 10,
1299                transactions_count: 10,
1300                transactions_next_index: 10,
1301            }],
1302            deny_vote_weight: Some(0),
1303            abstain_vote_weight: Some(0),
1304            veto_vote_weight: 0,
1305
1306            execution_flags: InstructionExecutionFlags::Ordered,
1307
1308            max_voting_time: Some(0),
1309            vote_threshold: Some(VoteThreshold::YesVotePercentage(100)),
1310
1311            reserved: [0; 64],
1312            reserved1: 0,
1313        }
1314    }
1315
1316    fn create_test_multi_option_proposal() -> ProposalV2 {
1317        let mut proposal = create_test_proposal();
1318        proposal.options = vec![
1319            ProposalOption {
1320                label: "option 1".to_string(),
1321                vote_weight: 0,
1322                vote_result: OptionVoteResult::None,
1323                transactions_executed_count: 10,
1324                transactions_count: 10,
1325                transactions_next_index: 10,
1326            },
1327            ProposalOption {
1328                label: "option 2".to_string(),
1329                vote_weight: 0,
1330                vote_result: OptionVoteResult::None,
1331                transactions_executed_count: 10,
1332                transactions_count: 10,
1333                transactions_next_index: 10,
1334            },
1335            ProposalOption {
1336                label: "option 3".to_string(),
1337                vote_weight: 0,
1338                vote_result: OptionVoteResult::None,
1339                transactions_executed_count: 10,
1340                transactions_count: 10,
1341                transactions_next_index: 10,
1342            },
1343        ];
1344
1345        proposal
1346    }
1347
1348    fn create_test_realm() -> RealmV2 {
1349        RealmV2 {
1350            account_type: GovernanceAccountType::RealmV2,
1351            community_mint: Pubkey::new_unique(),
1352            reserved: [0; 6],
1353
1354            authority: Some(Pubkey::new_unique()),
1355            name: "test-realm".to_string(),
1356            config: RealmConfig {
1357                council_mint: Some(Pubkey::new_unique()),
1358                reserved: [0; 6],
1359                legacy1: 0,
1360                legacy2: 0,
1361
1362                community_mint_max_voter_weight_source:
1363                    MintMaxVoterWeightSource::FULL_SUPPLY_FRACTION,
1364                min_community_weight_to_create_governance: 10,
1365            },
1366            legacy1: 0,
1367            reserved_v2: [0; 128],
1368        }
1369    }
1370
1371    fn create_test_governance_config() -> GovernanceConfig {
1372        GovernanceConfig {
1373            community_vote_threshold: VoteThreshold::YesVotePercentage(60),
1374            min_community_weight_to_create_proposal: 5,
1375            min_transaction_hold_up_time: 10,
1376            voting_base_time: 5,
1377            community_vote_tipping: VoteTipping::Strict,
1378            council_vote_threshold: VoteThreshold::YesVotePercentage(60),
1379            council_veto_vote_threshold: VoteThreshold::YesVotePercentage(50),
1380            min_council_weight_to_create_proposal: 1,
1381            council_vote_tipping: VoteTipping::Strict,
1382            community_veto_vote_threshold: VoteThreshold::YesVotePercentage(40),
1383            voting_cool_off_time: 0,
1384            deposit_exempt_proposal_count: 0,
1385        }
1386    }
1387
1388    #[test]
1389    fn test_max_size() {
1390        let mut proposal = create_test_proposal();
1391        proposal.vote_type = VoteType::MultiChoice {
1392            choice_type: MultiChoiceType::FullWeight,
1393            min_voter_options: 1,
1394            max_voter_options: 1,
1395            max_winning_options: 1,
1396        };
1397
1398        let size = proposal.try_to_vec().unwrap().len();
1399
1400        assert_eq!(proposal.get_max_size(), Some(size));
1401    }
1402
1403    #[test]
1404    fn test_multi_option_proposal_max_size() {
1405        let mut proposal = create_test_multi_option_proposal();
1406        proposal.vote_type = VoteType::MultiChoice {
1407            choice_type: MultiChoiceType::FullWeight,
1408            min_voter_options: 1,
1409            max_voter_options: 3,
1410            max_winning_options: 3,
1411        };
1412
1413        let size = proposal.try_to_vec().unwrap().len();
1414
1415        assert_eq!(proposal.get_max_size(), Some(size));
1416    }
1417
1418    prop_compose! {
1419        fn vote_results()(governing_token_supply in 1..=u64::MAX)(
1420            governing_token_supply in Just(governing_token_supply),
1421            vote_count in 0..=governing_token_supply,
1422        ) -> (u64, u64) {
1423            (vote_count, governing_token_supply)
1424        }
1425    }
1426
1427    fn editable_signatory_states() -> impl Strategy<Value = ProposalState> {
1428        prop_oneof![Just(ProposalState::Draft)]
1429    }
1430
1431    proptest! {
1432        #[test]
1433        fn test_assert_can_edit_signatories(state in editable_signatory_states()) {
1434
1435            let mut proposal = create_test_proposal();
1436            proposal.state = state;
1437            proposal.assert_can_edit_signatories().unwrap();
1438
1439        }
1440
1441    }
1442
1443    fn none_editable_signatory_states() -> impl Strategy<Value = ProposalState> {
1444        prop_oneof![
1445            Just(ProposalState::Voting),
1446            Just(ProposalState::Succeeded),
1447            Just(ProposalState::Executing),
1448            Just(ProposalState::ExecutingWithErrors),
1449            Just(ProposalState::Completed),
1450            Just(ProposalState::Cancelled),
1451            Just(ProposalState::Defeated),
1452            Just(ProposalState::Vetoed),
1453            Just(ProposalState::SigningOff),
1454        ]
1455    }
1456
1457    proptest! {
1458        #[test]
1459            fn test_assert_can_edit_signatories_with_invalid_state_error(state in none_editable_signatory_states()) {
1460                // Arrange
1461                let mut proposal = create_test_proposal();
1462                proposal.state = state;
1463
1464                // Act
1465                let err = proposal.assert_can_edit_signatories().err().unwrap();
1466
1467                // Assert
1468                assert_eq!(err, GovernanceError::InvalidStateCannotEditSignatories.into());
1469        }
1470
1471    }
1472
1473    fn sign_off_states() -> impl Strategy<Value = ProposalState> {
1474        prop_oneof![Just(ProposalState::SigningOff), Just(ProposalState::Draft),]
1475    }
1476    proptest! {
1477        #[test]
1478        fn test_assert_can_sign_off(state in sign_off_states()) {
1479            let mut proposal = create_test_proposal();
1480            proposal.state = state;
1481            proposal.assert_can_sign_off().unwrap();
1482        }
1483    }
1484
1485    fn none_sign_off_states() -> impl Strategy<Value = ProposalState> {
1486        prop_oneof![
1487            Just(ProposalState::Voting),
1488            Just(ProposalState::Succeeded),
1489            Just(ProposalState::Executing),
1490            Just(ProposalState::ExecutingWithErrors),
1491            Just(ProposalState::Completed),
1492            Just(ProposalState::Cancelled),
1493            Just(ProposalState::Defeated),
1494            Just(ProposalState::Vetoed),
1495        ]
1496    }
1497
1498    proptest! {
1499        #[test]
1500        fn test_assert_can_sign_off_with_state_error(state in none_sign_off_states()) {
1501                // Arrange
1502                let mut proposal = create_test_proposal();
1503                proposal.state = state;
1504
1505                // Act
1506                let err = proposal.assert_can_sign_off().err().unwrap();
1507
1508                // Assert
1509                assert_eq!(err, GovernanceError::InvalidStateCannotSignOff.into());
1510        }
1511    }
1512
1513    fn cancellable_states() -> impl Strategy<Value = ProposalState> {
1514        prop_oneof![
1515            Just(ProposalState::Draft),
1516            Just(ProposalState::SigningOff),
1517            Just(ProposalState::Voting),
1518        ]
1519    }
1520
1521    proptest! {
1522        #[test]
1523        fn test_assert_can_cancel(state in cancellable_states()) {
1524
1525            // Arrange
1526            let mut proposal = create_test_proposal();
1527            let governance_config = create_test_governance_config();
1528
1529            // Act
1530            proposal.state = state;
1531
1532            // Assert
1533            proposal.assert_can_cancel(&governance_config,1).unwrap();
1534
1535        }
1536
1537    }
1538
1539    fn none_cancellable_states() -> impl Strategy<Value = ProposalState> {
1540        prop_oneof![
1541            Just(ProposalState::Succeeded),
1542            Just(ProposalState::Executing),
1543            Just(ProposalState::ExecutingWithErrors),
1544            Just(ProposalState::Completed),
1545            Just(ProposalState::Cancelled),
1546            Just(ProposalState::Defeated),
1547            Just(ProposalState::Vetoed),
1548        ]
1549    }
1550
1551    proptest! {
1552        #[test]
1553            fn test_assert_can_cancel_with_invalid_state_error(state in none_cancellable_states()) {
1554                // Arrange
1555                let mut proposal = create_test_proposal();
1556                proposal.state = state;
1557
1558                let governance_config = create_test_governance_config();
1559
1560                // Act
1561                let err = proposal.assert_can_cancel(&governance_config,1).err().unwrap();
1562
1563                // Assert
1564                assert_eq!(err, GovernanceError::InvalidStateCannotCancelProposal.into());
1565        }
1566
1567    }
1568
1569    #[derive(Clone, Debug)]
1570    pub struct VoteCastTestCase {
1571        #[allow(dead_code)]
1572        name: &'static str,
1573        governing_token_supply: u64,
1574        yes_vote_threshold_percentage: u8,
1575        yes_votes_count: u64,
1576        no_votes_count: u64,
1577        expected_tipped_state: ProposalState,
1578        expected_finalized_state: ProposalState,
1579    }
1580
1581    fn vote_casting_test_cases() -> impl Strategy<Value = VoteCastTestCase> {
1582        prop_oneof![
1583            //  threshold < 50%
1584            Just(VoteCastTestCase {
1585                name: "45:10 @40 -- Nays can still outvote Yeahs",
1586                governing_token_supply: 100,
1587                yes_vote_threshold_percentage: 40,
1588                yes_votes_count: 45,
1589                no_votes_count: 10,
1590                expected_tipped_state: ProposalState::Voting,
1591                expected_finalized_state: ProposalState::Succeeded,
1592            }),
1593            Just(VoteCastTestCase {
1594                name: "49:50 @40 -- In best case scenario it can be 50:50 tie and hence Defeated",
1595                governing_token_supply: 100,
1596                yes_vote_threshold_percentage: 40,
1597                yes_votes_count: 49,
1598                no_votes_count: 50,
1599                expected_tipped_state: ProposalState::Defeated,
1600                expected_finalized_state: ProposalState::Defeated,
1601            }),
1602            Just(VoteCastTestCase {
1603                name: "40:40 @40 -- Still can go either way",
1604                governing_token_supply: 100,
1605                yes_vote_threshold_percentage: 40,
1606                yes_votes_count: 40,
1607                no_votes_count: 40,
1608                expected_tipped_state: ProposalState::Voting,
1609                expected_finalized_state: ProposalState::Defeated,
1610            }),
1611            Just(VoteCastTestCase {
1612                name: "45:45 @40 -- Still can go either way",
1613                governing_token_supply: 100,
1614                yes_vote_threshold_percentage: 40,
1615                yes_votes_count: 45,
1616                no_votes_count: 45,
1617                expected_tipped_state: ProposalState::Voting,
1618                expected_finalized_state: ProposalState::Defeated,
1619            }),
1620            Just(VoteCastTestCase {
1621                name: "50:10 @40 -- Nay sayers can still tie up",
1622                governing_token_supply: 100,
1623                yes_vote_threshold_percentage: 40,
1624                yes_votes_count: 50,
1625                no_votes_count: 10,
1626                expected_tipped_state: ProposalState::Voting,
1627                expected_finalized_state: ProposalState::Succeeded,
1628            }),
1629            Just(VoteCastTestCase {
1630                name: "50:50 @40 -- It's a tie and hence Defeated",
1631                governing_token_supply: 100,
1632                yes_vote_threshold_percentage: 40,
1633                yes_votes_count: 50,
1634                no_votes_count: 50,
1635                expected_tipped_state: ProposalState::Defeated,
1636                expected_finalized_state: ProposalState::Defeated,
1637            }),
1638            Just(VoteCastTestCase {
1639                name: "45:51 @ 40 -- Nays won",
1640                governing_token_supply: 100,
1641                yes_vote_threshold_percentage: 40,
1642                yes_votes_count: 45,
1643                no_votes_count: 51,
1644                expected_tipped_state: ProposalState::Defeated,
1645                expected_finalized_state: ProposalState::Defeated,
1646            }),
1647            Just(VoteCastTestCase {
1648                name: "40:55 @ 40 -- Nays won",
1649                governing_token_supply: 100,
1650                yes_vote_threshold_percentage: 40,
1651                yes_votes_count: 40,
1652                no_votes_count: 55,
1653                expected_tipped_state: ProposalState::Defeated,
1654                expected_finalized_state: ProposalState::Defeated,
1655            }),
1656            // threshold == 50%
1657            Just(VoteCastTestCase {
1658                name: "50:10 @50 -- +1 tie breaker required to tip",
1659                governing_token_supply: 100,
1660                yes_vote_threshold_percentage: 50,
1661                yes_votes_count: 50,
1662                no_votes_count: 10,
1663                expected_tipped_state: ProposalState::Voting,
1664                expected_finalized_state: ProposalState::Succeeded,
1665            }),
1666            Just(VoteCastTestCase {
1667                name: "10:50 @50 -- +1 tie breaker vote not possible any longer",
1668                governing_token_supply: 100,
1669                yes_vote_threshold_percentage: 50,
1670                yes_votes_count: 10,
1671                no_votes_count: 50,
1672                expected_tipped_state: ProposalState::Defeated,
1673                expected_finalized_state: ProposalState::Defeated,
1674            }),
1675            Just(VoteCastTestCase {
1676                name: "50:50 @50 -- +1 tie breaker vote not possible any longer",
1677                governing_token_supply: 100,
1678                yes_vote_threshold_percentage: 50,
1679                yes_votes_count: 50,
1680                no_votes_count: 50,
1681                expected_tipped_state: ProposalState::Defeated,
1682                expected_finalized_state: ProposalState::Defeated,
1683            }),
1684            Just(VoteCastTestCase {
1685                name: "51:10 @ 50 -- Nay sayers can't outvote any longer",
1686                governing_token_supply: 100,
1687                yes_vote_threshold_percentage: 50,
1688                yes_votes_count: 51,
1689                no_votes_count: 10,
1690                expected_tipped_state: ProposalState::Succeeded,
1691                expected_finalized_state: ProposalState::Succeeded,
1692            }),
1693            Just(VoteCastTestCase {
1694                name: "10:51 @ 50 -- Nays won",
1695                governing_token_supply: 100,
1696                yes_vote_threshold_percentage: 50,
1697                yes_votes_count: 10,
1698                no_votes_count: 51,
1699                expected_tipped_state: ProposalState::Defeated,
1700                expected_finalized_state: ProposalState::Defeated,
1701            }),
1702            // threshold > 50%
1703            Just(VoteCastTestCase {
1704                name: "10:10 @ 60 -- Can still go either way",
1705                governing_token_supply: 100,
1706                yes_vote_threshold_percentage: 60,
1707                yes_votes_count: 10,
1708                no_votes_count: 10,
1709                expected_tipped_state: ProposalState::Voting,
1710                expected_finalized_state: ProposalState::Defeated,
1711            }),
1712            Just(VoteCastTestCase {
1713                name: "55:10 @ 60 -- Can still go either way",
1714                governing_token_supply: 100,
1715                yes_vote_threshold_percentage: 60,
1716                yes_votes_count: 55,
1717                no_votes_count: 10,
1718                expected_tipped_state: ProposalState::Voting,
1719                expected_finalized_state: ProposalState::Defeated,
1720            }),
1721            Just(VoteCastTestCase {
1722                name: "60:10 @ 60 -- Yeah reached the required threshold",
1723                governing_token_supply: 100,
1724                yes_vote_threshold_percentage: 60,
1725                yes_votes_count: 60,
1726                no_votes_count: 10,
1727                expected_tipped_state: ProposalState::Succeeded,
1728                expected_finalized_state: ProposalState::Succeeded,
1729            }),
1730            Just(VoteCastTestCase {
1731                name: "61:10 @ 60 -- Yeah won",
1732                governing_token_supply: 100,
1733                yes_vote_threshold_percentage: 60,
1734                yes_votes_count: 61,
1735                no_votes_count: 10,
1736                expected_tipped_state: ProposalState::Succeeded,
1737                expected_finalized_state: ProposalState::Succeeded,
1738            }),
1739            Just(VoteCastTestCase {
1740                name: "10:40 @ 60 -- Yeah can still outvote Nay",
1741                governing_token_supply: 100,
1742                yes_vote_threshold_percentage: 60,
1743                yes_votes_count: 10,
1744                no_votes_count: 40,
1745                expected_tipped_state: ProposalState::Voting,
1746                expected_finalized_state: ProposalState::Defeated,
1747            }),
1748            Just(VoteCastTestCase {
1749                name: "60:40 @ 60 -- Yeah won",
1750                governing_token_supply: 100,
1751                yes_vote_threshold_percentage: 60,
1752                yes_votes_count: 60,
1753                no_votes_count: 40,
1754                expected_tipped_state: ProposalState::Succeeded,
1755                expected_finalized_state: ProposalState::Succeeded,
1756            }),
1757            Just(VoteCastTestCase {
1758                name: "10:41 @ 60 -- Aye can't outvote Nay any longer",
1759                governing_token_supply: 100,
1760                yes_vote_threshold_percentage: 60,
1761                yes_votes_count: 10,
1762                no_votes_count: 41,
1763                expected_tipped_state: ProposalState::Defeated,
1764                expected_finalized_state: ProposalState::Defeated,
1765            }),
1766            Just(VoteCastTestCase {
1767                name: "100:0",
1768                governing_token_supply: 100,
1769                yes_vote_threshold_percentage: 100,
1770                yes_votes_count: 100,
1771                no_votes_count: 0,
1772                expected_tipped_state: ProposalState::Succeeded,
1773                expected_finalized_state: ProposalState::Succeeded,
1774            }),
1775            Just(VoteCastTestCase {
1776                name: "0:100",
1777                governing_token_supply: 100,
1778                yes_vote_threshold_percentage: 100,
1779                yes_votes_count: 0,
1780                no_votes_count: 100,
1781                expected_tipped_state: ProposalState::Defeated,
1782                expected_finalized_state: ProposalState::Defeated,
1783            }),
1784        ]
1785    }
1786
1787    proptest! {
1788        #[test]
1789        fn test_try_tip_vote(test_case in vote_casting_test_cases()) {
1790            // Arrange
1791            let mut proposal = create_test_proposal();
1792
1793           proposal.options[0].vote_weight = test_case.yes_votes_count;
1794           proposal.deny_vote_weight = Some(test_case.no_votes_count);
1795
1796            proposal.state = ProposalState::Voting;
1797
1798
1799            let current_timestamp = 15_i64;
1800
1801            let realm = create_test_realm();
1802            let governing_token_mint = proposal.governing_token_mint;
1803            let vote_kind = VoteKind::Electorate;
1804            let vote_tipping = VoteTipping::Strict;
1805
1806            let max_voter_weight = proposal.get_max_voter_weight_from_mint_supply(&realm,&governing_token_mint, test_case.governing_token_supply,&vote_kind).unwrap();
1807            let vote_threshold = VoteThreshold::YesVotePercentage(test_case.yes_vote_threshold_percentage);
1808
1809
1810
1811            // Act
1812            proposal.try_tip_vote(max_voter_weight, &vote_tipping,current_timestamp,&vote_threshold,&vote_kind).unwrap();
1813
1814            // Assert
1815            assert_eq!(proposal.state,test_case.expected_tipped_state,"CASE: {:?}",test_case);
1816
1817            if test_case.expected_tipped_state != ProposalState::Voting {
1818                assert_eq!(Some(current_timestamp),proposal.voting_completed_at);
1819
1820            }
1821
1822            match proposal.options[0].vote_result {
1823                OptionVoteResult::Succeeded => {
1824                    assert_eq!(ProposalState::Succeeded,test_case.expected_tipped_state)
1825                },
1826                OptionVoteResult::Defeated => {
1827                    assert_eq!(ProposalState::Defeated,test_case.expected_tipped_state)
1828                },
1829                OptionVoteResult::None =>  {
1830                    assert_eq!(ProposalState::Voting,test_case.expected_tipped_state)
1831                },
1832            };
1833
1834        }
1835
1836        #[test]
1837        fn test_finalize_vote(test_case in vote_casting_test_cases()) {
1838            // Arrange
1839            let mut proposal = create_test_proposal();
1840
1841            proposal.options[0].vote_weight = test_case.yes_votes_count;
1842            proposal.deny_vote_weight = Some(test_case.no_votes_count);
1843
1844            proposal.state = ProposalState::Voting;
1845
1846            let governance_config = create_test_governance_config();
1847
1848            let current_timestamp = 16_i64;
1849
1850            let realm = create_test_realm();
1851            let governing_token_mint = proposal.governing_token_mint;
1852            let vote_kind = VoteKind::Electorate;
1853
1854            let max_voter_weight = proposal.get_max_voter_weight_from_mint_supply(&realm,&governing_token_mint,test_case.governing_token_supply,&vote_kind).unwrap();
1855            let vote_threshold = VoteThreshold::YesVotePercentage(test_case.yes_vote_threshold_percentage);
1856
1857            // Act
1858            proposal.finalize_vote(max_voter_weight, &governance_config,current_timestamp,&vote_threshold).unwrap();
1859
1860            // Assert
1861            assert_eq!(proposal.state,test_case.expected_finalized_state,"CASE: {:?}",test_case);
1862            assert_eq!(
1863                Some(proposal.voting_max_time_end(&governance_config)),
1864                proposal.voting_completed_at
1865            );
1866
1867            match proposal.options[0].vote_result {
1868                OptionVoteResult::Succeeded => {
1869                    assert_eq!(ProposalState::Succeeded,test_case.expected_finalized_state)
1870                },
1871                OptionVoteResult::Defeated => {
1872                    assert_eq!(ProposalState::Defeated,test_case.expected_finalized_state)
1873                },
1874                OptionVoteResult::None =>  {
1875                    panic!("Option result must be resolved for finalized vote")
1876                },
1877            };
1878
1879        }
1880    }
1881
1882    prop_compose! {
1883        fn full_vote_results()(governing_token_supply in 1..=u64::MAX, yes_vote_threshold in 1..100)(
1884            governing_token_supply in Just(governing_token_supply),
1885            yes_vote_threshold in Just(yes_vote_threshold),
1886
1887            yes_votes_count in 0..=governing_token_supply,
1888            no_votes_count in 0..=governing_token_supply,
1889
1890        ) -> (u64, u64, u64, u8) {
1891            (yes_votes_count, no_votes_count, governing_token_supply, yes_vote_threshold as u8)
1892        }
1893    }
1894
1895    proptest! {
1896        #[test]
1897        fn test_try_tip_vote_with_full_vote_results(
1898            (yes_votes_count, no_votes_count, governing_token_supply, yes_vote_threshold_percentage) in full_vote_results(),
1899
1900        ) {
1901            // Arrange
1902
1903            let mut proposal = create_test_proposal();
1904
1905            proposal.options[0].vote_weight = yes_votes_count;
1906            proposal.deny_vote_weight = Some(no_votes_count.min(governing_token_supply-yes_votes_count));
1907
1908
1909            proposal.state = ProposalState::Voting;
1910
1911
1912
1913            let  yes_vote_threshold_percentage = VoteThreshold::YesVotePercentage(yes_vote_threshold_percentage);
1914
1915            let current_timestamp = 15_i64;
1916
1917            let realm = create_test_realm();
1918            let governing_token_mint = proposal.governing_token_mint;
1919            let vote_kind = VoteKind::Electorate;
1920            let vote_tipping = VoteTipping::Strict;
1921
1922            let max_voter_weight = proposal.get_max_voter_weight_from_mint_supply(&realm,&governing_token_mint,governing_token_supply,&vote_kind).unwrap();
1923
1924            // Act
1925            proposal.try_tip_vote(max_voter_weight, &vote_tipping, current_timestamp,&yes_vote_threshold_percentage,&vote_kind).unwrap();
1926
1927            // Assert
1928            let yes_vote_threshold_count = get_min_vote_threshold_weight(&yes_vote_threshold_percentage,governing_token_supply).unwrap();
1929
1930            let no_vote_weight = proposal.deny_vote_weight.unwrap();
1931
1932            if yes_votes_count >= yes_vote_threshold_count && yes_votes_count > (governing_token_supply - yes_votes_count)
1933            {
1934                assert_eq!(proposal.state,ProposalState::Succeeded);
1935            } else if no_vote_weight > (governing_token_supply - yes_vote_threshold_count)
1936                || no_vote_weight >= (governing_token_supply - no_vote_weight ) {
1937                assert_eq!(proposal.state,ProposalState::Defeated);
1938            } else {
1939                assert_eq!(proposal.state,ProposalState::Voting);
1940            }
1941        }
1942    }
1943
1944    proptest! {
1945        #[test]
1946        fn test_finalize_vote_with_full_vote_results(
1947            (yes_votes_count, no_votes_count, governing_token_supply, yes_vote_threshold_percentage) in full_vote_results(),
1948
1949        ) {
1950            // Arrange
1951            let mut proposal = create_test_proposal();
1952
1953            proposal.options[0].vote_weight = yes_votes_count;
1954            proposal.deny_vote_weight = Some(no_votes_count.min(governing_token_supply-yes_votes_count));
1955
1956            proposal.state = ProposalState::Voting;
1957
1958
1959            let governance_config = create_test_governance_config();
1960            let  yes_vote_threshold_percentage = VoteThreshold::YesVotePercentage(yes_vote_threshold_percentage);
1961
1962
1963            let current_timestamp = 16_i64;
1964
1965            let realm = create_test_realm();
1966            let governing_token_mint = proposal.governing_token_mint;
1967            let vote_kind = VoteKind::Electorate;
1968
1969            let max_voter_weight = proposal.get_max_voter_weight_from_mint_supply(&realm,&governing_token_mint,governing_token_supply,&vote_kind).unwrap();
1970
1971            // Act
1972            proposal.finalize_vote(max_voter_weight, &governance_config,current_timestamp, &yes_vote_threshold_percentage).unwrap();
1973
1974            // Assert
1975            let no_vote_weight = proposal.deny_vote_weight.unwrap();
1976
1977            let yes_vote_threshold_count = get_min_vote_threshold_weight(&yes_vote_threshold_percentage,governing_token_supply).unwrap();
1978
1979            if yes_votes_count >= yes_vote_threshold_count &&  yes_votes_count > no_vote_weight
1980            {
1981                assert_eq!(proposal.state,ProposalState::Succeeded);
1982            } else {
1983                assert_eq!(proposal.state,ProposalState::Defeated);
1984            }
1985        }
1986    }
1987
1988    #[test]
1989    fn test_try_tip_vote_with_reduced_community_mint_max_vote_weight() {
1990        // Arrange
1991        let mut proposal = create_test_proposal();
1992
1993        proposal.options[0].vote_weight = 60;
1994        proposal.deny_vote_weight = Some(10);
1995
1996        proposal.state = ProposalState::Voting;
1997
1998        let current_timestamp = 15_i64;
1999
2000        let community_token_supply = 200;
2001
2002        let mut realm = create_test_realm();
2003        let governing_token_mint = proposal.governing_token_mint;
2004        let vote_kind = VoteKind::Electorate;
2005        let vote_tipping = VoteTipping::Strict;
2006
2007        // reduce max vote weight to 100
2008        realm.config.community_mint_max_voter_weight_source =
2009            MintMaxVoterWeightSource::SupplyFraction(
2010                MintMaxVoterWeightSource::SUPPLY_FRACTION_BASE / 2,
2011            );
2012
2013        let max_voter_weight = proposal
2014            .get_max_voter_weight_from_mint_supply(
2015                &realm,
2016                &governing_token_mint,
2017                community_token_supply,
2018                &vote_kind,
2019            )
2020            .unwrap();
2021
2022        let vote_threshold = &VoteThreshold::YesVotePercentage(60);
2023        let vote_kind = VoteKind::Electorate;
2024
2025        // Act
2026        proposal
2027            .try_tip_vote(
2028                max_voter_weight,
2029                &vote_tipping,
2030                current_timestamp,
2031                vote_threshold,
2032                &vote_kind,
2033            )
2034            .unwrap();
2035
2036        // Assert
2037        assert_eq!(proposal.state, ProposalState::Succeeded);
2038        assert_eq!(proposal.max_vote_weight, Some(100));
2039    }
2040
2041    #[test]
2042    fn test_try_tip_vote_with_reduced_absolute_community_mint_max_vote_weight() {
2043        // Arrange
2044        let mut proposal = create_test_proposal();
2045
2046        proposal.options[0].vote_weight = 60;
2047        proposal.deny_vote_weight = Some(10);
2048
2049        proposal.state = ProposalState::Voting;
2050
2051        let current_timestamp = 15_i64;
2052
2053        let community_token_supply = 200;
2054
2055        let mut realm = create_test_realm();
2056        let governing_token_mint = proposal.governing_token_mint;
2057        let vote_kind = VoteKind::Electorate;
2058        let vote_tipping = VoteTipping::Strict;
2059
2060        // set max vote weight to 100
2061        realm.config.community_mint_max_voter_weight_source =
2062            MintMaxVoterWeightSource::Absolute(community_token_supply / 2);
2063
2064        let max_voter_weight = proposal
2065            .get_max_voter_weight_from_mint_supply(
2066                &realm,
2067                &governing_token_mint,
2068                community_token_supply,
2069                &vote_kind,
2070            )
2071            .unwrap();
2072
2073        let vote_threshold = &VoteThreshold::YesVotePercentage(60);
2074        let vote_kind = VoteKind::Electorate;
2075
2076        // Act
2077        proposal
2078            .try_tip_vote(
2079                max_voter_weight,
2080                &vote_tipping,
2081                current_timestamp,
2082                vote_threshold,
2083                &vote_kind,
2084            )
2085            .unwrap();
2086
2087        // Assert
2088        assert_eq!(proposal.state, ProposalState::Succeeded);
2089        assert_eq!(proposal.max_vote_weight, Some(100));
2090    }
2091
2092    #[test]
2093    fn test_try_tip_vote_with_reduced_community_mint_max_vote_weight_and_vote_overflow() {
2094        // Arrange
2095        let mut proposal = create_test_proposal();
2096
2097        // no vote weight
2098        proposal.deny_vote_weight = Some(10);
2099
2100        proposal.state = ProposalState::Voting;
2101
2102        let current_timestamp = 15_i64;
2103
2104        let community_token_supply = 200;
2105
2106        let mut realm = create_test_realm();
2107        let governing_token_mint = proposal.governing_token_mint;
2108        let vote_kind = VoteKind::Electorate;
2109        let vote_tipping = VoteTipping::Strict;
2110
2111        // reduce max vote weight to 100
2112        realm.config.community_mint_max_voter_weight_source =
2113            MintMaxVoterWeightSource::SupplyFraction(
2114                MintMaxVoterWeightSource::SUPPLY_FRACTION_BASE / 2,
2115            );
2116
2117        // vote above reduced supply
2118        // Yes vote weight
2119        proposal.options[0].vote_weight = 120;
2120
2121        let max_voter_weight = proposal
2122            .get_max_voter_weight_from_mint_supply(
2123                &realm,
2124                &governing_token_mint,
2125                community_token_supply,
2126                &vote_kind,
2127            )
2128            .unwrap();
2129
2130        let vote_threshold = VoteThreshold::YesVotePercentage(60);
2131
2132        // Act
2133        proposal
2134            .try_tip_vote(
2135                max_voter_weight,
2136                &vote_tipping,
2137                current_timestamp,
2138                &vote_threshold,
2139                &vote_kind,
2140            )
2141            .unwrap();
2142
2143        // Assert
2144        assert_eq!(proposal.state, ProposalState::Succeeded);
2145        assert_eq!(proposal.max_vote_weight, Some(130));
2146    }
2147
2148    #[test]
2149    fn test_try_tip_vote_with_reduced_absolute_mint_max_vote_weight_and_vote_overflow() {
2150        // Arrange
2151        let mut proposal = create_test_proposal();
2152
2153        // no vote weight
2154        proposal.deny_vote_weight = Some(10);
2155
2156        proposal.state = ProposalState::Voting;
2157
2158        let current_timestamp = 15_i64;
2159
2160        let community_token_supply = 200;
2161
2162        let mut realm = create_test_realm();
2163        let governing_token_mint = proposal.governing_token_mint;
2164        let vote_kind = VoteKind::Electorate;
2165        let vote_tipping = VoteTipping::Strict;
2166
2167        // reduce max vote weight to 100
2168        realm.config.community_mint_max_voter_weight_source =
2169            MintMaxVoterWeightSource::Absolute(community_token_supply / 2);
2170
2171        // vote above reduced supply
2172        // Yes vote weight
2173        proposal.options[0].vote_weight = 120;
2174
2175        let max_voter_weight = proposal
2176            .get_max_voter_weight_from_mint_supply(
2177                &realm,
2178                &governing_token_mint,
2179                community_token_supply,
2180                &vote_kind,
2181            )
2182            .unwrap();
2183
2184        let vote_threshold = VoteThreshold::YesVotePercentage(60);
2185
2186        // Act
2187        proposal
2188            .try_tip_vote(
2189                max_voter_weight,
2190                &vote_tipping,
2191                current_timestamp,
2192                &vote_threshold,
2193                &vote_kind,
2194            )
2195            .unwrap();
2196
2197        // Assert
2198        assert_eq!(proposal.state, ProposalState::Succeeded);
2199        assert_eq!(proposal.max_vote_weight, Some(130)); // Deny Vote 10 +
2200                                                         // Approve Vote 120
2201    }
2202
2203    #[test]
2204    fn test_try_tip_vote_for_council_vote_with_reduced_community_mint_max_vote_weight() {
2205        // Arrange
2206        let mut proposal = create_test_proposal();
2207
2208        proposal.options[0].vote_weight = 60;
2209        proposal.deny_vote_weight = Some(10);
2210
2211        proposal.state = ProposalState::Voting;
2212
2213        let current_timestamp = 15_i64;
2214
2215        let community_token_supply = 200;
2216
2217        let mut realm = create_test_realm();
2218        let governing_token_mint = proposal.governing_token_mint;
2219        let vote_kind = VoteKind::Electorate;
2220        let vote_tipping = VoteTipping::Strict;
2221
2222        realm.config.community_mint_max_voter_weight_source =
2223            MintMaxVoterWeightSource::SupplyFraction(
2224                MintMaxVoterWeightSource::SUPPLY_FRACTION_BASE / 2,
2225            );
2226        realm.config.council_mint = Some(proposal.governing_token_mint);
2227
2228        let max_voter_weight = proposal
2229            .get_max_voter_weight_from_mint_supply(
2230                &realm,
2231                &governing_token_mint,
2232                community_token_supply,
2233                &vote_kind,
2234            )
2235            .unwrap();
2236
2237        let vote_threshold = VoteThreshold::YesVotePercentage(60);
2238
2239        // Act
2240        proposal
2241            .try_tip_vote(
2242                max_voter_weight,
2243                &vote_tipping,
2244                current_timestamp,
2245                &vote_threshold,
2246                &vote_kind,
2247            )
2248            .unwrap();
2249
2250        // Assert
2251        assert_eq!(proposal.state, ProposalState::Voting);
2252    }
2253
2254    #[test]
2255    fn test_finalize_vote_with_reduced_community_mint_max_vote_weight() {
2256        // Arrange
2257        let mut proposal = create_test_proposal();
2258
2259        proposal.options[0].vote_weight = 60;
2260        proposal.deny_vote_weight = Some(10);
2261
2262        proposal.state = ProposalState::Voting;
2263
2264        let governance_config = create_test_governance_config();
2265
2266        let current_timestamp = 16_i64;
2267        let community_token_supply = 200;
2268
2269        let mut realm = create_test_realm();
2270        let governing_token_mint = proposal.governing_token_mint;
2271        let vote_kind = VoteKind::Electorate;
2272
2273        // reduce max vote weight to 100
2274        realm.config.community_mint_max_voter_weight_source =
2275            MintMaxVoterWeightSource::SupplyFraction(
2276                MintMaxVoterWeightSource::SUPPLY_FRACTION_BASE / 2,
2277            );
2278
2279        let max_voter_weight = proposal
2280            .get_max_voter_weight_from_mint_supply(
2281                &realm,
2282                &governing_token_mint,
2283                community_token_supply,
2284                &vote_kind,
2285            )
2286            .unwrap();
2287
2288        let vote_threshold = VoteThreshold::YesVotePercentage(60);
2289
2290        // Act
2291        proposal
2292            .finalize_vote(
2293                max_voter_weight,
2294                &governance_config,
2295                current_timestamp,
2296                &vote_threshold,
2297            )
2298            .unwrap();
2299
2300        // Assert
2301        assert_eq!(proposal.state, ProposalState::Succeeded);
2302        assert_eq!(proposal.max_vote_weight, Some(100));
2303    }
2304
2305    #[test]
2306    fn test_finalize_vote_with_reduced_community_mint_max_vote_weight_and_vote_overflow() {
2307        // Arrange
2308        let mut proposal = create_test_proposal();
2309
2310        proposal.options[0].vote_weight = 60;
2311        proposal.deny_vote_weight = Some(10);
2312
2313        proposal.state = ProposalState::Voting;
2314
2315        let governance_config = create_test_governance_config();
2316
2317        let current_timestamp = 16_i64;
2318        let community_token_supply = 200;
2319
2320        let mut realm = create_test_realm();
2321        let governing_token_mint = proposal.governing_token_mint;
2322        let vote_kind = VoteKind::Electorate;
2323
2324        // reduce max vote weight to 100
2325        realm.config.community_mint_max_voter_weight_source =
2326            MintMaxVoterWeightSource::SupplyFraction(
2327                MintMaxVoterWeightSource::SUPPLY_FRACTION_BASE / 2,
2328            );
2329
2330        // vote above reduced supply
2331        proposal.options[0].vote_weight = 120;
2332
2333        let max_voter_weight = proposal
2334            .get_max_voter_weight_from_mint_supply(
2335                &realm,
2336                &governing_token_mint,
2337                community_token_supply,
2338                &vote_kind,
2339            )
2340            .unwrap();
2341
2342        let vote_threshold = VoteThreshold::YesVotePercentage(60);
2343
2344        // Act
2345        proposal
2346            .finalize_vote(
2347                max_voter_weight,
2348                &governance_config,
2349                current_timestamp,
2350                &vote_threshold,
2351            )
2352            .unwrap();
2353
2354        // Assert
2355        assert_eq!(proposal.state, ProposalState::Succeeded);
2356        assert_eq!(proposal.max_vote_weight, Some(130));
2357    }
2358
2359    #[test]
2360    pub fn test_finalize_vote_with_expired_voting_time_error() {
2361        // Arrange
2362        let mut proposal = create_test_proposal();
2363        proposal.state = ProposalState::Voting;
2364        let governance_config = create_test_governance_config();
2365
2366        let current_timestamp =
2367            proposal.voting_at.unwrap() + governance_config.voting_base_time as i64;
2368
2369        let realm = create_test_realm();
2370        let governing_token_mint = proposal.governing_token_mint;
2371        let vote_kind = VoteKind::Electorate;
2372
2373        let max_voter_weight = proposal
2374            .get_max_voter_weight_from_mint_supply(&realm, &governing_token_mint, 100, &vote_kind)
2375            .unwrap();
2376
2377        let vote_threshold = &governance_config.community_vote_threshold;
2378
2379        // Act
2380        let err = proposal
2381            .finalize_vote(
2382                max_voter_weight,
2383                &governance_config,
2384                current_timestamp,
2385                vote_threshold,
2386            )
2387            .err()
2388            .unwrap();
2389
2390        // Assert
2391        assert_eq!(err, GovernanceError::CannotFinalizeVotingInProgress.into());
2392    }
2393
2394    #[test]
2395    pub fn test_finalize_vote_after_voting_time() {
2396        // Arrange
2397        let mut proposal = create_test_proposal();
2398        proposal.state = ProposalState::Voting;
2399        let governance_config = create_test_governance_config();
2400
2401        let current_timestamp =
2402            proposal.voting_at.unwrap() + governance_config.voting_base_time as i64 + 1;
2403
2404        let realm = create_test_realm();
2405        let governing_token_mint = proposal.governing_token_mint;
2406        let vote_kind = VoteKind::Electorate;
2407
2408        let max_voter_weight = proposal
2409            .get_max_voter_weight_from_mint_supply(&realm, &governing_token_mint, 100, &vote_kind)
2410            .unwrap();
2411
2412        let vote_threshold = &governance_config.community_vote_threshold;
2413
2414        // Act
2415        let result = proposal.finalize_vote(
2416            max_voter_weight,
2417            &governance_config,
2418            current_timestamp,
2419            vote_threshold,
2420        );
2421
2422        // Assert
2423        assert_eq!(result, Ok(()));
2424    }
2425
2426    #[test]
2427    pub fn test_assert_can_vote_with_expired_voting_time_error() {
2428        // Arrange
2429        let mut proposal = create_test_proposal();
2430        proposal.state = ProposalState::Voting;
2431        let governance_config = create_test_governance_config();
2432
2433        let current_timestamp =
2434            proposal.voting_at.unwrap() + governance_config.voting_base_time as i64 + 1;
2435
2436        let vote = Vote::Approve(vec![]);
2437
2438        // Act
2439        let err = proposal
2440            .assert_can_cast_vote(&governance_config, &vote, current_timestamp)
2441            .err()
2442            .unwrap();
2443
2444        // Assert
2445        assert_eq!(err, GovernanceError::ProposalVotingTimeExpired.into());
2446    }
2447
2448    #[test]
2449    pub fn test_assert_can_vote_within_voting_time() {
2450        // Arrange
2451        let mut proposal = create_test_proposal();
2452        proposal.state = ProposalState::Voting;
2453        let governance_config = create_test_governance_config();
2454
2455        let current_timestamp =
2456            proposal.voting_at.unwrap() + governance_config.voting_base_time as i64;
2457
2458        let vote = Vote::Approve(vec![]);
2459
2460        // Act
2461        let result = proposal.assert_can_cast_vote(&governance_config, &vote, current_timestamp);
2462
2463        // Assert
2464        assert_eq!(result, Ok(()));
2465    }
2466
2467    #[test]
2468    pub fn test_assert_can_vote_approve_before_voting_cool_off_time() {
2469        // Arrange
2470        let mut proposal = create_test_proposal();
2471        proposal.state = ProposalState::Voting;
2472
2473        let mut governance_config = create_test_governance_config();
2474        governance_config.voting_cool_off_time = 2;
2475
2476        let current_timestamp =
2477            proposal.voting_at.unwrap() + governance_config.voting_base_time as i64 - 1;
2478
2479        let vote = Vote::Approve(vec![]);
2480
2481        // Act
2482        let result = proposal.assert_can_cast_vote(&governance_config, &vote, current_timestamp);
2483
2484        // Assert
2485        assert_eq!(result, Ok(()));
2486    }
2487
2488    #[test]
2489    pub fn test_assert_cannot_vote_approve_within_voting_cool_off_time() {
2490        // Arrange
2491        let mut proposal = create_test_proposal();
2492        proposal.state = ProposalState::Voting;
2493
2494        let mut governance_config = create_test_governance_config();
2495        governance_config.voting_cool_off_time = 2;
2496
2497        let current_timestamp =
2498            proposal.voting_at.unwrap() + governance_config.voting_base_time as i64 + 1;
2499
2500        let vote = Vote::Approve(vec![]);
2501
2502        // Act
2503        let err = proposal
2504            .assert_can_cast_vote(&governance_config, &vote, current_timestamp)
2505            .err()
2506            .unwrap();
2507
2508        // Assert
2509        assert_eq!(err, GovernanceError::VoteNotAllowedInCoolOffTime.into());
2510    }
2511
2512    #[test]
2513    pub fn test_assert_can_vote_veto_within_voting_cool_off_time() {
2514        // Arrange
2515        let mut proposal = create_test_proposal();
2516        proposal.state = ProposalState::Voting;
2517
2518        let mut governance_config = create_test_governance_config();
2519        governance_config.voting_cool_off_time = 2;
2520
2521        let current_timestamp =
2522            proposal.voting_at.unwrap() + governance_config.voting_base_time as i64 + 1;
2523
2524        let vote = Vote::Veto;
2525
2526        // Act
2527        let result = proposal.assert_can_cast_vote(&governance_config, &vote, current_timestamp);
2528
2529        // Assert
2530        assert_eq!(result, Ok(()));
2531    }
2532
2533    #[test]
2534    pub fn test_assert_can_vote_deny_within_voting_cool_off_time() {
2535        // Arrange
2536        let mut proposal = create_test_proposal();
2537        proposal.state = ProposalState::Voting;
2538
2539        let mut governance_config = create_test_governance_config();
2540        governance_config.voting_cool_off_time = 1;
2541
2542        let current_timestamp =
2543            proposal.voting_at.unwrap() + governance_config.voting_base_time as i64 + 1;
2544
2545        let vote = Vote::Deny;
2546
2547        // Act
2548        let result = proposal.assert_can_cast_vote(&governance_config, &vote, current_timestamp);
2549
2550        // Assert
2551        assert_eq!(result, Ok(()));
2552    }
2553
2554    #[test]
2555    pub fn test_assert_valid_vote_with_deny_vote_for_survey_only_proposal_error() {
2556        // Arrange
2557        let mut proposal = create_test_proposal();
2558        proposal.deny_vote_weight = None;
2559
2560        // Survey only proposal can't be denied
2561        let vote = Vote::Deny;
2562
2563        // Act
2564        let result = proposal.assert_valid_vote(&vote);
2565
2566        // Assert
2567        assert_eq!(result, Err(GovernanceError::DenyVoteIsNotAllowed.into()));
2568    }
2569
2570    #[test]
2571    pub fn test_assert_valid_vote_with_too_many_options_error() {
2572        // Arrange
2573        let proposal = create_test_proposal();
2574
2575        let choices = vec![
2576            VoteChoice {
2577                rank: 0,
2578                weight_percentage: 100,
2579            },
2580            VoteChoice {
2581                rank: 0,
2582                weight_percentage: 100,
2583            },
2584        ];
2585
2586        let vote = Vote::Approve(choices.clone());
2587
2588        // Ensure
2589        assert!(proposal.options.len() != choices.len());
2590
2591        // Act
2592        let result = proposal.assert_valid_vote(&vote);
2593
2594        // Assert
2595        assert_eq!(
2596            result,
2597            Err(GovernanceError::InvalidNumberOfVoteChoices.into())
2598        );
2599    }
2600
2601    #[test]
2602    pub fn test_assert_valid_vote_with_no_choice_for_single_choice_error() {
2603        // Arrange
2604        let proposal = create_test_proposal();
2605
2606        let choices = vec![VoteChoice {
2607            rank: 0,
2608            weight_percentage: 0,
2609        }];
2610
2611        let vote = Vote::Approve(choices.clone());
2612
2613        // Ensure
2614        assert_eq!(proposal.options.len(), choices.len());
2615
2616        // Act
2617        let result = proposal.assert_valid_vote(&vote);
2618
2619        // Assert
2620        assert_eq!(
2621            result,
2622            Err(GovernanceError::SingleChoiceOnlyIsAllowed.into())
2623        );
2624    }
2625
2626    #[test]
2627    pub fn test_assert_valid_vote_with_to_many_choices_for_single_choice_error() {
2628        // Arrange
2629        let proposal = create_test_multi_option_proposal();
2630        let choices = vec![
2631            VoteChoice {
2632                rank: 0,
2633                weight_percentage: 100,
2634            },
2635            VoteChoice {
2636                rank: 0,
2637                weight_percentage: 100,
2638            },
2639            VoteChoice {
2640                rank: 0,
2641                weight_percentage: 0,
2642            },
2643        ];
2644
2645        let vote = Vote::Approve(choices.clone());
2646
2647        // Ensure
2648        assert_eq!(proposal.options.len(), choices.len());
2649
2650        // Act
2651        let result = proposal.assert_valid_vote(&vote);
2652
2653        // Assert
2654        assert_eq!(
2655            result,
2656            Err(GovernanceError::SingleChoiceOnlyIsAllowed.into())
2657        );
2658    }
2659
2660    #[test]
2661    pub fn test_assert_valid_multi_choice_full_weight_vote() {
2662        // Arrange
2663        let mut proposal = create_test_multi_option_proposal();
2664        proposal.vote_type = VoteType::MultiChoice {
2665            choice_type: MultiChoiceType::FullWeight,
2666            min_voter_options: 1,
2667            max_voter_options: 4,
2668            max_winning_options: 4,
2669        };
2670        let choices = vec![
2671            VoteChoice {
2672                rank: 0,
2673                weight_percentage: 100,
2674            },
2675            VoteChoice {
2676                rank: 0,
2677                weight_percentage: 100,
2678            },
2679            VoteChoice {
2680                rank: 0,
2681                weight_percentage: 100,
2682            },
2683        ];
2684
2685        let vote = Vote::Approve(choices.clone());
2686
2687        // Ensure
2688        assert_eq!(proposal.options.len(), choices.len());
2689
2690        // Act
2691        let result = proposal.assert_valid_vote(&vote);
2692
2693        // Assert
2694        assert_eq!(result, Ok(()));
2695    }
2696
2697    #[test]
2698    pub fn test_assert_valid_vote_with_no_choices_for_multi_choice_error() {
2699        // Arrange
2700        let mut proposal = create_test_multi_option_proposal();
2701        proposal.vote_type = VoteType::MultiChoice {
2702            choice_type: MultiChoiceType::FullWeight,
2703            min_voter_options: 1,
2704            max_voter_options: 3,
2705            max_winning_options: 3,
2706        };
2707
2708        let choices = vec![
2709            VoteChoice {
2710                rank: 0,
2711                weight_percentage: 0,
2712            },
2713            VoteChoice {
2714                rank: 0,
2715                weight_percentage: 0,
2716            },
2717            VoteChoice {
2718                rank: 0,
2719                weight_percentage: 0,
2720            },
2721        ];
2722
2723        let vote = Vote::Approve(choices.clone());
2724
2725        // Ensure
2726        assert_eq!(proposal.options.len(), choices.len());
2727
2728        // Act
2729        let result = proposal.assert_valid_vote(&vote);
2730
2731        // Assert
2732        assert_eq!(
2733            result,
2734            Err(GovernanceError::AtLeastSingleChoiceIsRequired.into())
2735        );
2736    }
2737
2738    #[test]
2739    pub fn test_assert_valid_vote_with_choice_weight_not_100_percent_error() {
2740        // Arrange
2741        let mut proposal = create_test_multi_option_proposal();
2742        proposal.vote_type = VoteType::MultiChoice {
2743            choice_type: MultiChoiceType::FullWeight,
2744            min_voter_options: 1,
2745            max_voter_options: 3,
2746            max_winning_options: 3,
2747        };
2748
2749        let choices = vec![
2750            VoteChoice {
2751                rank: 0,
2752                weight_percentage: 50,
2753            },
2754            VoteChoice {
2755                rank: 0,
2756                weight_percentage: 50,
2757            },
2758            VoteChoice {
2759                rank: 0,
2760                weight_percentage: 0,
2761            },
2762        ];
2763
2764        let vote = Vote::Approve(choices.clone());
2765
2766        // Ensure
2767        assert_eq!(proposal.options.len(), choices.len());
2768
2769        // Act
2770        let result = proposal.assert_valid_vote(&vote);
2771
2772        // Assert
2773        assert_eq!(
2774            result,
2775            Err(GovernanceError::ChoiceWeightMustBe100Percent.into())
2776        );
2777    }
2778
2779    #[test]
2780    pub fn test_assert_valid_proposal_options_with_invalid_choice_number_for_multi_choice_vote_error(
2781    ) {
2782        // Arrange
2783        let vote_type = VoteType::MultiChoice {
2784            choice_type: MultiChoiceType::FullWeight,
2785            min_voter_options: 1,
2786            max_voter_options: 3,
2787            max_winning_options: 3,
2788        };
2789
2790        let options = vec!["option 1".to_string(), "option 2".to_string()];
2791
2792        // Act
2793        let result = assert_valid_proposal_options(&options, &vote_type);
2794
2795        // Assert
2796        assert_eq!(
2797            result,
2798            Err(GovernanceError::InvalidMultiChoiceProposalParameters.into())
2799        );
2800    }
2801
2802    #[test]
2803    pub fn test_assert_valid_proposal_options_with_no_options_for_multi_choice_vote_error() {
2804        // Arrange
2805        let vote_type = VoteType::MultiChoice {
2806            choice_type: MultiChoiceType::FullWeight,
2807            min_voter_options: 1,
2808            max_voter_options: 3,
2809            max_winning_options: 3,
2810        };
2811
2812        let options = vec![];
2813
2814        // Act
2815        let result = assert_valid_proposal_options(&options, &vote_type);
2816
2817        // Assert
2818        assert_eq!(result, Err(GovernanceError::InvalidProposalOptions.into()));
2819    }
2820
2821    #[test]
2822    pub fn test_assert_valid_proposal_options_with_no_options_for_single_choice_vote_error() {
2823        // Arrange
2824        let vote_type = VoteType::SingleChoice;
2825
2826        let options = vec![];
2827
2828        // Act
2829        let result = assert_valid_proposal_options(&options, &vote_type);
2830
2831        // Assert
2832        assert_eq!(result, Err(GovernanceError::InvalidProposalOptions.into()));
2833    }
2834
2835    #[test]
2836    pub fn test_assert_valid_proposal_options_for_multi_choice_vote() {
2837        // Arrange
2838        let vote_type = VoteType::MultiChoice {
2839            choice_type: MultiChoiceType::FullWeight,
2840            min_voter_options: 1,
2841            max_voter_options: 3,
2842            max_winning_options: 3,
2843        };
2844
2845        let options = vec![
2846            "option 1".to_string(),
2847            "option 2".to_string(),
2848            "option 3".to_string(),
2849        ];
2850
2851        // Act
2852        let result = assert_valid_proposal_options(&options, &vote_type);
2853
2854        // Assert
2855        assert_eq!(result, Ok(()));
2856    }
2857
2858    #[test]
2859    pub fn test_assert_valid_proposal_options_for_multi_choice_vote_with_empty_option_error() {
2860        // Arrange
2861        let vote_type = VoteType::MultiChoice {
2862            choice_type: MultiChoiceType::FullWeight,
2863            min_voter_options: 1,
2864            max_voter_options: 3,
2865            max_winning_options: 3,
2866        };
2867
2868        let options = vec![
2869            "".to_string(),
2870            "option 2".to_string(),
2871            "option 3".to_string(),
2872        ];
2873
2874        // Act
2875        let result = assert_valid_proposal_options(&options, &vote_type);
2876
2877        // Assert
2878        assert_eq!(result, Err(GovernanceError::InvalidProposalOptions.into()));
2879    }
2880
2881    #[test]
2882    pub fn test_assert_valid_vote_for_multi_weighted_choice() {
2883        // Multi weighted choice may be weighted but sum of choices has to be 100%
2884        // Arrange
2885        let mut proposal = create_test_multi_option_proposal();
2886        proposal.vote_type = VoteType::MultiChoice {
2887            choice_type: MultiChoiceType::Weighted,
2888            min_voter_options: 1,
2889            max_voter_options: 3,
2890            max_winning_options: 3,
2891        };
2892
2893        let choices = vec![
2894            VoteChoice {
2895                rank: 0,
2896                weight_percentage: 42,
2897            },
2898            VoteChoice {
2899                rank: 0,
2900                weight_percentage: 42,
2901            },
2902            VoteChoice {
2903                rank: 0,
2904                weight_percentage: 16,
2905            },
2906        ];
2907        let vote = Vote::Approve(choices.clone());
2908
2909        // Ensure
2910        assert_eq!(proposal.options.len(), choices.len());
2911
2912        // Act
2913        let result = proposal.assert_valid_vote(&vote);
2914
2915        // Assert
2916        assert_eq!(result, Ok(()));
2917    }
2918
2919    #[test]
2920    pub fn test_assert_valid_full_vote_for_multi_weighted_choice() {
2921        // Multi weighted choice may be weighted to 100% and 0% rest
2922        // Arrange
2923        let mut proposal = create_test_multi_option_proposal();
2924        proposal.vote_type = VoteType::MultiChoice {
2925            choice_type: MultiChoiceType::Weighted,
2926            min_voter_options: 1,
2927            max_voter_options: 3,
2928            max_winning_options: 3,
2929        };
2930
2931        let choices = vec![
2932            VoteChoice {
2933                rank: 0,
2934                weight_percentage: 0,
2935            },
2936            VoteChoice {
2937                rank: 0,
2938                weight_percentage: 100,
2939            },
2940            VoteChoice {
2941                rank: 0,
2942                weight_percentage: 0,
2943            },
2944        ];
2945        let vote = Vote::Approve(choices.clone());
2946
2947        // Ensure
2948        assert_eq!(proposal.options.len(), choices.len());
2949
2950        // Act
2951        let result = proposal.assert_valid_vote(&vote);
2952
2953        // Assert
2954        assert_eq!(result, Ok(()));
2955    }
2956
2957    #[test]
2958    pub fn test_assert_valid_vote_with_total_vote_weight_above_100_percent_for_multi_weighted_choice_error(
2959    ) {
2960        // Arrange
2961        let mut proposal = create_test_multi_option_proposal();
2962        proposal.vote_type = VoteType::MultiChoice {
2963            choice_type: MultiChoiceType::Weighted,
2964            min_voter_options: 1,
2965            max_voter_options: 2,
2966            max_winning_options: 2,
2967        };
2968
2969        let choices = vec![
2970            VoteChoice {
2971                rank: 0,
2972                weight_percentage: 34,
2973            },
2974            VoteChoice {
2975                rank: 0,
2976                weight_percentage: 34,
2977            },
2978            VoteChoice {
2979                rank: 0,
2980                weight_percentage: 34,
2981            },
2982        ];
2983        let vote = Vote::Approve(choices.clone());
2984
2985        // Ensure
2986        assert_eq!(proposal.options.len(), choices.len());
2987
2988        // Act
2989        let result = proposal.assert_valid_vote(&vote);
2990
2991        // Assert
2992        assert_eq!(
2993            result,
2994            Err(GovernanceError::TotalVoteWeightMustBe100Percent.into())
2995        );
2996    }
2997
2998    #[test]
2999    pub fn test_assert_valid_vote_with_over_percentage_for_multi_weighted_choice_error() {
3000        // Multi weighted choice does not permit vote with sum weight over 100%
3001        // Arrange
3002        let mut proposal = create_test_multi_option_proposal();
3003        proposal.vote_type = VoteType::MultiChoice {
3004            choice_type: MultiChoiceType::Weighted,
3005            min_voter_options: 1,
3006            max_voter_options: 3,
3007            max_winning_options: 3,
3008        };
3009
3010        let choices = vec![
3011            VoteChoice {
3012                rank: 0,
3013                weight_percentage: 34,
3014            },
3015            VoteChoice {
3016                rank: 0,
3017                weight_percentage: 34,
3018            },
3019            VoteChoice {
3020                rank: 0,
3021                weight_percentage: 34,
3022            },
3023        ];
3024        let vote = Vote::Approve(choices.clone());
3025
3026        // Ensure
3027        assert_eq!(proposal.options.len(), choices.len());
3028
3029        // Act
3030        let result = proposal.assert_valid_vote(&vote);
3031
3032        // Assert
3033        assert_eq!(
3034            result,
3035            Err(GovernanceError::TotalVoteWeightMustBe100Percent.into())
3036        );
3037    }
3038
3039    #[test]
3040    pub fn test_assert_valid_vote_with_overflow_weight_for_multi_weighted_choice_error() {
3041        // Multi weighted choice does not permit vote with sum weight over 100%
3042        // Arrange
3043        let mut proposal = create_test_multi_option_proposal();
3044        proposal.vote_type = VoteType::MultiChoice {
3045            choice_type: MultiChoiceType::Weighted,
3046            min_voter_options: 1,
3047            max_voter_options: 3,
3048            max_winning_options: 3,
3049        };
3050
3051        let choices = vec![
3052            VoteChoice {
3053                rank: 0,
3054                weight_percentage: 100,
3055            },
3056            VoteChoice {
3057                rank: 0,
3058                weight_percentage: 100,
3059            },
3060            VoteChoice {
3061                rank: 0,
3062                weight_percentage: 100,
3063            },
3064        ];
3065        let vote = Vote::Approve(choices.clone());
3066
3067        // Ensure
3068        assert_eq!(proposal.options.len(), choices.len());
3069
3070        // Act
3071        let result = proposal.assert_valid_vote(&vote);
3072
3073        // Assert
3074        assert_eq!(
3075            result,
3076            Err(GovernanceError::TotalVoteWeightMustBe100Percent.into())
3077        );
3078    }
3079
3080    #[test]
3081    pub fn test_assert_valid_proposal_options_with_invalid_choice_number_for_multi_weighted_choice_vote_error(
3082    ) {
3083        // Arrange
3084        let vote_type = VoteType::MultiChoice {
3085            choice_type: MultiChoiceType::Weighted,
3086            min_voter_options: 1,
3087            max_voter_options: 3,
3088            max_winning_options: 3,
3089        };
3090
3091        let options = vec!["option 1".to_string(), "option 2".to_string()];
3092
3093        // Act
3094        let result = assert_valid_proposal_options(&options, &vote_type);
3095
3096        // Assert
3097        assert_eq!(
3098            result,
3099            Err(GovernanceError::InvalidMultiChoiceProposalParameters.into())
3100        );
3101    }
3102
3103    #[test]
3104    pub fn test_assert_valid_proposal_options_with_no_options_for_multi_weighted_choice_vote_error()
3105    {
3106        // Arrange
3107        let vote_type = VoteType::MultiChoice {
3108            choice_type: MultiChoiceType::Weighted,
3109            min_voter_options: 1,
3110            max_voter_options: 3,
3111            max_winning_options: 3,
3112        };
3113
3114        let options = vec![];
3115
3116        // Act
3117        let result = assert_valid_proposal_options(&options, &vote_type);
3118
3119        // Assert
3120        assert_eq!(result, Err(GovernanceError::InvalidProposalOptions.into()));
3121    }
3122
3123    #[test]
3124    pub fn test_assert_valid_proposal_options_for_multi_weighted_choice_vote() {
3125        // Arrange
3126        let vote_type = VoteType::MultiChoice {
3127            choice_type: MultiChoiceType::Weighted,
3128            min_voter_options: 1,
3129            max_voter_options: 3,
3130            max_winning_options: 3,
3131        };
3132
3133        let options = vec![
3134            "option 1".to_string(),
3135            "option 2".to_string(),
3136            "option 3".to_string(),
3137        ];
3138
3139        // Act
3140        let result = assert_valid_proposal_options(&options, &vote_type);
3141
3142        // Assert
3143        assert_eq!(result, Ok(()));
3144    }
3145
3146    #[test]
3147    pub fn test_assert_valid_proposal_options_for_multi_weighted_choice_vote_with_empty_option_error(
3148    ) {
3149        // Arrange
3150        let vote_type = VoteType::MultiChoice {
3151            choice_type: MultiChoiceType::Weighted,
3152            min_voter_options: 1,
3153            max_voter_options: 3,
3154            max_winning_options: 3,
3155        };
3156
3157        let options = vec![
3158            "".to_string(),
3159            "option 2".to_string(),
3160            "option 3".to_string(),
3161        ];
3162
3163        // Act
3164        let result = assert_valid_proposal_options(&options, &vote_type);
3165
3166        // Assert
3167        assert_eq!(result, Err(GovernanceError::InvalidProposalOptions.into()));
3168    }
3169
3170    #[test]
3171    pub fn test_assert_more_than_ten_proposal_options_for_multi_weighted_choice_error() {
3172        // Arrange
3173        let vote_type = VoteType::MultiChoice {
3174            choice_type: MultiChoiceType::Weighted,
3175            min_voter_options: 1,
3176            max_voter_options: 3,
3177            max_winning_options: 3,
3178        };
3179
3180        let options = vec![
3181            "option 1".to_string(),
3182            "option 2".to_string(),
3183            "option 3".to_string(),
3184            "option 4".to_string(),
3185            "option 5".to_string(),
3186            "option 6".to_string(),
3187            "option 7".to_string(),
3188            "option 8".to_string(),
3189            "option 9".to_string(),
3190            "option 10".to_string(),
3191            "option 11".to_string(),
3192        ];
3193
3194        // Act
3195        let result = assert_valid_proposal_options(&options, &vote_type);
3196
3197        // Assert
3198        assert_eq!(result, Err(GovernanceError::InvalidProposalOptions.into()));
3199    }
3200
3201    #[test]
3202    fn test_proposal_v1_to_v2_serialisation_roundtrip() {
3203        // Arrange
3204
3205        let proposal_v1_source = ProposalV1 {
3206            account_type: GovernanceAccountType::ProposalV1,
3207            governance: Pubkey::new_unique(),
3208            governing_token_mint: Pubkey::new_unique(),
3209            state: ProposalState::Executing,
3210            token_owner_record: Pubkey::new_unique(),
3211            signatories_count: 5,
3212            signatories_signed_off_count: 4,
3213            yes_votes_count: 100,
3214            no_votes_count: 80,
3215            instructions_executed_count: 7,
3216            instructions_count: 8,
3217            instructions_next_index: 9,
3218            draft_at: 200,
3219            signing_off_at: Some(201),
3220            voting_at: Some(202),
3221            voting_at_slot: Some(203),
3222            voting_completed_at: Some(204),
3223            executing_at: Some(205),
3224            closed_at: Some(206),
3225            execution_flags: InstructionExecutionFlags::None,
3226            max_vote_weight: Some(250),
3227            vote_threshold: Some(VoteThreshold::YesVotePercentage(65)),
3228            name: "proposal".to_string(),
3229            description_link: "proposal-description".to_string(),
3230        };
3231
3232        let mut account_data = vec![];
3233        proposal_v1_source.serialize(&mut account_data).unwrap();
3234
3235        let program_id = Pubkey::new_unique();
3236
3237        let info_key = Pubkey::new_unique();
3238        let mut lamports = 10u64;
3239
3240        let account_info = AccountInfo::new(
3241            &info_key,
3242            false,
3243            false,
3244            &mut lamports,
3245            &mut account_data[..],
3246            &program_id,
3247            false,
3248            Epoch::default(),
3249        );
3250
3251        // Act
3252
3253        let proposal_v2 = get_proposal_data(&program_id, &account_info).unwrap();
3254
3255        proposal_v2
3256            .serialize(&mut account_info.data.borrow_mut()[..])
3257            .unwrap();
3258
3259        // Assert
3260        let proposal_v1_target =
3261            get_account_data::<ProposalV1>(&program_id, &account_info).unwrap();
3262
3263        assert_eq!(proposal_v1_source, proposal_v1_target)
3264    }
3265}