multisig-wallet
Multi-Signature Wallet — Multisig Treasury
Repo: Dark-Brain07/stacks-builder-contracts · 1 contract · ~103 lines
Audited: February 21, 2026
Overview
A multi-signature wallet contract requiring multiple signers to approve STX transactions before execution. Features signer management (add/remove), transaction proposals with signature collection, threshold-based execution, and deposit tracking. The contract aims to provide shared custody of funds with configurable signers and a fixed 2-of-N approval threshold.
Findings Summary
| Severity | Count | Description |
|---|---|---|
| CRITICAL | 2 | No shared treasury, owner-only execution |
| HIGH | 3 | Hardcoded threshold, signer count corruption, stale signatures |
| MEDIUM | 2 | Deposits go to owner, no cancellation |
| LOW | 3 | No events, duplicate add-signer, no re-signing check |
Critical Findings
C-01 Execute Transfers From Owner's Personal Balance, Not Contract
The execute-tx function transfers STX from tx-sender (the owner) rather than from a shared contract-held pool. The contract never holds any funds — there is no shared treasury.
(define-public (execute-tx (tx-id uint))
(let ((tx (unwrap! (map-get? transactions tx-id) ERR-TX-NOT-FOUND)))
(asserts! (is-eq tx-sender CONTRACT-OWNER) ERR-NOT-AUTHORIZED)
;; Sends from CONTRACT-OWNER's personal balance:
(try! (stx-transfer? (get amount tx) tx-sender (get to tx)))
...))
The entire multisig approval process is theater — the owner must personally fund every transfer at execution time. The deposit function also sends funds to the owner, not the contract.
Fix: Hold funds in the contract principal. Deposits: (stx-transfer? amount tx-sender (as-contract tx-sender)). Execution: (as-contract (stx-transfer? amount tx-sender (get to tx))).
C-02 Only Owner Can Execute — Single Point of Failure
The execute-tx function restricts execution to CONTRACT-OWNER:
(asserts! (is-eq tx-sender CONTRACT-OWNER) ERR-NOT-AUTHORIZED)
This gives the owner unilateral veto power over all approved transactions. Even if every signer approves, the owner can refuse to execute indefinitely. This fundamentally breaks the multisig security model — it's a single-signer wallet with an approval queue.
Fix: Allow any signer to execute a fully-approved transaction: (asserts! (is-signer tx-sender) ERR-NOT-SIGNER).
High Findings
H-01 Hardcoded 2-of-N Signature Threshold
The required signatures is a constant:
(define-constant REQUIRED-SIGS u2)
As signers are added, the security threshold never scales. A 2-of-100 multisig provides almost no security — any two colluding signers can drain the wallet. The threshold cannot be updated without redeploying the entire contract.
Fix: Use a data-var for threshold with an owner-controlled setter that validates threshold <= signer-count and threshold >= 2.
H-02 Signer Count Corruption on Remove
remove-signer decrements signer-count without checking whether the principal is actually a signer:
(define-public (remove-signer (signer principal))
(begin
(asserts! (is-eq tx-sender CONTRACT-OWNER) ERR-NOT-AUTHORIZED)
(map-delete signers signer)
(var-set signer-count (- (var-get signer-count) u1)) ;; no existence check!
(ok true)))
Removing a non-existent signer causes uint underflow (wraps to u340282366920938463463374607431768211455). Also, no guard prevents removing all signers or going below the required threshold, which would brick the wallet.
Fix: Assert (is-signer signer) before removal. Assert (> signer-count REQUIRED-SIGS) after decrement.
H-03 Removed Signers' Approvals Persist on Pending Transactions
When a signer is removed via remove-signer, their existing signatures on pending transactions remain valid. The sign-tx function checks signer status at signing time, but signatures already recorded are never invalidated.
A removed (potentially compromised) signer's past approvals still count toward the threshold, allowing transactions to be executed with approval from parties who are no longer authorized.
Fix: Either invalidate pending transactions on signer removal, or check signer status at execution time by re-verifying all signers.
Medium Findings
M-01 Deposits Go to Owner's Personal Address
The deposit function sends STX directly to CONTRACT-OWNER:
(try! (stx-transfer? amount tx-sender CONTRACT-OWNER))
Combined with C-01, funds flow through the owner's personal account with zero on-chain enforcement. The deposits map tracks contributions but has no connection to actual fund custody. There is no trustless custody.
M-02 No Transaction Cancellation or Expiry
Once submitted, a transaction cannot be cancelled by the creator or expired after a timeout. Proposals persist forever in an unexecuted state, and individual signatures cannot be revoked. If a transaction becomes undesirable after submission, the only option is to never execute it — but it remains in the map permanently.
Low Findings
L-01 No Event Emissions
No print statements for any operations — transaction creation, signing, execution, deposits, or signer changes. Off-chain indexers and monitoring tools cannot track multisig activity without polling every block.
L-02 Duplicate add-signer Corrupts Count
add-signer doesn't check if the principal is already a signer. Re-adding an existing signer increments signer-count without actually adding anyone, inflating the count and breaking invariants.
Fix: Assert (is-none (map-get? signers signer)) or (not (is-signer signer)) before adding.
L-03 Creator Auto-Signs With No Opt-Out
submit-tx automatically counts the creator as the first signer (sigs starts at 1). With REQUIRED-SIGS u2, only one additional signature is needed. The creator cannot submit a proposal without also signing it, which may not always be desired in governance workflows.
Architecture Assessment
| Feature | Status |
|---|---|
| Contract-held treasury | ❌ Funds go to owner personally |
| Decentralized execution | ❌ Owner-only execution (veto power) |
| Dynamic threshold | ❌ Hardcoded to 2 |
| Signer management | ⚠️ Works but count easily corrupted |
| Transaction proposals | ✅ Correct with deduplication |
| Signature collection | ✅ Has-signed map prevents double-signing |
| Deposit tracking | ⚠️ Bookkeeping only, no custody |
| Cancellation / expiry | ❌ Missing |
| Events / logging | ❌ No print statements |
Positive Aspects
- Clean, well-structured code with descriptive error constants
- Signature deduplication via
has-signedmap prevents double-signing - Amount validation on submit (
> u0) - Proper use of
unwrap!for transaction lookups - Executed-state check prevents double-execution
Verdict: The contract has a fundamentally broken trust model — it appears to be a multisig but operates as a single-owner wallet. The owner personally holds all deposited funds and has sole execution authority, making the multi-signature approval process advisory at best. Two critical architectural flaws (fund custody and execution centralization) plus signer count corruption bugs make this unsuitable for any real value. A good learning exercise in Clarity maps and assertions, but needs major redesign for production use.