Skip to content

[VPD-1589] Add Liquid Mesh as a hop-1 liquidation source#694

Open
Debugger022 wants to merge 26 commits into
feat/VPD-1598from
feat/VPD-1589
Open

[VPD-1589] Add Liquid Mesh as a hop-1 liquidation source#694
Debugger022 wants to merge 26 commits into
feat/VPD-1598from
feat/VPD-1589

Conversation

@Debugger022

@Debugger022 Debugger022 commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Summary

Extends the atomic bStock backstop liquidator with two independent capabilities, without changing the core repay / seize / redeem / minOut flow:

  • Liquid Mesh as a second hop-1 RFQ source alongside Native RFQ. Sources are priced per liquidation and the higher-out one wins; the contract stays source-agnostic.
  • VAI debt support — for a VAI borrow the hop-2 leg settles through the Peg Stability Module (swapStableForVAI), with calldata built locally in lib/psm.ts. No aggregator, no contract change.

The contract gains a routerSpender mapping so an aggregator whose settlement contract differs from the call target (Liquid Mesh pulls the input via a separate spender) is supported, keeping seize-then-sell atomic.

Contract

  • routerSpender mapping + setRouterSpender (owner-only, address(0) clears). _swap approves the router's configured spender, defaulting to the router itself so Native is unchanged. __gap 50 -> 49, storage-safe.
  • setRouterSpender is hardened against misconfiguration: reverts RouterNotAllowed unless the router is currently allowlisted, and SpenderNotContract for a non-zero spender with no code (the spender receives a live approval during _swap, so an EOA is always a misconfig). setRouter(router, false) also clears the configured spender, so a stale entry cannot silently reactivate on re-allowlist.
  • RouterSpenderSet event; source-agnostic natspec.

Off-chain (scripts/bstock/)

Hop-1 source registry

  • lib/sources.ts: a pluggable registry the driver is generic over. SOURCE=auto prices every source whose creds are present and takes the higher out; SOURCE=native,liquidmesh (or a single name) restricts the set. Adding a source is one adapter and no contract change.
  • lib/liquidmesh.ts: the Liquid Mesh client (Ed25519 JWT signed over the full path; /quote, and /swap with disableSimulate: true so calldata can be built for a contract that does not yet hold the input). Responses are fully typed and LM_PRIVATE_KEY_SEED is validated to decode to exactly 32 bytes with a legible error.
  • Firm vs indicative reconcile: Native's quote is firm (MM-signed, executes at exactly its amountOut), while Liquid Mesh's /quote is indicative and the built /swap order only guarantees its own floor. Each quote is tagged firm, and build() exposes the order's builtFloor; if an indicative winner's floor undercuts the best firm quote it beat, the driver executes the firm quote instead (logged as hop-1 reconcile:).
  • Order-liveness guards: a Liquid Mesh order with fewer than LM_MIN_TTL seconds (default 15) left at build time, or with an implausible millisecond-epoch expiry, is rejected before the settle tx is built rather than reverting on-chain mid-incident. Hop-1 source API calls time out instead of hanging.

VAI debt (hop-2 via the Peg Stability Module)

  • For a VAI debt the hop-2 leg is the PSM swapStableForVAI (USDT -> VAI at the oracle rate), with calldata encoded locally in lib/psm.ts. The PSM is just another allowlisted router2; the contract is unchanged.
  • The builder pre-flights the PSM and aborts legibly (use the Safe fallback) if it isPaused(), or if the mint-cap headroom (vaiMintCap - vaiMinted) cannot cover the pull — rather than shipping calldata that reverts on-chain. expectedOut comes from the real previewSwapStableForVAI (USDT prices under $1, so the swap is not 1:1). MODE=flash is rejected for VAI (there is no vVAI market to flash from).
  • Seize keys off liquidateVAICalculateSeizeTokens (VAI is $1, borrower-agnostic core incentive), matching what the gate's VAI branch computes; error messages name the actual seize function called on each branch.
  • safe-fallback.ts supports VAI as the normal approve -> liquidateBorrow batch (the VAI token is resolved via getVAIAddress()), detected before the vBNB native-debt fallback so a VAI debt is never misread as native BNB. Since it ships bStock to the CEX with no on-chain swap, it is the manual fallback exactly when the PSM hop is paused or cap-exhausted.

