Governance: Voting workflow

Co-authored-by: Jon Cinque <jon.cinque@gmail.com>
This commit is contained in:
Sebastian Bor 2021-05-29 22:51:48 +01:00 committed by GitHub
parent b8c2968cd1
commit f59e43757b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
39 changed files with 3218 additions and 315 deletions

View File

@ -35,21 +35,21 @@ pub enum GovernanceError {
#[error("Governing Token Owner or Delegate must sign transaction")]
GoverningTokenOwnerOrDelegateMustSign,
/// All active votes must be relinquished to withdraw governing tokens
#[error("All active votes must be relinquished to withdraw governing tokens")]
CannotWithdrawGoverningTokensWhenActiveVotesExist,
/// All votes must be relinquished to withdraw governing tokens
#[error("All votes must be relinquished to withdraw governing tokens")]
AllVotesMustBeRelinquishedToWithdrawGoverningTokens,
/// Invalid Token Owner Record account address
#[error("Invalid Token Owner Record account address")]
InvalidTokenOwnerRecordAccountAddress,
/// Invalid Token Owner Record Governing mint
#[error("Invalid Token Owner Record Governing mint")]
InvalidTokenOwnerRecordGoverningMint,
/// Invalid GoverningMint for TokenOwnerRecord
#[error("Invalid GoverningMint for TokenOwnerRecord")]
InvalidGoverningMintForTokenOwnerRecord,
/// Invalid Token Owner Record Realm
#[error("Invalid Token Owner Record Realm")]
InvalidTokenOwnerRecordRealm,
/// Invalid Realm for TokenOwnerRecord
#[error("Invalid Realm for TokenOwnerRecord")]
InvalidRealmForTokenOwnerRecord,
/// Invalid Signatory account address
#[error("Invalid Signatory account address")]
@ -67,6 +67,14 @@ pub enum GovernanceError {
#[error("Invalid Proposal Owner")]
InvalidProposalOwnerAccount,
/// Invalid Proposal for VoterRecord
#[error("Invalid Proposal for VoterRecord")]
InvalidProposalForVoterRecord,
/// Invalid GoverningTokenOwner for VoteRecord
#[error("Invalid GoverningTokenOwner for VoteRecord")]
InvalidGoverningTokenOwnerForVoteRecord,
/// Invalid Governance config
#[error("Invalid Governance config")]
InvalidGovernanceConfig,
@ -75,6 +83,10 @@ pub enum GovernanceError {
#[error("Proposal for the given Governance, Governing Token Mint and index already exists")]
ProposalAlreadyExists,
/// Token Owner already voted on the Proposal
#[error("Token Owner already voted on the Proposal")]
VoteAlreadyExists,
/// Owner doesn't have enough governing tokens to create Proposal
#[error("Owner doesn't have enough governing tokens to create Proposal")]
NotEnoughTokensToCreateProposal,
@ -83,10 +95,38 @@ pub enum GovernanceError {
#[error("Invalid State: Can't edit Signatories")]
InvalidStateCannotEditSignatories,
/// Invalid Proposal state
#[error("Invalid Proposal state")]
InvalidProposalState,
/// Invalid State: Can't sign off
#[error("Invalid State: Can't sign off")]
InvalidStateCannotSignOff,
/// Invalid State: Can't vote
#[error("Invalid State: Can't vote")]
InvalidStateCannotVote,
/// Invalid State: Can't finalize vote
#[error("Invalid State: Can't finalize vote")]
InvalidStateCannotFinalize,
/// Invalid State: Can't cancel Proposal
#[error("Invalid State: Can't cancel Proposal")]
InvalidStateCannotCancelProposal,
/// Vote already relinquished
#[error("Vote already relinquished")]
VoteAlreadyRelinquished,
/// Can't finalize vote. Voting still in progress
#[error("Can't finalize vote. Voting still in progress")]
CannotFinalizeVotingInProgress,
/// Proposal voting time expired
#[error("Proposal voting time expired")]
ProposalVotingTimeExpired,
/// Invalid Signatory Mint
#[error("Invalid Signatory Mint")]
InvalidSignatoryMint,
@ -101,11 +141,39 @@ pub enum GovernanceError {
#[error("Invalid Account type")]
InvalidAccountType,
/// ---- Token Tools Errors ----
/// Proposal does not belong to the given Governance
#[error("Proposal does not belong to the given Governance")]
InvalidGovernanceForProposal,
/// Proposal does not belong to given Governing Mint"
#[error("Proposal does not belong to given Governing Mint")]
InvalidGoverningMintForProposal,
/// ---- SPL Token Tools Errors ----
/// Invalid Token account owner
#[error("Invalid Token account owner")]
InvalidTokenAccountOwner,
SplTokenAccountWithInvalidOwner,
/// Invalid Mint account owner
#[error("Invalid Mint account owner")]
SplTokenMintWithInvalidOwner,
/// Token Account is not initialized
#[error("Token Account is not initialized")]
SplTokenAccountNotInitialized,
/// Token account data is invalid
#[error("Token account data is invalid")]
SplTokenInvalidTokenAccountData,
/// Token mint account data is invalid
#[error("Token mint account data is invalid")]
SplTokenInvalidMintAccountData,
/// Token Mint is not initialized
#[error("Token Mint account is not initialized")]
SplTokenMintNotInitialized,
/// ---- Bpf Upgradable Loader Tools Errors ----

View File

@ -11,6 +11,7 @@ use crate::{
signatory_record::get_signatory_record_address,
single_signer_instruction::InstructionData,
token_owner_record::get_token_owner_record_address,
vote_record::get_vote_record_address,
},
tools::bpf_loader_upgradeable::get_program_data_address,
};
@ -178,7 +179,6 @@ pub enum GovernanceInstruction {
/// 2. `[signer]` Governance Authority (Token Owner or Governance Delegate)
/// 3. `[writable]` Signatory Record Account
/// 4. `[writable]` Beneficiary Account which would receive lamports from the disposed Signatory Record Account
/// 5. `[]` Clock sysvar
RemoveSignatory {
#[allow(dead_code)]
/// Signatory to remove from the Proposal
@ -222,10 +222,11 @@ pub enum GovernanceInstruction {
hold_up_time: u64,
},
/// Cancels Proposal and moves it into Canceled
/// Cancels Proposal by changing its state to Canceled
///
/// 0. `[writable]` Proposal account
/// 1. `[signer]` Governance Authority (Token Owner or Governance Delegate)
/// 1. `[]` Token Owner Record account. PDA seeds: ['governance',realm, governing_token_mint, governing_token_owner]
/// 2 `[signer]` Governance Authority (Token Owner or Governance Delegate)
CancelProposal,
/// Signs off Proposal indicating the Signatory approves the Proposal
@ -241,26 +242,45 @@ pub enum GovernanceInstruction {
/// By doing so you indicate you approve or disapprove of running the Proposal set of instructions
/// If you tip the consensus then the instructions can begin to be run after their hold up time
///
/// 0. `[writable]` Proposal account
/// 1. `[writable]` Token Owner Record account. PDA seeds: ['governance',realm, governing_token_mint, governing_token_owner]
/// 2. `[writable]` Proposal Vote Record account. PDA seeds: ['governance',proposal,governing_token_owner]
/// 3. `[signer]` Governance Authority account
/// 4. `[]` Governance account
Vote {
/// 0. `[]` Governance account
/// 1. `[writable]` Proposal account
/// 2. `[writable]` Token Owner Record account. PDA seeds: ['governance',realm, governing_token_mint, governing_token_owner]
/// 3. `[signer]` Governance Authority (Token Owner or Governance Delegate)
/// 4. `[writable]` Proposal VoteRecord account. PDA seeds: ['governance',proposal,governing_token_owner_record]
/// 5. `[]` Governing Token Mint
/// 6. `[signer]` Payer
/// 7. `[]` System program
/// 8. `[]` Rent sysvar
/// 9. `[]` Clock sysvar
CastVote {
#[allow(dead_code)]
/// Yes/No vote
vote: Vote,
},
/// Finalizes vote in case the Vote was not automatically tipped within max_voting_time period
///
/// 0. `[]` Governance account
/// 1. `[writable]` Proposal account
/// 2. `[]` Governing Token Mint
/// 3. `[]` Clock sysvar
FinalizeVote {},
/// Relinquish Vote removes voter weight from a Proposal and removes it from voter's active votes
/// If the Proposal is still being voted on then the voter's weight won't count towards the vote outcome
/// If the Proposal is already in decided state then the instruction has no impact on the Proposal
/// and only allows voters to prune their outstanding votes in case they wanted to withdraw Governing tokens from the Realm
///
/// 0. `[writable]` Proposal account
/// 1. `[writable]` Token Owner Record account. PDA seeds: ['governance',realm, governing_token_mint, governing_token_owner]
/// 2. `[writable]` Proposal Vote Record account. PDA seeds: ['governance',proposal,governing_token_owner]
/// 3. `[signer]` Governance Authority account
/// 0. `[]` Governance account
/// 1. `[writable]` Proposal account
/// 2. `[writable]` TokenOwnerRecord account. PDA seeds: ['governance',realm, governing_token_mint, governing_token_owner]
/// 3. `[writable]` Proposal VoteRecord account. PDA seeds: ['governance',proposal,governing_token_owner_record]
/// 4. `[]` Governing Token Mint
/// 5. `[signer]` Optional Governance Authority (Token Owner or Governance Delegate)
/// It's required only when Proposal is still being voted on
/// 6. `[writable]` Optional Beneficiary account which would receive lamports when VoteRecord Account is disposed
/// It's required only when Proposal is still being voted on
RelinquishVote,
/// Executes an instruction in the Proposal
@ -494,7 +514,7 @@ pub fn create_proposal(
name: String,
description_link: String,
governing_token_mint: &Pubkey,
proposal_index: u16,
proposal_index: u32,
) -> Instruction {
let proposal_address = get_proposal_address(
governance,
@ -578,7 +598,6 @@ pub fn remove_signatory(
AccountMeta::new_readonly(*governance_authority, true),
AccountMeta::new(signatory_record_address, false),
AccountMeta::new(*beneficiary, false),
AccountMeta::new_readonly(sysvar::clock::id(), false),
];
let instruction = GovernanceInstruction::RemoveSignatory {
@ -615,3 +634,118 @@ pub fn sign_off_proposal(
data: instruction.try_to_vec().unwrap(),
}
}
/// Creates CastVote instruction
pub fn cast_vote(
// Accounts
governance: &Pubkey,
proposal: &Pubkey,
token_owner_record: &Pubkey,
governance_authority: &Pubkey,
governing_token_mint: &Pubkey,
payer: &Pubkey,
// Args
vote: Vote,
) -> Instruction {
let vote_record_address = get_vote_record_address(&proposal, &token_owner_record);
let accounts = vec![
AccountMeta::new_readonly(*governance, false),
AccountMeta::new(*proposal, false),
AccountMeta::new(*token_owner_record, false),
AccountMeta::new_readonly(*governance_authority, true),
AccountMeta::new(vote_record_address, false),
AccountMeta::new_readonly(*governing_token_mint, false),
AccountMeta::new_readonly(*payer, true),
AccountMeta::new_readonly(system_program::id(), false),
AccountMeta::new_readonly(sysvar::rent::id(), false),
AccountMeta::new_readonly(sysvar::clock::id(), false),
];
let instruction = GovernanceInstruction::CastVote { vote };
Instruction {
program_id: id(),
accounts,
data: instruction.try_to_vec().unwrap(),
}
}
/// Creates FinalizeVote instruction
pub fn finalize_vote(
// Accounts
governance: &Pubkey,
proposal: &Pubkey,
governing_token_mint: &Pubkey,
) -> Instruction {
let accounts = vec![
AccountMeta::new_readonly(*governance, false),
AccountMeta::new(*proposal, false),
AccountMeta::new_readonly(*governing_token_mint, false),
AccountMeta::new_readonly(sysvar::clock::id(), false),
];
let instruction = GovernanceInstruction::FinalizeVote {};
Instruction {
program_id: id(),
accounts,
data: instruction.try_to_vec().unwrap(),
}
}
/// Creates RelinquishVote instruction
pub fn relinquish_vote(
// Accounts
governance: &Pubkey,
proposal: &Pubkey,
token_owner_record: &Pubkey,
governing_token_mint: &Pubkey,
governance_authority: Option<Pubkey>,
beneficiary: Option<Pubkey>,
) -> Instruction {
let vote_record_address = get_vote_record_address(&proposal, &token_owner_record);
let mut accounts = vec![
AccountMeta::new_readonly(*governance, false),
AccountMeta::new(*proposal, false),
AccountMeta::new(*token_owner_record, false),
AccountMeta::new(vote_record_address, false),
AccountMeta::new_readonly(*governing_token_mint, false),
];
if let Some(governance_authority) = governance_authority {
accounts.push(AccountMeta::new_readonly(governance_authority, true));
accounts.push(AccountMeta::new(beneficiary.unwrap(), false));
}
let instruction = GovernanceInstruction::RelinquishVote {};
Instruction {
program_id: id(),
accounts,
data: instruction.try_to_vec().unwrap(),
}
}
/// Creates CancelProposal instruction
pub fn cancel_proposal(
// Accounts
proposal: &Pubkey,
token_owner_record: &Pubkey,
governance_authority: &Pubkey,
) -> Instruction {
let accounts = vec![
AccountMeta::new(*proposal, false),
AccountMeta::new_readonly(*token_owner_record, false),
AccountMeta::new_readonly(*governance_authority, true),
];
let instruction = GovernanceInstruction::CancelProposal {};
Instruction {
program_id: id(),
accounts,
data: instruction.try_to_vec().unwrap(),
}
}

View File

@ -1,11 +1,15 @@
//! Program processor
mod process_add_signatory;
mod process_cancel_proposal;
mod process_cast_vote;
mod process_create_account_governance;
mod process_create_program_governance;
mod process_create_proposal;
mod process_create_realm;
mod process_deposit_governing_tokens;
mod process_finalize_vote;
mod process_relinquish_vote;
mod process_remove_signatory;
mod process_set_governance_delegate;
mod process_sign_off_proposal;
@ -15,11 +19,15 @@ use crate::instruction::GovernanceInstruction;
use borsh::BorshDeserialize;
use process_add_signatory::*;
use process_cancel_proposal::*;
use process_cast_vote::*;
use process_create_account_governance::*;
use process_create_program_governance::*;
use process_create_proposal::*;
use process_create_realm::*;
use process_deposit_governing_tokens::*;
use process_finalize_vote::*;
use process_relinquish_vote::*;
use process_remove_signatory::*;
use process_set_governance_delegate::*;
use process_sign_off_proposal::*;
@ -57,6 +65,7 @@ pub fn process_instruction(
GovernanceInstruction::SetGovernanceDelegate {
new_governance_delegate,
} => process_set_governance_delegate(accounts, &new_governance_delegate),
GovernanceInstruction::CreateProgramGovernance {
config,
transfer_upgrade_authority,
@ -66,9 +75,11 @@ pub fn process_instruction(
config,
transfer_upgrade_authority,
),
GovernanceInstruction::CreateAccountGovernance { config } => {
process_create_account_governance(program_id, accounts, config)
}
GovernanceInstruction::CreateProposal {
name,
description_link,
@ -89,6 +100,14 @@ pub fn process_instruction(
GovernanceInstruction::SignOffProposal {} => {
process_sign_off_proposal(program_id, accounts)
}
GovernanceInstruction::CastVote { vote } => process_cast_vote(program_id, accounts, vote),
GovernanceInstruction::FinalizeVote {} => process_finalize_vote(program_id, accounts),
GovernanceInstruction::RelinquishVote {} => process_relinquish_vote(program_id, accounts),
GovernanceInstruction::CancelProposal {} => process_cancel_proposal(program_id, accounts),
_ => todo!("Instruction not implemented yet"),
}
}

View File

@ -12,14 +12,11 @@ use solana_program::{
use crate::{
state::{
enums::GovernanceAccountType,
proposal::deserialize_proposal_raw,
proposal::get_proposal_data,
signatory_record::{get_signatory_record_address_seeds, SignatoryRecord},
token_owner_record::deserialize_token_owner_record_for_proposal_owner,
},
tools::{
account::create_and_serialize_account_signed,
asserts::assert_token_owner_or_delegate_is_signer,
token_owner_record::get_token_owner_record_data_for_proposal_owner,
},
tools::account::create_and_serialize_account_signed,
};
/// Processes AddSignatory instruction
@ -42,15 +39,15 @@ pub fn process_add_signatory(
let rent_sysvar_info = next_account_info(account_info_iter)?; // 6
let rent = &Rent::from_account_info(rent_sysvar_info)?;
let mut proposal_data = deserialize_proposal_raw(proposal_info)?;
let mut proposal_data = get_proposal_data(proposal_info)?;
proposal_data.assert_can_edit_signatories()?;
let token_owner_record_data = deserialize_token_owner_record_for_proposal_owner(
let token_owner_record_data = get_token_owner_record_data_for_proposal_owner(
token_owner_record_info,
&proposal_data.token_owner_record,
)?;
assert_token_owner_or_delegate_is_signer(&token_owner_record_data, governance_authority_info)?;
token_owner_record_data.assert_token_owner_or_delegate_is_signer(governance_authority_info)?;
let signatory_record_data = SignatoryRecord {
account_type: GovernanceAccountType::SignatoryRecord,

View File

@ -0,0 +1,37 @@
//! Program state processor
use borsh::BorshSerialize;
use solana_program::{
account_info::{next_account_info, AccountInfo},
entrypoint::ProgramResult,
pubkey::Pubkey,
};
use crate::state::{
enums::ProposalState, proposal::get_proposal_data,
token_owner_record::get_token_owner_record_data_for_proposal_owner,
};
/// Processes CancelProposal instruction
pub fn process_cancel_proposal(_program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult {
let account_info_iter = &mut accounts.iter();
let proposal_info = next_account_info(account_info_iter)?; // 0
let token_owner_record_info = next_account_info(account_info_iter)?; // 1
let governance_authority_info = next_account_info(account_info_iter)?; // 2
let mut proposal_data = get_proposal_data(proposal_info)?;
proposal_data.assert_can_cancel()?;
let token_owner_record_data = get_token_owner_record_data_for_proposal_owner(
token_owner_record_info,
&proposal_data.token_owner_record,
)?;
token_owner_record_data.assert_token_owner_or_delegate_is_signer(governance_authority_info)?;
proposal_data.state = ProposalState::Cancelled;
proposal_data.serialize(&mut *proposal_info.data.borrow_mut())?;
Ok(())
}

View File

@ -0,0 +1,130 @@
//! Program state processor
use solana_program::{
account_info::{next_account_info, AccountInfo},
clock::Clock,
entrypoint::ProgramResult,
pubkey::Pubkey,
rent::Rent,
sysvar::Sysvar,
};
use crate::{
error::GovernanceError,
instruction::Vote,
state::{
enums::{GovernanceAccountType, VoteWeight},
governance::get_governance_data,
proposal::get_proposal_data_for_governance_and_governing_mint,
token_owner_record::get_token_owner_record_data_for_realm_and_governing_mint,
vote_record::{get_vote_record_address_seeds, VoteRecord},
},
tools::{account::create_and_serialize_account_signed, spl_token::get_spl_token_mint_supply},
};
use borsh::BorshSerialize;
/// Processes CastVote instruction
pub fn process_cast_vote(
program_id: &Pubkey,
accounts: &[AccountInfo],
vote: Vote,
) -> ProgramResult {
let account_info_iter = &mut accounts.iter();
let governance_info = next_account_info(account_info_iter)?; // 0
let proposal_info = next_account_info(account_info_iter)?; // 1
let token_owner_record_info = next_account_info(account_info_iter)?; // 2
let governance_authority_info = next_account_info(account_info_iter)?; // 3
let vote_record_info = next_account_info(account_info_iter)?; // 4
let governing_token_mint_info = next_account_info(account_info_iter)?; // 5
let payer_info = next_account_info(account_info_iter)?; // 6
let system_info = next_account_info(account_info_iter)?; // 7
let rent_sysvar_info = next_account_info(account_info_iter)?; // 8
let rent = &Rent::from_account_info(rent_sysvar_info)?;
let clock_info = next_account_info(account_info_iter)?; // 9
let clock = Clock::from_account_info(clock_info)?;
if !vote_record_info.data_is_empty() {
return Err(GovernanceError::VoteAlreadyExists.into());
}
let governance_data = get_governance_data(governance_info)?;
let mut proposal_data = get_proposal_data_for_governance_and_governing_mint(
&proposal_info,
governance_info.key,
governing_token_mint_info.key,
)?;
proposal_data.assert_can_cast_vote(&governance_data.config, clock.slot)?;
let mut token_owner_record_data = get_token_owner_record_data_for_realm_and_governing_mint(
&token_owner_record_info,
&governance_data.config.realm,
governing_token_mint_info.key,
)?;
token_owner_record_data.assert_token_owner_or_delegate_is_signer(governance_authority_info)?;
// Update TokenOwnerRecord vote counts
token_owner_record_data.unrelinquished_votes_count = token_owner_record_data
.unrelinquished_votes_count
.checked_add(1)
.unwrap();
token_owner_record_data.total_votes_count = token_owner_record_data
.total_votes_count
.checked_add(1)
.unwrap();
token_owner_record_data.serialize(&mut *token_owner_record_info.data.borrow_mut())?;
let vote_amount = token_owner_record_data.governing_token_deposit_amount;
// Calculate Proposal voting weights
let vote_weight = match vote {
Vote::Yes => {
proposal_data.yes_votes_count = proposal_data
.yes_votes_count
.checked_add(vote_amount)
.unwrap();
VoteWeight::Yes(vote_amount)
}
Vote::No => {
proposal_data.no_votes_count = proposal_data
.no_votes_count
.checked_add(vote_amount)
.unwrap();
VoteWeight::No(vote_amount)
}
};
let governing_token_supply = get_spl_token_mint_supply(&governing_token_mint_info)?;
proposal_data.try_tip_vote(governing_token_supply, &governance_data.config, clock.slot);
proposal_data.serialize(&mut *proposal_info.data.borrow_mut())?;
// Create and serialize VoteRecord
let vote_record_data = VoteRecord {
account_type: GovernanceAccountType::VoteRecord,
proposal: *proposal_info.key,
governing_token_owner: token_owner_record_data.governing_token_owner,
vote_weight,
is_relinquished: false,
};
create_and_serialize_account_signed::<VoteRecord>(
payer_info,
vote_record_info,
&vote_record_data,
&get_vote_record_address_seeds(proposal_info.key, token_owner_record_info.key),
program_id,
system_info,
rent,
)?;
Ok(())
}

View File

@ -14,14 +14,11 @@ use crate::{
error::GovernanceError,
state::{
enums::{GovernanceAccountType, ProposalState},
governance::deserialize_governance_raw,
governance::get_governance_data,
proposal::{get_proposal_address_seeds, Proposal},
token_owner_record::deserialize_token_owner_record_for_realm_and_governing_mint,
},
tools::{
account::create_and_serialize_account_signed,
asserts::assert_token_owner_or_delegate_is_signer,
token_owner_record::get_token_owner_record_data_for_realm_and_governing_mint,
},
tools::account::create_and_serialize_account_signed,
};
/// Processes CreateProposal instruction
@ -53,16 +50,16 @@ pub fn process_create_proposal(
return Err(GovernanceError::ProposalAlreadyExists.into());
}
let mut governance_data = deserialize_governance_raw(governance_info)?;
let mut governance_data = get_governance_data(governance_info)?;
let token_owner_record_data = deserialize_token_owner_record_for_realm_and_governing_mint(
let token_owner_record_data = get_token_owner_record_data_for_realm_and_governing_mint(
&token_owner_record_info,
&governance_data.config.realm,
&governing_token_mint,
)?;
// proposal_owner must be either governing token owner or governance_delegate and must sign this transaction
assert_token_owner_or_delegate_is_signer(&token_owner_record_data, governance_authority_info)?;
token_owner_record_data.assert_token_owner_or_delegate_is_signer(governance_authority_info)?;
if token_owner_record_data.governing_token_deposit_amount
< governance_data.config.min_tokens_to_create_proposal as u64
@ -92,6 +89,9 @@ pub fn process_create_proposal(
number_of_executed_instructions: 0,
number_of_instructions: 0,
yes_votes_count: 0,
no_votes_count: 0,
};
create_and_serialize_account_signed::<Proposal>(

View File

@ -14,7 +14,9 @@ use crate::{
enums::GovernanceAccountType,
realm::{get_governing_token_holding_address_seeds, get_realm_address_seeds, Realm},
},
tools::{account::create_and_serialize_account_signed, token::create_spl_token_account_signed},
tools::{
account::create_and_serialize_account_signed, spl_token::create_spl_token_account_signed,
},
};
/// Processes CreateRealm instruction

View File

@ -13,16 +13,16 @@ use crate::{
error::GovernanceError,
state::{
enums::GovernanceAccountType,
realm::deserialize_realm_raw,
realm::get_realm_data,
token_owner_record::{
deserialize_token_owner_record, get_token_owner_record_address_seeds, TokenOwnerRecord,
get_token_owner_record_address_seeds, get_token_owner_record_data_for_seeds,
TokenOwnerRecord,
},
},
tools::{
account::create_and_serialize_account_signed,
token::{
get_amount_from_token_account, get_mint_from_token_account,
get_owner_from_token_account, transfer_spl_tokens,
spl_token::{
get_spl_token_amount, get_spl_token_mint, get_spl_token_owner, transfer_spl_tokens,
},
},
};
@ -47,12 +47,12 @@ pub fn process_deposit_governing_tokens(
let rent_sysvar_info = next_account_info(account_info_iter)?; // 9
let rent = &Rent::from_account_info(rent_sysvar_info)?;
let realm_data = deserialize_realm_raw(realm_info)?;
let governing_token_mint = get_mint_from_token_account(governing_token_holding_info)?;
let realm_data = get_realm_data(realm_info)?;
let governing_token_mint = get_spl_token_mint(governing_token_holding_info)?;
realm_data.assert_is_valid_governing_token_mint(&governing_token_mint)?;
let amount = get_amount_from_token_account(governing_token_source_info)?;
let amount = get_spl_token_amount(governing_token_source_info)?;
transfer_spl_tokens(
&governing_token_source_info,
@ -70,7 +70,7 @@ pub fn process_deposit_governing_tokens(
if token_owner_record_info.data_is_empty() {
// Deposited tokens can only be withdrawn by the owner so let's make sure the owner signed the transaction
let governing_token_owner = get_owner_from_token_account(&governing_token_source_info)?;
let governing_token_owner = get_spl_token_owner(&governing_token_source_info)?;
if !(governing_token_owner == *governing_token_owner_info.key
&& governing_token_owner_info.is_signer)
@ -85,7 +85,7 @@ pub fn process_deposit_governing_tokens(
governing_token_deposit_amount: amount,
governing_token_mint,
governance_delegate: None,
active_votes_count: 0,
unrelinquished_votes_count: 0,
total_votes_count: 0,
};
@ -99,7 +99,7 @@ pub fn process_deposit_governing_tokens(
rent,
)?;
} else {
let mut token_owner_record_data = deserialize_token_owner_record(
let mut token_owner_record_data = get_token_owner_record_data_for_seeds(
token_owner_record_info,
&token_owner_record_address_seeds,
)?;

View File

@ -0,0 +1,48 @@
//! Program state processor
use solana_program::{
account_info::{next_account_info, AccountInfo},
clock::Clock,
entrypoint::ProgramResult,
pubkey::Pubkey,
sysvar::Sysvar,
};
use crate::{
state::{
governance::get_governance_data,
proposal::get_proposal_data_for_governance_and_governing_mint,
},
tools::spl_token::get_spl_token_mint_supply,
};
use borsh::BorshSerialize;
/// Processes FinalizeVote instruction
pub fn process_finalize_vote(_program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult {
let account_info_iter = &mut accounts.iter();
let governance_info = next_account_info(account_info_iter)?; // 0
let proposal_info = next_account_info(account_info_iter)?; // 1
let governing_token_mint_info = next_account_info(account_info_iter)?; // 2
let clock_info = next_account_info(account_info_iter)?; // 3
let clock = Clock::from_account_info(clock_info)?;
let governance_data = get_governance_data(governance_info)?;
let mut proposal_data = get_proposal_data_for_governance_and_governing_mint(
&proposal_info,
governance_info.key,
governing_token_mint_info.key,
)?;
let governing_token_supply = get_spl_token_mint_supply(&governing_token_mint_info)?;
proposal_data.finalize_vote(governing_token_supply, &governance_data.config, clock.slot)?;
proposal_data.serialize(&mut *proposal_info.data.borrow_mut())?;
Ok(())
}

View File

@ -0,0 +1,100 @@
//! Program state processor
use solana_program::{
account_info::{next_account_info, AccountInfo},
entrypoint::ProgramResult,
pubkey::Pubkey,
};
use crate::{
state::{
enums::{ProposalState, VoteWeight},
governance::get_governance_data,
proposal::get_proposal_data_for_governance_and_governing_mint,
token_owner_record::get_token_owner_record_data_for_realm_and_governing_mint,
vote_record::get_vote_record_data_for_proposal_and_token_owner,
},
tools::account::dispose_account,
};
use borsh::BorshSerialize;
/// Processes RelinquishVote instruction
pub fn process_relinquish_vote(_program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult {
let account_info_iter = &mut accounts.iter();
let governance_info = next_account_info(account_info_iter)?; // 0
let proposal_info = next_account_info(account_info_iter)?; // 1
let token_owner_record_info = next_account_info(account_info_iter)?; // 2
let vote_record_info = next_account_info(account_info_iter)?; // 3
let governing_token_mint_info = next_account_info(account_info_iter)?; // 4
let governance_data = get_governance_data(governance_info)?;
let mut proposal_data = get_proposal_data_for_governance_and_governing_mint(
&proposal_info,
governance_info.key,
governing_token_mint_info.key,
)?;
let mut token_owner_record_data = get_token_owner_record_data_for_realm_and_governing_mint(
&token_owner_record_info,
&governance_data.config.realm,
governing_token_mint_info.key,
)?;
let mut vote_record_data = get_vote_record_data_for_proposal_and_token_owner(
vote_record_info,
proposal_info.key,
&token_owner_record_data.governing_token_owner,
)?;
vote_record_data.assert_can_relinquish_vote()?;
// If the Proposal is still being voted on then the token owner vote won't count towards the outcome
if proposal_data.state == ProposalState::Voting {
let governance_authority_info = next_account_info(account_info_iter)?; // 5
let beneficiary_info = next_account_info(account_info_iter)?; // 6
// Note: It's only required to sign by governing_authority if relinquishing the vote results in vote change
// If the Proposal is already decided then anybody can prune active votes for token owner
token_owner_record_data
.assert_token_owner_or_delegate_is_signer(governance_authority_info)?;
match vote_record_data.vote_weight {
VoteWeight::Yes(vote_amount) => {
proposal_data.yes_votes_count = proposal_data
.yes_votes_count
.checked_sub(vote_amount)
.unwrap();
}
VoteWeight::No(vote_amount) => {
proposal_data.no_votes_count = proposal_data
.no_votes_count
.checked_sub(vote_amount)
.unwrap();
}
};
proposal_data.serialize(&mut *proposal_info.data.borrow_mut())?;
dispose_account(vote_record_info, beneficiary_info);
token_owner_record_data.total_votes_count = token_owner_record_data
.total_votes_count
.checked_sub(1)
.unwrap();
} else {
vote_record_data.is_relinquished = true;
vote_record_data.serialize(&mut *vote_record_info.data.borrow_mut())?;
}
// If the Proposal has been already voted on then we only have to decrease unrelinquished_votes_count
token_owner_record_data.unrelinquished_votes_count = token_owner_record_data
.unrelinquished_votes_count
.checked_sub(1)
.unwrap();
token_owner_record_data.serialize(&mut *token_owner_record_info.data.borrow_mut())?;
Ok(())
}

View File

@ -3,19 +3,16 @@
use borsh::BorshSerialize;
use solana_program::{
account_info::{next_account_info, AccountInfo},
clock::Clock,
entrypoint::ProgramResult,
pubkey::Pubkey,
sysvar::Sysvar,
};
use crate::{
state::{
enums::ProposalState, proposal::deserialize_proposal_raw,
signatory_record::deserialize_signatory_record,
token_owner_record::deserialize_token_owner_record_for_proposal_owner,
proposal::get_proposal_data, signatory_record::get_signatory_record_data_for_seeds,
token_owner_record::get_token_owner_record_data_for_proposal_owner,
},
tools::{account::dispose_account, asserts::assert_token_owner_or_delegate_is_signer},
tools::account::dispose_account,
};
/// Processes RemoveSignatory instruction
@ -33,33 +30,22 @@ pub fn process_remove_signatory(
let signatory_record_info = next_account_info(account_info_iter)?; // 3
let beneficiary_info = next_account_info(account_info_iter)?; // 4
let clock_info = next_account_info(account_info_iter)?; // 5
let clock = Clock::from_account_info(clock_info)?;
let mut proposal_data = deserialize_proposal_raw(proposal_info)?;
let mut proposal_data = get_proposal_data(proposal_info)?;
proposal_data.assert_can_edit_signatories()?;
let token_owner_record_data = deserialize_token_owner_record_for_proposal_owner(
let token_owner_record_data = get_token_owner_record_data_for_proposal_owner(
token_owner_record_info,
&proposal_data.token_owner_record,
)?;
assert_token_owner_or_delegate_is_signer(&token_owner_record_data, governance_authority_info)?;
token_owner_record_data.assert_token_owner_or_delegate_is_signer(governance_authority_info)?;
let signatory_record_data =
deserialize_signatory_record(signatory_record_info, proposal_info.key, &signatory)?;
get_signatory_record_data_for_seeds(signatory_record_info, proposal_info.key, &signatory)?;
signatory_record_data.assert_can_remove_signatory()?;
proposal_data.signatories_count = proposal_data.signatories_count.checked_sub(1).unwrap();
// If all the remaining signatories signed already then we can start voting
if proposal_data.signatories_count > 0
&& proposal_data.signatories_signed_off_count == proposal_data.signatories_count
{
proposal_data.voting_at = Some(clock.slot);
proposal_data.state = ProposalState::Voting;
}
proposal_data.serialize(&mut *proposal_info.data.borrow_mut())?;
dispose_account(signatory_record_info, beneficiary_info);

View File

@ -7,10 +7,7 @@ use solana_program::{
pubkey::Pubkey,
};
use crate::{
state::token_owner_record::deserialize_token_owner_record_raw,
tools::asserts::assert_token_owner_or_delegate_is_signer,
};
use crate::state::token_owner_record::get_token_owner_record_data;
/// Processes SetGovernanceDelegate instruction
pub fn process_set_governance_delegate(
@ -22,9 +19,9 @@ pub fn process_set_governance_delegate(
let governance_authority_info = next_account_info(account_info_iter)?; // 0
let token_owner_record_info = next_account_info(account_info_iter)?; // 1
let mut token_owner_record_data = deserialize_token_owner_record_raw(token_owner_record_info)?;
let mut token_owner_record_data = get_token_owner_record_data(token_owner_record_info)?;
assert_token_owner_or_delegate_is_signer(&token_owner_record_data, &governance_authority_info)?;
token_owner_record_data.assert_token_owner_or_delegate_is_signer(&governance_authority_info)?;
token_owner_record_data.governance_delegate = *new_governance_delegate;
token_owner_record_data.serialize(&mut *token_owner_record_info.data.borrow_mut())?;

View File

@ -10,8 +10,8 @@ use solana_program::{
};
use crate::state::{
enums::ProposalState, proposal::deserialize_proposal_raw,
signatory_record::deserialize_signatory_record,
enums::ProposalState, proposal::get_proposal_data,
signatory_record::get_signatory_record_data_for_seeds,
};
/// Processes SignOffProposal instruction
@ -26,11 +26,14 @@ pub fn process_sign_off_proposal(_program_id: &Pubkey, accounts: &[AccountInfo])
let clock_info = next_account_info(account_info_iter)?; // 3
let clock = Clock::from_account_info(clock_info)?;
let mut proposal_data = deserialize_proposal_raw(proposal_info)?;
let mut proposal_data = get_proposal_data(proposal_info)?;
proposal_data.assert_can_sign_off()?;
let mut signatory_record_data =
deserialize_signatory_record(signatory_record_info, proposal_info.key, signatory_info.key)?;
let mut signatory_record_data = get_signatory_record_data_for_seeds(
signatory_record_info,
proposal_info.key,
signatory_info.key,
)?;
signatory_record_data.assert_can_sign_off(signatory_info)?;
signatory_record_data.signed_off = true;

View File

@ -10,12 +10,12 @@ use solana_program::{
use crate::{
error::GovernanceError,
state::{
realm::{deserialize_realm_raw, get_realm_address_seeds},
realm::{get_realm_address_seeds, get_realm_data},
token_owner_record::{
deserialize_token_owner_record, get_token_owner_record_address_seeds,
get_token_owner_record_address_seeds, get_token_owner_record_data_for_seeds,
},
},
tools::token::{get_mint_from_token_account, transfer_spl_tokens_signed},
tools::spl_token::{get_spl_token_mint, transfer_spl_tokens_signed},
};
/// Processes WithdrawGoverningTokens instruction
@ -36,8 +36,8 @@ pub fn process_withdraw_governing_tokens(
return Err(GovernanceError::GoverningTokenOwnerMustSign.into());
}
let realm_data = deserialize_realm_raw(realm_info)?;
let governing_token_mint = get_mint_from_token_account(governing_token_holding_info)?;
let realm_data = get_realm_data(realm_info)?;
let governing_token_mint = get_spl_token_mint(governing_token_holding_info)?;
let token_owner_record_address_seeds = get_token_owner_record_address_seeds(
realm_info.key,
@ -45,11 +45,13 @@ pub fn process_withdraw_governing_tokens(
governing_token_owner_info.key,
);
let mut token_owner_record_data =
deserialize_token_owner_record(token_owner_record_info, &token_owner_record_address_seeds)?;
let mut token_owner_record_data = get_token_owner_record_data_for_seeds(
token_owner_record_info,
&token_owner_record_address_seeds,
)?;
if token_owner_record_data.active_votes_count > 0 {
return Err(GovernanceError::CannotWithdrawGoverningTokensWhenActiveVotesExist.into());
if token_owner_record_data.unrelinquished_votes_count > 0 {
return Err(GovernanceError::AllVotesMustBeRelinquishedToWithdrawGoverningTokens.into());
}
transfer_spl_tokens_signed(

View File

@ -28,7 +28,7 @@ pub enum GovernanceAccountType {
SignatoryRecord,
/// Vote record account for a given Proposal. Proposal can have 0..n voting records
ProposalVoteRecord,
VoteRecord,
/// Single Signer Instruction account which holds an instruction to execute for Proposal
SingleSignerInstruction,

View File

@ -2,7 +2,7 @@
use crate::{
error::GovernanceError, id, state::enums::GovernanceAccountType,
tools::account::deserialize_account, tools::account::AccountMaxSize,
tools::account::get_account_data, tools::account::AccountMaxSize,
};
use borsh::{BorshDeserialize, BorshSchema, BorshSerialize};
use solana_program::{
@ -10,7 +10,7 @@ use solana_program::{
pubkey::Pubkey,
};
use super::realm::assert_is_valid_realm;
use crate::state::realm::assert_is_valid_realm;
/// Governance config
#[repr(C)]
@ -22,9 +22,11 @@ pub struct GovernanceConfig {
/// Account governed by this Governance. It can be for example Program account, Mint account or Token Account
pub governed_account: Pubkey,
/// Voting threshold in % required to tip the vote
/// Voting threshold of Yes votes in % required to tip the vote
/// It's the percentage of tokens out of the entire pool of governance tokens eligible to vote
pub vote_threshold_percentage: u8,
// Note: If the threshold is below or equal to 50% then an even split of votes ex: 50:50 or 40:40 is always resolved as Defeated
// In other words +1 vote tie breaker is required to have successful vote
pub yes_vote_threshold_percentage: u8,
/// Minimum number of tokens a governance token owner must possess to be able to create a proposal
pub min_tokens_to_create_proposal: u16,
@ -47,7 +49,7 @@ pub struct Governance {
pub config: GovernanceConfig,
/// Running count of proposals
pub proposals_count: u16,
pub proposals_count: u32,
}
impl AccountMaxSize for Governance {}
@ -60,10 +62,8 @@ impl IsInitialized for Governance {
}
/// Deserializes account and checks owner program
pub fn deserialize_governance_raw(
governance_info: &AccountInfo,
) -> Result<Governance, ProgramError> {
deserialize_account::<Governance>(governance_info, &id())
pub fn get_governance_data(governance_info: &AccountInfo) -> Result<Governance, ProgramError> {
get_account_data::<Governance>(governance_info, &id())
}
/// Returns ProgramGovernance PDA seeds
@ -127,8 +127,8 @@ pub fn assert_is_valid_governance_config(
assert_is_valid_realm(realm_info)?;
if governance_config.vote_threshold_percentage < 50
|| governance_config.vote_threshold_percentage > 100
if governance_config.yes_vote_threshold_percentage < 1
|| governance_config.yes_vote_threshold_percentage > 100
{
return Err(GovernanceError::InvalidGovernanceConfig.into());
}

View File

@ -3,8 +3,8 @@
pub mod enums;
pub mod governance;
pub mod proposal;
pub mod proposal_vote_record;
pub mod realm;
pub mod signatory_record;
pub mod single_signer_instruction;
pub mod token_owner_record;
pub mod vote_record;

View File

@ -5,16 +5,14 @@ use solana_program::{
program_pack::IsInitialized, pubkey::Pubkey,
};
use crate::{
error::GovernanceError,
id,
tools::account::{deserialize_account, AccountMaxSize},
PROGRAM_AUTHORITY_SEED,
};
use crate::tools::account::get_account_data;
use crate::{error::GovernanceError, id, tools::account::AccountMaxSize, PROGRAM_AUTHORITY_SEED};
use crate::state::enums::{GovernanceAccountType, ProposalState};
use borsh::{BorshDeserialize, BorshSchema, BorshSerialize};
use crate::state::governance::GovernanceConfig;
/// Governance Proposal
#[repr(C)]
#[derive(Clone, Debug, PartialEq, BorshDeserialize, BorshSerialize, BorshSchema)]
@ -47,6 +45,12 @@ pub struct Proposal {
/// Proposal name
pub name: String,
/// The number of Yes votes
pub yes_votes_count: u64,
/// The number of No votes
pub no_votes_count: u64,
/// When the Proposal was created and entered Draft state
pub draft_at: Slot,
@ -74,7 +78,7 @@ pub struct Proposal {
impl AccountMaxSize for Proposal {
fn get_max_size(&self) -> Option<usize> {
Some(self.name.len() + self.description_link.len() + 163)
Some(self.name.len() + self.description_link.len() + 179)
}
}
@ -87,26 +91,220 @@ impl IsInitialized for Proposal {
impl Proposal {
/// Checks if Signatories can be edited (added or removed) for the Proposal in the given state
pub fn assert_can_edit_signatories(&self) -> Result<(), ProgramError> {
if !(self.state == ProposalState::Draft || self.state == ProposalState::SigningOff) {
return Err(GovernanceError::InvalidStateCannotEditSignatories.into());
}
Ok(())
self.assert_is_draft_state()
.map_err(|_| GovernanceError::InvalidStateCannotEditSignatories.into())
}
/// Checks if Proposal can be singed off
pub fn assert_can_sign_off(&self) -> Result<(), ProgramError> {
if !(self.state == ProposalState::Draft || self.state == ProposalState::SigningOff) {
return Err(GovernanceError::InvalidStateCannotSignOff.into());
match self.state {
ProposalState::Draft | ProposalState::SigningOff => Ok(()),
ProposalState::Executing
| ProposalState::Completed
| ProposalState::Cancelled
| ProposalState::Voting
| ProposalState::Succeeded
| ProposalState::Defeated => Err(GovernanceError::InvalidStateCannotSignOff.into()),
}
}
/// Checks the Proposal is in Voting state
fn assert_is_voting_state(&self) -> Result<(), ProgramError> {
if self.state != ProposalState::Voting {
return Err(GovernanceError::InvalidProposalState.into());
}
Ok(())
}
/// Checks the Proposal is in Draft state
fn assert_is_draft_state(&self) -> Result<(), ProgramError> {
if self.state != ProposalState::Draft {
return Err(GovernanceError::InvalidProposalState.into());
}
Ok(())
}
/// Checks if Proposal can be voted on
pub fn assert_can_cast_vote(
&self,
config: &GovernanceConfig,
current_slot: Slot,
) -> Result<(), ProgramError> {
self.assert_is_voting_state()
.map_err(|_| GovernanceError::InvalidStateCannotVote)?;
// Check if we are still within the configured max_voting_time period
if self
.voting_at
.unwrap()
.checked_add(config.max_voting_time)
.unwrap()
< current_slot
{
return Err(GovernanceError::ProposalVotingTimeExpired.into());
}
Ok(())
}
/// Checks if Proposal can be finalized
pub fn assert_can_finalize_vote(
&self,
config: &GovernanceConfig,
current_slot: Slot,
) -> Result<(), ProgramError> {
self.assert_is_voting_state()
.map_err(|_| GovernanceError::InvalidStateCannotFinalize)?;
// Check if we passed the configured max_voting_time period yet
if self
.voting_at
.unwrap()
.checked_add(config.max_voting_time)
.unwrap()
>= current_slot
{
return Err(GovernanceError::CannotFinalizeVotingInProgress.into());
}
Ok(())
}
/// Finalizes vote by moving it to final state Succeeded or Defeated if max_voting_time has passed
/// If Proposal is still within max_voting_time period then error is returned
pub fn finalize_vote(
&mut self,
governing_token_supply: u64,
config: &GovernanceConfig,
current_slot: Slot,
) -> Result<(), ProgramError> {
self.assert_can_finalize_vote(config, current_slot)?;
self.state = self.get_final_vote_state(governing_token_supply, config);
self.voting_completed_at = Some(current_slot);
Ok(())
}
fn get_final_vote_state(
&mut self,
governing_token_supply: u64,
config: &GovernanceConfig,
) -> ProposalState {
let yes_vote_threshold_count =
get_vote_threshold_count(config.yes_vote_threshold_percentage, governing_token_supply);
// Yes vote must be equal or above the required yes_vote_threshold_percentage and higher than No vote
// The same number of Yes and No votes is a tie and resolved as Defeated
// In other words +1 vote as a tie breaker is required to Succeed
if self.yes_votes_count >= yes_vote_threshold_count
&& self.yes_votes_count > self.no_votes_count
{
ProposalState::Succeeded
} else {
ProposalState::Defeated
}
}
/// Checks if vote can be tipped and automatically transitioned to Succeeded or Defeated state
/// If the conditions are met the state is updated accordingly
pub fn try_tip_vote(
&mut self,
governing_token_supply: u64,
config: &GovernanceConfig,
current_slot: Slot,
) {
if let Some(tipped_state) = self.try_get_tipped_vote_state(governing_token_supply, config) {
self.state = tipped_state;
self.voting_completed_at = Some(current_slot);
}
}
/// Checks if vote can be tipped and automatically transitioned to Succeeded or Defeated state
/// If yes then Some(ProposalState) is returned and None otherwise
#[allow(clippy::float_cmp)]
pub fn try_get_tipped_vote_state(
&self,
governing_token_supply: u64,
config: &GovernanceConfig,
) -> Option<ProposalState> {
if self.yes_votes_count == governing_token_supply {
return Some(ProposalState::Succeeded);
}
if self.no_votes_count == governing_token_supply {
return Some(ProposalState::Defeated);
}
let yes_vote_threshold_count =
get_vote_threshold_count(config.yes_vote_threshold_percentage, governing_token_supply);
if self.yes_votes_count >= yes_vote_threshold_count
&& self.yes_votes_count > (governing_token_supply - self.yes_votes_count)
{
return Some(ProposalState::Succeeded);
} else if self.no_votes_count > (governing_token_supply - yes_vote_threshold_count)
|| self.no_votes_count >= (governing_token_supply - self.no_votes_count)
{
return Some(ProposalState::Defeated);
}
None
}
/// Checks if Proposal can be canceled in the given state
pub fn assert_can_cancel(&self) -> Result<(), ProgramError> {
match self.state {
ProposalState::Draft | ProposalState::SigningOff | ProposalState::Voting => Ok(()),
ProposalState::Executing
| ProposalState::Completed
| ProposalState::Cancelled
| ProposalState::Succeeded
| ProposalState::Defeated => {
Err(GovernanceError::InvalidStateCannotCancelProposal.into())
}
}
}
}
/// Converts threshold in percentages to actual vote count
fn get_vote_threshold_count(threshold_percentage: u8, total_supply: u64) -> u64 {
let numerator = (threshold_percentage as u128)
.checked_mul(total_supply as u128)
.unwrap();
let mut threshold = numerator.checked_div(100).unwrap();
if threshold * 100 < numerator {
threshold += 1;
}
threshold as u64
}
/// Deserializes Proposal account and checks owner program
pub fn deserialize_proposal_raw(proposal_info: &AccountInfo) -> Result<Proposal, ProgramError> {
deserialize_account::<Proposal>(proposal_info, &id())
pub fn get_proposal_data(proposal_info: &AccountInfo) -> Result<Proposal, ProgramError> {
get_account_data::<Proposal>(proposal_info, &id())
}
/// Deserializes Proposal and validates it belongs to the given Governance and Governing Mint
pub fn get_proposal_data_for_governance_and_governing_mint(
proposal_info: &AccountInfo,
governance: &Pubkey,
governing_token_mint: &Pubkey,
) -> Result<Proposal, ProgramError> {
let proposal_data = get_proposal_data(proposal_info)?;
if proposal_data.governance != *governance {
return Err(GovernanceError::InvalidGovernanceForProposal.into());
}
if proposal_data.governing_token_mint != *governing_token_mint {
return Err(GovernanceError::InvalidGoverningMintForProposal.into());
}
Ok(proposal_data)
}
/// Returns Proposal PDA seeds
@ -127,10 +325,10 @@ pub fn get_proposal_address_seeds<'a>(
pub fn get_proposal_address<'a>(
governance: &'a Pubkey,
governing_token_mint: &'a Pubkey,
proposal_index_bytes: &'a [u8],
proposal_index_le_bytes: &'a [u8],
) -> Pubkey {
Pubkey::find_program_address(
&get_proposal_address_seeds(governance, governing_token_mint, &proposal_index_bytes),
&get_proposal_address_seeds(governance, governing_token_mint, &proposal_index_le_bytes),
&id(),
)
.0
@ -160,6 +358,19 @@ mod test {
closed_at: Some(10),
number_of_executed_instructions: 10,
number_of_instructions: 10,
yes_votes_count: 0,
no_votes_count: 0,
}
}
fn create_test_governance_config() -> GovernanceConfig {
GovernanceConfig {
realm: Pubkey::new_unique(),
governed_account: Pubkey::new_unique(),
yes_vote_threshold_percentage: 60,
min_tokens_to_create_proposal: 5,
min_instruction_hold_up_time: 10,
max_voting_time: 5,
}
}
@ -171,8 +382,17 @@ mod test {
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), Just(ProposalState::SigningOff),]
prop_oneof![Just(ProposalState::Draft)]
}
proptest! {
@ -195,6 +415,7 @@ mod test {
Just(ProposalState::Completed),
Just(ProposalState::Cancelled),
Just(ProposalState::Defeated),
Just(ProposalState::SigningOff),
]
}
@ -251,4 +472,469 @@ mod test {
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();
proposal.state = state;
proposal.assert_can_cancel().unwrap();
}
}
fn none_cancellable_states() -> impl Strategy<Value = ProposalState> {
prop_oneof![
Just(ProposalState::Succeeded),
Just(ProposalState::Executing),
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()) {
// Arrange
let mut proposal = create_test_proposal();
proposal.state = state;
// Act
let err = proposal.assert_can_cancel().err().unwrap();
// Assert
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![
// threshold < 50%
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,
}),
// threshold == 50%
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,
}),
// threshold > 50%
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()) {
// Arrange
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.yes_vote_threshold_percentage = test_case.vote_threshold_percentage;
let current_slot = 15_u64;
// Act
proposal.try_tip_vote(test_case.governing_token_supply, &governance_config,current_slot);
// Assert
assert_eq!(proposal.state,test_case.expected_tipped_state,"CASE: {:?}",test_case);
if test_case.expected_tipped_state != ProposalState::Voting {
assert_eq!(Some(current_slot),proposal.voting_completed_at)
}
}
#[test]
fn test_finalize_vote(test_case in vote_casting_test_cases()) {
// Arrange
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.yes_vote_threshold_percentage = test_case.vote_threshold_percentage;
let current_slot = 16_u64;
// Act
proposal.finalize_vote(test_case.governing_token_supply, &governance_config,current_slot).unwrap();
// Assert
assert_eq!(proposal.state,test_case.expected_finalized_state,"CASE: {:?}",test_case);
assert_eq!(Some(current_slot),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(),
) {
// Arrange
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();
governance_config.yes_vote_threshold_percentage = yes_vote_threshold_percentage;
let current_slot = 15_u64;
// Act
proposal.try_tip_vote(governing_token_supply, &governance_config,current_slot);
// Assert
let yes_vote_threshold_count = get_vote_threshold_count(yes_vote_threshold_percentage,governing_token_supply);
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(),
) {
// Arrange
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();
governance_config.yes_vote_threshold_percentage = yes_vote_threshold_percentage;
let current_slot = 16_u64;
// Act
proposal.finalize_vote(governing_token_supply, &governance_config,current_slot).unwrap();
// Assert
let yes_vote_threshold_count = get_vote_threshold_count(yes_vote_threshold_percentage,governing_token_supply);
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]
pub fn test_finalize_vote_with_expired_voting_time_error() {
// Arrange
let mut proposal = create_test_proposal();
proposal.state = ProposalState::Voting;
let governance_config = create_test_governance_config();
let current_slot = proposal.voting_at.unwrap() + governance_config.max_voting_time;
// Act
let err = proposal
.finalize_vote(100, &governance_config, current_slot)
.err()
.unwrap();
// Assert
assert_eq!(err, GovernanceError::CannotFinalizeVotingInProgress.into());
}
#[test]
pub fn test_finalize_vote_after_voting_time() {
// Arrange
let mut proposal = create_test_proposal();
proposal.state = ProposalState::Voting;
let governance_config = create_test_governance_config();
let current_slot = proposal.voting_at.unwrap() + governance_config.max_voting_time + 1;
// Act
let result = proposal.finalize_vote(100, &governance_config, current_slot);
// Assert
assert_eq!(result, Ok(()));
}
#[test]
pub fn test_assert_can_vote_with_expired_voting_time_error() {
// Arrange
let mut proposal = create_test_proposal();
proposal.state = ProposalState::Voting;
let governance_config = create_test_governance_config();
let current_slot = proposal.voting_at.unwrap() + governance_config.max_voting_time + 1;
// Act
let err = proposal
.assert_can_cast_vote(&governance_config, current_slot)
.err()
.unwrap();
// Assert
assert_eq!(err, GovernanceError::ProposalVotingTimeExpired.into());
}
#[test]
pub fn test_assert_can_vote_within_voting_time() {
// Arrange
let mut proposal = create_test_proposal();
proposal.state = ProposalState::Voting;
let governance_config = create_test_governance_config();
let current_slot = proposal.voting_at.unwrap() + governance_config.max_voting_time;
// Act
let result = proposal.assert_can_cast_vote(&governance_config, current_slot);
// Assert
assert_eq!(result, Ok(()));
}
}

View File

@ -1,24 +0,0 @@
//! Proposal Vote Record Account
use borsh::{BorshDeserialize, BorshSchema, BorshSerialize};
use solana_program::pubkey::Pubkey;
use crate::state::enums::{GovernanceAccountType, VoteWeight};
/// Proposal Vote Record
#[repr(C)]
#[derive(Clone, Debug, PartialEq, BorshDeserialize, BorshSerialize, BorshSchema)]
pub struct ProposalVoteRecord {
/// Governance account type
pub account_type: GovernanceAccountType,
/// Proposal account
pub proposal: Pubkey,
/// The user who casted this vote
/// This is the Governing Token Owner who deposited governing tokens into the Realm
pub governing_token_owner: Pubkey,
/// Voter's vote: Yes/No and amount
pub vote: Option<VoteWeight>,
}

View File

@ -9,7 +9,7 @@ use solana_program::{
use crate::{
error::GovernanceError,
id,
tools::account::{assert_is_valid_account, deserialize_account, AccountMaxSize},
tools::account::{assert_is_valid_account, get_account_data, AccountMaxSize},
PROGRAM_AUTHORITY_SEED,
};
@ -65,8 +65,8 @@ pub fn assert_is_valid_realm(realm_info: &AccountInfo) -> Result<(), ProgramErro
}
/// Deserializes account and checks owner program
pub fn deserialize_realm_raw(realm_info: &AccountInfo) -> Result<Realm, ProgramError> {
deserialize_account::<Realm>(realm_info, &id())
pub fn get_realm_data(realm_info: &AccountInfo) -> Result<Realm, ProgramError> {
get_account_data::<Realm>(realm_info, &id())
}
/// Returns Realm PDA seeds

View File

@ -9,7 +9,7 @@ use solana_program::{
use crate::{
error::GovernanceError,
id,
tools::account::{deserialize_account, AccountMaxSize},
tools::account::{get_account_data, AccountMaxSize},
PROGRAM_AUTHORITY_SEED,
};
@ -83,14 +83,14 @@ pub fn get_signatory_record_address<'a>(proposal: &'a Pubkey, signatory: &'a Pub
}
/// Deserializes SignatoryRecord account and checks owner program
pub fn deserialize_signatory_record_raw(
pub fn get_signatory_record_data(
signatory_record_info: &AccountInfo,
) -> Result<SignatoryRecord, ProgramError> {
deserialize_account::<SignatoryRecord>(signatory_record_info, &id())
get_account_data::<SignatoryRecord>(signatory_record_info, &id())
}
/// Deserializes SignatoryRecord and validates its PDA
pub fn deserialize_signatory_record(
pub fn get_signatory_record_data_for_seeds(
signatory_record_info: &AccountInfo,
proposal: &Pubkey,
signatory: &Pubkey,
@ -104,5 +104,5 @@ pub fn deserialize_signatory_record(
return Err(GovernanceError::InvalidSignatoryAddress.into());
}
deserialize_signatory_record_raw(signatory_record_info)
get_signatory_record_data(signatory_record_info)
}

View File

@ -3,7 +3,7 @@
use crate::{
error::GovernanceError,
id,
tools::account::{deserialize_account, AccountMaxSize},
tools::account::{get_account_data, AccountMaxSize},
PROGRAM_AUTHORITY_SEED,
};
@ -30,7 +30,7 @@ pub struct TokenOwnerRecord {
pub governing_token_mint: Pubkey,
/// The owner (either single or multisig) of the deposited governing SPL Tokens
/// This is who can authorize a withdrawal
/// This is who can authorize a withdrawal of the tokens
pub governing_token_owner: Pubkey,
/// The amount of governing tokens deposited into the Realm
@ -38,19 +38,21 @@ pub struct TokenOwnerRecord {
pub governing_token_deposit_amount: u64,
/// A single account that is allowed to operate governance with the deposited governing tokens
/// It's delegated to by the governing token owner or current governance_delegate
/// It can be delegated to by the governing_token_owner or current governance_delegate
pub governance_delegate: Option<Pubkey>,
/// The number of active votes cast by TokenOwner
pub active_votes_count: u16,
/// The number of votes cast by TokenOwner but not relinquished yet
/// Every time a vote is cast this number is increased and it's always decreased when relinquishing a vote regardless of the vote state
pub unrelinquished_votes_count: u32,
/// The total number of votes cast by the TokenOwner
pub total_votes_count: u16,
/// If TokenOwner withdraws vote while voting is still in progress total_votes_count is decreased and the vote doesn't count towards the total
pub total_votes_count: u32,
}
impl AccountMaxSize for TokenOwnerRecord {
fn get_max_size(&self) -> Option<usize> {
Some(142)
Some(146)
}
}
@ -60,6 +62,28 @@ impl IsInitialized for TokenOwnerRecord {
}
}
impl TokenOwnerRecord {
/// Checks whether the provided Governance Authority signed transaction
pub fn assert_token_owner_or_delegate_is_signer(
&self,
governance_authority_info: &AccountInfo,
) -> Result<(), ProgramError> {
if governance_authority_info.is_signer {
if &self.governing_token_owner == governance_authority_info.key {
return Ok(());
}
if let Some(governance_delegate) = self.governance_delegate {
if &governance_delegate == governance_authority_info.key {
return Ok(());
}
};
}
Err(GovernanceError::GoverningTokenOwnerOrDelegateMustSign.into())
}
}
/// Returns TokenOwnerRecord PDA address
pub fn get_token_owner_record_address(
realm: &Pubkey,
@ -88,14 +112,14 @@ pub fn get_token_owner_record_address_seeds<'a>(
}
/// Deserializes TokenOwnerRecord account and checks owner program
pub fn deserialize_token_owner_record_raw(
pub fn get_token_owner_record_data(
token_owner_record_info: &AccountInfo,
) -> Result<TokenOwnerRecord, ProgramError> {
deserialize_account::<TokenOwnerRecord>(token_owner_record_info, &id())
get_account_data::<TokenOwnerRecord>(token_owner_record_info, &id())
}
/// Deserializes TokenOwnerRecord account and checks its PDA against the provided seeds
pub fn deserialize_token_owner_record(
pub fn get_token_owner_record_data_for_seeds(
token_owner_record_info: &AccountInfo,
token_owner_record_seeds: &[&[u8]],
) -> Result<TokenOwnerRecord, ProgramError> {
@ -106,30 +130,30 @@ pub fn deserialize_token_owner_record(
return Err(GovernanceError::InvalidTokenOwnerRecordAccountAddress.into());
}
deserialize_token_owner_record_raw(token_owner_record_info)
get_token_owner_record_data(token_owner_record_info)
}
/// Deserializes TokenOwnerRecord account and checks that its PDA matches the given realm and governing mint
pub fn deserialize_token_owner_record_for_realm_and_governing_mint(
pub fn get_token_owner_record_data_for_realm_and_governing_mint(
token_owner_record_info: &AccountInfo,
realm: &Pubkey,
governing_token_mint: &Pubkey,
) -> Result<TokenOwnerRecord, ProgramError> {
let token_owner_record_data = deserialize_token_owner_record_raw(token_owner_record_info)?;
let token_owner_record_data = get_token_owner_record_data(token_owner_record_info)?;
if token_owner_record_data.governing_token_mint != *governing_token_mint {
return Err(GovernanceError::InvalidTokenOwnerRecordGoverningMint.into());
return Err(GovernanceError::InvalidGoverningMintForTokenOwnerRecord.into());
}
if token_owner_record_data.realm != *realm {
return Err(GovernanceError::InvalidTokenOwnerRecordRealm.into());
return Err(GovernanceError::InvalidRealmForTokenOwnerRecord.into());
}
Ok(token_owner_record_data)
}
/// Deserializes TokenOwnerRecord account and checks its address is the give proposal_owner
pub fn deserialize_token_owner_record_for_proposal_owner(
pub fn get_token_owner_record_data_for_proposal_owner(
token_owner_record_info: &AccountInfo,
proposal_owner: &Pubkey,
) -> Result<TokenOwnerRecord, ProgramError> {
@ -137,7 +161,7 @@ pub fn deserialize_token_owner_record_for_proposal_owner(
return Err(GovernanceError::InvalidProposalOwnerAccount.into());
}
deserialize_token_owner_record_raw(token_owner_record_info)
get_token_owner_record_data(token_owner_record_info)
}
#[cfg(test)]
@ -155,7 +179,7 @@ mod test {
governing_token_owner: Pubkey::new_unique(),
governing_token_deposit_amount: 10,
governance_delegate: Some(Pubkey::new_unique()),
active_votes_count: 1,
unrelinquished_votes_count: 1,
total_votes_count: 1,
};

View File

@ -1,8 +1,12 @@
//! Proposal Vote Record Account
use borsh::{BorshDeserialize, BorshSchema, BorshSerialize};
use solana_program::account_info::AccountInfo;
use solana_program::program_error::ProgramError;
use solana_program::{program_pack::IsInitialized, pubkey::Pubkey};
use crate::error::GovernanceError;
use crate::tools::account::get_account_data;
use crate::{id, tools::account::AccountMaxSize, PROGRAM_AUTHORITY_SEED};
use crate::state::enums::{GovernanceAccountType, VoteWeight};
@ -21,6 +25,9 @@ pub struct VoteRecord {
/// This is the Governing Token Owner who deposited governing tokens into the Realm
pub governing_token_owner: Pubkey,
/// Indicates whether the vote was relinquished by voter
pub is_relinquished: bool,
/// Voter's vote: Yes/No and amount
pub vote_weight: VoteWeight,
}
@ -32,6 +39,40 @@ impl IsInitialized for VoteRecord {
self.account_type == GovernanceAccountType::VoteRecord
}
}
impl VoteRecord {
/// Checks the vote can be relinquished
pub fn assert_can_relinquish_vote(&self) -> Result<(), ProgramError> {
if self.is_relinquished {
return Err(GovernanceError::VoteAlreadyRelinquished.into());
}
Ok(())
}
}
/// Deserializes VoteRecord account and checks owner program
pub fn get_vote_record_data(vote_record_info: &AccountInfo) -> Result<VoteRecord, ProgramError> {
get_account_data::<VoteRecord>(vote_record_info, &id())
}
/// Deserializes VoteRecord and checks it belongs to the provided Proposal and Governing Token Owner
pub fn get_vote_record_data_for_proposal_and_token_owner(
vote_record_info: &AccountInfo,
proposal: &Pubkey,
governing_token_owner: &Pubkey,
) -> Result<VoteRecord, ProgramError> {
let vote_record_data = get_vote_record_data(vote_record_info)?;
if vote_record_data.proposal != *proposal {
return Err(GovernanceError::InvalidProposalForVoterRecord.into());
}
if vote_record_data.governing_token_owner != *governing_token_owner {
return Err(GovernanceError::InvalidGoverningTokenOwnerForVoteRecord.into());
}
Ok(vote_record_data)
}
/// Returns VoteRecord PDA seeds
pub fn get_vote_record_address_seeds<'a>(

View File

@ -84,7 +84,7 @@ pub fn create_and_serialize_account_signed<'a, T: BorshSerialize + AccountMaxSiz
}
/// Deserializes account and checks it's initialized and owned by the specified program
pub fn deserialize_account<T: BorshDeserialize + IsInitialized>(
pub fn get_account_data<T: BorshDeserialize + IsInitialized>(
account_info: &AccountInfo,
owner_program_id: &Pubkey,
) -> Result<T, ProgramError> {
@ -128,11 +128,11 @@ pub fn assert_is_valid_account<T: BorshDeserialize + PartialEq>(
/// Disposes account by transferring its lamports to the beneficiary account and zeros its data
// After transaction completes the runtime would remove the account with no lamports
pub fn dispose_account(account_info: &AccountInfo, beneficiary_account: &AccountInfo) {
pub fn dispose_account(account_info: &AccountInfo, beneficiary_info: &AccountInfo) {
let account_lamports = account_info.lamports();
**account_info.lamports.borrow_mut() = 0;
**beneficiary_account.lamports.borrow_mut() = beneficiary_account
**beneficiary_info.lamports.borrow_mut() = beneficiary_info
.lamports()
.checked_add(account_lamports)
.unwrap();

View File

@ -1,25 +0,0 @@
//! Governance asserts
use solana_program::{account_info::AccountInfo, program_error::ProgramError};
use crate::{error::GovernanceError, state::token_owner_record::TokenOwnerRecord};
/// Checks whether the provided Governance Authority signed transaction
pub fn assert_token_owner_or_delegate_is_signer(
token_owner_record: &TokenOwnerRecord,
governance_authority_info: &AccountInfo,
) -> Result<(), ProgramError> {
if governance_authority_info.is_signer {
if &token_owner_record.governing_token_owner == governance_authority_info.key {
return Ok(());
}
if let Some(governance_delegate) = token_owner_record.governance_delegate {
if &governance_delegate == governance_authority_info.key {
return Ok(());
}
};
}
Err(GovernanceError::GoverningTokenOwnerOrDelegateMustSign.into())
}

View File

@ -2,8 +2,6 @@
pub mod account;
pub mod token;
pub mod asserts;
pub mod spl_token;
pub mod bpf_loader_upgradeable;

View File

@ -12,6 +12,7 @@ use solana_program::{
rent::Rent,
system_instruction,
};
use spl_token::state::{Account, Mint};
use crate::error::GovernanceError;
@ -165,14 +166,44 @@ pub fn transfer_spl_tokens_signed<'a>(
Ok(())
}
/// Asserts the given account_info represents a valid SPL Token account which is initialized and belongs to spl_token program
pub fn assert_is_valid_spl_token_account(account_info: &AccountInfo) -> Result<(), ProgramError> {
if account_info.data_is_empty() {
return Err(GovernanceError::SplTokenAccountNotInitialized.into());
}
if account_info.owner != &spl_token::id() {
return Err(GovernanceError::SplTokenAccountWithInvalidOwner.into());
}
if account_info.data_len() != Account::LEN {
return Err(GovernanceError::SplTokenInvalidTokenAccountData.into());
}
Ok(())
}
/// Asserts the given mint_info represents a valid SPL Token Mint account which is initialized and belongs to spl_token program
pub fn assert_is_valid_spl_token_mint(mint_info: &AccountInfo) -> Result<(), ProgramError> {
if mint_info.data_is_empty() {
return Err(GovernanceError::SplTokenMintNotInitialized.into());
}
if mint_info.owner != &spl_token::id() {
return Err(GovernanceError::SplTokenMintWithInvalidOwner.into());
}
if mint_info.data_len() != Mint::LEN {
return Err(GovernanceError::SplTokenInvalidMintAccountData.into());
}
Ok(())
}
/// Computationally cheap method to get amount from a token account
/// It reads amount without deserializing full account data
pub fn get_amount_from_token_account(
token_account_info: &AccountInfo,
) -> Result<u64, ProgramError> {
if token_account_info.owner != &spl_token::id() {
return Err(GovernanceError::InvalidTokenAccountOwner.into());
}
pub fn get_spl_token_amount(token_account_info: &AccountInfo) -> Result<u64, ProgramError> {
assert_is_valid_spl_token_account(token_account_info)?;
// TokeAccount layout: mint(32), owner(32), amount(8), ...
let data = token_account_info.try_borrow_data()?;
@ -182,12 +213,8 @@ pub fn get_amount_from_token_account(
/// Computationally cheap method to get mint from a token account
/// It reads mint without deserializing full account data
pub fn get_mint_from_token_account(
token_account_info: &AccountInfo,
) -> Result<Pubkey, ProgramError> {
if token_account_info.owner != &spl_token::id() {
return Err(GovernanceError::InvalidTokenAccountOwner.into());
}
pub fn get_spl_token_mint(token_account_info: &AccountInfo) -> Result<Pubkey, ProgramError> {
assert_is_valid_spl_token_account(token_account_info)?;
// TokeAccount layout: mint(32), owner(32), amount(8), ...
let data = token_account_info.try_borrow_data()?;
@ -197,15 +224,22 @@ pub fn get_mint_from_token_account(
/// Computationally cheap method to get owner from a token account
/// It reads owner without deserializing full account data
pub fn get_owner_from_token_account(
token_account_info: &AccountInfo,
) -> Result<Pubkey, ProgramError> {
if token_account_info.owner != &spl_token::id() {
return Err(GovernanceError::InvalidTokenAccountOwner.into());
}
pub fn get_spl_token_owner(token_account_info: &AccountInfo) -> Result<Pubkey, ProgramError> {
assert_is_valid_spl_token_account(token_account_info)?;
// TokeAccount layout: mint(32), owner(32), amount(8)
let data = token_account_info.try_borrow_data()?;
let owner_data = array_ref![data, 32, 32];
Ok(Pubkey::new_from_array(*owner_data))
}
/// Computationally cheap method to just get supply from a mint without unpacking the whole object
pub fn get_spl_token_mint_supply(mint_info: &AccountInfo) -> Result<u64, ProgramError> {
assert_is_valid_spl_token_mint(mint_info)?;
// In token program, 36, 8, 1, 1 is the layout, where the first 8 is supply u64.
// so we start at 36.
let data = mint_info.try_borrow_data().unwrap();
let bytes = array_ref![data, 36, 8];
Ok(u64::from_le_bytes(*bytes))
}

View File

@ -0,0 +1,129 @@
#![cfg(feature = "test-bpf")]
mod program_test;
use solana_program_test::tokio;
use program_test::*;
use spl_governance::{error::GovernanceError, instruction::Vote, state::enums::ProposalState};
#[tokio::test]
async fn test_cancel_proposal() {
// Arrange
let mut governance_test = GovernanceProgramTest::start_new().await;
let realm_cookie = governance_test.with_realm().await;
let governed_account_cookie = governance_test.with_governed_account().await;
let mut account_governance_cookie = governance_test
.with_account_governance(&realm_cookie, &governed_account_cookie)
.await
.unwrap();
let token_owner_record_cookie = governance_test
.with_initial_community_token_deposit(&realm_cookie)
.await;
let proposal_cookie = governance_test
.with_proposal(&token_owner_record_cookie, &mut account_governance_cookie)
.await
.unwrap();
// Act
governance_test
.cancel_proposal(&proposal_cookie, &token_owner_record_cookie)
.await
.unwrap();
// Assert
let proposal_account = governance_test
.get_proposal_account(&proposal_cookie.address)
.await;
assert_eq!(ProposalState::Cancelled, proposal_account.state);
}
#[tokio::test]
async fn test_cancel_proposal_with_already_completed_error() {
// Arrange
let mut governance_test = GovernanceProgramTest::start_new().await;
let realm_cookie = governance_test.with_realm().await;
let governed_account_cookie = governance_test.with_governed_account().await;
let mut account_governance_cookie = governance_test
.with_account_governance(&realm_cookie, &governed_account_cookie)
.await
.unwrap();
let token_owner_record_cookie = governance_test
.with_initial_community_token_deposit(&realm_cookie)
.await;
let proposal_cookie = governance_test
.with_signed_off_proposal(&token_owner_record_cookie, &mut account_governance_cookie)
.await
.unwrap();
governance_test
.with_cast_vote(&proposal_cookie, &token_owner_record_cookie, Vote::Yes)
.await
.unwrap();
// Act
let err = governance_test
.cancel_proposal(&proposal_cookie, &token_owner_record_cookie)
.await
.err()
.unwrap();
// Assert
assert_eq!(
err,
GovernanceError::InvalidStateCannotCancelProposal.into()
);
}
#[tokio::test]
async fn test_cancel_proposal_with_owner_or_delegate_must_sign_error() {
// Arrange
let mut governance_test = GovernanceProgramTest::start_new().await;
let realm_cookie = governance_test.with_realm().await;
let governed_account_cookie = governance_test.with_governed_account().await;
let mut account_governance_cookie = governance_test
.with_account_governance(&realm_cookie, &governed_account_cookie)
.await
.unwrap();
let mut token_owner_record_cookie = governance_test
.with_initial_community_token_deposit(&realm_cookie)
.await;
let proposal_cookie = governance_test
.with_proposal(&token_owner_record_cookie, &mut account_governance_cookie)
.await
.unwrap();
let token_owner_record_cookie2 = governance_test
.with_initial_council_token_deposit(&realm_cookie)
.await;
token_owner_record_cookie.token_owner = token_owner_record_cookie2.token_owner;
// Act
let err = governance_test
.cancel_proposal(&proposal_cookie, &token_owner_record_cookie)
.await
.err()
.unwrap();
// Assert
assert_eq!(
err,
GovernanceError::GoverningTokenOwnerOrDelegateMustSign.into()
);
}

View File

@ -0,0 +1,566 @@
#![cfg(feature = "test-bpf")]
mod program_test;
use solana_program_test::tokio;
use program_test::*;
use spl_governance::{error::GovernanceError, instruction::Vote, state::enums::ProposalState};
#[tokio::test]
async fn test_cast_vote() {
// Arrange
let mut governance_test = GovernanceProgramTest::start_new().await;
let realm_cookie = governance_test.with_realm().await;
let governed_account_cookie = governance_test.with_governed_account().await;
let mut account_governance_cookie = governance_test
.with_account_governance(&realm_cookie, &governed_account_cookie)
.await
.unwrap();
let token_owner_record_cookie = governance_test
.with_initial_community_token_deposit(&realm_cookie)
.await;
let proposal_cookie = governance_test
.with_signed_off_proposal(&token_owner_record_cookie, &mut account_governance_cookie)
.await
.unwrap();
// Act
let vote_record_cookie = governance_test
.with_cast_vote(&proposal_cookie, &token_owner_record_cookie, Vote::Yes)
.await
.unwrap();
// Assert
let vote_record_account = governance_test
.get_vote_record_account(&vote_record_cookie.address)
.await;
assert_eq!(vote_record_cookie.account, vote_record_account);
let proposal_account = governance_test
.get_proposal_account(&proposal_cookie.address)
.await;
assert_eq!(
token_owner_record_cookie
.account
.governing_token_deposit_amount,
proposal_account.yes_votes_count
);
assert_eq!(proposal_account.state, ProposalState::Succeeded);
let token_owner_record = governance_test
.get_token_owner_record_account(&token_owner_record_cookie.address)
.await;
assert_eq!(1, token_owner_record.unrelinquished_votes_count);
assert_eq!(1, token_owner_record.total_votes_count);
}
#[tokio::test]
async fn test_cast_vote_with_invalid_governance_error() {
// Arrange
let mut governance_test = GovernanceProgramTest::start_new().await;
let realm_cookie = governance_test.with_realm().await;
let governed_account_cookie = governance_test.with_governed_account().await;
let mut account_governance_cookie = governance_test
.with_account_governance(&realm_cookie, &governed_account_cookie)
.await
.unwrap();
let token_owner_record_cookie = governance_test
.with_initial_community_token_deposit(&realm_cookie)
.await;
let mut proposal_cookie = governance_test
.with_signed_off_proposal(&token_owner_record_cookie, &mut account_governance_cookie)
.await
.unwrap();
// Setup Governance for a different account
let governed_account_cookie2 = governance_test.with_governed_account().await;
let account_governance_cookie2 = governance_test
.with_account_governance(&realm_cookie, &governed_account_cookie2)
.await
.unwrap();
proposal_cookie.account.governance = account_governance_cookie2.address;
// Act
let err = governance_test
.with_cast_vote(&proposal_cookie, &token_owner_record_cookie, Vote::Yes)
.await
.err()
.unwrap();
assert_eq!(err, GovernanceError::InvalidGovernanceForProposal.into());
}
#[tokio::test]
async fn test_cast_vote_with_invalid_mint_error() {
// Arrange
let mut governance_test = GovernanceProgramTest::start_new().await;
let realm_cookie = governance_test.with_realm().await;
let governed_account_cookie = governance_test.with_governed_account().await;
let mut account_governance_cookie = governance_test
.with_account_governance(&realm_cookie, &governed_account_cookie)
.await
.unwrap();
let token_owner_record_cookie = governance_test
.with_initial_community_token_deposit(&realm_cookie)
.await;
let mut proposal_cookie = governance_test
.with_signed_off_proposal(&token_owner_record_cookie, &mut account_governance_cookie)
.await
.unwrap();
// Try to use Council Mint with Community Proposal
proposal_cookie.account.governing_token_mint = realm_cookie.account.council_mint.unwrap();
// Act
let err = governance_test
.with_cast_vote(&proposal_cookie, &token_owner_record_cookie, Vote::Yes)
.await
.err()
.unwrap();
assert_eq!(err, GovernanceError::InvalidGoverningMintForProposal.into());
}
#[tokio::test]
async fn test_cast_vote_with_invalid_token_owner_record_mint_error() {
// Arrange
let mut governance_test = GovernanceProgramTest::start_new().await;
let realm_cookie = governance_test.with_realm().await;
let governed_account_cookie = governance_test.with_governed_account().await;
let mut account_governance_cookie = governance_test
.with_account_governance(&realm_cookie, &governed_account_cookie)
.await
.unwrap();
let mut token_owner_record_cookie = governance_test
.with_initial_community_token_deposit(&realm_cookie)
.await;
let proposal_cookie = governance_test
.with_signed_off_proposal(&token_owner_record_cookie, &mut account_governance_cookie)
.await
.unwrap();
// Try to use token_owner_record for Council Mint with Community Proposal
let token_owner_record_cookie2 = governance_test
.with_initial_council_token_deposit(&realm_cookie)
.await;
token_owner_record_cookie.address = token_owner_record_cookie2.address;
// Act
let err = governance_test
.with_cast_vote(&proposal_cookie, &token_owner_record_cookie, Vote::Yes)
.await
.err()
.unwrap();
assert_eq!(
err,
GovernanceError::InvalidGoverningMintForTokenOwnerRecord.into()
);
}
#[tokio::test]
async fn test_cast_vote_with_invalid_token_owner_record_from_different_realm_error() {
// Arrange
let mut governance_test = GovernanceProgramTest::start_new().await;
let realm_cookie = governance_test.with_realm().await;
let governed_account_cookie = governance_test.with_governed_account().await;
let mut account_governance_cookie = governance_test
.with_account_governance(&realm_cookie, &governed_account_cookie)
.await
.unwrap();
let mut token_owner_record_cookie = governance_test
.with_initial_community_token_deposit(&realm_cookie)
.await;
let proposal_cookie = governance_test
.with_signed_off_proposal(&token_owner_record_cookie, &mut account_governance_cookie)
.await
.unwrap();
// Try to use token_owner_record from another Realm for the same mint
let realm_cookie2 = governance_test.with_realm_using_mints(&realm_cookie).await;
let token_owner_record_cookie2 = governance_test
.with_initial_community_token_deposit(&realm_cookie2)
.await;
token_owner_record_cookie.address = token_owner_record_cookie2.address;
// Act
let err = governance_test
.with_cast_vote(&proposal_cookie, &token_owner_record_cookie, Vote::Yes)
.await
.err()
.unwrap();
assert_eq!(err, GovernanceError::InvalidRealmForTokenOwnerRecord.into());
}
#[tokio::test]
async fn test_cast_vote_with_governance_authority_must_sign_error() {
// Arrange
let mut governance_test = GovernanceProgramTest::start_new().await;
let realm_cookie = governance_test.with_realm().await;
let governed_account_cookie = governance_test.with_governed_account().await;
let mut account_governance_cookie = governance_test
.with_account_governance(&realm_cookie, &governed_account_cookie)
.await
.unwrap();
let mut token_owner_record_cookie = governance_test
.with_initial_community_token_deposit(&realm_cookie)
.await;
let proposal_cookie = governance_test
.with_signed_off_proposal(&token_owner_record_cookie, &mut account_governance_cookie)
.await
.unwrap();
// Try to use a different owner to sign
let token_owner_record_cookie2 = governance_test
.with_initial_community_token_deposit(&realm_cookie)
.await;
token_owner_record_cookie.token_owner = token_owner_record_cookie2.token_owner;
// Act
let err = governance_test
.with_cast_vote(&proposal_cookie, &token_owner_record_cookie, Vote::Yes)
.await
.err()
.unwrap();
assert_eq!(
err,
GovernanceError::GoverningTokenOwnerOrDelegateMustSign.into()
);
}
#[tokio::test]
async fn test_cast_vote_with_vote_tipped_to_succeeded() {
// Arrange
let mut governance_test = GovernanceProgramTest::start_new().await;
let realm_cookie = governance_test.with_realm().await;
let governed_account_cookie = governance_test.with_governed_account().await;
let mut account_governance_cookie = governance_test
.with_account_governance(&realm_cookie, &governed_account_cookie)
.await
.unwrap();
let token_owner_record_cookie1 = governance_test
.with_initial_community_token_deposit(&realm_cookie)
.await;
let token_owner_record_cookie2 = governance_test
.with_initial_community_token_deposit(&realm_cookie)
.await;
let token_owner_record_cookie3 = governance_test
.with_initial_community_token_deposit(&realm_cookie)
.await;
governance_test
.mint_community_tokens(&realm_cookie, 20)
.await;
let proposal_cookie = governance_test
.with_signed_off_proposal(&token_owner_record_cookie1, &mut account_governance_cookie)
.await
.unwrap();
// Act
governance_test
.with_cast_vote(&proposal_cookie, &token_owner_record_cookie1, Vote::Yes)
.await
.unwrap();
// Assert
let proposal_account = governance_test
.get_proposal_account(&proposal_cookie.address)
.await;
assert_eq!(ProposalState::Voting, proposal_account.state);
// Act
governance_test
.with_cast_vote(&proposal_cookie, &token_owner_record_cookie2, Vote::No)
.await
.unwrap();
// Assert
let proposal_account = governance_test
.get_proposal_account(&proposal_cookie.address)
.await;
assert_eq!(ProposalState::Voting, proposal_account.state);
// Act
governance_test
.with_cast_vote(&proposal_cookie, &token_owner_record_cookie3, Vote::Yes)
.await
.unwrap();
// Assert
let proposal_account = governance_test
.get_proposal_account(&proposal_cookie.address)
.await;
assert_eq!(ProposalState::Succeeded, proposal_account.state);
}
#[tokio::test]
async fn test_cast_vote_with_vote_tipped_to_defeated() {
// Arrange
let mut governance_test = GovernanceProgramTest::start_new().await;
let realm_cookie = governance_test.with_realm().await;
let governed_account_cookie = governance_test.with_governed_account().await;
let mut account_governance_cookie = governance_test
.with_account_governance(&realm_cookie, &governed_account_cookie)
.await
.unwrap();
// 100 votes
let token_owner_record_cookie1 = governance_test
.with_initial_community_token_deposit(&realm_cookie)
.await;
// 100 votes
let token_owner_record_cookie2 = governance_test
.with_initial_community_token_deposit(&realm_cookie)
.await;
// 100 votes
let token_owner_record_cookie3 = governance_test
.with_initial_community_token_deposit(&realm_cookie)
.await;
// Total 320 votes
governance_test
.mint_community_tokens(&realm_cookie, 20)
.await;
let proposal_cookie = governance_test
.with_signed_off_proposal(&token_owner_record_cookie1, &mut account_governance_cookie)
.await
.unwrap();
// Act
governance_test
.with_cast_vote(&proposal_cookie, &token_owner_record_cookie1, Vote::Yes)
.await
.unwrap();
// Assert
let proposal_account = governance_test
.get_proposal_account(&proposal_cookie.address)
.await;
assert_eq!(ProposalState::Voting, proposal_account.state);
// Act
governance_test
.with_cast_vote(&proposal_cookie, &token_owner_record_cookie2, Vote::No)
.await
.unwrap();
// Assert
let proposal_account = governance_test
.get_proposal_account(&proposal_cookie.address)
.await;
assert_eq!(ProposalState::Voting, proposal_account.state);
// Act
governance_test
.with_cast_vote(&proposal_cookie, &token_owner_record_cookie3, Vote::No)
.await
.unwrap();
// Assert
let proposal_account = governance_test
.get_proposal_account(&proposal_cookie.address)
.await;
assert_eq!(ProposalState::Defeated, proposal_account.state);
}
#[tokio::test]
async fn test_cast_vote_with_threshold_below_50_and_vote_not_tipped() {
// Arrange
let mut governance_test = GovernanceProgramTest::start_new().await;
let realm_cookie = governance_test.with_realm().await;
let governed_account_cookie = governance_test.with_governed_account().await;
let mut governance_config =
governance_test.get_default_governance_config(&realm_cookie, &governed_account_cookie);
governance_config.yes_vote_threshold_percentage = 40;
let mut account_governance_cookie = governance_test
.with_account_governance_using_config(
&realm_cookie,
&governed_account_cookie,
&governance_config,
)
.await
.unwrap();
let token_owner_record_cookie = governance_test
.with_initial_community_token_deposit(&realm_cookie)
.await;
// Total 210 tokens
governance_test
.mint_community_tokens(&realm_cookie, 110)
.await;
let proposal_cookie = governance_test
.with_signed_off_proposal(&token_owner_record_cookie, &mut account_governance_cookie)
.await
.unwrap();
// Act
governance_test
.with_cast_vote(&proposal_cookie, &token_owner_record_cookie, Vote::Yes)
.await
.unwrap();
// Assert
let proposal_account = governance_test
.get_proposal_account(&proposal_cookie.address)
.await;
assert_eq!(ProposalState::Voting, proposal_account.state);
}
#[tokio::test]
async fn test_cast_vote_with_voting_time_expired_error() {
// Arrange
let mut governance_test = GovernanceProgramTest::start_new().await;
let realm_cookie = governance_test.with_realm().await;
let governed_account_cookie = governance_test.with_governed_account().await;
let mut account_governance_cookie = governance_test
.with_account_governance(&realm_cookie, &governed_account_cookie)
.await
.unwrap();
let token_owner_record_cookie = governance_test
.with_initial_community_token_deposit(&realm_cookie)
.await;
let proposal_cookie = governance_test
.with_signed_off_proposal(&token_owner_record_cookie, &mut account_governance_cookie)
.await
.unwrap();
let proposal_account = governance_test
.get_proposal_account(&proposal_cookie.address)
.await;
let vote_expired_at_slot = account_governance_cookie.account.config.max_voting_time
+ proposal_account.voting_at.unwrap()
+ 1;
governance_test
.context
.warp_to_slot(vote_expired_at_slot)
.unwrap();
// Act
let err = governance_test
.with_cast_vote(&proposal_cookie, &token_owner_record_cookie, Vote::No)
.await
.err()
.unwrap();
// Assert
assert_eq!(err, GovernanceError::ProposalVotingTimeExpired.into());
}
#[tokio::test]
async fn test_cast_vote_with_cast_twice_error() {
// Arrange
let mut governance_test = GovernanceProgramTest::start_new().await;
let realm_cookie = governance_test.with_realm().await;
let governed_account_cookie = governance_test.with_governed_account().await;
let mut account_governance_cookie = governance_test
.with_account_governance(&realm_cookie, &governed_account_cookie)
.await
.unwrap();
let token_owner_record_cookie = governance_test
.with_initial_community_token_deposit(&realm_cookie)
.await;
governance_test
.mint_community_tokens(&realm_cookie, 200)
.await;
let proposal_cookie = governance_test
.with_signed_off_proposal(&token_owner_record_cookie, &mut account_governance_cookie)
.await
.unwrap();
governance_test
.with_cast_vote(&proposal_cookie, &token_owner_record_cookie, Vote::Yes)
.await
.unwrap();
governance_test.context.warp_to_slot(5).unwrap();
// Act
let err = governance_test
.with_cast_vote(&proposal_cookie, &token_owner_record_cookie, Vote::Yes)
.await
.err()
.unwrap();
// Assert
assert_eq!(err, GovernanceError::VoteAlreadyExists.into());
}

View File

@ -66,11 +66,11 @@ async fn test_create_account_governance_with_invalid_config_error() {
let realm_cookie = governance_test.with_realm().await;
let governed_account_cookie = governance_test.with_governed_account().await;
// Arrange below 50% threshold
// Arrange below 1% threshold
let config = GovernanceConfig {
realm: realm_cookie.address,
governed_account: governed_account_cookie.address,
vote_threshold_percentage: 49, // below 50% threshold
yes_vote_threshold_percentage: 0, // below 1% threshold
min_tokens_to_create_proposal: 1,
min_instruction_hold_up_time: 1,
max_voting_time: 1,
@ -78,7 +78,7 @@ async fn test_create_account_governance_with_invalid_config_error() {
// Act
let err = governance_test
.with_account_governance_config(&realm_cookie, &governed_account_cookie, config)
.with_account_governance_using_config(&realm_cookie, &governed_account_cookie, &config)
.await
.err()
.unwrap();
@ -91,7 +91,7 @@ async fn test_create_account_governance_with_invalid_config_error() {
let config = GovernanceConfig {
realm: realm_cookie.address,
governed_account: governed_account_cookie.address,
vote_threshold_percentage: 101, // Above 100% threshold
yes_vote_threshold_percentage: 101, // Above 100% threshold
min_tokens_to_create_proposal: 1,
min_instruction_hold_up_time: 1,
max_voting_time: 1,
@ -99,7 +99,7 @@ async fn test_create_account_governance_with_invalid_config_error() {
// Act
let err = governance_test
.with_account_governance_config(&realm_cookie, &governed_account_cookie, config)
.with_account_governance_using_config(&realm_cookie, &governed_account_cookie, &config)
.await
.err()
.unwrap();

View File

@ -116,7 +116,7 @@ async fn test_create_program_governance_without_transferring_upgrade_authority_w
// Act
let err = governance_test
.with_program_governance_instruction(
.with_program_governance_using_instruction(
&realm_cookie,
&governed_program_cookie,
|i| {

View File

@ -234,7 +234,7 @@ async fn test_create_proposal_with_invalid_token_owner_record_error() {
// Act
let err = governance_test
.with_proposal_instruction(
.with_proposal_using_instruction(
&token_owner_record_cookie,
&mut account_governance_cookie,
|i| {
@ -250,6 +250,6 @@ async fn test_create_proposal_with_invalid_token_owner_record_error() {
// Assert
assert_eq!(
err,
GovernanceError::InvalidTokenOwnerRecordGoverningMint.into()
GovernanceError::InvalidGoverningMintForTokenOwnerRecord.into()
);
}

View File

@ -196,7 +196,7 @@ async fn test_deposit_initial_community_tokens_with_owner_must_sign_error() {
&token_source.pubkey(),
&token_owner.pubkey(),
&transfer_authority.pubkey(),
&governance_test.payer.pubkey(),
&governance_test.context.payer.pubkey(),
&realm_cookie.account.community_mint,
);
@ -241,7 +241,7 @@ async fn test_deposit_initial_community_tokens_with_invalid_owner_error() {
&token_source.pubkey(),
&invalid_owner.pubkey(),
&transfer_authority.pubkey(),
&governance_test.payer.pubkey(),
&governance_test.context.payer.pubkey(),
&realm_cookie.account.community_mint,
);

View File

@ -0,0 +1,262 @@
#![cfg(feature = "test-bpf")]
mod program_test;
use solana_program::pubkey::Pubkey;
use solana_program_test::tokio;
use program_test::*;
use spl_governance::{error::GovernanceError, instruction::Vote, state::enums::ProposalState};
#[tokio::test]
async fn test_finalize_vote_to_succeeded() {
// Arrange
let mut governance_test = GovernanceProgramTest::start_new().await;
let realm_cookie = governance_test.with_realm().await;
let governed_account_cookie = governance_test.with_governed_account().await;
let mut governance_config =
governance_test.get_default_governance_config(&realm_cookie, &governed_account_cookie);
governance_config.yes_vote_threshold_percentage = 40;
let mut account_governance_cookie = governance_test
.with_account_governance_using_config(
&realm_cookie,
&governed_account_cookie,
&governance_config,
)
.await
.unwrap();
let token_owner_record_cookie = governance_test
.with_initial_community_token_deposit(&realm_cookie)
.await;
// Total 210 tokens
governance_test
.mint_community_tokens(&realm_cookie, 110)
.await;
let proposal_cookie = governance_test
.with_signed_off_proposal(&token_owner_record_cookie, &mut account_governance_cookie)
.await
.unwrap();
governance_test
.with_cast_vote(&proposal_cookie, &token_owner_record_cookie, Vote::Yes)
.await
.unwrap();
// Ensure not tipped
let proposal_account = governance_test
.get_proposal_account(&proposal_cookie.address)
.await;
assert_eq!(ProposalState::Voting, proposal_account.state);
// Advance slot past max_voting_time
let vote_expired_at_slot = account_governance_cookie.account.config.max_voting_time
+ proposal_account.voting_at.unwrap()
+ 1;
governance_test
.context
.warp_to_slot(vote_expired_at_slot)
.unwrap();
// Act
governance_test
.finalize_vote(&proposal_cookie)
.await
.unwrap();
// Assert
let proposal_account = governance_test
.get_proposal_account(&proposal_cookie.address)
.await;
assert_eq!(ProposalState::Succeeded, proposal_account.state);
}
#[tokio::test]
async fn test_finalize_vote_to_defeated() {
// Arrange
let mut governance_test = GovernanceProgramTest::start_new().await;
let realm_cookie = governance_test.with_realm().await;
let governed_account_cookie = governance_test.with_governed_account().await;
let mut account_governance_cookie = governance_test
.with_account_governance(&realm_cookie, &governed_account_cookie)
.await
.unwrap();
let token_owner_record_cookie = governance_test
.with_initial_community_token_deposit(&realm_cookie)
.await;
// Total 300 tokens
governance_test
.mint_community_tokens(&realm_cookie, 200)
.await;
let proposal_cookie = governance_test
.with_signed_off_proposal(&token_owner_record_cookie, &mut account_governance_cookie)
.await
.unwrap();
governance_test
.with_cast_vote(&proposal_cookie, &token_owner_record_cookie, Vote::No)
.await
.unwrap();
// Ensure not tipped
let proposal_account = governance_test
.get_proposal_account(&proposal_cookie.address)
.await;
assert_eq!(ProposalState::Voting, proposal_account.state);
// Advance slot past max_voting_time
let vote_expired_at_slot = account_governance_cookie.account.config.max_voting_time
+ proposal_account.voting_at.unwrap()
+ 1;
governance_test
.context
.warp_to_slot(vote_expired_at_slot)
.unwrap();
// Act
governance_test
.finalize_vote(&proposal_cookie)
.await
.unwrap();
// Assert
let proposal_account = governance_test
.get_proposal_account(&proposal_cookie.address)
.await;
assert_eq!(ProposalState::Defeated, proposal_account.state);
}
#[tokio::test]
async fn test_finalize_vote_with_invalid_mint_error() {
// Arrange
let mut governance_test = GovernanceProgramTest::start_new().await;
let realm_cookie = governance_test.with_realm().await;
let governed_account_cookie = governance_test.with_governed_account().await;
let mut account_governance_cookie = governance_test
.with_account_governance(&realm_cookie, &governed_account_cookie)
.await
.unwrap();
let token_owner_record_cookie = governance_test
.with_initial_community_token_deposit(&realm_cookie)
.await;
// Total 300 tokens
governance_test
.mint_community_tokens(&realm_cookie, 200)
.await;
let mut proposal_cookie = governance_test
.with_signed_off_proposal(&token_owner_record_cookie, &mut account_governance_cookie)
.await
.unwrap();
governance_test
.with_cast_vote(&proposal_cookie, &token_owner_record_cookie, Vote::No)
.await
.unwrap();
// Ensure not tipped
let proposal_account = governance_test
.get_proposal_account(&proposal_cookie.address)
.await;
assert_eq!(ProposalState::Voting, proposal_account.state);
proposal_cookie.account.governing_token_mint = Pubkey::new_unique();
// Act
let err = governance_test
.finalize_vote(&proposal_cookie)
.await
.err()
.unwrap();
// Assert
assert_eq!(err, GovernanceError::InvalidGoverningMintForProposal.into());
}
#[tokio::test]
async fn test_finalize_vote_with_invalid_governance_error() {
// Arrange
let mut governance_test = GovernanceProgramTest::start_new().await;
let realm_cookie = governance_test.with_realm().await;
let governed_account_cookie = governance_test.with_governed_account().await;
let mut account_governance_cookie = governance_test
.with_account_governance(&realm_cookie, &governed_account_cookie)
.await
.unwrap();
let token_owner_record_cookie = governance_test
.with_initial_community_token_deposit(&realm_cookie)
.await;
// Total 300 tokens
governance_test
.mint_community_tokens(&realm_cookie, 200)
.await;
let mut proposal_cookie = governance_test
.with_signed_off_proposal(&token_owner_record_cookie, &mut account_governance_cookie)
.await
.unwrap();
governance_test
.with_cast_vote(&proposal_cookie, &token_owner_record_cookie, Vote::No)
.await
.unwrap();
// Ensure not tipped
let proposal_account = governance_test
.get_proposal_account(&proposal_cookie.address)
.await;
assert_eq!(ProposalState::Voting, proposal_account.state);
// Setup Governance for a different account
let governed_account_cookie2 = governance_test.with_governed_account().await;
let account_governance_cookie2 = governance_test
.with_account_governance(&realm_cookie, &governed_account_cookie2)
.await
.unwrap();
proposal_cookie.account.governance = account_governance_cookie2.address;
// Act
let err = governance_test
.finalize_vote(&proposal_cookie)
.await
.err()
.unwrap();
// Assert
assert_eq!(err, GovernanceError::InvalidGovernanceForProposal.into());
}

View File

@ -0,0 +1,402 @@
#![cfg(feature = "test-bpf")]
mod program_test;
use solana_program::{instruction::AccountMeta, pubkey::Pubkey};
use solana_program_test::tokio;
use program_test::*;
use spl_governance::{error::GovernanceError, instruction::Vote, state::enums::ProposalState};
#[tokio::test]
async fn test_relinquish_voted_proposal() {
// Arrange
let mut governance_test = GovernanceProgramTest::start_new().await;
let realm_cookie = governance_test.with_realm().await;
let governed_account_cookie = governance_test.with_governed_account().await;
let mut account_governance_cookie = governance_test
.with_account_governance(&realm_cookie, &governed_account_cookie)
.await
.unwrap();
let token_owner_record_cookie = governance_test
.with_initial_community_token_deposit(&realm_cookie)
.await;
let proposal_cookie = governance_test
.with_signed_off_proposal(&token_owner_record_cookie, &mut account_governance_cookie)
.await
.unwrap();
let mut vote_record_cookie = governance_test
.with_cast_vote(&proposal_cookie, &token_owner_record_cookie, Vote::Yes)
.await
.unwrap();
// Act
governance_test
.relinquish_vote(&proposal_cookie, &token_owner_record_cookie)
.await
.unwrap();
// Assert
let proposal_account = governance_test
.get_proposal_account(&proposal_cookie.address)
.await;
assert_eq!(100, proposal_account.yes_votes_count);
assert_eq!(ProposalState::Succeeded, proposal_account.state);
let token_owner_record = governance_test
.get_token_owner_record_account(&token_owner_record_cookie.address)
.await;
assert_eq!(0, token_owner_record.unrelinquished_votes_count);
assert_eq!(1, token_owner_record.total_votes_count);
let vote_record_account = governance_test
.get_vote_record_account(&vote_record_cookie.address)
.await;
vote_record_cookie.account.is_relinquished = true;
assert_eq!(vote_record_cookie.account, vote_record_account);
}
#[tokio::test]
async fn test_relinquish_active_yes_vote() {
// Arrange
let mut governance_test = GovernanceProgramTest::start_new().await;
let realm_cookie = governance_test.with_realm().await;
let governed_account_cookie = governance_test.with_governed_account().await;
let mut account_governance_cookie = governance_test
.with_account_governance(&realm_cookie, &governed_account_cookie)
.await
.unwrap();
let token_owner_record_cookie = governance_test
.with_initial_community_token_deposit(&realm_cookie)
.await;
// Total 300 tokens
governance_test
.mint_community_tokens(&realm_cookie, 200)
.await;
let proposal_cookie = governance_test
.with_signed_off_proposal(&token_owner_record_cookie, &mut account_governance_cookie)
.await
.unwrap();
let vote_record_cookie = governance_test
.with_cast_vote(&proposal_cookie, &token_owner_record_cookie, Vote::Yes)
.await
.unwrap();
// Act
governance_test
.relinquish_vote(&proposal_cookie, &token_owner_record_cookie)
.await
.unwrap();
// Assert
let proposal_account = governance_test
.get_proposal_account(&proposal_cookie.address)
.await;
assert_eq!(0, proposal_account.yes_votes_count);
assert_eq!(0, proposal_account.no_votes_count);
assert_eq!(ProposalState::Voting, proposal_account.state);
let token_owner_record = governance_test
.get_token_owner_record_account(&token_owner_record_cookie.address)
.await;
assert_eq!(0, token_owner_record.unrelinquished_votes_count);
assert_eq!(0, token_owner_record.total_votes_count);
let vote_record_account = governance_test
.get_account(&vote_record_cookie.address)
.await;
assert_eq!(None, vote_record_account);
}
#[tokio::test]
async fn test_relinquish_active_no_vote() {
// Arrange
let mut governance_test = GovernanceProgramTest::start_new().await;
let realm_cookie = governance_test.with_realm().await;
let governed_account_cookie = governance_test.with_governed_account().await;
let mut account_governance_cookie = governance_test
.with_account_governance(&realm_cookie, &governed_account_cookie)
.await
.unwrap();
let token_owner_record_cookie = governance_test
.with_initial_community_token_deposit(&realm_cookie)
.await;
// Total 300 tokens
governance_test
.mint_community_tokens(&realm_cookie, 200)
.await;
let proposal_cookie = governance_test
.with_signed_off_proposal(&token_owner_record_cookie, &mut account_governance_cookie)
.await
.unwrap();
let vote_record_cookie = governance_test
.with_cast_vote(&proposal_cookie, &token_owner_record_cookie, Vote::No)
.await
.unwrap();
// Act
governance_test
.relinquish_vote(&proposal_cookie, &token_owner_record_cookie)
.await
.unwrap();
// Assert
let proposal_account = governance_test
.get_proposal_account(&proposal_cookie.address)
.await;
assert_eq!(0, proposal_account.yes_votes_count);
assert_eq!(0, proposal_account.no_votes_count);
assert_eq!(ProposalState::Voting, proposal_account.state);
let token_owner_record = governance_test
.get_token_owner_record_account(&token_owner_record_cookie.address)
.await;
assert_eq!(0, token_owner_record.unrelinquished_votes_count);
assert_eq!(0, token_owner_record.total_votes_count);
let vote_record_account = governance_test
.get_account(&vote_record_cookie.address)
.await;
assert_eq!(None, vote_record_account);
}
#[tokio::test]
async fn test_relinquish_vote_with_invalid_mint_error() {
// Arrange
let mut governance_test = GovernanceProgramTest::start_new().await;
let realm_cookie = governance_test.with_realm().await;
let governed_account_cookie = governance_test.with_governed_account().await;
let mut account_governance_cookie = governance_test
.with_account_governance(&realm_cookie, &governed_account_cookie)
.await
.unwrap();
let token_owner_record_cookie = governance_test
.with_initial_community_token_deposit(&realm_cookie)
.await;
let mut proposal_cookie = governance_test
.with_signed_off_proposal(&token_owner_record_cookie, &mut account_governance_cookie)
.await
.unwrap();
governance_test
.with_cast_vote(&proposal_cookie, &token_owner_record_cookie, Vote::No)
.await
.unwrap();
proposal_cookie.account.governing_token_mint = Pubkey::new_unique();
// Act
let err = governance_test
.relinquish_vote(&proposal_cookie, &token_owner_record_cookie)
.await
.err()
.unwrap();
// Assert
assert_eq!(err, GovernanceError::InvalidGoverningMintForProposal.into());
}
#[tokio::test]
async fn test_relinquish_vote_with_governance_authority_must_sign_error() {
// Arrange
let mut governance_test = GovernanceProgramTest::start_new().await;
let realm_cookie = governance_test.with_realm().await;
let governed_account_cookie = governance_test.with_governed_account().await;
let mut account_governance_cookie = governance_test
.with_account_governance(&realm_cookie, &governed_account_cookie)
.await
.unwrap();
let mut token_owner_record_cookie = governance_test
.with_initial_community_token_deposit(&realm_cookie)
.await;
// Total 300 tokens
governance_test
.mint_community_tokens(&realm_cookie, 200)
.await;
let proposal_cookie = governance_test
.with_signed_off_proposal(&token_owner_record_cookie, &mut account_governance_cookie)
.await
.unwrap();
governance_test
.with_cast_vote(&proposal_cookie, &token_owner_record_cookie, Vote::No)
.await
.unwrap();
// Try to use a different owner to sign
let token_owner_record_cookie2 = governance_test
.with_initial_community_token_deposit(&realm_cookie)
.await;
token_owner_record_cookie.token_owner = token_owner_record_cookie2.token_owner;
// Act
let err = governance_test
.relinquish_vote(&proposal_cookie, &token_owner_record_cookie)
.await
.err()
.unwrap();
// Assert
assert_eq!(
err,
GovernanceError::GoverningTokenOwnerOrDelegateMustSign.into()
);
}
#[tokio::test]
async fn test_relinquish_vote_with_invalid_vote_record_error() {
// Arrange
let mut governance_test = GovernanceProgramTest::start_new().await;
let realm_cookie = governance_test.with_realm().await;
let governed_account_cookie = governance_test.with_governed_account().await;
let mut account_governance_cookie = governance_test
.with_account_governance(&realm_cookie, &governed_account_cookie)
.await
.unwrap();
let token_owner_record_cookie = governance_test
.with_initial_community_token_deposit(&realm_cookie)
.await;
let token_owner_record_cookie2 = governance_test
.with_initial_community_token_deposit(&realm_cookie)
.await;
// Total 400 tokens
governance_test
.mint_community_tokens(&realm_cookie, 200)
.await;
let proposal_cookie = governance_test
.with_signed_off_proposal(&token_owner_record_cookie, &mut account_governance_cookie)
.await
.unwrap();
governance_test
.with_cast_vote(&proposal_cookie, &token_owner_record_cookie, Vote::No)
.await
.unwrap();
let vote_record_cookie2 = governance_test
.with_cast_vote(&proposal_cookie, &token_owner_record_cookie2, Vote::Yes)
.await
.unwrap();
// // Act
let err = governance_test
.relinquish_vote_using_instruction(&proposal_cookie, &token_owner_record_cookie, |i| {
i.accounts[3] = AccountMeta::new(vote_record_cookie2.address, false)
// Try to use a vote_record for other token owner
})
.await
.err()
.unwrap();
// // Assert
assert_eq!(
err,
GovernanceError::InvalidGoverningTokenOwnerForVoteRecord.into()
);
}
#[tokio::test]
async fn test_relinquish_vote_with_already_relinquished_error() {
// Arrange
let mut governance_test = GovernanceProgramTest::start_new().await;
let realm_cookie = governance_test.with_realm().await;
let governed_account_cookie = governance_test.with_governed_account().await;
let mut account_governance_cookie = governance_test
.with_account_governance(&realm_cookie, &governed_account_cookie)
.await
.unwrap();
let token_owner_record_cookie = governance_test
.with_initial_community_token_deposit(&realm_cookie)
.await;
let proposal_cookie = governance_test
.with_signed_off_proposal(&token_owner_record_cookie, &mut account_governance_cookie)
.await
.unwrap();
let vote_record_cookie = governance_test
.with_cast_vote(&proposal_cookie, &token_owner_record_cookie, Vote::No)
.await
.unwrap();
governance_test
.relinquish_vote(&proposal_cookie, &token_owner_record_cookie)
.await
.unwrap();
// Ensure vote is relinquished
let vote_record_account = governance_test
.get_vote_record_account(&vote_record_cookie.address)
.await;
assert_eq!(true, vote_record_account.is_relinquished);
governance_test
.mint_community_tokens(&realm_cookie, 10)
.await;
// Act
let err = governance_test
.relinquish_vote(&proposal_cookie, &token_owner_record_cookie)
.await
.err()
.unwrap();
// Assert
assert_eq!(err, GovernanceError::VoteAlreadyRelinquished.into());
}

View File

@ -54,10 +54,8 @@ async fn test_remove_signatory() {
assert_eq!(ProposalState::Draft, proposal_account.state);
let signatory_account = governance_test
.banks_client
.get_account(signatory_record_cookie.address)
.await
.unwrap();
.get_account(&signatory_record_cookie.address)
.await;
assert_eq!(None, signatory_account);
}
@ -162,7 +160,7 @@ async fn test_remove_signatory_with_invalid_proposal_owner_error() {
}
#[tokio::test]
async fn test_remove_signatory_when_all_remaining_signed() {
async fn test_remove_signatory_with_not_editable_error() {
// Arrange
let mut governance_test = GovernanceProgramTest::start_new().await;
@ -199,21 +197,19 @@ async fn test_remove_signatory_when_all_remaining_signed() {
.unwrap();
// Act
governance_test
let err = governance_test
.remove_signatory(
&proposal_cookie,
&token_owner_record_cookie,
&signatory_record_cookie2,
)
.await
.err()
.unwrap();
// Assert
let proposal_account = governance_test
.get_proposal_account(&proposal_cookie.address)
.await;
assert_eq!(1, proposal_account.signatories_count);
assert_eq!(1, proposal_account.signatories_signed_off_count);
assert_eq!(ProposalState::Voting, proposal_account.state);
assert_eq!(
err,
GovernanceError::InvalidStateCannotEditSignatories.into()
);
}

View File

@ -2,10 +2,11 @@ use solana_program::pubkey::Pubkey;
use solana_sdk::signature::Keypair;
use spl_governance::state::{
governance::Governance, realm::Realm, token_owner_record::TokenOwnerRecord,
vote_record::VoteRecord,
};
use spl_governance::state::{proposal::Proposal, signatory_record::SignatoryRecord};
use super::tools::clone_keypair;
use crate::tools::clone_keypair;
#[derive(Debug)]
pub struct RealmCookie {
@ -37,8 +38,6 @@ pub struct TokeOwnerRecordCookie {
pub governance_authority: Option<Keypair>,
pub governance_delegate: Keypair,
pub governing_token_mint: Pubkey,
}
impl TokeOwnerRecordCookie {
@ -71,7 +70,7 @@ pub struct GovernedAccountCookie {
pub struct GovernanceCookie {
pub address: Pubkey,
pub account: Governance,
pub next_proposal_index: u16,
pub next_proposal_index: u32,
}
#[derive(Debug)]
@ -88,3 +87,9 @@ pub struct SignatoryRecordCookie {
pub account: SignatoryRecord,
pub signatory: Keypair,
}
#[derive(Debug)]
pub struct VoteRecordCookie {
pub address: Pubkey,
pub account: VoteRecord,
}

View File

@ -18,18 +18,20 @@ use solana_program_test::ProgramTest;
use solana_program_test::*;
use solana_sdk::{
account::Account,
signature::{Keypair, Signer},
transaction::Transaction,
};
use spl_governance::{
instruction::{
add_signatory, create_account_governance, create_program_governance, create_proposal,
create_realm, deposit_governing_tokens, remove_signatory, set_governance_delegate,
sign_off_proposal, withdraw_governing_tokens,
add_signatory, cancel_proposal, cast_vote, create_account_governance,
create_program_governance, create_proposal, create_realm, deposit_governing_tokens,
finalize_vote, relinquish_vote, remove_signatory, set_governance_delegate,
sign_off_proposal, withdraw_governing_tokens, Vote,
},
processor::process_instruction,
state::{
enums::{GovernanceAccountType, ProposalState},
enums::{GovernanceAccountType, ProposalState, VoteWeight},
governance::{
get_account_governance_address, get_program_governance_address, Governance,
GovernanceConfig,
@ -38,17 +40,18 @@ use spl_governance::{
realm::{get_governing_token_holding_address, get_realm_address, Realm},
signatory_record::{get_signatory_record_address, SignatoryRecord},
token_owner_record::{get_token_owner_record_address, TokenOwnerRecord},
vote_record::{get_vote_record_address, VoteRecord},
},
tools::bpf_loader_upgradeable::get_program_data_address,
};
pub mod cookies;
use crate::program_test::cookies::SignatoryRecordCookie;
use crate::program_test::{cookies::SignatoryRecordCookie, tools::clone_keypair};
use self::{
cookies::{
GovernanceCookie, GovernedAccountCookie, GovernedProgramCookie, ProposalCookie,
RealmCookie, TokeOwnerRecordCookie,
RealmCookie, TokeOwnerRecordCookie, VoteRecordCookie,
},
tools::NopOverride,
};
@ -57,9 +60,9 @@ pub mod tools;
use self::tools::map_transaction_error;
pub struct GovernanceProgramTest {
pub banks_client: BanksClient,
pub payer: Keypair,
pub context: ProgramTestContext,
pub rent: Rent,
pub next_realm_id: u8,
}
impl GovernanceProgramTest {
@ -70,14 +73,13 @@ impl GovernanceProgramTest {
processor!(process_instruction),
);
let (mut banks_client, payer, _) = program_test.start().await;
let rent = banks_client.get_rent().await.unwrap();
let mut context = program_test.start_with_context().await;
let rent = context.banks_client.get_rent().await.unwrap();
Self {
banks_client,
payer,
context,
rent,
next_realm_id: 0,
}
}
@ -87,19 +89,25 @@ impl GovernanceProgramTest {
signers: Option<&[&Keypair]>,
) -> Result<(), ProgramError> {
let mut transaction =
Transaction::new_with_payer(&instructions, Some(&self.payer.pubkey()));
Transaction::new_with_payer(&instructions, Some(&self.context.payer.pubkey()));
let mut all_signers = vec![&self.payer];
let mut all_signers = vec![&self.context.payer];
if let Some(signers) = signers {
all_signers.extend_from_slice(signers);
}
let recent_blockhash = self.banks_client.get_recent_blockhash().await.unwrap();
let recent_blockhash = self
.context
.banks_client
.get_recent_blockhash()
.await
.unwrap();
transaction.sign(&all_signers, recent_blockhash);
self.banks_client
self.context
.banks_client
.process_transaction(transaction)
.await
.map_err(map_transaction_error)?;
@ -109,7 +117,8 @@ impl GovernanceProgramTest {
#[allow(dead_code)]
pub async fn with_realm(&mut self) -> RealmCookie {
let name = "Realm".to_string();
let name = format!("Realm #{}", self.next_realm_id).to_string();
self.next_realm_id = self.next_realm_id + 1;
let realm_address = get_realm_address(&name);
@ -141,14 +150,14 @@ impl GovernanceProgramTest {
)
.await;
let create_proposal_instruction = create_realm(
let create_realm_instruction = create_realm(
&community_token_mint_keypair.pubkey(),
&self.payer.pubkey(),
&self.context.payer.pubkey(),
Some(council_token_mint_keypair.pubkey()),
name.clone(),
);
self.process_transaction(&[create_proposal_instruction], None)
self.process_transaction(&[create_realm_instruction], None)
.await
.unwrap();
@ -171,6 +180,54 @@ impl GovernanceProgramTest {
}
}
#[allow(dead_code)]
pub async fn with_realm_using_mints(&mut self, realm_cookie: &RealmCookie) -> RealmCookie {
let name = format!("Realm #{}", self.next_realm_id).to_string();
self.next_realm_id = self.next_realm_id + 1;
let realm_address = get_realm_address(&name);
let council_mint = realm_cookie.account.council_mint.unwrap();
let create_realm_instruction = create_realm(
&realm_cookie.account.community_mint,
&self.context.payer.pubkey(),
Some(council_mint),
name.clone(),
);
self.process_transaction(&[create_realm_instruction], None)
.await
.unwrap();
let account = Realm {
account_type: GovernanceAccountType::Realm,
community_mint: realm_cookie.account.community_mint,
council_mint: Some(council_mint),
name,
};
let community_token_holding_address = get_governing_token_holding_address(
&realm_address,
&realm_cookie.account.community_mint,
);
let council_token_holding_address =
get_governing_token_holding_address(&realm_address, &council_mint);
RealmCookie {
address: realm_address,
account,
community_mint_authority: clone_keypair(&realm_cookie.community_mint_authority),
community_token_holding_account: community_token_holding_address,
council_token_holding_account: Some(council_token_holding_address),
council_mint_authority: Some(clone_keypair(
&realm_cookie.council_mint_authority.as_ref().unwrap(),
)),
}
}
#[allow(dead_code)]
pub async fn with_initial_community_token_deposit(
&mut self,
@ -276,7 +333,7 @@ impl GovernanceProgramTest {
&token_source.pubkey(),
&token_owner.pubkey(),
&token_owner.pubkey(),
&self.payer.pubkey(),
&self.context.payer.pubkey(),
governing_mint,
);
@ -297,7 +354,7 @@ impl GovernanceProgramTest {
governing_token_owner: token_owner.pubkey(),
governing_token_deposit_amount: amount,
governance_delegate: None,
active_votes_count: 0,
unrelinquished_votes_count: 0,
total_votes_count: 0,
};
@ -312,10 +369,29 @@ impl GovernanceProgramTest {
token_owner,
governance_authority: None,
governance_delegate: governance_delegate,
governing_token_mint: *governing_mint,
}
}
#[allow(dead_code)]
pub async fn mint_community_tokens(&mut self, realm_cookie: &RealmCookie, amount: u64) {
let token_account_keypair = Keypair::new();
self.create_empty_token_account(
&token_account_keypair,
&realm_cookie.account.community_mint,
&self.context.payer.pubkey(),
)
.await;
self.mint_tokens(
&realm_cookie.account.community_mint,
&realm_cookie.community_mint_authority,
&token_account_keypair.pubkey(),
amount,
)
.await;
}
#[allow(dead_code)]
async fn with_governing_token_deposit(
&mut self,
@ -338,7 +414,7 @@ impl GovernanceProgramTest {
&token_owner_record_cookie.token_source,
&token_owner_record_cookie.token_owner.pubkey(),
&token_owner_record_cookie.token_owner.pubkey(),
&self.payer.pubkey(),
&self.context.payer.pubkey(),
governing_token_mint,
);
@ -484,38 +560,45 @@ impl GovernanceProgramTest {
}
}
pub fn get_default_governance_config(
&mut self,
realm_cookie: &RealmCookie,
governed_account_cookie: &GovernedAccountCookie,
) -> GovernanceConfig {
GovernanceConfig {
realm: realm_cookie.address,
governed_account: governed_account_cookie.address,
yes_vote_threshold_percentage: 60,
min_tokens_to_create_proposal: 5,
min_instruction_hold_up_time: 10,
max_voting_time: 10,
}
}
#[allow(dead_code)]
pub async fn with_account_governance(
&mut self,
realm_cookie: &RealmCookie,
governed_account_cookie: &GovernedAccountCookie,
) -> Result<GovernanceCookie, ProgramError> {
let config = GovernanceConfig {
realm: realm_cookie.address,
governed_account: governed_account_cookie.address,
vote_threshold_percentage: 60,
min_tokens_to_create_proposal: 5,
min_instruction_hold_up_time: 10,
max_voting_time: 100,
};
self.with_account_governance_config(realm_cookie, governed_account_cookie, config)
let config = self.get_default_governance_config(realm_cookie, governed_account_cookie);
self.with_account_governance_using_config(realm_cookie, governed_account_cookie, &config)
.await
}
#[allow(dead_code)]
pub async fn with_account_governance_config(
pub async fn with_account_governance_using_config(
&mut self,
realm_cookie: &RealmCookie,
governed_account_cookie: &GovernedAccountCookie,
governance_config: GovernanceConfig,
governance_config: &GovernanceConfig,
) -> Result<GovernanceCookie, ProgramError> {
let create_account_governance_instruction =
create_account_governance(&self.payer.pubkey(), governance_config.clone());
create_account_governance(&self.context.payer.pubkey(), governance_config.clone());
let account = Governance {
account_type: GovernanceAccountType::AccountGovernance,
config: governance_config,
config: governance_config.clone(),
proposals_count: 0,
};
@ -549,7 +632,7 @@ impl GovernanceProgramTest {
.minimum_balance(UpgradeableLoaderState::programdata_len(program_data.len()).unwrap());
let mut instructions = bpf_loader_upgradeable::create_buffer(
&self.payer.pubkey(),
&self.context.payer.pubkey(),
&program_buffer_keypair.pubkey(),
&program_upgrade_authority_keypair.pubkey(),
program_buffer_rent,
@ -573,7 +656,7 @@ impl GovernanceProgramTest {
.minimum_balance(UpgradeableLoaderState::program_len().unwrap());
let deploy_instructions = bpf_loader_upgradeable::deploy_with_max_program_len(
&self.payer.pubkey(),
&self.context.payer.pubkey(),
&program_keypair.pubkey(),
&program_buffer_keypair.pubkey(),
&program_upgrade_authority_keypair.pubkey(),
@ -609,7 +692,7 @@ impl GovernanceProgramTest {
realm_cookie: &RealmCookie,
governed_program_cookie: &GovernedProgramCookie,
) -> Result<GovernanceCookie, ProgramError> {
self.with_program_governance_instruction(
self.with_program_governance_using_instruction(
realm_cookie,
governed_program_cookie,
NopOverride,
@ -619,7 +702,7 @@ impl GovernanceProgramTest {
}
#[allow(dead_code)]
pub async fn with_program_governance_instruction<F: Fn(&mut Instruction)>(
pub async fn with_program_governance_using_instruction<F: Fn(&mut Instruction)>(
&mut self,
realm_cookie: &RealmCookie,
governed_program_cookie: &GovernedProgramCookie,
@ -632,12 +715,12 @@ impl GovernanceProgramTest {
min_tokens_to_create_proposal: 5,
min_instruction_hold_up_time: 10,
max_voting_time: 100,
vote_threshold_percentage: 60,
yes_vote_threshold_percentage: 60,
};
let mut create_program_governance_instruction = create_program_governance(
&governed_program_cookie.upgrade_authority.pubkey(),
&self.payer.pubkey(),
&self.context.payer.pubkey(),
config.clone(),
governed_program_cookie.transfer_upgrade_authority,
);
@ -672,12 +755,36 @@ impl GovernanceProgramTest {
token_owner_record_cookie: &TokeOwnerRecordCookie,
governance_cookie: &mut GovernanceCookie,
) -> Result<ProposalCookie, ProgramError> {
self.with_proposal_instruction(token_owner_record_cookie, governance_cookie, |_| {})
.await
self.with_proposal_using_instruction(
token_owner_record_cookie,
governance_cookie,
NopOverride,
)
.await
}
#[allow(dead_code)]
pub async fn with_proposal_instruction<F: Fn(&mut Instruction)>(
pub async fn with_signed_off_proposal(
&mut self,
token_owner_record_cookie: &TokeOwnerRecordCookie,
governance_cookie: &mut GovernanceCookie,
) -> Result<ProposalCookie, ProgramError> {
let proposal_cookie = self
.with_proposal(&token_owner_record_cookie, governance_cookie)
.await?;
let signatory_record_cookie = self
.with_signatory(&proposal_cookie, &token_owner_record_cookie)
.await?;
self.sign_off_proposal(&proposal_cookie, &signatory_record_cookie)
.await?;
Ok(proposal_cookie)
}
#[allow(dead_code)]
pub async fn with_proposal_using_instruction<F: Fn(&mut Instruction)>(
&mut self,
token_owner_record_cookie: &TokeOwnerRecordCookie,
governance_cookie: &mut GovernanceCookie,
@ -696,11 +803,11 @@ impl GovernanceProgramTest {
&governance_cookie.address,
&token_owner_record_cookie.token_owner.pubkey(),
&governance_authority.pubkey(),
&self.payer.pubkey(),
&self.context.payer.pubkey(),
&governance_cookie.account.config.realm,
name.clone(),
description_link.clone(),
&token_owner_record_cookie.governing_token_mint,
&token_owner_record_cookie.account.governing_token_mint,
proposal_index,
);
@ -717,7 +824,7 @@ impl GovernanceProgramTest {
description_link,
name: name.clone(),
governance: governance_cookie.address,
governing_token_mint: token_owner_record_cookie.governing_token_mint,
governing_token_mint: token_owner_record_cookie.account.governing_token_mint,
state: ProposalState::Draft,
signatories_count: 0,
// Clock always returns 1 when running under the test
@ -731,11 +838,13 @@ impl GovernanceProgramTest {
number_of_instructions: 0,
token_owner_record: token_owner_record_cookie.address,
signatories_signed_off_count: 0,
yes_votes_count: 0,
no_votes_count: 0,
};
let proposal_address = get_proposal_address(
&governance_cookie.address,
&token_owner_record_cookie.governing_token_mint,
&token_owner_record_cookie.account.governing_token_mint,
&proposal_index.to_le_bytes(),
);
@ -758,7 +867,7 @@ impl GovernanceProgramTest {
&proposal_cookie.address,
&token_owner_record_cookie.address,
&token_owner_record_cookie.token_owner.pubkey(),
&self.payer.pubkey(),
&self.context.payer.pubkey(),
&signatory.pubkey(),
);
@ -831,6 +940,136 @@ impl GovernanceProgramTest {
Ok(())
}
#[allow(dead_code)]
pub async fn finalize_vote(
&mut self,
proposal_cookie: &ProposalCookie,
) -> Result<(), ProgramError> {
let sign_off_proposal_instruction = finalize_vote(
&proposal_cookie.account.governance,
&proposal_cookie.address,
&proposal_cookie.account.governing_token_mint,
);
self.process_transaction(&[sign_off_proposal_instruction], None)
.await?;
Ok(())
}
#[allow(dead_code)]
pub async fn relinquish_vote(
&mut self,
proposal_cookie: &ProposalCookie,
token_owner_record_cookie: &TokeOwnerRecordCookie,
) -> Result<(), ProgramError> {
self.relinquish_vote_using_instruction(
proposal_cookie,
token_owner_record_cookie,
NopOverride,
)
.await
}
#[allow(dead_code)]
pub async fn relinquish_vote_using_instruction<F: Fn(&mut Instruction)>(
&mut self,
proposal_cookie: &ProposalCookie,
token_owner_record_cookie: &TokeOwnerRecordCookie,
instruction_override: F,
) -> Result<(), ProgramError> {
let mut relinquish_vote_instruction = relinquish_vote(
&proposal_cookie.account.governance,
&proposal_cookie.address,
&token_owner_record_cookie.address,
&proposal_cookie.account.governing_token_mint,
Some(token_owner_record_cookie.token_owner.pubkey()),
Some(self.context.payer.pubkey()),
);
instruction_override(&mut relinquish_vote_instruction);
self.process_transaction(
&[relinquish_vote_instruction],
Some(&[&token_owner_record_cookie.token_owner]),
)
.await?;
Ok(())
}
#[allow(dead_code)]
pub async fn cancel_proposal(
&mut self,
proposal_cookie: &ProposalCookie,
token_owner_record_cookie: &TokeOwnerRecordCookie,
) -> Result<(), ProgramError> {
let cancel_proposal_instruction = cancel_proposal(
&proposal_cookie.address,
&token_owner_record_cookie.address,
&token_owner_record_cookie.token_owner.pubkey(),
);
self.process_transaction(
&[cancel_proposal_instruction],
Some(&[&token_owner_record_cookie.token_owner]),
)
.await?;
Ok(())
}
#[allow(dead_code)]
pub async fn with_cast_vote(
&mut self,
proposal_cookie: &ProposalCookie,
token_owner_record_cookie: &TokeOwnerRecordCookie,
vote: Vote,
) -> Result<VoteRecordCookie, ProgramError> {
let vote_instruction = cast_vote(
&proposal_cookie.account.governance,
&proposal_cookie.address,
&token_owner_record_cookie.address,
&token_owner_record_cookie.token_owner.pubkey(),
&proposal_cookie.account.governing_token_mint,
&self.context.payer.pubkey(),
vote.clone(),
);
self.process_transaction(
&[vote_instruction],
Some(&[&token_owner_record_cookie.token_owner]),
)
.await?;
let vote_amount = token_owner_record_cookie
.account
.governing_token_deposit_amount;
let vote_weight = match vote {
Vote::Yes => VoteWeight::Yes(vote_amount),
Vote::No => VoteWeight::No(vote_amount),
};
let account = VoteRecord {
account_type: GovernanceAccountType::VoteRecord,
proposal: proposal_cookie.address,
governing_token_owner: token_owner_record_cookie.token_owner.pubkey(),
vote_weight,
is_relinquished: false,
};
let vote_record_cookie = VoteRecordCookie {
address: get_vote_record_address(
&proposal_cookie.address,
&token_owner_record_cookie.address,
),
account,
};
Ok(vote_record_cookie)
}
#[allow(dead_code)]
pub async fn get_token_owner_record_account(&mut self, address: &Pubkey) -> TokenOwnerRecord {
self.get_borsh_account::<TokenOwnerRecord>(address).await
@ -856,6 +1095,12 @@ impl GovernanceProgramTest {
self.get_borsh_account::<Proposal>(proposal_address).await
}
#[allow(dead_code)]
pub async fn get_vote_record_account(&mut self, vote_record_address: &Pubkey) -> VoteRecord {
self.get_borsh_account::<VoteRecord>(vote_record_address)
.await
}
#[allow(dead_code)]
pub async fn get_signatory_record_account(
&mut self,
@ -867,7 +1112,8 @@ impl GovernanceProgramTest {
#[allow(dead_code)]
async fn get_packed_account<T: Pack + IsInitialized>(&mut self, address: &Pubkey) -> T {
self.banks_client
self.context
.banks_client
.get_packed_account_data::<T>(*address)
.await
.unwrap()
@ -878,7 +1124,8 @@ impl GovernanceProgramTest {
&mut self,
address: &Pubkey,
) -> T {
self.banks_client
self.context
.banks_client
.get_account(*address)
.await
.unwrap()
@ -896,12 +1143,19 @@ impl GovernanceProgramTest {
/// TODO: Add to SDK
pub async fn get_borsh_account<T: BorshDeserialize>(&mut self, address: &Pubkey) -> T {
self.banks_client
self.get_account(address)
.await
.map(|a| try_from_slice_unchecked(&a.data).unwrap())
.expect(format!("GET-TEST-ACCOUNT-ERROR: Account {} not found", address).as_str())
}
#[allow(dead_code)]
pub async fn get_account(&mut self, address: &Pubkey) -> Option<Account> {
self.context
.banks_client
.get_account(*address)
.await
.unwrap()
.map(|a| try_from_slice_unchecked(&a.data).unwrap())
.expect(format!("GET-TEST-ACCOUNT-ERROR: Account {} not found", address).as_str())
}
#[allow(dead_code)]
@ -914,7 +1168,7 @@ impl GovernanceProgramTest {
let instructions = [
system_instruction::create_account(
&self.payer.pubkey(),
&self.context.payer.pubkey(),
&mint_keypair.pubkey(),
mint_rent,
spl_token::state::Mint::LEN as u64,
@ -935,6 +1189,38 @@ impl GovernanceProgramTest {
.unwrap();
}
#[allow(dead_code)]
pub async fn create_empty_token_account(
&mut self,
token_account_keypair: &Keypair,
token_mint: &Pubkey,
owner: &Pubkey,
) {
let create_account_instruction = system_instruction::create_account(
&self.context.payer.pubkey(),
&token_account_keypair.pubkey(),
self.rent
.minimum_balance(spl_token::state::Account::get_packed_len()),
spl_token::state::Account::get_packed_len() as u64,
&spl_token::id(),
);
let initialize_account_instruction = spl_token::instruction::initialize_account(
&spl_token::id(),
&token_account_keypair.pubkey(),
token_mint,
&owner,
)
.unwrap();
self.process_transaction(
&[create_account_instruction, initialize_account_instruction],
Some(&[&token_account_keypair]),
)
.await
.unwrap();
}
#[allow(dead_code)]
pub async fn create_token_account_with_transfer_authority(
&mut self,
@ -946,7 +1232,7 @@ impl GovernanceProgramTest {
transfer_authority: &Pubkey,
) {
let create_account_instruction = system_instruction::create_account(
&self.payer.pubkey(),
&self.context.payer.pubkey(),
&token_account_keypair.pubkey(),
self.rent
.minimum_balance(spl_token::state::Account::get_packed_len()),