From c99f4195f1295afad96e3af5cb1199554150b7b0 Mon Sep 17 00:00:00 2001 From: Sebastian Bor Date: Wed, 13 Oct 2021 13:15:22 +0100 Subject: [PATCH] Governance: Voters weights add-in (#2450) * feat: setup and configure voter weight addin feat: add use_voter_weight_add_in flag to realm config chore: use spl-governance 1.1.1 version chore: make clippy happy chore: add test to deserialise v1 CreateRealm instruction from v2 feat: add voter-weight-addin skeleton project chore: build voter-weight-addin before governance fix: temp workaround to make spl_governance_voter_weight_addin available in CI chore: add tests with voter-weight-addin feat: implement deposit instruction for voter weight addin feat: add voter_weight_expiry fix: set voter_weight_expiry chore: restore positive execute tests chore: restore ignored tests wip: pass voter weight accounts to create_account_governance2 wip: read voter weight account chore: make clippy happy wip: add realm and validation to voter_weight deposit fix: update addin chore: make clippy happy chore: fix voter_weight_record names feat: use voter weight provided by addin when governance created chore: update addin chore: remove governance stake pool program feat: remove time offset from revise chore: fix build feat: create RealmAddins account when realm with addin is created chore: make clippy happy feat: set voter weight addin using SetRealmConfig instruction chore: make clippy happy chore: update comments chore: reorder SetrealmConfig accounts chore: infer use_community_voter_weight_addin chore: infer use_community_voter_weight_addin chore: update voter weight addin comments feat: use voter weight addin id from RealmAddins account * feat: use voter weight addin to create proposal * feat: use voter weight addin to cast vote * chore: make clippy happy * feat: use voter weight addin to create token governance * feat: use voter weight addin to create mint governance * feat: use voter weight adding to create program governance * chore: create assert_can_withdraw_governing_tokens() helper function * chore: fix compilation * fix: ensure governance authority signed transaction to create governance * feat: implement CreateTokenOwnerRecord instruction * chore: fix chat tests * chore: update comments * chore: rename RealmAddins account to RealmConfig account * chore: add more reserved space to GovernanceConfig account * chore: update instruction comments * chore: update comments * chore: fix compilation * chore: remove ignore directive for tests * feat: panic when depositing tokens into a realm with voter weight addin * chore: rename community_voter_weight to community_voter_weight_addin * feat: make payer account optional for SetRealmConfig --- Cargo.lock | 45 ++- Cargo.toml | 1 + governance/chat/program/Cargo.toml | 2 +- .../chat/program/tests/program_test/mod.rs | 8 +- governance/program/Cargo.toml | 4 +- governance/program/src/addins/mod.rs | 2 + governance/program/src/addins/voter_weight.rs | 110 ++++++ governance/program/src/error.rs | 28 ++ governance/program/src/instruction.rs | 165 +++++++- governance/program/src/lib.rs | 1 + governance/program/src/processor/mod.rs | 5 + .../src/processor/process_cast_vote.rs | 15 +- .../process_create_account_governance.rs | 13 +- .../process_create_mint_governance.rs | 13 +- .../process_create_program_governance.rs | 13 +- .../src/processor/process_create_proposal.rs | 13 +- .../src/processor/process_create_realm.rs | 33 +- .../process_create_token_governance.rs | 13 +- .../process_create_token_owner_record.rs | 69 ++++ .../process_deposit_governing_tokens.rs | 2 + .../src/processor/process_set_realm_config.rs | 71 +++- .../process_withdraw_governing_tokens.rs | 8 +- governance/program/src/state/enums.rs | 3 + governance/program/src/state/mod.rs | 1 + governance/program/src/state/proposal.rs | 3 +- governance/program/src/state/realm.rs | 102 ++++- governance/program/src/state/realm_config.rs | 108 ++++++ .../program/src/state/token_owner_record.rs | 76 +++- .../spl_governance_voter_weight_addin.so | Bin 0 -> 131496 bytes .../program/tests/process_add_signatory.rs | 15 +- .../program/tests/process_cancel_proposal.rs | 12 +- governance/program/tests/process_cast_vote.rs | 54 ++- .../process_create_account_governance.rs | 15 +- .../tests/process_create_mint_governance.rs | 20 +- .../process_create_program_governance.rs | 20 +- .../program/tests/process_create_proposal.rs | 45 ++- .../program/tests/process_create_realm.rs | 1 + .../tests/process_create_token_governance.rs | 20 +- .../process_create_token_owner_record.rs | 29 ++ .../tests/process_deposit_governing_tokens.rs | 15 +- .../tests/process_execute_instruction.rs | 33 +- .../program/tests/process_finalize_vote.rs | 12 +- .../tests/process_flag_instruction_error.rs | 18 +- .../tests/process_insert_instruction.rs | 27 +- .../program/tests/process_relinquish_vote.rs | 27 +- .../tests/process_remove_instruction.rs | 24 +- .../program/tests/process_remove_signatory.rs | 21 +- .../tests/process_set_governance_config.rs | 6 +- .../tests/process_set_governance_delegate.rs | 15 +- .../program/tests/process_set_realm_config.rs | 7 + .../tests/process_sign_off_proposal.rs | 6 +- .../process_withdraw_governing_tokens.rs | 33 +- .../program/tests/program_test/cookies.rs | 27 +- governance/program/tests/program_test/mod.rs | 354 ++++++++++++++++-- .../setup_realm_with_voter_weight_addin.rs | 217 +++++++++++ .../use_realm_with_voter_weight_addin.rs | 261 +++++++++++++ governance/voter-weight-addin/README.md | 3 + .../voter-weight-addin/program/Cargo.toml | 39 ++ .../voter-weight-addin/program/Xargo.toml | 2 + .../program/src/entrypoint.rs | 22 ++ .../voter-weight-addin/program/src/error.rs | 31 ++ .../program/src/instruction.rs | 42 +++ .../voter-weight-addin/program/src/lib.rs | 12 + .../program/src/processor.rs | 84 +++++ 64 files changed, 2259 insertions(+), 237 deletions(-) create mode 100644 governance/program/src/addins/mod.rs create mode 100644 governance/program/src/addins/voter_weight.rs create mode 100644 governance/program/src/processor/process_create_token_owner_record.rs create mode 100644 governance/program/src/state/realm_config.rs create mode 100755 governance/program/tests/fixtures/spl_governance_voter_weight_addin.so create mode 100644 governance/program/tests/process_create_token_owner_record.rs create mode 100644 governance/program/tests/setup_realm_with_voter_weight_addin.rs create mode 100644 governance/program/tests/use_realm_with_voter_weight_addin.rs create mode 100644 governance/voter-weight-addin/README.md create mode 100644 governance/voter-weight-addin/program/Cargo.toml create mode 100644 governance/voter-weight-addin/program/Xargo.toml create mode 100644 governance/voter-weight-addin/program/src/entrypoint.rs create mode 100644 governance/voter-weight-addin/program/src/error.rs create mode 100644 governance/voter-weight-addin/program/src/instruction.rs create mode 100644 governance/voter-weight-addin/program/src/lib.rs create mode 100644 governance/voter-weight-addin/program/src/processor.rs diff --git a/Cargo.lock b/Cargo.lock index 6ed5ad421a1..263f1f209bc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3657,6 +3657,24 @@ dependencies = [ [[package]] name = "spl-governance" version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ae3df1aa25c5f5ce7a6595959b1b02c6ae6ea35274379ee64bcf80bae11a767" +dependencies = [ + "arrayref", + "bincode", + "borsh", + "num-derive", + "num-traits", + "serde", + "serde_derive", + "solana-program", + "spl-token 3.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "thiserror", +] + +[[package]] +name = "spl-governance" +version = "2.1.1" dependencies = [ "arrayref", "assert_matches", @@ -3671,6 +3689,7 @@ dependencies = [ "solana-program", "solana-program-test", "solana-sdk", + "spl-governance 1.1.1", "spl-governance-test-sdk", "spl-token 3.2.0", "thiserror", @@ -3693,7 +3712,7 @@ dependencies = [ "solana-program", "solana-program-test", "solana-sdk", - "spl-governance", + "spl-governance 2.1.1", "spl-governance-test-sdk", "spl-token 3.2.0", "thiserror", @@ -3717,6 +3736,30 @@ dependencies = [ "thiserror", ] +[[package]] +name = "spl-governance-voter-weight-addin" +version = "0.1.0" +dependencies = [ + "arrayref", + "assert_matches", + "base64 0.13.0", + "bincode", + "borsh", + "num-derive", + "num-traits", + "proptest", + "serde", + "serde_derive", + "solana-program", + "solana-program-test", + "solana-sdk", + "spl-governance 2.1.1", + "spl-governance-chat", + "spl-governance-test-sdk", + "spl-token 3.2.0", + "thiserror", +] + [[package]] name = "spl-math" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 919dd29e345..ad852ae1198 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,6 +10,7 @@ members = [ "examples/rust/transfer-lamports", "feature-proposal/program", "feature-proposal/cli", + "governance/voter-weight-addin/program", "governance/program", "governance/test-sdk", "governance/chat/program", diff --git a/governance/chat/program/Cargo.toml b/governance/chat/program/Cargo.toml index f311306fad2..705dcb7dd57 100644 --- a/governance/chat/program/Cargo.toml +++ b/governance/chat/program/Cargo.toml @@ -21,7 +21,7 @@ serde = "1.0.127" serde_derive = "1.0.103" solana-program = "1.8.0" spl-token = { version = "3.2", path = "../../../token/program", features = [ "no-entrypoint" ] } -spl-governance= { version = "1.1.0", path ="../../program", features = [ "no-entrypoint" ]} +spl-governance= { version = "2.1.0", path ="../../program", features = [ "no-entrypoint" ]} thiserror = "1.0" diff --git a/governance/chat/program/tests/program_test/mod.rs b/governance/chat/program/tests/program_test/mod.rs index 4808b299d8f..1d656df79d2 100644 --- a/governance/chat/program/tests/program_test/mod.rs +++ b/governance/chat/program/tests/program_test/mod.rs @@ -87,6 +87,7 @@ impl GovernanceChatProgramTest { &governing_token_mint_keypair.pubkey(), &self.bench.payer.pubkey(), None, + None, name.clone(), 1, MintMaxVoteWeightSource::FULL_SUPPLY_FRACTION, @@ -155,11 +156,13 @@ impl GovernanceChatProgramTest { &governed_account_address, &token_owner_record_address, &self.bench.payer.pubkey(), + &token_owner.pubkey(), + None, governance_config, ); self.bench - .process_transaction(&[create_account_governance_ix], None) + .process_transaction(&[create_account_governance_ix], Some(&[&token_owner])) .await .unwrap(); @@ -173,7 +176,7 @@ impl GovernanceChatProgramTest { let proposal_name = "Proposal #1".to_string(); let description_link = "Proposal Description".to_string(); - let proposal_index = 0; + let proposal_index: u32 = 0; let create_proposal_ix = create_proposal( &self.governance_program_id, @@ -181,6 +184,7 @@ impl GovernanceChatProgramTest { &token_owner_record_address, &token_owner.pubkey(), &self.bench.payer.pubkey(), + None, &realm_address, proposal_name, description_link.clone(), diff --git a/governance/program/Cargo.toml b/governance/program/Cargo.toml index 6f5c3620a9a..1fc8c17bb85 100644 --- a/governance/program/Cargo.toml +++ b/governance/program/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "spl-governance" -version = "1.1.1" +version = "2.1.1" description = "Solana Program Library Governance Program" authors = ["Solana Maintainers "] repository = "https://github.com/solana-labs/solana-program-library" @@ -30,6 +30,8 @@ proptest = "1.0" solana-program-test = "1.8.0" solana-sdk = "1.8.0" spl-governance-test-sdk = { version = "0.1.0", path ="../test-sdk"} +spl-governance-v1 = {package="spl-governance", version = "1.1.1", features = [ "no-entrypoint" ] } + [lib] crate-type = ["cdylib", "lib"] diff --git a/governance/program/src/addins/mod.rs b/governance/program/src/addins/mod.rs new file mode 100644 index 00000000000..a485c577583 --- /dev/null +++ b/governance/program/src/addins/mod.rs @@ -0,0 +1,2 @@ +//! Governance add-ins interfaces +pub mod voter_weight; diff --git a/governance/program/src/addins/voter_weight.rs b/governance/program/src/addins/voter_weight.rs new file mode 100644 index 00000000000..2ef864b37ee --- /dev/null +++ b/governance/program/src/addins/voter_weight.rs @@ -0,0 +1,110 @@ +//! VoterWeight Addin interface + +use borsh::{BorshDeserialize, BorshSchema, BorshSerialize}; +use solana_program::{ + account_info::AccountInfo, + clock::{Clock, Slot}, + program_error::ProgramError, + program_pack::IsInitialized, + pubkey::Pubkey, + sysvar::Sysvar, +}; + +use crate::{ + error::GovernanceError, + state::token_owner_record::TokenOwnerRecord, + tools::account::{get_account_data, AccountMaxSize}, +}; + +/// VoterWeight account type +#[derive(Clone, Debug, PartialEq, BorshDeserialize, BorshSerialize, BorshSchema)] +pub enum VoterWeightAccountType { + /// Default uninitialized account state + Uninitialized, + + /// Voter Weight Record + VoterWeightRecord, +} + +/// VoterWeight Record account +#[derive(Clone, Debug, PartialEq, BorshDeserialize, BorshSerialize, BorshSchema)] +pub struct VoterWeightRecord { + /// VoterWeightRecord account type + pub account_type: VoterWeightAccountType, + + /// The Realm the VoterWeightRecord belongs to + pub realm: Pubkey, + + /// Governing Token Mint the VoterWeightRecord is associated with + /// Note: The addin can take deposits of any tokens and is not restricted to the community or council tokens only + // The mint here is to link the record to either community or council mint of the realm + pub governing_token_mint: Pubkey, + + /// The owner of the governing token and voter + pub governing_token_owner: Pubkey, + + /// Voter's weight + pub voter_weight: u64, + + /// The slot when the voting weight expires + /// It should be set to None if the weight never expires + /// If the voter weight decays with time, for example for time locked based weights, then the expiry must be set + /// As a common pattern Revise instruction to update the weight should be invoked before governance instruction within the same transaction + /// and the expiry set to the current slot to provide up to date weight + pub voter_weight_expiry: Option, +} + +impl AccountMaxSize for VoterWeightRecord {} + +impl IsInitialized for VoterWeightRecord { + fn is_initialized(&self) -> bool { + self.account_type == VoterWeightAccountType::VoterWeightRecord + } +} + +impl VoterWeightRecord { + /// Asserts the VoterWeightRecord hasn't expired + pub fn assert_is_up_to_date(&self) -> Result<(), ProgramError> { + if let Some(voter_weight_expiry) = self.voter_weight_expiry { + let slot = Clock::get().unwrap().slot; + + if slot > voter_weight_expiry { + return Err(GovernanceError::VoterWeightRecordExpired.into()); + } + } + + Ok(()) + } +} + +/// Deserializes VoterWeightRecord account and checks owner program +pub fn get_voter_weight_record_data( + program_id: &Pubkey, + voter_weight_record_info: &AccountInfo, +) -> Result { + get_account_data::(voter_weight_record_info, program_id) +} + +/// Deserializes VoterWeightRecord account, checks owner program and asserts it's for the same realm, mint and token owner as the provided TokenOwnerRecord +pub fn get_voter_weight_record_data_for_token_owner_record( + program_id: &Pubkey, + voter_weight_record_info: &AccountInfo, + token_owner_record: &TokenOwnerRecord, +) -> Result { + let voter_weight_record_data = + get_voter_weight_record_data(program_id, voter_weight_record_info)?; + + if voter_weight_record_data.realm != token_owner_record.realm { + return Err(GovernanceError::InvalidVoterWeightRecordForRealm.into()); + } + + if voter_weight_record_data.governing_token_mint != token_owner_record.governing_token_mint { + return Err(GovernanceError::InvalidVoterWeightRecordForGoverningTokenMint.into()); + } + + if voter_weight_record_data.governing_token_owner != token_owner_record.governing_token_owner { + return Err(GovernanceError::InvalidVoterWeightRecordForTokenOwner.into()); + } + + Ok(voter_weight_record_data) +} diff --git a/governance/program/src/error.rs b/governance/program/src/error.rs index 7c9d9a40a15..75b04b1654f 100644 --- a/governance/program/src/error.rs +++ b/governance/program/src/error.rs @@ -323,6 +323,34 @@ pub enum GovernanceError { /// All proposals must be finalized to withdraw governing tokens #[error("All proposals must be finalized to withdraw governing tokens")] AllProposalsMustBeFinalisedToWithdrawGoverningTokens, + + /// Invalid VoterWeightRecord for Realm + #[error("Invalid VoterWeightRecord for Realm")] + InvalidVoterWeightRecordForRealm, + + /// Invalid VoterWeightRecord for GoverningTokenMint + #[error("Invalid VoterWeightRecord for GoverningTokenMint")] + InvalidVoterWeightRecordForGoverningTokenMint, + + /// Invalid VoterWeightRecord for TokenOwner + #[error("Invalid VoterWeightRecord for TokenOwner")] + InvalidVoterWeightRecordForTokenOwner, + + /// VoterWeightRecord expired + #[error("VoterWeightRecord expired")] + VoterWeightRecordExpired, + + /// Invalid RealmConfig for Realm + #[error("Invalid RealmConfig for Realm")] + InvalidRealmConfigForRealm, + + /// TokenOwnerRecord already exists + #[error("TokenOwnerRecord already exists")] + TokenOwnerRecordAlreadyExists, + + /// Governing token deposits not allowed + #[error("Governing token deposits not allowed")] + GoverningTokenDepositsNotAllowed, } impl PrintProgramError for GovernanceError { diff --git a/governance/program/src/instruction.rs b/governance/program/src/instruction.rs index dcc6f3c15d5..d6bd22febec 100644 --- a/governance/program/src/instruction.rs +++ b/governance/program/src/instruction.rs @@ -10,6 +10,7 @@ use crate::{ proposal::get_proposal_address, proposal_instruction::{get_proposal_instruction_address, InstructionData}, realm::{get_governing_token_holding_address, get_realm_address, RealmConfigArgs}, + realm_config::get_realm_config_address, signatory_record::get_signatory_record_address, token_owner_record::get_token_owner_record_address, vote_record::get_vote_record_address, @@ -50,9 +51,13 @@ pub enum GovernanceInstruction { /// 5. `[]` System /// 6. `[]` SPL Token /// 7. `[]` Sysvar Rent + /// 8. `[]` Council Token Mint - optional /// 9. `[writable]` Council Token Holding account - optional unless council is used. PDA seeds: ['governance',realm,council_mint] /// The account will be created with the Realm PDA as its owner + + /// 10. `[writable]` RealmConfig account. PDA seeds: ['realm-config', realm] + /// 11. `[]` Optional Community Voter Weight Addin Program Id CreateRealm { #[allow(dead_code)] /// UTF-8 encoded Governance Realm name @@ -113,6 +118,9 @@ pub enum GovernanceInstruction { /// 4. `[signer]` Payer /// 5. `[]` System program /// 6. `[]` Sysvar Rent + /// 7. `[signer]` Governance authority + /// 8. `[]` Optional Realm Config + /// 9. `[]` Optional Voter Weight Record CreateAccountGovernance { /// Governance config #[allow(dead_code)] @@ -131,6 +139,9 @@ pub enum GovernanceInstruction { /// 7. `[]` bpf_upgradeable_loader program /// 8. `[]` System program /// 9. `[]` Sysvar Rent + /// 10. `[signer]` Governance authority + /// 11. `[]` Optional Realm Config + /// 12. `[]` Optional Voter Weight Record CreateProgramGovernance { /// Governance config #[allow(dead_code)] @@ -154,6 +165,8 @@ pub enum GovernanceInstruction { /// 6. `[]` System program /// 7. `[]` Rent sysvar /// 8. `[]` Clock sysvar + /// 9. `[]` Optional Realm Config + /// 10. `[]` Optional Voter Weight Record CreateProposal { #[allow(dead_code)] /// UTF-8 encoded name of the proposal @@ -263,6 +276,8 @@ pub enum GovernanceInstruction { /// 8. `[]` System program /// 9. `[]` Rent sysvar /// 10. `[]` Clock sysvar + /// 11. `[]` Optional Realm Config + /// 12. `[]` Optional Voter Weight Record CastVote { #[allow(dead_code)] /// Yes/No vote @@ -317,6 +332,9 @@ pub enum GovernanceInstruction { /// 6. `[]` SPL Token program /// 7. `[]` System program /// 8. `[]` Sysvar Rent + /// 8. `[signer]` Governance authority + /// 9. `[]` Optional Realm Config + /// 10. `[]` Optional Voter Weight Record CreateMintGovernance { #[allow(dead_code)] /// Governance config @@ -340,6 +358,9 @@ pub enum GovernanceInstruction { /// 6. `[]` SPL Token program /// 7. `[]` System program /// 8. `[]` Sysvar Rent + /// 9. `[signer]` Governance authority + /// 10. `[]` Optional Realm Config + /// 11. `[]` Optional Voter Weight Record CreateTokenGovernance { #[allow(dead_code)] /// Governance config @@ -393,11 +414,26 @@ pub enum GovernanceInstruction { /// If that's required then it must be done before executing this instruction /// 3. `[writable]` Council Token Holding account - optional unless council is used. PDA seeds: ['governance',realm,council_mint] /// The account will be created with the Realm PDA as its owner + /// 4. `[]` System + /// 5. `[writable]` RealmConfig account. PDA seeds: ['realm-config', realm] + /// 6. `[signer]` Optional Payer + /// 7. `[]` Optional Community Voter Weight Addin Program Id SetRealmConfig { #[allow(dead_code)] /// Realm config args config_args: RealmConfigArgs, }, + + /// Creates TokenOwnerRecord with 0 deposit amount + /// It's used to register TokenOwner when voter weight addin is used and the Governance program doesn't take deposits + /// + /// 0. `[]` Realm account + /// 1. `[]` Governing Token Owner account + /// 2. `[writable]` TokenOwnerRecord account. PDA seeds: ['governance',realm, governing_token_mint, governing_token_owner] + /// 3. `[]` Governing Token Mint + /// 4. `[signer]` Payer + /// 5. `[]` System + CreateTokenOwnerRecord {}, } /// Creates CreateRealm instruction @@ -409,6 +445,7 @@ pub fn create_realm( community_token_mint: &Pubkey, payer: &Pubkey, council_token_mint: Option, + community_voter_weight_addin: Option, // Args name: String, min_community_tokens_to_create_governance: u64, @@ -440,11 +477,26 @@ pub fn create_realm( false }; + let realm_config_address = get_realm_config_address(program_id, &realm_address); + accounts.push(AccountMeta::new(realm_config_address, false)); + + let use_community_voter_weight_addin = + if let Some(community_voter_weight_addin) = community_voter_weight_addin { + accounts.push(AccountMeta::new_readonly( + community_voter_weight_addin, + false, + )); + true + } else { + false + }; + let instruction = GovernanceInstruction::CreateRealm { config_args: RealmConfigArgs { use_council_mint, min_community_tokens_to_create_governance, community_mint_max_vote_weight_source, + use_community_voter_weight_addin, }, name, }; @@ -572,7 +624,8 @@ pub fn set_governance_delegate( } } -/// Creates CreateAccountGovernance instruction +/// Creates CreateAccountGovernance instruction using optional voter weight addin +#[allow(clippy::too_many_arguments)] pub fn create_account_governance( program_id: &Pubkey, // Accounts @@ -580,13 +633,15 @@ pub fn create_account_governance( governed_account: &Pubkey, token_owner_record: &Pubkey, payer: &Pubkey, + governance_authority: &Pubkey, + voter_weight_record: Option, // Args config: GovernanceConfig, ) -> Instruction { let account_governance_address = get_account_governance_address(program_id, realm, governed_account); - let accounts = vec![ + let mut accounts = vec![ AccountMeta::new_readonly(*realm, false), AccountMeta::new(account_governance_address, false), AccountMeta::new_readonly(*governed_account, false), @@ -594,8 +649,11 @@ pub fn create_account_governance( AccountMeta::new_readonly(*payer, true), AccountMeta::new_readonly(system_program::id(), false), AccountMeta::new_readonly(sysvar::rent::id(), false), + AccountMeta::new_readonly(*governance_authority, true), ]; + with_voter_weight_accounts(program_id, &mut accounts, realm, voter_weight_record); + let instruction = GovernanceInstruction::CreateAccountGovernance { config }; Instruction { @@ -615,6 +673,8 @@ pub fn create_program_governance( governed_program_upgrade_authority: &Pubkey, token_owner_record: &Pubkey, payer: &Pubkey, + governance_authority: &Pubkey, + voter_weight_record: Option, // Args config: GovernanceConfig, transfer_upgrade_authority: bool, @@ -623,7 +683,7 @@ pub fn create_program_governance( get_program_governance_address(program_id, realm, governed_program); let governed_program_data_address = get_program_data_address(governed_program); - let accounts = vec![ + let mut accounts = vec![ AccountMeta::new_readonly(*realm, false), AccountMeta::new(program_governance_address, false), AccountMeta::new_readonly(*governed_program, false), @@ -634,8 +694,11 @@ pub fn create_program_governance( AccountMeta::new_readonly(bpf_loader_upgradeable::id(), false), AccountMeta::new_readonly(system_program::id(), false), AccountMeta::new_readonly(sysvar::rent::id(), false), + AccountMeta::new_readonly(*governance_authority, true), ]; + with_voter_weight_accounts(program_id, &mut accounts, realm, voter_weight_record); + let instruction = GovernanceInstruction::CreateProgramGovernance { config, transfer_upgrade_authority, @@ -658,13 +721,15 @@ pub fn create_mint_governance( governed_mint_authority: &Pubkey, token_owner_record: &Pubkey, payer: &Pubkey, + governance_authority: &Pubkey, + voter_weight_record: Option, // Args config: GovernanceConfig, transfer_mint_authority: bool, ) -> Instruction { let mint_governance_address = get_mint_governance_address(program_id, realm, governed_mint); - let accounts = vec![ + let mut accounts = vec![ AccountMeta::new_readonly(*realm, false), AccountMeta::new(mint_governance_address, false), AccountMeta::new(*governed_mint, false), @@ -674,8 +739,11 @@ pub fn create_mint_governance( AccountMeta::new_readonly(spl_token::id(), false), AccountMeta::new_readonly(system_program::id(), false), AccountMeta::new_readonly(sysvar::rent::id(), false), + AccountMeta::new_readonly(*governance_authority, true), ]; + with_voter_weight_accounts(program_id, &mut accounts, realm, voter_weight_record); + let instruction = GovernanceInstruction::CreateMintGovernance { config, transfer_mint_authority, @@ -698,13 +766,15 @@ pub fn create_token_governance( governed_token_owner: &Pubkey, token_owner_record: &Pubkey, payer: &Pubkey, + governance_authority: &Pubkey, + voter_weight_record: Option, // Args config: GovernanceConfig, transfer_token_owner: bool, ) -> Instruction { let token_governance_address = get_token_governance_address(program_id, realm, governed_token); - let accounts = vec![ + let mut accounts = vec![ AccountMeta::new_readonly(*realm, false), AccountMeta::new(token_governance_address, false), AccountMeta::new(*governed_token, false), @@ -714,8 +784,11 @@ pub fn create_token_governance( AccountMeta::new_readonly(spl_token::id(), false), AccountMeta::new_readonly(system_program::id(), false), AccountMeta::new_readonly(sysvar::rent::id(), false), + AccountMeta::new_readonly(*governance_authority, true), ]; + with_voter_weight_accounts(program_id, &mut accounts, realm, voter_weight_record); + let instruction = GovernanceInstruction::CreateTokenGovernance { config, transfer_token_owner, @@ -737,6 +810,7 @@ pub fn create_proposal( proposal_owner_record: &Pubkey, governance_authority: &Pubkey, payer: &Pubkey, + voter_weight_record: Option, // Args realm: &Pubkey, name: String, @@ -751,7 +825,7 @@ pub fn create_proposal( &proposal_index.to_le_bytes(), ); - let accounts = vec![ + let mut accounts = vec![ AccountMeta::new_readonly(*realm, false), AccountMeta::new(proposal_address, false), AccountMeta::new(*governance, false), @@ -763,6 +837,8 @@ pub fn create_proposal( AccountMeta::new_readonly(sysvar::clock::id(), false), ]; + with_voter_weight_accounts(program_id, &mut accounts, realm, voter_weight_record); + let instruction = GovernanceInstruction::CreateProposal { name, description_link, @@ -879,13 +955,14 @@ pub fn cast_vote( governance_authority: &Pubkey, governing_token_mint: &Pubkey, payer: &Pubkey, + voter_weight_record: Option, // Args vote: Vote, ) -> Instruction { let vote_record_address = get_vote_record_address(program_id, proposal, voter_token_owner_record); - let accounts = vec![ + let mut accounts = vec![ AccountMeta::new_readonly(*realm, false), AccountMeta::new_readonly(*governance, false), AccountMeta::new(*proposal, false), @@ -900,6 +977,8 @@ pub fn cast_vote( AccountMeta::new_readonly(sysvar::clock::id(), false), ]; + with_voter_weight_accounts(program_id, &mut accounts, realm, voter_weight_record); + let instruction = GovernanceInstruction::CastVote { vote }; Instruction { @@ -1165,13 +1244,15 @@ pub fn set_realm_authority( } /// Creates SetRealmConfig instruction +#[allow(clippy::too_many_arguments)] pub fn set_realm_config( program_id: &Pubkey, // Accounts realm: &Pubkey, realm_authority: &Pubkey, council_token_mint: Option, - + payer: &Pubkey, + community_voter_weight_addin: Option, // Args min_community_tokens_to_create_governance: u64, community_mint_max_vote_weight_source: MintMaxVoteWeightSource, @@ -1192,11 +1273,31 @@ pub fn set_realm_config( false }; + accounts.push(AccountMeta::new_readonly(system_program::id(), false)); + + // Always pass realm_config_address because it's needed when use_community_voter_weight_addin is set to true + // but also when it's set to false and the addin is being removed from the realm + let realm_config_address = get_realm_config_address(program_id, realm); + accounts.push(AccountMeta::new(realm_config_address, false)); + + let use_community_voter_weight_addin = + if let Some(community_voter_weight_addin) = community_voter_weight_addin { + accounts.push(AccountMeta::new(*payer, true)); + accounts.push(AccountMeta::new_readonly( + community_voter_weight_addin, + false, + )); + true + } else { + false + }; + let instruction = GovernanceInstruction::SetRealmConfig { config_args: RealmConfigArgs { use_council_mint, min_community_tokens_to_create_governance, community_mint_max_vote_weight_source, + use_community_voter_weight_addin, }, }; @@ -1206,3 +1307,51 @@ pub fn set_realm_config( data: instruction.try_to_vec().unwrap(), } } + +/// Adds voter weight accounts to the given accounts if voter_weight_record is Some +pub fn with_voter_weight_accounts( + program_id: &Pubkey, + accounts: &mut Vec, + realm: &Pubkey, + voter_weight_record: Option, +) { + if let Some(voter_weight_record) = voter_weight_record { + let realm_config_address = get_realm_config_address(program_id, realm); + accounts.push(AccountMeta::new_readonly(realm_config_address, false)); + accounts.push(AccountMeta::new_readonly(voter_weight_record, false)); + } +} + +/// Creates CreateTokenOwnerRecord instruction +pub fn create_token_owner_record( + program_id: &Pubkey, + // Accounts + realm: &Pubkey, + governing_token_owner: &Pubkey, + governing_token_mint: &Pubkey, + payer: &Pubkey, +) -> Instruction { + let token_owner_record_address = get_token_owner_record_address( + program_id, + realm, + governing_token_mint, + governing_token_owner, + ); + + let accounts = vec![ + AccountMeta::new_readonly(*realm, false), + AccountMeta::new_readonly(*governing_token_owner, false), + AccountMeta::new(token_owner_record_address, false), + AccountMeta::new_readonly(*governing_token_mint, false), + AccountMeta::new_readonly(*payer, true), + AccountMeta::new_readonly(system_program::id(), false), + ]; + + let instruction = GovernanceInstruction::CreateTokenOwnerRecord {}; + + Instruction { + program_id: *program_id, + accounts, + data: instruction.try_to_vec().unwrap(), + } +} diff --git a/governance/program/src/lib.rs b/governance/program/src/lib.rs index f69bc607ed2..47e4a7e56c5 100644 --- a/governance/program/src/lib.rs +++ b/governance/program/src/lib.rs @@ -1,6 +1,7 @@ #![deny(missing_docs)] //! A Governance program for the Solana blockchain. +pub mod addins; pub mod entrypoint; pub mod error; pub mod instruction; diff --git a/governance/program/src/processor/mod.rs b/governance/program/src/processor/mod.rs index d913088f3b0..67406e89092 100644 --- a/governance/program/src/processor/mod.rs +++ b/governance/program/src/processor/mod.rs @@ -9,6 +9,7 @@ mod process_create_program_governance; mod process_create_proposal; mod process_create_realm; mod process_create_token_governance; +mod process_create_token_owner_record; mod process_deposit_governing_tokens; mod process_execute_instruction; mod process_finalize_vote; @@ -36,6 +37,7 @@ use process_create_program_governance::*; use process_create_proposal::*; use process_create_realm::*; use process_create_token_governance::*; +use process_create_token_owner_record::*; use process_deposit_governing_tokens::*; use process_execute_instruction::*; use process_finalize_vote::*; @@ -176,5 +178,8 @@ pub fn process_instruction( GovernanceInstruction::SetRealmConfig { config_args } => { process_set_realm_config(program_id, accounts, config_args) } + GovernanceInstruction::CreateTokenOwnerRecord {} => { + process_create_token_owner_record(program_id, accounts) + } } } diff --git a/governance/program/src/processor/process_cast_vote.rs b/governance/program/src/processor/process_cast_vote.rs index 36eb81af3cd..13a130f450c 100644 --- a/governance/program/src/processor/process_cast_vote.rs +++ b/governance/program/src/processor/process_cast_vote.rs @@ -98,23 +98,28 @@ pub fn process_cast_vote( .checked_add(1) .unwrap(); - let vote_amount = voter_token_owner_record_data.governing_token_deposit_amount; + let voter_weight = voter_token_owner_record_data.resolve_voter_weight( + program_id, + account_info_iter, + realm_info.key, + &realm_data, + )?; // Calculate Proposal voting weights let vote_weight = match vote { Vote::Yes => { proposal_data.yes_votes_count = proposal_data .yes_votes_count - .checked_add(vote_amount) + .checked_add(voter_weight) .unwrap(); - VoteWeight::Yes(vote_amount) + VoteWeight::Yes(voter_weight) } Vote::No => { proposal_data.no_votes_count = proposal_data .no_votes_count - .checked_add(vote_amount) + .checked_add(voter_weight) .unwrap(); - VoteWeight::No(vote_amount) + VoteWeight::No(voter_weight) } }; diff --git a/governance/program/src/processor/process_create_account_governance.rs b/governance/program/src/processor/process_create_account_governance.rs index 19c0001e237..52e1560723e 100644 --- a/governance/program/src/processor/process_create_account_governance.rs +++ b/governance/program/src/processor/process_create_account_governance.rs @@ -40,13 +40,24 @@ pub fn process_create_account_governance( let rent_sysvar_info = next_account_info(account_info_iter)?; // 6 let rent = &Rent::from_account_info(rent_sysvar_info)?; + let governance_authority_info = next_account_info(account_info_iter)?; // 7 + assert_valid_create_governance_args(program_id, &config, realm_info)?; let realm_data = get_realm_data(program_id, realm_info)?; let token_owner_record_data = get_token_owner_record_data_for_realm(program_id, token_owner_record_info, realm_info.key)?; - token_owner_record_data.assert_can_create_governance(&realm_data)?; + token_owner_record_data.assert_token_owner_or_delegate_is_signer(governance_authority_info)?; + + let voter_weight = token_owner_record_data.resolve_voter_weight( + program_id, + account_info_iter, + realm_info.key, + &realm_data, + )?; + + token_owner_record_data.assert_can_create_governance(&realm_data, voter_weight)?; let account_governance_data = Governance { account_type: GovernanceAccountType::AccountGovernance, diff --git a/governance/program/src/processor/process_create_mint_governance.rs b/governance/program/src/processor/process_create_mint_governance.rs index 050b8733f79..9ecd2b34049 100644 --- a/governance/program/src/processor/process_create_mint_governance.rs +++ b/governance/program/src/processor/process_create_mint_governance.rs @@ -48,13 +48,24 @@ pub fn process_create_mint_governance( let rent_sysvar_info = next_account_info(account_info_iter)?; // 8 let rent = &Rent::from_account_info(rent_sysvar_info)?; + let governance_authority_info = next_account_info(account_info_iter)?; // 9 + assert_valid_create_governance_args(program_id, &config, realm_info)?; let realm_data = get_realm_data(program_id, realm_info)?; let token_owner_record_data = get_token_owner_record_data_for_realm(program_id, token_owner_record_info, realm_info.key)?; - token_owner_record_data.assert_can_create_governance(&realm_data)?; + token_owner_record_data.assert_token_owner_or_delegate_is_signer(governance_authority_info)?; + + let voter_weight = token_owner_record_data.resolve_voter_weight( + program_id, + account_info_iter, + realm_info.key, + &realm_data, + )?; + + token_owner_record_data.assert_can_create_governance(&realm_data, voter_weight)?; let mint_governance_data = Governance { account_type: GovernanceAccountType::MintGovernance, diff --git a/governance/program/src/processor/process_create_program_governance.rs b/governance/program/src/processor/process_create_program_governance.rs index 38d49c2e181..b7359ed70e6 100644 --- a/governance/program/src/processor/process_create_program_governance.rs +++ b/governance/program/src/processor/process_create_program_governance.rs @@ -52,13 +52,24 @@ pub fn process_create_program_governance( let rent_sysvar_info = next_account_info(account_info_iter)?; // 9 let rent = &Rent::from_account_info(rent_sysvar_info)?; + let governance_authority_info = next_account_info(account_info_iter)?; // 10 + assert_valid_create_governance_args(program_id, &config, realm_info)?; let realm_data = get_realm_data(program_id, realm_info)?; let token_owner_record_data = get_token_owner_record_data_for_realm(program_id, token_owner_record_info, realm_info.key)?; - token_owner_record_data.assert_can_create_governance(&realm_data)?; + token_owner_record_data.assert_token_owner_or_delegate_is_signer(governance_authority_info)?; + + let voter_weight = token_owner_record_data.resolve_voter_weight( + program_id, + account_info_iter, + realm_info.key, + &realm_data, + )?; + + token_owner_record_data.assert_can_create_governance(&realm_data, voter_weight)?; let program_governance_data = Governance { account_type: GovernanceAccountType::ProgramGovernance, diff --git a/governance/program/src/processor/process_create_proposal.rs b/governance/program/src/processor/process_create_proposal.rs index 9a52af5828c..4a094a6409d 100644 --- a/governance/program/src/processor/process_create_proposal.rs +++ b/governance/program/src/processor/process_create_proposal.rs @@ -68,8 +68,19 @@ pub fn process_create_proposal( proposal_owner_record_data .assert_token_owner_or_delegate_is_signer(governance_authority_info)?; + let voter_weight = proposal_owner_record_data.resolve_voter_weight( + program_id, + account_info_iter, + realm_info.key, + &realm_data, + )?; + // Ensure proposal owner (TokenOwner) has enough tokens to create proposal and no outstanding proposals - proposal_owner_record_data.assert_can_create_proposal(&realm_data, &governance_data.config)?; + proposal_owner_record_data.assert_can_create_proposal( + &realm_data, + &governance_data.config, + voter_weight, + )?; proposal_owner_record_data.outstanding_proposal_count = proposal_owner_record_data .outstanding_proposal_count diff --git a/governance/program/src/processor/process_create_realm.rs b/governance/program/src/processor/process_create_realm.rs index cfdca86ba0d..0e425efdfea 100644 --- a/governance/program/src/processor/process_create_realm.rs +++ b/governance/program/src/processor/process_create_realm.rs @@ -16,6 +16,7 @@ use crate::{ assert_valid_realm_config_args, get_governing_token_holding_address_seeds, get_realm_address_seeds, Realm, RealmConfig, RealmConfigArgs, }, + realm_config::{get_realm_config_address_seeds, RealmConfigAccount}, }, tools::{ account::create_and_serialize_account_signed, spl_token::create_spl_token_account_signed, @@ -62,8 +63,8 @@ pub fn process_create_realm( )?; let council_token_mint_address = if config_args.use_council_mint { - let council_token_mint_info = next_account_info(account_info_iter)?; - let council_token_holding_info = next_account_info(account_info_iter)?; + let council_token_mint_info = next_account_info(account_info_iter)?; // 8 + let council_token_holding_info = next_account_info(account_info_iter)?; // 9 create_spl_token_account_signed( payer_info, @@ -83,6 +84,31 @@ pub fn process_create_realm( None }; + if config_args.use_community_voter_weight_addin { + let realm_config_info = next_account_info(account_info_iter)?; // 10 + let community_voter_weight_addin_info = next_account_info(account_info_iter)?; //11 + + let realm_config_data = RealmConfigAccount { + account_type: GovernanceAccountType::RealmConfig, + realm: *realm_info.key, + community_voter_weight_addin: Some(*community_voter_weight_addin_info.key), + reserved_1: None, + reserved_2: None, + reserved_3: None, + reserved: [0; 128], + }; + + create_and_serialize_account_signed::( + payer_info, + realm_config_info, + &realm_config_data, + &get_realm_config_address_seeds(realm_info.key), + program_id, + system_info, + rent, + )?; + } + let realm_data = Realm { account_type: GovernanceAccountType::Realm, community_mint: *governance_token_mint_info.key, @@ -92,11 +118,12 @@ pub fn process_create_realm( authority: Some(*realm_authority_info.key), config: RealmConfig { council_mint: council_token_mint_address, - reserved: [0; 8], + reserved: [0; 7], community_mint_max_vote_weight_source: config_args .community_mint_max_vote_weight_source, min_community_tokens_to_create_governance: config_args .min_community_tokens_to_create_governance, + use_community_voter_weight_addin: config_args.use_community_voter_weight_addin, }, }; diff --git a/governance/program/src/processor/process_create_token_governance.rs b/governance/program/src/processor/process_create_token_governance.rs index 505299746c9..a1dde948154 100644 --- a/governance/program/src/processor/process_create_token_governance.rs +++ b/governance/program/src/processor/process_create_token_governance.rs @@ -48,13 +48,24 @@ pub fn process_create_token_governance( let rent_sysvar_info = next_account_info(account_info_iter)?; // 8 let rent = &Rent::from_account_info(rent_sysvar_info)?; + let governance_authority_info = next_account_info(account_info_iter)?; // 9 + assert_valid_create_governance_args(program_id, &config, realm_info)?; let realm_data = get_realm_data(program_id, realm_info)?; let token_owner_record_data = get_token_owner_record_data_for_realm(program_id, token_owner_record_info, realm_info.key)?; - token_owner_record_data.assert_can_create_governance(&realm_data)?; + token_owner_record_data.assert_token_owner_or_delegate_is_signer(governance_authority_info)?; + + let voter_weight = token_owner_record_data.resolve_voter_weight( + program_id, + account_info_iter, + realm_info.key, + &realm_data, + )?; + + token_owner_record_data.assert_can_create_governance(&realm_data, voter_weight)?; let token_governance_data = Governance { account_type: GovernanceAccountType::TokenGovernance, diff --git a/governance/program/src/processor/process_create_token_owner_record.rs b/governance/program/src/processor/process_create_token_owner_record.rs new file mode 100644 index 00000000000..d643592fdc0 --- /dev/null +++ b/governance/program/src/processor/process_create_token_owner_record.rs @@ -0,0 +1,69 @@ +//! 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_realm_data, + token_owner_record::{get_token_owner_record_address_seeds, TokenOwnerRecord}, + }, + tools::account::create_and_serialize_account_signed, +}; + +/// Processes CreateTokenOwnerRecord instruction +pub fn process_create_token_owner_record( + 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_owner_info = next_account_info(account_info_iter)?; // 1 + let token_owner_record_info = next_account_info(account_info_iter)?; // 2 + let governing_token_mint_info = next_account_info(account_info_iter)?; // 3 + let payer_info = next_account_info(account_info_iter)?; // 4 + let system_info = next_account_info(account_info_iter)?; // 5 + let rent = Rent::get().unwrap(); + + let realm_data = get_realm_data(program_id, realm_info)?; + realm_data.assert_is_valid_governing_token_mint(governing_token_mint_info.key)?; + + if !token_owner_record_info.data_is_empty() { + return Err(GovernanceError::TokenOwnerRecordAlreadyExists.into()); + } + + let token_owner_record_data = TokenOwnerRecord { + account_type: GovernanceAccountType::TokenOwnerRecord, + realm: *realm_info.key, + governing_token_owner: *governing_token_owner_info.key, + governing_token_deposit_amount: 0, + governing_token_mint: *governing_token_mint_info.key, + governance_delegate: None, + unrelinquished_votes_count: 0, + total_votes_count: 0, + outstanding_proposal_count: 0, + reserved: [0; 7], + }; + + create_and_serialize_account_signed( + payer_info, + token_owner_record_info, + &token_owner_record_data, + &get_token_owner_record_address_seeds( + realm_info.key, + governing_token_mint_info.key, + governing_token_owner_info.key, + ), + program_id, + system_info, + &rent, + ) +} diff --git a/governance/program/src/processor/process_deposit_governing_tokens.rs b/governance/program/src/processor/process_deposit_governing_tokens.rs index a65c6b9df03..08d434f25fd 100644 --- a/governance/program/src/processor/process_deposit_governing_tokens.rs +++ b/governance/program/src/processor/process_deposit_governing_tokens.rs @@ -50,6 +50,8 @@ pub fn process_deposit_governing_tokens( let realm_data = get_realm_data(program_id, realm_info)?; let governing_token_mint = get_spl_token_mint(governing_token_holding_info)?; + realm_data.asset_governing_tokens_deposits_allowed(&governing_token_mint)?; + realm_data.assert_is_valid_governing_token_mint_and_holding( program_id, realm_info.key, diff --git a/governance/program/src/processor/process_set_realm_config.rs b/governance/program/src/processor/process_set_realm_config.rs index 233971f6227..a9093a76d6f 100644 --- a/governance/program/src/processor/process_set_realm_config.rs +++ b/governance/program/src/processor/process_set_realm_config.rs @@ -5,18 +5,27 @@ use solana_program::{ account_info::{next_account_info, AccountInfo}, entrypoint::ProgramResult, pubkey::Pubkey, + rent::Rent, + sysvar::Sysvar, }; use crate::{ error::GovernanceError, - state::realm::{assert_valid_realm_config_args, get_realm_data_for_authority, RealmConfigArgs}, + state::{ + enums::GovernanceAccountType, + realm::{assert_valid_realm_config_args, get_realm_data_for_authority, RealmConfigArgs}, + realm_config::{ + get_realm_config_address_seeds, get_realm_config_data_for_realm, RealmConfigAccount, + }, + }, + tools::account::create_and_serialize_account_signed, }; /// Processes SetRealmConfig instruction pub fn process_set_realm_config( program_id: &Pubkey, accounts: &[AccountInfo], - config_args: RealmConfigArgs, + realm_config_args: RealmConfigArgs, ) -> ProgramResult { let account_info_iter = &mut accounts.iter(); @@ -30,10 +39,12 @@ pub fn process_set_realm_config( return Err(GovernanceError::RealmAuthorityMustSign.into()); } - assert_valid_realm_config_args(&config_args)?; + assert_valid_realm_config_args(&realm_config_args)?; - if config_args.use_council_mint { - let council_token_mint_info = next_account_info(account_info_iter)?; + // Setup council + if realm_config_args.use_council_mint { + let council_token_mint_info = next_account_info(account_info_iter)?; // 2 + let _council_token_holding_info = next_account_info(account_info_iter)?; // 3 // Council mint can only be at present set to none (removed) and changing it to other mint is not supported // It might be implemented in future versions but it needs careful planning @@ -53,10 +64,56 @@ pub fn process_set_realm_config( realm_data.config.council_mint = None; } + let system_info = next_account_info(account_info_iter)?; // 4 + let realm_config_info = next_account_info(account_info_iter)?; // 5 + + // Setup community voter weight addin + if realm_config_args.use_community_voter_weight_addin { + let payer_info = next_account_info(account_info_iter)?; // 6 + let community_voter_weight_addin_info = next_account_info(account_info_iter)?; // 7 + + if realm_config_info.data_is_empty() { + let realm_config_data = RealmConfigAccount { + account_type: GovernanceAccountType::RealmConfig, + realm: *realm_info.key, + community_voter_weight_addin: Some(*community_voter_weight_addin_info.key), + reserved_1: None, + reserved_2: None, + reserved_3: None, + reserved: [0; 128], + }; + + let rent = Rent::get().unwrap(); + + create_and_serialize_account_signed::( + payer_info, + realm_config_info, + &realm_config_data, + &get_realm_config_address_seeds(realm_info.key), + program_id, + system_info, + &rent, + )?; + } else { + let mut realm_config_data = + get_realm_config_data_for_realm(program_id, realm_config_info, realm_info.key)?; + realm_config_data.community_voter_weight_addin = + Some(*community_voter_weight_addin_info.key); + realm_config_data.serialize(&mut *realm_config_info.data.borrow_mut())?; + } + } else if realm_data.config.use_community_voter_weight_addin { + let mut realm_config_data = + get_realm_config_data_for_realm(program_id, realm_config_info, realm_info.key)?; + realm_config_data.community_voter_weight_addin = None; + realm_config_data.serialize(&mut *realm_config_info.data.borrow_mut())?; + } + realm_data.config.community_mint_max_vote_weight_source = - config_args.community_mint_max_vote_weight_source; + realm_config_args.community_mint_max_vote_weight_source; realm_data.config.min_community_tokens_to_create_governance = - config_args.min_community_tokens_to_create_governance; + realm_config_args.min_community_tokens_to_create_governance; + realm_data.config.use_community_voter_weight_addin = + realm_config_args.use_community_voter_weight_addin; realm_data.serialize(&mut *realm_info.data.borrow_mut())?; diff --git a/governance/program/src/processor/process_withdraw_governing_tokens.rs b/governance/program/src/processor/process_withdraw_governing_tokens.rs index dbdd6d0ef86..6d8b12e62d4 100644 --- a/governance/program/src/processor/process_withdraw_governing_tokens.rs +++ b/governance/program/src/processor/process_withdraw_governing_tokens.rs @@ -58,13 +58,7 @@ pub fn process_withdraw_governing_tokens( &token_owner_record_address_seeds, )?; - if token_owner_record_data.unrelinquished_votes_count > 0 { - return Err(GovernanceError::AllVotesMustBeRelinquishedToWithdrawGoverningTokens.into()); - } - - if token_owner_record_data.outstanding_proposal_count > 0 { - return Err(GovernanceError::AllProposalsMustBeFinalisedToWithdrawGoverningTokens.into()); - } + token_owner_record_data.assert_can_withdraw_governing_tokens()?; transfer_spl_tokens_signed( governing_token_holding_info, diff --git a/governance/program/src/state/enums.rs b/governance/program/src/state/enums.rs index 5e4546274f9..5d502fca3fc 100644 --- a/governance/program/src/state/enums.rs +++ b/governance/program/src/state/enums.rs @@ -38,6 +38,9 @@ pub enum GovernanceAccountType { /// Token Governance account TokenGovernance, + + /// Realm config account + RealmConfig, } impl Default for GovernanceAccountType { diff --git a/governance/program/src/state/mod.rs b/governance/program/src/state/mod.rs index 43a953b155d..0fd5812723c 100644 --- a/governance/program/src/state/mod.rs +++ b/governance/program/src/state/mod.rs @@ -5,6 +5,7 @@ pub mod governance; pub mod proposal; pub mod proposal_instruction; pub mod realm; +pub mod realm_config; pub mod signatory_record; pub mod token_owner_record; pub mod vote_record; diff --git a/governance/program/src/state/proposal.rs b/governance/program/src/state/proposal.rs index 64b5353e8a3..d48c8673ff3 100644 --- a/governance/program/src/state/proposal.rs +++ b/governance/program/src/state/proposal.rs @@ -565,7 +565,8 @@ mod test { name: "test-realm".to_string(), config: RealmConfig { council_mint: Some(Pubkey::new_unique()), - reserved: [0; 8], + reserved: [0; 7], + use_community_voter_weight_addin: false, community_mint_max_vote_weight_source: MintMaxVoteWeightSource::FULL_SUPPLY_FRACTION, diff --git a/governance/program/src/state/realm.rs b/governance/program/src/state/realm.rs index d301add5f21..6e04188df72 100644 --- a/governance/program/src/state/realm.rs +++ b/governance/program/src/state/realm.rs @@ -26,14 +26,21 @@ pub struct RealmConfigArgs { /// The source used for community mint max vote weight source pub community_mint_max_vote_weight_source: MintMaxVoteWeightSource, + + /// Indicates whether an external addin program should be used to provide community voters weights + /// If yes then the voters weight program account must be passed to the instruction + pub use_community_voter_weight_addin: bool, } /// Realm Config defining Realm parameters. #[repr(C)] #[derive(Clone, Debug, PartialEq, BorshDeserialize, BorshSerialize, BorshSchema)] pub struct RealmConfig { + /// Indicates whether an external addin program should be used to provide voters weights for the community mint + pub use_community_voter_weight_addin: bool, + /// Reserved space for future versions - pub reserved: [u8; 8], + pub reserved: [u8; 7], /// Min number of community tokens required to create a governance pub min_community_tokens_to_create_governance: u64, @@ -118,6 +125,21 @@ impl Realm { Ok(()) } + + /// Asserts the given governing token can be deposited into the realm + pub fn asset_governing_tokens_deposits_allowed( + &self, + governing_token_mint: &Pubkey, + ) -> Result<(), ProgramError> { + // If the deposit is for the community token and the realm uses community voter weight addin then panic + if self.config.use_community_voter_weight_addin + && self.community_mint == *governing_token_mint + { + return Err(GovernanceError::GoverningTokenDepositsNotAllowed.into()); + } + + Ok(()) + } } /// Checks whether realm account exists, is initialized and owned by Governance program @@ -222,6 +244,9 @@ pub fn assert_valid_realm_config_args(config_args: &RealmConfigArgs) -> Result<( #[cfg(test)] mod test { + use crate::instruction::GovernanceInstruction; + use solana_program::borsh::try_from_slice_unchecked; + use super::*; #[test] @@ -235,7 +260,8 @@ mod test { name: "test-realm".to_string(), config: RealmConfig { council_mint: Some(Pubkey::new_unique()), - reserved: [0; 8], + use_community_voter_weight_addin: false, + reserved: [0; 7], community_mint_max_vote_weight_source: MintMaxVoteWeightSource::Absolute(100), min_community_tokens_to_create_governance: 10, @@ -246,4 +272,76 @@ mod test { assert_eq!(realm.get_max_size(), Some(size)); } + + #[test] + fn test_deserialize_v2_realm_account_from_v1() { + // Arrange + let realm_v1 = spl_governance_v1::state::realm::Realm { + account_type: spl_governance_v1::state::enums::GovernanceAccountType::Realm, + community_mint: Pubkey::new_unique(), + config: spl_governance_v1::state::realm::RealmConfig { + council_mint: Some(Pubkey::new_unique()), + reserved: [0; 8], + community_mint_max_vote_weight_source: + spl_governance_v1::state::enums::MintMaxVoteWeightSource::Absolute(100), + min_community_tokens_to_create_governance: 10, + }, + reserved: [0; 8], + authority: Some(Pubkey::new_unique()), + name: "test-realm-v1".to_string(), + }; + + let mut realm_v1_data = vec![]; + realm_v1.serialize(&mut realm_v1_data).unwrap(); + + // Act + let realm_v2: Realm = try_from_slice_unchecked(&realm_v1_data).unwrap(); + + // Assert + assert!(!realm_v2.config.use_community_voter_weight_addin); + assert_eq!(realm_v2.account_type, GovernanceAccountType::Realm); + assert_eq!( + realm_v2.config.min_community_tokens_to_create_governance, + realm_v1.config.min_community_tokens_to_create_governance, + ); + } + + #[test] + fn test_deserialize_v1_create_realm_instruction_from_v2() { + // Arrange + let create_realm_ix = GovernanceInstruction::CreateRealm { + name: "test-realm".to_string(), + config_args: RealmConfigArgs { + use_council_mint: true, + min_community_tokens_to_create_governance: 100, + community_mint_max_vote_weight_source: + MintMaxVoteWeightSource::FULL_SUPPLY_FRACTION, + use_community_voter_weight_addin: false, + }, + }; + + let mut create_realm_ix_data = vec![]; + create_realm_ix + .serialize(&mut create_realm_ix_data) + .unwrap(); + + // Act + let create_realm_ix_v1: spl_governance_v1::instruction::GovernanceInstruction = + try_from_slice_unchecked(&create_realm_ix_data).unwrap(); + + // Assert + if let spl_governance_v1::instruction::GovernanceInstruction::CreateRealm { + name, + config_args, + } = create_realm_ix_v1 + { + assert_eq!("test-realm", name); + assert_eq!( + spl_governance_v1::state::enums::MintMaxVoteWeightSource::FULL_SUPPLY_FRACTION, + config_args.community_mint_max_vote_weight_source + ); + } else { + panic!("Can't deserialize v1 CreateRealm instruction from v2"); + } + } } diff --git a/governance/program/src/state/realm_config.rs b/governance/program/src/state/realm_config.rs new file mode 100644 index 00000000000..4b131d77579 --- /dev/null +++ b/governance/program/src/state/realm_config.rs @@ -0,0 +1,108 @@ +//! RealmConfig account + +use solana_program::{ + account_info::AccountInfo, program_error::ProgramError, program_pack::IsInitialized, + pubkey::Pubkey, +}; + +use borsh::{BorshDeserialize, BorshSchema, BorshSerialize}; + +use crate::{ + error::GovernanceError, + state::enums::GovernanceAccountType, + tools::account::{get_account_data, AccountMaxSize}, +}; + +/// RealmConfig account +/// The account is an optional extension to RealmConfig stored on Realm account +#[derive(Clone, Debug, PartialEq, BorshDeserialize, BorshSerialize, BorshSchema)] +pub struct RealmConfigAccount { + /// Governance account type + pub account_type: GovernanceAccountType, + + /// The realm the config belong to + pub realm: Pubkey, + + /// Addin providing voter weights for community token + pub community_voter_weight_addin: Option, + + /// Reserved for community max vote weight addin + pub reserved_1: Option, + + /// Reserved for council voter weight addin + pub reserved_2: Option, + + /// Reserved for council max vote weight addin + pub reserved_3: Option, + + /// Reserved + pub reserved: [u8; 128], +} + +impl AccountMaxSize for RealmConfigAccount { + fn get_max_size(&self) -> Option { + Some(1 + 32 + 33 * 4 + 128) + } +} + +impl IsInitialized for RealmConfigAccount { + fn is_initialized(&self) -> bool { + self.account_type == GovernanceAccountType::RealmConfig + } +} + +/// Deserializes RealmConfig account and checks owner program +pub fn get_realm_config_data( + program_id: &Pubkey, + realm_config_info: &AccountInfo, +) -> Result { + get_account_data::(realm_config_info, program_id) +} + +/// Deserializes RealmConfig account and checks the owner program and the Realm it belongs to +pub fn get_realm_config_data_for_realm( + program_id: &Pubkey, + realm_config_info: &AccountInfo, + realm: &Pubkey, +) -> Result { + let realm_config_data = get_realm_config_data(program_id, realm_config_info)?; + + if realm_config_data.realm != *realm { + return Err(GovernanceError::InvalidRealmConfigForRealm.into()); + } + + Ok(realm_config_data) +} + +/// Returns RealmConfig PDA seeds +pub fn get_realm_config_address_seeds(realm: &Pubkey) -> [&[u8]; 2] { + [b"realm-config", realm.as_ref()] +} + +/// Returns RealmConfig PDA address +pub fn get_realm_config_address(program_id: &Pubkey, realm: &Pubkey) -> Pubkey { + Pubkey::find_program_address(&get_realm_config_address_seeds(realm), program_id).0 +} + +#[cfg(test)] +mod test { + use super::*; + use crate::state::{enums::GovernanceAccountType, realm_config::RealmConfigAccount}; + + #[test] + fn test_max_size() { + let realm_config = RealmConfigAccount { + account_type: GovernanceAccountType::Realm, + realm: Pubkey::new_unique(), + community_voter_weight_addin: Some(Pubkey::new_unique()), + reserved_1: Some(Pubkey::new_unique()), + reserved_2: Some(Pubkey::new_unique()), + reserved_3: Some(Pubkey::new_unique()), + reserved: [0; 128], + }; + + let size = realm_config.try_to_vec().unwrap().len(); + + assert_eq!(realm_config.get_max_size(), Some(size)); + } +} diff --git a/governance/program/src/state/token_owner_record.rs b/governance/program/src/state/token_owner_record.rs index 5d2fd0d1d43..3aeed227729 100644 --- a/governance/program/src/state/token_owner_record.rs +++ b/governance/program/src/state/token_owner_record.rs @@ -1,15 +1,23 @@ //! Token Owner Record Account +use std::slice::Iter; + use crate::{ + addins::voter_weight::get_voter_weight_record_data_for_token_owner_record, error::GovernanceError, - state::{enums::GovernanceAccountType, governance::GovernanceConfig, realm::Realm}, + state::{ + enums::GovernanceAccountType, governance::GovernanceConfig, realm::Realm, + realm_config::get_realm_config_data_for_realm, + }, tools::account::{get_account_data, AccountMaxSize}, PROGRAM_AUTHORITY_SEED, }; use borsh::{BorshDeserialize, BorshSchema, BorshSerialize}; use solana_program::{ - account_info::AccountInfo, program_error::ProgramError, program_pack::IsInitialized, + account_info::{next_account_info, AccountInfo}, + program_error::ProgramError, + program_pack::IsInitialized, pubkey::Pubkey, }; @@ -95,8 +103,9 @@ impl TokenOwnerRecord { &self, realm_data: &Realm, config: &GovernanceConfig, + voter_weight: u64, ) -> Result<(), ProgramError> { - let min_tokens_to_create_proposal = + let min_weight_to_create_proposal = if self.governing_token_mint == realm_data.community_mint { config.min_community_tokens_to_create_proposal } else if Some(self.governing_token_mint) == realm_data.config.council_mint { @@ -105,11 +114,11 @@ impl TokenOwnerRecord { return Err(GovernanceError::InvalidGoverningTokenMint.into()); }; - if self.governing_token_deposit_amount < min_tokens_to_create_proposal { + if voter_weight < min_weight_to_create_proposal { return Err(GovernanceError::NotEnoughTokensToCreateProposal.into()); } - // The number of outstanding proposals is currently restricted to 1 + // The number of outstanding proposals is currently restricted to 10 // If there is a need to change it in the future then it should be added to realm or governance config if self.outstanding_proposal_count >= 10 { return Err(GovernanceError::TooManyOutstandingProposals.into()); @@ -119,8 +128,12 @@ impl TokenOwnerRecord { } /// Asserts TokenOwner has enough tokens to be allowed to create governance - pub fn assert_can_create_governance(&self, realm_data: &Realm) -> Result<(), ProgramError> { - let min_tokens_to_create_governance = + pub fn assert_can_create_governance( + &self, + realm_data: &Realm, + voter_weight: u64, + ) -> Result<(), ProgramError> { + let min_weight_to_create_governance = if self.governing_token_mint == realm_data.community_mint { realm_data.config.min_community_tokens_to_create_governance } else if Some(self.governing_token_mint) == realm_data.config.council_mint { @@ -130,13 +143,30 @@ impl TokenOwnerRecord { return Err(GovernanceError::InvalidGoverningTokenMint.into()); }; - if self.governing_token_deposit_amount < min_tokens_to_create_governance { + if voter_weight < min_weight_to_create_governance { return Err(GovernanceError::NotEnoughTokensToCreateGovernance.into()); } Ok(()) } + /// Asserts TokenOwner can withdraw tokens from Realm + pub fn assert_can_withdraw_governing_tokens(&self) -> Result<(), ProgramError> { + if self.unrelinquished_votes_count > 0 { + return Err( + GovernanceError::AllVotesMustBeRelinquishedToWithdrawGoverningTokens.into(), + ); + } + + if self.outstanding_proposal_count > 0 { + return Err( + GovernanceError::AllProposalsMustBeFinalisedToWithdrawGoverningTokens.into(), + ); + } + + Ok(()) + } + /// Decreases outstanding_proposal_count pub fn decrease_outstanding_proposal_count(&mut self) { // Previous versions didn't use the count and it can be already 0 @@ -146,6 +176,36 @@ impl TokenOwnerRecord { self.outstanding_proposal_count.checked_sub(1).unwrap(); } } + + /// Resolves voter's weight using either the amount deposited into the realm or weight provided by voter weight addin (if configured) + pub fn resolve_voter_weight( + &self, + program_id: &Pubkey, + account_info_iter: &mut Iter, + realm: &Pubkey, + realm_data: &Realm, + ) -> Result { + // if the realm uses addin for community voter weight then use the externally provided weight + if realm_data.config.use_community_voter_weight_addin + && realm_data.community_mint == self.governing_token_mint + { + let realm_config_info = next_account_info(account_info_iter)?; + let voter_weight_record_info = next_account_info(account_info_iter)?; + + let realm_config_data = + get_realm_config_data_for_realm(program_id, realm_config_info, realm)?; + + let voter_weight_record_data = get_voter_weight_record_data_for_token_owner_record( + &realm_config_data.community_voter_weight_addin.unwrap(), + voter_weight_record_info, + self, + )?; + voter_weight_record_data.assert_is_up_to_date()?; + Ok(voter_weight_record_data.voter_weight) + } else { + Ok(self.governing_token_deposit_amount) + } + } } /// Returns TokenOwnerRecord PDA address diff --git a/governance/program/tests/fixtures/spl_governance_voter_weight_addin.so b/governance/program/tests/fixtures/spl_governance_voter_weight_addin.so new file mode 100755 index 0000000000000000000000000000000000000000..ef99326389d0c668147b6f9ea8e8197929a0e3ec GIT binary patch literal 131496 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, pub realm_authority: Option, + + pub realm_config: Option, +} + +#[derive(Debug)] +pub struct RealmConfigCookie { + pub address: Pubkey, + pub account: RealmConfigAccount, } #[derive(Debug)] @@ -44,6 +55,8 @@ pub struct TokenOwnerRecordCookie { pub governance_authority: Option, pub governance_delegate: Keypair, + + pub voter_weight_record: Option, } impl TokenOwnerRecordCookie { @@ -145,3 +158,9 @@ pub struct ProposalInstructionCookie { pub account: ProposalInstruction, pub instruction: Instruction, } + +#[derive(Debug, Clone)] +pub struct VoterWeightRecordCookie { + pub address: Pubkey, + pub account: VoterWeightRecord, +} diff --git a/governance/program/tests/program_test/mod.rs b/governance/program/tests/program_test/mod.rs index 6214dc78903..fb666b3df94 100644 --- a/governance/program/tests/program_test/mod.rs +++ b/governance/program/tests/program_test/mod.rs @@ -7,6 +7,7 @@ use solana_program::{ program_error::ProgramError, program_pack::{IsInitialized, Pack}, pubkey::Pubkey, + system_program, }; use solana_program_test::*; @@ -14,13 +15,15 @@ use solana_program_test::*; use solana_sdk::signature::{Keypair, Signer}; 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, 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, withdraw_governing_tokens, Vote, + 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, + withdraw_governing_tokens, Vote, }, processor::process_instruction, state::{ @@ -41,6 +44,7 @@ use spl_governance::{ get_governing_token_holding_address, get_realm_address, Realm, RealmConfig, RealmConfigArgs, }, + realm_config::{get_realm_config_address, RealmConfigAccount}, signatory_record::{get_signatory_record_address, SignatoryRecord}, token_owner_record::{get_token_owner_record_address, TokenOwnerRecord}, vote_record::{get_vote_record_address, VoteRecord}, @@ -49,7 +53,10 @@ use spl_governance::{ }; pub mod cookies; -use crate::program_test::cookies::SignatoryRecordCookie; + +use crate::program_test::cookies::{ + RealmConfigCookie, SignatoryRecordCookie, VoterWeightRecordCookie, +}; use spl_governance_test_sdk::{ tools::{clone_keypair, NopOverride}, @@ -66,10 +73,24 @@ pub struct GovernanceProgramTest { pub bench: ProgramTestBench, pub next_realm_id: u8, pub program_id: Pubkey, + pub voter_weight_addin_id: Option, } impl GovernanceProgramTest { + #[allow(dead_code)] pub async fn start_new() -> Self { + Self::start_impl(false).await + } + + #[allow(dead_code)] + pub async fn start_with_voter_weight_addin() -> Self { + Self::start_impl(true).await + } + + #[allow(dead_code)] + async fn start_impl(use_voter_weight_addin: bool) -> Self { + let mut programs = vec![]; + let program_id = Pubkey::from_str("Governance111111111111111111111111111111111").unwrap(); let program = TestBenchProgram { @@ -78,25 +99,48 @@ impl GovernanceProgramTest { process_instruction: processor!(process_instruction), }; - let bench = ProgramTestBench::start_new(&[program]).await; + programs.push(program); + + let voter_weight_addin_id = if use_voter_weight_addin { + let voter_weight_addin_id = + Pubkey::from_str("VoterWeight11111111111111111111111111111111").unwrap(); + + let vote_weight_addin = TestBenchProgram { + program_name: "spl_governance_voter_weight_addin", + program_id: voter_weight_addin_id, + process_instruction: None, + }; + + programs.push(vote_weight_addin); + Some(voter_weight_addin_id) + } else { + None + }; + + let bench = ProgramTestBench::start_new(&programs).await; Self { bench, next_realm_id: 0, program_id, + voter_weight_addin_id, } } #[allow(dead_code)] - pub async fn with_realm(&mut self) -> RealmCookie { - let config_args = RealmConfigArgs { + pub fn get_default_realm_config_args(&mut self) -> RealmConfigArgs { + RealmConfigArgs { use_council_mint: true, - community_mint_max_vote_weight_source: MintMaxVoteWeightSource::FULL_SUPPLY_FRACTION, min_community_tokens_to_create_governance: 10, - }; + use_community_voter_weight_addin: self.voter_weight_addin_id.is_some(), + } + } - self.with_realm_using_config_args(&config_args).await + #[allow(dead_code)] + pub async fn with_realm(&mut self) -> RealmCookie { + let realm_config_args = self.get_default_realm_config_args(); + self.with_realm_using_config_args(&realm_config_args).await } #[allow(dead_code)] @@ -157,12 +201,19 @@ impl GovernanceProgramTest { let realm_authority = Keypair::new(); + let community_voter_weight_addin = if config_args.use_community_voter_weight_addin { + self.voter_weight_addin_id + } else { + None + }; + let create_realm_instruction = create_realm( &self.program_id, &realm_authority.pubkey(), &community_token_mint_keypair.pubkey(), &self.bench.payer.pubkey(), council_token_mint_pubkey, + community_voter_weight_addin, name.clone(), config_args.min_community_tokens_to_create_governance, config_args.community_mint_max_vote_weight_source.clone(), @@ -182,16 +233,34 @@ impl GovernanceProgramTest { authority: Some(realm_authority.pubkey()), config: RealmConfig { council_mint: council_token_mint_pubkey, - reserved: [0; 8], + reserved: [0; 7], min_community_tokens_to_create_governance: config_args .min_community_tokens_to_create_governance, community_mint_max_vote_weight_source: config_args .community_mint_max_vote_weight_source .clone(), + use_community_voter_weight_addin: false, }, }; + let realm_config_cookie = if config_args.use_community_voter_weight_addin { + Some(RealmConfigCookie { + address: get_realm_config_address(&self.program_id, &realm_address), + account: RealmConfigAccount { + account_type: GovernanceAccountType::RealmConfig, + realm: realm_address, + community_voter_weight_addin: self.voter_weight_addin_id, + reserved_1: None, + reserved_2: None, + reserved_3: None, + reserved: [0; 128], + }, + }) + } else { + None + }; + RealmCookie { address: realm_address, account, @@ -202,6 +271,7 @@ impl GovernanceProgramTest { council_token_holding_account: council_token_holding_address, council_mint_authority: council_token_mint_authority, realm_authority: Some(realm_authority), + realm_config: realm_config_cookie, } } @@ -224,6 +294,7 @@ impl GovernanceProgramTest { &realm_cookie.account.community_mint, &self.bench.context.payer.pubkey(), Some(council_mint), + None, name.clone(), min_community_tokens_to_create_governance, community_mint_max_vote_weight_source, @@ -243,11 +314,12 @@ impl GovernanceProgramTest { authority: Some(realm_authority.pubkey()), config: RealmConfig { council_mint: Some(council_mint), - reserved: [0; 8], + reserved: [0; 7], community_mint_max_vote_weight_source: MintMaxVoteWeightSource::FULL_SUPPLY_FRACTION, min_community_tokens_to_create_governance, + use_community_voter_weight_addin: false, }, }; @@ -272,6 +344,7 @@ impl GovernanceProgramTest { realm_cookie.council_mint_authority.as_ref().unwrap(), )), realm_authority: Some(realm_authority), + realm_config: None, } } @@ -279,7 +352,7 @@ impl GovernanceProgramTest { pub async fn with_community_token_deposit( &mut self, realm_cookie: &RealmCookie, - ) -> TokenOwnerRecordCookie { + ) -> Result { self.with_initial_governing_token_deposit( &realm_cookie.address, &realm_cookie.account.community_mint, @@ -289,12 +362,64 @@ impl GovernanceProgramTest { .await } + #[allow(dead_code)] + pub async fn with_token_owner_record( + &mut self, + realm_cookie: &RealmCookie, + ) -> TokenOwnerRecordCookie { + let token_owner = Keypair::new(); + + let create_token_owner_record_ix = create_token_owner_record( + &self.program_id, + &realm_cookie.address, + &token_owner.pubkey(), + &realm_cookie.account.community_mint, + &self.bench.payer.pubkey(), + ); + + self.bench + .process_transaction(&[create_token_owner_record_ix], None) + .await + .unwrap(); + + let account = TokenOwnerRecord { + account_type: GovernanceAccountType::TokenOwnerRecord, + realm: realm_cookie.address, + governing_token_mint: realm_cookie.account.community_mint, + governing_token_owner: token_owner.pubkey(), + governing_token_deposit_amount: 0, + governance_delegate: None, + unrelinquished_votes_count: 0, + total_votes_count: 0, + outstanding_proposal_count: 0, + reserved: [0; 7], + }; + + let token_owner_record_address = get_token_owner_record_address( + &self.program_id, + &realm_cookie.address, + &realm_cookie.account.community_mint, + &token_owner.pubkey(), + ); + + TokenOwnerRecordCookie { + address: token_owner_record_address, + account, + token_source_amount: 0, + token_source: Pubkey::new_unique(), + token_owner, + governance_authority: None, + governance_delegate: Keypair::new(), + voter_weight_record: None, + } + } + #[allow(dead_code)] pub async fn with_community_token_deposit_amount( &mut self, realm_cookie: &RealmCookie, amount: u64, - ) -> TokenOwnerRecordCookie { + ) -> Result { self.with_initial_governing_token_deposit( &realm_cookie.address, &realm_cookie.account.community_mint, @@ -343,7 +468,7 @@ impl GovernanceProgramTest { &mut self, realm_cookie: &RealmCookie, amount: u64, - ) -> TokenOwnerRecordCookie { + ) -> Result { self.with_initial_governing_token_deposit( &realm_cookie.address, &realm_cookie.account.config.council_mint.unwrap(), @@ -357,7 +482,7 @@ impl GovernanceProgramTest { pub async fn with_council_token_deposit( &mut self, realm_cookie: &RealmCookie, - ) -> TokenOwnerRecordCookie { + ) -> Result { self.with_initial_governing_token_deposit( &realm_cookie.address, &realm_cookie.account.config.council_mint.unwrap(), @@ -374,7 +499,7 @@ impl GovernanceProgramTest { governing_mint: &Pubkey, governing_mint_authority: &Keypair, amount: u64, - ) -> TokenOwnerRecordCookie { + ) -> Result { let token_owner = Keypair::new(); let token_source = Keypair::new(); @@ -406,8 +531,7 @@ impl GovernanceProgramTest { &[deposit_governing_tokens_instruction], Some(&[&token_owner]), ) - .await - .unwrap(); + .await?; let token_owner_record_address = get_token_owner_record_address( &self.program_id, @@ -431,7 +555,7 @@ impl GovernanceProgramTest { let governance_delegate = Keypair::from_base58_string(&token_owner.to_base58_string()); - TokenOwnerRecordCookie { + Ok(TokenOwnerRecordCookie { address: token_owner_record_address, account, @@ -440,7 +564,8 @@ impl GovernanceProgramTest { token_owner, governance_authority: None, governance_delegate, - } + voter_weight_record: None, + }) } #[allow(dead_code)] @@ -622,9 +747,9 @@ impl GovernanceProgramTest { pub async fn set_realm_config( &mut self, realm_cookie: &mut RealmCookie, - config_args: &RealmConfigArgs, + realm_config_args: &RealmConfigArgs, ) -> Result<(), ProgramError> { - self.set_realm_config_using_instruction(realm_cookie, config_args, NopOverride, None) + self.set_realm_config_using_instruction(realm_cookie, realm_config_args, NopOverride, None) .await } @@ -632,23 +757,33 @@ impl GovernanceProgramTest { pub async fn set_realm_config_using_instruction( &mut self, realm_cookie: &mut RealmCookie, - config_args: &RealmConfigArgs, + realm_config_args: &RealmConfigArgs, instruction_override: F, signers_override: Option<&[&Keypair]>, ) -> Result<(), ProgramError> { - let council_token_mint = if config_args.use_council_mint { + let council_token_mint = if realm_config_args.use_council_mint { realm_cookie.account.config.council_mint } else { None }; + let community_voter_weight_addin = if realm_config_args.use_community_voter_weight_addin { + self.voter_weight_addin_id + } else { + None + }; + let mut set_realm_config_ix = set_realm_config( &self.program_id, &realm_cookie.address, &realm_cookie.realm_authority.as_ref().unwrap().pubkey(), council_token_mint, - config_args.min_community_tokens_to_create_governance, - config_args.community_mint_max_vote_weight_source.clone(), + &self.bench.payer.pubkey(), + community_voter_weight_addin, + realm_config_args.min_community_tokens_to_create_governance, + realm_config_args + .community_mint_max_vote_weight_source + .clone(), ); instruction_override(&mut set_realm_config_ix); @@ -660,8 +795,31 @@ impl GovernanceProgramTest { realm_cookie .account .config - .community_mint_max_vote_weight_source = - config_args.community_mint_max_vote_weight_source.clone(); + .community_mint_max_vote_weight_source = realm_config_args + .community_mint_max_vote_weight_source + .clone(); + + if realm_config_args.use_community_voter_weight_addin { + let community_voter_weight_addin_index = if realm_config_args.use_council_mint { + 7 + } else { + 5 + }; + realm_cookie.realm_config = Some(RealmConfigCookie { + address: get_realm_config_address(&self.program_id, &realm_cookie.address), + account: RealmConfigAccount { + account_type: GovernanceAccountType::RealmConfig, + realm: realm_cookie.address, + community_voter_weight_addin: Some( + set_realm_config_ix.accounts[community_voter_weight_addin_index].pubkey, + ), + reserved_1: None, + reserved_2: None, + reserved_3: None, + reserved: [0; 128], + }, + }) + } self.bench .process_transaction(&[set_realm_config_ix], Some(signers)) @@ -820,12 +978,21 @@ impl GovernanceProgramTest { token_owner_record_cookie: &TokenOwnerRecordCookie, governance_config: &GovernanceConfig, ) -> Result { + let voter_weight_record = + if let Some(voter_weight_record) = &token_owner_record_cookie.voter_weight_record { + Some(voter_weight_record.address) + } else { + None + }; + let create_account_governance_instruction = create_account_governance( &self.program_id, &realm_cookie.address, &governed_account_cookie.address, &token_owner_record_cookie.address, &self.bench.payer.pubkey(), + &token_owner_record_cookie.token_owner.pubkey(), + voter_weight_record, governance_config.clone(), ); @@ -839,7 +1006,10 @@ impl GovernanceProgramTest { }; self.bench - .process_transaction(&[create_account_governance_instruction], None) + .process_transaction( + &[create_account_governance_instruction], + Some(&[&token_owner_record_cookie.token_owner]), + ) .await?; let account_governance_address = get_account_governance_address( @@ -957,6 +1127,13 @@ impl GovernanceProgramTest { ) -> Result { let config = self.get_default_governance_config(); + let voter_weight_record = + if let Some(voter_weight_record) = &token_owner_record_cookie.voter_weight_record { + Some(voter_weight_record.address) + } else { + None + }; + let mut create_program_governance_instruction = create_program_governance( &self.program_id, &realm_cookie.address, @@ -964,13 +1141,18 @@ impl GovernanceProgramTest { &governed_program_cookie.upgrade_authority.pubkey(), &token_owner_record_cookie.address, &self.bench.payer.pubkey(), + &token_owner_record_cookie.token_owner.pubkey(), + voter_weight_record, config.clone(), governed_program_cookie.transfer_upgrade_authority, ); instruction_override(&mut create_program_governance_instruction); - let default_signers = &[&governed_program_cookie.upgrade_authority]; + let default_signers = &[ + &governed_program_cookie.upgrade_authority, + &token_owner_record_cookie.token_owner, + ]; let signers = signers_override.unwrap_or(default_signers); self.bench @@ -1027,6 +1209,13 @@ impl GovernanceProgramTest { ) -> Result { let config = self.get_default_governance_config(); + let voter_weight_record = + if let Some(voter_weight_record) = &token_owner_record_cookie.voter_weight_record { + Some(voter_weight_record.address) + } else { + None + }; + let mut create_mint_governance_instruction = create_mint_governance( &self.program_id, &realm_cookie.address, @@ -1034,13 +1223,18 @@ impl GovernanceProgramTest { &governed_mint_cookie.mint_authority.pubkey(), &token_owner_record_cookie.address, &self.bench.payer.pubkey(), + &token_owner_record_cookie.token_owner.pubkey(), + voter_weight_record, config.clone(), governed_mint_cookie.transfer_mint_authority, ); instruction_override(&mut create_mint_governance_instruction); - let default_signers = &[&governed_mint_cookie.mint_authority]; + let default_signers = &[ + &governed_mint_cookie.mint_authority, + &token_owner_record_cookie.token_owner, + ]; let signers = signers_override.unwrap_or(default_signers); self.bench @@ -1097,6 +1291,13 @@ impl GovernanceProgramTest { ) -> Result { let config = self.get_default_governance_config(); + let voter_weight_record = + if let Some(voter_weight_record) = &token_owner_record_cookie.voter_weight_record { + Some(voter_weight_record.address) + } else { + None + }; + let mut create_token_governance_instruction = create_token_governance( &self.program_id, &realm_cookie.address, @@ -1104,13 +1305,18 @@ impl GovernanceProgramTest { &governed_token_cookie.token_owner.pubkey(), &token_owner_record_cookie.address, &self.bench.payer.pubkey(), + &token_owner_record_cookie.token_owner.pubkey(), + voter_weight_record, config.clone(), governed_token_cookie.transfer_token_owner, ); instruction_override(&mut create_token_governance_instruction); - let default_signers = &[&governed_token_cookie.token_owner]; + let default_signers = &[ + &governed_token_cookie.token_owner, + &token_owner_record_cookie.token_owner, + ]; let signers = signers_override.unwrap_or(default_signers); self.bench @@ -1189,12 +1395,20 @@ impl GovernanceProgramTest { let governance_authority = token_owner_record_cookie.get_governance_authority(); + let voter_weight_record = + if let Some(voter_weight_record) = &token_owner_record_cookie.voter_weight_record { + Some(voter_weight_record.address) + } else { + None + }; + let mut create_proposal_instruction = create_proposal( &self.program_id, &governance_cookie.address, &token_owner_record_cookie.address, &governance_authority.pubkey(), &self.bench.payer.pubkey(), + voter_weight_record, &governance_cookie.account.realm, name.clone(), description_link.clone(), @@ -1465,6 +1679,13 @@ impl GovernanceProgramTest { token_owner_record_cookie: &TokenOwnerRecordCookie, vote: Vote, ) -> Result { + let voter_weight_record = + if let Some(voter_weight_record) = &token_owner_record_cookie.voter_weight_record { + Some(voter_weight_record.address) + } else { + None + }; + let vote_instruction = cast_vote( &self.program_id, &token_owner_record_cookie.account.realm, @@ -1475,6 +1696,7 @@ impl GovernanceProgramTest { &token_owner_record_cookie.token_owner.pubkey(), &proposal_cookie.account.governing_token_mint, &self.bench.payer.pubkey(), + voter_weight_record, vote.clone(), ); @@ -1847,9 +2069,17 @@ impl GovernanceProgramTest { } #[allow(dead_code)] - pub async fn get_realm_account(&mut self, root_governance_address: &Pubkey) -> Realm { + pub async fn get_realm_account(&mut self, realm_address: &Pubkey) -> Realm { + self.bench.get_borsh_account::(realm_address).await + } + + #[allow(dead_code)] + pub async fn get_realm_config_data( + &mut self, + realm_config_address: &Pubkey, + ) -> RealmConfigAccount { self.bench - .get_borsh_account::(root_governance_address) + .get_borsh_account::(realm_config_address) .await } @@ -1951,4 +2181,56 @@ impl GovernanceProgramTest { pub async fn get_mint_account(&mut self, address: &Pubkey) -> spl_token::state::Mint { self.get_packed_account(address).await } + + /// ----------- VoterWeight Addin ----------------------------- + #[allow(dead_code)] + pub async fn with_voter_weight_addin_deposit( + &mut self, + token_owner_record_cookie: &mut TokenOwnerRecordCookie, + ) -> Result { + let voter_weight_record_account = Keypair::new(); + + // Governance program has no dependency on the voter-weight-addin program and hence we can't use its instruction creator here + // and the instruction has to be created manually + // TODO: Currently the addin spl_governance_voter_weight_addin.so must be manually copied to tests/fixtures to work on CI + // We should automate this step as part of the build to build the addin before governance + let accounts = vec![ + AccountMeta::new_readonly(self.program_id, false), + AccountMeta::new_readonly(token_owner_record_cookie.account.realm, false), + AccountMeta::new_readonly( + token_owner_record_cookie.account.governing_token_mint, + false, + ), + AccountMeta::new_readonly(token_owner_record_cookie.address, false), + AccountMeta::new(voter_weight_record_account.pubkey(), true), + AccountMeta::new_readonly(self.bench.payer.pubkey(), true), + AccountMeta::new_readonly(system_program::id(), false), + ]; + + let deposit_ix = Instruction { + program_id: self.voter_weight_addin_id.unwrap(), + accounts, + data: vec![1, 100, 0, 0, 0, 0, 0, 0, 0], // 1 - Deposit instruction, 100 amount (u64) + }; + + self.bench + .process_transaction(&[deposit_ix], Some(&[&voter_weight_record_account])) + .await?; + + let voter_weight_record_cookie = VoterWeightRecordCookie { + address: voter_weight_record_account.pubkey(), + account: VoterWeightRecord { + account_type: VoterWeightAccountType::VoterWeightRecord, + realm: token_owner_record_cookie.account.realm, + governing_token_mint: token_owner_record_cookie.account.governing_token_mint, + governing_token_owner: token_owner_record_cookie.account.governing_token_owner, + voter_weight: 100, + voter_weight_expiry: None, + }, + }; + + token_owner_record_cookie.voter_weight_record = Some(voter_weight_record_cookie.clone()); + + Ok(voter_weight_record_cookie) + } } diff --git a/governance/program/tests/setup_realm_with_voter_weight_addin.rs b/governance/program/tests/setup_realm_with_voter_weight_addin.rs new file mode 100644 index 00000000000..9218347e0cc --- /dev/null +++ b/governance/program/tests/setup_realm_with_voter_weight_addin.rs @@ -0,0 +1,217 @@ +#![cfg(feature = "test-bpf")] + +use solana_program::pubkey::Pubkey; +use solana_program_test::*; + +mod program_test; + +use program_test::*; + +#[tokio::test] +async fn test_create_realm_with_voter_weight_addin() { + // Arrange + let mut governance_test = GovernanceProgramTest::start_with_voter_weight_addin().await; + + // Act + + let realm_cookie = governance_test.with_realm().await; + + // Assert + + let realm_account_data = governance_test + .get_realm_account(&realm_cookie.address) + .await; + + assert!(realm_account_data.config.use_community_voter_weight_addin); + + let realm_config_cookie = realm_cookie.realm_config.unwrap(); + + let realm_config_data = governance_test + .get_realm_config_data(&realm_config_cookie.address) + .await; + + assert_eq!(realm_config_cookie.account, realm_config_data); +} + +#[tokio::test] +async fn test_set_realm_voter_weight_addin_for_realm_without_addins() { + // Arrange + let mut governance_test = GovernanceProgramTest::start_with_voter_weight_addin().await; + + let mut realm_config_args = governance_test.get_default_realm_config_args(); + realm_config_args.use_community_voter_weight_addin = false; + + let mut realm_cookie = governance_test + .with_realm_using_config_args(&realm_config_args) + .await; + + realm_config_args.use_community_voter_weight_addin = true; + + // Act + + governance_test + .set_realm_config(&mut realm_cookie, &realm_config_args) + .await + .unwrap(); + + // Assert + + let realm_account_data = governance_test + .get_realm_account(&realm_cookie.address) + .await; + + assert!(realm_account_data.config.use_community_voter_weight_addin); + + let realm_config_cookie = realm_cookie.realm_config.unwrap(); + + let realm_config_data = governance_test + .get_realm_config_data(&realm_config_cookie.address) + .await; + + assert_eq!(realm_config_cookie.account, realm_config_data); +} + +#[tokio::test] +async fn test_set_realm_voter_weight_addin_for_realm_without_council_and_addins() { + // Arrange + let mut governance_test = GovernanceProgramTest::start_with_voter_weight_addin().await; + + let mut realm_config_args = governance_test.get_default_realm_config_args(); + realm_config_args.use_community_voter_weight_addin = false; + realm_config_args.use_council_mint = false; + + let mut realm_cookie = governance_test + .with_realm_using_config_args(&realm_config_args) + .await; + + realm_config_args.use_community_voter_weight_addin = true; + + // Act + + governance_test + .set_realm_config(&mut realm_cookie, &realm_config_args) + .await + .unwrap(); + + // Assert + + let realm_account_data = governance_test + .get_realm_account(&realm_cookie.address) + .await; + + assert!(realm_account_data.config.use_community_voter_weight_addin); + + let realm_config_cookie = realm_cookie.realm_config.unwrap(); + + let realm_config_data = governance_test + .get_realm_config_data(&realm_config_cookie.address) + .await; + + assert_eq!(realm_config_cookie.account, realm_config_data); +} + +#[tokio::test] +async fn test_set_realm_voter_weight_addin_for_realm_with_existing_voter_weight_addin() { + // Arrange + let mut governance_test = GovernanceProgramTest::start_with_voter_weight_addin().await; + + let mut realm_cookie = governance_test.with_realm().await; + + let mut realm_config_args = governance_test.get_default_realm_config_args(); + realm_config_args.use_community_voter_weight_addin = true; + + let community_voter_weight_addin_address = Pubkey::new_unique(); + + // Act + + governance_test + .set_realm_config_using_instruction( + &mut realm_cookie, + &realm_config_args, + |i| i.accounts[7].pubkey = community_voter_weight_addin_address, + None, + ) + .await + .unwrap(); + + // Assert + + let realm_account_data = governance_test + .get_realm_account(&realm_cookie.address) + .await; + + assert!(realm_account_data.config.use_community_voter_weight_addin); + + let realm_config_cookie = realm_cookie.realm_config.unwrap(); + + let realm_config_data = governance_test + .get_realm_config_data(&realm_config_cookie.address) + .await; + + assert_eq!(realm_config_cookie.account, realm_config_data); + assert_eq!( + realm_config_data.community_voter_weight_addin, + Some(community_voter_weight_addin_address) + ); +} + +#[tokio::test] +async fn test_set_realm_config_with_no_voter_weight_addin_for_realm_without_addins() { + // Arrange + let mut governance_test = GovernanceProgramTest::start_with_voter_weight_addin().await; + + let mut realm_config_args = governance_test.get_default_realm_config_args(); + realm_config_args.use_community_voter_weight_addin = false; + + let mut realm_cookie = governance_test + .with_realm_using_config_args(&realm_config_args) + .await; + + realm_config_args.use_community_voter_weight_addin = false; + + // Act + + governance_test + .set_realm_config(&mut realm_cookie, &realm_config_args) + .await + .unwrap(); + + // Assert + + let realm_account_data = governance_test + .get_realm_account(&realm_cookie.address) + .await; + + assert!(!realm_account_data.config.use_community_voter_weight_addin); +} + +#[tokio::test] +async fn test_set_realm_config_with_no_voter_weight_addin_for_realm_with_existing_addin() { + // Arrange + let mut governance_test = GovernanceProgramTest::start_with_voter_weight_addin().await; + let mut realm_cookie = governance_test.with_realm().await; + + let mut realm_config_args = governance_test.get_default_realm_config_args(); + realm_config_args.use_community_voter_weight_addin = false; + + // Act + + governance_test + .set_realm_config(&mut realm_cookie, &realm_config_args) + .await + .unwrap(); + + // Assert + + let realm_account_data = governance_test + .get_realm_account(&realm_cookie.address) + .await; + + assert!(!realm_account_data.config.use_community_voter_weight_addin); + + let realm_config_data = governance_test + .get_realm_config_data(&realm_cookie.realm_config.unwrap().address) + .await; + + assert!(realm_config_data.community_voter_weight_addin.is_none()); +} diff --git a/governance/program/tests/use_realm_with_voter_weight_addin.rs b/governance/program/tests/use_realm_with_voter_weight_addin.rs new file mode 100644 index 00000000000..13dc0a1ff97 --- /dev/null +++ b/governance/program/tests/use_realm_with_voter_weight_addin.rs @@ -0,0 +1,261 @@ +#![cfg(feature = "test-bpf")] + +use solana_program_test::*; + +mod program_test; + +use program_test::*; +use spl_governance::{error::GovernanceError, instruction::Vote, state::enums::VoteWeight}; + +#[tokio::test] +async fn test_create_account_governance_with_voter_weight_addin() { + // Arrange + let mut governance_test = GovernanceProgramTest::start_with_voter_weight_addin().await; + let governed_account_cookie = governance_test.with_governed_account().await; + + let realm_cookie = governance_test.with_realm().await; + + let mut token_owner_record_cookie = + governance_test.with_token_owner_record(&realm_cookie).await; + + governance_test + .with_voter_weight_addin_deposit(&mut token_owner_record_cookie) + .await + .unwrap(); + + // Act + let account_governance_cookie = governance_test + .with_account_governance( + &realm_cookie, + &governed_account_cookie, + &token_owner_record_cookie, + ) + .await + .unwrap(); + + // // Assert + let account_governance_account = governance_test + .get_governance_account(&account_governance_cookie.address) + .await; + + assert_eq!( + account_governance_cookie.account, + account_governance_account + ); +} + +#[tokio::test] +async fn test_create_proposal_with_voter_weight_addin() { + // Arrange + let mut governance_test = GovernanceProgramTest::start_with_voter_weight_addin().await; + let governed_account_cookie = governance_test.with_governed_account().await; + + let realm_cookie = governance_test.with_realm().await; + + let mut token_owner_record_cookie = + governance_test.with_token_owner_record(&realm_cookie).await; + + governance_test + .with_voter_weight_addin_deposit(&mut token_owner_record_cookie) + .await + .unwrap(); + + let mut account_governance_cookie = governance_test + .with_account_governance( + &realm_cookie, + &governed_account_cookie, + &token_owner_record_cookie, + ) + .await + .unwrap(); + + // Act + let proposal_cookie = governance_test + .with_proposal(&token_owner_record_cookie, &mut account_governance_cookie) + .await + .unwrap(); + + // // Assert + let proposal_account = governance_test + .get_proposal_account(&proposal_cookie.address) + .await; + + assert_eq!(proposal_cookie.account, proposal_account); +} + +#[tokio::test] +async fn test_cast_vote_with_voter_weight_addin() { + // Arrange + let mut governance_test = GovernanceProgramTest::start_with_voter_weight_addin().await; + let governed_account_cookie = governance_test.with_governed_account().await; + + let realm_cookie = governance_test.with_realm().await; + + let mut token_owner_record_cookie = + governance_test.with_token_owner_record(&realm_cookie).await; + + governance_test + .with_voter_weight_addin_deposit(&mut token_owner_record_cookie) + .await + .unwrap(); + + let mut account_governance_cookie = governance_test + .with_account_governance( + &realm_cookie, + &governed_account_cookie, + &token_owner_record_cookie, + ) + .await + .unwrap(); + + let proposal_cookie = governance_test + .with_signed_off_proposal(&token_owner_record_cookie, &mut account_governance_cookie) + .await + .unwrap(); + + // Act + let vote_record_cookie = governance_test + .with_cast_vote(&proposal_cookie, &token_owner_record_cookie, Vote::Yes) + .await + .unwrap(); + + // Assert + + let vote_record_account = governance_test + .get_vote_record_account(&vote_record_cookie.address) + .await; + + assert_eq!(VoteWeight::Yes(100), vote_record_account.vote_weight); + + let proposal_account = governance_test + .get_proposal_account(&proposal_cookie.address) + .await; + + assert_eq!(100, proposal_account.yes_votes_count); +} + +#[tokio::test] +async fn test_create_token_governance_with_voter_weight_addin() { + // Arrange + let mut governance_test = GovernanceProgramTest::start_with_voter_weight_addin().await; + let governed_token_cookie = governance_test.with_governed_token().await; + + let realm_cookie = governance_test.with_realm().await; + + let mut token_owner_record_cookie = + governance_test.with_token_owner_record(&realm_cookie).await; + + governance_test + .with_voter_weight_addin_deposit(&mut token_owner_record_cookie) + .await + .unwrap(); + + // Act + let token_governance_cookie = governance_test + .with_token_governance( + &realm_cookie, + &governed_token_cookie, + &token_owner_record_cookie, + ) + .await + .unwrap(); + + // // Assert + let token_governance_account = governance_test + .get_governance_account(&token_governance_cookie.address) + .await; + + assert_eq!(token_governance_cookie.account, token_governance_account); +} + +#[tokio::test] +async fn test_create_mint_governance_with_voter_weight_addin() { + // Arrange + let mut governance_test = GovernanceProgramTest::start_with_voter_weight_addin().await; + let governed_mint_cookie = governance_test.with_governed_mint().await; + + let realm_cookie = governance_test.with_realm().await; + + let mut token_owner_record_cookie = + governance_test.with_token_owner_record(&realm_cookie).await; + + governance_test + .with_voter_weight_addin_deposit(&mut token_owner_record_cookie) + .await + .unwrap(); + + // Act + let mint_governance_cookie = governance_test + .with_mint_governance( + &realm_cookie, + &governed_mint_cookie, + &token_owner_record_cookie, + ) + .await + .unwrap(); + + // // Assert + let mint_governance_account = governance_test + .get_governance_account(&mint_governance_cookie.address) + .await; + + assert_eq!(mint_governance_cookie.account, mint_governance_account); +} + +#[tokio::test] +async fn test_create_program_governance_with_voter_weight_addin() { + // Arrange + let mut governance_test = GovernanceProgramTest::start_with_voter_weight_addin().await; + let governed_program_cookie = governance_test.with_governed_program().await; + + let realm_cookie = governance_test.with_realm().await; + + let mut token_owner_record_cookie = + governance_test.with_token_owner_record(&realm_cookie).await; + + governance_test + .with_voter_weight_addin_deposit(&mut token_owner_record_cookie) + .await + .unwrap(); + + // Act + let program_governance_cookie = governance_test + .with_program_governance( + &realm_cookie, + &governed_program_cookie, + &token_owner_record_cookie, + ) + .await + .unwrap(); + + // Assert + let program_governance_account = governance_test + .get_governance_account(&program_governance_cookie.address) + .await; + + assert_eq!( + program_governance_cookie.account, + program_governance_account + ); +} + +#[tokio::test] +async fn test_realm_with_voter_weight_addin_with_deposits_not_allowed() { + // Arrange + let mut governance_test = GovernanceProgramTest::start_with_voter_weight_addin().await; + let realm_cookie = governance_test.with_realm().await; + + // Act + + let err = governance_test + .with_community_token_deposit(&realm_cookie) + .await + .err() + .unwrap(); + + // Assert + assert_eq!( + err, + GovernanceError::GoverningTokenDepositsNotAllowed.into() + ); +} diff --git a/governance/voter-weight-addin/README.md b/governance/voter-weight-addin/README.md new file mode 100644 index 00000000000..5cac8570787 --- /dev/null +++ b/governance/voter-weight-addin/README.md @@ -0,0 +1,3 @@ +# Governance Voter Weight Addin + +Governance Voter Weight Addin is a skeleton program which can be used as a template to create custom voter weight addins diff --git a/governance/voter-weight-addin/program/Cargo.toml b/governance/voter-weight-addin/program/Cargo.toml new file mode 100644 index 00000000000..abc5468551e --- /dev/null +++ b/governance/voter-weight-addin/program/Cargo.toml @@ -0,0 +1,39 @@ +[package] +name = "spl-governance-voter-weight-addin" +version = "0.1.0" +description = "Solana Program Library Governance Voter Weight Addin Program" +authors = ["Solana Maintainers "] +repository = "https://github.com/solana-labs/solana-program-library" +license = "Apache-2.0" +edition = "2018" + +[features] +no-entrypoint = [] +test-bpf = [] + +[dependencies] +arrayref = "0.3.6" +bincode = "1.3.2" +borsh = "0.9.1" +num-derive = "0.3" +num-traits = "0.2" +serde = "1.0.127" +serde_derive = "1.0.103" +solana-program = "1.7.11" +spl-token = { version = "3.2", path = "../../../token/program", features = [ "no-entrypoint" ] } +spl-governance= { version = "2.1.1", path ="../../program", features = [ "no-entrypoint" ]} +spl-governance-chat= { version = "0.1.0", path ="../../chat/program", features = [ "no-entrypoint" ]} +thiserror = "1.0" + + +[dev-dependencies] +assert_matches = "1.5.0" +base64 = "0.13" +proptest = "1.0" +solana-program-test = "1.7.11" +solana-sdk = "1.7.11" +spl-governance-test-sdk = { version = "0.1.0", path ="../../test-sdk"} + + +[lib] +crate-type = ["cdylib", "lib"] diff --git a/governance/voter-weight-addin/program/Xargo.toml b/governance/voter-weight-addin/program/Xargo.toml new file mode 100644 index 00000000000..1744f098ae1 --- /dev/null +++ b/governance/voter-weight-addin/program/Xargo.toml @@ -0,0 +1,2 @@ +[target.bpfel-unknown-unknown.dependencies.std] +features = [] \ No newline at end of file diff --git a/governance/voter-weight-addin/program/src/entrypoint.rs b/governance/voter-weight-addin/program/src/entrypoint.rs new file mode 100644 index 00000000000..cfef2217179 --- /dev/null +++ b/governance/voter-weight-addin/program/src/entrypoint.rs @@ -0,0 +1,22 @@ +//! Program entrypoint +#![cfg(all(target_arch = "bpf", not(feature = "no-entrypoint")))] + +use crate::{error::VoterWeightAddinError, processor}; +use solana_program::{ + account_info::AccountInfo, entrypoint, entrypoint::ProgramResult, + program_error::PrintProgramError, pubkey::Pubkey, +}; + +entrypoint!(process_instruction); +fn process_instruction( + program_id: &Pubkey, + accounts: &[AccountInfo], + instruction_data: &[u8], +) -> ProgramResult { + if let Err(error) = processor::process_instruction(program_id, accounts, instruction_data) { + // catch the error so we can print it + error.print::(); + return Err(error); + } + Ok(()) +} diff --git a/governance/voter-weight-addin/program/src/error.rs b/governance/voter-weight-addin/program/src/error.rs new file mode 100644 index 00000000000..10dd08d4e40 --- /dev/null +++ b/governance/voter-weight-addin/program/src/error.rs @@ -0,0 +1,31 @@ +//! Error types + +use num_derive::FromPrimitive; +use solana_program::{ + decode_error::DecodeError, + msg, + program_error::{PrintProgramError, ProgramError}, +}; +use thiserror::Error; + +/// Errors that may be returned by the VoterWeightAddin program +#[derive(Clone, Debug, Eq, Error, FromPrimitive, PartialEq)] +pub enum VoterWeightAddinError {} + +impl PrintProgramError for VoterWeightAddinError { + fn print(&self) { + msg!("VOTER-WEIGHT-ADDIN-ERROR: {}", &self.to_string()); + } +} + +impl From for ProgramError { + fn from(e: VoterWeightAddinError) -> Self { + ProgramError::Custom(e as u32) + } +} + +impl DecodeError for VoterWeightAddinError { + fn type_of() -> &'static str { + "Voter Weight Addin Error" + } +} diff --git a/governance/voter-weight-addin/program/src/instruction.rs b/governance/voter-weight-addin/program/src/instruction.rs new file mode 100644 index 00000000000..b94ebe599ba --- /dev/null +++ b/governance/voter-weight-addin/program/src/instruction.rs @@ -0,0 +1,42 @@ +//! Program instructions + +use borsh::{BorshDeserialize, BorshSchema, BorshSerialize}; + +/// Instructions supported by the VoterWeightInstruction addin program +#[derive(Clone, Debug, PartialEq, BorshDeserialize, BorshSerialize, BorshSchema)] +#[allow(clippy::large_enum_variant)] +pub enum VoterWeightAddinInstruction { + /// Revises voter weight providing up to date voter weight + /// + /// 0. `[]` Governance Program Id + /// 1. `[]` Realm account + /// 2. `[]` Governing Token mint + /// 3. `[]` TokenOwnerRecord + /// 4. `[writable]` VoterWeightRecord + Revise {}, + + /// Deposits governing token + /// 0. `[]` Governance Program Id + /// 1. `[]` Realm account + /// 2. `[]` Governing Token mint + /// 3. `[]` TokenOwnerRecord + /// 4. `[writable]` VoterWeightRecord + /// 5. `[signer]` Payer + /// 6. `[]` System + Deposit { + /// The deposit amount + #[allow(dead_code)] + amount: u64, + }, + + /// Withdraws deposited tokens + /// Note: This instruction should ensure the tokens can be withdrawn form the Realm + /// by calling TokenOwnerRecord.assert_can_withdraw_governing_tokens() + /// + /// 0. `[]` Governance Program Id + /// 1. `[]` Realm account + /// 2. `[]` Governing Token mint + /// 3. `[]` TokenOwnerRecord + /// 4. `[writable]` VoterWeightRecord + Withdraw {}, +} diff --git a/governance/voter-weight-addin/program/src/lib.rs b/governance/voter-weight-addin/program/src/lib.rs new file mode 100644 index 00000000000..53b71f4a339 --- /dev/null +++ b/governance/voter-weight-addin/program/src/lib.rs @@ -0,0 +1,12 @@ +#![deny(missing_docs)] +//! Governance VoterWeight Addin program + +pub mod entrypoint; +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; diff --git a/governance/voter-weight-addin/program/src/processor.rs b/governance/voter-weight-addin/program/src/processor.rs new file mode 100644 index 00000000000..01cacd7ace2 --- /dev/null +++ b/governance/voter-weight-addin/program/src/processor.rs @@ -0,0 +1,84 @@ +//! Program processor + +use borsh::BorshDeserialize; +use spl_governance::{ + addins::voter_weight::{VoterWeightAccountType, VoterWeightRecord}, + state::token_owner_record::get_token_owner_record_data_for_realm_and_governing_mint, +}; +// TODO: Move to shared governance tools +use spl_governance_chat::tools::account::create_and_serialize_account; + +use solana_program::{ + account_info::{next_account_info, AccountInfo}, + entrypoint::ProgramResult, + msg, + program_error::ProgramError, + pubkey::Pubkey, +}; + +use crate::instruction::VoterWeightAddinInstruction; + +/// Processes an instruction +pub fn process_instruction( + program_id: &Pubkey, + accounts: &[AccountInfo], + input: &[u8], +) -> ProgramResult { + let instruction = VoterWeightAddinInstruction::try_from_slice(input) + .map_err(|_| ProgramError::InvalidInstructionData)?; + + msg!("GOVERNANCE-VOTER-WEIGHT-INSTRUCTION: {:?}", instruction); + + match instruction { + VoterWeightAddinInstruction::Revise {} => Ok(()), + VoterWeightAddinInstruction::Deposit { amount } => { + process_deposit(program_id, accounts, amount) + } + VoterWeightAddinInstruction::Withdraw {} => Ok(()), + } +} + +/// Processes Deposit instruction +pub fn process_deposit( + program_id: &Pubkey, + accounts: &[AccountInfo], + amount: u64, +) -> ProgramResult { + let account_info_iter = &mut accounts.iter(); + + let governance_program_info = next_account_info(account_info_iter)?; // 0 + let realm_info = next_account_info(account_info_iter)?; // 1 + let governing_token_mint_info = next_account_info(account_info_iter)?; // 2 + let token_owner_record_info = next_account_info(account_info_iter)?; // 3 + let voter_weight_record_info = next_account_info(account_info_iter)?; // 4 + let payer_info = next_account_info(account_info_iter)?; // 5 + let system_info = next_account_info(account_info_iter)?; // 6 + + let token_owner_record_data = get_token_owner_record_data_for_realm_and_governing_mint( + governance_program_info.key, + token_owner_record_info, + realm_info.key, + governing_token_mint_info.key, + )?; + + // TODO: Custom deposit logic and validation goes here + + let voter_weight_record_data = VoterWeightRecord { + account_type: VoterWeightAccountType::VoterWeightRecord, + realm: *realm_info.key, + governing_token_mint: *governing_token_mint_info.key, + governing_token_owner: token_owner_record_data.governing_token_owner, + voter_weight: amount, + voter_weight_expiry: None, + }; + + create_and_serialize_account( + payer_info, + voter_weight_record_info, + &voter_weight_record_data, + program_id, + system_info, + )?; + + Ok(()) +}