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