Skip to main content

squads_multisig_program/state/
vault_transaction.rs

1use anchor_lang::prelude::*;
2use anchor_lang::solana_program::borsh0_10::get_instance_packed_len;
3
4use crate::errors::*;
5use crate::instructions::{CompiledInstruction, MessageAddressTableLookup, TransactionMessage};
6
7/// Stores data required for tracking the voting and execution status of a vault transaction.
8/// Vault transaction is a transaction that's executed on behalf of the multisig vault PDA
9/// and wraps arbitrary Solana instructions, typically calling into other Solana programs.
10#[account]
11#[derive(Default)]
12pub struct VaultTransaction {
13    /// The multisig this belongs to.
14    pub multisig: Pubkey,
15    /// Member of the Multisig who submitted the transaction.
16    pub creator: Pubkey,
17    /// Index of this transaction within the multisig.
18    pub index: u64,
19    /// bump for the transaction seeds.
20    pub bump: u8,
21    /// Index of the vault this transaction belongs to.
22    pub vault_index: u8,
23    /// Derivation bump of the vault PDA this transaction belongs to.
24    pub vault_bump: u8,
25    /// Derivation bumps for additional signers.
26    /// Some transactions require multiple signers. Often these additional signers are "ephemeral" keypairs
27    /// that are generated on the client with a sole purpose of signing the transaction and be discarded immediately after.
28    /// When wrapping such transactions into multisig ones, we replace these "ephemeral" signing keypairs
29    /// with PDAs derived from the MultisigTransaction's `transaction_index` and controlled by the Multisig Program;
30    /// during execution the program includes the seeds of these PDAs into the `invoke_signed` calls,
31    /// thus "signing" on behalf of these PDAs.
32    pub ephemeral_signer_bumps: Vec<u8>,
33    /// data required for executing the transaction.
34    pub message: VaultTransactionMessage,
35}
36
37impl VaultTransaction {
38    pub fn size(ephemeral_signers_length: u8, transaction_message: &[u8]) -> Result<usize> {
39        let transaction_message: VaultTransactionMessage =
40            TransactionMessage::deserialize(&mut &transaction_message[..])?.try_into()?;
41        let message_size = get_instance_packed_len(&transaction_message).unwrap_or_default();
42
43        Ok(
44            8 +   // anchor account discriminator
45            32 +  // multisig
46            32 +  // creator
47            8 +   // index
48            1 +   // bump
49            1 +   // vault_index
50            1 +   // vault_bump
51            (4 + usize::from(ephemeral_signers_length)) +   // ephemeral_signers_bumps vec
52            message_size, // message
53        )
54    }
55    /// Reduces the VaultTransaction to its default empty value and moves
56    /// ownership of the data to the caller/return value.
57    pub fn take(&mut self) -> VaultTransaction {
58        core::mem::take(self)
59    }
60}
61
62#[derive(AnchorSerialize, AnchorDeserialize, Clone, Default)]
63pub struct VaultTransactionMessage {
64    /// The number of signer pubkeys in the account_keys vec.
65    pub num_signers: u8,
66    /// The number of writable signer pubkeys in the account_keys vec.
67    pub num_writable_signers: u8,
68    /// The number of writable non-signer pubkeys in the account_keys vec.
69    pub num_writable_non_signers: u8,
70    /// Unique account pubkeys (including program IDs) required for execution of the tx.
71    /// The signer pubkeys appear at the beginning of the vec, with writable pubkeys first, and read-only pubkeys following.
72    /// The non-signer pubkeys follow with writable pubkeys first and read-only ones following.
73    /// Program IDs are also stored at the end of the vec along with other non-signer non-writable pubkeys:
74    ///
75    /// ```plaintext
76    /// [pubkey1, pubkey2, pubkey3, pubkey4, pubkey5, pubkey6, pubkey7, pubkey8]
77    ///  |---writable---|  |---readonly---|  |---writable---|  |---readonly---|
78    ///  |------------signers-------------|  |----------non-singers-----------|
79    /// ```
80    pub account_keys: Vec<Pubkey>,
81    /// List of instructions making up the tx.
82    pub instructions: Vec<MultisigCompiledInstruction>,
83    /// List of address table lookups used to load additional accounts
84    /// for this transaction.
85    pub address_table_lookups: Vec<MultisigMessageAddressTableLookup>,
86}
87
88impl VaultTransactionMessage {
89    /// Returns the number of all the account keys (static + dynamic) in the message.
90    pub fn num_all_account_keys(&self) -> usize {
91        let num_account_keys_from_lookups = self
92            .address_table_lookups
93            .iter()
94            .map(|lookup| lookup.writable_indexes.len() + lookup.readonly_indexes.len())
95            .sum::<usize>();
96
97        self.account_keys.len() + num_account_keys_from_lookups
98    }
99
100    /// Returns true if the account at the specified index is a part of static `account_keys` and was requested to be writable.
101    pub fn is_static_writable_index(&self, key_index: usize) -> bool {
102        let num_account_keys = self.account_keys.len();
103        let num_signers = usize::from(self.num_signers);
104        let num_writable_signers = usize::from(self.num_writable_signers);
105        let num_writable_non_signers = usize::from(self.num_writable_non_signers);
106
107        if key_index >= num_account_keys {
108            // `index` is not a part of static `account_keys`.
109            return false;
110        }
111
112        if key_index < num_writable_signers {
113            // `index` is within the range of writable signer keys.
114            return true;
115        }
116
117        if key_index >= num_signers {
118            // `index` is within the range of non-signer keys.
119            let index_into_non_signers = key_index.saturating_sub(num_signers);
120            // Whether `index` is within the range of writable non-signer keys.
121            return index_into_non_signers < num_writable_non_signers;
122        }
123
124        false
125    }
126
127    /// Returns true if the account at the specified index was requested to be a signer.
128    pub fn is_signer_index(&self, key_index: usize) -> bool {
129        key_index < usize::from(self.num_signers)
130    }
131}
132
133impl TryFrom<TransactionMessage> for VaultTransactionMessage {
134    type Error = Error;
135
136    fn try_from(message: TransactionMessage) -> Result<Self> {
137        let account_keys: Vec<Pubkey> = message.account_keys.into();
138        let instructions: Vec<CompiledInstruction> = message.instructions.into();
139        let instructions: Vec<MultisigCompiledInstruction> = instructions
140            .into_iter()
141            .map(MultisigCompiledInstruction::from)
142            .collect();
143        let address_table_lookups: Vec<MessageAddressTableLookup> =
144            message.address_table_lookups.into();
145
146        let num_all_account_keys = account_keys.len()
147            + address_table_lookups
148                .iter()
149                .map(|lookup| lookup.writable_indexes.len() + lookup.readonly_indexes.len())
150                .sum::<usize>();
151
152        require!(
153            usize::from(message.num_signers) <= account_keys.len(),
154            MultisigError::InvalidTransactionMessage
155        );
156        require!(
157            message.num_writable_signers <= message.num_signers,
158            MultisigError::InvalidTransactionMessage
159        );
160        require!(
161            usize::from(message.num_writable_non_signers)
162                <= account_keys
163                    .len()
164                    .saturating_sub(usize::from(message.num_signers)),
165            MultisigError::InvalidTransactionMessage
166        );
167
168        // Validate that all program ID indices and account indices are within the bounds of the account keys.
169        for instruction in &instructions {
170            require!(
171                usize::from(instruction.program_id_index) < num_all_account_keys,
172                MultisigError::InvalidTransactionMessage
173            );
174
175            for account_index in &instruction.account_indexes {
176                require!(
177                    usize::from(*account_index) < num_all_account_keys,
178                    MultisigError::InvalidTransactionMessage
179                );
180            }
181        }
182
183        Ok(Self {
184            num_signers: message.num_signers,
185            num_writable_signers: message.num_writable_signers,
186            num_writable_non_signers: message.num_writable_non_signers,
187            account_keys,
188            instructions,
189            address_table_lookups: address_table_lookups
190                .into_iter()
191                .map(MultisigMessageAddressTableLookup::from)
192                .collect(),
193        })
194    }
195}
196
197/// Concise serialization schema for instructions that make up a transaction.
198/// Closely mimics the Solana transaction wire format.
199#[derive(AnchorSerialize, AnchorDeserialize, Clone)]
200pub struct MultisigCompiledInstruction {
201    pub program_id_index: u8,
202    /// Indices into the tx's `account_keys` list indicating which accounts to pass to the instruction.
203    pub account_indexes: Vec<u8>,
204    /// Instruction data.
205    pub data: Vec<u8>,
206}
207
208impl From<CompiledInstruction> for MultisigCompiledInstruction {
209    fn from(compiled_instruction: CompiledInstruction) -> Self {
210        Self {
211            program_id_index: compiled_instruction.program_id_index,
212            account_indexes: compiled_instruction.account_indexes.into(),
213            data: compiled_instruction.data.into(),
214        }
215    }
216}
217
218/// Address table lookups describe an on-chain address lookup table to use
219/// for loading more readonly and writable accounts into a transaction.
220#[derive(AnchorSerialize, AnchorDeserialize, Clone)]
221pub struct MultisigMessageAddressTableLookup {
222    /// Address lookup table account key.
223    pub account_key: Pubkey,
224    /// List of indexes used to load writable accounts.
225    pub writable_indexes: Vec<u8>,
226    /// List of indexes used to load readonly accounts.
227    pub readonly_indexes: Vec<u8>,
228}
229
230impl From<MessageAddressTableLookup> for MultisigMessageAddressTableLookup {
231    fn from(m: MessageAddressTableLookup) -> Self {
232        Self {
233            account_key: m.account_key,
234            writable_indexes: m.writable_indexes.into(),
235            readonly_indexes: m.readonly_indexes.into(),
236        }
237    }
238}