Summary
When a user pays transaction fees using a Token-2022 token with a TransferFeeConfig extension, Kora's verify_token_payment() credits the full raw transfer amount as the payment value. However, the on-chain SPL Token-2022 program withholds a portion of that amount as a transfer fee, so the paymaster's destination account only receives amount - transfer_fee. This means the paymaster consistently credits more value than it actually receives, resulting in systematic financial loss.
Severity
High
Affected Component
- File:
crates/lib/src/token/token.rs
- Function:
verify_token_payment()
- Lines: 529–654 (specifically 633–639)
Root Cause
In verify_token_payment(), the amount extracted from the parsed SPL transfer instruction is the pre-fee amount (what the sender specifies in the transfer_checked instruction). The function passes this raw amount to calculate_token_value_in_lamports() to determine how many lamports the payment is worth. It never subtracts the Token-2022 transfer fee.
The fee estimation path (fee.rs:analyze_payment_instructions) correctly accounts for transfer fees by calculating them and adding them to the total fee. But the verification path does not perform the inverse subtraction, creating an asymmetry.
Vulnerable Code
// crates/lib/src/token/token.rs:529-654
pub async fn verify_token_payment(
transaction_resolved: &mut VersionedTransactionResolved,
rpc_client: &RpcClient,
required_lamports: u64,
expected_destination_owner: &Pubkey,
) -> Result<bool, KoraError> {
let config = get_config()?;
let mut total_lamport_value = 0u64;
// ...
for instruction in transaction_resolved
.get_or_parse_spl_instructions()?
.get(&ParsedSPLInstructionType::SplTokenTransfer)
.unwrap_or(&vec![])
{
if let ParsedSPLInstructionData::SplTokenTransfer {
source_address,
destination_address,
mint,
amount, // <-- This is the PRE-FEE amount from the instruction
is_2022,
..
} = instruction
{
// ... destination validation ...
// LINE 633-639: Uses raw *amount without deducting transfer fee
let lamport_value = TokenUtil::calculate_token_value_in_lamports(
*amount, // <-- BUG: Should be (amount - transfer_fee)
&token_mint,
config.validation.price_source.clone(),
rpc_client,
)
.await?;
total_lamport_value = total_lamport_value
.checked_add(lamport_value)
.ok_or_else(|| {
KoraError::ValidationError("Payment accumulation overflow".to_string())
})?;
}
}
Ok(total_lamport_value >= required_lamports)
}