Skip to content
Draft
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
10 changes: 9 additions & 1 deletion Anchor.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ seeds = false
skip-lint = false

[programs.localnet]
multisig = "SMRTe6bnZAgJmXt9aJin7XgAzDn1XMHGNy95QATyzpk"
multisig = "GyhGAqjokLwF9UXdQ2dR5Zwiup242j4mX4J1tSMKyAmD"

[registry]
url = "https://api.apr.dev"
Expand All @@ -29,3 +29,11 @@ program = "tests/fixtures/noop.so"

[scripts]
test = "npx mocha --node-option require=ts-node/register --extension ts -t 1000000 tests/index.ts"
test-proposals = "npx mocha --node-option require=ts-node/register --extension ts -t 1000000 tests/proposals.ts"
test-transactions = "npx mocha --node-option require=ts-node/register --extension ts -t 1000000 tests/transactions.ts"
test-batches = "npx mocha --node-option require=ts-node/register --extension ts -t 1000000 tests/batches.ts"
test-settings = "npx mocha --node-option require=ts-node/register --extension ts -t 1000000 tests/settings.ts"
test-sync = "npx mocha --node-option require=ts-node/register --extension ts -t 1000000 tests/sync-execution.ts"
test-policies = "npx mocha --node-option require=ts-node/register --extension ts -t 1000000 tests/policies.ts"
test-sdk = "npx mocha --node-option require=ts-node/register --extension ts -t 1000000 tests/sdk.ts"
test-accounts = "npx mocha --node-option require=ts-node/register --extension ts -t 1000000 tests/accounts.ts"
90 changes: 90 additions & 0 deletions programs/squads_smart_account_program/src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -298,4 +298,94 @@ pub enum SmartAccountError {
AccountIndexLocked,
#[msg("Cannot exceed maximum free account index (250)")]
MaxAccountIndexReached,

// ===============================================
// V2 Signer Errors
// ===============================================
#[msg("Unsupported signer version in account data")]
UnsupportedSignerVersion,
#[msg("Failed to deserialize account data")]
DeserializationFailed,
#[msg("Failed to serialize account data")]
SerializationFailed,
#[msg("Cannot serialize external signers as V1 format")]
CannotSerializeExternalAsV1,
#[msg("Signer already exists with this key_id")]
SignerAlreadyExists,
#[msg("External signer not found")]
ExternalSignerNotFound,
#[msg("Use add_signer instruction for native signers")]
UseAddSignerForNative,
#[msg("Invalid signer type for this operation")]
InvalidSignerType,
#[msg("Invalid account data")]
InvalidAccountData,

// ===============================================
// Precompile Verification Errors
// ===============================================
#[msg("Missing precompile instruction for external signature")]
MissingPrecompileInstruction,
#[msg("Invalid precompile program ID")]
InvalidPrecompileProgram,
#[msg("Precompile public key does not match signer")]
PrecompilePublicKeyMismatch,
#[msg("Precompile message does not match expected")]
PrecompileMessageMismatch,
#[msg("Invalid precompile instruction data")]
InvalidPrecompileData,
#[msg("Precompile signature count mismatch")]
PrecompileSignatureCountMismatch,
#[msg("WebAuthn RP ID hash mismatch")]
WebauthnRpIdMismatch,
#[msg("WebAuthn user presence flag not set")]
WebauthnUserNotPresent,
#[msg("WebAuthn counter not incremented")]
WebauthnCounterNotIncremented,
#[msg("External signature already used in this transaction")]
DuplicateExternalSignature,

// ===============================================
// Version Migration Errors
// ===============================================
#[msg("V1 instruction called on V2 account - use V2 instruction")]
V1InstructionOnV2Account,
#[msg("V2 instruction called on V1 account - migrate first or use V1")]
V2InstructionOnV1Account,
#[msg("Cannot downgrade to V1 - external signers still present")]
CannotDowngradeWithExternalSigners,
#[msg("Signers already migrated to V2 format")]
AlreadyMigrated,
#[msg("Signer has already voted on this proposal")]
AlreadyVoted,
#[msg("Signers do not meet consensus threshold")]
NotEnoughSigners,
#[msg("Settings must be migrated to V2 format before using this instruction")]
MustMigrateToV2,

// ===============================================
// Session Key Errors
// ===============================================
#[msg("Session key expiration must be in the future")]
InvalidSessionKeyExpiration,
#[msg("Session key expiration exceeds maximum allowed (3 months)")]
SessionKeyExpirationTooLong,
#[msg("Session key is expired")]
SessionKeyExpired,
#[msg("Session key is not active")]
SessionKeyNotActive,
#[msg("Invalid session key (cannot be default pubkey)")]
InvalidSessionKey,
#[msg("Signer with this public key already exists")]
DuplicatePublicKey,
#[msg("Missing client data params for WebAuthn verification")]
MissingClientDataParams,
#[msg("Session key is already in use")]
DuplicateSessionKey,
#[msg("Required external signer not verified")]
MissingRequiredExternalSigner,
#[msg("Invalid permissions mask (must be < 8)")]
InvalidPermissions,
#[msg("Maximum number of signers reached")]
MaxSignersReached,
}
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ pub enum PolicyEventType {
Update,
UpdateDuringExecution,
Remove,
MigrateSigners,
}

#[derive(BorshSerialize, BorshDeserialize)]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@ use anchor_lang::prelude::*;

