Skip to main content

VSC-88: Multi-Sig Wallets

Multi-sig accounts require M-of-N approvals before executing transactions. This is the second tier of VSC-88, providing shared account control for teams, treasuries, and compliance-controlled assets.

Use Cases

  • Foundation treasury -- 3-of-5 core team approval for spending
  • Stablecoin issuer accounts -- 2-of-3 compliance officers
  • Bridge oracle key rotation -- 4-of-7 oracle operators
  • Project team treasuries -- Shared control with no single point of failure
  • Escrow accounts -- Neutral third-party involved in release

How It Works

  1. Create -- Define signers (2-20 addresses) and threshold (M approvals required)
  2. Propose -- Any signer proposes a transaction (proposer auto-approves)
  3. Approve -- Other signers approve. When threshold is met, the transaction executes automatically
  4. Execute -- Inner operations run with the multi-sig address as the sender

The multi-sig can execute any operation -- transfers, token creation, staking, DEX operations, bridge actions, and more.


Multi-Sig Address

The multi-sig account address is deterministic -- derived from the sorted signer list and threshold. This means you can compute the address locally before creating it on-chain.

info

The same set of signers with the same threshold always produces the same multi-sig address, regardless of who creates it or when.


CLI Usage

Create a Multi-Sig Account

# Create a 2-of-3 multi-sig
vexidus multisig create \
--signers Vx0Signer1...,Vx0Signer2...,Vx0Signer3... \
--threshold 2 \
--label "Team Treasury"

Propose a Transaction

# Propose a VXS transfer from the multi-sig
vexidus multisig propose \
--account Vx0MultiSigAddr... \
--operations '[{"Transfer":{"to":"Vx0Recipient...","token_symbol":"VXS","amount":50000000000}}]' \
--description "Monthly operational budget"

Approve a Pending Transaction

# Second signer approves -- if threshold met, tx executes automatically
vexidus multisig approve \
--account Vx0MultiSigAddr... \
--tx-id 0

Revoke an Approval

# Revoke before execution
vexidus multisig revoke \
--account Vx0MultiSigAddr... \
--tx-id 0

View Multi-Sig Details

# Show account info (signers, threshold, label)
vexidus multisig show --account Vx0MultiSigAddr...

# List all transactions (pending, executed)
vexidus multisig list-txs --account Vx0MultiSigAddr...

RPC Methods

MethodTypeDescription
vex_createMultiSigWriteCreate an M-of-N multi-sig account
vex_proposeMultiSigTxWritePropose a transaction (operations as JSON)
vex_approveMultiSigTxWriteApprove a pending transaction
vex_revokeMultiSigApprovalWriteRevoke an approval before execution
vex_getMultiSigReadGet multi-sig account details
vex_listMultiSigTxsReadList transactions for a multi-sig

Example: Create a Multi-Sig

curl -s https://testnet.vexidus.io \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"method": "vex_createMultiSig",
"params": [
"CREATOR_ADDRESS",
["SIGNER_1", "SIGNER_2", "SIGNER_3"],
2,
"Team Treasury"
],
"id": 1
}' | jq .

Example: Get Multi-Sig Info

curl -s https://testnet.vexidus.io \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"method": "vex_getMultiSig",
"params": ["MULTISIG_ADDRESS"],
"id": 1
}' | jq .

Response:

{
"address": "Vx0MultiSig...",
"signers": ["Vx0Signer1...", "Vx0Signer2...", "Vx0Signer3..."],
"threshold": 2,
"label": "Team Treasury",
"created_at_height": 145000
}

SDK Usage

use vexidus_sdk::BundleBuilder;
use vexidus_types::{Address, Operation};

// 1. Create a 2-of-3 multi-sig
let bundle = BundleBuilder::new(&creator)?
.create_multisig(
vec![signer_a, signer_b, signer_c],
2,
"Team Treasury".to_string(),
)
.nonce(nonce)
.sign(&creator_wallet);

// 2. Propose a transfer from the multi-sig
let ops = vec![Operation::Transfer {
to: recipient,
token_symbol: "VXS".to_string(),
amount: 50_000_000_000, // 50 VXS
}];

let bundle = BundleBuilder::new(&signer_a)?
.propose_multisig_tx(multisig_addr, ops, "Q1 budget".to_string())
.nonce(nonce)
.sign(&signer_a_wallet);

// 3. Approve (second signer -- triggers execution at threshold)
let bundle = BundleBuilder::new(&signer_b)?
.approve_multisig_tx(multisig_addr, 0) // tx_id=0
.nonce(nonce)
.sign(&signer_b_wallet);

// 4. (Optional) Revoke approval before execution
let bundle = BundleBuilder::new(&signer_b)?
.revoke_multisig_approval(multisig_addr, 0)
.nonce(nonce)
.sign(&signer_b_wallet);

Transaction Lifecycle

Proposed (Pending)
| proposer auto-approved (1/M)
|
+-- Signer B approves (2/M)
| |
| +-- Threshold met? --> Executed (operations run)
| |
| +-- Not yet --> still Pending
|
+-- Signer revokes --> remove approval, back to Pending

If threshold is 1 (single-signer multi-sig), the transaction executes immediately on proposal.


Constraints

ConstraintValue
Min signers2
Max signers20
Min threshold2
Max thresholdNumber of signers
Label length1 -- 64 characters
Description length1 -- 256 characters
Duplicate signersNot allowed
Zero addressesNot allowed
Supported inner operationsAll (transfer, stake, token create, DEX, bridge, etc.)
Self-modificationNot supported (cannot change signers/threshold)
To change signers

Create a new multi-sig account with the updated signer set and migrate funds from the old one. This prevents malicious signer reconfiguration.


Best Practices

  1. Use odd thresholds -- 2-of-3, 3-of-5, 4-of-7 prevent deadlocks where exactly half approve
  2. Store keys separately -- Different devices, different people, different physical locations
  3. Label descriptively -- The label is stored on-chain and visible in explorers
  4. Monitor pending transactions -- Signers should regularly check vex_listMultiSigTxs
  5. Start with higher thresholds -- You can always create a new multi-sig with a lower threshold, but you can't raise it without migration
  6. Test on testnet first -- Create a test multi-sig, propose, approve, and verify execution before using for real assets

Events

Multi-sig operations emit the following on-chain events (visible via vex_getTransactionReceipt):

EventWhen
MultiSigCreatedAccount created with signers and threshold
MultiSigTxProposedNew transaction proposed by a signer
MultiSigTxApprovedSigner approved (includes current approval count)
MultiSigTxExecutedThreshold met, inner operations executed
MultiSigApprovalRevokedSigner revoked their approval