Seize / proceeds sizing

  • The Venus Liquidator treasury cut is sized off getEffectiveLiquidationIncentive(borrower, vBStock) for every debt type, mirroring Liquidator._splitLiquidationIncentive; the resolved cut is logged (the live BSC gate keeps 50% of the bonus). For VAI the borrower is always core-pool-locked, so effective equals core there — using effective is gate-parity and future-proofing, and the genuinely divergent case (a non-core ERC20 borrower) is exercised in the fork suite.
  • Single-hop minOut is derived from the built order's guaranteed floor, not the indicative quote, so an in-slippage Liquid Mesh fill below the quote no longer reverts InsufficientOut. This matches the existing two-hop floor sizing.
  • Safe-fallback redeem/transfer amounts are haircut by SEIZE_BUFFER (default 0.1%) so ordinary oracle price drift between batch generation and signer quorum leaves sweepable dust rather than reverting the redeem.

Driver / guards

  • atomic-liquidate.ts prices all selected sources and settles through the winner.
  • SLIPPAGE and MIN_OUT_BUFFER are validated to [0,100) (matching SEIZE_BUFFER), so a typo fails loudly before a quote is burned.
  • The getAccountLiquidity error slot is read and an oracle failure reported distinctly (no longer mislabelled "no shortfall"). The no-shortfall abort is gated behind ALLOW_NO_SHORTFALL=1, so a forced liquidation of a healthy account is supported (the contract intentionally does not pre-check liquidatability).
  • verify-lm-fork.ts: an on-demand fork check that Liquid Mesh disableSimulate calldata executes on-chain (dev-time only, never part of an incident).

Docs

  • README operator runbook: a "which script?" decision flowchart, incident rules of thumb, the source registry and reconcile behaviour, the setRouterSpender prerequisite and enforced ordering, and the env knobs (LM_MIN_TTL, SETTLE_TTL_MARGIN, ALLOW_NO_SHORTFALL, SEIZE_BUFFER, PSM_ADDR).
  • The flowchart shows the pre-flight gates (debt-type detection, shortfall / ALLOW_NO_SHORTFALL, VAI-forces-inventory), hop-2 routing by debt type (single-hop USDT, AMM for other ERC20, WBNB unwrap for native BNB, PSM for VAI), and the three fallback triggers (no hop-1 source fills, VAI PSM paused/capped, dry-run fails). native-smoke is a Native-only probe; Liquid Mesh has no standalone liveness check and is validated inside the atomic dry-run.

Tests

  • BStockLiquidatorLiquidMesh.ts: split-spender atomic execution (mocked) + spender-hardening cases (unallowlisted router, EOA spender, spender cleared on de-allowlist).
  • BStockAtomicLiquidate.ts: source-registry selection (winner, forced source, bad SOURCE), firm/indicative reconcile, LM min-TTL and ms-epoch expiry rejection, SLIPPAGE/MIN_OUT_BUFFER validation, getAccountLiquidity error code, ALLOW_NO_SHORTFALL forced path, single-hop minOut-from-floor settle, VAI PSM hop-2 (via MockPSM), and VAI flash-reject.
  • BStockSafeFallback.ts: SEIZE_BUFFER haircut + range check and the VAI debt batch shape.
  • Fork/BStockLiquidatorFork.ts: Liquid Mesh split-spender against the real gate + market; two-hop and script-driven VAI liquidations through the real gate and real PSM; paused-PSM refusal; a forced-liquidation scenario under ALLOW_NO_SHORTFALL; and effective-incentive divergence on a non-core e-mode ERC20 borrower (proves getEffectiveLiquidationIncentive != getLiquidationIncentive on the live diamond and that the driver sizes the cut off the effective incentive).
  • Mocks: MockSpender, MockSplitRouter, MockPSM, plus a divergent effective-vs-core incentive and a getAccountLiquidity error slot.

