use litesvm::LiteSVM;
use solana_account::Account;
use solana_instruction::{account_meta::AccountMeta, Instruction};
use solana_keypair::Keypair;
use solana_message::Message;
use solana_pubkey::Pubkey;
use solana_signer::Signer;
use solana_transaction::Transaction;
use std::path::PathBuf;
use sha2::{Digest, Sha256};
fn so_path() -> PathBuf {
let mut p = PathBuf::from(env!("CARGO_MANIFEST_DIR")); p.pop(); p.push("target/deploy/verified_anchor_exploits.so");
p
}
fn program_id() -> Pubkey {
"VAExp11111111111111111111111111111111111111".parse().unwrap()
}
#[allow(dead_code)]
fn amm_prog() -> Pubkey { Pubkey::new_from_array([0x0A; 32]) }
#[allow(dead_code)]
fn disc(name: &str) -> [u8; 8] {
let mut h = Sha256::new();
h.update(b"account:");
h.update(name.as_bytes());
let out = h.finalize();
let mut d = [0u8; 8];
d.copy_from_slice(&out[..8]);
d
}
fn fresh_svm() -> (LiteSVM, Keypair) {
let mut svm = LiteSVM::new();
svm.add_program_from_file(program_id(), so_path())
.expect("load .so (run cargo-build-sbf first)");
let payer = Keypair::new();
svm.airdrop(&payer.pubkey(), 10_000_000).unwrap();
(svm, payer)
}
fn send(svm: &mut LiteSVM, payer: &Keypair, tag: u8, metas: Vec<AccountMeta>) -> Result<(), ()> {
let ix = Instruction { program_id: program_id(), data: vec![tag], accounts: metas };
let bh = svm.latest_blockhash();
let tx = Transaction::new(&[payer], Message::new(&[ix], Some(&payer.pubkey())), bh);
svm.send_transaction(tx).map(|_| ()).map_err(|_| ())
}
fn build_collateral(disc8: [u8;8], bank: Pubkey, amount: u64) -> Vec<u8> {
let mut d = vec![0u8; 48];
d[0..8].copy_from_slice(&disc8);
d[8..40].copy_from_slice(&bank.to_bytes());
d[40..48].copy_from_slice(&amount.to_le_bytes());
d
}
#[test]
fn scenario_1_cashio_has_one_before_after() {
let (mut svm, payer) = fresh_svm();
let bank = Pubkey::new_unique();
let out = Pubkey::new_unique();
let attacker_coll = Pubkey::new_unique();
let legit_coll = Pubkey::new_unique();
let attacker_owner = Pubkey::new_unique();
svm.set_account(bank, Account { lamports: 1, data: vec![], owner: program_id(), executable: false, rent_epoch: 0 }).unwrap();
svm.set_account(out, Account { lamports: 1, data: vec![0u8; 8], owner: program_id(), executable: false, rent_epoch: 0 }).unwrap();
svm.set_account(attacker_coll, Account {
lamports: 1, data: build_collateral([0xFF; 8], Pubkey::new_unique(), u64::MAX),
owner: attacker_owner, executable: false, rent_epoch: 0,
}).unwrap();
svm.set_account(legit_coll, Account {
lamports: 1, data: build_collateral(disc("Collateral"), bank, 42),
owner: program_id(), executable: false, rent_epoch: 0,
}).unwrap();
let metas_attacker = vec![
AccountMeta::new_readonly(attacker_coll, false),
AccountMeta::new_readonly(bank, false),
AccountMeta::new(out, false),
];
let metas_legit = vec![
AccountMeta::new_readonly(legit_coll, false),
AccountMeta::new_readonly(bank, false),
AccountMeta::new(out, false),
];
assert!(send(&mut svm, &payer, 0, metas_attacker.clone()).is_ok(),
"naive must accept the attacker accounts (the bug)");
let out_data = svm.get_account(&out).unwrap().data;
assert_eq!(u64::from_le_bytes(out_data[..8].try_into().unwrap()), u64::MAX,
"naive must credit the attacker's HUGE amount");
svm.set_account(out, Account { lamports: 1, data: vec![0u8; 8], owner: program_id(), executable: false, rent_epoch: 0 }).unwrap();
assert!(send(&mut svm, &payer, 1, metas_attacker).is_err(),
"verified must reject the attacker accounts");
assert!(send(&mut svm, &payer, 1, metas_legit).is_ok(),
"verified must accept legit collateral");
let out_data = svm.get_account(&out).unwrap().data;
assert_eq!(u64::from_le_bytes(out_data[..8].try_into().unwrap()), 42,
"verified must process the legit amount");
}
fn build_typed_acct(disc8: [u8;8], field: Pubkey) -> Vec<u8> {
let mut d = vec![0u8; 40];
d[0..8].copy_from_slice(&disc8);
d[8..40].copy_from_slice(&field.to_bytes());
d
}
#[test]
fn scenario_2_type_confusion_discriminator_before_after() {
let (mut svm, payer) = fresh_svm();
let out = Pubkey::new_unique();
svm.set_account(out, Account { lamports: 1, data: vec![0u8; 8], owner: program_id(), executable: false, rent_epoch: 0 }).unwrap();
let attacker_vault = Pubkey::new_unique();
svm.set_account(attacker_vault, Account {
lamports: 1, data: build_typed_acct(disc("Config"), payer.pubkey()),
owner: program_id(), executable: false, rent_epoch: 0,
}).unwrap();
let metas_attacker = vec![
AccountMeta::new_readonly(attacker_vault, false),
AccountMeta::new_readonly(payer.pubkey(), true), AccountMeta::new(out, false),
];
assert!(send(&mut svm, &payer, 2, metas_attacker.clone()).is_ok(),
"naive must accept the type-confused account");
assert_eq!(svm.get_account(&out).unwrap().data[0], 1,
"naive must set the authorized flag");
svm.set_account(out, Account { lamports: 1, data: vec![0u8; 8], owner: program_id(), executable: false, rent_epoch: 0 }).unwrap();
assert!(send(&mut svm, &payer, 3, metas_attacker).is_err(),
"verified must reject the wrong-disc account");
let legit_vault = Pubkey::new_unique();
svm.set_account(legit_vault, Account {
lamports: 1, data: build_typed_acct(disc("Vault"), payer.pubkey()),
owner: program_id(), executable: false, rent_epoch: 0,
}).unwrap();
let metas_legit = vec![
AccountMeta::new_readonly(legit_vault, false),
AccountMeta::new_readonly(payer.pubkey(), true),
AccountMeta::new(out, false),
];
assert!(send(&mut svm, &payer, 3, metas_legit).is_ok(),
"verified must accept legit Vault");
assert_eq!(svm.get_account(&out).unwrap().data[0], 1);
}
#[test]
fn scenario_3_crema_owner_before_after() {
let (mut svm, payer) = fresh_svm();
let out = Pubkey::new_unique();
svm.set_account(out, Account { lamports: 1, data: vec![0u8; 8], owner: program_id(), executable: false, rent_epoch: 0 }).unwrap();
let attacker_price = Pubkey::new_unique();
let attacker_owner = Pubkey::new_unique();
svm.set_account(attacker_price, Account {
lamports: 1, data: u64::MAX.to_le_bytes().to_vec(),
owner: attacker_owner, executable: false, rent_epoch: 0,
}).unwrap();
let legit_price = Pubkey::new_unique();
svm.set_account(legit_price, Account {
lamports: 1, data: 1_000_u64.to_le_bytes().to_vec(),
owner: amm_prog(), executable: false, rent_epoch: 0,
}).unwrap();
let metas_a = vec![AccountMeta::new_readonly(attacker_price, false), AccountMeta::new(out, false)];
let metas_l = vec![AccountMeta::new_readonly(legit_price, false), AccountMeta::new(out, false)];
assert!(send(&mut svm, &payer, 4, metas_a.clone()).is_ok());
assert_eq!(u64::from_le_bytes(svm.get_account(&out).unwrap().data[..8].try_into().unwrap()), u64::MAX);
svm.set_account(out, Account { lamports: 1, data: vec![0u8; 8], owner: program_id(), executable: false, rent_epoch: 0 }).unwrap();
assert!(send(&mut svm, &payer, 5, metas_a).is_err());
assert!(send(&mut svm, &payer, 5, metas_l).is_ok());
assert_eq!(u64::from_le_bytes(svm.get_account(&out).unwrap().data[..8].try_into().unwrap()), 1_000);
}
#[test]
fn scenario_4_seeds_pda_before_after() {
let (mut svm, payer) = fresh_svm();
let user = Keypair::new();
svm.airdrop(&user.pubkey(), 1_000_000).unwrap();
let out = Pubkey::new_unique();
svm.set_account(out, Account { lamports: 1, data: vec![0u8; 32], owner: program_id(), executable: false, rent_epoch: 0 }).unwrap();
let (pda, _bump) = Pubkey::find_program_address(&[b"vault", &user.pubkey().to_bytes()], &program_id());
let attacker_vault = Pubkey::new_unique();
let send_with_user = |svm: &mut LiteSVM, tag: u8, vault: Pubkey| -> Result<(), ()> {
let ix = Instruction {
program_id: program_id(), data: vec![tag],
accounts: vec![
AccountMeta::new_readonly(user.pubkey(), true), AccountMeta::new_readonly(vault, false),
AccountMeta::new(out, false),
],
};
let bh = svm.latest_blockhash();
let tx = Transaction::new(&[&payer, &user], Message::new(&[ix], Some(&payer.pubkey())), bh);
svm.send_transaction(tx).map(|_| ()).map_err(|_| ())
};
assert!(send_with_user(&mut svm, 6, attacker_vault).is_ok());
assert_eq!(&svm.get_account(&out).unwrap().data[..32], &attacker_vault.to_bytes());
svm.set_account(out, Account { lamports: 1, data: vec![0u8; 32], owner: program_id(), executable: false, rent_epoch: 0 }).unwrap();
assert!(send_with_user(&mut svm, 7, attacker_vault).is_err());
assert!(send_with_user(&mut svm, 7, pda).is_ok());
assert_eq!(&svm.get_account(&out).unwrap().data[..32], &pda.to_bytes());
}