Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
210 changes: 210 additions & 0 deletions contracts/hk-sbtc-real-receiver-v1.clar
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
;; HK sBTC Real Receiver v1
;;
;; External-developer flash-loan receiver that executes a REAL DEX round-trip on
;; canonical sBTC:
;; borrow sBTC from flashstack-sbtc-core -> swap sBTC->wSTX on Velar (pool 70) ->
;; swap wSTX->sBTC back -> repay principal + fee, atomically.
;;
;; This is the sBTC mirror of the Milestone-1 receiver hk-stx-bitflow-receiver-v1.
;; Composition (per Design ADR-S1..S7):
;; - hk-stx-bitflow-receiver-v1 skeleton : contract-caller gate + absolute
;; principals + dynamic fee lookup + fail-closed repay + owner rescue +
;; read-only estimate.
;; - velar-sbtc-arb-receiver legs : the proven sBTC->wSTX->sBTC Velar
;; round-trip (pool 70). The reference exposes a PUBLIC callback with NO
;; caller gate (velar-sbtc-arb-receiver.clar:47) -- the gate below closes
;; that direct-drain vector (ADR-S2, non-negotiable).
;;
;; Deployed under an EXTERNAL wallet (not the protocol deployer), so EVERY
;; cross-contract reference is an ABSOLUTE mainnet principal. `.contract` sugar
;; would resolve to THIS deployer and break for an external deploy (M1 ADR-001).
;; The Velar router's token/share-fee-to args are trait_reference params; we pass
;; ABSOLUTE-PRINCIPAL LITERALS there, mirroring the proven velar-sbtc-arb-receiver
;; exactly (literals are what the analyzer resolves for static trait conformance).
;;
;; Objective is NOT profit. It is: external strategy execution + successful flash
;; loan + real DEX interaction + successful repayment. min-out=u1 on both legs lets
;; the swaps clear; the repayment assert and the core's own before/after reserve
;; check are the safety gates. The seed covers the ~0.6% Velar round-trip loss + fee.
;;
;; Live contracts used:
;; Core: SP20XD46NGAX05ZQZDKFYCCX49A3852BQABNP0VG5.flashstack-sbtc-core
;; sBTC token: SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token
;; Velar router: SP1Y5YSTAHZ88XYK1VPDH24GY0HPX5J4JECTMY4A1.univ2-router
;; wSTX token: SP1Y5YSTAHZ88XYK1VPDH24GY0HPX5J4JECTMY4A1.wstx
;; share-fee-to: SP1Y5YSTAHZ88XYK1VPDH24GY0HPX5J4JECTMY4A1.univ2-share-fee-to
;; Velar pool 70: SP20X3DC5R091J8B6YPQT638J8NR1W83KN6TN5BJY.univ2-pool-v1_0_0-0070
;; (token0 = wSTX, token1 = sBTC)
;;
;; Clarity version: 3 (epoch 3.0 / Nakamoto)

