Skip to content

Commit 02160b8

Browse files
authored
solana: add instruction to transfer ownership in one step (#516)
1 parent 6cc8bee commit 02160b8

File tree

3 files changed

+83
-1
lines changed

3 files changed

+83
-1
lines changed

solana/programs/example-native-token-transfers/src/instructions/admin.rs

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,19 @@ use crate::{
1515

1616
// * Transfer ownership
1717

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

82+
pub fn transfer_ownership_one_step_unchecked(ctx: Context<TransferOwnership>) -> Result<()> {
83+
ctx.accounts.config.pending_owner = None;
84+
ctx.accounts.config.owner = ctx.accounts.new_owner.key();
85+
86+
// NOTE: unlike in `transfer_ownership`, we use the unchecked version of the
87+
// `set_upgrade_authority` instruction here. The checked version requires
88+
// the new owner to be a signer, which is what we want to avoid here.
89+
bpf_loader_upgradeable::set_upgrade_authority(
90+
CpiContext::new(
91+
ctx.accounts
92+
.bpf_loader_upgradeable_program
93+
.to_account_info(),
94+
bpf_loader_upgradeable::SetUpgradeAuthority {
95+
program_data: ctx.accounts.program_data.to_account_info(),
96+
current_authority: ctx.accounts.owner.to_account_info(),
97+
new_authority: Some(ctx.accounts.new_owner.to_account_info()),
98+
},
99+
),
100+
&crate::ID,
101+
)
102+
}
103+
76104
// * Claim ownership
77105

78106
#[derive(Accounts)]

solana/programs/example-native-token-transfers/src/lib.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,10 @@ pub mod example_native_token_transfers {
117117
instructions::transfer_ownership(ctx)
118118
}
119119

120+
pub fn transfer_ownership_one_step_unchecked(ctx: Context<TransferOwnership>) -> Result<()> {
121+
instructions::transfer_ownership_one_step_unchecked(ctx)
122+
}
123+
120124
pub fn claim_ownership(ctx: Context<ClaimOwnership>) -> Result<()> {
121125
instructions::claim_ownership(ctx)
122126
}

solana/programs/example-native-token-transfers/tests/governance.rs

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,9 @@ async fn test_governance() {
102102
data: inner_ix_data.data(),
103103
};
104104

105+
let config_account: Config = ctx.get_account_data_anchor(test_data.ntt.config()).await;
106+
assert!(!config_account.paused); // make sure not paused before
107+
105108
wrap_governance(
106109
&mut ctx,
107110
&test_data.governance,
@@ -131,6 +134,53 @@ async fn test_governance() {
131134
assert!(config_account.paused);
132135
}
133136

137+
#[tokio::test]
138+
async fn test_governance_one_step_transfer() {
139+
let (mut ctx, test_data) = setup(Mode::Locking).await;
140+
141+
let governance_pda = test_data.governance.governance();
142+
143+
// step 1. transfer ownership to governance (1 step)
144+
let ix = example_native_token_transfers::instruction::TransferOwnershipOneStepUnchecked;
145+
146+
let accs = example_native_token_transfers::accounts::TransferOwnership {
147+
config: test_data.ntt.config(),
148+
owner: test_data.program_owner.pubkey(),
149+
new_owner: governance_pda,
150+
upgrade_lock: test_data.ntt.upgrade_lock(),
151+
program_data: test_data.ntt.program_data(),
152+
bpf_loader_upgradeable_program: bpf_loader_upgradeable::id(),
153+
};
154+
155+
Instruction {
156+
program_id: test_data.ntt.program,
157+
accounts: accs.to_account_metas(None),
158+
data: ix.data(),
159+
}
160+
.submit_with_signers(&[&test_data.program_owner], &mut ctx)
161+
.await
162+
.unwrap();
163+
164+
let config_account: Config = ctx.get_account_data_anchor(test_data.ntt.config()).await;
165+
assert!(!config_account.paused); // make sure not paused before
166+
167+
// step 2. set paused
168+
wrap_governance(
169+
&mut ctx,
170+
&test_data.governance,
171+
&test_data.ntt.wormhole,
172+
set_paused(&test_data.ntt, SetPaused { owner: OWNER }, true),
173+
None,
174+
None,
175+
None,
176+
)
177+
.await
178+
.unwrap();
179+
180+
let config_account: Config = ctx.get_account_data_anchor(test_data.ntt.config()).await;
181+
assert!(config_account.paused);
182+
}
183+
134184
#[tokio::test]
135185
async fn test_governance_bad_emitter() {
136186
let (mut ctx, test_data) = setup(Mode::Locking).await;

0 commit comments

Comments
 (0)