Move.

Bài viết

Chia sẻ kiến thức của bạn.

Michael Ace.
Aug 29, 2025
Hỏi đáp Chuyên Gia

How to build a threshold multisig wallet

How to build a threshold multisig wallet in Move with on-chain policy-based invocation and safe key rotation?

  • Move
  • Smart Contract
  • Move Script
1
1
Chia sẻ
Bình luận
.

Câu trả lời

1
Kurosakisui.
Aug 29 2025, 22:27

Multisig wallets must allow N-of-M signatures to authorize actions, support on-chain policy changes (change threshold, add/remove signer), handle key rotation without losing access, and guarantee no double-spend or unauthorized execution.

Strategy

  1. Represent each signer by an SignerId resource stored in the wallet under a unique index; store a Policy resource with threshold and signer list.
  2. Use off-chain signatures (or on-chain approvals expressed as separate transactions) but verify them on-chain by collecting approvals as Approval objects. Alternatively (and simpler in Move), accept Approval objects that are linear resources created by each owner and submitted to the wallet; wallet consumes them to perform the action.
  3. Make policy-update a two-step operation: propose -> commit, where commit requires a threshold of approvals. This prevents a single admin from changing policy unilaterally.
  4. Support key rotation by allowing signer removal+addition in a single commit operation, but require old signers to sign the rotation (or use a trusted guardian).

Why this works

  • Approvals as linear resources mean an approval cannot be replayed or reused.
  • Two-step proposals and commit enforce explicit human consent.
  • Policy stored on-chain allows provable checks and easier auditing.

Working-style Move code

module 0xA::multisig {
    use std::signer;
    use std::vector;

    /// Each signer has an entry; address field points to owner account doing approval creation.
    struct SignerEntry has copy, drop, store { id: u8, owner: address }

    /// Policy: threshold + dynamic signer list
    struct Policy has key {
        threshold: u8,
        signers: vector<SignerEntry>,
        next_proposal_id: u64
    }

    /// A Proposal stores the action to be executed (call data is abstracted as bytes).
    struct Proposal has key {
        id: u64,
        proposer: address,
        action: vector<u8>,   // opaque action payload — in practice encode module + fn + args
        approvals: vector<address>, // addresses who approved
        executed: bool
    }

    public fun init(admin: &signer, initial_signers: vector<address>, threshold: u8) {
        assert!(vector::length(&initial_signers) as u8 >= threshold, 1);
        let signer_list = vector::empty<SignerEntry>();
        let mut i: u8 = 0;
        while (i < (vector::length(&initial_signers) as u8)) {
            vector::push_back(&mut signer_list, SignerEntry { id: i, owner: *vector::borrow(&initial_signers, (i as u64)) });
            i = i + 1;
        }
        move_to(admin, Policy { threshold, signers: signer_list, next_proposal_id: 0 });
    }

    // Anyone (usually signer) creates a proposal to run an action.
    public fun propose(caller: &signer, action: vector<u8>) acquires Policy {
        let p = borrow_global_mut<Policy>(signer::address_of(caller));
        let pid = p.next_proposal_id;
        p.next_proposal_id = pid + 1;
        let prop = Proposal { id: pid, proposer: signer::address_of(caller), action, approvals: vector::empty(), executed: false };
        // store proposal under proposer account for simplicity
        move_to(caller, prop);
    }

    // Approver calls approve by creating a signed Approval object (we represent as adding address)
    public fun approve(approver: &signer, proposer_addr: address, proposal_id: u64) acquires Proposal, Policy {
        // fetch policy to verify approver is an authorized signer
        let pol_owner = signer::address_of(approver); // adjust: policy stored at a canonical admin; simplified here
        let pol = borrow_global<Policy>(pol_owner);
        assert!(is_authorized(&pol.signers, signer::address_of(approver)), 2);

        // load proposal from proposer address (simplified)
        let prop = borrow_global_mut<Proposal>(proposer_addr);
        assert!(prop.id == proposal_id, 3);
        assert!(!prop.executed, 4);

        // Ensure not already approved by this approver
        assert!(!addr_in_vec(&prop.approvals, signer::address_of(approver)), 5);
        vector::push_back(&mut prop.approvals, signer::address_of(approver));
    }

    // Execute proposal if approvals >= threshold
    public fun execute(proposer: &signer, proposal_id: u64) acquires Proposal, Policy {
        let pol = borrow_global<Policy>(signer::address_of(proposer));
        let prop = borrow_global_mut<Proposal>(signer::address_of(proposer));
        assert!(prop.id == proposal_id && !prop.executed, 6);
        let num = vector::length(&prop.approvals) as u8;
        assert!(num >= pol.threshold, 7);

        // Here, interpret prop.action and call the intended module/function.
        // For demo we mark executed true and drop approvals
        prop.executed = true;
        // In production: decode action, call targeted module via pre-agreed interface, or have modules pull authorized actions.
    }

    fun is_authorized(signers: &vector<SignerEntry>, addr: address): bool {
        let i = 0;
        while (i < vector::length(signers)) {
            if ((*vector::borrow(signers, i)).owner == addr) { return true }
            i = i + 1;
        }
        false
    }

    fun addr_in_vec(vec: &vector<address>, a: address): bool {
        let i = 0;
        while (i < vector::length(vec)) {
            if (*vector::borrow(vec, i) == a) { return true }
            i = i + 1;
        }
        false
    }
}

Key properties & rotation

  • To rotate keys, propose an action whose action payload is a policy_update that includes add/remove signer ops. That proposal itself is subject to the same threshold, so rotation requires signers’ consent.
  • For emergency recovery: include a guardian role stored separately that can trigger a special recovery flow requiring a stronger threshold + time delay.
  • For auditability: emit events (off-chain) for proposals, approvals, and executions.

2
Bình luận
.

Bạn có biết câu trả lời không?

Hãy đăng nhập và chia sẻ nó.

Move is an executable bytecode language used to implement custom transactions and smart contracts.

242Bài viết541Câu trả lời
Sui.X.Peera.

Kiếm phần của bạn từ 1000 Sui

Tích lũy điểm danh tiếng và nhận phần thưởng khi giúp cộng đồng Sui phát triển.

Chiến dịch phần thưởngTháng Tám