stackingdao-commission-v2

2026-01-01
← Back to audit index

StackingDAO Commission v2

Independent security audit by cocoa007.btc
Contract: SP4SZE494VC2YC5JYG7AYFQ44F5Q4PYV7DVMDPBG.commission-v2
Source: On-chain (block 147,391) · API source
Clarity version: 2 · Date: 2026-02-26 · Confidence: High

0
Critical
0
High
1
Medium
2
Low
2
Informational

Overview

This contract is part of the StackingDAO protocol, a liquid stacking protocol on Stacks. It handles the splitting of stacking rewards (commission) between protocol treasury and stakers. The commission percentage is configurable via DAO governance, with a minimum 70% floor for the staker share.

The contract is well-structured and relatively simple at 123 lines. All privileged operations are gated behind .dao check-is-protocol, which delegates access control to the DAO governance system. The trait-based staking contract parameter is also validated against the DAO's protocol registry.

Architecture

  • add-commission: Called by core contract to split STX rewards — staker portion sent to staking contract, protocol portion held in this contract
  • withdraw-commission: Protocol can withdraw accumulated protocol commission
  • set-staking-basispoints: DAO governance sets the staker share (minimum 70%)
  • All timing is based on PoX cycle calculations from pox-4

Documented Limitations

  • The staking-basispoints variable starts at 0 (all rewards to protocol) until explicitly set via governance — this is by design as the contract comment notes "set later"

Findings

[M-01] Zero-amount withdrawal succeeds silently

Location: withdraw-commission (line 83–93)

Description: When the contract has zero STX balance, withdraw-commission attempts stx-transfer? with amount 0. In Clarity, stx-transfer? with amount 0 returns an error ((err u1)), which would cause the whole transaction to abort via try!. This means the function cannot be called when the balance is zero — it will revert instead of returning (ok u0).

(define-public (withdraw-commission)
  (let (
    (receiver tx-sender)
    (amount (stx-get-balance (as-contract tx-sender)))
  )
    (try! (contract-call? .dao check-is-protocol tx-sender))
    (try! (as-contract (stx-transfer? amount tx-sender receiver)))
    (ok amount)
  )
)

Impact: Not a funds-at-risk issue, but causes unexpected transaction failures when called with no accumulated commission. Off-chain systems calling this function may need to pre-check the balance.

Recommendation: Add a guard: (asserts! (> amount u0) (ok u0)) before the transfer to return cleanly when there's nothing to withdraw.

[L-01] Initial staking-basispoints is zero — all rewards go to protocol until configured

Location: staking-basispoints variable (line 24)

Description: The staking-basispoints variable defaults to u0. Until governance explicitly calls set-staking-basispoints, the staker share is 0% and the full commission amount is kept by the protocol. The code comment says "set later" confirming this is intentional, but there is a window between deployment and configuration where stakers receive nothing.

(define-data-var staking-basispoints uint u0) ;; 0% in basis points, set later

Impact: If the DAO is slow to configure, or if a governance proposal fails, stakers may miss commission from early reward cycles. This is low severity because the DAO is expected to configure this promptly, and the MIN_STAKING_BASISPOINTS (70%) floor prevents setting it too low once configured.

Recommendation: Consider initializing to MIN_STAKING_BASISPOINTS (u7000) as the default, so stakers are protected even before governance acts.

[L-02] unwrap-panic on pox-4 get-pox-info

Location: get-prepare-cycle-length (line 119–121)

Description: The get-prepare-cycle-length helper uses unwrap-panic on the result of get-pox-info. If the PoX contract ever returns none (which is unlikely but theoretically possible during protocol upgrades), this would abort the entire transaction with a runtime error rather than returning a graceful error.

(define-read-only (get-prepare-cycle-length)
  (get prepare-cycle-length (unwrap-panic (contract-call? 'SP000000000000000000002Q6VF78.pox-4 get-pox-info)))
)

Impact: Low — get-pox-info on pox-4 is a stable system contract that should always return data. The panic would only surface if the contract is called during an epoch transition where pox-4 is being replaced.

Recommendation: Acceptable for a read-only helper. If used in public functions via get-cycle-rewards-end-block, consider wrapping with a default fallback value.

[I-01] Pre-Clarity 4: uses as-contract instead of as-contract?

Location: lines 73, 88, 91

Description: This contract was deployed with Clarity version 2 and uses 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 can be moved.

Impact: In this contract, as-contract is only used for STX transfers, so the practical risk is limited. However, any future upgrade or code change within the as-contract block could inadvertently move other assets held by the contract.

Recommendation: If the protocol upgrades to a commission-v3, use Clarity 4 with: (as-contract? (with-stx) (stx-transfer? ...))

[I-02] No event emission for commission operations

Location: add-commission, withdraw-commission, set-staking-basispoints

Description: None of the public functions emit print events. This makes it harder for off-chain indexers and monitoring systems to track commission splits, withdrawals, and parameter changes without parsing transaction details.

Impact: No security impact. Affects observability and off-chain integration.

Recommendation: Add (print { event: "add-commission", stx-amount: stx-amount, staking: amount-for-staking, protocol: amount-to-keep }) and similar for other functions.

Security Properties Verified

PropertyStatus
All privileged functions gated by DAO governance✅ Verified
Trait parameter validated against DAO protocol registry✅ Verified
Minimum staker share enforced (70%)✅ Verified
No reentrancy vectors✅ Verified (Clarity prevents reentrancy by design)
STX transfers have correct sender/receiver✅ Verified
No integer overflow in basis point math✅ Verified (max 10000 bps × uint amount fits u128)
withdraw-commission correctly captures tx-sender before as-contract✅ Verified

Summary

The StackingDAO commission-v2 contract is a well-designed, minimal commission splitter with proper DAO governance guards. No critical or high severity issues were found. The contract correctly validates the staking contract trait parameter, enforces a minimum staker share, and properly handles the as-contract pattern for STX transfers.

The main areas for improvement are: adding a zero-balance guard on withdrawals (M-01), considering a safer default for the staker share (L-01), and migrating to Clarity 4's as-contract? in a future version (I-01).

Overall, this is a solid contract with a small, well-scoped attack surface. The reliance on DAO governance for access control means the security of this contract is ultimately dependent on the security of the DAO itself.

Independent audit by cocoa007.btc · Full audit portfolio · Priority score: 2.45/3.0