Skip to content

Commit 5288664

Browse files
authored
solana: Handle transferring mint authority using SPL Multisig (#587)
This PR adds: * 7 mint authority update instructions to handle all SPL Multisig cases * Add NTT namespace TS helpers for the instructions * Update TS test to cover multiple mint authority transfer instances * Refactor `admin.rs` and TS test * Update IDL
1 parent 2d402b9 commit 5288664

File tree

10 files changed

+2391
-919
lines changed

10 files changed

+2391
-919
lines changed

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

-546
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
use anchor_lang::prelude::*;
2+
use ntt_messages::chain_id::ChainId;
3+
4+
use crate::{
5+
config::Config,
6+
peer::NttManagerPeer,
7+
queue::{inbox::InboxRateLimit, outbox::OutboxRateLimit, rate_limit::RateLimitState},
8+
registered_transceiver::RegisteredTransceiver,
9+
};
10+
11+
pub mod transfer_ownership;
12+
pub mod transfer_token_authority;
13+
14+
pub use transfer_ownership::*;
15+
pub use transfer_token_authority::*;
16+
17+
// * Set peers
18+
19+
#[derive(Accounts)]
20+
#[instruction(args: SetPeerArgs)]
21+
pub struct SetPeer<'info> {
22+
#[account(mut)]
23+
pub payer: Signer<'info>,
24+
25+
pub owner: Signer<'info>,
26+
27+
#[account(
28+
has_one = owner,
29+
)]
30+
pub config: Account<'info, Config>,
31+
32+
#[account(
33+
init_if_needed,
34+
space = 8 + NttManagerPeer::INIT_SPACE,
35+
payer = payer,
36+
seeds = [NttManagerPeer::SEED_PREFIX, args.chain_id.id.to_be_bytes().as_ref()],
37+
bump
38+
)]
39+
pub peer: Account<'info, NttManagerPeer>,
40+
41+
#[account(
42+
init_if_needed,
43+
space = 8 + InboxRateLimit::INIT_SPACE,
44+
payer = payer,
45+
seeds = [
46+
InboxRateLimit::SEED_PREFIX,
47+
args.chain_id.id.to_be_bytes().as_ref()
48+
],
49+
bump,
50+
)]
51+
pub inbox_rate_limit: Account<'info, InboxRateLimit>,
52+
53+
pub system_program: Program<'info, System>,
54+
}
55+
56+
#[derive(AnchorDeserialize, AnchorSerialize)]
57+
pub struct SetPeerArgs {
58+
pub chain_id: ChainId,
59+
pub address: [u8; 32],
60+
pub limit: u64,
61+
/// The token decimals on the peer chain.
62+
pub token_decimals: u8,
63+
}
64+
65+
pub fn set_peer(ctx: Context<SetPeer>, args: SetPeerArgs) -> Result<()> {
66+
ctx.accounts.peer.set_inner(NttManagerPeer {
67+
bump: ctx.bumps.peer,
68+
address: args.address,
69+
token_decimals: args.token_decimals,
70+
});
71+
72+
ctx.accounts.inbox_rate_limit.set_inner(InboxRateLimit {
73+
bump: ctx.bumps.inbox_rate_limit,
74+
rate_limit: RateLimitState::new(args.limit),
75+
});
76+
Ok(())
77+
}
78+
79+
// * Register transceivers
80+
81+
#[derive(Accounts)]
82+
pub struct RegisterTransceiver<'info> {
83+
#[account(
84+
mut,
85+
has_one = owner,
86+
)]
87+
pub config: Account<'info, Config>,
88+
89+
pub owner: Signer<'info>,
90+
91+
#[account(mut)]
92+
pub payer: Signer<'info>,
93+
94+
#[account(executable)]
95+
/// CHECK: transceiver is meant to be a transceiver program. Arguably a `Program` constraint could be
96+
/// used here that wraps the Transceiver account type.
97+
pub transceiver: UncheckedAccount<'info>,
98+
99+
#[account(
100+
init,
101+
space = 8 + RegisteredTransceiver::INIT_SPACE,
102+
payer = payer,
103+
seeds = [RegisteredTransceiver::SEED_PREFIX, transceiver.key().as_ref()],
104+
bump
105+
)]
106+
pub registered_transceiver: Account<'info, RegisteredTransceiver>,
107+
108+
pub system_program: Program<'info, System>,
109+
}
110+
111+
pub fn register_transceiver(ctx: Context<RegisterTransceiver>) -> Result<()> {
112+
let id = ctx.accounts.config.next_transceiver_id;
113+
ctx.accounts.config.next_transceiver_id += 1;
114+
ctx.accounts
115+
.registered_transceiver
116+
.set_inner(RegisteredTransceiver {
117+
bump: ctx.bumps.registered_transceiver,
118+
id,
119+
transceiver_address: ctx.accounts.transceiver.key(),
120+
});
121+
122+
ctx.accounts.config.enabled_transceivers.set(id, true)?;
123+
Ok(())
124+
}
125+
126+
// * Limit rate adjustment
127+
128+
#[derive(Accounts)]
129+
pub struct SetOutboundLimit<'info> {
130+
#[account(
131+
has_one = owner,
132+
)]
133+
pub config: Account<'info, Config>,
134+
135+
pub owner: Signer<'info>,
136+
137+
#[account(mut)]
138+
pub rate_limit: Account<'info, OutboxRateLimit>,
139+
}
140+
141+
#[derive(AnchorDeserialize, AnchorSerialize)]
142+
pub struct SetOutboundLimitArgs {
143+
pub limit: u64,
144+
}
145+
146+
pub fn set_outbound_limit(
147+
ctx: Context<SetOutboundLimit>,
148+
args: SetOutboundLimitArgs,
149+
) -> Result<()> {
150+
ctx.accounts.rate_limit.set_limit(args.limit);
151+
Ok(())
152+
}
153+
154+
#[derive(Accounts)]
155+
#[instruction(args: SetInboundLimitArgs)]
156+
pub struct SetInboundLimit<'info> {
157+
#[account(
158+
has_one = owner,
159+
)]
160+
pub config: Account<'info, Config>,
161+
162+
pub owner: Signer<'info>,
163+
164+
#[account(
165+
mut,
166+
seeds = [
167+
InboxRateLimit::SEED_PREFIX,
168+
args.chain_id.id.to_be_bytes().as_ref()
169+
],
170+
bump = rate_limit.bump
171+
)]
172+
pub rate_limit: Account<'info, InboxRateLimit>,
173+
}
174+
175+
#[derive(AnchorDeserialize, AnchorSerialize)]
176+
pub struct SetInboundLimitArgs {
177+
pub limit: u64,
178+
pub chain_id: ChainId,
179+
}
180+
181+
pub fn set_inbound_limit(ctx: Context<SetInboundLimit>, args: SetInboundLimitArgs) -> Result<()> {
182+
ctx.accounts.rate_limit.set_limit(args.limit);
183+
Ok(())
184+
}
185+
186+
// * Pausing
187+
188+
#[derive(Accounts)]
189+
pub struct SetPaused<'info> {
190+
pub owner: Signer<'info>,
191+
192+
#[account(
193+
mut,
194+
has_one = owner,
195+
)]
196+
pub config: Account<'info, Config>,
197+
}
198+
199+
pub fn set_paused(ctx: Context<SetPaused>, paused: bool) -> Result<()> {
200+
ctx.accounts.config.paused = paused;
201+
Ok(())
202+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
use anchor_lang::prelude::*;
2+
use wormhole_solana_utils::cpi::bpf_loader_upgradeable::{self, BpfLoaderUpgradeable};
3+
4+
#[cfg(feature = "idl-build")]
5+
use crate::messages::Hack;
6+
7+
use crate::{config::Config, error::NTTError};
8+
9+
// * Transfer ownership
10+
11+
/// For safety reasons, transferring ownership is a 2-step process. The first step is to set the
12+
/// new owner, and the second step is for the new owner to claim the ownership.
13+
/// This is to prevent a situation where the ownership is transferred to an
14+
/// address that is not able to claim the ownership (by mistake).
15+
///
16+
/// The transfer can be cancelled by the existing owner invoking the [`claim_ownership`]
17+
/// instruction.
18+
///
19+
/// Alternatively, the ownership can be transferred in a single step by calling the
20+
/// [`transfer_ownership_one_step_unchecked`] instruction. This can be dangerous because if the new owner
21+
/// cannot actually sign transactions (due to setting the wrong address), the program will be
22+
/// permanently locked. If the intention is to transfer ownership to a program using this instruction,
23+
/// take extra care to ensure that the owner is a PDA, not the program address itself.
24+
#[derive(Accounts)]
25+
pub struct TransferOwnership<'info> {
26+
#[account(
27+
mut,
28+
has_one = owner,
29+
)]
30+
pub config: Account<'info, Config>,
31+
32+
pub owner: Signer<'info>,
33+
34+
/// CHECK: This account will be the signer in the [claim_ownership] instruction.
35+
new_owner: UncheckedAccount<'info>,
36+
37+
#[account(
38+
seeds = [b"upgrade_lock"],
39+
bump,
40+
)]
41+
/// CHECK: The seeds constraint enforces that this is the correct address
42+
upgrade_lock: UncheckedAccount<'info>,
43+
44+
#[account(
45+
mut,
46+
seeds = [crate::ID.as_ref()],
47+
bump,
48+
seeds::program = bpf_loader_upgradeable_program,
49+
)]
50+
program_data: Account<'info, ProgramData>,
51+
52+
bpf_loader_upgradeable_program: Program<'info, BpfLoaderUpgradeable>,
53+
}
54+
55+
pub fn transfer_ownership(ctx: Context<TransferOwnership>) -> Result<()> {
56+
ctx.accounts.config.pending_owner = Some(ctx.accounts.new_owner.key());
57+
58+
// TODO: only transfer authority when the authority is not already the upgrade lock
59+
bpf_loader_upgradeable::set_upgrade_authority_checked(
60+
CpiContext::new_with_signer(
61+
ctx.accounts
62+
.bpf_loader_upgradeable_program
63+
.to_account_info(),
64+
bpf_loader_upgradeable::SetUpgradeAuthorityChecked {
65+
program_data: ctx.accounts.program_data.to_account_info(),
66+
current_authority: ctx.accounts.owner.to_account_info(),
67+
new_authority: ctx.accounts.upgrade_lock.to_account_info(),
68+
},
69+
&[&[b"upgrade_lock", &[ctx.bumps.upgrade_lock]]],
70+
),
71+
&crate::ID,
72+
)
73+
}
74+
75+
pub fn transfer_ownership_one_step_unchecked(ctx: Context<TransferOwnership>) -> Result<()> {
76+
ctx.accounts.config.pending_owner = None;
77+
ctx.accounts.config.owner = ctx.accounts.new_owner.key();
78+
79+
// NOTE: unlike in `transfer_ownership`, we use the unchecked version of the
80+
// `set_upgrade_authority` instruction here. The checked version requires
81+
// the new owner to be a signer, which is what we want to avoid here.
82+
bpf_loader_upgradeable::set_upgrade_authority(
83+
CpiContext::new(
84+
ctx.accounts
85+
.bpf_loader_upgradeable_program
86+
.to_account_info(),
87+
bpf_loader_upgradeable::SetUpgradeAuthority {
88+
program_data: ctx.accounts.program_data.to_account_info(),
89+
current_authority: ctx.accounts.owner.to_account_info(),
90+
new_authority: Some(ctx.accounts.new_owner.to_account_info()),
91+
},
92+
),
93+
&crate::ID,
94+
)
95+
}
96+
97+
// * Claim ownership
98+
99+
#[derive(Accounts)]
100+
pub struct ClaimOwnership<'info> {
101+
#[account(
102+
mut,
103+
constraint = (
104+
config.pending_owner == Some(new_owner.key())
105+
|| config.owner == new_owner.key()
106+
) @ NTTError::InvalidPendingOwner
107+
)]
108+
pub config: Account<'info, Config>,
109+
110+
#[account(
111+
seeds = [b"upgrade_lock"],
112+
bump,
113+
)]
114+
/// CHECK: The seeds constraint enforces that this is the correct address
115+
upgrade_lock: UncheckedAccount<'info>,
116+
117+
pub new_owner: Signer<'info>,
118+
119+
#[account(
120+
mut,
121+
seeds = [crate::ID.as_ref()],
122+
bump,
123+
seeds::program = bpf_loader_upgradeable_program,
124+
)]
125+
program_data: Account<'info, ProgramData>,
126+
127+
bpf_loader_upgradeable_program: Program<'info, BpfLoaderUpgradeable>,
128+
}
129+
130+
pub fn claim_ownership(ctx: Context<ClaimOwnership>) -> Result<()> {
131+
ctx.accounts.config.pending_owner = None;
132+
ctx.accounts.config.owner = ctx.accounts.new_owner.key();
133+
134+
bpf_loader_upgradeable::set_upgrade_authority_checked(
135+
CpiContext::new_with_signer(
136+
ctx.accounts
137+
.bpf_loader_upgradeable_program
138+
.to_account_info(),
139+
bpf_loader_upgradeable::SetUpgradeAuthorityChecked {
140+
program_data: ctx.accounts.program_data.to_account_info(),
141+
current_authority: ctx.accounts.upgrade_lock.to_account_info(),
142+
new_authority: ctx.accounts.new_owner.to_account_info(),
143+
},
144+
&[&[b"upgrade_lock", &[ctx.bumps.upgrade_lock]]],
145+
),
146+
&crate::ID,
147+
)
148+
}

0 commit comments

Comments
 (0)