diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 955a31b..0153faf 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,20 +15,20 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - name: Cache cargo registry, git, bin, and target + - name: Cache cargo registry, git, bin, and target dirs uses: actions/cache@v4 with: path: | - ~/.cargo/bin ~/.cargo/registry ~/.cargo/git + ~/.cargo/bin target tests/target key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} - name: Install nightly + rustfmt run: rustup update nightly && rustup default nightly && rustup component add rustfmt clippy - name: Install dylint - run: cargo install --locked cargo-dylint@5.0.0 dylint-link@5.0.0 + run: cargo install --locked --force cargo-dylint@5.0.0 dylint-link@5.0.0 - name: Format run: cargo fmt --check - name: Build diff --git a/Cargo.lock b/Cargo.lock index a2f6de0..85b049d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -791,6 +791,16 @@ dependencies = [ "dylint_testing", ] +[[package]] +name = "missing_mut_constraint" +version = "0.1.0" +dependencies = [ + "anchor-lints-utils", + "clippy_utils", + "dylint_linting", + "dylint_testing", +] + [[package]] name = "missing_owner_check" version = "0.1.0" diff --git a/README.md b/README.md index cf616bc..fcfc7f3 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,7 @@ cargo install cargo-dylint dylint-link | [`direct_lamport_cpi_dos`](lints/direct_lamport_cpi_dos) | | [`overconstrained_seed_account`](lints/overconstrained_seed_account) | | [`unsafe_pyth_price_account`](lints/unsafe_pyth_price_account) | +| [`missing_mut_constraint`](lints/missing_mut_constraint) | ## Usage @@ -68,4 +69,5 @@ cargo test ata_should_use_init_if_needed_tests cargo test direct_lamport_cpi_dos_tests cargo test overconstrained_seed_account_tests cargo test unsafe_pyth_price_account_tests +cargo test missing_mut_constraint_tests ``` diff --git a/anchor-lints-utils/src/mir_analyzer/account_extraction.rs b/anchor-lints-utils/src/mir_analyzer/account_extraction.rs index 43df17b..680f919 100644 --- a/anchor-lints-utils/src/mir_analyzer/account_extraction.rs +++ b/anchor-lints-utils/src/mir_analyzer/account_extraction.rs @@ -1,7 +1,7 @@ use clippy_utils::{source::HasSession, ty::is_type_diagnostic_item}; use rustc_middle::{ - mir::{HasLocalDecls, Local}, - ty::TyKind, + mir::{HasLocalDecls, Local, Place, Rvalue}, + ty::{Mutability, TyKind}, }; use rustc_span::source_map::Spanned; use rustc_span::sym; @@ -183,6 +183,43 @@ impl<'cx, 'tcx> MirAnalyzer<'cx, 'tcx> { ); account_name_and_locals.first().cloned() } + + /// Extracts the account name from a place or rvalue + pub fn account_name_from_place_or_rvalue( + &self, + place: &Place<'_>, + rvalue: &Rvalue<'_>, + ) -> Option { + let base_local = place.local; + let resolved = self.resolve_to_original_local(base_local, &mut HashSet::new()); + if let Some(acc) = self.extract_account_name_from_local(&resolved, true) { + let name = acc + .account_name + .split('.') + .next() + .unwrap_or(&acc.account_name) + .to_string(); + return Some(name); + } + + if let Rvalue::Ref(_, borrow_kind, ref_place) = rvalue + && borrow_kind.mutability() == Mutability::Mut + { + let base = ref_place.local; + let resolved = self.resolve_to_original_local(base, &mut HashSet::new()); + if let Some(acc) = self.extract_account_name_from_local(&resolved, true) { + let name = acc + .account_name + .split('.') + .next() + .unwrap_or(&acc.account_name) + .to_string(); + return Some(name); + } + } + + None + } } fn push_account_name_and_return( diff --git a/lints/arbitrary_cpi_call/README.md b/lints/arbitrary_cpi_call/README.md index 951de82..681e745 100644 --- a/lints/arbitrary_cpi_call/README.md +++ b/lints/arbitrary_cpi_call/README.md @@ -1,7 +1,7 @@ -# `arbitraty_cpi_call` +# `arbitrary_cpi_call` ### What it does -Identifies CPI calls made using user-controlled program IDs without validations. +Identifies CPI calls made using user-controlled program IDs (from accounts or parameters) without validations before the CPI call. ### Why is this bad? Unvalidated program IDs in CPI calls let users to trigger arbitrary programs, leading to potential security breaches or fund loss. @@ -12,4 +12,42 @@ To avoid heavy analysis, we skip nested function analysis when: - **Cmps/switches threshold:** The number of program_id comparisons or if/else switches in the current function exceeds `MAX_CMPS_SWITCHES_RECURSION_THRESHOLD`. - **If/else nesting level:** The current basic block is nested deeper than `MAX_IF_ELSE_NESTING_LEVEL` depth (number of dominating `SwitchInt` blocks). -When any one of the condition triggers, we still run CPI checks for the current function (e.g. we still report arbitrary CPI in that function). We only skip propagating validation from nested functions, so very large or deeply nested code may not get full inter-procedural analysis for now. \ No newline at end of file +When any one of the condition triggers, we still run CPI checks for the current function (e.g. we still report arbitrary CPI in that function). We only skip propagating validation from nested functions, so very large or deeply nested code may not get full inter-procedural analysis for now. + +### Example (worst case) + +```rust +// BAD: program_id is user-controlled and never validated +pub fn invoke_unchecked_program(ctx: Context) -> Result<()> { + use anchor_lang::solana_program::instruction::Instruction; + use anchor_lang::solana_program::program::invoke; + let instruction = Instruction { + program_id: ctx.accounts.unchecked_program.key(), // user can pass any program + accounts: vec![], + data: vec![], + }; + let account_infos = vec![ctx.accounts.unchecked_program.to_account_info()]; + invoke(&instruction, &account_infos)?; // CPI + Ok(()) +} + +// GOOD: program_id validated against a constant before CPI +pub fn invoke_validated_program(ctx: Context) -> Result<()> { + use anchor_lang::solana_program::instruction::Instruction; + use anchor_lang::solana_program::program::invoke; + const ALLOWED_PROGRAM_ID: Pubkey = Pubkey::new_from_array([42u8; 32]); + require_keys_eq!( + ctx.accounts.unchecked_program.key(), + ALLOWED_PROGRAM_ID, + CustomError::InvalidProgram + ); + let instruction = Instruction { + program_id: ctx.accounts.unchecked_program.key(), + accounts: vec![], + data: vec![], + }; + let account_infos = vec![ctx.accounts.unchecked_program.to_account_info()]; + invoke(&instruction, &account_infos)?; // CPI + Ok(()) +} +``` \ No newline at end of file diff --git a/lints/arbitrary_cpi_call/src/lib.rs b/lints/arbitrary_cpi_call/src/lib.rs index 0a057f8..af5c90b 100644 --- a/lints/arbitrary_cpi_call/src/lib.rs +++ b/lints/arbitrary_cpi_call/src/lib.rs @@ -182,7 +182,7 @@ fn analyze_arbitrary_cpi_call<'tcx>( let mut switches: Vec = Vec::new(); let mut program_id_cmps: Vec = Vec::new(); - let mut instruction_to_program_id: HashMap = HashMap::new(); + let mut instruction_to_program_id: HashMap = HashMap::new(); for (bb, bbdata) in mir.basic_blocks.iter_enumerated() { for statement in &bbdata.statements { diff --git a/lints/arbitrary_cpi_call/src/utils.rs b/lints/arbitrary_cpi_call/src/utils.rs index 4b0fd61..19d4975 100644 --- a/lints/arbitrary_cpi_call/src/utils.rs +++ b/lints/arbitrary_cpi_call/src/utils.rs @@ -82,7 +82,7 @@ pub fn record_instruction_creation<'tcx>( mir_analyzer: &MirAnalyzer<'_, 'tcx>, bb: BasicBlock, statement: &Statement<'tcx>, - instruction_to_program_id: &mut HashMap, + instruction_to_program_id: &mut HashMap, ) { if let StatementKind::Assign(box (place, rvalue)) = &statement.kind && let Some(dest_local) = place.as_local() @@ -94,7 +94,7 @@ pub fn record_instruction_creation<'tcx>( && let Some(program_id_local) = place.as_local() && mir_analyzer.is_pubkey_type(program_id_local) { - instruction_to_program_id.insert(dest_local, bb); + instruction_to_program_id.insert(dest_local, (bb, program_id_local)); } } @@ -109,7 +109,7 @@ pub fn track_instruction_call<'tcx>( bb: BasicBlock, cpi_calls: &mut HashMap, cpi_contexts: &mut HashMap, - instruction_to_program_id: &HashMap, + instruction_to_program_id: &HashMap, ) { let mir = mir_analyzer.mir; let decl_ty = match mir @@ -128,9 +128,9 @@ pub fn track_instruction_call<'tcx>( let mut program_id_local = None; let mut program_id_bb = None; - if let Some(&pid) = instruction_to_program_id.get(&instruction_local) { - program_id_local = Some(instruction_local); - program_id_bb = Some(pid); + if let Some(&(pid_bb, actual_pid)) = instruction_to_program_id.get(&instruction_local) { + program_id_local = Some(actual_pid); + program_id_bb = Some(pid_bb); } else { let mut to_check = vec![instruction_local]; let mut visited = HashSet::new(); @@ -140,9 +140,9 @@ pub fn track_instruction_call<'tcx>( continue; } - if let Some(&pid) = instruction_to_program_id.get(¤t) { - program_id_local = Some(instruction_local); - program_id_bb = Some(pid); + if let Some(&(pid_bb, actual_pid)) = instruction_to_program_id.get(¤t) { + program_id_local = Some(actual_pid); + program_id_bb = Some(pid_bb); break; } diff --git a/lints/arbitrary_cpi_call/tests/test_program/Cargo.toml b/lints/arbitrary_cpi_call/tests/test_program/Cargo.toml index 693b456..d226a32 100644 --- a/lints/arbitrary_cpi_call/tests/test_program/Cargo.toml +++ b/lints/arbitrary_cpi_call/tests/test_program/Cargo.toml @@ -9,5 +9,5 @@ workspace = "../../../../tests" crate-type = ["cdylib"] [dependencies] -anchor-lang = { git = "https://github.com/jamie-osec/anchor", rev = "939b843" } -anchor-spl = { git = "https://github.com/jamie-osec/anchor", rev = "939b843" } +anchor-lang = { workspace = true } +anchor-spl = { workspace = true } diff --git a/lints/arbitrary_cpi_call/tests/test_program/src/lib.rs b/lints/arbitrary_cpi_call/tests/test_program/src/lib.rs index 8ad819f..1ed65e1 100644 --- a/lints/arbitrary_cpi_call/tests/test_program/src/lib.rs +++ b/lints/arbitrary_cpi_call/tests/test_program/src/lib.rs @@ -696,6 +696,98 @@ pub mod arbitrary_cpi_call_tests { ctx.accounts.cpi_call_unsafe(amount)?; Ok(()) } + + + // Case 36: Other CPI call(mint_to) with unchecked program ID - unsafe + pub fn cpi_mint_to_unchecked_program( + ctx: Context, + amount: u64, + ) -> Result<()> { + let cpi_accounts = anchor_spl::token::MintTo { + mint: ctx.accounts.mint.to_account_info(), + to: ctx.accounts.to.to_account_info(), + authority: ctx.accounts.authority.to_account_info(), + }; + let cpi_ctx = CpiContext::new(ctx.accounts.unchecked_program.key(), cpi_accounts); + anchor_spl::token::mint_to(cpi_ctx, amount)?; // [arbitrary_cpi_call] + Ok(()) + } + + // Case 37: Associated Token Program create_ata without validation - unsafe + pub fn cpi_create_ata_unchecked_program( + ctx: Context, + ) -> Result<()> { + use anchor_spl::associated_token::{self, Create}; + let cpi_accounts = Create { + payer: ctx.accounts.payer.to_account_info(), + associated_token: ctx.accounts.associated_token.to_account_info(), + authority: ctx.accounts.authority.to_account_info(), + mint: ctx.accounts.mint.to_account_info(), + system_program: ctx.accounts.system_program.to_account_info(), + token_program: ctx.accounts.token_program.to_account_info(), + }; + let cpi_ctx = CpiContext::new(ctx.accounts.unchecked_program.key(), cpi_accounts); + associated_token::create(cpi_ctx)?; // [arbitrary_cpi_call] + Ok(()) + } + + // Case 38: Associated Token Program create_ata with validated program ID - safe + pub fn cpi_create_ata_validated_program( + ctx: Context, + ) -> Result<()> { + use anchor_spl::associated_token::{self, Create}; + require_keys_eq!( + ctx.accounts.unchecked_program.key(), + anchor_spl::associated_token::ID, + CustomError::InvalidProgram + ); + let cpi_accounts = Create { + payer: ctx.accounts.payer.to_account_info(), + associated_token: ctx.accounts.associated_token.to_account_info(), + authority: ctx.accounts.authority.to_account_info(), + mint: ctx.accounts.mint.to_account_info(), + system_program: ctx.accounts.system_program.to_account_info(), + token_program: ctx.accounts.token_program.to_account_info(), + }; + let cpi_ctx = CpiContext::new(ctx.accounts.unchecked_program.key(), cpi_accounts); + associated_token::create(cpi_ctx)?; // [safe_cpi_call] + Ok(()) + } + + // Case 39: Invoke CPI with unchecked program ID - unsafe + pub fn cpi_custom_program_unchecked(ctx: Context) -> Result<()> { + use anchor_lang::solana_program::instruction::Instruction; + use anchor_lang::solana_program::program::invoke; + + let instruction = Instruction { + program_id: ctx.accounts.unchecked_program.key(), + accounts: vec![], + data: vec![], + }; + let account_infos = vec![ctx.accounts.unchecked_program.to_account_info()]; + invoke(&instruction, &account_infos)?; // [arbitrary_cpi_call] + Ok(()) + } + + // Case 40: Raw invoke with validated program ID - safe + pub fn cpi_custom_program_checked(ctx: Context) -> Result<()> { + use anchor_lang::solana_program::instruction::Instruction; + use anchor_lang::solana_program::program::invoke; + const VALIDATED_PROGRAM_ID: Pubkey = Pubkey::new_from_array([42u8; 32]); // Consider it as a constant program ID + require_keys_eq!( + ctx.accounts.unchecked_program.key(), + VALIDATED_PROGRAM_ID, + CustomError::InvalidProgram + ); + let instruction = Instruction { + program_id: ctx.accounts.unchecked_program.key(), + accounts: vec![], + data: vec![], + }; + let account_infos = vec![ctx.accounts.unchecked_program.to_account_info()]; + invoke(&instruction, &account_infos)?; // [safe_cpi_call] + Ok(()) + } } pub fn cpi_call_with_account<'info>( @@ -1023,6 +1115,32 @@ pub struct InnerAccount { pub data: u64, } + +#[derive(Accounts)] +pub struct CpiMintToAccounts<'info> { + pub mint: UncheckedAccount<'info>, + #[account(mut)] + pub to: UncheckedAccount<'info>, + pub authority: UncheckedAccount<'info>, + /// CHECK: Target program to validate + pub unchecked_program: UncheckedAccount<'info>, +} + +#[derive(Accounts)] +pub struct CpiCreateAtaAccounts<'info> { + #[account(mut)] + pub payer: UncheckedAccount<'info>, + #[account(mut)] + pub associated_token: UncheckedAccount<'info>, + pub authority: UncheckedAccount<'info>, + pub mint: UncheckedAccount<'info>, + pub system_program: Program<'info, System>, + pub token_program: Program<'info, anchor_spl::token::Token>, + /// CHECK: Target program to validate + pub unchecked_program: UncheckedAccount<'info>, +} + + #[error_code] pub enum CustomError { #[msg("Invalid program")] diff --git a/lints/ata_should_use_init_if_needed/tests/test_program/Cargo.toml b/lints/ata_should_use_init_if_needed/tests/test_program/Cargo.toml index 0e6a5ea..5e99e5a 100644 --- a/lints/ata_should_use_init_if_needed/tests/test_program/Cargo.toml +++ b/lints/ata_should_use_init_if_needed/tests/test_program/Cargo.toml @@ -9,6 +9,6 @@ workspace = "../../../../tests" crate-type = ["cdylib"] [dependencies] -anchor-lang = { git = "https://github.com/jamie-osec/anchor", rev = "939b843", features = ["init-if-needed"] } -anchor-spl = { git = "https://github.com/jamie-osec/anchor", rev = "939b843" } +anchor-lang = { workspace = true, features = ["init-if-needed"] } +anchor-spl = { workspace = true } diff --git a/lints/cpi_no_result/tests/test_program/Cargo.toml b/lints/cpi_no_result/tests/test_program/Cargo.toml index a10b8ea..717c1b7 100644 --- a/lints/cpi_no_result/tests/test_program/Cargo.toml +++ b/lints/cpi_no_result/tests/test_program/Cargo.toml @@ -9,6 +9,6 @@ workspace = "../../../../tests" crate-type = ["cdylib"] [dependencies] -anchor-lang = { git = "https://github.com/jamie-osec/anchor", rev = "939b843" } -anchor-spl = { git = "https://github.com/jamie-osec/anchor", rev = "939b843" } +anchor-lang = { workspace = true } +anchor-spl = { workspace = true } diff --git a/lints/direct_lamport_cpi_dos/tests/test_program/Cargo.toml b/lints/direct_lamport_cpi_dos/tests/test_program/Cargo.toml index 4624e15..bf2eed0 100644 --- a/lints/direct_lamport_cpi_dos/tests/test_program/Cargo.toml +++ b/lints/direct_lamport_cpi_dos/tests/test_program/Cargo.toml @@ -9,6 +9,6 @@ workspace = "../../../../tests" crate-type = ["cdylib"] [dependencies] -anchor-lang = { git = "https://github.com/jamie-osec/anchor", rev = "939b843" } -anchor-spl = { git = "https://github.com/jamie-osec/anchor", rev = "939b843" } +anchor-lang = { workspace = true } +anchor-spl = { workspace = true } diff --git a/lints/duplicate_mutable_accounts/tests/test_program/Cargo.toml b/lints/duplicate_mutable_accounts/tests/test_program/Cargo.toml index f91eacb..686a416 100644 --- a/lints/duplicate_mutable_accounts/tests/test_program/Cargo.toml +++ b/lints/duplicate_mutable_accounts/tests/test_program/Cargo.toml @@ -9,5 +9,5 @@ workspace = "../../../../tests" crate-type = ["cdylib"] [dependencies] -anchor-lang = { git = "https://github.com/jamie-osec/anchor", rev = "939b843" } -anchor-spl = { git = "https://github.com/jamie-osec/anchor", rev = "939b843" } +anchor-lang = { workspace = true } +anchor-spl = { workspace = true } diff --git a/lints/missing_account_field_init/tests/test_program/Cargo.toml b/lints/missing_account_field_init/tests/test_program/Cargo.toml index f85cacc..e0cb761 100644 --- a/lints/missing_account_field_init/tests/test_program/Cargo.toml +++ b/lints/missing_account_field_init/tests/test_program/Cargo.toml @@ -9,7 +9,7 @@ workspace = "../../../../tests" crate-type = ["cdylib"] [dependencies] -anchor-lang = { git = "https://github.com/jamie-osec/anchor.git", branch = "anchor-lint" } -anchor-spl = { git = "https://github.com/jamie-osec/anchor.git", branch = "anchor-lint" } +anchor-lang = { workspace = true } +anchor-spl = { workspace = true } diff --git a/lints/missing_account_reload/tests/test_program/Cargo.toml b/lints/missing_account_reload/tests/test_program/Cargo.toml index ad0bd3c..f6f1223 100644 --- a/lints/missing_account_reload/tests/test_program/Cargo.toml +++ b/lints/missing_account_reload/tests/test_program/Cargo.toml @@ -9,4 +9,4 @@ workspace = "../../../../tests" crate-type = ["cdylib"] [dependencies] -anchor-lang = { git = "https://github.com/jamie-osec/anchor", rev = "939b843" } +anchor-lang = { workspace = true } diff --git a/lints/missing_mut_constraint/Cargo.toml b/lints/missing_mut_constraint/Cargo.toml new file mode 100644 index 0000000..c2c4d7e --- /dev/null +++ b/lints/missing_mut_constraint/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "missing_mut_constraint" +version.workspace = true +edition.workspace = true +publish = false +description = "Detects accounts that are mutated in the instruction but not declared with #[account(mut)]" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +anchor-lints-utils.workspace = true +clippy_utils.workspace = true +dylint_linting.workspace = true + +[dev-dependencies] +dylint_testing.workspace = true + +[package.metadata.rust-analyzer] +rustc_private = true + +[lints] +workspace = true \ No newline at end of file diff --git a/lints/missing_mut_constraint/README.md b/lints/missing_mut_constraint/README.md new file mode 100644 index 0000000..2bf26f4 --- /dev/null +++ b/lints/missing_mut_constraint/README.md @@ -0,0 +1,36 @@ +# `missing_mut_constraint` + +### What it does +Detects when an account is mutated in the instruction body but not declared with `#[account(mut)]` in the Anchor accounts struct. + +### Why is this bad? +Mutating an account without the `mut` constraint can cause the runtime to reject the transaction or behave unexpectedly, because the account was not marked as writable. + +### Example + +**Bad:** account mutated without `#[account(mut)]` +```rust +#[derive(Accounts)] +pub struct Update<'info> { + pub vault: Account<'info, Vault>, // missing #[account(mut)] +} + +pub fn update(ctx: Context) -> Result<()> { + ctx.accounts.vault.amount += 1; // mutation + Ok(()) +} +``` + +**Good:** account has `#[account(mut)]` when mutated +```rust +#[derive(Accounts)] +pub struct Update<'info> { + #[account(mut)] + pub vault: Account<'info, Vault>, +} + +pub fn update(ctx: Context) -> Result<()> { + ctx.accounts.vault.amount += 1; + Ok(()) +} +``` \ No newline at end of file diff --git a/lints/missing_mut_constraint/src/lib.rs b/lints/missing_mut_constraint/src/lib.rs new file mode 100644 index 0000000..11be362 --- /dev/null +++ b/lints/missing_mut_constraint/src/lib.rs @@ -0,0 +1,163 @@ +#![feature(rustc_private)] +#![warn(unused_extern_crates)] +#![feature(box_patterns)] + +extern crate rustc_hir; +extern crate rustc_middle; +extern crate rustc_span; + +use anchor_lints_utils::{ + mir_analyzer::{AnchorContextInfo, MirAnalyzer}, + utils::{extract_account_constraints, should_skip_function}, +}; +use clippy_utils::diagnostics::span_lint; + +use rustc_hir::{Body as HirBody, FnDecl, def_id::LocalDefId, intravisit::FnKind}; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_middle::{mir::StatementKind, ty::TyKind}; +use rustc_span::Span; + +use std::collections::{HashMap, HashSet}; + +dylint_linting::impl_late_lint! { + /// ### What it does + /// Detects when an account is mutated in the instruction body but not declared + /// with `#[account(mut)]` in the Anchor accounts struct. + /// + /// ### Why is this bad? + /// Mutating an account without the `mut` constraint can cause the runtime to + /// reject the transaction or behave unexpectedly, as the account was not + /// marked as writable. + /// + /// ### Example + /// ```rust + /// #[derive(Accounts)] + /// pub struct Update<'info> { + /// pub vault: Account<'info, Vault>, // missing #[account(mut)] + /// } + /// pub fn update(ctx: Context) -> Result<()> { + /// ctx.accounts.vault.amount += 1; // mutation + /// Ok(()) + /// } + /// ``` + /// + /// ### Good + /// ```rust + /// #[derive(Accounts)] + /// pub struct Update<'info> { + /// #[account(mut)] + /// pub vault: Account<'info, Vault>, + /// } + /// ``` + pub MISSING_MUT_CONSTRAINT, + Warn, + "account is mutated but missing #[account(mut)]", + MissingMutConstraint +} + +#[derive(Default)] +pub struct MissingMutConstraint; + +struct AccountMutability { + span: Span, + mutable: bool, +} + +impl<'tcx> LateLintPass<'tcx> for MissingMutConstraint { + fn check_fn( + &mut self, + cx: &LateContext<'tcx>, + _kind: FnKind<'tcx>, + _: &FnDecl<'tcx>, + body: &HirBody<'tcx>, + main_fn_span: Span, + def_id: LocalDefId, + ) { + if should_skip_function(cx, main_fn_span, def_id) { + return; + } + + let mut mir_analyzer = MirAnalyzer::new(cx, body, def_id); + anchor_lints_utils::utils::ensure_anchor_context_initialized(&mut mir_analyzer, body); + + // Analyze functions that take Anchor context + let Some(anchor_context_info) = mir_analyzer.anchor_context_info.as_ref() else { + return; + }; + + analyze_missing_mut_constraint(cx, &mir_analyzer, anchor_context_info); + } +} + +fn analyze_missing_mut_constraint<'cx, 'tcx>( + cx: &'cx LateContext<'tcx>, + mir_analyzer: &MirAnalyzer<'cx, 'tcx>, + anchor_context_info: &AnchorContextInfo<'tcx>, +) { + let accounts_struct_ty = &anchor_context_info.anchor_context_account_type; + let TyKind::Adt(adt_def, _generics) = accounts_struct_ty.kind() else { + return; + }; + + if !adt_def.is_struct() && !adt_def.is_union() { + return; + } + + let variant = adt_def.non_enum_variant(); + let mut account_mutability: HashMap = HashMap::new(); + + for field in &variant.fields { + let account_name = field.ident(cx.tcx).to_string(); + let account_span = cx.tcx.def_span(field.did); + let constraints = extract_account_constraints(cx, field); + account_mutability.insert( + account_name, + AccountMutability { + span: account_span, + mutable: constraints.mutable, + }, + ); + } + + let mutated_accounts = collect_mutated_accounts(mir_analyzer); + let mut visited = HashSet::new(); + + for account_name in mutated_accounts { + if visited.contains(&account_name) { + continue; + } + visited.insert(account_name.clone()); + + if let Some(info) = account_mutability.get(&account_name) + && !info.mutable + { + span_lint( + cx, + MISSING_MUT_CONSTRAINT, + info.span, + format!( + "account `{}` is mutated in the instruction but is not declared with `#[account(mut)]`", + account_name + ), + ); + } + } +} + +/// Collects the accounts that are mutated in the instruction body +fn collect_mutated_accounts<'cx, 'tcx>(mir_analyzer: &MirAnalyzer<'cx, 'tcx>) -> HashSet { + let mut mutated = HashSet::new(); + + for (_bb, bbdata) in mir_analyzer.mir.basic_blocks.iter_enumerated() { + for stmt in &bbdata.statements { + if let StatementKind::Assign(box (place, rvalue)) = &stmt.kind + && let Some(account_name) = + mir_analyzer.account_name_from_place_or_rvalue(place, rvalue) + { + mutated.insert(account_name); + } + } + } + + mutated +} diff --git a/lints/missing_mut_constraint/tests/test_program/Cargo.toml b/lints/missing_mut_constraint/tests/test_program/Cargo.toml new file mode 100644 index 0000000..6acb062 --- /dev/null +++ b/lints/missing_mut_constraint/tests/test_program/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "missing_mut_constraint_test_program" +version = "0.1.0" +edition = "2021" +workspace = "../../../../tests" + + +[lib] +crate-type = ["cdylib"] + +[dependencies] +anchor-lang = { workspace = true } +anchor-spl = { workspace = true } \ No newline at end of file diff --git a/lints/missing_mut_constraint/tests/test_program/src/lib.rs b/lints/missing_mut_constraint/tests/test_program/src/lib.rs new file mode 100644 index 0000000..cfa144c --- /dev/null +++ b/lints/missing_mut_constraint/tests/test_program/src/lib.rs @@ -0,0 +1,76 @@ +use anchor_lang::prelude::*; + +declare_id!("11111111111111111111111111111111"); + +#[program] +pub mod missing_mut_constraint { + use super::*; + + // Bad: Account (vault) is mutated but not declared with #[account(mut)] + pub fn update_bad(ctx: Context) -> Result<()> { + ctx.accounts.vault.amount += 1; + Ok(()) + } + + // Good: Account (vault) has #[account(mut)] + pub fn update_good(ctx: Context) -> Result<()> { + ctx.accounts.vault.amount += 1; + Ok(()) + } + + // Good: Account (vault) is only read, no mut needed + pub fn read_only(ctx: Context) -> Result<()> { + let _ = ctx.accounts.vault.amount; + Ok(()) + } + + // Bad: Account (treasury) is mutated but not declared with #[account(mut)] (Account (vault) has mut) + pub fn transfer_bad(ctx: Context) -> Result<()> { + ctx.accounts.vault.amount -= 10; + ctx.accounts.treasury.amount += 10; + Ok(()) + } + + // Good: lamport mutation with #[account(mut)] on payer + pub fn pay_lamports(ctx: Context, amount: u64) -> Result<()> { + **ctx.accounts.payer.lamports.borrow_mut() -= amount; + **ctx.accounts.recipient.lamports.borrow_mut() += amount; + Ok(()) + } +} + +#[account] +pub struct Vault { + pub amount: u64, +} + +#[derive(Accounts)] +pub struct UpdateBad<'info> { + pub vault: Account<'info, Vault>, // [missing_mut_constraint] +} + +#[derive(Accounts)] +pub struct UpdateGood<'info> { + #[account(mut)] + pub vault: Account<'info, Vault>, +} + +#[derive(Accounts)] +pub struct ReadOnly<'info> { + pub vault: Account<'info, Vault>, +} + +#[derive(Accounts)] +pub struct TransferBad<'info> { + #[account(mut)] + pub vault: Account<'info, Vault>, + pub treasury: Account<'info, Vault>, // [missing_mut_constraint] +} + +#[derive(Accounts)] +pub struct PayLamports<'info> { + #[account(mut)] + pub payer: AccountInfo<'info>, + #[account(mut)] + pub recipient: AccountInfo<'info>, +} \ No newline at end of file diff --git a/lints/missing_owner_check/tests/test_program/Cargo.toml b/lints/missing_owner_check/tests/test_program/Cargo.toml index 73f7c99..99d8622 100644 --- a/lints/missing_owner_check/tests/test_program/Cargo.toml +++ b/lints/missing_owner_check/tests/test_program/Cargo.toml @@ -9,6 +9,6 @@ workspace = "../../../../tests" crate-type = ["cdylib"] [dependencies] -anchor-lang = { git = "https://github.com/jamie-osec/anchor", rev = "939b843" } -anchor-spl = { git = "https://github.com/jamie-osec/anchor", rev = "939b843" } +anchor-lang = { workspace = true } +anchor-spl = { workspace = true } mpl-token-metadata = "5.1.2-alpha.1" diff --git a/lints/missing_signer_validation/tests/test_program/Cargo.toml b/lints/missing_signer_validation/tests/test_program/Cargo.toml index b8105b0..3a5353e 100644 --- a/lints/missing_signer_validation/tests/test_program/Cargo.toml +++ b/lints/missing_signer_validation/tests/test_program/Cargo.toml @@ -9,6 +9,6 @@ workspace = "../../../../tests" crate-type = ["cdylib"] [dependencies] -anchor-lang = { git = "https://github.com/jamie-osec/anchor", rev = "939b843" } -anchor-spl = { git = "https://github.com/jamie-osec/anchor", rev = "939b843" } +anchor-lang = { workspace = true } +anchor-spl = { workspace = true } diff --git a/lints/overconstrained_seed_account/tests/test_program/Cargo.toml b/lints/overconstrained_seed_account/tests/test_program/Cargo.toml index 81837a8..1ad8461 100644 --- a/lints/overconstrained_seed_account/tests/test_program/Cargo.toml +++ b/lints/overconstrained_seed_account/tests/test_program/Cargo.toml @@ -9,6 +9,6 @@ workspace = "../../../../tests" crate-type = ["cdylib"] [dependencies] -anchor-lang = { git = "https://github.com/jamie-osec/anchor.git", branch = "anchor-lint" } -anchor-spl = { git = "https://github.com/jamie-osec/anchor.git", branch = "anchor-lint" } +anchor-lang = { workspace = true } +anchor-spl = { workspace = true } diff --git a/lints/pda_signer_account_overlap/tests/test_program/Cargo.toml b/lints/pda_signer_account_overlap/tests/test_program/Cargo.toml index f8a188a..8807277 100644 --- a/lints/pda_signer_account_overlap/tests/test_program/Cargo.toml +++ b/lints/pda_signer_account_overlap/tests/test_program/Cargo.toml @@ -9,5 +9,5 @@ workspace = "../../../../tests" crate-type = ["cdylib"] [dependencies] -anchor-lang = { git = "https://github.com/jamie-osec/anchor.git", branch = "anchor-lint" } -anchor-spl = { git = "https://github.com/jamie-osec/anchor.git", branch = "anchor-lint" } +anchor-lang = { workspace = true } +anchor-spl = { workspace = true } diff --git a/tests/Cargo.lock b/tests/Cargo.lock index 360dffb..d31c05a 100644 --- a/tests/Cargo.lock +++ b/tests/Cargo.lock @@ -71,20 +71,9 @@ dependencies = [ [[package]] name = "anchor-attribute-access-control" version = "0.32.1" -source = "git+https://github.com/jamie-osec/anchor.git?branch=anchor-lint#939b843af4c330d37be45b34e5971e0d1a10c07e" +source = "git+https://github.com/jamie-osec/anchor.git?rev=939b843#939b843af4c330d37be45b34e5971e0d1a10c07e" dependencies = [ - "anchor-syn 0.32.1 (git+https://github.com/jamie-osec/anchor.git?branch=anchor-lint)", - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "anchor-attribute-access-control" -version = "0.32.1" -source = "git+https://github.com/jamie-osec/anchor?rev=939b843#939b843af4c330d37be45b34e5971e0d1a10c07e" -dependencies = [ - "anchor-syn 0.32.1 (git+https://github.com/jamie-osec/anchor?rev=939b843)", + "anchor-syn 0.32.1 (git+https://github.com/jamie-osec/anchor.git?rev=939b843)", "proc-macro2", "quote", "syn 1.0.109", @@ -106,20 +95,9 @@ dependencies = [ [[package]] name = "anchor-attribute-account" version = "0.32.1" -source = "git+https://github.com/jamie-osec/anchor.git?branch=anchor-lint#939b843af4c330d37be45b34e5971e0d1a10c07e" +source = "git+https://github.com/jamie-osec/anchor.git?rev=939b843#939b843af4c330d37be45b34e5971e0d1a10c07e" dependencies = [ - "anchor-syn 0.32.1 (git+https://github.com/jamie-osec/anchor.git?branch=anchor-lint)", - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "anchor-attribute-account" -version = "0.32.1" -source = "git+https://github.com/jamie-osec/anchor?rev=939b843#939b843af4c330d37be45b34e5971e0d1a10c07e" -dependencies = [ - "anchor-syn 0.32.1 (git+https://github.com/jamie-osec/anchor?rev=939b843)", + "anchor-syn 0.32.1 (git+https://github.com/jamie-osec/anchor.git?rev=939b843)", "proc-macro2", "quote", "syn 1.0.109", @@ -139,19 +117,9 @@ dependencies = [ [[package]] name = "anchor-attribute-constant" version = "0.32.1" -source = "git+https://github.com/jamie-osec/anchor.git?branch=anchor-lint#939b843af4c330d37be45b34e5971e0d1a10c07e" -dependencies = [ - "anchor-syn 0.32.1 (git+https://github.com/jamie-osec/anchor.git?branch=anchor-lint)", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "anchor-attribute-constant" -version = "0.32.1" -source = "git+https://github.com/jamie-osec/anchor?rev=939b843#939b843af4c330d37be45b34e5971e0d1a10c07e" +source = "git+https://github.com/jamie-osec/anchor.git?rev=939b843#939b843af4c330d37be45b34e5971e0d1a10c07e" dependencies = [ - "anchor-syn 0.32.1 (git+https://github.com/jamie-osec/anchor?rev=939b843)", + "anchor-syn 0.32.1 (git+https://github.com/jamie-osec/anchor.git?rev=939b843)", "quote", "syn 1.0.109", ] @@ -170,19 +138,9 @@ dependencies = [ [[package]] name = "anchor-attribute-error" version = "0.32.1" -source = "git+https://github.com/jamie-osec/anchor.git?branch=anchor-lint#939b843af4c330d37be45b34e5971e0d1a10c07e" +source = "git+https://github.com/jamie-osec/anchor.git?rev=939b843#939b843af4c330d37be45b34e5971e0d1a10c07e" dependencies = [ - "anchor-syn 0.32.1 (git+https://github.com/jamie-osec/anchor.git?branch=anchor-lint)", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "anchor-attribute-error" -version = "0.32.1" -source = "git+https://github.com/jamie-osec/anchor?rev=939b843#939b843af4c330d37be45b34e5971e0d1a10c07e" -dependencies = [ - "anchor-syn 0.32.1 (git+https://github.com/jamie-osec/anchor?rev=939b843)", + "anchor-syn 0.32.1 (git+https://github.com/jamie-osec/anchor.git?rev=939b843)", "quote", "syn 1.0.109", ] @@ -202,20 +160,9 @@ dependencies = [ [[package]] name = "anchor-attribute-event" version = "0.32.1" -source = "git+https://github.com/jamie-osec/anchor.git?branch=anchor-lint#939b843af4c330d37be45b34e5971e0d1a10c07e" +source = "git+https://github.com/jamie-osec/anchor.git?rev=939b843#939b843af4c330d37be45b34e5971e0d1a10c07e" dependencies = [ - "anchor-syn 0.32.1 (git+https://github.com/jamie-osec/anchor.git?branch=anchor-lint)", - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "anchor-attribute-event" -version = "0.32.1" -source = "git+https://github.com/jamie-osec/anchor?rev=939b843#939b843af4c330d37be45b34e5971e0d1a10c07e" -dependencies = [ - "anchor-syn 0.32.1 (git+https://github.com/jamie-osec/anchor?rev=939b843)", + "anchor-syn 0.32.1 (git+https://github.com/jamie-osec/anchor.git?rev=939b843)", "proc-macro2", "quote", "syn 1.0.109", @@ -241,25 +188,10 @@ dependencies = [ [[package]] name = "anchor-attribute-program" version = "0.32.1" -source = "git+https://github.com/jamie-osec/anchor.git?branch=anchor-lint#939b843af4c330d37be45b34e5971e0d1a10c07e" +source = "git+https://github.com/jamie-osec/anchor.git?rev=939b843#939b843af4c330d37be45b34e5971e0d1a10c07e" dependencies = [ - "anchor-lang-idl 0.1.2 (git+https://github.com/jamie-osec/anchor.git?branch=anchor-lint)", - "anchor-syn 0.32.1 (git+https://github.com/jamie-osec/anchor.git?branch=anchor-lint)", - "anyhow", - "heck", - "proc-macro2", - "quote", - "serde_json", - "syn 1.0.109", -] - -[[package]] -name = "anchor-attribute-program" -version = "0.32.1" -source = "git+https://github.com/jamie-osec/anchor?rev=939b843#939b843af4c330d37be45b34e5971e0d1a10c07e" -dependencies = [ - "anchor-lang-idl 0.1.2 (git+https://github.com/jamie-osec/anchor?rev=939b843)", - "anchor-syn 0.32.1 (git+https://github.com/jamie-osec/anchor?rev=939b843)", + "anchor-lang-idl 0.1.2 (git+https://github.com/jamie-osec/anchor.git?rev=939b843)", + "anchor-syn 0.32.1 (git+https://github.com/jamie-osec/anchor.git?rev=939b843)", "anyhow", "heck", "proc-macro2", @@ -282,19 +214,9 @@ dependencies = [ [[package]] name = "anchor-derive-accounts" version = "0.32.1" -source = "git+https://github.com/jamie-osec/anchor.git?branch=anchor-lint#939b843af4c330d37be45b34e5971e0d1a10c07e" -dependencies = [ - "anchor-syn 0.32.1 (git+https://github.com/jamie-osec/anchor.git?branch=anchor-lint)", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "anchor-derive-accounts" -version = "0.32.1" -source = "git+https://github.com/jamie-osec/anchor?rev=939b843#939b843af4c330d37be45b34e5971e0d1a10c07e" +source = "git+https://github.com/jamie-osec/anchor.git?rev=939b843#939b843af4c330d37be45b34e5971e0d1a10c07e" dependencies = [ - "anchor-syn 0.32.1 (git+https://github.com/jamie-osec/anchor?rev=939b843)", + "anchor-syn 0.32.1 (git+https://github.com/jamie-osec/anchor.git?rev=939b843)", "quote", "syn 1.0.109", ] @@ -315,20 +237,9 @@ dependencies = [ [[package]] name = "anchor-derive-serde" version = "0.32.1" -source = "git+https://github.com/jamie-osec/anchor.git?branch=anchor-lint#939b843af4c330d37be45b34e5971e0d1a10c07e" +source = "git+https://github.com/jamie-osec/anchor.git?rev=939b843#939b843af4c330d37be45b34e5971e0d1a10c07e" dependencies = [ - "anchor-syn 0.32.1 (git+https://github.com/jamie-osec/anchor.git?branch=anchor-lint)", - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "anchor-derive-serde" -version = "0.32.1" -source = "git+https://github.com/jamie-osec/anchor?rev=939b843#939b843af4c330d37be45b34e5971e0d1a10c07e" -dependencies = [ - "anchor-syn 0.32.1 (git+https://github.com/jamie-osec/anchor?rev=939b843)", + "anchor-syn 0.32.1 (git+https://github.com/jamie-osec/anchor.git?rev=939b843)", "proc-macro2", "quote", "syn 1.0.109", @@ -348,17 +259,7 @@ dependencies = [ [[package]] name = "anchor-derive-space" version = "0.32.1" -source = "git+https://github.com/jamie-osec/anchor.git?branch=anchor-lint#939b843af4c330d37be45b34e5971e0d1a10c07e" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "anchor-derive-space" -version = "0.32.1" -source = "git+https://github.com/jamie-osec/anchor?rev=939b843#939b843af4c330d37be45b34e5971e0d1a10c07e" +source = "git+https://github.com/jamie-osec/anchor.git?rev=939b843#939b843af4c330d37be45b34e5971e0d1a10c07e" dependencies = [ "proc-macro2", "quote", @@ -410,60 +311,17 @@ dependencies = [ [[package]] name = "anchor-lang" version = "0.32.1" -source = "git+https://github.com/jamie-osec/anchor.git?branch=anchor-lint#939b843af4c330d37be45b34e5971e0d1a10c07e" -dependencies = [ - "anchor-attribute-access-control 0.32.1 (git+https://github.com/jamie-osec/anchor.git?branch=anchor-lint)", - "anchor-attribute-account 0.32.1 (git+https://github.com/jamie-osec/anchor.git?branch=anchor-lint)", - "anchor-attribute-constant 0.32.1 (git+https://github.com/jamie-osec/anchor.git?branch=anchor-lint)", - "anchor-attribute-error 0.32.1 (git+https://github.com/jamie-osec/anchor.git?branch=anchor-lint)", - "anchor-attribute-event 0.32.1 (git+https://github.com/jamie-osec/anchor.git?branch=anchor-lint)", - "anchor-attribute-program 0.32.1 (git+https://github.com/jamie-osec/anchor.git?branch=anchor-lint)", - "anchor-derive-accounts 0.32.1 (git+https://github.com/jamie-osec/anchor.git?branch=anchor-lint)", - "anchor-derive-serde 0.32.1 (git+https://github.com/jamie-osec/anchor.git?branch=anchor-lint)", - "anchor-derive-space 0.32.1 (git+https://github.com/jamie-osec/anchor.git?branch=anchor-lint)", - "base64 0.21.7", - "bincode", - "borsh 1.6.0", - "bytemuck", - "const-crypto", - "solana-account-info 3.1.0", - "solana-clock 3.0.0", - "solana-cpi 3.1.0", - "solana-define-syscall 3.0.0", - "solana-feature-gate-interface 3.1.0", - "solana-instruction 3.1.0", - "solana-instructions-sysvar 3.0.0", - "solana-invoke 0.5.0", - "solana-loader-v3-interface 6.1.0", - "solana-msg 3.0.0", - "solana-program-entrypoint 3.1.1", - "solana-program-error 3.0.0", - "solana-program-memory 3.1.0", - "solana-program-option 3.0.0", - "solana-program-pack 3.0.0", - "solana-pubkey 4.0.0", - "solana-sdk-ids 3.1.0", - "solana-stake-interface 2.0.2", - "solana-system-interface 2.0.0", - "solana-sysvar 3.0.0", - "solana-sysvar-id 3.1.0", - "thiserror 1.0.69", -] - -[[package]] -name = "anchor-lang" -version = "0.32.1" -source = "git+https://github.com/jamie-osec/anchor?rev=939b843#939b843af4c330d37be45b34e5971e0d1a10c07e" -dependencies = [ - "anchor-attribute-access-control 0.32.1 (git+https://github.com/jamie-osec/anchor?rev=939b843)", - "anchor-attribute-account 0.32.1 (git+https://github.com/jamie-osec/anchor?rev=939b843)", - "anchor-attribute-constant 0.32.1 (git+https://github.com/jamie-osec/anchor?rev=939b843)", - "anchor-attribute-error 0.32.1 (git+https://github.com/jamie-osec/anchor?rev=939b843)", - "anchor-attribute-event 0.32.1 (git+https://github.com/jamie-osec/anchor?rev=939b843)", - "anchor-attribute-program 0.32.1 (git+https://github.com/jamie-osec/anchor?rev=939b843)", - "anchor-derive-accounts 0.32.1 (git+https://github.com/jamie-osec/anchor?rev=939b843)", - "anchor-derive-serde 0.32.1 (git+https://github.com/jamie-osec/anchor?rev=939b843)", - "anchor-derive-space 0.32.1 (git+https://github.com/jamie-osec/anchor?rev=939b843)", +source = "git+https://github.com/jamie-osec/anchor.git?rev=939b843#939b843af4c330d37be45b34e5971e0d1a10c07e" +dependencies = [ + "anchor-attribute-access-control 0.32.1 (git+https://github.com/jamie-osec/anchor.git?rev=939b843)", + "anchor-attribute-account 0.32.1 (git+https://github.com/jamie-osec/anchor.git?rev=939b843)", + "anchor-attribute-constant 0.32.1 (git+https://github.com/jamie-osec/anchor.git?rev=939b843)", + "anchor-attribute-error 0.32.1 (git+https://github.com/jamie-osec/anchor.git?rev=939b843)", + "anchor-attribute-event 0.32.1 (git+https://github.com/jamie-osec/anchor.git?rev=939b843)", + "anchor-attribute-program 0.32.1 (git+https://github.com/jamie-osec/anchor.git?rev=939b843)", + "anchor-derive-accounts 0.32.1 (git+https://github.com/jamie-osec/anchor.git?rev=939b843)", + "anchor-derive-serde 0.32.1 (git+https://github.com/jamie-osec/anchor.git?rev=939b843)", + "anchor-derive-space 0.32.1 (git+https://github.com/jamie-osec/anchor.git?rev=939b843)", "base64 0.21.7", "bincode", "borsh 1.6.0", @@ -510,22 +368,9 @@ dependencies = [ [[package]] name = "anchor-lang-idl" version = "0.1.2" -source = "git+https://github.com/jamie-osec/anchor.git?branch=anchor-lint#939b843af4c330d37be45b34e5971e0d1a10c07e" -dependencies = [ - "anchor-lang-idl-spec 0.1.0 (git+https://github.com/jamie-osec/anchor.git?branch=anchor-lint)", - "anyhow", - "heck", - "serde", - "serde_json", - "sha2 0.10.9", -] - -[[package]] -name = "anchor-lang-idl" -version = "0.1.2" -source = "git+https://github.com/jamie-osec/anchor?rev=939b843#939b843af4c330d37be45b34e5971e0d1a10c07e" +source = "git+https://github.com/jamie-osec/anchor.git?rev=939b843#939b843af4c330d37be45b34e5971e0d1a10c07e" dependencies = [ - "anchor-lang-idl-spec 0.1.0 (git+https://github.com/jamie-osec/anchor?rev=939b843)", + "anchor-lang-idl-spec 0.1.0 (git+https://github.com/jamie-osec/anchor.git?rev=939b843)", "anyhow", "heck", "serde", @@ -546,16 +391,7 @@ dependencies = [ [[package]] name = "anchor-lang-idl-spec" version = "0.1.0" -source = "git+https://github.com/jamie-osec/anchor.git?branch=anchor-lint#939b843af4c330d37be45b34e5971e0d1a10c07e" -dependencies = [ - "anyhow", - "serde", -] - -[[package]] -name = "anchor-lang-idl-spec" -version = "0.1.0" -source = "git+https://github.com/jamie-osec/anchor?rev=939b843#939b843af4c330d37be45b34e5971e0d1a10c07e" +source = "git+https://github.com/jamie-osec/anchor.git?rev=939b843#939b843af4c330d37be45b34e5971e0d1a10c07e" dependencies = [ "anyhow", "serde", @@ -579,23 +415,9 @@ dependencies = [ [[package]] name = "anchor-spl" version = "0.32.1" -source = "git+https://github.com/jamie-osec/anchor.git?branch=anchor-lint#939b843af4c330d37be45b34e5971e0d1a10c07e" +source = "git+https://github.com/jamie-osec/anchor.git?rev=939b843#939b843af4c330d37be45b34e5971e0d1a10c07e" dependencies = [ - "anchor-lang 0.32.1 (git+https://github.com/jamie-osec/anchor.git?branch=anchor-lint)", - "spl-associated-token-account-interface", - "spl-pod 0.7.1", - "spl-token-2022-interface", - "spl-token-group-interface 0.7.1", - "spl-token-interface", - "spl-token-metadata-interface 0.8.0", -] - -[[package]] -name = "anchor-spl" -version = "0.32.1" -source = "git+https://github.com/jamie-osec/anchor?rev=939b843#939b843af4c330d37be45b34e5971e0d1a10c07e" -dependencies = [ - "anchor-lang 0.32.1 (git+https://github.com/jamie-osec/anchor?rev=939b843)", + "anchor-lang 0.32.1 (git+https://github.com/jamie-osec/anchor.git?rev=939b843)", "spl-associated-token-account-interface", "spl-pod 0.7.1", "spl-token-2022-interface", @@ -625,24 +447,7 @@ dependencies = [ [[package]] name = "anchor-syn" version = "0.32.1" -source = "git+https://github.com/jamie-osec/anchor.git?branch=anchor-lint#939b843af4c330d37be45b34e5971e0d1a10c07e" -dependencies = [ - "anyhow", - "bs58", - "heck", - "proc-macro2", - "quote", - "serde", - "serde_json", - "sha2 0.10.9", - "syn 1.0.109", - "thiserror 1.0.69", -] - -[[package]] -name = "anchor-syn" -version = "0.32.1" -source = "git+https://github.com/jamie-osec/anchor?rev=939b843#939b843af4c330d37be45b34e5971e0d1a10c07e" +source = "git+https://github.com/jamie-osec/anchor.git?rev=939b843#939b843af4c330d37be45b34e5971e0d1a10c07e" dependencies = [ "anyhow", "bs58", @@ -666,8 +471,8 @@ checksum = "5f0e0fee31ef5ed1ba1316088939cea399010ed7731dba877ed44aeb407a75ea" name = "arbitrary_cpi_call_test_program" version = "0.1.0" dependencies = [ - "anchor-lang 0.32.1 (git+https://github.com/jamie-osec/anchor?rev=939b843)", - "anchor-spl 0.32.1 (git+https://github.com/jamie-osec/anchor?rev=939b843)", + "anchor-lang 0.32.1 (git+https://github.com/jamie-osec/anchor.git?rev=939b843)", + "anchor-spl 0.32.1 (git+https://github.com/jamie-osec/anchor.git?rev=939b843)", ] [[package]] @@ -686,8 +491,8 @@ checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" name = "ata_should_use_init_if_needed_test_program" version = "0.1.0" dependencies = [ - "anchor-lang 0.32.1 (git+https://github.com/jamie-osec/anchor?rev=939b843)", - "anchor-spl 0.32.1 (git+https://github.com/jamie-osec/anchor?rev=939b843)", + "anchor-lang 0.32.1 (git+https://github.com/jamie-osec/anchor.git?rev=939b843)", + "anchor-spl 0.32.1 (git+https://github.com/jamie-osec/anchor.git?rev=939b843)", ] [[package]] @@ -971,8 +776,8 @@ checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b" name = "cpi_no_result_test_program" version = "0.1.0" dependencies = [ - "anchor-lang 0.32.1 (git+https://github.com/jamie-osec/anchor?rev=939b843)", - "anchor-spl 0.32.1 (git+https://github.com/jamie-osec/anchor?rev=939b843)", + "anchor-lang 0.32.1 (git+https://github.com/jamie-osec/anchor.git?rev=939b843)", + "anchor-spl 0.32.1 (git+https://github.com/jamie-osec/anchor.git?rev=939b843)", ] [[package]] @@ -1127,16 +932,16 @@ dependencies = [ name = "direct_lamport_cpi_dos_test_program" version = "0.1.0" dependencies = [ - "anchor-lang 0.32.1 (git+https://github.com/jamie-osec/anchor?rev=939b843)", - "anchor-spl 0.32.1 (git+https://github.com/jamie-osec/anchor?rev=939b843)", + "anchor-lang 0.32.1 (git+https://github.com/jamie-osec/anchor.git?rev=939b843)", + "anchor-spl 0.32.1 (git+https://github.com/jamie-osec/anchor.git?rev=939b843)", ] [[package]] name = "duplicate_mutable_accounts_test_program" version = "0.1.0" dependencies = [ - "anchor-lang 0.32.1 (git+https://github.com/jamie-osec/anchor?rev=939b843)", - "anchor-spl 0.32.1 (git+https://github.com/jamie-osec/anchor?rev=939b843)", + "anchor-lang 0.32.1 (git+https://github.com/jamie-osec/anchor.git?rev=939b843)", + "anchor-spl 0.32.1 (git+https://github.com/jamie-osec/anchor.git?rev=939b843)", ] [[package]] @@ -1578,23 +1383,31 @@ dependencies = [ name = "missing_account_field_init_test_program" version = "0.1.0" dependencies = [ - "anchor-lang 0.32.1 (git+https://github.com/jamie-osec/anchor.git?branch=anchor-lint)", - "anchor-spl 0.32.1 (git+https://github.com/jamie-osec/anchor.git?branch=anchor-lint)", + "anchor-lang 0.32.1 (git+https://github.com/jamie-osec/anchor.git?rev=939b843)", + "anchor-spl 0.32.1 (git+https://github.com/jamie-osec/anchor.git?rev=939b843)", ] [[package]] name = "missing_account_reload_test_program" version = "0.1.0" dependencies = [ - "anchor-lang 0.32.1 (git+https://github.com/jamie-osec/anchor?rev=939b843)", + "anchor-lang 0.32.1 (git+https://github.com/jamie-osec/anchor.git?rev=939b843)", +] + +[[package]] +name = "missing_mut_constraint_test_program" +version = "0.1.0" +dependencies = [ + "anchor-lang 0.32.1 (git+https://github.com/jamie-osec/anchor.git?rev=939b843)", + "anchor-spl 0.32.1 (git+https://github.com/jamie-osec/anchor.git?rev=939b843)", ] [[package]] name = "missing_owner_check_test_program" version = "0.1.0" dependencies = [ - "anchor-lang 0.32.1 (git+https://github.com/jamie-osec/anchor?rev=939b843)", - "anchor-spl 0.32.1 (git+https://github.com/jamie-osec/anchor?rev=939b843)", + "anchor-lang 0.32.1 (git+https://github.com/jamie-osec/anchor.git?rev=939b843)", + "anchor-spl 0.32.1 (git+https://github.com/jamie-osec/anchor.git?rev=939b843)", "mpl-token-metadata", ] @@ -1602,8 +1415,8 @@ dependencies = [ name = "missing_signer_validation_test_program" version = "0.1.0" dependencies = [ - "anchor-lang 0.32.1 (git+https://github.com/jamie-osec/anchor?rev=939b843)", - "anchor-spl 0.32.1 (git+https://github.com/jamie-osec/anchor?rev=939b843)", + "anchor-lang 0.32.1 (git+https://github.com/jamie-osec/anchor.git?rev=939b843)", + "anchor-spl 0.32.1 (git+https://github.com/jamie-osec/anchor.git?rev=939b843)", ] [[package]] @@ -1754,8 +1567,8 @@ checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" name = "overconstrained_seed_account_test_program" version = "0.1.0" dependencies = [ - "anchor-lang 0.32.1 (git+https://github.com/jamie-osec/anchor.git?branch=anchor-lint)", - "anchor-spl 0.32.1 (git+https://github.com/jamie-osec/anchor.git?branch=anchor-lint)", + "anchor-lang 0.32.1 (git+https://github.com/jamie-osec/anchor.git?rev=939b843)", + "anchor-spl 0.32.1 (git+https://github.com/jamie-osec/anchor.git?rev=939b843)", ] [[package]] @@ -1794,8 +1607,8 @@ dependencies = [ name = "pda_signer_account_overlap_test_program" version = "0.1.0" dependencies = [ - "anchor-lang 0.32.1 (git+https://github.com/jamie-osec/anchor.git?branch=anchor-lint)", - "anchor-spl 0.32.1 (git+https://github.com/jamie-osec/anchor.git?branch=anchor-lint)", + "anchor-lang 0.32.1 (git+https://github.com/jamie-osec/anchor.git?rev=939b843)", + "anchor-spl 0.32.1 (git+https://github.com/jamie-osec/anchor.git?rev=939b843)", ] [[package]] diff --git a/tests/Cargo.toml b/tests/Cargo.toml index eb0a2d3..7b8312d 100644 --- a/tests/Cargo.toml +++ b/tests/Cargo.toml @@ -3,3 +3,7 @@ [workspace] members = ["../lints/*/tests/test_program"] resolver = "2" + +[workspace.dependencies] +anchor-lang = { git = "https://github.com/jamie-osec/anchor.git", rev = "939b843" } +anchor-spl = { git = "https://github.com/jamie-osec/anchor.git", rev = "939b843" } diff --git a/tests/lint_tests.rs b/tests/lint_tests.rs index 78db7a6..7bb334c 100644 --- a/tests/lint_tests.rs +++ b/tests/lint_tests.rs @@ -84,6 +84,11 @@ async fn unsafe_pyth_price_account_tests() -> Result<()> { run_unsafe_pyth_price_account_tests().await } +#[tokio::test] +async fn missing_mut_constraint_tests() -> Result<()> { + run_missing_mut_constraint_tests().await +} + async fn run_missing_account_reload_tests() -> Result<()> { let lint_root = PathBuf::from(env!("CARGO_MANIFEST_DIR")); let test_program = lint_root.join("lints/missing_account_reload/tests/test_program"); @@ -425,6 +430,17 @@ async fn run_unsafe_pyth_price_account_tests() -> Result<()> { .await } +async fn run_missing_mut_constraint_tests() -> Result<()> { + run_standard_lint_test( + "missing_mut_constraint", + &["missing_mut_constraint"], + "warning: account", + Some("is mutated in the instruction but is not declared with `#[account(mut)]`"), + "missing_mut_constraint", + ) + .await +} + // Recursively find all .rs files fn find_rust_files(dir: &Path) -> std::io::Result> { let mut files = Vec::new();