deadman-vault
Deadman Vault Protocol — Security Audit
Deployed: SP1N3809W9CBWWX04KN3TCQHP8A9GN520BD4JMP8Z ·
6 contracts · 385 lines ·
Audited: February 23, 2026
Overview
The Deadman Vault Protocol is a dead man's switch system on Stacks. Users deposit STX into vaults with configurable release conditions: block-height timeout, inactivity detection, or multi-sig threshold approval. When conditions are met, a designated beneficiary receives the funds.
The protocol is split across 6 contracts: deadman-vault-core (orchestrator), release-handler (STX release), delegation-registry (beneficiary/cosigner management), condition-engine (trigger evaluation), admin-config (protocol parameters), and activity-tracker (inactivity monitoring).
Architecture
User → deadman-vault-core → admin-config (read config)
→ delegation-registry (set beneficiary, cosigners)
→ condition-engine → activity-tracker (check inactivity)
→ release-handler → delegation-registry (get beneficiary)
Findings
CRITICAL DV-C01: release-handler Cannot Transfer Funds — STX Held in Wrong Contract
Location: deadman-vault-core.create-vault / release-handler.execute-release
When a user creates a vault, STX is transferred to deadman-vault-core:
(try! (stx-transfer? amount tx-sender (as-contract tx-sender)))
But when release is triggered, release-handler.execute-release tries to send STX from its own balance:
(match (as-contract (stx-transfer? amount tx-sender beneficiary)) ...)
Inside as-contract, tx-sender becomes release-handler's address — but the STX is locked in deadman-vault-core, not release-handler. All releases will fail with insufficient balance. Funds are permanently locked in the core contract.
Impact: Total loss of deposited funds. No mechanism exists to transfer STX from core to release-handler.
Fix: Either (a) have deadman-vault-core do the STX transfer directly via as-contract, or (b) deposit into release-handler instead of core.
CRITICAL DV-C02: Inter-Contract Authorization Uses tx-sender — All Cross-Contract Calls Fail
Location: release-handler.is-authorized, delegation-registry.is-authorized
Both contracts check authorization via:
(define-private (is-authorized)
(or (is-eq tx-sender (var-get authorized-caller))
(is-eq tx-sender CONTRACT-OWNER)))
In Clarity, tx-sender is always the original transaction sender (the user), not the calling contract. When deadman-vault-core calls delegation-registry.set-beneficiary, tx-sender is the user — not deadman-vault-core.
Since authorized-caller defaults to CONTRACT-OWNER (the deployer), only the deployer can successfully execute any vault operation. Regular users will hit ERR-NOT-AUTHORIZED on vault creation (when it tries to set beneficiary).
Impact: Protocol is completely unusable for non-deployer users.
Fix: Use contract-caller instead of tx-sender for inter-contract authorization:
(define-private (is-authorized)
(or (is-eq contract-caller (var-get authorized-caller))
(is-eq contract-caller CONTRACT-OWNER)))
And call set-authorized-caller with deadman-vault-core's address after deployment.
HIGH DV-H01: cancel-vault Allows Bypass of Time-Lock
Location: deadman-vault-core.cancel-vault
The vault owner can cancel and reclaim funds at any time before release — including immediately after creation. This defeats the purpose of a dead man's switch: the owner can always cancel, making the beneficiary designation meaningless until the owner is truly incapacitated.
A dead man's switch should lock funds irrevocably once created, with only the condition-triggered release as the exit path. Otherwise the owner can simply cancel when the condition approaches.
Impact: Undermines the core value proposition. A beneficiary cannot rely on receiving funds.
Fix: Add a grace period after which cancellation is no longer possible, or remove cancel entirely and rely on the inactivity condition reset (ping) as the "I'm alive" mechanism.
HIGH DV-H02: No Re-entrancy Protection on cancel-vault
Location: deadman-vault-core.cancel-vault
(map-set vaults vault-id (merge vault { released: true }))
(match (as-contract (stx-transfer? ...))
success (begin
(print { event: "vault-cancelled", vault-id: vault-id, owner: tx-sender })
(ok true))
error (err u610))
The released flag is set before the STX transfer, which is good (checks-effects-interactions). However, if the transfer fails, the vault is permanently marked as released even though funds were not returned. The owner loses access to their STX.
Impact: On transfer failure, funds are permanently locked — vault marked released but STX not returned.
Fix: Only set released: true after confirming the transfer succeeded, or revert the flag on failure.
MEDIUM DV-M01: Single Beneficiary Despite max-beneficiaries Config
Location: admin-config / delegation-registry
admin-config defines max-beneficiaries (default 5), but delegation-registry only stores one beneficiary per vault via (define-map vault-beneficiary uint principal). The config parameter is unused and misleading.
Impact: Misleading configuration. Users may expect multi-beneficiary support that doesn't exist.
Fix: Either implement multi-beneficiary with split amounts, or remove the max-beneficiaries config parameter.
MEDIUM DV-M02: Cosigner Limit Hardcoded to 5 in is-cosigner Check
Location: delegation-registry.is-cosigner
(define-read-only (is-cosigner (vault-id uint) (who principal))
(or
(is-eq (some who) (map-get? vault-cosigner { vault-id: vault-id, index: u0 }))
(is-eq (some who) (map-get? vault-cosigner { vault-id: vault-id, index: u1 }))
(is-eq (some who) (map-get? vault-cosigner { vault-id: vault-id, index: u2 }))
(is-eq (some who) (map-get? vault-cosigner { vault-id: vault-id, index: u3 }))
(is-eq (some who) (map-get? vault-cosigner { vault-id: vault-id, index: u4 }))))
The admin can set max-cosigners up to 10, but is-cosigner only checks indices 0-4. Cosigners at indices 5-9 can be added but can never submit approvals (they fail the ERR-NOT-COSIGNER check).
Impact: Cosigners at index 5+ are silently ignored. Threshold conditions may become impossible to meet.
Fix: Extend the check to all 10 indices, or enforce that max-cosigners cannot exceed 5.
MEDIUM DV-M03: Activity Tracker Has No Integration with Vault Operations
Location: activity-tracker / deadman-vault-core
The inactivity condition relies on activity-tracker.ping(), but nothing in the protocol automatically pings when the owner interacts with their vault (create, cancel, add-cosigner). The owner must separately call ping() to prove liveness.
Worse: anyone can call ping on behalf of themselves, but there's no mechanism for the vault owner to be auto-pinged. If the owner forgets to ping but is otherwise active on-chain, they could lose their funds.
Impact: Poor UX; inactivity condition doesn't reflect actual on-chain activity.
Fix: Auto-ping in create-vault, cancel-vault, and add-cosigner.
LOW DV-L01: No Vault Deposit Top-Up
Location: deadman-vault-core
Once created, a vault's amount is fixed. There's no way to add more STX to an existing vault. Users must create a new vault for additional deposits, fragmenting their dead man's switch setup.
Impact: Inconvenience; vault fragmentation.
Fix: Add a deposit function that increases an existing vault's amount.
LOW DV-L02: No Event Emission for Cosigner Actions
Location: delegation-registry
Adding cosigners and submitting approvals don't emit print events. This makes off-chain monitoring and indexing difficult.
Impact: Reduced observability for vault participants.
Fix: Add print events for add-cosigner and submit-approval.
INFO DV-I01: Not Using Clarity 4
The contracts don't specify a Clarity version, defaulting to Clarity 3 or below. Clarity 4 introduced as-contract? with explicit asset allowances, which would help prevent the fund misdirection issue in DV-C01.
Recommendation: Upgrade to Clarity 4 and use as-contract? with with-stx allowances for explicit STX transfer authorization.
INFO DV-I02: Clean Modular Architecture
Despite the critical bugs, the architectural separation into 6 focused contracts is well-designed. Each contract has a single responsibility: config, tracking, conditions, delegation, release, and orchestration. The error code namespacing (u100s, u200s, etc.) is clean and avoids collisions.
This is a solid foundation that would work correctly once the authorization model and fund flow are fixed.
Summary
| ID | Severity | Title | Status |
|---|---|---|---|
| DV-C01 | CRITICAL | release-handler Cannot Transfer Funds — STX Held in Wrong Contract | Open |
| DV-C02 | CRITICAL | Inter-Contract Authorization Uses tx-sender — All Cross-Contract Calls Fail | Open |
| DV-H01 | HIGH | cancel-vault Allows Bypass of Time-Lock | Open |
| DV-H02 | HIGH | cancel-vault Marks Released on Transfer Failure | Open |
| DV-M01 | MEDIUM | Single Beneficiary Despite max-beneficiaries Config | Open |
| DV-M02 | MEDIUM | Cosigner Limit Hardcoded to 5 in is-cosigner | Open |
| DV-M03 | MEDIUM | Activity Tracker Not Integrated with Vault Operations | Open |
| DV-L01 | LOW | No Vault Deposit Top-Up | Open |
| DV-L02 | LOW | No Event Emission for Cosigner Actions | Open |
| DV-I01 | INFO | Not Using Clarity 4 | Open |
| DV-I02 | INFO | Clean Modular Architecture (Positive) | — |
Contracts Reviewed
| Contract | Lines | Purpose |
|---|---|---|
deadman-vault-core | 127 | Primary orchestrator — vault creation, release, cancellation |
release-handler | 38 | STX transfer to beneficiary on release |
delegation-registry | 81 | Beneficiary and co-signer management |
condition-engine | 46 | Release condition evaluation |
admin-config | 62 | Protocol-level parameters |
activity-tracker | 31 | Liveness ping tracking |