GHSA-725g-w329-g7qr: kora-lib Token-2022 Transfer Fee Not Deducted Vulnerability

MODERATE
Published
Advisory Summary

GHSA-725g-w329-g7qr is a MODERATE token-2022 transfer fee not deducted vulnerability (General Security Weakness) affecting kora-lib libraries in the crates.io ecosystem. The issue involves token-2022 transfer fee not deducted during payment verification. Remediation is available by upgrading affected packages to version 2.0.5.

GHSA-725g-w329-g7qr at a Glance

CVE Identifier N/A
GHSA Identifier GHSA-725g-w329-g7qr
Severity MODERATE
CVSS v3.1 Base Score N/A
CWE Classification General Security Weakness
Affected Product kora-lib
Package Ecosystem crates.io
Published Date
Last Updated

How to Fix GHSA-725g-w329-g7qr

To remediate this vulnerability, upgrade all affected kora-lib dependencies to a patched release. Both direct and transitive dependencies should be checked, as frameworks or security libraries may pull in vulnerable versions transitively.

kora-lib fixed: 2.0.5
Upgrade kora-lib to 2.0.5
Verify Direct and Transitive Dependencies

Run the following command in your project directory to verify that all resolved dependencies use the patched release:

inspect dependencies for kora-lib

How to Check if You Are Affected

To determine whether your application is affected by GHSA-725g-w329-g7qr, inspect your project dependencies for kora-lib:

inspect dependencies for kora-lib

Analyze the terminal output to locate any version of kora-lib in your dependency tree. If any resolved version matches the vulnerable ranges listed in the table below (for example, versions earlier than 2.0.5), your application is affected.

Note: Verify declared build manifests and deployed runtime container environments.

Technical Details

Vulnerability Mechanism & Technical Analysis

This vulnerability is classified under General Security Weakness. In kora-lib, software state validation or input boundaries are circumvented during processing. Review the official source advisory statement below for specific affected components, classes, and reproduction details.

Source Advisory Statement

