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
38 changes: 37 additions & 1 deletion crates/lib/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ pub struct ValidationConfig {
pub fee_payer_policy: FeePayerPolicy,
#[serde(default)]
pub price: PriceConfig,
#[serde(default)]
#[serde(default, alias = "token2022")]
pub token_2022: Token2022Config,
/// Allow durable transactions (nonce-based). Default: false.
/// When false, rejects any transaction containing AdvanceNonceAccount instruction.
Expand Down Expand Up @@ -834,6 +834,42 @@ mod tests {
assert!(config.validation.token_2022.get_blocked_account_extensions().is_empty());
}

#[test]
fn test_token2022_config_parsing_alias_token2022_table() {
let wrong_key_alias_toml = r#"
[validation]
max_allowed_lamports = 1
max_signatures = 1
allowed_programs = []
allowed_tokens = []
allowed_spl_paid_tokens = []
disallowed_accounts = []
price_source = "Mock"

[validation.token2022]
blocked_mint_extensions = ["interest_bearing_config"]
blocked_account_extensions = ["memo_transfer"]

[kora]
rate_limit = 1
"#;

let config = crate::tests::toml_mock::create_invalid_config(wrong_key_alias_toml)
.expect("Config with [validation.token2022] alias should parse");

assert!(
config
.validation
.token_2022
.is_mint_extension_blocked(ExtensionType::InterestBearingConfig),
"InterestBearingConfig should be blocked via [validation.token2022] alias"
);
assert!(
config.validation.token_2022.is_account_extension_blocked(ExtensionType::MemoTransfer),
"MemoTransfer should be blocked via [validation.token2022] alias"
);
}

#[test]
fn test_token2022_extension_blocking_check() {
let config = ConfigBuilder::new()
Expand Down
7 changes: 7 additions & 0 deletions crates/lib/src/constant.rs
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,13 @@ pub mod instruction_indexes {
pub const FREEZE_AUTHORITY_INDEX: usize = 2;
}

pub mod spl_token_reallocate {
pub const REQUIRED_NUMBER_OF_ACCOUNTS: usize = 4;
pub const ACCOUNT_INDEX: usize = 0;
pub const PAYER_INDEX: usize = 1;
pub const OWNER_INDEX: usize = 3;
}

// ATA instruction indexes
pub mod ata_instruction_indexes {
pub const ATA_ADDRESS_INDEX: usize = 1;
Expand Down
48 changes: 47 additions & 1 deletion crates/lib/src/signer/bundle_signer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,16 @@ impl BundleSigner {
};

let fee_payer_position = resolved.find_signer_position(fee_payer)?;
resolved.transaction.signatures[fee_payer_position] = signature;
let signatures_len = resolved.transaction.signatures.len();
let signature_slot = match resolved.transaction.signatures.get_mut(fee_payer_position) {
Some(slot) => slot,
None => {
return Err(KoraError::InvalidTransaction(format!(
"Signer position {fee_payer_position} is out of bounds for signatures (len={signatures_len})"
)));
}
};
*signature_slot = signature;

Ok(())
}
Expand All @@ -91,6 +100,18 @@ mod tests {
VersionedTransactionResolved::from_kora_built_transaction(&versioned).unwrap()
}

fn create_test_resolved_with_unsigned_fee_payer_occurrence(
signer_like_fee_payer: &Pubkey,
) -> VersionedTransactionResolved {
let attacker_fee_payer = Keypair::new();
let instruction = transfer(&attacker_fee_payer.pubkey(), signer_like_fee_payer, 1000);
let message = Message::new(&[instruction], Some(&attacker_fee_payer.pubkey()));
let transaction = Transaction::new_unsigned(message);
let versioned = solana_sdk::transaction::VersionedTransaction::from(transaction);

VersionedTransactionResolved::from_kora_built_transaction(&versioned).unwrap()
}

#[tokio::test]
async fn test_sign_transaction_for_bundle_success() {
let fee_payer_keypair = Keypair::new();
Expand Down Expand Up @@ -143,6 +164,31 @@ mod tests {
assert!(matches!(result.unwrap_err(), KoraError::InvalidTransaction(_)));
}

#[tokio::test]
async fn test_sign_transaction_for_bundle_rejects_unsigned_fee_payer_occurrence() {
let fee_payer_keypair = Keypair::new();
let fee_payer = fee_payer_keypair.pubkey();

let external_signer = Signer::from_memory(&fee_payer_keypair.to_base58_string()).unwrap();
let signer = Arc::new(external_signer);

let blockhash = Some(Hash::new_unique());
let config = ConfigMockBuilder::new().build();

let mut resolved = create_test_resolved_with_unsigned_fee_payer_occurrence(&fee_payer);
let result = BundleSigner::sign_transaction_for_bundle(
&mut resolved,
&signer,
&fee_payer,
&blockhash,
&config,
)
.await;

assert!(result.is_err());
assert!(matches!(result.unwrap_err(), KoraError::InvalidTransaction(_)));
}

#[tokio::test]
async fn test_sign_transaction_for_bundle_signature_position() {
let fee_payer_keypair = Keypair::new();
Expand Down
21 changes: 20 additions & 1 deletion crates/lib/src/tests/account_mock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,8 @@ pub struct MintAccountMockBuilder {
rent_epoch: u64,
// Token2022-specific fields
extensions: Vec<ExtensionType>,
transfer_hook_authority: Option<Pubkey>,
transfer_hook_program_id: Option<Pubkey>,
}

impl Default for MintAccountMockBuilder {
Expand All @@ -326,6 +328,8 @@ impl MintAccountMockBuilder {
lamports: 0,
rent_epoch: DEFAULT_RENT_EPOCH,
extensions: Vec::new(),
transfer_hook_authority: None,
transfer_hook_program_id: None,
}
}

Expand Down Expand Up @@ -365,6 +369,16 @@ impl MintAccountMockBuilder {
self
}

pub fn with_transfer_hook_authority(mut self, authority: Option<Pubkey>) -> Self {
self.transfer_hook_authority = authority;
self
}

pub fn with_transfer_hook_program_id(mut self, program_id: Option<Pubkey>) -> Self {
self.transfer_hook_program_id = program_id;
self
}

/// Add an extension type (Token2022 only)
pub fn with_extension(mut self, extension: ExtensionType) -> Self {
if !self.extensions.contains(&extension) {
Expand Down Expand Up @@ -472,7 +486,12 @@ impl MintAccountMockBuilder {
)?;
}
ExtensionType::TransferHook => {
state.init_extension::<extension::transfer_hook::TransferHook>(true)?;
let transfer_hook =
state.init_extension::<extension::transfer_hook::TransferHook>(true)?;
transfer_hook.authority =
OptionalNonZeroPubkey::try_from(self.transfer_hook_authority)?;
transfer_hook.program_id =
OptionalNonZeroPubkey::try_from(self.transfer_hook_program_id)?;
}
// Add other extension types as needed
_ => {}
Expand Down
Loading
Loading