Security Audit — February 25, 2026
Contract: SP4SZE494VC2YC5JYG7AYFQ44F5Q4PYV7DVMDPBG.ststx-token
Source: Stacks Explorer |
Hiro API
Deployed: Block 132,118 • Clarity 2 (pre-Clarity 4)
Confidence: HIGH — Simple contract, all paths reviewed
This is the stSTX fungible token contract — the liquid staking token issued by StackingDAO. When users deposit STX into the StackingDAO protocol, they receive stSTX representing their staked position. The token implements the SIP-010 standard and delegates all access control to StackingDAO's DAO contract (.dao).
At ~80 lines, this is a straightforward SIP-010 token with DAO-gated minting, burning, and metadata updates. The contract itself holds no funds — it is a token ledger only. The security surface is primarily about who can mint/burn and whether the authorization checks are consistent.
| ID | Severity | Title |
|---|---|---|
| M-01 | MEDIUM | Inconsistent auth: set-token-uri uses tx-sender while mint/burn use contract-caller |
| L-01 | LOW | burn-for-protocol can burn from arbitrary addresses |
| I-01 | INFO | No max supply defined on fungible token |
| I-02 | INFO | Pre-Clarity 4 deployment |
| I-03 | INFO | No event emission on mint/burn |
set-token-uri uses tx-sender while mint/burn use contract-callerSeverity: MEDIUM • Location: set-token-uri, mint-for-protocol, burn-for-protocol
Description: The three DAO-gated functions use different principals for authorization:
;; set-token-uri checks tx-sender
(define-public (set-token-uri (value (string-utf8 256)))
(begin
(try! (contract-call? .dao check-is-protocol tx-sender)) ;; <-- tx-sender
(ok (var-set token-uri value))
)
)
;; mint/burn check contract-caller
(define-public (mint-for-protocol (amount uint) (recipient principal))
(begin
(try! (contract-call? .dao check-is-protocol contract-caller)) ;; <-- contract-caller
(ft-mint? ststx amount recipient)
)
)
Impact: If a DAO-registered protocol contract calls set-token-uri, the check uses tx-sender (the original transaction signer), not contract-caller (the calling contract). This means:
set-token-uri will fail if the end user isn't themselves a registered protocol — the check validates the user, not the calling contract.set-token-uri even through unregistered intermediary contracts.This inconsistency doesn't create a direct exploit path (it's a metadata field, not funds), but it indicates a design inconsistency that could cause unexpected behavior if the DAO registry changes.
Recommendation: Use contract-caller consistently across all DAO-gated functions, or use tx-sender consistently. The mint/burn pattern (contract-caller) is generally safer for inter-contract calls.
burn-for-protocol can burn from arbitrary addressesSeverity: LOW • Location: burn-for-protocol
(define-public (burn-for-protocol (amount uint) (sender principal))
(begin
(try! (contract-call? .dao check-is-protocol contract-caller))
(ft-burn? ststx amount sender) ;; burns from any 'sender' address
)
)
Description: Any contract registered in the DAO as a "protocol" can burn stSTX from any address. There is no check that sender consented to the burn. This is by design for the staking withdrawal flow (burn stSTX to redeem STX), but it means the security of every stSTX holder's balance depends entirely on the trustworthiness of every DAO-registered protocol contract.
Impact: A malicious or buggy protocol contract added to the DAO registry could burn stSTX from any holder without their consent. The blast radius of a single compromised protocol contract extends to all stSTX balances.
Recommendation: This is a known trust assumption of the StackingDAO architecture. Consider documenting the invariant that protocol contracts must only burn from consenting parties. Post-conditions at the transaction level can mitigate this for individual users.
Severity: INFO • Location: define-fungible-token
(define-fungible-token ststx) ;; no max supply parameter
Description: The define-fungible-token call does not specify a max supply. This means any registered protocol contract can mint unlimited stSTX via mint-for-protocol. For a liquid staking token, this is expected (supply tracks staked STX), but there is no on-chain supply cap as a safety backstop.
Impact: Informational. The supply is effectively unbounded and controlled entirely by protocol logic.
Severity: INFO
Description: This contract is deployed on Clarity 2 (block 132,118, pre-Clarity 4). While the contract does not use as-contract itself, future upgrades or replacement contracts should target Clarity 4 to benefit from as-contract? with explicit asset allowances and other safety builtins.
Severity: INFO
Description: The transfer function emits a print event with action, sender, recipient, amount, and block-height. However, mint-for-protocol and burn-for-protocol do not emit any events. This makes off-chain tracking of supply changes harder.
Recommendation: Add print statements to mint and burn functions for indexer compatibility.
The contract delegates all access control to .dao check-is-protocol. This is a clean separation of concerns, but it means the security of stSTX depends entirely on:
This is the standard pattern for StackingDAO — all their contracts share this DAO-gated architecture. The token contract itself is a thin wrapper; the real attack surface is in the protocol contracts that call it.
See also: StackingDAO Core v2 | StackingDAO Core v3 — the core contracts that call mint-for-protocol and burn-for-protocol on this token.
(impl-trait .sip-010-trait-ft-standard.sip-010-trait)
;; Defines the Stacked STX according to the SIP010 Standard
(define-fungible-token ststx)
(define-constant ERR_NOT_AUTHORIZED u1401)
;;-------------------------------------
;; Variables
;;-------------------------------------
(define-data-var token-uri (string-utf8 256) u"")
;;-------------------------------------
;; SIP-010
;;-------------------------------------
(define-read-only (get-total-supply)
(ok (ft-get-supply ststx))
)
(define-read-only (get-name)
(ok "Stacked STX Token")
)
(define-read-only (get-symbol)
(ok "stSTX")
)
(define-read-only (get-decimals)
(ok u6)
)
(define-read-only (get-balance (account principal))
(ok (ft-get-balance ststx account))
)
(define-read-only (get-token-uri)
(ok (some (var-get token-uri)))
)
(define-public (transfer (amount uint) (sender principal) (recipient principal) (memo (optional (buff 34))))
(begin
(asserts! (is-eq tx-sender sender) (err ERR_NOT_AUTHORIZED))
(match (ft-transfer? ststx amount sender recipient)
response (begin
(print memo)
(print { action: "transfer", data: { sender: tx-sender, recipient: recipient, amount: amount, block-height: block-height } })
(ok response)
)
error (err error)
)
)
)
;;-------------------------------------
;; Admin
;;-------------------------------------
(define-public (set-token-uri (value (string-utf8 256)))
(begin
(try! (contract-call? .dao check-is-protocol tx-sender))
(ok (var-set token-uri value))
)
)
;;-------------------------------------
;; Mint / Burn
;;-------------------------------------
;; Mint method
(define-public (mint-for-protocol (amount uint) (recipient principal))
(begin
(try! (contract-call? .dao check-is-protocol contract-caller))
(ft-mint? ststx amount recipient)
)
)
;; Burn method
(define-public (burn-for-protocol (amount uint) (sender principal))
(begin
(try! (contract-call? .dao check-is-protocol contract-caller))
(ft-burn? ststx amount sender)
)
)
;; Burn external
(define-public (burn (amount uint))
(begin
(ft-burn? ststx amount tx-sender)
)
)