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/programs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ jobs:
- program: light-system-program-compression
sub-tests: '["cargo-test-sbf -p system-test -- test_with_compression", "cargo-test-sbf -p system-test --test test_re_init_cpi_account"]'
- program: compressed-token-and-e2e
sub-tests: '["cargo-test-sbf -p compressed-token-test --test v1", "cargo-test-sbf -p compressed-token-test --test mint"]'
sub-tests: '["cargo test -p light-compressed-token", "cargo-test-sbf -p compressed-token-test --test v1", "cargo-test-sbf -p compressed-token-test --test mint"]'
- program: compressed-token-batched-tree
sub-tests: '["cargo-test-sbf -p compressed-token-test -- test_transfer_with_photon_and_batched_tree"]'
- program: system-cpi-test
Expand Down
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{"pubkey":"ACXg8a7VaqecBWrSbdu73W4Pg9gsqXJ3EXAqkHyhvVXg","account":{"lamports":3048480,"data":["tATnGtyQN6gBAAH+Qgg4HTZ/YaoZoKlk0c8isySwNcDU3JXoIxQdOcQ0pr1CCDgdNn9hqhmgqWTRzyKzJLA1wNTclegjFB05xDSmvQyNmy4X80UX3keywHfcTHRpoDkggeuLDEJp3K8hdmjDNfsX+1mDkDWCPnO7jhPNTQ0DDtupetonCWjl6mDSxmj//4AA+CoBAgAACKbpmLBf6SuYoNn3ednb6LLz7vZ4Eyh766UmSYCRaL0AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==","base64"],"owner":"Lighton6oQpVkeewmo2mcPTQQp7kYHr4fWpAgJyEmDX","executable":false,"rentEpoch":0,"space":310}}
{"pubkey":"ACXg8a7VaqecBWrSbdu73W4Pg9gsqXJ3EXAqkHyhvVXg","account":{"lamports":3048480,"data":["tATnGtyQN6gBAAH+SAQtJ+UbyIx39TonBXNob6aj8IhJZXQDWxu0dUrRKtBIBC0n5RvIjHf1OicFc2hvpqPwiElldANbG7R1StEq0AyNmy4X80UX3keywHfcTHRpoDkggeuLDEJp3K8hdmjDNfsX+1mDkDWCPnO7jhPNTQ0DDtupetonCWjl6mDSxmj//4AA+CoBAoAwCKbpmLBf6SuYoNn3ednb6LLz7vZ4Eyh766UmSYCRaL0AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==","base64"],"owner":"Lighton6oQpVkeewmo2mcPTQQp7kYHr4fWpAgJyEmDX","executable":false,"rentEpoch":0,"space":310}}
1 change: 1 addition & 0 deletions program-libs/compressible/src/compression_info.rs
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ macro_rules! impl_is_compressible {
rent_exemption_lamports: u64,
) -> Result<u64, CompressibleError> {
let lamports_per_write: u32 = self.lamports_per_write.into();

// Calculate rent status using AccountRentState
let state = crate::rent::AccountRentState {
num_bytes,
Expand Down
26 changes: 23 additions & 3 deletions program-libs/compressible/src/rent/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ pub trait RentConfigTrait {
/// Get maximum funded epochs
fn max_funded_epochs(&self) -> u64;

/// Get maximum top-up amount per write operation
fn max_top_up(&self) -> u64;

/// Calculate rent per epoch for a given number of bytes
#[inline(always)]
fn rent_curve_per_epoch(&self, num_bytes: u64) -> u64 {
Expand Down Expand Up @@ -72,7 +75,9 @@ pub struct RentConfig {
pub compression_cost: u16,
pub lamports_per_byte_per_epoch: u8,
pub max_funded_epochs: u8, // once the account is funded for max_funded_epochs top up per write is not executed
pub _padding: [u8; 2],
/// Maximum lamports that can be charged per top-up operation.
/// Protects against griefing by accounts with high lamports_per_write.
pub max_top_up: u16,
}

impl Default for RentConfig {
Expand All @@ -82,7 +87,7 @@ impl Default for RentConfig {
compression_cost: COMPRESSION_COST + COMPRESSION_INCENTIVE,
lamports_per_byte_per_epoch: RENT_PER_BYTE,
max_funded_epochs: 2, // once the account is funded for max_funded_epochs top up per write is not executed
_padding: [0; 2],
max_top_up: 12416, // 48h rent for ctoken accounts of size 260 bytes
}
}
}
Expand All @@ -107,6 +112,11 @@ impl RentConfigTrait for RentConfig {
fn max_funded_epochs(&self) -> u64 {
self.max_funded_epochs as u64
}

#[inline(always)]
fn max_top_up(&self) -> u64 {
self.max_top_up as u64
}
}

impl RentConfig {
Expand Down Expand Up @@ -142,6 +152,11 @@ impl RentConfigTrait for ZRentConfig<'_> {
fn max_funded_epochs(&self) -> u64 {
self.max_funded_epochs as u64
}

#[inline(always)]
fn max_top_up(&self) -> u64 {
self.max_top_up.into()
}
}

// Implement trait for zero-copy mutable reference
Expand All @@ -165,6 +180,11 @@ impl RentConfigTrait for ZRentConfigMut<'_> {
fn max_funded_epochs(&self) -> u64 {
self.max_funded_epochs as u64
}

#[inline(always)]
fn max_top_up(&self) -> u64 {
self.max_top_up.into()
}
}

impl ZRentConfigMut<'_> {
Expand All @@ -174,6 +194,6 @@ impl ZRentConfigMut<'_> {
self.compression_cost = config.compression_cost.into();
self.lamports_per_byte_per_epoch = config.lamports_per_byte_per_epoch;
self.max_funded_epochs = config.max_funded_epochs;
self._padding = config._padding;
self.max_top_up = config.max_top_up.into();
}
}
8 changes: 8 additions & 0 deletions program-libs/ctoken-types/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,12 @@ pub enum CTokenError {

#[error("Too many PDA seeds. Maximum {0} seeds allowed")]
TooManySeeds(usize),

#[error("write_top_up exceeds max_top_up from RentConfig")]
WriteTopUpExceedsMaximum,

#[error("Calculated top-up exceeds sender's max_top_up limit")]
MaxTopUpExceeded,
}

