Skip to content

feat(midnight-liquidation): seed loan-as-collateral positions - #208

Open
haydenshively wants to merge 3 commits into
mainfrom
feature/bots-113-loan-as-collateral-live-test
Open

feat(midnight-liquidation): seed loan-as-collateral positions#208
haydenshively wants to merge 3 commits into
mainfrom
feature/bots-113-loan-as-collateral-live-test

Conversation

@haydenshively

@haydenshively haydenshively commented Sep 9, 2026

Copy link
Copy Markdown
Collaborator

Why

Loan-as-collateral support shipped on 2026-08-31 (6e17c519 and follow-ups, TIB-2026-08-28) but had never run against a real position — no borrower on any chain used the loan-token collateral slot, so no maturity would have exercised the swap-free path. BOTS-113 asked us to test it before the first production maturity carrying loan collateral.

Two constraints shape the tool, and neither applies to the existing seeder:

  1. A loan-as-collateral position can never become unhealthy. The identity oracle returns exactly ORACLE_PRICE_SCALE, Midnight debt is static face value with no accrual path, and take refuses to mint an unhealthy seller. isHealthy is therefore time-invariant and isLiquidatable leaves one door open: blockTimestamp > maturity. The --drawdown-bps mechanism in seed-liquidatable-positions.ts has no analogue.
  2. The market cannot be minted. The bot's whitelist is keyed on exact market id and is fail-closed, so a freshly-minted market (which is what varying rcfThreshold gets you) is invisible to it. The target has to be one that is already listed — and those markets have no order book, so the offer is signed here and taken by a second wallet.

What

  • scripts/seed-loan-collateral-position.ts — lender posts a bid, borrower supplies loan-token collateral and takes it, becoming the debtor.
  • scripts/seed/tx.tstxStep / confirmPrompt lifted out of the existing seeder now that two callers share them. No behavior change; the retry-on-read-lag loop and its rationale move verbatim.

Everything else is reused rather than rewritten: toId / hashOffer / signOfferTree / encodeRatifierData / isLeaf from scripts/seed/offers.ts, priceToTick from scripts/seed/price-tick.ts, mulDivUp / mulDivDown from src/sizing/math.ts, and readMidnightLiquidationLens for verification.

⚠️ Funds at risk

This signs an offer and opens a real debt position with real USDC on live Base. Per CLAUDE.md this needs a review pass before merge.