## 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 ```rust // 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) } ``` For comparison, the transfer fee calculation exists elsewhere in the codebase and is used during fee estimation: ```rust // crates/lib/src/token/spl_token_2022.rs:165-198 pub fn calculate_transfer_fee( &self, amount: u64, current_epoch: u64, ) -> Result<Option<u64>, KoraError> { if let Some(fee_config) = self.get_transfer_fee() { let transfer_fee = if current_epoch >= u64::from(fee_config.newer_transfer_fee.epoch) { &fee_config.newer_transfer_fee } else { &fee_config.older_transfer_fee }; let basis_points = u16::from(transfer_fee.transfer_fee_basis_points); let maximum_fee = u64::from(transfer_fee.maximum_fee); let fee_amount = (amount as u128) .checked_mul(basis_points as u128) .and_then(|product| product.checked_div(10_000)) // ... Ok(Some(std::cmp::min(fee_amount, maximum_fee))) } else { Ok(None) } } ``` This function exists but is **never called** in `verify_token_payment()`. ## Proof of Concept ### Arithmetic Demonstration Given: - Token-2022 token with 5% transfer fee (500 basis points), whitelisted in `allowed_spl_paid_tokens` - Transaction fee cost: 5000 lamports equivalent - Token price: 1 token = 5 lamports **What should happen:** - User needs to pay 5000 lamports worth → 1000 tokens - Transfer fee on 1000 tokens at 5% = 50 tokens - Paymaster destination receives: 1000 - 50 = 950 tokens (worth 4750 lamports) - User should be required to pay MORE to cover the fee **What actually happens:** - User sends `transfer_checked` for `amount = 1000` tokens - `verify_token_payment()` calculates: 1000 tokens * 5 lamports/token = 5000 lamports - 5000 >= 5000 required → **payment verified as sufficient** - But paymaster only received 950 tokens (worth 4750 lamports) - **Paymaster lost 250 lamports on this transaction** **Over 1000 transactions:** Paymaster loses 250,000 lamports (0.25 SOL) ### Runnable Test (using existing test infrastructure) ```rust #[tokio::test] async fn test_token2022_transfer_fee_not_deducted_in_verification() { // Setup: Token-2022 mint with 10% transfer fee (1000 bps) let transfer_fee_config = create_transfer_fee_config( 1000, // 10% basis points u64::MAX, // no maximum fee cap ); let mint_pubkey = Pubkey::new_unique(); let mint_account = MintAccountMockBuilder::new() .with_decimals(6) .with_supply(1_000_000_000_000) .with_extension(ExtensionType::TransferFeeConfig) .build_token2022(); // User sends transfer_checked for 1,000,000 tokens (1 token at 6 decimals) let transfer_amount: u64 = 1_000_000; // What verify_token_payment credits: let credited_amount = transfer_amount; // = 1,000,000 // What the paymaster actually receives (after 10% on-chain fee): let actual_received = transfer_amount - (transfer_amount * 1000 / 10000); // = 900,000 // BUG: credited_amount (1,000,000) > actual_received (900,000) // Paymaster is credited 11.1% MORE than it actually receives assert!(credited_amount > actual_received); assert_eq!(credited_amount - actual_received, 100_000); // 100,000 token units lost // The financial loss per transaction = 10% of the payment amount // This is NOT a rounding error — it is a full percentage-based loss } ``` ## Impact - **Systematic Financial Loss:** The paymaster consistently credits more token value than it receives for every transaction paid with a transfer-fee-bearing Token-2022 token. - **Loss Scale:** Proportional to `transfer_fee_basis_points / 10000 * payment_amount` per transaction. For a token with 5% fee and 100 transactions/day at $1 each, that is $5/day or $1,825/year in losses. - **Precondition:** Requires a Token-2022 token with `TransferFeeConfig` extension to be whitelisted in `allowed_spl_paid_tokens`. The existing test infrastructure already creates such tokens (`TestAccountSetup::create_usdc_mint_2022()` with 100 bps / 1% fee). ## Recommendation Deduct the Token-2022 transfer fee before calculating the lamport value of the payment: ```rust // In verify_token_payment(), after extracting amount: let effective_amount = if *is_2022 { // Fetch the mint to check for TransferFeeConfig let mint_account = CacheUtil::get_account( rpc_client, &token_mint, false, ).await?; let mint_info = Token2022MintInfo::from_account_data(&mint_account.data)?; if let Ok(Some(fee)) = mint_info.calculate_transfer_fee( *amount, rpc_client.get_epoch_info().await?.epoch, ) { amount.saturating_sub(fee) } else { *amount } } else { *amount }; let lamport_value = TokenUtil::calculate_token_value_in_lamports( effective_amount, // Use post-fee amount &token_mint, config.validation.price_source.clone(), rpc_client, ) .await?; ``` ## References - `crates/lib/src/token/token.rs:529-654` — `verify_token_payment()` using raw amount - `crates/lib/src/token/spl_token_2022.rs:165-198` — `calculate_transfer_fee()` (exists but not called in verification) - `crates/lib/src/fee/fee.rs:174-204` — `analyze_payment_instructions()` (correctly accounts for transfer fee in estimation) - SPL Token-2022 specification: transfer fees are deducted from the transfer amount by the on-chain program

GHSA-725g-w329-g7qr Affected kora-lib Versions

1 component(s)
Fixed Versions:
2.0.5
Product / Package Ecosystem Affected Versions Fixed Version
kora-lib
crates.io
< 2.0.5
2.0.5

References

Frequently Asked Questions

