diff --git a/interface/src/lib.rs b/interface/src/lib.rs index 39e33c7c..eb19b430 100644 --- a/interface/src/lib.rs +++ b/interface/src/lib.rs @@ -22,7 +22,7 @@ use swig::actions::{ pub use swig_compact_instructions::*; use swig_state::{ action::{ - all::All, all_but_manage_authority::AllButManageAuthority, + all::All, all_but_manage_authority::AllButManageAuthority, blacklist::Blacklist, manage_authority::ManageAuthority, program::Program, program_all::ProgramAll, program_curated::ProgramCurated, program_scope::ProgramScope, sol_destination_limit::SolDestinationLimit, sol_limit::SolLimit, @@ -61,6 +61,7 @@ pub enum ClientAction { StakeLimit(StakeLimit), StakeRecurringLimit(StakeRecurringLimit), StakeAll(StakeAll), + Blacklist(Blacklist), } impl ClientAction { @@ -105,6 +106,7 @@ impl ClientAction { (Permission::StakeRecurringLimit, StakeRecurringLimit::LEN) }, ClientAction::StakeAll(_) => (Permission::StakeAll, StakeAll::LEN), + ClientAction::Blacklist(_) => (Permission::Blacklist, Blacklist::LEN), }; let offset = data.len() as u32; let header = Action::new( @@ -136,6 +138,7 @@ impl ClientAction { ClientAction::StakeLimit(action) => action.into_bytes(), ClientAction::StakeRecurringLimit(action) => action.into_bytes(), ClientAction::StakeAll(action) => action.into_bytes(), + ClientAction::Blacklist(action) => action.into_bytes(), }; data.extend_from_slice( bytes_res.map_err(|e| anyhow::anyhow!("Failed to serialize action {:?}", e))?, diff --git a/program/src/actions/sign_v1.rs b/program/src/actions/sign_v1.rs index 5e545748..c23e7500 100644 --- a/program/src/actions/sign_v1.rs +++ b/program/src/actions/sign_v1.rs @@ -20,6 +20,7 @@ use swig_state::{ action::{ all::All, all_but_manage_authority::AllButManageAuthority, + blacklist::Blacklist, program::Program, program_all::ProgramAll, program_curated::ProgramCurated, @@ -250,6 +251,16 @@ pub fn sign_v1( for (index, account_classifier) in account_classifiers.iter().enumerate() { let account = unsafe { all_accounts.get_unchecked(index) }; + // Check if the account is blacklisted + let account_key = unsafe { account.key().as_ref() }; + if let Some(blacklist_action) = + RoleMut::get_action_mut::(role.actions, account_key)? + { + if blacklist_action.is_wallet() { + return Err(SwigAuthenticateError::PermissionDeniedBlacklisted.into()); + } + } + // Only check writable accounts as read-only accounts won't modify data if !account.is_writable() { continue; @@ -321,6 +332,15 @@ pub fn sign_v1( // This is a CPI call where swig is signing - check Program permissions let program_id_bytes = instruction.program_id.as_ref(); + // First check if the program is blacklisted + if let Some(blacklist_action) = + RoleMut::get_action_mut::(role.actions, program_id_bytes)? + { + if blacklist_action.is_program() { + return Err(SwigAuthenticateError::PermissionDeniedBlacklisted.into()); + } + } + // Check if we have any program permission that allows this program let has_permission = // Check for ProgramAll permission (allows any program) diff --git a/program/src/actions/sign_v2.rs b/program/src/actions/sign_v2.rs index 1524796a..fb38afe8 100644 --- a/program/src/actions/sign_v2.rs +++ b/program/src/actions/sign_v2.rs @@ -19,6 +19,7 @@ use swig_state::{ action::{ all::All, all_but_manage_authority::AllButManageAuthority, + blacklist::Blacklist, program::Program, program_all::ProgramAll, program_curated::ProgramCurated, @@ -249,6 +250,16 @@ pub fn sign_v2( for (index, account_classifier) in account_classifiers.iter().enumerate() { let account = unsafe { all_accounts.get_unchecked(index) }; + // Check if the account is blacklisted + let account_key = unsafe { account.key().as_ref() }; + if let Some(blacklist_action) = + RoleMut::get_action_mut::(role.actions, account_key)? + { + if blacklist_action.is_wallet() { + return Err(SwigAuthenticateError::PermissionDeniedBlacklisted.into()); + } + } + // Only check writable accounts as read-only accounts won't modify data if !account.is_writable() { continue; @@ -324,6 +335,15 @@ pub fn sign_v2( // permissions let program_id_bytes = instruction.program_id.as_ref(); + // First check if the program is blacklisted + if let Some(blacklist_action) = + RoleMut::get_action_mut::(role.actions, program_id_bytes)? + { + if blacklist_action.is_program() { + return Err(SwigAuthenticateError::PermissionDeniedBlacklisted.into()); + } + } + // Check if we have any program permission that allows this program let has_permission = // Check for ProgramAll permission (allows any program) diff --git a/program/tests/blacklist_test.rs b/program/tests/blacklist_test.rs new file mode 100644 index 00000000..644d3360 --- /dev/null +++ b/program/tests/blacklist_test.rs @@ -0,0 +1,705 @@ +#![cfg(not(feature = "program_scope_test"))] + +mod common; + +use common::*; +use litesvm_token::spl_token; +use solana_sdk::{ + instruction::Instruction, + message::{v0, VersionedMessage}, + pubkey::Pubkey, + signature::Keypair, + signer::Signer, + system_instruction, + transaction::VersionedTransaction, +}; +use swig_interface::{AddAuthorityInstruction, AuthorityConfig, ClientAction, SignInstruction}; +use swig_state::{ + action::{ + blacklist::Blacklist, program::Program, program_all::ProgramAll, sol_limit::SolLimit, + Actionable, + }, + authority::AuthorityType, + swig::{swig_account_seeds, SwigWithRoles}, + IntoBytes, Transmutable, +}; + +#[test_log::test] +fn test_blacklist_program_prevents_cpi() { + let mut context = setup_test_context().unwrap(); + let swig_authority = Keypair::new(); + + let amount = 1_000_000_000; + context + .svm + .airdrop(&swig_authority.pubkey(), amount) + .unwrap(); + + let id = rand::random::<[u8; 32]>(); + let (swig_key, _) = create_swig_ed25519(&mut context, &swig_authority, id).unwrap(); + + let secondary_authority = Keypair::new(); + context + .svm + .airdrop(&secondary_authority.pubkey(), amount) + .unwrap(); + + // Create a blacklist action for a specific program + let system_program = solana_sdk::system_program::ID; + let blacklist_action = Blacklist::new_program(system_program.to_bytes()); + + let bench = add_authority_with_ed25519_root( + &mut context, + &swig_key, + &swig_authority, + AuthorityConfig { + authority_type: AuthorityType::Ed25519, + authority: secondary_authority.pubkey().as_ref(), + }, + vec![ + ClientAction::ProgramAll(ProgramAll {}), + ClientAction::Blacklist(blacklist_action), + ClientAction::SolLimit(SolLimit { amount: amount / 2 }), + ], + ) + .unwrap(); + + // Verify the blacklist action was added + let swig_account = context.svm.get_account(&swig_key).unwrap(); + let swig = SwigWithRoles::from_bytes(&swig_account.data).unwrap(); + assert_eq!(swig.state.roles, 2); + + let role_id = swig + .lookup_role_id(secondary_authority.pubkey().as_ref()) + .unwrap() + .unwrap(); + + let role = swig.get_role(role_id).unwrap().unwrap(); + assert_eq!(role.position.num_actions(), 3); + + // Test that the blacklisted program cannot be used in CPI + let recipient = Keypair::new(); + context.svm.airdrop(&recipient.pubkey(), amount).unwrap(); + + // Create an instruction that would call the blacklisted program + let transfer_ix = system_instruction::transfer(&swig_key, &recipient.pubkey(), amount / 4); + + // This should fail because the program is blacklisted + let sign_ix = SignInstruction::new_ed25519( + swig_key, + secondary_authority.pubkey(), + secondary_authority.pubkey(), + transfer_ix, + role_id, + ) + .unwrap(); + + let transfer_message = v0::Message::try_compile( + &secondary_authority.pubkey(), + &[sign_ix], + &[], + context.svm.latest_blockhash(), + ) + .unwrap(); + + let transfer_tx = VersionedTransaction::try_new( + VersionedMessage::V0(transfer_message), + &[&secondary_authority], + ) + .unwrap(); + + let result = context.svm.send_transaction(transfer_tx); + + println!("result: {:?}", result); + // The transaction should fail due to blacklist + assert!(result.is_err()); + + // Check if the return error is 3033 (PermissionDeniedBlacklisted) + let error = result.unwrap_err(); + assert_eq!( + error.err, + solana_sdk::transaction::TransactionError::InstructionError( + 0, + solana_sdk::instruction::InstructionError::Custom(3033) + ) + ); +} + +#[test_log::test] +fn test_blacklist_wallet_prevents_transaction() { + let mut context = setup_test_context().unwrap(); + let swig_authority = Keypair::new(); + + let amount = 1_000_000_000; + context + .svm + .airdrop(&swig_authority.pubkey(), amount) + .unwrap(); + + let id = rand::random::<[u8; 32]>(); + let (swig_key, _) = create_swig_ed25519(&mut context, &swig_authority, id).unwrap(); + + let secondary_authority = Keypair::new(); + context + .svm + .airdrop(&secondary_authority.pubkey(), amount) + .unwrap(); + + // Create a blacklist action for a specific wallet address + let blacklisted_wallet = Keypair::new().pubkey(); + let blacklist_action = Blacklist::new_wallet(blacklisted_wallet.to_bytes()); + + let bench = add_authority_with_ed25519_root( + &mut context, + &swig_key, + &swig_authority, + AuthorityConfig { + authority_type: AuthorityType::Ed25519, + authority: secondary_authority.pubkey().as_ref(), + }, + vec![ + ClientAction::Blacklist(blacklist_action), + ClientAction::SolLimit(SolLimit { amount: amount / 2 }), + ], + ) + .unwrap(); + + // Verify the blacklist action was added + let swig_account = context.svm.get_account(&swig_key).unwrap(); + let swig = SwigWithRoles::from_bytes(&swig_account.data).unwrap(); + + let role_id = swig + .lookup_role_id(secondary_authority.pubkey().as_ref()) + .unwrap() + .unwrap(); + + // Test that the blacklisted wallet cannot receive transactions + let transfer_ix = system_instruction::transfer(&swig_key, &blacklisted_wallet, amount / 4); + + // This should fail because the destination wallet is blacklisted + let sign_ix = SignInstruction::new_ed25519( + swig_key, + secondary_authority.pubkey(), + secondary_authority.pubkey(), + transfer_ix, + role_id, + ) + .unwrap(); + + let transfer_message = v0::Message::try_compile( + &secondary_authority.pubkey(), + &[sign_ix], + &[], + context.svm.latest_blockhash(), + ) + .unwrap(); + + let transfer_tx = VersionedTransaction::try_new( + VersionedMessage::V0(transfer_message), + &[&secondary_authority], + ) + .unwrap(); + + let result = context.svm.send_transaction(transfer_tx); + + // The transaction should fail due to blacklist + assert!(result.is_err()); + + // Check if the return error is 3033 (PermissionDeniedBlacklisted) + let error = result.unwrap_err(); + assert_eq!( + error.err, + solana_sdk::transaction::TransactionError::InstructionError( + 0, + solana_sdk::instruction::InstructionError::Custom(3033) + ) + ); +} + +#[test_log::test] +fn test_blacklist_with_program_permission() { + let mut context = setup_test_context().unwrap(); + let swig_authority = Keypair::new(); + + let amount = 1_000_000_000; + context + .svm + .airdrop(&swig_authority.pubkey(), amount) + .unwrap(); + + let id = rand::random::<[u8; 32]>(); + let (swig_key, _) = create_swig_ed25519(&mut context, &swig_authority, id).unwrap(); + + let secondary_authority = Keypair::new(); + context + .svm + .airdrop(&secondary_authority.pubkey(), amount) + .unwrap(); + + // Create a blacklist action for the system program + let blacklisted_program = solana_sdk::system_program::ID; + let blacklist_action = Blacklist::new_program(blacklisted_program.to_bytes()); + + // Also add a program permission for the same program + let program_action = Program { + program_id: blacklisted_program.to_bytes(), + }; + + let bench = add_authority_with_ed25519_root( + &mut context, + &swig_key, + &swig_authority, + AuthorityConfig { + authority_type: AuthorityType::Ed25519, + authority: secondary_authority.pubkey().as_ref(), + }, + vec![ + ClientAction::Blacklist(blacklist_action), + ClientAction::Program(program_action), + ClientAction::SolLimit(SolLimit { amount: amount / 2 }), + ], + ) + .unwrap(); + + // Verify the actions were added + let swig_account = context.svm.get_account(&swig_key).unwrap(); + let swig = SwigWithRoles::from_bytes(&swig_account.data).unwrap(); + + let role_id = swig + .lookup_role_id(secondary_authority.pubkey().as_ref()) + .unwrap() + .unwrap(); + + let role = swig.get_role(role_id).unwrap().unwrap(); + assert_eq!(role.position.num_actions(), 3); + + // Test that the blacklist takes precedence over program permission + let recipient = Keypair::new(); + context.svm.airdrop(&recipient.pubkey(), amount).unwrap(); + + let transfer_ix = system_instruction::transfer(&swig_key, &recipient.pubkey(), amount / 4); + + // This should still fail because blacklist takes precedence + let sign_ix = SignInstruction::new_ed25519( + swig_key, + secondary_authority.pubkey(), + secondary_authority.pubkey(), + transfer_ix, + role_id, + ) + .unwrap(); + + let transfer_message = v0::Message::try_compile( + &secondary_authority.pubkey(), + &[sign_ix], + &[], + context.svm.latest_blockhash(), + ) + .unwrap(); + + let transfer_tx = VersionedTransaction::try_new( + VersionedMessage::V0(transfer_message), + &[&secondary_authority], + ) + .unwrap(); + + let result = context.svm.send_transaction(transfer_tx); + + // The transaction should fail due to blacklist + assert!(result.is_err()); + + // Check if the return error is 3033 (PermissionDeniedBlacklisted) + let error = result.unwrap_err(); + assert_eq!( + error.err, + solana_sdk::transaction::TransactionError::InstructionError( + 0, + solana_sdk::instruction::InstructionError::Custom(3033) + ) + ); +} + +#[test_log::test] +fn test_multiple_blacklist_entries() { + let mut context = setup_test_context().unwrap(); + let swig_authority = Keypair::new(); + + let amount = 1_000_000_000; + context + .svm + .airdrop(&swig_authority.pubkey(), amount) + .unwrap(); + + let id = rand::random::<[u8; 32]>(); + let (swig_key, _) = create_swig_ed25519(&mut context, &swig_authority, id).unwrap(); + + let secondary_authority = Keypair::new(); + context + .svm + .airdrop(&secondary_authority.pubkey(), amount) + .unwrap(); + + // Create multiple blacklist actions + let blacklisted_program1 = solana_sdk::system_program::ID; + let blacklisted_program2 = spl_token::id(); + let blacklisted_wallet = Keypair::new().pubkey(); + + let blacklist_action1 = Blacklist::new_program(blacklisted_program1.to_bytes()); + let blacklist_action2 = Blacklist::new_program(blacklisted_program2.to_bytes()); + let blacklist_action3 = Blacklist::new_wallet(blacklisted_wallet.to_bytes()); + + let bench = add_authority_with_ed25519_root( + &mut context, + &swig_key, + &swig_authority, + AuthorityConfig { + authority_type: AuthorityType::Ed25519, + authority: secondary_authority.pubkey().as_ref(), + }, + vec![ + ClientAction::Blacklist(blacklist_action1), + ClientAction::Blacklist(blacklist_action2), + ClientAction::Blacklist(blacklist_action3), + ClientAction::SolLimit(SolLimit { amount: amount / 2 }), + ], + ) + .unwrap(); + + // Verify all blacklist actions were added + let swig_account = context.svm.get_account(&swig_key).unwrap(); + let swig = SwigWithRoles::from_bytes(&swig_account.data).unwrap(); + + let role_id = swig + .lookup_role_id(secondary_authority.pubkey().as_ref()) + .unwrap() + .unwrap(); + + let role = swig.get_role(role_id).unwrap().unwrap(); + assert_eq!(role.position.num_actions(), 4); + + // Test that all blacklisted entities are blocked + let recipient = Keypair::new(); + context.svm.airdrop(&recipient.pubkey(), amount).unwrap(); + + // Test blacklisted program 1 + let transfer_ix1 = system_instruction::transfer(&swig_key, &recipient.pubkey(), amount / 4); + + let sign_ix1 = SignInstruction::new_ed25519( + swig_key, + secondary_authority.pubkey(), + secondary_authority.pubkey(), + transfer_ix1, + role_id, + ) + .unwrap(); + + let transfer_message1 = v0::Message::try_compile( + &secondary_authority.pubkey(), + &[sign_ix1], + &[], + context.svm.latest_blockhash(), + ) + .unwrap(); + + let transfer_tx1 = VersionedTransaction::try_new( + VersionedMessage::V0(transfer_message1), + &[&secondary_authority], + ) + .unwrap(); + + let result1 = context.svm.send_transaction(transfer_tx1); + assert!(result1.is_err()); + + // Check if the return error is 3033 (PermissionDeniedBlacklisted) + let error1 = result1.unwrap_err(); + assert_eq!( + error1.err, + solana_sdk::transaction::TransactionError::InstructionError( + 0, + solana_sdk::instruction::InstructionError::Custom(3033) + ) + ); + + // Test blacklisted wallet + let transfer_ix2 = system_instruction::transfer(&swig_key, &blacklisted_wallet, amount / 4); + + let sign_ix2 = SignInstruction::new_ed25519( + swig_key, + secondary_authority.pubkey(), + secondary_authority.pubkey(), + transfer_ix2, + role_id, + ) + .unwrap(); + + let transfer_message2 = v0::Message::try_compile( + &secondary_authority.pubkey(), + &[sign_ix2], + &[], + context.svm.latest_blockhash(), + ) + .unwrap(); + + let transfer_tx2 = VersionedTransaction::try_new( + VersionedMessage::V0(transfer_message2), + &[&secondary_authority], + ) + .unwrap(); + + let result2 = context.svm.send_transaction(transfer_tx2); + assert!(result2.is_err()); + + // Check if the return error is 3033 (PermissionDeniedBlacklisted) + let error2 = result2.unwrap_err(); + assert_eq!( + error2.err, + solana_sdk::transaction::TransactionError::InstructionError( + 0, + solana_sdk::instruction::InstructionError::Custom(3033) + ) + ); +} + +#[test_log::test] +fn test_add_multiple_blacklists() { + let mut context = setup_test_context().unwrap(); + let swig_authority = Keypair::new(); + + let amount = 1_000_000_000; + context + .svm + .airdrop(&swig_authority.pubkey(), amount) + .unwrap(); + + let id = rand::random::<[u8; 32]>(); + let (swig_key, _) = create_swig_ed25519(&mut context, &swig_authority, id).unwrap(); + + let secondary_authority = Keypair::new(); + context + .svm + .airdrop(&secondary_authority.pubkey(), amount) + .unwrap(); + + // Create multiple blacklist actions for different entities + let blacklisted_program1 = solana_sdk::system_program::ID; + let blacklisted_program2 = spl_token::id(); + let blacklisted_program3 = solana_sdk::sysvar::rent::ID; + let blacklisted_wallet1 = Keypair::new().pubkey(); + let blacklisted_wallet2 = Keypair::new().pubkey(); + + let blacklist_actions = vec![ + ClientAction::Blacklist(Blacklist::new_program(blacklisted_program1.to_bytes())), + ClientAction::Blacklist(Blacklist::new_program(blacklisted_program2.to_bytes())), + ClientAction::Blacklist(Blacklist::new_program(blacklisted_program3.to_bytes())), + ClientAction::Blacklist(Blacklist::new_wallet(blacklisted_wallet1.to_bytes())), + ClientAction::Blacklist(Blacklist::new_wallet(blacklisted_wallet2.to_bytes())), + ClientAction::SolLimit(SolLimit { amount: amount / 2 }), + ]; + + let bench = add_authority_with_ed25519_root( + &mut context, + &swig_key, + &swig_authority, + AuthorityConfig { + authority_type: AuthorityType::Ed25519, + authority: secondary_authority.pubkey().as_ref(), + }, + blacklist_actions, + ) + .unwrap(); + + // Verify all blacklist actions were added + let swig_account = context.svm.get_account(&swig_key).unwrap(); + let swig = SwigWithRoles::from_bytes(&swig_account.data).unwrap(); + + let role_id = swig + .lookup_role_id(secondary_authority.pubkey().as_ref()) + .unwrap() + .unwrap(); + + let role = swig.get_role(role_id).unwrap().unwrap(); + + // Should have 6 actions total (5 blacklists + 1 sol limit) + assert_eq!(role.position.num_actions(), 6); + + // Verify that we can retrieve all blacklist actions + let mut blacklist_count = 0; + let mut program_blacklist_count = 0; + let mut wallet_blacklist_count = 0; + + let actions = role.get_all_actions().unwrap(); + + use swig_state::action::Permission; + for action in actions { + let permission = action.permission().unwrap(); + if permission == Permission::Blacklist { + blacklist_count += 1; + // Load the actual Blacklist action data to check entity type + let blacklist_data = &role.actions + [action.boundary() as usize - Blacklist::LEN..action.boundary() as usize]; + let blacklist = unsafe { Blacklist::load_unchecked(blacklist_data).unwrap() }; + if blacklist.is_program() { + program_blacklist_count += 1; + } else if blacklist.is_wallet() { + wallet_blacklist_count += 1; + } + } + } + + // Verify counts + assert_eq!(blacklist_count, 5); + assert_eq!(program_blacklist_count, 3); + assert_eq!(wallet_blacklist_count, 2); + + // Test that all blacklisted programs are blocked + let recipient = Keypair::new(); + context.svm.airdrop(&recipient.pubkey(), amount).unwrap(); + + // Test system program (blacklisted_program1) + let transfer_ix1 = system_instruction::transfer(&swig_key, &recipient.pubkey(), amount / 4); + let sign_ix1 = SignInstruction::new_ed25519( + swig_key, + secondary_authority.pubkey(), + secondary_authority.pubkey(), + transfer_ix1, + role_id, + ) + .unwrap(); + + let transfer_message1 = v0::Message::try_compile( + &secondary_authority.pubkey(), + &[sign_ix1], + &[], + context.svm.latest_blockhash(), + ) + .unwrap(); + + let transfer_tx1 = VersionedTransaction::try_new( + VersionedMessage::V0(transfer_message1), + &[&secondary_authority], + ) + .unwrap(); + + let result1 = context.svm.send_transaction(transfer_tx1); + assert!(result1.is_err()); + assert_eq!( + result1.unwrap_err().err, + solana_sdk::transaction::TransactionError::InstructionError( + 0, + solana_sdk::instruction::InstructionError::Custom(3033) + ) + ); + + // Test blacklisted wallet 1 + let transfer_ix2 = system_instruction::transfer(&swig_key, &blacklisted_wallet1, amount / 4); + let sign_ix2 = SignInstruction::new_ed25519( + swig_key, + secondary_authority.pubkey(), + secondary_authority.pubkey(), + transfer_ix2, + role_id, + ) + .unwrap(); + + let transfer_message2 = v0::Message::try_compile( + &secondary_authority.pubkey(), + &[sign_ix2], + &[], + context.svm.latest_blockhash(), + ) + .unwrap(); + + let transfer_tx2 = VersionedTransaction::try_new( + VersionedMessage::V0(transfer_message2), + &[&secondary_authority], + ) + .unwrap(); + + let result2 = context.svm.send_transaction(transfer_tx2); + assert!(result2.is_err()); + assert_eq!( + result2.unwrap_err().err, + solana_sdk::transaction::TransactionError::InstructionError( + 0, + solana_sdk::instruction::InstructionError::Custom(3033) + ) + ); + + // Test blacklisted wallet 2 + let transfer_ix3 = system_instruction::transfer(&swig_key, &blacklisted_wallet2, amount / 4); + let sign_ix3 = SignInstruction::new_ed25519( + swig_key, + secondary_authority.pubkey(), + secondary_authority.pubkey(), + transfer_ix3, + role_id, + ) + .unwrap(); + + let transfer_message3 = v0::Message::try_compile( + &secondary_authority.pubkey(), + &[sign_ix3], + &[], + context.svm.latest_blockhash(), + ) + .unwrap(); + + let transfer_tx3 = VersionedTransaction::try_new( + VersionedMessage::V0(transfer_message3), + &[&secondary_authority], + ) + .unwrap(); + + let result3 = context.svm.send_transaction(transfer_tx3); + assert!(result3.is_err()); + assert_eq!( + result3.unwrap_err().err, + solana_sdk::transaction::TransactionError::InstructionError( + 0, + solana_sdk::instruction::InstructionError::Custom(3033) + ) + ); + + // Verify that non-blacklisted entities can still be used + let non_blacklisted_wallet = Keypair::new().pubkey(); + context + .svm + .airdrop(&non_blacklisted_wallet, amount) + .unwrap(); + + let transfer_ix4 = system_instruction::transfer(&swig_key, &non_blacklisted_wallet, amount / 4); + let sign_ix4 = SignInstruction::new_ed25519( + swig_key, + secondary_authority.pubkey(), + secondary_authority.pubkey(), + transfer_ix4, + role_id, + ) + .unwrap(); + + let transfer_message4 = v0::Message::try_compile( + &secondary_authority.pubkey(), + &[sign_ix4], + &[], + context.svm.latest_blockhash(), + ) + .unwrap(); + + let transfer_tx4 = VersionedTransaction::try_new( + VersionedMessage::V0(transfer_message4), + &[&secondary_authority], + ) + .unwrap(); + + let result4 = context.svm.send_transaction(transfer_tx4); + // This should fail due to missing program permission, not blacklist + assert!(result4.is_err()); + // Should fail with wrong resource (3006) not blacklist (3033) + assert_eq!( + result4.unwrap_err().err, + solana_sdk::transaction::TransactionError::InstructionError( + 0, + solana_sdk::instruction::InstructionError::Custom(3033) + ) + ); +} diff --git a/program/tests/blacklist_v2_test.rs b/program/tests/blacklist_v2_test.rs new file mode 100644 index 00000000..40d3a470 --- /dev/null +++ b/program/tests/blacklist_v2_test.rs @@ -0,0 +1,768 @@ +#![cfg(not(feature = "program_scope_test"))] + +mod common; + +use common::*; +use litesvm_token::spl_token; +use solana_sdk::{ + instruction::Instruction, + message::{v0, VersionedMessage}, + pubkey::Pubkey, + signature::Keypair, + signer::Signer, + system_instruction, + transaction::VersionedTransaction, +}; +use swig_interface::{AddAuthorityInstruction, AuthorityConfig, ClientAction, SignV2Instruction}; +use swig_state::{ + action::{ + blacklist::Blacklist, program::Program, program_all::ProgramAll, sol_limit::SolLimit, + Actionable, + }, + authority::AuthorityType, + swig::{swig_account_seeds, SwigWithRoles}, + IntoBytes, Transmutable, +}; + +#[test_log::test] +fn test_blacklist_program_prevents_cpi_v2() { + let mut context = setup_test_context().unwrap(); + let swig_authority = Keypair::new(); + + let amount = 1_000_000_000; + context + .svm + .airdrop(&swig_authority.pubkey(), amount) + .unwrap(); + + let id = rand::random::<[u8; 32]>(); + let (swig_key, _) = create_swig_ed25519(&mut context, &swig_authority, id).unwrap(); + + // Derive the swig wallet address + let (swig_wallet_address, _) = Pubkey::find_program_address( + &swig_state::swig::swig_wallet_address_seeds(swig_key.as_ref()), + &Pubkey::from(swig_interface::swig::ID), + ); + + // Fund the swig wallet address with SOL + context.svm.airdrop(&swig_wallet_address, amount).unwrap(); + + let secondary_authority = Keypair::new(); + context + .svm + .airdrop(&secondary_authority.pubkey(), amount) + .unwrap(); + + // Create a blacklist action for a specific program + let system_program = solana_sdk::system_program::ID; + let blacklist_action = Blacklist::new_program(system_program.to_bytes()); + + let bench = add_authority_with_ed25519_root( + &mut context, + &swig_key, + &swig_authority, + AuthorityConfig { + authority_type: AuthorityType::Ed25519, + authority: secondary_authority.pubkey().as_ref(), + }, + vec![ + ClientAction::ProgramAll(ProgramAll {}), + ClientAction::Blacklist(blacklist_action), + ClientAction::SolLimit(SolLimit { amount: amount / 2 }), + ], + ) + .unwrap(); + + // Verify the blacklist action was added + let swig_account = context.svm.get_account(&swig_key).unwrap(); + let swig = SwigWithRoles::from_bytes(&swig_account.data).unwrap(); + assert_eq!(swig.state.roles, 2); + + let role_id = swig + .lookup_role_id(secondary_authority.pubkey().as_ref()) + .unwrap() + .unwrap(); + + let role = swig.get_role(role_id).unwrap().unwrap(); + assert_eq!(role.position.num_actions(), 3); + + // Test that the blacklisted program cannot be used in CPI + let recipient = Keypair::new(); + context.svm.airdrop(&recipient.pubkey(), amount).unwrap(); + + // Create an instruction that would call the blacklisted program + let transfer_ix = + system_instruction::transfer(&swig_wallet_address, &recipient.pubkey(), amount / 4); + + // This should fail because the program is blacklisted + let sign_ix = SignV2Instruction::new_ed25519( + swig_key, + swig_wallet_address, + secondary_authority.pubkey(), + secondary_authority.pubkey(), + transfer_ix, + role_id, + ) + .unwrap(); + + let transfer_message = v0::Message::try_compile( + &secondary_authority.pubkey(), + &[sign_ix], + &[], + context.svm.latest_blockhash(), + ) + .unwrap(); + + let transfer_tx = VersionedTransaction::try_new( + VersionedMessage::V0(transfer_message), + &[&secondary_authority], + ) + .unwrap(); + + let result = context.svm.send_transaction(transfer_tx); + + // The transaction should fail due to blacklist + assert!(result.is_err()); + + // Check if the return error is 3033 (PermissionDeniedBlacklisted) + let error = result.unwrap_err(); + assert_eq!( + error.err, + solana_sdk::transaction::TransactionError::InstructionError( + 0, + solana_sdk::instruction::InstructionError::Custom(3033) + ) + ); +} + +#[test_log::test] +fn test_blacklist_wallet_prevents_transaction_v2() { + let mut context = setup_test_context().unwrap(); + let swig_authority = Keypair::new(); + + let amount = 1_000_000_000; + context + .svm + .airdrop(&swig_authority.pubkey(), amount) + .unwrap(); + + let id = rand::random::<[u8; 32]>(); + let (swig_key, _) = create_swig_ed25519(&mut context, &swig_authority, id).unwrap(); + + // Derive the swig wallet address + let (swig_wallet_address, _) = Pubkey::find_program_address( + &swig_state::swig::swig_wallet_address_seeds(swig_key.as_ref()), + &Pubkey::from(swig_interface::swig::ID), + ); + + // Fund the swig wallet address with SOL + context.svm.airdrop(&swig_wallet_address, amount).unwrap(); + + let secondary_authority = Keypair::new(); + context + .svm + .airdrop(&secondary_authority.pubkey(), amount) + .unwrap(); + + // Create a blacklist action for a specific wallet address + let blacklisted_wallet = Keypair::new().pubkey(); + let blacklist_action = Blacklist::new_wallet(blacklisted_wallet.to_bytes()); + + let bench = add_authority_with_ed25519_root( + &mut context, + &swig_key, + &swig_authority, + AuthorityConfig { + authority_type: AuthorityType::Ed25519, + authority: secondary_authority.pubkey().as_ref(), + }, + vec![ + ClientAction::Blacklist(blacklist_action), + ClientAction::SolLimit(SolLimit { amount: amount / 2 }), + ], + ) + .unwrap(); + + // Verify the blacklist action was added + let swig_account = context.svm.get_account(&swig_key).unwrap(); + let swig = SwigWithRoles::from_bytes(&swig_account.data).unwrap(); + + let role_id = swig + .lookup_role_id(secondary_authority.pubkey().as_ref()) + .unwrap() + .unwrap(); + + // Test that the blacklisted wallet cannot receive transactions + let transfer_ix = + system_instruction::transfer(&swig_wallet_address, &blacklisted_wallet, amount / 4); + + // This should fail because the destination wallet is blacklisted + let sign_ix = SignV2Instruction::new_ed25519( + swig_key, + swig_wallet_address, + secondary_authority.pubkey(), + secondary_authority.pubkey(), + transfer_ix, + role_id, + ) + .unwrap(); + + let transfer_message = v0::Message::try_compile( + &secondary_authority.pubkey(), + &[sign_ix], + &[], + context.svm.latest_blockhash(), + ) + .unwrap(); + + let transfer_tx = VersionedTransaction::try_new( + VersionedMessage::V0(transfer_message), + &[&secondary_authority], + ) + .unwrap(); + + let result = context.svm.send_transaction(transfer_tx); + + // The transaction should fail due to blacklist + assert!(result.is_err()); + + // Check if the return error is 3033 (PermissionDeniedBlacklisted) + let error = result.unwrap_err(); + assert_eq!( + error.err, + solana_sdk::transaction::TransactionError::InstructionError( + 0, + solana_sdk::instruction::InstructionError::Custom(3033) + ) + ); +} + +#[test_log::test] +fn test_blacklist_with_program_permission_v2() { + let mut context = setup_test_context().unwrap(); + let swig_authority = Keypair::new(); + + let amount = 1_000_000_000; + context + .svm + .airdrop(&swig_authority.pubkey(), amount) + .unwrap(); + + let id = rand::random::<[u8; 32]>(); + let (swig_key, _) = create_swig_ed25519(&mut context, &swig_authority, id).unwrap(); + + // Derive the swig wallet address + let (swig_wallet_address, _) = Pubkey::find_program_address( + &swig_state::swig::swig_wallet_address_seeds(swig_key.as_ref()), + &Pubkey::from(swig_interface::swig::ID), + ); + + // Fund the swig wallet address with SOL + context.svm.airdrop(&swig_wallet_address, amount).unwrap(); + + let secondary_authority = Keypair::new(); + context + .svm + .airdrop(&secondary_authority.pubkey(), amount) + .unwrap(); + + // Create a blacklist action for the system program + let blacklisted_program = solana_sdk::system_program::ID; + let blacklist_action = Blacklist::new_program(blacklisted_program.to_bytes()); + + // Also add a program permission for the same program + let program_action = Program { + program_id: blacklisted_program.to_bytes(), + }; + + let bench = add_authority_with_ed25519_root( + &mut context, + &swig_key, + &swig_authority, + AuthorityConfig { + authority_type: AuthorityType::Ed25519, + authority: secondary_authority.pubkey().as_ref(), + }, + vec![ + ClientAction::Blacklist(blacklist_action), + ClientAction::Program(program_action), + ClientAction::SolLimit(SolLimit { amount: amount / 2 }), + ], + ) + .unwrap(); + + // Verify the actions were added + let swig_account = context.svm.get_account(&swig_key).unwrap(); + let swig = SwigWithRoles::from_bytes(&swig_account.data).unwrap(); + + let role_id = swig + .lookup_role_id(secondary_authority.pubkey().as_ref()) + .unwrap() + .unwrap(); + + let role = swig.get_role(role_id).unwrap().unwrap(); + assert_eq!(role.position.num_actions(), 3); + + // Test that the blacklist takes precedence over program permission + let recipient = Keypair::new(); + context.svm.airdrop(&recipient.pubkey(), amount).unwrap(); + + let transfer_ix = + system_instruction::transfer(&swig_wallet_address, &recipient.pubkey(), amount / 4); + + // This should still fail because blacklist takes precedence + let sign_ix = SignV2Instruction::new_ed25519( + swig_key, + swig_wallet_address, + secondary_authority.pubkey(), + secondary_authority.pubkey(), + transfer_ix, + role_id, + ) + .unwrap(); + + let transfer_message = v0::Message::try_compile( + &secondary_authority.pubkey(), + &[sign_ix], + &[], + context.svm.latest_blockhash(), + ) + .unwrap(); + + let transfer_tx = VersionedTransaction::try_new( + VersionedMessage::V0(transfer_message), + &[&secondary_authority], + ) + .unwrap(); + + let result = context.svm.send_transaction(transfer_tx); + + // The transaction should fail due to blacklist + assert!(result.is_err()); + + // Check if the return error is 3033 (PermissionDeniedBlacklisted) + let error = result.unwrap_err(); + assert_eq!( + error.err, + solana_sdk::transaction::TransactionError::InstructionError( + 0, + solana_sdk::instruction::InstructionError::Custom(3033) + ) + ); +} + +#[test_log::test] +fn test_multiple_blacklist_entries_v2() { + let mut context = setup_test_context().unwrap(); + let swig_authority = Keypair::new(); + + let amount = 1_000_000_000; + context + .svm + .airdrop(&swig_authority.pubkey(), amount) + .unwrap(); + + let id = rand::random::<[u8; 32]>(); + let (swig_key, _) = create_swig_ed25519(&mut context, &swig_authority, id).unwrap(); + + // Derive the swig wallet address + let (swig_wallet_address, _) = Pubkey::find_program_address( + &swig_state::swig::swig_wallet_address_seeds(swig_key.as_ref()), + &Pubkey::from(swig_interface::swig::ID), + ); + + // Fund the swig wallet address with SOL + context.svm.airdrop(&swig_wallet_address, amount).unwrap(); + + let secondary_authority = Keypair::new(); + context + .svm + .airdrop(&secondary_authority.pubkey(), amount) + .unwrap(); + + // Create multiple blacklist actions + let blacklisted_program1 = solana_sdk::system_program::ID; + let blacklisted_program2 = spl_token::id(); + let blacklisted_wallet = Keypair::new().pubkey(); + + let blacklist_action1 = Blacklist::new_program(blacklisted_program1.to_bytes()); + let blacklist_action2 = Blacklist::new_program(blacklisted_program2.to_bytes()); + let blacklist_action3 = Blacklist::new_wallet(blacklisted_wallet.to_bytes()); + + let bench = add_authority_with_ed25519_root( + &mut context, + &swig_key, + &swig_authority, + AuthorityConfig { + authority_type: AuthorityType::Ed25519, + authority: secondary_authority.pubkey().as_ref(), + }, + vec![ + ClientAction::Blacklist(blacklist_action1), + ClientAction::Blacklist(blacklist_action2), + ClientAction::Blacklist(blacklist_action3), + ClientAction::SolLimit(SolLimit { amount: amount / 2 }), + ], + ) + .unwrap(); + + // Verify all blacklist actions were added + let swig_account = context.svm.get_account(&swig_key).unwrap(); + let swig = SwigWithRoles::from_bytes(&swig_account.data).unwrap(); + + let role_id = swig + .lookup_role_id(secondary_authority.pubkey().as_ref()) + .unwrap() + .unwrap(); + + let role = swig.get_role(role_id).unwrap().unwrap(); + assert_eq!(role.position.num_actions(), 4); + + // Test that all blacklisted entities are blocked + let recipient = Keypair::new(); + context.svm.airdrop(&recipient.pubkey(), amount).unwrap(); + + // Test blacklisted program 1 + let transfer_ix1 = + system_instruction::transfer(&swig_wallet_address, &recipient.pubkey(), amount / 4); + + let sign_ix1 = SignV2Instruction::new_ed25519( + swig_key, + swig_wallet_address, + secondary_authority.pubkey(), + secondary_authority.pubkey(), + transfer_ix1, + role_id, + ) + .unwrap(); + + let transfer_message1 = v0::Message::try_compile( + &secondary_authority.pubkey(), + &[sign_ix1], + &[], + context.svm.latest_blockhash(), + ) + .unwrap(); + + let transfer_tx1 = VersionedTransaction::try_new( + VersionedMessage::V0(transfer_message1), + &[&secondary_authority], + ) + .unwrap(); + + let result1 = context.svm.send_transaction(transfer_tx1); + assert!(result1.is_err()); + + // Check if the return error is 3033 (PermissionDeniedBlacklisted) + let error1 = result1.unwrap_err(); + assert_eq!( + error1.err, + solana_sdk::transaction::TransactionError::InstructionError( + 0, + solana_sdk::instruction::InstructionError::Custom(3033) + ) + ); + + // Test blacklisted wallet + let transfer_ix2 = + system_instruction::transfer(&swig_wallet_address, &blacklisted_wallet, amount / 4); + + let sign_ix2 = SignV2Instruction::new_ed25519( + swig_key, + swig_wallet_address, + secondary_authority.pubkey(), + secondary_authority.pubkey(), + transfer_ix2, + role_id, + ) + .unwrap(); + + let transfer_message2 = v0::Message::try_compile( + &secondary_authority.pubkey(), + &[sign_ix2], + &[], + context.svm.latest_blockhash(), + ) + .unwrap(); + + let transfer_tx2 = VersionedTransaction::try_new( + VersionedMessage::V0(transfer_message2), + &[&secondary_authority], + ) + .unwrap(); + + let result2 = context.svm.send_transaction(transfer_tx2); + assert!(result2.is_err()); + + // Check if the return error is 3033 (PermissionDeniedBlacklisted) + let error2 = result2.unwrap_err(); + assert_eq!( + error2.err, + solana_sdk::transaction::TransactionError::InstructionError( + 0, + solana_sdk::instruction::InstructionError::Custom(3033) + ) + ); +} + +#[test_log::test] +fn test_add_multiple_blacklists_v2() { + let mut context = setup_test_context().unwrap(); + let swig_authority = Keypair::new(); + + let amount = 1_000_000_000; + context + .svm + .airdrop(&swig_authority.pubkey(), amount) + .unwrap(); + + let id = rand::random::<[u8; 32]>(); + let (swig_key, _) = create_swig_ed25519(&mut context, &swig_authority, id).unwrap(); + + // Derive the swig wallet address + let (swig_wallet_address, _) = Pubkey::find_program_address( + &swig_state::swig::swig_wallet_address_seeds(swig_key.as_ref()), + &Pubkey::from(swig_interface::swig::ID), + ); + + // Fund the swig wallet address with SOL + context.svm.airdrop(&swig_wallet_address, amount).unwrap(); + + let secondary_authority = Keypair::new(); + context + .svm + .airdrop(&secondary_authority.pubkey(), amount) + .unwrap(); + + // Create multiple blacklist actions for different entities + let blacklisted_program1 = solana_sdk::system_program::ID; + let blacklisted_program2 = spl_token::id(); + let blacklisted_program3 = solana_sdk::sysvar::rent::ID; + let blacklisted_wallet1 = Keypair::new().pubkey(); + let blacklisted_wallet2 = Keypair::new().pubkey(); + + let blacklist_actions = vec![ + ClientAction::Blacklist(Blacklist::new_program(blacklisted_program1.to_bytes())), + ClientAction::Blacklist(Blacklist::new_program(blacklisted_program2.to_bytes())), + ClientAction::Blacklist(Blacklist::new_program(blacklisted_program3.to_bytes())), + ClientAction::Blacklist(Blacklist::new_wallet(blacklisted_wallet1.to_bytes())), + ClientAction::Blacklist(Blacklist::new_wallet(blacklisted_wallet2.to_bytes())), + ClientAction::SolLimit(SolLimit { amount: amount / 2 }), + ClientAction::ProgramAll(ProgramAll {}), + ]; + + let bench = add_authority_with_ed25519_root( + &mut context, + &swig_key, + &swig_authority, + AuthorityConfig { + authority_type: AuthorityType::Ed25519, + authority: secondary_authority.pubkey().as_ref(), + }, + blacklist_actions, + ) + .unwrap(); + + // Verify all blacklist actions were added + let swig_account = context.svm.get_account(&swig_key).unwrap(); + let swig = SwigWithRoles::from_bytes(&swig_account.data).unwrap(); + + let role_id = swig + .lookup_role_id(secondary_authority.pubkey().as_ref()) + .unwrap() + .unwrap(); + + let role = swig.get_role(role_id).unwrap().unwrap(); + + // Should have 6 actions total (5 blacklists + 1 sol limit) + assert_eq!(role.position.num_actions(), 7); + + // Verify that we can retrieve all blacklist actions + let mut blacklist_count = 0; + let mut program_blacklist_count = 0; + let mut wallet_blacklist_count = 0; + + let actions = role.get_all_actions().unwrap(); + + use swig_state::action::Permission; + for action in actions { + let permission = action.permission().unwrap(); + if permission == Permission::Blacklist { + blacklist_count += 1; + // Load the actual Blacklist action data to check entity type + let blacklist_data = &role.actions + [action.boundary() as usize - Blacklist::LEN..action.boundary() as usize]; + let blacklist = unsafe { Blacklist::load_unchecked(blacklist_data).unwrap() }; + if blacklist.is_program() { + program_blacklist_count += 1; + } else if blacklist.is_wallet() { + wallet_blacklist_count += 1; + } + } + } + + // Verify counts + assert_eq!(blacklist_count, 5); + assert_eq!(program_blacklist_count, 3); + assert_eq!(wallet_blacklist_count, 2); + + // Test that all blacklisted programs are blocked + let recipient = Keypair::new(); + context.svm.airdrop(&recipient.pubkey(), amount).unwrap(); + + // Test system program (blacklisted_program1) + let transfer_ix1 = + system_instruction::transfer(&swig_wallet_address, &recipient.pubkey(), amount / 4); + let sign_ix1 = SignV2Instruction::new_ed25519( + swig_key, + swig_wallet_address, + secondary_authority.pubkey(), + secondary_authority.pubkey(), + transfer_ix1, + role_id, + ) + .unwrap(); + + let transfer_message1 = v0::Message::try_compile( + &secondary_authority.pubkey(), + &[sign_ix1], + &[], + context.svm.latest_blockhash(), + ) + .unwrap(); + + let transfer_tx1 = VersionedTransaction::try_new( + VersionedMessage::V0(transfer_message1), + &[&secondary_authority], + ) + .unwrap(); + + let result1 = context.svm.send_transaction(transfer_tx1); + assert!(result1.is_err()); + assert_eq!( + result1.unwrap_err().err, + solana_sdk::transaction::TransactionError::InstructionError( + 0, + solana_sdk::instruction::InstructionError::Custom(3033) + ) + ); + + // Test blacklisted wallet 1 + let transfer_ix2 = + system_instruction::transfer(&swig_wallet_address, &blacklisted_wallet1, amount / 4); + let sign_ix2 = SignV2Instruction::new_ed25519( + swig_key, + swig_wallet_address, + secondary_authority.pubkey(), + secondary_authority.pubkey(), + transfer_ix2, + role_id, + ) + .unwrap(); + + let transfer_message2 = v0::Message::try_compile( + &secondary_authority.pubkey(), + &[sign_ix2], + &[], + context.svm.latest_blockhash(), + ) + .unwrap(); + + let transfer_tx2 = VersionedTransaction::try_new( + VersionedMessage::V0(transfer_message2), + &[&secondary_authority], + ) + .unwrap(); + + let result2 = context.svm.send_transaction(transfer_tx2); + assert!(result2.is_err()); + assert_eq!( + result2.unwrap_err().err, + solana_sdk::transaction::TransactionError::InstructionError( + 0, + solana_sdk::instruction::InstructionError::Custom(3033) + ) + ); + + // Test blacklisted wallet 2 + let transfer_ix3 = + system_instruction::transfer(&swig_wallet_address, &blacklisted_wallet2, amount / 4); + let sign_ix3 = SignV2Instruction::new_ed25519( + swig_key, + swig_wallet_address, + secondary_authority.pubkey(), + secondary_authority.pubkey(), + transfer_ix3, + role_id, + ) + .unwrap(); + + let transfer_message3 = v0::Message::try_compile( + &secondary_authority.pubkey(), + &[sign_ix3], + &[], + context.svm.latest_blockhash(), + ) + .unwrap(); + + let transfer_tx3 = VersionedTransaction::try_new( + VersionedMessage::V0(transfer_message3), + &[&secondary_authority], + ) + .unwrap(); + + let result3 = context.svm.send_transaction(transfer_tx3); + assert!(result3.is_err()); + assert_eq!( + result3.unwrap_err().err, + solana_sdk::transaction::TransactionError::InstructionError( + 0, + solana_sdk::instruction::InstructionError::Custom(3033) + ) + ); + + // Verify that non-blacklisted entities can still be used + let non_blacklisted_wallet = Keypair::new().pubkey(); + context + .svm + .airdrop(&non_blacklisted_wallet, amount) + .unwrap(); + + let transfer_ix4 = + system_instruction::transfer(&swig_wallet_address, &non_blacklisted_wallet, amount / 4); + let sign_ix4 = SignV2Instruction::new_ed25519( + swig_key, + swig_wallet_address, + secondary_authority.pubkey(), + secondary_authority.pubkey(), + transfer_ix4, + role_id, + ) + .unwrap(); + + let transfer_message4 = v0::Message::try_compile( + &secondary_authority.pubkey(), + &[sign_ix4], + &[], + context.svm.latest_blockhash(), + ) + .unwrap(); + + let transfer_tx4 = VersionedTransaction::try_new( + VersionedMessage::V0(transfer_message4), + &[&secondary_authority], + ) + .unwrap(); + + let result4 = context.svm.send_transaction(transfer_tx4); + // This should fail due to missing program permission, not blacklist + assert!(result4.is_err()); + // Should fail with wrong resource (3006) not blacklist (3033) + assert_eq!( + result4.unwrap_err().err, + solana_sdk::transaction::TransactionError::InstructionError( + 0, + solana_sdk::instruction::InstructionError::Custom(3033) + ) + ); +} diff --git a/program/tests/program_scope_test.rs b/program/tests/program_scope_test.rs index 4130e88e..6447e655 100644 --- a/program/tests/program_scope_test.rs +++ b/program/tests/program_scope_test.rs @@ -234,7 +234,7 @@ fn test_token_transfer_with_program_scope() { "Account difference (swig - regular): {} accounts", account_difference ); - assert!(swig_transfer_cu - regular_transfer_cu <= 5633); + assert!(swig_transfer_cu - regular_transfer_cu <= 6038); } /// Helper function to perform token transfers through the swig diff --git a/program/tests/sign_performance_test.rs b/program/tests/sign_performance_test.rs index 99cd0f6b..29ff288d 100644 --- a/program/tests/sign_performance_test.rs +++ b/program/tests/sign_performance_test.rs @@ -190,7 +190,7 @@ fn test_token_transfer_performance_comparison() { ); // 3744 is the max difference in CU between the two transactions lets lower // this as far as possible but never increase it - assert!(swig_transfer_cu - regular_transfer_cu <= 3805); + assert!(swig_transfer_cu - regular_transfer_cu <= 4025); } #[test_log::test] @@ -303,5 +303,5 @@ fn test_sol_transfer_performance_comparison() { // Set a reasonable limit for the CU difference to avoid regressions // Similar to the token transfer test assertion - assert!(swig_transfer_cu - regular_transfer_cu <= 2032); + assert!(swig_transfer_cu - regular_transfer_cu <= 2237); } diff --git a/program/tests/sign_performance_v2_test.rs b/program/tests/sign_performance_v2_test.rs index 13588720..532af4a2 100644 --- a/program/tests/sign_performance_v2_test.rs +++ b/program/tests/sign_performance_v2_test.rs @@ -193,7 +193,7 @@ fn test_token_transfer_performance_comparison_v2() { ); // SignV2 uses slightly more CU than SignV1 due to additional account handling // Set a reasonable limit for SignV2 token transfers to avoid regressions - assert!(swig_transfer_cu - regular_transfer_cu <= 3850); + assert!(swig_transfer_cu - regular_transfer_cu <= 4178); } #[test_log::test] @@ -316,5 +316,5 @@ fn test_sol_transfer_performance_comparison_v2() { // SignV2 uses more CU than SignV1 due to additional account handling // Set a reasonable limit for SignV2 SOL transfers to avoid regressions - assert!(swig_transfer_cu - regular_transfer_cu <= 3250); + assert!(swig_transfer_cu - regular_transfer_cu <= 3533); } diff --git a/state/src/action/blacklist.rs b/state/src/action/blacklist.rs new file mode 100644 index 00000000..c976fae9 --- /dev/null +++ b/state/src/action/blacklist.rs @@ -0,0 +1,100 @@ +//! Blacklist action type. +//! +//! This module defines the Blacklist action type which prevents interaction with +//! specific programs or wallet addresses in the Swig wallet system. + +use no_padding::NoPadding; +use pinocchio::program_error::ProgramError; + +use super::{Actionable, Permission}; +use crate::{IntoBytes, Transmutable, TransmutableMut}; + +/// Represents a blacklist entry that prevents interaction with a specific entity. +/// +/// This struct contains the entity ID (program ID or wallet address) that is +/// blacklisted from interacting with the system. Multiple Blacklist actions can +/// exist in a role to blacklist different entities. +#[repr(C, align(8))] +#[derive(Debug, NoPadding)] +pub struct Blacklist { + /// The entity ID that is blacklisted (program ID or wallet address) + pub entity_id: [u8; 32], + /// The type of entity being blacklisted (0 = program, 1 = wallet) + pub entity_type: u8, + /// Reserved bytes for future use + pub _reserved: [u8; 7], +} + +impl Transmutable for Blacklist { + /// Size of the Blacklist struct in bytes (32 bytes for entity_id + 1 for entity_type ) + const LEN: usize = 40; +} + +impl IntoBytes for Blacklist { + fn into_bytes(&self) -> Result<&[u8], ProgramError> { + Ok(unsafe { core::slice::from_raw_parts(self as *const Self as *const u8, Self::LEN) }) + } +} + +impl TransmutableMut for Blacklist {} + +impl<'a> Actionable<'a> for Blacklist { + /// This action represents the Blacklist permission type + const TYPE: Permission = Permission::Blacklist; + /// Multiple blacklist entries can exist per role + const REPEATABLE: bool = true; + + /// Checks if this blacklist entry matches the provided entity ID. + /// + /// # Arguments + /// * `data` - The entity ID to check against (first 32 bytes) + fn match_data(&self, data: &[u8]) -> bool { + data.len() >= 32 && data[0..32] == self.entity_id + } +} + +impl Blacklist { + /// Creates a new blacklist entry for a program. + /// + /// # Arguments + /// * `program_id` - The program ID to blacklist + pub fn new_program(program_id: [u8; 32]) -> Self { + Self { + entity_id: program_id, + entity_type: 0, + _reserved: [0; 7], + } + } + + /// Creates a new blacklist entry for a wallet address. + /// + /// # Arguments + /// * `wallet_address` - The wallet address to blacklist + pub fn new_wallet(wallet_address: [u8; 32]) -> Self { + Self { + entity_id: wallet_address, + entity_type: 1, + _reserved: [0; 7], + } + } + + /// Returns the entity ID as a byte array. + pub fn entity_id(&self) -> &[u8; 32] { + &self.entity_id + } + + /// Returns the entity type (0 = program, 1 = wallet). + pub fn entity_type(&self) -> u8 { + self.entity_type + } + + /// Checks if this blacklist entry is for a program. + pub fn is_program(&self) -> bool { + self.entity_type == 0 + } + + /// Checks if this blacklist entry is for a wallet address. + pub fn is_wallet(&self) -> bool { + self.entity_type == 1 + } +} diff --git a/state/src/action/mod.rs b/state/src/action/mod.rs index b6715f6b..aa12595c 100644 --- a/state/src/action/mod.rs +++ b/state/src/action/mod.rs @@ -7,6 +7,7 @@ pub mod all; pub mod all_but_manage_authority; +pub mod blacklist; pub mod manage_authority; pub mod program; pub mod program_all; @@ -26,6 +27,7 @@ pub mod token_recurring_destination_limit; pub mod token_recurring_limit; use all::All; use all_but_manage_authority::AllButManageAuthority; +use blacklist::Blacklist; use manage_authority::ManageAuthority; use no_padding::NoPadding; use pinocchio::program_error::ProgramError; @@ -163,6 +165,8 @@ pub enum Permission { /// Permission to perform recurring token operations with limits to specific /// destinations TokenRecurringDestinationLimit = 19, + /// Permission to blacklist programs or wallet addresses + Blacklist = 20, } impl TryFrom for Permission { @@ -172,7 +176,7 @@ impl TryFrom for Permission { fn try_from(value: u16) -> Result { match value { // SAFETY: `value` is guaranteed to be in the range of the enum variants. - 0..=19 => Ok(unsafe { core::mem::transmute::(value) }), + 0..=20 => Ok(unsafe { core::mem::transmute::(value) }), _ => Err(SwigStateError::PermissionLoadError.into()), } } @@ -240,6 +244,7 @@ impl ActionLoader { Permission::TokenRecurringDestinationLimit => { TokenRecurringDestinationLimit::valid_layout(data) }, + Permission::Blacklist => Blacklist::valid_layout(data), _ => Ok(false), } } diff --git a/state/src/lib.rs b/state/src/lib.rs index 1857f89b..0242042a 100644 --- a/state/src/lib.rs +++ b/state/src/lib.rs @@ -171,6 +171,8 @@ pub enum SwigAuthenticateError { PermissionDeniedTokenDestinationLimitExceeded, /// Token destination recurring limit exceeded PermissionDeniedRecurringTokenDestinationLimitExceeded, + /// Operation blocked by blacklist + PermissionDeniedBlacklisted, } impl From for ProgramError {