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
2 changes: 1 addition & 1 deletion .github/workflows/sdk-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ jobs:
- program: native
sub-tests: '["cargo-test-sbf -p sdk-native-test", "cargo-test-sbf -p sdk-v1-native-test", "cargo-test-sbf -p sdk-light-token-test", "cargo-test-sbf -p client-test"]'
- program: anchor & pinocchio
sub-tests: '["cargo-test-sbf -p sdk-anchor-test", "cargo-test-sbf -p sdk-compressible-test", "cargo-test-sbf -p csdk-anchor-derived-test", "cargo-test-sbf -p csdk-anchor-full-derived-test", "cargo-test-sbf -p sdk-pinocchio-v1-test", "cargo-test-sbf -p sdk-pinocchio-v2-test", "cargo-test-sbf -p pinocchio-nostd-test"]'
sub-tests: '["cargo-test-sbf -p sdk-anchor-test", "cargo-test-sbf -p sdk-compressible-test", "cargo-test-sbf -p csdk-anchor-derived-test", "cargo-test-sbf -p csdk-anchor-full-derived-test", "cargo-test-sbf -p sdk-pinocchio-v1-test", "cargo-test-sbf -p sdk-pinocchio-v2-test", "cargo-test-sbf -p pinocchio-nostd-test", "cargo-test-sbf -p single-mint-test", "cargo-test-sbf -p single-pda-test", "cargo-test-sbf -p single-ata-test", "cargo-test-sbf -p single-token-test"]'
- program: token test
sub-tests: '["cargo-test-sbf -p sdk-token-test"]'
- program: sdk-libs
Expand Down
128 changes: 128 additions & 0 deletions Cargo.lock

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

4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,10 @@ members = [
"sdk-tests/sdk-light-token-test",
"sdk-tests/csdk-anchor-full-derived-test",
"sdk-tests/csdk-anchor-full-derived-test-sdk",
"sdk-tests/single-mint-test",
"sdk-tests/single-pda-test",
"sdk-tests/single-ata-test",
"sdk-tests/single-token-test",
"forester-utils",
"forester",
"sparse-merkle-tree",
Expand Down
1 change: 1 addition & 0 deletions program-libs/token-interface/tests/token/spl_compat.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
#![cfg(feature = "test-only")]
//! Tests token solana account - spl token account layout compatibility
//!
//! Tests:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
#![cfg(feature = "test-only")]
//! Contains functional zero copy tests for:
//! - ZeroCopyNew
//!
Expand Down
13 changes: 13 additions & 0 deletions sdk-libs/macros/src/light_pdas/account/decompress_context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,18 @@ pub fn generate_decompress_context_trait_impl(
})
.collect();

// For mint-only programs (no PDA variants), add an arm for the Empty variant
let empty_variant_arm = if pda_ctx_seeds.is_empty() {
quote! {
// Mint-only programs have an Empty variant that should never be decompressed
LightAccountVariant::Empty => {
return std::result::Result::Err(solana_program_error::ProgramError::InvalidAccountData);
}
}
} else {
quote! {}
};

let packed_token_variant_ident = format_ident!("Packed{}", token_variant_ident);

Ok(quote! {
Expand Down Expand Up @@ -177,6 +189,7 @@ pub fn generate_decompress_context_trait_impl(
LightAccountVariant::CTokenData(_) => {
return std::result::Result::Err(light_sdk::error::LightSdkError::UnexpectedUnpackedVariant.into());
}
#empty_variant_arm
}
}

Expand Down
48 changes: 46 additions & 2 deletions sdk-libs/macros/src/light_pdas/account/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -104,13 +104,57 @@ pub(crate) fn is_pubkey_type(ty: &Type) -> bool {
}
}

