#36787 [SC-Insight] The vault program don't support token2022 transfer

Submitted on Nov 14th 2024 at 12:17:58 UTC by @Hoverfly9132 for Audit Comp | Jito Restaking

  • Report ID: #36787

  • Report Type: Smart Contract

  • Report severity: Insight

  • Target: https://github.com/jito-foundation/restaking/tree/master/vault_program

  • Impacts:

    • Smart contract unable to operate due to lack of token funds

Description

Bug Description

From the competition page we can know the vault program should support SPL token 2022:

The Vault and Restaking programs support the SPL Token and SPL Token 2022 standards.

But when calling process_mint() instruction, the token program id is hardcoded to be spl_token and not spl_token_2022. The spl token and spl token 2022 have different program id, the SPL token program id is TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA and SPL token 2022 program id is TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb, you can see the decalre for spl_token program id and spl_token_2022 program id.

pub fn process_mint(
    program_id: &Pubkey,
    accounts: &[AccountInfo],
    amount_in: u64,
    min_amount_out: u64,
) -> ProgramResult {
    ...
    // transfer tokens from depositor to vault
    {
        invoke(
            &transfer(
                // @audit - hardcoded spl token program id
                &spl_token::id(),
                depositor_token_account.key,
                vault_token_account.key,
                depositor.key,
                &[],
                amount_in,
            )?,
            &[
                depositor_token_account.clone(),
                vault_token_account.clone(),
                depositor.clone(),
            ],
        )?;
    }
    ...
}

If the users transfer SPL token2022 to the vault by calling the process_mint() instruction, the transfer will fail because wrong token program id.

And in the process_initialize_vault() instruction, the token program id is loaded by load_token_program(), this function will check the token program id is spl_token or not, if not, it will return an error:

pub fn load_token_program(info: &AccountInfo) -> Result<(), ProgramError> {
    if info.key.ne(&spl_token::id()) {
        msg!("Account is not the spl token program");
        return Err(ProgramError::IncorrectProgramId);
    }

    Ok(())
}

So the vault program doesn't support SPL token2022 init.

Impact

The vault program doesn't support SPL token 2022 init and transfer.

Recommendation

Add the SPL token2022 feature to the vault program.

Proof of Concept

Proof of Concept

Place the case in the vault_program/src/ path, run it by cargo test --package jito-vault-program --lib -- test_token2022_basic:

use solana_program::{
    pubkey::Pubkey,
    system_instruction,
};
use spl_token_2022::{
    instruction::{initialize_mint, initialize_account, mint_to, transfer_checked},
    state::{Mint, Account},
    extension::{ExtensionType, StateWithExtensions},
    ID as TOKEN_2022_ID,
};
use solana_program_test::*;
use solana_sdk::{
    signature::Keypair,
    signer::Signer,
    transaction::Transaction,
};