It fails closed before it spends anything:

  • market must be in the bot's whitelist — checked with the bot's own createListedMarketFilter against --markets-api (the target bot's MARKETS_API_URL), so exact market-id + chain match, no bespoke parsing
  • loan token must be Base USDC (--face-usdc / --max-spend-usdc assume 6 decimals)
  • toId(toMarket(id)) must round-trip, proving the local IdLib port still matches this deployment before anything is signed against that struct
  • enterGate and liquidatorGate must both be zero; maturity must be at least MIN_SECONDS_TO_MATURITY (600s) away so every tx lands before take
  • the borrower must have no debt and no activated slot in the market yet
  • the loan-collateral slot is derived, not assumed — it is index 1 in some listed markets
  • its oracle must read exactly ORACLE_PRICE_SCALE
  • the maker's offer group (the market id) must be unused (consumed == 0), since consumed[maker][group] accumulates across takes
  • the projected post-maturity seize must be non-zero — this is the skip that silently strands every existing staging fixture, so it is a loud pre-flight check rather than a post-maturity surprise
  • --markets-api is required, not defaulted: that endpoint decides which deployment's bot will act on
    the position, and the bot itself takes it from an operator-set variable rather than from the repo, so
    defaulting it would pick an environment on the operator's behalf for a run that spends real funds
  • --max-spend-usdc bounds total outlay; --dry-run does the whole read path and every assertion above and sends nothing

Verification then goes through the bot's own lens, not a bespoke read, asserting valid && hasDebt && healthy && gateAllows && !locked that only the loan-collateral slot is activated, and that debt and collateral amt equal exactly what was sized — so the planner yields exactly one candidate and swapFree is the only shape under test.

Evidence — it worked in production

Seeded market 0xa1312ab8… on Base (staging-listed, maturity 2026-09-09 15:00 UTC). staging-bot-8453 liquidated it 3 seconds after maturity, across three swap-free liquidations:

Time Seized Venue Tx Gas
15:00:03 697,901 no-swap 0x8b449f77… 177,052
15:00:47 2,097 no-swap 0x40efe773… 149,540
15:01:31 6 no-swap 0x97dc9a4b… 155,303

plan.built carried collateralIndex: 0, postMaturityMode: true, routeCostBps: 0; quote.ok carried venue: "no-swap" with expected == oracle == amountOutMinimum (the zero-step swap plan). On-chain: debt 700000 → 1, collateral 750001 → 49997. 700,004 seized against 699,999 repaid.

This is also the first transaction staging has ever broadcast.

Known, not addressed here

One unit of debt is stranded after a successful post-maturity liquidation: capBoundPlan shaves the cap by SEIZE_CAP_MARGIN_BPS before sizing, and mulDivDown(1, 9970, 10000) == 0, so a 1-unit debt is permanently unsizeable on Base at any price scale. Post-maturity repays are ceil(seized / lif), and that ceiling is what lands on 1 rather than 0. Filed as BOTS-116 together with the staging fixture sizing gap and the resulting plan.skipped log spam (~35k lines/47min from ~25 stuck positions).

Checks

typecheck, pnpm lint, pnpm knip, pnpm format clean; 326 midnight-liquidation tests pass. No new unit test: the script is an operator tool whose logic is the fail-closed assertions against live chain state, and it has now been exercised end-to-end in production. The cryptography it depends on is already covered by test/seed/offers.test.ts.

Refs BOTS-113

🤖 Generated with Claude Code

Link to Devin session: https://app.devin.ai/sessions/84fec01ca7ea4d4ca85e9f47fa2a2e3a
Open in Devin Desktop: https://app.devin.ai/desktop/session/84fec01ca7ea4d4ca85e9f47fa2a2e3a?variant=devin
Requested by: @haydenshively

A loan-as-collateral slot is priced by the identity oracle at exactly
ORACLE_PRICE_SCALE and Midnight debt does not accrue, so `isHealthy` is
time-invariant and the existing seeder's price-drawdown trigger has no
analogue: maturity is the only way such a position becomes liquidatable.
The market therefore cannot be minted either, because the bot's whitelist
is keyed on exact market id and is fail-closed — it has to be one that is
already listed, and these markets have no order book, so the offer is
signed here and taken by a second wallet.

Adds a sibling seeder for that shape and lifts `txStep`/`confirmPrompt`
into scripts/seed/tx.ts now that two callers share them.

Fail-closed before it spends: the market must be listed for the chain,
`toId(toMarket(id))` must round-trip, both gates must be zero, maturity
must be in the future, the loan-collateral slot is derived rather than
assumed (it is index 1 in some listed markets), its oracle must read
exactly ORACLE_PRICE_SCALE, and the maker's offer group must be unused.
It then projects the post-maturity seize and refuses to run when that
rounds to zero, which is the skip that silently strands every existing
staging fixture.

Verified live on Base: seeded market 0xa1312ab8… and staging-bot-8453
liquidated it 3s after maturity across three swap-free liquidations
(venue "no-swap", routeCostBps 0, zero-step swap plan), 700004 seized
against 699999 repaid. One unit of debt is stranded afterwards by the
seize-cap margin shave — filed as BOTS-116, not addressed here.

Refs BOTS-113

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@linear-code

linear-code Bot commented Sep 9, 2026

Copy link
Copy Markdown

BOTS-113

@haydenshively haydenshively self-assigned this Sep 9, 2026
The markets endpoint decides which deployment's bot will act on the
seeded position, and the bot itself takes it from an operator-set
variable rather than from the repo. Defaulting it picked an environment
on the operator's behalf for a run that spends real funds, and baked a
non-public hostname into the tree in the process.

Require --markets-api and validate it as a URL, matching how the rest of
the script treats ambiguous config.

Refs BOTS-113

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@haydenshively
haydenshively marked this pull request as ready for review September 10, 2026 03:26
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 10, 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-10T03:42:04.289350Z bcb0fdf Draft marked ready
🔒 Security Review Completed 2026-09-10T03:50:09.062527Z bcb0fdf 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.

@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 5 potential issues.

2 flags not posted on this PR by your GitHub settings — view them in Devin Review. (Configure)

Devin Review

Comment thread bots/midnight-liquidation/scripts/seed-loan-collateral-position.ts
Comment thread bots/midnight-liquidation/scripts/seed-loan-collateral-position.ts
Comment thread bots/midnight-liquidation/scripts/seed-loan-collateral-position.ts Outdated
Comment thread bots/midnight-liquidation/scripts/seed-loan-collateral-position.ts Outdated
Comment thread bots/midnight-liquidation/scripts/seed-loan-collateral-position.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: bcb0fdf40b

ℹ️ 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/scripts/seed/tx.ts
Comment thread bots/midnight-liquidation/scripts/seed-loan-collateral-position.ts Outdated
Comment thread bots/midnight-liquidation/scripts/seed-loan-collateral-position.ts Outdated
Comment thread bots/midnight-liquidation/scripts/seed/tx.ts
Comment thread bots/midnight-liquidation/scripts/seed-loan-collateral-position.ts Outdated
Comment thread bots/midnight-liquidation/scripts/seed-loan-collateral-position.ts
Comment thread bots/midnight-liquidation/scripts/seed-loan-collateral-position.ts
Comment thread bots/midnight-liquidation/scripts/seed-loan-collateral-position.ts
Comment thread bots/midnight-liquidation/scripts/seed-loan-collateral-position.ts Outdated
Comment thread bots/midnight-liquidation/scripts/seed-loan-collateral-position.ts
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@devin-ai-integration
devin-ai-integration Bot force-pushed the feature/bots-113-loan-as-collateral-live-test branch from ff12c7a to 9cf8cd6 Compare September 10, 2026 20:26
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