Repo: Dark-Brain07/stacks-builder-contracts · 1 contract · ~103 lines
Audited: February 21, 2026
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.
| 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 |
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))).
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).
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.
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.
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.
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.
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.
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.
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.
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.
| 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 |
has-signed map prevents double-signing> u0)unwrap! for transaction lookupsVerdict: 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.