Skip to main content

squads_multisig_program/instructions/
transaction_accounts_close.rs

1//! Contains instructions for closing accounts related to ConfigTransactions,
2//! VaultTransactions and Batches.
3//!
4//! The differences between the 3 is minor but still exist. For example,
5//! a ConfigTransaction's accounts can always be closed if the proposal is stale,
6//! while for VaultTransactions and Batches it's not allowed if the proposal is stale but Approved,
7//! because they still can be executed in such a case.
8//!
9//! The other reason we have 3 different instructions is purely related to Anchor API which
10//! allows adding the `close` attribute only to `Account<'info, XXX>` types, which forces us
11//! into having 3 different `Accounts` structs.
12use anchor_lang::prelude::*;
13
14use crate::errors::*;
15use crate::state::*;
16use crate::utils;
17
18#[derive(Accounts)]
19pub struct ConfigTransactionAccountsClose<'info> {
20    #[account(
21        seeds = [SEED_PREFIX, SEED_MULTISIG, multisig.create_key.as_ref()],
22        bump = multisig.bump,
23        constraint = multisig.rent_collector.is_some() @ MultisigError::RentReclamationDisabled,
24    )]
25    pub multisig: Account<'info, Multisig>,
26
27    /// CHECK: `seeds` and `bump` verify that the account is the canonical Proposal,
28    ///         the logic within `config_transaction_accounts_close` does the rest of the checks.
29    #[account(
30        mut,
31        seeds = [
32            SEED_PREFIX,
33            multisig.key().as_ref(),
34            SEED_TRANSACTION,
35            &transaction.index.to_le_bytes(),
36            SEED_PROPOSAL,
37        ],
38        bump,
39    )]
40    pub proposal: AccountInfo<'info>,
41
42    /// ConfigTransaction corresponding to the `proposal`.
43    #[account(
44        mut,
45        has_one = multisig @ MultisigError::TransactionForAnotherMultisig,
46        close = rent_collector
47    )]
48    pub transaction: Account<'info, ConfigTransaction>,
49
50    /// The rent collector.
51    /// CHECK: We only need to validate the address.
52    #[account(
53        mut,
54        address = multisig.rent_collector.unwrap().key() @ MultisigError::InvalidRentCollector,
55    )]
56    pub rent_collector: AccountInfo<'info>,
57
58    pub system_program: Program<'info, System>,
59}
60
61impl ConfigTransactionAccountsClose<'_> {
62    /// Closes a `ConfigTransaction` and the corresponding `Proposal`.
63    /// `transaction` can be closed if either:
64    /// - the `proposal` is in a terminal state: `Executed`, `Rejected`, or `Cancelled`.
65    /// - the `proposal` is stale.
66    pub fn config_transaction_accounts_close(ctx: Context<Self>) -> Result<()> {
67        let multisig = &ctx.accounts.multisig;
68        let transaction = &ctx.accounts.transaction;
69        let proposal = &mut ctx.accounts.proposal;
70        let rent_collector = &ctx.accounts.rent_collector;
71
72        let is_stale = transaction.index <= multisig.stale_transaction_index;
73
74        let proposal_account = if proposal.data.borrow().is_empty() {
75            None
76        } else {
77            Some(Proposal::try_deserialize(
78                &mut &**proposal.data.borrow_mut(),
79            )?)
80        };
81
82        #[allow(deprecated)]
83        let can_close = if let Some(proposal_account) = &proposal_account {
84            match proposal_account.status {
85                // Draft proposals can only be closed if stale,
86                // so they can't be activated anymore.
87                ProposalStatus::Draft { .. } => is_stale,
88                // Active proposals can only be closed if stale,
89                // so they can't be voted on anymore.
90                ProposalStatus::Active { .. } => is_stale,
91                // Approved proposals for ConfigTransactions can be closed if stale,
92                // because they cannot be executed anymore.
93                ProposalStatus::Approved { .. } => is_stale,
94                // Rejected proposals can be closed.
95                ProposalStatus::Rejected { .. } => true,
96                // Executed proposals can be closed.
97                ProposalStatus::Executed { .. } => true,
98                // Cancelled proposals can be closed.
99                ProposalStatus::Cancelled { .. } => true,
100                // Should never really be in this state.
101                ProposalStatus::Executing => false,
102            }
103        } else {
104            // If no Proposal account exists then the ConfigTransaction can only be closed if stale
105            is_stale
106        };
107
108        require!(can_close, MultisigError::InvalidProposalStatus);
109
110        // Close the `proposal` account if exists.
111        if proposal_account.is_some() {
112            utils::close(
113                ctx.accounts.proposal.to_account_info(),
114                rent_collector.to_account_info(),
115            )?;
116        }
117
118        // Anchor will close the `transaction` account for us.
119        Ok(())
120    }
121}
122
123#[derive(Accounts)]
124pub struct VaultTransactionAccountsClose<'info> {
125    #[account(
126        seeds = [SEED_PREFIX, SEED_MULTISIG, multisig.create_key.as_ref()],
127        bump = multisig.bump,
128        constraint = multisig.rent_collector.is_some() @ MultisigError::RentReclamationDisabled,
129    )]
130    pub multisig: Account<'info, Multisig>,
131
132    /// CHECK: `seeds` and `bump` verify that the account is the canonical Proposal,
133    ///         the logic within `vault_transaction_accounts_close` does the rest of the checks.
134    #[account(
135        mut,
136        seeds = [
137            SEED_PREFIX,
138            multisig.key().as_ref(),
139            SEED_TRANSACTION,
140            &transaction.index.to_le_bytes(),
141            SEED_PROPOSAL,
142        ],
143        bump,
144    )]
145    pub proposal: AccountInfo<'info>,
146
147    /// VaultTransaction corresponding to the `proposal`.
148    #[account(
149        mut,
150        has_one = multisig @ MultisigError::TransactionForAnotherMultisig,
151        close = rent_collector
152    )]
153    pub transaction: Account<'info, VaultTransaction>,
154
155    /// The rent collector.
156    /// CHECK: We only need to validate the address.
157    #[account(
158        mut,
159        address = multisig.rent_collector.unwrap().key() @ MultisigError::InvalidRentCollector,
160    )]
161    pub rent_collector: AccountInfo<'info>,
162
163    pub system_program: Program<'info, System>,
164}
165
166impl VaultTransactionAccountsClose<'_> {
167    /// Closes a `VaultTransaction` and the corresponding `Proposal`.
168    /// `transaction` can be closed if either:
169    /// - the `proposal` is in a terminal state: `Executed`, `Rejected`, or `Cancelled`.
170    /// - the `proposal` is stale and not `Approved`.
171    pub fn vault_transaction_accounts_close(
172        ctx: Context<VaultTransactionAccountsClose>,
173    ) -> Result<()> {
174        let multisig = &ctx.accounts.multisig;
175        let transaction = &ctx.accounts.transaction;
176        let proposal = &mut ctx.accounts.proposal;
177        let rent_collector = &ctx.accounts.rent_collector;
178
179        let is_stale = transaction.index <= multisig.stale_transaction_index;
180
181        let proposal_account = if proposal.data.borrow().is_empty() {
182            None
183        } else {
184            Some(Proposal::try_deserialize(
185                &mut &**proposal.data.borrow_mut(),
186            )?)
187        };
188
189        #[allow(deprecated)]
190        let can_close = if let Some(proposal_account) = &proposal_account {
191            match proposal_account.status {
192                // Draft proposals can only be closed if stale,
193                // so they can't be activated anymore.
194                ProposalStatus::Draft { .. } => is_stale,
195                // Active proposals can only be closed if stale,
196                // so they can't be voted on anymore.
197                ProposalStatus::Active { .. } => is_stale,
198                // Approved proposals for VaultTransactions cannot be closed even if stale,
199                // because they still can be executed.
200                ProposalStatus::Approved { .. } => false,
201                // Rejected proposals can be closed.
202                ProposalStatus::Rejected { .. } => true,
203                // Executed proposals can be closed.
204                ProposalStatus::Executed { .. } => true,
205                // Cancelled proposals can be closed.
206                ProposalStatus::Cancelled { .. } => true,
207                // Should never really be in this state.
208                ProposalStatus::Executing => false,
209            }
210        } else {
211            // If no Proposal account exists then the VaultTransaction can only be closed if stale
212            is_stale
213        };
214
215        require!(can_close, MultisigError::InvalidProposalStatus);
216
217        // Close the `proposal` account if exists.
218        if proposal_account.is_some() {
219            utils::close(
220                ctx.accounts.proposal.to_account_info(),
221                rent_collector.to_account_info(),
222            )?;
223        }
224
225        // Anchor will close the `transaction` account for us.
226        Ok(())
227    }
228}
229
230//region VaultBatchTransactionAccountClose
231#[derive(Accounts)]
232pub struct VaultBatchTransactionAccountClose<'info> {
233    #[account(
234        seeds = [SEED_PREFIX, SEED_MULTISIG, multisig.create_key.as_ref()],
235        bump = multisig.bump,
236        constraint = multisig.rent_collector.is_some() @ MultisigError::RentReclamationDisabled,
237    )]
238    pub multisig: Account<'info, Multisig>,
239
240    #[account(
241        has_one = multisig @ MultisigError::ProposalForAnotherMultisig,
242    )]
243    pub proposal: Account<'info, Proposal>,
244
245    /// `Batch` corresponding to the `proposal`.
246    #[account(
247        mut,
248        has_one = multisig @ MultisigError::TransactionForAnotherMultisig,
249        constraint = batch.index == proposal.transaction_index @ MultisigError::TransactionNotMatchingProposal,
250    )]
251    pub batch: Account<'info, Batch>,
252
253    /// `VaultBatchTransaction` account to close.
254    /// The transaction must be the current last one in the batch.
255    #[account(
256        mut,
257        close = rent_collector,
258    )]
259    pub transaction: Account<'info, VaultBatchTransaction>,
260
261    /// The rent collector.
262    /// CHECK: We only need to validate the address.
263    #[account(
264        mut,
265        address = multisig.rent_collector.unwrap().key() @ MultisigError::InvalidRentCollector,
266    )]
267    pub rent_collector: AccountInfo<'info>,
268
269    pub system_program: Program<'info, System>,
270}
271
272impl VaultBatchTransactionAccountClose<'_> {
273    fn validate(&self) -> Result<()> {
274        let Self {
275            multisig,
276            proposal,
277            batch,
278            transaction,
279            ..
280        } = self;
281
282        // Transaction must be the last one in the batch.
283        // We do it here instead of the Anchor macro because we want to throw a more specific error,
284        // and the macro doesn't allow us to override the default "seeds constraint is violated" one.
285        // First, derive the address of the last transaction as if provided transaction is the last one.
286        let last_transaction_address = Pubkey::create_program_address(
287            &[
288                SEED_PREFIX,
289                multisig.key().as_ref(),
290                SEED_TRANSACTION,
291                &batch.index.to_le_bytes(),
292                SEED_BATCH_TRANSACTION,
293                // Last transaction index.
294                &batch.size.to_le_bytes(),
295                // We can assume the transaction bump is correct here.
296                &transaction.bump.to_le_bytes(),
297            ],
298            &crate::id(),
299        )
300        .map_err(|_| MultisigError::TransactionNotLastInBatch)?;
301
302        // Then compare it to the provided transaction address.
303        require_keys_eq!(
304            transaction.key(),
305            last_transaction_address,
306            MultisigError::TransactionNotLastInBatch
307        );
308
309        let is_proposal_stale = proposal.transaction_index <= multisig.stale_transaction_index;
310
311        #[allow(deprecated)]
312        let can_close = match proposal.status {
313            // Transactions of Draft proposals can only be closed if stale,
314            // so the proposal can't be activated anymore.
315            ProposalStatus::Draft { .. } => is_proposal_stale,
316            // Transactions of Active proposals can only be closed if stale,
317            // so the proposal can't be voted on anymore.
318            ProposalStatus::Active { .. } => is_proposal_stale,
319            // Transactions of Approved proposals for `Batch`es cannot be closed even if stale,
320            // because they still can be executed.
321            ProposalStatus::Approved { .. } => false,
322            // Transactions of Rejected proposals can be closed.
323            ProposalStatus::Rejected { .. } => true,
324            // Transactions of Executed proposals can be closed.
325            ProposalStatus::Executed { .. } => true,
326            // Transactions of Cancelled proposals can be closed.
327            ProposalStatus::Cancelled { .. } => true,
328            // Should never really be in this state.
329            ProposalStatus::Executing => false,
330        };
331
332        require!(can_close, MultisigError::InvalidProposalStatus);
333
334        Ok(())
335    }
336
337    /// Closes a `VaultBatchTransaction` belonging to the `batch` and `proposal`.
338    /// Closing a transaction reduces the `batch.size` by 1.
339    /// `transaction` must be closed in the order from the last to the first,
340    /// and the operation is only allowed if any of the following conditions is met:
341    /// - the `proposal` is in a terminal state: `Executed`, `Rejected`, or `Cancelled`.
342    /// - the `proposal` is stale and not `Approved`.
343    #[access_control(ctx.accounts.validate())]
344    pub fn vault_batch_transaction_account_close(ctx: Context<Self>) -> Result<()> {
345        let batch = &mut ctx.accounts.batch;
346
347        batch.size = batch.size.checked_sub(1).expect("overflow");
348
349        // Anchor macro will close the `transaction` account for us.
350
351        Ok(())
352    }
353}
354//endregion
355
356//region BatchAccountsClose
357#[derive(Accounts)]
358pub struct BatchAccountsClose<'info> {
359    #[account(
360        seeds = [SEED_PREFIX, SEED_MULTISIG, multisig.create_key.as_ref()],
361        bump = multisig.bump,
362        constraint = multisig.rent_collector.is_some() @ MultisigError::RentReclamationDisabled,
363    )]
364    pub multisig: Account<'info, Multisig>,
365
366    // pub proposal: Account<'info, Proposal>,
367    /// CHECK: `seeds` and `bump` verify that the account is the canonical Proposal,
368    ///         the logic within `batch_accounts_close` does the rest of the checks.
369    #[account(
370        mut,
371        seeds = [
372            SEED_PREFIX,
373            multisig.key().as_ref(),
374            SEED_TRANSACTION,
375            &batch.index.to_le_bytes(),
376            SEED_PROPOSAL,
377        ],
378        bump,
379    )]
380    pub proposal: AccountInfo<'info>,
381
382    /// `Batch` corresponding to the `proposal`.
383    #[account(
384        mut,
385        has_one = multisig @ MultisigError::TransactionForAnotherMultisig,
386        close = rent_collector
387    )]
388    pub batch: Account<'info, Batch>,
389
390    /// The rent collector.
391    /// CHECK: We only need to validate the address.
392    #[account(
393        mut,
394        address = multisig.rent_collector.unwrap().key() @ MultisigError::InvalidRentCollector,
395    )]
396    pub rent_collector: AccountInfo<'info>,
397
398    pub system_program: Program<'info, System>,
399}
400
401impl BatchAccountsClose<'_> {
402    /// Closes Batch and the corresponding Proposal accounts for proposals in terminal states:
403    /// `Executed`, `Rejected`, or `Cancelled` or stale proposals that aren't `Approved`.
404    ///
405    /// This instruction is only allowed to be executed when all `VaultBatchTransaction` accounts
406    /// in the `batch` are already closed: `batch.size == 0`.
407    pub fn batch_accounts_close(ctx: Context<Self>) -> Result<()> {
408        let multisig = &ctx.accounts.multisig;
409        let batch = &ctx.accounts.batch;
410        let proposal = &mut ctx.accounts.proposal;
411        let rent_collector = &ctx.accounts.rent_collector;
412
413        let is_stale = batch.index <= multisig.stale_transaction_index;
414
415        let proposal_account = if proposal.data.borrow().is_empty() {
416            None
417        } else {
418            Some(Proposal::try_deserialize(
419                &mut &**proposal.data.borrow_mut(),
420            )?)
421        };
422
423        #[allow(deprecated)]
424        let can_close = if let Some(proposal_account) = &proposal_account {
425            match proposal_account.status {
426                // Draft proposals can only be closed if stale,
427                // so they can't be activated anymore.
428                ProposalStatus::Draft { .. } => is_stale,
429                // Active proposals can only be closed if stale,
430                // so they can't be voted on anymore.
431                ProposalStatus::Active { .. } => is_stale,
432                // Approved proposals for `Batch`s cannot be closed even if stale,
433                // because they still can be executed.
434                ProposalStatus::Approved { .. } => false,
435                // Rejected proposals can be closed.
436                ProposalStatus::Rejected { .. } => true,
437                // Executed proposals can be closed.
438                ProposalStatus::Executed { .. } => true,
439                // Cancelled proposals can be closed.
440                ProposalStatus::Cancelled { .. } => true,
441                // Should never really be in this state.
442                ProposalStatus::Executing => false,
443            }
444        } else {
445            // If no Proposal account exists then the Batch can only be closed if stale
446            is_stale
447        };
448
449        require!(can_close, MultisigError::InvalidProposalStatus);
450
451        // Batch must be empty.
452        require_eq!(batch.size, 0, MultisigError::BatchNotEmpty);
453
454        // Close the `proposal` account if exists.
455        if proposal_account.is_some() {
456            utils::close(
457                ctx.accounts.proposal.to_account_info(),
458                rent_collector.to_account_info(),
459            )?;
460        }
461
462        // Anchor will close the `batch` account for us.
463        Ok(())
464    }
465}
466//endregion