Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions programs/squads_smart_account_program/src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -290,4 +290,12 @@ pub enum SmartAccountError {
PolicyExpirationViolationHashExpired,
#[msg("Policy expiration violation: timestamp has expired")]
PolicyExpirationViolationTimestampExpired,

// ===============================================
// Account Index Errors
// ===============================================
#[msg("Account index is locked, must increment_account_index first")]
AccountIndexLocked,
#[msg("Cannot exceed maximum free account index (250)")]
MaxAccountIndexReached,
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@ pub struct CreateSmartAccountEvent {
pub new_settings_content: Settings,
}

#[derive(BorshSerialize, BorshDeserialize)]
pub struct IncrementAccountIndexEvent {
pub settings_pubkey: Pubkey,
pub settings_state: Settings,
}

#[derive(BorshSerialize, BorshDeserialize)]
pub struct SynchronousTransactionEventV2 {
pub consensus_account: Pubkey,
Expand Down
1 change: 1 addition & 0 deletions programs/squads_smart_account_program/src/events/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ pub use account_events::*;
#[derive(BorshSerialize, BorshDeserialize)]
pub enum SmartAccountEvent {
CreateSmartAccountEvent(CreateSmartAccountEvent),
IncrementAccountIndexEvent(IncrementAccountIndexEvent),
SynchronousTransactionEvent(SynchronousTransactionEvent),
SynchronousSettingsTransactionEvent(SynchronousSettingsTransactionEvent),
AddSpendingLimitEvent(AddSpendingLimitEvent),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,13 +45,15 @@ pub struct CreateBatch<'info> {
}

impl CreateBatch<'_> {
fn validate(&self) -> Result<()> {
fn validate(&self, args: &CreateBatchArgs) -> Result<()> {
let Self {
settings,
creator,
..
} = self;

settings.validate_account_index_unlocked(args.account_index)?;

// creator
require!(
settings.is_signer(creator.key()).is_some(),
Expand All @@ -66,7 +68,7 @@ impl CreateBatch<'_> {
}

/// Create a new batch.
#[access_control(ctx.accounts.validate())]
#[access_control(ctx.accounts.validate(&args))]
pub fn create_batch(ctx: Context<Self>, args: CreateBatchArgs) -> Result<()> {
let settings = &mut ctx.accounts.settings;
let creator = &mut ctx.accounts.creator;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
use anchor_lang::prelude::*;

use crate::{
errors::SmartAccountError,
events::{IncrementAccountIndexEvent, LogAuthorityInfo, SmartAccountEvent},
interface::consensus_trait::Consensus,
program::SquadsSmartAccountProgram,
state::{
get_settings_signer_seeds, Permission, Settings, FREE_ACCOUNT_MAX_INDEX, SEED_PREFIX,
SEED_SETTINGS,
},
};

#[derive(Accounts)]
pub struct IncrementAccountIndex<'info> {
#[account(
mut,
seeds = [
SEED_PREFIX,
SEED_SETTINGS,
&settings.seed.to_le_bytes(),
],
bump = settings.bump,
)]
pub settings: Account<'info, Settings>,

pub signer: Signer<'info>,

pub program: Program<'info, SquadsSmartAccountProgram>,
}

impl IncrementAccountIndex<'_> {
fn validate(&self) -> Result<()> {
let settings = &self.settings;
let signer_key = self.signer.key();

// Signer must be a member of the smart account
let signer_index = settings
.is_signer(signer_key)
.ok_or(SmartAccountError::NotASigner)?;

// Permission: Initiate OR Vote OR Execute (mask & 7 != 0)
let permissions = settings.signers[signer_index].permissions;
require!(
permissions.has(Permission::Initiate)
|| permissions.has(Permission::Vote)
|| permissions.has(Permission::Execute),
SmartAccountError::Unauthorized
);

// Cannot exceed free account range
require!(
settings.account_utilization < FREE_ACCOUNT_MAX_INDEX,
SmartAccountError::MaxAccountIndexReached
);

Ok(())
}

#[access_control(ctx.accounts.validate())]
pub fn increment_account_index(ctx: Context<Self>) -> Result<()> {
let settings = &mut ctx.accounts.settings;
settings.increment_account_utilization_index();

let event = IncrementAccountIndexEvent {
settings_pubkey: settings.key(),
settings_state: settings.clone().into_inner(),
};
let log_authority_info = LogAuthorityInfo {
authority: settings.to_account_info(),
authority_seeds: get_settings_signer_seeds(settings.seed),
bump: settings.bump,
program: ctx.accounts.program.to_account_info(),
};
SmartAccountEvent::IncrementAccountIndexEvent(event).log(&log_authority_info)?;
Ok(())
}
}
2 changes: 2 additions & 0 deletions programs/squads_smart_account_program/src/instructions/mod.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
pub use activate_proposal::*;
pub use increment_account_index::*;
pub use authority_settings_transaction_execute::*;
pub use authority_spending_limit_add::*;
pub use authority_spending_limit_remove::*;
Expand Down Expand Up @@ -26,6 +27,7 @@ pub use transaction_execute_sync_legacy::*;
pub use use_spending_limit::*;

mod activate_proposal;
mod increment_account_index;
mod authority_settings_transaction_execute;
mod authority_spending_limit_add;
mod authority_spending_limit_remove;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,10 +81,19 @@ impl<'info> CreateTransaction<'info> {
// Validate the transaction payload
match consensus_account.account_type() {
ConsensusAccountType::Settings => {
assert!(matches!(
args,
CreateTransactionArgs::TransactionPayload { .. }
));
match args {
CreateTransactionArgs::TransactionPayload(TransactionPayload {
account_index,
..
}) => {
// Validate the account index is unlocked
let settings = consensus_account.read_only_settings()?;
settings.validate_account_index_unlocked(*account_index)?;
}
_ => {
return Err(SmartAccountError::InvalidTransactionMessage.into());
}
}
}
ConsensusAccountType::Policy => {
let policy = consensus_account.read_only_policy()?;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,12 @@ impl<'info> SyncTransaction<'info> {
// Check that the consensus account is active (policy)
consensus_account.is_active(&remaining_accounts[args.num_signers as usize..])?;

// Validate account index is unlocked for Settings-based transactions
if consensus_account.account_type() == ConsensusAccountType::Settings {
let settings = consensus_account.read_only_settings()?;
settings.validate_account_index_unlocked(args.account_index)?;
}

// Validate policy payload if necessary
if consensus_account.account_type() == ConsensusAccountType::Policy {
let policy = consensus_account.read_only_policy()?;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,11 @@ impl LegacySyncTransaction<'_> {
remaining_accounts: &[AccountInfo],
) -> Result<()> {
let Self { consensus_account, .. } = self;

// Validate account index is unlocked
let settings = consensus_account.read_only_settings()?;
settings.validate_account_index_unlocked(args.account_index)?;

validate_synchronous_consensus(&consensus_account, args.num_signers, remaining_accounts)
}
#[access_control(ctx.accounts.validate(&args, &ctx.remaining_accounts))]
Expand Down
7 changes: 7 additions & 0 deletions programs/squads_smart_account_program/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -340,4 +340,11 @@ pub mod squads_smart_account_program {
) -> Result<()> {
LogEvent::log_event(ctx, args)
}

/// Increment the account utilization index, unlocking the next vault index.
/// Callable by any signer with Initiate, Vote, or Execute permissions.
// Future: consider decrement instruction for account index management
pub fn increment_account_index(ctx: Context<IncrementAccountIndex>) -> Result<()> {
IncrementAccountIndex::increment_account_index(ctx)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use crate::{
state::policies::implementations::InternalFundTransferPayload,
InternalFundTransferPolicyCreationPayload, ProgramInteractionPayload,
ProgramInteractionPolicyCreationPayload, ProgramInteractionPolicyCreationPayloadLegacy,
SettingsChangePayload, SettingsChangePolicyCreationPayload, SpendingLimitPayload,
Settings, SettingsChangePayload, SettingsChangePolicyCreationPayload, SpendingLimitPayload,
SpendingLimitPolicyCreationPayload,
};

Expand Down Expand Up @@ -33,6 +33,23 @@ impl PolicyCreationPayload {
PolicyCreationPayload::ProgramInteraction(payload) => payload.policy_state_size(),
}
}

/// Validates that all account indices used by this policy are unlocked.
/// SettingsChange policies don't use vault accounts and always pass validation.
pub fn validate_account_indices(&self, settings: &Settings) -> Result<()> {
let indices: Vec<u8> = match self {
PolicyCreationPayload::SpendingLimit(p) => vec![p.source_account_index],
PolicyCreationPayload::ProgramInteraction(p) => vec![p.account_index],
PolicyCreationPayload::LegacyProgramInteraction(p) => vec![p.account_index],
PolicyCreationPayload::InternalFundTransfer(p) => {
let mut indices = p.source_account_indices.clone();
indices.extend(&p.destination_account_indices);
indices
}
PolicyCreationPayload::SettingsChange(_) => vec![],
};
settings.validate_account_indices_unlocked(&indices)
}
}

/// Unified enum for all policy execution payloads
Expand Down
35 changes: 34 additions & 1 deletion programs/squads_smart_account_program/src/state/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@ use crate::{
};
pub const MAX_TIME_LOCK: u32 = 3 * 30 * 24 * 60 * 60; // 3 months

// Account index constants
// Free accounts: 0-249 (250 accounts)
// Reserved accounts: 250-255 (6 accounts) - bypass index validation
pub const FREE_ACCOUNT_MAX_INDEX: u8 = 249;

#[account]
pub struct Settings {
/// An integer that is used seed the settings PDA. Its incremented by 1
Expand Down Expand Up @@ -428,6 +433,9 @@ impl Settings {
start_timestamp,
expiration_args,
} => {
// Validate that all account indices used by the policy are unlocked
policy_creation_payload.validate_account_indices(self)?;

// Increment the policy seed if it exists, otherwise set it to
// 1 (First policy is being created)
let next_policy_seed = if let Some(policy_seed) = self.policy_seed {
Expand Down Expand Up @@ -570,6 +578,9 @@ impl Settings {
policy_update_payload,
expiration_args,
} => {
// Validate that all account indices used by the policy are unlocked
policy_update_payload.validate_account_indices(self)?;

// Find the policy account
let policy_info = remaining_accounts
.iter()
Expand Down Expand Up @@ -723,9 +734,31 @@ impl Settings {
Ok(())
}

pub fn increment_account_utilization(&mut self) {
pub fn increment_account_utilization_index(&mut self) {
self.account_utilization = self.account_utilization.checked_add(1).unwrap();
}

/// Validates that the given account index is unlocked.
/// Reserved accounts (250-255) bypass this check.
pub fn validate_account_index_unlocked(&self, index: u8) -> Result<()> {
// Reserved accounts (250-255) bypass the check
if index > FREE_ACCOUNT_MAX_INDEX {
return Ok(());
}
require!(
index <= self.account_utilization,
SmartAccountError::AccountIndexLocked
);
Ok(())
}

/// Validates that all given account indices are unlocked.
pub fn validate_account_indices_unlocked(&self, indices: &[u8]) -> Result<()> {
for index in indices {
self.validate_account_index_unlocked(*index)?;
}
Ok(())
}
}

#[derive(AnchorDeserialize, AnchorSerialize, InitSpace, Eq, PartialEq, Clone)]
Expand Down
37 changes: 36 additions & 1 deletion sdk/smart-account/idl/squads_smart_account_program.json
Original file line number Diff line number Diff line change
Expand Up @@ -1858,6 +1858,31 @@
}
}
]
},
{
"name": "incrementAccountIndex",
"docs": [
"Increment the account utilization index, unlocking the next vault index.",
"Callable by any signer with Initiate, Vote, or Execute permissions."
],
"accounts": [
{
"name": "settings",
"isMut": true,
"isSigner": false
},
{
"name": "signer",
"isMut": false,
"isSigner": true
},
{
"name": "program",
"isMut": false,
"isSigner": false
}
],
"args": []
}
],
"accounts": [
Expand Down Expand Up @@ -6359,12 +6384,22 @@
"code": 6128,
"name": "PolicyExpirationViolationTimestampExpired",
"msg": "Policy expiration violation: timestamp has expired"
},
{
"code": 6129,
"name": "AccountIndexLocked",
"msg": "Account index is locked, must increment_account_index first"
},
{
"code": 6130,
"name": "MaxAccountIndexReached",
"msg": "Cannot exceed maximum free account index (250)"
}
],
"metadata": {
"address": "SMRTzfY6DfH5ik3TKiyLFfXexV8uSG3d2UksSCYdunG",
"origin": "anchor",
"binaryVersion": "0.29.0",
"binaryVersion": "0.32.1",
"libVersion": "=0.29.0"
}
}
3 changes: 2 additions & 1 deletion sdk/smart-account/scripts/fix-smallvec.js
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,8 @@ function processFile(filePath) {
}

if (fileName === 'CompiledHook.ts') {
content = content.replace(/\['instructionData', beet\.bytes\]/g, "['instructionData', smallArray(beet.u8, beet.u8)]");
// instruction_data: SmallVec<u16, u8> (note: u16 length prefix)
content = content.replace(/\['instructionData', beet\.bytes\]/g, "['instructionData', smallArray(beet.u16, beet.u8)]");
content = content.replace(/instructionData: Uint8Array/g, 'instructionData: number[]');
}

Expand Down
Loading