diff --git a/CHANGELOG.md b/CHANGELOG.md index ec5dbd5838..9068776333 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ All notable changes to this project will be documented in this file. ### Changes - Serviceability + - Require a Permission account (or the legacy foundation authority) for GlobalState, GlobalConfig, and foundation/QA allowlist admin instructions, gated on `GLOBALSTATE_ADMIN` via `authorize()`. (#3977) - Gate Contributor instructions (create/update/suspend/resume/delete) on `CONTRIBUTOR_ADMIN` or foundation; the contributor owner retains the ops-manager-only update path. (#3978) - Gate Location and Exchange instructions (create/update/suspend/resume/delete, exchange setdevice) on `INFRA_ADMIN` or foundation via `authorize()`. (#3979) - Collector diff --git a/smartcontract/programs/doublezero-serviceability/PERMISSION.md b/smartcontract/programs/doublezero-serviceability/PERMISSION.md index 09f44e04b5..809eff085e 100644 --- a/smartcontract/programs/doublezero-serviceability/PERMISSION.md +++ b/smartcontract/programs/doublezero-serviceability/PERMISSION.md @@ -94,11 +94,11 @@ Falls back to `GlobalState` fields: | `SENTINEL` | `sentinel_authority_pk == payer` | | `HEALTH_ORACLE` | `health_oracle_pk == payer` | | `FEED_AUTHORITY` | `feed_authority_pk == payer` | -| `USER_ADMIN` | `foundation_allowlist` OR `activator_authority_pk` | -| `ACCESS_PASS_ADMIN` | `foundation_allowlist` OR `sentinel_authority_pk` | -| `NETWORK_ADMIN` | `foundation_allowlist` OR `activator_authority_pk` | +| `USER_ADMIN` | `foundation_allowlist` | +| `ACCESS_PASS_ADMIN` | `foundation_allowlist` OR `sentinel_authority_pk` OR `feed_authority_pk` | +| `NETWORK_ADMIN` | `foundation_allowlist` | | `TENANT_ADMIN` | `foundation_allowlist` OR `sentinel_authority_pk` | -| `MULTICAST_ADMIN` | `foundation_allowlist` OR `activator_authority_pk` OR `sentinel_authority_pk` | +| `MULTICAST_ADMIN` | `foundation_allowlist` OR `sentinel_authority_pk` | | `PERMISSION_ADMIN` | `foundation_allowlist` | | `INFRA_ADMIN` | `foundation_allowlist` | | `GLOBALSTATE_ADMIN` | `foundation_allowlist` | diff --git a/smartcontract/programs/doublezero-serviceability/src/processors/allowlist/foundation/add.rs b/smartcontract/programs/doublezero-serviceability/src/processors/allowlist/foundation/add.rs index 15038bc70d..458f82ab49 100644 --- a/smartcontract/programs/doublezero-serviceability/src/processors/allowlist/foundation/add.rs +++ b/smartcontract/programs/doublezero-serviceability/src/processors/allowlist/foundation/add.rs @@ -1,5 +1,8 @@ use crate::{ - error::DoubleZeroError, pda::*, serializer::try_acc_write, state::globalstate::GlobalState, + authorize::authorize, + pda::*, + serializer::try_acc_write, + state::{globalstate::GlobalState, permission::permission_flags}, }; use borsh::BorshSerialize; use borsh_incremental::BorshDeserializeIncremental; @@ -68,11 +71,15 @@ pub fn process_add_foundation_allowlist_globalconfig( "Invalid System Program Account Owner" ); - // Parse the global state account & check if the payer is in the allowlist + // Authorization: GLOBALSTATE_ADMIN (Permission account) or foundation (legacy). let mut globalstate = GlobalState::try_from(globalstate_account)?; - if !globalstate.foundation_allowlist.contains(payer_account.key) { - return Err(DoubleZeroError::NotAllowed.into()); - } + authorize( + program_id, + accounts_iter, + payer_account.key, + &globalstate, + permission_flags::GLOBALSTATE_ADMIN, + )?; if globalstate.foundation_allowlist.contains(&value.pubkey) { return Err(ProgramError::InvalidArgument); diff --git a/smartcontract/programs/doublezero-serviceability/src/processors/allowlist/foundation/remove.rs b/smartcontract/programs/doublezero-serviceability/src/processors/allowlist/foundation/remove.rs index 85c5050306..0720b94a2f 100644 --- a/smartcontract/programs/doublezero-serviceability/src/processors/allowlist/foundation/remove.rs +++ b/smartcontract/programs/doublezero-serviceability/src/processors/allowlist/foundation/remove.rs @@ -1,5 +1,8 @@ use crate::{ - error::DoubleZeroError, pda::*, serializer::try_acc_write, state::globalstate::GlobalState, + authorize::authorize, + pda::*, + serializer::try_acc_write, + state::{globalstate::GlobalState, permission::permission_flags}, }; use borsh::BorshSerialize; use borsh_incremental::BorshDeserializeIncremental; @@ -70,11 +73,15 @@ pub fn process_remove_foundation_allowlist_globalconfig( "Invalid System Program Account Owner" ); - // Parse the global state account & check if the payer is in the allowlist + // Authorization: GLOBALSTATE_ADMIN (Permission account) or foundation (legacy). let mut globalstate = GlobalState::try_from(globalstate_account)?; - if !globalstate.foundation_allowlist.contains(payer_account.key) { - return Err(DoubleZeroError::NotAllowed.into()); - } + authorize( + program_id, + accounts_iter, + payer_account.key, + &globalstate, + permission_flags::GLOBALSTATE_ADMIN, + )?; assert_ne!( value.pubkey, *payer_account.key, diff --git a/smartcontract/programs/doublezero-serviceability/src/processors/allowlist/qa/add.rs b/smartcontract/programs/doublezero-serviceability/src/processors/allowlist/qa/add.rs index 86fdf4356b..7785f9a192 100644 --- a/smartcontract/programs/doublezero-serviceability/src/processors/allowlist/qa/add.rs +++ b/smartcontract/programs/doublezero-serviceability/src/processors/allowlist/qa/add.rs @@ -1,5 +1,8 @@ use crate::{ - error::DoubleZeroError, pda::*, serializer::try_acc_write, state::globalstate::GlobalState, + authorize::authorize, + pda::*, + serializer::try_acc_write, + state::{globalstate::GlobalState, permission::permission_flags}, }; use borsh::BorshSerialize; use borsh_incremental::BorshDeserializeIncremental; @@ -68,11 +71,15 @@ pub fn process_add_qa_allowlist_globalconfig( "Invalid System Program Account Owner" ); - // Parse the global state account & check if the payer is in the allowlist + // Authorization: GLOBALSTATE_ADMIN (Permission account) or foundation (legacy). let mut globalstate = GlobalState::try_from(globalstate_account)?; - if !globalstate.foundation_allowlist.contains(payer_account.key) { - return Err(DoubleZeroError::NotAllowed.into()); - } + authorize( + program_id, + accounts_iter, + payer_account.key, + &globalstate, + permission_flags::GLOBALSTATE_ADMIN, + )?; if globalstate.qa_allowlist.contains(&value.pubkey) { return Err(ProgramError::InvalidArgument); diff --git a/smartcontract/programs/doublezero-serviceability/src/processors/allowlist/qa/remove.rs b/smartcontract/programs/doublezero-serviceability/src/processors/allowlist/qa/remove.rs index 3d12a3aae3..6f9fd7ca7e 100644 --- a/smartcontract/programs/doublezero-serviceability/src/processors/allowlist/qa/remove.rs +++ b/smartcontract/programs/doublezero-serviceability/src/processors/allowlist/qa/remove.rs @@ -1,5 +1,8 @@ use crate::{ - error::DoubleZeroError, pda::*, serializer::try_acc_write, state::globalstate::GlobalState, + authorize::authorize, + pda::*, + serializer::try_acc_write, + state::{globalstate::GlobalState, permission::permission_flags}, }; use borsh::BorshSerialize; use borsh_incremental::BorshDeserializeIncremental; @@ -67,11 +70,15 @@ pub fn process_remove_qa_allowlist_globalconfig( "Invalid System Program Account Owner" ); - // Parse the global state account & check if the payer is in the allowlist + // Authorization: GLOBALSTATE_ADMIN (Permission account) or foundation (legacy). let mut globalstate = GlobalState::try_from(globalstate_account)?; - if !globalstate.foundation_allowlist.contains(payer_account.key) { - return Err(DoubleZeroError::NotAllowed.into()); - } + authorize( + program_id, + accounts_iter, + payer_account.key, + &globalstate, + permission_flags::GLOBALSTATE_ADMIN, + )?; globalstate.qa_allowlist.retain(|x| x != &value.pubkey); diff --git a/smartcontract/programs/doublezero-serviceability/src/processors/globalconfig/set.rs b/smartcontract/programs/doublezero-serviceability/src/processors/globalconfig/set.rs index 1e8f46a155..79f09b74e0 100644 --- a/smartcontract/programs/doublezero-serviceability/src/processors/globalconfig/set.rs +++ b/smartcontract/programs/doublezero-serviceability/src/processors/globalconfig/set.rs @@ -1,4 +1,5 @@ use crate::{ + authorize::authorize, error::DoubleZeroError, helper::is_private_or_link_local, pda::*, @@ -8,7 +9,7 @@ use crate::{ serializer::{try_acc_create, try_acc_write}, state::{ accounttype::AccountType, exchange::BGP_COMMUNITY_MIN, globalconfig::GlobalConfig, - globalstate::GlobalState, + globalstate::GlobalState, permission::permission_flags, }, }; use borsh::BorshSerialize; @@ -86,10 +87,15 @@ pub fn process_set_globalconfig( "Invalid System Program" ); + // Authorization: GLOBALSTATE_ADMIN (Permission account) or foundation (legacy). let globalstate = GlobalState::try_from(globalstate_account)?; - if !globalstate.foundation_allowlist.contains(payer_account.key) { - return Err(DoubleZeroError::NotAllowed.into()); - } + authorize( + program_id, + accounts_iter, + payer_account.key, + &globalstate, + permission_flags::GLOBALSTATE_ADMIN, + )?; let (expected_pda_account, bump_seed) = get_globalconfig_pda(program_id); assert_eq!( diff --git a/smartcontract/programs/doublezero-serviceability/src/processors/globalstate/setairdrop.rs b/smartcontract/programs/doublezero-serviceability/src/processors/globalstate/setairdrop.rs index 838aeb52b3..dbbe0552b8 100644 --- a/smartcontract/programs/doublezero-serviceability/src/processors/globalstate/setairdrop.rs +++ b/smartcontract/programs/doublezero-serviceability/src/processors/globalstate/setairdrop.rs @@ -1,6 +1,8 @@ use crate::{ - error::DoubleZeroError, pda::get_globalstate_pda, serializer::try_acc_write, - state::globalstate::GlobalState, + authorize::authorize, + pda::get_globalstate_pda, + serializer::try_acc_write, + state::{globalstate::GlobalState, permission::permission_flags}, }; use borsh::BorshSerialize; @@ -64,11 +66,15 @@ pub fn process_set_airdrop( "Invalid GlobalState Pubkey", ); - // Fetch the globalstate and ensure payer authorization to adjust airdrop + // Authorization: GLOBALSTATE_ADMIN (Permission account) or foundation (legacy). let mut globalstate = GlobalState::try_from(globalstate_account)?; - if !globalstate.foundation_allowlist.contains(payer_account.key) { - return Err(DoubleZeroError::NotAllowed.into()); - } + authorize( + program_id, + accounts_iter, + payer_account.key, + &globalstate, + permission_flags::GLOBALSTATE_ADMIN, + )?; if let Some(contributor_airdrop_lamports) = value.contributor_airdrop_lamports { globalstate.contributor_airdrop_lamports = contributor_airdrop_lamports; diff --git a/smartcontract/programs/doublezero-serviceability/src/processors/globalstate/setauthority.rs b/smartcontract/programs/doublezero-serviceability/src/processors/globalstate/setauthority.rs index 8493de3cf2..5e404c8379 100644 --- a/smartcontract/programs/doublezero-serviceability/src/processors/globalstate/setauthority.rs +++ b/smartcontract/programs/doublezero-serviceability/src/processors/globalstate/setauthority.rs @@ -1,5 +1,8 @@ use crate::{ - error::DoubleZeroError, pda::*, serializer::try_acc_write, state::globalstate::GlobalState, + authorize::authorize, + pda::*, + serializer::try_acc_write, + state::{globalstate::GlobalState, permission::permission_flags}, }; use borsh::BorshSerialize; use borsh_incremental::BorshDeserializeIncremental; @@ -69,11 +72,15 @@ pub fn process_set_authority( "Invalid GlobalState PubKey" ); - // Parse the global state account & check if the payer is in the allowlist + // Authorization: GLOBALSTATE_ADMIN (Permission account) or foundation (legacy). let mut globalstate = GlobalState::try_from(globalstate_account)?; - if !globalstate.foundation_allowlist.contains(payer_account.key) { - return Err(DoubleZeroError::NotAllowed.into()); - } + authorize( + program_id, + accounts_iter, + payer_account.key, + &globalstate, + permission_flags::GLOBALSTATE_ADMIN, + )?; if let Some(activator_authority_pk) = value.activator_authority_pk { globalstate.activator_authority_pk = activator_authority_pk; diff --git a/smartcontract/programs/doublezero-serviceability/src/processors/globalstate/setfeatureflags.rs b/smartcontract/programs/doublezero-serviceability/src/processors/globalstate/setfeatureflags.rs index a297c9a2d5..7ff0fabc8a 100644 --- a/smartcontract/programs/doublezero-serviceability/src/processors/globalstate/setfeatureflags.rs +++ b/smartcontract/programs/doublezero-serviceability/src/processors/globalstate/setfeatureflags.rs @@ -1,6 +1,8 @@ use crate::{ - error::DoubleZeroError, pda::get_globalstate_pda, serializer::try_acc_write, - state::globalstate::GlobalState, + authorize::authorize, + pda::get_globalstate_pda, + serializer::try_acc_write, + state::{globalstate::GlobalState, permission::permission_flags}, }; use borsh::BorshSerialize; @@ -59,11 +61,15 @@ pub fn process_set_feature_flags( "Invalid GlobalState Pubkey", ); - // Fetch the globalstate and ensure payer authorization + // Authorization: GLOBALSTATE_ADMIN (Permission account) or foundation (legacy). let mut globalstate = GlobalState::try_from(globalstate_account)?; - if !globalstate.foundation_allowlist.contains(payer_account.key) { - return Err(DoubleZeroError::NotAllowed.into()); - } + authorize( + program_id, + accounts_iter, + payer_account.key, + &globalstate, + permission_flags::GLOBALSTATE_ADMIN, + )?; globalstate.feature_flags = value.feature_flags; diff --git a/smartcontract/programs/doublezero-serviceability/src/processors/globalstate/setversion.rs b/smartcontract/programs/doublezero-serviceability/src/processors/globalstate/setversion.rs index 42b70272d0..5761908062 100644 --- a/smartcontract/programs/doublezero-serviceability/src/processors/globalstate/setversion.rs +++ b/smartcontract/programs/doublezero-serviceability/src/processors/globalstate/setversion.rs @@ -1,9 +1,10 @@ use crate::{ + authorize::authorize, error::DoubleZeroError, pda::*, programversion::ProgramVersion, serializer::try_acc_write, - state::{globalstate::GlobalState, programconfig::ProgramConfig}, + state::{globalstate::GlobalState, permission::permission_flags, programconfig::ProgramConfig}, }; use borsh::BorshSerialize; use borsh_incremental::BorshDeserializeIncremental; @@ -75,11 +76,15 @@ pub fn process_set_version( "Invalid GlobalState PubKey" ); - // Parse the global state account & check if the payer is in the allowlist + // Authorization: GLOBALSTATE_ADMIN (Permission account) or foundation (legacy). let globalstate = GlobalState::try_from(globalstate_account)?; - if !globalstate.foundation_allowlist.contains(payer_account.key) { - return Err(DoubleZeroError::NotAllowed.into()); - } + authorize( + program_id, + accounts_iter, + payer_account.key, + &globalstate, + permission_flags::GLOBALSTATE_ADMIN, + )?; let mut program_config = ProgramConfig::try_from(program_config_account)?; diff --git a/smartcontract/programs/doublezero-serviceability/tests/feature_flags_test.rs b/smartcontract/programs/doublezero-serviceability/tests/feature_flags_test.rs index 8db012d5d9..bc871ef77d 100644 --- a/smartcontract/programs/doublezero-serviceability/tests/feature_flags_test.rs +++ b/smartcontract/programs/doublezero-serviceability/tests/feature_flags_test.rs @@ -1,9 +1,16 @@ use doublezero_serviceability::{ - instructions::*, pda::*, processors::globalstate::setfeatureflags::SetFeatureFlagsArgs, - state::feature_flags::FeatureFlag, + instructions::*, + pda::*, + processors::{ + globalstate::setfeatureflags::SetFeatureFlagsArgs, permission::create::PermissionCreateArgs, + }, + state::{feature_flags::FeatureFlag, permission::permission_flags}, }; use solana_program_test::*; -use solana_sdk::{instruction::AccountMeta, signature::Signer}; +use solana_sdk::{ + instruction::AccountMeta, + signature::{Keypair, Signer}, +}; mod test_helpers; use test_helpers::*; @@ -106,6 +113,75 @@ async fn test_set_feature_flags_non_foundation_fails() { println!("✅ SetFeatureFlags correctly rejected non-foundation member"); } +/// A non-foundation key holding a GLOBALSTATE_ADMIN Permission account can set +/// feature flags — exercises the new Permission-account authorization path. +#[tokio::test] +async fn test_set_feature_flags_with_permission_account_allowed() { + let (mut banks_client, program_id, payer, recent_blockhash) = init_test().await; + + let (program_config_pubkey, _) = get_program_config_pda(&program_id); + let (globalstate_pubkey, _) = get_globalstate_pda(&program_id); + + // Initialize global state (payer is the foundation member). + execute_transaction( + &mut banks_client, + recent_blockhash, + program_id, + DoubleZeroInstruction::InitGlobalState(), + vec![ + AccountMeta::new(program_config_pubkey, false), + AccountMeta::new(globalstate_pubkey, false), + ], + &payer, + ) + .await; + + // A keypair that is NOT in the foundation allowlist. + let gs_admin = Keypair::new(); + transfer(&mut banks_client, &payer, &gs_admin.pubkey(), 10_000_000).await; + + // Foundation grants it a Permission account with GLOBALSTATE_ADMIN. + let (permission_pda, _) = get_permission_pda(&program_id, &gs_admin.pubkey()); + let recent_blockhash = wait_for_new_blockhash(&mut banks_client).await; + execute_transaction( + &mut banks_client, + recent_blockhash, + program_id, + DoubleZeroInstruction::CreatePermission(PermissionCreateArgs { + user_payer: gs_admin.pubkey(), + permissions: permission_flags::GLOBALSTATE_ADMIN, + }), + vec![ + AccountMeta::new(permission_pda, false), + AccountMeta::new_readonly(globalstate_pubkey, false), + ], + &payer, + ) + .await; + + // The GLOBALSTATE_ADMIN holder sets feature flags, passing its Permission PDA + // as the optional trailing account that authorize() reads. + let feature_flags = FeatureFlag::OnChainAllocationDeprecated.to_mask(); + let recent_blockhash = wait_for_new_blockhash(&mut banks_client).await; + let mut tx = create_transaction_with_extra_accounts( + program_id, + &DoubleZeroInstruction::SetFeatureFlags(SetFeatureFlagsArgs { feature_flags }), + &vec![AccountMeta::new(globalstate_pubkey, false)], + &gs_admin, + &[AccountMeta::new_readonly(permission_pda, false)], + ); + tx.try_sign(&[&gs_admin], recent_blockhash).unwrap(); + banks_client + .process_transaction(tx) + .await + .expect("GLOBALSTATE_ADMIN permission holder should be able to set feature flags"); + + let globalstate = get_globalstate(&mut banks_client, globalstate_pubkey).await; + assert_eq!(globalstate.feature_flags, feature_flags); + + println!("✅ SetFeatureFlags with GLOBALSTATE_ADMIN permission succeeded"); +} + #[tokio::test] async fn test_feature_flags_persistence() { let (mut banks_client, program_id, payer, recent_blockhash) = init_test().await; diff --git a/smartcontract/programs/doublezero-serviceability/tests/foundation_allowlist_test.rs b/smartcontract/programs/doublezero-serviceability/tests/foundation_allowlist_test.rs index b4fb06971b..e989de0b3a 100644 --- a/smartcontract/programs/doublezero-serviceability/tests/foundation_allowlist_test.rs +++ b/smartcontract/programs/doublezero-serviceability/tests/foundation_allowlist_test.rs @@ -1,13 +1,20 @@ use doublezero_serviceability::{ instructions::*, pda::*, - processors::allowlist::foundation::{ - add::AddFoundationAllowlistArgs, remove::RemoveFoundationAllowlistArgs, + processors::{ + allowlist::foundation::{ + add::AddFoundationAllowlistArgs, remove::RemoveFoundationAllowlistArgs, + }, + permission::create::PermissionCreateArgs, }, - state::accounttype::AccountType, + state::{accounttype::AccountType, permission::permission_flags}, }; use solana_program_test::*; -use solana_sdk::{instruction::AccountMeta, pubkey::Pubkey}; +use solana_sdk::{ + instruction::AccountMeta, + pubkey::Pubkey, + signature::{Keypair, Signer}, +}; mod test_helpers; use test_helpers::*; @@ -141,3 +148,77 @@ async fn foundation_allowlist_test() { /*****************************************************************************************************************************************************/ println!("🟢🟢🟢 End test_foundation_allowlist 🟢🟢🟢"); } + +/// A non-foundation key holding a GLOBALSTATE_ADMIN Permission account can add to +/// the foundation allowlist — exercises the new Permission-account authorization path. +#[tokio::test] +async fn test_foundation_allowlist_add_with_permission_account_allowed() { + let (mut banks_client, program_id, payer, recent_blockhash) = init_test().await; + + let (program_config_pubkey, _) = get_program_config_pda(&program_id); + let (globalstate_pubkey, _) = get_globalstate_pda(&program_id); + + execute_transaction( + &mut banks_client, + recent_blockhash, + program_id, + DoubleZeroInstruction::InitGlobalState(), + vec![ + AccountMeta::new(program_config_pubkey, false), + AccountMeta::new(globalstate_pubkey, false), + ], + &payer, + ) + .await; + + // A keypair that is NOT in the foundation allowlist. + let gs_admin = Keypair::new(); + transfer(&mut banks_client, &payer, &gs_admin.pubkey(), 10_000_000).await; + + // Foundation grants it a Permission account with GLOBALSTATE_ADMIN. + let (permission_pda, _) = get_permission_pda(&program_id, &gs_admin.pubkey()); + let recent_blockhash = wait_for_new_blockhash(&mut banks_client).await; + execute_transaction( + &mut banks_client, + recent_blockhash, + program_id, + DoubleZeroInstruction::CreatePermission(PermissionCreateArgs { + user_payer: gs_admin.pubkey(), + permissions: permission_flags::GLOBALSTATE_ADMIN, + }), + vec![ + AccountMeta::new(permission_pda, false), + AccountMeta::new_readonly(globalstate_pubkey, false), + ], + &payer, + ) + .await; + + // The GLOBALSTATE_ADMIN holder adds a new member to the foundation allowlist, + // passing its Permission PDA as the optional trailing account authorize() reads. + let new_member = Pubkey::new_unique(); + let recent_blockhash = wait_for_new_blockhash(&mut banks_client).await; + let mut tx = create_transaction_with_extra_accounts( + program_id, + &DoubleZeroInstruction::AddFoundationAllowlist(AddFoundationAllowlistArgs { + pubkey: new_member, + }), + &vec![AccountMeta::new(globalstate_pubkey, false)], + &gs_admin, + &[AccountMeta::new_readonly(permission_pda, false)], + ); + tx.try_sign(&[&gs_admin], recent_blockhash).unwrap(); + banks_client + .process_transaction(tx) + .await + .expect("GLOBALSTATE_ADMIN permission holder should add to foundation allowlist"); + + let state = get_account_data(&mut banks_client, globalstate_pubkey) + .await + .expect("Unable to get Account") + .get_global_state() + .unwrap(); + assert!(state.foundation_allowlist.contains(&new_member)); + + println!("✅ AddFoundationAllowlist with GLOBALSTATE_ADMIN permission succeeded"); +} diff --git a/smartcontract/programs/doublezero-serviceability/tests/globalconfig_test.rs b/smartcontract/programs/doublezero-serviceability/tests/globalconfig_test.rs index 5f90bb23b0..b12db4eb90 100644 --- a/smartcontract/programs/doublezero-serviceability/tests/globalconfig_test.rs +++ b/smartcontract/programs/doublezero-serviceability/tests/globalconfig_test.rs @@ -4,13 +4,20 @@ //! rejected with `DoubleZeroError::InvalidDeviceTunnelBlock` (custom code 88). use doublezero_serviceability::{ - error::DoubleZeroError, instructions::DoubleZeroInstruction, pda::*, - processors::globalconfig::set::SetGlobalConfigArgs, resource::ResourceType, + error::DoubleZeroError, + instructions::DoubleZeroInstruction, + pda::*, + processors::{ + globalconfig::set::SetGlobalConfigArgs, permission::create::PermissionCreateArgs, + }, + resource::ResourceType, + state::permission::permission_flags, }; use solana_program::program_error::ProgramError; use solana_program_test::*; use solana_sdk::{ instruction::{AccountMeta, InstructionError}, + signature::{Keypair, Signer}, transaction::TransactionError, }; @@ -127,3 +134,103 @@ async fn test_set_globalconfig_accepts_link_local_device_tunnel_block() { "link-local device_tunnel_block should be accepted: {result:?}" ); } + +/// A non-foundation key holding a GLOBALSTATE_ADMIN Permission account can set the +/// global config — exercises the new Permission-account path on the long +/// (10-account) instruction layout. +#[tokio::test] +async fn test_set_globalconfig_with_permission_account_allowed() { + let (mut banks_client, program_id, payer, recent_blockhash) = init_test().await; + + let (program_config_pubkey, _) = get_program_config_pda(&program_id); + let (globalstate_pubkey, _) = get_globalstate_pda(&program_id); + + execute_transaction( + &mut banks_client, + recent_blockhash, + program_id, + DoubleZeroInstruction::InitGlobalState(), + vec![ + AccountMeta::new(program_config_pubkey, false), + AccountMeta::new(globalstate_pubkey, false), + ], + &payer, + ) + .await; + + // A keypair that is NOT in the foundation allowlist. + let gs_admin = Keypair::new(); + transfer(&mut banks_client, &payer, &gs_admin.pubkey(), 1_000_000_000).await; + + // Foundation grants it a Permission account with GLOBALSTATE_ADMIN. + let (permission_pda, _) = get_permission_pda(&program_id, &gs_admin.pubkey()); + let recent_blockhash = wait_for_new_blockhash(&mut banks_client).await; + execute_transaction( + &mut banks_client, + recent_blockhash, + program_id, + DoubleZeroInstruction::CreatePermission(PermissionCreateArgs { + user_payer: gs_admin.pubkey(), + permissions: permission_flags::GLOBALSTATE_ADMIN, + }), + vec![ + AccountMeta::new(permission_pda, false), + AccountMeta::new_readonly(globalstate_pubkey, false), + ], + &payer, + ) + .await; + + let (config_pubkey, _) = get_globalconfig_pda(&program_id); + let (device_tunnel_block_pda, _, _) = + get_resource_extension_pda(&program_id, ResourceType::DeviceTunnelBlock); + let (user_tunnel_block_pda, _, _) = + get_resource_extension_pda(&program_id, ResourceType::UserTunnelBlock); + let (multicastgroup_block_pda, _, _) = + get_resource_extension_pda(&program_id, ResourceType::MulticastGroupBlock); + let (link_ids_pda, _, _) = get_resource_extension_pda(&program_id, ResourceType::LinkIds); + let (segment_routing_ids_pda, _, _) = + get_resource_extension_pda(&program_id, ResourceType::SegmentRoutingIds); + let (multicast_publisher_block_pda, _, _) = + get_resource_extension_pda(&program_id, ResourceType::MulticastPublisherBlock); + let (vrf_ids_pda, _, _) = get_resource_extension_pda(&program_id, ResourceType::VrfIds); + let (admin_group_bits_pda, _, _) = + get_resource_extension_pda(&program_id, ResourceType::AdminGroupBits); + + // The GLOBALSTATE_ADMIN holder sets the global config, passing its Permission + // PDA as the optional trailing account authorize() reads. + let recent_blockhash = wait_for_new_blockhash(&mut banks_client).await; + let mut tx = create_transaction_with_extra_accounts( + program_id, + &DoubleZeroInstruction::SetGlobalConfig(SetGlobalConfigArgs { + local_asn: 65000, + remote_asn: 65001, + device_tunnel_block: "10.100.0.0/24".parse().unwrap(), + user_tunnel_block: "169.254.0.0/24".parse().unwrap(), + multicastgroup_block: "239.0.0.0/24".parse().unwrap(), + multicast_publisher_block: "148.51.120.0/21".parse().unwrap(), + next_bgp_community: None, + }), + &vec![ + AccountMeta::new(config_pubkey, false), + AccountMeta::new(globalstate_pubkey, false), + AccountMeta::new(device_tunnel_block_pda, false), + AccountMeta::new(user_tunnel_block_pda, false), + AccountMeta::new(multicastgroup_block_pda, false), + AccountMeta::new(link_ids_pda, false), + AccountMeta::new(segment_routing_ids_pda, false), + AccountMeta::new(multicast_publisher_block_pda, false), + AccountMeta::new(vrf_ids_pda, false), + AccountMeta::new(admin_group_bits_pda, false), + ], + &gs_admin, + &[AccountMeta::new_readonly(permission_pda, false)], + ); + tx.try_sign(&[&gs_admin], recent_blockhash).unwrap(); + banks_client + .process_transaction(tx) + .await + .expect("GLOBALSTATE_ADMIN permission holder should be able to set global config"); + + println!("✅ SetGlobalConfig with GLOBALSTATE_ADMIN permission succeeded"); +} diff --git a/smartcontract/sdk/rs/src/commands/allowlist/foundation/add.rs b/smartcontract/sdk/rs/src/commands/allowlist/foundation/add.rs index 8ae4f5cee4..549a5b90b6 100644 --- a/smartcontract/sdk/rs/src/commands/allowlist/foundation/add.rs +++ b/smartcontract/sdk/rs/src/commands/allowlist/foundation/add.rs @@ -15,7 +15,7 @@ impl AddFoundationAllowlistCommand { pub fn execute(&self, client: &dyn DoubleZeroClient) -> eyre::Result { let (pda_pubkey, _) = get_globalstate_pda(&client.get_program_id()); - client.execute_transaction( + client.execute_authorized_transaction( DoubleZeroInstruction::AddFoundationAllowlist(AddFoundationAllowlistArgs { pubkey: self.pubkey, }), diff --git a/smartcontract/sdk/rs/src/commands/allowlist/foundation/remove.rs b/smartcontract/sdk/rs/src/commands/allowlist/foundation/remove.rs index d710c6edfb..f735961935 100644 --- a/smartcontract/sdk/rs/src/commands/allowlist/foundation/remove.rs +++ b/smartcontract/sdk/rs/src/commands/allowlist/foundation/remove.rs @@ -15,7 +15,7 @@ impl RemoveFoundationAllowlistCommand { pub fn execute(&self, client: &dyn DoubleZeroClient) -> eyre::Result { let (pda_pubkey, _) = get_globalstate_pda(&client.get_program_id()); - client.execute_transaction( + client.execute_authorized_transaction( DoubleZeroInstruction::RemoveFoundationAllowlist(RemoveFoundationAllowlistArgs { pubkey: self.pubkey, }), diff --git a/smartcontract/sdk/rs/src/commands/allowlist/qa/add.rs b/smartcontract/sdk/rs/src/commands/allowlist/qa/add.rs index 6a2de18e92..a6f48f9a12 100644 --- a/smartcontract/sdk/rs/src/commands/allowlist/qa/add.rs +++ b/smartcontract/sdk/rs/src/commands/allowlist/qa/add.rs @@ -15,7 +15,7 @@ impl AddQaAllowlistCommand { pub fn execute(&self, client: &dyn DoubleZeroClient) -> eyre::Result { let (pda_pubkey, _) = get_globalstate_pda(&client.get_program_id()); - client.execute_transaction( + client.execute_authorized_transaction( DoubleZeroInstruction::AddQaAllowlist(AddQaAllowlistArgs { pubkey: self.pubkey, }), diff --git a/smartcontract/sdk/rs/src/commands/allowlist/qa/remove.rs b/smartcontract/sdk/rs/src/commands/allowlist/qa/remove.rs index 8ed1f6979c..7e998c7ef9 100644 --- a/smartcontract/sdk/rs/src/commands/allowlist/qa/remove.rs +++ b/smartcontract/sdk/rs/src/commands/allowlist/qa/remove.rs @@ -15,7 +15,7 @@ impl RemoveQaAllowlistCommand { pub fn execute(&self, client: &dyn DoubleZeroClient) -> eyre::Result { let (pda_pubkey, _) = get_globalstate_pda(&client.get_program_id()); - client.execute_transaction( + client.execute_authorized_transaction( DoubleZeroInstruction::RemoveQaAllowlist(RemoveQaAllowlistArgs { pubkey: self.pubkey, }), diff --git a/smartcontract/sdk/rs/src/commands/globalconfig/set.rs b/smartcontract/sdk/rs/src/commands/globalconfig/set.rs index 637654f49b..ce7451fe54 100644 --- a/smartcontract/sdk/rs/src/commands/globalconfig/set.rs +++ b/smartcontract/sdk/rs/src/commands/globalconfig/set.rs @@ -49,7 +49,7 @@ impl SetGlobalConfigCommand { let (admin_group_bits_pda, _, _) = get_resource_extension_pda(&client.get_program_id(), ResourceType::AdminGroupBits); - client.execute_transaction( + client.execute_authorized_transaction( DoubleZeroInstruction::SetGlobalConfig(set_config_args), vec![ AccountMeta::new(pda_pubkey, false), @@ -142,7 +142,7 @@ mod tests { .returning(|_| Err(eyre::eyre!("not initialized"))); client - .expect_execute_transaction() + .expect_execute_authorized_transaction() .times(1) .returning(|_, _| Ok(Signature::new_unique())); diff --git a/smartcontract/sdk/rs/src/commands/globalstate/setairdrop.rs b/smartcontract/sdk/rs/src/commands/globalstate/setairdrop.rs index 37acce7350..ef0498fc40 100644 --- a/smartcontract/sdk/rs/src/commands/globalstate/setairdrop.rs +++ b/smartcontract/sdk/rs/src/commands/globalstate/setairdrop.rs @@ -16,7 +16,7 @@ impl SetAirdropCommand { .execute(client) .map_err(|_err| eyre::eyre!("GlobalState not initialized"))?; - client.execute_transaction( + client.execute_authorized_transaction( DoubleZeroInstruction::SetAirdrop(SetAirdropArgs { contributor_airdrop_lamports: self.contributor_airdrop_lamports, user_airdrop_lamports: self.user_airdrop_lamports, @@ -49,7 +49,7 @@ mod tests { let user_airdrop_lamports = Some(40_000); client - .expect_execute_transaction() + .expect_execute_authorized_transaction() .with( predicate::eq(DoubleZeroInstruction::SetAirdrop(SetAirdropArgs { contributor_airdrop_lamports, diff --git a/smartcontract/sdk/rs/src/commands/globalstate/setauthority.rs b/smartcontract/sdk/rs/src/commands/globalstate/setauthority.rs index cd632146cb..bc8ef6ba03 100644 --- a/smartcontract/sdk/rs/src/commands/globalstate/setauthority.rs +++ b/smartcontract/sdk/rs/src/commands/globalstate/setauthority.rs @@ -18,7 +18,7 @@ impl SetAuthorityCommand { .execute(client) .map_err(|_err| eyre::eyre!("Globalstate not initialized"))?; - client.execute_transaction( + client.execute_authorized_transaction( DoubleZeroInstruction::SetAuthority(SetAuthorityArgs { activator_authority_pk: self.activator_authority_pk, sentinel_authority_pk: self.sentinel_authority_pk, @@ -55,7 +55,7 @@ mod tests { let feed_authority_pk = Pubkey::new_unique(); client - .expect_execute_transaction() + .expect_execute_authorized_transaction() .with( predicate::eq(DoubleZeroInstruction::SetAuthority(SetAuthorityArgs { activator_authority_pk: Some(activator_authority_pk), diff --git a/smartcontract/sdk/rs/src/commands/globalstate/setfeatureflags.rs b/smartcontract/sdk/rs/src/commands/globalstate/setfeatureflags.rs index d3a1dd40cc..3dc968078c 100644 --- a/smartcontract/sdk/rs/src/commands/globalstate/setfeatureflags.rs +++ b/smartcontract/sdk/rs/src/commands/globalstate/setfeatureflags.rs @@ -16,7 +16,7 @@ impl SetFeatureFlagsCommand { .execute(client) .map_err(|_err| eyre::eyre!("GlobalState not initialized"))?; - client.execute_transaction( + client.execute_authorized_transaction( DoubleZeroInstruction::SetFeatureFlags(SetFeatureFlagsArgs { feature_flags: self.feature_flags, }), @@ -47,7 +47,7 @@ mod tests { let feature_flags = 1u128; client - .expect_execute_transaction() + .expect_execute_authorized_transaction() .with( predicate::eq(DoubleZeroInstruction::SetFeatureFlags( SetFeatureFlagsArgs { feature_flags }, diff --git a/smartcontract/sdk/rs/src/commands/globalstate/setversion.rs b/smartcontract/sdk/rs/src/commands/globalstate/setversion.rs index bb809251db..97f4ee6ecd 100644 --- a/smartcontract/sdk/rs/src/commands/globalstate/setversion.rs +++ b/smartcontract/sdk/rs/src/commands/globalstate/setversion.rs @@ -16,7 +16,7 @@ impl SetVersionCommand { .execute(client) .map_err(|_err| eyre::eyre!("Globalstate not initialized"))?; - client.execute_transaction( + client.execute_authorized_transaction( DoubleZeroInstruction::SetMinVersion(SetVersionArgs { min_compatible_version: self.min_compatible_version.clone(), }), @@ -45,7 +45,7 @@ mod tests { let (globalstate_pubkey, _globalstate) = get_globalstate_pda(&client.get_program_id()); client - .expect_execute_transaction() + .expect_execute_authorized_transaction() .with( predicate::eq(DoubleZeroInstruction::SetMinVersion(SetVersionArgs { min_compatible_version: "1.0.0".parse().unwrap(),