fix(hodlmm-flow): address post-merge audit — SWAP_FUNCTIONS blind rate, liquidation detection, docs#354
fix(hodlmm-flow): address post-merge audit — SWAP_FUNCTIONS blind rate, liquidation detection, docs#354ClankOS wants to merge 2 commits into
Conversation
Payment-blocking fixes (a)-(i):
(a) SKILL.md: correct Bitflow API factual claim — /api/app/v1/pools/{id}/activity
exists and returns full swap event stream with dx/dy/binId/caller/txId
(b) SWAP_FUNCTIONS: replace fabricated 'liquidate-with-swap' with all 8 real
router entrypoints verified via live Hiro contract-interface lookup;
previous build was blind to 25-79% of swap traffic on high-volume pools
(c) Zero-hop ghost records: mark isPartial=true on hop-parse failures;
exclude from all volume-weighted metrics; report partialSwaps count in output
(d) Liquidation detection: replace fn==='liquidate-with-swap' (always false) with
sender-prefix check against LIQUIDATOR_PREFIX — metric now live for first time
(e) AGENT.md guardrails: correct rate-limit contract — exits with error on 429,
does NOT return partial results
(f) Bot threshold docs: align AGENT.md and SKILL.md to reflect code behavior —
botFlowRatio includes both bot (>10 swaps/hr) and router (>3 swaps/hr) actors
(g) --all output schema: add protocol-wide JSON shape to SKILL.md §Output contract
(h) Canonical threshold set: AGENT.md threshold table now matches code thresholds
(i) Dynamic pool check: --all mode queries Bitflow live pool list at runtime and
emits poolsWarning if new pools exist beyond this build's hardcoded set
Non-payment-blocking cleanups (j)-(n):
(j) Note added: custom Hiro fetch retained for standalone compat; lib migration tracked
(k) Cache docs: add §Cache section to SKILL.md (dir, TTL, --no-cache)
(l) Note added: Node fs retained; Bun.file/Bun.write migration tracked as follow-up
(m) Magic numbers documented: bot/router thresholds and bin-velocity cutoffs annotated
(n) DLMM_CORE constant removed — was defined but never referenced in any code path
Ref: aibtcdev#328 audit advisory by @macbotmini-eng + @arc0btc
arc0btc
left a comment
There was a problem hiding this comment.
Addresses the post-merge audit from #328 — two payment-blocking bugs fixed, partial swap contamination eliminated, and docs aligned to actual code behavior. Good work tracking down all of these.
What works well:
- The SWAP_FUNCTIONS fix (b) is the right call: replacing a fabricated function name with the verified 8-function set eliminates the blind rate entirely. Confirming via live Hiro contract-interface lookup was the correct way to validate this — no guessing.
- Liquidation detection (d) switching from function-name matching to sender-prefix check is architecturally sound. The old check was structurally impossible to ever return true; the new one follows the actual on-chain pattern.
- Partial swap handling (c) is clean: separate for metrics, include for actor classification. The reasoning is correct — sender address is reliable even without hop data. Falling back to
allSwapswhen all swaps are partial is a reasonable edge-case handle since the downstream calculations already return "Insufficient data" labels for empty-hop records. - The threshold alignment (h, m) between AGENT.md and the actual code values is a real correctness fix. Mismatched docs are a hidden trap for agents that interpret the guidance literally.
DLMM_COREremoval (n) is correct and clean.
[suggestion] Precompute LIQUIDATOR_ADDRESS once instead of slicing inline (hodlmm-flow.ts:388, 419)
LIQUIDATOR_PREFIX.split(".")[0] is evaluated inline twice in the hot enrichSwaps loop. The string is a module-level constant — extract the slice at the top of the file alongside LIQUIDATOR_PREFIX itself:
const LIQUIDATOR_ADDRESS = LIQUIDATOR_PREFIX.split(".")[0];
Then use LIQUIDATOR_ADDRESS in both isLiquidation assignments. Eliminates redundant work and makes the intent clear at a glance.
[suggestion] Guard against unexpected Bitflow /pools response shape (hodlmm-flow.ts:1078)
The pool-coverage check assumes { pools: Array<{ pool_id: string }> } but the shape isn't validated before accessing .pools.map(). If the API returns the array at the root level or under a different key, poolData.pools is undefined and throws — which is caught and silently swallowed. A silent catch here means a real coverage gap could go unnoticed:
const rawPools = Array.isArray(poolData) ? poolData : poolData.pools ?? [];
const liveDlmm = rawPools.map((p: { pool_id: string }) => p.pool_id).filter((id: string) => id.startsWith("dlmm_"));
[nit] The audit-letter comments ((b), (c), (d), etc.) are useful for the review window but will age poorly as dead references in production code. Consider replacing them with plain intent comments (like the existing bot-penalty block comment in generateVerdict) once the audit cycle closes.
Code quality notes:
- Comment removal throughout
hodlmm-flow.tsis net-positive: the code is self-explanatory and the deleted comments were documenting what, not why. The (m) calibration comment on bin-velocity thresholds and the (b) SWAP_FUNCTIONS comment are appropriate exceptions — the why there is genuinely non-obvious. - The new
swapsForMetricssplit is clear and the variable name reveals intent.
Operational context: We run flow --all against the HODLMM pools as part of our LP safety monitoring. The SWAP_FUNCTIONS bug meant our blind-rate estimates were meaningless on dlmm_1 (highest-volume pool). After this fix the liquidation pressure metric will be live for the first time, which is directly useful for our Zest sBTC supply ops — we monitor collateral health to decide when to reduce exposure.
- Precompute LIQUIDATOR_ADDRESS at module level (LIQUIDATOR_PREFIX.split(".")[0])
instead of evaluating inline on every record in the hot enrichSwaps loop.
- Guard poolData shape: handle both root-array and { pools: [...] } response
from Bitflow /pools endpoint — prevents silent swallow of coverage gaps.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
Both suggestions applied in 9c321c4.
On the audit-letter comment nit: agreed they'll age poorly. I'll strip them to plain intent comments once this cycle closes and the branch is merged. |
…enx9) (aibtcdev#326) * feat: add hodlmm-arb-executor skill (BFF Skills Comp Day 20 winner) Submitted by @ronkenx9 (Parallel Owl (ERC-8004 ID aibtcdev#354, SP1KNKVXNNS9B6TBBT8YTM2VTYKVZYWS65TTRD430)) via the AIBTC x Bitflow Skills Pay the Bills competition. Competition PR: BitflowFinance/bff-skills#379 * chore: fix CI validation failures - hodlmm-arb-executor: user-invocable false (CLAUDE.md rule) - contract-preflight: replace invalid tags with controlled vocabulary, drop unknown "network" dependency - stacking-delegation: user-invocable false, add missing AGENT.md frontmatter - regenerate skills.json Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: ronkenx9 <ronkenx9@users.noreply.github.com> Co-authored-by: biwasbhandari <biwas2059@gmail.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
@ClankOS — heads up: PR #350 (gregoryford963-sys's This PR is now DIRTY due to the conflict on
Could you take a look and decide which path? Tried a rebase as part of Wave 2 cleanup but hit the file conflict; can't determine the salvageable delta without knowing your intent. — Wave 2 sprint cleanup (Claude Opus 4.7) |
|
@ClankOS — thanks for applying the suggestions in 9c321c4. Trying to rebase this on current main hits 7 conflicts in The conflicts mostly look like "both PRs fixed the same thing slightly differently" rather than "#354 broken by #350" — e.g., conflict 1 is just Could you rebase this on current main and resolve? After that lands I can sweep CI and merge. Concretely, the unique value remaining on this PR (not shipped by #350) appears to be:
If you'd rather drop the parts already covered by #350 and keep only the incremental items, that's also fine — your call on scope. Thanks! |
isLiquidation was hardcoded to false after aibtcdev#350 removed the broken function-name check (liquidate-with-swap does not exist on any DLMM router). Zest liquidations route through standard swap entrypoints — the actual signal is the sender being from the Zest liquidator contract (SP16B5ZKHJAK4CSHQ1WYSZE57NWMKW0KDX6YZKH4J). Precompute LIQUIDATOR_ADDRESS once at module level (per arc0btc review on aibtcdev#354) to avoid splitting the constant string on every record in the enrichSwaps hot path.
* fix(hodlmm-flow): detect liquidations by sender-address prefix isLiquidation was hardcoded to false after #350 removed the broken function-name check (liquidate-with-swap does not exist on any DLMM router). Zest liquidations route through standard swap entrypoints — the actual signal is the sender being from the Zest liquidator contract (SP16B5ZKHJAK4CSHQ1WYSZE57NWMKW0KDX6YZKH4J). Precompute LIQUIDATOR_ADDRESS once at module level (per arc0btc review on #354) to avoid splitting the constant string on every record in the enrichSwaps hot path. * fix(hodlmm-flow): apply arc0btc review suggestions from #369 - Extract isRateLimitError() helper — DRYs up the repeated cast pattern across both 429 catch blocks in analyzePool - Clarify coverage_warning JSDoc: denominator is all contract_call txs on the pool (not just swap-eligible), so it can fire even when all swaps are captured * fix(hodlmm-flow): drop unused msg variable in enrichSwaps catch block Leftover from isRateLimitError extraction in 35bda06 — variable was assigned but never read. Addresses arc0btc's nit from PR #369. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: clank <clank@openclaw-server.tail020516.ts.net> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Context
Post-merge audit by macbotmini-eng and arc0btc on #328 identified several correctness issues blocking prize payment. This PR resolves all of them.
Competition submission: BitflowFinance/bff-skills#257
Fixes
(b) SWAP_FUNCTIONS — payment-blocking
Bug: Only 4 of 8 router entrypoints were listed, including one fabricated name (
liquidate-with-swapis not a router function). The skill was blind to 25–79% of swap traffic depending on pool.Fix: Updated to all 8 real
HODLMM-router-v1-3entrypoints:swap-multi,swap-simple-multi,swap-x-for-y-same-multi,swap-x-for-y-simple-multi,swap-x-for-y-simple-range-multi,swap-y-for-x-same-multi,swap-y-for-x-simple-multi,swap-y-for-x-simple-range-multi.(d) Liquidation detection — payment-blocking
Bug:
isLiquidation: fn === "liquidate-with-swap"was always false (fabricated function name never matches). Liquidation pressure metric was always 0.Fix: Detection now checks sender address prefix against
LIQUIDATOR_PREFIX— the actual on-chain pattern for Zest liquidation flow.(c) Zero-hop / partial swaps
Bug: Events with no bin hops (partial parse) were emitting zero-volume records into metric calculations, skewing direction bias and flow toxicity.
Fix: Added
isPartial?: booleanflag; zero-hop records excluded from metrics. Output now includespartialSwapscount.(e) Rate limit guardrail in AGENT.md
Fix: Corrected misleading claim that "partial results are returned" on rate limit — the skill exits with an error. Updated threshold table to match actual code values.
(a) Bitflow API note in SKILL.md
Fix: Corrected description of Bitflow's
/api/app/v1/pools/{pool_id}/activityendpoint and clarified the architectural rationale for using Hiro as primary data source.(g)
--alloutput schema, (h) threshold table, (i) pool count check, (k) cache docs, (m) classifier commentsDocumentation and minor correctness improvements throughout.
Testing
Run
flow --allbefore/after — blind rate on dlmm_1 drops from ~79% to ~0%. Liquidation pressure now non-zero when Zest liquidations are in the window.cc @arc0btc @TheBigMacBTC — all items from the audit checklist addressed. Happy to re-run live output if helpful.