Skip to content

Commit b63fcbe

Browse files
committed
feat(missing_account_reload): LazyAccount support
1 parent 6349a34 commit b63fcbe

4 files changed

Lines changed: 114 additions & 11 deletions

File tree

anchor-lints-utils/src/diag_items.rs

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,8 @@ pub enum DiagnoticItem {
7878
SplTokenAccount,
7979
/// `spl_token::state::Mint`
8080
SplTokenMint,
81+
/// `anchor_lang::prelude::LazyAccount`
82+
AnchorLazyAccount,
8183
}
8284

8385
impl DiagnoticItem {
@@ -149,6 +151,9 @@ impl DiagnoticItem {
149151
DiagnoticItem::SplTokenMint => {
150152
return None;
151153
}
154+
DiagnoticItem::AnchorLazyAccount => {
155+
return None;
156+
}
152157
})
153158
}
154159

@@ -255,6 +260,7 @@ impl DiagnoticItem {
255260
}
256261
DiagnoticItem::SplTokenAccount => &["spl_token::state::Account"],
257262
DiagnoticItem::SplTokenMint => &["spl_token::state::Mint"],
263+
DiagnoticItem::AnchorLazyAccount => &["anchor_lang::prelude::LazyAccount"],
258264
}
259265
}
260266

@@ -463,10 +469,10 @@ pub fn is_solana_instruction_type(tcx: TyCtxt, ty: Ty) -> bool {
463469

464470
pub fn is_box_type(tcx: TyCtxt, ty: Ty) -> bool {
465471
let ty = ty.peel_refs();
466-
if let ty::Adt(adt_def, _) = ty.kind() {
467-
if let Some(box_def_id) = tcx.lang_items().owned_box() {
468-
return adt_def.did() == box_def_id;
469-
}
472+
if let ty::Adt(adt_def, _) = ty.kind()
473+
&& let Some(box_def_id) = tcx.lang_items().owned_box()
474+
{
475+
return adt_def.did() == box_def_id;
470476
}
471477
false
472478
}
@@ -492,3 +498,8 @@ pub fn is_cpi_builder_constructor_fn(tcx: TyCtxt, def_id: DefId) -> bool {
492498
|| path.contains("UnlockV1")
493499
|| path.contains("RevokeStaking")
494500
}
501+
502+
pub fn is_anchor_lazy_account_type(tcx: TyCtxt, ty: Ty) -> bool {
503+
let ty = ty.peel_refs();
504+
DiagnoticItem::AnchorLazyAccount.defid_is_type(tcx, ty)
505+
}

lints/missing_account_reload/src/lib.rs

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ use std::collections::{HashMap, HashSet};
1010

1111
use anchor_lints_utils::utils::should_skip_function;
1212
use anchor_lints_utils::{
13-
diag_items::{DiagnoticItem, is_cpi_invoke_fn},
13+
diag_items::{DiagnoticItem, is_anchor_lazy_account_type, is_cpi_invoke_fn},
1414
mir_analyzer::{AnchorContextInfo, MirAnalyzer},
1515
utils::get_hir_body_from_local_def_id,
1616
};
@@ -124,6 +124,25 @@ impl<'tcx> LateLintPass<'tcx> for MissingAccountReload {
124124
}
125125
}
126126
}
127+
// Check for LazyAccount::load/LazyAccount::load_mut (reloads for LazyAccount)
128+
else if let Some(name) = cx.tcx.opt_item_name(*fn_def_id)
129+
&& (name.as_str() == "load" || name.as_str() == "load_mut")
130+
&& let Some(receiver) = args.first()
131+
&& let Operand::Move(place) | Operand::Copy(place) = &receiver.node
132+
&& let Some(local) = place.as_local()
133+
&& let Some(decl) = mir.local_decls.get(local)
134+
{
135+
let receiver_ty = decl.ty.peel_refs();
136+
if is_anchor_lazy_account_type(cx.tcx, receiver_ty)
137+
&& let Some(account_name_and_local) =
138+
mir_analyzer.extract_account_name_from_local(&local, false)
139+
{
140+
account_reloads
141+
.entry(account_name_and_local.account_name)
142+
.or_default()
143+
.insert(bb);
144+
}
145+
}
127146
// Or a CPI invoke function
128147
else if is_cpi_invoke_fn(cx.tcx, *fn_def_id)
129148
|| mir_analyzer.takes_cpi_context(args)
@@ -375,6 +394,27 @@ pub fn analyze_nested_function_operations<'tcx>(
375394
nested_function_blocks.push(block);
376395
}
377396
}
397+
// Check for LazyAccount::load/LazyAccount::load_mut (reloads for LazyAccount)
398+
else if let Some(name) = cx.tcx.opt_item_name(*def_id)
399+
&& (name.as_str() == "load" || name.as_str() == "load_mut")
400+
&& let Some(receiver) = args.first()
401+
&& let Operand::Move(place) | Operand::Copy(place) = &receiver.node
402+
&& let Some(local) = place.as_local()
403+
&& let Some(decl) = mir_body.local_decls.get(local)
404+
{
405+
let receiver_ty = decl.ty.peel_refs();
406+
if is_anchor_lazy_account_type(cx.tcx, receiver_ty)
407+
&& let Some(block) = handle_account_reload_in_nested_function(
408+
&mir_analyzer,
409+
mir_body,
410+
args,
411+
*fn_span,
412+
bb,
413+
)
414+
{
415+
nested_function_blocks.push(block);
416+
}
417+
}
378418
// Handle account access (deref) in nested function
379419
else if cx
380420
.tcx

