Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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` |
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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);

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use crate::{
authorize::authorize,
error::DoubleZeroError,
helper::is_private_or_link_local,
pda::*,
Expand All @@ -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;
Expand Down Expand Up @@ -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!(
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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;

Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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)?;

Expand Down
Original file line number Diff line number Diff line change
@@ -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::*;
Expand Down Expand Up @@ -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;
Expand Down
Loading
Loading