Post
Share your knowledge.
Implementing Multi-Signature Wallets in Sui Move
Goal:
Build a secure and gas-efficient multisig wallet supporting:
✔ Flexible M-of-N signing policies
✔ Secure key rotation & recovery mechanisms
✔ Transaction replay protection
✔ Compatibility with existing wallet standards
Current Challenges:
- Basic shared ownership lacks threshold enforcement
- Secure key rotation implementation unclear
- Need robust replay attack prevention
Key Questions
-
Storage Design
- What’s the optimal data structure for managing dynamic signer sets?
-
Approval Logic
- How to efficiently implement M-of-N signature validation in Move?
-
Security & Maintenance
- Best practices for secure key rotation and recovery?
-
Attack Mitigation
- How to prevent signature replay attacks in Sui’s object model?
- Sui
- Transaction Processing
- Move
Answers
41. Core Multisig Architecture
Minimal Viable Implementation
module multisig::wallet {
use sui::tx_context;
use sui::object::{Self, UID};
use sui::dynamic_field;
use sui::vec_map;
struct MultisigWallet has key {
id: UID,
threshold: u8,
signers: vector<address>,
next_tx_id: u64
}
struct PendingTx has store {
approvals: vec_map::VecMap<address, bool>,
executed: bool
}
Key Features:
- Threshold-based execution (e.g., 2-of-3)
- Dynamic field storage for pending transactions
- Sequential TX IDs for replay protection
2. Transaction Flow
Proposal Submission
public entry fn propose_tx(
wallet: &mut MultisigWallet,
call: vector<u8>, // Serialized MoveCall
signer: &signer,
ctx: &mut TxContext
) {
assert!(vec_map::contains(&wallet.signers, address_of(signer)), ENotSigner);
let tx_id = wallet.next_tx_id;
wallet.next_tx_id = wallet.next_tx_id + 1;
dynamic_field::add(
&mut wallet.id,
tx_id,
PendingTx {
approvals: vec_map::empty().add(address_of(signer), true),
executed: false
}
);
}
Approval & Execution
public entry fn approve_tx(
wallet: &mut MultisigWallet,
tx_id: u64,
signer: &signer,
ctx: &mut TxContext
) {
let pending_tx = dynamic_field::borrow_mut(&wallet.id, tx_id);
pending_tx.approvals = pending_tx.approvals.add(address_of(signer), true);
if (vec_map::length(&pending_tx.approvals) >= wallet.threshold && !pending_tx.executed) {
pending_tx.executed = true;
move_call::execute(call, ctx); // Execute the payload
}
}
3. Advanced Features
Key Rotation
public entry fn rotate_signer(
wallet: &mut MultisigWallet,
old: address,
new: address,
approvals: vector<signer> // M existing signers
) {
assert!(count_approvals(approvals) >= wallet.threshold, EInsufficientApprovals);
wallet.signers = vec_map::replace(&wallet.signers, old, new);
}
Gas Optimization
// Batch approvals
public entry fn bulk_approve(
wallet: &mut MultisigWallet,
tx_ids: vector<u64>,
signer: &signer
) {
let i = 0;
while (i < vector::length(&tx_ids)) {
approve_tx(wallet, tx_ids[i], signer);
i = i + 1;
}
}
Recovery Mode
struct RecoveryConfig has key {
id: UID,
guardians: vector<address>,
unlock_time: u64
}
public entry fn initiate_recovery(
wallet: &mut MultisigWallet,
new_signers: vector<address>,
guardian_sigs: vector<signer>,
clock: &Clock
) {
assert!(clock.timestamp_ms >= recovery.unlock_time, ELocked);
assert!(count_guardian_sigs(guardian_sigs) >= 2/3, EInsufficientGuardians);
wallet.signers = new_signers;
}
4. Security Considerations
Replay Protection
// TX IDs increment monotonically
assert!(tx_id == wallet.next_tx_id, EStaleTx);
Signature Verification
// Prevent approval spoofing
assert!(
vec_map::contains(&wallet.signers, address_of(signer)),
EInvalidApprover
);
Threshold Safety
// Validate during initialization
assert!(threshold > 0 && threshold <= vec_map::length(&signers), EInvalidThreshold);
5. Testing Strategies
Unit Tests
#[test]
fun test_threshold_logic() {
let wallet = new_multisig(2, vector[@A, @B, @C]);
propose_tx(&mut wallet, b"call", &signer(@A));
approve_tx(&mut wallet, 0, &signer(@B));
assert!(tx_executed(0), ETxNotExecuted);
}
Fuzz Testing
#[test_only]
fun fuzz_thresholds(
m: u8,
n: u8
) {
assert!(m <= n, "Threshold cannot exceed signer count");
}
6. Integration Tips
Wallet Compatibility
// Implement SUI Wallet Standard:
public fun as_shared_object(wallet: &MultisigWallet): &mut UID {
&mut wallet.id
}
Event Emission
event::emit(MultisigEvent {
tx_id,
approved_by: address_of(signer)
});
Gas Savings
- Use
u16for thresholds (supports up to 65K signers) - Store signer addresses in
VecMapfor O(1) lookups
-
A multi-signature wallet allows multiple parties to approve a transaction before it can execute.
-
In Sui, you'll implement this as a shared object to allow composability and shared access.
-
For managing signer sets dynamically, use a struct containing a vector
and a BTreeMap<address, bool> for fast lookup. -
The wallet object should store the threshold M, the total number of signers N, and the current proposal state.
-
To enforce M-of-N logic, each signer must submit a signed approval that’s stored with the transaction proposal.
-
Approvals can be stored as vector
in the proposal object, and once M unique addresses are present, the action executes. -
Ensure replay protection by including a nonce or transaction counter in the wallet object.
-
Only allow transactions with a higher nonce than the previous, rejecting duplicate signatures or outdated proposals.
-
For gas efficiency, reject duplicate approvals early and use BTreeSet to avoid linear scans.
-
To support key rotation, implement an update_signers method that replaces signer sets and increments a config version.
-
Prevent unauthorized updates by gating signer changes behind an M-of-N consensus.
-
Use the has key ability so the multisig wallet is indexable and retrievable by explorers or wallets.
-
Wrap transactions in a Proposal object that includes action type, metadata, and current approval state.
-
Expose view functions to retrieve the current proposal state and remaining approvals required.
-
For safety, disallow re-using Proposal objects or hash their contents to ensure immutability.
-
Always test signer removal/addition scenarios, especially edge cases like signer overlap or full rotation.
-
Avoid replay vulnerabilities by rejecting transactions that attempt to re-use an already-executed nonce.
-
You can hash proposals with their nonce and store the hash in a ledger to prevent duplicates.
-
Integrate a fail-safe: a time-locked override or guardian key for recovery if signer access is lost.
-
Finally, document expected signer behavior and edge cases, and validate with localnet and CLI tests before mainnet deployment.
Implementing a multi-signature wallet in Sui using Move requires leveraging Sui’s object-centric architecture while building secure and gas-efficient systems for threshold-based signing, key management
🔹 1. Storage Design: Signer Set
Use a custom MultiSigWallet object with:
struct SignerSet has key { signers: vector
, // Current signer addresses threshold: u8, // Required number of approvals nonce: u64, // Replay protection }Keep signer list dynamic by designing update functions using object ownership rules.
Avoid storing vector
🔹 2. Approval Logic: M-of-N Threshold
To validate a transaction:
Accept an action + vector
Check each signature against stored signer addresses.
Ensure M >= threshold.
Use a deterministic ordering or hash to avoid replay from reordering.
Example in pseudo-Move:
fun validate_signatures(sigs: vector
🔹 3. Replay Protection
Include a nonce: u64 in the wallet object.
Every action should include the current nonce in its signed payload.
After a successful execution, increment the nonce to prevent reuse.
🔹 4. Key Rotation & Recovery
Design a special “governance” function like rotate_signers:
Must be signed by the existing M-of-N set.
Optionally, store a recovery address or trusted guardian with override permissions.
Structure:
public fun rotate_signers(wallet: &mut SignerSet, new_signers: vector
, new_threshold: u8) { assert!(validate_signatures(...)); // M-of-N approval wallet.signers = new_signers; wallet.threshold = new_threshold; }🔹 5. Attack Mitigation
Signature Replay: Use nonce and ensure it's signed.
Sybil or rogue signer: Add signer uniqueness validation.
Excess gas use: Avoid looping through many signers in execution path—optimize with hashed sets or tables.
🔹 6. Move Best Practices
Limit dynamic computation in critical paths.
Avoid deep recursion.
Use Table module (if needed) for dynamic signer access if the signer list is large.
Consider building a helper module for signature verification and signer management.
🔹 7. CLI / On-Chain Deployment
Use sui client or sui move build/publish to:
Compile and publish your multisig module.
Use sui client call or SDK to instantiate the wallet object.
Do you know the answer?
Please log in and share it.
Sui is a Layer 1 protocol blockchain designed as the first internet-scale programmable blockchain platform.
- How to Maximize Profit Holding SUI: Sui Staking vs Liquid Staking616
- Why does BCS require exact field order for deserialization when Move structs have named fields?65
- Multiple Source Verification Errors" in Sui Move Module Publications - Automated Error Resolution55
- Sui Move Error - Unable to process transaction No valid gas coins found for the transaction419
- Sui Transaction Failing: Objects Reserved for Another Transaction410