Test plan

  • Unit (deterministic): npx hardhat test tests/hardhat/BStock*.ts — 106 passing.
  • Fork Liquid Mesh: FORKED_NETWORK=bscmainnet npx hardhat test tests/hardhat/Fork/BStockLiquidatorFork.ts --grep "split router"
  • Fork VAI: ... --grep "VAI" — two-hop and script-driven VAI settle through the real gate + real PSM; paused-PSM is refused.
  • Fork effective-incentive divergence: ... --grep "effective-incentive divergence"
  • Fork forced liquidation: ... --grep "script-driven forced liquidation" — settles a healthy account under ALLOW_NO_SHORTFALL.
  • Live Liquid Mesh disableSimulate proof (fork at head, chainId 56, LM creds): npx hardhat run scripts/bstock/verify-lm-fork.ts.

Note: the currently-deployed proxy predates both routerSpender and the VAI path — a new deploy/upgrade is required before the Liquid Mesh or VAI liquidations work in production.

…swaps

Some aggregators (e.g. Liquid Mesh) pull the input token through a
settlement contract distinct from the call target. Add a routerSpender
mapping so _swap approves the puller, defaulting to the router itself so
Native is unchanged. Gap shrinks 50->49; storage-safe.
Price Native and Liquid Mesh per liquidation and settle through the
higher-out source. The LM client uses disableSimulate to build swap
calldata for the not-yet-holding contract, keeping seize->sell atomic;
sources sit behind a registry so future ones need no contract change

@claude claude 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.

Claude Code Review

Claude Code Review is paused for this repository. To reconnect it, an admin of this repository's GitHub organization (or the account owner, for personal repositories) who can also manage your Claude organization's Code Review settings needs to re-link GitHub in Code Review settings. This is a one-time step.

Tip: disable this comment in your organization's Code Review settings.

@Debugger022 Debugger022 self-assigned this Jul 10, 2026
@Debugger022 Debugger022 changed the title [VPD-1589] [VPD-1589] Add Liquid Mesh as a hop-1 liquidation source Jul 10, 2026
Couple the spender lifecycle to the router allowlist:
- setRouterSpender reverts unless the router is currently allowlisted
- a non-zero spender must be a deployed contract (it receives a live
  exact-amount approval during _swap; an EOA is always a misconfig)
- setRouter(router, false) clears any configured spender so a stale
  entry cannot silently reactivate on a later re-allowlist

The spender was previously unvalidated: a fat-fingered address would
silently receive the hop approval on every swap through that router.
… build

Liquid Mesh's /quote number is indicative — the separately built /swap
order only guarantees its own floor — while Native's firm quote executes
at exactly its amountOut. Comparing the two raw outs let an optimistic
LM quote win the hop-1 selection and then fill worse than Native
guaranteed.

Tag each SourceQuote as firm/indicative and expose the built order's
builtFloor from build(); when an indicative winner's floor undercuts the
best firm loser, execute the firm quote instead.
LM RFQ orders are short-lived; an order already too tight to survive
signing + submission + inclusion would only fail on-chain with
DeadlineExpired, mid-incident. Abort at build time instead when fewer
than LM_MIN_TTL seconds (default 15) remain on the order.
Replace the two any-typed JSON parses with a typed LmResponse<T>
envelope, and reject an LM_PRIVATE_KEY_SEED that does not decode to
exactly 32 bytes with a legible error instead of the cryptic
createPrivateKey DER failure.
Runbook lagged two behavior changes:
- indicative LM winner is reconciled against the best firm quote after
  build; operators see an expected `hop-1 reconcile:` source switch
- setRouterSpender now requires an allowlisted router and a contract
  spender; de-allowlisting clears the spender entry
Zero-context operators had to infer the run order from scattered
hints; give them a mermaid decision tree (smoke -> dry-run -> send,
Safe fallback branches) plus rules of thumb up front.

Copilot AI 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.

Pull request overview

Adds Liquid Mesh as an additional hop-1 RFQ liquidity source for bStock liquidations, extending the existing BStockLiquidator to support “split spender” routers (where the approval target differs from the call target) and updating the off-chain liquidation tooling to select the best available quote source.

