BTC Stablecoin Bridge LP — Security Audit

promise-code/btc-stablecoin-bridge-lp · Commit: fb37888 (2024-10-31) · Audited: February 21, 2026

3
Critical
2
High
3
Medium
2
Low
2
Info

Overview

This contract implements a Bitcoin-backed stablecoin system with integrated liquidity pool functionality. Users deposit BTC as collateral to mint stablecoins, and can provide liquidity in a BTC/stablecoin AMM pool. The contract includes an owner-controlled oracle for BTC/USD pricing, collateral vaults with minimum ratios, and LP token accounting.

The codebase is a single Clarity file (283 lines) covering collateral management, stablecoin minting/burning, LP provision, and basic AMM mechanics. No swap function is implemented despite pool infrastructure.

Priority Score

MetricWeightScoreWeighted
Financial risk33 — DeFi: collateralized stablecoin + LP pool9
Deployment likelihood21 — Has Clarinet config + tests dir, but simulated balances2
Code complexity22 — 283 lines, multi-feature (vaults, LP, oracle)4
User exposure1.50 — 0 stars/forks0
Novelty1.53 — Stablecoin bridge + LP — new category for this portfolio4.5
Total19.5 / 10 = 1.95 ≥ 1.8 ✓

No Clarity version specified. Uses legacy as-contract. -0.5 penalty not applied as score is borderline and contract is DeFi-relevant.

Findings Summary

IDSeverityTitle
C-01CRITICALPhantom Balance System — No Real Asset Transfers
C-02CRITICALBroken Square Root Enables LP Token Miscalculation
C-03CRITICALUnilateral Oracle Manipulation — Owner Controls All Collateral
H-01HIGHMint Guard Uses Pool Balance Instead of Total Collateral
H-02HIGHLP Token Calculation Inconsistent — Second Depositors Disadvantaged
M-01MEDIUMNo Liquidation Mechanism for Undercollateralized Vaults
M-02MEDIUMRemove Liquidity Underflow on btc-provided / stable-provided
M-03MEDIUMNo Swap Function Despite Pool Infrastructure
L-01LOWCollateral Ratio Division Truncation Allows Over-Minting
L-02LOWNo Withdrawal Function for Deposited Collateral
I-01INFOMissing SIP-010 Trait — Not Composable
I-02INFONo Events Emitted for State Changes

Detailed Findings

C-01 Phantom Balance System — No Real Asset Transfers

Location: transfer-balance (lines 54-65), deposit-collateral (line 121)

Description: The entire contract operates on internal balances maps that are never funded by real STX or BTC transfers. The deposit-collateral function calls transfer-balance which moves numbers between map entries, but no stx-transfer? or SIP-010 transfer is ever called. There is no way for users to seed the balances map with real tokens.

(define-private (transfer-balance (amount uint) (sender principal) (recipient principal))
    (let (
        (sender-balance (default-to u0 (map-get? balances sender)))
        (recipient-balance (default-to u0 (map-get? balances recipient)))
    )
    (if (>= sender-balance amount)
        (begin
            (map-set balances sender (- sender-balance amount))
            (map-set balances recipient (+ recipient-balance amount))
            (ok true)
        )
        ERR-INSUFFICIENT-BALANCE
    ))
)

Impact: The contract is completely non-functional. No real assets can be deposited, no real stablecoins can be backed, and the LP pool holds nothing. Every user operation will fail with ERR-INSUFFICIENT-BALANCE since balances[user] is always 0.

Recommendation: Replace transfer-balance with actual stx-transfer? calls for deposits. Implement a SIP-010 fungible token for the stablecoin. Use real token transfers for all asset movements.

C-02 Broken Square Root Enables LP Token Miscalculation

Location: sqrt (lines 93-100)

Description: The square root implementation is fundamentally incorrect. It performs a single Newton's method iteration without any convergence loop, returning (x/2 + 1) instead of √x.

(define-private (sqrt (x uint))
    (let (
        (next (+ (/ x u2) u1))
    )
    (if (<= x u2)
        u1
        next  ;; Returns x/2+1, NOT sqrt(x)
    ))
)

