Sui.

Post

Share your knowledge.

Arnold.
Arnold3036
Jul 23, 2025
Expert Q&A

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

  1. Storage Design

    • What’s the optimal data structure for managing dynamic signer sets?
  2. Approval Logic

    • How to efficiently implement M-of-N signature validation in Move?
  3. Security & Maintenance

    • Best practices for secure key rotation and recovery?
  4. Attack Mitigation

    • How to prevent signature replay attacks in Sui’s object model?
  • Sui
  • Transaction Processing
  • Move
5
4
Share
Comments
.

Answers

4
Bekky.
Bekky1762
Jul 23 2025, 14:16

1. 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 u16 for thresholds (supports up to 65K signers)
  • Store signer addresses in VecMap for O(1) lookups

5
Best Answer
Comments
.
BigSneh.
Jul 29 2025, 23:32
  1. A multi-signature wallet allows multiple parties to approve a transaction before it can execute.

  2. In Sui, you'll implement this as a shared object to allow composability and shared access.

  3. For managing signer sets dynamically, use a struct containing a vector

    and a BTreeMap<address, bool> for fast lookup.

  4. The wallet object should store the threshold M, the total number of signers N, and the current proposal state.

  5. To enforce M-of-N logic, each signer must submit a signed approval that’s stored with the transaction proposal.

  6. Approvals can be stored as vector

    in the proposal object, and once M unique addresses are present, the action executes.

  7. Ensure replay protection by including a nonce or transaction counter in the wallet object.

  8. Only allow transactions with a higher nonce than the previous, rejecting duplicate signatures or outdated proposals.

  9. For gas efficiency, reject duplicate approvals early and use BTreeSet to avoid linear scans.

  10. To support key rotation, implement an update_signers method that replaces signer sets and increments a config version.

  11. Prevent unauthorized updates by gating signer changes behind an M-of-N consensus.

  12. Use the has key ability so the multisig wallet is indexable and retrievable by explorers or wallets.

  13. Wrap transactions in a Proposal object that includes action type, metadata, and current approval state.

  14. Expose view functions to retrieve the current proposal state and remaining approvals required.

  15. For safety, disallow re-using Proposal objects or hash their contents to ensure immutability.

  16. Always test signer removal/addition scenarios, especially edge cases like signer overlap or full rotation.

  17. Avoid replay vulnerabilities by rejecting transactions that attempt to re-use an already-executed nonce.

  18. You can hash proposals with their nonce and store the hash in a ledger to prevent duplicates.

  19. Integrate a fail-safe: a time-locked override or guardian key for recovery if signer access is lost.

  20. Finally, document expected signer behavior and edge cases, and validate with localnet and CLI tests before mainnet deployment.

3
Comments
.
SuiLover.
Jul 29 2025, 14:45

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 approvals; use table or bitmask patterns for efficiency.

🔹 2. Approval Logic: M-of-N Threshold

To validate a transaction:

Accept an action + vector signed by M unique signers.

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, signers: vector

, threshold: u8): bool { let valid_count = 0; for sig in &sigs { if is_valid(sig, signers) { valid_count = valid_count + 1; } }; valid_count >= threshold }

🔹 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.

-1
Comments
.

Do you know the answer?

Please log in and share it.