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
11 changes: 10 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,7 @@ num-traits = "0.2.19"
zerocopy = { version = "0.8.25" }
base64 = "0.13"
zeroize = "=1.3.0"

bitvec = { version = "1.0.1", default-features = false }
# HTTP client
reqwest = "0.12"

Expand Down
2 changes: 1 addition & 1 deletion program-libs/bloom-filter/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ solana = ["dep:solana-program-error"]
pinocchio = ["dep:pinocchio"]

[dependencies]
bitvec = "1.0.1"
bitvec = { workspace = true }
solana-nostd-keccak = "0.1.3"
num-bigint = { workspace = true }
solana-program-error = { workspace = true, optional = true }
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
use light_client::rpc::Rpc;
use light_ctoken_types::state::ZExtensionStructMut;
use light_zero_copy::traits::ZeroCopyAtMut;
use solana_sdk::signer::Signer;

use super::shared::*;

Expand Down Expand Up @@ -844,20 +846,3 @@ async fn test_compress_and_close_output_validation_errors() {
light_program_test::utils::assert::assert_rpc_error(result, 0, 18036).unwrap();
}
}

// ============================================================================
// Failure Tests - Compressibility and Missing Accounts
// ============================================================================

#[tokio::test]
#[serial]
async fn test_compress_and_close_compressibility_and_missing_accounts() {
// Note: These tests would require either:
// 1. Manual instruction building to omit required accounts
// 2. Trying to close before the account is compressible
//
// These would require manual instruction building or special setup:
// - Test 12: Rent authority tries to close before account is compressible
// - Test 13: No destination account provided (error 6087 - CompressAndCloseDestinationMissing)
// - Test 14: Rent authority closes but no compressed output exists
}
7 changes: 3 additions & 4 deletions program-tests/compressed-token-test/tests/ctoken/shared.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ pub use light_program_test::{
forester::compress_and_close_forester, program_test::TestRpc, LightProgramTest,
ProgramTestConfig,
};
use light_registry::compressible::compressed_token::CompressAndCloseIndices;
pub use light_test_utils::{
assert_close_token_account::assert_close_token_account,
assert_create_token_account::{
Expand Down Expand Up @@ -756,7 +757,6 @@ pub async fn compress_and_close_forester_with_invalid_output(
use std::str::FromStr;

use anchor_lang::{InstructionData, ToAccountMetas};
use light_compressed_token_sdk::instructions::compress_and_close::CompressAndCloseIndices;
use light_compressible::config::CompressibleConfig;
use light_ctoken_types::state::{CToken, ZExtensionStruct};
use light_registry::{
Expand Down Expand Up @@ -842,9 +842,7 @@ pub async fn compress_and_close_forester_with_invalid_output(
source_index,
mint_index,
owner_index,
authority_index,
rent_sponsor_index,
destination_index,
};

// Add system accounts
Expand All @@ -869,13 +867,14 @@ pub async fn compress_and_close_forester_with_invalid_output(
registered_forester_pda,
compression_authority,
compressible_config,
compressed_token_program: compressed_token_program_id,
};

let mut accounts = compress_and_close_accounts.to_account_metas(Some(true));
accounts.extend(remaining_account_metas);

let instruction = CompressAndClose {
authority_index,
destination_index,
indices: vec![indices],
};
let instruction_data = instruction.data();
Expand Down
2 changes: 2 additions & 0 deletions programs/compressed-token/anchor/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -422,6 +422,8 @@ pub enum ErrorCode {
MintActionInvalidCpiContextForCreateMint,
#[msg("Invalid address tree pubkey in CPI context")]
MintActionInvalidCpiContextAddressTreePubkey,
#[msg("CompressAndClose: Cannot use the same compressed output account for multiple closures")]
CompressAndCloseDuplicateOutput,
}

impl From<ErrorCode> for ProgramError {
Expand Down
1 change: 1 addition & 0 deletions programs/compressed-token/program/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ light-array-map = { workspace = true }
pinocchio-pubkey = { workspace = true }
pinocchio-system = { workspace = true }
pinocchio-token-program = { workspace = true }
bitvec = { workspace = true }

[dev-dependencies]
rand = { workspace = true }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,7 @@ When compression processing occurs (in both Path A and Path B):
- Token account balance is set to 0
- Account is marked for closing after the transaction
- **Security guarantee:** Unlike Compress which only adds to sum checks, CompressAndClose ensures the exact compressed account exists, preventing token loss or misdirection
- **Uniqueness validation:** All CompressAndClose operations in a single instruction must use different compressed output account indices. Duplicate output indices are rejected to prevent fund theft attacks where a rent authority could close multiple accounts but route all funds to a single compressed output
- Calculate compressible extension top-up if present (returns Option<u64>)
- **Transfer deduplication optimization:**
- Collects all transfers into a 40-element array indexed by account
Expand Down Expand Up @@ -318,6 +319,7 @@ When compression processing occurs (in both Path A and Path B):
- `ErrorCode::CompressAndCloseAuthorityMissing` (error code: 6088) - Missing authority for CompressAndClose
- `ErrorCode::CompressAndCloseAmountMismatch` (error code: 6090) - CompressAndClose amount doesn't match balance
- `ErrorCode::CompressAndCloseDelegateNotAllowed` (error code: 6092) - Delegates cannot use CompressAndClose
- `ErrorCode::CompressAndCloseDuplicateOutput` (error code: 6420) - Cannot use the same compressed output account for multiple CompressAndClose operations (security protection against fund theft)
- `AccountError::InvalidSigner` (error code: 12015) - Required signer account is not signing
- `AccountError::AccountNotMutable` (error code: 12008) - Required mutable account is not mutable
- Additional errors from close_token_account for CompressAndClose operations
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use anchor_compressed_token::ErrorCode;
use anchor_lang::prelude::ProgramError;
use bitvec::prelude::*;
use light_account_checks::{checks::check_signer, packed_accounts::ProgramPackedAccounts};
use light_ctoken_types::{
instructions::transfer2::{ZCompression, ZCompressionMode, ZMultiTokenTransferOutputData},
Expand All @@ -12,8 +13,7 @@ use spl_pod::solana_msg::msg;
use super::inputs::CompressAndCloseInputs;
use crate::{
close_token_account::{
accounts::CloseTokenAccountAccounts,
processor::{close_token_account, validate_token_account_for_close_transfer2},
accounts::CloseTokenAccountAccounts, processor::validate_token_account_for_close_transfer2,
},
transfer2::accounts::Transfer2Accounts,
};
Expand Down Expand Up @@ -168,33 +168,58 @@ fn validate_compressed_token_account(
/// Close ctoken accounts after compress and close operations
pub fn close_for_compress_and_close(
compressions: &[ZCompression<'_>],
validated_accounts: &Transfer2Accounts,
_validated_accounts: &Transfer2Accounts,
) -> Result<(), ProgramError> {
// Track used compressed account indices for CompressAndClose to prevent duplicate outputs
let mut used_compressed_account_indices = [0u8; 32]; // 256 bits
let used_bits = used_compressed_account_indices.view_bits_mut::<Msb0>();

for compression in compressions
.iter()
.filter(|c| c.mode == ZCompressionMode::CompressAndClose)
{
let token_account_info = validated_accounts.packed_accounts.get_u8(
compression.source_or_recipient,
"CompressAndClose: source_or_recipient",
)?;
let destination = validated_accounts.packed_accounts.get_u8(
compression.get_destination_index()?,
"CompressAndClose: destination",
)?;
let rent_sponsor = validated_accounts.packed_accounts.get_u8(
compression.get_rent_sponsor_index()?,
"CompressAndClose: rent_sponsor",
)?;
let authority = validated_accounts
.packed_accounts
.get_u8(compression.authority, "CompressAndClose: authority")?;
close_token_account(&CloseTokenAccountAccounts {
token_account: token_account_info,
destination,
authority,
rent_sponsor: Some(rent_sponsor),
})?;
// Check for duplicate compressed account indices in CompressAndClose operations
let compressed_idx = compression.get_compressed_token_account_index()?;
if let Some(mut bit) = used_bits.get_mut(compressed_idx as usize) {
if *bit {
msg!(
"Duplicate compressed account index {} in CompressAndClose operations",
compressed_idx
);
return Err(ErrorCode::CompressAndCloseDuplicateOutput.into());
}
*bit = true;
} else {
msg!("Compressed account index {} out of bounds", compressed_idx);
return Err(ProgramError::InvalidInstructionData);
}

#[cfg(target_os = "solana")]
{
let validated_accounts = _validated_accounts;
let token_account_info = validated_accounts.packed_accounts.get_u8(
compression.source_or_recipient,
"CompressAndClose: source_or_recipient",
)?;
let destination = validated_accounts.packed_accounts.get_u8(
compression.get_destination_index()?,
"CompressAndClose: destination",
)?;
let rent_sponsor = validated_accounts.packed_accounts.get_u8(
compression.get_rent_sponsor_index()?,
"CompressAndClose: rent_sponsor",
)?;
let authority = validated_accounts
.packed_accounts
.get_u8(compression.authority, "CompressAndClose: authority")?;
use crate::close_token_account::processor::close_token_account;
close_token_account(&CloseTokenAccountAccounts {
token_account: token_account_info,
destination,
authority,
rent_sponsor: Some(rent_sponsor),
})?;
}
}
Ok(())
}
Loading
Loading