Skip to content

Commit

Permalink
solana: add instruction to transfer ownership in one step (#516)
Browse files Browse the repository at this point in the history
  • Loading branch information
kcsongor authored Sep 13, 2024
1 parent 6cc8bee commit 02160b8
Show file tree
Hide file tree
Showing 3 changed files with 83 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,19 @@ use crate::{

// * Transfer ownership

/// Transferring the ownership is a 2-step process. The first step is to set the
/// For safety reasons, transferring ownership is a 2-step process. The first step is to set the
/// new owner, and the second step is for the new owner to claim the ownership.
/// This is to prevent a situation where the ownership is transferred to an
/// address that is not able to claim the ownership (by mistake).
///
/// The transfer can be cancelled by the existing owner invoking the [`claim_ownership`]
/// instruction.
///
/// Alternatively, the ownership can be transferred in a single step by calling the
/// [`transfer_ownership_one_step_unchecked`] instruction. This can be dangerous because if the new owner
/// cannot actually sign transactions (due to setting the wrong address), the program will be
/// permanently locked. If the intention is to transfer ownership to a program using this instruction,
/// take extra care to ensure that the owner is a PDA, not the program address itself.
#[derive(Accounts)]
pub struct TransferOwnership<'info> {
#[account(
Expand Down Expand Up @@ -73,6 +79,28 @@ pub fn transfer_ownership(ctx: Context<TransferOwnership>) -> Result<()> {
)
}

pub fn transfer_ownership_one_step_unchecked(ctx: Context<TransferOwnership>) -> Result<()> {
ctx.accounts.config.pending_owner = None;
ctx.accounts.config.owner = ctx.accounts.new_owner.key();

// NOTE: unlike in `transfer_ownership`, we use the unchecked version of the
// `set_upgrade_authority` instruction here. The checked version requires
// the new owner to be a signer, which is what we want to avoid here.
bpf_loader_upgradeable::set_upgrade_authority(
CpiContext::new(
ctx.accounts
.bpf_loader_upgradeable_program
.to_account_info(),
bpf_loader_upgradeable::SetUpgradeAuthority {
program_data: ctx.accounts.program_data.to_account_info(),
current_authority: ctx.accounts.owner.to_account_info(),
new_authority: Some(ctx.accounts.new_owner.to_account_info()),
},
),
&crate::ID,
)
}

// * Claim ownership

#[derive(Accounts)]
Expand Down
4 changes: 4 additions & 0 deletions solana/programs/example-native-token-transfers/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,10 @@ pub mod example_native_token_transfers {
instructions::transfer_ownership(ctx)
}

pub fn transfer_ownership_one_step_unchecked(ctx: Context<TransferOwnership>) -> Result<()> {
instructions::transfer_ownership_one_step_unchecked(ctx)
}

pub fn claim_ownership(ctx: Context<ClaimOwnership>) -> Result<()> {
instructions::claim_ownership(ctx)
}
Expand Down
50 changes: 50 additions & 0 deletions solana/programs/example-native-token-transfers/tests/governance.rs
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,9 @@ async fn test_governance() {
data: inner_ix_data.data(),
};

let config_account: Config = ctx.get_account_data_anchor(test_data.ntt.config()).await;
assert!(!config_account.paused); // make sure not paused before

wrap_governance(
&mut ctx,
&test_data.governance,
Expand Down Expand Up @@ -131,6 +134,53 @@ async fn test_governance() {
assert!(config_account.paused);
}

#[tokio::test]
async fn test_governance_one_step_transfer() {
let (mut ctx, test_data) = setup(Mode::Locking).await;

let governance_pda = test_data.governance.governance();

// step 1. transfer ownership to governance (1 step)
let ix = example_native_token_transfers::instruction::TransferOwnershipOneStepUnchecked;

let accs = example_native_token_transfers::accounts::TransferOwnership {
config: test_data.ntt.config(),
owner: test_data.program_owner.pubkey(),
new_owner: governance_pda,
upgrade_lock: test_data.ntt.upgrade_lock(),
program_data: test_data.ntt.program_data(),
bpf_loader_upgradeable_program: bpf_loader_upgradeable::id(),
};

Instruction {
program_id: test_data.ntt.program,
accounts: accs.to_account_metas(None),
data: ix.data(),
}
.submit_with_signers(&[&test_data.program_owner], &mut ctx)
.await
.unwrap();

let config_account: Config = ctx.get_account_data_anchor(test_data.ntt.config()).await;
assert!(!config_account.paused); // make sure not paused before

// step 2. set paused
wrap_governance(
&mut ctx,
&test_data.governance,
&test_data.ntt.wormhole,
set_paused(&test_data.ntt, SetPaused { owner: OWNER }, true),
None,
None,
None,
)
.await
.unwrap();

let config_account: Config = ctx.get_account_data_anchor(test_data.ntt.config()).await;
assert!(config_account.paused);
}

#[tokio::test]
async fn test_governance_bad_emitter() {
let (mut ctx, test_data) = setup(Mode::Locking).await;
Expand Down

0 comments on commit 02160b8

Please sign in to comment.