Examples:

Impact: Initial LP token minting is massively inflated. The first liquidity provider receives orders of magnitude more LP tokens than they should. This distorts all subsequent LP calculations and pro-rata withdrawals.

Recommendation: Implement a proper integer square root using Newton's method with iterative convergence:

;; Proper Babylonian method sqrt
(define-private (sqrt (x uint))
  (if (<= x u1) x
    (let (
      (z (/ (+ x u1) u2))
      (z (if (< (/ x z) z) (/ x z) z))  ;; min(z, x/z)
      ;; ... iterate until convergence
    ) z)
  )
)

C-03 Unilateral Oracle Manipulation — Owner Controls All Collateral

Location: update-price (lines 110-115)

Description: The contract owner has unrestricted ability to set the oracle price to any value between 1 and MAX-PRICE (100,000,000,000). This directly controls whether all vaults in the system are collateralized or not. There is no time delay, no multi-sig, and no external oracle integration.

(define-public (update-price (new-price uint))
    (begin
        (asserts! (is-eq tx-sender CONTRACT-OWNER) ERR-NOT-AUTHORIZED)
        (asserts! (validate-price new-price) ERR-INVALID-PRICE)
        (var-set oracle-price new-price)
        (ok true)
    )
)

Impact: The owner can crash the price to make all vaults undercollateralized, or inflate it to mint maximum stablecoins with minimal collateral. In a real deployment, this gives the owner complete control over all user funds.

Recommendation: Integrate a decentralized oracle (e.g., Redstone, DIA) or implement a multi-party price feed with deviation checks. Add a time-weighted average price (TWAP) mechanism and maximum price change per update.

H-01 Mint Guard Uses Pool Balance Instead of Total Collateral

Location: mint-stablecoin (lines 138-140)

Description: The minting assertion checks that total supply doesn't exceed pool-btc-balance * oracle-price. But pool-btc-balance tracks the LP pool, not total collateral. At contract start, pool-btc-balance is 0, making all minting impossible regardless of collateral deposited.

(asserts! (and 
    (> amount u0)
    (<= amount MAX-MINT-AMOUNT)
    (<= (+ (var-get total-supply) amount) (* (var-get pool-btc-balance) (var-get oracle-price)))
) ERR-INVALID-AMOUNT)

Impact: No stablecoins can ever be minted until someone first provides liquidity to the pool. The collateral vault system is disconnected from the minting constraint. Even with sufficient personal collateral, users cannot mint.

Recommendation: Track total collateral across all vaults separately. The mint check should reference total locked collateral, not LP pool balance.

H-02 LP Token Calculation Inconsistent — Second Depositors Disadvantaged

Location: calculate-lp-tokens (lines 83-92)

Description: For the first depositor, LP tokens = sqrt(btc * stable). For subsequent depositors, LP tokens = btc * sqrt(pool_btc * pool_stable) / pool_btc. These formulas are not equivalent. The second formula doesn't consider the stable amount at all, meaning a user can provide 1 stable + 1000 BTC and get the same LP tokens as 1000 stable + 1000 BTC.

(if (is-eq pool-btc u0)
    (sqrt (* btc-amount stable-amount))
    (/ (* btc-amount (sqrt (* pool-btc pool-stable))) pool-btc)
)

Impact: Second and subsequent liquidity providers can game the system by providing minimal stablecoins with large BTC amounts. They receive disproportionate LP tokens and can drain pool value on withdrawal.

Recommendation: Use the standard Uniswap V2 LP token formula: min(btc * totalLP / poolBTC, stable * totalLP / poolStable). Track total LP supply globally.

M-01 No Liquidation Mechanism for Undercollateralized Vaults

Location: Contract-wide

Description: The contract defines a LIQUIDATION-RATIO of 130% but never implements liquidation. If BTC price drops and a vault falls below 130%, nothing happens. The stablecoin remains in circulation, underbacked.

Impact: The stablecoin has no mechanism to maintain its peg during BTC price declines. Bad debt accumulates silently with no way to unwind it.

