deadman-vault

2026-01-01

Deadman Vault Protocol — Security Audit

Deployed: SP1N3809W9CBWWX04KN3TCQHP8A9GN520BD4JMP8Z · 6 contracts · 385 lines · Audited: February 23, 2026

2
Critical
2
High
3
Medium
2
Low
2
Info

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

IDSeverityTitleStatus
DV-C01CRITICALrelease-handler Cannot Transfer Funds — STX Held in Wrong ContractOpen
DV-C02CRITICALInter-Contract Authorization Uses tx-sender — All Cross-Contract Calls FailOpen
DV-H01HIGHcancel-vault Allows Bypass of Time-LockOpen
DV-H02HIGHcancel-vault Marks Released on Transfer FailureOpen
DV-M01MEDIUMSingle Beneficiary Despite max-beneficiaries ConfigOpen
DV-M02MEDIUMCosigner Limit Hardcoded to 5 in is-cosignerOpen
DV-M03MEDIUMActivity Tracker Not Integrated with Vault OperationsOpen
DV-L01LOWNo Vault Deposit Top-UpOpen
DV-L02LOWNo Event Emission for Cosigner ActionsOpen
DV-I01INFONot Using Clarity 4Open
DV-I02INFOClean Modular Architecture (Positive)

Contracts Reviewed

ContractLinesPurpose
deadman-vault-core127Primary orchestrator — vault creation, release, cancellation
release-handler38STX transfer to beneficiary on release
delegation-registry81Beneficiary and co-signer management
condition-engine46Release condition evaluation
admin-config62Protocol-level parameters
activity-tracker31Liveness ping tracking