Skip to content

feat(cli): vote subcommands for vault-controlled vote accounts (MUL-2311, 1/3) - #207

Closed
bguillaumat-squads wants to merge 2 commits into
mainfrom
feature/mul-2311-v4-cli-first-class-validator-commands-vote-account-stake
Closed

bguillaumat-squads wants to merge 2 commits into
mainfrom
feature/mul-2311-v4-cli-first-class-validator-commands-vote-account-stake

Conversation

@bguillaumat-squads

Copy link
Copy Markdown
Contributor

Part of MUL-2311 (1 of 3: vote account. Next: stake account commands, then the version bump and release).

What

New vote <op> subcommands let a validator manage a vote account whose authorized withdrawer is a Squads vault, using only the open source CLI:

  • vote withdraw (--amount <SOL> | --all | --close)
  • vote update-commission
  • vote authorize (--authority-type voter|withdrawer)
  • vote update-identity (the new identity co-signs at execution: vault-transaction-execute --extra-keypair)
  • vote update-commission-collector (--kind inflation-rewards|block-revenue, SIMD-0232)

Each command creates one vault transaction and its proposal (one operation per proposal, like the solana vote-* commands). display-transaction now labels vault accounts (vault N) and decodes vote instructions, because approvers on a hardware wallet only see the outer Squads instruction. The README documents the commands and adds a validator runbook.

Why

A validator asked whether they can operate a Squads-controlled vote account without any third-party UI. Before this PR, the CLI only offered vault-transaction-create with a raw serialized message, which nobody can use by hand (see also MUL-740).

How

  • Shared plumbing (vault_proposal.rs): flattened common flags (--vault-index is required, no auto-detection), then one propose() that builds the same VaultTransactionCreate + ProposalCreate (+ ProposalApprove) transaction as the existing initiate-* commands. Existing commands are untouched.
  • Checks before proposing:
    • The vote account must be owned by the vote program, and its withdrawer must be the vault at --vault-index; otherwise the command stops with an error.
    • The vault instructions are simulated with sig_verify: false, and the result is shown above the message hash. A failed simulation asks "propose anyway?" (default no) rather than refusing, because some operations fail now but succeed later (a commission change outside the allowed part of the epoch).
    • Moving the withdrawer, or a commission collector, to an address that is not one of the multisig's vaults needs an extra confirmation (default no) and is flagged in red by display-transaction. Voter changes are not flagged, since voters are normally hot keys.
    • A partial --amount withdrawal that would leave the account below its rent-exempt minimum is refused, since it can never execute.
  • Vote account parsing:
    • Accounts are read by byte offset, because solana-vote-interface 2.x (the version in the lockfile) cannot deserialize VoteStateV4, which is live on mainnet (SIMD-0185).
    • The withdrawer sits at bytes 36..68 in V1_14_11, V3 and V4.
    • In V4, the BLS key option tag is at byte 144. A test pins this against a real V4 account captured from a test validator.
  • SIMD-0232 instruction: vote ix 17 UpdateCommissionCollector is not in solana-vote-interface 2.x, so it is hand-encoded as [17,0,0,0,kind,0,0,0]. The bytes and account metas match update_commission_collector in solana-vote-interface 6, and it executed on chain in the end-to-end run below. Version 6 can't be used here: it needs a newer solana-pubkey major and Rust 1.89.
  • Voter change on BLS-keyed vote accounts: under SIMD-0387 (active on mainnet), a vote account that holds a BLS key rejects a plain Authorize(Voter) with "invalid instruction data". vote authorize --authority-type voter now stops before proposing on such accounts and points to solana vote-authorize-voter-checked, signed by the current voter. That command was checked to work on a BLS account: it builds the BLS variant itself. Supporting the BLS voter change from the multisig is left as a follow-up.
  • New dependency: solana-vote-interface = { version = "2.2", features = ["bincode"] }. It is already in Cargo.lock at 2.2.6 through solana-program, so no new crate is downloaded.
  • Not in this PR: native SOL in initiate-transfer (split into its own change), the stake commands (next PR), and the crate version bump (with the release PR).

Acceptance criteria (vote part)

# Criterion Status Evidence
1 Unit tests with exact instruction data and metas; ix 17 against the SIMD-0232 layout met (vote) update_commission_collector_matches_simd_0232, plus ix 17 executed on chain. The other four ops use the official solana-vote-interface builders.
2 End to end: propose → approve → execute, on-chain state checked, including identity change with --extra-keypair met (vote) Local solana-test-validator 4.3, with the mainnet Squads program and program config cloned (details under Testing).
3 Fails before proposing, with a readable error, when the vault isn't the required authority met check_vote_account_requires_the_vault_as_withdrawer; end to end with --vault-index 1 → "Vault … (index 1) is not the authorized withdrawer …"
4 Moving control off a vault needs an explicit confirm and is flagged by display-transaction met flags_withdrawer_moving_out_of_the_multisig; end to end, declining the warning proposed nothing; a collector moved outside the vaults showed the red flag in display-transaction.
5 display-transaction decodes every instruction these commands produce met instruction_decode tests, and checked on every proposal in the end-to-end run.
6 At least one command run with a Ledger signer not run Needs a device. The signer goes through the existing create_signer_from_path.
7 README and runbook merged, crate published partial README and runbook are in this PR. Publishing comes with the release PR.
– Stake account commands not in this PR Next PR.