lints/missing_account_reload/src/utils/paths.rs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
use anchor_lints_utils::{
22
diag_items::{
33
is_account_info_type, is_anchor_account_loader_type, is_anchor_account_type,
4-
is_anchor_interface_account_type, is_anchor_signer_type, is_anchor_system_account_type,
5-
is_anchor_unchecked_account_type, is_box_type,
4+
is_anchor_interface_account_type, is_anchor_lazy_account_type, is_anchor_signer_type,
5+
is_anchor_system_account_type, is_anchor_unchecked_account_type, is_box_type,
66
},
77
mir_analyzer::AnchorContextInfo,
88
};
@@ -58,7 +58,9 @@ pub fn contains_deserialized_data<'tcx>(cx: &LateContext<'tcx>, ty: Ty<'tcx>) ->
5858
if is_anchor_account_loader_type(cx.tcx, ty) {
5959
return false;
6060
}
61-
if is_anchor_interface_account_type(cx.tcx, ty) || is_anchor_system_account_type(cx.tcx, ty)
61+
if is_anchor_interface_account_type(cx.tcx, ty)
62+
|| is_anchor_system_account_type(cx.tcx, ty)
63+
|| is_anchor_lazy_account_type(cx.tcx, ty)
6264
{
6365
return true;
6466
}

lints/missing_account_reload/tests/test_program/src/lib.rs

Lines changed: 53 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use anchor_lang::solana_program::{
44
system_instruction,
55
};
66

7-
use anchor_lang::system_program::{transfer, Transfer};
7+
use anchor_lang::system_program::{Transfer, transfer};
88

99
declare_id!("11111111111111111111111111111111");
1010

@@ -319,18 +319,56 @@ pub mod missing_account_reload_tests {
319319
}
320320

321321
// Pattern 17: CPI call with self implementation - safe
322-
pub fn invoke_with_self_implementation_safe(ctx: Context<SolTransfer3>, amount: u64) -> Result<()> {
322+
pub fn invoke_with_self_implementation_safe(
323+
ctx: Context<SolTransfer3>,
324+
amount: u64,
325+
) -> Result<()> {
323326
ctx.accounts.cpi_call_safe(amount)?;
324327
let _final_data = ctx.accounts.pda_account.data; // [safe_account_accessed]
325328
Ok(())
326329
}
327330

328331
// Pattern 18: CPI call with self implementation - unsafe
329-
pub fn invoke_with_self_implementation_unsafe(ctx: Context<SolTransfer3>, amount: u64) -> Result<()> {
332+
pub fn invoke_with_self_implementation_unsafe(
333+
ctx: Context<SolTransfer3>,
334+
amount: u64,
335+
) -> Result<()> {
330336
ctx.accounts.cpi_call_unsafe(amount)?;
331337
let _final_data = ctx.accounts.pda_account.data; // [unsafe_account_accessed]
332338
Ok(())
333339
}
340+
341+
/// Patten 19: load_mut() then CPI — unsafe.
342+
pub fn test_lazy_account_unsafe(
343+
ctx: Context<LazyAccountCpiAccounts>,
344+
amount: u64,
345+
) -> Result<()> {
346+
let data = ctx.accounts.lazy_acc.load_mut()?; // ref held across CPI
347+
let _ = data.value;
348+
let new_space = ctx.accounts.lazy_acc.to_account_info().data_len() as u64 + amount;
349+
let ix = system_instruction::allocate(&ctx.accounts.lazy_acc.key(), new_space);
350+
let account_infos = vec![
351+
ctx.accounts.lazy_acc.to_account_info(),
352+
ctx.accounts.system_program.to_account_info(),
353+
];
354+
invoke(&ix, &account_infos)?; // [cpi_call]
355+
let _ = data.value; // [unsafe_account_accessed]
356+
Ok(())
357+
}
358+
359+
// Pattern 20: load_mut() then CPI with load_mut() — safe
360+
pub fn test_lazy_account_safe(ctx: Context<LazyAccountCpiAccounts>, amount: u64) -> Result<()> {
361+
let new_space = ctx.accounts.lazy_acc.to_account_info().data_len() as u64 + amount;
362+
let ix = system_instruction::allocate(&ctx.accounts.lazy_acc.key(), new_space);
363+
let account_infos = vec![
364+
ctx.accounts.lazy_acc.to_account_info(),
365+
ctx.accounts.system_program.to_account_info(),
366+
];
367+
invoke(&ix, &account_infos)?;
368+
let data = ctx.accounts.lazy_acc.load_mut()?;
369+
let _ = data.value; // [safe_account_accessed]
370+
Ok(())
371+
}
334372
}
335373
pub fn cpi_call_safe(ctx_a: &mut Context<SolTransfer3>, amount: u64) -> Result<()> {
336374
let from_pubkey = ctx_a.accounts.pda_account.to_account_info();
@@ -614,6 +652,18 @@ pub struct UserState {
614652
pub data: u64,
615653
}
616654

655+
#[account]
656+
pub struct LazyLoadable {
657+
pub value: u64,
658+
}
659+
660+
#[derive(Accounts)]
661+
pub struct LazyAccountCpiAccounts<'info> {
662+
#[account(mut)]
663+
pub lazy_acc: LazyAccount<'info, LazyLoadable>,
664+
pub system_program: Program<'info, System>,
665+
}
666+
617667
#[account]
618668
pub struct InnerAccount {
619669
pub data: u64,

0 commit comments

Comments
 (0)