Governance: Voting workflow
Co-authored-by: Jon Cinque <jon.cinque@gmail.com>
This commit is contained in:
parent
b8c2968cd1
commit
f59e43757b
|
@ -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 ----
|
||||
|
||||
|
|
|
@ -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(),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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(())
|
||||
}
|
|
@ -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(())
|
||||
}
|
|
@ -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>(
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
)?;
|
||||
|
|
|
@ -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(())
|
||||
}
|
|
@ -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(())
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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())?;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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(()));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>,
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
||||
|
|
|
@ -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>(
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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())
|
||||
}
|
|
@ -2,8 +2,6 @@
|
|||
|
||||
pub mod account;
|
||||
|
||||
pub mod token;
|
||||
|
||||
pub mod asserts;
|
||||
pub mod spl_token;
|
||||
|
||||
pub mod bpf_loader_upgradeable;
|
||||
|
|
|
@ -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))
|
||||
}
|
|
@ -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()
|
||||
);
|
||||
}
|
|
@ -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());
|
||||
}
|
|
@ -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();
|
||||
|
|
|
@ -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| {
|
||||
|
|
|
@ -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()
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
);
|
||||
|
||||
|
|
|
@ -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());
|
||||
}
|
|
@ -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());
|
||||
}
|
|
@ -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()
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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()),
|
||||
|
|
Loading…
Reference in New Issue