Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion .github/workflows/anchor-idl.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -20,13 +25,15 @@ 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
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
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/anchor.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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: |
Expand Down
28 changes: 0 additions & 28 deletions .github/workflows/metrics_deploy.yml

This file was deleted.

2 changes: 1 addition & 1 deletion staking/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

67 changes: 67 additions & 0 deletions staking/integration-tests/src/staking/instructions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -510,3 +510,70 @@ pub fn merge_target_positions(

svm.send_transaction(tx)
}

pub fn transfer_account(
svm: &mut litesvm::LiteSVM,
governance_authority: &Keypair,
stake_account_positions: Pubkey,
new_owner: Pubkey,
) -> TransactionResult {
let config = 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::TransferAccount {
governance_authority: governance_authority.pubkey(),
config,
stake_account_metadata,
stake_account_positions,
voter_record,
new_owner,
};

let ix = Instruction::new_with_bytes(
staking::ID,
&staking::instruction::TransferAccount {}.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)
}

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)
}
128 changes: 128 additions & 0 deletions staking/integration-tests/tests/transfer_account.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
use {
anchor_lang::error::ErrorCode,
integration_tests::{
assert_anchor_program_error,
setup::{
setup,
SetupProps,
SetupResult,
},
solana::utils::{
fetch_account_data,
fetch_positions_account,
},
staking::{
helper_functions::initialize_new_stake_account,
instructions::{
create_position,
create_voter_record,
transfer_account,
},
pda::{
get_stake_account_metadata_address,
get_voter_record_address,
},
},
},
solana_sdk::{
native_token::LAMPORTS_PER_SOL,
signature::Keypair,
signer::Signer,
},
staking::{
error::ErrorCode as StakingError,
state::{
positions::TargetWithParameters,
stake_account::StakeAccountMetadataV2,
voter_weight_record::VoterWeightRecord,
},
},
};

#[test]
fn test_transfer_account() {
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();
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, &new_owner, stake_account_positions).unwrap();

assert_anchor_program_error!(
transfer_account(
&mut svm,
&owner, // governance_authority has to sign
stake_account_positions,
new_owner.pubkey()
),
ErrorCode::ConstraintHasOne,
0
);

transfer_account(
&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.governing_token_owner, new_owner.pubkey());

// new_owner creates a new position
create_position(
&mut svm,
&new_owner,
stake_account_positions,
TargetWithParameters::Voting,
None,
100,
)
.unwrap();

svm.expire_blockhash();
// now the account can't be recovered
assert_anchor_program_error!(
transfer_account(
&mut svm,
&governance_authority,
stake_account_positions,
new_owner.pubkey()
),
StakingError::RecoverWithStake,
0
);
}
2 changes: 1 addition & 1 deletion staking/programs/staking/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "pyth-staking-program"
version = "2.0.0"
version = "2.1.0"
description = "Created with Anchor"
edition = "2018"

Expand Down
33 changes: 32 additions & 1 deletion staking/programs/staking/src/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -413,7 +413,6 @@ pub struct AdvanceClock<'info> {

#[derive(Accounts)]
pub struct RecoverAccount<'info> {
// Native payer:
pub governance_authority: Signer<'info>,

// Token account:
Expand Down Expand Up @@ -445,6 +444,38 @@ pub struct RecoverAccount<'info> {
pub config: Account<'info, global_config::GlobalConfig>,
}

#[derive(Accounts)]
pub struct TransferAccount<'info> {
pub governance_authority: Signer<'info>,

/// CHECK : A new arbitrary owner 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,
)]
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> {
Expand Down
23 changes: 23 additions & 0 deletions staking/programs/staking/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -794,6 +794,29 @@ pub mod staking {
Ok(())
}

/** 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 transferred.
*/
pub fn transfer_account(ctx: Context<TransferAccount>) -> 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<SlashAccount>,
// a number between 0 and 1 with 6 decimals of precision
Expand Down
Loading
Loading