#[tokio::test]
async fn test_token2022_basic_transfer() {
    let mut program_test = ProgramTest::new(
        "spl_token_2022",
        TOKEN_2022_ID,
        processor!(spl_token_2022::processor::Processor::process),
    );
    program_test.prefer_bpf(false);

    let mut context = program_test.start_with_context().await;
    let payer = &context.payer;
    let mint_authority = Keypair::new();
    let mint = Keypair::new();
    let owner = Keypair::new();
    let recipient = Keypair::new();

    // Create a regular Token-2022 mint
    let mint_size = ExtensionType::try_calculate_account_len::<Mint>(&[]).unwrap();
    let mint_rent = context.banks_client.get_rent().await.unwrap().minimum_balance(mint_size);

    let transaction = Transaction::new_signed_with_payer(
        &[
            system_instruction::create_account(
                &payer.pubkey(),
                &mint.pubkey(),
                mint_rent,
                mint_size as u64,
                &TOKEN_2022_ID,
            ),
            initialize_mint(
                &TOKEN_2022_ID,
                &mint.pubkey(),
                &mint_authority.pubkey(),
                Some(&mint_authority.pubkey()),
                9,
            ).unwrap(),
        ],
        Some(&payer.pubkey()),
        &[payer, &mint],
        context.last_blockhash,
    );
    context.banks_client.process_transaction(transaction).await.unwrap();

    // Create accounts and mint tokens
    let account_size = ExtensionType::try_calculate_account_len::<Account>(&[]).unwrap();
    let rent: u64 = context.banks_client.get_rent().await.unwrap().minimum_balance(account_size);

    // Source account
    let source_account = Keypair::new();
    let transaction = Transaction::new_signed_with_payer(
        &[
            system_instruction::create_account(
                &payer.pubkey(),
                &source_account.pubkey(),
                rent,
                account_size as u64,
                &TOKEN_2022_ID,
            ),
            initialize_account(
                &TOKEN_2022_ID,
                &source_account.pubkey(),
                &mint.pubkey(),
                &owner.pubkey(),
            ).unwrap(),
        ],
        Some(&payer.pubkey()),
        &[payer, &source_account],
        context.last_blockhash,
    );
    context.banks_client.process_transaction(transaction).await.unwrap();

    // Mint tokens
    const MINT_AMOUNT: u64 = 1_000_000;
    let transaction = Transaction::new_signed_with_payer(
        &[mint_to(
            &TOKEN_2022_ID,
            &mint.pubkey(),
            &source_account.pubkey(),
            &mint_authority.pubkey(),
            &[],
            MINT_AMOUNT,
        ).unwrap()],
        Some(&payer.pubkey()),
        &[payer, &mint_authority],
        context.last_blockhash,
    );
    context.banks_client.process_transaction(transaction).await.unwrap();

    // Destination account
    let destination_account = Keypair::new();
    let transaction = Transaction::new_signed_with_payer(
        &[
            system_instruction::create_account(
                &payer.pubkey(),
                &destination_account.pubkey(),
                rent,
                account_size as u64,
                &TOKEN_2022_ID,
            ),
            initialize_account(
                &TOKEN_2022_ID,
                &destination_account.pubkey(),
                &mint.pubkey(),
                &recipient.pubkey(),
            ).unwrap(),
        ],
        Some(&payer.pubkey()),
        &[payer, &destination_account],
        context.last_blockhash,
    );
    context.banks_client.process_transaction(transaction).await.unwrap();

    // Transfer using Token-2022
    const TRANSFER_AMOUNT: u64 = 100_000;
    let transaction = Transaction::new_signed_with_payer(
        &[transfer_checked(
            &TOKEN_2022_ID,
            &source_account.pubkey(),
            &mint.pubkey(),
            &destination_account.pubkey(),
            &owner.pubkey(),
            &[],
            TRANSFER_AMOUNT,
            9,
        ).unwrap()],
        Some(&payer.pubkey()),
        &[payer, &owner],
        context.last_blockhash,
    );
    context.banks_client.process_transaction(transaction).await.unwrap();

    // Try SPL Token transfer (should fail)
    let spl_transfer = Transaction::new_signed_with_payer(
        &[spl_token::instruction::transfer(
            &spl_token::id(),
            &source_account.pubkey(),
            &destination_account.pubkey(),
            &owner.pubkey(),
            &[],
            TRANSFER_AMOUNT,
        ).unwrap()],
        Some(&payer.pubkey()),
        &[payer, &owner],
        context.last_blockhash,
    );
    let result = context.banks_client.process_transaction(spl_transfer).await;
    assert!(result.is_err());

    // Verify balances
    let source = context.banks_client.get_account(source_account.pubkey()).await.unwrap().unwrap();
    let destination = context.banks_client.get_account(destination_account.pubkey()).await.unwrap().unwrap();

    let source_token = StateWithExtensions::<Account>::unpack(&source.data).unwrap();
    let destination_token = StateWithExtensions::<Account>::unpack(&destination.data).unwrap();

    assert_eq!(source_token.base.amount, MINT_AMOUNT - TRANSFER_AMOUNT);
    assert_eq!(destination_token.base.amount, TRANSFER_AMOUNT);
}