What is GHSA-725g-w329-g7qr?
GHSA-725g-w329-g7qr is a security vulnerability in kora-lib involving Token-2022 Transfer Fee Not Deducted During Payment Verification under General Security Weakness.
Which kora-lib versions are affected?
Affected versions include releases prior to 2.0.5 across affected packages: kora-lib.
What version fixes GHSA-725g-w329-g7qr?
Upgrading to 2.0.5 (or the latest patched release for your respective branch) resolves the vulnerability.
Is kora-lib affected?
Yes, kora-lib is affected in releases prior to 2.0.5. Upgrade your dependency to 2.0.5 or later.
What CWE is associated with GHSA-725g-w329-g7qr?
GHSA-725g-w329-g7qr is classified under General Security Weakness.
How can I check crates.io dependencies for GHSA-725g-w329-g7qr?
Run the dependency tree command: 'inspect dependencies for kora-lib'. This command lists direct and transitive dependencies in your build configuration so you can verify if a vulnerable version is resolved.
Machine-readable OSV JSON OSV v1.5 Schema
Normalized schema output
{
  "schema_version" : "1.4.0",
  "id" : "GHSA-725g-w329-g7qr",
  "modified" : "2026-03-12T14:50:43Z",
  "published" : "2026-03-12T14:50:43Z",
  "aliases" : [ ],
  "summary" : "kora-lib: Token-2022 Transfer Fee Not Deducted During Payment Verification",
  "details" : "## Summary\n\nWhen 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.\n\n## Severity\n\n**High**\n\n## Affected Component\n\n- **File:** `crates/lib/src/token/token.rs`\n- **Function:** `verify_token_payment()`\n- **Lines:** 529–654 (specifically 633–639)\n\n## Root Cause\n\nIn `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.\n\nThe 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.\n\n## Vulnerable Code\n\n```rust\n// crates/lib/src/token/token.rs:529-654\npub async fn verify_token_payment(\n    transaction_resolved: &mut VersionedTransactionResolved,\n    rpc_client: &RpcClient,\n    required_lamports: u64,\n    expected_destination_owner: &Pubkey,\n) -> Result<bool, KoraError> {\n    let config = get_config()?;\n    let mut total_lamport_value = 0u64;\n\n    // ...\n\n    for instruction in transaction_resolved\n        .get_or_parse_spl_instructions()?\n        .get(&ParsedSPLInstructionType::SplTokenTransfer)\n        .unwrap_or(&vec![])\n    {\n        if let ParsedSPLInstructionData::SplTokenTransfer {\n            source_address,\n            destination_address,\n            mint,\n            amount,      // <-- This is the PRE-FEE amount from the instruction\n            is_2022,\n            ..\n        } = instruction\n        {\n            // ... destination validation ...\n\n            // LINE 633-639: Uses raw *amount without deducting transfer fee\n            let lamport_value = TokenUtil::calculate_token_value_in_lamports(\n                *amount,        // <-- BUG: Should be (amount - transfer_fee)\n                &token_mint,\n                config.validation.price_source.clone(),\n                rpc_client,\n            )\n            .await?;\n\n            total_lamport_value = total_lamport_value\n                .checked_add(lamport_value)\n                .ok_or_else(|| {\n                    KoraError::ValidationError(\"Payment accumulation overflow\".to_string())\n                })?;\n        }\n    }\n\n    Ok(total_lamport_value >= required_lamports)\n}\n```\n\nFor comparison, the transfer fee calculation exists elsewhere in the codebase and is used during fee estimation:\n\n```rust\n// crates/lib/src/token/spl_token_2022.rs:165-198\npub fn calculate_transfer_fee(\n    &self,\n    amount: u64,\n    current_epoch: u64,\n) -> Result<Option<u64>, KoraError> {\n    if let Some(fee_config) = self.get_transfer_fee() {\n        let transfer_fee = if current_epoch >= u64::from(fee_config.newer_transfer_fee.epoch) {\n            &fee_config.newer_transfer_fee\n        } else {\n            &fee_config.older_transfer_fee\n        };\n        let basis_points = u16::from(transfer_fee.transfer_fee_basis_points);\n        let maximum_fee = u64::from(transfer_fee.maximum_fee);\n        let fee_amount = (amount as u128)\n            .checked_mul(basis_points as u128)\n            .and_then(|product| product.checked_div(10_000))\n            // ...\n        Ok(Some(std::cmp::min(fee_amount, maximum_fee)))\n    } else {\n        Ok(None)\n    }\n}\n```\n\nThis function exists but is **never called** in `verify_token_payment()`.\n\n## Proof of Concept\n\n### Arithmetic Demonstration\n\nGiven:\n- Token-2022 token with 5% transfer fee (500 basis points), whitelisted in `allowed_spl_paid_tokens`\n- Transaction fee cost: 5000 lamports equivalent\n- Token price: 1 token = 5 lamports\n\n**What should happen:**\n- User needs to pay 5000 lamports worth → 1000 tokens\n- Transfer fee on 1000 tokens at 5% = 50 tokens\n- Paymaster destination receives: 1000 - 50 = 950 tokens (worth 4750 lamports)\n- User should be required to pay MORE to cover the fee\n\n**What actually happens:**\n- User sends `transfer_checked` for `amount = 1000` tokens\n- `verify_token_payment()` calculates: 1000 tokens * 5 lamports/token = 5000 lamports\n- 5000 >= 5000 required → **payment verified as sufficient**\n- But paymaster only received 950 tokens (worth 4750 lamports)\n- **Paymaster lost 250 lamports on this transaction**\n\n**Over 1000 transactions:** Paymaster loses 250,000 lamports (0.25 SOL)\n\n### Runnable Test (using existing test infrastructure)\n\n```rust\n#[tokio::test]\nasync fn test_token2022_transfer_fee_not_deducted_in_verification() {\n    // Setup: Token-2022 mint with 10% transfer fee (1000 bps)\n    let transfer_fee_config = create_transfer_fee_config(\n        1000,      // 10% basis points\n        u64::MAX,  // no maximum fee cap\n    );\n\n    let mint_pubkey = Pubkey::new_unique();\n    let mint_account = MintAccountMockBuilder::new()\n        .with_decimals(6)\n        .with_supply(1_000_000_000_000)\n        .with_extension(ExtensionType::TransferFeeConfig)\n        .build_token2022();\n\n    // User sends transfer_checked for 1,000,000 tokens (1 token at 6 decimals)\n    let transfer_amount: u64 = 1_000_000;\n\n    // What verify_token_payment credits:\n    let credited_amount = transfer_amount;  // = 1,000,000\n\n    // What the paymaster actually receives (after 10% on-chain fee):\n    let actual_received = transfer_amount - (transfer_amount * 1000 / 10000);  // = 900,000\n\n    // BUG: credited_amount (1,000,000) > actual_received (900,000)\n    // Paymaster is credited 11.1% MORE than it actually receives\n    assert!(credited_amount > actual_received);\n    assert_eq!(credited_amount - actual_received, 100_000); // 100,000 token units lost\n\n    // The financial loss per transaction = 10% of the payment amount\n    // This is NOT a rounding error — it is a full percentage-based loss\n}\n```\n\n## Impact\n\n- **Systematic Financial Loss:** The paymaster consistently credits more token value than it receives for every transaction paid with a transfer-fee-bearing Token-2022 token.\n- **Loss Scale:** Proportional to `transfer_fee_basis_points / 10000 * payment_amount` per transaction. For a token with 5% fee and 100 transactions/day at $1 each, that is $5/day or $1,825/year in losses.\n- **Precondition:** Requires a Token-2022 token with `TransferFeeConfig` extension to be whitelisted in `allowed_spl_paid_tokens`. The existing test infrastructure already creates such tokens (`TestAccountSetup::create_usdc_mint_2022()` with 100 bps / 1% fee).\n\n## Recommendation\n\nDeduct the Token-2022 transfer fee before calculating the lamport value of the payment:\n\n```rust\n// In verify_token_payment(), after extracting amount:\nlet effective_amount = if *is_2022 {\n    // Fetch the mint to check for TransferFeeConfig\n    let mint_account = CacheUtil::get_account(\n        rpc_client,\n        &token_mint,\n        false,\n    ).await?;\n    let mint_info = Token2022MintInfo::from_account_data(&mint_account.data)?;\n\n    if let Ok(Some(fee)) = mint_info.calculate_transfer_fee(\n        *amount,\n        rpc_client.get_epoch_info().await?.epoch,\n    ) {\n        amount.saturating_sub(fee)\n    } else {\n        *amount\n    }\n} else {\n    *amount\n};\n\nlet lamport_value = TokenUtil::calculate_token_value_in_lamports(\n    effective_amount,  // Use post-fee amount\n    &token_mint,\n    config.validation.price_source.clone(),\n    rpc_client,\n)\n.await?;\n```\n\n## References\n\n- `crates/lib/src/token/token.rs:529-654` — `verify_token_payment()` using raw amount\n- `crates/lib/src/token/spl_token_2022.rs:165-198` — `calculate_transfer_fee()` (exists but not called in verification)\n- `crates/lib/src/fee/fee.rs:174-204` — `analyze_payment_instructions()` (correctly accounts for transfer fee in estimation)\n- SPL Token-2022 specification: transfer fees are deducted from the transfer amount by the on-chain program",
  "severity" : [ ],
  "affected" : [ {
    "package" : {
      "ecosystem" : "crates.io",
      "name" : "kora-lib"
    },
    "ranges" : [ {
      "type" : "ECOSYSTEM",
      "events" : [ {
        "introduced" : "0"
      }, {
        "fixed" : "2.0.5"
      } ]
    } ]
  } ],
  "references" : [ {
    "type" : "WEB",
    "url" : "https://github.com/solana-foundation/kora/security/advisories/GHSA-725g-w329-g7qr"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/solana-foundation/kora/commit/8cbd8217ee505e6b37c63ef835ff095cfa8ab318"
  }, {
    "type" : "PACKAGE",
    "url" : "https://github.com/solana-foundation/kora"
  } ],
  "database_specific" : {
    "cwe_ids" : [ ],
    "severity" : "MODERATE",
    "github_reviewed" : true,
    "github_reviewed_at" : "2026-03-12T14:50:43Z",
    "nvd_published_at" : null
  }
}