feat(midnight-liquidation): gate plans on incentive headroom - #182
Merged
haydenshively merged 10 commits intoAug 28, 2026
Merged
Conversation
haydenshively
force-pushed
the
feat/bots-35-headroom-gate
branch
from
August 27, 2026 17:35
d72a413 to
959d0ef
Compare
cashd
approved these changes
Aug 27, 2026
haydenshively
marked this pull request as ready for review
August 28, 2026 02:00
There was a problem hiding this comment.
Devin Review found 1 potential issue.
1 flag not posted on this PR by your GitHub settings — view it in Devin Review. (Configure)
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 1093e2f794
ℹ️ 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".
spennyp
approved these changes
Aug 28, 2026
haydenshively
force-pushed
the
feat/bots-35-headroom-gate
branch
from
August 28, 2026 05:52
1093e2f to
75f0b2a
Compare
A seize-exact plan's entire margin is the liquidation incentive `(lif - 1)/lif`, which post-maturity ramps from zero over an hour. Below that, no swap route can fund the repay: the shortfall surfaces either as the router's min-out revert or as Midnight's repay pull failing with `ERC20: transfer amount exceeds allowance`. On the 31 Jul cbBTC/USDC maturity, 824 plans produced 81 quotes and 81 simulation reverts, 1:1 — every quote spent before the incentive crossed our execution cost bought a guaranteed revert. Skip those plans in sizing, where the arithmetic already lives: - `LiquidationPlan` carries the LIF it was sized at and the repay the contract will ceil-derive from it. Neither is recoverable from `postMaturityMode` alone, because the matured-and-unhealthy branch chooses a mode by surplus. `planSurplus` reads both instead of recomputing `lifAt`, so it can no longer disagree with the plan. - `HEADROOM_FLOOR_BPS` (default 3) is a LOWER BOUND on execution cost, not a typical-cost estimate. The gate suppresses until `headroom(t) >= floor`, making it a pure time gate — 3 bps hides roughly the first 25s on a 4.4%-maxLif tier. Higher values blind the earliest, most contested part of a maturity, which costs far more than a wasted quote. - The floor reads the CHOSEN plan's LIF, downstream of mode selection. Normal mode pays the full `maxLif` with no ramp, so a floor derived from a ramping post-maturity LIF would reject matured-and-unhealthy positions the chain funds immediately. Covered by regression tests in both the sizing and tick suites. - `insufficient_headroom` rides `plan.skipped` at `debug`: headroom is scale-invariant, so it fires identically for every candidate in a `(maturity, maxLif, mode)` group. One line per position per block is what buried the post-mortem. Because a sizing skip records neither backoff nor cooldown, a gated position is re-planned every block — I/O-free, and `planSkipped` will be correspondingly large. No current tier can sit permanently under the floor: the ceilings are 420/255/60 bps against 3. `isBadDebtRealization` now takes only the two amounts it reads, so a caller holding wire-verified params need not synthesize a plan. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`liquidate` ends by pulling its own re-derived `repaidUnits` from the payer, while the Executor's callback approves only its live balance. A route returning less than that repay therefore reverts as `ERC20: transfer amount exceeds allowance` — a balance shortfall wearing an allowance error's clothes, with no amounts attached. On the 31 Jul cbBTC/USDC maturity that string was emitted 131 times and the approval encoding was suspected for a month. Compare the quote against `plan.impliedRepaidUnits` before simulating, and skip with `quote.unprofitable` carrying requiredRepay / achievableOut / shortfallBps. Break-even is read off the plan rather than recomputed: the matured-and-unhealthy branch picks a mode by surplus, so the LIF a plan was sized at is not recoverable from `postMaturityMode` or from chain time, and recomputing it would overstate the repay for a normal-mode plan. Deliberately no backoff and no cooldown on this exit. Both sides of the comparison move on a ten-second scale — the ramp lifts break-even while route cost is itself volatile — so the outcome says almost nothing about the next attempt. Quote volume is bounded by the pre-quote headroom gate instead. `MIN_SURPLUS_BPS` defaults to 0, i.e. pure break-even: both sides then come from the contract's own formula with no tuned value, so the gate can only reject plans that would have reverted on-chain. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The tick works candidates serially — one quote and one simulation each — in whatever order discovery returned them, which is ascending checksummed address order. At the 31 Jul cbBTC/USDC maturity the largest position sat fifth of fourteen, behind three worth under $1.10 combined, in an auction where the first mover takes the whole position. Split the tick in two. Phase A is pure: the lens batch is already in memory, so building plans and scoring them costs nothing and ordering the result is free. Phase B runs the expensive serial stages — cooldown, quote, simulate, submit — over that order. `plan.built` moves into phase B and gains `rank`, `surplus` and `surplusUsd`, emitted per candidate so the timestamp sequence remains the record of what was worked and when; that sequence is how this maturity was reconstructed at all. The sort key is oracle surplus converted to USD. Loan units alone are not comparable across markets — an 18-decimal loan token would systematically starve a 6-decimal USDC market — so `discovery/token-prices.ts` keeps a snapshot of loan-token prices from the markets tokens endpoint, whose typed client is already generated and already consumed by borrower discovery. Ranking only, never gating, which is what makes an indexed price acceptable: - `usdValueOf` is synchronous and reads the snapshot, so no candidate waits on HTTP before being planned. Awaiting a price mid-burst would add exactly the latency this ordering exists to remove. - An unpriced token sorts last rather than as zero — absent price means unrankable, not worthless. The sort is stable, so a total outage degrades to the previous discovery order rather than to an untested fallback. - The snapshot has no max-age ceiling, unlike the listed-markets whitelist: a stale whitelist can keep a delisted market in scope, whereas a stale price only misorders work. It fails open and reports its age instead. - Prices refresh on their own timer. `fetchWithRetry` is a 5s deadline times three retries, so folding this into the whitelist loop could stall a fail-closed safety refresh for ~22s on behalf of a cosmetic one. USD figures use the 1e8 scale the Blue profitability-gate design settled on for this same endpoint, so the two can converge without a rescale. Conversion goes through `parseUnits` on fixed-notation text, so no float arithmetic survives the API boundary and a price with no usable 1e8 precision reads as unpriced rather than as zero. `planSurplus` is exported and reads the plan's own recorded `impliedRepaidUnits`, so it cannot disagree with the LIF the plan was sized at. Its docstring states what it is not: oracle-only, excluding DEX cost and gas, and structurally positive for every post-maturity candidate — a ranking key, not a profitability measure. `unpriced` joins TickCounters as an attribute rather than a loop exit, so it deliberately participates in none of the sum identities: an unpriced candidate is still worked, just ordered last. A persistently high value means the snapshot is not covering the loan tokens actually being liquidated. Ordering is necessary but not sufficient. Early in the post-maturity LIF ramp every candidate is uniformly unviable regardless of size, so ordering only decides anything once more than one candidate clears a viability threshold. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A liquidation's entire margin is the protocol's liquidation incentive, so
break-even — not a percentage — is the economically correct min-out floor.
`SLIPPAGE_BPS` at its 100 default is wrong in BOTH directions, and which
one depends on where the incentive sits, so no single value is right:
midnight cbBTC/USDC, maxLif 1.043841, oracle value 10_000
t incentive break-even minOut @1% the fixed guard is
+123s 15.0bp 9985.04 9883.17 too loose
+600s 72.5bp 9927.46 9883.17 too loose
+1800s 214.5bp 9785.50 9883.17 too tight
+3600s 420.0bp 9580.00 9883.17 too tight
Below break-even the router accepts a route that cannot settle, and the
shortfall surfaces at the repay as `ERC20: transfer amount exceeds
allowance` — which reads as an approval bug and is not one. Above
break-even the router rejects fills that would have settled profitably.
`QuoteParameters`/`QuoteRequest` gain an optional `minAcceptableAmountOut`.
When set, the allowance is derived from it instead of from `slippageBps`.
It is expressed as a percentage of the oracle reference rather than as an
absolute because the aggregators' only lever IS a slippage parameter —
they bake the min-out themselves from it. Uniswap derives its min-out as
`reference · (1 - slippage)`, so the same percentage lands it on the
absolute floor exactly, and no venue needed changing.
An aggregator lands slightly below the floor, by its quote's shortfall
against the oracle (the execution cost). That is the safe direction: it can
never reject a fill that would have settled.
Wired in both liquidators, and omitting the field reproduces today's
behavior exactly:
- midnight reads `plan.impliedRepaidUnits` straight off the plan. The
matured-and-unhealthy branch picks a mode by surplus, so the LIF is
recoverable from neither `postMaturityMode` nor chain time.
- blue derives it at the seam from `lifFromLltv(lltv)`. Its LIF is a pure
function of the market — no maturity ramp, no mode choice — so there is
nothing about it a plan could know that the market does not.
This also makes the knob monotone. Raising `SLIPPAGE_BPS` previously
loosened the router's protection and pushed shortfalls onto the repay; an
economic floor cannot be widened below break-even.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Both consumers of `@repo/swaps` supply an economic min-out floor, and the package is private/unpublished, so nothing could reach the operator percentage any more. Deleting it rather than leaving a knob that silently does nothing: - `QuoteRequest.minAcceptableAmountOut` is required, and the derivation is unconditional. - `composeMultiVenueQuoting` no longer takes `slippageBps`, and the entries it synthesizes no longer carry one. Venues already read `params.slippageBps` — the derived value — never the entry's, so `entry.slippageBps` was vestigial before this too. - `SLIPPAGE_BPS` and `VenueConfig.slippageBps` are gone from both bots. - `parseSwapConfig` keeps accepting `slippageBps`, now optional: the seed script reads only `venue`/`router`/`fee`, so existing operator JSON still parses. Prod's stale `SWAP_CONFIG_PATH` is likewise inert. Leaving the env var unread rather than rejected is deliberate — a deployment that still sets it must keep starting, since an unknown env var is not a misconfiguration. Both bots have a test pinning that. `minAcceptableAmountOut` also comes off `QuoteParameters`: no venue reads it, only the quoting layer that derives the percentage from it, so it has no business on the venue-facing type. `slippageBps` stays there as the derived value a venue consumes — which is why the fork suites, which call `quoteUniswapV3` directly with their own percentage, are untouched. A ceiling would have been the wrong repair. Capping the allowance below the incentive reintroduces the too-tight failure late in the ramp, which is exactly what the previous commit fixed. `PENDLE_SLIPPAGE_BPS` is unrelated and unaffected — it bounds the PT → underlying unwrap hop, not a venue's min-out. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
An external review (gpt-5.6-sol) found three defects in the preceding commits. All three verified; all three were places a claim was made and the code did not deliver it. **1. The aggregator min-out sat below break-even — the whole point of the floor.** The percentage was derived against the oracle reference `R`, but aggregators apply it to their own quote `Q < R`, landing their min-out at `Q·F/R`. With `R=10000, F=9580, Q=9700` a 420bps request floors at 9292: a fill of 9342 clears the router and then reverts at the protocol's repay pull, which is the exact failure the floor exists to prevent. Calling that "the safe direction" conflated safety-against-false-rejection with an economic floor. `firmQuoteVenue` now re-derives against the venue's own quoted output when the first pass comes back short, so the floor lands where it belongs. One extra call, taken only when needed and only for the already-chosen venue; Uniswap applies the percentage to the reference itself and never takes the branch. A failed re-quote keeps the first quote rather than losing the position — it is still executable, just with a looser on-chain floor, and simulation plus the pre-broadcast check both still stand in front of it. The old tests asserted only the percentage sent, never the resulting min-out. That is why this escaped them, so the new ones assert the min-out. **2. Blue's break-even understated what Blue pulls.** It reused `expectedLoanOut`, which floors, where Blue ceils, and skipped the shares round-trip entirely. Blue settles the repay in shares — `toSharesUp` then `toAssetsUp` — and both conversions round up, so the estimate was short and the floor derived from it did not protect the repay. `LiquidationPlan` now carries `impliedRepaidAssets`, mirroring the contract chain step for step. Checked against `contractRepaidShares`, an independent reimplementation already in the test file for the underflow sweep, so the assertion is not circular. **3. Headroom was not scale-invariant, as claimed.** `seizedValueOf` floors while `impliedRepaidUnits` double-ceils, so the amount-wise ratio was neither exact nor monotone in size: at one LIF, 167 units reported 0bps where 168 reported 59, and 1000 units reported 50. It is now derived from `lif` directly — `(lif - WAD)·BPS/lif` — which is exactly scale-invariant by construction, since no amount enters it. Also from the review: `quote.unprofitable` measured its shortfall against break-even while viability compared against break-even plus the configured surplus, so a rejected route could report a NEGATIVE shortfall and omit the bar that rejected it. It now reports `requiredThreshold` and `minSurplusBps` and measures against the threshold. `plan.skipped/insufficient_headroom` gained the floor, `maxLif` and seconds-since-maturity; the realized headroom is deliberately absent, since a skip discards the plan and the chosen mode's LIF is not recoverable. `expectedLoanOut` narrows to `Pick<LiquidationPlan, 'seizedAssets'>` in both bots — it reads only that, and requiring the derived fields would force callers to synthesize them. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Second review round found the retry did not actually guarantee the floor.
Three ways it could still ship a quote whose encoded minimum sits under
break-even:
- The retry derives its percentage from the FIRST quote's output, so a
second quote that comes back lower lands under the floor again. Floor
9580, first quote 9700 -> ask 123bps, second quote 9600 -> minimum 9481,
which is 99 units short. The previous test held both responses at 9700,
so it never exercised drift.
- A failed retry knowingly kept the first, under-floor quote.
- 1inch returns opaque calldata and we RECONSTRUCT its minimum
arithmetically, so comparing that value against the floor compares our
arithmetic with itself and always agrees.
The retry is now only an attempt to satisfy a postcondition, and the
postcondition is the guarantee: after the final response, a quote whose
encoded minimum is not known to clear break-even is refused, and the caller
falls through to the next venue with `quote.floor_unmet`. Refusing is
correct — a floor that does not protect the repay is the original defect,
and simulation does not restore the invariant, since it proves one state
rather than preventing a later fill between the router minimum and the
protocol pull.
`Swap` gains `minOutSource: 'venue' | 'derived'` so this is decidable at
all. uniswap-v3 encodes the calldata here; 0x, LiFi and LiquidSwap report
their own minimum; 1inch does not, so it can never satisfy an enforced
floor until its `minReturn` parameter is wired. It is not enabled in prod
(no ONEINCH_API_KEY) and it still falls through rather than failing the
tick, but an operator enabling it will see every attempt refused with
`minOutSource: 'derived'` in the event.
Also: the claim that a skip's realized headroom was unrecoverable was
wrong. `gateOnHeadroom` holds the chosen plan when it rejects it; the value
was being discarded by `PlanOutcome`'s shape, not lost. The skip now
carries `{ bps, lif, postMaturityMode }`, and `plan.skipped` logs all
three. `maxLif` plus chain time cannot substitute: a matured-and-unhealthy
position may be sized in either mode, so neither identifies the LIF that
was applied.
Test fixtures across three packages had break-even set at or above their
stubs' reported minimums, which the postcondition correctly refuses. They
now sit below it, so those cases exercise what they are about — lens
projection and venue routing — rather than the floor. The 0x stub in the
floor tests now applies the requested slippage to its own quote, which is
what the real API does and what a fixed-minimum stub could not model.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Third review round. Both holes let a quote through whose encoded minimum sits under break-even. **The postcondition was checking a weakened floor.** A floor above the oracle reference was clamped, and the check then used the clamped value — so a request for 1001 against a reference of 1000 could return a swap whose minimum was 1000, and a test preserved that. The clamp exists only because a percentage cannot express a floor above its own denominator (at `lif == WAD` the double-ceil can land a unit over the floored reference); it is a bound on what we can ASK for, never on what we accept. The postcondition now uses the requested `minAcceptableAmountOut`, so that case is refused rather than silently downgraded. **The unwrap-only path skipped the floor entirely.** A chain ending in the loan token returns before any venue is quoted, and only route quality was checked — an unrelated threshold. Reference 1000, route-quality floor 950, unwrap worst case 970, break-even 990: passed, with a bound 20 units short. Live for Blue in particular, which has no pre-simulation profitability gate. It now enforces the same floor and logs `quote.floor_unmet`. **1inch now asks for an absolute `minReturn`** rather than a percentage the API applies to its own quote, so its bound can be reported instead of reconstructed, and it stops being permanently unusable under a floor. Two reviews independently cited the v6.1 docs for `minReturn` being an absolute base-unit minimum. If the parameter is rejected the quote fails and falls through to the next venue, which is the same outcome as before, so the downside is bounded. That required restoring `minAcceptableAmountOut` on `QuoteParameters`, which an earlier commit removed on the argument that no venue reads it. A venue that takes an absolute minimum does; venues that take a percentage still get `slippageBps` derived from it. Also: the retry is skipped for a `derived` minimum, which could never clear the floor however it was asked for — it was costing a second API call guaranteed not to help. `clearsFloor` is exported and unit-tested on both branches, because no shipped venue reports a `derived` minimum any more and the rule would otherwise be untested. Two new tests pin each bot's projection of plan break-even into the slippage it asks for; both fail if the adapter stops threading it, which the loosened fixtures no longer prove on their own. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Both doc blocks still described the pre-minReturn shape: one said the on-chain min-out is an oracle-derived floor recorded for observability, the other that a percentage is all any aggregator accepts and so the absolute floor never reaches a venue. The adapter now asks for an absolute minimum and reports the router's own bound, which is what makes it usable under an enforced floor.
Review round 5. A venue whose guaranteed output missed break-even collapsed into `bad_route`, so the tick recorded backoff and cooldown on what the design calls an economic skip — sampling the LIF ramp exponentially and potentially skipping the block where a position first becomes fundable. - `QuoteFailureReason` gains `floor_unmet`, split from the transport/route failures it is not; midnight counts it as `quoteUnprofitable` and retries the next block. The per-venue log drops to `info`, since every venue misses the floor for every candidate through the early ramp. Blue keeps today's backoff: its incentive is static, so a miss there does carry information. - Token prices derive their base URL from the *candidates* path, not the tokens path — the old comparison could never match, so a gateway prefix silently collapsed to the origin and left every candidate unpriced. - The boot price fetch moves into its refresh loop: a hanging tokens endpoint delayed the first tick by ~22s on a source that is ranking-only and fail-open. - README: document `MIN_SURPLUS_BPS`, including that it gates the expected output and not the min-out encoded in calldata, and restore the venue section's truncated sentence. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
haydenshively
force-pushed
the
feat/bots-35-headroom-gate
branch
from
August 28, 2026 13:31
1d3fdeb to
1b6f8c7
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this does
Two related guards, both aimed at the same failure: the bot broadcasting a liquidation whose economics
cannot close.
quote is spent on it.
break-even repay, and refuse the quote if it cannot.
Together they replace the operator-set
SLIPPAGE_BPS— a fixed percentage with no relationship to agiven liquidation's economics — with a floor derived from the position itself.
Why
A liquidation's entire margin is its incentive:
headroom = (lif - 1) / lif. On Midnight aftermaturity that ramps from 0 over an hour, so early on the margin is a few basis points. A swap quoted
with a flat 1% slippage allowance can legally return 1% less than quoted, which is an order of
magnitude more than the whole incentive. The bot pays gas to broadcast a transaction that then reverts
at the repay transfer, or lands at a loss.
The reverts seen in production (
Error(return too low), and the allowance revert investigated inBOTS-35) are the same shape: a min-out baked at head N that no longer clears when
eth_estimateGasre-runs at head N+k.The min-out floor is set equal to the incentive
Rather than picking a slippage number, the floor is the break-even repay the contract will derive
for the plan. If no venue can guarantee it, that is the correct answer — the position is not yet
profitable, and the bot should wait for more incentive or a price move rather than allow the swap to
eat the margin. This is the design decision that drove the rest of the PR.
Commits
959d0efeLiquidationPlan.lif/impliedRepaidUnits,insufficient_headroomskip reason, headroom diagnostics on the skip pathc43331b3749ca77f130308aeQuoteRequest.minAcceptableAmountOut; the venue slippage percentage is derived from it78529c88SLIPPAGE_BPS— unreachable once the floor is economice84047felif49a19571floor_unmet), not just a retry;Swap.minOutSourceso a reconstructed minimum can never satisfy a floor208c545aunwrapOnlyPlanenforces it too; 1inch asks for an absoluteminReturn1093e2f7minReturn1d3fdeb0floor_unmetbecomes an economic outcome that does not back off; token prices keep a gateway path prefix; the boot price fetch leaves the startup path;MIN_SURPLUS_BPSdocumentedKey design points
The floor is a postcondition, not an attempt. Asking a venue for a tighter percentage and hoping
is not a guarantee — a second quote can drift lower and still land under the floor. Every path out of
firmQuoteVenuenow ends inswap.amountOutMinimum >= minAcceptableAmountOutor a refusal.Swap.minOutSource: 'venue' | 'derived'. A minimum the bot reconstructed from its own arithmeticcannot be checked against the bot's own floor — that compares a value with itself. Only a
venue-reported bound can satisfy the floor. This forces any future adapter to declare which it has.
A floor miss is an economic verdict, not a failure. Every venue missing break-even is the normal
state of the early LIF ramp, so
floor_unmetis its ownQuoteFailureReason: Midnight counts it asquoteUnprofitableand retries the position on the next block, rather than recording the exponentialbackoff that would sample the ramp at t+73s and t+137s and miss the block where it first becomes
fundable. Blue keeps today's backoff — its incentive is static, so a miss there does carry information.
The encoded min-out stays at break-even.
MIN_SURPLUS_BPSgates the expected output before asimulation is spent; it deliberately does not tighten the minimum baked into calldata. Raising that
minimum would buy margin against oracle drift between simulation and inclusion, but the size of that
buffer is exactly what the per-maturity basis readout (BOTS-35 item 1) exists to determine, and this PR
does not pick a number it cannot justify. The residual is a quote-age exposure, now documented in the
bot's README rather than papered over.
The percentage clamp is arithmetic only. A percentage cannot express a floor above its own
denominator, so
askableFloorclamps what we ask for. It never lowers what we accept.Blue is wired too.
impliedRepaidAssetsmirrors Blue's contract chain —mulDivUpthe quotedvalue,
wDivUpby lif, then thetoSharesUp/toAssetsUpround-trip, since Blue repays in shares.Blue has no pre-simulation profitability gate, so the floor matters more there.
Verification
pnpm lint,@repo/swaps/ midnight / blue typechecks: 0 (checked by exit code —grep "error TS"silently matches nothing here, because pnpm emits ANSI codes mid-token)pnpm test: 2466 passed, 12 skipped, 0 failed. The 4 failing files are pre-existing fork/e2e suites needingRPC_URL_8453.Reviewed externally over five rounds; eleven defects found and fixed, three of them in code previously
reported as fixed. Round 5 raised one more design question — the drift buffer on the encoded min-out —
which is answered above rather than coded.
Linear
Part of BOTS-35
This PR must not close BOTS-35. It satisfies two of the four acceptance criteria:
749ca77f,test/runner/ranking.test.tsOn criterion 2: the ticket attributes the revert to a just-in-time exact-amount approve racing the
simulation.
approvePairdoes not emit an exact-amount approval — it approves the Executor's livebalance (
packages/swaps/src/execution/executor-calls.ts), soallowance == balancebyconstruction and
ERC20: transfer amount exceeds allowancecan only mean the loan balance after theswap was short of the derived repay. That is a shortfall, not an approval-ordering bug, and it is what
the min-out floor in this PR addresses. Nothing was reproduced by anyone, per the ticket's own
instruction not to assume it fixed — so criterion 2 should be rewritten or struck rather than ticked.
Criterion 4 is the hard blocker: production has logged zero
tx.*and zeroplan.builtevents in14 days, so there is no maturity to verify against yet.
Referenced, not closed
Ref BOTS-87 — Blue does not yet have this PR's counters.
Ref BOTS-81 — sweeping stranded sub-cent debt. Relevant because it pulls the opposite way from a
MIN_NET_PROFIT_USDfloor: BOTS-81 wants the bot to take dust deliberately, while BOTS-35's ownout-of-scope note calls dust sweeping gas spent for nothing. Gas evidence for that trade-off is
posted on BOTS-81. No dust floor ships in this PR.
Ref CRTR-3124 — 1inch sends the Executor as
originwhere the API wants the initiating EOA. Pre-existing (b7fdeb724), needs an EOA threaded through both bots.Also still open, untracked: the LIF-rate-vs-integer-margin mismatch remains a documented inefficiency
(wasted quoting, not a loss path).