Skip to content

Commit 9e72b52

Browse files
committed
feat(multisig_add_member,multisig_remove_member): implement ixs and refactor code to use invariants
1 parent 401eb6c commit 9e72b52

29 files changed

Lines changed: 545 additions & 171 deletions

Anchor.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ seeds = false
33
skip-lint = false
44

55
[programs.localnet]
6-
multisig = "7YYnaRgQeHYd2FKGKkwASM2ZNZHTo1GvcicsyKKFvcoh"
6+
multisig = "SQDS4ep65T869zMMBKyuUq6aD6EgTu8psMjkvj52pCf"
77

88
[registry]
99
url = "https://api.apr.dev"

programs/multisig/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[package]
22
name = "multisig"
33
version = "0.1.0"
4-
description = "Created with Anchor"
4+
description = "Squads Multisig Program V4"
55
edition = "2021"
66

77
[lib]

programs/multisig/src/errors.rs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,10 @@ use anchor_lang::prelude::*;
44
pub enum MultisigError {
55
#[msg("Found multiple members with the same pubkey")]
66
DuplicateMember,
7-
#[msg("Member is already in multisig")]
8-
MemberAlreadyExists,
97
#[msg("Members array is empty")]
108
EmptyMembers,
119
#[msg("Too many members, can be up to 65535")]
1210
TooManyMembers,
13-
#[msg("Maximum number of members already reached")]
14-
MaxMembersReached,
1511
#[msg("Invalid threshold, must be between 1 and number of members with Vote permission")]
1612
InvalidThreshold,
1713
#[msg("Attempted to perform an unauthorized action")]
@@ -36,4 +32,10 @@ pub enum MultisigError {
3632
InvalidAccount,
3733
#[msg("transaction_execute reentrancy is forbidden")]
3834
ExecuteReentrancy,
35+
#[msg("Cannot remove last member")]
36+
RemoveLastMember,
37+
#[msg("Members don't include any voters")]
38+
NoVoters,
39+
#[msg("config_authority must be set to non-default key")]
40+
InvalidStaleTransactionIndex,
3941
}

programs/multisig/src/instructions/multisig_config.rs

Lines changed: 80 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,21 @@ use crate::state::*;
88
#[derive(AnchorSerialize, AnchorDeserialize)]
99
pub struct MultisigAddMemberArgs {
1010
new_member: Member,
11-
/// Memo isn't used for anything, but is included in `CreatedEvent` that can later be parsed and indexed.
11+
/// Memo isn't used for anything, but is included in `AddMemberEvent` that can later be parsed and indexed.
12+
pub memo: Option<String>,
13+
}
14+
15+
#[derive(AnchorSerialize, AnchorDeserialize)]
16+
pub struct MultisigRemoveMemberArgs {
17+
old_member: Member,
18+
/// Memo isn't used for anything, but is included in `RemoveMemberEvent` that can later be parsed and indexed.
19+
pub memo: Option<String>,
20+
}
21+
22+
#[derive(AnchorSerialize, AnchorDeserialize)]
23+
pub struct MultisigChangeThresholdArgs {
24+
new_threshold: u16,
25+
/// Memo isn't used for anything, but is included in `ChangeThreshold` that can later be parsed and indexed.
1226
pub memo: Option<String>,
1327
}
1428

@@ -18,11 +32,13 @@ pub struct MultisigConfig<'info> {
1832
mut,
1933
seeds = [SEED_PREFIX, multisig.create_key.as_ref(), SEED_MULTISIG],
2034
bump = multisig.bump,
21-
has_one = config_authority @ MultisigError::Unauthorized,
2235
)]
2336
multisig: Account<'info, Multisig>,
2437

25-
#[account(mut)]
38+
#[account(
39+
mut,
40+
constraint = config_authority.key() == multisig.config_authority @ MultisigError::Unauthorized,
41+
)]
2642
pub config_authority: Signer<'info>,
2743

