From 60f47c0eab87901c114e7525c56a07ae85d3f1ff Mon Sep 17 00:00:00 2001 From: ananas-block Date: Tue, 9 Dec 2025 15:44:28 +0000 Subject: [PATCH] fix: require custom rent sponsor to sign --- .../tests/ctoken/create.rs | 43 +++++++++++++++++++ .../tests/ctoken/create_ata.rs | 40 +++++++++++++++++ .../src/create_associated_token_account.rs | 6 +++ .../program/src/create_token_account.rs | 6 +++ 4 files changed, 95 insertions(+) diff --git a/program-tests/compressed-token-test/tests/ctoken/create.rs b/program-tests/compressed-token-test/tests/ctoken/create.rs index d2c7c9cd4a..4432b80bcf 100644 --- a/program-tests/compressed-token-test/tests/ctoken/create.rs +++ b/program-tests/compressed-token-test/tests/ctoken/create.rs @@ -518,4 +518,47 @@ async fn test_create_compressible_token_account_failing() { // Should fail with InvalidDiscriminator (20000) from account-checks light_program_test::utils::assert::assert_rpc_error(result, 0, 20000).unwrap(); } + + // Test 9: Non-signer custom rent payer (DoS prevention) + // Custom rent payer must be a signer to prevent setting executable accounts as rent_sponsor. + // This prevents DoS attacks where an attacker sets an executable account as rent_sponsor, + // making the token account impossible to close (lamport transfers to executable accounts fail). + // Error: 8 (MissingRequiredSignature) + { + context.token_account_keypair = Keypair::new(); + + // Use account compression program as custom rent payer (executable, cannot sign) + let executable_rent_payer = light_sdk::constants::ACCOUNT_COMPRESSION_PROGRAM_ID.into(); + + let compressible_params = CompressibleParams { + compressible_config: context.compressible_config, + rent_sponsor: executable_rent_payer, // Executable account! + pre_pay_num_epochs: 2, + lamports_per_write: Some(100), + compress_to_account_pubkey: None, + token_account_version: light_ctoken_interface::state::TokenDataVersion::ShaFlat, + }; + + let create_token_account_ix = CreateCTokenAccount::new( + payer_pubkey, + context.token_account_keypair.pubkey(), + context.mint_pubkey, + context.owner_keypair.pubkey(), + ) + .with_compressible(compressible_params) + .instruction() + .unwrap(); + + let result = context + .rpc + .create_and_send_transaction( + &[create_token_account_ix], + &payer_pubkey, + &[&context.payer, &context.token_account_keypair], + ) + .await; + + // Should fail with MissingRequiredSignature (8) + light_program_test::utils::assert::assert_rpc_error(result, 0, 8).unwrap(); + } } diff --git a/program-tests/compressed-token-test/tests/ctoken/create_ata.rs b/program-tests/compressed-token-test/tests/ctoken/create_ata.rs index b9358875ad..d418c1bd4b 100644 --- a/program-tests/compressed-token-test/tests/ctoken/create_ata.rs +++ b/program-tests/compressed-token-test/tests/ctoken/create_ata.rs @@ -1,3 +1,5 @@ +use light_sdk::constants::ACCOUNT_COMPRESSION_PROGRAM_ID; + use super::shared::*; #[tokio::test] @@ -558,6 +560,44 @@ async fn test_create_ata_failing() { // Should fail with InvalidDiscriminator (20000) from account-checks light_program_test::utils::assert::assert_rpc_error(result, 0, 20000).unwrap(); } + + // Test 9: Non-signer custom rent payer (DoS prevention) + // Custom rent payer must be a signer to prevent setting executable accounts as rent_sponsor. + // This prevents DoS attacks where an attacker sets an executable account as rent_sponsor, + // making the token account impossible to close (lamport transfers to executable accounts fail). + // Error: 8 (MissingRequiredSignature) + { + context.mint_pubkey = solana_sdk::pubkey::Pubkey::new_unique(); + + // Use system program as custom rent payer (executable, cannot sign) + let executable_rent_payer = ACCOUNT_COMPRESSION_PROGRAM_ID.into(); + + let compressible_params = CompressibleParams { + compressible_config: context.compressible_config, + rent_sponsor: executable_rent_payer, // Executable account! + pre_pay_num_epochs: 2, + lamports_per_write: Some(100), + compress_to_account_pubkey: None, + token_account_version: light_ctoken_interface::state::TokenDataVersion::ShaFlat, + }; + + let create_ata_ix = CreateAssociatedCTokenAccount::new( + payer_pubkey, + context.owner_keypair.pubkey(), + context.mint_pubkey, + ) + .with_compressible(compressible_params) + .instruction() + .unwrap(); + + let result = context + .rpc + .create_and_send_transaction(&[create_ata_ix], &payer_pubkey, &[&context.payer]) + .await; + + // Should fail with MissingRequiredSignature (8) + light_program_test::utils::assert::assert_rpc_error(result, 0, 8).unwrap(); + } } #[tokio::test] diff --git a/programs/compressed-token/program/src/create_associated_token_account.rs b/programs/compressed-token/program/src/create_associated_token_account.rs index 22b8380c63..50eb19893f 100644 --- a/programs/compressed-token/program/src/create_associated_token_account.rs +++ b/programs/compressed-token/program/src/create_associated_token_account.rs @@ -195,6 +195,12 @@ fn process_compressible_config<'info>( let custom_rent_payer = *rent_payer.key() != compressible_config_account.rent_sponsor.to_bytes(); + // Prevents setting executable accounts as rent_sponsor + if custom_rent_payer && !rent_payer.is_signer() { + msg!("Custom rent payer must be a signer"); + return Err(ProgramError::MissingRequiredSignature); + } + let rent = compressible_config_account .rent_config .get_rent_with_compression_cost( diff --git a/programs/compressed-token/program/src/create_token_account.rs b/programs/compressed-token/program/src/create_token_account.rs index 9447b1ba91..681756e62c 100644 --- a/programs/compressed-token/program/src/create_token_account.rs +++ b/programs/compressed-token/program/src/create_token_account.rs @@ -184,6 +184,12 @@ pub fn process_create_token_account( let custom_rent_payer = *compressible.rent_payer.key() != config_account.rent_sponsor.to_bytes(); + // Prevents setting executable accounts as rent_sponsor + if custom_rent_payer && !compressible.rent_payer.is_signer() { + msg!("Custom rent payer must be a signer"); + return Err(ProgramError::MissingRequiredSignature); + } + // Build fee_payer seeds (rent_sponsor PDA or None for custom keypair) let version_bytes = config_account.version.to_le_bytes(); let bump_seed = [config_account.rent_sponsor_bump];