Skip to content

add signing by multisig to test-cli - #177

Open
kaze-cow wants to merge 4 commits into
mainfrom
kaze/sc-439-add-ability-to-submit-transaction-to-a-squads-multisig-to
Open

kaze-cow wants to merge 4 commits into
mainfrom
kaze/sc-439-add-ability-to-submit-transaction-to-a-squads-multisig-to

Conversation

@kaze-cow

@kaze-cow kaze-cow commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

Description

Add --multisig-address global option, which allows for redirecting any transaction to be sent via the test cli to come through a Squads Multisig instead.

Motivation

As we near launch, we need a way to propose transactions efficiently and safely. We are already executing many management operations via EOAs with the test cli, so being able to do this the same way would be very natural.

Implementation Strategy

Squads Instruction Setup & Encoding

3 operations need to be executed on the Squads multisig to stage and be the initial signer for a transaction:

  • VaultTransactionCreate -- writes the details of what should be executed on-chain
  • ProposalCreate -- creates a PDA to tally the votes
  • ProposalApprove -- approves the proposed transaction with a signer

Squads provides an official library that is intended for this purpose, but it appears to be poorly maintained and primarily designed for use with their own CLI client.

Squads V4 is an Anchor program, which means it has an IDL, downloadable from the squads program on solscan. So using the existing tooling we already have for codama IDL generation in the repo, we can generate the squads library from IDL and have compatibility with our more modern libraries.

The generated files are checked into the repo and validated for no drift via a newly added CI job.

Where does the generated library go?

Right now its embedded within the test cli as a subcrate. Technically we could have also added it to our existing IDL generation facilities in program/settlement/idl.

Approving a transaction

The user may optional specify an additional transaction, --squads-transaction-index

How to test

When reviewing the PR, note that a lot of the code is generated. In general, don't get lost in the weeds for all of the files within test-cli/src/utils/squads/, with the exception of test-cli/src/utils/squads/mod.rs which is not generated and used to tie the module together.

Probably the hardest (and unfortunately most critical) part to review is the chain of logic for confirming a proposal corresponds to the correct/staged transaction.

Probably the easiest wya to test this is to run some test transactions.

I have already been able to execute quite a few multisig bound transactions using thi branch, such as creating an order (with ATA creation!), transfer authority, and solver add/remove.

There are some edge cases we should check:

  • when creating an order, is the generated ATA/receiver account corresponding to the safe (as one would expect?)

@linear-code

linear-code Bot commented Oct 1, 2026

Copy link
Copy Markdown
Contributor

SC-439

@socket-security

socket-security Bot commented Oct 1, 2026 •

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Addednpm/​@​codama/​nodes-from-anchor@​1.5.6981008594100
Addednpm/​@​codama/​renderers-rust@​3.1.38710010095100
Addednpm/​codama@​1.11.09810010094100

View full report

Comment thread test-cli/src/cmd/mod.rs Outdated
Comment on lines +87 to +98
/// The keypair at `path` to sign as some role, defaulting to [`Self::payer`].
/// A Squads vault can't co-sign with other keypairs, so `path` is rejected
/// when proposing.
pub fn signer_or(&self, path: Option<String>) -> anyhow::Result<TxSigner<'_>> {
match (&self.squads, path) {
(Some(squads), None) => Ok(TxSigner::Vault(squads.vault)),
(Some(_), Some(path)) => anyhow::bail!(
"can't sign with {path} when proposing to a Squads multisig: the vault signs instead"
),
(None, path) => read_keypair_or(path, &self.signer).map(TxSigner::Keypair),
}
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

some background on this function as it was kind of confusing when I first read it:

its used in cases where a secondary/co-signing key may be supported. The problem is squads does not support co-signing/being an additional signer, so we need to provide a function here to make sure the user doesn't expect their keypair to be used when it actually isn't.

Comment thread test-cli/src/cmd/mod.rs
Comment on lines +118 to +123
Some(index) => multisig.approve(
&member,
squads.vault_index,
ixs,
&multisig.fetch_transaction(&self.rpc, index)?,
)?,

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this approve function is actually doing a lot of heavy lifting. While it doesn't look like it, it validates that the vault_index and ixs match up to the fetched transaction on line 122. If everything is good, it adds a signature to the multisig.

Comment thread test-cli/src/cmd/mod.rs
ixs,
&multisig.fetch_transaction(&self.rpc, index)?,
)?,
None => multisig.propose(&member, squads.vault_index, ixs)?,

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this also does some heavy lifting. its actually 3 instructions (VaultTransactionCreate, ProposalCreate, and ProposalApprove) rolled into one!

Comment on lines +30 to +37
const SEED_PREFIX: &[u8] = b"multisig";
const SEED_VAULT: &[u8] = b"vault";
const SEED_TRANSACTION: &[u8] = b"transaction";
const SEED_PROPOSAL: &[u8] = b"proposal";

/// `Permissions` mask bits a member needs to propose and to vote.
const PERMISSION_INITIATE: u8 = 1 << 0;
const PERMISSION_VOTE: u8 = 1 << 1;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

apparently these seeds weren't, or are not able, to be included as part of the actual IDL or read from the generated codama library. sorry.

@kaze-cow
kaze-cow marked this pull request as ready for review October 13, 2026 07:19
@kaze-cow
kaze-cow requested a review from a team as a code owner October 13, 2026 07:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant