From 0e35474c178c6e1d83f1cbb992cb531ff2f88df6 Mon Sep 17 00:00:00 2001 From: Guillermo Bescos Date: Mon, 19 May 2025 14:15:59 +0100 Subject: [PATCH 01/16] add feature --- staking/integration-tests/src/solana/utils.rs | 4 +- .../src/staking/instructions.rs | 33 +- staking/integration-tests/tests/recover2.rs | 296 ++++++++++++++++++ staking/programs/staking/src/context.rs | 37 ++- staking/programs/staking/src/lib.rs | 24 ++ 5 files changed, 389 insertions(+), 5 deletions(-) create mode 100644 staking/integration-tests/tests/recover2.rs diff --git a/staking/integration-tests/src/solana/utils.rs b/staking/integration-tests/src/solana/utils.rs index 7d7a706b..6e252f9b 100644 --- a/staking/integration-tests/src/solana/utils.rs +++ b/staking/integration-tests/src/solana/utils.rs @@ -20,7 +20,9 @@ pub fn fetch_account_data( svm: &mut litesvm::LiteSVM, account: &Pubkey, ) -> T { - T::try_deserialize(&mut svm.get_account(account).unwrap().data.as_ref()).unwrap() + let account_data = svm.get_account(account).unwrap().data; + println!("account_data: {:?}", account_data); + T::try_deserialize(&mut account_data.as_ref()).unwrap() } pub fn fetch_governance_account_data( diff --git a/staking/integration-tests/src/staking/instructions.rs b/staking/integration-tests/src/staking/instructions.rs index 7c48d787..5cec9533 100644 --- a/staking/integration-tests/src/staking/instructions.rs +++ b/staking/integration-tests/src/staking/instructions.rs @@ -15,7 +15,7 @@ use { MAINNET_REALM_ID, }, integrity_pool::pda::get_pool_config_address, - solana::utils::fetch_account_data, + solana::utils::{fetch_account_data, fetch_positions_account}, }, anchor_lang::{ prelude::AccountMeta, @@ -38,11 +38,11 @@ use { signer::Signer, transaction::Transaction, }, - staking::state::{ + staking::{instruction, state::{ global_config::GlobalConfig, positions::TargetWithParameters, voter_weight_record::VoterWeightAction, - }, + }}, }; pub fn init_config_account(svm: &mut litesvm::LiteSVM, payer: &Keypair, pyth_token_mint: Pubkey) { @@ -510,3 +510,30 @@ pub fn merge_target_positions( svm.send_transaction(tx) } + +pub fn recover_account_2(svm: &mut litesvm::LiteSVM, governance_authority: &Keypair, stake_account_positions: Pubkey, new_owner: Pubkey) -> TransactionResult { + let config = get_config_address(); + let owner = fetch_positions_account(svm, &stake_account_positions).to_dynamic_position_array().owner().unwrap(); + let stake_account_metadata = get_stake_account_metadata_address(stake_account_positions); + let voter_record = get_voter_record_address(stake_account_positions); + + let accs = staking::accounts::RecoverAccount2 { + governance_authority: governance_authority.pubkey(), + owner, + config, + stake_account_metadata, + stake_account_positions, + voter_record, + new_owner, + }; + + let ix = Instruction::new_with_bytes(staking::ID, &staking::instruction::RecoverAccount2{}.data(), accs.to_account_metas(None)); + let tx = Transaction::new_signed_with_payer( + &[ix], + Some(&governance_authority.pubkey()), + &[&governance_authority], + svm.latest_blockhash(), + ); + + svm.send_transaction(tx) +} diff --git a/staking/integration-tests/tests/recover2.rs b/staking/integration-tests/tests/recover2.rs new file mode 100644 index 00000000..285cee93 --- /dev/null +++ b/staking/integration-tests/tests/recover2.rs @@ -0,0 +1,296 @@ +use { + anchor_lang::error::{AnchorError, ErrorCode}, integration_tests::{ + assert_anchor_program_error, governance::{ + addresses::MAINNET_GOVERNANCE_PROGRAM_ID, + helper_functions::create_proposal_and_vote, + instructions::create_token_owner_record, + }, setup::{ + setup, + SetupProps, + SetupResult, + STARTING_EPOCH, + }, solana::utils::{ + fetch_account_data, + fetch_positions_account, + }, staking::{ + helper_functions::initialize_new_stake_account, instructions::{ + create_position, create_stake_account, join_dao_llc, merge_target_positions, recover_account_2, update_token_list_time, update_voter_weight + }, pda::{ + get_stake_account_metadata_address, get_target_address, get_voter_record_address + } + }, utils::clock::advance_n_epochs + }, litesvm::LiteSVM, solana_cli_output::CliAccount, solana_sdk::{ + account::{ + AccountSharedData, + WritableAccount, + }, native_token::LAMPORTS_PER_SOL, pubkey::Pubkey, signature::Keypair, signer::Signer + }, staking::state::{ + max_voter_weight_record::MAX_VOTER_WEIGHT, positions::{ + TargetWithParameters, + POSITION_BUFFER_SIZE, + }, stake_account::StakeAccountMetadataV2, target::TargetMetadata, voter_weight_record::VoterWeightRecord + }, std::{ + fs::File, + io::Read, + str::FromStr, + } +}; + +const MAINNET_TOKENS_LIST_TIME: i64 = 1684591200; +const MAINNET_ELAPSED_EPOCHS: u64 = 2850; + +#[test] +/// This test has two purposes: +/// 1) to test the voting functionality against the deployed governance program and configuration +/// 2) to test that the new staking account is compatible with stake account positions with the old +/// fixed sized position array and such accounts can be turned into the new version by calling +/// merge_target_positions and nothing breaks +fn test_recover2() { + let SetupResult { + mut svm, + payer : governance_authority, + pyth_token_mint, + publisher_keypair: _, + pool_data_pubkey: _, + reward_program_authority: _, + maybe_publisher_index: _, + } = setup(SetupProps { + init_config: true, + init_target: true, + init_mint: true, + init_pool_data: true, + init_publishers: true, + reward_amount_override: None, + }); + + let owner = Keypair::new(); + let new_owner = Keypair::new(); + + svm.airdrop(&owner.pubkey(), LAMPORTS_PER_SOL).unwrap(); + let stake_account_positions = initialize_new_stake_account(&mut svm, &owner, &pyth_token_mint, true, true); + + assert_anchor_program_error!(recover_account_2(&mut svm, &owner, stake_account_positions, new_owner.pubkey()), + ErrorCode::ConstraintHasOne, + 0 + ); + + recover_account_2(&mut svm, &governance_authority, stake_account_positions, new_owner.pubkey()).unwrap(); + + let mut positions_account = fetch_positions_account(&mut svm, &stake_account_positions); + let positions = positions_account.to_dynamic_position_array(); + assert_eq!(positions.owner().unwrap(), new_owner.pubkey()); + + let stake_account_metadata: StakeAccountMetadataV2 = fetch_account_data(&mut svm, &get_stake_account_metadata_address(stake_account_positions)); + assert_eq!(stake_account_metadata.owner, new_owner.pubkey()); + + let voter_record: VoterWeightRecord = fetch_account_data(&mut svm, &get_voter_record_address(stake_account_positions)); + assert_eq!(voter_record.voter_weight, 0); + + + + + // let stake_account_positions = + // load_stake_accounts(&mut svm, &payer.pubkey(), &pyth_token_mint.pubkey()); +// let governance_address = load_governance_accounts(&mut svm, &pyth_token_mint.pubkey()); + +// update_token_list_time(&mut svm, &payer, MAINNET_TOKENS_LIST_TIME); +// advance_n_epochs(&mut svm, &payer, MAINNET_ELAPSED_EPOCHS); +// join_dao_llc(&mut svm, &payer, stake_account_positions).unwrap(); +// update_voter_weight(&mut svm, &payer, stake_account_positions).unwrap(); + +// let target_account: TargetMetadata = fetch_account_data(&mut svm, &get_target_address()); +// let voter_record: VoterWeightRecord = +// fetch_account_data(&mut svm, &get_voter_record_address(stake_account_positions)); + +// // Check that the voter weight is calculated correctly +// let mut positions_account = fetch_positions_account(&mut svm, &stake_account_positions); +// let positions = positions_account.to_dynamic_position_array(); + +// let pos1 = positions.read_position(0).unwrap().unwrap(); +// let pos2 = positions.read_position(1).unwrap().unwrap(); +// assert!(positions.get_position_capacity() == 20); + +// for i in 2..positions.get_position_capacity() { +// assert!(positions.read_position(i).unwrap().is_none()); +// } + +// let expected_voter_weight = (((pos1.amount + pos2.amount) as u128) * MAX_VOTER_WEIGHT as u128 +// / target_account.locked as u128) as u64; + +// assert_eq!(voter_record.voter_weight, expected_voter_weight); + +// // Try voting against the actual governance program +// create_token_owner_record(&mut svm, &payer).unwrap(); +// let actual_proposal_data = create_proposal_and_vote( +// &mut svm, +// &payer, +// &stake_account_positions, +// &governance_address, +// ); +// assert_eq!(actual_proposal_data.options.len(), 1); +// assert_eq!( +// actual_proposal_data.options[0].vote_weight, +// expected_voter_weight +// ); + +// // Test some other actions +// // create_positions, merge_positions +// create_position( +// &mut svm, +// &payer, +// stake_account_positions, +// TargetWithParameters::Voting, +// None, +// 100, +// ) +// .unwrap(); + +// let mut positions_account = fetch_positions_account(&mut svm, &stake_account_positions); +// let positions = positions_account.to_dynamic_position_array(); + +// let post_pos1 = positions.read_position(0).unwrap().unwrap(); +// let post_pos2 = positions.read_position(1).unwrap().unwrap(); +// let post_pos3 = positions.read_position(2).unwrap().unwrap(); + +// assert_eq!(post_pos1, pos1); +// assert_eq!(post_pos2, pos2); + +// assert_eq!(post_pos3.activation_epoch, STARTING_EPOCH + 2851); + +// assert!(positions.get_position_capacity() == 20); + +// for i in 3..positions.get_position_capacity() { +// assert!(positions.read_position(i).unwrap().is_none()); +// } + +// advance_n_epochs(&mut svm, &payer, 1); +// update_voter_weight(&mut svm, &payer, stake_account_positions).unwrap(); + +// let voter_record: VoterWeightRecord = +// fetch_account_data(&mut svm, &get_voter_record_address(stake_account_positions)); + +// let expected_voter_weight = (((post_pos1.amount + post_pos2.amount + post_pos3.amount) as u128) +// * MAX_VOTER_WEIGHT as u128 +// / (target_account.locked + post_pos3.amount) as u128) +// as u64; + +// assert_eq!(voter_record.voter_weight, expected_voter_weight); + +// merge_target_positions(&mut svm, &payer, stake_account_positions).unwrap(); + +// let mut positions_account = fetch_positions_account(&mut svm, &stake_account_positions); +// let positions = positions_account.to_dynamic_position_array(); + +// assert!(positions.get_position_capacity() == 2); +// assert_eq!(positions.acc_info.data_len(), 40 + 2 * POSITION_BUFFER_SIZE); + + +// let post_merge_pos1 = positions.read_position(0).unwrap().unwrap(); +// let post_merge_pos2 = positions.read_position(1).unwrap().unwrap(); + +// assert_eq!( +// post_merge_pos1.activation_epoch, +// std::cmp::min(post_pos1.activation_epoch, post_pos2.activation_epoch) +// ); +// assert_eq!(post_merge_pos1.amount, post_pos1.amount + post_pos2.amount); +// assert_eq!(post_merge_pos1.unlocking_start, None); + +// assert_eq!(post_merge_pos2.activation_epoch, post_pos3.activation_epoch); +// assert_eq!(post_merge_pos2.amount, post_pos3.amount); +// assert_eq!(post_merge_pos2.unlocking_start, None); + + +// assert_eq!( +// positions.read_position(2).unwrap_err(), +// staking::error::ErrorCode::PositionOutOfBounds.into() +// ); + +// // Voter weight should be the same after merging +// svm.expire_blockhash(); +// update_voter_weight(&mut svm, &payer, stake_account_positions).unwrap(); +// let voter_record: VoterWeightRecord = +// fetch_account_data(&mut svm, &get_voter_record_address(stake_account_positions)); +// assert_eq!(voter_record.voter_weight, expected_voter_weight); + +// // Vote again, after merging +// let proposal_account = create_proposal_and_vote( +// &mut svm, +// &payer, +// &stake_account_positions, +// &governance_address, +// ); +// assert_eq!(proposal_account.options.len(), 1); +// assert_eq!( +// proposal_account.options[0].vote_weight, +// expected_voter_weight +// ); +// } + +// // These accounts were snapshotted on 16th August 2024 +// /// When loading these stake accounts, we need to replace the mainnet owner of the account by a key +// /// we have access to in the tests. We also need to replace the mainnet pyth mint address by the one +// /// in the tests. +// fn load_stake_accounts(svm: &mut LiteSVM, payer: &Pubkey, pyth_token_mint: &Pubkey) -> Pubkey { +// let mut stake_account_positions = load_account_file("staking/stake_account_positions.json"); +// stake_account_positions.account.data_as_mut_slice()[8..40].copy_from_slice(&payer.to_bytes()); +// svm.set_account( +// stake_account_positions.address, +// stake_account_positions.account.into(), +// ) +// .unwrap(); + +// let mut stake_account_metadata = load_account_file("staking/stake_account_metadata.json"); +// stake_account_metadata.account.data_as_mut_slice()[12..44].copy_from_slice(&payer.to_bytes()); +// svm.set_account( +// stake_account_metadata.address, +// stake_account_metadata.account.into(), +// ) +// .unwrap(); + + +// let mut stake_account_custody = load_account_file("staking/stake_account_custody.json"); +// stake_account_custody.account.data_as_mut_slice()[..32] +// .copy_from_slice(&pyth_token_mint.to_bytes()); +// svm.set_account( +// stake_account_custody.address, +// stake_account_custody.account.into(), +// ) +// .unwrap(); + +// let mut voter_record = load_account_file("staking/voter_record.json"); +// voter_record.account.data_as_mut_slice()[40..72].copy_from_slice(&pyth_token_mint.to_bytes()); +// voter_record.account.data_as_mut_slice()[72..104].copy_from_slice(&payer.to_bytes()); +// svm.set_account(voter_record.address, voter_record.account.into()) +// .unwrap(); + + +// let target_account = load_account_file("staking/target_account.json"); +// svm.set_account(target_account.address, target_account.account.into()) +// .unwrap(); + +// stake_account_positions.address +// } + +// // These accounts were snapshotted on 16th August 2024 +// fn load_governance_accounts(svm: &mut LiteSVM, pyth_token_mint: &Pubkey) -> Pubkey { +// svm.add_program_from_file( +// MAINNET_GOVERNANCE_PROGRAM_ID, +// "fixtures/governance/governance.so", +// ) +// .unwrap(); + +// let mut realm = load_account_file("governance/realm.json"); +// realm.account.data_as_mut_slice()[1..33].copy_from_slice(&pyth_token_mint.to_bytes()); +// svm.set_account(realm.address, realm.account.into()) +// .unwrap(); + +// let governance = load_account_file("governance/governance.json"); +// svm.set_account(governance.address, governance.account.into()) +// .unwrap(); + +// let realm_config = load_account_file("governance/realm_config.json"); +// svm.set_account(realm_config.address, realm_config.account.into()) +// .unwrap(); + +// governance.address +} diff --git a/staking/programs/staking/src/context.rs b/staking/programs/staking/src/context.rs index 0e69ec80..1c84dc7d 100644 --- a/staking/programs/staking/src/context.rs +++ b/staking/programs/staking/src/context.rs @@ -413,7 +413,6 @@ pub struct AdvanceClock<'info> { #[derive(Accounts)] pub struct RecoverAccount<'info> { - // Native payer: pub governance_authority: Signer<'info>, // Token account: @@ -445,6 +444,42 @@ pub struct RecoverAccount<'info> { pub config: Account<'info, global_config::GlobalConfig>, } +#[derive(Accounts)] +pub struct RecoverAccount2<'info> { + pub governance_authority: Signer<'info>, + + /// CHECK : This AccountInfo is safe because it's checked against stake_account_metadata + pub owner: AccountInfo<'info>, + + /// CHECK : A new owner is provided by the governance_authority + pub new_owner: AccountInfo<'info>, + + // Stake program accounts: + #[account(mut)] + pub stake_account_positions: AccountLoader<'info, positions::PositionData>, + + #[account( + mut, + seeds = [ + STAKE_ACCOUNT_METADATA_SEED.as_bytes(), + stake_account_positions.key().as_ref() + ], + bump = stake_account_metadata.metadata_bump, + has_one = owner + )] + pub stake_account_metadata: Account<'info, stake_account::StakeAccountMetadataV2>, + + #[account( + mut, + seeds = [VOTER_RECORD_SEED.as_bytes(), stake_account_positions.key().as_ref()], + bump = stake_account_metadata.voter_bump + )] + pub voter_record: Account<'info, voter_weight_record::VoterWeightRecord>, + + #[account(seeds = [CONFIG_SEED.as_bytes()], bump = config.bump, has_one = governance_authority)] + pub config: Account<'info, global_config::GlobalConfig>, +} + #[derive(Accounts)] #[instruction(slash_ratio: u64)] pub struct SlashAccount<'info> { diff --git a/staking/programs/staking/src/lib.rs b/staking/programs/staking/src/lib.rs index c2412cea..85d73b0e 100644 --- a/staking/programs/staking/src/lib.rs +++ b/staking/programs/staking/src/lib.rs @@ -794,6 +794,30 @@ pub mod staking { Ok(()) } + /** Recovers a user's `stake account` ownership by transferring ownership + * from a token account to the `owner` of that token account. + * + * This functionality addresses the scenario where a user mistakenly + * created a stake account using their token account address as the owner. + */ + pub fn recover_account_2(ctx: Context) -> Result<()> { + // Check that there aren't any positions (i.e., staked tokens) in the account. + // Transferring accounts with staked tokens might lead to double voting + require!( + ctx.accounts.stake_account_metadata.next_index == 0, + ErrorCode::RecoverWithStake + ); + + let new_owner = ctx.accounts.new_owner.key(); + ctx.accounts.stake_account_metadata.owner = new_owner; + let stake_account_positions = + &mut DynamicPositionArray::load_mut(&ctx.accounts.stake_account_positions)?; + stake_account_positions.set_owner(&new_owner)?; + ctx.accounts.voter_record.governing_token_owner = new_owner; + + Ok(()) + } + pub fn slash_account( ctx: Context, // a number between 0 and 1 with 6 decimals of precision From a86205f487e83185e5c011a999be1ef8d21a5d59 Mon Sep 17 00:00:00 2001 From: Guillermo Bescos Date: Mon, 19 May 2025 14:54:18 +0100 Subject: [PATCH 02/16] test: add tests --- staking/integration-tests/src/solana/utils.rs | 1 - .../src/staking/instructions.rs | 69 +++- staking/integration-tests/tests/recover2.rs | 344 ++++++------------ staking/programs/staking/src/context.rs | 4 +- 4 files changed, 170 insertions(+), 248 deletions(-) diff --git a/staking/integration-tests/src/solana/utils.rs b/staking/integration-tests/src/solana/utils.rs index 6e252f9b..ec832e83 100644 --- a/staking/integration-tests/src/solana/utils.rs +++ b/staking/integration-tests/src/solana/utils.rs @@ -21,7 +21,6 @@ pub fn fetch_account_data( account: &Pubkey, ) -> T { let account_data = svm.get_account(account).unwrap().data; - println!("account_data: {:?}", account_data); T::try_deserialize(&mut account_data.as_ref()).unwrap() } diff --git a/staking/integration-tests/src/staking/instructions.rs b/staking/integration-tests/src/staking/instructions.rs index 5cec9533..e0cfcee6 100644 --- a/staking/integration-tests/src/staking/instructions.rs +++ b/staking/integration-tests/src/staking/instructions.rs @@ -15,7 +15,10 @@ use { MAINNET_REALM_ID, }, integrity_pool::pda::get_pool_config_address, - solana::utils::{fetch_account_data, fetch_positions_account}, + solana::utils::{ + fetch_account_data, + fetch_positions_account, + }, }, anchor_lang::{ prelude::AccountMeta, @@ -38,11 +41,14 @@ use { signer::Signer, transaction::Transaction, }, - staking::{instruction, state::{ - global_config::GlobalConfig, - positions::TargetWithParameters, - voter_weight_record::VoterWeightAction, - }}, + staking::{ + instruction, + state::{ + global_config::GlobalConfig, + positions::TargetWithParameters, + voter_weight_record::VoterWeightAction, + }, + }, }; pub fn init_config_account(svm: &mut litesvm::LiteSVM, payer: &Keypair, pyth_token_mint: Pubkey) { @@ -511,9 +517,17 @@ pub fn merge_target_positions( svm.send_transaction(tx) } -pub fn recover_account_2(svm: &mut litesvm::LiteSVM, governance_authority: &Keypair, stake_account_positions: Pubkey, new_owner: Pubkey) -> TransactionResult { +pub fn recover_account_2( + svm: &mut litesvm::LiteSVM, + governance_authority: &Keypair, + stake_account_positions: Pubkey, + new_owner: Pubkey, +) -> TransactionResult { let config = get_config_address(); - let owner = fetch_positions_account(svm, &stake_account_positions).to_dynamic_position_array().owner().unwrap(); + let owner = fetch_positions_account(svm, &stake_account_positions) + .to_dynamic_position_array() + .owner() + .unwrap(); let stake_account_metadata = get_stake_account_metadata_address(stake_account_positions); let voter_record = get_voter_record_address(stake_account_positions); @@ -527,7 +541,11 @@ pub fn recover_account_2(svm: &mut litesvm::LiteSVM, governance_authority: &Keyp new_owner, }; - let ix = Instruction::new_with_bytes(staking::ID, &staking::instruction::RecoverAccount2{}.data(), accs.to_account_metas(None)); + let ix = Instruction::new_with_bytes( + staking::ID, + &staking::instruction::RecoverAccount2 {}.data(), + accs.to_account_metas(None), + ); let tx = Transaction::new_signed_with_payer( &[ix], Some(&governance_authority.pubkey()), @@ -537,3 +555,36 @@ pub fn recover_account_2(svm: &mut litesvm::LiteSVM, governance_authority: &Keyp svm.send_transaction(tx) } + +pub fn create_voter_record( + svm: &mut litesvm::LiteSVM, + payer: &Keypair, + stake_account_positions: Pubkey, +) -> TransactionResult { + let config_account = get_config_address(); + let stake_account_metadata = get_stake_account_metadata_address(stake_account_positions); + let voter_record = get_voter_record_address(stake_account_positions); + + let accs = staking::accounts::CreateVoterRecord { + payer: payer.pubkey(), + stake_account_positions, + stake_account_metadata, + voter_record, + config: config_account, + system_program: system_program::ID, + }; + + let ix = Instruction::new_with_bytes( + staking::ID, + &staking::instruction::CreateVoterRecord {}.data(), + accs.to_account_metas(None), + ); + let tx = Transaction::new_signed_with_payer( + &[ix], + Some(&payer.pubkey()), + &[&payer], + svm.latest_blockhash(), + ); + + svm.send_transaction(tx) +} diff --git a/staking/integration-tests/tests/recover2.rs b/staking/integration-tests/tests/recover2.rs index 285cee93..b2be65a5 100644 --- a/staking/integration-tests/tests/recover2.rs +++ b/staking/integration-tests/tests/recover2.rs @@ -1,39 +1,75 @@ use { - anchor_lang::error::{AnchorError, ErrorCode}, integration_tests::{ - assert_anchor_program_error, governance::{ + anchor_lang::error::{ + AnchorError, + ErrorCode, + }, + integration_tests::{ + assert_anchor_program_error, + governance::{ addresses::MAINNET_GOVERNANCE_PROGRAM_ID, helper_functions::create_proposal_and_vote, instructions::create_token_owner_record, - }, setup::{ + }, + setup::{ setup, SetupProps, SetupResult, STARTING_EPOCH, - }, solana::utils::{ + }, + solana::utils::{ fetch_account_data, fetch_positions_account, - }, staking::{ - helper_functions::initialize_new_stake_account, instructions::{ - create_position, create_stake_account, join_dao_llc, merge_target_positions, recover_account_2, update_token_list_time, update_voter_weight - }, pda::{ - get_stake_account_metadata_address, get_target_address, get_voter_record_address - } - }, utils::clock::advance_n_epochs - }, litesvm::LiteSVM, solana_cli_output::CliAccount, solana_sdk::{ + }, + staking::{ + helper_functions::initialize_new_stake_account, + instructions::{ + create_position, + create_stake_account, + create_voter_record, + join_dao_llc, + merge_target_positions, + recover_account_2, + update_token_list_time, + update_voter_weight, + }, + pda::{ + get_stake_account_metadata_address, + get_target_address, + get_voter_record_address, + }, + }, + utils::clock::advance_n_epochs, + }, + litesvm::LiteSVM, + solana_cli_output::CliAccount, + solana_sdk::{ account::{ AccountSharedData, WritableAccount, - }, native_token::LAMPORTS_PER_SOL, pubkey::Pubkey, signature::Keypair, signer::Signer - }, staking::state::{ - max_voter_weight_record::MAX_VOTER_WEIGHT, positions::{ - TargetWithParameters, - POSITION_BUFFER_SIZE, - }, stake_account::StakeAccountMetadataV2, target::TargetMetadata, voter_weight_record::VoterWeightRecord - }, std::{ + }, + native_token::LAMPORTS_PER_SOL, + pubkey::Pubkey, + signature::Keypair, + signer::Signer, + }, + staking::{ + error::ErrorCode as StakingError, + state::{ + max_voter_weight_record::MAX_VOTER_WEIGHT, + positions::{ + TargetWithParameters, + POSITION_BUFFER_SIZE, + }, + stake_account::StakeAccountMetadataV2, + target::TargetMetadata, + voter_weight_record::VoterWeightRecord, + }, + }, + std::{ fs::File, io::Read, str::FromStr, - } + }, }; const MAINNET_TOKENS_LIST_TIME: i64 = 1684591200; @@ -48,7 +84,7 @@ const MAINNET_ELAPSED_EPOCHS: u64 = 2850; fn test_recover2() { let SetupResult { mut svm, - payer : governance_authority, + payer: governance_authority, pyth_token_mint, publisher_keypair: _, pool_data_pubkey: _, @@ -67,230 +103,66 @@ fn test_recover2() { let new_owner = Keypair::new(); svm.airdrop(&owner.pubkey(), LAMPORTS_PER_SOL).unwrap(); - let stake_account_positions = initialize_new_stake_account(&mut svm, &owner, &pyth_token_mint, true, true); - - assert_anchor_program_error!(recover_account_2(&mut svm, &owner, stake_account_positions, new_owner.pubkey()), - ErrorCode::ConstraintHasOne, - 0 + svm.airdrop(&new_owner.pubkey(), LAMPORTS_PER_SOL).unwrap(); + + let stake_account_positions = + initialize_new_stake_account(&mut svm, &owner, &pyth_token_mint, true, true); + // make sure voter record can be created permissionlessly if it doesn't exist + create_voter_record(&mut svm, &governance_authority, stake_account_positions).unwrap(); + + assert_anchor_program_error!( + recover_account_2( + &mut svm, + &owner, + stake_account_positions, + new_owner.pubkey() + ), + ErrorCode::ConstraintHasOne, + 0 ); - recover_account_2(&mut svm, &governance_authority, stake_account_positions, new_owner.pubkey()).unwrap(); + recover_account_2( + &mut svm, + &governance_authority, + stake_account_positions, + new_owner.pubkey(), + ) + .unwrap(); let mut positions_account = fetch_positions_account(&mut svm, &stake_account_positions); let positions = positions_account.to_dynamic_position_array(); assert_eq!(positions.owner().unwrap(), new_owner.pubkey()); - let stake_account_metadata: StakeAccountMetadataV2 = fetch_account_data(&mut svm, &get_stake_account_metadata_address(stake_account_positions)); + let stake_account_metadata: StakeAccountMetadataV2 = fetch_account_data( + &mut svm, + &get_stake_account_metadata_address(stake_account_positions), + ); assert_eq!(stake_account_metadata.owner, new_owner.pubkey()); - let voter_record: VoterWeightRecord = fetch_account_data(&mut svm, &get_voter_record_address(stake_account_positions)); + let voter_record: VoterWeightRecord = + fetch_account_data(&mut svm, &get_voter_record_address(stake_account_positions)); assert_eq!(voter_record.voter_weight, 0); - - - - // let stake_account_positions = - // load_stake_accounts(&mut svm, &payer.pubkey(), &pyth_token_mint.pubkey()); -// let governance_address = load_governance_accounts(&mut svm, &pyth_token_mint.pubkey()); - -// update_token_list_time(&mut svm, &payer, MAINNET_TOKENS_LIST_TIME); -// advance_n_epochs(&mut svm, &payer, MAINNET_ELAPSED_EPOCHS); -// join_dao_llc(&mut svm, &payer, stake_account_positions).unwrap(); -// update_voter_weight(&mut svm, &payer, stake_account_positions).unwrap(); - -// let target_account: TargetMetadata = fetch_account_data(&mut svm, &get_target_address()); -// let voter_record: VoterWeightRecord = -// fetch_account_data(&mut svm, &get_voter_record_address(stake_account_positions)); - -// // Check that the voter weight is calculated correctly -// let mut positions_account = fetch_positions_account(&mut svm, &stake_account_positions); -// let positions = positions_account.to_dynamic_position_array(); - -// let pos1 = positions.read_position(0).unwrap().unwrap(); -// let pos2 = positions.read_position(1).unwrap().unwrap(); -// assert!(positions.get_position_capacity() == 20); - -// for i in 2..positions.get_position_capacity() { -// assert!(positions.read_position(i).unwrap().is_none()); -// } - -// let expected_voter_weight = (((pos1.amount + pos2.amount) as u128) * MAX_VOTER_WEIGHT as u128 -// / target_account.locked as u128) as u64; - -// assert_eq!(voter_record.voter_weight, expected_voter_weight); - -// // Try voting against the actual governance program -// create_token_owner_record(&mut svm, &payer).unwrap(); -// let actual_proposal_data = create_proposal_and_vote( -// &mut svm, -// &payer, -// &stake_account_positions, -// &governance_address, -// ); -// assert_eq!(actual_proposal_data.options.len(), 1); -// assert_eq!( -// actual_proposal_data.options[0].vote_weight, -// expected_voter_weight -// ); - -// // Test some other actions -// // create_positions, merge_positions -// create_position( -// &mut svm, -// &payer, -// stake_account_positions, -// TargetWithParameters::Voting, -// None, -// 100, -// ) -// .unwrap(); - -// let mut positions_account = fetch_positions_account(&mut svm, &stake_account_positions); -// let positions = positions_account.to_dynamic_position_array(); - -// let post_pos1 = positions.read_position(0).unwrap().unwrap(); -// let post_pos2 = positions.read_position(1).unwrap().unwrap(); -// let post_pos3 = positions.read_position(2).unwrap().unwrap(); - -// assert_eq!(post_pos1, pos1); -// assert_eq!(post_pos2, pos2); - -// assert_eq!(post_pos3.activation_epoch, STARTING_EPOCH + 2851); - -// assert!(positions.get_position_capacity() == 20); - -// for i in 3..positions.get_position_capacity() { -// assert!(positions.read_position(i).unwrap().is_none()); -// } - -// advance_n_epochs(&mut svm, &payer, 1); -// update_voter_weight(&mut svm, &payer, stake_account_positions).unwrap(); - -// let voter_record: VoterWeightRecord = -// fetch_account_data(&mut svm, &get_voter_record_address(stake_account_positions)); - -// let expected_voter_weight = (((post_pos1.amount + post_pos2.amount + post_pos3.amount) as u128) -// * MAX_VOTER_WEIGHT as u128 -// / (target_account.locked + post_pos3.amount) as u128) -// as u64; - -// assert_eq!(voter_record.voter_weight, expected_voter_weight); - -// merge_target_positions(&mut svm, &payer, stake_account_positions).unwrap(); - -// let mut positions_account = fetch_positions_account(&mut svm, &stake_account_positions); -// let positions = positions_account.to_dynamic_position_array(); - -// assert!(positions.get_position_capacity() == 2); -// assert_eq!(positions.acc_info.data_len(), 40 + 2 * POSITION_BUFFER_SIZE); - - -// let post_merge_pos1 = positions.read_position(0).unwrap().unwrap(); -// let post_merge_pos2 = positions.read_position(1).unwrap().unwrap(); - -// assert_eq!( -// post_merge_pos1.activation_epoch, -// std::cmp::min(post_pos1.activation_epoch, post_pos2.activation_epoch) -// ); -// assert_eq!(post_merge_pos1.amount, post_pos1.amount + post_pos2.amount); -// assert_eq!(post_merge_pos1.unlocking_start, None); - -// assert_eq!(post_merge_pos2.activation_epoch, post_pos3.activation_epoch); -// assert_eq!(post_merge_pos2.amount, post_pos3.amount); -// assert_eq!(post_merge_pos2.unlocking_start, None); - - -// assert_eq!( -// positions.read_position(2).unwrap_err(), -// staking::error::ErrorCode::PositionOutOfBounds.into() -// ); - -// // Voter weight should be the same after merging -// svm.expire_blockhash(); -// update_voter_weight(&mut svm, &payer, stake_account_positions).unwrap(); -// let voter_record: VoterWeightRecord = -// fetch_account_data(&mut svm, &get_voter_record_address(stake_account_positions)); -// assert_eq!(voter_record.voter_weight, expected_voter_weight); - -// // Vote again, after merging -// let proposal_account = create_proposal_and_vote( -// &mut svm, -// &payer, -// &stake_account_positions, -// &governance_address, -// ); -// assert_eq!(proposal_account.options.len(), 1); -// assert_eq!( -// proposal_account.options[0].vote_weight, -// expected_voter_weight -// ); -// } - -// // These accounts were snapshotted on 16th August 2024 -// /// When loading these stake accounts, we need to replace the mainnet owner of the account by a key -// /// we have access to in the tests. We also need to replace the mainnet pyth mint address by the one -// /// in the tests. -// fn load_stake_accounts(svm: &mut LiteSVM, payer: &Pubkey, pyth_token_mint: &Pubkey) -> Pubkey { -// let mut stake_account_positions = load_account_file("staking/stake_account_positions.json"); -// stake_account_positions.account.data_as_mut_slice()[8..40].copy_from_slice(&payer.to_bytes()); -// svm.set_account( -// stake_account_positions.address, -// stake_account_positions.account.into(), -// ) -// .unwrap(); - -// let mut stake_account_metadata = load_account_file("staking/stake_account_metadata.json"); -// stake_account_metadata.account.data_as_mut_slice()[12..44].copy_from_slice(&payer.to_bytes()); -// svm.set_account( -// stake_account_metadata.address, -// stake_account_metadata.account.into(), -// ) -// .unwrap(); - - -// let mut stake_account_custody = load_account_file("staking/stake_account_custody.json"); -// stake_account_custody.account.data_as_mut_slice()[..32] -// .copy_from_slice(&pyth_token_mint.to_bytes()); -// svm.set_account( -// stake_account_custody.address, -// stake_account_custody.account.into(), -// ) -// .unwrap(); - -// let mut voter_record = load_account_file("staking/voter_record.json"); -// voter_record.account.data_as_mut_slice()[40..72].copy_from_slice(&pyth_token_mint.to_bytes()); -// voter_record.account.data_as_mut_slice()[72..104].copy_from_slice(&payer.to_bytes()); -// svm.set_account(voter_record.address, voter_record.account.into()) -// .unwrap(); - - -// let target_account = load_account_file("staking/target_account.json"); -// svm.set_account(target_account.address, target_account.account.into()) -// .unwrap(); - -// stake_account_positions.address -// } - -// // These accounts were snapshotted on 16th August 2024 -// fn load_governance_accounts(svm: &mut LiteSVM, pyth_token_mint: &Pubkey) -> Pubkey { -// svm.add_program_from_file( -// MAINNET_GOVERNANCE_PROGRAM_ID, -// "fixtures/governance/governance.so", -// ) -// .unwrap(); - -// let mut realm = load_account_file("governance/realm.json"); -// realm.account.data_as_mut_slice()[1..33].copy_from_slice(&pyth_token_mint.to_bytes()); -// svm.set_account(realm.address, realm.account.into()) -// .unwrap(); - -// let governance = load_account_file("governance/governance.json"); -// svm.set_account(governance.address, governance.account.into()) -// .unwrap(); - -// let realm_config = load_account_file("governance/realm_config.json"); -// svm.set_account(realm_config.address, realm_config.account.into()) -// .unwrap(); - -// governance.address + // new_owner creates a new position + create_position( + &mut svm, + &new_owner, + stake_account_positions, + TargetWithParameters::Voting, + None, + 100, + ) + .unwrap(); + + // now the account can't be recovered + assert_anchor_program_error!( + recover_account_2( + &mut svm, + &governance_authority, + stake_account_positions, + new_owner.pubkey() + ), + StakingError::RecoverWithStake, + 0 + ); } diff --git a/staking/programs/staking/src/context.rs b/staking/programs/staking/src/context.rs index 1c84dc7d..4016ceca 100644 --- a/staking/programs/staking/src/context.rs +++ b/staking/programs/staking/src/context.rs @@ -449,10 +449,10 @@ pub struct RecoverAccount2<'info> { pub governance_authority: Signer<'info>, /// CHECK : This AccountInfo is safe because it's checked against stake_account_metadata - pub owner: AccountInfo<'info>, + pub owner: AccountInfo<'info>, /// CHECK : A new owner is provided by the governance_authority - pub new_owner: AccountInfo<'info>, + pub new_owner: AccountInfo<'info>, // Stake program accounts: #[account(mut)] From 23923c696a26016c36120431005aa6e525873b31 Mon Sep 17 00:00:00 2001 From: Guillermo Bescos Date: Mon, 19 May 2025 14:54:43 +0100 Subject: [PATCH 03/16] test: add tests --- staking/integration-tests/tests/recover2.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/staking/integration-tests/tests/recover2.rs b/staking/integration-tests/tests/recover2.rs index b2be65a5..409dd8b8 100644 --- a/staking/integration-tests/tests/recover2.rs +++ b/staking/integration-tests/tests/recover2.rs @@ -72,9 +72,6 @@ use { }, }; -const MAINNET_TOKENS_LIST_TIME: i64 = 1684591200; -const MAINNET_ELAPSED_EPOCHS: u64 = 2850; - #[test] /// This test has two purposes: /// 1) to test the voting functionality against the deployed governance program and configuration From 088666da39f1d28c37490007ced875bf49ddf190 Mon Sep 17 00:00:00 2001 From: Guillermo Bescos Date: Mon, 19 May 2025 14:55:48 +0100 Subject: [PATCH 04/16] don't touch this file --- staking/integration-tests/src/solana/utils.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/staking/integration-tests/src/solana/utils.rs b/staking/integration-tests/src/solana/utils.rs index ec832e83..7d7a706b 100644 --- a/staking/integration-tests/src/solana/utils.rs +++ b/staking/integration-tests/src/solana/utils.rs @@ -20,8 +20,7 @@ pub fn fetch_account_data( svm: &mut litesvm::LiteSVM, account: &Pubkey, ) -> T { - let account_data = svm.get_account(account).unwrap().data; - T::try_deserialize(&mut account_data.as_ref()).unwrap() + T::try_deserialize(&mut svm.get_account(account).unwrap().data.as_ref()).unwrap() } pub fn fetch_governance_account_data( From c99feb149364b4cdf9d2a5020372d323632f45b1 Mon Sep 17 00:00:00 2001 From: Guillermo Bescos Date: Mon, 19 May 2025 15:11:32 +0100 Subject: [PATCH 05/16] clean --- staking/integration-tests/src/staking/instructions.rs | 5 ----- staking/integration-tests/tests/recover2.rs | 1 + staking/programs/staking/src/context.rs | 6 +----- staking/programs/staking/src/lib.rs | 6 +++--- 4 files changed, 5 insertions(+), 13 deletions(-) diff --git a/staking/integration-tests/src/staking/instructions.rs b/staking/integration-tests/src/staking/instructions.rs index e0cfcee6..35a8d916 100644 --- a/staking/integration-tests/src/staking/instructions.rs +++ b/staking/integration-tests/src/staking/instructions.rs @@ -524,16 +524,11 @@ pub fn recover_account_2( new_owner: Pubkey, ) -> TransactionResult { let config = get_config_address(); - let owner = fetch_positions_account(svm, &stake_account_positions) - .to_dynamic_position_array() - .owner() - .unwrap(); let stake_account_metadata = get_stake_account_metadata_address(stake_account_positions); let voter_record = get_voter_record_address(stake_account_positions); let accs = staking::accounts::RecoverAccount2 { governance_authority: governance_authority.pubkey(), - owner, config, stake_account_metadata, stake_account_positions, diff --git a/staking/integration-tests/tests/recover2.rs b/staking/integration-tests/tests/recover2.rs index 409dd8b8..d1c7ba79 100644 --- a/staking/integration-tests/tests/recover2.rs +++ b/staking/integration-tests/tests/recover2.rs @@ -151,6 +151,7 @@ fn test_recover2() { ) .unwrap(); + svm.expire_blockhash(); // now the account can't be recovered assert_anchor_program_error!( recover_account_2( diff --git a/staking/programs/staking/src/context.rs b/staking/programs/staking/src/context.rs index 4016ceca..c40ba992 100644 --- a/staking/programs/staking/src/context.rs +++ b/staking/programs/staking/src/context.rs @@ -448,10 +448,7 @@ pub struct RecoverAccount<'info> { pub struct RecoverAccount2<'info> { pub governance_authority: Signer<'info>, - /// CHECK : This AccountInfo is safe because it's checked against stake_account_metadata - pub owner: AccountInfo<'info>, - - /// CHECK : A new owner is provided by the governance_authority + /// CHECK : A new arbitrary owner provided by the governance_authority pub new_owner: AccountInfo<'info>, // Stake program accounts: @@ -465,7 +462,6 @@ pub struct RecoverAccount2<'info> { stake_account_positions.key().as_ref() ], bump = stake_account_metadata.metadata_bump, - has_one = owner )] pub stake_account_metadata: Account<'info, stake_account::StakeAccountMetadataV2>, diff --git a/staking/programs/staking/src/lib.rs b/staking/programs/staking/src/lib.rs index 85d73b0e..f8fa669c 100644 --- a/staking/programs/staking/src/lib.rs +++ b/staking/programs/staking/src/lib.rs @@ -795,10 +795,10 @@ pub mod staking { } /** Recovers a user's `stake account` ownership by transferring ownership - * from a token account to the `owner` of that token account. + * to a new owner provided by the governance_authority. * - * This functionality addresses the scenario where a user mistakenly - * created a stake account using their token account address as the owner. + * This functionality addresses the scenario where a user doesn't have access to their owner + * key. Only accounts with staked tokens can be recovered. */ pub fn recover_account_2(ctx: Context) -> Result<()> { // Check that there aren't any positions (i.e., staked tokens) in the account. From d16bd4b5c8e7a8da31f730091569407744ce3ebb Mon Sep 17 00:00:00 2001 From: Guillermo Bescos Date: Mon, 19 May 2025 15:16:52 +0100 Subject: [PATCH 06/16] add comment --- staking/programs/staking/src/lib.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/staking/programs/staking/src/lib.rs b/staking/programs/staking/src/lib.rs index f8fa669c..41fbe076 100644 --- a/staking/programs/staking/src/lib.rs +++ b/staking/programs/staking/src/lib.rs @@ -794,11 +794,11 @@ pub mod staking { Ok(()) } - /** Recovers a user's `stake account` ownership by transferring ownership - * to a new owner provided by the governance_authority. + /** Recovers a user's stake account by transferring ownership + * to a new owner provided by the `governance_authority`. * * This functionality addresses the scenario where a user doesn't have access to their owner - * key. Only accounts with staked tokens can be recovered. + * key. Only accounts without any staked tokens can be recovered. */ pub fn recover_account_2(ctx: Context) -> Result<()> { // Check that there aren't any positions (i.e., staked tokens) in the account. From a8516b5c68913c3122dd1b12196c23a0030e4369 Mon Sep 17 00:00:00 2001 From: Guillermo Bescos Date: Mon, 19 May 2025 15:31:07 +0100 Subject: [PATCH 07/16] clean imports --- staking/integration-tests/tests/recover2.rs | 42 +-------------------- 1 file changed, 2 insertions(+), 40 deletions(-) diff --git a/staking/integration-tests/tests/recover2.rs b/staking/integration-tests/tests/recover2.rs index d1c7ba79..9e9a7df2 100644 --- a/staking/integration-tests/tests/recover2.rs +++ b/staking/integration-tests/tests/recover2.rs @@ -1,20 +1,11 @@ use { - anchor_lang::error::{ - AnchorError, - ErrorCode, - }, + anchor_lang::error::ErrorCode, integration_tests::{ assert_anchor_program_error, - governance::{ - addresses::MAINNET_GOVERNANCE_PROGRAM_ID, - helper_functions::create_proposal_and_vote, - instructions::create_token_owner_record, - }, setup::{ setup, SetupProps, SetupResult, - STARTING_EPOCH, }, solana::utils::{ fetch_account_data, @@ -24,60 +15,31 @@ use { helper_functions::initialize_new_stake_account, instructions::{ create_position, - create_stake_account, create_voter_record, - join_dao_llc, - merge_target_positions, recover_account_2, - update_token_list_time, - update_voter_weight, }, pda::{ get_stake_account_metadata_address, - get_target_address, get_voter_record_address, }, }, - utils::clock::advance_n_epochs, }, - litesvm::LiteSVM, - solana_cli_output::CliAccount, solana_sdk::{ - account::{ - AccountSharedData, - WritableAccount, - }, native_token::LAMPORTS_PER_SOL, - pubkey::Pubkey, signature::Keypair, signer::Signer, }, staking::{ error::ErrorCode as StakingError, state::{ - max_voter_weight_record::MAX_VOTER_WEIGHT, - positions::{ - TargetWithParameters, - POSITION_BUFFER_SIZE, - }, + positions::TargetWithParameters, stake_account::StakeAccountMetadataV2, - target::TargetMetadata, voter_weight_record::VoterWeightRecord, }, }, - std::{ - fs::File, - io::Read, - str::FromStr, - }, }; #[test] -/// This test has two purposes: -/// 1) to test the voting functionality against the deployed governance program and configuration -/// 2) to test that the new staking account is compatible with stake account positions with the old -/// fixed sized position array and such accounts can be turned into the new version by calling -/// merge_target_positions and nothing breaks fn test_recover2() { let SetupResult { mut svm, From 01910b050225a7daba4b559c5af82a8da7511548 Mon Sep 17 00:00:00 2001 From: Guillermo Bescos Date: Mon, 19 May 2025 15:35:35 +0100 Subject: [PATCH 08/16] fix typo --- staking/integration-tests/tests/recover2.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/staking/integration-tests/tests/recover2.rs b/staking/integration-tests/tests/recover2.rs index 9e9a7df2..a8abbed1 100644 --- a/staking/integration-tests/tests/recover2.rs +++ b/staking/integration-tests/tests/recover2.rs @@ -67,12 +67,12 @@ fn test_recover2() { let stake_account_positions = initialize_new_stake_account(&mut svm, &owner, &pyth_token_mint, true, true); // make sure voter record can be created permissionlessly if it doesn't exist - create_voter_record(&mut svm, &governance_authority, stake_account_positions).unwrap(); + create_voter_record(&mut svm, &new_owner, stake_account_positions).unwrap(); assert_anchor_program_error!( recover_account_2( &mut svm, - &owner, + &owner, // governance_authority has to sign stake_account_positions, new_owner.pubkey() ), @@ -100,7 +100,7 @@ fn test_recover2() { let voter_record: VoterWeightRecord = fetch_account_data(&mut svm, &get_voter_record_address(stake_account_positions)); - assert_eq!(voter_record.voter_weight, 0); + assert_eq!(voter_record.governing_token_owner, new_owner.pubkey()); // new_owner creates a new position create_position( From 6e6c395d754bf8a0c711a663854fa3ee89f24d09 Mon Sep 17 00:00:00 2001 From: Guillermo Bescos Date: Mon, 19 May 2025 15:39:28 +0100 Subject: [PATCH 09/16] fix workflows --- .github/workflows/anchor.yml | 2 +- .github/workflows/metrics_deploy.yml | 28 ---------------------------- 2 files changed, 1 insertion(+), 29 deletions(-) delete mode 100644 .github/workflows/metrics_deploy.yml diff --git a/.github/workflows/anchor.yml b/.github/workflows/anchor.yml index 6d679dd2..f082833f 100644 --- a/.github/workflows/anchor.yml +++ b/.github/workflows/anchor.yml @@ -22,7 +22,7 @@ jobs: run: npm ci - name: Install Solana run: | - sh -c "$(curl -sSfL https://release.solana.com/v1.18.16/install)" + sh -c "$(curl -sSfL https://release.anza.xyz/v1.18.16/install)" echo "/home/runner/.local/share/solana/install/active_release/bin" >> $GITHUB_PATH - name: Install Solana Verify CLI run: | diff --git a/.github/workflows/metrics_deploy.yml b/.github/workflows/metrics_deploy.yml deleted file mode 100644 index d42fc992..00000000 --- a/.github/workflows/metrics_deploy.yml +++ /dev/null @@ -1,28 +0,0 @@ -name: CI -on: - push: - branches: [main] - pull_request: - branches: [main] - -permissions: - contents: read - id-token: write -jobs: - build-and-push-image: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - uses: aws-actions/configure-aws-credentials@v1 - with: - role-to-assume: arn:aws:iam::192824654885:role/github-actions-ecr - aws-region: eu-west-2 - - uses: aws-actions/amazon-ecr-login@v1 - id: ecr_login - - run: | - docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG ./metrics - docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG - env: - ECR_REGISTRY: ${{ steps.ecr_login.outputs.registry }} - ECR_REPOSITORY: governance-metrics - IMAGE_TAG: ${{ github.sha }} From 62448908f33c4faee012a9ae5b86bd61f39357fe Mon Sep 17 00:00:00 2001 From: Guillermo Bescos Date: Mon, 19 May 2025 15:41:34 +0100 Subject: [PATCH 10/16] clippy --- .../src/staking/instructions.rs | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/staking/integration-tests/src/staking/instructions.rs b/staking/integration-tests/src/staking/instructions.rs index 35a8d916..73ee61ca 100644 --- a/staking/integration-tests/src/staking/instructions.rs +++ b/staking/integration-tests/src/staking/instructions.rs @@ -15,10 +15,7 @@ use { MAINNET_REALM_ID, }, integrity_pool::pda::get_pool_config_address, - solana::utils::{ - fetch_account_data, - fetch_positions_account, - }, + solana::utils::fetch_account_data, }, anchor_lang::{ prelude::AccountMeta, @@ -41,13 +38,10 @@ use { signer::Signer, transaction::Transaction, }, - staking::{ - instruction, - state::{ - global_config::GlobalConfig, - positions::TargetWithParameters, - voter_weight_record::VoterWeightAction, - }, + staking::state::{ + global_config::GlobalConfig, + positions::TargetWithParameters, + voter_weight_record::VoterWeightAction, }, }; From 0fc69c1c0df0496b60b6cf5080060c5299e46456 Mon Sep 17 00:00:00 2001 From: Guillermo Bescos Date: Mon, 19 May 2025 15:53:33 +0100 Subject: [PATCH 11/16] fix idls --- staking/target/idl/staking.json | 114 ++++++++++++++++++++++++++++++++ staking/target/types/staking.ts | 114 ++++++++++++++++++++++++++++++++ 2 files changed, 228 insertions(+) diff --git a/staking/target/idl/staking.json b/staking/target/idl/staking.json index 4c2c4047..0194436d 100644 --- a/staking/target/idl/staking.json +++ b/staking/target/idl/staking.json @@ -1395,6 +1395,120 @@ ], "args": [] }, + { + "name": "recover_account_2", + "docs": [ + "Recovers a user's stake account by transferring ownership\n * to a new owner provided by the `governance_authority`.\n *\n * This functionality addresses the scenario where a user doesn't have access to their owner\n * key. Only accounts without any staked tokens can be recovered." + ], + "discriminator": [ + 0, + 28, + 239, + 39, + 84, + 86, + 71, + 247 + ], + "accounts": [ + { + "name": "governance_authority", + "signer": true, + "relations": [ + "config" + ] + }, + { + "name": "new_owner", + "docs": [ + "CHECK : A new arbitrary owner provided by the governance_authority" + ] + }, + { + "name": "stake_account_positions", + "writable": true + }, + { + "name": "stake_account_metadata", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 115, + 116, + 97, + 107, + 101, + 95, + 109, + 101, + 116, + 97, + 100, + 97, + 116, + 97 + ] + }, + { + "kind": "account", + "path": "stake_account_positions" + } + ] + } + }, + { + "name": "voter_record", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 118, + 111, + 116, + 101, + 114, + 95, + 119, + 101, + 105, + 103, + 104, + 116 + ] + }, + { + "kind": "account", + "path": "stake_account_positions" + } + ] + } + }, + { + "name": "config", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 99, + 111, + 110, + 102, + 105, + 103 + ] + } + ] + } + } + ], + "args": [] + }, { "name": "request_split", "docs": [ diff --git a/staking/target/types/staking.ts b/staking/target/types/staking.ts index a4c70c29..9f1296b6 100644 --- a/staking/target/types/staking.ts +++ b/staking/target/types/staking.ts @@ -1401,6 +1401,120 @@ export type Staking = { ], "args": [] }, + { + "name": "recoverAccount2", + "docs": [ + "Recovers a user's stake account by transferring ownership\n * to a new owner provided by the `governance_authority`.\n *\n * This functionality addresses the scenario where a user doesn't have access to their owner\n * key. Only accounts without any staked tokens can be recovered." + ], + "discriminator": [ + 0, + 28, + 239, + 39, + 84, + 86, + 71, + 247 + ], + "accounts": [ + { + "name": "governanceAuthority", + "signer": true, + "relations": [ + "config" + ] + }, + { + "name": "newOwner", + "docs": [ + "CHECK : A new arbitrary owner provided by the governance_authority" + ] + }, + { + "name": "stakeAccountPositions", + "writable": true + }, + { + "name": "stakeAccountMetadata", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 115, + 116, + 97, + 107, + 101, + 95, + 109, + 101, + 116, + 97, + 100, + 97, + 116, + 97 + ] + }, + { + "kind": "account", + "path": "stakeAccountPositions" + } + ] + } + }, + { + "name": "voterRecord", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 118, + 111, + 116, + 101, + 114, + 95, + 119, + 101, + 105, + 103, + 104, + 116 + ] + }, + { + "kind": "account", + "path": "stakeAccountPositions" + } + ] + } + }, + { + "name": "config", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 99, + 111, + 110, + 102, + 105, + 103 + ] + } + ] + } + } + ], + "args": [] + }, { "name": "requestSplit", "docs": [ From 10c7b3a714f00b02eec5bbf5d15b9876e2d00682 Mon Sep 17 00:00:00 2001 From: Guillermo Bescos Date: Mon, 19 May 2025 15:54:52 +0100 Subject: [PATCH 12/16] update to anza releases --- .github/workflows/anchor-idl.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/anchor-idl.yml b/.github/workflows/anchor-idl.yml index 3ecd8d64..377a9333 100644 --- a/.github/workflows/anchor-idl.yml +++ b/.github/workflows/anchor-idl.yml @@ -20,7 +20,7 @@ jobs: run: npm ci - name: Install Solana run: | - sh -c "$(curl -sSfL https://release.solana.com/v1.18.16/install)" + sh -c "$(curl -sSfL https://release.anza.xyz/v1.18.16/install)" echo "/home/runner/.local/share/solana/install/active_release/bin" >> $GITHUB_PATH - name: Install Anchor working-directory: ./staking From 605cbff47f8d847ca99aeed413a674b4b2358a81 Mon Sep 17 00:00:00 2001 From: Guillermo Bescos Date: Mon, 19 May 2025 17:30:43 +0100 Subject: [PATCH 13/16] use nightly for the idl --- .github/workflows/anchor-idl.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/workflows/anchor-idl.yml b/.github/workflows/anchor-idl.yml index 377a9333..ed34ceac 100644 --- a/.github/workflows/anchor-idl.yml +++ b/.github/workflows/anchor-idl.yml @@ -11,6 +11,11 @@ jobs: runs-on: ubuntu-latest steps: + - uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: nightly-2024-02-01 + components: rustfmt, clippy - uses: actions/checkout@v2 - name: Setup Node.js uses: actions/setup-node@v2 @@ -27,6 +32,8 @@ jobs: run: npm i -g @coral-xyz/anchor-cli@0.30.1 - name: Build IDL working-directory: ./staking + env: + RUSTUP_TOOLCHAIN: nightly-2024-02-01 run: anchor build - name: Check commited idl is up to date working-directory: ./staking From 4f97574d1e8000e12e6d40fcadc8ebbf6205ce23 Mon Sep 17 00:00:00 2001 From: Guillermo Bescos Date: Tue, 20 May 2025 16:18:31 +0100 Subject: [PATCH 14/16] rename to transfer_instruction --- .../src/staking/instructions.rs | 6 +- .../{recover2.rs => transfer_account.rs} | 8 +- staking/programs/staking/src/context.rs | 2 +- staking/programs/staking/src/lib.rs | 7 +- staking/target/idl/staking.json | 228 +++++++++--------- staking/target/types/staking.ts | 228 +++++++++--------- 6 files changed, 239 insertions(+), 240 deletions(-) rename staking/integration-tests/tests/{recover2.rs => transfer_account.rs} (96%) diff --git a/staking/integration-tests/src/staking/instructions.rs b/staking/integration-tests/src/staking/instructions.rs index 73ee61ca..31619b42 100644 --- a/staking/integration-tests/src/staking/instructions.rs +++ b/staking/integration-tests/src/staking/instructions.rs @@ -511,7 +511,7 @@ pub fn merge_target_positions( svm.send_transaction(tx) } -pub fn recover_account_2( +pub fn transfer_account( svm: &mut litesvm::LiteSVM, governance_authority: &Keypair, stake_account_positions: Pubkey, @@ -521,7 +521,7 @@ pub fn recover_account_2( let stake_account_metadata = get_stake_account_metadata_address(stake_account_positions); let voter_record = get_voter_record_address(stake_account_positions); - let accs = staking::accounts::RecoverAccount2 { + let accs = staking::accounts::TransferAccount { governance_authority: governance_authority.pubkey(), config, stake_account_metadata, @@ -532,7 +532,7 @@ pub fn recover_account_2( let ix = Instruction::new_with_bytes( staking::ID, - &staking::instruction::RecoverAccount2 {}.data(), + &staking::instruction::TransferAccount {}.data(), accs.to_account_metas(None), ); let tx = Transaction::new_signed_with_payer( diff --git a/staking/integration-tests/tests/recover2.rs b/staking/integration-tests/tests/transfer_account.rs similarity index 96% rename from staking/integration-tests/tests/recover2.rs rename to staking/integration-tests/tests/transfer_account.rs index a8abbed1..a377180d 100644 --- a/staking/integration-tests/tests/recover2.rs +++ b/staking/integration-tests/tests/transfer_account.rs @@ -16,7 +16,7 @@ use { instructions::{ create_position, create_voter_record, - recover_account_2, + transfer_account, }, pda::{ get_stake_account_metadata_address, @@ -70,7 +70,7 @@ fn test_recover2() { create_voter_record(&mut svm, &new_owner, stake_account_positions).unwrap(); assert_anchor_program_error!( - recover_account_2( + transfer_account( &mut svm, &owner, // governance_authority has to sign stake_account_positions, @@ -80,7 +80,7 @@ fn test_recover2() { 0 ); - recover_account_2( + transfer_account( &mut svm, &governance_authority, stake_account_positions, @@ -116,7 +116,7 @@ fn test_recover2() { svm.expire_blockhash(); // now the account can't be recovered assert_anchor_program_error!( - recover_account_2( + transfer_account( &mut svm, &governance_authority, stake_account_positions, diff --git a/staking/programs/staking/src/context.rs b/staking/programs/staking/src/context.rs index c40ba992..cc11fe17 100644 --- a/staking/programs/staking/src/context.rs +++ b/staking/programs/staking/src/context.rs @@ -445,7 +445,7 @@ pub struct RecoverAccount<'info> { } #[derive(Accounts)] -pub struct RecoverAccount2<'info> { +pub struct TransferAccount<'info> { pub governance_authority: Signer<'info>, /// CHECK : A new arbitrary owner provided by the governance_authority diff --git a/staking/programs/staking/src/lib.rs b/staking/programs/staking/src/lib.rs index 41fbe076..2ea58122 100644 --- a/staking/programs/staking/src/lib.rs +++ b/staking/programs/staking/src/lib.rs @@ -794,13 +794,12 @@ pub mod staking { Ok(()) } - /** Recovers a user's stake account by transferring ownership - * to a new owner provided by the `governance_authority`. + /** Transfers a user's stake account to a new owner provided by the `governance_authority`. * * This functionality addresses the scenario where a user doesn't have access to their owner - * key. Only accounts without any staked tokens can be recovered. + * key. Only accounts without any staked tokens can be transferred. */ - pub fn recover_account_2(ctx: Context) -> Result<()> { + pub fn transfer_account(ctx: Context) -> Result<()> { // Check that there aren't any positions (i.e., staked tokens) in the account. // Transferring accounts with staked tokens might lead to double voting require!( diff --git a/staking/target/idl/staking.json b/staking/target/idl/staking.json index 0194436d..f8bef47d 100644 --- a/staking/target/idl/staking.json +++ b/staking/target/idl/staking.json @@ -1395,120 +1395,6 @@ ], "args": [] }, - { - "name": "recover_account_2", - "docs": [ - "Recovers a user's stake account by transferring ownership\n * to a new owner provided by the `governance_authority`.\n *\n * This functionality addresses the scenario where a user doesn't have access to their owner\n * key. Only accounts without any staked tokens can be recovered." - ], - "discriminator": [ - 0, - 28, - 239, - 39, - 84, - 86, - 71, - 247 - ], - "accounts": [ - { - "name": "governance_authority", - "signer": true, - "relations": [ - "config" - ] - }, - { - "name": "new_owner", - "docs": [ - "CHECK : A new arbitrary owner provided by the governance_authority" - ] - }, - { - "name": "stake_account_positions", - "writable": true - }, - { - "name": "stake_account_metadata", - "writable": true, - "pda": { - "seeds": [ - { - "kind": "const", - "value": [ - 115, - 116, - 97, - 107, - 101, - 95, - 109, - 101, - 116, - 97, - 100, - 97, - 116, - 97 - ] - }, - { - "kind": "account", - "path": "stake_account_positions" - } - ] - } - }, - { - "name": "voter_record", - "writable": true, - "pda": { - "seeds": [ - { - "kind": "const", - "value": [ - 118, - 111, - 116, - 101, - 114, - 95, - 119, - 101, - 105, - 103, - 104, - 116 - ] - }, - { - "kind": "account", - "path": "stake_account_positions" - } - ] - } - }, - { - "name": "config", - "pda": { - "seeds": [ - { - "kind": "const", - "value": [ - 99, - 111, - 110, - 102, - 105, - 103 - ] - } - ] - } - } - ], - "args": [] - }, { "name": "request_split", "docs": [ @@ -1807,6 +1693,120 @@ } ] }, + { + "name": "transfer_account", + "docs": [ + "Transfers a user's stake account to a new owner provided by the `governance_authority`.\n *\n * This functionality addresses the scenario where a user doesn't have access to their owner\n * key. Only accounts without any staked tokens can be transferred." + ], + "discriminator": [ + 219, + 120, + 55, + 105, + 3, + 139, + 205, + 6 + ], + "accounts": [ + { + "name": "governance_authority", + "signer": true, + "relations": [ + "config" + ] + }, + { + "name": "new_owner", + "docs": [ + "CHECK : A new arbitrary owner provided by the governance_authority" + ] + }, + { + "name": "stake_account_positions", + "writable": true + }, + { + "name": "stake_account_metadata", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 115, + 116, + 97, + 107, + 101, + 95, + 109, + 101, + 116, + 97, + 100, + 97, + 116, + 97 + ] + }, + { + "kind": "account", + "path": "stake_account_positions" + } + ] + } + }, + { + "name": "voter_record", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 118, + 111, + 116, + 101, + 114, + 95, + 119, + 101, + 105, + 103, + 104, + 116 + ] + }, + { + "kind": "account", + "path": "stake_account_positions" + } + ] + } + }, + { + "name": "config", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 99, + 111, + 110, + 102, + 105, + 103 + ] + } + ] + } + } + ], + "args": [] + }, { "name": "update_agreement_hash", "discriminator": [ diff --git a/staking/target/types/staking.ts b/staking/target/types/staking.ts index 9f1296b6..4cbd1165 100644 --- a/staking/target/types/staking.ts +++ b/staking/target/types/staking.ts @@ -1401,120 +1401,6 @@ export type Staking = { ], "args": [] }, - { - "name": "recoverAccount2", - "docs": [ - "Recovers a user's stake account by transferring ownership\n * to a new owner provided by the `governance_authority`.\n *\n * This functionality addresses the scenario where a user doesn't have access to their owner\n * key. Only accounts without any staked tokens can be recovered." - ], - "discriminator": [ - 0, - 28, - 239, - 39, - 84, - 86, - 71, - 247 - ], - "accounts": [ - { - "name": "governanceAuthority", - "signer": true, - "relations": [ - "config" - ] - }, - { - "name": "newOwner", - "docs": [ - "CHECK : A new arbitrary owner provided by the governance_authority" - ] - }, - { - "name": "stakeAccountPositions", - "writable": true - }, - { - "name": "stakeAccountMetadata", - "writable": true, - "pda": { - "seeds": [ - { - "kind": "const", - "value": [ - 115, - 116, - 97, - 107, - 101, - 95, - 109, - 101, - 116, - 97, - 100, - 97, - 116, - 97 - ] - }, - { - "kind": "account", - "path": "stakeAccountPositions" - } - ] - } - }, - { - "name": "voterRecord", - "writable": true, - "pda": { - "seeds": [ - { - "kind": "const", - "value": [ - 118, - 111, - 116, - 101, - 114, - 95, - 119, - 101, - 105, - 103, - 104, - 116 - ] - }, - { - "kind": "account", - "path": "stakeAccountPositions" - } - ] - } - }, - { - "name": "config", - "pda": { - "seeds": [ - { - "kind": "const", - "value": [ - 99, - 111, - 110, - 102, - 105, - 103 - ] - } - ] - } - } - ], - "args": [] - }, { "name": "requestSplit", "docs": [ @@ -1813,6 +1699,120 @@ export type Staking = { } ] }, + { + "name": "transferAccount", + "docs": [ + "Transfers a user's stake account to a new owner provided by the `governance_authority`.\n *\n * This functionality addresses the scenario where a user doesn't have access to their owner\n * key. Only accounts without any staked tokens can be transferred." + ], + "discriminator": [ + 219, + 120, + 55, + 105, + 3, + 139, + 205, + 6 + ], + "accounts": [ + { + "name": "governanceAuthority", + "signer": true, + "relations": [ + "config" + ] + }, + { + "name": "newOwner", + "docs": [ + "CHECK : A new arbitrary owner provided by the governance_authority" + ] + }, + { + "name": "stakeAccountPositions", + "writable": true + }, + { + "name": "stakeAccountMetadata", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 115, + 116, + 97, + 107, + 101, + 95, + 109, + 101, + 116, + 97, + 100, + 97, + 116, + 97 + ] + }, + { + "kind": "account", + "path": "stakeAccountPositions" + } + ] + } + }, + { + "name": "voterRecord", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 118, + 111, + 116, + 101, + 114, + 95, + 119, + 101, + 105, + 103, + 104, + 116 + ] + }, + { + "kind": "account", + "path": "stakeAccountPositions" + } + ] + } + }, + { + "name": "config", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 99, + 111, + 110, + 102, + 105, + 103 + ] + } + ] + } + } + ], + "args": [] + }, { "name": "updateAgreementHash", "discriminator": [ From d78c1f84ef03fb965bba714b225d129c8c7f9176 Mon Sep 17 00:00:00 2001 From: Guillermo Bescos Date: Tue, 20 May 2025 16:20:23 +0100 Subject: [PATCH 15/16] continue rename --- staking/integration-tests/tests/transfer_account.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/staking/integration-tests/tests/transfer_account.rs b/staking/integration-tests/tests/transfer_account.rs index a377180d..c4a5a75d 100644 --- a/staking/integration-tests/tests/transfer_account.rs +++ b/staking/integration-tests/tests/transfer_account.rs @@ -40,7 +40,7 @@ use { }; #[test] -fn test_recover2() { +fn test_transfer_account() { let SetupResult { mut svm, payer: governance_authority, From b279e0c96f9b675297a8b533353518c673c38a4d Mon Sep 17 00:00:00 2001 From: Guillermo Bescos Date: Tue, 20 May 2025 16:22:20 +0100 Subject: [PATCH 16/16] bump program --- staking/Cargo.lock | 2 +- staking/programs/staking/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/staking/Cargo.lock b/staking/Cargo.lock index d4d30632..6259e30d 100644 --- a/staking/Cargo.lock +++ b/staking/Cargo.lock @@ -3076,7 +3076,7 @@ dependencies = [ [[package]] name = "pyth-staking-program" -version = "2.0.0" +version = "2.1.0" dependencies = [ "ahash 0.8.11", "anchor-lang", diff --git a/staking/programs/staking/Cargo.toml b/staking/programs/staking/Cargo.toml index 57c8dd66..486b58ee 100644 --- a/staking/programs/staking/Cargo.toml +++ b/staking/programs/staking/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pyth-staking-program" -version = "2.0.0" +version = "2.1.0" description = "Created with Anchor" edition = "2018"