Governance: Create Proposals and Sign Off workflow (#1767)

Co-authored-by: Jon Cinque <jon.cinque@gmail.com>
This commit is contained in:
Sebastian Bor 2021-05-26 16:40:18 +01:00 committed by GitHub
parent ecc25f7d26
commit 94350d0e8e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
41 changed files with 2571 additions and 746 deletions

1
Cargo.lock generated
View File

@ -3819,6 +3819,7 @@ dependencies = [
"borsh 0.8.2",
"num-derive",
"num-traits",
"proptest",
"serde",
"serde_derive",
"solana-program",

View File

@ -25,6 +25,7 @@ thiserror = "1.0"
[dev-dependencies]
assert_matches = "1.5.0"
proptest = "0.10"
solana-program-test = "1.6.7"
solana-sdk = "1.6.7"

View File

@ -31,22 +31,66 @@ pub enum GovernanceError {
#[error("Governing Token Owner must sign transaction")]
GoverningTokenOwnerMustSign,
/// Governing Token Owner or Vote Authority must sign transaction
#[error("Governing Token Owner or Vote Authority must sign transaction")]
GoverningTokenOwnerOrVoteAuthrotiyMustSign,
/// Governing Token Owner or Delegate must sign transaction
#[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,
/// Invalid Voter account address
#[error("Invalid Voter account address")]
InvalidVoterAccountAddress,
/// 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 Token Owner Record Realm
#[error("Invalid Token Owner Record Realm")]
InvalidTokenOwnerRecordRealm,
/// Invalid Signatory account address
#[error("Invalid Signatory account address")]
InvalidSignatoryAddress,
/// Signatory already signed off
#[error("Signatory already signed off")]
SignatoryAlreadySignedOff,
/// Signatory must sign
#[error("Signatory must sign")]
SignatoryMustSign,
/// Invalid Proposal Owner
#[error("Invalid Proposal Owner")]
InvalidProposalOwnerAccount,
/// Invalid Governance config
#[error("Invalid Governance config")]
InvalidGovernanceConfig,
/// Proposal for the given Governance, Governing Token Mint and index already exists
#[error("Proposal for the given Governance, Governing Token Mint and index already exists")]
ProposalAlreadyExists,
/// Owner doesn't have enough governing tokens to create Proposal
#[error("Owner doesn't have enough governing tokens to create Proposal")]
NotEnoughTokensToCreateProposal,
/// Invalid State: Can't edit Signatories
#[error("Invalid State: Can't edit Signatories")]
InvalidStateCannotEditSignatories,
/// Invalid State: Can't sign off
#[error("Invalid State: Can't sign off")]
InvalidStateCannotSignOff,
/// Invalid Signatory Mint
#[error("Invalid Signatory Mint")]
InvalidSignatoryMint,
/// ---- Account Tools Errors ----
/// Invalid account owner

View File

@ -3,13 +3,14 @@
use crate::{
id,
state::{
enums::GoverningTokenType,
governance::{
get_account_governance_address, get_program_governance_address, GovernanceConfig,
},
proposal::get_proposal_address,
realm::{get_governing_token_holding_address, get_realm_address},
signatory_record::get_signatory_record_address,
single_signer_instruction::InstructionData,
voter_record::get_voter_record_address,
token_owner_record::get_token_owner_record_address,
},
tools::bpf_loader_upgradeable::get_program_data_address,
};
@ -64,7 +65,7 @@ pub enum GovernanceInstruction {
/// 2. `[writable]` Governing Token Source account. All tokens from the account will be transferred to the Holding account
/// 3. `[signer]` Governing Token Owner account
/// 4. `[signer]` Governing Token Transfer authority
/// 5. `[writable]` Voter Record account. PDA seeds: ['governance',realm, governing_token_mint, governing_token_owner]
/// 5. `[writable]` Token Owner Record account. PDA seeds: ['governance',realm, governing_token_mint, governing_token_owner]
/// 6. `[signer]` Payer
/// 7. `[]` System
/// 8. `[]` SPL Token
@ -79,40 +80,30 @@ pub enum GovernanceInstruction {
/// 1. `[writable]` Governing Token Holding account. PDA seeds: ['governance',realm, governing_token_mint]
/// 2. `[writable]` Governing Token Destination account. All tokens will be transferred to this account
/// 3. `[signer]` Governing Token Owner account
/// 4. `[writable]` Voter Record account. PDA seeds: ['governance',realm, governing_token_mint, governing_token_owner]
/// 4. `[writable]` Token Owner Record account. PDA seeds: ['governance',realm, governing_token_mint, governing_token_owner]
/// 5. `[]` SPL Token
WithdrawGoverningTokens {},
/// Sets vote authority for the given Realm and Governing Token Mint (Community or Council)
/// The vote authority would have voting rights and could vote on behalf of the Governing Token Owner
/// Note: This doesn't take voting rights from the Token Owner who still can vote and change vote_authority
/// Sets Governance Delegate for the given Realm and Governing Token Mint (Community or Council)
/// The Delegate would have voting rights and could vote on behalf of the Governing Token Owner
/// The Delegate would also be able to create Proposals on behalf of the Governing Token Owner
/// Note: This doesn't take voting rights from the Token Owner who still can vote and change governance_delegate
///
/// 0. `[signer]` Current Vote authority or Governing Token owner
/// 1. `[writable]` Voter Record
SetVoteAuthority {
/// 0. `[signer]` Current Governance Delegate or Governing Token owner
/// 1. `[writable]` Token Owner Record
SetGovernanceDelegate {
#[allow(dead_code)]
/// Governance Realm the new vote authority is set for
realm: Pubkey,
#[allow(dead_code)]
/// Governing Token Mint the vote authority is granted over
governing_token_mint: Pubkey,
#[allow(dead_code)]
/// Governing Token Owner the vote authority is set for
governing_token_owner: Pubkey,
#[allow(dead_code)]
/// New vote authority
new_vote_authority: Option<Pubkey>,
/// New Governance Delegate
new_governance_delegate: Option<Pubkey>,
},
/// Creates Account Governance account which can be used to govern an arbitrary account
///
/// 0. `[writable]` Account Governance account. PDA seeds: ['account-governance', realm, governed_account]
/// 1. `[signer]` Payer
/// 2. `[]` System program
/// 3. `[]` Sysvar Rent
/// 0. `[]` Realm account the created Governance belongs to
/// 1. `[writable]` Account Governance account. PDA seeds: ['account-governance', realm, governed_account]
/// 2. `[signer]` Payer
/// 3. `[]` System program
/// 4. `[]` Sysvar Rent
CreateAccountGovernance {
/// Governance config
#[allow(dead_code)]
@ -121,13 +112,14 @@ pub enum GovernanceInstruction {
/// Creates Program Governance account which governs an upgradable program
///
/// 0. `[writable]` Program Governance account. PDA seeds: ['program-governance', realm, governed_program]
/// 1. `[writable]` Program Data account of the Program governed by this Governance account
/// 2. `[signer]` Current Upgrade Authority account of the Program governed by this Governance account
/// 3. `[signer]` Payer
/// 4. `[]` bpf_upgradeable_loader program
/// 5. `[]` System program
/// 6. `[]` Sysvar Rent
/// 0. `[]` Realm account the created Governance belongs to
/// 1. `[writable]` Program Governance account. PDA seeds: ['program-governance', realm, governed_program]
/// 2. `[writable]` Program Data account of the Program governed by this Governance account
/// 3. `[signer]` Current Upgrade Authority account of the Program governed by this Governance account
/// 4. `[signer]` Payer
/// 5. `[]` bpf_upgradeable_loader program
/// 6. `[]` System program
/// 7. `[]` Sysvar Rent
CreateProgramGovernance {
/// Governance config
#[allow(dead_code)]
@ -140,58 +132,64 @@ pub enum GovernanceInstruction {
transfer_upgrade_authority: bool,
},
/// Create Proposal account for Instructions that will be executed at various slots in the future
/// The instruction also grants Admin and Signatory token to the provided account
/// Creates Proposal account for Instructions that will be executed at various slots in the future
///
/// 0. `[writable]` Uninitialized Proposal account
/// 1. `[writable]` Initialized Governance account
/// 2. `[writable]` Initialized Signatory Mint account
/// 3. `[writable]` Initialized Admin Mint account
/// 4. `[writable]` Initialized Admin account for the issued admin token
/// 5. `[writable]` Initialized Signatory account for the issued signatory token
/// 6. '[]` Token program account
/// 7. `[]` Rent sysvar
/// 0. `[writable]` Proposal account. PDA seeds ['governance',governance, governing_token_mint, proposal_index]
/// 1. `[writable]` Governance account
/// 2. `[]` Token Owner Record account
/// 3. `[signer]` Governance Authority (Token Owner or Governance Delegate)
/// 4. `[signer]` Payer
/// 5. `[]` System program
/// 6. `[]` Rent sysvar
/// 7. `[]` Clock sysvar
CreateProposal {
#[allow(dead_code)]
/// Link to gist explaining proposal
description_link: String,
#[allow(dead_code)]
/// UTF-8 encoded name of the proposal
name: String,
#[allow(dead_code)]
/// The Governing token (Community or Council) which will be used for voting on the Proposal
governing_token_type: GoverningTokenType,
/// Link to gist explaining proposal
description_link: String,
#[allow(dead_code)]
/// Governing Token Mint the Proposal is created for
governing_token_mint: Pubkey,
},
/// [Requires Admin token]
/// Adds a signatory to the Proposal which means this Proposal can't leave Draft state until yet another Signatory signs
/// As a result of this call the new Signatory will receive a Signatory Token which then can be used to Sign proposal
///
/// 0. `[writable]` Proposal account
/// 1. `[writable]` Initialized Signatory account
/// 2. `[writable]` Initialized Signatory Mint account
/// 3. `[signer]` Admin account
/// 4. '[]` Token program account
AddSignatory,
/// 1. `[]` Token Owner Record account
/// 2. `[signer]` Governance Authority (Token Owner or Governance Delegate)
/// 3. `[writable]` Signatory Record Account
/// 4. `[signer]` Payer
/// 5. `[]` System program
/// 6. `[]` Rent sysvar
AddSignatory {
#[allow(dead_code)]
/// Signatory to add to the Proposal
signatory: Pubkey,
},
/// [Requires Admin token]
/// Removes a Signatory from the Proposal
///
/// 0. `[writable]` Proposal account
/// 1. `[writable]` Signatory account to remove token from
/// 2. `[writable]` Signatory Mint account
/// 3. `[signer]` Admin account
/// 4. '[]` Token program account
RemoveSignatory,
/// 1. `[]` Token Owner Record account
/// 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
signatory: Pubkey,
},
/// [Requires Admin token]
/// Adds an instruction to the Proposal. Max of 5 of any type. More than 5 will throw error
///
/// 0. `[writable]` Proposal account
/// 1. `[writable]` Uninitialized Proposal SingleSignerInstruction account
/// 2. `[signer]` Admin account
/// 2. `[signer]` Governance Authority (Token Owner or Governance Delegate)
AddSingleSignerInstruction {
#[allow(dead_code)]
/// Slot waiting time between vote period ending and this being eligible for execution
@ -206,42 +204,37 @@ pub enum GovernanceInstruction {
position: u8,
},
/// [Requires Admin token]
/// Remove instruction from the Proposal
///
/// 0. `[writable]` Proposal account
/// 1. `[writable]` Proposal SingleSignerInstruction account
/// 2. `[signer]` Admin account
/// 2. `[signer]` Governance Authority (Token Owner or Governance Delegate)
RemoveInstruction,
/// [Requires Admin token]
/// Update instruction hold up time in the Proposal
///
/// 0. `[]` Proposal account
/// 1. `[writable]` Proposal SingleSignerInstruction account
/// 2. `[signer]` Admin account
/// 2. `[signer]` Governance Authority (Token Owner or Governance Delegate)
UpdateInstructionHoldUpTime {
#[allow(dead_code)]
/// Minimum waiting time in slots for an instruction to be executed after proposal is voted on
hold_up_time: u64,
},
/// [Requires Admin token]
/// Cancels Proposal and moves it into Canceled
///
/// 0. `[writable]` Proposal account
/// 1. `[writable]` Admin account
/// 1. `[signer]` Governance Authority (Token Owner or Governance Delegate)
CancelProposal,
/// [Requires Signatory token]
/// Burns signatory token, indicating you approve and sign off on moving this Proposal from Draft state to Voting state
/// The last Signatory token to be burned moves the state to Voting
/// Signs off Proposal indicating the Signatory approves the Proposal
/// When the last Signatory signs the Proposal state moves to Voting state
///
/// 0. `[writable]` Proposal account
/// 1. `[writable]` Signatory account
/// 2. `[writable]` Signatory Mint account
/// 3. `[]` Token program account
/// 4. `[]` Clock sysvar
/// 1. `[writable]` Signatory Record account
/// 2. `[signer]` Signatory account
/// 3. `[]` Clock sysvar
SignOffProposal,
/// Uses your voter weight (deposited Community or Council tokens) to cast a vote on a Proposal
@ -249,9 +242,9 @@ pub enum GovernanceInstruction {
/// If you tip the consensus then the instructions can begin to be run after their hold up time
///
/// 0. `[writable]` Proposal account
/// 1. `[writable]` Voter Record account. PDA seeds: ['governance',realm, governing_token_mint, governing_token_owner]
/// 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]` Vote Authority account
/// 3. `[signer]` Governance Authority account
/// 4. `[]` Governance account
Vote {
#[allow(dead_code)]
@ -265,9 +258,9 @@ pub enum GovernanceInstruction {
/// 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]` Voter Record account. PDA seeds: ['governance',realm, governing_token_mint, governing_token_owner]
/// 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]` Vote Authority account
/// 3. `[signer]` Governance Authority account
RelinquishVote,
/// Executes an instruction in the Proposal
@ -336,7 +329,7 @@ pub fn deposit_governing_tokens(
governing_token_mint: &Pubkey,
) -> Instruction {
let vote_record_address =
get_voter_record_address(realm, governing_token_mint, governing_token_owner);
get_token_owner_record_address(realm, governing_token_mint, governing_token_owner);
let governing_token_holding_address =
get_governing_token_holding_address(realm, governing_token_mint);
@ -373,7 +366,7 @@ pub fn withdraw_governing_tokens(
governing_token_mint: &Pubkey,
) -> Instruction {
let vote_record_address =
get_voter_record_address(realm, governing_token_mint, governing_token_owner);
get_token_owner_record_address(realm, governing_token_mint, governing_token_owner);
let governing_token_holding_address =
get_governing_token_holding_address(realm, governing_token_mint);
@ -396,29 +389,26 @@ pub fn withdraw_governing_tokens(
}
}
/// Creates SetVoteAuthority instruction
pub fn set_vote_authority(
/// Creates SetGovernanceDelegate instruction
pub fn set_governance_delegate(
// Accounts
vote_authority: &Pubkey,
governance_authority: &Pubkey,
// Args
realm: &Pubkey,
governing_token_mint: &Pubkey,
governing_token_owner: &Pubkey,
new_vote_authority: &Option<Pubkey>,
new_governance_delegate: &Option<Pubkey>,
) -> Instruction {
let vote_record_address =
get_voter_record_address(realm, governing_token_mint, governing_token_owner);
get_token_owner_record_address(realm, governing_token_mint, governing_token_owner);
let accounts = vec![
AccountMeta::new_readonly(*vote_authority, true),
AccountMeta::new_readonly(*governance_authority, true),
AccountMeta::new(vote_record_address, false),
];
let instruction = GovernanceInstruction::SetVoteAuthority {
realm: *realm,
governing_token_mint: *governing_token_mint,
governing_token_owner: *governing_token_owner,
new_vote_authority: *new_vote_authority,
let instruction = GovernanceInstruction::SetGovernanceDelegate {
new_governance_delegate: *new_governance_delegate,
};
Instruction {
@ -490,3 +480,138 @@ pub fn create_program_governance(
data: instruction.try_to_vec().unwrap(),
}
}
/// Creates CreateProposal instruction
#[allow(clippy::too_many_arguments)]
pub fn create_proposal(
// Accounts
governance: &Pubkey,
governing_token_owner: &Pubkey,
governance_authority: &Pubkey,
payer: &Pubkey,
// Args
realm: &Pubkey,
name: String,
description_link: String,
governing_token_mint: &Pubkey,
proposal_index: u16,
) -> Instruction {
let proposal_address = get_proposal_address(
governance,
governing_token_mint,
&proposal_index.to_le_bytes(),
);
let token_owner_record_address =
get_token_owner_record_address(realm, governing_token_mint, governing_token_owner);
let accounts = vec![
AccountMeta::new(proposal_address, false),
AccountMeta::new(*governance, false),
AccountMeta::new_readonly(token_owner_record_address, false),
AccountMeta::new_readonly(*governance_authority, true),
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::CreateProposal {
name,
description_link,
governing_token_mint: *governing_token_mint,
};
Instruction {
program_id: id(),
accounts,
data: instruction.try_to_vec().unwrap(),
}
}
/// Creates AddSignatory instruction
pub fn add_signatory(
// Accounts
proposal: &Pubkey,
token_owner_record: &Pubkey,
governance_authority: &Pubkey,
payer: &Pubkey,
// Args
signatory: &Pubkey,
) -> Instruction {
let signatory_record_address = get_signatory_record_address(proposal, signatory);
let accounts = vec![
AccountMeta::new(*proposal, false),
AccountMeta::new_readonly(*token_owner_record, false),
AccountMeta::new_readonly(*governance_authority, true),
AccountMeta::new(signatory_record_address, false),
AccountMeta::new_readonly(*payer, true),
AccountMeta::new_readonly(system_program::id(), false),
AccountMeta::new_readonly(sysvar::rent::id(), false),
];
let instruction = GovernanceInstruction::AddSignatory {
signatory: *signatory,
};
Instruction {
program_id: id(),
accounts,
data: instruction.try_to_vec().unwrap(),
}
}
/// Creates RemoveSignatory instruction
pub fn remove_signatory(
// Accounts
proposal: &Pubkey,
token_owner_record: &Pubkey,
governance_authority: &Pubkey,
signatory: &Pubkey,
beneficiary: &Pubkey,
) -> Instruction {
let signatory_record_address = get_signatory_record_address(proposal, signatory);
let accounts = vec![
AccountMeta::new(*proposal, false),
AccountMeta::new_readonly(*token_owner_record, false),
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 {
signatory: *signatory,
};
Instruction {
program_id: id(),
accounts,
data: instruction.try_to_vec().unwrap(),
}
}
/// Creates SignOffProposal instruction
pub fn sign_off_proposal(
// Accounts
proposal: &Pubkey,
signatory: &Pubkey,
) -> Instruction {
let signatory_record_address = get_signatory_record_address(proposal, signatory);
let accounts = vec![
AccountMeta::new(*proposal, false),
AccountMeta::new(signatory_record_address, false),
AccountMeta::new_readonly(*signatory, true),
AccountMeta::new_readonly(sysvar::clock::id(), false),
];
let instruction = GovernanceInstruction::SignOffProposal;
Instruction {
program_id: id(),
accounts,
data: instruction.try_to_vec().unwrap(),
}
}

View File

@ -1,20 +1,28 @@
//! Program processor
mod process_add_signatory;
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_set_vote_authority;
mod process_remove_signatory;
mod process_set_governance_delegate;
mod process_sign_off_proposal;
mod process_withdraw_governing_tokens;
use crate::instruction::GovernanceInstruction;
use borsh::BorshDeserialize;
use process_add_signatory::*;
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_set_vote_authority::*;
use process_remove_signatory::*;
use process_set_governance_delegate::*;
use process_sign_off_proposal::*;
use process_withdraw_governing_tokens::*;
use solana_program::{
@ -31,7 +39,7 @@ pub fn process_instruction(
let instruction = GovernanceInstruction::try_from_slice(input)
.map_err(|_| ProgramError::InvalidInstructionData)?;
msg!("Instruction: {:?}", instruction);
msg!("GOVERNANCE-INSTRUCTION: {:?}", instruction);
match instruction {
GovernanceInstruction::CreateRealm { name } => {
@ -46,18 +54,9 @@ pub fn process_instruction(
process_withdraw_governing_tokens(program_id, accounts)
}
GovernanceInstruction::SetVoteAuthority {
realm,
governing_token_mint,
governing_token_owner,
new_vote_authority,
} => process_set_vote_authority(
accounts,
&realm,
&governing_token_mint,
&governing_token_owner,
&new_vote_authority,
),
GovernanceInstruction::SetGovernanceDelegate {
new_governance_delegate,
} => process_set_governance_delegate(accounts, &new_governance_delegate),
GovernanceInstruction::CreateProgramGovernance {
config,
transfer_upgrade_authority,
@ -70,6 +69,26 @@ pub fn process_instruction(
GovernanceInstruction::CreateAccountGovernance { config } => {
process_create_account_governance(program_id, accounts, config)
}
GovernanceInstruction::CreateProposal {
name,
description_link,
governing_token_mint,
} => process_create_proposal(
program_id,
accounts,
name,
description_link,
governing_token_mint,
),
GovernanceInstruction::AddSignatory { signatory } => {
process_add_signatory(program_id, accounts, signatory)
}
GovernanceInstruction::RemoveSignatory { signatory } => {
process_remove_signatory(program_id, accounts, signatory)
}
GovernanceInstruction::SignOffProposal {} => {
process_sign_off_proposal(program_id, accounts)
}
_ => todo!("Instruction not implemented yet"),
}
}

View File

@ -0,0 +1,76 @@
//! Program state processor
use borsh::BorshSerialize;
use solana_program::{
account_info::{next_account_info, AccountInfo},
entrypoint::ProgramResult,
pubkey::Pubkey,
rent::Rent,
sysvar::Sysvar,
};
use crate::{
state::{
enums::GovernanceAccountType,
proposal::deserialize_proposal_raw,
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,
},
};
/// Processes AddSignatory instruction
pub fn process_add_signatory(
program_id: &Pubkey,
accounts: &[AccountInfo],
signatory: Pubkey,
) -> 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 signatory_record_info = next_account_info(account_info_iter)?; // 3
let payer_info = next_account_info(account_info_iter)?; // 4
let system_info = next_account_info(account_info_iter)?; // 5
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)?;
proposal_data.assert_can_edit_signatories()?;
let token_owner_record_data = deserialize_token_owner_record_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)?;
let signatory_record_data = SignatoryRecord {
account_type: GovernanceAccountType::SignatoryRecord,
proposal: *proposal_info.key,
signatory,
signed_off: false,
};
create_and_serialize_account_signed::<SignatoryRecord>(
payer_info,
signatory_record_info,
&signatory_record_data,
&get_signatory_record_address_seeds(proposal_info.key, &signatory),
program_id,
system_info,
rent,
)?;
proposal_data.signatories_count = proposal_data.signatories_count.checked_add(1).unwrap();
proposal_data.serialize(&mut *proposal_info.data.borrow_mut())?;
Ok(())
}

View File

@ -39,7 +39,7 @@ pub fn process_create_account_governance(
let account_governance_data = Governance {
account_type: GovernanceAccountType::AccountGovernance,
config: config.clone(),
proposal_count: 0,
proposals_count: 0,
};
create_and_serialize_account_signed::<Governance>(

View File

@ -52,7 +52,7 @@ pub fn process_create_program_governance(
let program_governance_data = Governance {
account_type: GovernanceAccountType::ProgramGovernance,
config: config.clone(),
proposal_count: 0,
proposals_count: 0,
};
create_and_serialize_account_signed::<Governance>(

View File

@ -0,0 +1,115 @@
//! Program state processor
use borsh::BorshSerialize;
use solana_program::{
account_info::{next_account_info, AccountInfo},
clock::Clock,
entrypoint::ProgramResult,
pubkey::Pubkey,
rent::Rent,
sysvar::Sysvar,
};
use crate::{
error::GovernanceError,
state::{
enums::{GovernanceAccountType, ProposalState},
governance::deserialize_governance_raw,
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,
},
};
/// Processes CreateProposal instruction
pub fn process_create_proposal(
program_id: &Pubkey,
accounts: &[AccountInfo],
name: String,
description_link: String,
governing_token_mint: Pubkey,
) -> ProgramResult {
let account_info_iter = &mut accounts.iter();
let proposal_info = next_account_info(account_info_iter)?; // 0
let governance_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 payer_info = next_account_info(account_info_iter)?; // 4
let system_info = next_account_info(account_info_iter)?; // 5
let rent_sysvar_info = next_account_info(account_info_iter)?; // 6
let rent = &Rent::from_account_info(rent_sysvar_info)?;
let clock_info = next_account_info(account_info_iter)?; // 7
let clock = Clock::from_account_info(clock_info)?;
if !proposal_info.data_is_empty() {
return Err(GovernanceError::ProposalAlreadyExists.into());
}
let mut governance_data = deserialize_governance_raw(governance_info)?;
let token_owner_record_data = deserialize_token_owner_record_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)?;
if token_owner_record_data.governing_token_deposit_amount
< governance_data.config.min_tokens_to_create_proposal as u64
{
return Err(GovernanceError::NotEnoughTokensToCreateProposal.into());
}
let proposal_data = Proposal {
account_type: GovernanceAccountType::Proposal,
governance: *governance_info.key,
governing_token_mint,
state: ProposalState::Draft,
token_owner_record: *token_owner_record_info.key,
signatories_count: 0,
signatories_signed_off_count: 0,
name,
description_link,
draft_at: clock.slot,
signing_off_at: None,
voting_at: None,
voting_completed_at: None,
executing_at: None,
closed_at: None,
number_of_executed_instructions: 0,
number_of_instructions: 0,
};
create_and_serialize_account_signed::<Proposal>(
payer_info,
proposal_info,
&proposal_data,
&get_proposal_address_seeds(
governance_info.key,
&governing_token_mint,
&governance_data.proposals_count.to_le_bytes(),
),
program_id,
system_info,
rent,
)?;
governance_data.proposals_count = governance_data.proposals_count.checked_add(1).unwrap();
governance_data.serialize(&mut *governance_info.data.borrow_mut())?;
Ok(())
}

View File

@ -12,9 +12,11 @@ use solana_program::{
use crate::{
error::GovernanceError,
state::{
enums::{GovernanceAccountType, GoverningTokenType},
realm::deserialize_realm,
voter_record::{deserialize_voter_record, get_voter_record_address_seeds, VoterRecord},
enums::GovernanceAccountType,
realm::deserialize_realm_raw,
token_owner_record::{
deserialize_token_owner_record, get_token_owner_record_address_seeds, TokenOwnerRecord,
},
},
tools::{
account::create_and_serialize_account_signed,
@ -37,7 +39,7 @@ pub fn process_deposit_governing_tokens(
let governing_token_source_info = next_account_info(account_info_iter)?; // 2
let governing_token_owner_info = next_account_info(account_info_iter)?; // 3
let governing_token_transfer_authority_info = next_account_info(account_info_iter)?; // 4
let voter_record_info = next_account_info(account_info_iter)?; // 5
let token_owner_record_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 spl_token_info = next_account_info(account_info_iter)?; // 8
@ -45,16 +47,10 @@ 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(realm_info)?;
let realm_data = deserialize_realm_raw(realm_info)?;
let governing_token_mint = get_mint_from_token_account(governing_token_holding_info)?;
let governing_token_type = if governing_token_mint == realm_data.community_mint {
GoverningTokenType::Community
} else if Some(governing_token_mint) == realm_data.council_mint {
GoverningTokenType::Council
} else {
return Err(GovernanceError::InvalidGoverningTokenMint.into());
};
realm_data.assert_is_valid_governing_token_mint(&governing_token_mint)?;
let amount = get_amount_from_token_account(governing_token_source_info)?;
@ -66,13 +62,13 @@ pub fn process_deposit_governing_tokens(
spl_token_info,
)?;
let voter_record_address_seeds = get_voter_record_address_seeds(
let token_owner_record_address_seeds = get_token_owner_record_address_seeds(
realm_info.key,
&governing_token_mint,
governing_token_owner_info.key,
);
if voter_record_info.data_is_empty() {
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)?;
@ -82,36 +78,38 @@ pub fn process_deposit_governing_tokens(
return Err(GovernanceError::GoverningTokenOwnerMustSign.into());
}
let voter_record_data = VoterRecord {
account_type: GovernanceAccountType::VoterRecord,
let token_owner_record_data = TokenOwnerRecord {
account_type: GovernanceAccountType::TokenOwnerRecord,
realm: *realm_info.key,
token_owner: *governing_token_owner_info.key,
token_deposit_amount: amount,
token_type: governing_token_type,
vote_authority: None,
governing_token_owner: *governing_token_owner_info.key,
governing_token_deposit_amount: amount,
governing_token_mint,
governance_delegate: None,
active_votes_count: 0,
total_votes_count: 0,
};
create_and_serialize_account_signed(
payer_info,
voter_record_info,
&voter_record_data,
&voter_record_address_seeds,
token_owner_record_info,
&token_owner_record_data,
&token_owner_record_address_seeds,
program_id,
system_info,
rent,
)?;
} else {
let mut voter_record_data =
deserialize_voter_record(voter_record_info, &voter_record_address_seeds)?;
let mut token_owner_record_data = deserialize_token_owner_record(
token_owner_record_info,
&token_owner_record_address_seeds,
)?;
voter_record_data.token_deposit_amount = voter_record_data
.token_deposit_amount
token_owner_record_data.governing_token_deposit_amount = token_owner_record_data
.governing_token_deposit_amount
.checked_add(amount)
.unwrap();
voter_record_data.serialize(&mut *voter_record_info.data.borrow_mut())?;
token_owner_record_data.serialize(&mut *token_owner_record_info.data.borrow_mut())?;
}
Ok(())

View File

@ -0,0 +1,68 @@
//! Program state processor
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,
},
tools::{account::dispose_account, asserts::assert_token_owner_or_delegate_is_signer},
};
/// Processes RemoveSignatory instruction
pub fn process_remove_signatory(
_program_id: &Pubkey,
accounts: &[AccountInfo],
signatory: Pubkey,
) -> 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 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)?;
proposal_data.assert_can_edit_signatories()?;
let token_owner_record_data = deserialize_token_owner_record_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)?;
let signatory_record_data =
deserialize_signatory_record(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);
Ok(())
}

View File

@ -0,0 +1,33 @@
//! Program state processor
use borsh::BorshSerialize;
use solana_program::{
account_info::{next_account_info, AccountInfo},
entrypoint::ProgramResult,
pubkey::Pubkey,
};
use crate::{
state::token_owner_record::deserialize_token_owner_record_raw,
tools::asserts::assert_token_owner_or_delegate_is_signer,
};
/// Processes SetGovernanceDelegate instruction
pub fn process_set_governance_delegate(
accounts: &[AccountInfo],
new_governance_delegate: &Option<Pubkey>,
) -> ProgramResult {
let account_info_iter = &mut accounts.iter();
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)?;
assert_token_owner_or_delegate_is_signer(&token_owner_record_data, &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())?;
Ok(())
}

View File

@ -1,39 +0,0 @@
//! Program state processor
use borsh::BorshSerialize;
use solana_program::{
account_info::{next_account_info, AccountInfo},
entrypoint::ProgramResult,
pubkey::Pubkey,
};
use crate::{
state::voter_record::{deserialize_voter_record, get_voter_record_address_seeds},
tools::asserts::assert_is_signed_by_owner_or_vote_authority,
};
/// Processes SetVoteAuthority instruction
pub fn process_set_vote_authority(
accounts: &[AccountInfo],
realm: &Pubkey,
governing_token_mint: &Pubkey,
governing_token_owner: &Pubkey,
new_vote_authority: &Option<Pubkey>,
) -> ProgramResult {
let account_info_iter = &mut accounts.iter();
let vote_authority_info = next_account_info(account_info_iter)?; // 0
let voter_record_info = next_account_info(account_info_iter)?; // 1
let mut voter_record_data = deserialize_voter_record(
voter_record_info,
&get_voter_record_address_seeds(realm, &governing_token_mint, governing_token_owner),
)?;
assert_is_signed_by_owner_or_vote_authority(&voter_record_data, &vote_authority_info)?;
voter_record_data.vote_authority = *new_vote_authority;
voter_record_data.serialize(&mut *voter_record_info.data.borrow_mut())?;
Ok(())
}

View File

@ -0,0 +1,58 @@
//! Program state processor
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,
};
/// Processes SignOffProposal instruction
pub fn process_sign_off_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 signatory_record_info = next_account_info(account_info_iter)?; // 1
let signatory_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 mut proposal_data = deserialize_proposal_raw(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)?;
signatory_record_data.assert_can_sign_off(signatory_info)?;
signatory_record_data.signed_off = true;
signatory_record_data.serialize(&mut *signatory_record_info.data.borrow_mut())?;
if proposal_data.signatories_signed_off_count == 0 {
proposal_data.signing_off_at = Some(clock.slot);
proposal_data.state = ProposalState::SigningOff;
}
proposal_data.signatories_signed_off_count = proposal_data
.signatories_signed_off_count
.checked_add(1)
.unwrap();
// If all Signatories signed off we can start voting
if 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())?;
Ok(())
}

View File

@ -10,8 +10,10 @@ use solana_program::{
use crate::{
error::GovernanceError,
state::{
realm::{deserialize_realm, get_realm_address_seeds},
voter_record::{deserialize_voter_record, get_voter_record_address_seeds},
realm::{deserialize_realm_raw, get_realm_address_seeds},
token_owner_record::{
deserialize_token_owner_record, get_token_owner_record_address_seeds,
},
},
tools::token::{get_mint_from_token_account, transfer_spl_tokens_signed},
};
@ -27,26 +29,26 @@ pub fn process_withdraw_governing_tokens(
let governing_token_holding_info = next_account_info(account_info_iter)?; // 1
let governing_token_destination_info = next_account_info(account_info_iter)?; // 2
let governing_token_owner_info = next_account_info(account_info_iter)?; // 3
let voter_record_info = next_account_info(account_info_iter)?; // 4
let token_owner_record_info = next_account_info(account_info_iter)?; // 4
let spl_token_info = next_account_info(account_info_iter)?; // 5
if !governing_token_owner_info.is_signer {
return Err(GovernanceError::GoverningTokenOwnerMustSign.into());
}
let realm_data = deserialize_realm(realm_info)?;
let realm_data = deserialize_realm_raw(realm_info)?;
let governing_token_mint = get_mint_from_token_account(governing_token_holding_info)?;
let voter_record_address_seeds = get_voter_record_address_seeds(
let token_owner_record_address_seeds = get_token_owner_record_address_seeds(
realm_info.key,
&governing_token_mint,
governing_token_owner_info.key,
);
let mut voter_record_data =
deserialize_voter_record(voter_record_info, &voter_record_address_seeds)?;
let mut token_owner_record_data =
deserialize_token_owner_record(token_owner_record_info, &token_owner_record_address_seeds)?;
if voter_record_data.active_votes_count > 0 {
if token_owner_record_data.active_votes_count > 0 {
return Err(GovernanceError::CannotWithdrawGoverningTokensWhenActiveVotesExist.into());
}
@ -56,12 +58,12 @@ pub fn process_withdraw_governing_tokens(
&realm_info,
&get_realm_address_seeds(&realm_data.name),
program_id,
voter_record_data.token_deposit_amount,
token_owner_record_data.governing_token_deposit_amount,
spl_token_info,
)?;
voter_record_data.token_deposit_amount = 0;
voter_record_data.serialize(&mut *voter_record_info.data.borrow_mut())?;
token_owner_record_data.governing_token_deposit_amount = 0;
token_owner_record_data.serialize(&mut *token_owner_record_info.data.borrow_mut())?;
Ok(())
}

View File

@ -12,8 +12,8 @@ pub enum GovernanceAccountType {
/// Top level aggregation for governances with Community Token (and optional Council Token)
Realm,
/// Voter record for each voter and given governing token type within a Realm
VoterRecord,
/// Token Owner Record for given governing token owner within a Realm
TokenOwnerRecord,
/// Generic Account Governance account
AccountGovernance,
@ -24,6 +24,9 @@ pub enum GovernanceAccountType {
/// Proposal account for Governance account. A single Governance account can have multiple Proposal accounts
Proposal,
/// Proposal Signatory account
SignatoryRecord,
/// Vote record account for a given Proposal. Proposal can have 0..n voting records
ProposalVoteRecord,
@ -48,16 +51,6 @@ pub enum VoteWeight {
No(u64),
}
/// Governing Token type
#[repr(C)]
#[derive(Clone, Debug, PartialEq, BorshDeserialize, BorshSerialize, BorshSchema)]
pub enum GoverningTokenType {
/// Community token
Community,
/// Council token
Council,
}
/// What state a Proposal is in
#[repr(C)]
#[derive(Clone, Debug, PartialEq, BorshDeserialize, BorshSerialize, BorshSchema)]
@ -65,8 +58,9 @@ pub enum ProposalState {
/// Draft - Proposal enters Draft state when it's created
Draft,
/// Signing - The Proposal is being signed by Signatories. Proposal enters the state when first Signatory Sings and leaves it when last Signatory signs
Signing,
/// SigningOff - The Proposal is being signed off by Signatories
/// Proposal enters the state when first Signatory Sings and leaves it when last Signatory signs
SigningOff,
/// Taking votes
Voting,

View File

@ -1,7 +1,8 @@
//! Governance Account
use crate::{
error::GovernanceError, id, state::enums::GovernanceAccountType, tools::account::AccountMaxSize,
error::GovernanceError, id, state::enums::GovernanceAccountType,
tools::account::deserialize_account, tools::account::AccountMaxSize,
};
use borsh::{BorshDeserialize, BorshSchema, BorshSerialize};
use solana_program::{
@ -46,7 +47,7 @@ pub struct Governance {
pub config: GovernanceConfig,
/// Running count of proposals
pub proposal_count: u32,
pub proposals_count: u16,
}
impl AccountMaxSize for Governance {}
@ -58,6 +59,13 @@ 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())
}
/// Returns ProgramGovernance PDA seeds
pub fn get_program_governance_address_seeds<'a>(
realm: &'a Pubkey,

View File

@ -5,5 +5,6 @@ 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 voter_record;
pub mod token_owner_record;

View File

@ -1,8 +1,18 @@
//! Proposal Account
use solana_program::{epoch_schedule::Slot, pubkey::Pubkey};
use solana_program::{
account_info::AccountInfo, epoch_schedule::Slot, program_error::ProgramError,
program_pack::IsInitialized, pubkey::Pubkey,
};
use super::enums::{GovernanceAccountType, GoverningTokenType, ProposalState};
use crate::{
error::GovernanceError,
id,
tools::account::{deserialize_account, AccountMaxSize},
PROGRAM_AUTHORITY_SEED,
};
use crate::state::enums::{GovernanceAccountType, ProposalState};
use borsh::{BorshDeserialize, BorshSchema, BorshSerialize};
/// Governance Proposal
@ -15,23 +25,21 @@ pub struct Proposal {
/// Governance account the Proposal belongs to
pub governance: Pubkey,
/// Mint that creates signatory tokens of this Proposal
/// If there are outstanding signatory tokens, then cannot leave draft state. Signatories must burn tokens (ie agree
/// to move instruction to voting state) and bring mint to net 0 tokens outstanding. Each signatory gets 1 (serves as flag)
pub signatory_mint: Pubkey,
/// Admin ownership mint. One token is minted, can be used to grant admin status to a new person
pub admin_mint: Pubkey,
/// Indicates which Governing Token is used to vote on the Proposal
/// Whether the general Community token owners or the Council tokens owners vote on this Proposal
pub voting_token_type: GoverningTokenType,
pub governing_token_mint: Pubkey,
/// Current state of the proposal
/// Current proposal state
pub state: ProposalState,
/// Total signatory tokens minted, for use comparing to supply remaining during draft period
pub total_signatory_tokens_minted: u64,
/// The TokenOwnerRecord representing the user who created and owns this Proposal
pub token_owner_record: Pubkey,
/// The number of signatories assigned to the Proposal
pub signatories_count: u8,
/// The number of signatories who already signed
pub signatories_signed_off_count: u8,
/// Link to proposal's description
pub description_link: String,
@ -39,27 +47,208 @@ pub struct Proposal {
/// Proposal name
pub name: String,
/// When the Proposal ended voting - this will also be when the set was defeated or began executing naturally
pub voting_ended_at: Option<Slot>,
/// When the Proposal was created and entered Draft state
pub draft_at: Slot,
/// When Signatories started signing off the Proposal
pub signing_off_at: Option<Slot>,
/// When the Proposal began voting
pub voting_began_at: Option<Slot>,
pub voting_at: Option<Slot>,
/// when the Proposal entered draft state
pub created_at: Option<Slot>,
/// When the Proposal ended voting and entered either Succeeded or Defeated
pub voting_completed_at: Option<Slot>,
/// when the Proposal entered completed state, also when execution ended naturally.
pub completed_at: Option<Slot>,
/// When the Proposal entered Executing state
pub executing_at: Option<Slot>,
/// when the Proposal entered deleted state
pub deleted_at: Option<Slot>,
/// When the Proposal entered final state Completed or Cancelled and was closed
pub closed_at: Option<Slot>,
/// The number of the instructions already executed
pub number_of_executed_instructions: u8,
/// The number of instructions included in the proposal
pub number_of_instructions: u8,
/// Array of pubkeys pointing at Proposal instructions, up to 5
pub instruction: Vec<Pubkey>,
}
impl AccountMaxSize for Proposal {
fn get_max_size(&self) -> Option<usize> {
Some(self.name.len() + self.description_link.len() + 163)
}
}
impl IsInitialized for Proposal {
fn is_initialized(&self) -> bool {
self.account_type == GovernanceAccountType::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(())
}
/// 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());
}
Ok(())
}
}
/// Deserializes Proposal account and checks owner program
pub fn deserialize_proposal_raw(proposal_info: &AccountInfo) -> Result<Proposal, ProgramError> {
deserialize_account::<Proposal>(proposal_info, &id())
}
/// Returns Proposal PDA seeds
pub fn get_proposal_address_seeds<'a>(
governance: &'a Pubkey,
governing_token_mint: &'a Pubkey,
proposal_index_le_bytes: &'a [u8],
) -> [&'a [u8]; 4] {
[
PROGRAM_AUTHORITY_SEED,
governance.as_ref(),
governing_token_mint.as_ref(),
&proposal_index_le_bytes,
]
}
/// Returns Proposal PDA address
pub fn get_proposal_address<'a>(
governance: &'a Pubkey,
governing_token_mint: &'a Pubkey,
proposal_index_bytes: &'a [u8],
) -> Pubkey {
Pubkey::find_program_address(
&get_proposal_address_seeds(governance, governing_token_mint, &proposal_index_bytes),
&id(),
)
.0
}
#[cfg(test)]
mod test {
use {super::*, proptest::prelude::*};
fn create_test_proposal() -> Proposal {
Proposal {
account_type: GovernanceAccountType::TokenOwnerRecord,
governance: Pubkey::new_unique(),
governing_token_mint: Pubkey::new_unique(),
state: ProposalState::Draft,
token_owner_record: Pubkey::new_unique(),
signatories_count: 10,
signatories_signed_off_count: 5,
description_link: "This is my description".to_string(),
name: "This is my name".to_string(),
draft_at: 10,
signing_off_at: Some(10),
voting_at: Some(10),
voting_completed_at: Some(10),
executing_at: Some(10),
closed_at: Some(10),
number_of_executed_instructions: 10,
number_of_instructions: 10,
}
}
#[test]
fn test_max_size() {
let proposal = create_test_proposal();
let size = proposal.try_to_vec().unwrap().len();
assert_eq!(proposal.get_max_size(), Some(size));
}
fn editable_signatory_states() -> impl Strategy<Value = ProposalState> {
prop_oneof![Just(ProposalState::Draft), Just(ProposalState::SigningOff),]
}
proptest! {
#[test]
fn test_assert_can_edit_signatories(state in editable_signatory_states()) {
let mut proposal = create_test_proposal();
proposal.state = state;
proposal.assert_can_edit_signatories().unwrap();
}
}
fn none_editable_signatory_states() -> impl Strategy<Value = ProposalState> {
prop_oneof![
Just(ProposalState::Voting),
Just(ProposalState::Succeeded),
Just(ProposalState::Executing),
Just(ProposalState::Completed),
Just(ProposalState::Cancelled),
Just(ProposalState::Defeated),
]
}
proptest! {
#[test]
fn test_assert_can_edit_signatories_with_invalid_state_error(state in none_editable_signatory_states()) {
// Arrange
let mut proposal = create_test_proposal();
proposal.state = state;
// Act
let err = proposal.assert_can_edit_signatories().err().unwrap();
// Assert
assert_eq!(err, GovernanceError::InvalidStateCannotEditSignatories.into());
}
}
fn sign_off_states() -> impl Strategy<Value = ProposalState> {
prop_oneof![Just(ProposalState::SigningOff), Just(ProposalState::Draft),]
}
proptest! {
#[test]
fn test_assert_can_sign_off(state in sign_off_states()) {
let mut proposal = create_test_proposal();
proposal.state = state;
proposal.assert_can_sign_off().unwrap();
}
}
fn none_sign_off_states() -> impl Strategy<Value = ProposalState> {
prop_oneof![
Just(ProposalState::Voting),
Just(ProposalState::Succeeded),
Just(ProposalState::Executing),
Just(ProposalState::Completed),
Just(ProposalState::Cancelled),
Just(ProposalState::Defeated),
]
}
proptest! {
#[test]
fn test_assert_can_sign_off_with_state_error(state in none_sign_off_states()) {
// Arrange
let mut proposal = create_test_proposal();
proposal.state = state;
// Act
let err = proposal.assert_can_sign_off().err().unwrap();
// Assert
assert_eq!(err, GovernanceError::InvalidStateCannotSignOff.into());
}
}
}

View File

@ -3,7 +3,7 @@
use borsh::{BorshDeserialize, BorshSchema, BorshSerialize};
use solana_program::pubkey::Pubkey;
use super::enums::{GovernanceAccountType, VoteWeight};
use crate::state::enums::{GovernanceAccountType, VoteWeight};
/// Proposal Vote Record
#[repr(C)]

View File

@ -7,12 +7,13 @@ use solana_program::{
};
use crate::{
error::GovernanceError,
id,
tools::account::{assert_is_valid_account, deserialize_account, AccountMaxSize},
PROGRAM_AUTHORITY_SEED,
};
use super::enums::GovernanceAccountType;
use crate::state::enums::GovernanceAccountType;
/// Governance Realm Account
/// Account PDA seeds" ['governance', name]
@ -40,13 +41,31 @@ impl IsInitialized for Realm {
}
}
impl Realm {
/// Asserts the given mint is either Community or Council mint of the Realm
pub fn assert_is_valid_governing_token_mint(
&self,
governing_token_mint: &Pubkey,
) -> Result<(), ProgramError> {
if self.community_mint == *governing_token_mint {
return Ok(());
}
if self.council_mint == Some(*governing_token_mint) {
return Ok(());
}
Err(GovernanceError::InvalidGoverningTokenMint.into())
}
}
/// Checks whether realm account exists, is initialized and owned by Governance program
pub fn assert_is_valid_realm(realm_info: &AccountInfo) -> Result<(), ProgramError> {
assert_is_valid_account(realm_info, GovernanceAccountType::Realm, &id())
}
/// Deserializes account and checks owner program
pub fn deserialize_realm(realm_info: &AccountInfo) -> Result<Realm, ProgramError> {
pub fn deserialize_realm_raw(realm_info: &AccountInfo) -> Result<Realm, ProgramError> {
deserialize_account::<Realm>(realm_info, &id())
}

View File

@ -0,0 +1,108 @@
//! Signatory Record
use borsh::{BorshDeserialize, BorshSchema, BorshSerialize};
use solana_program::{
account_info::AccountInfo, program_error::ProgramError, program_pack::IsInitialized,
pubkey::Pubkey,
};
use crate::{
error::GovernanceError,
id,
tools::account::{deserialize_account, AccountMaxSize},
PROGRAM_AUTHORITY_SEED,
};
use crate::state::enums::GovernanceAccountType;
/// Account PDA seeds: ['governance', proposal, signatory]
#[repr(C)]
#[derive(Clone, Debug, PartialEq, BorshDeserialize, BorshSerialize, BorshSchema)]
pub struct SignatoryRecord {
/// Governance account type
pub account_type: GovernanceAccountType,
/// Proposal the signatory is assigned for
pub proposal: Pubkey,
/// The account of the signatory who can sign off the proposal
pub signatory: Pubkey,
/// Indicates whether the signatory signed off the proposal
pub signed_off: bool,
}
impl AccountMaxSize for SignatoryRecord {}
impl IsInitialized for SignatoryRecord {
fn is_initialized(&self) -> bool {
self.account_type == GovernanceAccountType::SignatoryRecord
}
}
impl SignatoryRecord {
/// Checks signatory hasn't signed off yet and is transaction signer
pub fn assert_can_sign_off(&self, signatory_info: &AccountInfo) -> Result<(), ProgramError> {
if self.signed_off {
return Err(GovernanceError::SignatoryAlreadySignedOff.into());
}
if !signatory_info.is_signer {
return Err(GovernanceError::SignatoryMustSign.into());
}
Ok(())
}
/// Checks signatory can be removed from Proposal
pub fn assert_can_remove_signatory(&self) -> Result<(), ProgramError> {
if self.signed_off {
return Err(GovernanceError::SignatoryAlreadySignedOff.into());
}
Ok(())
}
}
/// Returns SignatoryRecord PDA seeds
pub fn get_signatory_record_address_seeds<'a>(
proposal: &'a Pubkey,
signatory: &'a Pubkey,
) -> [&'a [u8]; 3] {
[
PROGRAM_AUTHORITY_SEED,
proposal.as_ref(),
signatory.as_ref(),
]
}
/// Returns SignatoryRecord PDA address
pub fn get_signatory_record_address<'a>(proposal: &'a Pubkey, signatory: &'a Pubkey) -> Pubkey {
Pubkey::find_program_address(
&get_signatory_record_address_seeds(proposal, signatory),
&id(),
)
.0
}
/// Deserializes SignatoryRecord account and checks owner program
pub fn deserialize_signatory_record_raw(
signatory_record_info: &AccountInfo,
) -> Result<SignatoryRecord, ProgramError> {
deserialize_account::<SignatoryRecord>(signatory_record_info, &id())
}
/// Deserializes SignatoryRecord and validates its PDA
pub fn deserialize_signatory_record(
signatory_record_info: &AccountInfo,
proposal: &Pubkey,
signatory: &Pubkey,
) -> Result<SignatoryRecord, ProgramError> {
let (signatory_record_address, _) = Pubkey::find_program_address(
&get_signatory_record_address_seeds(proposal, signatory),
&id(),
);
if signatory_record_address != *signatory_record_info.key {
return Err(GovernanceError::InvalidSignatoryAddress.into());
}
deserialize_signatory_record_raw(signatory_record_info)
}

View File

@ -1,6 +1,6 @@
//! SingleSignerInstruction Account
use super::enums::GovernanceAccountType;
use crate::state::enums::GovernanceAccountType;
use borsh::{BorshDeserialize, BorshSchema, BorshSerialize};
/// Account for an instruction to be executed for Proposal

View File

@ -0,0 +1,166 @@
//! Token Owner Record Account
use crate::{
error::GovernanceError,
id,
tools::account::{deserialize_account, AccountMaxSize},
PROGRAM_AUTHORITY_SEED,
};
use crate::state::enums::GovernanceAccountType;
use borsh::{BorshDeserialize, BorshSchema, BorshSerialize};
use solana_program::{
account_info::AccountInfo, program_error::ProgramError, program_pack::IsInitialized,
pubkey::Pubkey,
};
/// Governance Token Owner Record
/// Account PDA seeds: ['governance', realm, token_mint, token_owner ]
#[repr(C)]
#[derive(Clone, Debug, PartialEq, BorshDeserialize, BorshSerialize, BorshSchema)]
pub struct TokenOwnerRecord {
/// Governance account type
pub account_type: GovernanceAccountType,
/// The Realm the TokenOwnerRecord belongs to
pub realm: Pubkey,
/// Governing Token Mint the TokenOwnerRecord holds deposit for
pub governing_token_mint: Pubkey,
/// The owner (either single or multisig) of the deposited governing SPL Tokens
/// This is who can authorize a withdrawal
pub governing_token_owner: Pubkey,
/// The amount of governing tokens deposited into the Realm
/// This amount is the voter weight used when voting on proposals
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
pub governance_delegate: Option<Pubkey>,
/// The number of active votes cast by TokenOwner
pub active_votes_count: u16,
/// The total number of votes cast by the TokenOwner
pub total_votes_count: u16,
}
impl AccountMaxSize for TokenOwnerRecord {
fn get_max_size(&self) -> Option<usize> {
Some(142)
}
}
impl IsInitialized for TokenOwnerRecord {
fn is_initialized(&self) -> bool {
self.account_type == GovernanceAccountType::TokenOwnerRecord
}
}
/// Returns TokenOwnerRecord PDA address
pub fn get_token_owner_record_address(
realm: &Pubkey,
governing_token_mint: &Pubkey,
governing_token_owner: &Pubkey,
) -> Pubkey {
Pubkey::find_program_address(
&get_token_owner_record_address_seeds(realm, governing_token_mint, governing_token_owner),
&id(),
)
.0
}
/// Returns TokenOwnerRecord PDA seeds
pub fn get_token_owner_record_address_seeds<'a>(
realm: &'a Pubkey,
governing_token_mint: &'a Pubkey,
governing_token_owner: &'a Pubkey,
) -> [&'a [u8]; 4] {
[
PROGRAM_AUTHORITY_SEED,
realm.as_ref(),
governing_token_mint.as_ref(),
governing_token_owner.as_ref(),
]
}
/// Deserializes TokenOwnerRecord account and checks owner program
pub fn deserialize_token_owner_record_raw(
token_owner_record_info: &AccountInfo,
) -> Result<TokenOwnerRecord, ProgramError> {
deserialize_account::<TokenOwnerRecord>(token_owner_record_info, &id())
}
/// Deserializes TokenOwnerRecord account and checks its PDA against the provided seeds
pub fn deserialize_token_owner_record(
token_owner_record_info: &AccountInfo,
token_owner_record_seeds: &[&[u8]],
) -> Result<TokenOwnerRecord, ProgramError> {
let (token_owner_record_address, _) =
Pubkey::find_program_address(token_owner_record_seeds, &id());
if token_owner_record_address != *token_owner_record_info.key {
return Err(GovernanceError::InvalidTokenOwnerRecordAccountAddress.into());
}
deserialize_token_owner_record_raw(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(
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)?;
if token_owner_record_data.governing_token_mint != *governing_token_mint {
return Err(GovernanceError::InvalidTokenOwnerRecordGoverningMint.into());
}
if token_owner_record_data.realm != *realm {
return Err(GovernanceError::InvalidTokenOwnerRecordRealm.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(
token_owner_record_info: &AccountInfo,
proposal_owner: &Pubkey,
) -> Result<TokenOwnerRecord, ProgramError> {
if token_owner_record_info.key != proposal_owner {
return Err(GovernanceError::InvalidProposalOwnerAccount.into());
}
deserialize_token_owner_record_raw(token_owner_record_info)
}
#[cfg(test)]
mod test {
use solana_program::borsh::get_packed_len;
use super::*;
#[test]
fn test_max_size() {
let token_owner_record = TokenOwnerRecord {
account_type: GovernanceAccountType::TokenOwnerRecord,
realm: Pubkey::new_unique(),
governing_token_mint: Pubkey::new_unique(),
governing_token_owner: Pubkey::new_unique(),
governing_token_deposit_amount: 10,
governance_delegate: Some(Pubkey::new_unique()),
active_votes_count: 1,
total_votes_count: 1,
};
let size = get_packed_len::<TokenOwnerRecord>();
assert_eq!(token_owner_record.get_max_size(), Some(size));
}
}

View File

@ -0,0 +1,55 @@
//! Proposal Vote Record Account
use borsh::{BorshDeserialize, BorshSchema, BorshSerialize};
use solana_program::{program_pack::IsInitialized, pubkey::Pubkey};
use crate::{id, tools::account::AccountMaxSize, PROGRAM_AUTHORITY_SEED};
use crate::state::enums::{GovernanceAccountType, VoteWeight};
/// Proposal VoteRecord
#[repr(C)]
#[derive(Clone, Debug, PartialEq, BorshDeserialize, BorshSerialize, BorshSchema)]
pub struct VoteRecord {
/// 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_weight: VoteWeight,
}
impl AccountMaxSize for VoteRecord {}
impl IsInitialized for VoteRecord {
fn is_initialized(&self) -> bool {
self.account_type == GovernanceAccountType::VoteRecord
}
}
/// Returns VoteRecord PDA seeds
pub fn get_vote_record_address_seeds<'a>(
proposal: &'a Pubkey,
token_owner_record: &'a Pubkey,
) -> [&'a [u8]; 3] {
[
PROGRAM_AUTHORITY_SEED,
proposal.as_ref(),
token_owner_record.as_ref(),
]
}
/// Returns VoteRecord PDA address
pub fn get_vote_record_address<'a>(proposal: &'a Pubkey, token_owner_record: &'a Pubkey) -> Pubkey {
Pubkey::find_program_address(
&get_vote_record_address_seeds(proposal, token_owner_record),
&id(),
)
.0
}

View File

@ -1,126 +0,0 @@
//! Voter Record Account
use crate::{
error::GovernanceError,
id,
tools::account::{deserialize_account, AccountMaxSize},
PROGRAM_AUTHORITY_SEED,
};
use super::enums::{GovernanceAccountType, GoverningTokenType};
use borsh::{BorshDeserialize, BorshSchema, BorshSerialize};
use solana_program::{
account_info::AccountInfo, program_error::ProgramError, program_pack::IsInitialized,
pubkey::Pubkey,
};
/// Governance Voter Record
/// Account PDA seeds: ['governance', realm, token_mint, token_owner ]
#[repr(C)]
#[derive(Clone, Debug, PartialEq, BorshDeserialize, BorshSerialize, BorshSchema)]
pub struct VoterRecord {
/// Governance account type
pub account_type: GovernanceAccountType,
/// The Realm the VoterRecord belongs to
pub realm: Pubkey,
/// The type of the Governing Token the VoteRecord is for
pub token_type: GoverningTokenType,
/// The owner (either single or multisig) of the deposited governing SPL Tokens
/// This is who can authorize a withdrawal
pub token_owner: Pubkey,
/// The amount of governing tokens deposited into the Realm
/// This amount is the voter weight used when voting on proposals
pub 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 vote_authority
pub vote_authority: Option<Pubkey>,
/// The number of active votes cast by voter
pub active_votes_count: u8,
/// The total number of votes cast by the voter
pub total_votes_count: u8,
}
impl AccountMaxSize for VoterRecord {
fn get_max_size(&self) -> Option<usize> {
Some(109)
}
}
impl IsInitialized for VoterRecord {
fn is_initialized(&self) -> bool {
self.account_type == GovernanceAccountType::VoterRecord
}
}
/// Returns VoteRecord PDA address
pub fn get_voter_record_address(
realm: &Pubkey,
governing_token_mint: &Pubkey,
governing_token_owner: &Pubkey,
) -> Pubkey {
Pubkey::find_program_address(
&get_voter_record_address_seeds(realm, governing_token_mint, governing_token_owner),
&id(),
)
.0
}
/// Returns VoterRecord PDA seeds
pub fn get_voter_record_address_seeds<'a>(
realm: &'a Pubkey,
governing_token_mint: &'a Pubkey,
governing_token_owner: &'a Pubkey,
) -> [&'a [u8]; 4] {
[
PROGRAM_AUTHORITY_SEED,
realm.as_ref(),
governing_token_mint.as_ref(),
governing_token_owner.as_ref(),
]
}
/// Deserializes VoterRecord and checks account PDA and owner program
pub fn deserialize_voter_record(
voter_record_info: &AccountInfo,
voter_record_seeds: &[&[u8]],
) -> Result<VoterRecord, ProgramError> {
let (voter_record_address, _) = Pubkey::find_program_address(voter_record_seeds, &id());
if voter_record_address != *voter_record_info.key {
return Err(GovernanceError::InvalidVoterAccountAddress.into());
}
deserialize_account::<VoterRecord>(voter_record_info, &id())
}
#[cfg(test)]
mod test {
use solana_program::borsh::get_packed_len;
use super::*;
#[test]
fn test_max_size() {
let vote_record = VoterRecord {
account_type: GovernanceAccountType::VoterRecord,
realm: Pubkey::new_unique(),
token_type: GoverningTokenType::Community,
token_owner: Pubkey::new_unique(),
token_deposit_amount: 10,
vote_authority: Some(Pubkey::new_unique()),
active_votes_count: 1,
total_votes_count: 1,
};
let size = get_packed_len::<VoterRecord>();
assert_eq!(vote_record.get_max_size(), Some(size));
}
}

View File

@ -88,13 +88,13 @@ pub fn deserialize_account<T: BorshDeserialize + IsInitialized>(
account_info: &AccountInfo,
owner_program_id: &Pubkey,
) -> Result<T, ProgramError> {
if account_info.data_is_empty() {
return Err(ProgramError::UninitializedAccount);
}
if account_info.owner != owner_program_id {
return Err(GovernanceError::InvalidAccountOwner.into());
}
if account_info.data_is_empty() {
return Err(ProgramError::UninitializedAccount);
}
let account: T = try_from_slice_unchecked(&account_info.data.borrow())?;
if !account.is_initialized() {
Err(ProgramError::UninitializedAccount)
@ -125,3 +125,19 @@ pub fn assert_is_valid_account<T: BorshDeserialize + PartialEq>(
Ok(())
}
/// 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) {
let account_lamports = account_info.lamports();
**account_info.lamports.borrow_mut() = 0;
**beneficiary_account.lamports.borrow_mut() = beneficiary_account
.lamports()
.checked_add(account_lamports)
.unwrap();
let mut account_data = account_info.data.borrow_mut();
account_data.fill(0);
}

View File

@ -2,24 +2,24 @@
use solana_program::{account_info::AccountInfo, program_error::ProgramError};
use crate::{error::GovernanceError, state::voter_record::VoterRecord};
use crate::{error::GovernanceError, state::token_owner_record::TokenOwnerRecord};
/// Checks whether the provided vote authority can set new vote authority
pub fn assert_is_signed_by_owner_or_vote_authority(
voter_record: &VoterRecord,
vote_authority_info: &AccountInfo,
/// 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 vote_authority_info.is_signer {
if &voter_record.token_owner == vote_authority_info.key {
if governance_authority_info.is_signer {
if &token_owner_record.governing_token_owner == governance_authority_info.key {
return Ok(());
}
if let Some(vote_authority) = voter_record.vote_authority {
if &vote_authority == vote_authority_info.key {
if let Some(governance_delegate) = token_owner_record.governance_delegate {
if &governance_delegate == governance_authority_info.key {
return Ok(());
}
};
}
Err(GovernanceError::GoverningTokenOwnerOrVoteAuthrotiyMustSign.into())
Err(GovernanceError::GoverningTokenOwnerOrDelegateMustSign.into())
}

View File

@ -72,27 +72,25 @@ pub fn assert_program_upgrade_authority_is_signer(
return Err(GovernanceError::InvalidProgramDataAccountAddress.into());
}
let upgrade_authority = match deserialize(&program_data_info.data.borrow())
.map_err(|_| GovernanceError::InvalidProgramDataAccountData)?
{
UpgradeableLoaderState::ProgramData {
let upgrade_authority = if let UpgradeableLoaderState::ProgramData {
slot: _,
upgrade_authority_address,
} => upgrade_authority_address,
_ => None,
} = deserialize(&program_data_info.data.borrow())
.map_err(|_| GovernanceError::InvalidProgramDataAccountData)?
{
upgrade_authority_address
} else {
None
};
match upgrade_authority {
Some(upgrade_authority) => {
let upgrade_authority = upgrade_authority.ok_or(GovernanceError::ProgramNotUpgradable)?;
if upgrade_authority != *program_upgrade_authority_info.key {
return Err(GovernanceError::InvalidUpgradeAuthority.into());
}
if !program_upgrade_authority_info.is_signer {
return Err(GovernanceError::UpgradeAuthorityMustSign.into());
}
}
None => return Err(GovernanceError::ProgramNotUpgradable.into()),
}
Ok(())
}

View File

@ -190,7 +190,7 @@ pub fn get_mint_from_token_account(
}
// TokeAccount layout: mint(32), owner(32), amount(8), ...
let data = token_account_info.try_borrow_data().unwrap();
let data = token_account_info.try_borrow_data()?;
let mint_data = array_ref![data, 0, 32];
Ok(Pubkey::new_from_array(*mint_data))
}
@ -205,7 +205,7 @@ pub fn get_owner_from_token_account(
}
// TokeAccount layout: mint(32), owner(32), amount(8)
let data = token_account_info.try_borrow_data().unwrap();
let data = token_account_info.try_borrow_data()?;
let owner_data = array_ref![data, 32, 32];
Ok(Pubkey::new_from_array(*owner_data))
}

View File

@ -0,0 +1,132 @@
#![cfg(feature = "test-bpf")]
mod program_test;
use solana_program_test::tokio;
use program_test::*;
use spl_governance::error::GovernanceError;
#[tokio::test]
async fn test_add_signatory() {
// 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
let signatory_record_cookie = governance_test
.with_signatory(&proposal_cookie, &token_owner_record_cookie)
.await
.unwrap();
// Assert
let signatory_record_account = governance_test
.get_signatory_record_account(&signatory_record_cookie.address)
.await;
assert_eq!(signatory_record_cookie.account, signatory_record_account);
let proposal_account = governance_test
.get_proposal_account(&proposal_cookie.address)
.await;
assert_eq!(1, proposal_account.signatories_count);
}
#[tokio::test]
async fn test_add_signatory_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 other_token_owner_record_cookie = governance_test
.with_initial_council_token_deposit(&realm_cookie)
.await;
token_owner_record_cookie.token_owner = other_token_owner_record_cookie.token_owner;
// Act
let err = governance_test
.with_signatory(&proposal_cookie, &token_owner_record_cookie)
.await
.err()
.unwrap();
// Assert
assert_eq!(
err,
GovernanceError::GoverningTokenOwnerOrDelegateMustSign.into()
);
}
#[tokio::test]
async fn test_add_signatory_with_invalid_proposal_owner_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 other_token_owner_record_cookie = governance_test
.with_initial_council_token_deposit(&realm_cookie)
.await;
token_owner_record_cookie.address = other_token_owner_record_cookie.address;
// Act
let err = governance_test
.with_signatory(&proposal_cookie, &token_owner_record_cookie)
.await
.err()
.unwrap();
// Assert
assert_eq!(err, GovernanceError::InvalidProposalOwnerAccount.into());
}

View File

@ -0,0 +1,255 @@
#![cfg(feature = "test-bpf")]
use solana_program::instruction::AccountMeta;
use solana_program_test::*;
mod program_test;
use program_test::*;
use solana_sdk::signature::Keypair;
use spl_governance::error::GovernanceError;
#[tokio::test]
async fn test_community_proposal_created() {
// 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;
// Act
let proposal_cookie = governance_test
.with_proposal(&token_owner_record_cookie, &mut account_governance_cookie)
.await
.unwrap();
// Assert
let proposal_account = governance_test
.get_proposal_account(&proposal_cookie.address)
.await;
assert_eq!(proposal_cookie.account, proposal_account);
let account_governance_account = governance_test
.get_governance_account(&account_governance_cookie.address)
.await;
assert_eq!(1, account_governance_account.proposals_count);
}
#[tokio::test]
async fn test_multiple_proposals_created() {
// 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 community_token_owner_record_cookie = governance_test
.with_initial_community_token_deposit(&realm_cookie)
.await;
let council_token_owner_record_cookie = governance_test
.with_initial_council_token_deposit(&realm_cookie)
.await;
// Act
let community_proposal_cookie = governance_test
.with_proposal(
&community_token_owner_record_cookie,
&mut account_governance_cookie,
)
.await
.unwrap();
let council_proposal_cookie = governance_test
.with_proposal(
&council_token_owner_record_cookie,
&mut account_governance_cookie,
)
.await
.unwrap();
// Assert
let community_proposal_account = governance_test
.get_proposal_account(&community_proposal_cookie.address)
.await;
assert_eq!(
community_proposal_cookie.account,
community_proposal_account
);
let council_proposal_account = governance_test
.get_proposal_account(&council_proposal_cookie.address)
.await;
assert_eq!(council_proposal_cookie.account, council_proposal_account);
let account_governance_account = governance_test
.get_governance_account(&account_governance_cookie.address)
.await;
assert_eq!(2, account_governance_account.proposals_count);
}
#[tokio::test]
async fn test_create_proposal_with_not_authorized_governance_authority_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;
token_owner_record_cookie.governance_authority = Some(Keypair::new());
// Act
let err = governance_test
.with_proposal(&token_owner_record_cookie, &mut account_governance_cookie)
.await
.err()
.unwrap();
// Assert
assert_eq!(
err,
GovernanceError::GoverningTokenOwnerOrDelegateMustSign.into()
);
}
#[tokio::test]
async fn test_create_proposal_with_governance_delegate_signer() {
// 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;
governance_test
.with_community_governance_delegate(&realm_cookie, &mut token_owner_record_cookie)
.await;
token_owner_record_cookie.governance_authority =
Some(token_owner_record_cookie.clone_governance_delegate());
// Act
let proposal_cookie = governance_test
.with_proposal(&token_owner_record_cookie, &mut account_governance_cookie)
.await
.unwrap();
// Assert
let proposal_account = governance_test
.get_proposal_account(&proposal_cookie.address)
.await;
assert_eq!(proposal_cookie.account, proposal_account);
}
#[tokio::test]
async fn test_create_proposal_with_not_enough_tokens_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_amount = account_governance_cookie
.account
.config
.min_tokens_to_create_proposal as u64
- 1;
let token_owner_record_cookie = governance_test
.with_initial_community_token_deposit_amount(&realm_cookie, token_amount)
.await;
// Act
let err = governance_test
.with_proposal(&token_owner_record_cookie, &mut account_governance_cookie)
.await
.err()
.unwrap();
// Assert
assert_eq!(err, GovernanceError::NotEnoughTokensToCreateProposal.into());
}
#[tokio::test]
async fn test_create_proposal_with_invalid_token_owner_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 council_token_owner_record_cookie = governance_test
.with_initial_council_token_deposit(&realm_cookie)
.await;
// Act
let err = governance_test
.with_proposal_instruction(
&token_owner_record_cookie,
&mut account_governance_cookie,
|i| {
// Set token_owner_record_address for different (Council) mint
i.accounts[2] =
AccountMeta::new_readonly(council_token_owner_record_cookie.address, false);
},
)
.await
.err()
.unwrap();
// Assert
assert_eq!(
err,
GovernanceError::InvalidTokenOwnerRecordGoverningMint.into()
);
}

View File

@ -16,24 +16,27 @@ async fn test_deposit_initial_community_tokens() {
let realm_cookie = governance_test.with_realm().await;
// Act
let voter_record_cookie = governance_test
let token_owner_record_cookie = governance_test
.with_initial_community_token_deposit(&realm_cookie)
.await;
// Assert
let voter_record = governance_test
.get_voter_record_account(&voter_record_cookie.address)
let token_owner_record = governance_test
.get_token_owner_record_account(&token_owner_record_cookie.address)
.await;
assert_eq!(voter_record_cookie.account, voter_record);
assert_eq!(token_owner_record_cookie.account, token_owner_record);
let source_account = governance_test
.get_token_account(&voter_record_cookie.token_source)
.get_token_account(&token_owner_record_cookie.token_source)
.await;
assert_eq!(
voter_record_cookie.token_source_amount - voter_record_cookie.account.token_deposit_amount,
token_owner_record_cookie.token_source_amount
- token_owner_record_cookie
.account
.governing_token_deposit_amount,
source_account.amount
);
@ -41,7 +44,10 @@ async fn test_deposit_initial_community_tokens() {
.get_token_account(&realm_cookie.community_token_holding_account)
.await;
assert_eq!(voter_record.token_deposit_amount, holding_account.amount);
assert_eq!(
token_owner_record.governing_token_deposit_amount,
holding_account.amount
);
}
#[tokio::test]
@ -53,23 +59,26 @@ async fn test_deposit_initial_council_tokens() {
let council_token_holding_account = realm_cookie.council_token_holding_account.unwrap();
// Act
let voter_record_cookie = governance_test
let token_owner_record_cookie = governance_test
.with_initial_council_token_deposit(&realm_cookie)
.await;
// Assert
let voter_record = governance_test
.get_voter_record_account(&voter_record_cookie.address)
let token_owner_record = governance_test
.get_token_owner_record_account(&token_owner_record_cookie.address)
.await;
assert_eq!(voter_record_cookie.account, voter_record);
assert_eq!(token_owner_record_cookie.account, token_owner_record);
let source_account = governance_test
.get_token_account(&voter_record_cookie.token_source)
.get_token_account(&token_owner_record_cookie.token_source)
.await;
assert_eq!(
voter_record_cookie.token_source_amount - voter_record_cookie.account.token_deposit_amount,
token_owner_record_cookie.token_source_amount
- token_owner_record_cookie
.account
.governing_token_deposit_amount,
source_account.amount
);
@ -77,7 +86,10 @@ async fn test_deposit_initial_council_tokens() {
.get_token_account(&council_token_holding_account)
.await;
assert_eq!(voter_record.token_deposit_amount, holding_account.amount);
assert_eq!(
token_owner_record.governing_token_deposit_amount,
holding_account.amount
);
}
#[tokio::test]
@ -86,24 +98,30 @@ async fn test_deposit_subsequent_community_tokens() {
let mut governance_test = GovernanceProgramTest::start_new().await;
let realm_cookie = governance_test.with_realm().await;
let voter_record_cookie = governance_test
let token_owner_record_cookie = governance_test
.with_initial_community_token_deposit(&realm_cookie)
.await;
let deposit_amount = 5;
let total_deposit_amount = voter_record_cookie.account.token_deposit_amount + deposit_amount;
let total_deposit_amount = token_owner_record_cookie
.account
.governing_token_deposit_amount
+ deposit_amount;
// Act
governance_test
.with_community_token_deposit(&realm_cookie, &voter_record_cookie, deposit_amount)
.with_community_token_deposit(&realm_cookie, &token_owner_record_cookie, deposit_amount)
.await;
// Assert
let voter_record = governance_test
.get_voter_record_account(&voter_record_cookie.address)
let token_owner_record = governance_test
.get_token_owner_record_account(&token_owner_record_cookie.address)
.await;
assert_eq!(total_deposit_amount, voter_record.token_deposit_amount);
assert_eq!(
total_deposit_amount,
token_owner_record.governing_token_deposit_amount
);
let holding_account = governance_test
.get_token_account(&realm_cookie.community_token_holding_account)
@ -120,24 +138,30 @@ async fn test_deposit_subsequent_council_tokens() {
let council_token_holding_account = realm_cookie.council_token_holding_account.unwrap();
let voter_record_cookie = governance_test
let token_owner_record_cookie = governance_test
.with_initial_council_token_deposit(&realm_cookie)
.await;
let deposit_amount = 5;
let total_deposit_amount = voter_record_cookie.account.token_deposit_amount + deposit_amount;
let total_deposit_amount = token_owner_record_cookie
.account
.governing_token_deposit_amount
+ deposit_amount;
// Act
governance_test
.with_council_token_deposit(&realm_cookie, &voter_record_cookie, deposit_amount)
.with_council_token_deposit(&realm_cookie, &token_owner_record_cookie, deposit_amount)
.await;
// Assert
let voter_record = governance_test
.get_voter_record_account(&voter_record_cookie.address)
let token_owner_record = governance_test
.get_token_owner_record_account(&token_owner_record_cookie.address)
.await;
assert_eq!(total_deposit_amount, voter_record.token_deposit_amount);
assert_eq!(
total_deposit_amount,
token_owner_record.governing_token_deposit_amount
);
let holding_account = governance_test
.get_token_account(&council_token_holding_account)

View File

@ -0,0 +1,219 @@
#![cfg(feature = "test-bpf")]
mod program_test;
use solana_program_test::tokio;
use program_test::*;
use spl_governance::{error::GovernanceError, state::enums::ProposalState};
#[tokio::test]
async fn test_remove_signatory() {
// 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();
let signatory_record_cookie = governance_test
.with_signatory(&proposal_cookie, &token_owner_record_cookie)
.await
.unwrap();
// Act
governance_test
.remove_signatory(
&proposal_cookie,
&token_owner_record_cookie,
&signatory_record_cookie,
)
.await
.unwrap();
// Assert
let proposal_account = governance_test
.get_proposal_account(&proposal_cookie.address)
.await;
assert_eq!(0, proposal_account.signatories_count);
assert_eq!(ProposalState::Draft, proposal_account.state);
let signatory_account = governance_test
.banks_client
.get_account(signatory_record_cookie.address)
.await
.unwrap();
assert_eq!(None, signatory_account);
}
#[tokio::test]
async fn test_remove_signatory_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 signatory_record_cookie = governance_test
.with_signatory(&proposal_cookie, &token_owner_record_cookie)
.await
.unwrap();
let other_token_owner_record_cookie = governance_test
.with_initial_council_token_deposit(&realm_cookie)
.await;
token_owner_record_cookie.token_owner = other_token_owner_record_cookie.token_owner;
// Act
let err = governance_test
.remove_signatory(
&proposal_cookie,
&token_owner_record_cookie,
&signatory_record_cookie,
)
.await
.err()
.unwrap();
// Assert
assert_eq!(
err,
GovernanceError::GoverningTokenOwnerOrDelegateMustSign.into()
);
}
#[tokio::test]
async fn test_remove_signatory_with_invalid_proposal_owner_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 signatory_record_cookie = governance_test
.with_signatory(&proposal_cookie, &token_owner_record_cookie)
.await
.unwrap();
let other_token_owner_record_cookie = governance_test
.with_initial_council_token_deposit(&realm_cookie)
.await;
token_owner_record_cookie.address = other_token_owner_record_cookie.address;
// Act
let err = governance_test
.remove_signatory(
&proposal_cookie,
&token_owner_record_cookie,
&signatory_record_cookie,
)
.await
.err()
.unwrap();
// Assert
assert_eq!(err, GovernanceError::InvalidProposalOwnerAccount.into());
}
#[tokio::test]
async fn test_remove_signatory_when_all_remaining_signed() {
// 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();
let signatory_record_cookie1 = governance_test
.with_signatory(&proposal_cookie, &token_owner_record_cookie)
.await
.unwrap();
let signatory_record_cookie2 = governance_test
.with_signatory(&proposal_cookie, &token_owner_record_cookie)
.await
.unwrap();
governance_test
.sign_off_proposal(&proposal_cookie, &signatory_record_cookie1)
.await
.unwrap();
// Act
governance_test
.remove_signatory(
&proposal_cookie,
&token_owner_record_cookie,
&signatory_record_cookie2,
)
.await
.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);
}

View File

@ -0,0 +1,165 @@
#![cfg(feature = "test-bpf")]
use solana_program::instruction::AccountMeta;
use solana_program_test::*;
mod program_test;
use program_test::*;
use solana_sdk::signature::{Keypair, Signer};
use spl_governance::{error::GovernanceError, instruction::set_governance_delegate};
#[tokio::test]
async fn test_set_community_governance_delegate() {
// Arrange
let mut governance_test = GovernanceProgramTest::start_new().await;
let realm_cookie = governance_test.with_realm().await;
let mut token_owner_record_cookie = governance_test
.with_initial_community_token_deposit(&realm_cookie)
.await;
// Act
governance_test
.with_community_governance_delegate(&realm_cookie, &mut token_owner_record_cookie)
.await;
// Assert
let token_owner_record = governance_test
.get_token_owner_record_account(&token_owner_record_cookie.address)
.await;
assert_eq!(
Some(token_owner_record_cookie.governance_delegate.pubkey()),
token_owner_record.governance_delegate
);
}
#[tokio::test]
async fn test_set_governance_delegate_to_none() {
// Arrange
let mut governance_test = GovernanceProgramTest::start_new().await;
let realm_cookie = governance_test.with_realm().await;
let mut token_owner_record_cookie = governance_test
.with_initial_community_token_deposit(&realm_cookie)
.await;
governance_test
.with_community_governance_delegate(&realm_cookie, &mut token_owner_record_cookie)
.await;
// Act
governance_test
.set_governance_delegate(
&realm_cookie,
&token_owner_record_cookie,
&token_owner_record_cookie.token_owner,
&realm_cookie.account.community_mint,
&None,
)
.await;
// Assert
let token_owner_record = governance_test
.get_token_owner_record_account(&token_owner_record_cookie.address)
.await;
assert_eq!(None, token_owner_record.governance_delegate);
}
#[tokio::test]
async fn test_set_council_governance_delegate() {
// Arrange
let mut governance_test = GovernanceProgramTest::start_new().await;
let realm_cookie = governance_test.with_realm().await;
let mut token_owner_record_cookie = governance_test
.with_initial_council_token_deposit(&realm_cookie)
.await;
// Act
governance_test
.with_council_governance_delegate(&realm_cookie, &mut token_owner_record_cookie)
.await;
// Assert
let token_owner_record = governance_test
.get_token_owner_record_account(&token_owner_record_cookie.address)
.await;
assert_eq!(
Some(token_owner_record_cookie.governance_delegate.pubkey()),
token_owner_record.governance_delegate
);
}
#[tokio::test]
async fn test_set_community_governance_delegate_with_owner_must_sign_error() {
// Arrange
let mut governance_test = GovernanceProgramTest::start_new().await;
let realm_cookie = governance_test.with_realm().await;
let token_owner_record_cookie = governance_test
.with_initial_community_token_deposit(&realm_cookie)
.await;
let hacker_governance_delegate = Keypair::new();
let mut instruction = set_governance_delegate(
&token_owner_record_cookie.token_owner.pubkey(),
&realm_cookie.address,
&realm_cookie.account.community_mint,
&token_owner_record_cookie.token_owner.pubkey(),
&Some(hacker_governance_delegate.pubkey()),
);
instruction.accounts[0] =
AccountMeta::new_readonly(token_owner_record_cookie.token_owner.pubkey(), false);
// Act
let err = governance_test
.process_transaction(&[instruction], None)
.await
.err()
.unwrap();
// Assert
assert_eq!(
err,
GovernanceError::GoverningTokenOwnerOrDelegateMustSign.into()
);
}
#[tokio::test]
async fn test_set_community_governance_delegate_signed_by_governance_delegate() {
// Arrange
let mut governance_test = GovernanceProgramTest::start_new().await;
let realm_cookie = governance_test.with_realm().await;
let mut token_owner_record_cookie = governance_test
.with_initial_community_token_deposit(&realm_cookie)
.await;
governance_test
.with_community_governance_delegate(&realm_cookie, &mut token_owner_record_cookie)
.await;
let new_governance_delegate = Keypair::new();
// Act
governance_test
.set_governance_delegate(
&realm_cookie,
&token_owner_record_cookie,
&token_owner_record_cookie.governance_delegate,
&realm_cookie.account.community_mint,
&Some(new_governance_delegate.pubkey()),
)
.await;
// Assert
let token_owner_record = governance_test
.get_token_owner_record_account(&token_owner_record_cookie.address)
.await;
assert_eq!(
Some(new_governance_delegate.pubkey()),
token_owner_record.governance_delegate
);
}

View File

@ -1,165 +0,0 @@
#![cfg(feature = "test-bpf")]
use solana_program::instruction::AccountMeta;
use solana_program_test::*;
mod program_test;
use program_test::*;
use solana_sdk::signature::{Keypair, Signer};
use spl_governance::{error::GovernanceError, instruction::set_vote_authority};
#[tokio::test]
async fn test_set_community_vote_authority() {
// Arrange
let mut governance_test = GovernanceProgramTest::start_new().await;
let realm_cookie = governance_test.with_realm().await;
let mut voter_record_cookie = governance_test
.with_initial_community_token_deposit(&realm_cookie)
.await;
// Act
governance_test
.with_community_vote_authority(&realm_cookie, &mut voter_record_cookie)
.await;
// Assert
let voter_record = governance_test
.get_voter_record_account(&voter_record_cookie.address)
.await;
assert_eq!(
Some(voter_record_cookie.vote_authority.pubkey()),
voter_record.vote_authority
);
}
#[tokio::test]
async fn test_set_vote_authority_to_none() {
// Arrange
let mut governance_test = GovernanceProgramTest::start_new().await;
let realm_cookie = governance_test.with_realm().await;
let mut voter_record_cookie = governance_test
.with_initial_community_token_deposit(&realm_cookie)
.await;
governance_test
.with_community_vote_authority(&realm_cookie, &mut voter_record_cookie)
.await;
// Act
governance_test
.set_vote_authority(
&realm_cookie,
&voter_record_cookie,
&voter_record_cookie.token_owner,
&realm_cookie.account.community_mint,
&None,
)
.await;
// Assert
let voter_record = governance_test
.get_voter_record_account(&voter_record_cookie.address)
.await;
assert_eq!(None, voter_record.vote_authority);
}
#[tokio::test]
async fn test_set_council_vote_authority() {
// Arrange
let mut governance_test = GovernanceProgramTest::start_new().await;
let realm_cookie = governance_test.with_realm().await;
let mut voter_record_cookie = governance_test
.with_initial_council_token_deposit(&realm_cookie)
.await;
// Act
governance_test
.with_council_vote_authority(&realm_cookie, &mut voter_record_cookie)
.await;
// Assert
let voter_record = governance_test
.get_voter_record_account(&voter_record_cookie.address)
.await;
assert_eq!(
Some(voter_record_cookie.vote_authority.pubkey()),
voter_record.vote_authority
);
}
#[tokio::test]
async fn test_set_community_vote_authority_with_owner_must_sign_error() {
// Arrange
let mut governance_test = GovernanceProgramTest::start_new().await;
let realm_cookie = governance_test.with_realm().await;
let voter_record_cookie = governance_test
.with_initial_community_token_deposit(&realm_cookie)
.await;
let hacker_vote_authority = Keypair::new();
let mut instruction = set_vote_authority(
&voter_record_cookie.token_owner.pubkey(),
&realm_cookie.address,
&realm_cookie.account.community_mint,
&voter_record_cookie.token_owner.pubkey(),
&Some(hacker_vote_authority.pubkey()),
);
instruction.accounts[0] =
AccountMeta::new_readonly(voter_record_cookie.token_owner.pubkey(), false);
// Act
let err = governance_test
.process_transaction(&[instruction], None)
.await
.err()
.unwrap();
// Assert
assert_eq!(
err,
GovernanceError::GoverningTokenOwnerOrVoteAuthrotiyMustSign.into()
);
}
#[tokio::test]
async fn test_set_community_vote_authority_signed_by_vote_authority() {
// Arrange
let mut governance_test = GovernanceProgramTest::start_new().await;
let realm_cookie = governance_test.with_realm().await;
let mut voter_record_cookie = governance_test
.with_initial_community_token_deposit(&realm_cookie)
.await;
governance_test
.with_community_vote_authority(&realm_cookie, &mut voter_record_cookie)
.await;
let new_vote_authority = Keypair::new();
// Act
governance_test
.set_vote_authority(
&realm_cookie,
&voter_record_cookie,
&voter_record_cookie.vote_authority,
&realm_cookie.account.community_mint,
&Some(new_vote_authority.pubkey()),
)
.await;
// Assert
let voter_record = governance_test
.get_voter_record_account(&voter_record_cookie.address)
.await;
assert_eq!(
Some(new_vote_authority.pubkey()),
voter_record.vote_authority
);
}

View File

@ -0,0 +1,59 @@
#![cfg(feature = "test-bpf")]
mod program_test;
use solana_program_test::tokio;
use program_test::*;
use spl_governance::state::enums::ProposalState;
#[tokio::test]
async fn test_sign_off_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();
let signatory_record_cookie = governance_test
.with_signatory(&proposal_cookie, &token_owner_record_cookie)
.await
.unwrap();
// Act
governance_test
.sign_off_proposal(&proposal_cookie, &signatory_record_cookie)
.await
.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!(Some(1), proposal_account.signing_off_at);
assert_eq!(Some(1), proposal_account.voting_at);
let signatory_record_account = governance_test
.get_signatory_record_account(&signatory_record_cookie.address)
.await;
assert_eq!(true, signatory_record_account.signed_off);
}

View File

@ -10,7 +10,7 @@ use solana_sdk::signature::Signer;
use spl_governance::{
error::GovernanceError, instruction::withdraw_governing_tokens,
state::voter_record::get_voter_record_address,
state::token_owner_record::get_token_owner_record_address,
};
#[tokio::test]
@ -19,22 +19,22 @@ async fn test_withdraw_community_tokens() {
let mut governance_test = GovernanceProgramTest::start_new().await;
let realm_cookie = governance_test.with_realm().await;
let voter_record_cookie = governance_test
let token_owner_record_cookie = governance_test
.with_initial_community_token_deposit(&realm_cookie)
.await;
// Act
governance_test
.withdraw_community_tokens(&realm_cookie, &voter_record_cookie)
.withdraw_community_tokens(&realm_cookie, &token_owner_record_cookie)
.await
.unwrap();
// Assert
let voter_record = governance_test
.get_voter_record_account(&voter_record_cookie.address)
let token_owner_record = governance_test
.get_token_owner_record_account(&token_owner_record_cookie.address)
.await;
assert_eq!(0, voter_record.token_deposit_amount);
assert_eq!(0, token_owner_record.governing_token_deposit_amount);
let holding_account = governance_test
.get_token_account(&realm_cookie.community_token_holding_account)
@ -43,11 +43,11 @@ async fn test_withdraw_community_tokens() {
assert_eq!(0, holding_account.amount);
let source_account = governance_test
.get_token_account(&voter_record_cookie.token_source)
.get_token_account(&token_owner_record_cookie.token_source)
.await;
assert_eq!(
voter_record_cookie.token_source_amount,
token_owner_record_cookie.token_source_amount,
source_account.amount
);
}
@ -58,22 +58,22 @@ async fn test_withdraw_council_tokens() {
let mut governance_test = GovernanceProgramTest::start_new().await;
let realm_cookie = governance_test.with_realm().await;
let voter_record_cookie = governance_test
let token_owner_record_cookie = governance_test
.with_initial_council_token_deposit(&realm_cookie)
.await;
// Act
governance_test
.withdraw_council_tokens(&realm_cookie, &voter_record_cookie)
.withdraw_council_tokens(&realm_cookie, &token_owner_record_cookie)
.await
.unwrap();
// Assert
let voter_record = governance_test
.get_voter_record_account(&voter_record_cookie.address)
let token_owner_record = governance_test
.get_token_owner_record_account(&token_owner_record_cookie.address)
.await;
assert_eq!(0, voter_record.token_deposit_amount);
assert_eq!(0, token_owner_record.governing_token_deposit_amount);
let holding_account = governance_test
.get_token_account(&realm_cookie.council_token_holding_account.unwrap())
@ -82,11 +82,11 @@ async fn test_withdraw_council_tokens() {
assert_eq!(0, holding_account.amount);
let source_account = governance_test
.get_token_account(&voter_record_cookie.token_source)
.get_token_account(&token_owner_record_cookie.token_source)
.await;
assert_eq!(
voter_record_cookie.token_source_amount,
token_owner_record_cookie.token_source_amount,
source_account.amount
);
}
@ -97,7 +97,7 @@ async fn test_withdraw_community_tokens_with_owner_must_sign_error() {
let mut governance_test = GovernanceProgramTest::start_new().await;
let realm_cookie = governance_test.with_realm().await;
let voter_record_cookie = governance_test
let token_owner_record_cookie = governance_test
.with_initial_community_token_deposit(&realm_cookie)
.await;
@ -106,12 +106,12 @@ async fn test_withdraw_community_tokens_with_owner_must_sign_error() {
let mut instruction = withdraw_governing_tokens(
&realm_cookie.address,
&hacker_token_destination,
&voter_record_cookie.token_owner.pubkey(),
&token_owner_record_cookie.token_owner.pubkey(),
&realm_cookie.account.community_mint,
);
instruction.accounts[3] =
AccountMeta::new_readonly(voter_record_cookie.token_owner.pubkey(), false);
AccountMeta::new_readonly(token_owner_record_cookie.token_owner.pubkey(), false);
// Act
let err = governance_test
@ -126,19 +126,19 @@ async fn test_withdraw_community_tokens_with_owner_must_sign_error() {
}
#[tokio::test]
async fn test_withdraw_community_tokens_with_voter_record_address_mismatch_error() {
async fn test_withdraw_community_tokens_with_token_owner_record_address_mismatch_error() {
// Arrange
let mut governance_test = GovernanceProgramTest::start_new().await;
let realm_cookie = governance_test.with_realm().await;
let voter_record_cookie = governance_test
let token_owner_record_cookie = governance_test
.with_initial_community_token_deposit(&realm_cookie)
.await;
let vote_record_address = get_voter_record_address(
let vote_record_address = get_token_owner_record_address(
&realm_cookie.address,
&realm_cookie.account.community_mint,
&voter_record_cookie.token_owner.pubkey(),
&token_owner_record_cookie.token_owner.pubkey(),
);
let hacker_record_cookie = governance_test
@ -163,5 +163,8 @@ async fn test_withdraw_community_tokens_with_voter_record_address_mismatch_error
// Assert
assert_eq!(err, GovernanceError::InvalidVoterAccountAddress.into());
assert_eq!(
err,
GovernanceError::InvalidTokenOwnerRecordAccountAddress.into()
);
}

View File

@ -1,6 +1,11 @@
use solana_program::pubkey::Pubkey;
use solana_sdk::signature::Keypair;
use spl_governance::state::{governance::Governance, realm::Realm, voter_record::VoterRecord};
use spl_governance::state::{
governance::Governance, realm::Realm, token_owner_record::TokenOwnerRecord,
};
use spl_governance::state::{proposal::Proposal, signatory_record::SignatoryRecord};
use super::tools::clone_keypair;
#[derive(Debug)]
pub struct RealmCookie {
@ -18,10 +23,10 @@ pub struct RealmCookie {
}
#[derive(Debug)]
pub struct VoterRecordCookie {
pub struct TokeOwnerRecordCookie {
pub address: Pubkey,
pub account: VoterRecord,
pub account: TokenOwnerRecord,
pub token_source: Pubkey,
@ -29,7 +34,24 @@ pub struct VoterRecordCookie {
pub token_owner: Keypair,
pub vote_authority: Keypair,
pub governance_authority: Option<Keypair>,
pub governance_delegate: Keypair,
pub governing_token_mint: Pubkey,
}
impl TokeOwnerRecordCookie {
pub fn get_governance_authority(&self) -> &Keypair {
self.governance_authority
.as_ref()
.unwrap_or(&self.token_owner)
}
#[allow(dead_code)]
pub fn clone_governance_delegate(&self) -> Keypair {
clone_keypair(&self.governance_delegate)
}
}
#[derive(Debug)]
@ -49,4 +71,20 @@ pub struct GovernedAccountCookie {
pub struct GovernanceCookie {
pub address: Pubkey,
pub account: Governance,
pub next_proposal_index: u16,
}
#[derive(Debug)]
pub struct ProposalCookie {
pub address: Pubkey,
pub account: Proposal,
pub proposal_owner: Pubkey,
}
#[derive(Debug)]
pub struct SignatoryRecordCookie {
pub address: Pubkey,
pub account: SignatoryRecord,
pub signatory: Keypair,
}

View File

@ -23,25 +23,34 @@ use solana_sdk::{
};
use spl_governance::{
instruction::{
create_account_governance, create_program_governance, create_realm,
deposit_governing_tokens, set_vote_authority, withdraw_governing_tokens,
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,
},
processor::process_instruction,
state::{
enums::{GovernanceAccountType, GoverningTokenType},
enums::{GovernanceAccountType, ProposalState},
governance::{
get_account_governance_address, get_program_governance_address, Governance,
GovernanceConfig,
},
proposal::{get_proposal_address, Proposal},
realm::{get_governing_token_holding_address, get_realm_address, Realm},
voter_record::{get_voter_record_address, VoterRecord},
signatory_record::{get_signatory_record_address, SignatoryRecord},
token_owner_record::{get_token_owner_record_address, TokenOwnerRecord},
},
tools::bpf_loader_upgradeable::get_program_data_address,
};
pub mod cookies;
use self::cookies::{
GovernanceCookie, GovernedAccountCookie, GovernedProgramCookie, RealmCookie, VoterRecordCookie,
use crate::program_test::cookies::SignatoryRecordCookie;
use self::{
cookies::{
GovernanceCookie, GovernedAccountCookie, GovernedProgramCookie, ProposalCookie,
RealmCookie, TokeOwnerRecordCookie,
},
tools::NopOverride,
};
pub mod tools;
@ -166,12 +175,27 @@ impl GovernanceProgramTest {
pub async fn with_initial_community_token_deposit(
&mut self,
realm_cookie: &RealmCookie,
) -> VoterRecordCookie {
) -> TokeOwnerRecordCookie {
self.with_initial_governing_token_deposit(
&realm_cookie.address,
GoverningTokenType::Community,
&realm_cookie.account.community_mint,
&realm_cookie.community_mint_authority,
100,
)
.await
}
#[allow(dead_code)]
pub async fn with_initial_community_token_deposit_amount(
&mut self,
realm_cookie: &RealmCookie,
amount: u64,
) -> TokeOwnerRecordCookie {
self.with_initial_governing_token_deposit(
&realm_cookie.address,
&realm_cookie.account.community_mint,
&realm_cookie.community_mint_authority,
amount,
)
.await
}
@ -180,14 +204,14 @@ impl GovernanceProgramTest {
pub async fn with_community_token_deposit(
&mut self,
realm_cookie: &RealmCookie,
voter_record_cookie: &VoterRecordCookie,
token_owner_record_cookie: &TokeOwnerRecordCookie,
amount: u64,
) {
self.with_governing_token_deposit(
&realm_cookie.address,
&realm_cookie.account.community_mint,
&realm_cookie.community_mint_authority,
voter_record_cookie,
token_owner_record_cookie,
amount,
)
.await;
@ -197,14 +221,14 @@ impl GovernanceProgramTest {
pub async fn with_council_token_deposit(
&mut self,
realm_cookie: &RealmCookie,
voter_record_cookie: &VoterRecordCookie,
token_owner_record_cookie: &TokeOwnerRecordCookie,
amount: u64,
) {
self.with_governing_token_deposit(
&realm_cookie.address,
&realm_cookie.account.council_mint.unwrap(),
&realm_cookie.council_mint_authority.as_ref().unwrap(),
voter_record_cookie,
token_owner_record_cookie,
amount,
)
.await;
@ -214,12 +238,12 @@ impl GovernanceProgramTest {
pub async fn with_initial_council_token_deposit(
&mut self,
realm_cookie: &RealmCookie,
) -> VoterRecordCookie {
) -> TokeOwnerRecordCookie {
self.with_initial_governing_token_deposit(
&realm_cookie.address,
GoverningTokenType::Council,
&realm_cookie.account.council_mint.unwrap(),
&realm_cookie.council_mint_authority.as_ref().unwrap(),
100,
)
.await
}
@ -228,21 +252,20 @@ impl GovernanceProgramTest {
pub async fn with_initial_governing_token_deposit(
&mut self,
realm_address: &Pubkey,
governing_token_type: GoverningTokenType,
governing_mint: &Pubkey,
governing_mint_authority: &Keypair,
) -> VoterRecordCookie {
amount: u64,
) -> TokeOwnerRecordCookie {
let token_owner = Keypair::new();
let token_source = Keypair::new();
let source_amount = 100;
let transfer_authority = Keypair::new();
self.create_token_account_with_transfer_authority(
&token_source,
governing_mint,
governing_mint_authority,
source_amount,
amount,
&token_owner,
&transfer_authority.pubkey(),
)
@ -264,30 +287,32 @@ impl GovernanceProgramTest {
.await
.unwrap();
let voter_record_address =
get_voter_record_address(realm_address, &governing_mint, &token_owner.pubkey());
let token_owner_record_address =
get_token_owner_record_address(realm_address, &governing_mint, &token_owner.pubkey());
let account = VoterRecord {
account_type: GovernanceAccountType::VoterRecord,
let account = TokenOwnerRecord {
account_type: GovernanceAccountType::TokenOwnerRecord,
realm: *realm_address,
token_type: governing_token_type,
token_owner: token_owner.pubkey(),
token_deposit_amount: source_amount,
vote_authority: None,
governing_token_mint: *governing_mint,
governing_token_owner: token_owner.pubkey(),
governing_token_deposit_amount: amount,
governance_delegate: None,
active_votes_count: 0,
total_votes_count: 0,
};
let vote_authority = Keypair::from_base58_string(&token_owner.to_base58_string());
let governance_delegate = Keypair::from_base58_string(&token_owner.to_base58_string());
VoterRecordCookie {
address: voter_record_address,
TokeOwnerRecordCookie {
address: token_owner_record_address,
account,
token_source_amount: source_amount,
token_source_amount: amount,
token_source: token_source.pubkey(),
token_owner,
vote_authority,
governance_authority: None,
governance_delegate: governance_delegate,
governing_token_mint: *governing_mint,
}
}
@ -297,103 +322,103 @@ impl GovernanceProgramTest {
realm: &Pubkey,
governing_token_mint: &Pubkey,
governing_token_mint_authority: &Keypair,
voter_record_cookie: &VoterRecordCookie,
token_owner_record_cookie: &TokeOwnerRecordCookie,
amount: u64,
) {
self.mint_tokens(
governing_token_mint,
governing_token_mint_authority,
&voter_record_cookie.token_source,
&token_owner_record_cookie.token_source,
amount,
)
.await;
let deposit_governing_tokens_instruction = deposit_governing_tokens(
realm,
&voter_record_cookie.token_source,
&voter_record_cookie.token_owner.pubkey(),
&voter_record_cookie.token_owner.pubkey(),
&token_owner_record_cookie.token_source,
&token_owner_record_cookie.token_owner.pubkey(),
&token_owner_record_cookie.token_owner.pubkey(),
&self.payer.pubkey(),
governing_token_mint,
);
self.process_transaction(
&[deposit_governing_tokens_instruction],
Some(&[&voter_record_cookie.token_owner]),
Some(&[&token_owner_record_cookie.token_owner]),
)
.await
.unwrap();
}
#[allow(dead_code)]
pub async fn with_community_vote_authority(
pub async fn with_community_governance_delegate(
&mut self,
realm_cookie: &RealmCookie,
voter_record_cookie: &mut VoterRecordCookie,
token_owner_record_cookie: &mut TokeOwnerRecordCookie,
) {
self.with_governing_token_vote_authority(
self.with_governing_token_governance_delegate(
&realm_cookie,
&realm_cookie.account.community_mint,
voter_record_cookie,
token_owner_record_cookie,
)
.await;
}
#[allow(dead_code)]
pub async fn with_council_vote_authority(
pub async fn with_council_governance_delegate(
&mut self,
realm_cookie: &RealmCookie,
voter_record_cookie: &mut VoterRecordCookie,
token_owner_record_cookie: &mut TokeOwnerRecordCookie,
) {
self.with_governing_token_vote_authority(
self.with_governing_token_governance_delegate(
&realm_cookie,
&realm_cookie.account.council_mint.unwrap(),
voter_record_cookie,
token_owner_record_cookie,
)
.await;
}
#[allow(dead_code)]
pub async fn with_governing_token_vote_authority(
pub async fn with_governing_token_governance_delegate(
&mut self,
realm_cookie: &RealmCookie,
governing_token_mint: &Pubkey,
voter_record_cookie: &mut VoterRecordCookie,
token_owner_record_cookie: &mut TokeOwnerRecordCookie,
) {
let new_vote_authority = Keypair::new();
let new_governance_delegate = Keypair::new();
self.set_vote_authority(
self.set_governance_delegate(
realm_cookie,
voter_record_cookie,
&voter_record_cookie.token_owner,
token_owner_record_cookie,
&token_owner_record_cookie.token_owner,
governing_token_mint,
&Some(new_vote_authority.pubkey()),
&Some(new_governance_delegate.pubkey()),
)
.await;
voter_record_cookie.vote_authority = new_vote_authority;
token_owner_record_cookie.governance_delegate = new_governance_delegate;
}
#[allow(dead_code)]
pub async fn set_vote_authority(
pub async fn set_governance_delegate(
&mut self,
realm_cookie: &RealmCookie,
voter_record_cookie: &VoterRecordCookie,
signing_vote_authority: &Keypair,
token_owner_record_cookie: &TokeOwnerRecordCookie,
signing_governance_authority: &Keypair,
governing_token_mint: &Pubkey,
new_vote_authority: &Option<Pubkey>,
new_governance_delegate: &Option<Pubkey>,
) {
let set_vote_authority_instruction = set_vote_authority(
&signing_vote_authority.pubkey(),
let set_governance_delegate_instruction = set_governance_delegate(
&signing_governance_authority.pubkey(),
&realm_cookie.address,
governing_token_mint,
&voter_record_cookie.token_owner.pubkey(),
new_vote_authority,
&token_owner_record_cookie.token_owner.pubkey(),
new_governance_delegate,
);
self.process_transaction(
&[set_vote_authority_instruction],
Some(&[&signing_vote_authority]),
&[set_governance_delegate_instruction],
Some(&[&signing_governance_authority]),
)
.await
.unwrap();
@ -403,13 +428,13 @@ impl GovernanceProgramTest {
pub async fn withdraw_community_tokens(
&mut self,
realm_cookie: &RealmCookie,
voter_record_cookie: &VoterRecordCookie,
token_owner_record_cookie: &TokeOwnerRecordCookie,
) -> Result<(), ProgramError> {
self.withdraw_governing_tokens(
realm_cookie,
voter_record_cookie,
token_owner_record_cookie,
&realm_cookie.account.community_mint,
&voter_record_cookie.token_owner,
&token_owner_record_cookie.token_owner,
)
.await
}
@ -418,13 +443,13 @@ impl GovernanceProgramTest {
pub async fn withdraw_council_tokens(
&mut self,
realm_cookie: &RealmCookie,
voter_record_cookie: &VoterRecordCookie,
token_owner_record_cookie: &TokeOwnerRecordCookie,
) -> Result<(), ProgramError> {
self.withdraw_governing_tokens(
realm_cookie,
voter_record_cookie,
token_owner_record_cookie,
&realm_cookie.account.council_mint.unwrap(),
&voter_record_cookie.token_owner,
&token_owner_record_cookie.token_owner,
)
.await
}
@ -433,14 +458,14 @@ impl GovernanceProgramTest {
async fn withdraw_governing_tokens(
&mut self,
realm_cookie: &RealmCookie,
voter_record_cookie: &VoterRecordCookie,
token_owner_record_cookie: &TokeOwnerRecordCookie,
governing_token_mint: &Pubkey,
governing_token_owner: &Keypair,
) -> Result<(), ProgramError> {
let deposit_governing_tokens_instruction = withdraw_governing_tokens(
&realm_cookie.address,
&voter_record_cookie.token_source,
&token_owner_record_cookie.token_source,
&governing_token_owner.pubkey(),
governing_token_mint,
);
@ -491,7 +516,7 @@ impl GovernanceProgramTest {
let account = Governance {
account_type: GovernanceAccountType::AccountGovernance,
config: governance_config,
proposal_count: 0,
proposals_count: 0,
};
self.process_transaction(&[create_account_governance_instruction], None)
@ -503,6 +528,7 @@ impl GovernanceProgramTest {
Ok(GovernanceCookie {
address: account_governance_address,
account,
next_proposal_index: 0,
})
}
@ -586,7 +612,7 @@ impl GovernanceProgramTest {
self.with_program_governance_instruction(
realm_cookie,
governed_program_cookie,
|_| {},
NopOverride,
None,
)
.await
@ -603,10 +629,10 @@ impl GovernanceProgramTest {
let config = GovernanceConfig {
realm: realm_cookie.address,
governed_account: governed_program_cookie.address,
vote_threshold_percentage: 60,
min_tokens_to_create_proposal: 5,
min_instruction_hold_up_time: 10,
max_voting_time: 100,
vote_threshold_percentage: 60,
};
let mut create_program_governance_instruction = create_program_governance(
@ -627,7 +653,7 @@ impl GovernanceProgramTest {
let account = Governance {
account_type: GovernanceAccountType::ProgramGovernance,
config,
proposal_count: 0,
proposals_count: 0,
};
let program_governance_address =
@ -636,12 +662,178 @@ impl GovernanceProgramTest {
Ok(GovernanceCookie {
address: program_governance_address,
account,
next_proposal_index: 0,
})
}
#[allow(dead_code)]
pub async fn get_voter_record_account(&mut self, address: &Pubkey) -> VoterRecord {
self.get_borsh_account::<VoterRecord>(address).await
pub async fn with_proposal(
&mut self,
token_owner_record_cookie: &TokeOwnerRecordCookie,
governance_cookie: &mut GovernanceCookie,
) -> Result<ProposalCookie, ProgramError> {
self.with_proposal_instruction(token_owner_record_cookie, governance_cookie, |_| {})
.await
}
#[allow(dead_code)]
pub async fn with_proposal_instruction<F: Fn(&mut Instruction)>(
&mut self,
token_owner_record_cookie: &TokeOwnerRecordCookie,
governance_cookie: &mut GovernanceCookie,
instruction_override: F,
) -> Result<ProposalCookie, ProgramError> {
let proposal_index = governance_cookie.next_proposal_index;
governance_cookie.next_proposal_index = governance_cookie.next_proposal_index + 1;
let name = format!("Proposal #{}", proposal_index);
let description_link = "Proposal Description".to_string();
let governance_authority = token_owner_record_cookie.get_governance_authority();
let mut create_proposal_instruction = create_proposal(
&governance_cookie.address,
&token_owner_record_cookie.token_owner.pubkey(),
&governance_authority.pubkey(),
&self.payer.pubkey(),
&governance_cookie.account.config.realm,
name.clone(),
description_link.clone(),
&token_owner_record_cookie.governing_token_mint,
proposal_index,
);
instruction_override(&mut create_proposal_instruction);
self.process_transaction(
&[create_proposal_instruction],
Some(&[&governance_authority]),
)
.await?;
let account = Proposal {
account_type: GovernanceAccountType::Proposal,
description_link,
name: name.clone(),
governance: governance_cookie.address,
governing_token_mint: token_owner_record_cookie.governing_token_mint,
state: ProposalState::Draft,
signatories_count: 0,
// Clock always returns 1 when running under the test
draft_at: 1,
signing_off_at: None,
voting_at: None,
voting_completed_at: None,
executing_at: None,
closed_at: None,
number_of_executed_instructions: 0,
number_of_instructions: 0,
token_owner_record: token_owner_record_cookie.address,
signatories_signed_off_count: 0,
};
let proposal_address = get_proposal_address(
&governance_cookie.address,
&token_owner_record_cookie.governing_token_mint,
&proposal_index.to_le_bytes(),
);
Ok(ProposalCookie {
address: proposal_address,
account,
proposal_owner: governance_authority.pubkey(),
})
}
#[allow(dead_code)]
pub async fn with_signatory(
&mut self,
proposal_cookie: &ProposalCookie,
token_owner_record_cookie: &TokeOwnerRecordCookie,
) -> Result<SignatoryRecordCookie, ProgramError> {
let signatory = Keypair::new();
let add_signatory_instruction = add_signatory(
&proposal_cookie.address,
&token_owner_record_cookie.address,
&token_owner_record_cookie.token_owner.pubkey(),
&self.payer.pubkey(),
&signatory.pubkey(),
);
self.process_transaction(
&[add_signatory_instruction],
Some(&[&token_owner_record_cookie.token_owner]),
)
.await?;
let signatory_record_address =
get_signatory_record_address(&proposal_cookie.address, &signatory.pubkey());
let signatory_record_data = SignatoryRecord {
account_type: GovernanceAccountType::SignatoryRecord,
proposal: proposal_cookie.address,
signatory: signatory.pubkey(),
signed_off: false,
};
let signatory_record_cookie = SignatoryRecordCookie {
address: signatory_record_address,
account: signatory_record_data,
signatory: signatory,
};
Ok(signatory_record_cookie)
}
#[allow(dead_code)]
pub async fn remove_signatory(
&mut self,
proposal_cookie: &ProposalCookie,
token_owner_record_cookie: &TokeOwnerRecordCookie,
signatory_record_cookie: &SignatoryRecordCookie,
) -> Result<(), ProgramError> {
let remove_signatory_instruction = remove_signatory(
&proposal_cookie.address,
&token_owner_record_cookie.address,
&token_owner_record_cookie.token_owner.pubkey(),
&signatory_record_cookie.account.signatory,
&token_owner_record_cookie.token_owner.pubkey(),
);
self.process_transaction(
&[remove_signatory_instruction],
Some(&[&token_owner_record_cookie.token_owner]),
)
.await?;
Ok(())
}
#[allow(dead_code)]
pub async fn sign_off_proposal(
&mut self,
proposal_cookie: &ProposalCookie,
signatory_record_cookie: &SignatoryRecordCookie,
) -> Result<(), ProgramError> {
let sign_off_proposal_instruction = sign_off_proposal(
&proposal_cookie.address,
&signatory_record_cookie.signatory.pubkey(),
);
self.process_transaction(
&[sign_off_proposal_instruction],
Some(&[&signatory_record_cookie.signatory]),
)
.await?;
Ok(())
}
#[allow(dead_code)]
pub async fn get_token_owner_record_account(&mut self, address: &Pubkey) -> TokenOwnerRecord {
self.get_borsh_account::<TokenOwnerRecord>(address).await
}
#[allow(dead_code)]
@ -659,6 +851,20 @@ impl GovernanceProgramTest {
.await
}
#[allow(dead_code)]
pub async fn get_proposal_account(&mut self, proposal_address: &Pubkey) -> Proposal {
self.get_borsh_account::<Proposal>(proposal_address).await
}
#[allow(dead_code)]
pub async fn get_signatory_record_account(
&mut self,
proposal_address: &Pubkey,
) -> SignatoryRecord {
self.get_borsh_account::<SignatoryRecord>(proposal_address)
.await
}
#[allow(dead_code)]
async fn get_packed_account<T: Pack + IsInitialized>(&mut self, address: &Pubkey) -> T {
self.banks_client
@ -695,7 +901,7 @@ impl GovernanceProgramTest {
.await
.unwrap()
.map(|a| try_from_slice_unchecked(&a.data).unwrap())
.expect(format!("GET-TEST-ACCOUNT-ERROR: Account {}", address).as_str())
.expect(format!("GET-TEST-ACCOUNT-ERROR: Account {} not found", address).as_str())
}
#[allow(dead_code)]
@ -729,54 +935,6 @@ impl GovernanceProgramTest {
.unwrap();
}
#[allow(dead_code)]
pub async fn create_token_account(
&mut self,
token_account_keypair: &Keypair,
token_mint: &Pubkey,
token_mint_authority: &Keypair,
amount: u64,
owner: &Pubkey,
) {
let create_account_instruction = system_instruction::create_account(
&self.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();
let mint_instruction = spl_token::instruction::mint_to(
&spl_token::id(),
token_mint,
&token_account_keypair.pubkey(),
&token_mint_authority.pubkey(),
&[],
amount,
)
.unwrap();
self.process_transaction(
&[
create_account_instruction,
initialize_account_instruction,
mint_instruction,
],
Some(&[&token_account_keypair, &token_mint_authority]),
)
.await
.unwrap();
}
#[allow(dead_code)]
pub async fn create_token_account_with_transfer_authority(
&mut self,

View File

@ -1,7 +1,7 @@
use std::convert::TryFrom;
use solana_program::{instruction::InstructionError, program_error::ProgramError};
use solana_sdk::{transaction::TransactionError, transport::TransportError};
use solana_sdk::{signature::Keypair, transaction::TransactionError, transport::TransportError};
/// TODO: Add to SDK
/// Instruction errors not mapped in the sdk
@ -35,3 +35,11 @@ pub fn map_transaction_error(transport_error: TransportError) -> ProgramError {
_ => panic!("TEST-TRANSPORT-ERROR: {:?}", transport_error),
}
}
pub fn clone_keypair(source: &Keypair) -> Keypair {
Keypair::from_bytes(&source.to_bytes()).unwrap()
}
/// NOP (No Operation) Override function
#[allow(non_snake_case)]
pub fn NopOverride<T>(_: &mut T) {}