Skip to content

fix(stacks-alpha-engine): post-#339 audit sweep — rebase of #346#367

Merged
whoabuddy merged 1 commit into
mainfrom
fix/stacks-alpha-engine-post-339-nits-rebase
Apr 30, 2026
Merged

fix(stacks-alpha-engine): post-#339 audit sweep — rebase of #346#367
whoabuddy merged 1 commit into
mainfrom
fix/stacks-alpha-engine-post-339-nits-rebase

Conversation

@whoabuddy
Copy link
Copy Markdown
Contributor

Summary

Rebased version of #346 (cliqueengagements) onto current main. Same content, single squashed commit.

Why a fresh PR

#346 was 18 commits behind main with cascading skills.json conflicts on each commit during the rebase chain (each commit touched the manifest, and main moved a lot since Apr 21). Rather than walk through 18 separate take-theirs + regen resolutions, took the cleaner approach: reset to current main, cherry-pick the stacks-alpha-engine/ directory contents from the PR's tip in one shot, regen manifest. Same end-state, single clean commit.

Per the established Wave 2 pattern (#427→#490 etc.) for forks where rebase-on-PR is impractical or would obscure history.

Diff

  • stacks-alpha-engine/stacks-alpha-engine.ts — main audit-sweep changes
  • stacks-alpha-engine/SKILL.md — doc updates
  • stacks-alpha-engine/AGENT.md — agent guidance updates
  • skills.json — regen for manifest

Total: +496/-105 lines (matches the original PR's +503/-105 closely; small delta is just from the manifest format).

Closes #346.

Test plan

  • bun run typecheck clean
  • bun run manifest clean (95 skills)
  • CI green here

🤖 Generated with Claude Code

Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com

Squashed rebase of #346 (cliqueengagements). Original 18 commits replaced
with single squash on current main + manifest regen. All audit-sweep
content from the original PR preserved (+496/-105 lines across
stacks-alpha-engine.ts/SKILL.md/AGENT.md).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings April 30, 2026 09:23
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e165e2e9a0

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +1156 to +1158
return "dlmm_1";
}
return "dlmm_1";
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Exclude borrow/repay from pool-based guardian checks

The new borrow/repay commands are still funneled through inferTargetPoolId, which falls back to dlmm_1 for any non-deploy/migrate command, so _runPipeline applies HODLMM slippage/volume gates to Zest debt operations that do not touch DLMM at all. In practice this means borrow/repay can be refused when dlmm_1 volume is low or Bitflow pool data is unavailable, even though the Zest transaction itself is valid.

Useful? React with 👍 / 👎.

@whoabuddy whoabuddy merged commit 4e02afe into main Apr 30, 2026
7 checks passed
@whoabuddy whoabuddy deleted the fix/stacks-alpha-engine-post-339-nits-rebase branch April 30, 2026 09:30
Copy link
Copy Markdown
Contributor

@arc0btc arc0btc left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Post-#339 audit sweep: adds borrow/repay commands to stacks-alpha-engine, fixes DLMM swap min-received calculation, corrects FT assetName bindings across protocols, and flips safety gates to fail-CLOSED. Each change is backed by on-chain proof — clean work.

What works well:

  • The DLMM swap min-received fix is the right call. The old Math.floor(amount * (1 - slip / 100)) computed the floor in input-token units, which is meaningless to the router. Computing in expected output-token units is correct, and expectedSwapOutput is a clean helper for it.
  • FT assetName corrections (usdh-tokenusdh, bridged-usdcaeUSDC, etc.) match what the contracts actually emit — these were silent failures waiting to happen.
  • Granite redeem PC shape is now right. The old bindings (lp-v1 as principal, bridged-usdc as asset name) didn't match the real flow; anchoring to state-v1 + 3-PC envelope (receive floor, receive cap, burn floor) is the correct pattern. The on-chain proof txids close the loop.
  • Fail-CLOSED on slippage, volume, and gas exceptions is a meaningful safety improvement. The old catch { /* allow */ } pattern was a silent footgun.
  • inferTargetPoolId correctly routes guardian checks to the pool the operation will actually touch rather than always measuring dlmm_1. That's an important fix for Hermetica/Granite deploys.
  • max-steps 6 → 230 per the #339 audit recommendation.
  • migrate --amount now required. The old implicit sBTC-balance default was a silent foot-gun for stablecoin protocol migrations.

[suggestion] Guardian slippage/volume gates don't apply to borrow/repay (stacks-alpha-engine.ts borrow/repay pipeline, inferTargetPoolId)

borrow and repay don't touch any DLMM pool — they call zest_borrow / zest_repay directly against the Zest market contract. But inferTargetPoolId falls through to "dlmm_1" for these commands, so the Guardian will refuse a valid borrow if dlmm_1 happens to have high slippage or low 24h volume that day. The cooldown and gas gates make sense for borrow/repay; the HODLMM slippage and volume gates don't.

One fix: add borrow and repay to inferTargetPoolId and return a sentinel like "none", then skip the slippage/volume blocks in checkGuardian when targetPoolId === "none". Alternatively, extract a dedicated checkGuardianForDebt() that only runs price, gas, and cooldown gates.

[nit] validTokens_borrowRepay: Record<string, string[]> is a local const initialized inline as { zest: ["usdh"] } and only read once two lines later. A direct const validBorrowTokens = ["usdh"] (dropping the protocol nesting, since we've already asserted protocol === "zest" in the preceding guard) would be simpler and avoids the surprising mixed-case name.

Operational context: We run Zest sBTC supply/withdraw ops nightly. From MEMORY: borrow-helper-v2-1-7 is confirmed required for mainnet borrow operations — the USDh-only gate here matches what we've seen empirically. The on-chain proof txids for the full borrow/repay cycle at the top of SKILL.md are the right way to document this. The DLMM dual-pin post-condition pattern (willSendLte on caller + willSendGte on pool) matches what we validated from bff-skills#494.

The fail-CLOSED refactors alone are worth merging. The borrow/repay guardian issue is an improvement-not-blocker — borrow will still work most of the time; it'll just refuse on days dlmm_1 liquidity dips. Worth a follow-up if you want the gate to be purely debt-condition-aware.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Rebases and lands the post-#339 audit sweep for the stacks-alpha-engine skill, focusing on fund-safety hardening (post-conditions), more accurate guardian gating, and adding Zest USDh borrow/repay to support a documented leveraged-yield flow.

Changes:

  • Hardened Bitflow DLMM swap safety (dual-pin allow-mode envelope, output-based min-received, higher max-steps) and improved Guardian pool targeting/fail-closed behavior.
  • Added Zest borrow / repay command surface (USDh-only) plus updated docs/agent guidance.
  • Regenerated skills.json manifest to include new commands.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 6 comments.

File Description
stacks-alpha-engine/stacks-alpha-engine.ts Adds borrow/repay commands, refines guardian gating, updates DLMM swap post-conditions, and parameterizes HODLMM deploy by pool-id.
stacks-alpha-engine/SKILL.md Updates surfaced command list and expands operational docs (proofs + leveraged-yield pattern + post-condition rationale).
stacks-alpha-engine/AGENT.md Updates agent operational rules to match the new Zest borrow/repay scope and restrictions.
skills.json Manifest regeneration to include new commands and updated generated timestamp.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +46 to +47
- **Zest USDh borrow**: [`0x2b465aae…`](https://explorer.hiro.so/txid/0x2b465aae05812d25e4f52799b5f2882b21ca411d892359aba5157dba85d1162a?chain=mainnet) — 50M µUSDh borrowed against sBTC collateral via `v0-4-market.borrow`
- **Zest USDh repay**: [`0xd3b46ae7…`](https://explorer.hiro.so/txid/0xd3b46ae74b666af2e06a765d29e30bd2b0341507266827a2140cc4d9e6053fba?chain=mainnet) — full 50M µUSDh debt cleared via `v0-4-market.repay`
Copy link

Copilot AI Apr 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The on-chain proof bullets label USDh amounts as “µUSDh” (micro), but elsewhere (and in code) USDh is treated as 8-decimal. Using “µUSDh” implies 6 decimals and can lead to 100× mis-sized amounts. Suggest rewording these to “atomic USDh (8 decimals)” or “10^-8 units” (and similarly in the leveraged-yield example placeholders).

Suggested change
- **Zest USDh borrow**: [`0x2b465aae…`](https://explorer.hiro.so/txid/0x2b465aae05812d25e4f52799b5f2882b21ca411d892359aba5157dba85d1162a?chain=mainnet) — 50M µUSDh borrowed against sBTC collateral via `v0-4-market.borrow`
- **Zest USDh repay**: [`0xd3b46ae7…`](https://explorer.hiro.so/txid/0xd3b46ae74b666af2e06a765d29e30bd2b0341507266827a2140cc4d9e6053fba?chain=mainnet) — full 50M µUSDh debt cleared via `v0-4-market.repay`
- **Zest USDh borrow**: [`0x2b465aae…`](https://explorer.hiro.so/txid/0x2b465aae05812d25e4f52799b5f2882b21ca411d892359aba5157dba85d1162a?chain=mainnet) — 50M atomic USDh (8 decimals) borrowed against sBTC collateral via `v0-4-market.borrow`
- **Zest USDh repay**: [`0xd3b46ae7…`](https://explorer.hiro.so/txid/0xd3b46ae74b666af2e06a765d29e30bd2b0341507266827a2140cc4d9e6053fba?chain=mainnet) — full 50M atomic USDh (8 decimals) debt cleared via `v0-4-market.repay`

Copilot uses AI. Check for mistakes.
Comment on lines +61 to +64
borrow --protocol zest --token usdh --amount <debt_micro_usdh> # take USDh debt (~7% APR)
deploy --protocol hermetica --token usdh --amount <debt_micro_usdh> # stake for 40% APY

# ---- earning ~33% positive carry on debt_micro_usdh while sBTC exposure preserved ----
Copy link

Copilot AI Apr 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the leveraged-yield example, the placeholders use <debt_micro_usdh> for USDh amounts, but the repo treats USDh as 8-decimal. Consider renaming the placeholder to reflect 10^-8 base units (or just “atomic units”) to avoid users passing 6-decimal-scaled values.

Suggested change
borrow --protocol zest --token usdh --amount <debt_micro_usdh> # take USDh debt (~7% APR)
deploy --protocol hermetica --token usdh --amount <debt_micro_usdh> # stake for 40% APY
# ---- earning ~33% positive carry on debt_micro_usdh while sBTC exposure preserved ----
borrow --protocol zest --token usdh --amount <debt_atomic_usdh> # take USDh debt (~7% APR)
deploy --protocol hermetica --token usdh --amount <debt_atomic_usdh> # stake for 40% APY
# ---- earning ~33% positive carry on debt_atomic_usdh while sBTC exposure preserved ----

Copilot uses AI. Check for mistakes.
Comment on lines 1104 to 1116
// USDCx → aeUSDC (pool: aeUSDC/USDCx, selling Y for X)
if ((tokenIn === "usdcx" || tokenIn === "stx") && tokenOut === "aeusdc") {
return {
pool: "SM1FKXGNZJWSTWDWXQZJNF7B5TV5ZB235JTCXYXKD.dlmm-pool-aeusdc-usdcx-v-1-bps-1",
xToken: AEUSDC_TOKEN, yToken: USDCX_TOKEN, xForY: false,
inputSymbol: tokenIn, outputSymbol: tokenOut,
};
}
// USDCx → USDh (pool: USDh/USDCx, selling Y for X)
if ((tokenIn === "usdcx" || tokenIn === "stx") && tokenOut === "usdh") {
return {
pool: "SM1FKXGNZJWSTWDWXQZJNF7B5TV5ZB235JTCXYXKD.dlmm-pool-usdh-usdcx-v-1-bps-1",
xToken: USDH_TOKEN, yToken: USDCX_TOKEN, xForY: false,
Copy link

Copilot AI Apr 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getDlmmSwapRoute currently treats tokenIn === "stx" as routable directly into the USDh/USDCx and aeUSDC/USDCx pools, but those pools don’t support STX as an input asset. As written, a deploy --protocol hermetica --token stx (or granite) path will build a swap that actually sends USDCx, causing an invalid/failed instruction. Either remove stx from these single-hop routes (and from the deploy validTokens list), or implement an explicit STX→USDCx hop (using an STX/USDCx pool) and emit a 2-step swap-simple-multi route.

Copilot uses AI. Check for mistakes.
Comment on lines 994 to 1014
const guardianPools = await fetchBitflowPools().catch(() => [] as BitflowPoolData[]);
try {
const dlmm1 = guardianPools.find(p => p.poolId === "dlmm_1");
if (dlmm1?.tokens) {
const pool1 = HODLMM_POOLS[0];
const abr = await callReadOnly(pool1.contract, "get-active-bin-id", []);
const targetPool = guardianPools.find(p => p.poolId === targetPoolId);
if (targetPool?.tokens) {
const abr = await callReadOnly(targetPoolDef.contract, "get-active-bin-id", []);
if (abr.okay && abr.result) {
const binsData = await fetchJson<{ bins?: Array<{ bin_id: number; price?: string }>; active_bin_id?: number }>(`${BITFLOW_API}/api/quotes/v1/bins/dlmm_1`);
const binsData = await fetchJson<{ bins?: Array<{ bin_id: number; price?: string }>; active_bin_id?: number }>(`${BITFLOW_API}/api/quotes/v1/bins/${targetPoolId}`);
const activeBinId = binsData.active_bin_id ?? 0;
const activeBinData = binsData.bins?.find(b => b.bin_id === activeBinId);
if (activeBinData?.price) {
const binPrice = parseFloat(activeBinData.price);
const hodlmmPriceUsd = (binPrice / PRICE_SCALE) * Math.pow(10, dlmm1.tokens.tokenX.decimals - dlmm1.tokens.tokenY.decimals);
const marketPrice = dlmm1.tokens.tokenX.priceUsd;
const hodlmmPriceUsd = (binPrice / PRICE_SCALE) * Math.pow(10, targetPool.tokens.tokenX.decimals - targetPool.tokens.tokenY.decimals);
const marketPrice = targetPool.tokens.tokenX.priceUsd;
if (marketPrice > 0) {
slippagePct = round(Math.abs(hodlmmPriceUsd - marketPrice) / marketPrice * 100, 4);
slippageOk = slippagePct <= MAX_SLIPPAGE_PCT;
if (!slippageOk) refusals.push(`Slippage ${slippagePct}% > ${MAX_SLIPPAGE_PCT}% cap`);
if (!slippageOk) refusals.push(`Slippage ${slippagePct}% > ${MAX_SLIPPAGE_PCT}% cap on ${targetPoolId}`);
}
}
}
}
Copy link

Copilot AI Apr 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The slippage gate only fails-closed on thrown errors. If fetchBitflowPools() returns an empty list or the targetPoolId entry is missing/has no tokens, the code silently skips the slippage check (leaving slippageOk=true) and may allow writes without measuring price drift. Consider treating “pool not found / tokens missing” as an unavailable slippage check and refusing (similar to the catch-path), and likewise distinguishing “pool data unavailable” from a real $0 volume reading.

Copilot uses AI. Check for mistakes.
const from = opts.from as Protocol;
const to = opts.to as Protocol;
instructions.push(...buildWithdrawInstructions(from, scout));
const token = opts.token ?? inferToken(to);
Copy link

Copilot AI Apr 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the migrate executor path, const token = opts.token ?? inferToken(to); is passed through to buildDeployInstructions without normalization. If a user supplies --token with different casing (e.g. USDH), the downstream comparisons (token === "usdh", route lookup keys, etc.) won’t match and the migrate will take the wrong branch. Normalize opts.token to lowercase (and ideally validate it against the target protocol’s allowed tokens) the same way the deploy path does.

Suggested change
const token = opts.token ?? inferToken(to);
const token = (opts.token?.toLowerCase()) ?? inferToken(to);

Copilot uses AI. Check for mistakes.
Comment on lines +2224 to +2241
.requiredOption("--protocol <name>", "Protocol: zest")
.requiredOption("--token <symbol>", "Borrow asset (Zest: usdh)")
.requiredOption("--amount <value>", "Amount in smallest unit (µUSDh for usdh; USDh has 8 decimals)")
.option("--confirm", "Execute the transaction (without this flag, outputs a dry-run preview)")
.action(async (opts: Record<string, string>) => {
const result = await runPipeline(opts.wallet, "borrow", opts);
console.log(JSON.stringify(result, null, 2));
if (result.status !== "ok") process.exit(1);
});

program
.command("repay")
.description("Repay a borrowed Zest debt asset")
.requiredOption("--wallet <address>", "Stacks wallet address (SP...)")
.requiredOption("--protocol <name>", "Protocol: zest")
.requiredOption("--token <symbol>", "Repay asset (Zest: usdh)")
.requiredOption("--amount <value>", "Amount in smallest unit (µUSDh for usdh)")
.option("--confirm", "Execute the transaction (without this flag, outputs a dry-run preview)")
Copy link

Copilot AI Apr 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The borrow/repay CLI help strings describe the smallest USDh unit as “µUSDh”, but the code/config treats USDh as having 8 decimals, so the smallest unit is 1e-8 (not micro/1e-6). Please adjust the wording to avoid implying 6 decimals (e.g., “atomic units (10^-8)” or similar) so users don’t pass incorrectly-scaled amounts.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants