Skip to content
Merged
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
2 changes: 2 additions & 0 deletions programs/squads_smart_account_program/src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,8 @@ pub enum SmartAccountError {
InternalFundTransferPolicyInvariantAmountZero,
#[msg("Internal fund transfer policy invariant violation: cannot have duplicate mints")]
InternalFundTransferPolicyInvariantDuplicateMints,
#[msg("Internal fund transfer policy: destination token account has a delegate")]
InternalFundTransferPolicyDestinationHasDelegate,

// ===============================================
// Consensus Account Errors
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ pub struct ActivateProposal<'info> {
)]
pub settings: Account<'info, Settings>,

#[account(mut)]
pub signer: Signer<'info>,

#[account(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ impl AddSpendingLimitAsAuthority<'_> {
#[access_control(ctx.accounts.validate(args.expiration))]
pub fn add_spending_limit(ctx: Context<Self>, args: AddSpendingLimitArgs) -> Result<()> {
let settings = &ctx.accounts.settings;
settings.validate_account_index_unlocked(args.account_index)?;
let spending_limit = &mut ctx.accounts.spending_limit;

// Make sure there are no duplicate keys in this direct invocation by sorting so the invariant will catch
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,6 @@ use crate::state::*;
pub struct CreateTransactionBufferArgs {
/// Index of the buffer account to seed the account derivation
pub buffer_index: u8,
/// Index of the smart account this transaction belongs to.
pub account_index: u8,
/// Hash of the final assembled transaction message.
pub final_buffer_hash: [u8; 32],
/// Final size of the buffer.
Expand Down Expand Up @@ -96,7 +94,6 @@ impl CreateTransactionBuffer<'_> {
// Initialize the transaction fields.
transaction_buffer.settings = consensus_account.key();
transaction_buffer.creator = creator.key();
transaction_buffer.account_index = args.account_index;
transaction_buffer.buffer_index = buffer_index;
transaction_buffer.final_buffer_hash = args.final_buffer_hash;
transaction_buffer.final_buffer_size = args.final_buffer_size;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -520,7 +520,7 @@ impl CloseBatch<'_> {
pub struct CloseEmptyPolicyTransaction<'info> {
/// Global program config account. (Just using this for logging purposes,
/// since we no longer have the consensus account)
#[account(mut, seeds = [SEED_PREFIX, SEED_PROGRAM_CONFIG], bump)]
#[account(seeds = [SEED_PREFIX, SEED_PROGRAM_CONFIG], bump)]
pub program_config: Account<'info, ProgramConfig>,


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,8 @@ impl UseSpendingLimit<'_> {
SmartAccountError::Unauthorized
);

// spending_limit - needs no checking.
// spending_limit - validate the account index is unlocked.
self.settings.validate_account_index_unlocked(spending_limit.account_index)?;

// mint
if spending_limit.mint == Pubkey::default() {
Expand Down
3 changes: 2 additions & 1 deletion programs/squads_smart_account_program/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -343,7 +343,8 @@ pub mod squads_smart_account_program {

/// 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
// Decrement is intentionally not supported. If ever implemented, it MUST ensure
// no existing vaults, spending limits, or pending transactions reference the index being decremented.
pub fn increment_account_index(ctx: Context<IncrementAccountIndex>) -> Result<()> {
IncrementAccountIndex::increment_account_index(ctx)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -382,6 +382,10 @@ impl InternalFundTransferPolicy {
&& destination_token_account.mint == args.mint,
SmartAccountError::InvalidAccount
);
require!(
Option::<Pubkey>::from(destination_token_account.delegate).is_none(),
SmartAccountError::InternalFundTransferPolicyDestinationHasDelegate
);
// Check the token program
require_eq!(TokenInterface::ids().contains(&token_program.key()), true);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,7 @@ pub enum PolicyCreationPayload {
impl PolicyCreationPayload {
/// Calculate the size of the resulting policy data after creation
pub fn policy_state_size(&self) -> usize {
// 1 for the Wrapper enum type
1 + match self {
match self {
PolicyCreationPayload::InternalFundTransfer(payload) => payload.policy_state_size(),
PolicyCreationPayload::SpendingLimit(payload) => payload.policy_state_size(),
PolicyCreationPayload::SettingsChange(payload) => payload.policy_state_size(),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
use anchor_lang::{prelude::*, Ids};
use anchor_lang::{prelude::*, solana_program::hash::hash, Ids};
use anchor_spl::token_interface::{TokenAccount, TokenInterface};

use crate::{
errors::SmartAccountError, state::policies::utils::spending_limit_v2::SpendingLimitV2,
};

const BASE_TOKEN_ACCOUNT_LEN: usize = 165;

pub struct TrackedTokenAccount<'info> {
pub account: &'info AccountInfo<'info>,
pub balance: u64,
pub delegate: Option<(Pubkey, u64)>,
pub authority: Pubkey,
pub extensions_hash: Option<[u8; 32]>,
}

pub struct TrackedExecutingAccount<'info> {
Expand Down Expand Up @@ -61,12 +64,26 @@ pub fn check_pre_balances<'info>(
};
let authority = token_account.owner;

// For Token-2022 accounts, hash the extension data to detect
// modifications during CPI (e.g. adding CpiGuard, MemoTransfer)
let extensions_hash = {
let data = account.data.borrow();
if *account.owner == anchor_spl::token_2022::ID
&& data.len() > BASE_TOKEN_ACCOUNT_LEN
{
Some(hash(&data[BASE_TOKEN_ACCOUNT_LEN..]).to_bytes())
} else {
None
}
};

// Add the token account to the tracked token accounts
tracked_token_accounts.push(TrackedTokenAccount {
account,
balance,
delegate,
authority,
extensions_hash,
});
}
}
Expand Down Expand Up @@ -189,6 +206,22 @@ impl<'info> Balances<'info> {
tracked_token_account.authority,
SmartAccountError::ProgramInteractionIllegalTokenAccountModification
);

// For Token-2022 accounts, verify extension data hasn't been modified
if let Some(pre_hash) = tracked_token_account.extensions_hash {
let data = tracked_token_account.account.data.borrow();
if data.len() > BASE_TOKEN_ACCOUNT_LEN {
let post_hash = hash(&data[BASE_TOKEN_ACCOUNT_LEN..]).to_bytes();
require!(
pre_hash == post_hash,
SmartAccountError::ProgramInteractionIllegalTokenAccountModification
);
} else {
return Err(
SmartAccountError::ProgramInteractionIllegalTokenAccountModification.into(),
);
}
}
}
Ok(())
}
Expand Down
2 changes: 2 additions & 0 deletions programs/squads_smart_account_program/src/state/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,8 @@ impl Settings {
destinations,
expiration,
} => {
self.validate_account_index_unlocked(*account_index)?;

let (spending_limit_key, spending_limit_bump) = Pubkey::find_program_address(
&[
SEED_PREFIX,
Expand Down
62 changes: 30 additions & 32 deletions sdk/smart-account/idl/squads_smart_account_program.json
Original file line number Diff line number Diff line change
Expand Up @@ -1200,7 +1200,7 @@
},
{
"name": "signer",
"isMut": true,
"isMut": false,
"isSigner": true
},
{
Expand Down Expand Up @@ -1570,7 +1570,7 @@
"accounts": [
{
"name": "programConfig",
"isMut": true,
"isMut": false,
"isSigner": false,
"docs": [
"Global program config account. (Just using this for logging purposes,",
Expand Down Expand Up @@ -3267,13 +3267,6 @@
],
"type": "u8"
},
{
"name": "accountIndex",
"docs": [
"Index of the smart account this transaction belongs to."
],
"type": "u8"
},
{
"name": "finalBufferHash",
"docs": [
Expand Down Expand Up @@ -6282,124 +6275,129 @@
},
{
"code": 6108,
"name": "InternalFundTransferPolicyDestinationHasDelegate",
"msg": "Internal fund transfer policy: destination token account has a delegate"
},
{
"code": 6109,
"name": "ConsensusAccountNotSettings",
"msg": "Consensus account is not a settings"
},
{
"code": 6109,
"code": 6110,
"name": "ConsensusAccountNotPolicy",
"msg": "Consensus account is not a policy"
},
{
"code": 6110,
"code": 6111,
"name": "SettingsChangePolicyActionsMustBeNonZero",
"msg": "Settings change policy invariant violation: actions must be non-zero"
},
{
"code": 6111,
"code": 6112,
"name": "SettingsChangeInvalidSettingsKey",
"msg": "Settings change policy violation: submitted settings account must match policy settings key"
},
{
"code": 6112,
"code": 6113,
"name": "SettingsChangeInvalidSettingsAccount",
"msg": "Settings change policy violation: submitted settings account must be writable"
},
{
"code": 6113,
"code": 6114,
"name": "SettingsChangeInvalidRentPayer",
"msg": "Settings change policy violation: rent payer must be writable and signer"
},
{
"code": 6114,
"code": 6115,
"name": "SettingsChangeInvalidSystemProgram",
"msg": "Settings change policy violation: system program must be the system program"
},
{
"code": 6115,
"code": 6116,
"name": "SettingsChangeAddSignerViolation",
"msg": "Settings change policy violation: signer does not match allowed signer"
},
{
"code": 6116,
"code": 6117,
"name": "SettingsChangeAddSignerPermissionsViolation",
"msg": "Settings change policy violation: signer permissions does not match allowed signer permissions"
},
{
"code": 6117,
"code": 6118,
"name": "SettingsChangeRemoveSignerViolation",
"msg": "Settings change policy violation: signer removal does not mach allowed signer removal"
},
{
"code": 6118,
"code": 6119,
"name": "SettingsChangeChangeTimelockViolation",
"msg": "Settings change policy violation: time lock does not match allowed time lock"
},
{
"code": 6119,
"code": 6120,
"name": "SettingsChangeActionMismatch",
"msg": "Settings change policy violation: action does not match allowed action"
},
{
"code": 6120,
"code": 6121,
"name": "SettingsChangePolicyInvariantDuplicateActions",
"msg": "Settings change policy invariant violation: cannot have duplicate actions"
},
{
"code": 6121,
"code": 6122,
"name": "SettingsChangePolicyInvariantActionIndicesActionsLengthMismatch",
"msg": "Settings change policy invariant violation: action indices must match actions length"
},
{
"code": 6122,
"code": 6123,
"name": "SettingsChangePolicyInvariantActionIndexOutOfBounds",
"msg": "Settings change policy invariant violation: action index out of bounds"
},
{
"code": 6123,
"code": 6124,
"name": "PolicyNotActiveYet",
"msg": "Policy is not active yet"
},
{
"code": 6124,
"code": 6125,
"name": "PolicyInvariantInvalidExpiration",
"msg": "Policy invariant violation: invalid policy expiration"
},
{
"code": 6125,
"code": 6126,
"name": "PolicyExpirationViolationPolicySettingsKeyMismatch",
"msg": "Policy expiration violation: submitted settings key does not match policy settings key"
},
{
"code": 6126,
"code": 6127,
"name": "PolicyExpirationViolationSettingsAccountNotPresent",
"msg": "Policy expiration violation: state expiration requires the settings to be submitted"
},
{
"code": 6127,
"code": 6128,
"name": "PolicyExpirationViolationHashExpired",
"msg": "Policy expiration violation: state hash has expired"
},
{
"code": 6128,
"code": 6129,
"name": "PolicyExpirationViolationTimestampExpired",
"msg": "Policy expiration violation: timestamp has expired"
},
{
"code": 6129,
"code": 6130,
"name": "AccountIndexLocked",
"msg": "Account index is locked, must increment_account_index first"
},
{
"code": 6130,
"code": 6131,
"name": "MaxAccountIndexReached",
"msg": "Cannot exceed maximum free account index (250)"
}
],
"metadata": {
"address": "SMRTzfY6DfH5ik3TKiyLFfXexV8uSG3d2UksSCYdunG",
"origin": "anchor",
"binaryVersion": "0.32.1",
"binaryVersion": "0.29.0",
"libVersion": "=0.29.0"
}
}
4 changes: 3 additions & 1 deletion sdk/smart-account/scripts/fix-smallvec.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,10 @@ const SMALLVEC_U8_BEET_TYPES_FILE_SPECIFIC = {
'CompiledInstructionConstraint.ts',
'CompiledAccountConstraintType.ts',
],
// NOTE: SmartAccountTransactionMessage.ts is intentionally NOT here.
// The stored state uses Vec<Pubkey> (4-byte prefix), not SmallVec.
// Only the instruction-level TransactionMessage uses SmallVec<u8, Pubkey>.
'beetSolana.publicKey': [
'SmartAccountTransactionMessage.ts',
'ProgramInteractionPolicyCreationPayload.ts',
],
};
Expand Down
Loading