stacking-dao-core-v5

2026-01-01
← Back to index

StackingDAO Core v5 — Security Audit

Liquid stacking protocol — deposit STX, receive stSTX, withdraw via NFT receipt or idle pool (v5 upgrade)

Contract: On-chain: SP4SZE494VC2YC5JYG7AYFQ44F5Q4PYV7DVMDPBG.stacking-dao-core-v5 · 394 lines

Clarity version: Pre-Clarity 4 (uses as-contract, not as-contract?)

Prior version: StackingDAO Core v4 audit

Date: March 1, 2026

Auditor: cocoa007.btc

Audit confidence: Medium. Well-structured contract with clear separation of concerns. Multi-contract architecture (depends on .dao, .data-core-v1, .data-core-v2, .data-core-v3, .reserve, .ststx-token, .ststx-withdraw-nft-v2, and four trait-parameterized contracts). Core logic reviewed thoroughly; trust assumptions on external contracts noted.

V4 → V5 Changes

  • stSTX burned at init-withdraw, not at withdraw. In v4, init-withdraw transferred stSTX to the contract and burned it during withdraw. In v5, stSTX is burned immediately during init-withdraw via burn-for-protocol. This eliminates the contract holding stSTX tokens between init and withdraw — a meaningful improvement that reduces contract surface area.
  • Commission address validation. New commission-address variable with setter and getter. deposit, withdraw-idle, and withdraw now assert the passed commission contract matches the configured address. Prevents using an arbitrary (but protocol-registered) commission contract.
  • Data source upgrade. Exchange rate now read from .data-core-v3 (was .data-core-v2 in v4).
  • Print event fix. Deposit event field corrected from stxstx-amountststx-amount.

Summary

SeverityCount
MEDIUM2
LOW2
INFO3

Findings

MEDIUM M-01: No slippage protection on deposit and withdrawal

Location: deposit, withdraw-idle, withdraw

Description: None of the user-facing functions accept a minimum output parameter. The stSTX/STX exchange rate is read from .data-core-v3 at execution time. Between transaction submission and inclusion in a block, the rate can change due to stacking rewards, other deposits/withdrawals, or oracle updates. Users may receive fewer stSTX (on deposit) or less STX (on withdrawal) than expected.

;; deposit: no min-ststx-out parameter
(stx-ststx (try! (contract-call? .data-core-v3 get-stx-per-ststx reserve)))
(ststx-amount (/ (* stx-user-amount DENOMINATOR_6) stx-ststx))

Impact: Users are exposed to exchange rate changes between tx submission and execution. In a volatile period or during a large rebase event, users could receive significantly less than expected. While Stacks blocks are relatively slow (~10 min), the exchange rate changes at PoX cycle boundaries which could coincide with pending transactions.

Recommendation: Add a min-ststx-out parameter to deposit and min-stx-out parameters to withdraw-idle and withdraw. Assert the output meets the minimum before completing the operation.

MEDIUM M-02: as-contract blanket authority (pre-Clarity 4)

Location: Multiple — deposit (line ~140), withdraw-idle (lines ~187-195), init-withdraw (lines ~229-231), withdraw (lines ~273-280)

Description: The contract uses as-contract (Clarity 3) which grants blanket authority to move any asset the contract holds. Clarity 4 introduced as-contract? with explicit asset allowances (with-stx, with-ft, with-nft) that restrict which assets can be moved within the scope.

;; v5 uses blanket as-contract
(try! (as-contract (contract-call? reserve request-stx-for-withdrawal stx-user-amount receiver)))

;; Clarity 4 would restrict to only STX:
(try! (as-contract? (with-stx stx-user-amount)
  (contract-call? reserve request-stx-for-withdrawal stx-user-amount receiver)))

Impact: If any of the trait-parameterized contracts (reserve, commission-contract, staking-contract, direct-helpers) contains a vulnerability or is upgraded to a malicious implementation, the as-contract scope could be abused to move any asset held by this contract. The DAO protocol validation (check-is-protocol) mitigates this significantly, but a compromised DAO could register a malicious contract.

Recommendation: Upgrade to Clarity 4 and use as-contract? with explicit with-stx allowances. This provides language-level enforcement that only STX can be moved, regardless of what the called contracts attempt.

LOW L-01: unwrap-panic in user-facing functions can cause silent transaction failures

Location: deposit (line ~129: get-idle-cycle), withdraw-idle (line ~170), init-withdraw (lines ~212, ~219: get-withdraw-unlock-burn-height and get-last-token-id)