2844
/// We might need it in case reallocation is needed.
@@ -32,28 +48,19 @@ pub struct MultisigConfig<'info> {
3248
impl MultisigConfig<'_> {
3349
/// Add a member/key to the multisig and reallocate space if necessary.
3450
pub fn multisig_add_member(ctx: Context<Self>, args: MultisigAddMemberArgs) -> Result<()> {
35-
let multisig = &mut ctx.accounts.multisig;
36-
3751
let MultisigAddMemberArgs { new_member, memo } = args;
3852

39-
require!(
40-
multisig.is_member(new_member.key).is_none(),
41-
MultisigError::MemberAlreadyExists
42-
);
53+
let multisig = &mut ctx.accounts.multisig;
54+
let multisig_key = multisig.to_account_info().key();
4355

4456
let current_members_length = multisig.members.len();
45-
// If max is already reached, we can't have more members.
46-
require!(
47-
current_members_length < usize::from(u16::MAX),
48-
MultisigError::MaxMembersReached
49-
);
50-
5157
let current_account_size = multisig.to_account_info().data.borrow().len();
58+
5259
// Check if we need to reallocate space.
5360
let reallocated = if current_account_size < Multisig::size(current_members_length + 1) {
5461
// We need to allocate more space. To avoid doing this operation too often, we increment it by 10 members.
5562
let new_size = current_account_size + (10 * Member::size());
56-
// Reallocate more space
63+
// Reallocate more space.
5764
AccountInfo::realloc(&multisig.to_account_info(), new_size, false)?;
5865

5966
// If more lamports are needed, transfer them to the account
@@ -78,9 +85,10 @@ impl MultisigConfig<'_> {
7885
false
7986
};
8087

81-
multisig.add_member_if_not_exists(new_member);
88+
multisig.add_member(new_member);
89+
90+
multisig.invariant()?;
8291

83-
let multisig_key = multisig.to_account_info().key();
8492
multisig.config_updated(
8593
multisig_key,
8694
ConfigUpdateType::AddMember { reallocated },
@@ -89,4 +97,58 @@ impl MultisigConfig<'_> {
8997

9098
Ok(())
9199
}
100+
101+
/// Remove a member/key from the multisig.
102+
pub fn multisig_remove_member(
103+
ctx: Context<Self>,
104+
args: MultisigRemoveMemberArgs,
105+
) -> Result<()> {
106+
let multisig = &mut ctx.accounts.multisig;
107+
let multisig_key = multisig.to_account_info().key();
108+
109+
require!(multisig.members.len() > 1, MultisigError::RemoveLastMember);
110+
111+
let old_member_index = match multisig.is_member(args.old_member.key) {
112+
Some(old_member_index) => old_member_index,
113+
None => return err!(MultisigError::NotAMember),
114+
};
115+
116+
multisig.members.remove(old_member_index);
117+
118+
// Update the threshold if necessary.
119+
if usize::from(multisig.threshold) > multisig.members.len() {
120+
multisig.threshold = multisig
121+
.members
122+
.len()
123+
.try_into()
124+
.expect("didn't expect more that `u16::MAX` members");
125+
}
126+
127+
multisig.invariant()?;
128+
129+
multisig.config_updated(multisig_key, ConfigUpdateType::RemoveMember, args.memo);
130+
131+
Ok(())
132+
}
133+
134+
pub fn multisig_change_threshold(
135+
ctx: Context<Self>,
136+
args: MultisigChangeThresholdArgs,
137+
) -> Result<()> {
138+
let MultisigChangeThresholdArgs {
139+
new_threshold,
140+
memo,
141+
} = args;
142+
143+
let multisig = &mut ctx.accounts.multisig;
144+
let multisig_key = multisig.to_account_info().key();
145+
146+
multisig.threshold = new_threshold;
147+
148+
multisig.invariant()?;
149+
150+
multisig.config_updated(multisig_key, ConfigUpdateType::ChangeThreshold, memo);
151+
152+
Ok(())
153+
}
92154
}

programs/multisig/src/instructions/multisig_create.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,8 +74,10 @@ impl MultisigCreate<'_> {
7474
multisig.create_key = args.create_key;
7575
multisig.bump = *ctx.bumps.get("multisig").unwrap();
7676

77+
multisig.invariant()?;
78+
7779
emit!(MultisigCreated {
78-
multisig: ctx.accounts.multisig.to_account_info().key(),
80+
multisig: multisig.to_account_info().key(),
7981
memo: args.memo,
8082
});
8183

programs/multisig/src/instructions/transaction_create.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,8 @@ impl TransactionCreate<'_> {
105105
// Updated last transaction index in the multisig account.
106106
multisig.transaction_index = transaction_index;
107107

108+
multisig.invariant()?;
109+
108110
emit!(TransactionCreated {
109111
multisig: multisig_key,
110112
transaction: transaction.key(),

programs/multisig/src/instructions/transaction_execute.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ use crate::utils::*;
1111
#[derive(Accounts)]
1212
pub struct TransactionExecute<'info> {
1313
#[account(
14-
mut,
1514
seeds = [SEED_PREFIX, multisig.create_key.as_ref(), SEED_MULTISIG],
1615
bump = multisig.bump,
1716
)]

programs/multisig/src/lib.rs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
#![allow(clippy::result_large_err)]
2+
#![deny(unused_must_use)]
23

34
use anchor_lang::prelude::*;
45

@@ -12,7 +13,7 @@ pub mod instructions;
1213
pub mod state;
1314
mod utils;
1415

