diff --git a/Cargo.lock b/Cargo.lock index e28bcf39..8e8ec7b1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1076,6 +1076,22 @@ dependencies = [ "spl-token", ] +[[package]] +name = "gpl-squads-voter" +version = "0.0.1" +dependencies = [ + "anchor-lang", + "anchor-spl", + "arrayref", + "borsh", + "solana-program", + "solana-program-test", + "solana-sdk", + "spl-governance", + "spl-governance-tools", + "spl-token", +] + [[package]] name = "h2" version = "0.3.12" diff --git a/programs/squads-voter/Cargo.toml b/programs/squads-voter/Cargo.toml new file mode 100644 index 00000000..edd39fa1 --- /dev/null +++ b/programs/squads-voter/Cargo.toml @@ -0,0 +1,31 @@ +[package] +name = "gpl-squads-voter" +version = "0.0.1" +description = "SPL Governance addin implementing Squads Protocol based governance" +license = "Apache-2.0" +edition = "2018" + +[lib] +crate-type = ["cdylib", "lib"] +name = "gpl_squads_voter" + +[features] +no-entrypoint = [] +no-idl = [] +no-log-ix-name = [] +cpi = ["no-entrypoint"] +default = [] + +[dependencies] +arrayref = "0.3.6" +anchor-lang = { version = "0.24.2", features = ["init-if-needed"] } +anchor-spl = "0.24.2" +solana-program = "1.9.13" +spl-governance = { version = "2.2.2", features = ["no-entrypoint"] } +spl-governance-tools= "0.1.2" +spl-token = { version = "3.3", features = [ "no-entrypoint" ] } + +[dev-dependencies] +borsh = "0.9.1" +solana-sdk = "1.9.5" +solana-program-test = "1.9.13" \ No newline at end of file diff --git a/programs/squads-voter/Xargo.toml b/programs/squads-voter/Xargo.toml new file mode 100644 index 00000000..475fb71e --- /dev/null +++ b/programs/squads-voter/Xargo.toml @@ -0,0 +1,2 @@ +[target.bpfel-unknown-unknown.dependencies.std] +features = [] diff --git a/programs/squads-voter/src/error.rs b/programs/squads-voter/src/error.rs new file mode 100644 index 00000000..44b1d538 --- /dev/null +++ b/programs/squads-voter/src/error.rs @@ -0,0 +1,31 @@ +use anchor_lang::prelude::*; + +#[error_code] +pub enum SquadsVoterError { + #[msg("Invalid Realm Authority")] + InvalidRealmAuthority, + + #[msg("Invalid Realm for Registrar")] + InvalidRealmForRegistrar, + + #[msg("Invalid MaxVoterWeightRecord Realm")] + InvalidMaxVoterWeightRecordRealm, + + #[msg("Invalid MaxVoterWeightRecord Mint")] + InvalidMaxVoterWeightRecordMint, + + #[msg("Invalid VoterWeightRecord Realm")] + InvalidVoterWeightRecordRealm, + + #[msg("Invalid VoterWeightRecord Mint")] + InvalidVoterWeightRecordMint, + + #[msg("Invalid TokenOwner for VoterWeightRecord")] + InvalidTokenOwnerForVoterWeightRecord, + + #[msg("Squad not found")] + SquadNotFound, + + #[msg("Duplicated Squad detected")] + DuplicatedSquadDetected, +} diff --git a/programs/squads-voter/src/instructions/configure_squad.rs b/programs/squads-voter/src/instructions/configure_squad.rs new file mode 100644 index 00000000..40cf1de1 --- /dev/null +++ b/programs/squads-voter/src/instructions/configure_squad.rs @@ -0,0 +1,77 @@ +use anchor_lang::{ + account, + prelude::{Context, Signer}, + Accounts, +}; + +use anchor_lang::prelude::*; +use spl_governance::state::realm; + +use crate::error::SquadsVoterError; +use crate::state::{Registrar, SquadConfig}; + +/// Creates or updates Squad configuration which defines what Squads can be used for governances +/// and what weight they have +#[derive(Accounts)] +pub struct ConfigureSquad<'info> { + /// Registrar for which we configure this Squad + #[account(mut)] + pub registrar: Account<'info, Registrar>, + + #[account( + address = registrar.realm @ SquadsVoterError::InvalidRealmForRegistrar, + owner = registrar.governance_program_id + )] + /// CHECK: Owned by spl-governance instance specified in registrar.governance_program_id + pub realm: UncheckedAccount<'info>, + + /// Authority of the Realm must sign and match Realm.authority + pub realm_authority: Signer<'info>, + + // Squad which is going to be used for governance + /// CHECK: Owned by squads-protocol + pub squad: UncheckedAccount<'info>, +} + +pub fn configure_squad(ctx: Context, weight: u64) -> Result<()> { + let registrar = &mut ctx.accounts.registrar; + + let realm = realm::get_realm_data_for_governing_token_mint( + ®istrar.governance_program_id, + &ctx.accounts.realm, + ®istrar.governing_token_mint, + )?; + + require!( + realm.authority.unwrap() == ctx.accounts.realm_authority.key(), + SquadsVoterError::InvalidRealmAuthority + ); + + let squad = &ctx.accounts.squad; + + // TODO: Assert Squad owned by squads-protocol + + let squad_config = SquadConfig { + squad: squad.key(), + weight, + reserved: [0; 8], + }; + + let squad_idx = registrar + .squads_configs + .iter() + .position(|cc| cc.squad == squad.key()); + + if let Some(squad_idx) = squad_idx { + registrar.squads_configs[squad_idx] = squad_config; + } else { + // Note: In the current runtime version push() would throw an error if we exceed + // max_squads specified when the Registrar was created + registrar.squads_configs.push(squad_config); + } + + // TODO: if weight == 0 then remove the Squad from config + // If weight is set to 0 then the Squad won't be removed but it won't have any governance power + + Ok(()) +} diff --git a/programs/squads-voter/src/instructions/create_max_voter_weight_record.rs b/programs/squads-voter/src/instructions/create_max_voter_weight_record.rs new file mode 100644 index 00000000..0749fd4e --- /dev/null +++ b/programs/squads-voter/src/instructions/create_max_voter_weight_record.rs @@ -0,0 +1,57 @@ +use anchor_lang::prelude::*; +use anchor_spl::token::Mint; +use spl_governance::state::realm; + +use crate::state::max_voter_weight_record::MaxVoterWeightRecord; + +/// Creates MaxVoterWeightRecord used by spl-gov +/// This instruction should only be executed once per realm/governing_token_mint to create the account +#[derive(Accounts)] +pub struct CreateMaxVoterWeightRecord<'info> { + #[account( + init, + seeds = [ b"max-voter-weight-record".as_ref(), + realm.key().as_ref(), + realm_governing_token_mint.key().as_ref()], + bump, + payer = payer, + space = MaxVoterWeightRecord::get_space() + )] + pub max_voter_weight_record: Account<'info, MaxVoterWeightRecord>, + + /// The program id of the spl-governance program the realm belongs to + /// CHECK: Can be any instance of spl-governance and it's not known at the compilation time + #[account(executable)] + pub governance_program_id: UncheckedAccount<'info>, + + #[account(owner = governance_program_id.key())] + /// CHECK: Owned by spl-governance instance specified in governance_program_id + pub realm: UncheckedAccount<'info>, + + /// Either the realm community mint or the council mint. + pub realm_governing_token_mint: Account<'info, Mint>, + + #[account(mut)] + pub payer: Signer<'info>, + + pub system_program: Program<'info, System>, +} + +pub fn create_max_voter_weight_record(ctx: Context) -> Result<()> { + // Deserialize the Realm to validate it + let _realm = realm::get_realm_data_for_governing_token_mint( + &ctx.accounts.governance_program_id.key(), + &ctx.accounts.realm, + &ctx.accounts.realm_governing_token_mint.key(), + )?; + + let max_voter_weight_record = &mut ctx.accounts.max_voter_weight_record; + + max_voter_weight_record.realm = ctx.accounts.realm.key(); + max_voter_weight_record.governing_token_mint = ctx.accounts.realm_governing_token_mint.key(); + + // Set expiry to expired + max_voter_weight_record.max_voter_weight_expiry = Some(0); + + Ok(()) +} diff --git a/programs/squads-voter/src/instructions/create_registrar.rs b/programs/squads-voter/src/instructions/create_registrar.rs new file mode 100644 index 00000000..896673e1 --- /dev/null +++ b/programs/squads-voter/src/instructions/create_registrar.rs @@ -0,0 +1,81 @@ +use crate::error::SquadsVoterError; +use crate::state::*; +use anchor_lang::prelude::*; +use anchor_spl::token::Mint; +use spl_governance::state::realm; + +/// Creates Registrar storing Squads governance configuration for spl-gov Realm +/// This instruction should only be executed once per realm/governing_token_mint to create the account +#[derive(Accounts)] +#[instruction(max_squads: u8)] +pub struct CreateRegistrar<'info> { + /// The Squads voting Registrar + /// There can only be a single registrar per governance Realm and governing mint of the Realm + #[account( + init, + seeds = [b"registrar".as_ref(),realm.key().as_ref(), governing_token_mint.key().as_ref()], + bump, + payer = payer, + space = Registrar::get_space(max_squads) + )] + pub registrar: Account<'info, Registrar>, + + /// The program id of the spl-governance program the realm belongs to + /// CHECK: Can be any instance of spl-governance and it's not known at the compilation time + #[account(executable)] + pub governance_program_id: UncheckedAccount<'info>, + + /// An spl-governance Realm + /// + /// Realm is validated in the instruction: + /// - Realm is owned by the governance_program_id + /// - governing_token_mint must be the community or council mint + /// - realm_authority is realm.authority + /// CHECK: Owned by spl-governance instance specified in governance_program_id + #[account(owner = governance_program_id.key())] + pub realm: UncheckedAccount<'info>, + + /// Either the realm community mint or the council mint. + /// It must match Realm.community_mint or Realm.config.council_mint + /// + /// Note: Once the Squads plugin is enabled the governing_token_mint is used only as identity + /// for the voting population and the tokens of that are no longer used + pub governing_token_mint: Account<'info, Mint>, + + /// realm_authority must sign and match Realm.authority + pub realm_authority: Signer<'info>, + + #[account(mut)] + pub payer: Signer<'info>, + + pub system_program: Program<'info, System>, +} + +/// Creates a new Registrar which stores Squads voting configuration for given Realm +/// +/// To use the registrar, call ConfigureSquad to register Squads which will be +/// used for governance +/// +/// max_squads is used to allocate account size for the maximum number of governing Squads +/// Note: Once Solana runtime supports account resizing the max value won't be required +pub fn create_registrar(ctx: Context, _max_squads: u8) -> Result<()> { + let registrar = &mut ctx.accounts.registrar; + registrar.governance_program_id = ctx.accounts.governance_program_id.key(); + registrar.realm = ctx.accounts.realm.key(); + registrar.governing_token_mint = ctx.accounts.governing_token_mint.key(); + + // Verify that realm_authority is the expected authority of the Realm + // and that the mint matches one of the realm mints too + let realm = realm::get_realm_data_for_governing_token_mint( + ®istrar.governance_program_id, + &ctx.accounts.realm, + ®istrar.governing_token_mint, + )?; + + require!( + realm.authority.unwrap() == ctx.accounts.realm_authority.key(), + SquadsVoterError::InvalidRealmAuthority + ); + + Ok(()) +} diff --git a/programs/squads-voter/src/instructions/create_voter_weight_record.rs b/programs/squads-voter/src/instructions/create_voter_weight_record.rs new file mode 100644 index 00000000..d1ef66db --- /dev/null +++ b/programs/squads-voter/src/instructions/create_voter_weight_record.rs @@ -0,0 +1,63 @@ +use crate::state::*; +use anchor_lang::prelude::*; +use anchor_spl::token::Mint; +use spl_governance::state::realm; + +/// Creates VoterWeightRecord used by spl-gov +/// This instruction should only be executed once per realm/governing_token_mint/governing_token_owner +/// to create the account +#[derive(Accounts)] +#[instruction(governing_token_owner: Pubkey)] +pub struct CreateVoterWeightRecord<'info> { + #[account( + init, + seeds = [ b"voter-weight-record".as_ref(), + realm.key().as_ref(), + realm_governing_token_mint.key().as_ref(), + governing_token_owner.as_ref()], + bump, + payer = payer, + space = VoterWeightRecord::get_space() + )] + pub voter_weight_record: Account<'info, VoterWeightRecord>, + + /// The program id of the spl-governance program the realm belongs to + /// CHECK: Can be any instance of spl-governance and it's not known at the compilation time + #[account(executable)] + pub governance_program_id: UncheckedAccount<'info>, + + /// CHECK: Owned by spl-governance instance specified in governance_program_id + #[account(owner = governance_program_id.key())] + pub realm: UncheckedAccount<'info>, + + /// Either the realm community mint or the council mint. + pub realm_governing_token_mint: Account<'info, Mint>, + + #[account(mut)] + pub payer: Signer<'info>, + + pub system_program: Program<'info, System>, +} + +pub fn create_voter_weight_record( + ctx: Context, + governing_token_owner: Pubkey, +) -> Result<()> { + // Deserialize the Realm to validate it + let _realm = realm::get_realm_data_for_governing_token_mint( + &ctx.accounts.governance_program_id.key(), + &ctx.accounts.realm, + &ctx.accounts.realm_governing_token_mint.key(), + )?; + + let voter_weight_record = &mut ctx.accounts.voter_weight_record; + + voter_weight_record.realm = ctx.accounts.realm.key(); + voter_weight_record.governing_token_mint = ctx.accounts.realm_governing_token_mint.key(); + voter_weight_record.governing_token_owner = governing_token_owner; + + // Set expiry to expired + voter_weight_record.voter_weight_expiry = Some(0); + + Ok(()) +} diff --git a/programs/squads-voter/src/instructions/mod.rs b/programs/squads-voter/src/instructions/mod.rs new file mode 100644 index 00000000..4a5c8b9f --- /dev/null +++ b/programs/squads-voter/src/instructions/mod.rs @@ -0,0 +1,17 @@ +pub use configure_squad::*; +mod configure_squad; + +pub use create_registrar::*; +mod create_registrar; + +pub use create_voter_weight_record::*; +mod create_voter_weight_record; + +pub use create_max_voter_weight_record::*; +mod create_max_voter_weight_record; + +pub use update_voter_weight_record::*; +mod update_voter_weight_record; + +pub use update_max_voter_weight_record::*; +mod update_max_voter_weight_record; diff --git a/programs/squads-voter/src/instructions/update_max_voter_weight_record.rs b/programs/squads-voter/src/instructions/update_max_voter_weight_record.rs new file mode 100644 index 00000000..4d79d9c0 --- /dev/null +++ b/programs/squads-voter/src/instructions/update_max_voter_weight_record.rs @@ -0,0 +1,55 @@ +use crate::error::SquadsVoterError; +use crate::state::max_voter_weight_record::MaxVoterWeightRecord; +use crate::state::*; +use anchor_lang::prelude::*; + +/// Updates MaxVoterWeightRecord to evaluate max governance power for the configured Squads +/// This instruction updates MaxVoterWeightRecord which is valid for the current Slot only +/// The instruction must be executed inside the same transaction as the corresponding spl-gov instruction +#[derive(Accounts)] +pub struct UpdateMaxVoterWeightRecord<'info> { + /// The Squads voting Registrar + pub registrar: Account<'info, Registrar>, + + #[account( + mut, + constraint = max_voter_weight_record.realm == registrar.realm + @ SquadsVoterError::InvalidVoterWeightRecordRealm, + + constraint = max_voter_weight_record.governing_token_mint == registrar.governing_token_mint + @ SquadsVoterError::InvalidVoterWeightRecordMint, + )] + pub max_voter_weight_record: Account<'info, MaxVoterWeightRecord>, + // + // Remaining Accounts: Squads +} + +pub fn update_max_voter_weight_record(ctx: Context) -> Result<()> { + let registrar = &ctx.accounts.registrar; + + let mut max_voter_weight = 0u64; + + for squad_config in registrar.squads_configs.iter() { + let _squad_info = ctx + .remaining_accounts + .iter() + .find(|ai| ai.key() == squad_config.squad) + .unwrap(); + + // TODO: Assert squad_info is owned by squads-protocol program + // TODO: Get the Squad size from squad_info + let squad_size = 10; + + max_voter_weight = max_voter_weight + .checked_add(squad_config.weight.checked_mul(squad_size).unwrap()) + .unwrap(); + } + + let voter_weight_record = &mut ctx.accounts.max_voter_weight_record; + voter_weight_record.max_voter_weight = max_voter_weight; + + // Record is only valid as of the current slot + voter_weight_record.max_voter_weight_expiry = Some(Clock::get()?.slot); + + Ok(()) +} diff --git a/programs/squads-voter/src/instructions/update_voter_weight_record.rs b/programs/squads-voter/src/instructions/update_voter_weight_record.rs new file mode 100644 index 00000000..671500f5 --- /dev/null +++ b/programs/squads-voter/src/instructions/update_voter_weight_record.rs @@ -0,0 +1,63 @@ +use crate::error::SquadsVoterError; +use crate::state::*; +use anchor_lang::prelude::*; + +/// Updates VoterWeightRecord to evaluate governance power for users and the Squads they belong to +/// This instruction updates VoterWeightRecord which is valid for the current Slot only +/// The instruction must be executed inside the same transaction as the corresponding spl-gov instruction +#[derive(Accounts)] +pub struct UpdateVoterWeightRecord<'info> { + /// The Squads voting Registrar + pub registrar: Account<'info, Registrar>, + + #[account( + mut, + constraint = voter_weight_record.realm == registrar.realm + @ SquadsVoterError::InvalidVoterWeightRecordRealm, + + constraint = voter_weight_record.governing_token_mint == registrar.governing_token_mint + @ SquadsVoterError::InvalidVoterWeightRecordMint, + )] + pub voter_weight_record: Account<'info, VoterWeightRecord>, + // + // Remaining Accounts: Squads Membership +} + +pub fn update_voter_weight_record(ctx: Context) -> Result<()> { + let registrar = &ctx.accounts.registrar; + let _governing_token_owner = &ctx.accounts.voter_weight_record.governing_token_owner; + + let mut voter_weight = 0u64; + + let mut unique_squads = vec![]; + + for squad_info in ctx.remaining_accounts.iter() { + // Ensure the same Squad was not provided more than once + if unique_squads.contains(&squad_info.key) { + return Err(SquadsVoterError::DuplicatedSquadDetected.into()); + } + unique_squads.push(squad_info.key); + + // TODO: Assert squad_info is owned by squads-protocol program + // TODO: Validate Squad membership for governing_token_owner and squad_info + + let squad_config = registrar.get_squad_config(squad_info.key)?; + + voter_weight = voter_weight + .checked_add(squad_config.weight as u64) + .unwrap(); + } + + let voter_weight_record = &mut ctx.accounts.voter_weight_record; + + voter_weight_record.voter_weight = voter_weight; + + // Record is only valid as of the current slot + voter_weight_record.voter_weight_expiry = Some(Clock::get()?.slot); + + // Set action and target to None to indicate the weight is valid for any action + voter_weight_record.weight_action = None; + voter_weight_record.weight_action_target = None; + + Ok(()) +} diff --git a/programs/squads-voter/src/lib.rs b/programs/squads-voter/src/lib.rs new file mode 100644 index 00000000..e05f470f --- /dev/null +++ b/programs/squads-voter/src/lib.rs @@ -0,0 +1,51 @@ +use anchor_lang::prelude::*; + +pub mod error; + +mod instructions; +use instructions::*; + +pub mod state; + +pub mod tools; + +declare_id!("GSqds6KYQf5tXEHwrDszu6AqkVXinFCKDwUfTLzp1jEH"); + +#[program] +pub mod squads_voter { + + use super::*; + pub fn create_registrar(ctx: Context, max_squads: u8) -> Result<()> { + log_version(); + instructions::create_registrar(ctx, max_squads) + } + pub fn create_voter_weight_record( + ctx: Context, + governing_token_owner: Pubkey, + ) -> Result<()> { + log_version(); + instructions::create_voter_weight_record(ctx, governing_token_owner) + } + pub fn create_max_voter_weight_record(ctx: Context) -> Result<()> { + log_version(); + instructions::create_max_voter_weight_record(ctx) + } + pub fn update_voter_weight_record(ctx: Context) -> Result<()> { + log_version(); + instructions::update_voter_weight_record(ctx) + } + pub fn update_max_voter_weight_record(ctx: Context) -> Result<()> { + log_version(); + instructions::update_max_voter_weight_record(ctx) + } + + pub fn configure_squad(ctx: Context, weight: u64) -> Result<()> { + log_version(); + instructions::configure_squad(ctx, weight) + } +} + +fn log_version() { + // TODO: Check if Anchor allows to log it before instruction is deserialized + msg!("VERSION:{:?}", env!("CARGO_PKG_VERSION")); +} diff --git a/programs/squads-voter/src/state/max_voter_weight_record.rs b/programs/squads-voter/src/state/max_voter_weight_record.rs new file mode 100644 index 00000000..a4c94a60 --- /dev/null +++ b/programs/squads-voter/src/state/max_voter_weight_record.rs @@ -0,0 +1,95 @@ +use crate::id; +use crate::tools::anchor::{DISCRIMINATOR_SIZE, PUBKEY_SIZE}; +use anchor_lang::prelude::Pubkey; +use anchor_lang::prelude::*; + +/// MaxVoterWeightRecord account as defined in spl-governance-addin-api +/// It's redefined here without account_discriminator for Anchor to treat it as native account +/// +/// The account is used as an api interface to provide max voting power to the governance program from external addin contracts +#[account] +#[derive(Debug, PartialEq)] +pub struct MaxVoterWeightRecord { + /// The Realm the MaxVoterWeightRecord belongs to + pub realm: Pubkey, + + /// Governing Token Mint the MaxVoterWeightRecord 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, + + /// Max voter weight + /// The max voter weight provided by the addin for the given realm and governing_token_mint + pub max_voter_weight: u64, + + /// The slot when the max voting weight expires + /// It should be set to None if the weight never expires + /// If the max vote weight decays with time, for example for time locked based weights, then the expiry must be set + /// As a pattern Revise instruction to update the max 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 max_voter_weight_expiry: Option, + + /// Reserved space for future versions + pub reserved: [u8; 8], +} + +impl Default for MaxVoterWeightRecord { + fn default() -> Self { + Self { + realm: Default::default(), + governing_token_mint: Default::default(), + max_voter_weight: Default::default(), + max_voter_weight_expiry: Some(0), + reserved: Default::default(), + } + } +} + +impl MaxVoterWeightRecord { + pub fn get_space() -> usize { + DISCRIMINATOR_SIZE + PUBKEY_SIZE * 2 + 8 + 1 + 8 + 8 + } +} + +/// Returns MaxVoterWeightRecord PDA seeds +pub fn get_max_voter_weight_record_seeds<'a>( + realm: &'a Pubkey, + governing_token_mint: &'a Pubkey, +) -> [&'a [u8]; 3] { + [ + b"max-voter-weight-record", + realm.as_ref(), + governing_token_mint.as_ref(), + ] +} + +/// Returns MaxVoterWeightRecord PDA address +pub fn get_max_voter_weight_record_address( + realm: &Pubkey, + governing_token_mint: &Pubkey, +) -> Pubkey { + Pubkey::find_program_address( + &get_max_voter_weight_record_seeds(realm, governing_token_mint), + &id(), + ) + .0 +} + +#[cfg(test)] +mod test { + + use super::*; + + #[test] + fn test_get_space() { + // Arrange + let expected_space = MaxVoterWeightRecord::get_space(); + + // Act + let actual_space = + DISCRIMINATOR_SIZE + MaxVoterWeightRecord::default().try_to_vec().unwrap().len(); + + // Assert + assert_eq!(expected_space, actual_space); + } +} diff --git a/programs/squads-voter/src/state/mod.rs b/programs/squads-voter/src/state/mod.rs new file mode 100644 index 00000000..9bf3426f --- /dev/null +++ b/programs/squads-voter/src/state/mod.rs @@ -0,0 +1,10 @@ +pub use registrar::*; +pub mod registrar; + +pub use squad_config::*; +pub mod squad_config; + +pub mod max_voter_weight_record; + +pub use voter_weight_record::*; +pub mod voter_weight_record; diff --git a/programs/squads-voter/src/state/registrar.rs b/programs/squads-voter/src/state/registrar.rs new file mode 100644 index 00000000..ec208f71 --- /dev/null +++ b/programs/squads-voter/src/state/registrar.rs @@ -0,0 +1,89 @@ +use crate::{ + error::SquadsVoterError, + id, + state::SquadConfig, + tools::anchor::{DISCRIMINATOR_SIZE, PUBKEY_SIZE}, +}; +use anchor_lang::prelude::*; + +/// Registrar which stores Squads voting configuration for the given Realm +#[account] +#[derive(Debug, PartialEq)] +pub struct Registrar { + /// spl-governance program the Realm belongs to + pub governance_program_id: Pubkey, + + /// Realm of the Registrar + pub realm: Pubkey, + + /// Governing token mint the Registrar is for + /// It can either be the Community or the Council mint of the Realm + /// When the plugin is used the mint is only used as identity of the governing power (voting population) + /// and the actual token of the mint is not used + pub governing_token_mint: Pubkey, + + /// Squads used for governance + pub squads_configs: Vec, + + /// Reserved for future upgrades + pub reserved: [u8; 128], +} + +impl Registrar { + pub fn get_space(max_squads: u8) -> usize { + DISCRIMINATOR_SIZE + PUBKEY_SIZE * 3 + 4 + max_squads as usize * (PUBKEY_SIZE + 8 + 8) + 128 + } +} + +/// Returns Registrar PDA seeds +pub fn get_registrar_seeds<'a>( + realm: &'a Pubkey, + governing_token_mint: &'a Pubkey, +) -> [&'a [u8]; 3] { + [b"registrar", realm.as_ref(), governing_token_mint.as_ref()] +} + +/// Returns Registrar PDA address +pub fn get_registrar_address(realm: &Pubkey, governing_token_mint: &Pubkey) -> Pubkey { + Pubkey::find_program_address(&get_registrar_seeds(realm, governing_token_mint), &id()).0 +} + +impl Registrar { + pub fn get_squad_config(&self, squad: &Pubkey) -> Result<&SquadConfig> { + return self + .squads_configs + .iter() + .find(|sc| sc.squad == *squad) + .ok_or_else(|| SquadsVoterError::SquadNotFound.into()); + } +} + +#[cfg(test)] +mod test { + + use super::*; + + #[test] + fn test_get_space() { + // Arrange + let expected_space = Registrar::get_space(3); + + let registrar = Registrar { + governance_program_id: Pubkey::default(), + realm: Pubkey::default(), + governing_token_mint: Pubkey::default(), + squads_configs: vec![ + SquadConfig::default(), + SquadConfig::default(), + SquadConfig::default(), + ], + reserved: [0; 128], + }; + + // Act + let actual_space = DISCRIMINATOR_SIZE + registrar.try_to_vec().unwrap().len(); + + // Assert + assert_eq!(expected_space, actual_space); + } +} diff --git a/programs/squads-voter/src/state/squad_config.rs b/programs/squads-voter/src/state/squad_config.rs new file mode 100644 index 00000000..95de23a2 --- /dev/null +++ b/programs/squads-voter/src/state/squad_config.rs @@ -0,0 +1,18 @@ +use anchor_lang::prelude::*; + +/// Configuration of a Squad used for governance power +#[derive(AnchorSerialize, AnchorDeserialize, Debug, Clone, Copy, PartialEq, Default)] +pub struct SquadConfig { + /// The Squad used for governance + pub squad: Pubkey, + + /// Governance power weight of the Squad + /// Membership in the Squad gives governance power equal to the weight + /// + /// Note: The weight is scaled accordingly to the governing_token_mint decimals + /// Ex: if the the mint has 2 decimal places then weight of 1 should be stored as 100 + pub weight: u64, + + /// Reserved for future upgrades + pub reserved: [u8; 8], +} diff --git a/programs/squads-voter/src/state/voter_weight_record.rs b/programs/squads-voter/src/state/voter_weight_record.rs new file mode 100644 index 00000000..7032aa33 --- /dev/null +++ b/programs/squads-voter/src/state/voter_weight_record.rs @@ -0,0 +1,109 @@ +use anchor_lang::prelude::*; + +use crate::tools::anchor::{DISCRIMINATOR_SIZE, PUBKEY_SIZE}; + +/// VoterWeightAction enum as defined in spl-governance-addin-api +/// It's redefined here for Anchor to export it to IDL +#[derive(AnchorSerialize, AnchorDeserialize, Debug, Clone, Copy, PartialEq)] +pub enum VoterWeightAction { + /// Cast vote for a proposal. Target: Proposal + CastVote, + + /// Comment a proposal. Target: Proposal + CommentProposal, + + /// Create Governance within a realm. Target: Realm + CreateGovernance, + + /// Create a proposal for a governance. Target: Governance + CreateProposal, + + /// Signs off a proposal for a governance. Target: Proposal + /// Note: SignOffProposal is not supported in the current version + SignOffProposal, +} + +/// VoterWeightRecord account as defined in spl-governance-addin-api +/// It's redefined here without account_discriminator for Anchor to treat it as native account +/// +/// The account is used as an api interface to provide voting power to the governance program from external addin contracts +#[account] +#[derive(Debug, PartialEq)] +pub struct VoterWeightRecord { + /// 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 + /// This is the actual owner (voter) and corresponds to TokenOwnerRecord.governing_token_owner + pub governing_token_owner: Pubkey, + + /// Voter's weight + /// The weight of the voter provided by the addin for the given realm, governing_token_mint and governing_token_owner (voter) + 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, + + /// The governance action the voter's weight pertains to + /// It allows to provided voter's weight specific to the particular action the weight is evaluated for + /// When the action is provided then the governance program asserts the executing action is the same as specified by the addin + pub weight_action: Option, + + /// The target the voter's weight action pertains to + /// It allows to provided voter's weight specific to the target the weight is evaluated for + /// For example when addin supplies weight to vote on a particular proposal then it must specify the proposal as the action target + /// When the target is provided then the governance program asserts the target is the same as specified by the addin + pub weight_action_target: Option, + + /// Reserved space for future versions + pub reserved: [u8; 8], +} + +impl VoterWeightRecord { + pub fn get_space() -> usize { + DISCRIMINATOR_SIZE + PUBKEY_SIZE * 4 + 8 + 1 + 8 + 1 + 1 + 1 + 8 + } +} + +impl Default for VoterWeightRecord { + fn default() -> Self { + Self { + realm: Default::default(), + governing_token_mint: Default::default(), + governing_token_owner: Default::default(), + voter_weight: Default::default(), + voter_weight_expiry: Some(0), + weight_action: Some(VoterWeightAction::CastVote), + weight_action_target: Some(Default::default()), + reserved: Default::default(), + } + } +} + +#[cfg(test)] +mod test { + + use super::*; + + #[test] + fn test_get_space() { + // Arrange + let expected_space = VoterWeightRecord::get_space(); + + // Act + let actual_space = + DISCRIMINATOR_SIZE + VoterWeightRecord::default().try_to_vec().unwrap().len(); + + // Assert + assert_eq!(expected_space, actual_space); + } +} diff --git a/programs/squads-voter/src/tools/anchor.rs b/programs/squads-voter/src/tools/anchor.rs new file mode 100644 index 00000000..461f8871 --- /dev/null +++ b/programs/squads-voter/src/tools/anchor.rs @@ -0,0 +1,2 @@ +pub const DISCRIMINATOR_SIZE: usize = 8; +pub const PUBKEY_SIZE: usize = 32; diff --git a/programs/squads-voter/src/tools/mod.rs b/programs/squads-voter/src/tools/mod.rs new file mode 100644 index 00000000..37794aee --- /dev/null +++ b/programs/squads-voter/src/tools/mod.rs @@ -0,0 +1 @@ +pub mod anchor; diff --git a/programs/squads-voter/tests/configure_squad.rs b/programs/squads-voter/tests/configure_squad.rs new file mode 100644 index 00000000..26e70f27 --- /dev/null +++ b/programs/squads-voter/tests/configure_squad.rs @@ -0,0 +1,371 @@ +use solana_program_test::*; +use solana_sdk::transport::TransportError; + +use crate::program_test::squads_voter_test::{ConfigureSquadArgs, SquadsVoterTest}; + +mod program_test; + +#[tokio::test] +async fn test_configure_squad() -> Result<(), TransportError> { + // Arrange + let mut squads_voter_test = SquadsVoterTest::start_new().await; + + let realm_cookie = squads_voter_test.governance.with_realm().await?; + + let registrar_cookie = squads_voter_test.with_registrar(&realm_cookie).await?; + + let squad_cookie = squads_voter_test.squads.with_squad().await?; + + // Act + let squad_config_cookie = squads_voter_test + .with_squad_config(®istrar_cookie, &squad_cookie, None) + .await?; + + // // Assert + let registrar = squads_voter_test + .get_registrar_account(®istrar_cookie.address) + .await; + + assert_eq!(registrar.squads_configs.len(), 1); + + assert_eq!( + registrar.squads_configs[0], + squad_config_cookie.squad_config + ); + + Ok(()) +} + +#[tokio::test] +async fn test_configure_multiple_squads() -> Result<(), TransportError> { + // Arrange + let mut squads_voter_test = SquadsVoterTest::start_new().await; + + let realm_cookie = squads_voter_test.governance.with_realm().await?; + + let registrar_cookie = squads_voter_test.with_registrar(&realm_cookie).await?; + + let squad_cookie1 = squads_voter_test.squads.with_squad().await?; + let squad_cookie2 = squads_voter_test.squads.with_squad().await?; + + // Act + squads_voter_test + .with_squad_config( + ®istrar_cookie, + &squad_cookie1, + Some(ConfigureSquadArgs { weight: 1 }), + ) + .await?; + + squads_voter_test + .with_squad_config( + ®istrar_cookie, + &squad_cookie2, + Some(ConfigureSquadArgs { weight: 2 }), + ) + .await?; + + // Assert + let registrar = squads_voter_test + .get_registrar_account(®istrar_cookie.address) + .await; + + assert_eq!(registrar.squads_configs.len(), 2); + + Ok(()) +} + +// #[tokio::test] +// async fn test_configure_max_collections() -> Result<(), TransportError> { +// // Arrange +// let mut squads_voter_test = NftVoterTest::start_new().await; + +// let realm_cookie = squads_voter_test.governance.with_realm().await?; + +// let registrar_cookie = squads_voter_test.with_registrar(&realm_cookie).await?; + +// let max_voter_weight_record_cookie = squads_voter_test +// .with_max_voter_weight_record(®istrar_cookie) +// .await?; + +// // Act + +// for _ in 0..registrar_cookie.max_collections { +// let nft_collection_cookie = squads_voter_test.token_metadata.with_nft_collection().await?; + +// squads_voter_test +// .with_collection( +// ®istrar_cookie, +// &nft_collection_cookie, +// &max_voter_weight_record_cookie, +// None, +// ) +// .await?; +// } + +// // Assert +// let registrar = squads_voter_test +// .get_registrar_account(®istrar_cookie.address) +// .await; + +// assert_eq!( +// registrar.collection_configs.len() as u8, +// registrar_cookie.max_collections +// ); + +// let max_voter_weight_record = squads_voter_test +// .get_max_voter_weight_record(&max_voter_weight_record_cookie.address) +// .await; + +// assert_eq!(max_voter_weight_record.max_voter_weight_expiry, None); +// assert_eq!(max_voter_weight_record.max_voter_weight, 30); + +// Ok(()) +// } + +// #[tokio::test] +// async fn test_configure_existing_collection() -> Result<(), TransportError> { +// // Arrange +// let mut squads_voter_test = NftVoterTest::start_new().await; + +// let realm_cookie = squads_voter_test.governance.with_realm().await?; + +// let registrar_cookie = squads_voter_test.with_registrar(&realm_cookie).await?; + +// let nft_collection_cookie = squads_voter_test.token_metadata.with_nft_collection().await?; + +// let max_voter_weight_record_cookie = squads_voter_test +// .with_max_voter_weight_record(®istrar_cookie) +// .await?; + +// squads_voter_test +// .with_collection( +// ®istrar_cookie, +// &nft_collection_cookie, +// &max_voter_weight_record_cookie, +// None, +// ) +// .await?; + +// // Act + +// squads_voter_test +// .with_collection( +// ®istrar_cookie, +// &nft_collection_cookie, +// &max_voter_weight_record_cookie, +// Some(ConfigureCollectionArgs { +// weight: 2, +// size: 10, +// }), +// ) +// .await?; + +// // Assert +// let registrar = squads_voter_test +// .get_registrar_account(®istrar_cookie.address) +// .await; + +// assert_eq!(registrar.collection_configs.len(), 1); + +// let max_voter_weight_record = squads_voter_test +// .get_max_voter_weight_record(&max_voter_weight_record_cookie.address) +// .await; + +// assert_eq!(max_voter_weight_record.max_voter_weight_expiry, None); +// assert_eq!(max_voter_weight_record.max_voter_weight, 20); + +// Ok(()) +// } + +// // TODO: Remove collection test + +// #[tokio::test] +// async fn test_configure_collection_with_invalid_realm_error() -> Result<(), TransportError> { +// // Arrange +// let mut squads_voter_test = NftVoterTest::start_new().await; + +// let realm_cookie = squads_voter_test.governance.with_realm().await?; + +// let registrar_cookie = squads_voter_test.with_registrar(&realm_cookie).await?; + +// let nft_collection_cookie = squads_voter_test.token_metadata.with_nft_collection().await?; + +// let max_voter_weight_record_cookie = squads_voter_test +// .with_max_voter_weight_record(®istrar_cookie) +// .await?; + +// // Try to use a different Realm +// let realm_cookie2 = squads_voter_test.governance.with_realm().await?; + +// // Act +// let err = squads_voter_test +// .with_collection_using_ix( +// ®istrar_cookie, +// &nft_collection_cookie, +// &max_voter_weight_record_cookie, +// None, +// |i| i.accounts[1].pubkey = realm_cookie2.address, // realm +// None, +// ) +// .await +// .err() +// .unwrap(); + +// // Assert + +// assert_nft_voter_err(err, NftVoterError::InvalidRealmForRegistrar); + +// Ok(()) +// } + +// #[tokio::test] +// async fn test_configure_collection_with_realm_authority_must_sign_error( +// ) -> Result<(), TransportError> { +// // Arrange +// let mut squads_voter_test = NftVoterTest::start_new().await; + +// let realm_cookie = squads_voter_test.governance.with_realm().await?; + +// let registrar_cookie = squads_voter_test.with_registrar(&realm_cookie).await?; + +// let nft_collection_cookie = squads_voter_test.token_metadata.with_nft_collection().await?; + +// let max_voter_weight_record_cookie = squads_voter_test +// .with_max_voter_weight_record(®istrar_cookie) +// .await?; + +// // Act +// let err = squads_voter_test +// .with_collection_using_ix( +// ®istrar_cookie, +// &nft_collection_cookie, +// &max_voter_weight_record_cookie, +// None, +// |i| i.accounts[2].is_signer = false, // realm_authority +// Some(&[]), +// ) +// .await +// .err() +// .unwrap(); + +// // Assert + +// assert_anchor_err(err, anchor_lang::error::ErrorCode::AccountNotSigner); + +// Ok(()) +// } + +// #[tokio::test] +// async fn test_configure_collection_with_invalid_realm_authority_error() -> Result<(), TransportError> +// { +// // Arrange +// let mut squads_voter_test = NftVoterTest::start_new().await; + +// let realm_cookie = squads_voter_test.governance.with_realm().await?; + +// let registrar_cookie = squads_voter_test.with_registrar(&realm_cookie).await?; + +// let nft_collection_cookie = squads_voter_test.token_metadata.with_nft_collection().await?; + +// let max_voter_weight_record_cookie = squads_voter_test +// .with_max_voter_weight_record(®istrar_cookie) +// .await?; + +// let realm_authority = Keypair::new(); + +// // Act +// let err = squads_voter_test +// .with_collection_using_ix( +// ®istrar_cookie, +// &nft_collection_cookie, +// &max_voter_weight_record_cookie, +// None, +// |i| i.accounts[2].pubkey = realm_authority.pubkey(), // realm_authority +// Some(&[&realm_authority]), +// ) +// .await +// .err() +// .unwrap(); + +// // Assert + +// assert_nft_voter_err(err, NftVoterError::InvalidRealmAuthority); + +// Ok(()) +// } + +// #[tokio::test] +// async fn test_configure_collection_with_invalid_max_voter_weight_realm_error( +// ) -> Result<(), TransportError> { +// // Arrange +// let mut squads_voter_test = NftVoterTest::start_new().await; + +// let realm_cookie = squads_voter_test.governance.with_realm().await?; +// let registrar_cookie = squads_voter_test.with_registrar(&realm_cookie).await?; + +// let nft_collection_cookie = squads_voter_test.token_metadata.with_nft_collection().await?; + +// let realm_cookie2 = squads_voter_test.governance.with_realm().await?; +// let registrar_cookie2 = squads_voter_test.with_registrar(&realm_cookie2).await?; + +// let max_voter_weight_record_cookie = squads_voter_test +// .with_max_voter_weight_record(®istrar_cookie2) +// .await?; + +// // Act +// let err = squads_voter_test +// .with_collection( +// ®istrar_cookie, +// &nft_collection_cookie, +// &max_voter_weight_record_cookie, +// None, +// ) +// .await +// .err() +// .unwrap(); + +// // Assert + +// assert_nft_voter_err(err, NftVoterError::InvalidMaxVoterWeightRecordRealm); + +// Ok(()) +// } + +// #[tokio::test] +// async fn test_configure_collection_with_invalid_max_voter_weight_mint_error( +// ) -> Result<(), TransportError> { +// // Arrange +// let mut squads_voter_test = NftVoterTest::start_new().await; + +// let mut realm_cookie = squads_voter_test.governance.with_realm().await?; +// let registrar_cookie = squads_voter_test.with_registrar(&realm_cookie).await?; + +// let nft_collection_cookie = squads_voter_test.token_metadata.with_nft_collection().await?; + +// // Create Registrar for council mint +// realm_cookie.account.community_mint = realm_cookie.account.config.council_mint.unwrap(); +// let registrar_cookie2 = squads_voter_test.with_registrar(&realm_cookie).await?; + +// let max_voter_weight_record_cookie = squads_voter_test +// .with_max_voter_weight_record(®istrar_cookie2) +// .await?; + +// // Act +// let err = squads_voter_test +// .with_collection( +// ®istrar_cookie, +// &nft_collection_cookie, +// &max_voter_weight_record_cookie, +// None, +// ) +// .await +// .err() +// .unwrap(); + +// // Assert + +// assert_nft_voter_err(err, NftVoterError::InvalidMaxVoterWeightRecordMint); + +// Ok(()) +// } diff --git a/programs/squads-voter/tests/create_max_voter_weight_record.rs b/programs/squads-voter/tests/create_max_voter_weight_record.rs new file mode 100644 index 00000000..9c893ba2 --- /dev/null +++ b/programs/squads-voter/tests/create_max_voter_weight_record.rs @@ -0,0 +1,122 @@ +use crate::program_test::squads_voter_test::SquadsVoterTest; +use solana_program_test::*; +use solana_sdk::transport::TransportError; + +mod program_test; + +#[tokio::test] +async fn test_create_max_voter_weight_record() -> Result<(), TransportError> { + // Arrange + let mut squads_voter_test = SquadsVoterTest::start_new().await; + + let realm_cookie = squads_voter_test.governance.with_realm().await?; + + let registrar_cookie = squads_voter_test.with_registrar(&realm_cookie).await?; + + // Act + let max_voter_weight_record_cookie = squads_voter_test + .with_max_voter_weight_record(®istrar_cookie) + .await?; + + // Assert + + let max_voter_weight_record = squads_voter_test + .get_max_voter_weight_record(&max_voter_weight_record_cookie.address) + .await; + + assert_eq!( + max_voter_weight_record_cookie.account, + max_voter_weight_record + ); + + Ok(()) +} + +// #[tokio::test] +// async fn test_create_max_voter_weight_record_with_invalid_realm_error() -> Result<(), TransportError> +// { +// // Arrange +// let mut squads_voter_test = NftVoterTest::start_new().await; + +// let realm_cookie = squads_voter_test.governance.with_realm().await?; + +// let registrar_cookie = squads_voter_test.with_registrar(&realm_cookie).await?; + +// let realm_cookie2 = squads_voter_test.governance.with_realm().await?; + +// // Act +// let err = squads_voter_test +// .with_max_voter_weight_record_using_ix(®istrar_cookie, |i| { +// i.accounts[2].pubkey = realm_cookie2.address // Realm +// }) +// .await +// .err() +// .unwrap(); + +// // Assert + +// // PDA doesn't match and hence the error is PrivilegeEscalation +// assert_ix_err(err, InstructionError::PrivilegeEscalation); + +// Ok(()) +// } + +// #[tokio::test] +// async fn test_create_max_voter_weight_record_with_invalid_mint_error() -> Result<(), TransportError> +// { +// // Arrange +// let mut squads_voter_test = NftVoterTest::start_new().await; + +// let realm_cookie = squads_voter_test.governance.with_realm().await?; + +// let registrar_cookie = squads_voter_test.with_registrar(&realm_cookie).await?; + +// let realm_cookie2 = squads_voter_test.governance.with_realm().await?; + +// // Act +// let err = squads_voter_test +// .with_max_voter_weight_record_using_ix(®istrar_cookie, |i| { +// i.accounts[2].pubkey = realm_cookie2.address // Mint +// }) +// .await +// .err() +// .unwrap(); + +// // Assert + +// // PDA doesn't match and hence the error is PrivilegeEscalation +// assert_ix_err(err, InstructionError::PrivilegeEscalation); + +// Ok(()) +// } + +// #[tokio::test] +// async fn test_create_max_voter_weight_record_with_already_exists_error( +// ) -> Result<(), TransportError> { +// // Arrange +// let mut squads_voter_test = NftVoterTest::start_new().await; + +// let realm_cookie = squads_voter_test.governance.with_realm().await?; + +// let registrar_cookie = squads_voter_test.with_registrar(&realm_cookie).await?; + +// squads_voter_test +// .with_max_voter_weight_record(®istrar_cookie) +// .await?; + +// squads_voter_test.bench.advance_clock().await; + +// // Act +// let err = squads_voter_test +// .with_max_voter_weight_record(®istrar_cookie) +// .await +// .err() +// .unwrap(); + +// // Assert + +// // InstructionError::Custom(0) is returned for TransactionError::AccountInUse +// assert_ix_err(err, InstructionError::Custom(0)); + +// Ok(()) +// } diff --git a/programs/squads-voter/tests/create_registrar.rs b/programs/squads-voter/tests/create_registrar.rs new file mode 100644 index 00000000..95989986 --- /dev/null +++ b/programs/squads-voter/tests/create_registrar.rs @@ -0,0 +1,157 @@ +mod program_test; + +use anchor_lang::prelude::Pubkey; +use gpl_squads_voter::error::SquadsVoterError; +use program_test::squads_voter_test::SquadsVoterTest; + +use solana_program::instruction::InstructionError; +use solana_program_test::*; +use solana_sdk::{signature::Keypair, transport::TransportError}; + +use program_test::tools::{assert_anchor_err, assert_ix_err, assert_squads_voter_err}; + +#[tokio::test] +async fn test_create_registrar() -> Result<(), TransportError> { + // Arrange + let mut squads_voter_test = SquadsVoterTest::start_new().await; + + let realm_cookie = squads_voter_test.governance.with_realm().await?; + + // Act + let registrar_cookie = squads_voter_test.with_registrar(&realm_cookie).await?; + + // Assert + let registrar = squads_voter_test + .get_registrar_account(®istrar_cookie.address) + .await; + + assert_eq!(registrar, registrar_cookie.account); + + Ok(()) +} + +#[tokio::test] +async fn test_create_registrar_with_invalid_realm_authority_error() -> Result<(), TransportError> { + // Arrange + let mut squads_voter_test = SquadsVoterTest::start_new().await; + + let mut realm_cookie = squads_voter_test.governance.with_realm().await?; + realm_cookie.realm_authority = Keypair::new(); + + // Act + let err = squads_voter_test + .with_registrar(&realm_cookie) + .await + .err() + .unwrap(); + + assert_squads_voter_err(err, SquadsVoterError::InvalidRealmAuthority); + + Ok(()) +} + +#[tokio::test] +async fn test_create_registrar_with_realm_authority_must_sign_error() -> Result<(), TransportError> +{ + // Arrange + let mut squads_voter_test = SquadsVoterTest::start_new().await; + + let mut realm_cookie = squads_voter_test.governance.with_realm().await?; + realm_cookie.realm_authority = Keypair::new(); + + // Act + let err = squads_voter_test + .with_registrar_using_ix( + &realm_cookie, + |i| i.accounts[4].is_signer = false, // realm_authority + Some(&[]), + ) + .await + .err() + .unwrap(); + + assert_anchor_err(err, anchor_lang::error::ErrorCode::AccountNotSigner); + + Ok(()) +} + +#[tokio::test] +async fn test_create_registrar_with_invalid_spl_gov_program_id_error() -> Result<(), TransportError> +{ + // Arrange + let mut squads_voter_test = SquadsVoterTest::start_new().await; + + let mut realm_cookie = squads_voter_test.governance.with_realm().await?; + realm_cookie.realm_authority = Keypair::new(); + + // Try to use a different program id + let governance_program_id = squads_voter_test.program_id; + + // Act + let err = squads_voter_test + .with_registrar_using_ix( + &realm_cookie, + |i| i.accounts[1].pubkey = governance_program_id, //governance_program_id + None, + ) + .await + .err() + .unwrap(); + + assert_anchor_err(err, anchor_lang::error::ErrorCode::ConstraintOwner); + + Ok(()) +} + +#[tokio::test] +async fn test_create_registrar_with_invalid_realm_error() -> Result<(), TransportError> { + // Arrange + let mut squads_voter_test = SquadsVoterTest::start_new().await; + + let mut realm_cookie = squads_voter_test.governance.with_realm().await?; + realm_cookie.realm_authority = Keypair::new(); + + // Act + let err = squads_voter_test + .with_registrar_using_ix( + &realm_cookie, + |i| i.accounts[2].pubkey = Pubkey::new_unique(), // realm + None, + ) + .await + .err() + .unwrap(); + + // PDA doesn't match and hence the error is PrivilegeEscalation + assert_ix_err(err, InstructionError::PrivilegeEscalation); + + Ok(()) +} + +#[tokio::test] +async fn test_create_registrar_with_invalid_governing_token_mint_error( +) -> Result<(), TransportError> { + // Arrange + let mut squads_voter_test = SquadsVoterTest::start_new().await; + + let mut realm_cookie = squads_voter_test.governance.with_realm().await?; + realm_cookie.realm_authority = Keypair::new(); + + let mint_cookie = squads_voter_test.bench.with_mint().await?; + + // Act + let err = squads_voter_test + .with_registrar_using_ix( + &realm_cookie, + |i| i.accounts[3].pubkey = mint_cookie.address, // governing_token_mint + None, + ) + .await + .err() + .unwrap(); + + // PDA doesn't match and hence the error is PrivilegeEscalation + assert_ix_err(err, InstructionError::PrivilegeEscalation); + + Ok(()) +} diff --git a/programs/squads-voter/tests/create_voter_weight_record.rs b/programs/squads-voter/tests/create_voter_weight_record.rs new file mode 100644 index 00000000..2a398c16 --- /dev/null +++ b/programs/squads-voter/tests/create_voter_weight_record.rs @@ -0,0 +1,124 @@ +use crate::program_test::squads_voter_test::SquadsVoterTest; +use solana_program_test::*; +use solana_sdk::transport::TransportError; + +mod program_test; + +#[tokio::test] +async fn test_create_voter_weight_record() -> Result<(), TransportError> { + // Arrange + let mut squads_voter_test = SquadsVoterTest::start_new().await; + + let realm_cookie = squads_voter_test.governance.with_realm().await?; + + let registrar_cookie = squads_voter_test.with_registrar(&realm_cookie).await?; + + let voter_cookie = squads_voter_test.bench.with_wallet().await; + + // Act + let voter_weight_record_cookie = squads_voter_test + .with_voter_weight_record(®istrar_cookie, &voter_cookie) + .await?; + + // Assert + + let voter_weight_record = squads_voter_test + .get_voter_weight_record(&voter_weight_record_cookie.address) + .await; + + assert_eq!(voter_weight_record_cookie.account, voter_weight_record); + + Ok(()) +} + +// #[tokio::test] +// async fn test_create_voter_weight_record_with_invalid_realm_error() -> Result<(), TransportError> { +// // Arrange +// let mut squads_voter_test = NftVoterTest::start_new().await; + +// let realm_cookie = squads_voter_test.governance.with_realm().await?; + +// let registrar_cookie = squads_voter_test.with_registrar(&realm_cookie).await?; + +// let realm_cookie2 = squads_voter_test.governance.with_realm().await?; + +// let voter_cookie = squads_voter_test.bench.with_wallet().await; + +// // Act +// let err = squads_voter_test +// .with_voter_weight_record_using_ix(®istrar_cookie, &voter_cookie, |i| { +// i.accounts[2].pubkey = realm_cookie2.address // Realm +// }) +// .await +// .err() +// .unwrap(); + +// // Assert + +// // PDA doesn't match and hence the error is PrivilegeEscalation +// assert_ix_err(err, InstructionError::PrivilegeEscalation); + +// Ok(()) +// } + +// #[tokio::test] +// async fn test_create_voter_weight_record_with_invalid_mint_error() -> Result<(), TransportError> { +// // Arrange +// let mut squads_voter_test = NftVoterTest::start_new().await; + +// let realm_cookie = squads_voter_test.governance.with_realm().await?; + +// let registrar_cookie = squads_voter_test.with_registrar(&realm_cookie).await?; + +// let realm_cookie2 = squads_voter_test.governance.with_realm().await?; + +// let voter_cookie = squads_voter_test.bench.with_wallet().await; + +// // Act +// let err = squads_voter_test +// .with_voter_weight_record_using_ix(®istrar_cookie, &voter_cookie, |i| { +// i.accounts[2].pubkey = realm_cookie2.address // Mint +// }) +// .await +// .err() +// .unwrap(); + +// // Assert + +// // PDA doesn't match and hence the error is PrivilegeEscalation +// assert_ix_err(err, InstructionError::PrivilegeEscalation); + +// Ok(()) +// } + +// #[tokio::test] +// async fn test_create_voter_weight_record_with_already_exists_error() -> Result<(), TransportError> { +// // Arrange +// let mut squads_voter_test = NftVoterTest::start_new().await; + +// let realm_cookie = squads_voter_test.governance.with_realm().await?; + +// let registrar_cookie = squads_voter_test.with_registrar(&realm_cookie).await?; + +// let voter_cookie = squads_voter_test.bench.with_wallet().await; + +// squads_voter_test +// .with_voter_weight_record(®istrar_cookie, &voter_cookie) +// .await?; + +// squads_voter_test.bench.advance_clock().await; + +// // Act +// let err = squads_voter_test +// .with_voter_weight_record(®istrar_cookie, &voter_cookie) +// .await +// .err() +// .unwrap(); + +// // Assert + +// // InstructionError::Custom(0) is returned for TransactionError::AccountInUse +// assert_ix_err(err, InstructionError::Custom(0)); + +// Ok(()) +// } diff --git a/programs/squads-voter/tests/fixtures/spl_governance.so b/programs/squads-voter/tests/fixtures/spl_governance.so new file mode 100755 index 00000000..9a6f5962 Binary files /dev/null and b/programs/squads-voter/tests/fixtures/spl_governance.so differ diff --git a/programs/squads-voter/tests/program_test/governance_test.rs b/programs/squads-voter/tests/program_test/governance_test.rs new file mode 100644 index 00000000..1e560ba6 --- /dev/null +++ b/programs/squads-voter/tests/program_test/governance_test.rs @@ -0,0 +1,389 @@ +use std::{str::FromStr, sync::Arc}; + +use anchor_lang::prelude::Pubkey; +use solana_program_test::ProgramTest; +use solana_sdk::{signature::Keypair, signer::Signer, transport::TransportError}; +use spl_governance::{ + instruction::{ + create_governance, create_proposal, create_realm, create_token_owner_record, + deposit_governing_tokens, relinquish_vote, sign_off_proposal, + }, + state::{ + enums::{ + GovernanceAccountType, MintMaxVoteWeightSource, ProposalState, VoteThresholdPercentage, + VoteTipping, + }, + governance::get_governance_address, + proposal::{get_proposal_address, ProposalV2}, + realm::{get_realm_address, RealmConfig, RealmV2}, + token_owner_record::{get_token_owner_record_address, TokenOwnerRecordV2}, + }, +}; + +use crate::program_test::{ + program_test_bench::{MintCookie, ProgramTestBench, WalletCookie}, + tools::clone_keypair, +}; + +pub struct RealmCookie { + pub address: Pubkey, + pub account: RealmV2, + pub realm_authority: Keypair, + pub community_mint_cookie: MintCookie, + pub council_mint_cookie: Option, +} + +impl RealmCookie { + pub fn get_realm_authority(&self) -> Keypair { + clone_keypair(&self.realm_authority) + } +} + +pub struct ProposalCookie { + pub address: Pubkey, + pub account: ProposalV2, +} + +pub struct TokenOwnerRecordCookie { + pub address: Pubkey, + pub account: TokenOwnerRecordV2, +} + +pub struct GovernanceTest { + pub program_id: Pubkey, + pub bench: Arc, + pub next_id: u8, + pub community_voter_weight_addin: Option, + pub max_community_voter_weight_addin: Option, +} + +impl GovernanceTest { + pub fn program_id() -> Pubkey { + Pubkey::from_str("Governance111111111111111111111111111111111").unwrap() + } + + #[allow(dead_code)] + pub fn add_program(program_test: &mut ProgramTest) { + program_test.add_program("spl_governance", Self::program_id(), None); + } + + #[allow(dead_code)] + pub fn new( + bench: Arc, + community_voter_weight_addin: Option, + max_community_voter_weight_addin: Option, + ) -> Self { + GovernanceTest { + bench, + program_id: Self::program_id(), + next_id: 0, + community_voter_weight_addin, + max_community_voter_weight_addin, + } + } + + #[allow(dead_code)] + pub async fn with_realm(&mut self) -> Result { + let realm_authority = Keypair::new(); + + let community_mint_cookie = self.bench.with_mint().await?; + let council_mint_cookie = self.bench.with_mint().await?; + + self.next_id += 1; + let realm_name = format!("Realm #{}", self.next_id).to_string(); + + let min_community_weight_to_create_governance = 1; + let community_mint_max_vote_weight_source = MintMaxVoteWeightSource::FULL_SUPPLY_FRACTION; + + let realm_key = get_realm_address(&self.program_id, &realm_name); + + let create_realm_ix = create_realm( + &self.program_id, + &realm_authority.pubkey(), + &community_mint_cookie.address, + &self.bench.payer.pubkey(), + Some(council_mint_cookie.address), + self.community_voter_weight_addin, + self.max_community_voter_weight_addin, + realm_name.clone(), + min_community_weight_to_create_governance, + community_mint_max_vote_weight_source.clone(), + ); + + self.bench + .process_transaction(&[create_realm_ix], None) + .await?; + + let account = RealmV2 { + account_type: GovernanceAccountType::RealmV2, + community_mint: community_mint_cookie.address, + + name: realm_name, + reserved: [0; 6], + authority: Some(realm_authority.pubkey()), + config: RealmConfig { + council_mint: Some(council_mint_cookie.address), + reserved: [0; 6], + min_community_weight_to_create_governance, + community_mint_max_vote_weight_source, + use_community_voter_weight_addin: false, + use_max_community_voter_weight_addin: false, + }, + voting_proposal_count: 0, + reserved_v2: [0; 128], + }; + + Ok(RealmCookie { + address: realm_key, + account, + realm_authority, + community_mint_cookie, + council_mint_cookie: Some(council_mint_cookie), + }) + } + + #[allow(dead_code)] + pub async fn with_proposal( + &mut self, + realm_cookie: &RealmCookie, + ) -> Result { + let token_account_cookie = self + .bench + .with_token_account(&realm_cookie.account.community_mint) + .await?; + + let token_owner = self.bench.payer.pubkey(); + let council_mint_cookie = realm_cookie.council_mint_cookie.as_ref().unwrap(); + let governing_token_mint = council_mint_cookie.address; + + let governing_token_account_cookie = self + .bench + .with_tokens(council_mint_cookie, &token_owner, 1) + .await?; + + let proposal_owner_record_key = get_token_owner_record_address( + &self.program_id, + &realm_cookie.address, + &governing_token_mint, + &token_owner, + ); + + let create_tor_ix = create_token_owner_record( + &self.program_id, + &realm_cookie.address, + &self.bench.payer.pubkey(), + &governing_token_mint, + &self.bench.payer.pubkey(), + ); + + self.bench + .process_transaction(&[create_tor_ix], None) + .await?; + + let deposit_ix = deposit_governing_tokens( + &self.program_id, + &realm_cookie.address, + &governing_token_account_cookie.address, + &token_owner, + &token_owner, + &self.bench.payer.pubkey(), + 1, + &governing_token_mint, + ); + + self.bench.process_transaction(&[deposit_ix], None).await?; + + let governance_key = get_governance_address( + &self.program_id, + &realm_cookie.address, + &token_account_cookie.address, + ); + + let create_governance_ix = create_governance( + &self.program_id, + &realm_cookie.address, + Some(&token_account_cookie.address), + &proposal_owner_record_key, + &self.bench.payer.pubkey(), + &realm_cookie.realm_authority.pubkey(), + None, + spl_governance::state::governance::GovernanceConfig { + vote_threshold_percentage: VoteThresholdPercentage::YesVote(60), + min_community_weight_to_create_proposal: 1, + min_transaction_hold_up_time: 0, + max_voting_time: 600, + vote_tipping: VoteTipping::Disabled, + proposal_cool_off_time: 0, + min_council_weight_to_create_proposal: 1, + }, + ); + + self.bench + .process_transaction( + &[create_governance_ix], + Some(&[&realm_cookie.realm_authority]), + ) + .await?; + + let proposal_index: u32 = 0; + let proposal_governing_token_mint = realm_cookie.account.community_mint; + + let proposal_key = get_proposal_address( + &self.program_id, + &governance_key, + &proposal_governing_token_mint, + &proposal_index.to_le_bytes(), + ); + + let create_proposal_ix = create_proposal( + &self.program_id, + &governance_key, + &proposal_owner_record_key, + &token_owner, + &self.bench.payer.pubkey(), + None, + &realm_cookie.address, + String::from("Proposal #1"), + String::from("Proposal #1 link"), + &proposal_governing_token_mint, + spl_governance::state::proposal::VoteType::SingleChoice, + vec!["Yes".to_string()], + true, + 0_u32, + ); + + let sign_off_proposal_ix = sign_off_proposal( + &self.program_id, + &realm_cookie.address, + &governance_key, + &proposal_key, + &token_owner, + Some(&proposal_owner_record_key), + ); + + self.bench + .process_transaction(&[create_proposal_ix, sign_off_proposal_ix], None) + .await?; + + let account = ProposalV2 { + account_type: GovernanceAccountType::GovernanceV2, + governing_token_mint: proposal_governing_token_mint, + state: ProposalState::Voting, + governance: governance_key, + token_owner_record: proposal_owner_record_key, + signatories_count: 1, + signatories_signed_off_count: 1, + vote_type: spl_governance::state::proposal::VoteType::SingleChoice, + options: vec![], + deny_vote_weight: Some(1), + veto_vote_weight: None, + abstain_vote_weight: None, + start_voting_at: None, + draft_at: 1, + signing_off_at: None, + voting_at: None, + voting_at_slot: None, + voting_completed_at: None, + executing_at: None, + closed_at: None, + execution_flags: spl_governance::state::enums::InstructionExecutionFlags::None, + max_vote_weight: None, + max_voting_time: None, + vote_threshold_percentage: None, + reserved: [0; 64], + name: String::from("Proposal #1"), + description_link: String::from("Proposal #1 link"), + }; + + Ok(ProposalCookie { + address: proposal_key, + account, + }) + } + + #[allow(dead_code)] + pub async fn with_token_owner_record( + &mut self, + realm_cookie: &RealmCookie, + token_owner_cookie: &WalletCookie, + ) -> Result { + let token_owner_record_key = get_token_owner_record_address( + &self.program_id, + &realm_cookie.address, + &realm_cookie.account.community_mint, + &token_owner_cookie.address, + ); + + let create_tor_ix = create_token_owner_record( + &self.program_id, + &realm_cookie.address, + &token_owner_cookie.address, + &realm_cookie.account.community_mint, + &self.bench.payer.pubkey(), + ); + + self.bench + .process_transaction(&[create_tor_ix], None) + .await?; + + let account = TokenOwnerRecordV2 { + account_type: GovernanceAccountType::TokenOwnerRecordV2, + realm: realm_cookie.address, + governing_token_mint: realm_cookie.account.community_mint, + governing_token_owner: token_owner_cookie.address, + governing_token_deposit_amount: 0, + unrelinquished_votes_count: 0, + total_votes_count: 0, + outstanding_proposal_count: 0, + reserved: [0; 7], + governance_delegate: None, + reserved_v2: [0; 128], + }; + + Ok(TokenOwnerRecordCookie { + address: token_owner_record_key, + account, + }) + } + + #[allow(dead_code)] + pub async fn relinquish_vote( + &mut self, + proposal_cookie: &ProposalCookie, + token_owner_cookie: &WalletCookie, + token_owner_record_cookie: &TokenOwnerRecordCookie, + ) -> Result<(), TransportError> { + let relinquish_vote_ix = relinquish_vote( + &self.program_id, + &proposal_cookie.account.governance, + &proposal_cookie.address, + &token_owner_record_cookie.address, + &proposal_cookie.account.governing_token_mint, + Some(token_owner_record_cookie.account.governing_token_owner), + Some(self.bench.payer.pubkey()), + ); + + self.bench + .process_transaction(&[relinquish_vote_ix], Some(&[&token_owner_cookie.signer])) + .await?; + + Ok(()) + } + + #[allow(dead_code)] + pub async fn get_proposal(&mut self, proposal_key: &Pubkey) -> ProposalV2 { + self.bench + .get_borsh_account::(proposal_key) + .await + } + + #[allow(dead_code)] + pub async fn get_token_owner_record( + &mut self, + token_owner_record_key: &Pubkey, + ) -> TokenOwnerRecordV2 { + self.bench + .get_borsh_account::(token_owner_record_key) + .await + } +} diff --git a/programs/squads-voter/tests/program_test/mod.rs b/programs/squads-voter/tests/program_test/mod.rs new file mode 100644 index 00000000..6ee8cb84 --- /dev/null +++ b/programs/squads-voter/tests/program_test/mod.rs @@ -0,0 +1,5 @@ +pub mod governance_test; +pub mod program_test_bench; +pub mod squads_test; +pub mod squads_voter_test; +pub mod tools; diff --git a/programs/squads-voter/tests/program_test/program_test_bench.rs b/programs/squads-voter/tests/program_test/program_test_bench.rs new file mode 100644 index 00000000..150a6009 --- /dev/null +++ b/programs/squads-voter/tests/program_test/program_test_bench.rs @@ -0,0 +1,324 @@ +use std::cell::RefCell; + +use anchor_lang::{ + prelude::{Pubkey, Rent}, + AccountDeserialize, +}; + +use solana_program::{borsh::try_from_slice_unchecked, system_program}; +use solana_program_test::{ProgramTest, ProgramTestContext}; +use solana_sdk::{ + account::{Account, ReadableAccount}, + instruction::Instruction, + program_pack::Pack, + signature::Keypair, + signer::Signer, + system_instruction, + transaction::Transaction, + transport::TransportError, +}; + +use borsh::BorshDeserialize; + +use crate::program_test::tools::clone_keypair; + +pub struct MintCookie { + pub address: Pubkey, + pub mint_authority: Keypair, + pub freeze_authority: Option, +} +pub struct TokenAccountCookie { + pub address: Pubkey, +} + +#[derive(Debug)] +pub struct WalletCookie { + pub address: Pubkey, + pub account: Account, + + pub signer: Keypair, +} + +pub struct ProgramTestBench { + pub context: RefCell, + pub payer: Keypair, + pub rent: Rent, +} + +impl ProgramTestBench { + /// Create new bench given a ProgramTest instance populated with all of the + /// desired programs. + pub async fn start_new(program_test: ProgramTest) -> Self { + let mut context = program_test.start_with_context().await; + + let payer = clone_keypair(&context.payer); + + let rent = context.banks_client.get_rent().await.unwrap(); + + Self { + payer, + context: RefCell::new(context), + rent, + } + } + + #[allow(dead_code)] + pub async fn process_transaction( + &self, + instructions: &[Instruction], + signers: Option<&[&Keypair]>, + ) -> Result<(), TransportError> { + let mut context = self.context.borrow_mut(); + + let mut transaction = + Transaction::new_with_payer(&instructions, Some(&context.payer.pubkey())); + + let mut all_signers = vec![&context.payer]; + + if let Some(signers) = signers { + all_signers.extend_from_slice(signers); + } + + transaction.sign(&all_signers, context.last_blockhash); + + context + .banks_client + .process_transaction_with_commitment( + transaction, + solana_sdk::commitment_config::CommitmentLevel::Processed, + ) + .await + } + + pub async fn get_clock(&self) -> solana_program::clock::Clock { + self.context + .borrow_mut() + .banks_client + .get_sysvar::() + .await + .unwrap() + } + + #[allow(dead_code)] + pub async fn advance_clock(&self) { + let clock = self.get_clock().await; + self.context + .borrow_mut() + .warp_to_slot(clock.slot + 2) + .unwrap(); + } + + pub async fn with_mint(&self) -> Result { + let mint_keypair = Keypair::new(); + let mint_authority = Keypair::new(); + let freeze_authority = Keypair::new(); + + self.create_mint(&mint_keypair, &mint_authority.pubkey(), None) + .await?; + + Ok(MintCookie { + address: mint_keypair.pubkey(), + mint_authority, + freeze_authority: Some(freeze_authority), + }) + } + + #[allow(dead_code)] + pub async fn create_mint( + &self, + mint_keypair: &Keypair, + mint_authority: &Pubkey, + freeze_authority: Option<&Pubkey>, + ) -> Result<(), TransportError> { + let mint_rent = self.rent.minimum_balance(spl_token::state::Mint::LEN); + + let instructions = [ + system_instruction::create_account( + &self.context.borrow().payer.pubkey(), + &mint_keypair.pubkey(), + mint_rent, + spl_token::state::Mint::LEN as u64, + &spl_token::id(), + ), + spl_token::instruction::initialize_mint( + &spl_token::id(), + &mint_keypair.pubkey(), + mint_authority, + freeze_authority, + 0, + ) + .unwrap(), + ]; + + self.process_transaction(&instructions, Some(&[mint_keypair])) + .await + } + + #[allow(dead_code)] + pub async fn with_token_account( + &self, + token_mint: &Pubkey, + ) -> Result { + let token_account_keypair = Keypair::new(); + self.create_token_account(&token_account_keypair, token_mint, &self.payer.pubkey()) + .await?; + + Ok(TokenAccountCookie { + address: token_account_keypair.pubkey(), + }) + } + + #[allow(dead_code)] + pub async fn with_tokens( + &self, + mint_cookie: &MintCookie, + owner: &Pubkey, + amount: u64, + ) -> Result { + let token_account_keypair = Keypair::new(); + + self.create_token_account(&token_account_keypair, &mint_cookie.address, owner) + .await?; + + self.mint_tokens( + &mint_cookie.address, + &mint_cookie.mint_authority, + &token_account_keypair.pubkey(), + amount, + ) + .await?; + + Ok(TokenAccountCookie { + address: token_account_keypair.pubkey(), + }) + } + + pub async fn mint_tokens( + &self, + token_mint: &Pubkey, + token_mint_authority: &Keypair, + token_account: &Pubkey, + amount: u64, + ) -> Result<(), TransportError> { + let mint_instruction = spl_token::instruction::mint_to( + &spl_token::id(), + token_mint, + token_account, + &token_mint_authority.pubkey(), + &[], + amount, + ) + .unwrap(); + + self.process_transaction(&[mint_instruction], Some(&[token_mint_authority])) + .await + } + + #[allow(dead_code)] + pub async fn create_token_account( + &self, + token_account_keypair: &Keypair, + token_mint: &Pubkey, + owner: &Pubkey, + ) -> Result<(), TransportError> { + let rent = self + .context + .borrow_mut() + .banks_client + .get_rent() + .await + .unwrap(); + + let create_account_instruction = system_instruction::create_account( + &self.context.borrow().payer.pubkey(), + &token_account_keypair.pubkey(), + rent.minimum_balance(spl_token::state::Account::get_packed_len()), + spl_token::state::Account::get_packed_len() as u64, + &spl_token::id(), + ); + + let initialize_account_instruction = spl_token::instruction::initialize_account( + &spl_token::id(), + &token_account_keypair.pubkey(), + token_mint, + owner, + ) + .unwrap(); + + self.process_transaction( + &[create_account_instruction, initialize_account_instruction], + Some(&[token_account_keypair]), + ) + .await + } + + #[allow(dead_code)] + pub async fn with_wallet(&self) -> WalletCookie { + let account_rent = self.rent.minimum_balance(0); + let account_keypair = Keypair::new(); + + let create_account_ix = system_instruction::create_account( + &self.context.borrow().payer.pubkey(), + &account_keypair.pubkey(), + account_rent, + 0, + &system_program::id(), + ); + + self.process_transaction(&[create_account_ix], Some(&[&account_keypair])) + .await + .unwrap(); + + let account = Account { + lamports: account_rent, + data: vec![], + owner: system_program::id(), + executable: false, + rent_epoch: 0, + }; + + WalletCookie { + address: account_keypair.pubkey(), + account, + signer: account_keypair, + } + } + + #[allow(dead_code)] + pub async fn get_account(&self, address: &Pubkey) -> Option { + self.context + .borrow_mut() + .banks_client + .get_account(*address) + .await + .unwrap() + } + + #[allow(dead_code)] + pub async fn get_borsh_account(&self, address: &Pubkey) -> T { + self.get_account(address) + .await + .map(|a| try_from_slice_unchecked(&a.data).unwrap()) + .unwrap_or_else(|| panic!("GET-TEST-ACCOUNT-ERROR: Account {} not found", address)) + } + + #[allow(dead_code)] + pub async fn get_account_data(&self, address: Pubkey) -> Vec { + self.context + .borrow_mut() + .banks_client + .get_account(address) + .await + .unwrap() + .unwrap() + .data() + .to_vec() + } + + #[allow(dead_code)] + pub async fn get_anchor_account(&self, address: Pubkey) -> T { + let data = self.get_account_data(address).await; + let mut data_slice: &[u8] = &data; + AccountDeserialize::try_deserialize(&mut data_slice).unwrap() + } +} diff --git a/programs/squads-voter/tests/program_test/squads_test.rs b/programs/squads-voter/tests/program_test/squads_test.rs new file mode 100644 index 00000000..25f3e53d --- /dev/null +++ b/programs/squads-voter/tests/program_test/squads_test.rs @@ -0,0 +1,67 @@ +use crate::program_test::program_test_bench::ProgramTestBench; +use anchor_lang::prelude::Pubkey; +use solana_program_test::ProgramTest; +use solana_sdk::transport::TransportError; +use std::{str::FromStr, sync::Arc}; + +pub struct SquadCookie { + pub address: Pubkey, +} + +pub struct SquadMemberCookie { + pub address: Pubkey, + pub squad_address: Pubkey, +} + +pub struct SquadsTest { + pub bench: Arc, + pub program_id: Pubkey, +} + +impl SquadsTest { + pub fn program_id() -> Pubkey { + Pubkey::from_str("Sqds1ufWkcv5z7K4RPXnrVTNq6Yw3zhEuajPkfhLpek").unwrap() + } + + #[allow(dead_code)] + pub fn add_program(program_test: &mut ProgramTest) { + // TODO: Add squads_protocol program to fixtures and replace it's name + program_test.add_program("spl_governance", Self::program_id(), None); + } + + #[allow(dead_code)] + pub fn new(bench: Arc) -> Self { + SquadsTest { + bench, + program_id: Self::program_id(), + } + } + + #[allow(dead_code)] + pub async fn with_squad(&mut self) -> Result { + // TODO: Create Squad + + let squad_address = Pubkey::new_unique(); + let squad_cookie = SquadCookie { + address: squad_address, + }; + + Ok(squad_cookie) + } + + #[allow(dead_code)] + pub async fn with_squad_member( + &mut self, + squad_cookie: &SquadCookie, + ) -> Result { + // TODO: Create Squad Member + + let squad_member = Pubkey::new_unique(); + let squad_member_cookie = SquadMemberCookie { + address: squad_member, + squad_address: squad_cookie.address, + }; + + Ok(squad_member_cookie) + } +} diff --git a/programs/squads-voter/tests/program_test/squads_voter_test.rs b/programs/squads-voter/tests/program_test/squads_voter_test.rs new file mode 100644 index 00000000..9bf4b553 --- /dev/null +++ b/programs/squads-voter/tests/program_test/squads_voter_test.rs @@ -0,0 +1,441 @@ +use std::sync::Arc; + +use anchor_lang::prelude::{AccountMeta, Pubkey}; + +use gpl_squads_voter::state::max_voter_weight_record::{ + get_max_voter_weight_record_address, MaxVoterWeightRecord, +}; +use gpl_squads_voter::state::*; +use solana_sdk::transport::TransportError; + +use solana_program_test::ProgramTest; +use solana_sdk::instruction::Instruction; +use solana_sdk::signature::Keypair; +use solana_sdk::signer::Signer; + +use crate::program_test::governance_test::GovernanceTest; +use crate::program_test::program_test_bench::ProgramTestBench; + +use crate::program_test::governance_test::RealmCookie; +use crate::program_test::program_test_bench::WalletCookie; + +use crate::program_test::tools::NopOverride; + +use crate::program_test::squads_test::{SquadCookie, SquadMemberCookie, SquadsTest}; + +#[derive(Debug, PartialEq)] +pub struct RegistrarCookie { + pub address: Pubkey, + pub account: Registrar, + + pub realm_authority: Keypair, + pub max_squads: u8, +} + +pub struct VoterWeightRecordCookie { + pub address: Pubkey, + pub account: VoterWeightRecord, +} + +pub struct MaxVoterWeightRecordCookie { + pub address: Pubkey, + pub account: MaxVoterWeightRecord, +} + +pub struct SquadConfigCookie { + pub squad_config: SquadConfig, +} + +pub struct ConfigureSquadArgs { + pub weight: u64, +} + +impl Default for ConfigureSquadArgs { + fn default() -> Self { + Self { weight: 1 } + } +} + +pub struct SquadsVoterTest { + pub program_id: Pubkey, + pub bench: Arc, + pub governance: GovernanceTest, + pub squads: SquadsTest, +} + +impl SquadsVoterTest { + #[allow(dead_code)] + pub fn add_program(program_test: &mut ProgramTest) { + program_test.add_program("gpl_squads_voter", gpl_squads_voter::id(), None); + } + + #[allow(dead_code)] + pub async fn start_new() -> Self { + let mut program_test = ProgramTest::default(); + + SquadsVoterTest::add_program(&mut program_test); + GovernanceTest::add_program(&mut program_test); + SquadsTest::add_program(&mut program_test); + + let program_id = gpl_squads_voter::id(); + + let bench = ProgramTestBench::start_new(program_test).await; + let bench_rc = Arc::new(bench); + + let governance_bench = + GovernanceTest::new(bench_rc.clone(), Some(program_id), Some(program_id)); + let squads_bench = SquadsTest::new(bench_rc.clone()); + + Self { + program_id, + bench: bench_rc, + governance: governance_bench, + squads: squads_bench, + } + } + + #[allow(dead_code)] + pub async fn with_registrar( + &mut self, + realm_cookie: &RealmCookie, + ) -> Result { + self.with_registrar_using_ix(realm_cookie, NopOverride, None) + .await + } + + #[allow(dead_code)] + pub async fn with_registrar_using_ix( + &mut self, + realm_cookie: &RealmCookie, + instruction_override: F, + signers_override: Option<&[&Keypair]>, + ) -> Result { + let registrar_key = + get_registrar_address(&realm_cookie.address, &realm_cookie.account.community_mint); + + let max_squads = 10; + + let data = + anchor_lang::InstructionData::data(&gpl_squads_voter::instruction::CreateRegistrar { + max_squads, + }); + + let accounts = anchor_lang::ToAccountMetas::to_account_metas( + &gpl_squads_voter::accounts::CreateRegistrar { + registrar: registrar_key, + realm: realm_cookie.address, + governance_program_id: self.governance.program_id, + governing_token_mint: realm_cookie.account.community_mint, + realm_authority: realm_cookie.get_realm_authority().pubkey(), + payer: self.bench.payer.pubkey(), + system_program: solana_sdk::system_program::id(), + }, + None, + ); + + let mut create_registrar_ix = Instruction { + program_id: gpl_squads_voter::id(), + accounts, + data, + }; + + instruction_override(&mut create_registrar_ix); + + let default_signers = &[&realm_cookie.realm_authority]; + let signers = signers_override.unwrap_or(default_signers); + + self.bench + .process_transaction(&[create_registrar_ix], Some(signers)) + .await?; + + let account = Registrar { + governance_program_id: self.governance.program_id, + realm: realm_cookie.address, + governing_token_mint: realm_cookie.account.community_mint, + squads_configs: vec![], + reserved: [0; 128], + }; + + Ok(RegistrarCookie { + address: registrar_key, + account, + realm_authority: realm_cookie.get_realm_authority(), + max_squads, + }) + } + + #[allow(dead_code)] + pub async fn with_voter_weight_record( + &self, + registrar_cookie: &RegistrarCookie, + voter_cookie: &WalletCookie, + ) -> Result { + self.with_voter_weight_record_using_ix(registrar_cookie, voter_cookie, NopOverride) + .await + } + + #[allow(dead_code)] + pub async fn with_voter_weight_record_using_ix( + &self, + registrar_cookie: &RegistrarCookie, + voter_cookie: &WalletCookie, + instruction_override: F, + ) -> Result { + let governing_token_owner = voter_cookie.address; + + let (voter_weight_record_key, _) = Pubkey::find_program_address( + &[ + b"voter-weight-record".as_ref(), + registrar_cookie.account.realm.as_ref(), + registrar_cookie.account.governing_token_mint.as_ref(), + governing_token_owner.as_ref(), + ], + &gpl_squads_voter::id(), + ); + + let data = anchor_lang::InstructionData::data( + &gpl_squads_voter::instruction::CreateVoterWeightRecord { + governing_token_owner, + }, + ); + + let accounts = gpl_squads_voter::accounts::CreateVoterWeightRecord { + governance_program_id: self.governance.program_id, + realm: registrar_cookie.account.realm, + realm_governing_token_mint: registrar_cookie.account.governing_token_mint, + voter_weight_record: voter_weight_record_key, + payer: self.bench.payer.pubkey(), + system_program: solana_sdk::system_program::id(), + }; + + let mut create_voter_weight_record_ix = Instruction { + program_id: gpl_squads_voter::id(), + accounts: anchor_lang::ToAccountMetas::to_account_metas(&accounts, None), + data, + }; + + instruction_override(&mut create_voter_weight_record_ix); + + self.bench + .process_transaction(&[create_voter_weight_record_ix], None) + .await?; + + let account = VoterWeightRecord { + realm: registrar_cookie.account.realm, + governing_token_mint: registrar_cookie.account.governing_token_mint, + governing_token_owner, + voter_weight: 0, + voter_weight_expiry: Some(0), + weight_action: None, + weight_action_target: None, + reserved: [0; 8], + }; + + Ok(VoterWeightRecordCookie { + address: voter_weight_record_key, + account, + }) + } + + #[allow(dead_code)] + pub async fn with_max_voter_weight_record( + &mut self, + registrar_cookie: &RegistrarCookie, + ) -> Result { + self.with_max_voter_weight_record_using_ix(registrar_cookie, NopOverride) + .await + } + + #[allow(dead_code)] + pub async fn with_max_voter_weight_record_using_ix( + &mut self, + registrar_cookie: &RegistrarCookie, + instruction_override: F, + ) -> Result { + let max_voter_weight_record_key = get_max_voter_weight_record_address( + ®istrar_cookie.account.realm, + ®istrar_cookie.account.governing_token_mint, + ); + + let data = anchor_lang::InstructionData::data( + &gpl_squads_voter::instruction::CreateMaxVoterWeightRecord {}, + ); + + let accounts = gpl_squads_voter::accounts::CreateMaxVoterWeightRecord { + governance_program_id: self.governance.program_id, + realm: registrar_cookie.account.realm, + realm_governing_token_mint: registrar_cookie.account.governing_token_mint, + max_voter_weight_record: max_voter_weight_record_key, + payer: self.bench.payer.pubkey(), + system_program: solana_sdk::system_program::id(), + }; + + let mut create_max_voter_weight_record_ix = Instruction { + program_id: gpl_squads_voter::id(), + accounts: anchor_lang::ToAccountMetas::to_account_metas(&accounts, None), + data, + }; + + instruction_override(&mut create_max_voter_weight_record_ix); + + self.bench + .process_transaction(&[create_max_voter_weight_record_ix], None) + .await?; + + let account = MaxVoterWeightRecord { + realm: registrar_cookie.account.realm, + governing_token_mint: registrar_cookie.account.governing_token_mint, + max_voter_weight: 0, + max_voter_weight_expiry: Some(0), + reserved: [0; 8], + }; + + Ok(MaxVoterWeightRecordCookie { + account, + address: max_voter_weight_record_key, + }) + } + + #[allow(dead_code)] + pub async fn update_voter_weight_record( + &self, + registrar_cookie: &RegistrarCookie, + voter_weight_record_cookie: &mut VoterWeightRecordCookie, + squads_member_cookies: &[&SquadMemberCookie], + ) -> Result<(), TransportError> { + let data = anchor_lang::InstructionData::data( + &gpl_squads_voter::instruction::UpdateVoterWeightRecord {}, + ); + + let accounts = gpl_squads_voter::accounts::UpdateVoterWeightRecord { + registrar: registrar_cookie.address, + voter_weight_record: voter_weight_record_cookie.address, + }; + + let mut account_metas = anchor_lang::ToAccountMetas::to_account_metas(&accounts, None); + + for squad_member_cookie in squads_member_cookies { + account_metas.push(AccountMeta::new_readonly( + squad_member_cookie.squad_address, + false, + )); + } + + let instructions = vec![Instruction { + program_id: gpl_squads_voter::id(), + accounts: account_metas, + data, + }]; + + self.bench.process_transaction(&instructions, None).await + } + + #[allow(dead_code)] + pub async fn update_max_voter_weight_record( + &self, + registrar_cookie: &RegistrarCookie, + max_voter_weight_record_cookie: &mut MaxVoterWeightRecordCookie, + squads_cookies: &[&SquadCookie], + ) -> Result<(), TransportError> { + let data = anchor_lang::InstructionData::data( + &gpl_squads_voter::instruction::UpdateMaxVoterWeightRecord {}, + ); + + let accounts = gpl_squads_voter::accounts::UpdateMaxVoterWeightRecord { + registrar: registrar_cookie.address, + max_voter_weight_record: max_voter_weight_record_cookie.address, + }; + + let mut account_metas = anchor_lang::ToAccountMetas::to_account_metas(&accounts, None); + + for squad_cookie in squads_cookies { + account_metas.push(AccountMeta::new_readonly(squad_cookie.address, false)); + } + + let instructions = vec![Instruction { + program_id: gpl_squads_voter::id(), + accounts: account_metas, + data, + }]; + + self.bench.process_transaction(&instructions, None).await + } + + #[allow(dead_code)] + pub async fn with_squad_config( + &mut self, + registrar_cookie: &RegistrarCookie, + squad_cookie: &SquadCookie, + args: Option, + ) -> Result { + self.with_squad_config_using_ix(registrar_cookie, squad_cookie, args, NopOverride, None) + .await + } + + #[allow(dead_code)] + pub async fn with_squad_config_using_ix( + &mut self, + registrar_cookie: &RegistrarCookie, + squad_cookie: &SquadCookie, + args: Option, + instruction_override: F, + signers_override: Option<&[&Keypair]>, + ) -> Result { + let args = args.unwrap_or_default(); + + let data = + anchor_lang::InstructionData::data(&gpl_squads_voter::instruction::ConfigureSquad { + weight: args.weight, + }); + + let accounts = gpl_squads_voter::accounts::ConfigureSquad { + registrar: registrar_cookie.address, + realm: registrar_cookie.account.realm, + realm_authority: registrar_cookie.realm_authority.pubkey(), + squad: squad_cookie.address, + }; + + let mut configure_squad_ix = Instruction { + program_id: gpl_squads_voter::id(), + accounts: anchor_lang::ToAccountMetas::to_account_metas(&accounts, None), + data, + }; + + instruction_override(&mut configure_squad_ix); + + let default_signers = &[®istrar_cookie.realm_authority]; + let signers = signers_override.unwrap_or(default_signers); + + self.bench + .process_transaction(&[configure_squad_ix], Some(signers)) + .await?; + + let squad_config = SquadConfig { + squad: squad_cookie.address, + weight: args.weight, + reserved: [0; 8], + }; + + Ok(SquadConfigCookie { squad_config }) + } + + #[allow(dead_code)] + pub async fn get_registrar_account(&mut self, registrar: &Pubkey) -> Registrar { + self.bench.get_anchor_account::(*registrar).await + } + + #[allow(dead_code)] + pub async fn get_max_voter_weight_record( + &self, + max_voter_weight_record: &Pubkey, + ) -> MaxVoterWeightRecord { + self.bench + .get_anchor_account(*max_voter_weight_record) + .await + } + + #[allow(dead_code)] + pub async fn get_voter_weight_record(&self, voter_weight_record: &Pubkey) -> VoterWeightRecord { + self.bench.get_anchor_account(*voter_weight_record).await + } +} diff --git a/programs/squads-voter/tests/program_test/tools.rs b/programs/squads-voter/tests/program_test/tools.rs new file mode 100644 index 00000000..eb1959f5 --- /dev/null +++ b/programs/squads-voter/tests/program_test/tools.rs @@ -0,0 +1,79 @@ +use anchor_lang::prelude::ERROR_CODE_OFFSET; +use gpl_squads_voter::error::SquadsVoterError; +use solana_program::instruction::InstructionError; +use solana_sdk::{signature::Keypair, transaction::TransactionError, transport::TransportError}; +use spl_governance_tools::error::GovernanceToolsError; + +pub fn clone_keypair(source: &Keypair) -> Keypair { + Keypair::from_bytes(&source.to_bytes()).unwrap() +} + +/// NOP (No Operation) Override function +#[allow(non_snake_case)] +pub fn NopOverride(_: &mut T) {} + +#[allow(dead_code)] +pub fn assert_squads_voter_err( + banks_client_error: TransportError, + squad_voter_error: SquadsVoterError, +) { + let tx_error = banks_client_error.unwrap(); + + match tx_error { + TransactionError::InstructionError(_, instruction_error) => match instruction_error { + InstructionError::Custom(e) => { + assert_eq!(e, squad_voter_error as u32 + ERROR_CODE_OFFSET) + } + _ => panic!("{:?} Is not InstructionError::Custom()", instruction_error), + }, + _ => panic!("{:?} Is not InstructionError", tx_error), + }; +} + +#[allow(dead_code)] +pub fn assert_gov_tools_err( + banks_client_error: TransportError, + gov_tools_error: GovernanceToolsError, +) { + let tx_error = banks_client_error.unwrap(); + + match tx_error { + TransactionError::InstructionError(_, instruction_error) => match instruction_error { + InstructionError::Custom(e) => { + assert_eq!(e, gov_tools_error as u32) + } + _ => panic!("{:?} Is not InstructionError::Custom()", instruction_error), + }, + _ => panic!("{:?} Is not InstructionError", tx_error), + }; +} + +#[allow(dead_code)] +pub fn assert_anchor_err( + banks_client_error: TransportError, + anchor_error: anchor_lang::error::ErrorCode, +) { + let tx_error = banks_client_error.unwrap(); + + match tx_error { + TransactionError::InstructionError(_, instruction_error) => match instruction_error { + InstructionError::Custom(e) => { + assert_eq!(e, anchor_error as u32) + } + _ => panic!("{:?} Is not InstructionError::Custom()", instruction_error), + }, + _ => panic!("{:?} Is not InstructionError", tx_error), + }; +} + +#[allow(dead_code)] +pub fn assert_ix_err(banks_client_error: TransportError, ix_error: InstructionError) { + let tx_error = banks_client_error.unwrap(); + + match tx_error { + TransactionError::InstructionError(_, instruction_error) => { + assert_eq!(instruction_error, ix_error); + } + _ => panic!("{:?} Is not InstructionError", tx_error), + }; +} diff --git a/programs/squads-voter/tests/update_max_voter_weight_record.rs b/programs/squads-voter/tests/update_max_voter_weight_record.rs new file mode 100644 index 00000000..f06eb5d0 --- /dev/null +++ b/programs/squads-voter/tests/update_max_voter_weight_record.rs @@ -0,0 +1,60 @@ +use crate::program_test::squads_voter_test::{ConfigureSquadArgs, SquadsVoterTest}; +use solana_program_test::*; +use solana_sdk::transport::TransportError; + +mod program_test; + +#[tokio::test] +async fn test_update_max_voter_weight_record() -> Result<(), TransportError> { + // Arrange + let mut squads_voter_test = SquadsVoterTest::start_new().await; + + let realm_cookie = squads_voter_test.governance.with_realm().await?; + + let registrar_cookie = squads_voter_test.with_registrar(&realm_cookie).await?; + + let squad_cookie = squads_voter_test.squads.with_squad().await?; + + squads_voter_test + .with_squad_config( + ®istrar_cookie, + &squad_cookie, + Some(ConfigureSquadArgs { weight: 1 }), + ) + .await?; + + let mut max_voter_weight_record_cookie = squads_voter_test + .with_max_voter_weight_record(®istrar_cookie) + .await?; + + squads_voter_test.bench.advance_clock().await; + let clock = squads_voter_test.bench.get_clock().await; + + // Act + squads_voter_test + .update_max_voter_weight_record( + ®istrar_cookie, + &mut max_voter_weight_record_cookie, + &[&squad_cookie], + ) + .await?; + + // Assert + + let max_voter_weight_record = squads_voter_test + .get_max_voter_weight_record(&max_voter_weight_record_cookie.address) + .await; + + assert_eq!(max_voter_weight_record.max_voter_weight, 10); + assert_eq!( + max_voter_weight_record.max_voter_weight_expiry, + Some(clock.slot) + ); + assert_eq!(max_voter_weight_record.realm, realm_cookie.address); + assert_eq!( + max_voter_weight_record.governing_token_mint, + realm_cookie.account.community_mint + ); + + Ok(()) +} diff --git a/programs/squads-voter/tests/update_voter_weight_record.rs b/programs/squads-voter/tests/update_voter_weight_record.rs new file mode 100644 index 00000000..c3e1fd36 --- /dev/null +++ b/programs/squads-voter/tests/update_voter_weight_record.rs @@ -0,0 +1,558 @@ +use crate::program_test::squads_voter_test::{ConfigureSquadArgs, SquadsVoterTest}; +use solana_program_test::*; +use solana_sdk::transport::TransportError; + +mod program_test; + +#[tokio::test] +async fn test_update_voter_weight_record() -> Result<(), TransportError> { + // Arrange + let mut squads_voter_test = SquadsVoterTest::start_new().await; + + let realm_cookie = squads_voter_test.governance.with_realm().await?; + + let registrar_cookie = squads_voter_test.with_registrar(&realm_cookie).await?; + + let squad_cookie = squads_voter_test.squads.with_squad().await?; + + squads_voter_test + .with_squad_config( + ®istrar_cookie, + &squad_cookie, + Some(ConfigureSquadArgs { weight: 10 }), + ) + .await?; + + let voter_cookie = squads_voter_test.bench.with_wallet().await; + + let mut voter_weight_record_cookie = squads_voter_test + .with_voter_weight_record(®istrar_cookie, &voter_cookie) + .await?; + + let squad_member_cookie = squads_voter_test + .squads + .with_squad_member(&squad_cookie) + .await?; + + squads_voter_test.bench.advance_clock().await; + let clock = squads_voter_test.bench.get_clock().await; + + // Act + squads_voter_test + .update_voter_weight_record( + ®istrar_cookie, + &mut voter_weight_record_cookie, + &[&squad_member_cookie], + ) + .await?; + + // Assert + + let voter_weight_record = squads_voter_test + .get_voter_weight_record(&voter_weight_record_cookie.address) + .await; + + assert_eq!(voter_weight_record.voter_weight, 10); + assert_eq!(voter_weight_record.voter_weight_expiry, Some(clock.slot)); + assert_eq!(voter_weight_record.weight_action, None); + assert_eq!(voter_weight_record.weight_action_target, None); + + Ok(()) +} + +// #[tokio::test] +// async fn test_update_voter_weight_with_multiple_nfts() -> Result<(), TransportError> { +// // Arrange +// let mut squads_voter_test = NftVoterTest::start_new().await; + +// let realm_cookie = squads_voter_test.governance.with_realm().await?; + +// let registrar_cookie = squads_voter_test.with_registrar(&realm_cookie).await?; + +// let nft_collection_cookie = squads_voter_test.token_metadata.with_nft_collection().await?; + +// let max_voter_weight_record_cookie = squads_voter_test +// .with_max_voter_weight_record(®istrar_cookie) +// .await?; + +// let _collection_config_cookie = squads_voter_test +// .with_collection( +// ®istrar_cookie, +// &nft_collection_cookie, +// &max_voter_weight_record_cookie, +// Some(ConfigureCollectionArgs { +// weight: 10, +// size: 20, +// }), +// ) +// .await?; + +// let voter_cookie = squads_voter_test.bench.with_wallet().await; + +// let mut voter_weight_record_cookie = squads_voter_test +// .with_voter_weight_record(®istrar_cookie, &voter_cookie) +// .await?; + +// let nft_cookie1 = squads_voter_test +// .token_metadata +// .with_nft_v2(&nft_collection_cookie, &voter_cookie, None) +// .await?; + +// let nft_cookie2 = squads_voter_test +// .token_metadata +// .with_nft_v2(&nft_collection_cookie, &voter_cookie, None) +// .await?; + +// squads_voter_test.bench.advance_clock().await; +// let clock = squads_voter_test.bench.get_clock().await; + +// // Act +// squads_voter_test +// .update_voter_weight_record( +// ®istrar_cookie, +// &mut voter_weight_record_cookie, +// VoterWeightAction::CreateProposal, +// &[&nft_cookie1, &nft_cookie2], +// ) +// .await?; + +// // Assert + +// let voter_weight_record = squads_voter_test +// .get_voter_weight_record(&voter_weight_record_cookie.address) +// .await; + +// assert_eq!(voter_weight_record.voter_weight, 20); +// assert_eq!(voter_weight_record.voter_weight_expiry, Some(clock.slot)); +// assert_eq!( +// voter_weight_record.weight_action, +// Some(VoterWeightAction::CreateProposal.into()) +// ); +// assert_eq!(voter_weight_record.weight_action_target, None); + +// Ok(()) +// } + +// #[tokio::test] +// async fn test_update_voter_weight_with_cast_vote_not_allowed_error() -> Result<(), TransportError> { +// // Arrange +// let mut squads_voter_test = NftVoterTest::start_new().await; + +// let realm_cookie = squads_voter_test.governance.with_realm().await?; + +// let registrar_cookie = squads_voter_test.with_registrar(&realm_cookie).await?; + +// let nft_collection_cookie = squads_voter_test.token_metadata.with_nft_collection().await?; + +// let max_voter_weight_record_cookie = squads_voter_test +// .with_max_voter_weight_record(®istrar_cookie) +// .await?; + +// squads_voter_test +// .with_collection( +// ®istrar_cookie, +// &nft_collection_cookie, +// &max_voter_weight_record_cookie, +// Some(ConfigureCollectionArgs { +// weight: 10, +// size: 20, +// }), +// ) +// .await?; + +// let voter_cookie = squads_voter_test.bench.with_wallet().await; + +// let mut voter_weight_record_cookie = squads_voter_test +// .with_voter_weight_record(®istrar_cookie, &voter_cookie) +// .await?; + +// let nft1_cookie = squads_voter_test +// .token_metadata +// .with_nft_v2(&nft_collection_cookie, &voter_cookie, None) +// .await?; + +// // Act +// let err = squads_voter_test +// .update_voter_weight_record( +// ®istrar_cookie, +// &mut voter_weight_record_cookie, +// VoterWeightAction::CastVote, +// &[&nft1_cookie], +// ) +// .await +// .err() +// .unwrap(); + +// // Assert +// assert_nft_voter_err(err, NftVoterError::CastVoteIsNotAllowed); + +// Ok(()) +// } + +// #[tokio::test] +// async fn test_update_voter_weight_with_unverified_collection_error() -> Result<(), TransportError> { +// // Arrange +// let mut squads_voter_test = NftVoterTest::start_new().await; + +// let realm_cookie = squads_voter_test.governance.with_realm().await?; + +// let registrar_cookie = squads_voter_test.with_registrar(&realm_cookie).await?; + +// let nft_collection_cookie = squads_voter_test.token_metadata.with_nft_collection().await?; + +// let max_voter_weight_record_cookie = squads_voter_test +// .with_max_voter_weight_record(®istrar_cookie) +// .await?; + +// squads_voter_test +// .with_collection( +// ®istrar_cookie, +// &nft_collection_cookie, +// &max_voter_weight_record_cookie, +// Some(ConfigureCollectionArgs { +// weight: 10, +// size: 20, +// }), +// ) +// .await?; + +// let voter_cookie = squads_voter_test.bench.with_wallet().await; + +// let mut voter_weight_record_cookie = squads_voter_test +// .with_voter_weight_record(®istrar_cookie, &voter_cookie) +// .await?; + +// // Create NFT without verified collection +// let nft1_cookie = squads_voter_test +// .token_metadata +// .with_nft_v2( +// &nft_collection_cookie, +// &voter_cookie, +// Some(CreateNftArgs { +// verify_collection: false, +// ..Default::default() +// }), +// ) +// .await?; + +// // Act +// let err = squads_voter_test +// .update_voter_weight_record( +// ®istrar_cookie, +// &mut voter_weight_record_cookie, +// VoterWeightAction::CreateGovernance, +// &[&nft1_cookie], +// ) +// .await +// .err() +// .unwrap(); + +// // Assert +// assert_nft_voter_err(err, NftVoterError::CollectionMustBeVerified); + +// Ok(()) +// } + +// #[tokio::test] +// async fn test_update_voter_weight_with_invalid_owner_error() -> Result<(), TransportError> { +// // Arrange +// let mut squads_voter_test = NftVoterTest::start_new().await; + +// let realm_cookie = squads_voter_test.governance.with_realm().await?; + +// let registrar_cookie = squads_voter_test.with_registrar(&realm_cookie).await?; + +// let nft_collection_cookie = squads_voter_test.token_metadata.with_nft_collection().await?; + +// let max_voter_weight_record_cookie = squads_voter_test +// .with_max_voter_weight_record(®istrar_cookie) +// .await?; + +// squads_voter_test +// .with_collection( +// ®istrar_cookie, +// &nft_collection_cookie, +// &max_voter_weight_record_cookie, +// Some(ConfigureCollectionArgs { +// weight: 10, +// size: 20, +// }), +// ) +// .await?; + +// let voter_cookie = squads_voter_test.bench.with_wallet().await; + +// let mut voter_weight_record_cookie = squads_voter_test +// .with_voter_weight_record(®istrar_cookie, &voter_cookie) +// .await?; + +// let voter_cookie2 = squads_voter_test.bench.with_wallet().await; + +// let nft1_cookie = squads_voter_test +// .token_metadata +// .with_nft_v2(&nft_collection_cookie, &voter_cookie2, None) +// .await?; + +// // Act +// let err = squads_voter_test +// .update_voter_weight_record( +// ®istrar_cookie, +// &mut voter_weight_record_cookie, +// VoterWeightAction::CreateGovernance, +// &[&nft1_cookie], +// ) +// .await +// .err() +// .unwrap(); + +// // Assert +// assert_nft_voter_err(err, NftVoterError::VoterDoesNotOwnNft); + +// Ok(()) +// } + +// #[tokio::test] +// async fn test_update_voter_weight_with_invalid_collection_error() -> Result<(), TransportError> { +// // Arrange +// let mut squads_voter_test = NftVoterTest::start_new().await; + +// let realm_cookie = squads_voter_test.governance.with_realm().await?; + +// let registrar_cookie = squads_voter_test.with_registrar(&realm_cookie).await?; + +// let nft_collection_cookie = squads_voter_test.token_metadata.with_nft_collection().await?; + +// let max_voter_weight_record_cookie = squads_voter_test +// .with_max_voter_weight_record(®istrar_cookie) +// .await?; + +// squads_voter_test +// .with_collection( +// ®istrar_cookie, +// &nft_collection_cookie, +// &max_voter_weight_record_cookie, +// Some(ConfigureCollectionArgs { +// weight: 10, +// size: 20, +// }), +// ) +// .await?; + +// let voter_cookie = squads_voter_test.bench.with_wallet().await; + +// let mut voter_weight_record_cookie = squads_voter_test +// .with_voter_weight_record(®istrar_cookie, &voter_cookie) +// .await?; + +// let nft_collection_cookie2 = squads_voter_test.token_metadata.with_nft_collection().await?; + +// let nft1_cookie = squads_voter_test +// .token_metadata +// .with_nft_v2(&nft_collection_cookie2, &voter_cookie, None) +// .await?; + +// // Act +// let err = squads_voter_test +// .update_voter_weight_record( +// ®istrar_cookie, +// &mut voter_weight_record_cookie, +// VoterWeightAction::CreateGovernance, +// &[&nft1_cookie], +// ) +// .await +// .err() +// .unwrap(); + +// // Assert +// assert_nft_voter_err(err, NftVoterError::CollectionNotFound); + +// Ok(()) +// } + +// #[tokio::test] +// async fn test_update_voter_weight_with_invalid_metadata_error() -> Result<(), TransportError> { +// // Arrange +// let mut squads_voter_test = NftVoterTest::start_new().await; + +// let realm_cookie = squads_voter_test.governance.with_realm().await?; + +// let registrar_cookie = squads_voter_test.with_registrar(&realm_cookie).await?; + +// let nft_collection_cookie = squads_voter_test.token_metadata.with_nft_collection().await?; + +// let max_voter_weight_record_cookie = squads_voter_test +// .with_max_voter_weight_record(®istrar_cookie) +// .await?; + +// squads_voter_test +// .with_collection( +// ®istrar_cookie, +// &nft_collection_cookie, +// &max_voter_weight_record_cookie, +// Some(ConfigureCollectionArgs { +// weight: 10, +// size: 20, +// }), +// ) +// .await?; + +// let voter_cookie = squads_voter_test.bench.with_wallet().await; + +// let mut voter_weight_record_cookie = squads_voter_test +// .with_voter_weight_record(®istrar_cookie, &voter_cookie) +// .await?; + +// let mut nft1_cookie = squads_voter_test +// .token_metadata +// .with_nft_v2( +// &nft_collection_cookie, +// &voter_cookie, +// Some(CreateNftArgs { +// verify_collection: false, +// ..Default::default() +// }), +// ) +// .await?; + +// let nft2_cookie = squads_voter_test +// .token_metadata +// .with_nft_v2(&nft_collection_cookie, &voter_cookie, None) +// .await?; + +// // Try to use verified NFT Metadata +// nft1_cookie.metadata = nft2_cookie.metadata; + +// // Act +// let err = squads_voter_test +// .update_voter_weight_record( +// ®istrar_cookie, +// &mut voter_weight_record_cookie, +// VoterWeightAction::CreateGovernance, +// &[&nft1_cookie], +// ) +// .await +// .err() +// .unwrap(); + +// // Assert +// assert_nft_voter_err(err, NftVoterError::TokenMetadataDoesNotMatch); + +// Ok(()) +// } + +// #[tokio::test] +// async fn test_update_voter_weight_with_same_nft_error() -> Result<(), TransportError> { +// // Arrange +// let mut squads_voter_test = NftVoterTest::start_new().await; + +// let realm_cookie = squads_voter_test.governance.with_realm().await?; + +// let registrar_cookie = squads_voter_test.with_registrar(&realm_cookie).await?; + +// let nft_collection_cookie = squads_voter_test.token_metadata.with_nft_collection().await?; + +// let max_voter_weight_record_cookie = squads_voter_test +// .with_max_voter_weight_record(®istrar_cookie) +// .await?; + +// squads_voter_test +// .with_collection( +// ®istrar_cookie, +// &nft_collection_cookie, +// &max_voter_weight_record_cookie, +// None, +// ) +// .await?; + +// let voter_cookie = squads_voter_test.bench.with_wallet().await; + +// let mut voter_weight_record_cookie = squads_voter_test +// .with_voter_weight_record(®istrar_cookie, &voter_cookie) +// .await?; + +// let nft_cookie = squads_voter_test +// .token_metadata +// .with_nft_v2(&nft_collection_cookie, &voter_cookie, None) +// .await?; + +// // Act +// let err = squads_voter_test +// .update_voter_weight_record( +// ®istrar_cookie, +// &mut voter_weight_record_cookie, +// VoterWeightAction::CreateProposal, +// &[&nft_cookie, &nft_cookie], +// ) +// .await +// .err() +// .unwrap(); + +// // Assert + +// assert_nft_voter_err(err, NftVoterError::DuplicatedNftDetected); + +// Ok(()) +// } + +// #[tokio::test] +// async fn test_update_voter_weight_record_with_no_nft_error() -> Result<(), TransportError> { +// // Arrange +// let mut squads_voter_test = NftVoterTest::start_new().await; + +// let realm_cookie = squads_voter_test.governance.with_realm().await?; + +// let registrar_cookie = squads_voter_test.with_registrar(&realm_cookie).await?; + +// let nft_collection_cookie = squads_voter_test.token_metadata.with_nft_collection().await?; + +// let max_voter_weight_record_cookie = squads_voter_test +// .with_max_voter_weight_record(®istrar_cookie) +// .await?; + +// let _collection_config_cookie = squads_voter_test +// .with_collection( +// ®istrar_cookie, +// &nft_collection_cookie, +// &max_voter_weight_record_cookie, +// Some(ConfigureCollectionArgs { +// weight: 10, +// size: 20, +// }), +// ) +// .await?; + +// let voter_cookie = squads_voter_test.bench.with_wallet().await; + +// let mut voter_weight_record_cookie = squads_voter_test +// .with_voter_weight_record(®istrar_cookie, &voter_cookie) +// .await?; + +// let nft1_cookie = squads_voter_test +// .token_metadata +// .with_nft_v2( +// &nft_collection_cookie, +// &voter_cookie, +// Some(CreateNftArgs { +// amount: 0, +// ..Default::default() +// }), +// ) +// .await?; + +// // Act +// let err = squads_voter_test +// .update_voter_weight_record( +// ®istrar_cookie, +// &mut voter_weight_record_cookie, +// VoterWeightAction::CreateProposal, +// &[&nft1_cookie], +// ) +// .await +// .err() +// .unwrap(); + +// // Assert +// assert_nft_voter_err(err, NftVoterError::InvalidNftAmount); + +// Ok(()) +// }