ststxbtc-tracking-v2

2026-01-01
← Back to index

stSTXbtc Tracking v2 — Security Audit

sBTC reward distribution system for stSTXbtc holders — tracks wallet & external protocol positions

Contract: On-chain: SP4SZE494VC2YC5JYG7AYFQ44F5Q4PYV7DVMDPBG.ststxbtc-tracking-v2 · ~180 lines

Data contract: SP4SZE494VC2YC5JYG7AYFQ44F5Q4PYV7DVMDPBG.ststxbtc-tracking-data-v2

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

Source: Verified via Hiro API — on-chain source matches local copy

Date: February 27, 2026

Auditor: cocoa007.btc

Audit confidence: Medium. Single-contract logic is straightforward. Depends on external .dao, .ststxbtc-tracking-data-v2, .sbtc-token, and dynamic position-trait contracts. Core reward math and position lifecycle reviewed thoroughly; DAO governance trust assumptions noted.

Architecture Overview

This contract distributes sBTC rewards to holders of stSTXbtc tokens. It supports two types of positions:

  1. Wallet positions: Tracked via refresh-wallet — a DAO-approved protocol updates a holder's balance directly.
  2. External protocol positions: Tracked via refresh-position — calls a position-trait contract to get the holder's balance in an external protocol (e.g., AMM LP).

Rewards are added via add-rewards, which computes a per-token cumulative reward. When positions refresh, pending rewards are saved. Holders claim via claim-pending-rewards, which transfers sBTC from the contract.

The data contract (ststxbtc-tracking-data-v2) stores all state: total supply, cumulative reward, holder positions, supported positions registry. All its setters are gated by .dao check-is-protocol.

Documented Limitations

  • Protocol depends on DAO governance for contract registry and access control
  • Position trait contracts are trusted to report accurate balances
  • Reserve mechanism caps external position growth to prevent over-allocation

Priority Score

MetricScoreRationale
Financial risk (×3)3Holds and transfers sBTC; DeFi reward distribution
Deployment likelihood (×2)3Deployed on mainnet
Code complexity (×2)2~180 lines, multi-contract with traits
User exposure (×1.5)3StackingDAO — major Stacks protocol
Novelty (×1.5)2Reward distribution with position abstraction
Score: 2.65Well above 1.8 threshold

Summary

SeverityCount
HIGH1
MEDIUM3
LOW2
INFO3

Findings

H-01 Reward truncation in add-rewards silently loses small deposits

Location: add-rewards

Description: The reward-added-per-token calculation divides by total-supply and truncates to integer. While the contract multiplies by u10000000000 (10-decimal precision) before dividing, the truncation manifests when reward amounts are small relative to total supply. For example, if amount = u1 (1 sat) and total-supply = u20000000000 (200 BTC worth of stSTXbtc), then reward-added-per-token = (1 × 10^10) / 2×10^10 = 0 — the entire reward is lost. The sBTC transfers into the contract but zero rewards are distributed.

(reward-added-per-token (/ (* amount u10000000000) 
  (contract-call? .ststxbtc-tracking-data-v2 get-total-supply)))

Impact: Small reward deposits (relative to total supply) are silently absorbed — sBTC transfers in but zero rewards are distributed. The tokens are permanently locked in the contract. The truncation also accumulates over many add-rewards calls — each call loses up to total-supply / 10^10 sats.

Recommendation: Add (asserts! (> reward-added-per-token u0) (err ERR_REWARD_TOO_SMALL)) to prevent silent loss. Alternatively, accumulate a dust remainder across calls.

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

Location: claim-pending-rewards, withdraw-tokens

Description: The contract uses as-contract in two places to transfer sBTC. In pre-Clarity 4, this grants unrestricted access to ALL assets held by the contract principal — not just the intended sBTC transfer. If any contract called within the as-contract scope had a reentrancy vector, it could drain all contract-held assets.

;; claim-pending-rewards
(try! (as-contract (contract-call? .sbtc-token transfer pending-rewards tx-sender holder none)))

;; withdraw-tokens
(try! (as-contract (contract-call? .sbtc-token transfer amount tx-sender recipient none)))

Impact: If the referenced .sbtc-token contract is malicious or compromised, all contract assets could be drained in a single call.

Recommendation: Migrate to Clarity 4's as-contract? with with-ft .sbtc-token sbtc to restrict asset scope to only the intended token.

M-02 claim-pending-rewards is callable by anyone for any holder

Location: claim-pending-rewards

Description: The function takes a holder parameter and has no access control — anyone can call it for any holder. While rewards are always sent to the correct holder address (not the caller), this allows third parties to trigger claims without consent.

