Bài viết
Chia sẻ kiến thức của bạn.
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
1Kurosakisui400
Aug 29 2025, 22:27Multisig 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
- Represent each signer by an
SignerId
resource stored in the wallet under a unique index; store aPolicy
resource with threshold and signer list. - 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), acceptApproval
objects that are linear resources created by each owner and submitted to the wallet; wallet consumes them to perform the action. - 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.
- 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 apolicy_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
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
- ... SUIMatthardy+2095
- ... SUIacher+1666
- ... SUIChubbycheeks +1091
- ... SUIjakodelarin+1060
- ... SUITucker+1047
- ... SUIKurosakisui+1034
- ... SUIOpiiii+861
Bài đăng tiền thưởng