Governance: Implement Realms

Implemented instructions:
- CreateRealm
- DepositGoverningTokens
- WithdrawGoverningTokens
-SetVoteAuthority

Co-authored-by: Jon Cinque <jon.cinque@gmail.com>
Co-authored-by: Michael Vines <mvines@gmail.com>
This commit is contained in:
Sebastian Bor 2021-05-19 12:20:53 +01:00 committed by GitHub
parent 79f31e320f
commit addc6bf4b4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 2407 additions and 50 deletions

6
Cargo.lock generated
View File

@ -3813,12 +3813,18 @@ dependencies = [
name = "spl-governance" name = "spl-governance"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"arrayref",
"assert_matches", "assert_matches",
"bincode",
"borsh 0.8.2",
"num-derive", "num-derive",
"num-traits", "num-traits",
"serde",
"serde_derive",
"solana-program", "solana-program",
"solana-program-test", "solana-program-test",
"solana-sdk", "solana-sdk",
"spl-token 3.1.0",
"thiserror", "thiserror",
] ]

View File

@ -7,23 +7,26 @@ repository = "https://github.com/solana-labs/solana-program-library"
license = "Apache-2.0" license = "Apache-2.0"
edition = "2018" edition = "2018"
[features] [features]
no-entrypoint = [] no-entrypoint = []
test-bpf = [] test-bpf = []
[dependencies] [dependencies]
solana-program = "1.6.7" arrayref = "0.3.6"
thiserror = "1.0" bincode = "1.3.2"
borsh = "0.8.1"
num-derive = "0.3" num-derive = "0.3"
num-traits = "0.2" num-traits = "0.2"
serde = "1.0.121"
serde_derive = "1.0.103"
solana-program = "1.6.7"
spl-token = { path = "../../token/program", features = [ "no-entrypoint" ] }
thiserror = "1.0"
[dev-dependencies] [dev-dependencies]
assert_matches = "1.5.0" assert_matches = "1.5.0"
solana-program-test = "1.6.7" solana-program-test = "1.6.7"
solana-sdk = "1.6.7" solana-sdk = "1.6.7"
[lib] [lib]
crate-type = ["cdylib", "lib"] crate-type = ["cdylib", "lib"]

View File

@ -8,17 +8,53 @@ use solana_program::{
}; };
use thiserror::Error; use thiserror::Error;
/// Errors that may be returned by the Governance program. /// Errors that may be returned by the Governance program
#[derive(Clone, Debug, Eq, Error, FromPrimitive, PartialEq)] #[derive(Clone, Debug, Eq, Error, FromPrimitive, PartialEq)]
pub enum GovernanceError { pub enum GovernanceError {
/// Invalid instruction passed to program. /// Invalid instruction passed to program
#[error("Invalid instruction passed to program")] #[error("Invalid instruction passed to program")]
InvalidInstruction, InvalidInstruction,
/// Realm with the given name and governing mints already exists
#[error("Realm with the given name and governing mints already exists")]
RealmAlreadyExists,
/// Invalid Governing Token Mint
#[error("Invalid Governing Token Mint")]
InvalidGoverningTokenMint,
/// Governing Token Owner must sign transaction
#[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,
/// 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,
/// ---- Account Tools Errors -----
/// Invalid account owner
#[error("Invalid account owner")]
InvalidAccountOwner,
/// ---- Token Tools Errors -----
/// Invalid Token account owner
#[error("Invalid Token account owner")]
InvalidTokenAccountOwner,
} }
impl PrintProgramError for GovernanceError { impl PrintProgramError for GovernanceError {
fn print<E>(&self) { fn print<E>(&self) {
msg!(&self.to_string()); msg!("GOVERNANCE-ERROR: {}", &self.to_string());
} }
} }

View File

