Skip to content

fix(bots): treat a reverted send as economic, not a send failure - #188

Merged
haydenshively merged 4 commits into
feat/venue-cost-curvefrom
fix/send-revert-classification
Sep 1, 2026
Merged

fix(bots): treat a reverted send as economic, not a send failure#188
haydenshively merged 4 commits into
feat/venue-cost-curvefrom
fix/send-revert-classification

Conversation

@haydenshively

@haydenshively haydenshively commented Aug 31, 2026

Copy link
Copy Markdown
Collaborator

Closes BOTS-88 and delivers BOTS-89's measurement. Stacked on #184 → the venue cost curve PR — this PR's own diff is feat/venue-cost-curve...fix/send-revert-classification.

Why

The 2026-08-28 15:00 UTC maturity was the first with #181 and #182 in production. All 13 positions cleared, but clearing took 4–9 minutes each against a competitor's 123 seconds on 31 Jul.

planned 926 = backoffSkipped 637 + quoteUnprofitable 81 + ok 167 + reverted 41

Backoff suppressed 69% of planned candidates. Of the 167 that reached simulate.ok, 153 returned sent: false. Their decoded reasons: Error(return too low) ×103, Execution reverted for an unknown reason. ×47, Error(Return amount is not enough) ×3.

I traced the emitting contracts by searching deployed Base bytecode: return too low comes from the AMM pool 0x routed through (0x71c2ed90…, Metric/Kipseli family), and Return amount is not enough from KyberSwap's MetaAggregationRouterV2 inside a LiFi route. The string is a property of whichever pool the aggregator picked, not of the venue — so a decoded-string allowlist is open-ended by construction, and 47 of 153 carry a selector our decoder doesn't know.

Mean gap between consecutive send attempts on one position was 25–31 s against BACKOFF_BASE_BLOCKS=2 — the exponential ramp fully engaged. That is the 4–9 minutes. Post-maturity LIF ramps, so a min-out shortfall says nothing about the next block.

What

  1. packages/bot-kit/src/tx-error.ts split into tx-send.error.ts (the TxSendError class alone) and revert.utils.ts (the revert utilities as arrow constants), with all importers and each bot's mirrored wrapper renamed. New revertSelector returns the 4-byte selector — what makes the 47 unclassified reverts attributable next maturity.
  2. SubmitOutcome widened to { sent: false; reason: 'send_failed'; executionRevert: boolean; selector?: Hex }, both also logged on tx.submit_failed.
  3. Midnight's tick splits the no-broadcast branch into sendRefused / sendReverted / sendRejected, with a new identity notSent === sendRefused + sendReverted + sendRejected. An execution-reverted send does not arm backoff.
  4. A per-position revert-streak store, reporting count, duration, selector and selector-constancy, escalating past 15 minutes. Report-only.