Description: Multiple unwrap-panic calls are used in user-facing public functions. If the unwrapped value is none or (err ...), the entire transaction aborts with a runtime error rather than returning a meaningful error code.

(idle-cycle (unwrap-panic (get-idle-cycle)))
(unlock-burn-height (unwrap-panic (get-withdraw-unlock-burn-height)))
(nft-id (unwrap-panic (contract-call? .ststx-withdraw-nft-v2 get-last-token-id)))

Impact: Users see opaque "runtime error" instead of a descriptive error code. Difficult to debug failed transactions. In practice, these functions should always return valid values, so the risk is low.

Recommendation: Replace unwrap-panic with unwrap! and specific error constants for better debuggability.

LOW L-02: Fee parameters can be set up to 100%

Location: set-stack-fee, set-unstack-fee, set-withdraw-idle-fee

Description: Fee setters only check (<= fee DENOMINATOR_BPS), allowing fees up to 10000 bps (100%). A compromised or malicious DAO governance action could set fees to confiscate all user deposits or withdrawals.

(define-public (set-stack-fee (fee uint))
  (begin
    (try! (contract-call? .dao check-is-protocol contract-caller))
    (asserts! (<= fee DENOMINATOR_BPS) (err ERR_WRONG_BPS))
    (var-set stack-fee fee)
    (ok true)
  )
)

Impact: If governance is compromised, fees could be set to extract all user value. Current DAO governance provides the primary protection. This is a defense-in-depth concern.

Recommendation: Add a reasonable maximum fee cap (e.g., 1000 bps / 10%) as a hard-coded constant. This limits damage even if governance is compromised.

INFO I-01: Rounding consistently favors the protocol

Location: deposit, withdraw-idle, withdraw

Description: All integer divisions round down (Clarity's default behavior). On deposit, ststx-amount rounds down (user gets slightly less stSTX). On withdrawal, stx-amount rounds down (user gets slightly less STX). Fee calculations also round down (protocol gets slightly less fee). This is standard practice and not a vulnerability, but worth noting for completeness.

Impact: Negligible dust amounts (< 1 microSTX per operation) lost by users. Consistent with industry standard.

INFO I-02: No minimum deposit or withdrawal amount

Location: deposit, withdraw-idle, init-withdraw

Description: There is no minimum amount enforced for deposits or withdrawals. Extremely small amounts (e.g., 1 microSTX) could be deposited, which would mint 0 stSTX due to rounding, effectively donating the deposit to the protocol.

Impact: User error only — no economic attack vector. The gas cost of a transaction far exceeds any dust amount lost.

INFO I-03: Commission address is a single point of trust

Location: set-commission-address, deposit, withdraw-idle, withdraw

Description: The v5 upgrade adds commission address validation, which is an improvement over v4. However, the commission-address can be changed by any protocol-authorized caller via set-commission-address. If governance sets this to a malicious commission contract, all fees would be redirected.

Impact: Governance-dependent risk. The commission contract must be protocol-registered AND match the configured address, providing two layers of validation. This is a documented trust assumption, not a vulnerability.

Positive Observations

  • Burn-at-init pattern (new in v5). Burning stSTX immediately during init-withdraw instead of holding it until withdraw is a significant improvement. Reduces the contract's token surface area and eliminates the risk of stSTX being stuck if the withdrawal flow fails.
  • Commission address pinning (new in v5). Requiring the passed commission contract to match the configured address prevents using an arbitrary protocol-registered contract as the commission target.
  • Consistent protocol validation. All four trait parameters are validated via check-is-protocol in every function.
  • Granular shutdown controls. Four independent shutdown flags allow disabling specific operations without affecting others.
  • Clean fee handling. Fee-on-top model with explicit zero-check avoids unnecessary transfers.

Architecture Notes

StackingDAO Core v5 is part of a multi-contract system. Key trust dependencies:

  • .dao — access control (check-is-enabled, check-is-protocol)
  • .data-core-v1 — withdrawal NFT data, cycle offsets
  • .data-core-v2 — idle STX tracking, withdraw inset
  • .data-core-v3 — exchange rate (stx-per-ststx) — new in v5
  • .ststx-token — SIP-010 liquid stacking token (mint/burn)
  • .ststx-withdraw-nft-v2 — withdrawal receipt NFTs
  • .commission-v2 — default commission handler
  • Four trait-parameterized contracts passed by the caller (all validated)

The exchange rate source upgrade from .data-core-v2 to .data-core-v3 is the most significant external dependency change.

Independent audit by cocoa007.btc · Full audit portfolio