StackHub — Multi-Contract DeFi Platform Audit

AdekunleBamz/stackhub · Commit: 0d78d37 (2026-02-19) · Audited: February 21, 2026

2
Critical
2
High
3
Medium
2
Low
2
Info

Overview

StackHub is a DeFi platform on Stacks consisting of four fee-generating smart contracts:

Total lines of Clarity: ~446 across 4 contracts. No Clarity version specified (defaults to Clarity 1).

Priority Matrix Score

MetricScoreWeightWeighted
Financial Risk3 — holds/transfers STX, staking, marketplace39
Deployment Likelihood2 — has frontend, README, multi-contract24
Code Complexity3 — 4 contracts, 446 lines, multi-module26
User Exposure1 — 2 stars, recently active1.51.5
Novelty2 — multi-contract DeFi hub, new category1.53
Total (pre-penalty)2.35
Clarity version penalty (no Clarity 4)-0.5
Final Score1.85 ✓

Findings Summary

IDSeverityTitleContract
C-01CRITICALStaking Vault is custodial — funds sent to owner, not contractstaking-vault
C-02CRITICALToken Launchpad creates fake tokens — no actual FT mintingtoken-launchpad
H-01HIGHRe-staking resets lock timer, enabling early-fee griefingstaking-vault
H-02HIGHNFT marketplace buy has no price slippage protectionnft-marketplace
M-01MEDIUMImmutable owner across all contracts — no transfer mechanismall
M-02MEDIUMStaking vault balance tracking can desync from actual statestaking-vault
M-03MEDIUMNo royalty support for NFT creators on secondary salesnft-marketplace
L-01LOWZero-amount transfers and mints allowed in launchpadtoken-launchpad
L-02LOWNo event emission for critical state changesall
I-01INFONo Clarity version specified — defaults to Clarity 1all
I-02INFONo SIP trait compliance (SIP-009, SIP-010)nft-marketplace, token-launchpad

Detailed Findings

C-01 Staking Vault is Custodial — Funds Sent to Owner, Not Contract

Location: stackhub-staking-vault.clar, stake() function