@ -1,12 +1,24 @@
//! Program instructions //! Program instructions
use solana_program::{instruction::Instruction, pubkey::Pubkey}; use crate::{
id,
use crate::state::enums::GoverningTokenType; state::{
enums::GoverningTokenType,
realm::{get_governing_token_holding_address, get_realm_address},
single_signer_instruction::InstructionData,
voter_record::get_voter_record_address,
},
};
use borsh::{BorshDeserialize, BorshSchema, BorshSerialize};
use solana_program::{
instruction::{AccountMeta, Instruction},
pubkey::Pubkey,
system_program, sysvar,
};
/// Yes/No Vote /// Yes/No Vote
#[repr(C)] #[repr(C)]
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq, BorshDeserialize, BorshSerialize, BorshSchema)]
pub enum Vote { pub enum Vote {
/// Yes vote /// Yes vote
Yes, Yes,
@ -15,7 +27,7 @@ pub enum Vote {
} }
/// Instructions supported by the Governance program /// Instructions supported by the Governance program
#[derive(Clone)] #[derive(Clone, Debug, PartialEq, BorshDeserialize, BorshSerialize, BorshSchema)]
#[repr(C)] #[repr(C)]
#[allow(clippy::large_enum_variant)] #[allow(clippy::large_enum_variant)]
pub enum GovernanceInstruction { pub enum GovernanceInstruction {
@ -33,6 +45,7 @@ pub enum GovernanceInstruction {
/// 8. `[writable]` Council Token Holding account - optional. . PDA seeds: ['governance',realm,council_mint] /// 8. `[writable]` Council Token Holding account - optional. . PDA seeds: ['governance',realm,council_mint]
/// The account will be created with the Realm PDA as its owner /// The account will be created with the Realm PDA as its owner
CreateRealm { CreateRealm {
#[allow(dead_code)]
/// UTF-8 encoded Governance Realm name /// UTF-8 encoded Governance Realm name
name: String, name: String,
}, },
@ -45,10 +58,11 @@ pub enum GovernanceInstruction {
/// 1. `[writable]` Governing Token Holding account. PDA seeds: ['governance',realm, governing_token_mint] /// 1. `[writable]` Governing Token Holding account. PDA seeds: ['governance',realm, governing_token_mint]
/// 2. `[writable]` Governing Token Source account. All tokens from the account will be transferred to the Holding account /// 2. `[writable]` Governing Token Source account. All tokens from the account will be transferred to the Holding account
/// 3. `[signer]` Governing Token Owner account /// 3. `[signer]` Governing Token Owner account
/// 4. `[writable]` Voter Record account. PDA seeds: ['governance',realm, governing_token_mint, governing_token_owner] /// 4. `[signer]` Governing Token Transfer authority
/// 5. `[signer]` Payer /// 5. `[writable]` Voter Record account. PDA seeds: ['governance',realm, governing_token_mint, governing_token_owner]
/// 6. `[]` System /// 6. `[signer]` Payer
/// 7. `[]` SPL Token /// 7. `[]` System
/// 8. `[]` SPL Token
DepositGoverningTokens {}, DepositGoverningTokens {},
/// Withdraws governing tokens (Community or Council) from Governance Realm and downgrades your voter weight within the Realm /// Withdraws governing tokens (Community or Council) from Governance Realm and downgrades your voter weight within the Realm
@ -65,8 +79,9 @@ pub enum GovernanceInstruction {
/// Sets vote authority for the given Realm and Governing Token Mint (Community or Council) /// 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 /// 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
/// ///
/// 0. `[signer]` Governing Token Owner /// 0. `[signer]` Current Vote authority or Governing Token owner
/// 1. `[writable]` Voter Record /// 1. `[writable]` Voter Record
SetVoteAuthority { SetVoteAuthority {
#[allow(dead_code)] #[allow(dead_code)]
@ -77,9 +92,13 @@ pub enum GovernanceInstruction {
/// Governing Token Mint the vote authority is granted over /// Governing Token Mint the vote authority is granted over
governing_token_mint: Pubkey, governing_token_mint: Pubkey,
#[allow(dead_code)]
/// Governing Token Owner the vote authority is set for
governing_token_owner: Pubkey,
#[allow(dead_code)] #[allow(dead_code)]
/// New vote authority /// New vote authority
vote_authority: Pubkey, new_vote_authority: Option<Pubkey>,
}, },
/// Creates Program Governance account which governs an upgradable program /// Creates Program Governance account which governs an upgradable program
@ -93,16 +112,20 @@ pub enum GovernanceInstruction {
/// 6. `[]` System account /// 6. `[]` System account
/// 7. `[]` Bpf_upgrade_loader account /// 7. `[]` Bpf_upgrade_loader account
CreateProgramGovernance { CreateProgramGovernance {
#[allow(dead_code)]
/// Voting threshold in % required to tip the vote /// Voting threshold in % required to tip the vote
/// It's the percentage of tokens out of the entire pool of governance tokens eligible to vote /// It's the percentage of tokens out of the entire pool of governance tokens eligible to vote
vote_threshold: u8, vote_threshold: u8,
#[allow(dead_code)]
/// Minimum waiting time in slots for an instruction to be executed after proposal is voted on /// Minimum waiting time in slots for an instruction to be executed after proposal is voted on
min_instruction_hold_up_time: u64, min_instruction_hold_up_time: u64,
#[allow(dead_code)]
/// Time limit in slots for proposal to be open for voting /// Time limit in slots for proposal to be open for voting
max_voting_time: u64, max_voting_time: u64,
#[allow(dead_code)]
/// Minimum % of tokens for a governance token owner to be able to create proposal /// Minimum % of tokens for a governance token owner to be able to create proposal
/// It's the percentage of tokens out of the entire pool of governance tokens eligible to vote /// It's the percentage of tokens out of the entire pool of governance tokens eligible to vote
token_threshold_to_create_proposal: u8, token_threshold_to_create_proposal: u8,
@ -120,12 +143,15 @@ pub enum GovernanceInstruction {
/// 6. '[]` Token program account /// 6. '[]` Token program account
/// 7. `[]` Rent sysvar /// 7. `[]` Rent sysvar
CreateProposal { CreateProposal {
#[allow(dead_code)]
/// Link to gist explaining proposal /// Link to gist explaining proposal
description_link: String, description_link: String,
#[allow(dead_code)]
/// UTF-8 encoded name of the proposal /// UTF-8 encoded name of the proposal
name: String, name: String,
#[allow(dead_code)]
/// The Governing token (Community or Council) which will be used for voting on the Proposal /// The Governing token (Community or Council) which will be used for voting on the Proposal
governing_token_type: GoverningTokenType, governing_token_type: GoverningTokenType,
}, },
@ -158,12 +184,15 @@ pub enum GovernanceInstruction {
/// 1. `[writable]` Uninitialized Proposal SingleSignerInstruction account /// 1. `[writable]` Uninitialized Proposal SingleSignerInstruction account
/// 2. `[signer]` Admin account /// 2. `[signer]` Admin account
AddSingleSignerInstruction { AddSingleSignerInstruction {
#[allow(dead_code)]
/// Slot waiting time between vote period ending and this being eligible for execution /// Slot waiting time between vote period ending and this being eligible for execution
hold_up_time: u64, hold_up_time: u64,
#[allow(dead_code)]
/// Instruction /// Instruction
instruction: Instruction, instruction: InstructionData,
#[allow(dead_code)]
/// Position in instruction array /// Position in instruction array
position: u8, position: u8,
}, },
@ -183,6 +212,7 @@ pub enum GovernanceInstruction {
/// 1. `[writable]` Proposal SingleSignerInstruction account /// 1. `[writable]` Proposal SingleSignerInstruction account
/// 2. `[signer]` Admin account /// 2. `[signer]` Admin account
UpdateInstructionHoldUpTime { UpdateInstructionHoldUpTime {
#[allow(dead_code)]
/// Minimum waiting time in slots for an instruction to be executed after proposal is voted on /// Minimum waiting time in slots for an instruction to be executed after proposal is voted on
hold_up_time: u64, hold_up_time: u64,
}, },
@ -215,6 +245,7 @@ pub enum GovernanceInstruction {
/// 3. `[signer]` Vote Authority account /// 3. `[signer]` Vote Authority account
/// 4. `[]` Governance account /// 4. `[]` Governance account
Vote { Vote {
#[allow(dead_code)]
/// Yes/No vote /// Yes/No vote
vote: Vote, vote: Vote,
}, },
@ -243,3 +274,147 @@ pub enum GovernanceInstruction {
/// 5+ Any extra accounts that are part of the instruction, in order /// 5+ Any extra accounts that are part of the instruction, in order
Execute, Execute,
} }
/// Creates CreateRealm instruction
pub fn create_realm(
// Accounts
community_token_mint: &Pubkey,
payer: &Pubkey,
council_token_mint: Option<Pubkey>,
// Args
name: String,
) -> Instruction {
let realm_address = get_realm_address(&name);
let community_token_holding_address =
get_governing_token_holding_address(&realm_address, &community_token_mint);
let mut accounts = vec![
AccountMeta::new(realm_address, false),
AccountMeta::new_readonly(*community_token_mint, false),
AccountMeta::new(community_token_holding_address, false),
AccountMeta::new_readonly(*payer, true),
AccountMeta::new_readonly(system_program::id(), false),
AccountMeta::new_readonly(spl_token::id(), false),
AccountMeta::new_readonly(sysvar::rent::id(), false),
];
if let Some(council_token_mint) = council_token_mint {
let council_token_holding_address =
get_governing_token_holding_address(&realm_address, &council_token_mint);
accounts.push(AccountMeta::new_readonly(council_token_mint, false));
accounts.push(AccountMeta::new(council_token_holding_address, false));
}
let instruction = GovernanceInstruction::CreateRealm { name };
Instruction {
program_id: id(),
accounts,
data: instruction.try_to_vec().unwrap(),
}
}
/// Creates DepositGoverningTokens instruction
pub fn deposit_governing_tokens(
// Accounts
realm: &Pubkey,
governing_token_source: &Pubkey,
governing_token_owner: &Pubkey,
governing_token_transfer_authority: &Pubkey,
payer: &Pubkey,
// Args
governing_token_mint: &Pubkey,
) -> Instruction {
let vote_record_address =
get_voter_record_address(realm, governing_token_mint, governing_token_owner);
let governing_token_holding_address =
get_governing_token_holding_address(realm, governing_token_mint);
let accounts = vec![
AccountMeta::new_readonly(*realm, false),
AccountMeta::new(governing_token_holding_address, false),
AccountMeta::new(*governing_token_source, false),
AccountMeta::new_readonly(*governing_token_owner, true),
AccountMeta::new_readonly(*governing_token_transfer_authority, true),
AccountMeta::new(vote_record_address, false),
AccountMeta::new_readonly(*payer, true),
AccountMeta::new_readonly(system_program::id(), false),
AccountMeta::new_readonly(spl_token::id(), false),
AccountMeta::new_readonly(sysvar::rent::id(), false),
];
let instruction = GovernanceInstruction::DepositGoverningTokens {};
Instruction {
program_id: id(),
accounts,
data: instruction.try_to_vec().unwrap(),
}
}
/// Creates WithdrawGoverningTokens instruction
pub fn withdraw_governing_tokens(
// Accounts
realm: &Pubkey,
governing_token_destination: &Pubkey,
governing_token_owner: &Pubkey,
// Args
governing_token_mint: &Pubkey,
) -> Instruction {
let vote_record_address =
get_voter_record_address(realm, governing_token_mint, governing_token_owner);
let governing_token_holding_address =
get_governing_token_holding_address(realm, governing_token_mint);
let accounts = vec![
AccountMeta::new_readonly(*realm, false),
AccountMeta::new(governing_token_holding_address, false),
AccountMeta::new(*governing_token_destination, false),
AccountMeta::new_readonly(*governing_token_owner, true),
AccountMeta::new(vote_record_address, false),
AccountMeta::new_readonly(spl_token::id(), false),
];
let instruction = GovernanceInstruction::WithdrawGoverningTokens {};
Instruction {
program_id: id(),
accounts,
data: instruction.try_to_vec().unwrap(),
}
}
/// Creates SetVoteAuthority instruction
pub fn set_vote_authority(
// Accounts
vote_authority: &Pubkey,
// Args
realm: &Pubkey,
governing_token_mint: &Pubkey,
governing_token_owner: &Pubkey,
new_vote_authority: &Option<Pubkey>,
) -> Instruction {
let vote_record_address =
get_voter_record_address(realm, governing_token_mint, governing_token_owner);
let accounts = vec![
AccountMeta::new_readonly(*vote_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,
};
Instruction {
program_id: id(),
accounts,
data: instruction.try_to_vec().unwrap(),
}
}

View File

@ -6,8 +6,12 @@ pub mod error;
pub mod instruction; pub mod instruction;
pub mod processor; pub mod processor;
pub mod state; pub mod state;
pub mod tools;
// Export current sdk types for downstream users building with a different sdk version // Export current sdk types for downstream users building with a different sdk version
pub use solana_program; pub use solana_program;
solana_program::declare_id!("Governance111111111111111111111111111111111"); solana_program::declare_id!("GovernancerdmUu324nahyv33G5poQdLUEZ1nEytDeP");
/// Seed prefix for Governance PDAs
pub const PROGRAM_AUTHORITY_SEED: &[u8] = b"governance";

View File

@ -1,14 +0,0 @@
//! Instruction processor
use solana_program::{account_info::AccountInfo, entrypoint::ProgramResult, pubkey::Pubkey};
use crate::error::GovernanceError;
/// Processes an instruction
pub fn process_instruction(
_program_id: &Pubkey,
_accounts: &[AccountInfo],
_input: &[u8],
) -> ProgramResult {
Err(GovernanceError::InvalidInstruction.into())
}

View File

@ -0,0 +1,59 @@
//! Program processor
mod process_create_realm;
mod process_deposit_governing_tokens;
mod process_set_vote_authority;
mod process_withdraw_governing_tokens;
use crate::instruction::GovernanceInstruction;
use borsh::BorshDeserialize;
use process_create_realm::*;
use process_deposit_governing_tokens::*;
use process_set_vote_authority::*;
use process_withdraw_governing_tokens::*;
use solana_program::{
account_info::AccountInfo, entrypoint::ProgramResult, msg, program_error::ProgramError,
pubkey::Pubkey,
};
/// Processes an instruction
pub fn process_instruction(
program_id: &Pubkey,
accounts: &[AccountInfo],
input: &[u8],
) -> ProgramResult {
let instruction = GovernanceInstruction::try_from_slice(input)
.map_err(|_| ProgramError::InvalidInstructionData)?;
msg!("Instruction: {:?}", instruction);
match instruction {
GovernanceInstruction::CreateRealm { name } => {
process_create_realm(program_id, accounts, name)
}
GovernanceInstruction::DepositGoverningTokens {} => {
process_deposit_governing_tokens(program_id, accounts)
}
GovernanceInstruction::WithdrawGoverningTokens {} => {
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,
),
_ => todo!("Instruction not implemented yet"),
}
}

View File

@ -0,0 +1,97 @@
//! Program state processor
use solana_program::{
account_info::{next_account_info, AccountInfo},
entrypoint::ProgramResult,
pubkey::Pubkey,
rent::Rent,
sysvar::Sysvar,
};
use crate::{
error::GovernanceError,
state::{
enums::GovernanceAccountType,
realm::{get_governing_token_holding_address_seeds, get_realm_address_seeds, Realm},
},
tools::{account::create_and_serialize_account_signed, token::create_spl_token_account_signed},
};
/// Processes CreateRealm instruction
pub fn process_create_realm(
program_id: &Pubkey,
accounts: &[AccountInfo],
name: String,
) -> ProgramResult {
let account_info_iter = &mut accounts.iter();
let realm_info = next_account_info(account_info_iter)?; // 0
let governance_token_mint_info = next_account_info(account_info_iter)?; // 1
let governance_token_holding_info = next_account_info(account_info_iter)?; // 2
let payer_info = next_account_info(account_info_iter)?; // 3
let system_info = next_account_info(account_info_iter)?; // 4
let spl_token_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)?;
if !realm_info.data_is_empty() {
return Err(GovernanceError::RealmAlreadyExists.into());
}
create_spl_token_account_signed(
payer_info,
governance_token_holding_info,
&get_governing_token_holding_address_seeds(realm_info.key, governance_token_mint_info.key),
governance_token_mint_info,
realm_info,
program_id,
system_info,
spl_token_info,
rent_sysvar_info,
rent,
)?;
let council_token_mint_address = if let Ok(council_token_mint_info) =
next_account_info(account_info_iter)
// 7
{
let council_token_holding_info = next_account_info(account_info_iter)?; //8
create_spl_token_account_signed(
payer_info,
council_token_holding_info,
&get_governing_token_holding_address_seeds(realm_info.key, council_token_mint_info.key),
council_token_mint_info,
realm_info,
program_id,
system_info,
spl_token_info,
rent_sysvar_info,
rent,
)?;
Some(*council_token_mint_info.key)
} else {
None
};
let realm_data = Realm {
account_type: GovernanceAccountType::Realm,
community_mint: *governance_token_mint_info.key,
council_mint: council_token_mint_address,
name: name.clone(),
};
create_and_serialize_account_signed::<Realm>(
payer_info,
&realm_info,
&realm_data,
&get_realm_address_seeds(&name),
program_id,
system_info,
rent,
)?;
Ok(())
}

View File

@ -0,0 +1,118 @@
//! 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::{
error::GovernanceError,
state::{
enums::{GovernanceAccountType, GoverningTokenType},
realm::deserialize_realm,
voter_record::{deserialize_voter_record, get_voter_record_address_seeds, VoterRecord},
},
tools::{
account::create_and_serialize_account_signed,
token::{
get_amount_from_token_account, get_mint_from_token_account,
get_owner_from_token_account, transfer_spl_tokens,
},
},
};
/// Processes DepositGoverningTokens instruction
pub fn process_deposit_governing_tokens(
program_id: &Pubkey,
accounts: &[AccountInfo],
) -> ProgramResult {
let account_info_iter = &mut accounts.iter();
let realm_info = next_account_info(account_info_iter)?; // 0
let governing_token_holding_info = next_account_info(account_info_iter)?; // 1
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 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
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 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());
};
let amount = get_amount_from_token_account(governing_token_source_info)?;
transfer_spl_tokens(
&governing_token_source_info,
&governing_token_holding_info,
&governing_token_transfer_authority_info,
amount,
spl_token_info,
)?;
let voter_record_address_seeds = get_voter_record_address_seeds(
realm_info.key,
&governing_token_mint,
governing_token_owner_info.key,
);
if voter_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)?;
if !(governing_token_owner == *governing_token_owner_info.key
&& governing_token_owner_info.is_signer)
{
return Err(GovernanceError::GoverningTokenOwnerMustSign.into());
}
let voter_record_data = VoterRecord {
account_type: GovernanceAccountType::VoterRecord,
realm: *realm_info.key,
token_owner: *governing_token_owner_info.key,
token_deposit_amount: amount,
token_type: governing_token_type,
vote_authority: 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,
program_id,
system_info,
rent,
)?;
} else {
let mut voter_record_data =
deserialize_voter_record(voter_record_info, &voter_record_address_seeds)?;
voter_record_data.token_deposit_amount = voter_record_data
.token_deposit_amount
.checked_add(amount)
.unwrap();
voter_record_data.serialize(&mut *voter_record_info.data.borrow_mut())?;
}
Ok(())
}

View File

@ -0,0 +1,39 @@
//! 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,67 @@
//! Program state processor
use borsh::BorshSerialize;
use solana_program::{
account_info::{next_account_info, AccountInfo},
entrypoint::ProgramResult,
pubkey::Pubkey,
};
use crate::{
error::GovernanceError,
state::{
realm::{deserialize_realm, get_realm_address_seeds},
voter_record::{deserialize_voter_record, get_voter_record_address_seeds},
},
tools::token::{get_mint_from_token_account, transfer_spl_tokens_signed},
};
/// Processes WithdrawGoverningTokens instruction
pub fn process_withdraw_governing_tokens(
program_id: &Pubkey,
accounts: &[AccountInfo],
) -> ProgramResult {
let account_info_iter = &mut accounts.iter();
let realm_info = next_account_info(account_info_iter)?; // 0
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 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 governing_token_mint = get_mint_from_token_account(governing_token_holding_info)?;
let voter_record_address_seeds = get_voter_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)?;
if voter_record_data.active_votes_count > 0 {
return Err(GovernanceError::CannotWithdrawGoverningTokensWhenActiveVotesExist.into());
}
transfer_spl_tokens_signed(
&governing_token_holding_info,
&governing_token_destination_info,
&realm_info,
&get_realm_address_seeds(&realm_data.name),
program_id,
voter_record_data.token_deposit_amount,
spl_token_info,
)?;
voter_record_data.token_deposit_amount = 0;
voter_record_data.serialize(&mut *voter_record_info.data.borrow_mut())?;
Ok(())
}

View File

@ -1,8 +1,10 @@
//! State enumerations //! State enumerations
use borsh::{BorshDeserialize, BorshSchema, BorshSerialize};
/// Defines all Governance accounts types /// Defines all Governance accounts types
#[repr(C)] #[repr(C)]
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq, BorshDeserialize, BorshSerialize, BorshSchema)]
pub enum GovernanceAccountType { pub enum GovernanceAccountType {
/// Default uninitialized account state /// Default uninitialized account state
Uninitialized, Uninitialized,
@ -34,7 +36,7 @@ impl Default for GovernanceAccountType {
/// Vote with number of votes /// Vote with number of votes
#[repr(C)] #[repr(C)]
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq, BorshDeserialize, BorshSerialize, BorshSchema)]
pub enum VoteWeight { pub enum VoteWeight {
/// Yes vote /// Yes vote
Yes(u64), Yes(u64),
@ -45,7 +47,7 @@ pub enum VoteWeight {
/// Governing Token type /// Governing Token type
#[repr(C)] #[repr(C)]
#[derive(Clone)] #[derive(Clone, Debug, PartialEq, BorshDeserialize, BorshSerialize, BorshSchema)]
pub enum GoverningTokenType { pub enum GoverningTokenType {
/// Community token /// Community token
Community, Community,
@ -55,7 +57,7 @@ pub enum GoverningTokenType {
/// What state a Proposal is in /// What state a Proposal is in
#[repr(C)] #[repr(C)]
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq, BorshDeserialize, BorshSerialize, BorshSchema)]
pub enum ProposalState { pub enum ProposalState {
/// Draft - Proposal enters Draft state when it's created /// Draft - Proposal enters Draft state when it's created
Draft, Draft,

View File

@ -1,12 +1,13 @@
//! Program Governance Account //! Program Governance Account
use borsh::{BorshDeserialize, BorshSchema, BorshSerialize};
use solana_program::pubkey::Pubkey; use solana_program::pubkey::Pubkey;
use super::enums::GovernanceAccountType; use super::enums::GovernanceAccountType;
/// Program Governance Account /// Program Governance Account
#[repr(C)] #[repr(C)]
#[derive(Clone, Debug, Default, PartialEq)] #[derive(Clone, Debug, PartialEq, BorshDeserialize, BorshSerialize, BorshSchema)]
pub struct ProgramGovernance { pub struct ProgramGovernance {
/// Account type /// Account type
pub account_type: GovernanceAccountType, pub account_type: GovernanceAccountType,

View File

@ -3,10 +3,11 @@
use solana_program::{epoch_schedule::Slot, pubkey::Pubkey}; use solana_program::{epoch_schedule::Slot, pubkey::Pubkey};
use super::enums::{GovernanceAccountType, GoverningTokenType, ProposalState}; use super::enums::{GovernanceAccountType, GoverningTokenType, ProposalState};
use borsh::{BorshDeserialize, BorshSchema, BorshSerialize};
/// Governance Proposal /// Governance Proposal
#[repr(C)] #[repr(C)]
#[derive(Clone)] #[derive(Clone, Debug, PartialEq, BorshDeserialize, BorshSerialize, BorshSchema)]
pub struct Proposal { pub struct Proposal {
/// Governance account type /// Governance account type
pub account_type: GovernanceAccountType, pub account_type: GovernanceAccountType,

View File

@ -1,12 +1,13 @@
//! Proposal Vote Record Account //! Proposal Vote Record Account
use borsh::{BorshDeserialize, BorshSchema, BorshSerialize};
use solana_program::pubkey::Pubkey; use solana_program::pubkey::Pubkey;
use super::enums::{GovernanceAccountType, VoteWeight}; use super::enums::{GovernanceAccountType, VoteWeight};
/// Proposal Vote Record /// Proposal Vote Record
#[repr(C)] #[repr(C)]
#[derive(Clone, Debug, Default, PartialEq)] #[derive(Clone, Debug, PartialEq, BorshDeserialize, BorshSerialize, BorshSchema)]
pub struct ProposalVoteRecord { pub struct ProposalVoteRecord {
/// Governance account type /// Governance account type
pub account_type: GovernanceAccountType, pub account_type: GovernanceAccountType,

View File

@ -1,12 +1,23 @@
//! Realm Account //! Realm Account
use solana_program::pubkey::Pubkey; use borsh::{BorshDeserialize, BorshSchema, BorshSerialize};
use solana_program::{
account_info::AccountInfo, program_error::ProgramError, program_pack::IsInitialized,
pubkey::Pubkey,
};
use crate::{
id,
tools::account::{deserialize_account, AccountMaxSize},
PROGRAM_AUTHORITY_SEED,
};
use super::enums::GovernanceAccountType; use super::enums::GovernanceAccountType;
/// Governance Realm Account /// Governance Realm Account
/// Account PDA seeds" ['governance', name] /// Account PDA seeds" ['governance', name]
#[repr(C)] #[repr(C)]
#[derive(Clone, Debug, PartialEq, BorshDeserialize, BorshSerialize, BorshSchema)]
pub struct Realm { pub struct Realm {
/// Governance account type /// Governance account type
pub account_type: GovernanceAccountType, pub account_type: GovernanceAccountType,
@ -20,3 +31,50 @@ pub struct Realm {
/// Governance Realm name /// Governance Realm name
pub name: String, pub name: String,
} }
impl AccountMaxSize for Realm {}
impl IsInitialized for Realm {
fn is_initialized(&self) -> bool {
self.account_type == GovernanceAccountType::Realm
}
}
/// Deserializes account and checks owner program
pub fn deserialize_realm(realm_info: &AccountInfo) -> Result<Realm, ProgramError> {
deserialize_account::<Realm>(realm_info, &id())
}
/// Returns Realm PDA seeds
pub fn get_realm_address_seeds(name: &str) -> [&[u8]; 2] {
[PROGRAM_AUTHORITY_SEED, &name.as_bytes()]
}
/// Returns Realm PDA address
pub fn get_realm_address(name: &str) -> Pubkey {
Pubkey::find_program_address(&get_realm_address_seeds(&name), &id()).0
}
/// Returns Realm Token Holding PDA seeds
pub fn get_governing_token_holding_address_seeds<'a>(
realm: &'a Pubkey,
governing_token_mint: &'a Pubkey,
) -> [&'a [u8]; 3] {
[
PROGRAM_AUTHORITY_SEED,
realm.as_ref(),
governing_token_mint.as_ref(),
]
}
/// Returns Realm Token Holding PDA address
pub fn get_governing_token_holding_address(
realm: &Pubkey,
governing_token_mint: &Pubkey,
) -> Pubkey {
Pubkey::find_program_address(
&get_governing_token_holding_address_seeds(realm, governing_token_mint),
&id(),
)
.0
}

View File

@ -1,12 +1,11 @@
//! SingleSignerInstruction Account //! SingleSignerInstruction Account
use solana_program::instruction::Instruction;
use super::enums::GovernanceAccountType; use super::enums::GovernanceAccountType;
use borsh::{BorshDeserialize, BorshSchema, BorshSerialize};
/// Account for an instruction to be executed for Proposal /// Account for an instruction to be executed for Proposal
#[repr(C)] #[repr(C)]
#[derive(Clone)] #[derive(Clone, Debug, PartialEq, BorshDeserialize, BorshSerialize, BorshSchema)]
pub struct SingleSignerInstruction { pub struct SingleSignerInstruction {
/// Governance Account type /// Governance Account type
pub account_type: GovernanceAccountType, pub account_type: GovernanceAccountType,
@ -17,8 +16,13 @@ pub struct SingleSignerInstruction {
/// Instruction to execute /// Instruction to execute
/// The instruction will be signed by Governance PDA the Proposal belongs to /// The instruction will be signed by Governance PDA the Proposal belongs to
// For example for ProgramGovernance the instruction to upgrade program will be signed by ProgramGovernance PDA // For example for ProgramGovernance the instruction to upgrade program will be signed by ProgramGovernance PDA
pub instruction: Instruction, pub instruction: InstructionData,
/// Executed flag /// Executed flag
pub executed: bool, pub executed: bool,
} }
/// Temp. placeholder until I get Borsh serialization for Instruction working
#[derive(Clone, Debug, PartialEq, BorshDeserialize, BorshSerialize, BorshSchema)]
#[repr(C)]
pub struct InstructionData {}

View File

@ -1,12 +1,23 @@
//! Voter Record Account //! Voter Record Account
use solana_program::pubkey::Pubkey; use crate::{
error::GovernanceError,
id,
tools::account::{deserialize_account, AccountMaxSize},
PROGRAM_AUTHORITY_SEED,
};
use super::enums::{GovernanceAccountType, GoverningTokenType}; 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 /// Governance Voter Record
/// Account PDA seeds: ['governance', realm, token_mint, token_owner ] /// Account PDA seeds: ['governance', realm, token_mint, token_owner ]
#[repr(C)] #[repr(C)]
#[derive(Clone, Debug, PartialEq, BorshDeserialize, BorshSerialize, BorshSchema)]
pub struct VoterRecord { pub struct VoterRecord {
/// Governance account type /// Governance account type
pub account_type: GovernanceAccountType, pub account_type: GovernanceAccountType,
@ -26,8 +37,8 @@ pub struct VoterRecord {
pub token_deposit_amount: u64, pub token_deposit_amount: u64,
/// A single account that is allowed to operate governance with the deposited governing tokens /// A single account that is allowed to operate governance with the deposited governing tokens
/// It's delegated to by the token owner /// It's delegated to by the governing token owner or current vote_authority
pub vote_authority: Pubkey, pub vote_authority: Option<Pubkey>,
/// The number of active votes cast by voter /// The number of active votes cast by voter
pub active_votes_count: u8, pub active_votes_count: u8,
@ -35,3 +46,81 @@ pub struct VoterRecord {
/// The total number of votes cast by the voter /// The total number of votes cast by the voter
pub total_votes_count: u8, 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

@ -0,0 +1,101 @@
//! General purpose account utility functions
use borsh::{BorshDeserialize, BorshSerialize};
use solana_program::{
account_info::AccountInfo, borsh::try_from_slice_unchecked, msg, program::invoke_signed,
program_error::ProgramError, program_pack::IsInitialized, pubkey::Pubkey, rent::Rent,
system_instruction::create_account,
};
use crate::error::GovernanceError;
/// Trait for accounts to return their max size
pub trait AccountMaxSize {
/// Returns max account size or None if max size is not known and actual instance size should be used
fn get_max_size(&self) -> Option<usize> {
None
}
}
/// Creates a new account and serializes data into it using the provided seeds to invoke signed CPI call
/// Note: This functions also checks the provided account PDA matches the supplied seeds
pub fn create_and_serialize_account_signed<'a, T: BorshSerialize + AccountMaxSize>(
payer_info: &AccountInfo<'a>,
account_info: &AccountInfo<'a>,
account_data: &T,
account_address_seeds: &[&[u8]],
program_id: &Pubkey,
system_info: &AccountInfo<'a>,
rent: &Rent,
) -> Result<(), ProgramError> {
// Get PDA and assert it's the same as the requested account address
let (account_address, bump_seed) =
Pubkey::find_program_address(account_address_seeds, program_id);
if account_address != *account_info.key {
msg!(
"Create account with PDA: {:?} was requested while PDA: {:?} was expected",
account_info.key,
account_address
);
return Err(ProgramError::InvalidSeeds);
}
let (serialized_data, account_size) = if let Some(max_size) = account_data.get_max_size() {
(None, max_size)
} else {
let serialized_data = account_data.try_to_vec()?;
let account_size = serialized_data.len();
(Some(serialized_data), account_size)
};
let create_account_instruction = create_account(
payer_info.key,
account_info.key,
rent.minimum_balance(account_size),
account_size as u64,
program_id,
);
let mut signers_seeds = account_address_seeds.to_vec();
let bump = &[bump_seed];
signers_seeds.push(bump);
invoke_signed(
&create_account_instruction,
&[
payer_info.clone(),
account_info.clone(),
system_info.clone(),
],
&[&signers_seeds[..]],
)?;
if let Some(serialized_data) = serialized_data {
account_info
.data
.borrow_mut()
.copy_from_slice(&serialized_data);
} else {
account_data.serialize(&mut *account_info.data.borrow_mut())?;
}
Ok(())
}
/// Deserializes account and checks it's initialized and owned by the specified program
pub fn deserialize_account<T: BorshDeserialize + IsInitialized>(
account_info: &AccountInfo,
owner_program_id: &Pubkey,
) -> Result<T, ProgramError> {
if account_info.owner != owner_program_id {
return Err(GovernanceError::InvalidAccountOwner.into());
}
let account: T = try_from_slice_unchecked(&account_info.data.borrow())?;
if !account.is_initialized() {
Err(ProgramError::UninitializedAccount)
} else {
Ok(account)
}
}

View File

@ -0,0 +1,25 @@
//! Governance asserts
use solana_program::{account_info::AccountInfo, program_error::ProgramError};
use crate::{error::GovernanceError, state::voter_record::VoterRecord};
/// Checks wether 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,
) -> Result<(), ProgramError> {
if vote_authority_info.is_signer {
if &voter_record.token_owner == vote_authority_info.key {
return Ok(());
}
if let Some(vote_authority) = voter_record.vote_authority {
if &vote_authority == vote_authority_info.key {
return Ok(());
}
};
}
Err(GovernanceError::GoverningTokenOwnerOrVoteAuthrotiyMustSign.into())
}

View File

@ -0,0 +1,7 @@
//! Utility functions
pub mod account;
pub mod token;
pub mod asserts;

View File

@ -0,0 +1,211 @@
//! General purpose SPL token utility functions
use arrayref::array_ref;
use solana_program::{
account_info::AccountInfo,
entrypoint::ProgramResult,
msg,
program::{invoke, invoke_signed},
program_error::ProgramError,
program_pack::Pack,
pubkey::Pubkey,
rent::Rent,
system_instruction,
};
use crate::error::GovernanceError;
/// Creates and initializes SPL token account with PDA using the provided PDA seeds
#[allow(clippy::too_many_arguments)]
pub fn create_spl_token_account_signed<'a>(
payer_info: &AccountInfo<'a>,
token_account_info: &AccountInfo<'a>,
token_account_address_seeds: &[&[u8]],
token_mint_info: &AccountInfo<'a>,
token_account_owner_info: &AccountInfo<'a>,
program_id: &Pubkey,
system_info: &AccountInfo<'a>,
spl_token_info: &AccountInfo<'a>,
rent_sysvar_info: &AccountInfo<'a>,
rent: &Rent,
) -> Result<(), ProgramError> {
let create_account_instruction = system_instruction::create_account(
payer_info.key,
token_account_info.key,
1.max(rent.minimum_balance(spl_token::state::Account::get_packed_len())),
spl_token::state::Account::get_packed_len() as u64,
&spl_token::id(),
);
let (account_address, bump_seed) =
Pubkey::find_program_address(token_account_address_seeds, program_id);
if account_address != *token_account_info.key {
msg!(
"Create SPL Token Account with PDA: {:?} was requested while PDA: {:?} was expected",
token_account_info.key,
account_address
);
return Err(ProgramError::InvalidSeeds);
}
let mut signers_seeds = token_account_address_seeds.to_vec();
let bump = &[bump_seed];
signers_seeds.push(bump);
invoke_signed(
&create_account_instruction,
&[
payer_info.clone(),
token_account_info.clone(),
system_info.clone(),
],
&[&signers_seeds[..]],
)?;
let initialize_account_instruction = spl_token::instruction::initialize_account(
&spl_token::id(),
token_account_info.key,
token_mint_info.key,
token_account_owner_info.key,
)?;
invoke(
&initialize_account_instruction,
&[
payer_info.clone(),
token_account_info.clone(),
token_account_owner_info.clone(),
token_mint_info.clone(),
spl_token_info.clone(),
rent_sysvar_info.clone(),
],
)?;
Ok(())
}
/// Transfers SPL Tokens
pub fn transfer_spl_tokens<'a>(
source_info: &AccountInfo<'a>,
destination_info: &AccountInfo<'a>,
authority_info: &AccountInfo<'a>,
amount: u64,
spl_token_info: &AccountInfo<'a>,
) -> ProgramResult {
let transfer_instruction = spl_token::instruction::transfer(
&spl_token::id(),
source_info.key,
destination_info.key,
authority_info.key,
&[],
amount,
)
.unwrap();
invoke(
&transfer_instruction,
&[
spl_token_info.clone(),
authority_info.clone(),
source_info.clone(),
destination_info.clone(),
],
)?;
Ok(())
}
/// Transfers SPL Tokens from a token account owned by the provided PDA authority with seeds
pub fn transfer_spl_tokens_signed<'a>(
source_info: &AccountInfo<'a>,
destination_info: &AccountInfo<'a>,
authority_info: &AccountInfo<'a>,
authority_seeds: &[&[u8]],
program_id: &Pubkey,
amount: u64,
spl_token_info: &AccountInfo<'a>,
) -> ProgramResult {
let (authority_address, bump_seed) = Pubkey::find_program_address(authority_seeds, program_id);
if authority_address != *authority_info.key {
msg!(
"Transfer SPL Token with Authority PDA: {:?} was requested while PDA: {:?} was expected",
authority_info.key,
authority_address
);
return Err(ProgramError::InvalidSeeds);
}
let transfer_instruction = spl_token::instruction::transfer(
&spl_token::id(),
source_info.key,
destination_info.key,
authority_info.key,
&[],
amount,
)
.unwrap();
let mut signers_seeds = authority_seeds.to_vec();
let bump = &[bump_seed];
signers_seeds.push(bump);
invoke_signed(
&transfer_instruction,
&[
spl_token_info.clone(),
authority_info.clone(),
source_info.clone(),
destination_info.clone(),
],
&[&signers_seeds[..]],
)?;
Ok(())
}
/// Computationally cheap method to get amount from a token account
/// It reads amount without deserializing full account data
pub fn get_amount_from_token_account(
token_account_info: &AccountInfo,
) -> Result<u64, ProgramError> {
if token_account_info.owner != &spl_token::id() {
return Err(GovernanceError::InvalidTokenAccountOwner.into());
}
// TokeAccount layout: mint(32), owner(32), amount(8), ...
let data = token_account_info.try_borrow_data()?;
let amount = array_ref![data, 64, 8];
Ok(u64::from_le_bytes(*amount))
}
/// Computationally cheap method to get mint from a token account
/// It reads mint without deserializing full account data
pub fn get_mint_from_token_account(
token_account_info: &AccountInfo,
) -> Result<Pubkey, ProgramError> {
if token_account_info.owner != &spl_token::id() {
return Err(GovernanceError::InvalidTokenAccountOwner.into());
}
// TokeAccount layout: mint(32), owner(32), amount(8), ...
let data = token_account_info.try_borrow_data().unwrap();
let mint_data = array_ref![data, 0, 32];
Ok(Pubkey::new_from_array(*mint_data))
}
/// Computationally cheap method to get owner from a token account
/// It reads owner without deserializing full account data
pub fn get_owner_from_token_account(
token_account_info: &AccountInfo,
) -> Result<Pubkey, ProgramError> {
if token_account_info.owner != &spl_token::id() {
return Err(GovernanceError::InvalidTokenAccountOwner.into());
}
// TokeAccount layout: mint(32), owner(32), amount(8)
let data = token_account_info.try_borrow_data().unwrap();
let owner_data = array_ref![data, 32, 32];
Ok(Pubkey::new_from_array(*owner_data))
}

View File

@ -0,0 +1,23 @@
#![cfg(feature = "test-bpf")]
use solana_program_test::*;
mod program_test;
use program_test::*;
#[tokio::test]
async fn test_realm_created() {
// Arrange
let mut governance_test = GovernanceProgramTest::start_new().await;
// Act
let realm_cookie = governance_test.with_realm().await;
// Assert
let realm_account = governance_test
.get_realm_account(&realm_cookie.address)
.await;
assert_eq!(realm_cookie.account, realm_account);
}

View File

@ -0,0 +1,234 @@
#![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::deposit_governing_tokens};
#[tokio::test]
async fn test_deposit_initial_community_tokens() {
// Arrange
let mut governance_test = GovernanceProgramTest::start_new().await;
let realm_cookie = governance_test.with_realm().await;
// Act
let voter_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)
.await;
assert_eq!(voter_record_cookie.account, voter_record);
let source_account = governance_test
.get_token_account(&voter_record_cookie.token_source)
.await;
assert_eq!(
voter_record_cookie.token_source_amount - voter_record_cookie.account.token_deposit_amount,
source_account.amount
);
let holding_account = governance_test
.get_token_account(&realm_cookie.community_token_holding_account)
.await;
assert_eq!(voter_record.token_deposit_amount, holding_account.amount);
}
#[tokio::test]
async fn test_deposit_initial_council_tokens() {
// Arrange
let mut governance_test = GovernanceProgramTest::start_new().await;
let realm_cookie = governance_test.with_realm().await;
let council_token_holding_account = realm_cookie.council_token_holding_account.unwrap();
// Act
let voter_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)
.await;
assert_eq!(voter_record_cookie.account, voter_record);
let source_account = governance_test
.get_token_account(&voter_record_cookie.token_source)
.await;
assert_eq!(
voter_record_cookie.token_source_amount - voter_record_cookie.account.token_deposit_amount,
source_account.amount
);
let holding_account = governance_test
.get_token_account(&council_token_holding_account)
.await;
assert_eq!(voter_record.token_deposit_amount, holding_account.amount);
}
#[tokio::test]
async fn test_deposit_subsequent_community_tokens() {
// 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 deposit_amount = 5;
let total_deposit_amount = voter_record_cookie.account.token_deposit_amount + deposit_amount;
// Act
governance_test
.with_community_token_deposit(&realm_cookie, &voter_record_cookie, deposit_amount)
.await;
// Assert
let voter_record = governance_test
.get_voter_record_account(&voter_record_cookie.address)
.await;
assert_eq!(total_deposit_amount, voter_record.token_deposit_amount);
let holding_account = governance_test
.get_token_account(&realm_cookie.community_token_holding_account)
.await;
assert_eq!(total_deposit_amount, holding_account.amount);
}
#[tokio::test]
async fn test_deposit_subsequent_council_tokens() {
// Arrange
let mut governance_test = GovernanceProgramTest::start_new().await;
let realm_cookie = governance_test.with_realm().await;
let council_token_holding_account = realm_cookie.council_token_holding_account.unwrap();
let voter_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;
// Act
governance_test
.with_council_token_deposit(&realm_cookie, &voter_record_cookie, deposit_amount)
.await;
// Assert
let voter_record = governance_test
.get_voter_record_account(&voter_record_cookie.address)
.await;
assert_eq!(total_deposit_amount, voter_record.token_deposit_amount);
let holding_account = governance_test
.get_token_account(&council_token_holding_account)
.await;
assert_eq!(total_deposit_amount, holding_account.amount);
}
#[tokio::test]
async fn test_deposit_initial_community_tokens_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 = Keypair::new();
let transfer_authority = Keypair::new();
let token_source = Keypair::new();
governance_test
.create_token_account_with_transfer_authority(
&token_source,
&realm_cookie.account.community_mint,
&realm_cookie.community_mint_authority,
10,
&token_owner,
&transfer_authority.pubkey(),
)
.await;
let mut instruction = deposit_governing_tokens(
&realm_cookie.address,
&token_source.pubkey(),
&token_owner.pubkey(),
&transfer_authority.pubkey(),
&governance_test.payer.pubkey(),
&realm_cookie.account.community_mint,
);
instruction.accounts[3] = AccountMeta::new_readonly(token_owner.pubkey(), false);
// // Act
let error = governance_test
.process_transaction(&[instruction], Some(&[&transfer_authority]))
.await
.err()
.unwrap();
// Assert
assert_eq!(error, GovernanceError::GoverningTokenOwnerMustSign.into());
}
#[tokio::test]
async fn test_deposit_initial_community_tokens_with_invalid_owner_error() {
// Arrange
let mut governance_test = GovernanceProgramTest::start_new().await;
let realm_cookie = governance_test.with_realm().await;
let token_owner = Keypair::new();
let transfer_authority = Keypair::new();
let token_source = Keypair::new();
let invalid_owner = Keypair::new();
governance_test
.create_token_account_with_transfer_authority(
&token_source,
&realm_cookie.account.community_mint,
&realm_cookie.community_mint_authority,
10,
&token_owner,
&transfer_authority.pubkey(),
)
.await;
let instruction = deposit_governing_tokens(
&realm_cookie.address,
&token_source.pubkey(),
&invalid_owner.pubkey(),
&transfer_authority.pubkey(),
&governance_test.payer.pubkey(),
&realm_cookie.account.community_mint,
);
// // Act
let error = governance_test
.process_transaction(&[instruction], Some(&[&transfer_authority, &invalid_owner]))
.await
.err()
.unwrap();
// Assert
assert_eq!(error, GovernanceError::GoverningTokenOwnerMustSign.into());
}

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_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,167 @@
#![cfg(feature = "test-bpf")]
use solana_program::{instruction::AccountMeta, pubkey::Pubkey};
use solana_program_test::*;
mod program_test;
use program_test::*;
use solana_sdk::signature::Signer;
use spl_governance::{
error::GovernanceError, instruction::withdraw_governing_tokens,
state::voter_record::get_voter_record_address,
};
#[tokio::test]
async fn test_withdraw_community_tokens() {
// 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;
// Act
governance_test
.withdraw_community_tokens(&realm_cookie, &voter_record_cookie)
.await
.unwrap();
// Assert
let voter_record = governance_test
.get_voter_record_account(&voter_record_cookie.address)
.await;
assert_eq!(0, voter_record.token_deposit_amount);
let holding_account = governance_test
.get_token_account(&realm_cookie.community_token_holding_account)
.await;
assert_eq!(0, holding_account.amount);
let source_account = governance_test
.get_token_account(&voter_record_cookie.token_source)
.await;
assert_eq!(
voter_record_cookie.token_source_amount,
source_account.amount
);
}
#[tokio::test]
async fn test_withdraw_council_tokens() {
// 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_council_token_deposit(&realm_cookie)
.await;
// Act
governance_test
.withdraw_council_tokens(&realm_cookie, &voter_record_cookie)
.await
.unwrap();
// Assert
let voter_record = governance_test
.get_voter_record_account(&voter_record_cookie.address)
.await;
assert_eq!(0, voter_record.token_deposit_amount);
let holding_account = governance_test
.get_token_account(&realm_cookie.council_token_holding_account.unwrap())
.await;
assert_eq!(0, holding_account.amount);
let source_account = governance_test
.get_token_account(&voter_record_cookie.token_source)
.await;
assert_eq!(
voter_record_cookie.token_source_amount,
source_account.amount
);
}
#[tokio::test]
async fn test_withdraw_community_tokens_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_token_destination = Pubkey::new_unique();
let mut instruction = withdraw_governing_tokens(
&realm_cookie.address,
&hacker_token_destination,
&voter_record_cookie.token_owner.pubkey(),
&realm_cookie.account.community_mint,
);
instruction.accounts[3] =
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::GoverningTokenOwnerMustSign.into());
}
#[tokio::test]
async fn test_withdraw_community_tokens_with_voter_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
.with_initial_community_token_deposit(&realm_cookie)
.await;
let vote_record_address = get_voter_record_address(
&realm_cookie.address,
&realm_cookie.account.community_mint,
&voter_record_cookie.token_owner.pubkey(),
);
let hacker_record_cookie = governance_test
.with_initial_community_token_deposit(&realm_cookie)
.await;
let mut instruction = withdraw_governing_tokens(
&realm_cookie.address,
&hacker_record_cookie.token_source,
&hacker_record_cookie.token_owner.pubkey(),
&realm_cookie.account.community_mint,
);
instruction.accounts[4] = AccountMeta::new(vote_record_address, false);
// Act
let err = governance_test
.process_transaction(&[instruction], Some(&[&hacker_record_cookie.token_owner]))
.await
.err()
.unwrap();
// Assert
assert_eq!(err, GovernanceError::InvalidVoterAccountAddress.into());
}

View File

@ -0,0 +1,33 @@
use solana_program::pubkey::Pubkey;
use solana_sdk::signature::Keypair;
use spl_governance::state::{realm::Realm, voter_record::VoterRecord};
#[derive(Debug)]
pub struct RealmCookie {
pub address: Pubkey,
pub account: Realm,
pub community_mint_authority: Keypair,
pub community_token_holding_account: Pubkey,
pub council_mint_authority: Option<Keypair>,
pub council_token_holding_account: Option<Pubkey>,
}
#[derive(Debug)]
pub struct VoterRecordCookie {
pub address: Pubkey,
pub account: VoterRecord,
pub token_source: Pubkey,
pub token_source_amount: u64,
pub token_owner: Keypair,
pub vote_authority: Keypair,
}

View File

@ -0,0 +1,633 @@
use borsh::BorshDeserialize;
use solana_program::{
borsh::try_from_slice_unchecked,
instruction::Instruction,
program_error::ProgramError,
program_pack::{IsInitialized, Pack},
pubkey::Pubkey,
rent::Rent,
system_instruction,
};
use solana_program_test::ProgramTest;
use solana_program_test::*;
use solana_sdk::{
signature::{Keypair, Signer},
transaction::Transaction,
};
use spl_governance::{
instruction::{
create_realm, deposit_governing_tokens, set_vote_authority, withdraw_governing_tokens,
},
processor::process_instruction,
state::{
enums::{GovernanceAccountType, GoverningTokenType},
realm::{get_governing_token_holding_address, get_realm_address, Realm},
voter_record::{get_voter_record_address, VoterRecord},
},
};
pub mod cookies;
use self::cookies::{RealmCookie, VoterRecordCookie};
pub mod tools;
use self::tools::map_transaction_error;
pub struct GovernanceProgramTest {
pub banks_client: BanksClient,
pub payer: Keypair,
pub rent: Rent,
}
impl GovernanceProgramTest {
pub async fn start_new() -> Self {
let program_test = ProgramTest::new(
"spl_governance",
spl_governance::id(),
processor!(process_instruction),
);
let (mut banks_client, payer, _) = program_test.start().await;
let rent = banks_client.get_rent().await.unwrap();
Self {
banks_client,
payer,
rent,
}
}
pub async fn process_transaction(
&mut self,
instructions: &[Instruction],
signers: Option<&[&Keypair]>,
) -> Result<(), ProgramError> {
let mut transaction =
Transaction::new_with_payer(&instructions, Some(&self.payer.pubkey()));
let mut all_signers = vec![&self.payer];
if let Some(signers) = signers {
all_signers.extend_from_slice(signers);
}
let recent_blockhash = self.banks_client.get_recent_blockhash().await.unwrap();
transaction.sign(&all_signers, recent_blockhash);
self.banks_client
.process_transaction(transaction)
.await
.map_err(map_transaction_error)
}
pub async fn get_account<T: BorshDeserialize>(&mut self, address: &Pubkey) -> T {
let raw_account = self
.banks_client
.get_account(*address)
.await
.unwrap()
.expect("GET-TEST-ACCOUNT-ERROR: Account not found");
try_from_slice_unchecked(&raw_account.data).unwrap()
}
#[allow(dead_code)]
pub async fn with_realm(&mut self) -> RealmCookie {
let name = "Realm".to_string();
let realm_address = get_realm_address(&name);
let community_token_mint_keypair = Keypair::new();
let community_token_mint_authority = Keypair::new();
let community_token_holding_address = get_governing_token_holding_address(
&realm_address,
&community_token_mint_keypair.pubkey(),
);
self.create_mint(
&community_token_mint_keypair,
&community_token_mint_authority.pubkey(),
)
.await;
let council_token_mint_keypair = Keypair::new();
let council_token_mint_authority = Keypair::new();
let council_token_holding_address = get_governing_token_holding_address(
&realm_address,
&council_token_mint_keypair.pubkey(),
);
self.create_mint(
&council_token_mint_keypair,
&council_token_mint_authority.pubkey(),
)
.await;
let create_proposal_instruction = create_realm(
&community_token_mint_keypair.pubkey(),
&self.payer.pubkey(),
Some(council_token_mint_keypair.pubkey()),
name.clone(),
);
self.process_transaction(&[create_proposal_instruction], None)
.await
.unwrap();
let account = Realm {
account_type: GovernanceAccountType::Realm,
community_mint: community_token_mint_keypair.pubkey(),
council_mint: Some(council_token_mint_keypair.pubkey()),
name,
};
RealmCookie {
address: realm_address,
account,
community_mint_authority: community_token_mint_authority,
community_token_holding_account: community_token_holding_address,
council_token_holding_account: Some(council_token_holding_address),
council_mint_authority: Some(council_token_mint_authority),
}
}
#[allow(dead_code)]
pub async fn with_initial_community_token_deposit(
&mut self,
realm_cookie: &RealmCookie,
) -> VoterRecordCookie {
self.with_initial_governing_token_deposit(
&realm_cookie.address,
GoverningTokenType::Community,
&realm_cookie.account.community_mint,
&realm_cookie.community_mint_authority,
)
.await
}
#[allow(dead_code)]
pub async fn with_community_token_deposit(
&mut self,
realm_cookie: &RealmCookie,
voter_record_cookie: &VoterRecordCookie,
amount: u64,
) {
self.with_governing_token_deposit(
&realm_cookie.address,
&realm_cookie.account.community_mint,
&realm_cookie.community_mint_authority,
voter_record_cookie,
amount,
)
.await;
}
#[allow(dead_code)]
pub async fn with_council_token_deposit(
&mut self,
realm_cookie: &RealmCookie,
voter_record_cookie: &VoterRecordCookie,
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,
amount,
)
.await;
}
#[allow(dead_code)]
pub async fn with_initial_council_token_deposit(
&mut self,
realm_cookie: &RealmCookie,
) -> VoterRecordCookie {
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(),
)
.await
}
#[allow(dead_code)]
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 {
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,
&token_owner,
&transfer_authority.pubkey(),
)
.await;
let deposit_governing_tokens_instruction = deposit_governing_tokens(
realm_address,
&token_source.pubkey(),
&token_owner.pubkey(),
&token_owner.pubkey(),
&self.payer.pubkey(),
governing_mint,
);
self.process_transaction(
&[deposit_governing_tokens_instruction],
Some(&[&token_owner]),
)
.await
.unwrap();
let voter_record_address =
get_voter_record_address(realm_address, &governing_mint, &token_owner.pubkey());
let account = VoterRecord {
account_type: GovernanceAccountType::VoterRecord,
realm: *realm_address,
token_type: governing_token_type,
token_owner: token_owner.pubkey(),
token_deposit_amount: source_amount,
vote_authority: None,
active_votes_count: 0,
total_votes_count: 0,
};
let vote_authority = Keypair::from_base58_string(&token_owner.to_base58_string());
VoterRecordCookie {
address: voter_record_address,
account,
token_source_amount: source_amount,
token_source: token_source.pubkey(),
token_owner,
vote_authority,
}
}
#[allow(dead_code)]
async fn with_governing_token_deposit(
&mut self,
realm: &Pubkey,
governing_token_mint: &Pubkey,
governing_token_mint_authority: &Keypair,
voter_record_cookie: &VoterRecordCookie,
amount: u64,
) {
self.mint_tokens(
governing_token_mint,
governing_token_mint_authority,
&voter_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(),
&self.payer.pubkey(),
governing_token_mint,
);
self.process_transaction(
&[deposit_governing_tokens_instruction],
Some(&[&voter_record_cookie.token_owner]),
)
.await
.unwrap();
}
#[allow(dead_code)]
pub async fn with_community_vote_authority(
&mut self,
realm_cookie: &RealmCookie,
voter_record_cookie: &mut VoterRecordCookie,
) {
self.with_governing_token_vote_authority(
&realm_cookie,
&realm_cookie.account.community_mint,
voter_record_cookie,
)
.await;
}
#[allow(dead_code)]
pub async fn with_council_vote_authority(
&mut self,
realm_cookie: &RealmCookie,
voter_record_cookie: &mut VoterRecordCookie,
) {
self.with_governing_token_vote_authority(
&realm_cookie,
&realm_cookie.account.council_mint.unwrap(),
voter_record_cookie,
)
.await;
}
#[allow(dead_code)]
pub async fn with_governing_token_vote_authority(
&mut self,
realm_cookie: &RealmCookie,
governing_token_mint: &Pubkey,
voter_record_cookie: &mut VoterRecordCookie,
) {
let new_vote_authority = Keypair::new();
self.set_vote_authority(
realm_cookie,
voter_record_cookie,
&voter_record_cookie.token_owner,
governing_token_mint,
&Some(new_vote_authority.pubkey()),
)
.await;
voter_record_cookie.vote_authority = new_vote_authority;
}
#[allow(dead_code)]
pub async fn set_vote_authority(
&mut self,
realm_cookie: &RealmCookie,
voter_record_cookie: &VoterRecordCookie,
signing_vote_authority: &Keypair,
governing_token_mint: &Pubkey,
new_vote_authority: &Option<Pubkey>,
) {
let set_vote_authority_instruction = set_vote_authority(
&signing_vote_authority.pubkey(),
&realm_cookie.address,
governing_token_mint,
&voter_record_cookie.token_owner.pubkey(),
new_vote_authority,
);
self.process_transaction(
&[set_vote_authority_instruction],
Some(&[&signing_vote_authority]),
)
.await
.unwrap();
}
#[allow(dead_code)]
pub async fn withdraw_community_tokens(
&mut self,
realm_cookie: &RealmCookie,
voter_record_cookie: &VoterRecordCookie,
) -> Result<(), ProgramError> {
self.withdraw_governing_tokens(
realm_cookie,
voter_record_cookie,
&realm_cookie.account.community_mint,
&voter_record_cookie.token_owner,
)
.await
}
#[allow(dead_code)]
pub async fn withdraw_council_tokens(
&mut self,
realm_cookie: &RealmCookie,
voter_record_cookie: &VoterRecordCookie,
) -> Result<(), ProgramError> {
self.withdraw_governing_tokens(
realm_cookie,
voter_record_cookie,
&realm_cookie.account.council_mint.unwrap(),
&voter_record_cookie.token_owner,
)
.await
}
#[allow(dead_code)]
async fn withdraw_governing_tokens(
&mut self,
realm_cookie: &RealmCookie,
voter_record_cookie: &VoterRecordCookie,
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,
&governing_token_owner.pubkey(),
governing_token_mint,
);
self.process_transaction(
&[deposit_governing_tokens_instruction],
Some(&[&governing_token_owner]),
)
.await
}
#[allow(dead_code)]
pub async fn get_voter_record_account(&mut self, address: &Pubkey) -> VoterRecord {
self.get_account::<VoterRecord>(address).await
}
#[allow(dead_code)]
pub async fn get_realm_account(&mut self, root_governance_address: &Pubkey) -> Realm {
self.get_account::<Realm>(root_governance_address).await
}
#[allow(dead_code)]
async fn get_packed_account<T: Pack + IsInitialized>(&mut self, address: &Pubkey) -> T {
let raw_account = self
.banks_client
.get_account(*address)
.await
.unwrap()
.unwrap();
T::unpack(&raw_account.data).unwrap()
}
#[allow(dead_code)]
pub async fn get_token_account(&mut self, address: &Pubkey) -> spl_token::state::Account {
self.get_packed_account(address).await
}
pub async fn create_mint(&mut self, mint_keypair: &Keypair, mint_authority: &Pubkey) {
let mint_rent = self.rent.minimum_balance(spl_token::state::Mint::LEN);
let instructions = [
system_instruction::create_account(
&self.payer.pubkey(),
&mint_keypair.pubkey(),
mint_rent,
spl_token::state::Mint::LEN as u64,
&spl_token::id(),
),
spl_token::instruction::initialize_mint(
&spl_token::id(),
&mint_keypair.pubkey(),
&mint_authority,
None,
0,
)
.unwrap(),
];
self.process_transaction(&instructions, Some(&[&mint_keypair]))
.await
.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,
token_account_keypair: &Keypair,
token_mint: &Pubkey,
token_mint_authority: &Keypair,
amount: u64,
owner: &Keypair,
transfer_authority: &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.pubkey(),
)
.unwrap();
let mint_instruction = spl_token::instruction::mint_to(
&spl_token::id(),
token_mint,
&token_account_keypair.pubkey(),
&token_mint_authority.pubkey(),
&[],
amount,
)
.unwrap();
let approve_instruction = spl_token::instruction::approve(
&spl_token::id(),
&token_account_keypair.pubkey(),
transfer_authority,
&owner.pubkey(),
&[],
amount,
)
.unwrap();
self.process_transaction(
&[
create_account_instruction,
initialize_account_instruction,
mint_instruction,
approve_instruction,
],
Some(&[&token_account_keypair, &token_mint_authority, &owner]),
)
.await
.unwrap();
}
pub async fn mint_tokens(
&mut self,
token_mint: &Pubkey,
token_mint_authority: &Keypair,
token_account: &Pubkey,
amount: u64,
) {
let mint_instruction = spl_token::instruction::mint_to(
&spl_token::id(),
&token_mint,
&token_account,
&token_mint_authority.pubkey(),
&[],
amount,
)
.unwrap();
self.process_transaction(&[mint_instruction], Some(&[&token_mint_authority]))
.await
.unwrap();
}
}

View File

@ -0,0 +1,12 @@
use solana_program::{instruction::InstructionError, program_error::ProgramError};
use solana_sdk::{transaction::TransactionError, transport::TransportError};
pub fn map_transaction_error(transport_error: TransportError) -> ProgramError {
match transport_error {
TransportError::TransactionError(TransactionError::InstructionError(
_,
InstructionError::Custom(error_index),
)) => ProgramError::Custom(error_index),
_ => panic!("TEST-ERROR: {:?}", transport_error),
}
}