use {
crate::{
addins::voter_weight::{
assert_is_valid_voter_weight, get_voter_weight_record_data_for_token_owner_record,
},
error::GovernanceError,
state::{
enums::GovernanceAccountType, governance::GovernanceConfig, legacy::TokenOwnerRecordV1,
realm::RealmV2, realm_config::RealmConfigAccount,
},
PROGRAM_AUTHORITY_SEED,
},
borsh::{maybestd::io::Write, BorshDeserialize, BorshSchema, BorshSerialize},
solana_program::{
account_info::{next_account_info, AccountInfo},
program_error::ProgramError,
program_pack::IsInitialized,
pubkey::Pubkey,
},
spl_governance_addin_api::voter_weight::VoterWeightAction,
spl_governance_tools::account::{get_account_data, get_account_type, AccountMaxSize},
std::slice::Iter,
};
#[derive(Clone, Debug, PartialEq, Eq, BorshDeserialize, BorshSerialize, BorshSchema)]
pub struct TokenOwnerRecordV2 {
pub account_type: GovernanceAccountType,
pub realm: Pubkey,
pub governing_token_mint: Pubkey,
pub governing_token_owner: Pubkey,
pub governing_token_deposit_amount: u64,
pub unrelinquished_votes_count: u64,
pub outstanding_proposal_count: u8,
pub version: u8,
pub reserved: [u8; 6],
pub governance_delegate: Option<Pubkey>,
pub reserved_v2: [u8; 128],
}
pub const TOKEN_OWNER_RECORD_LAYOUT_VERSION: u8 = 1;
impl AccountMaxSize for TokenOwnerRecordV2 {
fn get_max_size(&self) -> Option<usize> {
Some(282)
}
}
impl IsInitialized for TokenOwnerRecordV2 {
fn is_initialized(&self) -> bool {
self.account_type == GovernanceAccountType::TokenOwnerRecordV2
}
}
impl TokenOwnerRecordV2 {
pub fn assert_token_owner_or_delegate_is_signer(
&self,
governance_authority_info: &AccountInfo,
) -> Result<(), ProgramError> {
if governance_authority_info.is_signer {
if &self.governing_token_owner == governance_authority_info.key {
return Ok(());
}
if let Some(governance_delegate) = self.governance_delegate {
if &governance_delegate == governance_authority_info.key {
return Ok(());
}
};
}
Err(GovernanceError::GoverningTokenOwnerOrDelegateMustSign.into())
}
pub fn assert_can_create_proposal(
&self,
realm_data: &RealmV2,
config: &GovernanceConfig,
voter_weight: u64,
) -> Result<(), ProgramError> {
let min_weight_to_create_proposal =
if self.governing_token_mint == realm_data.community_mint {
config.min_community_weight_to_create_proposal
} else if Some(self.governing_token_mint) == realm_data.config.council_mint {
config.min_council_weight_to_create_proposal
} else {
return Err(GovernanceError::InvalidGoverningTokenMint.into());
};
if min_weight_to_create_proposal == u64::MAX {
return Err(GovernanceError::VoterWeightThresholdDisabled.into());
}
if voter_weight < min_weight_to_create_proposal {
return Err(GovernanceError::NotEnoughTokensToCreateProposal.into());
}
if self.outstanding_proposal_count >= 10 {
return Err(GovernanceError::TooManyOutstandingProposals.into());
}
Ok(())
}
pub fn assert_can_create_governance(
&self,
realm_data: &RealmV2,
voter_weight: u64,
) -> Result<(), ProgramError> {
let min_weight_to_create_governance =
if self.governing_token_mint == realm_data.community_mint {
realm_data.config.min_community_weight_to_create_governance
} else if Some(self.governing_token_mint) == realm_data.config.council_mint {
1
} else {
return Err(GovernanceError::InvalidGoverningTokenMint.into());
};
if min_weight_to_create_governance == u64::MAX {
return Err(GovernanceError::VoterWeightThresholdDisabled.into());
}
if voter_weight < min_weight_to_create_governance {
return Err(GovernanceError::NotEnoughTokensToCreateGovernance.into());
}
Ok(())
}
pub fn assert_can_withdraw_governing_tokens(&self) -> Result<(), ProgramError> {
if self.unrelinquished_votes_count > 0 {
return Err(
GovernanceError::AllVotesMustBeRelinquishedToWithdrawGoverningTokens.into(),
);
}
if self.outstanding_proposal_count > 0 {
return Err(
GovernanceError::AllProposalsMustBeFinalisedToWithdrawGoverningTokens.into(),
);
}
Ok(())
}
pub fn decrease_outstanding_proposal_count(&mut self) {
if self.outstanding_proposal_count != 0 {
self.outstanding_proposal_count =
self.outstanding_proposal_count.checked_sub(1).unwrap();
}
}
#[allow(clippy::too_many_arguments)]
pub fn resolve_voter_weight(
&self,
account_info_iter: &mut Iter<AccountInfo>,
realm_data: &RealmV2,
realm_config_data: &RealmConfigAccount,
weight_action: VoterWeightAction,
weight_action_target: &Pubkey,
) -> Result<u64, ProgramError> {
if let Some(voter_weight_addin) = realm_config_data
.get_token_config(realm_data, &self.governing_token_mint)?
.voter_weight_addin
{
let voter_weight_record_info = next_account_info(account_info_iter)?;
let voter_weight_record_data = get_voter_weight_record_data_for_token_owner_record(
&voter_weight_addin,
voter_weight_record_info,
self,
)?;
assert_is_valid_voter_weight(
&voter_weight_record_data,
weight_action,
weight_action_target,
)?;
Ok(voter_weight_record_data.voter_weight)
} else {
Ok(self.governing_token_deposit_amount)
}
}
pub fn serialize<W: Write>(self, writer: W) -> Result<(), ProgramError> {
if self.account_type == GovernanceAccountType::TokenOwnerRecordV2 {
borsh::to_writer(writer, &self)?
} else if self.account_type == GovernanceAccountType::TokenOwnerRecordV1 {
if self.reserved_v2 != [0; 128] {
panic!("Extended data not supported by TokenOwnerRecordV1")
}
let token_owner_record_data_v1 = TokenOwnerRecordV1 {
account_type: self.account_type,
realm: self.realm,
governing_token_mint: self.governing_token_mint,
governing_token_owner: self.governing_token_owner,
governing_token_deposit_amount: self.governing_token_deposit_amount,
unrelinquished_votes_count: self.unrelinquished_votes_count,
outstanding_proposal_count: self.outstanding_proposal_count,
version: self.version,
reserved: self.reserved,
governance_delegate: self.governance_delegate,
};
borsh::to_writer(writer, &token_owner_record_data_v1)?
}
Ok(())
}
}
pub fn get_token_owner_record_address(
program_id: &Pubkey,
realm: &Pubkey,
governing_token_mint: &Pubkey,
governing_token_owner: &Pubkey,
) -> Pubkey {
Pubkey::find_program_address(
&get_token_owner_record_address_seeds(realm, governing_token_mint, governing_token_owner),
program_id,
)
.0
}
pub fn get_token_owner_record_address_seeds<'a>(
realm: &'a Pubkey,
governing_token_mint: &'a Pubkey,
governing_token_owner: &'a Pubkey,
) -> [&'a [u8]; 4] {
[
PROGRAM_AUTHORITY_SEED,
realm.as_ref(),
governing_token_mint.as_ref(),
governing_token_owner.as_ref(),
]
}
pub fn get_token_owner_record_data(
program_id: &Pubkey,
token_owner_record_info: &AccountInfo,
) -> Result<TokenOwnerRecordV2, ProgramError> {
let account_type: GovernanceAccountType =
get_account_type(program_id, token_owner_record_info)?;
let mut token_owner_record_data = if account_type == GovernanceAccountType::TokenOwnerRecordV1 {
let token_owner_record_data_v1 =
get_account_data::<TokenOwnerRecordV1>(program_id, token_owner_record_info)?;
TokenOwnerRecordV2 {
account_type,
realm: token_owner_record_data_v1.realm,
governing_token_mint: token_owner_record_data_v1.governing_token_mint,
governing_token_owner: token_owner_record_data_v1.governing_token_owner,
governing_token_deposit_amount: token_owner_record_data_v1
.governing_token_deposit_amount,
unrelinquished_votes_count: token_owner_record_data_v1.unrelinquished_votes_count,
outstanding_proposal_count: token_owner_record_data_v1.outstanding_proposal_count,
version: token_owner_record_data_v1.version,
reserved: token_owner_record_data_v1.reserved,
governance_delegate: token_owner_record_data_v1.governance_delegate,
reserved_v2: [0; 128],
}
} else {
get_account_data::<TokenOwnerRecordV2>(program_id, token_owner_record_info)?
};
if token_owner_record_data.version < 1 {
token_owner_record_data.version = 1;
token_owner_record_data.unrelinquished_votes_count &= u32::MAX as u64;
}
Ok(token_owner_record_data)
}
pub fn get_token_owner_record_data_for_seeds(
program_id: &Pubkey,
token_owner_record_info: &AccountInfo,
token_owner_record_seeds: &[&[u8]],
) -> Result<TokenOwnerRecordV2, ProgramError> {
let (token_owner_record_address, _) =
Pubkey::find_program_address(token_owner_record_seeds, program_id);
if token_owner_record_address != *token_owner_record_info.key {
return Err(GovernanceError::InvalidTokenOwnerRecordAccountAddress.into());
}
get_token_owner_record_data(program_id, token_owner_record_info)
}
pub fn get_token_owner_record_data_for_realm(
program_id: &Pubkey,
token_owner_record_info: &AccountInfo,
realm: &Pubkey,
) -> Result<TokenOwnerRecordV2, ProgramError> {
let token_owner_record_data = get_token_owner_record_data(program_id, token_owner_record_info)?;
if token_owner_record_data.realm != *realm {
return Err(GovernanceError::InvalidRealmForTokenOwnerRecord.into());
}
Ok(token_owner_record_data)
}
pub fn get_token_owner_record_data_for_realm_and_governing_mint(
program_id: &Pubkey,
token_owner_record_info: &AccountInfo,
realm: &Pubkey,
governing_token_mint: &Pubkey,
) -> Result<TokenOwnerRecordV2, ProgramError> {
let token_owner_record_data =
get_token_owner_record_data_for_realm(program_id, token_owner_record_info, realm)?;
if token_owner_record_data.governing_token_mint != *governing_token_mint {
return Err(GovernanceError::InvalidGoverningMintForTokenOwnerRecord.into());
}
Ok(token_owner_record_data)
}
pub fn get_token_owner_record_data_for_proposal_owner(
program_id: &Pubkey,
token_owner_record_info: &AccountInfo,
proposal_owner: &Pubkey,
) -> Result<TokenOwnerRecordV2, ProgramError> {
if token_owner_record_info.key != proposal_owner {
return Err(GovernanceError::InvalidProposalOwnerAccount.into());
}
get_token_owner_record_data(program_id, token_owner_record_info)
}
#[cfg(test)]
mod test {
use {
super::*,
solana_program::{borsh0_10::get_packed_len, stake_history::Epoch},
};
fn create_test_token_owner_record() -> TokenOwnerRecordV2 {
TokenOwnerRecordV2 {
account_type: GovernanceAccountType::TokenOwnerRecordV2,
realm: Pubkey::new_unique(),
governing_token_mint: Pubkey::new_unique(),
governing_token_owner: Pubkey::new_unique(),
governing_token_deposit_amount: 10,
governance_delegate: Some(Pubkey::new_unique()),
unrelinquished_votes_count: 1,
outstanding_proposal_count: 1,
version: 1,
reserved: [0; 6],
reserved_v2: [0; 128],
}
}
fn create_test_program_v1_token_owner_record() -> TokenOwnerRecordV1 {
TokenOwnerRecordV1 {
account_type: GovernanceAccountType::TokenOwnerRecordV1,
realm: Pubkey::new_unique(),
governing_token_mint: Pubkey::new_unique(),
governing_token_owner: Pubkey::new_unique(),
governing_token_deposit_amount: 10,
governance_delegate: Some(Pubkey::new_unique()),
unrelinquished_votes_count: 1,
outstanding_proposal_count: 1,
version: 0,
reserved: [0; 6],
}
}
#[test]
fn test_max_size() {
let token_owner_record = create_test_token_owner_record();
let size = get_packed_len::<TokenOwnerRecordV2>();
assert_eq!(token_owner_record.get_max_size(), Some(size));
}
#[test]
fn test_program_v1_token_owner_record_size() {
let governance = create_test_program_v1_token_owner_record();
let size = governance.try_to_vec().unwrap().len();
assert_eq!(154, size);
}
#[derive(Clone, Debug, PartialEq, Eq, BorshDeserialize, BorshSerialize, BorshSchema)]
pub struct LegacyTokenOwnerRecord {
pub account_type: GovernanceAccountType,
pub realm: Pubkey,
pub governing_token_mint: Pubkey,
pub governing_token_owner: Pubkey,
pub governing_token_deposit_amount: u64,
pub unrelinquished_votes_count: u32,
pub total_votes_count: u32,
pub outstanding_proposal_count: u8,
pub reserved: [u8; 7],
pub governance_delegate: Option<Pubkey>,
pub reserved_v2: [u8; 128],
}
#[test]
fn test_migrate_token_owner_record_from_legacy_data_to_program_v3() {
let legacy_token_owner_record = LegacyTokenOwnerRecord {
account_type: GovernanceAccountType::TokenOwnerRecordV2,
realm: Pubkey::new_unique(),
governing_token_mint: Pubkey::new_unique(),
governing_token_owner: Pubkey::new_unique(),
governing_token_deposit_amount: 10,
unrelinquished_votes_count: 10,
total_votes_count: 100,
outstanding_proposal_count: 1,
reserved: [0; 7],
governance_delegate: Some(Pubkey::new_unique()),
reserved_v2: [0; 128],
};
let mut legacy_data = vec![];
borsh::to_writer(&mut legacy_data, &legacy_token_owner_record).unwrap();
let program_id = Pubkey::new_unique();
let info_key = Pubkey::new_unique();
let mut lamports = 10u64;
let legacy_account_info = AccountInfo::new(
&info_key,
false,
false,
&mut lamports,
&mut legacy_data[..],
&program_id,
false,
Epoch::default(),
);
let token_owner_record_program_v3 =
get_token_owner_record_data(&program_id, &legacy_account_info).unwrap();
assert_eq!(token_owner_record_program_v3.unrelinquished_votes_count, 10);
assert_eq!(
token_owner_record_program_v3.version,
TOKEN_OWNER_RECORD_LAYOUT_VERSION
);
}
}