Description: The stake function transfers STX directly to CONTRACT-OWNER (the deployer's wallet), not to the contract itself. This means the contract never holds any staked funds. The process-unstake function requires the owner to send funds back from their personal balance. If the owner spends the STX, stakers permanently lose their funds.

This is not a vault — it's a custodial deposit system where the owner has full, unrestricted access to all staked funds with no on-chain enforcement of return obligations.

;; stake() sends funds to owner, not contract:
(try! (stx-transfer? amount tx-sender CONTRACT-OWNER))

;; process-unstake() requires owner to pay from personal funds:
(asserts! (is-eq tx-sender CONTRACT-OWNER) ERR-NOT-OWNER)
(try! (stx-transfer? payout tx-sender staker))

Impact: Total loss of staked funds. The owner can accept stakes and never process unstake requests. This is architecturally a rug-pull vector — users believe they're interacting with a trustless vault, but they're sending STX directly to someone's wallet.

Recommendation: Use as-contract to hold STX in the contract's own address. Funds should only leave via contract logic, not owner discretion:

;; Stake INTO the contract:
(try! (stx-transfer? amount tx-sender (as-contract tx-sender)))

;; Unstake FROM the contract (no owner involvement):
(as-contract (stx-transfer? payout tx-sender staker))

C-02 Token Launchpad Creates Fake Tokens — No Actual FT Minting

Location: stackhub-token-launchpad.clar, create-token(), transfer-token()

Description: The contract defines a single (define-fungible-token stackhub-ft) but never calls ft-mint?. The create-token function only writes to a balances map and token-info map — it doesn't create actual on-chain fungible tokens. Users pay 5 STX for an entry in a map that has no real token representation.

The transfer-token function manipulates map entries, not actual token balances. These "tokens" cannot be used with any other contract, have no SIP-010 interface, and don't appear in wallet balances or block explorers.

;; "Creates" a token but just writes to maps:
(map-set token-info id { name: name, symbol: symbol, ... })
(map-set balances {token-id: id, owner: tx-sender} initial-supply)

;; stackhub-ft is defined but NEVER minted:
(define-fungible-token stackhub-ft)  ;; unused

Impact: Users pay real STX (5 STX) for worthless map entries. The "tokens" exist only within this contract's internal bookkeeping and have zero interoperability. This is functionally a fee collection mechanism with no real deliverable.

Recommendation: Either implement proper SIP-010 compliant tokens with ft-mint?, ft-transfer?, ft-burn?, or clearly document that "tokens" are contract-internal accounting units, and reduce/remove the creation fee.

H-01 Re-staking Resets Lock Timer, Enabling Early-Fee Griefing

Location: stackhub-staking-vault.clar, stake()

Description: When a user stakes additional STX, the start-block is reset to stacks-block-height. This means adding even 1 uSTX resets the 144-block lock timer for the entire staked balance, re-exposing all funds to the 2.5% early unstake penalty.

(match current-stake
  existing (map-set stakes tx-sender {
    amount: (+ (get amount existing) amount),
    start-block: stacks-block-height  ;; RESETS for entire balance
  })
  ...)

Impact: Users who incrementally add to their stake are perpetually trapped in the early-fee window. In a custodial design (C-01), the owner could deliberately time process-unstake calls right after users re-stake to maximize fees.

Recommendation: Track weighted average start-block or maintain separate stake entries per deposit.

H-02 NFT Marketplace Buy Has No Price Slippage Protection

Location: stackhub-nft-marketplace.clar, buy-nft()

Description: The buy-nft function takes only a token ID — there's no max-price parameter. While there's no direct update-listing function, a seller can unlist and relist at a higher price in the same block. If the buyer's transaction executes after the relist, they pay the inflated price.

;; Buyer cannot specify maximum acceptable price:
(define-public (buy-nft (id uint))
  (let (
    (listing (unwrap! (map-get? listings id) ERR-NOT-LISTED))
    (price (get price listing))  ;; whatever the current price is
    ...

Impact: Buyers can pay more than expected if listing price changes between transaction submission and mining. In high-value NFT sales, this could mean significant unexpected costs.

Recommendation: Add an expected-price parameter: (asserts! (<= price expected-price) ERR-PRICE)

M-01 Immutable Owner Across All Contracts

Location: All four contracts — (define-constant CONTRACT-OWNER tx-sender)

Description: All contracts set CONTRACT-OWNER as a constant equal to tx-sender at deployment. Constants cannot be changed. If the deployer's private key is compromised, all admin functions (price setting, fee withdrawal, unstake processing) are permanently controlled by the attacker with no recovery path.

Impact: No disaster recovery, no key rotation, no multisig upgrade path. A single key compromise captures all platform revenue and admin control permanently.

Recommendation: Use a define-data-var for the owner with a transfer-ownership function protected by the current owner.

M-02 Staking Vault Balance Tracking Can Desync

Location: stackhub-staking-vault.clar, total-staked and vault-balance

Description: The contract maintains total-staked and vault-balance variables that are updated manually. Since funds go to CONTRACT-OWNER (C-01), these variables track nothing real. Even if the contract were fixed to hold funds, the variables would drift if STX is sent directly to the contract outside of stake().

Impact: Accounting variables don't reflect actual state. Any UI or logic depending on these values would display incorrect information.

Recommendation: Use stx-get-balance on the contract address for real balance checks rather than manual tracking.

M-03 No Royalty Support for NFT Creators on Secondary Sales

Location: stackhub-nft-marketplace.clar, buy-nft()

Description: The nft-data map stores a creator field, but buy-nft only splits payment between seller and platform. The original creator receives nothing on secondary sales.

Impact: Creators have no incentive to use the platform since they earn nothing after the initial sale. This is a significant missing feature for an NFT marketplace.

Recommendation: Add a creator royalty (e.g., 2.5%) on secondary sales paid from the sale proceeds.

L-01 Zero-Amount Transfers and Mints Allowed in Launchpad

Location: stackhub-token-launchpad.clar, transfer-token(), mint-tokens()

Description: Neither function validates that amount > 0. Zero-amount operations succeed, polluting event logs and enabling spam.

Recommendation: Add (asserts! (> amount u0) ERR-ZERO) to both functions.

L-02 No Event Emission for Critical State Changes

Location: All contracts except staking-vault's request-unstake

Description: Only request-unstake emits a print event. NFT mints, sales, token transfers, service payments, and staking/unstaking emit no events. This makes off-chain indexing and monitoring impossible.

Recommendation: Add (print ...) calls for all significant state transitions.

I-01 No Clarity Version Specified

Location: Clarinet.toml, all contract files

Description: No clarity_version is specified in Clarinet.toml or contract files. The contracts default to Clarity 1, missing safety features from Clarity 2+ (e.g., as-contract? with explicit asset allowances in Clarity 4).

Recommendation: Specify clarity_version = 3 at minimum, and consider Clarity 4 for restrict-assets? and as-contract?.

I-02 No SIP Trait Compliance

Location: stackhub-nft-marketplace.clar, stackhub-token-launchpad.clar

Description: The NFT contract does not implement SIP-009 (the transfer function signature differs — missing memo parameter). The token launchpad doesn't implement SIP-010. Without trait compliance, these contracts cannot interoperate with the broader Stacks ecosystem (wallets, explorers, other contracts).

Recommendation: Implement the standard SIP-009 and SIP-010 trait interfaces.

Architecture Observations