Changes:

  • On-chain: introduce routerSpender + setRouterSpender and update _swap to approve the configured spender (defaulting to the router).
  • Off-chain: add a Liquid Mesh API client and a hop-1 source registry (SOURCE=auto|native|liquidmesh|...) with firm-vs-indicative reconciliation and LM TTL guarding.
  • Tests/tools/docs: add unit + fork coverage for split-spender routing, plus a fork verification script and updated README/flowchart.

Reviewed changes

Copilot reviewed 11 out of 12 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
tests/hardhat/Fork/BStockLiquidatorFork.ts Fork test covering split-router (separate spender) atomic settle via a freshly deployed proxy.
tests/hardhat/BStockLiquidatorLiquidMesh.ts New unit suite validating Liquid Mesh split-spender semantics and spender hardening behavior.
tests/hardhat/BStockAtomicLiquidate.ts Extends script tests to cover source registry selection, reconcile behavior, and LM TTL guard.
scripts/bstock/verify-lm-fork.ts New fork-only diagnostic proving disableSimulate:true LM calldata executes on-chain.
scripts/bstock/README.md Updates operational documentation for source registry, LM prerequisites, and incident flow.
scripts/bstock/liquidation-flowchart.svg New decision flowchart asset referenced by the README.
scripts/bstock/lib/sources.ts New hop-1 source registry (Native + Liquid Mesh) with lazy build and reconcile logic.
scripts/bstock/lib/liquidmesh.ts New Liquid Mesh API client with typed responses and Ed25519 JWT auth.
scripts/bstock/atomic-liquidate.ts Updates atomic liquidation script to price/select hop-1 sources via the registry and adds percent validation.
contracts/test/BStockLiquidationMocks.sol Adds MockSpender / MockSplitRouter to model split-spender aggregators in tests.
contracts/BStock/IBStockLiquidator.sol Extends interface with RouterSpenderSet event, SpenderNotContract error, and setRouterSpender.
contracts/BStock/BStockLiquidator.sol Implements routerSpender, clears spender on de-allowlist, and approves spender in _swap.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread scripts/bstock/lib/sources.ts Outdated
Comment thread scripts/bstock/lib/sources.ts Outdated
}

// Price every selected source in parallel; a source that errors (API down / no route) drops out.
const settled = await Promise.allSettled(sources.map(async s => ({ name: s.name, quote: await s.getQuote(args) })));

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.

should we have a timeout here, otherwise like comment describes if api pending it could block both sources ?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

ae6b50f
Added a shared fetchWithTimeout (AbortSignal.timeout, env SOURCE_TIMEOUT_MS, default 8s) to both clients, so a hung source aborts and drops out of the race instead of blocking the live one. Covered by BStockSourceTimeout.ts.

Comment thread scripts/bstock/atomic-liquidate.ts Outdated
deadline = hop1.deadline; // settle tx reverts on-chain past the quote's expiry
router = hop1.router;
swapCalldata = hop1.calldata;
const midOut = hop1.out; // USDT out of hop 1

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.

i understood for that native, output from quote always equal to the amt we received, is it the same case for liquidMesh ? Unsure if there is a minimumReceived or should we use hop1.out * (100% - slippage)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Native's out is firm (MM-signed) so received == quoted. LM's /quote out is indicative, the binding number is the built order's floor, builtFloor = out * (1 - slippageBps) (sources.ts:120), so your out * (1 - slippage) instinct is right. No underfill is possible either way: the contract reverts if debtOut < minOut (BStockLiquidator.sol:459), and at default config minOut already lands on 0.995·out == LM's floor. The only caveat is that this holds because MIN_OUT_BUFFER == SLIPPAGE; if someone sets the buffer below slippage on a sole-LM run it would false-revert (fails safe, no loss). Can harden minOut to derive from builtFloor directly if we want to decouple those knobs, but nothing is at risk as-is.

@fred-venus fred-venus Jul 15, 2026

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.

lets just keep it then.

In fact, my original thought is that the actual amount of tokens we received from hop1 could be slightly smaller than this value. This could cause the hop2 swap to fail because the amount we used in the call request is more than we can actually provide. But yeah, like u said, no harm, maybe add a comment here indicating if this fail can slightly lower this value or use builtFloor value ?

@Debugger022 Debugger022 Jul 15, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

