squads_multisig_program/instructions/
config_transaction_execute.rs

1use anchor_lang::prelude::*;
2
3use crate::errors::*;
4use crate::id;
5use crate::state::*;
6use crate::utils::*;
7
8#[derive(Accounts)]
9pub struct ConfigTransactionExecute<'info> {
10    /// The multisig account that owns the transaction.
11    #[account(
12        mut,
13        seeds = [SEED_PREFIX, SEED_MULTISIG, multisig.create_key.as_ref()],
14        bump = multisig.bump,
15    )]
16    pub multisig: Box<Account<'info, Multisig>>,
17
18    /// One of the multisig members with `Execute` permission.
19    pub member: Signer<'info>,
20
21    /// The proposal account associated with the transaction.
22    #[account(
23        mut,
24        seeds = [
25            SEED_PREFIX,
26            multisig.key().as_ref(),
27            SEED_TRANSACTION,
28            &transaction.index.to_le_bytes(),
29            SEED_PROPOSAL,
30        ],
31        bump = proposal.bump,
32    )]
33    pub proposal: Account<'info, Proposal>,
34
35    /// The transaction to execute.
36    #[account(
37        seeds = [
38            SEED_PREFIX,
39            multisig.key().as_ref(),
40            SEED_TRANSACTION,
41            &transaction.index.to_le_bytes(),
42        ],
43        bump = transaction.bump,
44    )]
45    pub transaction: Account<'info, ConfigTransaction>,
46
47    /// The account that will be charged/credited in case the config transaction causes space reallocation,
48    /// for example when adding a new member, adding or removing a spending limit.
49    /// This is usually the same as `member`, but can be a different account if needed.
50    #[account(mut)]
51    pub rent_payer: Option<Signer<'info>>,
52
53    /// We might need it in case reallocation is needed.
54    pub system_program: Option<Program<'info, System>>,
55    // In case the transaction contains Add(Remove)SpendingLimit actions,
56    // `remaining_accounts` must contain the SpendingLimit accounts to be initialized/closed.
57    // remaining_accounts
58}
59
60impl<'info> ConfigTransactionExecute<'info> {
61    fn validate(&self) -> Result<()> {
62        let Self {
63            multisig,
64            proposal,
65            member,
66            ..
67        } = self;
68
69        // member
70        require!(
71            multisig.is_member(member.key()).is_some(),
72            MultisigError::NotAMember
73        );
74        require!(
75            multisig.member_has_permission(member.key(), Permission::Execute),
76            MultisigError::Unauthorized
77        );
78
79        // proposal
80        match proposal.status {
81            ProposalStatus::Approved { timestamp } => {
82                require!(
83                    Clock::get()?.unix_timestamp - timestamp >= i64::from(multisig.time_lock),
84                    MultisigError::TimeLockNotReleased
85                );
86            }
87            _ => return err!(MultisigError::InvalidProposalStatus),
88        }
89        // Stale config transaction proposals CANNOT be executed even if approved.
90        require!(
91            proposal.transaction_index > multisig.stale_transaction_index,
92            MultisigError::StaleProposal
93        );
94
95        // `transaction` is validated by its seeds.
96
97        Ok(())
98    }
99
100    /// Execute the multisig transaction.
101    /// The transaction must be `Approved`.
102    #[access_control(ctx.accounts.validate())]
103    pub fn config_transaction_execute(ctx: Context<'_, '_, 'info, 'info, Self>) -> Result<()> {
104        let multisig = &mut ctx.accounts.multisig;
105        let transaction = &ctx.accounts.transaction;
106        let proposal = &mut ctx.accounts.proposal;
107
108        let rent = Rent::get()?;
109
110        // Execute the actions one by one.
111        for action in transaction.actions.iter() {
112            match action {
113                ConfigAction::AddMember { new_member } => {
114                    multisig.add_member(new_member.to_owned());
115
116                    multisig.invalidate_prior_transactions();
117                }
118
119                ConfigAction::RemoveMember { old_member } => {
120                    multisig.remove_member(old_member.to_owned())?;
121
122                    multisig.invalidate_prior_transactions();
123                }
124
125                ConfigAction::ChangeThreshold { new_threshold } => {
126                    multisig.threshold = *new_threshold;
127
128                    multisig.invalidate_prior_transactions();
129                }
130
131                ConfigAction::SetTimeLock { new_time_lock } => {
132                    multisig.time_lock = *new_time_lock;
133
134                    multisig.invalidate_prior_transactions();
135                }
136
137                ConfigAction::AddSpendingLimit {
138                    create_key,
139                    vault_index,
140                    mint,
141                    amount,
142                    period,
143                    members,
144                    destinations,
145                } => {
146                    // SpendingLimit members must all be members of the multisig.
147                    for sl_member in members.iter() {
148                        require!(
149                            multisig.is_member(*sl_member).is_some(),
150                            MultisigError::NotAMember
151                        );
152                    }
153
154                    let (spending_limit_key, spending_limit_bump) = Pubkey::find_program_address(
155                        &[
156                            SEED_PREFIX,
157                            multisig.key().as_ref(),
158                            SEED_SPENDING_LIMIT,
159                            create_key.as_ref(),
160                        ],
161                        ctx.program_id,
162                    );
163
164                    // Find the SpendingLimit account in `remaining_accounts`.
165                    let spending_limit_info = ctx
166                        .remaining_accounts
167                        .iter()
168                        .find(|acc| acc.key == &spending_limit_key)
169                        .ok_or(MultisigError::MissingAccount)?;
170
171                    // `rent_payer` and `system_program` must also be present.
172                    let rent_payer = &ctx
173                        .accounts
174                        .rent_payer
175                        .as_ref()
176                        .ok_or(MultisigError::MissingAccount)?;
177                    let system_program = &ctx
178                        .accounts
179                        .system_program
180                        .as_ref()
181                        .ok_or(MultisigError::MissingAccount)?;
182
183                    // Initialize the SpendingLimit account.
184                    create_account(
185                        rent_payer,
186                        spending_limit_info,
187                        system_program,
188                        &id(),
189                        &rent,
190                        SpendingLimit::size(members.len(), destinations.len()),
191                        vec![
192                            SEED_PREFIX.to_vec(),
193                            multisig.key().as_ref().to_vec(),
194                            SEED_SPENDING_LIMIT.to_vec(),
195                            create_key.as_ref().to_vec(),
196                            vec![spending_limit_bump],
197                        ],
198                    )?;
199
200                    let mut members = members.to_vec();
201                    // Make sure members are sorted.
202                    members.sort();
203
204                    // Serialize the SpendingLimit data into the account info.
205                    let spending_limit = SpendingLimit {
206                        multisig: multisig.key().to_owned(),
207                        create_key: create_key.to_owned(),
208                        vault_index: *vault_index,
209                        amount: *amount,
210                        mint: *mint,
211                        period: *period,
212                        remaining_amount: *amount,
213                        last_reset: Clock::get()?.unix_timestamp,
214                        bump: spending_limit_bump,
215                        members,
216                        destinations: destinations.to_vec(),
217                    };
218
219                    spending_limit.invariant()?;
220
221                    spending_limit
222                        .try_serialize(&mut &mut spending_limit_info.data.borrow_mut()[..])?;
223                }
224
225                ConfigAction::RemoveSpendingLimit {
226                    spending_limit: spending_limit_key,
227                } => {
228                    // Find the SpendingLimit account in `remaining_accounts`.
229                    let spending_limit_info = ctx
230                        .remaining_accounts
231                        .iter()
232                        .find(|acc| acc.key == spending_limit_key)
233                        .ok_or(MultisigError::MissingAccount)?;
234
235                    // `rent_payer` must also be present.
236                    let rent_payer = &ctx
237                        .accounts
238                        .rent_payer
239                        .as_ref()
240                        .ok_or(MultisigError::MissingAccount)?;
241
242                    let spending_limit = Account::<SpendingLimit>::try_from(spending_limit_info)?;
243
244                    // SpendingLimit must belong to the `multisig`.
245                    require_keys_eq!(
246                        spending_limit.multisig,
247                        multisig.key(),
248                        MultisigError::InvalidAccount
249                    );
250
251                    spending_limit.close(rent_payer.to_account_info())?;
252
253                    // We don't need to invalidate prior transactions here because adding
254                    // a spending limit doesn't affect the consensus parameters of the multisig.
255                }
256
257                ConfigAction::SetRentCollector { new_rent_collector } => {
258                    multisig.rent_collector = *new_rent_collector;
259
260                    // We don't need to invalidate prior transactions here because changing
261                    // `rent_collector` doesn't affect the consensus parameters of the multisig.
262                }
263            }
264        }
265
266        // Make sure the multisig account can fit the updated state: added members or newly set rent_collector.
267        Multisig::realloc_if_needed(
268            multisig.to_account_info(),
269            multisig.members.len(),
270            ctx.accounts
271                .rent_payer
272                .as_ref()
273                .map(ToAccountInfo::to_account_info),
274            ctx.accounts
275                .system_program
276                .as_ref()
277                .map(ToAccountInfo::to_account_info),
278        )?;
279
280        // Make sure the multisig state is valid after applying the actions.
281        multisig.invariant()?;
282
283        // Mark the proposal as executed.
284        proposal.status = ProposalStatus::Executed {
285            timestamp: Clock::get()?.unix_timestamp,
286        };
287
288        Ok(())
289    }
290}