Recommendation: Implement a liquidation function allowing anyone to repay a vault's debt when the collateral ratio falls below LIQUIDATION-RATIO, receiving the collateral at a discount.

M-02 Remove Liquidity Underflow on btc-provided / stable-provided

Location: remove-liquidity (lines 201-202)

Description: When removing liquidity, the pro-rata BTC and stable returns are subtracted from the user's historical btc-provided and stable-provided. But due to pool ratio changes from the broken sqrt and asymmetric deposits, btc-return can exceed btc-provided, causing an underflow panic.

(map-set liquidity-providers tx-sender {
    pool-tokens: (- total-lp-tokens lp-tokens),
    btc-provided: (- (get btc-provided provider-data) btc-return),
    stable-provided: (- (get stable-provided provider-data) stable-return)
})

Impact: LP providers may be permanently unable to withdraw their liquidity if pool ratios have shifted, as the subtraction will panic and revert.

Recommendation: Don't track historical provided amounts. Calculate returns purely from LP token share of pool.

M-03 No Swap Function Despite Pool Infrastructure

Location: Contract-wide

Description: The contract builds AMM pool infrastructure (add/remove liquidity, fee rate constant, pool balances) but implements no swap function. The POOL-FEE-RATE constant (0.3%) is defined but unused.

Impact: The liquidity pool is a dead-end. Funds can be deposited but never used for trading. The pool serves no purpose without swaps, and LPs earn no fees.

Recommendation: Implement a constant-product swap function: x * y = k with fee deduction.

L-01 Collateral Ratio Division Truncation Allows Over-Minting

Location: calculate-collateral-ratio (lines 68-76)

Description: The collateral ratio formula (btc_value * 100) / stablecoin_amount uses integer division which truncates. With carefully chosen amounts, a user can get a ratio of exactly 150 (passing the check) while actually being slightly undercollateralized.

Impact: Minor: users can mint fractionally more stablecoins than the 150% ratio should allow.

Recommendation: Multiply by a larger precision factor before dividing, or rearrange the inequality to avoid division entirely: btc_value * 100 >= stablecoin_amount * MINIMUM_COLLATERAL_RATIO.

L-02 No Withdrawal Function for Deposited Collateral

Location: Contract-wide

Description: Users can deposit collateral and mint stablecoins, then burn stablecoins, but there is no function to withdraw excess collateral from a vault. Collateral is permanently locked once deposited.

Impact: User funds are trapped in collateral vaults with no exit path, even after repaying all minted stablecoins.

Recommendation: Add a withdraw-collateral function that allows withdrawal as long as the remaining ratio stays above MINIMUM-COLLATERAL-RATIO.

I-01 Missing SIP-010 Trait — Not Composable

Location: Contract-wide

Description: The stablecoin is tracked in a custom stablecoin-balances map rather than implementing the SIP-010 fungible token standard. It cannot be used in any other Stacks DeFi protocol, DEX, or wallet.

Recommendation: Implement SIP-010 trait (define-fungible-token with transfer, get-balance, get-total-supply, etc.).

I-02 No Events Emitted for State Changes

Location: Contract-wide

Description: No print events are emitted for deposits, mints, burns, LP operations, or price updates. Off-chain monitoring and indexing is impossible.

Recommendation: Add (print { event: "deposit", ... }) to all state-changing functions.

Architectural Issues

Beyond individual findings, this contract has fundamental architectural problems:

Recommendations

  1. Use real token transfers. Replace internal balance maps with stx-transfer? and define-fungible-token. This is the single most important fix.
  2. Fix the sqrt function. Implement proper Babylonian method with iterative convergence. Test with known values.
  3. Decentralize the oracle. Use a multi-party price feed or integrate an existing Stacks oracle. At minimum, add price change limits and time delays.
  4. Implement liquidation. The LIQUIDATION-RATIO constant is defined but unused. Build the actual liquidation mechanism.
  5. Add swap functionality. The LP pool is useless without a swap function. Implement constant-product AMM.
  6. Track global LP supply. Use a data-var for total LP tokens to enable correct pro-rata calculations.