From addc6bf4b44b909fb145d04c97e1316afe45c99c Mon Sep 17 00:00:00 2001 From: Sebastian Bor Date: Wed, 19 May 2021 12:20:53 +0100 Subject: [PATCH] Governance: Implement Realms Implemented instructions: - CreateRealm - DepositGoverningTokens - WithdrawGoverningTokens -SetVoteAuthority Co-authored-by: Jon Cinque Co-authored-by: Michael Vines --- Cargo.lock | 6 + governance/program/Cargo.toml | 13 +- governance/program/src/error.rs | 42 +- governance/program/src/instruction.rs | 199 +++++- governance/program/src/lib.rs | 6 +- governance/program/src/processor.rs | 14 - governance/program/src/processor/mod.rs | 59 ++ .../src/processor/process_create_realm.rs | 97 +++ .../process_deposit_governing_tokens.rs | 118 ++++ .../processor/process_set_vote_authority.rs | 39 ++ .../process_withdraw_governing_tokens.rs | 67 ++ governance/program/src/state/enums.rs | 10 +- .../program/src/state/program_governance.rs | 3 +- governance/program/src/state/proposal.rs | 3 +- .../program/src/state/proposal_vote_record.rs | 3 +- governance/program/src/state/realm.rs | 60 +- .../src/state/single_signer_instruction.rs | 12 +- governance/program/src/state/voter_record.rs | 95 ++- governance/program/src/tools/account.rs | 101 +++ governance/program/src/tools/asserts.rs | 25 + governance/program/src/tools/mod.rs | 7 + governance/program/src/tools/token.rs | 211 ++++++ .../program/tests/process_create_realm.rs | 23 + .../tests/process_deposit_governing_tokens.rs | 234 +++++++ .../tests/process_set_vote_authority.rs | 165 +++++ .../process_withdraw_governing_tokens.rs | 167 +++++ .../program/tests/program_test/cookies.rs | 33 + governance/program/tests/program_test/mod.rs | 633 ++++++++++++++++++ .../program/tests/program_test/tools.rs | 12 + 29 files changed, 2407 insertions(+), 50 deletions(-) delete mode 100644 governance/program/src/processor.rs create mode 100644 governance/program/src/processor/mod.rs create mode 100644 governance/program/src/processor/process_create_realm.rs create mode 100644 governance/program/src/processor/process_deposit_governing_tokens.rs create mode 100644 governance/program/src/processor/process_set_vote_authority.rs create mode 100644 governance/program/src/processor/process_withdraw_governing_tokens.rs create mode 100644 governance/program/src/tools/account.rs create mode 100644 governance/program/src/tools/asserts.rs create mode 100644 governance/program/src/tools/mod.rs create mode 100644 governance/program/src/tools/token.rs create mode 100644 governance/program/tests/process_create_realm.rs create mode 100644 governance/program/tests/process_deposit_governing_tokens.rs create mode 100644 governance/program/tests/process_set_vote_authority.rs create mode 100644 governance/program/tests/process_withdraw_governing_tokens.rs create mode 100644 governance/program/tests/program_test/cookies.rs create mode 100644 governance/program/tests/program_test/mod.rs create mode 100644 governance/program/tests/program_test/tools.rs diff --git a/Cargo.lock b/Cargo.lock index 94dce5c3..c7feaf55 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3813,12 +3813,18 @@ dependencies = [ name = "spl-governance" version = "0.1.0" dependencies = [ + "arrayref", "assert_matches", + "bincode", + "borsh 0.8.2", "num-derive", "num-traits", + "serde", + "serde_derive", "solana-program", "solana-program-test", "solana-sdk", + "spl-token 3.1.0", "thiserror", ] diff --git a/governance/program/Cargo.toml b/governance/program/Cargo.toml index c4500dcb..96183e65 100644 --- a/governance/program/Cargo.toml +++ b/governance/program/Cargo.toml @@ -7,23 +7,26 @@ repository = "https://github.com/solana-labs/solana-program-library" license = "Apache-2.0" edition = "2018" - [features] no-entrypoint = [] test-bpf = [] - [dependencies] -solana-program = "1.6.7" -thiserror = "1.0" +arrayref = "0.3.6" +bincode = "1.3.2" +borsh = "0.8.1" num-derive = "0.3" 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] assert_matches = "1.5.0" solana-program-test = "1.6.7" solana-sdk = "1.6.7" - [lib] crate-type = ["cdylib", "lib"] diff --git a/governance/program/src/error.rs b/governance/program/src/error.rs index 497ad0df..d32b7eda 100644 --- a/governance/program/src/error.rs +++ b/governance/program/src/error.rs @@ -8,17 +8,53 @@ use solana_program::{ }; 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)] pub enum GovernanceError { - /// Invalid instruction passed to program. + /// Invalid instruction passed to program #[error("Invalid instruction passed to program")] 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 { fn print(&self) { - msg!(&self.to_string()); + msg!("GOVERNANCE-ERROR: {}", &self.to_string()); } } diff --git a/governance/program/src/instruction.rs b/governance/program/src/instruction.rs index 28dbce7d..b7eeec97 100644 --- a/governance/program/src/instruction.rs +++ b/governance/program/src/instruction.rs @@ -1,12 +1,24 @@ //! Program instructions -use solana_program::{instruction::Instruction, pubkey::Pubkey}; - -use crate::state::enums::GoverningTokenType; +use crate::{ + id, + 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 #[repr(C)] -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug, PartialEq, BorshDeserialize, BorshSerialize, BorshSchema)] pub enum Vote { /// Yes vote Yes, @@ -15,7 +27,7 @@ pub enum Vote { } /// Instructions supported by the Governance program -#[derive(Clone)] +#[derive(Clone, Debug, PartialEq, BorshDeserialize, BorshSerialize, BorshSchema)] #[repr(C)] #[allow(clippy::large_enum_variant)] pub enum GovernanceInstruction { @@ -33,6 +45,7 @@ pub enum GovernanceInstruction { /// 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 CreateRealm { + #[allow(dead_code)] /// UTF-8 encoded Governance Realm name name: String, }, @@ -45,10 +58,11 @@ pub enum GovernanceInstruction { /// 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 /// 3. `[signer]` Governing Token Owner account - /// 4. `[writable]` Voter Record account. PDA seeds: ['governance',realm, governing_token_mint, governing_token_owner] - /// 5. `[signer]` Payer - /// 6. `[]` System - /// 7. `[]` SPL Token + /// 4. `[signer]` Governing Token Transfer authority + /// 5. `[writable]` Voter Record account. PDA seeds: ['governance',realm, governing_token_mint, governing_token_owner] + /// 6. `[signer]` Payer + /// 7. `[]` System + /// 8. `[]` SPL Token DepositGoverningTokens {}, /// 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) /// 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 SetVoteAuthority { #[allow(dead_code)] @@ -77,9 +92,13 @@ pub enum GovernanceInstruction { /// 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 - vote_authority: Pubkey, + new_vote_authority: Option, }, /// Creates Program Governance account which governs an upgradable program @@ -93,16 +112,20 @@ pub enum GovernanceInstruction { /// 6. `[]` System account /// 7. `[]` Bpf_upgrade_loader account CreateProgramGovernance { + #[allow(dead_code)] /// 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 vote_threshold: u8, + #[allow(dead_code)] /// Minimum waiting time in slots for an instruction to be executed after proposal is voted on min_instruction_hold_up_time: u64, + #[allow(dead_code)] /// Time limit in slots for proposal to be open for voting max_voting_time: u64, + #[allow(dead_code)] /// 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 token_threshold_to_create_proposal: u8, @@ -120,12 +143,15 @@ pub enum GovernanceInstruction { /// 6. '[]` Token program account /// 7. `[]` Rent 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, }, @@ -158,12 +184,15 @@ pub enum GovernanceInstruction { /// 1. `[writable]` Uninitialized Proposal SingleSignerInstruction account /// 2. `[signer]` Admin account AddSingleSignerInstruction { + #[allow(dead_code)] /// Slot waiting time between vote period ending and this being eligible for execution hold_up_time: u64, + #[allow(dead_code)] /// Instruction - instruction: Instruction, + instruction: InstructionData, + #[allow(dead_code)] /// Position in instruction array position: u8, }, @@ -183,6 +212,7 @@ pub enum GovernanceInstruction { /// 1. `[writable]` Proposal SingleSignerInstruction account /// 2. `[signer]` Admin account UpdateInstructionHoldUpTime { + #[allow(dead_code)] /// Minimum waiting time in slots for an instruction to be executed after proposal is voted on hold_up_time: u64, }, @@ -215,6 +245,7 @@ pub enum GovernanceInstruction { /// 3. `[signer]` Vote Authority account /// 4. `[]` Governance account Vote { + #[allow(dead_code)] /// Yes/No vote vote: Vote, }, @@ -243,3 +274,147 @@ pub enum GovernanceInstruction { /// 5+ Any extra accounts that are part of the instruction, in order Execute, } + +/// Creates CreateRealm instruction +pub fn create_realm( + // Accounts + community_token_mint: &Pubkey, + payer: &Pubkey, + council_token_mint: Option, + // 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, +) -> 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(), + } +} diff --git a/governance/program/src/lib.rs b/governance/program/src/lib.rs index f251b1c5..664756c0 100644 --- a/governance/program/src/lib.rs +++ b/governance/program/src/lib.rs @@ -6,8 +6,12 @@ pub mod error; pub mod instruction; pub mod processor; pub mod state; +pub mod tools; // Export current sdk types for downstream users building with a different sdk version 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"; diff --git a/governance/program/src/processor.rs b/governance/program/src/processor.rs deleted file mode 100644 index da750da6..00000000 --- a/governance/program/src/processor.rs +++ /dev/null @@ -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()) -} diff --git a/governance/program/src/processor/mod.rs b/governance/program/src/processor/mod.rs new file mode 100644 index 00000000..dd6dc0dd --- /dev/null +++ b/governance/program/src/processor/mod.rs @@ -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"), + } +} diff --git a/governance/program/src/processor/process_create_realm.rs b/governance/program/src/processor/process_create_realm.rs new file mode 100644 index 00000000..0a92d088 --- /dev/null +++ b/governance/program/src/processor/process_create_realm.rs @@ -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::( + payer_info, + &realm_info, + &realm_data, + &get_realm_address_seeds(&name), + program_id, + system_info, + rent, + )?; + + Ok(()) +} diff --git a/governance/program/src/processor/process_deposit_governing_tokens.rs b/governance/program/src/processor/process_deposit_governing_tokens.rs new file mode 100644 index 00000000..ec3398eb --- /dev/null +++ b/governance/program/src/processor/process_deposit_governing_tokens.rs @@ -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(()) +} diff --git a/governance/program/src/processor/process_set_vote_authority.rs b/governance/program/src/processor/process_set_vote_authority.rs new file mode 100644 index 00000000..6962b434 --- /dev/null +++ b/governance/program/src/processor/process_set_vote_authority.rs @@ -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, +) -> 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(()) +} diff --git a/governance/program/src/processor/process_withdraw_governing_tokens.rs b/governance/program/src/processor/process_withdraw_governing_tokens.rs new file mode 100644 index 00000000..d6f08ac7 --- /dev/null +++ b/governance/program/src/processor/process_withdraw_governing_tokens.rs @@ -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(()) +} diff --git a/governance/program/src/state/enums.rs b/governance/program/src/state/enums.rs index 2bae1ff1..04d75e6f 100644 --- a/governance/program/src/state/enums.rs +++ b/governance/program/src/state/enums.rs @@ -1,8 +1,10 @@ //! State enumerations +use borsh::{BorshDeserialize, BorshSchema, BorshSerialize}; + /// Defines all Governance accounts types #[repr(C)] -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug, PartialEq, BorshDeserialize, BorshSerialize, BorshSchema)] pub enum GovernanceAccountType { /// Default uninitialized account state Uninitialized, @@ -34,7 +36,7 @@ impl Default for GovernanceAccountType { /// Vote with number of votes #[repr(C)] -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug, PartialEq, BorshDeserialize, BorshSerialize, BorshSchema)] pub enum VoteWeight { /// Yes vote Yes(u64), @@ -45,7 +47,7 @@ pub enum VoteWeight { /// Governing Token type #[repr(C)] -#[derive(Clone)] +#[derive(Clone, Debug, PartialEq, BorshDeserialize, BorshSerialize, BorshSchema)] pub enum GoverningTokenType { /// Community token Community, @@ -55,7 +57,7 @@ pub enum GoverningTokenType { /// What state a Proposal is in #[repr(C)] -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug, PartialEq, BorshDeserialize, BorshSerialize, BorshSchema)] pub enum ProposalState { /// Draft - Proposal enters Draft state when it's created Draft, diff --git a/governance/program/src/state/program_governance.rs b/governance/program/src/state/program_governance.rs index fb1d4580..f74765ec 100644 --- a/governance/program/src/state/program_governance.rs +++ b/governance/program/src/state/program_governance.rs @@ -1,12 +1,13 @@ //! Program Governance Account +use borsh::{BorshDeserialize, BorshSchema, BorshSerialize}; use solana_program::pubkey::Pubkey; use super::enums::GovernanceAccountType; /// Program Governance Account #[repr(C)] -#[derive(Clone, Debug, Default, PartialEq)] +#[derive(Clone, Debug, PartialEq, BorshDeserialize, BorshSerialize, BorshSchema)] pub struct ProgramGovernance { /// Account type pub account_type: GovernanceAccountType, diff --git a/governance/program/src/state/proposal.rs b/governance/program/src/state/proposal.rs index df0ce1f9..296cbf28 100644 --- a/governance/program/src/state/proposal.rs +++ b/governance/program/src/state/proposal.rs @@ -3,10 +3,11 @@ use solana_program::{epoch_schedule::Slot, pubkey::Pubkey}; use super::enums::{GovernanceAccountType, GoverningTokenType, ProposalState}; +use borsh::{BorshDeserialize, BorshSchema, BorshSerialize}; /// Governance Proposal #[repr(C)] -#[derive(Clone)] +#[derive(Clone, Debug, PartialEq, BorshDeserialize, BorshSerialize, BorshSchema)] pub struct Proposal { /// Governance account type pub account_type: GovernanceAccountType, diff --git a/governance/program/src/state/proposal_vote_record.rs b/governance/program/src/state/proposal_vote_record.rs index 412ac570..b7f069c6 100644 --- a/governance/program/src/state/proposal_vote_record.rs +++ b/governance/program/src/state/proposal_vote_record.rs @@ -1,12 +1,13 @@ //! Proposal Vote Record Account +use borsh::{BorshDeserialize, BorshSchema, BorshSerialize}; use solana_program::pubkey::Pubkey; use super::enums::{GovernanceAccountType, VoteWeight}; /// Proposal Vote Record #[repr(C)] -#[derive(Clone, Debug, Default, PartialEq)] +#[derive(Clone, Debug, PartialEq, BorshDeserialize, BorshSerialize, BorshSchema)] pub struct ProposalVoteRecord { /// Governance account type pub account_type: GovernanceAccountType, diff --git a/governance/program/src/state/realm.rs b/governance/program/src/state/realm.rs index 34b972c7..4a9b25c9 100644 --- a/governance/program/src/state/realm.rs +++ b/governance/program/src/state/realm.rs @@ -1,12 +1,23 @@ //! 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; /// Governance Realm Account /// Account PDA seeds" ['governance', name] #[repr(C)] +#[derive(Clone, Debug, PartialEq, BorshDeserialize, BorshSerialize, BorshSchema)] pub struct Realm { /// Governance account type pub account_type: GovernanceAccountType, @@ -20,3 +31,50 @@ pub struct Realm { /// Governance Realm name 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 { + deserialize_account::(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 +} diff --git a/governance/program/src/state/single_signer_instruction.rs b/governance/program/src/state/single_signer_instruction.rs index 883b2132..c12fdc90 100644 --- a/governance/program/src/state/single_signer_instruction.rs +++ b/governance/program/src/state/single_signer_instruction.rs @@ -1,12 +1,11 @@ //! SingleSignerInstruction Account -use solana_program::instruction::Instruction; - use super::enums::GovernanceAccountType; +use borsh::{BorshDeserialize, BorshSchema, BorshSerialize}; /// Account for an instruction to be executed for Proposal #[repr(C)] -#[derive(Clone)] +#[derive(Clone, Debug, PartialEq, BorshDeserialize, BorshSerialize, BorshSchema)] pub struct SingleSignerInstruction { /// Governance Account type pub account_type: GovernanceAccountType, @@ -17,8 +16,13 @@ pub struct SingleSignerInstruction { /// Instruction to execute /// 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 - pub instruction: Instruction, + pub instruction: InstructionData, /// Executed flag 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 {} diff --git a/governance/program/src/state/voter_record.rs b/governance/program/src/state/voter_record.rs index 70fb04fa..27c46608 100644 --- a/governance/program/src/state/voter_record.rs +++ b/governance/program/src/state/voter_record.rs @@ -1,12 +1,23 @@ //! 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 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, @@ -26,8 +37,8 @@ pub struct VoterRecord { 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 token owner - pub vote_authority: Pubkey, + /// It's delegated to by the governing token owner or current vote_authority + pub vote_authority: Option, /// The number of active votes cast by voter pub active_votes_count: u8, @@ -35,3 +46,81 @@ pub struct VoterRecord { /// The total number of votes cast by the voter pub total_votes_count: u8, } + +impl AccountMaxSize for VoterRecord { + fn get_max_size(&self) -> Option { + 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 { + 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::(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::(); + + assert_eq!(vote_record.get_max_size(), Some(size)); + } +} diff --git a/governance/program/src/tools/account.rs b/governance/program/src/tools/account.rs new file mode 100644 index 00000000..658950b9 --- /dev/null +++ b/governance/program/src/tools/account.rs @@ -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 { + 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( + account_info: &AccountInfo, + owner_program_id: &Pubkey, +) -> Result { + 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) + } +} diff --git a/governance/program/src/tools/asserts.rs b/governance/program/src/tools/asserts.rs new file mode 100644 index 00000000..5d2623f3 --- /dev/null +++ b/governance/program/src/tools/asserts.rs @@ -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()) +} diff --git a/governance/program/src/tools/mod.rs b/governance/program/src/tools/mod.rs new file mode 100644 index 00000000..3865f382 --- /dev/null +++ b/governance/program/src/tools/mod.rs @@ -0,0 +1,7 @@ +//! Utility functions + +pub mod account; + +pub mod token; + +pub mod asserts; diff --git a/governance/program/src/tools/token.rs b/governance/program/src/tools/token.rs new file mode 100644 index 00000000..55f8b0a5 --- /dev/null +++ b/governance/program/src/tools/token.rs @@ -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 { + 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 { + 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 { + 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)) +} diff --git a/governance/program/tests/process_create_realm.rs b/governance/program/tests/process_create_realm.rs new file mode 100644 index 00000000..23c73312 --- /dev/null +++ b/governance/program/tests/process_create_realm.rs @@ -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); +} diff --git a/governance/program/tests/process_deposit_governing_tokens.rs b/governance/program/tests/process_deposit_governing_tokens.rs new file mode 100644 index 00000000..92744294 --- /dev/null +++ b/governance/program/tests/process_deposit_governing_tokens.rs @@ -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()); +} diff --git a/governance/program/tests/process_set_vote_authority.rs b/governance/program/tests/process_set_vote_authority.rs new file mode 100644 index 00000000..477ccc5a --- /dev/null +++ b/governance/program/tests/process_set_vote_authority.rs @@ -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 + ); +} diff --git a/governance/program/tests/process_withdraw_governing_tokens.rs b/governance/program/tests/process_withdraw_governing_tokens.rs new file mode 100644 index 00000000..b4185773 --- /dev/null +++ b/governance/program/tests/process_withdraw_governing_tokens.rs @@ -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()); +} diff --git a/governance/program/tests/program_test/cookies.rs b/governance/program/tests/program_test/cookies.rs new file mode 100644 index 00000000..1810101a --- /dev/null +++ b/governance/program/tests/program_test/cookies.rs @@ -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, + + pub council_token_holding_account: Option, +} + +#[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, +} diff --git a/governance/program/tests/program_test/mod.rs b/governance/program/tests/program_test/mod.rs new file mode 100644 index 00000000..09ba0adf --- /dev/null +++ b/governance/program/tests/program_test/mod.rs @@ -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(&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, + ) { + 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::(address).await + } + + #[allow(dead_code)] + pub async fn get_realm_account(&mut self, root_governance_address: &Pubkey) -> Realm { + self.get_account::(root_governance_address).await + } + + #[allow(dead_code)] + async fn get_packed_account(&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(); + } +} diff --git a/governance/program/tests/program_test/tools.rs b/governance/program/tests/program_test/tools.rs new file mode 100644 index 00000000..a1c98439 --- /dev/null +++ b/governance/program/tests/program_test/tools.rs @@ -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), + } +}