use gemachain_program::clock::{Slot, UnixTimestamp};
use gemachain_program::{
account_info::AccountInfo, program_error::ProgramError, program_pack::IsInitialized,
pubkey::Pubkey,
};
use gpl_governance_tools::account::{get_account_data, AccountMaxSize};
use crate::{
error::GovernanceError,
state::{
enums::{
GovernanceAccountType, InstructionExecutionFlags, InstructionExecutionStatus,
MintMaxVoteWeightSource, ProposalState, VoteThresholdPercentage,
},
governance::GovernanceConfig,
proposal_instruction::ProposalInstruction,
realm::Realm,
},
PROGRAM_AUTHORITY_SEED,
};
use borsh::{BorshDeserialize, BorshSchema, BorshSerialize};
#[repr(C)]
#[derive(Clone, Debug, PartialEq, BorshDeserialize, BorshSerialize, BorshSchema)]
pub struct Proposal {
pub account_type: GovernanceAccountType,
pub governance: Pubkey,
pub governing_token_mint: Pubkey,
pub state: ProposalState,
pub token_owner_record: Pubkey,
pub signatories_count: u8,
pub signatories_signed_off_count: u8,
pub yes_votes_count: u64,
pub no_votes_count: u64,
pub instructions_executed_count: u16,
pub instructions_count: u16,
pub instructions_next_index: u16,
pub draft_at: UnixTimestamp,
pub signing_off_at: Option<UnixTimestamp>,
pub voting_at: Option<UnixTimestamp>,
pub voting_at_slot: Option<Slot>,
pub voting_completed_at: Option<UnixTimestamp>,
pub executing_at: Option<UnixTimestamp>,
pub closed_at: Option<UnixTimestamp>,
pub execution_flags: InstructionExecutionFlags,
pub max_vote_weight: Option<u64>,
pub vote_threshold_percentage: Option<VoteThresholdPercentage>,
pub name: String,
pub description_link: String,
}
impl AccountMaxSize for Proposal {
fn get_max_size(&self) -> Option<usize> {
Some(self.name.len() + self.description_link.len() + 205)
}
}
impl IsInitialized for Proposal {
fn is_initialized(&self) -> bool {
self.account_type == GovernanceAccountType::Proposal
}
}
impl Proposal {
pub fn assert_can_edit_signatories(&self) -> Result<(), ProgramError> {
self.assert_is_draft_state()
.map_err(|_| GovernanceError::InvalidStateCannotEditSignatories.into())
}
pub fn assert_can_sign_off(&self) -> Result<(), ProgramError> {
match self.state {
ProposalState::Draft | ProposalState::SigningOff => Ok(()),
ProposalState::Executing
| ProposalState::ExecutingWithErrors
| ProposalState::Completed
| ProposalState::Cancelled
| ProposalState::Voting
| ProposalState::Succeeded
| ProposalState::Defeated => Err(GovernanceError::InvalidStateCannotSignOff.into()),
}
}
fn assert_is_voting_state(&self) -> Result<(), ProgramError> {
if self.state != ProposalState::Voting {
return Err(GovernanceError::InvalidProposalState.into());
}
Ok(())
}
fn assert_is_draft_state(&self) -> Result<(), ProgramError> {
if self.state != ProposalState::Draft {
return Err(GovernanceError::InvalidProposalState.into());
}
Ok(())
}
pub fn assert_can_cast_vote(
&self,
config: &GovernanceConfig,
current_unix_timestamp: UnixTimestamp,
) -> Result<(), ProgramError> {
self.assert_is_voting_state()
.map_err(|_| GovernanceError::InvalidStateCannotVote)?;
if self.has_vote_time_ended(config, current_unix_timestamp) {
return Err(GovernanceError::ProposalVotingTimeExpired.into());
}
Ok(())
}
pub fn has_vote_time_ended(
&self,
config: &GovernanceConfig,
current_unix_timestamp: UnixTimestamp,
) -> bool {
self.voting_at
.unwrap()
.checked_add(config.max_voting_time as i64)
.unwrap()
< current_unix_timestamp
}
pub fn assert_can_finalize_vote(
&self,
config: &GovernanceConfig,
current_unix_timestamp: UnixTimestamp,
) -> Result<(), ProgramError> {
self.assert_is_voting_state()
.map_err(|_| GovernanceError::InvalidStateCannotFinalize)?;
if !self.has_vote_time_ended(config, current_unix_timestamp) {
return Err(GovernanceError::CannotFinalizeVotingInProgress.into());
}
Ok(())
}
pub fn finalize_vote(
&mut self,
governing_token_mint_supply: u64,
config: &GovernanceConfig,
realm_data: &Realm,
current_unix_timestamp: UnixTimestamp,
) -> Result<(), ProgramError> {
self.assert_can_finalize_vote(config, current_unix_timestamp)?;
let max_vote_weight = self.get_max_vote_weight(realm_data, governing_token_mint_supply)?;
self.state = self.get_final_vote_state(max_vote_weight, config);
self.voting_completed_at = Some(current_unix_timestamp);
self.max_vote_weight = Some(max_vote_weight);
self.vote_threshold_percentage = Some(config.vote_threshold_percentage.clone());
Ok(())
}
fn get_final_vote_state(
&mut self,
max_vote_weight: u64,
config: &GovernanceConfig,
) -> ProposalState {
let yes_vote_threshold_count =
get_yes_vote_threshold_count(&config.vote_threshold_percentage, max_vote_weight)
.unwrap();
if self.yes_votes_count >= yes_vote_threshold_count
&& self.yes_votes_count > self.no_votes_count
{
ProposalState::Succeeded
} else {
ProposalState::Defeated
}
}
fn get_max_vote_weight(
&mut self,
realm_data: &Realm,
governing_token_mint_supply: u64,
) -> Result<u64, ProgramError> {
if Some(self.governing_token_mint) == realm_data.config.council_mint {
return Ok(governing_token_mint_supply);
}
match realm_data.config.community_mint_max_vote_weight_source {
MintMaxVoteWeightSource::SupplyFraction(fraction) => {
if fraction == MintMaxVoteWeightSource::SUPPLY_FRACTION_BASE {
return Ok(governing_token_mint_supply);
}
let max_vote_weight = (governing_token_mint_supply as u128)
.checked_mul(fraction as u128)
.unwrap()
.checked_div(MintMaxVoteWeightSource::SUPPLY_FRACTION_BASE as u128)
.unwrap() as u64;
let total_vote_count = self
.yes_votes_count
.checked_add(self.no_votes_count)
.unwrap();
Ok(max_vote_weight.max(total_vote_count))
}
MintMaxVoteWeightSource::Absolute(_) => {
Err(GovernanceError::VoteWeightSourceNotSupported.into())
}
}
}
pub fn try_tip_vote(
&mut self,
governing_token_mint_supply: u64,
config: &GovernanceConfig,
realm_data: &Realm,
current_unix_timestamp: UnixTimestamp,
) -> Result<bool, ProgramError> {
let max_vote_weight = self.get_max_vote_weight(realm_data, governing_token_mint_supply)?;
if let Some(tipped_state) = self.try_get_tipped_vote_state(max_vote_weight, config) {
self.state = tipped_state;
self.voting_completed_at = Some(current_unix_timestamp);
self.max_vote_weight = Some(max_vote_weight);
self.vote_threshold_percentage = Some(config.vote_threshold_percentage.clone());
Ok(true)
} else {
Ok(false)
}
}
#[allow(clippy::float_cmp)]
pub fn try_get_tipped_vote_state(
&self,
max_vote_weight: u64,
config: &GovernanceConfig,
) -> Option<ProposalState> {
if self.yes_votes_count == max_vote_weight {
return Some(ProposalState::Succeeded);
}
if self.no_votes_count == max_vote_weight {
return Some(ProposalState::Defeated);
}
let yes_vote_threshold_count =
get_yes_vote_threshold_count(&config.vote_threshold_percentage, max_vote_weight)
.unwrap();
if self.yes_votes_count >= yes_vote_threshold_count
&& self.yes_votes_count > (max_vote_weight - self.yes_votes_count)
{
return Some(ProposalState::Succeeded);
} else if self.no_votes_count > (max_vote_weight - yes_vote_threshold_count)
|| self.no_votes_count >= (max_vote_weight - self.no_votes_count)
{
return Some(ProposalState::Defeated);
}
None
}
pub fn assert_can_cancel(
&self,
config: &GovernanceConfig,
current_unix_timestamp: UnixTimestamp,
) -> Result<(), ProgramError> {
match self.state {
ProposalState::Draft | ProposalState::SigningOff => Ok(()),
ProposalState::Voting => {
if self.has_vote_time_ended(config, current_unix_timestamp) {
return Err(GovernanceError::ProposalVotingTimeExpired.into());
}
Ok(())
}
ProposalState::Executing
| ProposalState::ExecutingWithErrors
| ProposalState::Completed
| ProposalState::Cancelled
| ProposalState::Succeeded
| ProposalState::Defeated => {
Err(GovernanceError::InvalidStateCannotCancelProposal.into())
}
}
}
pub fn assert_can_edit_instructions(&self) -> Result<(), ProgramError> {
self.assert_is_draft_state()
.map_err(|_| GovernanceError::InvalidStateCannotEditInstructions.into())
}
pub fn assert_can_execute_instruction(
&self,
proposal_instruction_data: &ProposalInstruction,
current_unix_timestamp: UnixTimestamp,
) -> Result<(), ProgramError> {
match self.state {
ProposalState::Succeeded
| ProposalState::Executing
| ProposalState::ExecutingWithErrors => {}
ProposalState::Draft
| ProposalState::SigningOff
| ProposalState::Completed
| ProposalState::Voting
| ProposalState::Cancelled
| ProposalState::Defeated => {
return Err(GovernanceError::InvalidStateCannotExecuteInstruction.into())
}
}
if self
.voting_completed_at
.unwrap()
.checked_add(proposal_instruction_data.hold_up_time as i64)
.unwrap()
>= current_unix_timestamp
{
return Err(GovernanceError::CannotExecuteInstructionWithinHoldUpTime.into());
}
if proposal_instruction_data.executed_at.is_some() {
return Err(GovernanceError::InstructionAlreadyExecuted.into());
}
Ok(())
}
pub fn assert_can_flag_instruction_error(
&self,
proposal_instruction_data: &ProposalInstruction,
current_unix_timestamp: UnixTimestamp,
) -> Result<(), ProgramError> {
self.assert_can_execute_instruction(proposal_instruction_data, current_unix_timestamp)?;
if proposal_instruction_data.execution_status == InstructionExecutionStatus::Error {
return Err(GovernanceError::InstructionAlreadyFlaggedWithError.into());
}
Ok(())
}
}
fn get_yes_vote_threshold_count(
vote_threshold_percentage: &VoteThresholdPercentage,
max_vote_weight: u64,
) -> Result<u64, ProgramError> {
let yes_vote_threshold_percentage = match vote_threshold_percentage {
VoteThresholdPercentage::YesVote(yes_vote_threshold_percentage) => {
*yes_vote_threshold_percentage
}
_ => {
return Err(GovernanceError::VoteThresholdPercentageTypeNotSupported.into());
}
};
let numerator = (yes_vote_threshold_percentage as u128)
.checked_mul(max_vote_weight as u128)
.unwrap();
let mut yes_vote_threshold = numerator.checked_div(100).unwrap();
if yes_vote_threshold * 100 < numerator {
yes_vote_threshold += 1;
}
Ok(yes_vote_threshold as u64)
}
pub fn get_proposal_data(
program_id: &Pubkey,
proposal_info: &AccountInfo,
) -> Result<Proposal, ProgramError> {
get_account_data::<Proposal>(program_id, proposal_info)
}
pub fn get_proposal_data_for_governance_and_governing_mint(
program_id: &Pubkey,
proposal_info: &AccountInfo,
governance: &Pubkey,
governing_token_mint: &Pubkey,
) -> Result<Proposal, ProgramError> {
let proposal_data = get_proposal_data_for_governance(program_id, proposal_info, governance)?;
if proposal_data.governing_token_mint != *governing_token_mint {
return Err(GovernanceError::InvalidGoverningMintForProposal.into());
}
Ok(proposal_data)
}
pub fn get_proposal_data_for_governance(
program_id: &Pubkey,
proposal_info: &AccountInfo,
governance: &Pubkey,
) -> Result<Proposal, ProgramError> {
let proposal_data = get_proposal_data(program_id, proposal_info)?;
if proposal_data.governance != *governance {
return Err(GovernanceError::InvalidGovernanceForProposal.into());
}
Ok(proposal_data)
}
pub fn get_proposal_address_seeds<'a>(
governance: &'a Pubkey,
governing_token_mint: &'a Pubkey,
proposal_index_le_bytes: &'a [u8],
) -> [&'a [u8]; 4] {
[
PROGRAM_AUTHORITY_SEED,
governance.as_ref(),
governing_token_mint.as_ref(),
proposal_index_le_bytes,
]
}
pub fn get_proposal_address<'a>(
program_id: &Pubkey,
governance: &'a Pubkey,
governing_token_mint: &'a Pubkey,
proposal_index_le_bytes: &'a [u8],
) -> Pubkey {
Pubkey::find_program_address(
&get_proposal_address_seeds(governance, governing_token_mint, proposal_index_le_bytes),
program_id,
)
.0
}
#[cfg(test)]
mod test {
use crate::state::{
enums::{MintMaxVoteWeightSource, VoteThresholdPercentage, VoteWeightSource},
realm::RealmConfig,
};
use {super::*, proptest::prelude::*};
fn create_test_proposal() -> Proposal {
Proposal {
account_type: GovernanceAccountType::TokenOwnerRecord,
governance: Pubkey::new_unique(),
governing_token_mint: Pubkey::new_unique(),
max_vote_weight: Some(10),
state: ProposalState::Draft,
token_owner_record: Pubkey::new_unique(),
signatories_count: 10,
signatories_signed_off_count: 5,
description_link: "This is my description".to_string(),
name: "This is my name".to_string(),
draft_at: 10,
signing_off_at: Some(10),
voting_at: Some(10),
voting_at_slot: Some(500),
voting_completed_at: Some(10),
executing_at: Some(10),
closed_at: Some(10),
yes_votes_count: 0,
no_votes_count: 0,
execution_flags: InstructionExecutionFlags::Ordered,
instructions_executed_count: 10,
instructions_count: 10,
instructions_next_index: 10,
vote_threshold_percentage: Some(VoteThresholdPercentage::YesVote(100)),
}
}
fn create_test_realm() -> Realm {
Realm {
account_type: GovernanceAccountType::Realm,
community_mint: Pubkey::new_unique(),
reserved: [0; 8],
authority: Some(Pubkey::new_unique()),
name: "test-realm".to_string(),
config: RealmConfig {
council_mint: Some(Pubkey::new_unique()),
reserved: [0; 7],
use_community_voter_weight_addin: false,
community_mint_max_vote_weight_source:
MintMaxVoteWeightSource::FULL_SUPPLY_FRACTION,
min_community_tokens_to_create_governance: 10,
},
}
}
fn create_test_governance_config() -> GovernanceConfig {
GovernanceConfig {
min_community_tokens_to_create_proposal: 5,
min_council_tokens_to_create_proposal: 1,
min_instruction_hold_up_time: 10,
max_voting_time: 5,
vote_threshold_percentage: VoteThresholdPercentage::YesVote(60),
vote_weight_source: VoteWeightSource::Deposit,
proposal_cool_off_time: 0,
}
}
#[test]
fn test_max_size() {
let proposal = create_test_proposal();
let size = proposal.try_to_vec().unwrap().len();
assert_eq!(proposal.get_max_size(), Some(size));
}
prop_compose! {
fn vote_results()(governing_token_supply in 1..=u64::MAX)(
governing_token_supply in Just(governing_token_supply),
vote_count in 0..=governing_token_supply,
) -> (u64, u64) {
(vote_count as u64, governing_token_supply as u64)
}
}
fn editable_signatory_states() -> impl Strategy<Value = ProposalState> {
prop_oneof![Just(ProposalState::Draft)]
}
proptest! {
#[test]
fn test_assert_can_edit_signatories(state in editable_signatory_states()) {
let mut proposal = create_test_proposal();
proposal.state = state;
proposal.assert_can_edit_signatories().unwrap();
}
}
fn none_editable_signatory_states() -> impl Strategy<Value = ProposalState> {
prop_oneof![
Just(ProposalState::Voting),
Just(ProposalState::Succeeded),
Just(ProposalState::Executing),
Just(ProposalState::ExecutingWithErrors),
Just(ProposalState::Completed),
Just(ProposalState::Cancelled),
Just(ProposalState::Defeated),
Just(ProposalState::SigningOff),
]
}
proptest! {
#[test]
fn test_assert_can_edit_signatories_with_invalid_state_error(state in none_editable_signatory_states()) {
let mut proposal = create_test_proposal();
proposal.state = state;
let err = proposal.assert_can_edit_signatories().err().unwrap();
assert_eq!(err, GovernanceError::InvalidStateCannotEditSignatories.into());
}
}
fn sign_off_states() -> impl Strategy<Value = ProposalState> {
prop_oneof![Just(ProposalState::SigningOff), Just(ProposalState::Draft),]
}
proptest! {
#[test]
fn test_assert_can_sign_off(state in sign_off_states()) {
let mut proposal = create_test_proposal();
proposal.state = state;
proposal.assert_can_sign_off().unwrap();
}
}
fn none_sign_off_states() -> impl Strategy<Value = ProposalState> {
prop_oneof![
Just(ProposalState::Voting),
Just(ProposalState::Succeeded),
Just(ProposalState::Executing),
Just(ProposalState::ExecutingWithErrors),
Just(ProposalState::Completed),
Just(ProposalState::Cancelled),
Just(ProposalState::Defeated),
]
}
proptest! {
#[test]
fn test_assert_can_sign_off_with_state_error(state in none_sign_off_states()) {
let mut proposal = create_test_proposal();
proposal.state = state;
let err = proposal.assert_can_sign_off().err().unwrap();
assert_eq!(err, GovernanceError::InvalidStateCannotSignOff.into());
}
}
fn cancellable_states() -> impl Strategy<Value = ProposalState> {
prop_oneof![
Just(ProposalState::Draft),
Just(ProposalState::SigningOff),
Just(ProposalState::Voting),
]
}
proptest! {
#[test]
fn test_assert_can_cancel(state in cancellable_states()) {
let mut proposal = create_test_proposal();
let governance_config = create_test_governance_config();
proposal.state = state;
proposal.assert_can_cancel(&governance_config,1).unwrap();
}
}
fn none_cancellable_states() -> impl Strategy<Value = ProposalState> {
prop_oneof![
Just(ProposalState::Succeeded),
Just(ProposalState::Executing),
Just(ProposalState::ExecutingWithErrors),
Just(ProposalState::Completed),
Just(ProposalState::Cancelled),
Just(ProposalState::Defeated),
]
}
proptest! {
#[test]
fn test_assert_can_cancel_with_invalid_state_error(state in none_cancellable_states()) {
let mut proposal = create_test_proposal();
proposal.state = state;
let governance_config = create_test_governance_config();
let err = proposal.assert_can_cancel(&governance_config,1).err().unwrap();
assert_eq!(err, GovernanceError::InvalidStateCannotCancelProposal.into());
}
}
#[derive(Clone, Debug)]
pub struct VoteCastTestCase {
name: &'static str,
governing_token_supply: u64,
vote_threshold_percentage: u8,
yes_votes_count: u64,
no_votes_count: u64,
expected_tipped_state: ProposalState,
expected_finalized_state: ProposalState,
}
fn vote_casting_test_cases() -> impl Strategy<Value = VoteCastTestCase> {
prop_oneof![
Just(VoteCastTestCase {
name: "45:10 @40 -- Nays can still outvote Yeahs",
governing_token_supply: 100,
vote_threshold_percentage: 40,
yes_votes_count: 45,
no_votes_count: 10,
expected_tipped_state: ProposalState::Voting,
expected_finalized_state: ProposalState::Succeeded,
}),
Just(VoteCastTestCase {
name: "49:50 @40 -- In best case scenario it can be 50:50 tie and hence Defeated",
governing_token_supply: 100,
vote_threshold_percentage: 40,
yes_votes_count: 49,
no_votes_count: 50,
expected_tipped_state: ProposalState::Defeated,
expected_finalized_state: ProposalState::Defeated,
}),
Just(VoteCastTestCase {
name: "40:40 @40 -- Still can go either way",
governing_token_supply: 100,
vote_threshold_percentage: 40,
yes_votes_count: 40,
no_votes_count: 40,
expected_tipped_state: ProposalState::Voting,
expected_finalized_state: ProposalState::Defeated,
}),
Just(VoteCastTestCase {
name: "45:45 @40 -- Still can go either way",
governing_token_supply: 100,
vote_threshold_percentage: 40,
yes_votes_count: 45,
no_votes_count: 45,
expected_tipped_state: ProposalState::Voting,
expected_finalized_state: ProposalState::Defeated,
}),
Just(VoteCastTestCase {
name: "50:10 @40 -- Nay sayers can still tie up",
governing_token_supply: 100,
vote_threshold_percentage: 40,
yes_votes_count: 50,
no_votes_count: 10,
expected_tipped_state: ProposalState::Voting,
expected_finalized_state: ProposalState::Succeeded,
}),
Just(VoteCastTestCase {
name: "50:50 @40 -- It's a tie and hence Defeated",
governing_token_supply: 100,
vote_threshold_percentage: 40,
yes_votes_count: 50,
no_votes_count: 50,
expected_tipped_state: ProposalState::Defeated,
expected_finalized_state: ProposalState::Defeated,
}),
Just(VoteCastTestCase {
name: "45:51 @ 40 -- Nays won",
governing_token_supply: 100,
vote_threshold_percentage: 40,
yes_votes_count: 45,
no_votes_count: 51,
expected_tipped_state: ProposalState::Defeated,
expected_finalized_state: ProposalState::Defeated,
}),
Just(VoteCastTestCase {
name: "40:55 @ 40 -- Nays won",
governing_token_supply: 100,
vote_threshold_percentage: 40,
yes_votes_count: 40,
no_votes_count: 55,
expected_tipped_state: ProposalState::Defeated,
expected_finalized_state: ProposalState::Defeated,
}),
Just(VoteCastTestCase {
name: "50:10 @50 -- +1 tie breaker required to tip",
governing_token_supply: 100,
vote_threshold_percentage: 50,
yes_votes_count: 50,
no_votes_count: 10,
expected_tipped_state: ProposalState::Voting,
expected_finalized_state: ProposalState::Succeeded,
}),
Just(VoteCastTestCase {
name: "10:50 @50 -- +1 tie breaker vote not possible any longer",
governing_token_supply: 100,
vote_threshold_percentage: 50,
yes_votes_count: 10,
no_votes_count: 50,
expected_tipped_state: ProposalState::Defeated,
expected_finalized_state: ProposalState::Defeated,
}),
Just(VoteCastTestCase {
name: "50:50 @50 -- +1 tie breaker vote not possible any longer",
governing_token_supply: 100,
vote_threshold_percentage: 50,
yes_votes_count: 50,
no_votes_count: 50,
expected_tipped_state: ProposalState::Defeated,
expected_finalized_state: ProposalState::Defeated,
}),
Just(VoteCastTestCase {
name: "51:10 @ 50 -- Nay sayers can't outvote any longer",
governing_token_supply: 100,
vote_threshold_percentage: 50,
yes_votes_count: 51,
no_votes_count: 10,
expected_tipped_state: ProposalState::Succeeded,
expected_finalized_state: ProposalState::Succeeded,
}),
Just(VoteCastTestCase {
name: "10:51 @ 50 -- Nays won",
governing_token_supply: 100,
vote_threshold_percentage: 50,
yes_votes_count: 10,
no_votes_count: 51,
expected_tipped_state: ProposalState::Defeated,
expected_finalized_state: ProposalState::Defeated,
}),
Just(VoteCastTestCase {
name: "10:10 @ 60 -- Can still go either way",
governing_token_supply: 100,
vote_threshold_percentage: 60,
yes_votes_count: 10,
no_votes_count: 10,
expected_tipped_state: ProposalState::Voting,
expected_finalized_state: ProposalState::Defeated,
}),
Just(VoteCastTestCase {
name: "55:10 @ 60 -- Can still go either way",
governing_token_supply: 100,
vote_threshold_percentage: 60,
yes_votes_count: 55,
no_votes_count: 10,
expected_tipped_state: ProposalState::Voting,
expected_finalized_state: ProposalState::Defeated,
}),
Just(VoteCastTestCase {
name: "60:10 @ 60 -- Yeah reached the required threshold",
governing_token_supply: 100,
vote_threshold_percentage: 60,
yes_votes_count: 60,
no_votes_count: 10,
expected_tipped_state: ProposalState::Succeeded,
expected_finalized_state: ProposalState::Succeeded,
}),
Just(VoteCastTestCase {
name: "61:10 @ 60 -- Yeah won",
governing_token_supply: 100,
vote_threshold_percentage: 60,
yes_votes_count: 61,
no_votes_count: 10,
expected_tipped_state: ProposalState::Succeeded,
expected_finalized_state: ProposalState::Succeeded,
}),
Just(VoteCastTestCase {
name: "10:40 @ 60 -- Yeah can still outvote Nay",
governing_token_supply: 100,
vote_threshold_percentage: 60,
yes_votes_count: 10,
no_votes_count: 40,
expected_tipped_state: ProposalState::Voting,
expected_finalized_state: ProposalState::Defeated,
}),
Just(VoteCastTestCase {
name: "60:40 @ 60 -- Yeah won",
governing_token_supply: 100,
vote_threshold_percentage: 60,
yes_votes_count: 60,
no_votes_count: 40,
expected_tipped_state: ProposalState::Succeeded,
expected_finalized_state: ProposalState::Succeeded,
}),
Just(VoteCastTestCase {
name: "10:41 @ 60 -- Aye can't outvote Nay any longer",
governing_token_supply: 100,
vote_threshold_percentage: 60,
yes_votes_count: 10,
no_votes_count: 41,
expected_tipped_state: ProposalState::Defeated,
expected_finalized_state: ProposalState::Defeated,
}),
Just(VoteCastTestCase {
name: "100:0",
governing_token_supply: 100,
vote_threshold_percentage: 100,
yes_votes_count: 100,
no_votes_count: 0,
expected_tipped_state: ProposalState::Succeeded,
expected_finalized_state: ProposalState::Succeeded,
}),
Just(VoteCastTestCase {
name: "0:100",
governing_token_supply: 100,
vote_threshold_percentage: 100,
yes_votes_count: 0,
no_votes_count: 100,
expected_tipped_state: ProposalState::Defeated,
expected_finalized_state: ProposalState::Defeated,
}),
]
}
proptest! {
#[test]
fn test_try_tip_vote(test_case in vote_casting_test_cases()) {
let mut proposal = create_test_proposal();
proposal.yes_votes_count = test_case.yes_votes_count;
proposal.no_votes_count = test_case.no_votes_count;
proposal.state = ProposalState::Voting;
let mut governance_config = create_test_governance_config();
governance_config.vote_threshold_percentage = VoteThresholdPercentage::YesVote(test_case.vote_threshold_percentage);
let current_timestamp = 15_i64;
let realm = create_test_realm();
proposal.try_tip_vote(test_case.governing_token_supply, &governance_config,&realm,current_timestamp).unwrap();
assert_eq!(proposal.state,test_case.expected_tipped_state,"CASE: {:?}",test_case);
if test_case.expected_tipped_state != ProposalState::Voting {
assert_eq!(Some(current_timestamp),proposal.voting_completed_at)
}
}
#[test]
fn test_finalize_vote(test_case in vote_casting_test_cases()) {
let mut proposal = create_test_proposal();
proposal.yes_votes_count = test_case.yes_votes_count;
proposal.no_votes_count = test_case.no_votes_count;
proposal.state = ProposalState::Voting;
let mut governance_config = create_test_governance_config();
governance_config.vote_threshold_percentage = VoteThresholdPercentage::YesVote(test_case.vote_threshold_percentage);
let current_timestamp = 16_i64;
let realm = create_test_realm();
proposal.finalize_vote(test_case.governing_token_supply, &governance_config,&realm,current_timestamp).unwrap();
assert_eq!(proposal.state,test_case.expected_finalized_state,"CASE: {:?}",test_case);
assert_eq!(Some(current_timestamp),proposal.voting_completed_at);
}
}
prop_compose! {
fn full_vote_results()(governing_token_supply in 1..=u64::MAX, yes_vote_threshold in 1..100)(
governing_token_supply in Just(governing_token_supply),
yes_vote_threshold in Just(yes_vote_threshold),
yes_votes_count in 0..=governing_token_supply,
no_votes_count in 0..=governing_token_supply,
) -> (u64, u64, u64, u8) {
(yes_votes_count as u64, no_votes_count as u64, governing_token_supply as u64,yes_vote_threshold as u8)
}
}
proptest! {
#[test]
fn test_try_tip_vote_with_full_vote_results(
(yes_votes_count, no_votes_count, governing_token_supply, yes_vote_threshold_percentage) in full_vote_results(),
) {
let mut proposal = create_test_proposal();
proposal.yes_votes_count = yes_votes_count;
proposal.no_votes_count =no_votes_count.min(governing_token_supply-yes_votes_count);
proposal.state = ProposalState::Voting;
let mut governance_config = create_test_governance_config();
let yes_vote_threshold_percentage = VoteThresholdPercentage::YesVote(yes_vote_threshold_percentage);
governance_config.vote_threshold_percentage = yes_vote_threshold_percentage.clone();
let current_timestamp = 15_i64;
let realm = create_test_realm();
proposal.try_tip_vote(governing_token_supply, &governance_config,&realm, current_timestamp).unwrap();
let yes_vote_threshold_count = get_yes_vote_threshold_count(&yes_vote_threshold_percentage,governing_token_supply).unwrap();
if yes_votes_count >= yes_vote_threshold_count && yes_votes_count > (governing_token_supply - yes_votes_count)
{
assert_eq!(proposal.state,ProposalState::Succeeded);
} else if proposal.no_votes_count > (governing_token_supply - yes_vote_threshold_count)
|| proposal.no_votes_count >= (governing_token_supply - proposal.no_votes_count ) {
assert_eq!(proposal.state,ProposalState::Defeated);
} else {
assert_eq!(proposal.state,ProposalState::Voting);
}
}
}
proptest! {
#[test]
fn test_finalize_vote_with_full_vote_results(
(yes_votes_count, no_votes_count, governing_token_supply, yes_vote_threshold_percentage) in full_vote_results(),
) {
let mut proposal = create_test_proposal();
proposal.yes_votes_count = yes_votes_count;
proposal.no_votes_count = no_votes_count.min(governing_token_supply-yes_votes_count);
proposal.state = ProposalState::Voting;
let mut governance_config = create_test_governance_config();
let yes_vote_threshold_percentage = VoteThresholdPercentage::YesVote(yes_vote_threshold_percentage);
governance_config.vote_threshold_percentage = yes_vote_threshold_percentage.clone();
let current_timestamp = 16_i64;
let realm = create_test_realm();
proposal.finalize_vote(governing_token_supply, &governance_config,&realm,current_timestamp).unwrap();
let yes_vote_threshold_count = get_yes_vote_threshold_count(&yes_vote_threshold_percentage,governing_token_supply).unwrap();
if yes_votes_count >= yes_vote_threshold_count && yes_votes_count > proposal.no_votes_count
{
assert_eq!(proposal.state,ProposalState::Succeeded);
} else {
assert_eq!(proposal.state,ProposalState::Defeated);
}
}
}
#[test]
fn test_try_tip_vote_with_reduced_community_mint_max_vote_weight() {
let mut proposal = create_test_proposal();
proposal.yes_votes_count = 60;
proposal.no_votes_count = 10;
proposal.state = ProposalState::Voting;
let mut governance_config = create_test_governance_config();
governance_config.vote_threshold_percentage = VoteThresholdPercentage::YesVote(60);
let current_timestamp = 15_i64;
let community_token_supply = 200;
let mut realm = create_test_realm();
realm.config.community_mint_max_vote_weight_source =
MintMaxVoteWeightSource::SupplyFraction(
MintMaxVoteWeightSource::SUPPLY_FRACTION_BASE / 2,
);
proposal
.try_tip_vote(
community_token_supply,
&governance_config,
&realm,
current_timestamp,
)
.unwrap();
assert_eq!(proposal.state, ProposalState::Succeeded);
assert_eq!(proposal.max_vote_weight, Some(100));
}
#[test]
fn test_try_tip_vote_with_reduced_community_mint_max_vote_weight_and_vote_overflow() {
let mut proposal = create_test_proposal();
proposal.no_votes_count = 10;
proposal.state = ProposalState::Voting;
let mut governance_config = create_test_governance_config();
governance_config.vote_threshold_percentage = VoteThresholdPercentage::YesVote(60);
let current_timestamp = 15_i64;
let community_token_supply = 200;
let mut realm = create_test_realm();
realm.config.community_mint_max_vote_weight_source =
MintMaxVoteWeightSource::SupplyFraction(
MintMaxVoteWeightSource::SUPPLY_FRACTION_BASE / 2,
);
proposal.yes_votes_count = 120;
proposal
.try_tip_vote(
community_token_supply,
&governance_config,
&realm,
current_timestamp,
)
.unwrap();
assert_eq!(proposal.state, ProposalState::Succeeded);
assert_eq!(proposal.max_vote_weight, Some(130));
}
#[test]
fn test_try_tip_vote_for_council_vote_with_reduced_community_mint_max_vote_weight() {
let mut proposal = create_test_proposal();
proposal.yes_votes_count = 60;
proposal.no_votes_count = 10;
proposal.state = ProposalState::Voting;
let mut governance_config = create_test_governance_config();
governance_config.vote_threshold_percentage = VoteThresholdPercentage::YesVote(60);
let current_timestamp = 15_i64;
let community_token_supply = 200;
let mut realm = create_test_realm();
realm.config.community_mint_max_vote_weight_source =
MintMaxVoteWeightSource::SupplyFraction(
MintMaxVoteWeightSource::SUPPLY_FRACTION_BASE / 2,
);
realm.config.council_mint = Some(proposal.governing_token_mint);
proposal
.try_tip_vote(
community_token_supply,
&governance_config,
&realm,
current_timestamp,
)
.unwrap();
assert_eq!(proposal.state, ProposalState::Voting);
}
#[test]
fn test_finalize_vote_with_reduced_community_mint_max_vote_weight() {
let mut proposal = create_test_proposal();
proposal.yes_votes_count = 60;
proposal.no_votes_count = 10;
proposal.state = ProposalState::Voting;
let mut governance_config = create_test_governance_config();
governance_config.vote_threshold_percentage = VoteThresholdPercentage::YesVote(60);
let current_timestamp = 16_i64;
let community_token_supply = 200;
let mut realm = create_test_realm();
realm.config.community_mint_max_vote_weight_source =
MintMaxVoteWeightSource::SupplyFraction(
MintMaxVoteWeightSource::SUPPLY_FRACTION_BASE / 2,
);
proposal
.finalize_vote(
community_token_supply,
&governance_config,
&realm,
current_timestamp,
)
.unwrap();
assert_eq!(proposal.state, ProposalState::Succeeded);
assert_eq!(proposal.max_vote_weight, Some(100));
}
#[test]
fn test_finalize_vote_with_reduced_community_mint_max_vote_weight_and_vote_overflow() {
let mut proposal = create_test_proposal();
proposal.yes_votes_count = 60;
proposal.no_votes_count = 10;
proposal.state = ProposalState::Voting;
let mut governance_config = create_test_governance_config();
governance_config.vote_threshold_percentage = VoteThresholdPercentage::YesVote(60);
let current_timestamp = 16_i64;
let community_token_supply = 200;
let mut realm = create_test_realm();
realm.config.community_mint_max_vote_weight_source =
MintMaxVoteWeightSource::SupplyFraction(
MintMaxVoteWeightSource::SUPPLY_FRACTION_BASE / 2,
);
proposal.yes_votes_count = 120;
proposal
.finalize_vote(
community_token_supply,
&governance_config,
&realm,
current_timestamp,
)
.unwrap();
assert_eq!(proposal.state, ProposalState::Succeeded);
assert_eq!(proposal.max_vote_weight, Some(130));
}
#[test]
pub fn test_finalize_vote_with_expired_voting_time_error() {
let mut proposal = create_test_proposal();
proposal.state = ProposalState::Voting;
let governance_config = create_test_governance_config();
let current_timestamp =
proposal.voting_at.unwrap() + governance_config.max_voting_time as i64;
let realm = create_test_realm();
let err = proposal
.finalize_vote(100, &governance_config, &realm, current_timestamp)
.err()
.unwrap();
assert_eq!(err, GovernanceError::CannotFinalizeVotingInProgress.into());
}
#[test]
pub fn test_finalize_vote_after_voting_time() {
let mut proposal = create_test_proposal();
proposal.state = ProposalState::Voting;
let governance_config = create_test_governance_config();
let current_timestamp =
proposal.voting_at.unwrap() + governance_config.max_voting_time as i64 + 1;
let realm = create_test_realm();
let result = proposal.finalize_vote(100, &governance_config, &realm, current_timestamp);
assert_eq!(result, Ok(()));
}
#[test]
pub fn test_assert_can_vote_with_expired_voting_time_error() {
let mut proposal = create_test_proposal();
proposal.state = ProposalState::Voting;
let governance_config = create_test_governance_config();
let current_timestamp =
proposal.voting_at.unwrap() + governance_config.max_voting_time as i64 + 1;
let err = proposal
.assert_can_cast_vote(&governance_config, current_timestamp)
.err()
.unwrap();
assert_eq!(err, GovernanceError::ProposalVotingTimeExpired.into());
}
#[test]
pub fn test_assert_can_vote_within_voting_time() {
let mut proposal = create_test_proposal();
proposal.state = ProposalState::Voting;
let governance_config = create_test_governance_config();
let current_timestamp =
proposal.voting_at.unwrap() + governance_config.max_voting_time as i64;
let result = proposal.assert_can_cast_vote(&governance_config, current_timestamp);
assert_eq!(result, Ok(()));
}
}