15-
declare_id!("7YYnaRgQeHYd2FKGKkwASM2ZNZHTo1GvcicsyKKFvcoh");
16+
declare_id!("SQDS4ep65T869zMMBKyuUq6aD6EgTu8psMjkvj52pCf");
1617

1718
#[program]
1819
pub mod multisig {
@@ -31,6 +32,14 @@ pub mod multisig {
3132
MultisigConfig::multisig_add_member(ctx, args)
3233
}
3334

35+
/// Remove a member/key from the multisig.
36+
pub fn multisig_remove_member(
37+
ctx: Context<MultisigConfig>,
38+
args: MultisigRemoveMemberArgs,
39+
) -> Result<()> {
40+
MultisigConfig::multisig_remove_member(ctx, args)
41+
}
42+
3443
/// Create a new multisig transaction.
3544
pub fn transaction_create(
3645
ctx: Context<TransactionCreate>,

programs/multisig/src/state.rs

Lines changed: 49 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,6 @@ pub struct Multisig {
3535
/// Last stale transaction index. All transactions up until this index are stale.
3636
/// This index is updated when multisig config (members/threshold) changes.
3737
pub stale_transaction_index: u64,
38-
/// Reserved for future use.
39-
pub _reserved: u8,
4038
/// Key that is used to seed the multisig PDA.
4139
/// Used solely as bytes for the seed, doesn't have any other meaning or function.
4240
pub create_key: Pubkey,
@@ -54,11 +52,51 @@ impl Multisig {
5452
1 + // authority_index
5553
8 + // transaction_index
5654
8 + // stale_transaction_index
57-
1 + // allow_external_execute
5855
32 + // create_key
5956
1 // bump
6057
}
6158

59+
// Makes sure the multisig state is valid.
60+
// This must be called at the end of every instruction that modifies a Multisig account.
61+
pub fn invariant(&self) -> Result<()> {
62+
let Self {
63+
threshold,
64+
members,
65+
transaction_index,
66+
stale_transaction_index,
67+
..
68+
} = self;
69+
// Max number of members is u16::MAX.
70+
require!(
71+
members.len() <= usize::from(u16::MAX),
72+
MultisigError::TooManyMembers
73+
);
74+
75+
// There must be no duplicate members.
76+
let has_duplicates = members.windows(2).any(|win| win[0].key == win[1].key);
77+
require!(!has_duplicates, MultisigError::DuplicateMember);
78+
79+
// There must be at least one member with Vote permissions.
80+
let num_voters: u16 = Self::num_voters(members)
81+
.try_into()
82+
.expect("didn't expect more that `u16::MAX` members");
83+
require!(num_voters > 0, MultisigError::NoVoters);
84+
85+
// Threshold must be greater than 0.
86+
require!(*threshold > 0, MultisigError::InvalidThreshold);
87+
88+
// Threshold must not exceed the number of voters.
89+
require!(*threshold <= num_voters, MultisigError::InvalidThreshold);
90+
91+
// `state.stale_transaction_index` must be less than or equal to `state.transaction_index`.
92+
require!(
93+
stale_transaction_index <= transaction_index,
94+
MultisigError::InvalidStaleTransactionIndex
95+
);
96+
97+
Ok(())
98+
}
99+
62100
/// Captures the fact that the multisig config has changed in the multisig state
63101
/// and emits a `ConfigUpdatedEvent`.
64102
pub fn config_updated(
@@ -99,10 +137,14 @@ impl Multisig {
99137
.count()
100138
}
101139

102-
pub fn add_member_if_not_exists(&mut self, new_member: Member) {
103-
if self.is_member(new_member.key).is_none() {
104-
self.members.push(new_member);
105-
self.members.sort_by_key(|m| m.key);
140+
pub fn add_member(&mut self, new_member: Member) {
141+
self.members.push(new_member);
142+
self.members.sort_by_key(|m| m.key);
143+
}
144+
145+
pub fn remove_member(&mut self, member_pubkey: Pubkey) {
146+
if let Some(index) = self.is_member(member_pubkey) {
147+
self.members.remove(index);
106148
}
107149
}
108150
}

sdk/multisig/.solitarc.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ const binaryInstallDir = path.join(__dirname, ".crates");
99
module.exports = {
1010
idlGenerator: "anchor",
1111
programName: "multisig",
12-
programId: "7YYnaRgQeHYd2FKGKkwASM2ZNZHTo1GvcicsyKKFvcoh",
12+
programId: "SQDS4ep65T869zMMBKyuUq6aD6EgTu8psMjkvj52pCf",
1313
idlDir,
1414
sdkDir,
1515
binaryInstallDir,

0 commit comments

Comments
 (0)