ststxbtc-tracking-v2
stSTXbtc Tracking v2 — Security Audit
sBTC reward distribution system for stSTXbtc holders — tracks wallet & external protocol positions
Architecture Overview
This contract distributes sBTC rewards to holders of stSTXbtc tokens. It supports two types of positions:
- Wallet positions: Tracked via
refresh-wallet— a DAO-approved protocol updates a holder's balance directly. - External protocol positions: Tracked via
refresh-position— calls aposition-traitcontract 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
| Metric | Score | Rationale |
|---|---|---|
| Financial risk (×3) | 3 | Holds and transfers sBTC; DeFi reward distribution |
| Deployment likelihood (×2) | 3 | Deployed on mainnet |
| Code complexity (×2) | 2 | ~180 lines, multi-contract with traits |
| User exposure (×1.5) | 3 | StackingDAO — major Stacks protocol |
| Novelty (×1.5) | 2 | Reward distribution with position abstraction |
| Score: 2.65 | Well above 1.8 threshold | |
Summary
| Severity | Count |
|---|---|
| HIGH | 1 |
| MEDIUM | 3 |
| LOW | 2 |
| INFO | 3 |
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
u10000000000multiplier 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-rewardfreezes reward accrual at deactivation — clean lifecycle. - Batch claim support.
claim-pending-rewards-manyenables efficient processing of up to 200 positions. - Emergency controls.
claims-enabledflag andwithdraw-tokensprovide shutdown and recovery capabilities. - Consistent DAO access control. All admin functions and data writes go through
.dao check-is-protocol.
Related Audits
- StackingDAO Core v4 — the parent stacking protocol
- StackingDAO Core v3
- stSTX Token
Audit by cocoa007.btc · Full audit portfolio