flashstack
FlashStack Protocol — Flash Loan Security Audit
mattglory/Flashstack ·
Commit: 5928466 (2026-02-11) ·
Audited: February 21, 2026
Overview
FlashStack is an atomic flash-minting protocol for sBTC on the Stacks blockchain. Users with STX locked in PoX-4 can flash-mint sBTC (up to 1/3 of their locked STX value), execute arbitrary receiver callbacks, and repay within the same transaction. The protocol includes a core flash-mint engine, a custom sBTC token with mint/burn privileges, a receiver trait interface, and 10+ receiver implementations for arbitrage, liquidation, leverage looping, collateral swaps, and yield optimization.
The codebase spans 13 Clarity contracts totaling ~1,200 lines. All contracts use Clarity v2 (epoch 2.5). The architecture follows the EIP-3156 flash loan pattern adapted for Clarity's single-transaction execution model.
Priority Score
| Metric | Weight | Score | Weighted |
|---|---|---|---|
| Financial risk | 3 | 3 — Flash loans, sBTC minting/burning, DeFi composability | 9 |
| Deployment likelihood | 2 | 2 — Has tests, scripts, multiple iterations (v1→v2→v3) | 4 |
| Code complexity | 2 | 3 — 13 contracts, 1200+ lines, trait-based callback pattern | 6 |
| User exposure | 1.5 | 0 — 0 stars, 1 fork | 0 |
| Novelty | 1.5 | 3 — Flash loans on Stacks, unique mechanism | 4.5 |
| Total (pre-penalty) | 23.5 / 10 = 2.35 | ||
| Clarity v2 penalty | -0.5 | ||
| Final Score | 1.85 ≥ 1.8 ✓ | ||
Findings Summary
| ID | Severity | Title |
|---|---|---|
| C-01 | CRITICAL | sBTC token allows unrestricted minting by deployer — infinite token creation |
| C-02 | CRITICAL | Flash-mint repayment check is circumventable — receiver can drain minted tokens |
| H-01 | HIGH | Burned tokens disappear but fees are never collected — protocol earns nothing |
| H-02 | HIGH | Admin has unilateral power to drain all flash-mintable value |
| H-03 | HIGH | Receiver contracts use hardcoded fee calculations — desync with core fee rate |
| M-01 | MEDIUM | Collateral is based on test map, not real PoX-4 — mock never removed |
| M-02 | MEDIUM | Block volume tracking uses block-height not stacks-block-height |
| M-03 | MEDIUM | DEX price-setting functions in receivers have no access control |
| L-01 | LOW | Fee rounding to zero on small amounts — free flash loans |
| L-02 | LOW | No fee upper bound validation allows admin to set 100% fee |
| I-01 | INFO | Clarity v2 — should migrate to v4 for as-contract? and restrict-assets? |
| I-02 | INFO | flashstack-core-v2 is incomplete — missing variables, constants, and admin functions |
Detailed Findings
C-01 sBTC token allows unrestricted minting by deployer — infinite token creation
Location: sbtc-token.clar, mint function
Description: The sBTC token's mint function has a dual authorization path: contract-caller == flash-minter OR tx-sender == CONTRACT-OWNER. The tx-sender check means the deployer can mint unlimited sBTC at any time by calling mint directly, completely outside the flash-loan mechanism. This is not just an admin privilege — it's a backdoor that undermines the entire flash-loan invariant (tokens should only exist during flash execution).
(define-public (mint (amount uint) (recipient principal))
(begin
(asserts! (or (is-eq contract-caller (var-get flash-minter))
(is-eq tx-sender CONTRACT-OWNER)) ;; <-- deployer can mint anytime
ERR-NOT-AUTHORIZED)
(ft-mint? sbtc amount recipient)
)
)
Impact: The deployer can mint infinite sBTC to any address, inflating supply without collateral. If sBTC has any real value (pegged or traded), this is a rug-pull vector. Even the burn function has the same backdoor — the deployer can burn anyone's tokens.
Recommendation: Remove tx-sender == CONTRACT-OWNER from both mint and burn. Only the flash-minter contract should be able to mint/burn. For initial setup, use a one-time setup function or constructor pattern.
C-02 Flash-mint repayment check is circumventable — receiver can drain minted tokens
Location: flashstack-core.clar, flash-mint function
Description: The repayment verification checks that the contract's own balance increased by total-owed after the callback. However, the minted tokens go to the receiver contract, not the core contract. The receiver can transfer tokens to the core contract's balance AND keep the remainder. Since total-owed = amount + fee and the protocol mints exactly total-owed to the receiver, the receiver must transfer all of it back — but the check only verifies the core contract balance delta, not that the receiver's balance is zero.
;; Mint amount + fee to receiver
(try! (contract-call? .sbtc-token mint total-owed receiver-principal))
;; Execute callback
(match (contract-call? receiver execute-flash amount borrower)
success (begin
(let (
(balance-after (unwrap! (as-contract (contract-call? .sbtc-token get-balance tx-sender)) ...))
)
;; Only checks core balance increased
(asserts! (>= balance-after (+ balance-before total-owed)) ERR-REPAY-FAILED)
;; Then burns total-owed from core
(try! (as-contract (contract-call? .sbtc-token burn total-owed tx-sender)))
)))
Impact: A malicious receiver can: (1) receive total-owed tokens, (2) transfer total-owed to the core contract (passing the check), (3) but also mint additional tokens or accumulate from previous flash loans. More critically, if a receiver is whitelisted but has a re-entrancy path or secondary callback, it could manipulate the balance check. The fundamental issue is that the invariant should verify total supply unchanged, not just core balance.
Recommendation: Instead of checking balance delta, check that the total sBTC supply after the callback equals the supply before the callback. This is the canonical flash-loan invariant: supply-after == supply-before (the minted tokens must be fully returned for burning).
H-01 Burned tokens disappear but fees are never collected — protocol earns nothing
Location: flashstack-core.clar, flash-mint
Description: The protocol mints amount + fee, then burns amount + fee (the full total-owed) from the core contract. The fee is burned along with the principal — it's destroyed, not collected. The total-fees-collected variable increments as a counter, but no sBTC is ever retained by the protocol or transferred to an admin/treasury.
;; Burns the entire total-owed, including the fee
(try! (as-contract (contract-call? .sbtc-token burn total-owed tx-sender)))
Impact: The protocol has zero revenue. The fee mechanism is purely cosmetic. If the protocol is meant to generate revenue for maintenance, development, or stakers, it currently cannot.
Recommendation: Burn only amount and transfer fee to a treasury address. Alternatively, keep the fee in the core contract for later withdrawal by admin.
H-02 Admin has unilateral power to drain all flash-mintable value
Location: flashstack-core.clar, admin functions; sbtc-token.clar, set-flash-minter
Description: The admin (single EOA) can: (1) set-admin to transfer ownership, (2) set-fee up to 1% per loan, (3) set-max-single-loan and set-max-block-volume to arbitrary values, (4) add-approved-receiver to whitelist a malicious receiver, (5) set-test-stx-locked to fake any collateral. Combined with C-01 (deployer mint), a compromised admin key means total loss of protocol integrity.
Impact: Single point of failure. Admin key compromise = total protocol compromise.
Recommendation: Implement timelocked admin actions, multisig requirement, or at minimum separate roles for different admin functions. Remove set-test-stx-locked before any deployment.
H-03 Receiver contracts use hardcoded fee calculations — desync with core fee rate
Location: example-arbitrage-receiver.clar, liquidation-receiver.clar, snp-flashstack-receiver.clar, and others
Description: Most receiver contracts hardcode the fee calculation as (/ (* amount u5) u10000) (5 basis points). Only test-receiver.clar and snp-flashstack-receiver-v3.clar correctly query the core contract's fee rate via get-fee-basis-points. If the admin changes the fee rate, these hardcoded receivers will calculate the wrong repayment amount and either fail (if fee increased) or overpay (if fee decreased).
;; Hardcoded in most receivers:
(fee (/ (* amount u5) u10000))
;; Correct approach (only in test-receiver and v3):
(fee-bp (unwrap! (contract-call? .flashstack-core get-fee-basis-points) ...))
(fee (/ (* amount fee-bp) u10000))
Impact: Fee desync causes transaction failures or fund leakage. If fee is raised, existing receivers revert (denial of service). If lowered, receivers overpay (wasted funds).
Recommendation: All receivers should query the fee dynamically from the core contract, as test-receiver and snp-flashstack-receiver-v3 already do.
M-01 Collateral is based on test map, not real PoX-4 — mock never removed
Location: flashstack-core.clar, get-stx-locked and set-test-stx-locked
Description: The production PoX-4 integration is commented out and replaced with a test-locked-stx map that the admin can set arbitrarily. The comment says "TODO: Remove this before mainnet deployment" but it's the only active implementation. There's no compile-time or deploy-time guard against shipping the test version.
Impact: If deployed as-is, collateral checks are meaningless — admin can grant flash-mint access to anyone by setting fake locked STX values.
Recommendation: Implement a deployment checklist. Use Clarinet deployment plans to ensure the production version is deployed. Consider a feature flag pattern that disables test functions after initialization.
M-02 Block volume tracking uses block-height not stacks-block-height
Location: flashstack-core.clar, flash-mint circuit breaker
Description: The per-block volume limit uses block-height (the keyword, which in Clarity v2 refers to the burn chain block height). Since Stacks 2.1+, stacks-block-height should be used for Stacks-specific logic. Using Bitcoin block height for rate limiting means the volume window is ~10 minutes instead of the expected Stacks block time, which can vary.
(let (
(current-block-volume (default-to u0 (map-get? block-loan-volume block-height)))
...
)
Impact: Rate limiting may be more or less restrictive than intended depending on Bitcoin vs Stacks block time divergence.
Recommendation: Use stacks-block-height for Stacks-specific rate limiting.
M-03 DEX price-setting functions in receivers have no access control
Location: example-arbitrage-receiver.clar (set-dex-a-price, set-dex-b-price), dex-aggregator-receiver.clar (set-alex-price, set-velar-price, set-bitflow-price)
Description: These public functions allow anyone to set the simulated DEX prices. While labeled as "for testing," they're public functions on deployed contracts with no access control.
(define-public (set-dex-a-price (price uint))
(begin
(asserts! (> price u0) ERR-ARBITRAGE-FAILED)
(ok (var-set dex-a-price price)) ;; Anyone can call
)
)
Impact: If these receivers are deployed alongside the core contract, anyone can manipulate the price feeds that guide arbitrage decisions.
Recommendation: Add admin-only checks or remove test functions before deployment.
L-01 Fee rounding to zero on small amounts — free flash loans
Location: flashstack-core.clar, fee calculation
Description: The fee formula (/ (* amount 5) 10000) rounds down to zero for amounts less than 2,000 sats (0.00002 sBTC). This enables free flash loans for small amounts.
Impact: Minimal financial impact given the small amounts involved, but it creates a free-rider vector for high-frequency small flash loans.
Recommendation: Enforce a minimum fee: (max u1 (/ (* amount fee-bp) u10000)).
L-02 No fee upper bound validation — admin can set punitive fees
Location: flashstack-core.clar, set-fee
Description: The fee validation caps at 100 basis points (1%), but uses ERR-UNAUTHORIZED as the error code instead of a dedicated ERR-INVALID-FEE. The cap itself is reasonable but the error message is misleading.
Impact: Minor UX issue — confusing error when fee validation fails.
Recommendation: Use ERR-INVALID-AMOUNT for fee validation errors.
I-01 Clarity v2 — should migrate to v4 for enhanced security builtins
Description: All contracts use Clarity v2 (epoch 2.5). Clarity v4 introduces as-contract? with explicit asset allowances and restrict-assets?, which would significantly improve the security of the flash-mint pattern. The current as-contract usage in the core contract (for balance checks and burns) would benefit from explicit asset restrictions.
Recommendation: Upgrade to Clarity v4 and use as-contract? with with-ft allowances for all sBTC operations.
I-02 flashstack-core-v2 is incomplete — missing variables, constants, and admin functions
Description: The flashstack-core-v2.clar file contains only the flash-mint function but references undefined variables (flash-fee-basis-points, admin, total-flash-mints, etc.) and constants (MIN-COLLATERAL-RATIO). It also uses the flash-receiver trait but doesn't re-import it. This file will not compile.
Recommendation: Either complete v2 or remove it to avoid confusion. The active contracts in Clarinet.toml don't include it, but its presence in the repo is misleading.
Architecture Analysis
Flash Loan Flow
- Borrower calls
flash-mint(amount, receiver) - Core checks: pause state, whitelist, circuit breakers, collateral (PoX-4 locked STX)
- Core records
balance-beforeof its own sBTC balance - Core mints
amount + feeto the receiver contract - Core calls
receiver.execute-flash(amount, borrower) - Receiver executes strategy (arbitrage, liquidation, etc.)
- Receiver transfers
amount + feeback to core contract - Core checks
balance-after >= balance-before + total-owed - Core burns
total-owedfrom its own balance
What Works Well
- Circuit breakers: Per-loan and per-block volume limits are a good safety measure
- Receiver whitelist: Prevents arbitrary callback targets
- Collateral requirement: 300% collateralization ratio is conservative
- Trait-based receiver pattern: Clean separation of concerns
- Balance-check repayment: Atomic verification within the same transaction
Recommendations (Priority Order)
- Remove deployer mint/burn backdoor (C-01) — This is the highest-priority fix. The sBTC token should only be mintable/burnable by the flash-minter contract.
- Use supply-based repayment check (C-02) — Replace balance delta check with total supply invariant:
supply-after == supply-before. - Separate fee from burn (H-01) — Burn only the principal amount; retain or transfer the fee to a treasury.
- Remove test collateral map (M-01) — Enable the real PoX-4 integration before any deployment.
- Upgrade to Clarity v4 (I-01) — Use
as-contract?with explicit asset allowances for all privileged operations.