Testing

Gate @ 3a1cd6e (tree clean)
cargo build -p squads-multisig-cli --locked        pass
cargo test -p squads-multisig-cli --locked         pass (20 tests)
cargo clippy -p squads-multisig-cli --locked       no warnings in new code (the only warning in a touched file, display_transaction.rs:9 unused import, predates this PR)
rustfmt --check on the new files                   pass (the repo is not rustfmt-clean overall; untouched files left as they are)
yarn build / anchor test (CI)                      not run locally: the programs are untouched, and CI does not build or test the cli crate

End to end on solana-test-validator 4.3.0-rc with --clone-upgradeable-program SQDS4ep65… and the program config cloned from mainnet. Setup: a threshold-1 multisig, and a vote account created by solana CLI 4.3 (V4 layout with a BLS key) with vault 0 as withdrawer. Commands were driven through a pty, because of the dialoguer prompts.

  • update-commission 10% → 5%, on chain.
  • withdraw --amount 1.25: the recipient gained 1.25 SOL.
  • update-identity, executed with --extra-keypair: the identity changed, and a block revenue collector pointing at the vault stayed on the vault.
  • update-commission-collector:
    • block revenue → a system account: the warning was shown and the collector changed;
    • inflation rewards → the vote account: no warning.
  • authorize withdrawer → vault 1: no warning; the withdrawer changed.
  • withdraw --all, then --close, from vault 1: the account was closed.
  • Runbook flow, once: propose without --approve → display-proposals → proposal-vote --action Approve → execute.

Refusals and errors:

  • Wrong vault: refused before proposing.
  • Withdrawer moved to an outside key with the warning declined: nothing proposed.
  • Voter change on a BLS account: refused before proposing. Before that guard, it failed in simulation and again at execution.
  • Partial withdrawal that would leave dust: refused.
  • Missing collector, or a non-vote account: readable errors.

Not run: Ledger; a voter change on a vote account without a BLS key (a 4.3 validator always creates one); mainnet.

Independent review: two rounds. Fixed:

  • the authority check moved into a pure function, with a test;
  • a real V4 account fixture;
  • "not found" vs RPC errors;
  • the close label for a full --amount.

Dismissed, with evidence:

  • "an identity change resets the block revenue collector": not the case on chain;
  • "--all ignores pending delegator rewards": SIMD-0123 is inactive on mainnet, and the simulation would catch it.

Pre-PR advisor check:

  • fixed: undecodable vote instructions are now flagged in display-transaction; the dust guard was added;
  • dismissed: "the solana vote-authorize-voter-checked advice fails on BLS accounts", which was run and works.

Risk / rollout

CLI only: no program, migration or backend change. The existing commands are unchanged apart from display-transaction, which prints extra labels and decoded rows. Publishing waits for the release PR, so nothing ships until then.

Follow-ups:

  • Voter change from the multisig on BLS-keyed accounts.
  • display-transaction decoding for vote instructions newer than 2.x, and for *WithSeed authority moves.
  • get_multisig in the SDK reports an RPC failure as "AccountNotFound: … error sending request".

…ount

Validators that make a Squads vault the authorized withdrawer of their vote
account had no way to operate it from open source tooling: the CLI only
offered raw-byte vault transactions. Add `vote withdraw`, `update-commission`,
`authorize`, `update-identity` and `update-commission-collector` (SIMD-0232),
one operation per proposal like the solana CLI.

Each command checks that the vault is the withdrawer, simulates the vault
instructions before proposing, and asks for an extra confirmation when the
withdrawer or a commission collector would leave the multisig's vaults.
display-transaction now labels vault accounts and decodes vote instructions,
since approvers on a hardware wallet only see the outer Squads instruction.

Part of MUL-2311.
@vercel

vercel Bot commented Oct 29, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
v4-sdk-typedoc Ready Ready Preview Oct 29, 2026 1:07pm UTC

Request Review

@jeff-sqds

Copy link
Copy Markdown
Contributor

should this work actually be ported over to the newer msig CLI tool (at least until the v4 repo is fully updated) https://github.com/Solana-Multisig-Tools/multisig-cli

@bguillaumat-squads
bguillaumat-squads deleted the feature/mul-2311-v4-cli-first-class-validator-commands-vote-account-stake branch November 6, 2026 23:27

This branch was successfully deployed

1 active deployment
Preview — 3a1cd6e9 Deployed Oct 29, 2026 by vercel[bot]
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.

2 participants