Skip to content

Commit e09f9a9

Browse files
committed
feat(vault_transaction_execute,batch_execute_transaction): enforce ms accounts readonly in CPI instead of reentrancy checks
1 parent e4ce513 commit e09f9a9

8 files changed

Lines changed: 95 additions & 102 deletions

File tree

programs/multisig/src/errors.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,6 @@ pub enum MultisigError {
3232
InvalidNumberOfAccounts,
3333
#[msg("Invalid account provided")]
3434
InvalidAccount,
35-
#[msg("`transaction_execute` reentrancy is forbidden")]
36-
ExecuteReentrancy,
3735
#[msg("Cannot remove last member")]
3836
RemoveLastMember,
3937
#[msg("Members don't include any voters")]
@@ -62,4 +60,6 @@ pub enum MultisigError {
6260
DecimalsMismatch,
6361
#[msg("Member has unknown permission")]
6462
UnknownPermission,
63+
#[msg("Account is protected, it cannot be passed into a CPI as writable")]
64+
ProtectedAccount,
6565
}

programs/multisig/src/instructions/batch_execute_transaction.rs

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ impl BatchExecuteTransaction<'_> {
104104

105105
/// Execute a transaction from the batch.
106106
#[access_control(ctx.accounts.validate())]
107-
pub fn batch_execute_transaction(ctx: Context<BatchExecuteTransaction>) -> Result<()> {
107+
pub fn batch_execute_transaction(ctx: Context<Self>) -> Result<()> {
108108
let multisig = &mut ctx.accounts.multisig;
109109
let proposal = &mut ctx.accounts.proposal;
110110
let batch = &mut ctx.accounts.batch;
@@ -146,11 +146,7 @@ impl BatchExecuteTransaction<'_> {
146146
&ephemeral_signer_keys,
147147
)?;
148148

149-
let current_status = proposal.status.clone();
150-
// Set the proposal state to Executing to prevent reentrancy attacks (e.g. cancelling proposal) in the middle of execution.
151-
proposal.status = ProposalStatus::Executing;
152-
let proposal_account_info = proposal.to_account_info();
153-
proposal.try_serialize(&mut &mut proposal_account_info.data.borrow_mut()[..])?;
149+
let protected_accounts = &[proposal.key(), batch_key];
154150

155151
// Execute the transaction message instructions one-by-one.
156152
executable_message.execute_message(
@@ -159,11 +155,9 @@ impl BatchExecuteTransaction<'_> {
159155
.map(|seed| seed.to_vec())
160156
.collect::<Vec<Vec<u8>>>(),
161157
&ephemeral_signer_seeds,
158+
protected_accounts,
162159
)?;
163160

164-
// Restore the proposal status after execution.
165-
proposal.status = current_status;
166-
167161
// Increment the executed transaction index.
168162
batch.executed_transaction_index = batch
169163
.executed_transaction_index

programs/multisig/src/instructions/vault_transaction_execute.rs

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@ pub struct VaultTransactionExecute<'info> {
2828

2929
/// The transaction to execute.
3030
#[account(
31-
mut,
3231
seeds = [
3332
SEED_PREFIX,
3433
multisig.key().as_ref(),
@@ -39,7 +38,6 @@ pub struct VaultTransactionExecute<'info> {
3938
)]
4039
pub transaction: Account<'info, VaultTransaction>,
4140

42-
#[account(mut)]
4341
pub member: Signer<'info>,
4442
// `remaining_accounts` must include the following accounts in the exact order:
4543
// 1. AddressLookupTable accounts in the order they appear in `message.address_table_lookups`.
@@ -128,10 +126,7 @@ impl VaultTransactionExecute<'_> {
128126
&ephemeral_signer_keys,
129127
)?;
130128

131-
// Set the proposal state to Executing to prevent reentrancy attacks (e.g. cancelling proposal) in the middle of execution.
132-
proposal.status = ProposalStatus::Executing;
133-
let proposal_account_info = proposal.to_account_info();
134-
proposal.try_serialize(&mut &mut proposal_account_info.data.borrow_mut()[..])?;
129+
let protected_accounts = &[proposal.key()];
135130

136131
// Execute the transaction message instructions one-by-one.
137132
executable_message.execute_message(
@@ -140,6 +135,7 @@ impl VaultTransactionExecute<'_> {
140135
.map(|seed| seed.to_vec())
141136
.collect::<Vec<Vec<u8>>>(),
142137
&ephemeral_signer_seeds,
138+
protected_accounts,
143139
)?;
144140

145141
// Mark the proposal as executed.

programs/multisig/src/state/proposal.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,9 @@ pub enum ProposalStatus {
137137
/// Proposal has been approved and is pending execution.
138138
Approved { timestamp: i64 },
139139
/// Proposal is being executed. This is a transient state that always transitions to `Executed` in the span of a single transaction.
140+
#[deprecated(
141+
note = "This status used to be used to prevent reentrancy attacks. It is no longer needed."
142+
)]
140143
Executing,
141144
/// Proposal has been executed.
142145
Executed { timestamp: i64 },

programs/multisig/src/utils/executable_transaction_message.rs

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,9 @@ use std::convert::From;
44
use anchor_lang::prelude::*;
55
use anchor_lang::solana_program::instruction::Instruction;
66
use anchor_lang::solana_program::program::invoke_signed;
7-
use anchor_lang::Discriminator;
87
use solana_address_lookup_table_program::state::AddressLookupTable;
98

