Skip to content

Commit 5b71650

Browse files
k9dreamer-graphite-elank9dreamerclaude
authored
fix(dca): validated configurable fee; fix(defi-portfolio-scanner): LTV threshold scale consistency (dormant until ltv is populated) (#410)
* fix(dca, defi-portfolio-scanner): configurable dca fee (stuck-nonce risk); fix LTV threshold scale so risk flags can fire Field audit F-14 / F-13. dca hardcoded fee 5000 uSTX with no override — underpriced fees strand the head nonce. Scanner compared raw ltv > 85/70 against a fractional [0,1] value (displayed as ltv*100), so zest-ltv-critical/warning could never trigger. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * review(arc0btc): named LTV threshold constants; document that the risk block is dormant until ltv is populated Blocking review point confirmed valid: ZestPosition.ltv is only ever constructed as null, so the liquidation flags cannot fire before OR after the threshold rescale — the rescale makes them correct-when-populated, not live. Population needs the on-chain get-user-reserve-data value scale verified against a real position and is tracked separately; this commit makes that status explicit at the definition site instead of implying the flags now work. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * review(biwasxyz): validated DCA fee parsing (+cap), align balance precheck with actual fee, document DCA_FEE_USTX; LTV framing already restated - resolveDcaFeeUstx(): strict ^\d+$ parse with labeled errors — empty env var no longer becomes BigInt('') = 0n (silent zero-fee mainnet tx, the exact stuck-nonce failure this fee prevents); decimals error loudly; 1 STX sanity cap. - STX balance precheck now reserves the same fee the tx pays (was a hardcoded +5000 vs a 50000 default — guard could pass, broadcast dips below reserve). - SKILL.md/AGENT.md document the env var. - LTV: dormancy already restated in named-constants commit; PR title/body amended to match (dormant-code consistency fix, not enablement). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: k9dreamer <k9dreamer@gmail.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent e0334ef commit 5b71650

4 files changed

Lines changed: 50 additions & 4 deletions

File tree

dca/AGENT.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,3 +156,5 @@ User: "DCA 100 STX into sBTC, 10 orders, daily"
156156
- Confirm the on-chain result (tx hash)
157157
- Update plan state file with execution log entry
158158
- Report completion with summary: order number, amount swapped, avg entry price, remaining orders
159+
160+
- `DCA_FEE_USTX` (env, optional): swap fee in micro-STX. Validated whole number, > 0, <= 1000000. Default 50000.

dca/SKILL.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,3 +200,7 @@ Three wallet sources (checked in order):
200200
Winner of AIBTC x Bitflow Skills Pay the Bills competition.
201201
Original author: @k9dreamermacmini-coder
202202
Competition PR: https://github.com/BitflowFinance/bff-skills/pull/31
203+
204+
## Fee configuration (2026-07-15 field audit F-14)
205+
206+
The swap fee is `DCA_FEE_USTX` (env), validated (`^\d+$`, must be > 0, capped at 1,000,000 µSTX = 1 STX), default **50000 µSTX** — previously a hardcoded 5000 µSTX, which is 10–50× below peer skills and an underpriced-fee stuck-nonce risk. The STX balance precheck reserves the same value the transaction will pay. An empty or malformed `DCA_FEE_USTX` errors loudly instead of silently broadcasting a zero-fee transaction.

dca/dca.ts

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,31 @@ const DCA_DIR = path.join(os.homedir(), ".aibtc", "dca");
2222
const WALLETS_FILE = path.join(os.homedir(), ".aibtc", "wallets.json");
2323
const WALLETS_DIR = path.join(os.homedir(), ".aibtc", "wallets");
2424
const STACKS_API = "https://api.hiro.so";
25+
26+
// F-14: default swap fee in micro-STX. 5000 was 10–50x below every peer skill
27+
// (deposit/withdraw 50k, zest/swap-aggregator 70k, limit-order 100k,
28+
// move-liquidity 250k); 50000 is the modal value.
29+
const DEFAULT_DCA_FEE_USTX = 50_000n;
30+
// Sanity ceiling: 1 STX. A fat-fingered DCA_FEE_USTX should error, not pay it.
31+
const MAX_DCA_FEE_USTX = 1_000_000n;
32+
33+
function resolveDcaFeeUstx(): bigint {
34+
const raw = process.env.DCA_FEE_USTX;
35+
if (raw === undefined || raw === "") return DEFAULT_DCA_FEE_USTX;
36+
// Strict integer parse: BigInt("") is 0n (a silent ZERO-FEE mainnet tx —
37+
// the exact stuck-nonce failure this fee exists to prevent) and
38+
// BigInt("0.05") throws an unlabeled SyntaxError. Validate like the
39+
// repo's parseNonNegativeBigInt convention instead.
40+
if (!/^\d+$/.test(raw)) {
41+
throw new Error(`DCA_FEE_USTX must be a whole number of micro-STX, got "${raw}"`);
42+
}
43+
const fee = BigInt(raw);
44+
if (fee === 0n) throw new Error("DCA_FEE_USTX must be > 0 (a zero-fee tx strands the head nonce)");
45+
if (fee > MAX_DCA_FEE_USTX) {
46+
throw new Error(`DCA_FEE_USTX ${raw} exceeds the ${MAX_DCA_FEE_USTX} uSTX (1 STX) sanity cap`);
47+
}
48+
return fee;
49+
}
2550
const EXPLORER_BASE = "https://explorer.hiro.so/txid";
2651

2752
const FREQUENCIES: Record<string, number> = {
@@ -373,7 +398,11 @@ async function executeDirectSwap(opts: {
373398
network,
374399
senderKey: opts.stxPrivateKey,
375400
anchorMode: AnchorMode.Any,
376-
fee: 5000n,
401+
// 2026-07-15 field audit F-14: an underpriced hardcoded fee is a
402+
// stuck-head-nonce seed (one stuck tx stalls every later tx from the
403+
// signer). Configurable via DCA_FEE_USTX (validated); default matches
404+
// the repo's modal contract-call fee.
405+
fee: resolveDcaFeeUstx(),
377406
});
378407

379408
const broadcastRes = await broadcastTransaction({ transaction: tx, network });
@@ -860,7 +889,7 @@ async function cmdRun(
860889
if (plan.tokenInSymbol.toUpperCase() === "STX") {
861890
try {
862891
const balAtomic = await getStxBalance(walletKeys.stxAddress);
863-
const neededAtomic = Number(humanToAtomic(plan.orderSizeHuman, plan.tokenInDecimals)) + 5000; // +fee
892+
const neededAtomic = Number(humanToAtomic(plan.orderSizeHuman, plan.tokenInDecimals)) + Number(resolveDcaFeeUstx()); // + the same fee the tx will pay
864893
if (balAtomic < neededAtomic) {
865894
const balHuman = atomicToHuman(balAtomic, 6);
866895
fail(

defi-portfolio-scanner/defi-portfolio-scanner.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,17 @@ const SKILL_NAME = "defi-portfolio-scanner";
99
const REQUEST_TIMEOUT = 10_000; // 10 seconds per protocol
1010
const HIRO_TIMEOUT = 15_000;
1111

12+
// Zest LTV liquidation-risk thresholds, as FRACTIONS in [0, 1] (display
13+
// multiplies by 100). NOTE (2026-07-15 field audit F-13): no scanner code path
14+
// currently populates ZestPosition.ltv with a number — both construction sites
15+
// set null — so this risk block is DORMANT until the Zest reserve-data parser
16+
// computes a real fractional LTV. The threshold scale below is corrected now so
17+
// the flags work the moment ltv is populated; population is tracked separately
18+
// (requires verifying the on-chain get-user-reserve-data value scale against a
19+
// live position).
20+
const ZEST_LTV_CRITICAL = 0.85;
21+
const ZEST_LTV_WARNING = 0.70;
22+
1223
const ENDPOINTS = {
1324
bitflowPools: "https://bff.bitflowapis.finance/api/app/v1/pools",
1425
zestContract: {
@@ -731,14 +742,14 @@ function computeRiskScore(scanData: ScanData): {
731742
// 3. Zest LTV risk
732743
for (const pos of protocols.zest.positions) {
733744
if (pos.ltv !== null) {
734-
if (pos.ltv > 85) {
745+
if (pos.ltv > ZEST_LTV_CRITICAL) {
735746
score += 30;
736747
factors.push({
737748
factor: "zest-ltv-critical",
738749
severity: "critical",
739750
detail: `Zest position ${pos.asset} has LTV ${(pos.ltv * 100).toFixed(1)}% — liquidation risk imminent.`,
740751
});
741-
} else if (pos.ltv > 70) {
752+
} else if (pos.ltv > ZEST_LTV_WARNING) {
742753
score += 15;
743754
factors.push({
744755
factor: "zest-ltv-warning",

0 commit comments

Comments
 (0)