/// Generates an empty TokenAccountVariant enum.
/// Generates placeholder TokenAccountVariant and PackedTokenAccountVariant enums.
///
/// This is used when no token accounts are specified in compressible instructions.
/// We use a placeholder variant since Rust doesn't support empty enums with #[repr(u8)].
pub(crate) fn generate_empty_ctoken_enum() -> proc_macro2::TokenStream {
quote::quote! {
#[derive(anchor_lang::AnchorSerialize, anchor_lang::AnchorDeserialize, Debug, Clone, Copy)]
#[repr(u8)]
pub enum TokenAccountVariant {}
pub enum TokenAccountVariant {
/// Placeholder variant for programs without token accounts
Empty = 0,
}

#[derive(anchor_lang::AnchorSerialize, anchor_lang::AnchorDeserialize, Debug, Clone, Copy)]
#[repr(u8)]
pub enum PackedTokenAccountVariant {
/// Placeholder variant for programs without token accounts
Empty = 0,
}

impl light_token::pack::Pack for TokenAccountVariant {
type Packed = PackedTokenAccountVariant;
fn pack(&self, _remaining_accounts: &mut light_sdk::instruction::PackedAccounts) -> std::result::Result<Self::Packed, solana_program_error::ProgramError> {
Ok(PackedTokenAccountVariant::Empty)
}
}

impl light_token::pack::Unpack for PackedTokenAccountVariant {
type Unpacked = TokenAccountVariant;
fn unpack(&self, _remaining_accounts: &[solana_account_info::AccountInfo]) -> std::result::Result<Self::Unpacked, solana_program_error::ProgramError> {
Ok(TokenAccountVariant::Empty)
}
}

impl light_sdk::interface::TokenSeedProvider for TokenAccountVariant {
fn get_seeds(&self, _program_id: &Pubkey) -> std::result::Result<(Vec<Vec<u8>>, Pubkey), solana_program_error::ProgramError> {
Err(solana_program_error::ProgramError::InvalidAccountData)
}

fn get_authority_seeds(&self, _program_id: &Pubkey) -> std::result::Result<(Vec<Vec<u8>>, Pubkey), solana_program_error::ProgramError> {
Err(solana_program_error::ProgramError::InvalidAccountData)
}
}

impl light_sdk::interface::IntoCTokenVariant<LightAccountVariant, light_token::compat::TokenData> for TokenAccountVariant {
fn into_ctoken_variant(self, _token_data: light_token::compat::TokenData) -> LightAccountVariant {
// This function should never be called for programs without token accounts.
// The Empty variant only exists in mint-only programs (no PDAs).
// For programs with PDAs but no tokens, this impl exists only to satisfy trait bounds.
unreachable!("into_ctoken_variant called on program without token accounts")
Comment on lines +151 to +156
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, let's explore the file structure and find the file in question
cd /repo && find . -name "utils.rs" | grep -E "macros/.*light_pdas.*account"

Repository: Lightprotocol/light-protocol

Length of output: 127


🏁 Script executed:

# Read the specific file to see the context around lines 151-156
wc -l sdk-libs/macros/src/light_pdas/account/utils.rs

Repository: Lightprotocol/light-protocol

Length of output: 123


🏁 Script executed:

# Read the file with context
cat -n sdk-libs/macros/src/light_pdas/account/utils.rs | head -200 | tail -80

Repository: Lightprotocol/light-protocol

Length of output: 4068


🏁 Script executed:

# Search for the IntoCTokenVariant trait definition
rg "trait IntoCTokenVariant" --type rs -A 10

Repository: Lightprotocol/light-protocol

Length of output: 98


🏁 Script executed:

# Search for usages of TokenAccountVariant and into_ctoken_variant
rg "TokenAccountVariant" --type rs -B 2 -A 2

Repository: Lightprotocol/light-protocol

Length of output: 98


🏁 Script executed:

# Search for other unreachable! patterns in similar macros code
rg "unreachable!" sdk-libs/macros --type rs -B 2 -A 2

Repository: Lightprotocol/light-protocol

Length of output: 98


🏁 Script executed:

# Check for LightAccountVariant::Empty definition and usage
rg "LightAccountVariant::Empty" --type rs -B 2 -A 2

Repository: Lightprotocol/light-protocol

Length of output: 98


🏁 Script executed:

rg "trait IntoCTokenVariant" --type-list

Repository: Lightprotocol/light-protocol

Length of output: 461


🏁 Script executed:

rg "trait IntoCTokenVariant" -A 10

Repository: Lightprotocol/light-protocol

Length of output: 805


🏁 Script executed:

rg "into_ctoken_variant" -B 3 -A 3

Repository: Lightprotocol/light-protocol

Length of output: 4496


🏁 Script executed:

rg "TokenAccountVariant" -B 2 -A 2 | head -100

Repository: Lightprotocol/light-protocol

Length of output: 8886


🏁 Script executed:

# Look at how other generated Empty enums handle trait implementations
rg "generate_empty_ctoken_enum" -B 5 -A 20

Repository: Lightprotocol/light-protocol

Length of output: 5140


🏁 Script executed:

# Check if there are other unreachable! macros in the generated code sections
rg "unreachable!" sdk-libs/macros -B 3 -A 1

Repository: Lightprotocol/light-protocol

Length of output: 1516


🏁 Script executed:

rg "enum LightAccountVariant" -A 15

Repository: Lightprotocol/light-protocol

Length of output: 3172


🏁 Script executed:

# Check how this Empty TokenAccountVariant is used/called
rg "into_ctoken_variant" --context 5

Repository: Lightprotocol/light-protocol

Length of output: 5900


🏁 Script executed:

# See if there are defensive patterns (returning a default/empty variant) elsewhere
rg "LightAccountVariant::" sdk-libs/sdk -A 1 | head -50

Repository: Lightprotocol/light-protocol

Length of output: 54


🏁 Script executed:

# Check the trait documentation more carefully
cat -n sdk-libs/sdk/src/interface/traits.rs | grep -A 20 "trait IntoCTokenVariant"

Repository: Lightprotocol/light-protocol

Length of output: 505


🏁 Script executed:

# Search for where into_ctoken_variant is invoked
rg "\.into_ctoken_variant\(" -B 5 -A 3

Repository: Lightprotocol/light-protocol

Length of output: 54


🏁 Script executed:

# Search for calls that might guard against the empty case
rg "into_ctoken_variant" -B 10 | head -80

Repository: Lightprotocol/light-protocol

Length of output: 6725


🏁 Script executed:

# Check if there's any match/guard logic before calling
rg "match.*TokenAccountVariant" -A 5 | head -60

Repository: Lightprotocol/light-protocol

Length of output: 54


Replace unreachable!() with safe fallback to LightAccountVariant::Empty.

The into_ctoken_variant implementation for the empty token variant currently uses unreachable!(), which will panic on-chain if ever invoked. While the intent is that this path should never execute, the other trait impls on TokenAccountVariant::Empty (Pack, Unpack, TokenSeedProvider) all use defensive returns. For consistency and safety, return LightAccountVariant::Empty:

Suggested fix
-                unreachable!("into_ctoken_variant called on program without token accounts")
+                LightAccountVariant::Empty
🤖 Prompt for AI Agents
In `@sdk-libs/macros/src/light_pdas/account/utils.rs` around lines 151 - 156, The
impl of IntoCTokenVariant for TokenAccountVariant uses unreachable!() in
into_ctoken_variant which can panic; change it to return the safe fallback
LightAccountVariant::Empty instead. Locate the method
TokenAccountVariant::into_ctoken_variant (impl
light_sdk::interface::IntoCTokenVariant<LightAccountVariant,
light_token::compat::TokenData> for TokenAccountVariant) and replace the
unreachable!() call with a deterministic return of LightAccountVariant::Empty so
it matches the defensive behavior of Pack/Unpack/TokenSeedProvider.

}
}
}
}
42 changes: 28 additions & 14 deletions sdk-libs/macros/src/light_pdas/program/decompress.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