use crate::consensus_trait::Consensus;
use crate::errors::*;
use crate::interface::consensus::ConsensusAccount;
use crate::state::*;
use crate::utils::{create_proposal_activate_message, verify_v2_context};

// TODO: rework the signer to be: Signer

#[derive(Accounts)]
pub struct ActivateProposal<'info> {
Expand All @@ -29,46 +33,125 @@ pub struct ActivateProposal<'info> {
pub proposal: Account<'info, Proposal>,
}

impl ActivateProposal<'_> {
fn validate(&self) -> Result<()> {
let Self {
settings,
proposal,
signer,
..
} = self;

// Signer is part of the settings
require!(
settings.is_signer(signer.key()).is_some(),
SmartAccountError::NotASigner
);
require!(
// We consider this action a part of the proposal initiation.
settings.signer_has_permission(signer.key(), Permission::Initiate),
SmartAccountError::Unauthorized
);
// TODO: is_valid_signer that accepts both V1 and V2 validations.

// Proposal must be in draft status and not stale
require!(
matches!(proposal.status, ProposalStatus::Draft { .. }),
SmartAccountError::InvalidProposalStatus
);
require!(
proposal.transaction_index > settings.stale_transaction_index,
SmartAccountError::StaleProposal
);
// if account_info.is_signer() = consensus.is_signer() if not means key_id
// then find key_id: enum -> we know if we need to use instruction introspection
// or if we need to use the additional data.
//
// Then check if there is a session key active. If yes remaining account.signer() after the sysvar
//
// Then create the message since the message don't always need the additional data

// Check fucntion args: Takes in the key_id (signer or not), consensus, additional_args, remaining_accounts,
// message (this message is the current one that we have without additional args. We add the additional args
// in the function if needed or keep as it is)

Ok(())
impl ActivateProposal<'_> {
fn validate(&self) -> Result<()> {
validate_activate_proposal(&*self.settings, &self.proposal, self.signer.key())
}

/// Update status of a multisig proposal from `Draft` to `Active`.
#[access_control(ctx.accounts.validate())]
pub fn activate_proposal(ctx: Context<Self>) -> Result<()> {
ctx.accounts.proposal.status = ProposalStatus::Active {
timestamp: Clock::get()?.unix_timestamp,
};
activate_proposal_inner(&mut ctx.accounts.proposal)
}
}

#[derive(AnchorSerialize, AnchorDeserialize)]
pub struct ActivateProposalV2Args {
/// The key (Native) or key_id (External) of the activating signer
pub activator_key: Pubkey, // TODO: NO NEED THIS BECAUSE ACTIVATOR KEY BECOMES SIGNER
/// Client data params for WebAuthn verification (required for WebAuthn signers)
pub client_data_params: Option<ClientDataJsonReconstructionParams>,
}

// TODO: client_data_params -> extra_verification data: SmallVec(u16)[u8]

#[derive(Accounts)]
pub struct ActivateProposalV2<'info> {
#[account(
mut,
constraint = consensus_account.check_derivation(consensus_account.key()).is_ok()
)]
pub consensus_account: InterfaceAccount<'info, ConsensusAccount>,

#[account(
mut,
seeds = [
SEED_PREFIX,
consensus_account.key().as_ref(),
SEED_TRANSACTION,
&proposal.transaction_index.to_le_bytes(),
SEED_PROPOSAL,
],
bump = proposal.bump,
)]
pub proposal: Account<'info, Proposal>,
}

Ok(())
impl ActivateProposalV2<'_> {
fn validate(&self, args: &ActivateProposalV2Args) -> Result<()> {

validate_activate_proposal(&*self.consensus_account, &self.proposal, args.activator_key)
}

/// Update status of a multisig proposal from `Draft` to `Active` with V2 signer support.
#[access_control(ctx.accounts.validate(&args))]
pub fn activate_proposal_v2(ctx: Context<Self>, args: ActivateProposalV2Args) -> Result<()> {
let expected_message = create_proposal_activate_message(
&ctx.accounts.proposal.key(),
ctx.accounts.proposal.transaction_index,
);

verify_v2_context(
&mut ctx.accounts.consensus_account,
args.activator_key,
&ctx.remaining_accounts,
&expected_message,
args.client_data_params.as_ref(),
)?;

activate_proposal_inner(&mut ctx.accounts.proposal)
}
}

// TODO: use a message with the discriminator

fn validate_activate_proposal<C: Consensus>(
consensus: &C,
proposal: &Proposal,
signer_key: Pubkey
) -> Result<()> {
// Signer is part of the settings
require!(
consensus.is_signer(signer_key).is_some(),
SmartAccountError::NotASigner
);
require!(
// We consider this action a part of the proposal initiation.
consensus.signer_has_permission(signer_key, Permission::Initiate),
SmartAccountError::Unauthorized
);

// Proposal must be in draft status and not stale
require!(
matches!(proposal.status, ProposalStatus::Draft { .. }),
SmartAccountError::InvalidProposalStatus
);
require!(
proposal.transaction_index > consensus.stale_transaction_index(),
SmartAccountError::StaleProposal
);

Ok(())
}

fn activate_proposal_inner(proposal: &mut Proposal) -> Result<()> {
proposal.status = ProposalStatus::Active {
timestamp: Clock::get()?.unix_timestamp,
};

Ok(())
}
Loading