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

SeverityCountDescription
CRITICAL2No shared treasury, owner-only execution
HIGH3Hardcoded threshold, signer count corruption, stale signatures
MEDIUM2Deposits go to owner, no cancellation
LOW3No 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

FeatureStatus
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

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.