Skip to content

Commit cbf7225

Browse files
authored
feat: (PRO-255) add adversarial tests for fee payer policy violations… (#224)
**Cache issue for github action, tested locally successfully** * feat: (PRO-255) add adversarial tests for fee payer policy violations and other scenarios - Introduced new tests for fee payer policy violations, including SOL and SPL transfer restrictions. - Added adversarial test suite to validate fee payer behavior under restrictive policies. - Updated configuration files for testing scenarios and added new test cases for various fee payer policy violations. - Refactored existing test utilities to support new test cases and improved error assertions. * PR Fixes
1 parent ed44607 commit cbf7225

File tree

17 files changed

+815
-24
lines changed

17 files changed

+815
-24
lines changed

CLAUDE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ make test-integration
113113
- Tests: Payment address validation and wrong destination rejection
114114

115115
**Multi-Signer Tests**
116-
- Config: `tests/src/common/fixtures/multi-signers.toml`
116+
- Config: `tests/src/common/fixtures/signers-multi.toml`
117117
- Tests: Multiple signer configurations
118118

119119
**TypeScript Tests**

crates/lib/src/validator/transaction_validator.rs

Lines changed: 27 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -175,11 +175,11 @@ impl TransactionValidator {
175175
) -> Result<(), KoraError> {
176176
let system_instructions = transaction_resolved.get_or_parse_system_instructions()?;
177177

178-
let check_if_allowed = |address: &Pubkey, policy_allowed: bool| {
178+
let check_if_allowed = |address: &Pubkey, policy_allowed: bool, instruction_type: &str| {
179179
if *address == self.fee_payer_pubkey && !policy_allowed {
180-
return Err(KoraError::InvalidTransaction(
181-
"Fee payer cannot be used as source account".to_string(),
182-
));
180+
return Err(KoraError::InvalidTransaction(format!(
181+
"Fee payer cannot be used for '{instruction_type}'",
182+
)));
183183
}
184184
Ok(())
185185
};
@@ -189,15 +189,19 @@ impl TransactionValidator {
189189
system_instructions.get(&ParsedSystemInstructionType::SystemTransfer).unwrap_or(&vec![])
190190
{
191191
if let ParsedSystemInstructionData::SystemTransfer { sender, .. } = instruction {
192-
check_if_allowed(sender, self.fee_payer_policy.allow_sol_transfers)?;
192+
check_if_allowed(
193+
sender,
194+
self.fee_payer_policy.allow_sol_transfers,
195+
"System Transfer",
196+
)?;
193197
}
194198
}
195199

196200
for instruction in
197201
system_instructions.get(&ParsedSystemInstructionType::SystemAssign).unwrap_or(&vec![])
198202
{
199203
if let ParsedSystemInstructionData::SystemAssign { authority } = instruction {
200-
check_if_allowed(authority, self.fee_payer_policy.allow_assign)?;
204+
check_if_allowed(authority, self.fee_payer_policy.allow_assign, "System Assign")?;
201205
}
202206
}
203207

@@ -209,9 +213,17 @@ impl TransactionValidator {
209213
{
210214
if let ParsedSPLInstructionData::SplTokenTransfer { owner, is_2022, .. } = instruction {
211215
if *is_2022 {
212-
check_if_allowed(owner, self.fee_payer_policy.allow_token2022_transfers)?;
216+
check_if_allowed(
217+
owner,
218+
self.fee_payer_policy.allow_token2022_transfers,
219+
"Token2022 Token Transfer",
220+
)?;
213221
} else {
214-
check_if_allowed(owner, self.fee_payer_policy.allow_spl_transfers)?;
222+
check_if_allowed(
223+
owner,
224+
self.fee_payer_policy.allow_spl_transfers,
225+
"SPL Token Transfer",
226+
)?;
215227
}
216228
}
217229
}
@@ -220,23 +232,27 @@ impl TransactionValidator {
220232
spl_instructions.get(&ParsedSPLInstructionType::SplTokenApprove).unwrap_or(&vec![])
221233
{
222234
if let ParsedSPLInstructionData::SplTokenApprove { owner, .. } = instruction {
223-
check_if_allowed(owner, self.fee_payer_policy.allow_approve)?;
235+
check_if_allowed(owner, self.fee_payer_policy.allow_approve, "SPL Token Approve")?;
224236
}
225237
}
226238

227239
for instruction in
228240
spl_instructions.get(&ParsedSPLInstructionType::SplTokenBurn).unwrap_or(&vec![])
229241
{
230242
if let ParsedSPLInstructionData::SplTokenBurn { owner, .. } = instruction {
231-
check_if_allowed(owner, self.fee_payer_policy.allow_burn)?;
243+
check_if_allowed(owner, self.fee_payer_policy.allow_burn, "SPL Token Burn")?;
232244
}
233245
}
234246

235247
for instruction in
236248
spl_instructions.get(&ParsedSPLInstructionType::SplTokenCloseAccount).unwrap_or(&vec![])
237249
{
238250
if let ParsedSPLInstructionData::SplTokenCloseAccount { owner, .. } = instruction {
239-
check_if_allowed(owner, self.fee_payer_policy.allow_close_account)?;
251+
check_if_allowed(
252+
owner,
253+
self.fee_payer_policy.allow_close_account,
254+
"SPL Token Close Account",
255+
)?;
240256
}
241257
}
242258

tests/Cargo.toml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,14 @@ path = "tokens/main.rs"
2020
name = "auth"
2121
path = "auth/main.rs"
2222

23+
[[test]]
24+
name = "adversarial"
25+
path = "adversarial/main.rs"
26+
27+
[[test]]
28+
name = "fee_payer_policy"
29+
path = "fee_payer_policy/main.rs"
30+
2331
[[test]]
2432
name = "multi_signer"
2533
path = "multi_signer/main.rs"
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
use crate::common::{assertions::RpcErrorAssertions, *};
2+
use jsonrpsee::rpc_params;
3+
use solana_sdk::{signer::Signer, transaction::Transaction};
4+
use solana_system_interface::instruction::transfer;
5+
use spl_associated_token_account::get_associated_token_address;
6+
use spl_token::instruction as token_instruction;
7+
8+
#[tokio::test]
9+
async fn test_fee_payer_as_sol_transfer_source() {
10+
let ctx = TestContext::new().await.expect("Failed to create test context");
11+
let setup = TestAccountSetup::new().await;
12+
13+
let fee_payer_pubkey = FeePayerTestHelper::get_fee_payer_pubkey();
14+
15+
let large_sol_transfer = transfer(
16+
&fee_payer_pubkey,
17+
&setup.sender_keypair.pubkey(),
18+
1_000_000, // 0.001 SOL in lamports
19+
);
20+
21+
let malicious_tx = ctx
22+
.transaction_builder()
23+
.with_fee_payer(fee_payer_pubkey)
24+
.with_spl_payment(
25+
&setup.usdc_mint.pubkey(),
26+
&setup.sender_keypair.pubkey(),
27+
&fee_payer_pubkey,
28+
10, // Small payment: 0.00001 USDC tokens (much less than 0.001 SOL value)
29+
)
30+
.with_instruction(large_sol_transfer)
31+
.build()
32+
.await
33+
.expect("Failed to create transaction with fee payer as SOL source");
34+
35+
let result = ctx
36+
.rpc_call::<serde_json::Value, _>("signTransactionIfPaid", rpc_params![malicious_tx])
37+
.await;
38+
39+
match result {
40+
Err(error) => {
41+
error.assert_contains_message("Insufficient token payment");
42+
}
43+
Ok(_) => panic!("Expected error for fee payer as SOL transfer source"),
44+
}
45+
}
46+
47+
#[tokio::test]
48+
async fn test_fee_payer_as_spl_transfer_source() {
49+
let ctx = TestContext::new().await.expect("Failed to create test context");
50+
let setup = TestAccountSetup::new().await;
51+
52+
let fee_payer_pubkey = FeePayerTestHelper::get_fee_payer_pubkey();
53+
54+
let fee_payer_token_account =
55+
get_associated_token_address(&fee_payer_pubkey, &setup.usdc_mint.pubkey());
56+
let sender_token_account =
57+
get_associated_token_address(&setup.sender_keypair.pubkey(), &setup.usdc_mint.pubkey());
58+
59+
// Mint tokens for the fee payer
60+
setup
61+
.mint_tokens_to_account(&fee_payer_token_account, 100_000)
62+
.await
63+
.expect("Failed to mint tokens");
64+
65+
// Run the test transaction
66+
let large_token_transfer = token_instruction::transfer(
67+
&spl_token::id(),
68+
&fee_payer_token_account,
69+
&sender_token_account,
70+
&fee_payer_pubkey,
71+
&[&fee_payer_pubkey],
72+
100_000,
73+
)
74+
.expect("Failed to create token transfer instruction");
75+
76+
let malicious_tx = ctx
77+
.transaction_builder()
78+
.with_fee_payer(fee_payer_pubkey)
79+
.with_spl_payment(
80+
&setup.usdc_mint.pubkey(),
81+
&setup.sender_keypair.pubkey(),
82+
&fee_payer_pubkey,
83+
1_000, // Smaller than the 100,000 USDC transfer
84+
)
85+
.with_instruction(large_token_transfer)
86+
.build()
87+
.await
88+
.expect("Failed to create transaction with fee payer as USDC source");
89+
90+
let result = ctx
91+
.rpc_call::<serde_json::Value, _>("signTransactionIfPaid", rpc_params![malicious_tx])
92+
.await;
93+
94+
match result {
95+
Err(error) => {
96+
error.assert_contains_message("Insufficient token payment");
97+
}
98+
Ok(_) => panic!("Expected error for fee payer as USDC transfer source"),
99+
}
100+
}

tests/adversarial/main.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// Adversarial Basic Tests
2+
//
3+
// CONFIG: Uses tests/src/common/fixtures/kora-test.toml (permissive policies)
4+
// TESTS: Security and robustness testing with normal configuration
5+
// - Program validation attacks (disallowed programs)
6+
// - Invalid token states (frozen)
7+
// - Fee payer exploitation
8+
9+
mod fee_payer_exploitation;
10+
mod program_validation;
11+
mod token_states;
12+
13+
// Make common utilities available
14+
#[path = "../src/common/mod.rs"]
15+
mod common;
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
use crate::common::{assertions::RpcErrorAssertions, *};
2+
use jsonrpsee::rpc_params;
3+
use solana_sdk::{instruction::Instruction, pubkey::Pubkey};
4+
use std::str::FromStr;
5+
6+
#[tokio::test]
7+
async fn test_disallowed_memo_program() {
8+
let ctx = TestContext::new().await.expect("Failed to create test context");
9+
10+
let disallowed_program_id = Pubkey::from_str("MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr")
11+
.expect("Failed to parse SPL Memo program ID");
12+
13+
let malicious_instruction = Instruction::new_with_bincode(disallowed_program_id, &(), vec![]);
14+
15+
let malicious_tx = ctx
16+
.transaction_builder()
17+
.with_fee_payer(FeePayerTestHelper::get_fee_payer_pubkey())
18+
.with_instruction(malicious_instruction)
19+
.build()
20+
.await
21+
.expect("Failed to create transaction with disallowed program");
22+
23+
let result =
24+
ctx.rpc_call::<serde_json::Value, _>("signTransaction", rpc_params![malicious_tx]).await;
25+
26+
match result {
27+
Err(error) => {
28+
let expected_message =
29+
format!("Program {disallowed_program_id} is not in the allowed list");
30+
error.assert_error_type_and_message("Invalid transaction", &expected_message);
31+
}
32+
Ok(_) => panic!("Expected error for transaction with disallowed program"),
33+
}
34+
}
35+
36+
#[tokio::test]
37+
async fn test_disallowed_program_v0_transaction() {
38+
let ctx = TestContext::new().await.expect("Failed to create test context");
39+
40+
let disallowed_program_id = Pubkey::from_str("MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr")
41+
.expect("Failed to parse BPF Loader Upgradeable program ID");
42+
43+
let malicious_instruction = Instruction::new_with_bincode(disallowed_program_id, &(), vec![]);
44+
45+
let malicious_tx = ctx
46+
.v0_transaction_builder()
47+
.with_fee_payer(FeePayerTestHelper::get_fee_payer_pubkey())
48+
.with_instruction(malicious_instruction)
49+
.build()
50+
.await
51+
.expect("Failed to create V0 transaction with disallowed program");
52+
53+
let result =
54+
ctx.rpc_call::<serde_json::Value, _>("signTransaction", rpc_params![malicious_tx]).await;
55+
56+
match result {
57+
Err(error) => {
58+
let expected_message =
59+
format!("Program {disallowed_program_id} is not in the allowed list");
60+
error.assert_error_type_and_message("Invalid transaction", &expected_message);
61+
}
62+
Ok(_) => panic!("Expected error for V0 transaction with disallowed program"),
63+
}
64+
}

tests/adversarial/token_states.rs

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
use crate::common::{assertions::RpcErrorAssertions, *};
2+
use jsonrpsee::rpc_params;
3+
use solana_sdk::{
4+
program_pack::Pack, signature::Keypair, signer::Signer, transaction::Transaction,
5+
};
6+
use solana_system_interface::instruction::create_account;
7+
use spl_associated_token_account::get_associated_token_address;
8+
use spl_token::instruction as token_instruction;
9+
10+
#[tokio::test]
11+
async fn test_frozen_token_account_as_fee_payment() {
12+
let ctx = TestContext::new().await.expect("Failed to create test context");
13+
let setup = TestAccountSetup::new().await;
14+
15+
let frozen_token_account_keypair = Keypair::new();
16+
17+
let rent = setup
18+
.rpc_client
19+
.get_minimum_balance_for_rent_exemption(spl_token::state::Account::LEN)
20+
.await
21+
.expect("Failed to get rent exemption");
22+
23+
let create_account_ix = create_account(
24+
&setup.sender_keypair.pubkey(),
25+
&frozen_token_account_keypair.pubkey(),
26+
rent,
27+
spl_token::state::Account::LEN as u64,
28+
&spl_token::id(),
29+
);
30+
31+
let create_frozen_token_account_ix = spl_token::instruction::initialize_account(
32+
&spl_token::id(),
33+
&frozen_token_account_keypair.pubkey(),
34+
&setup.usdc_mint.pubkey(),
35+
&setup.sender_keypair.pubkey(),
36+
)
37+
.expect("Failed to create initialize account instruction");
38+
39+
let mint_tokens_ix = token_instruction::mint_to(
40+
&spl_token::id(),
41+
&setup.usdc_mint.pubkey(),
42+
&frozen_token_account_keypair.pubkey(),
43+
&setup.sender_keypair.pubkey(),
44+
&[&setup.sender_keypair.pubkey()],
45+
100_000,
46+
)
47+
.expect("Failed to create mint instruction");
48+
49+
let freeze_instruction = token_instruction::freeze_account(
50+
&spl_token::id(),
51+
&frozen_token_account_keypair.pubkey(),
52+
&setup.usdc_mint.pubkey(),
53+
&setup.sender_keypair.pubkey(),
54+
&[&setup.sender_keypair.pubkey()],
55+
)
56+
.expect("Failed to create freeze instruction");
57+
58+
let recent_blockhash = setup.rpc_client.get_latest_blockhash().await.unwrap();
59+
let setup_tx = Transaction::new_signed_with_payer(
60+
&[create_account_ix, create_frozen_token_account_ix, mint_tokens_ix, freeze_instruction],
61+
Some(&setup.sender_keypair.pubkey()),
62+
&[&setup.sender_keypair, &frozen_token_account_keypair],
63+
recent_blockhash,
64+
);
65+
66+
setup
67+
.rpc_client
68+
.send_and_confirm_transaction(&setup_tx)
69+
.await
70+
.expect("Failed to setup and freeze token account");
71+
72+
let malicious_tx = ctx
73+
.transaction_builder()
74+
.with_fee_payer(FeePayerTestHelper::get_fee_payer_pubkey())
75+
.with_spl_payment_with_accounts(
76+
&frozen_token_account_keypair.pubkey(),
77+
&get_associated_token_address(
78+
&FeePayerTestHelper::get_fee_payer_pubkey(),
79+
&setup.usdc_mint.pubkey(),
80+
),
81+
&setup.sender_keypair.pubkey(),
82+
50_000,
83+
)
84+
.build()
85+
.await
86+
.expect("Failed to create transaction with frozen fee payer token account");
87+
88+
let result =
89+
ctx.rpc_call::<serde_json::Value, _>("signTransaction", rpc_params![malicious_tx]).await;
90+
91+
match result {
92+
// 0x11: Frozen token account
93+
Err(error) => {
94+
error.assert_contains_message("custom program error: 0x11");
95+
}
96+
Ok(_) => panic!("Expected error for transaction with frozen fee payment account"),
97+
}
98+
}

0 commit comments

Comments
 (0)