FastPool xbtc-sbtc-swap-v4

One-way xBTC-to-sBTC swap contract with custodial bridge model

ContractSP2PABAF9FTAJYNFZH93XENAJ8FVY99RRM50D2JG9.xbtc-sbtc-swap-v4
Receipt TokenSP2PABAF9FTAJYNFZH93XENAJ8FVY99RRM50D2JG9.swapping-xbtc-v4
ProtocolFastPool xBTC Swap
Clarity VersionClarity 4 (Stacks 3.x — uses as-contract?, with-ft)
Lines of Code~130 (swap) + ~80 (receipt token)
ConfidenceHIGH — full source available on-chain, both contracts reviewed, simple architecture
Audit Methodaibtcdev/skills clarity-audit framework
DateApril 1, 2026
0
Critical
0
High
1
Medium
2
Low
2
Info

Verdict: PASS — Core swap logic is sound. The receipt token is well-guarded. The custodial model is explicit, intentional, and appropriate for the xBTC deprecation use case. One medium finding related to the enroll function's open trait parameter. No funds at direct risk.

Overview

This contract implements a one-way xBTC-to-sBTC migration path:

  1. Deposit: User sends xBTC to the contract, receives swxBTC receipt tokens 1:1
  2. Unwrap: Operator sends accumulated xBTC to custodian for BTC conversion and sBTC bridge deposit
  3. Claim: User burns swxBTC receipt tokens and receives sBTC 1:1 (when available)
  4. Withdraw: User can cancel and reclaim xBTC before the unwrap cycle (if xBTC still in contract)

The architecture is deliberately simple: two contracts (swap + receipt token), two privileged roles (deployer + operator), one custodian, and a hardcoded excess receiver. No maps, no complex state. The attack surface is minimal.

What Works Well

Findings

MEDIUM

M-01: enroll accepts arbitrary trait-implementing contracts

Location: enroll function

(define-public (enroll
    (enroll-contract <enroll-trait>)
    (receiver (optional principal))
  )
  (begin
    (asserts! (is-eq tx-sender deployer) err-unauthorized)
    (as-contract? () (try! (contract-call? enroll-contract enroll receiver)))
  )
)

Description: The enroll function calls an arbitrary contract (any contract implementing enroll-trait) using as-contract?, which executes as the swap contract's principal. While this is deployer-only, if the deployer key is ever compromised, an attacker could pass a malicious contract whose enroll function performs unauthorized actions (e.g., transferring tokens held by the swap contract).

Context: This is currently used for dual-stacking enrollment. The deployer is a trusted operator, and in practice the risk requires key compromise — not a code logic bug. The trait interface (enroll ((optional principal)) (response bool uint)) limits what can be called, but the body of a malicious implementation is unconstrained.

Impact: If deployer key is compromised, could drain contract funds via crafted enrollment contract. This is a privilege escalation vector, not a direct vulnerability.

Recommendation: Consider hardcoding the enrollment contract address or maintaining an allowlist. Alternatively, document that the deployer key must be kept in cold storage or behind a multisig.

LOW

L-01: unwrap-panic in read-only functions

Location: get-xbtc-balance, get-swapping-xbtc-balance, get-sbtc-balance, get-swapping-xbtc-supply

(define-read-only (get-xbtc-balance (user principal))
  (unwrap-panic (contract-call? 'SP3DX3H4FEYZJZ586MFBS25ZW3HZDMEW92260R2PR.Wrapped-Bitcoin
    get-balance user
  ))
)

Description: All four read-only helper functions use unwrap-panic. If the underlying token contract ever returns an error (unlikely but possible during chain upgrades or if the token contract is replaced), these functions panic with an opaque runtime error instead of a readable error code.

Impact: Low — these are convenience wrappers. The public functions that depend on them would fail with an uninformative error, but no funds are at risk.

Recommendation: Use (unwrap! ... (err u520)) for better error diagnostics. Not urgent.

LOW

L-02: No pause or emergency stop mechanism

Description: The contract has no circuit breaker. If a vulnerability is discovered after deployment, there's no way to halt deposits, claims, or withdrawals without deploying a new contract version.

Context: For a simple migration contract with a finite lifespan (xBTC deprecation), this is a reasonable trade-off. Adding a pause mechanism increases complexity and introduces its own centralization risk (who controls the pause?).

Impact: Low — acceptable design choice for a time-limited migration tool.

Recommendation: For future versions, consider a minimal (define-data-var paused bool false) toggle.

INFO

I-01: withdraw-excess-sbtc is permissionless

Description: Anyone can call withdraw-excess-sbtc, which sends any sBTC balance exceeding the swxBTC supply to the hardcoded excess-sbtc-receiver. The excess only goes to a known, fixed address — never to the caller.

Assessment: This is a design choice, not a vulnerability. Permissionless sweeping means excess sBTC doesn't sit idle. A griefing actor could trigger it repeatedly, but the effect is just moving funds to where they belong faster. No user funds are affected.

INFO

I-02: Custodial trust model is explicit and appropriate

Description: The swap relies on a custodian to convert xBTC to BTC and deposit into the sBTC bridge. This is a centralized trust point by design — the xBTC-to-sBTC migration fundamentally requires someone to unwrap xBTC (which is a custodial wrapped Bitcoin) and re-wrap it as sBTC.

Assessment: Users should understand that between init-unwrap (when xBTC leaves the contract) and sBTC becoming available, their funds are in the custodian's hands. The contract provides withdraw-xbtc as an escape hatch before the unwrap cycle. This is a reasonable design for a bridge/migration tool.

Receipt Token Analysis: swapping-xbtc-v4

CheckResultDetail
SIP-010 compliancePASSAll required functions implemented
Mint access controlPASSOnly swap contract can mint (enforced by is-swap-contract-calling)
Burn access controlPASSOnly swap contract can burn
Transfer authorizationPASSChecks tx-sender or contract-caller = sender
Swap contract assignmentPASSOne-shot via is-none guard, deployer-only
Supply capN/ANo cap — appropriate since supply is 1:1 backed by xBTC deposits

The receipt token is clean and well-implemented. The set-swap-contract one-shot pattern is a good practice — it prevents the deployer from redirecting mint/burn authority after deployment.

Architecture Diagram

User                      Swap Contract                 Custodian
  |                            |                            |
  |-- deposit-xbtc ---------->|                            |
  |<-- swxBTC minted ---------|                            |
  |                            |                            |
  |                            |-- init-unwrap (operator) ->|
  |                            |   (all xBTC sent)          |
  |                            |                            |
  |                            |                [converts xBTC -> BTC -> sBTC bridge]
  |                            |                            |
  |                            |<-- sBTC deposited ---------|
  |                            |                            |
  |-- claim-sbtc ------------>|                            |
  |<-- sBTC received ---------|                            |
  |   (swxBTC burned)          |                            |

Summary

This is a well-scoped, minimal contract that does one thing: migrate xBTC holders to sBTC. The code is clean, the access controls are correct, and the receipt token pattern is solid. The one medium finding (open trait in enroll) is a defense-in-depth concern, not an active exploit vector — it requires deployer key compromise. The custodial model is inherent to the xBTC deprecation process and is handled transparently.

For users: Safe to use. Understand that after the operator triggers init-unwrap, your xBTC is with the custodian until sBTC arrives. Use withdraw-xbtc if you change your mind before the unwrap cycle.