verified-anchor 0.1.2

Formally verified (Lean 4) account-validation runtime for Solana — Anchor-compatible, proof-producing.
Documentation
//! M6 empirical suite: per scenario, naive(attacker)->Ok+badeffect; verified(attacker)->Err;
//! verified(legit)->Ok. Loads the verified-anchor-exploits BPF program.
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")); // rust/verified-anchor
    p.pop(); // rust/
    p.push("target/deploy/verified_anchor_exploits.so");
    p
}

/// The same fixed program id the exploits crate's `declare_id!` uses.
fn program_id() -> Pubkey {
    "VAExp11111111111111111111111111111111111111".parse().unwrap()
}

/// AMM_PROG constant — must match the one in verified-anchor-exploits/src/lib.rs.
#[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();

    // bank: any account (verified struct has no constraints on `bank`'s bytes).
    svm.set_account(bank, Account { lamports: 1, data: vec![], owner: program_id(), executable: false, rent_epoch: 0 }).unwrap();
    // out: program-owned, writable, 8 bytes.
    svm.set_account(out, Account { lamports: 1, data: vec![0u8; 8], owner: program_id(), executable: false, rent_epoch: 0 }).unwrap();
    // Fake collateral: wrong owner, wrong disc, wrong bank, HUGE amount.
    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();
    // Legit collateral: program-owned, real disc, right bank, modest amount.
    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),
    ];

    // 1) naive(attacker) -> Ok + observable bad effect (out.minted == u64::MAX)
    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");

    // Reset out for the next call's positive control.
    svm.set_account(out, Account { lamports: 1, data: vec![0u8; 8], owner: program_id(), executable: false, rent_epoch: 0 }).unwrap();

    // 2) verified(attacker) -> Err
    assert!(send(&mut svm, &payer, 1, metas_attacker).is_err(),
            "verified must reject the attacker accounts");

    // 3) verified(legit) -> Ok + correct effect (out.minted == 42)
    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();

    // out is program-owned writable.
    svm.set_account(out, Account { lamports: 1, data: vec![0u8; 8], owner: program_id(), executable: false, rent_epoch: 0 }).unwrap();

    // Attacker accounts:
    //   "vault" is actually a Config account whose offset-8 bytes equal the attacker's pubkey
    //   so the naive auth-by-bytes check passes; signer = attacker (payer).
    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),  // signer
        AccountMeta::new(out, false),
    ];

    // 1) naive(attacker) -> out[0] = 1 (authorized)
    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();

    // 2) verified(attacker) -> Err (discriminator mismatch)
    assert!(send(&mut svm, &payer, 3, metas_attacker).is_err(),
            "verified must reject the wrong-disc account");

    // 3) verified(legit) -> Ok + out[0] = 1
    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();

    // Attacker price: attacker-owned, fake price u64::MAX.
    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();
    // Legit price: AMM_PROG-owned, modest price.
    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)];

    // 1) naive(attacker) -> Ok + out.price == u64::MAX
    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();

    // 2) verified(attacker) -> Err (WrongOwner)
    assert!(send(&mut svm, &payer, 5, metas_a).is_err());

    // 3) verified(legit) -> Ok + out.price == 1000
    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();   // NOT the derived PDA

    // We must sign with both payer (fee payer) and user (signer in metas).
    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),  // signer
                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(|_| ())
    };

    // 1) naive(attacker) credits the attacker's account.
    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();
    // 2) verified(attacker) -> Err
    assert!(send_with_user(&mut svm, 7, attacker_vault).is_err());

    // 3) verified(legit PDA) -> Ok + credits the PDA.
    assert!(send_with_user(&mut svm, 7, pda).is_ok());
    assert_eq!(&svm.get_account(&out).unwrap().data[..32], &pda.to_bytes());
}