Skip to content
Open
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
13 changes: 9 additions & 4 deletions interface/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,10 @@ pub use swig_compact_instructions::*;
use swig_state::{
action::{
all::All, all_but_manage_authority::AllButManageAuthority,
manage_authority::ManageAuthority, program::Program, program_all::ProgramAll,
program_curated::ProgramCurated, program_scope::ProgramScope,
sol_destination_limit::SolDestinationLimit, sol_limit::SolLimit,
sol_recurring_destination_limit::SolRecurringDestinationLimit,
external_kill_switch::ExternalKillSwitch, manage_authority::ManageAuthority,
program::Program, program_all::ProgramAll, program_curated::ProgramCurated,
program_scope::ProgramScope, sol_destination_limit::SolDestinationLimit,
sol_limit::SolLimit, sol_recurring_destination_limit::SolRecurringDestinationLimit,
sol_recurring_limit::SolRecurringLimit, stake_all::StakeAll, stake_limit::StakeLimit,
stake_recurring_limit::StakeRecurringLimit, sub_account::SubAccount,
token_destination_limit::TokenDestinationLimit, token_limit::TokenLimit,
Expand Down Expand Up @@ -61,6 +61,7 @@ pub enum ClientAction {
StakeLimit(StakeLimit),
StakeRecurringLimit(StakeRecurringLimit),
StakeAll(StakeAll),
ExternalKillSwitch(ExternalKillSwitch),
}

impl ClientAction {
Expand Down Expand Up @@ -105,6 +106,9 @@ impl ClientAction {
(Permission::StakeRecurringLimit, StakeRecurringLimit::LEN)
},
ClientAction::StakeAll(_) => (Permission::StakeAll, StakeAll::LEN),
ClientAction::ExternalKillSwitch(_) => {
(Permission::ExternalKillSwitch, ExternalKillSwitch::LEN)
},
};
let offset = data.len() as u32;
let header = Action::new(
Expand Down Expand Up @@ -136,6 +140,7 @@ impl ClientAction {
ClientAction::StakeLimit(action) => action.into_bytes(),
ClientAction::StakeRecurringLimit(action) => action.into_bytes(),
ClientAction::StakeAll(action) => action.into_bytes(),
ClientAction::ExternalKillSwitch(action) => action.into_bytes(),
};
data.extend_from_slice(
bytes_res.map_err(|e| anyhow::anyhow!("Failed to serialize action {:?}", e))?,
Expand Down
6 changes: 5 additions & 1 deletion program/src/actions/add_authority_v1.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ use crate::{
accounts::{AddAuthorityV1Accounts, Context},
SwigInstruction,
},
util::validate_external_kill_switch,
};

/// Struct representing the complete add authority instruction data.
Expand Down Expand Up @@ -182,7 +183,10 @@ pub fn add_authority_v1(
if acting_role.is_none() {
return Err(SwigError::InvalidAuthorityNotFoundByRoleId.into());
}
let acting_role = acting_role.unwrap();
let mut acting_role = acting_role.unwrap();

// Validate external kill switch if present
validate_external_kill_switch(&mut acting_role, all_accounts)?;

// Authenticate the caller
let clock = Clock::get()?;
Expand Down
7 changes: 6 additions & 1 deletion program/src/actions/create_session_v1.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ use crate::{
accounts::{Context, CreateSessionV1Accounts},
SwigInstruction,
},
util::validate_external_kill_switch,
};

/// Arguments for creating a new session in a Swig wallet.
Expand Down Expand Up @@ -142,7 +143,11 @@ pub fn create_session_v1(
if role.is_none() {
return Err(SwigError::InvalidAuthorityNotFoundByRoleId.into());
}
let role = role.unwrap();
let mut role = role.unwrap();

// Validate external kill switch if present
validate_external_kill_switch(&mut role, account_infos)?;

let clock = Clock::get()?;
let slot = clock.slot;
if !role.authority.session_based() {
Expand Down
7 changes: 6 additions & 1 deletion program/src/actions/create_sub_account_v1.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ use crate::{
accounts::{Context, CreateSubAccountV1Accounts},
SwigInstruction,
},
util::validate_external_kill_switch,
};

/// Arguments for creating a new sub-account in a Swig wallet.
Expand Down Expand Up @@ -163,7 +164,11 @@ pub fn create_sub_account_v1(
if role_opt.is_none() {
return Err(SwigError::InvalidAuthorityNotFoundByRoleId.into());
}
let role = role_opt.unwrap();
let mut role = role_opt.unwrap();

// Validate external kill switch if present
validate_external_kill_switch(&mut role, &all_accounts)?;

// Authenticate the authority
let clock = Clock::get()?;
let slot = clock.slot;
Expand Down
6 changes: 5 additions & 1 deletion program/src/actions/remove_authority_v1.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ use crate::{
accounts::{Context, RemoveAuthorityV1Accounts},
SwigInstruction,
},
util::validate_external_kill_switch,
};

/// Struct representing the complete remove authority instruction data.
Expand Down Expand Up @@ -167,7 +168,10 @@ pub fn remove_authority_v1(
if acting_role.is_none() {
return Err(SwigError::InvalidAuthorityNotFoundByRoleId.into());
}
let acting_role = acting_role.unwrap();
let mut acting_role = acting_role.unwrap();

// Validate external kill switch if present
validate_external_kill_switch(&mut acting_role, all_accounts)?;

// Authenticate the caller
let clock = Clock::get()?;
Expand Down
8 changes: 6 additions & 2 deletions program/src/actions/sign_v1.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ use swig_state::{
action::{
all::All,
all_but_manage_authority::AllButManageAuthority,
external_kill_switch::ExternalKillSwitch,
program::Program,
program_all::ProgramAll,
program_curated::ProgramCurated,
Expand Down Expand Up @@ -49,7 +50,7 @@ use crate::{
accounts::{Context, SignV1Accounts},
SwigInstruction,
},
util::{build_restricted_keys, hash_except},
util::{build_restricted_keys, hash_except, validate_external_kill_switch},
AccountClassification, SPL_TOKEN_2022_ID, SPL_TOKEN_ID, SYSTEM_PROGRAM_ID,
};
// use swig_instructions::InstructionIterator;
Expand Down Expand Up @@ -204,7 +205,7 @@ pub fn sign_v1(
if role.is_none() {
return Err(SwigError::InvalidAuthorityNotFoundByRoleId.into());
}
let role = role.unwrap();
let mut role = role.unwrap();
let clock = Clock::get()?;
let slot = clock.slot;
if role.authority.session_based() {
Expand Down Expand Up @@ -247,6 +248,9 @@ pub fn sign_v1(
let seeds = swig_account_signer(&swig.id, &b);
let signer = seeds.as_slice();

// Validate external kill switch if present
validate_external_kill_switch(&mut role, all_accounts)?;

// Check if we have All or AllButManageAuthority permission to skip CPI
// validation
let has_all_permission = RoleMut::get_action_mut::<All>(role.actions, &[])?.is_some()
Expand Down
9 changes: 7 additions & 2 deletions program/src/actions/sign_v2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ use crate::{
accounts::{Context, SignV2Accounts},
SwigInstruction,
},
util::hash_except,
util::{hash_except, validate_external_kill_switch},
AccountClassification, SPL_TOKEN_2022_ID, SPL_TOKEN_ID, SYSTEM_PROGRAM_ID,
};
// use swig_instructions::InstructionIterator;
Expand Down Expand Up @@ -209,9 +209,10 @@ pub fn sign_v2(
if role.is_none() {
return Err(SwigError::InvalidAuthorityNotFoundByRoleId.into());
}
let role = role.unwrap();
let mut role = role.unwrap();
let clock = Clock::get()?;
let slot = clock.slot;

if role.authority.session_based() {
role.authority.authenticate_session(
all_accounts,
Expand All @@ -227,6 +228,10 @@ pub fn sign_v2(
slot,
)?;
}

// Validate external kill switch if present
validate_external_kill_switch(&mut role, all_accounts)?;

let rkeys: &[&Pubkey] = &[];
let ix_iter = InstructionIterator::new(
all_accounts,
Expand Down
7 changes: 5 additions & 2 deletions program/src/actions/sub_account_sign_v1.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ use crate::{
accounts::{Context, SubAccountSignV1Accounts},
SwigInstruction,
},
util::build_restricted_keys,
util::{build_restricted_keys, validate_external_kill_switch},
AccountClassification,
};

Expand Down Expand Up @@ -147,11 +147,14 @@ pub fn sub_account_sign_v1(
return Err(SwigError::InvalidAuthorityNotFoundByRoleId.into());
}

let role = role_opt.unwrap();
let mut role = role_opt.unwrap();

// Store authority info before authentication (to avoid borrow checker issues)
let is_session_based = role.authority.session_based();

// Validate external kill switch if present
validate_external_kill_switch(&mut role, all_accounts)?;

let clock = Clock::get()?;
let slot = clock.slot;

Expand Down
8 changes: 8 additions & 0 deletions program/src/actions/toggle_sub_account_v1.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ use crate::{
accounts::{Context, ToggleSubAccountV1Accounts},
SwigInstruction,
},
util::validate_external_kill_switch,
};

/// Arguments for toggling a sub-account's enabled state.
Expand Down Expand Up @@ -165,6 +166,10 @@ pub fn toggle_sub_account_v1(
return Err(SwigError::InvalidAuthorityNotFoundByRoleId.into());
}
let mut role = role_opt.unwrap();

// Validate external kill switch if present
validate_external_kill_switch(&mut role, all_accounts)?;

authenticate_authority(&mut role, all_accounts, &toggle_sub_account)?;
let all_action = role.get_action::<All>(&[])?;
let manage_authority_action = role.get_action::<ManageAuthority>(&[])?;
Expand Down Expand Up @@ -194,6 +199,9 @@ pub fn toggle_sub_account_v1(
}
let mut sub_account_role = sub_acc_role_opt.unwrap();

// Validate external kill switch if present
validate_external_kill_switch(&mut sub_account_role, all_accounts)?;

authenticate_authority(&mut sub_account_role, all_accounts, &toggle_sub_account)?;

let sub_account_action_mut = RoleMut::get_action_mut::<SubAccount>(
Expand Down
6 changes: 5 additions & 1 deletion program/src/actions/update_authority_v1.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ use crate::{
accounts::{Context, UpdateAuthorityV1Accounts},
SwigInstruction,
},
util::validate_external_kill_switch,
};

/// Calculates the actual number of actions in the provided actions data.
Expand Down Expand Up @@ -576,7 +577,10 @@ pub fn update_authority_v1(
if acting_role.is_none() {
return Err(SwigError::InvalidAuthorityNotFoundByRoleId.into());
}
let acting_role = acting_role.unwrap();
let mut acting_role = acting_role.unwrap();

// Validate external kill switch if present
validate_external_kill_switch(&mut acting_role, all_accounts)?;

// Authenticate the caller
let clock = Clock::get()?;
Expand Down
7 changes: 5 additions & 2 deletions program/src/actions/withdraw_from_sub_account_v1.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ use crate::{
accounts::{Context, WithdrawFromSubAccountV1Accounts},
SwigInstruction,
},
util::TokenTransfer,
util::{validate_external_kill_switch, TokenTransfer},
AccountClassification, SPL_TOKEN_2022_ID, SPL_TOKEN_ID,
};

Expand Down Expand Up @@ -144,7 +144,10 @@ pub fn withdraw_from_sub_account_v1(
if role_opt.is_none() {
return Err(SwigError::InvalidAuthorityNotFoundByRoleId.into());
}
let role = role_opt.unwrap();
let mut role = role_opt.unwrap();

// Validate external kill switch if present
validate_external_kill_switch(&mut role, all_accounts)?;

// Authenticate the authority
let clock = Clock::get()?;
Expand Down
52 changes: 51 additions & 1 deletion program/src/util/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ use pinocchio::{
};
use swig_state::{
action::{
external_kill_switch::ExternalKillSwitch,
program_scope::{NumericType, ProgramScope},
Action, Permission,
},
Expand All @@ -28,7 +29,7 @@ use swig_state::{
read_numeric_field,
role::RoleMut,
swig::{Swig, SwigWithRoles},
Transmutable,
SwigAuthenticateError, Transmutable,
};

use crate::error::SwigError;
Expand Down Expand Up @@ -415,3 +416,52 @@ pub fn hash_except(

data_payload_hash
}

/// Validates external kill switch for a role if one exists.
///
/// This function checks if the specified role has an external kill switch
/// configured and validates it against the external account. If the kill switch
/// exists and the external account data doesn't match the expected data, it
/// prevents the instruction from executing.
///
/// # Arguments
/// * `role` - The role to check for kill switch actions
/// * `all_accounts` - All accounts in the instruction (external account must be
/// last)
///
/// # Returns
/// * `Result<(), ProgramError>` - Ok if no kill switch or validation passes,
/// Err if blocked
///
/// # Errors
/// Returns error if:
/// * External kill switch is configured but external account data doesn't match
/// expected data
/// * External account is not provided when kill switch is configured
/// * External account key doesn't match the configured external account key
pub fn validate_external_kill_switch(
role: &mut RoleMut,
all_accounts: &[AccountInfo],
) -> Result<(), ProgramError> {
// Check if role has a kill switch action (only one allowed per role)
if let Some(kill_switch) = RoleMut::get_action_mut::<ExternalKillSwitch>(role.actions, &[])? {
// The external account must be the last account in the transaction
if all_accounts.is_empty() {
return Err(SwigAuthenticateError::PermissionDeniedInvalidExternalKillSwitch.into());
}

let last_account_index = all_accounts.len() - 1;
let external_account = unsafe { all_accounts.get_unchecked(last_account_index) };

// Verify the last account matches the expected external account key
if external_account.key().as_ref() != &kill_switch.external_account {
return Err(SwigAuthenticateError::PermissionDeniedInvalidExternalKillSwitch.into());
}

// Validate the external account data
let account_data = unsafe { external_account.borrow_data_unchecked() };
kill_switch.validate_external_account(&account_data)?;
}

Ok(())
}
Loading
Loading