diff --git a/solana/programs/example-native-token-transfers/src/bitmap.rs b/solana/programs/example-native-token-transfers/src/bitmap.rs index 572c3b331..b05d11f7c 100644 --- a/solana/programs/example-native-token-transfers/src/bitmap.rs +++ b/solana/programs/example-native-token-transfers/src/bitmap.rs @@ -48,8 +48,11 @@ impl Bitmap { .expect("Bitmap length must not exceed the bounds of u8") } - pub fn len(self) -> usize { - BM::<128>::from_value(self.map).len() + pub fn len(self) -> u8 { + BM::<128>::from_value(self.map) + .len() + .try_into() + .expect("Bitmap length must not exceed the bounds of u8") } pub fn is_empty(self) -> bool { diff --git a/solana/programs/example-native-token-transfers/src/error.rs b/solana/programs/example-native-token-transfers/src/error.rs index 2156b584a..9f1b957c9 100644 --- a/solana/programs/example-native-token-transfers/src/error.rs +++ b/solana/programs/example-native-token-transfers/src/error.rs @@ -61,6 +61,10 @@ pub enum NTTError { IncorrectRentPayer, #[msg("InvalidMultisig")] InvalidMultisig, + #[msg("ThresholdTooHigh")] + ThresholdTooHigh, + #[msg("InvalidTransceiverProgram")] + InvalidTransceiverProgram, } impl From<ScalingError> for NTTError { diff --git a/solana/programs/example-native-token-transfers/src/instructions/admin/mod.rs b/solana/programs/example-native-token-transfers/src/instructions/admin/mod.rs index a8cb12c5e..9229f7b38 100644 --- a/solana/programs/example-native-token-transfers/src/instructions/admin/mod.rs +++ b/solana/programs/example-native-token-transfers/src/instructions/admin/mod.rs @@ -3,6 +3,7 @@ use ntt_messages::chain_id::ChainId; use crate::{ config::Config, + error::NTTError, peer::NttManagerPeer, queue::{inbox::InboxRateLimit, outbox::OutboxRateLimit, rate_limit::RateLimitState}, registered_transceiver::RegisteredTransceiver, @@ -76,7 +77,7 @@ pub fn set_peer(ctx: Context<SetPeer>, args: SetPeerArgs) -> Result<()> { Ok(()) } -// * Register transceivers +// * Transceiver registration #[derive(Accounts)] pub struct RegisterTransceiver<'info> { @@ -91,13 +92,16 @@ pub struct RegisterTransceiver<'info> { #[account(mut)] pub payer: Signer<'info>, - #[account(executable)] + #[account( + executable, + constraint = transceiver.key() != Pubkey::default() @ NTTError::InvalidTransceiverProgram + )] /// CHECK: transceiver is meant to be a transceiver program. Arguably a `Program` constraint could be /// used here that wraps the Transceiver account type. pub transceiver: UncheckedAccount<'info>, #[account( - init, + init_if_needed, space = 8 + RegisteredTransceiver::INIT_SPACE, payer = payer, seeds = [RegisteredTransceiver::SEED_PREFIX, transceiver.key().as_ref()], @@ -109,17 +113,62 @@ pub struct RegisterTransceiver<'info> { } pub fn register_transceiver(ctx: Context<RegisterTransceiver>) -> Result<()> { - let id = ctx.accounts.config.next_transceiver_id; - ctx.accounts.config.next_transceiver_id += 1; + // initialize registered transceiver with new id on init + if ctx.accounts.registered_transceiver.transceiver_address == Pubkey::default() { + let id = ctx.accounts.config.next_transceiver_id; + ctx.accounts.config.next_transceiver_id += 1; + ctx.accounts + .registered_transceiver + .set_inner(RegisteredTransceiver { + bump: ctx.bumps.registered_transceiver, + id, + transceiver_address: ctx.accounts.transceiver.key(), + }); + } + ctx.accounts - .registered_transceiver - .set_inner(RegisteredTransceiver { - bump: ctx.bumps.registered_transceiver, - id, - transceiver_address: ctx.accounts.transceiver.key(), - }); - - ctx.accounts.config.enabled_transceivers.set(id, true)?; + .config + .enabled_transceivers + .set(ctx.accounts.registered_transceiver.id, true)?; + Ok(()) +} + +#[derive(Accounts)] +pub struct DeregisterTransceiver<'info> { + #[account( + mut, + has_one = owner, + )] + pub config: Account<'info, Config>, + + #[account(mut)] + pub owner: Signer<'info>, + + #[account(executable)] + /// CHECK: transceiver is meant to be a transceiver program. Arguably a `Program` constraint could be + /// used here that wraps the Transceiver account type. + pub transceiver: UncheckedAccount<'info>, + + #[account( + seeds = [RegisteredTransceiver::SEED_PREFIX, transceiver.key().as_ref()], + bump, + constraint = config.enabled_transceivers.get(registered_transceiver.id)? @ NTTError::DisabledTransceiver, + )] + pub registered_transceiver: Account<'info, RegisteredTransceiver>, +} + +pub fn deregister_transceiver(ctx: Context<DeregisterTransceiver>) -> Result<()> { + ctx.accounts + .config + .enabled_transceivers + .set(ctx.accounts.registered_transceiver.id, false)?; + + // decrement threshold if too high + let num_enabled_transceivers = ctx.accounts.config.enabled_transceivers.len(); + if num_enabled_transceivers < ctx.accounts.config.threshold { + // threshold should be at least 1 + ctx.accounts.config.threshold = num_enabled_transceivers.max(1); + } Ok(()) } @@ -200,3 +249,26 @@ pub fn set_paused(ctx: Context<SetPaused>, paused: bool) -> Result<()> { ctx.accounts.config.paused = paused; Ok(()) } + +// * Set Threshold + +#[derive(Accounts)] +#[instruction(threshold: u8)] +pub struct SetThreshold<'info> { + pub owner: Signer<'info>, + + #[account( + mut, + has_one = owner, + constraint = threshold <= config.enabled_transceivers.len() @ NTTError::ThresholdTooHigh + )] + pub config: Account<'info, Config>, +} + +pub fn set_threshold(ctx: Context<SetThreshold>, threshold: u8) -> Result<()> { + if threshold == 0 { + return Err(NTTError::ZeroThreshold.into()); + } + ctx.accounts.config.threshold = threshold; + Ok(()) +} diff --git a/solana/programs/example-native-token-transfers/src/instructions/initialize.rs b/solana/programs/example-native-token-transfers/src/instructions/initialize.rs index dfa889bb9..f87896e3d 100644 --- a/solana/programs/example-native-token-transfers/src/instructions/initialize.rs +++ b/solana/programs/example-native-token-transfers/src/instructions/initialize.rs @@ -155,7 +155,7 @@ fn initialize_config_and_rate_limit( pending_owner: None, paused: false, next_transceiver_id: 0, - // NOTE: can't be changed for now + // NOTE: can be changed via `set_threshold` ix threshold: 1, enabled_transceivers: Bitmap::new(), custody: common.custody.key(), diff --git a/solana/programs/example-native-token-transfers/src/lib.rs b/solana/programs/example-native-token-transfers/src/lib.rs index f3aed6dfb..c581a35dd 100644 --- a/solana/programs/example-native-token-transfers/src/lib.rs +++ b/solana/programs/example-native-token-transfers/src/lib.rs @@ -188,6 +188,10 @@ pub mod example_native_token_transfers { instructions::register_transceiver(ctx) } + pub fn deregister_transceiver(ctx: Context<DeregisterTransceiver>) -> Result<()> { + instructions::deregister_transceiver(ctx) + } + pub fn set_outbound_limit( ctx: Context<SetOutboundLimit>, args: SetOutboundLimitArgs, @@ -206,6 +210,10 @@ pub mod example_native_token_transfers { instructions::mark_outbox_item_as_released(ctx) } + pub fn set_threshold(ctx: Context<SetThreshold>, threshold: u8) -> Result<()> { + instructions::set_threshold(ctx, threshold) + } + // standalone transceiver stuff pub fn set_wormhole_peer( diff --git a/solana/programs/example-native-token-transfers/tests/admin.rs b/solana/programs/example-native-token-transfers/tests/admin.rs new file mode 100644 index 000000000..8db1b0eb6 --- /dev/null +++ b/solana/programs/example-native-token-transfers/tests/admin.rs @@ -0,0 +1,222 @@ +#![cfg(feature = "test-sbf")] +#![feature(type_changing_struct_update)] + +use anchor_lang::{prelude::Pubkey, system_program::System, Id}; +use example_native_token_transfers::{ + config::Config, error::NTTError, registered_transceiver::RegisteredTransceiver, +}; +use ntt_messages::mode::Mode; +use solana_program_test::*; +use solana_sdk::{instruction::InstructionError, signer::Signer, transaction::TransactionError}; + +use crate::{ + common::{ + query::GetAccountDataAnchor, + setup::{setup, TestData}, + submit::Submittable, + }, + sdk::instructions::admin::{ + deregister_transceiver, register_transceiver, set_threshold, DeregisterTransceiver, + RegisterTransceiver, SetThreshold, + }, +}; + +pub mod common; +pub mod sdk; + +async fn assert_threshold( + ctx: &mut ProgramTestContext, + test_data: &TestData, + expected_threshold: u8, +) { + let config_account: Config = ctx.get_account_data_anchor(test_data.ntt.config()).await; + assert_eq!(config_account.threshold, expected_threshold); +} + +async fn assert_transceiver_id( + ctx: &mut ProgramTestContext, + test_data: &TestData, + transceiver: &Pubkey, + expected_id: u8, +) { + let registered_transceiver_account: RegisteredTransceiver = ctx + .get_account_data_anchor(test_data.ntt.registered_transceiver(transceiver)) + .await; + assert_eq!( + registered_transceiver_account.transceiver_address, + *transceiver + ); + assert_eq!(registered_transceiver_account.id, expected_id); +} + +#[tokio::test] +async fn test_invalid_transceiver() { + let (mut ctx, test_data) = setup(Mode::Locking).await; + + // try registering system program + let err = register_transceiver( + &test_data.ntt, + RegisterTransceiver { + payer: ctx.payer.pubkey(), + owner: test_data.program_owner.pubkey(), + transceiver: System::id(), + }, + ) + .submit_with_signers(&[&test_data.program_owner], &mut ctx) + .await + .unwrap_err(); + assert_eq!( + err.unwrap(), + TransactionError::InstructionError( + 0, + InstructionError::Custom(NTTError::InvalidTransceiverProgram.into()) + ) + ); +} + +#[tokio::test] +async fn test_reregister_all_transceivers() { + let (mut ctx, test_data) = setup(Mode::Locking).await; + + // Transceivers are expected to be executable which requires them to be added on setup + // Thus, we pass all available executable program IDs as dummy_transceivers + let dummy_transceivers = vec![ + wormhole_anchor_sdk::wormhole::program::ID, + wormhole_governance::ID, + ]; + let num_dummy_transceivers: u8 = dummy_transceivers.len().try_into().unwrap(); + + // register dummy transceivers + for (idx, transceiver) in dummy_transceivers.iter().enumerate() { + register_transceiver( + &test_data.ntt, + RegisterTransceiver { + payer: ctx.payer.pubkey(), + owner: test_data.program_owner.pubkey(), + transceiver: *transceiver, + }, + ) + .submit_with_signers(&[&test_data.program_owner], &mut ctx) + .await + .unwrap(); + assert_transceiver_id(&mut ctx, &test_data, transceiver, idx as u8 + 1).await; + } + + // set threshold = 1 (for baked-in transceiver) + num_dummy_transceivers + set_threshold( + &test_data.ntt, + SetThreshold { + owner: test_data.program_owner.pubkey(), + }, + 1 + num_dummy_transceivers, + ) + .submit_with_signers(&[&test_data.program_owner], &mut ctx) + .await + .unwrap(); + + // deregister dummy transceivers + for (idx, transceiver) in dummy_transceivers.iter().enumerate() { + deregister_transceiver( + &test_data.ntt, + DeregisterTransceiver { + owner: test_data.program_owner.pubkey(), + transceiver: *transceiver, + }, + ) + .submit_with_signers(&[&test_data.program_owner], &mut ctx) + .await + .unwrap(); + assert_threshold(&mut ctx, &test_data, num_dummy_transceivers - idx as u8).await; + } + + // deregister baked-in transceiver + deregister_transceiver( + &test_data.ntt, + DeregisterTransceiver { + owner: test_data.program_owner.pubkey(), + transceiver: example_native_token_transfers::ID, + }, + ) + .submit_with_signers(&[&test_data.program_owner], &mut ctx) + .await + .unwrap(); + assert_threshold(&mut ctx, &test_data, 1).await; + + // reregister dummy transceiver + for (idx, transceiver) in dummy_transceivers.iter().enumerate() { + register_transceiver( + &test_data.ntt, + RegisterTransceiver { + payer: ctx.payer.pubkey(), + owner: test_data.program_owner.pubkey(), + transceiver: *transceiver, + }, + ) + .submit_with_signers(&[&test_data.program_owner], &mut ctx) + .await + .unwrap(); + assert_transceiver_id(&mut ctx, &test_data, transceiver, idx as u8 + 1).await; + assert_threshold(&mut ctx, &test_data, 1).await; + } + + // reregister baked-in transceiver + register_transceiver( + &test_data.ntt, + RegisterTransceiver { + payer: ctx.payer.pubkey(), + owner: test_data.program_owner.pubkey(), + transceiver: example_native_token_transfers::ID, + }, + ) + .submit_with_signers(&[&test_data.program_owner], &mut ctx) + .await + .unwrap(); + assert_transceiver_id(&mut ctx, &test_data, &example_native_token_transfers::ID, 0).await; + assert_threshold(&mut ctx, &test_data, 1).await; +} + +#[tokio::test] +async fn test_zero_threshold() { + let (mut ctx, test_data) = setup(Mode::Locking).await; + + let err = set_threshold( + &test_data.ntt, + SetThreshold { + owner: test_data.program_owner.pubkey(), + }, + 0, + ) + .submit_with_signers(&[&test_data.program_owner], &mut ctx) + .await + .unwrap_err(); + assert_eq!( + err.unwrap(), + TransactionError::InstructionError( + 0, + InstructionError::Custom(NTTError::ZeroThreshold.into()) + ) + ); +} + +#[tokio::test] +async fn test_threshold_too_high() { + let (mut ctx, test_data) = setup(Mode::Burning).await; + + let err = set_threshold( + &test_data.ntt, + SetThreshold { + owner: test_data.program_owner.pubkey(), + }, + 2, + ) + .submit_with_signers(&[&test_data.program_owner], &mut ctx) + .await + .unwrap_err(); + assert_eq!( + err.unwrap(), + TransactionError::InstructionError( + 0, + InstructionError::Custom(NTTError::ThresholdTooHigh.into()) + ) + ); +} diff --git a/solana/programs/example-native-token-transfers/tests/sdk/instructions/admin.rs b/solana/programs/example-native-token-transfers/tests/sdk/instructions/admin.rs index 2f860b439..6ebf892ed 100644 --- a/solana/programs/example-native-token-transfers/tests/sdk/instructions/admin.rs +++ b/solana/programs/example-native-token-transfers/tests/sdk/instructions/admin.rs @@ -73,3 +73,44 @@ pub fn register_transceiver(ntt: &NTT, accounts: RegisterTransceiver) -> Instruc data: data.data(), } } + +pub struct DeregisterTransceiver { + pub owner: Pubkey, + pub transceiver: Pubkey, +} + +pub fn deregister_transceiver(ntt: &NTT, accounts: DeregisterTransceiver) -> Instruction { + let data = example_native_token_transfers::instruction::DeregisterTransceiver {}; + + let accounts = example_native_token_transfers::accounts::DeregisterTransceiver { + config: ntt.config(), + owner: accounts.owner, + transceiver: accounts.transceiver, + registered_transceiver: ntt.registered_transceiver(&accounts.transceiver), + }; + + Instruction { + program_id: ntt.program, + accounts: accounts.to_account_metas(None), + data: data.data(), + } +} + +pub struct SetThreshold { + pub owner: Pubkey, +} + +pub fn set_threshold(ntt: &NTT, accounts: SetThreshold, threshold: u8) -> Instruction { + let data = example_native_token_transfers::instruction::SetThreshold { threshold }; + + let accounts = example_native_token_transfers::accounts::SetThreshold { + config: ntt.config(), + owner: accounts.owner, + }; + + Instruction { + program_id: example_native_token_transfers::ID, + accounts: accounts.to_account_metas(None), + data: data.data(), + } +} diff --git a/solana/ts/idl/3_0_0/json/example_native_token_transfers.json b/solana/ts/idl/3_0_0/json/example_native_token_transfers.json index 457cfd5f8..5c81a0e1a 100644 --- a/solana/ts/idl/3_0_0/json/example_native_token_transfers.json +++ b/solana/ts/idl/3_0_0/json/example_native_token_transfers.json @@ -1350,6 +1350,35 @@ ], "args": [] }, + { + "name": "deregisterTransceiver", + "accounts": [ + { + "name": "config", + "isMut": true, + "isSigner": false + }, + { + "name": "owner", + "isMut": true, + "isSigner": true + }, + { + "name": "transceiver", + "isMut": false, + "isSigner": false, + "docs": [ + "used here that wraps the Transceiver account type." + ] + }, + { + "name": "registeredTransceiver", + "isMut": false, + "isSigner": false + } + ], + "args": [] + }, { "name": "setOutboundLimit", "accounts": [ @@ -1438,6 +1467,27 @@ "args": [], "returns": "bool" }, + { + "name": "setThreshold", + "accounts": [ + { + "name": "owner", + "isMut": false, + "isSigner": true + }, + { + "name": "config", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "threshold", + "type": "u8" + } + ] + }, { "name": "setWormholePeer", "accounts": [ @@ -2587,6 +2637,16 @@ "code": 6027, "name": "InvalidMultisig", "msg": "InvalidMultisig" + }, + { + "code": 6028, + "name": "ThresholdTooHigh", + "msg": "ThresholdTooHigh" + }, + { + "code": 6029, + "name": "InvalidTransceiverProgram", + "msg": "InvalidTransceiverProgram" } ] } diff --git a/solana/ts/idl/3_0_0/ts/example_native_token_transfers.ts b/solana/ts/idl/3_0_0/ts/example_native_token_transfers.ts index e0305fc6b..f49692f66 100644 --- a/solana/ts/idl/3_0_0/ts/example_native_token_transfers.ts +++ b/solana/ts/idl/3_0_0/ts/example_native_token_transfers.ts @@ -1350,6 +1350,35 @@ export type ExampleNativeTokenTransfers = { ], "args": [] }, + { + "name": "deregisterTransceiver", + "accounts": [ + { + "name": "config", + "isMut": true, + "isSigner": false + }, + { + "name": "owner", + "isMut": true, + "isSigner": true + }, + { + "name": "transceiver", + "isMut": false, + "isSigner": false, + "docs": [ + "used here that wraps the Transceiver account type." + ] + }, + { + "name": "registeredTransceiver", + "isMut": false, + "isSigner": false + } + ], + "args": [] + }, { "name": "setOutboundLimit", "accounts": [ @@ -1438,6 +1467,27 @@ export type ExampleNativeTokenTransfers = { "args": [], "returns": "bool" }, + { + "name": "setThreshold", + "accounts": [ + { + "name": "owner", + "isMut": false, + "isSigner": true + }, + { + "name": "config", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "threshold", + "type": "u8" + } + ] + }, { "name": "setWormholePeer", "accounts": [ @@ -2587,6 +2637,16 @@ export type ExampleNativeTokenTransfers = { "code": 6027, "name": "InvalidMultisig", "msg": "InvalidMultisig" + }, + { + "code": 6028, + "name": "ThresholdTooHigh", + "msg": "ThresholdTooHigh" + }, + { + "code": 6029, + "name": "InvalidTransceiverProgram", + "msg": "InvalidTransceiverProgram" } ] } @@ -3942,6 +4002,35 @@ export const IDL: ExampleNativeTokenTransfers = { ], "args": [] }, + { + "name": "deregisterTransceiver", + "accounts": [ + { + "name": "config", + "isMut": true, + "isSigner": false + }, + { + "name": "owner", + "isMut": true, + "isSigner": true + }, + { + "name": "transceiver", + "isMut": false, + "isSigner": false, + "docs": [ + "used here that wraps the Transceiver account type." + ] + }, + { + "name": "registeredTransceiver", + "isMut": false, + "isSigner": false + } + ], + "args": [] + }, { "name": "setOutboundLimit", "accounts": [ @@ -4030,6 +4119,27 @@ export const IDL: ExampleNativeTokenTransfers = { "args": [], "returns": "bool" }, + { + "name": "setThreshold", + "accounts": [ + { + "name": "owner", + "isMut": false, + "isSigner": true + }, + { + "name": "config", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "threshold", + "type": "u8" + } + ] + }, { "name": "setWormholePeer", "accounts": [ @@ -5179,6 +5289,16 @@ export const IDL: ExampleNativeTokenTransfers = { "code": 6027, "name": "InvalidMultisig", "msg": "InvalidMultisig" + }, + { + "code": 6028, + "name": "ThresholdTooHigh", + "msg": "ThresholdTooHigh" + }, + { + "code": 6029, + "name": "InvalidTransceiverProgram", + "msg": "InvalidTransceiverProgram" } ] } diff --git a/solana/ts/lib/ntt.ts b/solana/ts/lib/ntt.ts index 26d0f2d59..67ec69bff 100644 --- a/solana/ts/lib/ntt.ts +++ b/solana/ts/lib/ntt.ts @@ -1103,6 +1103,24 @@ export namespace NTT { .instruction(); } + export async function createSetThresholdInstruction( + program: Program<NttBindings.NativeTokenTransfer<IdlVersion>>, + args: { + owner: PublicKey; + threshold: number; + }, + pdas?: Pdas + ) { + pdas = pdas ?? NTT.pdas(program.programId); + return program.methods + .setThreshold(args.threshold) + .accountsStrict({ + owner: args.owner, + config: pdas.configAccount(), + }) + .instruction(); + } + export async function createSetOutboundLimitInstruction( program: Program<NttBindings.NativeTokenTransfer<IdlVersion>>, args: { diff --git a/solana/ts/sdk/ntt.ts b/solana/ts/sdk/ntt.ts index 5a4577de0..62d78b532 100644 --- a/solana/ts/sdk/ntt.ts +++ b/solana/ts/sdk/ntt.ts @@ -759,6 +759,28 @@ export class SolanaNtt<N extends Network, C extends SolanaChains> .instruction(); } + async createDeregisterTransceiverIx( + ix: number, + owner: web3.PublicKey + ): Promise<web3.TransactionInstruction> { + const transceiver = await this.getTransceiver(ix); + if (!transceiver) { + throw new Error(`Transceiver not found`); + } + const transceiverProgramId = transceiver.programId; + + return this.program.methods + .deregisterTransceiver() + .accountsStrict({ + owner, + config: this.pdas.configAccount(), + transceiver: transceiverProgramId, + registeredTransceiver: + this.pdas.registeredTransceiver(transceiverProgramId), + }) + .instruction(); + } + async *setWormholeTransceiverPeer( peer: ChainAddress, payer: AccountAddress<C>