use proc_macro2::TokenStream;
use quote::{format_ident, quote};
use syn::{Ident, Result, Type};
use syn::{Ident, Result};

use super::{
expr_traversal::transform_expr_for_ctx_seeds,
Expand All @@ -30,8 +30,6 @@ pub(super) struct DecompressBuilder {
pda_ctx_seeds: Vec<PdaCtxSeedInfo>,
/// Token variant identifier (e.g., TokenAccountVariant).
token_variant_ident: Ident,
/// Account types that can be decompressed.
account_types: Vec<Type>,
/// PDA seed specifications.
pda_seeds: Option<Vec<TokenSeedSpec>>,
}
Expand All @@ -42,18 +40,15 @@ impl DecompressBuilder {
/// # Arguments
/// * `pda_ctx_seeds` - PDA context seed information for each variant
/// * `token_variant_ident` - Token variant identifier
/// * `account_types` - Account types that can be decompressed
/// * `pda_seeds` - PDA seed specifications
pub fn new(
pda_ctx_seeds: Vec<PdaCtxSeedInfo>,
token_variant_ident: Ident,
account_types: Vec<Type>,
pda_seeds: Option<Vec<TokenSeedSpec>>,
) -> Self {
Self {
pda_ctx_seeds,
token_variant_ident,
account_types,
pda_seeds,
}
}
Expand Down Expand Up @@ -161,15 +156,34 @@ impl DecompressBuilder {
}

/// Generate PDA seed provider implementations.
/// Returns empty Vec for mint-only or token-only programs that have no PDA seeds.
pub fn generate_seed_provider_impls(&self) -> Result<Vec<TokenStream>> {
let pda_seed_specs = self.pda_seeds.as_ref().ok_or_else(|| {
let span_source = self
.account_types
.first()
.map(|t| quote::quote!(#t))
.unwrap_or_else(|| quote::quote!(unknown));
super::parsing::macro_error!(span_source, "No seed specifications provided")
})?;
// For mint-only or token-only programs, there are no PDA seeds - return empty Vec
let pda_seed_specs = match self.pda_seeds.as_ref() {
Some(specs) if !specs.is_empty() => specs,
_ => {
// Fail fast if pda_ctx_seeds has variants but pda_seeds is missing
if !self.pda_ctx_seeds.is_empty() {
let variant_names: Vec<_> = self
.pda_ctx_seeds
.iter()
.map(|v| v.variant_name.to_string())
.collect();
return Err(syn::Error::new(
proc_macro2::Span::call_site(),
format!(
"generate_seed_provider_impls: pda_seeds is None/empty but \
pda_ctx_seeds contains {} variant(s): [{}]. \
Each pda_ctx_seeds variant requires a corresponding PDA seed \
specification in pda_seeds.",
self.pda_ctx_seeds.len(),
variant_names.join(", ")
),
));
}
return Ok(Vec::new());
}
};

let mut results = Vec::with_capacity(self.pda_ctx_seeds.len());

Expand Down
Loading
Loading