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