impl From<CTokenError> for u32 {
Expand Down Expand Up @@ -175,6 +181,8 @@ impl From<CTokenError> for u32 {
CTokenError::TooManyAdditionalMetadata => 18039,
CTokenError::DuplicateMetadataKey => 18040,
CTokenError::TooManySeeds(_) => 18041,
CTokenError::WriteTopUpExceedsMaximum => 18042,
CTokenError::MaxTopUpExceeded => 18043,
CTokenError::HasherError(e) => u32::from(e),
CTokenError::ZeroCopyError(e) => u32::from(e),
CTokenError::CompressedAccountError(e) => u32::from(e),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ impl MintActionCompressedInstructionData {
compressed_address: mint_with_context.address,
token_pool_bump: 0,
token_pool_index: 0,
max_top_up: 0, // No limit by default
create_mint: None,
actions: Vec::new(),
proof,
Expand All @@ -54,6 +55,7 @@ impl MintActionCompressedInstructionData {
compressed_address,
token_pool_bump: 0,
token_pool_index: 0,
max_top_up: 0, // No limit by default
create_mint: Some(CreateMint::default()),
actions: Vec::new(),
proof: Some(proof),
Expand All @@ -75,6 +77,7 @@ impl MintActionCompressedInstructionData {
compressed_address,
token_pool_bump: 0,
token_pool_index: 0,
max_top_up: 0, // No limit by default
create_mint: Some(CreateMint::default()),
actions: Vec::new(),
proof: None, // Proof is verified with execution not write
Expand Down Expand Up @@ -131,6 +134,12 @@ impl MintActionCompressedInstructionData {
self
}

#[must_use = "with_max_top_up returns a new value"]
pub fn with_max_top_up(mut self, max_top_up: u16) -> Self {
self.max_top_up = max_top_up;
self
}

#[must_use = "write_to_cpi_context_first returns a new value"]
pub fn write_to_cpi_context_first(mut self) -> Self {
if let Some(ref mut ctx) = self.cpi_context {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ pub struct MintActionCompressedInstructionData {
/// Used to check token pool derivation.
/// Only required if associated spl mint exists and actions contain mint actions.
pub token_pool_index: u8,
/// Maximum lamports for rent and top-up combined. Transaction fails if exceeded. (0 = no limit)
pub max_top_up: u16,
pub create_mint: Option<CreateMint>,
pub actions: Vec<Action>,
pub proof: Option<CompressedProof>,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ pub struct CompressedTokenInstructionDataTransfer2 {
/// Placeholder currently unimplemented.
pub lamports_change_account_owner_index: u8,
pub output_queue: u8,
/// Maximum lamports for rent and top-up combined. Transaction fails if exceeded. (0 = no limit)
pub max_top_up: u16,
pub cpi_context: Option<CompressedCpiContext>,
pub compressions: Option<Vec<Compression>>,
pub proof: Option<CompressedProof>,
Expand Down
2 changes: 1 addition & 1 deletion program-libs/ctoken-types/tests/ctoken/spl_compat.rs
Original file line number Diff line number Diff line change
Expand Up @@ -540,7 +540,7 @@ fn test_compressible_extension_partial_eq() {
compression_cost: 0,
lamports_per_byte_per_epoch: 0,
max_funded_epochs: 0,
_padding: [0; 2],
max_top_up: 0,
},
};

Expand Down
34 changes: 32 additions & 2 deletions program-tests/compressed-token-test/tests/ctoken/create.rs
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,8 @@ async fn test_create_account_random() {
}
},
lamports_per_write: if rng.gen_bool(0.5) {
Some(rng.gen_range(0..=u16::MAX as u32))
// Limit to max_top_up to avoid WriteTopUpExceedsMaximum error
Some(rng.gen_range(0..=RentConfig::default().max_top_up as u32))
} else {
None
},
Expand Down Expand Up @@ -447,7 +448,36 @@ async fn test_create_compressible_token_account_failing() {
light_program_test::utils::assert::assert_rpc_error(result, 0, 20001).unwrap();
}

// Test 7: Wrong account type (correct program owner, wrong discriminator)
// Test 7: write_top_up exceeds max_top_up from RentConfig
// Accounts cannot be created with lamports_per_write > max_top_up.
// This protects against griefing attacks where recipient creates account with excessive top-up.
// Error: 18042 (WriteTopUpExceedsMaximum from CTokenError)
{
context.token_account_keypair = Keypair::new();

// Default max_top_up is 6208, so use 6209 to exceed it
let excessive_lamports_per_write = RentConfig::default().max_top_up as u32 + 1;

let compressible_data = CompressibleData {
compression_authority: context.compression_authority,
rent_sponsor: context.rent_sponsor,
num_prepaid_epochs: 2,
lamports_per_write: Some(excessive_lamports_per_write),
account_version: light_ctoken_types::state::TokenDataVersion::ShaFlat,
compress_to_pubkey: false,
payer: payer_pubkey,
};

create_and_assert_token_account_fails(
&mut context,
compressible_data,
"write_top_up_exceeds_max_top_up",
18042, // WriteTopUpExceedsMaximum from CTokenError
)
.await;
}

// Test 8: Wrong account type (correct program owner, wrong discriminator)
// Passing an account owned by the registry program but not a CompressibleConfig.
// Using the protocol config account which has a different discriminator.
// Error: 2 (InvalidDiscriminator from account-checks)
Expand Down
36 changes: 34 additions & 2 deletions program-tests/compressed-token-test/tests/ctoken/create_ata.rs
Original file line number Diff line number Diff line change
Expand Up @@ -479,7 +479,38 @@ async fn test_create_ata_failing() {
light_program_test::utils::assert::assert_rpc_error(result, 0, 20001).unwrap();
}

// Test 7: Wrong account type (correct program owner, wrong discriminator)
// Test 7: write_top_up exceeds max_top_up from RentConfig
// Accounts cannot be created with lamports_per_write > max_top_up.
// This protects against griefing attacks where recipient creates account with excessive top-up.
// Error: 18042 (WriteTopUpExceedsMaximum from CTokenError)
{
// Use different mint for this test
context.mint_pubkey = solana_sdk::pubkey::Pubkey::new_unique();

// Default max_top_up is 6208, so use 6209 to exceed it
let excessive_lamports_per_write = RentConfig::default().max_top_up as u32 + 1;

let compressible_data = CompressibleData {
compression_authority: context.compression_authority,
rent_sponsor: context.rent_sponsor,
num_prepaid_epochs: 2,
lamports_per_write: Some(excessive_lamports_per_write),
account_version: light_ctoken_types::state::TokenDataVersion::ShaFlat,
compress_to_pubkey: false,
payer: payer_pubkey,
};

create_and_assert_ata_fails(
&mut context,
Some(compressible_data),
false, // Non-idempotent
"write_top_up_exceeds_max_top_up",
18042, // WriteTopUpExceedsMaximum from CTokenError
)
.await;
}

// Test 8: Wrong account type (correct program owner, wrong discriminator)
// Passing an account owned by the registry program but not a CompressibleConfig.
// Using the protocol config account which has a different discriminator.
// Error: 20000 (InvalidDiscriminator from account-checks)
Expand Down Expand Up @@ -749,7 +780,8 @@ async fn test_create_ata_random() {
}
},
lamports_per_write: if rng.gen_bool(0.5) {
Some(rng.gen_range(0..=u16::MAX as u32))
// Limit to max_top_up to avoid WriteTopUpExceedsMaximum error
Some(rng.gen_range(0..=RentConfig::default().max_top_up as u32))
} else {
None
},
Expand Down
76 changes: 76 additions & 0 deletions program-tests/compressed-token-test/tests/ctoken/transfer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,35 @@ fn build_transfer_instruction(
}
}

/// Build a ctoken transfer instruction with max_top_up parameter
fn build_transfer_instruction_with_max_top_up(
source: Pubkey,
destination: Pubkey,
amount: u64,
authority: Pubkey,
max_top_up: u16,
) -> solana_sdk::instruction::Instruction {
use anchor_lang::prelude::AccountMeta;
use solana_sdk::instruction::Instruction;

// Build instruction data: discriminator (3) + amount (8 bytes) + max_top_up (2 bytes)
let mut data = vec![3]; // CTokenTransfer discriminator
data.extend_from_slice(&amount.to_le_bytes()); // Amount as u64 little-endian
data.extend_from_slice(&max_top_up.to_le_bytes()); // max_top_up as u16 little-endian

// Build instruction
Instruction {
program_id: light_compressed_token::ID,
accounts: vec![
AccountMeta::new(source, false),
AccountMeta::new(destination, false),
AccountMeta::new(authority, true), // Authority must sign (also acts as payer for top-ups)
AccountMeta::new_readonly(Pubkey::default(), false), // System program for lamport transfers during top-up
],
data,
}
}

/// Execute a ctoken transfer and assert success
async fn transfer_and_assert(
context: &mut AccountTestContext,
Expand Down Expand Up @@ -498,3 +527,50 @@ async fn test_ctoken_transfer_mixed_compressible_non_compressible() {
)
.await;
}

// ============================================================================
// max_top_up Tests
// ============================================================================

/// Test that ctoken_transfer fails when max_top_up is exceeded.
/// Creates compressible accounts with num_prepaid_epochs = 0 (no prepaid rent),
/// which requires rent top-up on every write. Setting max_top_up = 1 (too low)
/// should trigger MaxTopUpExceeded error (18043).
#[tokio::test]
async fn test_ctoken_transfer_max_top_up_exceeded() {
// Create compressible accounts with num_prepaid_epochs = 0 (needs top-up immediately)
let (mut context, source, destination, _mint_amount, _source_keypair, _dest_keypair) =
setup_transfer_test(Some(0), 1000).await.unwrap();

// Fund owner to pay for potential top-up
context
.rpc
.airdrop_lamports(&context.owner_keypair.pubkey(), 100_000_000)
.await
.unwrap();

let owner_keypair = context.owner_keypair.insecure_clone();
let payer_pubkey = context.payer.pubkey();

// Build transfer instruction with max_top_up = 1 (too low to cover rent top-up)
let transfer_ix = build_transfer_instruction_with_max_top_up(
source,
destination,
100,
owner_keypair.pubkey(),
1, // max_top_up = 1 lamport (way too low for any rent top-up)
);

// Execute transfer expecting failure
let result = context
.rpc
.create_and_send_transaction(
&[transfer_ix],
&payer_pubkey,
&[&context.payer, &owner_keypair],
)
.await;

// Assert MaxTopUpExceeded (error code 18043)
light_program_test::utils::assert::assert_rpc_error(result, 0, 18043).unwrap();
}
Loading