diff --git a/lang/src/lib.rs b/lang/src/lib.rs index 5a5a9230..2b4354d7 100644 --- a/lang/src/lib.rs +++ b/lang/src/lib.rs @@ -305,19 +305,32 @@ pub mod __internal { } else { // Dup branch: borrow_state != NOT_BORROWED means the SVM // deduplicated this account slot. - if flags.is_ref_mut && !flags.allow_dup { - // Mutable dups without #[account(dup)] are rejected. - return Err(ProgramError::AccountBorrowFailed); - } - let idx = (actual_header & 0xFF) as usize; if crate::utils::hint::unlikely(idx >= offset) { return Err(ProgramError::InvalidAccountData); } + // Read the original AccountView first — `raw` in the dup branch only + // points to an 8-byte dup entry, not a full RuntimeAccount. + let orig_view = core::ptr::read(base.add(idx)); + + // Optional None sentinels (address == program_id) represent absent + // accounts, not real duplicate borrows. Skip borrow tracking for + // them to avoid false positives when multiple Option fields are + // simultaneously None. + if flags.is_optional && crate::keys_eq(orig_view.address(), program_id) { + core::ptr::write(base.add(offset), orig_view); + let input = input.add(core::mem::size_of::()); + return Ok(input); + } + + if flags.is_ref_mut && !flags.allow_dup { + // Mutable dups without #[account(dup)] are rejected. + return Err(ProgramError::AccountBorrowFailed); + } + if flags.is_ref_mut { // Mutable dup: claim exclusive access. - let orig_view = core::ptr::read(base.add(idx)); let bs_ptr = orig_view.account_ptr() as *mut u8; let bs = *bs_ptr; if crate::utils::hint::unlikely(bs != NOT_BORROWED) { @@ -326,7 +339,6 @@ pub mod __internal { *bs_ptr = 0; } else { // Immutable dup: consume one immutable borrow slot. - let orig_view = core::ptr::read(base.add(idx)); let bs_ptr = orig_view.account_ptr() as *mut u8; let bs = *bs_ptr; if crate::utils::hint::unlikely(bs <= 1) { @@ -335,7 +347,7 @@ pub mod __internal { *bs_ptr = bs - 1; } - core::ptr::write(base.add(offset), core::ptr::read(base.add(idx))); + core::ptr::write(base.add(offset), orig_view); let input = input.add(core::mem::size_of::()); Ok(input) } diff --git a/tests/programs/test-misc/src/instructions/mod.rs b/tests/programs/test-misc/src/instructions/mod.rs index a24bedd8..d0cfcfa6 100644 --- a/tests/programs/test-misc/src/instructions/mod.rs +++ b/tests/programs/test-misc/src/instructions/mod.rs @@ -142,3 +142,6 @@ pub use dynamic_view_mut_missing_field::*; pub mod cpi_mut_readback; pub use cpi_mut_readback::*; + +pub mod optional_mut_accounts; +pub use optional_mut_accounts::*; diff --git a/tests/programs/test-misc/src/instructions/optional_mut_accounts.rs b/tests/programs/test-misc/src/instructions/optional_mut_accounts.rs new file mode 100644 index 00000000..9735538f --- /dev/null +++ b/tests/programs/test-misc/src/instructions/optional_mut_accounts.rs @@ -0,0 +1,26 @@ +use {crate::state::SimpleAccount, quasar_derive::Accounts, quasar_lang::prelude::*}; + +/// Accounts struct with multiple `mut` Option fields that can all be `None`. +/// Used to verify that the sentinel-first dup path skips borrow tracking for +/// program-ID sentinels. +#[derive(Accounts)] +pub struct OptionalMutAccounts { + #[account(mut)] + pub authority: Signer, + + #[account(mut)] + pub first: Option>, + + #[account(mut)] + pub second: Option>, + + #[account(mut)] + pub third: Option>, +} + +impl OptionalMutAccounts { + #[inline(always)] + pub fn handler(&self) -> Result<(), ProgramError> { + Ok(()) + } +} diff --git a/tests/programs/test-misc/src/lib.rs b/tests/programs/test-misc/src/lib.rs index 694848d3..250e5f7e 100644 --- a/tests/programs/test-misc/src/lib.rs +++ b/tests/programs/test-misc/src/lib.rs @@ -338,4 +338,9 @@ mod quasar_test_misc { let _ = (maybe_name, maybe_addrs); ctx.accounts.handler() } + + #[instruction(discriminator = 62)] + pub fn optional_mut_accounts(ctx: Ctx) -> Result<(), ProgramError> { + ctx.accounts.handler() + } } diff --git a/tests/suite/src/optional_accounts.rs b/tests/suite/src/optional_accounts.rs index 93a014d2..a8769e46 100644 --- a/tests/suite/src/optional_accounts.rs +++ b/tests/suite/src/optional_accounts.rs @@ -158,3 +158,32 @@ fn some_wrong_discriminator() { assert!(result.is_err(), "wrong disc on present optional"); result.assert_error(ProgramError::InvalidAccountData); } + +/// Multiple `#[account(mut)] Option` fields can all be `None` (program-ID +/// sentinel) without triggering the duplicate-account borrow checker. +#[test] +fn multiple_mut_optional_none_sentinels() { + let mut svm = svm_misc(); + let authority = Pubkey::new_unique(); + let sentinel = quasar_test_misc::ID; + + let ix: Instruction = OptionalMutAccountsInstruction { + authority, + first: sentinel, + second: sentinel, + third: sentinel, + } + .into(); + + let result = svm.process_instruction( + &ix, + &[ + signer_account(authority), + ], + ); + assert!( + result.is_ok(), + "all mut optionals as None sentinel should parse: {:?}", + result.raw_result + ); +}