From 24bb1c81589f62db6d1b8ab90b5fb89f9e8d86ea Mon Sep 17 00:00:00 2001 From: Sebastian Bor Date: Mon, 3 Jan 2022 22:30:34 +0000 Subject: [PATCH] Governance: Native Treasuries (#2692) * feat: Implement CreateNativeTreasury instruction * chore: fix fmt * chore: make clippy happy * chore: remove account check * feat: sign with treasury account seeds * chore: comment out unused code * feat: Use system as the treasury account owner * feat: Assert valid governance account * chore: Make clippy happy * fix: Use explicit system program id * feat: add treasury seeds only when required by the instruction * chore: review celanup * chore: make clippy happy * chore: rename create_and_serialize_account_signed2 Co-authored-by: Jon Cinque * chore: update create_and_serialize_account_signed2 name * fix: remove NativeTreasury from account enums * chore: update treasury seeds prefix Co-authored-by: Jon Cinque --- Cargo.lock | 79 ++++------- governance/program/src/instruction.rs | 35 +++++ governance/program/src/processor/mod.rs | 5 + .../process_create_native_treasury.rs | 48 +++++++ .../processor/process_execute_instruction.rs | 26 +++- governance/program/src/state/governance.rs | 19 ++- governance/program/src/state/mod.rs | 1 + .../program/src/state/native_treasury.rs | 26 ++++ .../tests/process_create_native_treasury.rs | 127 ++++++++++++++++++ .../program/tests/program_test/cookies.rs | 10 +- governance/program/tests/program_test/mod.rs | 75 ++++++++++- governance/test-sdk/src/cookies.rs | 7 + governance/test-sdk/src/lib.rs | 34 ++++- governance/tools/src/account.rs | 41 +++++- 14 files changed, 459 insertions(+), 74 deletions(-) create mode 100644 governance/program/src/processor/process_create_native_treasury.rs create mode 100644 governance/program/src/state/native_treasury.rs create mode 100644 governance/program/tests/process_create_native_treasury.rs diff --git a/Cargo.lock b/Cargo.lock index de2eb6f7..bfdd73a8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -607,16 +607,6 @@ dependencies = [ "subtle", ] -[[package]] -name = "ctor" -version = "0.1.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccc0a48a9b826acdf4028595adc9db92caea352f7af011a3034acd172a52a0aa" -dependencies = [ - "quote 1.0.14", - "syn 1.0.84", -] - [[package]] name = "curve25519-dalek" version = "3.2.0" @@ -1091,17 +1081,6 @@ dependencies = [ "wasi 0.10.0+wasi-snapshot-preview1", ] -[[package]] -name = "ghost" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a5bcf1bbeab73aa4cf2fde60a846858dc036163c7c33bec309f8d17de785479" -dependencies = [ - "proc-macro2 1.0.36", - "quote 1.0.14", - "syn 1.0.84", -] - [[package]] name = "gimli" version = "0.26.1" @@ -1391,28 +1370,6 @@ dependencies = [ "cfg-if", ] -[[package]] -name = "inventory" -version = "0.1.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0eb5160c60ba1e809707918ee329adb99d222888155835c6feedba19f6c3fd4" -dependencies = [ - "ctor", - "ghost", - "inventory-impl", -] - -[[package]] -name = "inventory-impl" -version = "0.1.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e41b53715c6f0c4be49510bb82dee2c1e51c8586d885abe65396e82ed518548" -dependencies = [ - "proc-macro2 1.0.36", - "quote 1.0.14", - "syn 1.0.84", -] - [[package]] name = "ipnet" version = "2.3.1" @@ -2051,38 +2008,48 @@ dependencies = [ [[package]] name = "pyo3" -version = "0.12.4" +version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf6bbbe8f70d179260b3728e5d04eb012f4f0c7988e58c11433dd689cecaa72e" +checksum = "7cf01dbf1c05af0a14c7779ed6f3aa9deac9c3419606ac9de537a2d649005720" dependencies = [ - "ctor", + "cfg-if", "indoc", - "inventory", "libc", "parking_lot", "paste", - "pyo3cls", + "pyo3-build-config", + "pyo3-macros", "unindent", ] [[package]] -name = "pyo3-derive-backend" -version = "0.12.4" +name = "pyo3-build-config" +version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10ecd0eb6ed7b3d9965b4f4370b5b9e99e3e5e8742000e1c452c018f8c2a322f" +checksum = "dbf9e4d128bfbddc898ad3409900080d8d5095c379632fbbfbb9c8cfb1fb852b" dependencies = [ - "proc-macro2 1.0.36", + "once_cell", +] + +[[package]] +name = "pyo3-macros" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67701eb32b1f9a9722b4bc54b548ff9d7ebfded011c12daece7b9063be1fd755" +dependencies = [ + "pyo3-macros-backend", "quote 1.0.14", "syn 1.0.84", ] [[package]] -name = "pyo3cls" -version = "0.12.4" +name = "pyo3-macros-backend" +version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d344fdaa6a834a06dd1720ff104ea12fe101dad2e8db89345af9db74c0bb11a0" +checksum = "f44f09e825ee49a105f2c7b23ebee50886a9aee0746f4dd5a704138a64b0218a" dependencies = [ - "pyo3-derive-backend", + "proc-macro2 1.0.36", + "pyo3-build-config", "quote 1.0.14", "syn 1.0.84", ] diff --git a/governance/program/src/instruction.rs b/governance/program/src/instruction.rs index 978835a6..22e3dee7 100644 --- a/governance/program/src/instruction.rs +++ b/governance/program/src/instruction.rs @@ -7,6 +7,7 @@ use crate::{ get_account_governance_address, get_mint_governance_address, get_program_governance_address, get_token_governance_address, GovernanceConfig, }, + native_treasury::get_native_treasury_address, program_metadata::get_program_metadata_address, proposal::{get_proposal_address, VoteType}, proposal_instruction::{get_proposal_instruction_address, InstructionData}, @@ -452,6 +453,15 @@ pub enum GovernanceInstruction { /// 1. `[signer]` Payer /// 2. `[]` System UpdateProgramMetadata {}, + + /// Creates native SOL treasury account for a Governance account + /// The account has no data and can be used as a payer for instructions signed by Governance PDAs or as a native SOL treasury + /// + /// 0. `[]` Governance account the treasury account is for + /// 1. `[writable]` NativeTreasury account. PDA seeds: ['treasury', governance] + /// 2. `[signer]` Payer + /// 3. `[]` System + CreateNativeTreasury, } /// Creates CreateRealm instruction @@ -1412,3 +1422,28 @@ pub fn upgrade_program_metadata( data: instruction.try_to_vec().unwrap(), } } + +/// Creates CreateNativeTreasury instruction +pub fn create_native_treasury( + program_id: &Pubkey, + // Accounts + governance: &Pubkey, + payer: &Pubkey, +) -> Instruction { + let native_treasury_address = get_native_treasury_address(program_id, governance); + + let accounts = vec![ + AccountMeta::new_readonly(*governance, false), + AccountMeta::new(native_treasury_address, false), + AccountMeta::new(*payer, true), + AccountMeta::new_readonly(system_program::id(), false), + ]; + + let instruction = GovernanceInstruction::CreateNativeTreasury {}; + + Instruction { + program_id: *program_id, + accounts, + data: instruction.try_to_vec().unwrap(), + } +} diff --git a/governance/program/src/processor/mod.rs b/governance/program/src/processor/mod.rs index a0708b13..4c864ddf 100644 --- a/governance/program/src/processor/mod.rs +++ b/governance/program/src/processor/mod.rs @@ -5,6 +5,7 @@ mod process_cancel_proposal; mod process_cast_vote; mod process_create_account_governance; mod process_create_mint_governance; +mod process_create_native_treasury; mod process_create_program_governance; mod process_create_proposal; mod process_create_realm; @@ -33,6 +34,7 @@ use process_cancel_proposal::*; use process_cast_vote::*; use process_create_account_governance::*; use process_create_mint_governance::*; +use process_create_native_treasury::*; use process_create_program_governance::*; use process_create_proposal::*; use process_create_realm::*; @@ -200,5 +202,8 @@ pub fn process_instruction( GovernanceInstruction::UpdateProgramMetadata {} => { process_update_program_metadata(program_id, accounts) } + GovernanceInstruction::CreateNativeTreasury {} => { + process_create_native_treasury(program_id, accounts) + } } } diff --git a/governance/program/src/processor/process_create_native_treasury.rs b/governance/program/src/processor/process_create_native_treasury.rs new file mode 100644 index 00000000..46cdc45c --- /dev/null +++ b/governance/program/src/processor/process_create_native_treasury.rs @@ -0,0 +1,48 @@ +//! Program state processor + +use solana_program::{ + account_info::{next_account_info, AccountInfo}, + entrypoint::ProgramResult, + pubkey::Pubkey, + rent::Rent, + system_program, + sysvar::Sysvar, +}; +use spl_governance_tools::account::create_and_serialize_account_with_owner_signed; + +use crate::state::{ + governance::assert_is_valid_governance, + native_treasury::{get_native_treasury_address_seeds, NativeTreasury}, +}; + +/// Processes CreateNativeTreasury instruction +pub fn process_create_native_treasury( + program_id: &Pubkey, + accounts: &[AccountInfo], +) -> ProgramResult { + let account_info_iter = &mut accounts.iter(); + + let governance_info = next_account_info(account_info_iter)?; // 0 + let native_treasury_info = next_account_info(account_info_iter)?; // 1 + let payer_info = next_account_info(account_info_iter)?; // 2 + let system_info = next_account_info(account_info_iter)?; // 3 + + let rent = Rent::get()?; + + assert_is_valid_governance(program_id, governance_info)?; + + let native_treasury_data = NativeTreasury {}; + + create_and_serialize_account_with_owner_signed( + payer_info, + native_treasury_info, + &native_treasury_data, + &get_native_treasury_address_seeds(governance_info.key), + program_id, + &system_program::id(), // System program as the PDA owner + system_info, + &rent, + )?; + + Ok(()) +} diff --git a/governance/program/src/processor/process_execute_instruction.rs b/governance/program/src/processor/process_execute_instruction.rs index 414e216b..47284f53 100644 --- a/governance/program/src/processor/process_execute_instruction.rs +++ b/governance/program/src/processor/process_execute_instruction.rs @@ -13,6 +13,7 @@ use solana_program::{ use crate::state::{ enums::{InstructionExecutionStatus, ProposalState}, governance::get_governance_data, + native_treasury::get_native_treasury_address_seeds, proposal::{get_proposal_data_for_governance, OptionVoteResult}, proposal_instruction::get_proposal_instruction_data_for_proposal, }; @@ -47,16 +48,31 @@ pub fn process_execute_instruction(program_id: &Pubkey, accounts: &[AccountInfo] let instruction_account_infos = account_info_iter.as_slice(); + let mut signers_seeds: Vec<&[&[u8]]> = vec![]; + + // Sign the transaction using the governance PDA let mut governance_seeds = governance_data.get_governance_address_seeds()?.to_vec(); let (_, bump_seed) = Pubkey::find_program_address(&governance_seeds, program_id); let bump = &[bump_seed]; governance_seeds.push(bump); - invoke_signed( - &instruction, - instruction_account_infos, - &[&governance_seeds[..]], - )?; + signers_seeds.push(&governance_seeds[..]); + + // Sign the transaction using the governance treasury PDA if required by the instruction + let mut treasury_seeds = get_native_treasury_address_seeds(governance_info.key).to_vec(); + let (treasury_address, treasury_bump_seed) = + Pubkey::find_program_address(&treasury_seeds, program_id); + let treasury_bump = &[treasury_bump_seed]; + + if instruction_account_infos + .iter() + .any(|a| a.key == &treasury_address) + { + treasury_seeds.push(treasury_bump); + signers_seeds.push(&treasury_seeds[..]); + } + + invoke_signed(&instruction, instruction_account_infos, &signers_seeds[..])?; // Update proposal and instruction accounts if proposal_data.state == ProposalState::Succeeded { diff --git a/governance/program/src/state/governance.rs b/governance/program/src/state/governance.rs index 875da18e..5db3adf5 100644 --- a/governance/program/src/state/governance.rs +++ b/governance/program/src/state/governance.rs @@ -13,7 +13,7 @@ use solana_program::{ pubkey::Pubkey, }; use spl_governance_tools::{ - account::{get_account_data, AccountMaxSize}, + account::{assert_is_valid_account2, get_account_data, AccountMaxSize}, error::GovernanceToolsError, }; @@ -225,6 +225,23 @@ pub fn get_account_governance_address<'a>( .0 } +/// Checks whether governance account exists, is initialized and owned by the Governance program +pub fn assert_is_valid_governance( + program_id: &Pubkey, + governance_info: &AccountInfo, +) -> Result<(), ProgramError> { + assert_is_valid_account2( + governance_info, + &[ + GovernanceAccountType::AccountGovernance, + GovernanceAccountType::ProgramGovernance, + GovernanceAccountType::TokenGovernance, + GovernanceAccountType::MintGovernance, + ], + program_id, + ) +} + /// Validates args supplied to create governance account pub fn assert_valid_create_governance_args( program_id: &Pubkey, diff --git a/governance/program/src/state/mod.rs b/governance/program/src/state/mod.rs index 93d5d92d..79ff01b2 100644 --- a/governance/program/src/state/mod.rs +++ b/governance/program/src/state/mod.rs @@ -3,6 +3,7 @@ pub mod enums; pub mod governance; pub mod legacy; +pub mod native_treasury; pub mod program_metadata; pub mod proposal; pub mod proposal_instruction; diff --git a/governance/program/src/state/native_treasury.rs b/governance/program/src/state/native_treasury.rs new file mode 100644 index 00000000..ec313d76 --- /dev/null +++ b/governance/program/src/state/native_treasury.rs @@ -0,0 +1,26 @@ +//! Native treasury account + +use borsh::{BorshDeserialize, BorshSchema, BorshSerialize}; +use solana_program::pubkey::Pubkey; +use spl_governance_tools::account::AccountMaxSize; + +/// Treasury account +/// The account has no data and can be used as a payer for instruction signed by Governance PDAs or as a native SOL treasury +#[derive(Clone, Debug, PartialEq, BorshDeserialize, BorshSerialize, BorshSchema)] +pub struct NativeTreasury {} + +impl AccountMaxSize for NativeTreasury { + fn get_max_size(&self) -> Option { + Some(0) + } +} + +/// Returns NativeTreasury PDA seeds +pub fn get_native_treasury_address_seeds(governance: &Pubkey) -> [&[u8]; 2] { + [b"native-treasury", governance.as_ref()] +} + +/// Returns NativeTreasury PDA address +pub fn get_native_treasury_address(program_id: &Pubkey, governance: &Pubkey) -> Pubkey { + Pubkey::find_program_address(&get_native_treasury_address_seeds(governance), program_id).0 +} diff --git a/governance/program/tests/process_create_native_treasury.rs b/governance/program/tests/process_create_native_treasury.rs new file mode 100644 index 00000000..fb38a13a --- /dev/null +++ b/governance/program/tests/process_create_native_treasury.rs @@ -0,0 +1,127 @@ +#![cfg(feature = "test-bpf")] + +use solana_program_test::*; + +mod program_test; + +use program_test::*; + +#[tokio::test] +async fn test_create_native_treasury() { + // Arrange + let mut governance_test = GovernanceProgramTest::start_new().await; + + let realm_cookie = governance_test.with_realm().await; + let governed_account_cookie = governance_test.with_governed_account().await; + + let token_owner_record_cookie = governance_test + .with_community_token_deposit(&realm_cookie) + .await + .unwrap(); + + let account_governance_cookie = governance_test + .with_account_governance( + &realm_cookie, + &governed_account_cookie, + &token_owner_record_cookie, + ) + .await + .unwrap(); + + // Act + let native_treasury_cookie = governance_test + .with_native_treasury(&account_governance_cookie) + .await; + + // Assert + + let native_treasury_account = governance_test + .get_native_treasury_account(&native_treasury_cookie.address) + .await; + + assert_eq!(native_treasury_cookie.account, native_treasury_account); +} + +#[tokio::test] +async fn test_execute_transfer_from_native_treasury() { + // Arrange + let mut governance_test = GovernanceProgramTest::start_new().await; + + let realm_cookie = governance_test.with_realm().await; + let governed_account_cookie = governance_test.with_governed_account().await; + + let token_owner_record_cookie = governance_test + .with_community_token_deposit(&realm_cookie) + .await + .unwrap(); + + let mut account_governance_cookie = governance_test + .with_account_governance( + &realm_cookie, + &governed_account_cookie, + &token_owner_record_cookie, + ) + .await + .unwrap(); + + governance_test + .with_native_treasury(&account_governance_cookie) + .await; + + let mut proposal_cookie = governance_test + .with_proposal(&token_owner_record_cookie, &mut account_governance_cookie) + .await + .unwrap(); + + let signatory_record_cookie = governance_test + .with_signatory(&proposal_cookie, &token_owner_record_cookie) + .await + .unwrap(); + + let wallet_cookie = governance_test.bench.with_wallet().await; + let transfer_amount = 100; + + let proposal_instruction_cookie = governance_test + .with_native_transfer_instruction( + &account_governance_cookie, + &mut proposal_cookie, + &token_owner_record_cookie, + &wallet_cookie, + transfer_amount, + ) + .await + .unwrap(); + + governance_test + .sign_off_proposal(&proposal_cookie, &signatory_record_cookie) + .await + .unwrap(); + + governance_test + .with_cast_vote(&proposal_cookie, &token_owner_record_cookie, YesNoVote::Yes) + .await + .unwrap(); + + // Advance timestamp past hold_up_time + governance_test + .advance_clock_by_min_timespan(proposal_instruction_cookie.account.hold_up_time as u64) + .await; + + // Act + governance_test + .execute_instruction(&proposal_cookie, &proposal_instruction_cookie) + .await + .unwrap(); + + // Assert + let wallet_account = governance_test + .bench + .get_account(&wallet_cookie.address) + .await + .unwrap(); + + assert_eq!( + wallet_account.lamports, + wallet_cookie.account.lamports + transfer_amount + ) +} diff --git a/governance/program/tests/program_test/cookies.rs b/governance/program/tests/program_test/cookies.rs index 44fd22b7..4d8299d4 100644 --- a/governance/program/tests/program_test/cookies.rs +++ b/governance/program/tests/program_test/cookies.rs @@ -3,8 +3,8 @@ use solana_sdk::signature::Keypair; use spl_governance::{ addins::voter_weight::VoterWeightRecord, state::{ - governance::Governance, program_metadata::ProgramMetadata, proposal::ProposalV2, - proposal_instruction::ProposalInstructionV2, realm::Realm, + governance::Governance, native_treasury::NativeTreasury, program_metadata::ProgramMetadata, + proposal::ProposalV2, proposal_instruction::ProposalInstructionV2, realm::Realm, realm_config::RealmConfigAccount, signatory_record::SignatoryRecord, token_owner_record::TokenOwnerRecord, vote_record::VoteRecordV2, }, @@ -171,3 +171,9 @@ pub struct ProgramMetadataCookie { pub address: Pubkey, pub account: ProgramMetadata, } + +#[derive(Debug, Clone)] +pub struct NativeTreasuryCookie { + pub address: Pubkey, + pub account: NativeTreasury, +} diff --git a/governance/program/tests/program_test/mod.rs b/governance/program/tests/program_test/mod.rs index b0d50ba3..6d54a309 100644 --- a/governance/program/tests/program_test/mod.rs +++ b/governance/program/tests/program_test/mod.rs @@ -7,7 +7,7 @@ use solana_program::{ program_error::ProgramError, program_pack::{IsInitialized, Pack}, pubkey::Pubkey, - system_program, + system_instruction, system_program, }; use solana_program_test::*; @@ -18,8 +18,8 @@ use spl_governance::{ addins::voter_weight::{VoterWeightAccountType, VoterWeightRecord}, instruction::{ add_signatory, cancel_proposal, cast_vote, create_account_governance, - create_mint_governance, create_program_governance, create_proposal, create_realm, - create_token_governance, create_token_owner_record, deposit_governing_tokens, + create_mint_governance, create_native_treasury, create_program_governance, create_proposal, + create_realm, create_token_governance, create_token_owner_record, deposit_governing_tokens, execute_instruction, finalize_vote, flag_instruction_error, insert_instruction, relinquish_vote, remove_instruction, remove_signatory, set_governance_config, set_governance_delegate, set_realm_authority, set_realm_config, sign_off_proposal, @@ -36,6 +36,7 @@ use spl_governance::{ get_program_governance_address, get_token_governance_address, Governance, GovernanceConfig, }, + native_treasury::{get_native_treasury_address, NativeTreasury}, program_metadata::{get_program_metadata_address, ProgramMetadata}, proposal::{get_proposal_address, OptionVoteResult, ProposalOption, ProposalV2, VoteType}, proposal_instruction::{ @@ -61,6 +62,7 @@ use crate::program_test::cookies::{ }; use spl_governance_test_sdk::{ + cookies::WalletCookie, tools::{clone_keypair, NopOverride}, ProgramTestBench, }; @@ -69,8 +71,8 @@ use self::{ addins::ensure_voter_weight_addin_is_built, cookies::{ GovernanceCookie, GovernedAccountCookie, GovernedMintCookie, GovernedProgramCookie, - GovernedTokenCookie, ProgramMetadataCookie, ProposalCookie, ProposalInstructionCookie, - RealmCookie, TokenOwnerRecordCookie, VoteRecordCookie, + GovernedTokenCookie, NativeTreasuryCookie, ProgramMetadataCookie, ProposalCookie, + ProposalInstructionCookie, RealmCookie, TokenOwnerRecordCookie, VoteRecordCookie, }, }; @@ -454,6 +456,37 @@ impl GovernanceProgramTest { } } + #[allow(dead_code)] + pub async fn with_native_treasury( + &mut self, + governance_cookie: &GovernanceCookie, + ) -> NativeTreasuryCookie { + let create_treasury_ix = create_native_treasury( + &self.program_id, + &governance_cookie.address, + &self.bench.payer.pubkey(), + ); + + let treasury_address = + get_native_treasury_address(&self.program_id, &governance_cookie.address); + + let transfer_ix = system_instruction::transfer( + &self.bench.payer.pubkey(), + &treasury_address, + 1_000_000_000, + ); + + self.bench + .process_transaction(&[create_treasury_ix, transfer_ix], None) + .await + .unwrap(); + + NativeTreasuryCookie { + address: treasury_address, + account: NativeTreasury {}, + } + } + #[allow(dead_code)] pub async fn with_community_token_deposit_amount( &mut self, @@ -1992,6 +2025,31 @@ impl GovernanceProgramTest { .await } + #[allow(dead_code)] + pub async fn with_native_transfer_instruction( + &mut self, + governance_cookie: &GovernanceCookie, + proposal_cookie: &mut ProposalCookie, + token_owner_record_cookie: &TokenOwnerRecordCookie, + to_wallet_cookie: &WalletCookie, + lamports: u64, + ) -> Result { + let treasury_address = + get_native_treasury_address(&self.program_id, &governance_cookie.address); + + let mut transfer_ix = + system_instruction::transfer(&treasury_address, &to_wallet_cookie.address, lamports); + + self.with_instruction( + proposal_cookie, + token_owner_record_cookie, + 0, + None, + &mut transfer_ix, + ) + .await + } + #[allow(dead_code)] pub async fn with_upgrade_program_instruction( &mut self, @@ -2245,6 +2303,13 @@ impl GovernanceProgramTest { .await } + #[allow(dead_code)] + pub async fn get_native_treasury_account(&mut self, address: &Pubkey) -> NativeTreasury { + self.bench + .get_borsh_account::(address) + .await + } + #[allow(dead_code)] pub async fn get_realm_account(&mut self, realm_address: &Pubkey) -> Realm { self.bench.get_borsh_account::(realm_address).await diff --git a/governance/test-sdk/src/cookies.rs b/governance/test-sdk/src/cookies.rs index 5f33c19f..2f47c625 100644 --- a/governance/test-sdk/src/cookies.rs +++ b/governance/test-sdk/src/cookies.rs @@ -1,6 +1,13 @@ use solana_program::pubkey::Pubkey; +use solana_sdk::account::Account; #[derive(Debug)] pub struct TokenAccountCookie { pub address: Pubkey, } + +#[derive(Debug)] +pub struct WalletCookie { + pub address: Pubkey, + pub account: Account, +} diff --git a/governance/test-sdk/src/lib.rs b/governance/test-sdk/src/lib.rs index b552e048..7b7c51fd 100644 --- a/governance/test-sdk/src/lib.rs +++ b/governance/test-sdk/src/lib.rs @@ -1,11 +1,11 @@ use std::borrow::Borrow; use borsh::BorshDeserialize; -use cookies::TokenAccountCookie; +use cookies::{TokenAccountCookie, WalletCookie}; use solana_program::{ borsh::try_from_slice_unchecked, clock::Clock, instruction::Instruction, program_error::ProgramError, program_pack::Pack, pubkey::Pubkey, rent::Rent, - system_instruction, sysvar, + system_instruction, system_program, sysvar, }; use solana_program_test::{ProgramTest, ProgramTestContext}; use solana_sdk::{account::Account, signature::Keypair, signer::Signer, transaction::Transaction}; @@ -82,6 +82,36 @@ impl ProgramTestBench { Ok(()) } + pub async fn with_wallet(&mut self) -> WalletCookie { + let account_rent = self.rent.minimum_balance(0); + let account_keypair = Keypair::new(); + + let create_account_ix = system_instruction::create_account( + &self.context.payer.pubkey(), + &account_keypair.pubkey(), + account_rent, + 0, + &system_program::id(), + ); + + self.process_transaction(&[create_account_ix], Some(&[&account_keypair])) + .await + .unwrap(); + + let account = Account { + lamports: account_rent, + data: vec![], + owner: system_program::id(), + executable: false, + rent_epoch: 0, + }; + + WalletCookie { + address: account_keypair.pubkey(), + account, + } + } + 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); diff --git a/governance/tools/src/account.rs b/governance/tools/src/account.rs index 526d6704..3d8d0642 100644 --- a/governance/tools/src/account.rs +++ b/governance/tools/src/account.rs @@ -70,6 +70,7 @@ pub fn create_and_serialize_account<'a, T: BorshSerialize + AccountMaxSize>( } /// Creates a new account and serializes data into it using the provided seeds to invoke signed CPI call +/// The owner of the account is set to the PDA program /// 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>, @@ -79,6 +80,31 @@ pub fn create_and_serialize_account_signed<'a, T: BorshSerialize + AccountMaxSiz program_id: &Pubkey, system_info: &AccountInfo<'a>, rent: &Rent, +) -> Result<(), ProgramError> { + create_and_serialize_account_with_owner_signed( + payer_info, + account_info, + account_data, + account_address_seeds, + program_id, + program_id, // By default use PDA program_id as the owner of the account + system_info, + rent, + ) +} + +/// 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 +#[allow(clippy::too_many_arguments)] +pub fn create_and_serialize_account_with_owner_signed<'a, T: BorshSerialize + AccountMaxSize>( + payer_info: &AccountInfo<'a>, + account_info: &AccountInfo<'a>, + account_data: &T, + account_address_seeds: &[&[u8]], + program_id: &Pubkey, + owner_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) = @@ -106,7 +132,7 @@ pub fn create_and_serialize_account_signed<'a, T: BorshSerialize + AccountMaxSiz account_info.key, rent.minimum_balance(account_size), account_size as u64, - program_id, + owner_program_id, ); let mut signers_seeds = account_address_seeds.to_vec(); @@ -128,7 +154,7 @@ pub fn create_and_serialize_account_signed<'a, T: BorshSerialize + AccountMaxSiz .data .borrow_mut() .copy_from_slice(&serialized_data); - } else { + } else if account_size > 0 { account_data.serialize(&mut *account_info.data.borrow_mut())?; } @@ -161,6 +187,15 @@ pub fn assert_is_valid_account( account_info: &AccountInfo, expected_account_type: T, owner_program_id: &Pubkey, +) -> Result<(), ProgramError> { + assert_is_valid_account2(account_info, &[expected_account_type], owner_program_id) +} +/// Asserts the given account is not empty, owned by the given program and one of the expected types +/// Note: The function assumes the account type T is stored as the first element in the account data +pub fn assert_is_valid_account2( + account_info: &AccountInfo, + expected_account_types: &[T], + owner_program_id: &Pubkey, ) -> Result<(), ProgramError> { if account_info.owner != owner_program_id { return Err(GovernanceToolsError::InvalidAccountOwner.into()); @@ -172,7 +207,7 @@ pub fn assert_is_valid_account( let account_type: T = try_from_slice_unchecked(&account_info.data.borrow())?; - if account_type != expected_account_type { + if expected_account_types.iter().all(|a| a != &account_type) { return Err(GovernanceToolsError::InvalidAccountType.into()); };