Review notes

  • The exemption is a latch, not a delete. pendingBackoff is position-level and shared across sibling candidates (feat(midnight-liquidation): support loan-as-collateral markets #184), and submittedLabels only blocks siblings after a successful send — so a lower-ranked sibling running after a reverted send re-armed backoff and silently undid the exemption. Caught in review; the two reverse orderings are now pinned by tests that fail against the pre-fix source.
  • The escalation threshold is duration, not attempt count. LIF ramps on wall-clock, so attempts-to-clear is clearing_time / sweep_period — and the sweep period is exactly what this stack shrinks. A count would need recalibrating on every latency win and would fire on healthy positions as sweeps got faster.
  • The streak is deliberately report-only — no throttle, circuit breaker or suppression on that path. Recorded in the TIB as an accepted risk with the mitigation named.
  • Blue deliberately diverges: it keeps backoff on every rejected send, because its incentive is static rather than ramping. Pinned by a test so nobody "harmonizes" the two bots later.
  • The sim/send divergence is not pending-state, as first assumed — prepareTransactionRequest passes no blockTag and experimental_blockTag is set nowhere, so both run at latest. They diverge because they are issued by different clients over separate failover transport pairs. Still a state race; the mechanism was wrong and is corrected in the code comments.

What BOTS-89's measurement actually showed

Receipts for all 14 confirmed fills give quoted−realized of p50 0.0 / mean 6.35 bps, and −0.5…+2.2 bps for the nine 0x fills. So there is no uniform realized-output bias and nothing to discount — but this rules the structural explanation out only for the 0x AMM subset that landed. The 153 rejected attempts are unobserved and the 47 unclassified reverts remain unproven. The TIB states it that way on purpose.

Verification

pnpm test 211 files / 2837 tests, 0 failed · lint 0 warnings · knip clean · typecheck across bot-kit, swaps, both liquidators, both reallocation bots and crossed-books · fork suite 3/3. Break-one-assertion on every new assertion, plus source mutation for the latch.

🤖 Generated with Claude Code

@haydenshively haydenshively changed the title fix/send revert classification fix(bots): treat a reverted send as economic, not a send failure Aug 31, 2026
@linear-code

linear-code Bot commented Aug 31, 2026

Copy link
Copy Markdown

BOTS-88

BOTS-89

@haydenshively haydenshively self-assigned this Sep 1, 2026
@haydenshively
haydenshively marked this pull request as ready for review September 1, 2026 03:51
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 1, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-01T04:03:09.111317Z adbb90b Draft marked ready
ℹ️ 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

haydenshively and others added 4 commits August 31, 2026 22:52
Split bot-kit's tx-error.ts into tx-send.error.ts (TxSendError only) and
revert.utils.ts (arrow-const revert utilities), per the utility-isolation
and one-error-class-per-file rules, and mirror the rename in each bot's
own revert formatter.

Add revertSelector, which unwraps a TxSendError and returns the revert
payload's 4-byte selector, so a revert no ABI in reach can decode is still
attributable. Widen SubmitOutcome's send_failed arm with executionRevert
and log both it and the selector on tx.submit_failed; callers can now tell
the chain declining a plan from the send machinery breaking.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Blue's liquidation incentive is static, so a shortfall on this block does
predict the next one. Document at the site that blue keeps backoff on
every rejected send — including an execution revert — and pin it with a
test so nobody harmonizes it with midnight's ramp-driven exemption.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
An execution-reverted send is the chain's economic verdict on a ramping
incentive, not a fact about the next block: post-maturity LIF ramps over an
hour, so backing off on a min-out shortfall samples that ramp exponentially and
skips the contested block where the position first becomes fundable.

Splits the no-broadcast outcome three ways (sendRefused / sendReverted /
sendRejected, summing to notSent) and, on the reverted arm, CLEARS the
position's deferred backoff rather than merely not arming it — the set is keyed
by position, so a lower-ranked sibling's quote or simulation failure has usually
armed it already, and a chain-declined send is later evidence than either. An
opted-in cooldown is deliberately left armed. backoff.clear stays gated on a
real broadcast.

Adds a per-position streak of consecutive execution-reverted sends, reported as
send.revert_streak past 15 minutes with the revert count, the last 4-byte
selector, and whether it stayed constant — the backstop that makes running with
no retry throttle defensible when a persistent estimator-only revert would
otherwise re-quote forever. The threshold is a duration because the incentive
ramps on wall clock, so attempts-to-clear shrinks every time the bot gets
faster.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`pendingBackoff.delete(label)` only held when the reverted send was the
position's last event of the tick. Unlike a broadcast, an execution
revert never enters `submittedLabels`, so the next-ranked sibling still
runs and re-arms the entry — silently restoring the suppression on the
ordinary multi-collateral position. Exempt the label instead and skip it
in the tick's `finally`, so the exemption is order-independent.
`pendingCooldown` stays armed, as decided.

Cover both reverse orderings (revert then quote failure, revert then
simulation revert); the pre-fix source fails exactly those two.

Also report `send.revert_streak` on the crossing alone. The path has no
throttle by design, so an escalated streak was shipping a warn every
tick — two on a tick where both siblings reverted — for as long as the
position stayed stuck.

Correct the state-race mechanism while here: the simulation and the send's
gas estimate both run at `latest`. They diverge because they come from
different clients over their own failover transport pairs, not because
either sees pending state.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
haydenshively added a commit that referenced this pull request Sep 1, 2026
A regression sweep over #184/#187/#188/#189 found four behaviors the stack
changed without meaning to. None are new functionality; each restores what
the bots did on main.

Detection-only blue could broadcast. The no-venues gate moved to AFTER unwrap
resolution and the swap-free branch lost its `steps.length > 0` guard, so a
venue-less deployment returned `kind: 'swap'` — which the tick takes straight
to simulate+submit. docker-compose defaults Robinhood (4663) to
ALLOW_DETECTION_ONLY with a funded key, documented as skipping every routed
liquidation. `swapFreeWithoutVenues` (default false) restores the immediate
refusal; midnight opts in, because its loan-as-collateral slots need no route
and ALLOW_BAD_DEBT_ONLY is a supported posture there.

That same reorder downgraded a transient unwrapper RPC failure from
`no_config` (skip) to `failed` (arms backoff), pushing a deliberately unarmed
deployment into a suppression state machine it never entered. Refusing before
the unwrap chain fixes both at once, and spends no reads doing it.

A send REJECTION now arms backoff even when a sibling execution-reverted.
Both sets are keyed by position, so `backoffExempt` was cancelling the backoff
a broken nonce/funds/RPC send earned; the position then re-sent every block
while the send machinery was still broken.

Phase A.5 no longer resolves routes for suppressed positions. It ran ahead of
the cooldown/backoff gates, so a backed-off position spent an uncached read
per candidate per tick — breaking backoff's contract that it bounds API and
RPC usage under a backlog, and putting that latency in front of the first
send of a maturity burst.

Each of the 8 new tests was verified to fail against the pre-fix source.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@haydenshively
haydenshively force-pushed the fix/send-revert-classification branch from b2c9b96 to adbb90b Compare September 1, 2026 03:52
@haydenshively
haydenshively marked this pull request as draft September 1, 2026 03:54

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

Copy link
Copy Markdown

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: b2c9b96795

ℹ️ 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 thread bots/midnight-liquidation/src/runner/tick.ts
Comment thread bots/midnight-liquidation/src/runner/revert-streak.ts
Comment thread bots/midnight-liquidation/src/runner/revert-streak.ts
@haydenshively
haydenshively marked this pull request as ready for review September 1, 2026 03:59

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Devin Review found 2 potential issues.

1 flag not posted on this PR by your GitHub settings — view it in Devin Review. (Configure)

Devin Review

Comment thread bots/midnight-liquidation/src/runner/tick.ts
Comment thread bots/midnight-liquidation/src/runner/revert-streak.ts

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

Copy link
Copy Markdown

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: adbb90b312

ℹ️ 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 thread bots/midnight-liquidation/src/runner/tick.ts
haydenshively added a commit that referenced this pull request Sep 1, 2026
Triaged all 20 Codex/Devin findings across #184, #187, #188, #189 and #192.

Fixed:
- the venue-less swap-free exception admitted unwrap-only plans, so an
  ALLOW_BAD_DEBT_ONLY deployment could broadcast an asset-moving liquidation
- candidate ranking trusted stale and incomplete curves that quoting refuses;
  both now share one exported `curveIsTrusted`
- a Pendle PT resolved its unwrap chain twice per tick, both hosted API calls;
  new `previewTokenOut` seam answers phase A.5 from cache
- phase A.5 resolved routes serially, in discovery order
- a sibling's `no_route` suppressed a position whose other candidate was
  `floor_unmet`, which is meant to retry every block as the LIF ramps
- the wall-clock cooldown verdict could flip mid-tick, leaving a candidate
  quoted but unpriced
- revert-streak state never expired, so a reused label reported false crossings
- `tx.submit_failed` carried no candidate discriminator
- docs: the swap-free "iff" guarantee, `selectorConstant`, two README rows

Declined, with reasoning for the threads: narrowing the backoff exemption to
post-maturity plans (a regression — the sets are per-position while the mode is
per-candidate), `stopAfterWinner`, and the sub-1e-18 rate truncation.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@haydenshively
haydenshively merged commit 7d07f26 into main Sep 1, 2026
17 checks passed
@haydenshively
haydenshively deleted the fix/send-revert-classification branch September 1, 2026 16:18
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.

2 participants