591f6c7 went with builtFloor.

Comment thread scripts/bstock/README.md
## Prerequisites

- **Native API key** (`NATIVE_API_KEY`) for anything that fetches a quote. Never commit it.
- **Native API key** (`NATIVE_API_KEY`) for anything that fetches a Native quote. Never commit it.

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.

  1. lets confirm is the api key here (both native / lm) ok for long term use, or do we need to reply a new once
  2. once done, create a new page here, deploy the contract and update everything there so that the script is ready to use out of box

@Debugger022 Debugger022 Jul 14, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Checked both vendors' docs, neither documents a key expiry or rotation policy. Liquid Mesh issues the key via support, bound to your registered Ed25519 pubkey, and says to store it in a vault (i.e. long-lived); the only short TTL is the per-request JWT (<=2s), which we already mint fresh per call and not the key.
Native's docs only cover endpoints, nothing on key lifetime. So both read as long-term keys, but since neither publishes a written SLA, we will confirm with each provider before prod.

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.

cool then, i forgot to attach the doc

  1. lets create a page here under Credentials section
  2. as for the format can ref other doc

Promise.allSettled waits for the slowest source, so a hung API blocked the
live one. Add fetchWithTimeout (SOURCE_TIMEOUT_MS, default 8s) to the Native
and LM clients so a hung source aborts and drops out.
@Debugger022
Debugger022 changed the base branch from feat/VPD-1430 to feat/VPD-1598 July 15, 2026 08:10

@claude claude 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.

Claude Code Review

Claude Code Review is paused for this repository. To reconnect it, an admin of this repository's GitHub organization (or the account owner, for personal repositories) who can also manage your Claude organization's Code Review settings needs to re-link GitHub in Code Review settings. This is a one-time step.

Tip: disable this comment in your organization's Code Review settings.