(define-public (claim-pending-rewards (holder principal) (position principal))
  (let (
    (pending-rewards (unwrap-panic (get-pending-rewards holder position)))
  )
    ;; No check that tx-sender == holder or is authorized
    (asserts! (var-get claims-enabled) (err ERR_CLAIMS_DISABLED))
    ...

Impact: Anyone can force a holder to receive sBTC rewards at any time. In jurisdictions with taxable-on-receipt rules, this could create unwanted tax events. Also enables griefing by claiming tiny amounts to reset saved-rewards state.

Recommendation: Add (asserts! (is-eq tx-sender holder) (err ERR_NOT_AUTHORIZED)), or gate permissionless claims behind a DAO-approved caller check. The batch function could remain DAO-only for administrative use.

M-03 Reserve check in refresh-position uses potentially stale data

Location: refresh-position

Description: The reserve check reads the reserve principal's position amount from storage. If the reserve principal's actual balance has decreased since last refresh, the stored value is stale-high, allowing more external positions than the reserve currently supports.

(supported-position-reserve (get amount (contract-call? .ststxbtc-tracking-data-v2 
  get-holder-position (get reserve supported-position) (get reserve supported-position))))

(asserts! (not (and 
  (> new-position-balance prev-position-balance) 
  (> supported-position-new-total supported-position-reserve))) 
  (err ERR_OVER_RESERVE))

Impact: External positions could exceed the actual reserve amount if the reserve holder's stored balance is stale. The severity depends on how frequently the reserve holder's position is refreshed.

Recommendation: Fetch the reserve's live balance via the position trait rather than relying on stored (potentially stale) values. Alternatively, ensure the reserve holder is always refreshed before external positions.

L-01 unwrap-panic in reward calculation paths

Location: save-pending-rewards, claim-pending-rewards

Description: Both functions use unwrap-panic on get-pending-rewards. Since get-pending-rewards always returns (ok ...), the panic is unreachable — but if the function signature ever changes, the entire transaction would abort without a meaningful error.

(pending-rewards (unwrap-panic (get-pending-rewards holder position)))

Impact: No current risk; defensive coding concern.

Recommendation: Replace with (try! (get-pending-rewards holder position)).

L-02 Position reactivation permanently blocked after deactivation

Location: set-supported-positions

Description: Activation requires (is-eq (get total supported-position) u0). Deactivation sets total to the current non-zero value. Once deactivated, a position can never be reactivated — its total is permanently non-zero.

;; Activation requires total == 0
(asserts! (is-eq (get total supported-position) u0) (err ERR_POSITION_USED))
;; Deactivation sets total to current value
(contract-call? .ststxbtc-tracking-data-v2 set-supported-positions 
  position-address active (get reserve supported-position) 
  (get total supported-position) ...)

Impact: Protocol integrations are one-way — temporary disable/re-enable requires a new contract address. Likely intentional to prevent reward accounting issues.

Recommendation: Document this behavior. The data contract's set-supported-positions could be called directly by DAO to reset total if reactivation is needed.

I-01 TODO comments reference placeholder .sbtc-token

Location: add-rewards, claim-pending-rewards, withdraw-tokens

Description: Three TODO comments state "update with mainnet sbtc token". Since the contract is deployed, references are immutable. The .sbtc-token resolves to SP4SZE494VC2YC5JYG7AYFQ44F5Q4PYV7DVMDPBG.sbtc-token — StackingDAO's own wrapper, not necessarily the canonical sBTC contract.

Impact: The TODOs are cosmetic post-deployment. However, their presence suggests the contract may need redeployment if the intended sBTC token differs from the deployed reference.

Recommendation: Verify the deployed .sbtc-token is the intended token. If it's a wrapper, document the wrapper's trust assumptions.

I-02 Position contracts (reserve addresses) always receive zero rewards

Location: get-pending-rewards

Description: If the holder is an active supported position contract, the function returns (ok u0) regardless of actual pending rewards. This prevents position contracts from accumulating rewards — correct by design since their underlying users earn individually.

(is-holder-position (get active (contract-call? .ststxbtc-tracking-data-v2 
  get-supported-positions holder)))
(if is-holder-position (ok u0) (ok (+ rewards rewards-saved)))

Impact: Informational — correct by design but could surprise integrators.

I-03 Rounding consistently favors the protocol

Location: add-rewards, get-pending-rewards

Description: Integer division truncation occurs in two places: (1) reward-added-per-token truncates when dividing by total supply, and (2) per-holder rewards truncate when dividing by u10000000000. Both round down. The 10-decimal precision mitigates this to sub-satoshi levels for most practical amounts.

Impact: Negligible dust — standard for integer-arithmetic reward systems.

Positive Observations

  • Clean separation of logic and data. Tracking contract handles logic; data contract stores all state. Both gated by DAO protocol checks.
  • 10-decimal precision for reward math. The u10000000000 multiplier provides good precision for per-token reward tracking.
  • Reserve mechanism for external positions. Prevents unbounded external protocol positions from diluting wallet holders' rewards.
  • Deactivation snapshots cumulative reward. deactivated-cumm-reward freezes reward accrual at deactivation — clean lifecycle.
  • Batch claim support. claim-pending-rewards-many enables efficient processing of up to 200 positions.
  • Emergency controls. claims-enabled flag and withdraw-tokens provide shutdown and recovery capabilities.
  • Consistent DAO access control. All admin functions and data writes go through .dao check-is-protocol.

Related Audits

Audit by cocoa007.btc · Full audit portfolio