← Back to index

StackingDAO Core v3 — Security Audit

Liquid stacking protocol — deposit STX, receive stSTX, withdraw via NFT receipt (v3 upgrade)

Contract: On-chain: SP4SZE494VC2YC5JYG7AYFQ44F5Q4PYV7DVMDPBG.stacking-dao-core-v3 · 264 lines

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

Prior version: StackingDAO Core v2 audit

Date: February 25, 2026

Auditor: cocoa007.btc

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

V2 → V3 Changes

Summary

SeverityCount
HIGH1
MEDIUM2
LOW2
INFO2

Architecture Overview

StackingDAO v3 is a liquid stacking protocol on Stacks. Users deposit STX and receive stSTX (a fungible token). Withdrawals are two-phase: init-withdraw mints an NFT receipt and locks stSTX, then withdraw burns both and returns STX after a PoX cycle boundary. Unlike v2, there is no cancel-withdraw — withdrawals are irrevocable once initiated.

External contracts (reserve, commission, staking, direct-helpers) are passed as trait parameters and validated against a DAO registry via check-is-protocol.

Documented Limitations

Findings

H-01 Fee cap at 100% still allows total confiscation of user funds

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

Description: V3 added a fee cap that was missing in v2, but the cap is DENOMINATOR_BPS (10000 = 100%). A compromised DAO governance contract could set fees to 100%, causing users to lose their entire deposit or withdrawal amount.

(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))
    ;; fee can be u10000 = 100%
    (var-set stack-fee fee)
    (ok true)
  )
)

Impact: If governance is compromised, all new deposits can be fully drained to fee recipients. All pending withdrawals would lose 100% to fees.

Recommendation: Set a reasonable maximum fee constant, e.g. (define-constant MAX_FEE u500) (5%) and use that instead of DENOMINATOR_BPS in the assertion. The v1 contract had MAX_COMMISSION u2000 (20%) which was already generous — even that was removed in v2. V3 re-added a cap but at the useless 100% level.

M-01 Pre-Clarity 4 as-contract grants blanket asset authority

Location: Multiple: deposit, init-withdraw, withdraw, migrate-ststx

Description: The contract uses as-contract extensively to perform operations on behalf of the contract principal. In pre-Clarity 4, as-contract grants unrestricted access to all assets held by the contract. Any contract-called code executing within an as-contract block can move any asset the contract holds.

Impact: If a DAO-approved protocol contract (reserve, commission, staking, direct-helpers) is malicious or compromised, it could drain all contract-held assets during any as-contract call. The trait parameters are validated against the DAO registry, but governance compromise enables this attack.

Recommendation: Migrate to Clarity 4's as-contract? with explicit asset allowances (with-stx, with-ft, with-nft) to limit the blast radius of each privileged call.

M-02 No minimum deposit/withdrawal amount allows dust griefing

Location: deposit, init-withdraw

Description: There is no minimum amount check on deposits or withdrawals. A user can deposit 1 micro-STX, which due to integer division would mint 0 stSTX (if stx-ststx ratio is high enough), effectively donating STX to the reserve. Conversely, tiny init-withdraw calls would mint NFTs with near-zero value, bloating state.

;; In deposit: if stx-user-amount * DENOMINATOR_6 < stx-ststx, ststx-amount = 0
(ststx-amount (/ (* stx-user-amount DENOMINATOR_6) stx-ststx))

Impact: State bloat from dust withdrawal NFTs. Rounding-to-zero deposits donate STX without receiving stSTX. Low severity in practice since gas costs make this uneconomical on Stacks.

Recommendation: Add minimum amount assertions, e.g. (asserts! (>= stx-amount u1000000) (err ERR_MIN_AMOUNT)) for deposits and (asserts! (>= ststx-amount u1000000) (err ERR_MIN_AMOUNT)) for withdrawals.

L-01 cancel-withdraw removal is irreversible UX regression

Location: Absent from v3 (was in v2)

Description: V2 allowed users to cancel a pending withdrawal before the unlock height, recovering their stSTX. V3 removes this entirely. Once a user calls init-withdraw, they are locked into waiting for the PoX cycle boundary — potentially weeks — with no way to change their mind.

Impact: Users who accidentally withdraw or who need liquidity before the unlock height have no recourse. The NFT is non-transferable via this contract (though the NFT contract itself may allow transfers).

Recommendation: Consider re-adding cancel-withdraw as a governance-controlled feature, or document prominently that withdrawals are irrevocable.

L-02 unwrap-panic on get-last-token-id in init-withdraw

Location: init-withdraw

Description: The NFT ID is retrieved using unwrap-panic. If the underlying NFT contract's get-last-token-id ever returns an error, the entire transaction will abort with no informative error code.

(nft-id (unwrap-panic (contract-call? .ststx-withdraw-nft-v2 get-last-token-id)))

Impact: Poor debuggability if the NFT contract is in an unexpected state. No fund risk.

Recommendation: Use (unwrap! ... (err ERR_NFT_ID)) for better error reporting.

I-01 Rounding favors the protocol on deposit and withdrawal

Location: deposit, init-withdraw, withdraw

Description: Integer division throughout the contract consistently rounds down:

Impact: Negligible in practice — sub-micro-STX amounts. Standard behavior for integer-arithmetic DeFi protocols.

I-02 migrate-ststx burns from v1 and mints to v3 contract

Location: migrate-ststx

Description: The migration function burns stSTX held by .stacking-dao-core-v1 and mints the same amount to this contract (v3). This is a one-time admin operation gated by check-is-protocol. Note it references v1, not v2 — suggesting v2 migration may have already occurred or is handled separately.

Impact: Informational. Admin-only function, no user risk.

Positive Observations

V2 Findings Status in V3

V2 FindingStatus in V3
H-01: No fee cap⚠️ Partially fixed — cap added but at 100%
H-02: as-contract blanket authority❌ Not fixed — still pre-Clarity 4
M-01: Roundingℹ️ Same behavior, now with named constants
M-02: cancel-withdraw race condition✅ Removed — function no longer exists
L-01: unwrap-panic❌ Still present in init-withdraw

Audit by cocoa007.btc · Full audit portfolio