An LM /quote is indicative, but hop-2 calldata bakes in a fixed amountIn
while the contract approves only the real hop-1 delta — an underfill left
the AMM pulling past the approval. Sizing off the guaranteed floor keeps
the pull covered; the surplus stays sweepable. Native unchanged (floor==out).
A borrower whose only debt is VAI was unliquidatable: _liquidate called
underlying(), which the VAIController does not have. Resolve VAI via
getVAIAddress() and route it through the gate's _liquidateVAI branch,
two-hop bStock->USDT->VAI with the PSM as hop 2. INVENTORY only — VAI is
burned on repay, so there is no market to flash from.
It detected vBNB by catching underlying(), which the VAIController also
lacks, so a VAI debt built a {value: repay} batch the gate rejects. Detect
VAI explicitly and use its own seize math (liquidateVAICalculateSeizeTokens,
borrower-agnostic incentive). This path ships the seized bStock instead of
swapping it, so it is the fallback when the atomic path's PSM hop is down.
Comment thread scripts/bstock/atomic-liquidate.ts Outdated
// more than the approval — hop 2 reverts on allowance. `floor <= midDelta` always holds, so the pull
// always fits. Any surplus (`midDelta - floor`, bounded by slippage) stays as USDT inventory and the
// contract emits `PartialSwapLeftover` for it — sweepable, not lost.
const amm = await getAmmSwap(

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.

for VAI, its gonna be swapStableForVAI ? But where is the code to construct the calldata ?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The contract and its tests already specified the Peg Stability Module
(swapStableForVAI) as router2 for a VAI debt, but atomic-liquidate.ts
had no code to construct that calldata — a VAI run fell through to the
AMM aggregator path. Add lib/psm.ts: encodes swapStableForVAI locally
(receiver = the liquidator, amountIn = the hop-1 floor), derives
expectedOut from previewSwapStableForVAI, and pre-flights isPaused and
mint-cap headroom. Reject MODE=flash for VAI before quoting, mirroring
the contract's FlashNotSupportedForVai.

@claude claude 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.

Claude Code Review

Claude Code Review is paused for this repository. To reconnect it, an admin of this repository's GitHub organization (or the account owner, for personal repositories) who can also manage your Claude organization's Code Review settings needs to re-link GitHub in Code Review settings. This is a one-time step.

Tip: disable this comment in your organization's Code Review settings.

… module

Settles a real underwater VAI position on a bscmainnet fork: bStock
collateral on the live diamond, VAI minted through the real
VAIController (prime-only flag flipped via the real ACM + timelock),
repay through the real gate's VAI branch, and hop 2 executed with the
exact swapStableForVAI calldata lib/psm.ts builds — asserting the real
PSM mints exactly its previewSwapStableForVAI amount to the contract.
A fresh proxy is used because the deployed one predates VAI support.
Also proves getPsmSwap refuses to build when the real PSM is paused.
Runs atomicLiquidate() itself end-to-end on the bscmainnet fork for a
constructed VAI-debt borrower (prime-only mint flag flipped through the
real ACM + timelock, bStock collateral gapped into shortfall): the
script detects the VAIController debt, sizes the seize with the VAI
overload, builds the swapStableForVAI calldata against its real default
PSM address, derives minOut from the live preview, and settles — the
real PSM mints exactly the previewed amount and the undershot hop-1
surplus stays as sweepable USDT. Also asserts the script refuses
MODE=flash for VAI before quoting. VAI scaffolding extracted into
shared helpers.
…ht guards

Correctness and safety fixes to the off-chain liquidation scripts, each with
tests (mocked script suites + a bscmainnet-fork scenario):

- Size the Venus Liquidator treasury cut off getEffectiveLiquidationIncentive
  for every debt type, VAI included, mirroring Liquidator._splitLiquidationIncentive
  exactly. The VAI path previously used the borrower-agnostic core incentive,
  which diverges when the borrower sits in a non-core pool. Correct the stale
  "cut is 0 today" comments (the live BSC gate keeps 50% of the bonus) and log
  the resolved cut.
- Derive the single-hop minOut from the built order's guaranteed floor instead
  of the indicative quote, so an in-slippage Liquid Mesh fill below the quote no
  longer reverts InsufficientOut (matches the two-hop sizing).
- Buffer the safe-fallback redeem/transfer amounts by SEIZE_BUFFER so ordinary
  oracle price drift between batch generation and signer quorum leaves sweepable
  dust rather than reverting the redeem.
- Validate SLIPPAGE and MIN_OUT_BUFFER to [0,100) like SEIZE_BUFFER.
- Read the getAccountLiquidity error slot and report an oracle failure distinctly;
  gate the no-shortfall abort behind ALLOW_NO_SHORTFALL=1 so forced liquidations
  of healthy accounts are supported.
- Sanity-bound the Liquid Mesh order expiry to reject a millisecond-epoch value
  that would silently disable the off-chain TTL guards.

Test mocks gain a divergent effective-vs-core incentive and a getAccountLiquidity
error slot to exercise the above.
…omments

- README: add SETTLE_TTL_MARGIN and ALLOW_NO_SHORTFALL to the atomic env table,
  SEIZE_BUFFER to the safe-fallback table, an advanced/override env subsection,
  and correct "4 txs" to "3-4" (native BNB drops the approve). Note the
  single-hop minOut is derived from the guaranteed floor and that price drift
  alone can invalidate a safe-fallback batch.
- amm.ts: correct the stale comments that described exact-match hop-2 sizing;
  the caller passes the hop-1 floor and relies on floor <= midDelta.
@Debugger022

Copy link
Copy Markdown
Contributor Author

@claude please review

Copilot AI 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.

Pull request overview

Copilot reviewed 20 out of 21 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (2)

scripts/bstock/atomic-liquidate.ts:291

  • The seize-calculation errors still hardcode liquidateCalculateSeizeTokens in the message, even when isVai takes the liquidateVAICalculateSeizeTokens branch. That makes failures/debugging misleading for VAI liquidations.
  if (!seizeErr.eq(0)) throw new Error(`liquidateCalculateSeizeTokens error ${seizeErr}`);
  // A zero seize means the incentive resolved to 0 (e.g. bStock unlisted in the borrower's pool):
  // surface it here rather than building a degenerate quote that reverts on-chain.
  if (seizeTokens.eq(0)) throw new Error(`liquidateCalculateSeizeTokens returned 0 seize for ${borrower}`);

scripts/bstock/safe-fallback.ts:182

  • The seize-calculation errors mention liquidateCalculateSeizeTokens even when isVai uses liquidateVAICalculateSeizeTokens, which makes Safe-batch generation failures confusing for VAI debt cases.
  if (!seizeErr.eq(0)) throw new Error(`liquidateCalculateSeizeTokens error code ${seizeErr}`);
  // A zero seize means the incentive resolved to 0 (e.g. bStock unlisted in the borrower's pool):
  // surface it here rather than baking a degenerate redeem amount into the batch.
  if (seizeTokens.eq(0)) throw new Error(`liquidateCalculateSeizeTokens returned 0 seize for ${borrower}`);

…n scripts

The treasury-cut comments claimed getEffectiveLiquidationIncentive diverges from
the core getLiquidationIncentive "whenever the borrower sits in a non-core pool"
— false for VAI. A VAI borrower is core-pool-locked (VAIController.mintVAI requires
the core pool and MarketFacet.hasValidPoolBorrows bars leaving it while mintedVAIs>0),
so userPoolId is always 0 and effective == core for VAI. Divergence is only reachable
for a non-VAI ERC20 borrower in a non-core pool. Both scripts already call effective
for the cut (gate-parity with Liquidator._splitLiquidationIncentive + future-proofing);
only the rationale was wrong. Comments updated in atomic-liquidate.ts and safe-fallback.ts.
…e ERC20 borrower

Add a bscmainnet-fork test that stands up a real non-core e-mode pool listing vBStock
at a divergent liquidation incentive (1.25x vs core 1.1x), puts a USDT borrower in it,
and gaps the stock into shortfall. It proves (a) getEffectiveLiquidationIncentive and
getLiquidationIncentive actually diverge on the live diamond for a non-VAI debt,
(b) atomic-liquidate.ts sizes the treasury cut off the effective incentive (the hop-1
quote amount matches the effective-cut seize, not the core one), and (c) it settles
end-to-end through the real gate.

Also mark the VAI treasury-cut unit test as defensive: its forced core-vs-effective
divergence is on-chain-impossible for VAI (core-pool-locked), so it guards the script's
cut formula, not a reproducible mainnet state; the genuine case is the new fork test.
…fallback framing

Rework the operator flowchart to reflect the current multi-source, VAI-capable
scripts: pre-flight gates (debt-type detect, shortfall/ALLOW_NO_SHORTFALL,
VAI-forces-inventory), hop-2 routing by debt type (single-hop USDT, AMM for other
ERC20, WBNB unwrap for native BNB, PSM swapStableForVAI for VAI), and three distinct
fallback triggers merging into one rail (no hop-1 source fills, VAI PSM paused/capped,
dry-run fails).

native-smoke probes Native ONLY — there is no Liquid Mesh liveness probe, so LM is
only validated inside atomic-liquidate. The chart now says "Native quote alive?" and
routes a Native-down case into atomic (SOURCE=liquidmesh) rather than straight to the
Safe fallback.

Also correct the safe-fallback.ts and README framing: the fallback triggers when the
whole quote path is down (every RFQ source, or the VAI PSM hop), and it is
RFQ/PSM-independent because it ships bStock to the CEX with no on-chain swap.
…label

Split the group header so "2 · atomic-liquidate.ts" stays left and "DRY_RUN=1" is
right-aligned, leaving the vertical "alive · auto" edge a clear center gap.
The seize error strings hardcoded liquidateCalculateSeizeTokens even on the isVai
branch, which calls liquidateVAICalculateSeizeTokens — misleading when a VAI
liquidation or Safe-batch build fails. Derive the function name from isVai and
interpolate it into both throws in atomic-liquidate.ts and safe-fallback.ts.
@Debugger022
Debugger022 requested a review from fred-venus July 16, 2026 21:01

Copilot AI 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.

Pull request overview

Copilot reviewed 20 out of 21 changed files in this pull request and generated 1 comment.

Comment thread scripts/bstock/lib/psm.ts

Copilot AI 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.

Pull request overview

Copilot reviewed 20 out of 21 changed files in this pull request and generated no new comments.

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.

3 participants