Security audit of jcnelson/stx-future
Commit: 22b4155 (2021-09-03) · Audited: February 21, 2026 · Clarity version: 1 (pre-Clarity 4)
A Clarity smart contract for creating futures tranches from Stacked STX. Users deposit STX and receive fungible stx-future tokens 1:1. An authorized stacker locks the pooled STX in PoX (Proof of Transfer) for a configured reward cycle. After the lock period ends, token holders redeem their futures for the underlying STX. Written by Jude Nelson, a Stacks core developer.
The contract implements a simple but elegant mechanism: STX-in → futures tokens → Stack → unlock → redeem STX. It includes a SIP-010-like token interface, authorized stacker list, and self-consistency checks at deployment.
The README explicitly acknowledges several trust assumptions:
These are not reported as findings below since they are acknowledged design decisions. The findings focus on implementation bugs and improvements beyond the documented scope.
| Metric | Score | Weight | Weighted |
|---|---|---|---|
| Financial Risk | 3 — DeFi/staking, holds STX in PoX | 3 | 9 |
| Deployment Likelihood | 2 — has tests, README, by core dev | 2 | 4 |
| Code Complexity | 2 — 213 lines, PoX integration | 2 | 4 |
| User Exposure | 2 — 15 stars, 4 forks | 1.5 | 3 |
| Novelty | 3 — unique futures tranche mechanism | 1.5 | 4.5 |
| Raw Score | 2.45 | ||
| Clarity version penalty (pre-v4) | -0.50 | ||
| Final Score | 1.95 ✅ | ||
Single contract with three functional areas:
buy-stx-futures → stack-stx-tranche → redeem-stx-futuresAUTHORIZED-STACKERS list checked via foldtransfer, get-balance-of, get-name, etc.)get-total-supply Returns STX Balance Instead of Token SupplyLocation: get-total-supply (line 205)
Description: The function returns stx-get-balance of the contract instead of ft-get-supply stx-future. These values diverge in critical scenarios:
stx-get-balance includes locked (stacked) STX, so it won't drop to zero during stacking — but it still tracks a different quantity than outstanding futures tokensft-get-supply may not (if tokens aren't burned)(define-public (get-total-supply)
(ok (stx-get-balance (as-contract tx-sender))))
;; Should be: (ok (ft-get-supply stx-future))
Impact: The reported "total supply" reflects the contract's STX holdings rather than outstanding futures tokens. If extra STX arrives at the contract address, early redeemers can drain more than their fair share (first-come-first-served extraction of excess). DEX integrations using this function will price the token incorrectly.
Recommendation: Use (ft-get-supply stx-future) instead. Make it define-read-only.
Location: buy-stx-futures / redeem-stx-futures
Description: Once a user buys futures tokens, they cannot redeem until either (a) the stacking lock ends, or (b) the stacker fails to stack and the reward cycle begins. If the authorized stacker delays stacking, users' STX is locked in the contract with no exit mechanism until FIRST-REWARD-CYCLE starts.
;; Redemption requires:
(asserts! (or (>= cur-reward-cycle unlock-cycle)
(and (>= cur-reward-cycle FIRST-REWARD-CYCLE) (not locked?)))
(err ERR-NOT-YET-REDEEMABLE))
Impact: Illiquidity risk. Users cannot exit their position before the lock period. Combined with the tradeable token interface, this creates a secondary market where the futures would trade at a significant discount due to illiquidity and zero yield.
Recommendation: Add a cancel-futures function that allows users to redeem before stacking occurs (while locked is still false).
memo ParameterLocation: transfer (line 192)
Description: The transfer function is missing the required memo parameter from SIP-010. The standard requires (transfer (amount uint) (from principal) (to principal) (memo (optional (buff 34)))).
;; Current:
(define-public (transfer (amount uint) (from principal) (to principal))
;; SIP-010 requires:
;; (define-public (transfer (amount uint) (from principal) (to principal) (memo (optional (buff 34))))
Impact: Incompatible with SIP-010 trait. Cannot be listed on DEXs or used with standard tooling that expects the trait. Wallets cannot send memos with transfers.
Recommendation: Add the memo parameter and implement SIP-010 trait.
Location: get-name, get-symbol, get-decimals, get-balance-of, get-total-supply, get-token-uri (lines 195-210)
Description: Six functions that return static/read-only data are defined as define-public instead of define-read-only. This means callers must submit a transaction (with fees) to read token metadata.
Impact: Unnecessary transaction costs. Users/contracts calling these functions pay gas for read operations. SIP-010 specifies these as read-only.
Recommendation: Change all six to define-read-only.
Location: burn-height-to-reward-cycle, reward-cycle-to-burn-height, stack-stx-tranche
Description: The contract hardcodes 'SP000000000000000000002Q6VF78.pox (PoX v1). Stacks mainnet has since upgraded through PoX v2, v3, and PoX-4. The v1 contract is no longer active — calls to it will fail.
(contract-call? 'SP000000000000000000002Q6VF78.pox get-pox-info)
(contract-call? 'SP000000000000000000002Q6VF78.pox stack-stx ...)
Impact: Contract is non-functional on current mainnet without updating the PoX contract reference. Deployment would fail or stacking would revert.
Recommendation: Update to current PoX version. Consider using a configurable trait for PoX to allow future upgrades.
as-contract — No Asset AllowancesLocation: Multiple (lines 82, 104, 107, 159, 175)
Description: The contract uses as-contract (legacy) instead of Clarity 4's as-contract? with explicit asset allowances (with-stx, with-ft). The legacy form grants blanket authority over all contract assets within the block.
Impact: If composed with other contracts, the blanket as-contract could be exploited. Clarity 4's restricted form (as-contract? (with-stx amount)) limits the blast radius.
Recommendation: Migrate to Clarity 4 and use as-contract? with explicit allowances.
unwrap-panic Used for Recoverable ErrorsLocation: buy-stx-futures (lines 82-83), redeem-stx-futures (lines 104, 107), stack-stx-tranche (line 175)
Description: Multiple uses of unwrap-panic where unwrap! with a proper error code would be safer. If any of these operations fail unexpectedly, the entire transaction aborts with a runtime panic instead of a clean error.
Impact: Poor error handling. Debugging failed transactions is harder. In buy-stx-futures, the STX transfer succeeding but NFT mint failing triggers a panic abort (though the transaction atomically reverts, the user experience is degraded).
Recommendation: Replace unwrap-panic with unwrap! and appropriate error codes.
Location: All public functions
Description: The contract emits no print events for buy, redeem, stack, or transfer operations. Off-chain indexers and UIs cannot easily track contract activity.
Recommendation: Add (print { event: "buy", amount: amount-ustx, buyer: tx-sender }) style events to all state-changing functions.
Location: is-authorized / auth-check (lines 118-130)
Description: The authorization check iterates through AUTHORIZED-STACKERS using fold. While functional, this is O(n) and allocates intermediate data structures. For a small hardcoded list this is acceptable, but a map-based lookup would be more idiomatic.
Recommendation: Use a define-map for authorized stackers, or simply chain (or (is-eq tx-sender s1) (is-eq tx-sender s2) ...).
FIRST-REWARD-CYCLE is in the past or REWARD-CYCLE-LOCK-PERIOD is out of range, deployment fails. This is excellent defensive practice.locked flag prevents double-stacking, and the reward cycle checks create clear phase transitions (buy → stack → redeem).get-total-supply to use ft-get-supply — prevents supply/balance mismatch and first-come-first-served extraction.as-contract? with explicit asset allowances.Independent audit by cocoa007.btc · Full audit portfolio