Skip to content
Merged
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,9 @@ settings/Testnet.toml
secrets.json
wallet.json
mnemonics.txt
# External-deployer mainnet mnemonic files (24-word secrets) β€” NEVER commit
mbegu
mbegu2

# ============================================
# Build Artifacts
Expand Down
176 changes: 176 additions & 0 deletions contracts/hk-stx-bitflow-receiver-v1.clar
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
;; HK STX Bitflow Receiver v1
;;
;; External-developer flash-loan receiver that executes a REAL DEX round-trip:
;; borrow STX from flashstack-stx-core -> swap STX->stSTX on Bitflow ->
;; swap stSTX->STX back -> repay principal + fee, atomically.
;;
;; Deployed under an EXTERNAL wallet (not the protocol deployer), so every
;; cross-contract reference uses the ABSOLUTE mainnet principal. The `.flashstack-stx-core`
;; sugar used by the in-repo bitflow-arb-receiver would resolve to THIS deployer's
;; address and break for an external deploy.
;;
;; Combines:
;; - hk-stx-real-receiver-v2 : contract-caller gate + absolute principals
;; - bitflow-arb-receiver-v4 : the STX/stSTX Bitflow round-trip
;;
;; Objective is NOT profit. It is: external strategy execution + successful flash
;; loan + real DEX interaction + successful repayment. The repayment assert and the
;; core's own reserve check are the safety gates; min-out=u1 lets the swaps clear.
;;
;; Live contracts used:
;; Core: SP20XD46NGAX05ZQZDKFYCCX49A3852BQABNP0VG5.flashstack-stx-core
;; Bitflow pool: SPQC38PW542EQJ5M11CR25P7BS1CA6QT4TBXGB3M.stableswap-stx-ststx-v-1-2
;; stSTX token: SP4SZE494VC2YC5JYG7AYFQ44F5Q4PYV7DVMDPBG.ststx-token
;; Bitflow LP: SPQC38PW542EQJ5M11CR25P7BS1CA6QT4TBXGB3M.stx-ststx-lp-token-v-1-2
;;
;; Clarity version: 3 (epoch 3.0 / Nakamoto)

