StackingDAO Core v1 — Security Audit

On-chain: SP4SZE494VC2YC5JYG7AYFQ44F5Q4PYV7DVMDPBG.stacking-dao-core-v1 · Clarity 2 · Audited: February 24, 2026

Also reviewed: SP4SZE494VC2YC5JYG7AYFQ44F5Q4PYV7DVMDPBG.reserve-v1 (119 lines) · SP4SZE494VC2YC5JYG7AYFQ44F5Q4PYV7DVMDPBG.ststx-withdraw-nft (183 lines)

0
Critical
1
High
2
Medium
2
Low
3
Info

Overview

StackingDAO is a liquid staking protocol on Stacks. Users deposit STX and receive stSTX tokens representing their staked position. The core contract manages deposits, withdrawal initiation (via NFT receipts), actual withdrawals after cycle completion, and reward distribution with configurable commission.

The architecture follows a DAO pattern: all privileged operations are gated by .dao check-is-protocol, meaning security depends entirely on the integrity of the DAO's protocol registry. The reserve contract holds idle STX and tracks stacking balances. A withdrawal NFT contract provides transferable receipts for pending withdrawals.

Audit confidence: Medium. Core contract logic is straightforward and well-structured. The main risk is the hardcoded pox-3 dependency which makes this version obsolete — StackingDAO has likely migrated to a v2 core referencing pox-4. Multi-contract interactions were reviewed across core, reserve, and NFT contracts.

Documented Limitations

Priority Score

MetricScoreReasoning
Financial risk (×3)3DeFi liquid staking — holds and transfers significant STX value
Deployment (×2)3Deployed on Stacks mainnet
Complexity (×2)2~300 lines core + 119 reserve + 183 NFT = ~600 lines total across 3 contracts
User exposure (×1.5)3Well-known protocol, significant TVL
Novelty (×1.5)2First liquid staking protocol audited in this series
Final score: (9+6+4+4.5+3) / 10 = 2.65 ✅ Above 1.8 threshold

Clarity 2 penalty: 2.65 − 0.5 = 2.15

Findings

HIGH H-01: Hardcoded pox-3 Reference Makes Contract Non-Functional on Current Mainnet

Location: stacking-dao-core-v1get-pox-cycle, get-next-withdraw-cycle, withdraw

Description: The contract hardcodes calls to SP000000000000000000002Q6VF78.pox-3 for all PoX cycle calculations. Since the Stacks network has upgraded to pox-4, these calls may return stale or incorrect cycle data, causing deposits to calculate wrong stSTX amounts and withdrawals to use incorrect timing.