109
use crate::errors::*;
11-
use crate::id;
1210
use crate::state::*;
1311

1412
/// Sanitized and validated combination of a `MsTransactionMessage` and `AccountInfo`s it references.
@@ -171,21 +169,23 @@ impl<'a, 'info> ExecutableTransactionMessage<'a, 'info> {
171169
})
172170
}
173171

172+
/// Executes all instructions in the message via CPI calls.
173+
/// # Arguments
174+
/// * `vault_seeds` - Seeds for the vault PDA.
175+
/// * `ephemeral_signer_seeds` - Seeds for the ephemeral signer PDAs.
176+
/// * `protected_accounts` - Accounts that must not be passed as writable to the CPI calls to prevent potential reentrancy attacks.
174177
pub fn execute_message(
175178
&self,
176179
vault_seeds: &[Vec<u8>],
177180
ephemeral_signer_seeds: &[Vec<Vec<u8>>],
181+
protected_accounts: &[Pubkey],
178182
) -> Result<()> {
179183
for (ix, account_infos) in self.to_instructions_and_accounts().iter() {
180-
// Make sure we don't allow reentrancy of transaction_execute.
181-
if ix.program_id == id() {
182-
require!(
183-
ix.data[..8] != crate::instruction::VaultTransactionExecute::DISCRIMINATOR,
184-
MultisigError::ExecuteReentrancy
185-
);
184+
// Make sure we don't pass protected accounts as writable to CPI calls.
185+
for account_meta in ix.accounts.iter().filter(|m| m.is_writable) {
186186
require!(
187-
ix.data[..8] != crate::instruction::BatchExecuteTransaction::DISCRIMINATOR,
188-
MultisigError::ExecuteReentrancy
187+
!protected_accounts.contains(&account_meta.pubkey),
188+
MultisigError::ProtectedAccount
189189
);
190190
}
191191

sdk/multisig/idl/multisig.json

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -396,15 +396,15 @@
396396
},
397397
{
398398
"name": "transaction",
399-
"isMut": true,
399+
"isMut": false,
400400
"isSigner": false,
401401
"docs": [
402402
"The transaction to execute."
403403
]
404404
},
405405
{
406406
"name": "member",
407-
"isMut": true,
407+
"isMut": false,
408408
"isSigner": true
409409
}
410410
],
@@ -2113,78 +2113,78 @@
21132113
},
21142114
{
21152115
"code": 6015,
2116-
"name": "ExecuteReentrancy",
2117-
"msg": "`transaction_execute` reentrancy is forbidden"
2118-
},
2119-
{
2120-
"code": 6016,
21212116
"name": "RemoveLastMember",
21222117
"msg": "Cannot remove last member"
21232118
},
21242119
{
2125-
"code": 6017,
2120+
"code": 6016,
21262121
"name": "NoVoters",
21272122
"msg": "Members don't include any voters"
21282123
},
21292124
{
2130-
"code": 6018,
2125+
"code": 6017,
21312126
"name": "NoProposers",
21322127
"msg": "Members don't include any proposers"
21332128
},
21342129
{
2135-
"code": 6019,
2130+
"code": 6018,
21362131
"name": "NoExecutors",
21372132
"msg": "Members don't include any executors"
21382133
},
21392134
{
2140-
"code": 6020,
2135+
"code": 6019,
21412136
"name": "InvalidStaleTransactionIndex",
21422137
"msg": "`stale_transaction_index` must be <= `transaction_index`"
21432138
},
21442139
{
2145-
"code": 6021,
2140+
"code": 6020,
21462141
"name": "NotSupportedForControlled",
21472142
"msg": "Instruction not supported for controlled multisig"
21482143
},
21492144
{
2150-
"code": 6022,
2145+
"code": 6021,
21512146
"name": "TimeLockNotReleased",
21522147
"msg": "Proposal time lock has not been released"
21532148
},
21542149
{
2155-
"code": 6023,
2150+
"code": 6022,
21562151
"name": "NoActions",
21572152
"msg": "Config transaction must have at least one action"
21582153
},
21592154
{
2160-
"code": 6024,
2155+
"code": 6023,
21612156
"name": "MissingAccount",
21622157
"msg": "Missing account"
21632158
},
21642159
{
2165-
"code": 6025,
2160+
"code": 6024,
21662161
"name": "InvalidMint",
21672162
"msg": "Invalid mint"
21682163
},
21692164
{
2170-
"code": 6026,
2165+
"code": 6025,
21712166
"name": "InvalidDestination",
21722167
"msg": "Invalid destination"
21732168
},
21742169
{
2175-
"code": 6027,
2170+
"code": 6026,
21762171
"name": "SpendingLimitExceeded",
21772172
"msg": "Spending limit exceeded"
21782173
},
21792174
{
2180-
"code": 6028,
2175+
"code": 6027,
21812176
"name": "DecimalsMismatch",
21822177
"msg": "Decimals don't match the mint"
21832178
},
21842179
{
2185-
"code": 6029,
2180+
"code": 6028,
21862181
"name": "UnknownPermission",
21872182
"msg": "Member has unknown permission"
2183+
},
2184+
{
2185+
"code": 6029,
2186+
"name": "ProtectedAccount",
2187+
"msg": "Account is protected, it cannot be passed into a CPI as writable"
21882188
}
21892189
],
21902190
"metadata": {

0 commit comments

Comments
 (0)