(impl-trait 'SP3TGRVG7DKGFVRTTVGGS60S59R916FWB4DAB9STZ.stx-flash-receiver-trait.stx-flash-receiver-trait)

;; Minimal SIP-010 trait for calling the stSTX token
(define-trait sip-010-trait
(
(transfer (uint principal principal (optional (buff 34))) (response bool uint))
(get-name () (response (string-ascii 32) uint))
(get-symbol () (response (string-ascii 32) uint))
(get-decimals () (response uint uint))
(get-balance (principal) (response uint uint))
(get-total-supply () (response uint uint))
(get-token-uri () (response (optional (string-utf8 256)) uint))
)
)

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

(define-constant CONTRACT-OWNER tx-sender)

;; The ONLY legitimate caller of execute-stx-flash. Hard-coded (Form A gate).
(define-constant FLASHSTACK-STX-CORE 'SP20XD46NGAX05ZQZDKFYCCX49A3852BQABNP0VG5.flashstack-stx-core)

(define-constant ERR-NOT-OWNER (err u400))
(define-constant ERR-SWAP-FAILED (err u401))
(define-constant ERR-WRONG-CALLER (err u403))
(define-constant ERR-REPAY-FAILED (err u500))

;; Bitflow STX/stSTX stableswap pool
(define-constant BITFLOW-POOL 'SPQC38PW542EQJ5M11CR25P7BS1CA6QT4TBXGB3M.stableswap-stx-ststx-v-1-2)

;; stSTX SIP-010 token (y-token in the pool)
(define-constant STSTX 'SP4SZE494VC2YC5JYG7AYFQ44F5Q4PYV7DVMDPBG.ststx-token)

;; Bitflow STX/stSTX LP token (lp-token parameter)
(define-constant BITFLOW-LP 'SPQC38PW542EQJ5M11CR25P7BS1CA6QT4TBXGB3M.stx-ststx-lp-token-v-1-2)

;; 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-stx-flash (amount uint) (core principal))
(begin
;; Gate: only the live flashstack-stx-core may invoke this callback.
;; Closes the direct-drain path a public execute-stx-flash would otherwise expose.
(asserts! (is-eq contract-caller FLASHSTACK-STX-CORE) ERR-WRONG-CALLER)
(let (
;; Repayment math - look up the fee dynamically (never hard-code u5).
(fee-bp (unwrap! (contract-call? FLASHSTACK-STX-CORE get-fee-basis-points) ERR-SWAP-FAILED))
(raw-fee (/ (* amount fee-bp) u10000))
(fee (if (> raw-fee u0) raw-fee u1))
(total-owed (+ amount fee))

;; min-out=u1 on both legs - the swap always clears; the repay assert is the gate.
(min-ststx u1)
(min-stx u1)
)
;; Leg 1: STX -> stSTX on Bitflow.
;; as-contract: the borrowed STX sits in THIS contract's balance.
(unwrap! (as-contract (contract-call? BITFLOW-POOL swap-x-for-y
STSTX ;; y-token (stSTX SIP-010)
BITFLOW-LP ;; lp-token
amount ;; STX in (microSTX)
min-ststx ;; min stSTX out
)) ERR-SWAP-FAILED)

;; How much stSTX did we receive?
(let (
(ststx-balance (unwrap!
(contract-call? STSTX get-balance (as-contract tx-sender))
ERR-SWAP-FAILED))
)
(asserts! (> ststx-balance u0) ERR-SWAP-FAILED)

;; Leg 2: stSTX -> STX on Bitflow.
(unwrap! (as-contract (contract-call? BITFLOW-POOL swap-y-for-x
STSTX
BITFLOW-LP
ststx-balance ;; all stSTX we hold
min-stx ;; min STX back
)) ERR-SWAP-FAILED)

;; Repay STX + fee back to the core. Fail closed if the round-trip came up short.
(let ((stx-now (stx-get-balance (as-contract tx-sender))))
(asserts! (>= stx-now total-owed) ERR-REPAY-FAILED)
(unwrap! (as-contract (stx-transfer? total-owed tx-sender core)) ERR-REPAY-FAILED)
(print { event: "bitflow-roundtrip", amount: amount, fee: fee,
ststx-mid: ststx-balance, stx-after: stx-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-SWAP-FAILED) ;; max 5%
(ok (var-set slippage-bp new-bp))
)
)

;; Rescue stuck STX (owner only) - escape hatch if a partial round-trip strands STX.
(define-public (rescue-stx (amount uint) (to principal))
(begin
(asserts! (is-eq tx-sender CONTRACT-OWNER) ERR-NOT-OWNER)
(unwrap! (as-contract (stx-transfer? amount tx-sender to)) 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 (estimate-repayment (amount uint))
(let (
;; Literal principal (not the constant) so the analyzer can prove this
;; cross-contract call is read-only inside a define-read-only function.
(fee-bp (unwrap-panic (contract-call? 'SP20XD46NGAX05ZQZDKFYCCX49A3852BQABNP0VG5.flashstack-stx-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: "Round-trip must return >= total-owed or the tx reverts. Seed covers any shortfall."
})
)
)
148 changes: 148 additions & 0 deletions scripts/deploy-hk-bitflow-receiver.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
/**
* FlashStack -- Deploy hk-stx-bitflow-receiver-v1 to mainnet
*
* External-developer receiver that runs a REAL Bitflow STX->stSTX->STX round-trip
* inside a flash loan and repays principal + fee atomically.
*
* Deploys from OUR own mainnet wallet (NOT the protocol deployer). The admin-only
* whitelist call (`add-approved-receiver` on flashstack-stx-core) must be done by
* Matt separately before the receiver can borrow.
*
* Usage:
* MAINNET_MNEMONIC="word1 ... word24" node scripts/deploy-hk-bitflow-receiver.mjs
* Or, if ./mbegu2 holds the 24-word mnemonic on a single line:
* node scripts/deploy-hk-bitflow-receiver.mjs
*
* Optional env:
* DRY_RUN=1 Build and print the tx fields but do NOT broadcast.
* DEPLOY_FEE_USTX=... Override the deploy fee (default 500_000 microSTX = 0.5 STX).
*/

import { makeContractDeploy, PostConditionMode, ClarityVersion, privateKeyToAddress } from "@stacks/transactions";
import networkPkg from "@stacks/network";
const { STACKS_MAINNET } = networkPkg;
import walletPkg from "@stacks/wallet-sdk";
const { generateWallet } = walletPkg;
import { readFileSync, existsSync } from "fs";

const NAME = "hk-stx-bitflow-receiver-v1";
const PATH = "contracts/hk-stx-bitflow-receiver-v1.clar";
const API = "https://api.hiro.so";
const EXPLORER = "https://explorer.hiro.so/txid";
const FEE = Number(process.env.DEPLOY_FEE_USTX ?? 500_000);
const DRY_RUN = process.env.DRY_RUN === "1";
const network = STACKS_MAINNET;

function loadMnemonic() {
if (process.env.MAINNET_MNEMONIC) return process.env.MAINNET_MNEMONIC.trim();
if (existsSync("mbegu2")) return readFileSync("mbegu2", "utf8").trim();
if (existsSync("mbegu")) return readFileSync("mbegu", "utf8").trim();
throw new Error("Set MAINNET_MNEMONIC env var, or place 24-word mnemonic in ./mbegu2");
}

async function getNonce(addr) {
const res = await fetch(`${API}/v2/accounts/${addr}?proof=0`);
return (await res.json()).nonce;
}

async function getBalance(addr) {
const res = await fetch(`${API}/extended/v1/address/${addr}/balances`);
return BigInt((await res.json()).stx.balance);
}

async function broadcast(tx) {
const raw = tx.serialize();
const body = typeof raw === "string" ? Buffer.from(raw.replace(/^0x/, ""), "hex") : raw;
const res = await fetch(`${API}/v2/transactions`, {
method: "POST", headers: { "Content-Type": "application/octet-stream" }, body,
});
const text = await res.text();
let data; try { data = JSON.parse(text); } catch { throw new Error(`Non-JSON: ${text.slice(0, 200)}`); }
if (data?.error) throw new Error(`${data.error} -- ${data.reason ?? ""}`);
const txid = typeof data === "string" ? data : data.txid;
if (!txid) throw new Error(`No txid: ${text.slice(0, 200)}`);
return txid;
}

async function waitForConfirm(txid, label) {
process.stdout.write(` Waiting for "${label}"`);
for (let i = 0; i < 80; i++) {
await new Promise(r => setTimeout(r, 8000));
const res = await fetch(`${API}/extended/v1/tx/0x${txid}`);
const data = await res.json();
if (data.tx_status === "success") { console.log(" confirmed."); return; }
if (data.tx_status?.startsWith("abort")) {
console.log(`\n FAILED: ${data.tx_result?.repr ?? "unknown"}`);
throw new Error(`"${label}" failed`);
}
process.stdout.write(".");
}
throw new Error(`Timeout: "${label}"`);
}

async function main() {
const mnemonic = loadMnemonic();
const wc = mnemonic.split(/\s+/).length;
if (wc !== 24) throw new Error(`Expected 24-word mnemonic, got ${wc} words`);

const wallet = await generateWallet({ secretKey: mnemonic, password: "" });
const pk = wallet.accounts[0].stxPrivateKey;
const sender = privateKeyToAddress(pk, "mainnet");
const balance = await getBalance(sender);
const nonce = await getNonce(sender);

console.log("=======================================================");
console.log(" FlashStack -- Deploy hk-stx-bitflow-receiver-v1 ");
console.log("=======================================================");
console.log(` Sender: ${sender}`);
console.log(` Balance: ${Number(balance) / 1e6} STX`);
console.log(` Nonce: ${nonce}`);
console.log(` Fee: ${FEE} microSTX (${FEE / 1e6} STX)`);
console.log(` Contract: ${sender}.${NAME}`);
console.log(` Source: ${PATH}`);
console.log(` Network: mainnet`);
console.log(` Mode: ${DRY_RUN ? "DRY RUN (no broadcast)" : "LIVE BROADCAST"}`);
console.log();

if (balance < BigInt(FEE)) {
throw new Error(`Insufficient balance: ${balance} microSTX < fee ${FEE} microSTX`);
}

const tx = await makeContractDeploy({
contractName: NAME,
codeBody: readFileSync(PATH, "utf8"),
senderKey: pk,
network,
clarityVersion: ClarityVersion.Clarity3,
postConditionMode: PostConditionMode.Allow,
anchorMode: 1,
fee: FEE,
nonce,
});

if (DRY_RUN) {
console.log(" DRY_RUN=1 -- not broadcasting. Set DRY_RUN=0 (or unset) to broadcast.");
console.log(` Built tx OK. Serialized length: ${tx.serialize().length} bytes-ish.`);
return;
}

const txid = await broadcast(tx);
console.log(` Broadcast: ${txid}`);
console.log(` Explorer: ${EXPLORER}/${txid}?chain=mainnet`);
await waitForConfirm(txid, "deploy hk-stx-bitflow-receiver-v1");

console.log();
console.log("=======================================================");
console.log(" DEPLOYMENT COMPLETE ");
console.log("=======================================================");
console.log(` Contract: ${sender}.${NAME}`);
console.log(` Tx: ${EXPLORER}/${txid}?chain=mainnet`);
console.log();
console.log(" Next steps:");
console.log(` 1. Ask Matt to whitelist ${sender}.${NAME}`);
console.log(" (admin-only: add-approved-receiver on flashstack-stx-core)");
console.log(" 2. Send >=1 STX to the receiver to cover principal + fee + 2x Bitflow pool fee on repay");
console.log(" 3. Call flashstack-stx-core.flash-loan(u1000000, receiver) ;; 1 STX round-trip");
}

main().catch(e => { console.error("\nFAILED:", e.message); process.exit(1); });
Loading
Loading