(define-read-only (get-pox-cycle)
  (contract-call? 'SP000000000000000000002Q6VF78.pox-3 current-pox-reward-cycle)
)

(define-read-only (get-next-withdraw-cycle)
  (let (
    (current-cycle (get-pox-cycle))
    (prepare-length (get prepare-cycle-length
      (unwrap-panic (contract-call? 'SP000000000000000000002Q6VF78.pox-3 get-pox-info))))
    (start-block-next-cycle
      (contract-call? 'SP000000000000000000002Q6VF78.pox-3
        reward-cycle-to-burn-height (+ current-cycle u1)))
  ) ...

Impact: All cycle-dependent logic (deposits, withdrawal initiation, withdrawal timing, reward accounting) relies on pox-3 data. If pox-3 returns outdated values after the pox-4 upgrade, users could initiate withdrawals for the wrong cycle or be unable to complete withdrawals at expected times. The contract is effectively deprecated.

Recommendation: Migrate to a new core contract version referencing pox-4 (or the current active PoX contract). StackingDAO likely already has a v2 core contract addressing this. Users should be migrated away from this version.

MEDIUM M-01: No Slippage Protection on Deposit

Location: stacking-dao-core-v1deposit

Description: The deposit function calculates the stSTX amount based on the current exchange rate but provides no min-ststx-amount parameter for slippage protection. The exchange rate can change between transaction submission and execution.

(define-public (deposit (reserve-contract <reserve-trait>)
                        (stx-amount uint)
                        (referrer (optional principal)))
  (let (
    (stx-ststx (try! (get-stx-per-ststx reserve-contract)))
    (ststx-to-receive (/ (* stx-amount u1000000) stx-ststx))
  )
    ;; No minimum output check
    ...

Impact: If the exchange rate shifts unfavorably between tx submission and mining (e.g., via a large reward addition changing the rate), users receive fewer stSTX than expected. While add-rewards is protocol-restricted (limiting sandwich attack vectors), natural rate changes during high-activity periods could still cause unexpected outcomes.

Recommendation: Add an optional min-ststx parameter: (asserts! (>= ststx-to-receive min-ststx) (err ERR_SLIPPAGE))

MEDIUM M-02: add-rewards Accepts Arbitrary Cycle IDs Without Bounds Checking

Location: stacking-dao-core-v1add-rewards

Description: The add-rewards function accepts any cycle-id without validating that it corresponds to an actual past cycle that generated rewards. A protocol operator could accidentally (or maliciously) attribute rewards to the wrong cycle, corrupting the accounting.

(define-public (add-rewards
  (commission-contract <commission-trait>)
  (staking-contract <staking-trait>)
  (reserve principal) (stx-amount uint) (cycle-id uint))
  (let (
    (current-cycle-info (get-cycle-info cycle-id))
    ;; No validation: cycle-id <= current-pox-cycle
    ...

Impact: Rewards attributed to future cycles would inflate the reported rewards for cycles that haven't happened. Rewards attributed to the wrong past cycle corrupt the historical record. The caller must be a registered protocol, limiting the attack surface, but operational errors are plausible.

Recommendation: Add validation: (asserts! (<= cycle-id (get-pox-cycle)) (err ERR_INVALID_CYCLE))

LOW L-01: Pre-Clarity 4 as-contract Without Asset Restrictions

Location: stacking-dao-core-v1init-withdraw, withdraw; reserve-v1request-stx-for-withdrawal, request-stx-to-stack, get-stx

Description: Both contracts use the legacy as-contract form which grants blanket asset access to the enclosed expression. Clarity 4 introduced as-contract? with explicit asset allowances (with-stx, with-ft, etc.) that restrict which assets the contract can move.

Impact: Any code executing under as-contract has unrestricted ability to transfer any asset the contract holds. In a complex multi-contract system, this increases the blast radius if any protocol-registered contract is compromised. With as-contract?, only explicitly allowed asset types could be moved.

Recommendation: When migrating to a Clarity 4 version, replace all as-contract calls with (as-contract? (with-stx) ...) for STX-only operations, or (as-contract? (with-ft .ststx-token) ...) where stSTX transfers are needed.

LOW L-02: Potential Arithmetic Underflow in Reserve Tracking Variables

Location: reserve-v1request-stx-for-withdrawal, return-stx-from-stacking

Description: The reserve uses unsigned integer subtraction to track stx-for-withdrawals and stx-stacking. If these values become desynchronized from actual balances (e.g., due to an operational error by a protocol contract), the subtraction underflows and the transaction aborts.

;; If requested-stx > stx-for-withdrawals, this underflows and aborts
(var-set stx-for-withdrawals (- (var-get stx-for-withdrawals) requested-stx))

;; Same risk here
(var-set stx-stacking (- (unwrap-panic (get-stx-stacking)) stx-amount))

Impact: An accounting desync would make withdrawals or stacking return operations permanently fail until the tracking variables are corrected. Since these functions are protocol-restricted, the risk is limited to operational errors rather than external attacks.

Recommendation: Add defensive checks: (asserts! (>= (var-get stx-for-withdrawals) requested-stx) (err ERR_INSUFFICIENT)) to fail gracefully instead of aborting.

INFO I-01: Commission Rounding Loss for Small Reward Amounts

Location: stacking-dao-core-v1add-rewards

Description: Commission is calculated as (/ (* stx-amount commission) u10000). For small stx-amount values (under ~20 STX at 5% commission), integer division truncates the commission to 0, meaning no commission is collected. The full amount goes to the reserve as rewards.

Impact: Negligible — rewards are typically large aggregate amounts. The protocol benefits (more goes to stakers), but the commission contract/stakers miss out on small fractions.

INFO I-02: Entire Security Model Depends on DAO Protocol Registry

Location: All contracts — .dao check-is-protocol

Description: Every privileged operation in the system is gated by a single check: whether the caller is registered in the DAO's protocol list. The reserve's get-stx function allows any registered protocol to extract arbitrary amounts of STX to any address. This means compromise of the DAO contract (or a malicious governance proposal) could drain all funds.

Impact: This is a deliberate architectural choice for upgradability — the DAO can register new protocol contracts without redeploying core infrastructure. However, it concentrates all trust in the DAO's governance mechanism. Users should understand that their deposited STX is ultimately secured by DAO governance, not by the core contract's logic alone.

INFO I-03: Zero-Indexed NFT IDs in Withdrawal NFT

Location: ststx-withdraw-nftmint-for-protocol

Description: The withdrawal NFT starts minting at ID 0 (the initial value of last-id). While get-last-token-id per SIP-009 typically returns the highest minted ID, here it returns the next ID to be minted (since it's incremented after minting). This is a minor deviation from convention but doesn't cause functional issues — the core contract correctly uses get-last-token-id before minting to predict the NFT ID.

(define-data-var last-id uint u0)

(define-public (mint-for-protocol (recipient principal))
  (let ((next-id (+ u1 (var-get last-id))))
    ;; Mints at current last-id (starts at 0), then increments
    (try! (nft-mint? ststx-withdraw (var-get last-id) recipient))
    (var-set last-id next-id)
    (ok true)))

Impact: Third-party tools or marketplaces that assume 1-indexed SIP-009 NFTs may miscount or skip NFT ID 0. No functional impact on the core withdrawal flow.

Architecture Notes