diff --git a/Cargo.lock b/Cargo.lock index 95c7919..d777646 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1379,6 +1379,27 @@ dependencies = [ "walkdir", ] +[[package]] +name = "dirs-next" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" +dependencies = [ + "cfg-if", + "dirs-sys-next", +] + +[[package]] +name = "dirs-sys-next" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + [[package]] name = "displaydoc" version = "0.2.5" @@ -1899,6 +1920,33 @@ dependencies = [ "spl-token", ] +[[package]] +name = "gpl-token-voter" +version = "0.0.1" +dependencies = [ + "ahash 0.8.7", + "anchor-lang", + "anchor-spl", + "arrayref", + "borsh 0.10.3", + "env_logger", + "log", + "solana-program", + "solana-program-test", + "solana-sdk", + "spl-associated-token-account 3.0.4", + "spl-governance", + "spl-governance-addin-api", + "spl-governance-tools", + "spl-tlv-account-resolution 0.6.5", + "spl-token", + "spl-token-2022 3.0.4", + "spl-token-client", + "spl-transfer-hook-example", + "spl-transfer-hook-interface 0.6.5", + "static_assertions", +] + [[package]] name = "h2" version = "0.3.26" @@ -2310,6 +2358,16 @@ version = "0.2.155" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" +[[package]] +name = "libredox" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" +dependencies = [ + "bitflags 2.6.0", + "libc", +] + [[package]] name = "libsecp256k1" version = "0.6.0" @@ -3064,6 +3122,12 @@ dependencies = [ "termtree", ] +[[package]] +name = "pretty-hex" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6fa0831dd7cc608c38a5e323422a0077678fa5744aa2be4ad91c4ece8eec8d5" + [[package]] name = "proc-macro-crate" version = "0.1.5" @@ -3332,6 +3396,17 @@ dependencies = [ "bitflags 2.6.0", ] +[[package]] +name = "redox_users" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +dependencies = [ + "getrandom 0.2.15", + "libredox", + "thiserror", +] + [[package]] name = "regex" version = "1.10.5" @@ -3728,6 +3803,19 @@ dependencies = [ "syn 2.0.71", ] +[[package]] +name = "serde_yaml" +version = "0.9.34+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" +dependencies = [ + "indexmap 2.2.6", + "itoa", + "ryu", + "serde", + "unsafe-libyaml", +] + [[package]] name = "sha1" version = "0.10.6" @@ -4065,6 +4153,49 @@ dependencies = [ "url", ] +[[package]] +name = "solana-cli-config" +version = "1.18.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cb4459594cbdbc6f3cd199bbca486f0ef7f1ecf107dce9cffa3f9d69df31dc7" +dependencies = [ + "dirs-next", + "lazy_static", + "serde", + "serde_derive", + "serde_yaml", + "solana-clap-utils", + "solana-sdk", + "url", +] + +[[package]] +name = "solana-cli-output" +version = "1.18.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb454e8df10d664ef0e1137c69af0f2d9fa0de713c1b608656318a76a2f5d3cc" +dependencies = [ + "Inflector", + "base64 0.21.7", + "chrono", + "clap 2.34.0", + "console", + "humantime", + "indicatif", + "pretty-hex", + "semver", + "serde", + "serde_json", + "solana-account-decoder", + "solana-clap-utils", + "solana-cli-config", + "solana-rpc-client-api", + "solana-sdk", + "solana-transaction-status", + "solana-vote-program", + "spl-memo", +] + [[package]] name = "solana-client" version = "1.18.18" @@ -5375,6 +5506,32 @@ dependencies = [ "thiserror", ] +[[package]] +name = "spl-token-client" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb6cb970d91422060de53f5ffc106003a56ca8b2e37ae814b7d174187a4d09" +dependencies = [ + "async-trait", + "curve25519-dalek", + "futures", + "futures-util", + "solana-banks-interface", + "solana-cli-output", + "solana-program-test", + "solana-rpc-client", + "solana-rpc-client-api", + "solana-sdk", + "spl-associated-token-account 3.0.4", + "spl-memo", + "spl-token", + "spl-token-2022 3.0.4", + "spl-token-group-interface 0.2.5", + "spl-token-metadata-interface 0.3.5", + "spl-transfer-hook-interface 0.6.5", + "thiserror", +] + [[package]] name = "spl-token-group-interface" version = "0.1.0" @@ -5429,6 +5586,20 @@ dependencies = [ "spl-type-length-value 0.4.6", ] +[[package]] +name = "spl-transfer-hook-example" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dea8c4ea17c18e6126c7f332b2e55cda99660cb67ad19e494c4c2fde49287e23" +dependencies = [ + "arrayref", + "solana-program", + "spl-tlv-account-resolution 0.6.5", + "spl-token-2022 3.0.4", + "spl-transfer-hook-interface 0.6.5", + "spl-type-length-value 0.4.6", +] + [[package]] name = "spl-transfer-hook-interface" version = "0.4.1" @@ -6163,6 +6334,12 @@ dependencies = [ "void", ] +[[package]] +name = "unsafe-libyaml" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" + [[package]] name = "untrusted" version = "0.7.1" diff --git a/Cargo.toml b/Cargo.toml index 9776f9e..49d4a3c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,6 +5,7 @@ members = [ "programs/realm-voter", "programs/nft-voter", "programs/token-haver", + "programs/token-voter", ] [profile.release] diff --git a/programs/token-voter/src/error.rs b/programs/token-voter/src/error.rs index a32c4e3..4876475 100644 --- a/programs/token-voter/src/error.rs +++ b/programs/token-voter/src/error.rs @@ -99,4 +99,7 @@ pub enum TokenVoterError { #[msg("Mint Index mismatch!")] MintIndexMismatch, + + #[msg("Inactive Deposit Index!")] + DepositIndexInactive, } diff --git a/programs/token-voter/src/instructions/close_voter.rs b/programs/token-voter/src/instructions/close_voter.rs index d3471dd..66c3d5f 100644 --- a/programs/token-voter/src/instructions/close_voter.rs +++ b/programs/token-voter/src/instructions/close_voter.rs @@ -26,6 +26,14 @@ pub struct CloseVoter<'info> { )] pub voter: Box>, + #[account( + mut, + seeds = [registrar.key().as_ref(), b"voter-weight-record".as_ref(), voter_authority.key().as_ref()], + bump, + close = sol_destination + )] + pub voter_weight_record: Box>, + pub voter_authority: Signer<'info>, /// CHECK: Destination may be any address. diff --git a/programs/token-voter/src/instructions/configure_mint_config.rs b/programs/token-voter/src/instructions/configure_mint_config.rs index 44e0b28..c67fdff 100644 --- a/programs/token-voter/src/instructions/configure_mint_config.rs +++ b/programs/token-voter/src/instructions/configure_mint_config.rs @@ -63,14 +63,12 @@ pub fn configure_mint_config( ctx.accounts.realm_authority.key(), TokenVoterError::InvalidRealmAuthority ); - - let token_supply = mint.supply; - let supply_with_digit_shift = - VotingMintConfig::compute_digit_shift_native(digit_shift, token_supply)?; + let voting_mint_config = VotingMintConfig { mint: mint.key(), digit_shift, - reserved1: [0; 63], + mint_supply: mint.supply, + reserved1: [0; 55], }; let mint_config_idx = registrar @@ -87,10 +85,9 @@ pub fn configure_mint_config( } // Update MaxVoterWeightRecord.max_voter_weight - max_voter_weight_record.max_voter_weight = max_voter_weight_record - .max_voter_weight - .checked_add(supply_with_digit_shift) - .ok_or_else(|| error!(TokenVoterError::VoterWeightOverflow))?; + // recalculate the max voter weight as mint supply has possibly changed + max_voter_weight_record.max_voter_weight = registrar.max_vote_weight()?; + max_voter_weight_record.max_voter_weight_expiry = None; diff --git a/programs/token-voter/src/instructions/create_voter_weight_record.rs b/programs/token-voter/src/instructions/create_voter_weight_record.rs index 6cda295..6263dd4 100644 --- a/programs/token-voter/src/instructions/create_voter_weight_record.rs +++ b/programs/token-voter/src/instructions/create_voter_weight_record.rs @@ -65,7 +65,8 @@ pub fn create_voter_weight_record( voter.voter_weight_record_bump = ctx.bumps.voter_weight_record; voter.voter_authority = voter_authority.key(); voter.registrar = registrar.key(); - voter.deposits = vec![]; + voter.deposits = DepositEntry::init_deposits(registrar.max_mints as usize); + let voter_weight_record = &mut ctx.accounts.voter_weight_record; diff --git a/programs/token-voter/src/instructions/deposit.rs b/programs/token-voter/src/instructions/deposit.rs index a200b1b..3c6b268 100644 --- a/programs/token-voter/src/instructions/deposit.rs +++ b/programs/token-voter/src/instructions/deposit.rs @@ -155,13 +155,13 @@ pub fn deposit<'key, 'accounts, 'remaining, 'info>( is_used: true, reserved: [0; 38], }; - voter.deposits.push(deposit_entry); + voter.deposits[mint_idx] = deposit_entry; } } let voter_weight_record = &mut ctx.accounts.voter_weight_record; - let governance_program_id = ctx.accounts.token_owner_record.owner; + let governance_program_id = &ctx.accounts.registrar.governance_program_id; let token_owner_record = token_owner_record::get_token_owner_record_data( governance_program_id, @@ -178,8 +178,9 @@ pub fn deposit<'key, 'accounts, 'remaining, 'info>( // Setup voter_weight voter_weight_record.voter_weight = voter.weight(registrar)?; - // Record is only valid as of the current slot - voter_weight_record.voter_weight_expiry = Some(Clock::get()?.slot); + // Voter Weight Expiry is always set to None after a deposit + // since no other action other than deposit and withdraw could invalidate it + voter_weight_record.voter_weight_expiry = None; // Set action and target to None to indicate the weight is valid for any action and target voter_weight_record.weight_action = None; diff --git a/programs/token-voter/src/instructions/withdraw.rs b/programs/token-voter/src/instructions/withdraw.rs index fd53a81..2731c64 100644 --- a/programs/token-voter/src/instructions/withdraw.rs +++ b/programs/token-voter/src/instructions/withdraw.rs @@ -149,7 +149,10 @@ pub fn withdraw<'key, 'accounts, 'remaining, 'info>( // Update the voter weight record let voter_weight_record = &mut ctx.accounts.voter_weight_record; voter_weight_record.voter_weight = voter.weight(registrar)?; - voter_weight_record.voter_weight_expiry = Some(Clock::get()?.slot); + // Voter Weight Expiry is always set to None after a deposit + // since no other action other than deposit and withdraw could invalidate it + voter_weight_record.voter_weight_expiry = None; + Ok(()) } diff --git a/programs/token-voter/src/state/deposit_entry.rs b/programs/token-voter/src/state/deposit_entry.rs index 9d5359f..595764e 100644 --- a/programs/token-voter/src/state/deposit_entry.rs +++ b/programs/token-voter/src/state/deposit_entry.rs @@ -31,6 +31,22 @@ const_assert!(std::mem::size_of::() == 8 + 1 + 8 + 1 + 38); const_assert!(std::mem::size_of::() % 8 == 0); impl DepositEntry { + /// Creates a new DepositEntry with default values + pub fn new() -> Self { + Self { + amount_deposited_native: 0, + voting_mint_config_idx: 0, + deposit_slot_hash: 0, + is_used: false, + reserved: [0; 38], + } + } + + /// Initializes a vector of DepositEntry with a given length + pub fn init_deposits(length: usize) -> Vec { + vec![Self::new(); length] + } + /// Voting Power Caclulation /// Returns the voting power for the deposit. pub fn voting_power(&self, mint_config: &VotingMintConfig) -> Result { diff --git a/programs/token-voter/src/state/registrar.rs b/programs/token-voter/src/state/registrar.rs index b10a2d2..7ff1836 100644 --- a/programs/token-voter/src/state/registrar.rs +++ b/programs/token-voter/src/state/registrar.rs @@ -1,7 +1,7 @@ use { crate::{ error::TokenVoterError, id, state::VotingMintConfig, - tools::spl_token::get_spl_token_mint_supply, vote_weight_record, max_voter_weight_record + vote_weight_record, max_voter_weight_record }, anchor_lang::{prelude::*, Discriminator}, solana_program::pubkey::PUBKEY_BYTES, @@ -60,24 +60,18 @@ impl Registrar { .ok_or_else(|| error!(TokenVoterError::MintNotFound)) } - /// Returns the max vote weight based on the mint_accounts - /// throws an error if the mint address does not exist - pub fn max_vote_weight(&self, mint_accounts: &[AccountInfo]) -> Result { + /// Returns the max vote weight based on the supply initially set for each mint + /// throws an error if the sum of the vote weights overflows + pub fn max_vote_weight(&self) -> Result { self.voting_mint_configs .iter() - .try_fold(0u64, |mut sum, mint_config| -> Result { + .try_fold(0u64, |sum, mint_config| -> Result { if !mint_config.in_use() { return Ok(sum); } - let mint_account = mint_accounts - .iter() - .find(|a| a.key() == mint_config.mint) - .ok_or_else(|| error!(TokenVoterError::MintNotFound))?; - let mint_supply = get_spl_token_mint_supply(mint_account)?; - sum = sum - .checked_add(mint_config.digit_shift_native(mint_supply)?) - .ok_or_else(|| error!(TokenVoterError::VoterWeightOverflow))?; - Ok(sum) + let mint_supply = mint_config.mint_supply; + sum.checked_add(mint_config.digit_shift_native(mint_supply)?) + .ok_or_else(|| error!(TokenVoterError::VoterWeightOverflow)) }) } } @@ -136,7 +130,8 @@ mod test { let mint_config = VotingMintConfig { mint: Pubkey::default(), digit_shift: 0, - reserved1: [0; 63], + mint_supply: 0, + reserved1: [0; 55], }; let registrar = Registrar { @@ -154,4 +149,61 @@ mod test { // Assert assert_eq!(expected_space, actual_space); } + + + #[test] + fn test_max_vote_weight() { + // Arrange + let mint_config1 = VotingMintConfig { + mint: Pubkey::new_unique(), + digit_shift: 2, + mint_supply: 1000, + reserved1: [0; 55], + }; + + let mint_config2 = VotingMintConfig { + mint: Pubkey::new_unique(), + digit_shift: 1, + mint_supply: 500, + reserved1: [0; 55], + }; + + let mut mint_config3 = VotingMintConfig { + mint: Pubkey::new_unique(), + digit_shift: 0, + mint_supply: 200, + reserved1: [0; 55], + }; + + let mut registrar = Registrar { + governance_program_id: Pubkey::default(), + voting_mint_configs: vec![mint_config1, mint_config2, mint_config3.clone()], + realm: Pubkey::default(), + governing_token_mint: Pubkey::default(), + max_mints: 3, + reserved: [0; 127], + }; + + // Act & Assert - Initial state + let result = registrar.max_vote_weight(); + assert!(result.is_ok()); + let max_weight = result.unwrap(); + assert_eq!(max_weight, 105200); + + // Modify mint_config3 and update registrar + mint_config3.digit_shift = 3; + registrar.voting_mint_configs[2] = mint_config3; + + // Act & Assert - After modification + let result_after_mod = registrar.max_vote_weight(); + assert!(result_after_mod.is_ok()); + let max_weight_after_mod = result_after_mod.unwrap(); + + // Expected calculation after modification: + // mint_config1: 1000 * 10^2 = 100,000 + // mint_config2: 500 * 10^1 = 5,000 + // mint_config3: 200 * 10^3 = 200,000 (now in use and with digit_shift 3) + // Total: 100,000 + 5,000 + 200,000 = 305,000 + assert_eq!(max_weight_after_mod, 305000); + } } diff --git a/programs/token-voter/src/state/voter.rs b/programs/token-voter/src/state/voter.rs index 5790965..700a55f 100644 --- a/programs/token-voter/src/state/voter.rs +++ b/programs/token-voter/src/state/voter.rs @@ -55,7 +55,13 @@ impl Voter { index, TokenVoterError::OutOfBoundsDepositEntryIndex ); + let d = &mut self.deposits[index]; + // if deposit_slot_hash is 0 then deposit is inactive + if d.deposit_slot_hash == 0 { + return Err(TokenVoterError::DepositIndexInactive.into()); + } + Ok(d) } diff --git a/programs/token-voter/src/state/voting_mint_config.rs b/programs/token-voter/src/state/voting_mint_config.rs index 7e077a6..6027280 100644 --- a/programs/token-voter/src/state/voting_mint_config.rs +++ b/programs/token-voter/src/state/voting_mint_config.rs @@ -14,11 +14,14 @@ pub struct VotingMintConfig { /// Number of digits to shift native amounts, applying a 10^digit_shift factor. pub digit_shift: i8, + // The mint_supply is used to calculate the vote weight + pub mint_supply: u64, + // Empty bytes for future upgrades. - pub reserved1: [u8; 63], + pub reserved1: [u8; 55], } -const_assert!(std::mem::size_of::() == 32 + 1 + 63); +const_assert!(std::mem::size_of::() == 32 + 1 + 8 + 55); const_assert!(std::mem::size_of::() % 8 == 0); impl VotingMintConfig { diff --git a/programs/token-voter/tests/program_test/token_voter_test.rs b/programs/token-voter/tests/program_test/token_voter_test.rs index ec38de7..dd53204 100644 --- a/programs/token-voter/tests/program_test/token_voter_test.rs +++ b/programs/token-voter/tests/program_test/token_voter_test.rs @@ -527,7 +527,9 @@ impl TokenVoterTest { Ok(VotingMintConfig { mint: mint_cookie.address, digit_shift, - reserved1: [0; 63], + // hard coded + mint_supply: 100 * 10u64.pow(6), + reserved1: [0; 55], }) } @@ -768,6 +770,7 @@ impl TokenVoterTest { let accounts = gpl_token_voter::accounts::CloseVoter { registrar: registrar_cookie.address, voter: voter_cookie.address, + voter_weight_record: voter_cookie.voter_weight_record, sol_destination: user_cookie.key.pubkey(), voter_authority: user_cookie.key.pubkey(), token_program: *token_program,