(impl-trait 'SP20XD46NGAX05ZQZDKFYCCX49A3852BQABNP0VG5.sbtc-flash-receiver-trait.sbtc-flash-receiver-trait)

;; =============================================
;; Constants
;; =============================================

(define-constant CONTRACT-OWNER tx-sender)

;; The ONLY legitimate caller of execute-sbtc-flash. Hard-coded (Form A gate).
;; Used in the caller-gate is-eq comparison below.
(define-constant FLASHSTACK-SBTC-CORE 'SP20XD46NGAX05ZQZDKFYCCX49A3852BQABNP0VG5.flashstack-sbtc-core)

(define-constant ERR-NOT-OWNER (err u400))
(define-constant ERR-SWAP-LEG1 (err u401))
(define-constant ERR-SWAP-LEG2 (err u402))
(define-constant ERR-WRONG-CALLER (err u403))
(define-constant ERR-FEE-LOOKUP (err u404))
(define-constant ERR-REPAY-FAILED (err u500))
(define-constant ERR-INSUFFICIENT (err u501))

;; Slippage tolerance in basis points (default 300 = 3%). Retained as an ops knob;
;; the live legs use min-out=u1 and rely on the repayment assert as the safety gate.
(define-data-var slippage-bp uint u300)

;; =============================================
;; Flash Loan Callback
;; =============================================

(define-public (execute-sbtc-flash (amount uint) (core principal))
(begin
;; Gate: only the live flashstack-sbtc-core may invoke this callback.
;; Closes the direct-drain path a public execute-sbtc-flash would otherwise
;; expose (the velar-sbtc-arb-receiver reference lacks this).
(asserts! (is-eq contract-caller FLASHSTACK-SBTC-CORE) ERR-WRONG-CALLER)
(let (
;; Repayment math - look up the fee dynamically (never hard-code u5).
(fee-bp (unwrap! (contract-call? 'SP20XD46NGAX05ZQZDKFYCCX49A3852BQABNP0VG5.flashstack-sbtc-core
get-fee-basis-points) ERR-FEE-LOOKUP))
(raw-fee (/ (* amount fee-bp) u10000))
(fee (if (> raw-fee u0) raw-fee u1))
(total-owed (+ amount fee))
)
;; Leg 1: sBTC -> wSTX on Velar pool 70 (token0=wSTX, token1=sBTC).
;; as-contract: the borrowed sBTC sits in THIS contract's balance.
(unwrap! (as-contract (contract-call? 'SP1Y5YSTAHZ88XYK1VPDH24GY0HPX5J4JECTMY4A1.univ2-router
swap-exact-tokens-for-tokens
u70 ;; pool id
'SP1Y5YSTAHZ88XYK1VPDH24GY0HPX5J4JECTMY4A1.wstx ;; token0 (wSTX)
'SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token ;; token1 (sBTC)
'SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token ;; token-in (sBTC)
'SP1Y5YSTAHZ88XYK1VPDH24GY0HPX5J4JECTMY4A1.wstx ;; token-out (wSTX)
'SP1Y5YSTAHZ88XYK1VPDH24GY0HPX5J4JECTMY4A1.univ2-share-fee-to ;; share-fee-to
amount ;; amt-in (sBTC sats)
u1 ;; amt-out-min (accept any)
)) ERR-SWAP-LEG1)

;; How much wSTX did we receive? Swap the entire wSTX balance back.
(let (
(wstx-balance (unwrap!
(as-contract (contract-call? 'SP1Y5YSTAHZ88XYK1VPDH24GY0HPX5J4JECTMY4A1.wstx
get-balance tx-sender))
ERR-SWAP-LEG2))
)
(asserts! (> wstx-balance u0) ERR-SWAP-LEG2)

;; Leg 2: wSTX -> sBTC on Velar pool 70.
(unwrap! (as-contract (contract-call? 'SP1Y5YSTAHZ88XYK1VPDH24GY0HPX5J4JECTMY4A1.univ2-router
swap-exact-tokens-for-tokens
u70 ;; pool id
'SP1Y5YSTAHZ88XYK1VPDH24GY0HPX5J4JECTMY4A1.wstx ;; token0 (wSTX)
'SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token ;; token1 (sBTC)
'SP1Y5YSTAHZ88XYK1VPDH24GY0HPX5J4JECTMY4A1.wstx ;; token-in (wSTX)
'SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token ;; token-out (sBTC)
'SP1Y5YSTAHZ88XYK1VPDH24GY0HPX5J4JECTMY4A1.univ2-share-fee-to ;; share-fee-to
wstx-balance ;; amt-in (all wSTX)
u1 ;; amt-out-min
)) ERR-SWAP-LEG2)

;; Repay sBTC + fee back to the core. Fail closed if the round-trip + seed
;; came up short (no funds at risk - the whole tx reverts).
(let ((sbtc-now (unwrap!
(as-contract (contract-call? 'SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token
get-balance tx-sender))
ERR-REPAY-FAILED)))
(asserts! (>= sbtc-now total-owed) ERR-INSUFFICIENT)
(unwrap!
(as-contract (contract-call? 'SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token
transfer total-owed tx-sender core none))
ERR-REPAY-FAILED)
(print { event: "sbtc-velar-roundtrip", amount: amount, fee: fee,
wstx-mid: wstx-balance, sbtc-after: sbtc-now })
(ok true)
)
)
)
)
)

;; =============================================
;; Admin
;; =============================================

(define-public (set-slippage-bp (new-bp uint))
(begin
(asserts! (is-eq tx-sender CONTRACT-OWNER) ERR-NOT-OWNER)
(asserts! (<= new-bp u500) ERR-NOT-OWNER) ;; max 5%
(ok (var-set slippage-bp new-bp))
)
)

;; Rescue stuck sBTC (owner only) - escape hatch to recover the seed, or any
;; sBTC stranded by a partial round-trip.
(define-public (rescue-sbtc (amount uint) (to principal))
(begin
(asserts! (is-eq tx-sender CONTRACT-OWNER) ERR-NOT-OWNER)
(unwrap!
(as-contract (contract-call? 'SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token
transfer amount tx-sender to none))
ERR-NOT-OWNER)
(ok true)
)
)

;; Rescue stuck wSTX (owner only) - escape hatch if leg 2 ever fails to consume
;; all wSTX (should not happen with min-out=u1, but fail-safe recovery).
(define-public (rescue-wstx (amount uint) (to principal))
(begin
(asserts! (is-eq tx-sender CONTRACT-OWNER) ERR-NOT-OWNER)
(unwrap!
(as-contract (contract-call? 'SP1Y5YSTAHZ88XYK1VPDH24GY0HPX5J4JECTMY4A1.wstx
transfer amount tx-sender to none))
ERR-NOT-OWNER)
(ok true)
)
)

;; =============================================
;; Read-only
;; =============================================

(define-read-only (get-slippage-bp)
(ok (var-get slippage-bp))
)

(define-read-only (get-owner)
(ok CONTRACT-OWNER)
)

(define-read-only (get-sbtc-balance)
(contract-call? 'SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token get-balance (as-contract tx-sender))
)

(define-read-only (get-wstx-balance)
(contract-call? 'SP1Y5YSTAHZ88XYK1VPDH24GY0HPX5J4JECTMY4A1.wstx get-balance (as-contract tx-sender))
)

(define-read-only (estimate-repayment (amount uint))
(let (
(fee-bp (unwrap-panic (contract-call? 'SP20XD46NGAX05ZQZDKFYCCX49A3852BQABNP0VG5.flashstack-sbtc-core get-fee-basis-points)))
(raw-fee (/ (* amount fee-bp) u10000))
(fee (if (> raw-fee u0) raw-fee u1))
)
(ok {
loan-amount: amount,
fee-to-pay: fee,
total-owed: (+ amount fee),
note: "sBTC round-trip must return >= total-owed or the tx reverts. Seed covers the ~0.6% Velar round-trip loss + fee."
})
)
)
84 changes: 84 additions & 0 deletions hk-sbtc-receiver-deployment-plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# hk-sbtc-real-receiver-v1 — Deployment Plan, ADRs & Validation Record

**Author:** External integrator (HK)
**Date:** 2026-06-06
**Milestone:** 2 (external sBTC receiver on mainnet)
**Status:** Built & validated off-chain. **Mainnet deploy/execute is GATED on:** (1) acquiring the sBTC seed, (2) Matt whitelisting the receiver. No mainnet transactions broadcast yet.
**Companion docs:** `minimum_seed_analysis.md` · `sbtc-architecture-review.md`

---

## 1. What this is

`hk-sbtc-real-receiver-v1.clar` is the canonical-sBTC mirror of the Milestone-1 receiver. Inside one `flashstack-sbtc-core` flash loan it borrows sBTC, runs a **real Velar sBTC→wSTX→sBTC round-trip** (pool 70), and repays principal + fee atomically. Objective is **external strategy execution + repayment, not profit** — the same bar M1 met.

Architecture = **M1 skeleton** (caller gate, dynamic fee, fail-closed repay, owner rescue, read-only estimate) **⊕ `velar-sbtc-arb-receiver` legs** (the proven on-chain sBTC round-trip). Both halves are already deployed-and-proven on mainnet; this contract composes them and adds the caller gate the velar reference lacks.

---

## 2. Architectural Decision Records

| ADR | Decision | Rationale |
|---|---|---|
| **ADR-S1** | Reuse the M1 receiver skeleton; swap STX primitives → sBTC SIP-010 calls | M1 is proven on mainnet; minimizes new surface |
| **ADR-S2** | Add a **caller gate** (`contract-caller == flashstack-sbtc-core` → `ERR-WRONG-CALLER u403`) | `velar-sbtc-arb-receiver` exposes a **public** `execute-sbtc-flash` with no gate — a direct-drain vector. Non-negotiable. **Runtime-tested: a non-core caller returns `(err u403)`** (simnet). |
| **ADR-S3** | Venue = **Velar univ2 pool 70** (wSTX/sBTC) | Only proven, whitelisted on-chain sBTC round-trip in-repo. Live reserves: 223,275 wSTX / 65,845,796 sats. Accept ~0.6% round-trip cost; objective is execution, not profit. |
| **ADR-S4** | `min-out = u1` on both legs; repay-assert + core reserve check are the gates | Same as M1; avoids percentage-floor reverts. No funds at risk — under-repay reverts the whole tx. |
| **ADR-S5** | **Absolute** mainnet principals everywhere | External deploy; `.sugar` would resolve to our wallet (M1 ADR-001) |
| **ADR-S6** | Seed the receiver with real canonical sBTC, sized to round-trip loss + fee + margin; recover via `rescue-sbtc` | Receiver pays fee + slippage from its own balance. See `minimum_seed_analysis.md`: 1,000-sat loan → ~7-sat min seed, **seed 500 sats (safe)**. |
| **ADR-S7** | Size the loan to the live reserve (15,010 sats) | Guard `reserve ≥ amount`. Matt: 1,000-sat demo is sufficient; no top-up wait. |
| **ADR-S8** (new, implementation) | Pass **absolute-principal LITERALS** to the Velar router's `trait_reference` args (token0/1/in/out, share-fee-to), not named constants | The router's token/share-fee-to params are `trait_reference`. The clarity analyzer resolves a **literal** contract principal for static trait conformance; a named constant in that position fails to resolve (`use of unresolved contract`). Mirrors `velar-sbtc-arb-receiver` exactly. Constants are still fine for the call *target* and for non-trait positions, but we use literals throughout the swap calls for one consistent, proven pattern. |

---

## 3. Validation record (off-chain, no broadcast)

| Check | Tool | Result |
|---|---|---|
| Static type-check | `clarinet check` against logic-equivalent stubs (sBTC/Velar live suite won't assemble into simnet — same gap M1 hit with Bitflow) | **✔ 9 contracts checked**, 0 errors, 0 warnings on the receiver |
| Caller-gate negative test | `clarinet console` (simnet) | non-core caller of `execute-sbtc-flash` → **`(err u403)`** |
| Router signature conformance | live Hiro interface read | `swap-exact-tokens-for-tokens(id, token0, token1, token-in, token-out, share-fee-to, amt-in, amt-out-min)` — args match exactly |
| Deploy tx build | `deploy-hk-sbtc-receiver.mjs DRY_RUN=1` | tx built OK (19,264-byte body), Clarity 3, balance/nonce confirmed |
| Execute preflight + tx build | `execute-sbtc-flash-loan.mjs DRY_RUN=1` | reserve→15,010, is-approved→false, seed→0 correctly read; warnings fire; tx built OK |
| Buy-seed quote + tx build | `buy-sbtc.mjs DRY_RUN=1` | 3.5 STX → ~1,029 sats quoted (matches seed analysis); swap tx built OK |
| Parallel-to-deployed-reference | manual diff | swap legs identical in shape to the live `velar-sbtc-arb-receiver`; skeleton identical to the live M1 receiver |

---

## 4. Deliverables (this milestone)

- `contracts/hk-sbtc-real-receiver-v1.clar` — the receiver.
- `scripts/deploy-hk-sbtc-receiver.mjs` — deploy (DRY_RUN, balance/nonce/fee checks).
- `scripts/buy-sbtc.mjs` — acquire sBTC seed via Velar (DRY_RUN, live quote).
- `scripts/seed-hk-sbtc-receiver.mjs` — SIP-010 sBTC seed transfer (DRY_RUN, balance guard).
- `scripts/execute-sbtc-flash-loan.mjs` — execute flash loan (DRY_RUN, full preflight: approved/reserve/seed/estimate).
- `minimum_seed_analysis.md` — economic validation (aggressive/recommended/safe seed tiers).
- `sbtc-architecture-review.md` — updated with live state + implementation findings.
- This deployment plan + ADRs.

---

## 5. Mainnet runbook (execute only when ready)

All scripts read the 24-word mnemonic from `./mbegu2` then `./mbegu` (gitignored live key) or `MAINNET_MNEMONIC`. Run each with `DRY_RUN=1` first.

1. **Acquire sBTC seed:** `node scripts/buy-sbtc.mjs` (default 3.5 STX → ~1,000 sats). Verify wallet sBTC balance.
2. **Deploy:** `node scripts/deploy-hk-sbtc-receiver.mjs` (~0.5 STX). Note the contract id.
3. **Whitelist (Matt):** ask Matt to call `add-approved-receiver` on `flashstack-sbtc-core` for `SP3NZYZA88ENNF0FCR57KBGPFY5RAXWHXXVSB6FBW.hk-sbtc-real-receiver-v1`. Verify `is-approved-receiver → (ok true)`.
4. **Seed:** `SEED_SATS=500 node scripts/seed-hk-sbtc-receiver.mjs`. Verify receiver sBTC balance = 500.
5. **Execute:** `AMOUNT_SATS=1000 node scripts/execute-sbtc-flash-loan.mjs`. Expect `(ok true)` + a `sbtc-velar-roundtrip` print event + reserve `+fee`.
6. **Collect evidence:** txid, print event, Velar swap events, reserve delta, `total-loans` 2→3.
7. **Recover seed:** call `rescue-sbtc(residual, SP3NZYZA…)` to return the ~493-sat residual; optionally sell sBTC back to STX.

---

## 6. Risk posture

- **Reserve at risk: none.** Atomic; any under-repayment reverts the entire tx (core before/after check + our repay-assert).
- **Worst realistic failure:** a failed tx (wasted gas) if the pool drifts and seed margin is too thin — mitigated by seeding the **safe** tier (500 sats).
- **External deps:** acquire sBTC (we control), Matt whitelist (same as M1), Velar pool 70 liquidity (deep), optional reserve top-up (not needed for 1,000-sat demo).
- **Known core bug (`set-fee-basis-points`: wrong error path + no lower bound):** documented in `sbtc-architecture-review.md`; Matt acknowledged; **not a blocker** — our dynamic fee lookup tolerates any valid fee.

---

*Built and validated 2026-06-06 against live mainnet reads and clarinet simnet. No mainnet transactions broadcast. Definition of done (M2): deployed → whitelisted → seeded → `flash-loan` `(ok true)` with evidence → seed recovered.*
Loading
Loading