Skip to content

[VPD-1430, 1431, 1432] feat(bstock): atomic backstop liquidator + off-chain scripts #683

Merged
fred-venus merged 84 commits into
developfrom
feat/VPD-1430
Jul 21, 2026
Merged

[VPD-1430, 1431, 1432] feat(bstock): atomic backstop liquidator + off-chain scripts #683
fred-venus merged 84 commits into
developfrom
feat/VPD-1430

Conversation

@Debugger022

@Debugger022 Debugger022 commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

Description

Backstop liquidation for bStock (ERC-8056 tokenized stocks: TSLAB, NVDAB, SPCXB) collateral. bStock is RFQ-only (Native is the sole market maker, no AMM), so third-party liquidators may not show up. This adds Venus's own fallback liquidator plus the off-chain tooling to drive it. Liquidation stays permissionless; this is only the backstop so the protocol never carries bad debt on an RFQ-only asset.

Changes

VPD-1430: BStockLiquidator contract + tests

Upgradeable, owner/operator-gated atomic liquidator. In one tx it repays an underwater borrow, seizes + redeems the bStock vToken, and sells the bStock to USDT via a pre-signed Native firm-quote with a hard minOut (a bad or late quote reverts the whole tx, so the protocol never holds the RFQ-only asset across a price-drift window).

  • Two funding modes: inventory (own USDT) and flash (Venus executeFlashLoan, repaid + premium in the same tx).
  • Routes the repay through the pool-wide Venus Liquidator gate when set, else liquidates directly; seize measured by balance delta so the liquidator treasury cut is handled correctly.
  • Router-allowlisted and operator-gated (it custodies funds and forwards caller-supplied swap calldata to an external router).
  • Extends the shared IComptroller (InterfacesV8) with getAccountLiquidity and executeFlashLoan (additive).

VPD-1431: off-chain liquidation scripts

The on-chain contract only does the atomic settle; every liquidation must be paired with an off-chain Native firm-quote. These scripts are that off-chain half.

VPD-1432: Safe multisig fallback

A read-only generator for a Safe Transaction Builder batch, used to settle a liquidation manually when the Native RFQ path is unavailable.

Scripts (scripts/bstock/)

lib/native.ts — Native (nativefi) RFQ API client (library). Orderbook / indicative / firm-quote, parses the signed txRequest + TTL, holds the TSLAB/NVDAB/USDT constants. API key via NATIVE_API_KEY, never hardcoded. Not run directly.

native-quote.ts — live quote smoke test (read-only, no chain). Sanity-checks the Native integration and shows price / spread / TTL / book depth before anything goes on-chain (depth is thin, so quotes above book size fail).

NATIVE_API_KEY=... npx hardhat run scripts/bstock/native-quote.ts
# options (env): TOKEN=TSLAB|NVDAB (def TSLAB), AMOUNT=1, FROM=0x..

atomic-liquidate.ts — driver for the deployed BStockLiquidator (the normal path). Confirms shortfall, precomputes the seize (adjusted for the redeem treasuryPercent), fetches the MM-signed firm-quote with the contract as taker, checks the router is allowlisted, computes minOut, then calls liquidate (inventory) or flashLiquidate (flash loan).

NATIVE_API_KEY=... LIQUIDATOR=0x.. BORROWER=0x.. VBSTOCK=0x.. VDEBT=0x.. REPAY_AMOUNT=5000 npx hardhat run scripts/bstock/atomic-liquidate.ts --network bscmainnet
# MODE=inventory|flash (def inventory), SLIPPAGE=0.5, MIN_OUT_BUFFER=0.5, DRY_RUN=1 (callStatic only), MOCK_NATIVE=<router>:<calldata> (fork tests)

lib/safe.ts — Safe Transaction Builder JSON helper (library). Encodes calls and builds the batch JSON the Safe{Wallet} Transaction Builder imports. Not run directly.

safe-fallback.ts — manual multisig fallback (read-only, emits JSON). When the RFQ path is down (API outage, halt, weekend, thin depth), a Safe multisig settles manually. Emits a 4-tx batch (approve, liquidateBorrow, redeem, transfer raw bStock to a custody/Binance address), seize amount snapshotted from chain. Sends nothing.

RPC_URL=... BORROWER=0x.. VBSTOCK=0x.. VDEBT=0x.. REPAY_AMOUNT=5000 TARGET=0x.. npx ts-node scripts/bstock/safe-fallback.ts
# ALLOW_PLACEHOLDER=1 emits a draft with a zero TARGET; OUT=path overrides output

Testing

  • Unit (contract) tests/hardhat/BStockLiquidator.ts: 23 passing — both funding modes, gate + treasury-cut path, all guards and error paths.
  • Driver (mock) tests/hardhat/BStockAtomicLiquidate.ts: 4 passing — drives atomic-liquidate.ts end to end (inventory + flash, healthy-borrower and router-allowlist guards).
  • Fork (real Venus Core) tests/hardhat/Fork/BStockLiquidatorFork.ts: passing — real liquidateBorrow → seize → redeem through the live Venus Liquidator, with strict profit/borrow assertions (stand-in markets + mock router, since no bStock market is listed yet).
  • Native RFQ client validated against the live API (quote, TTL, router).

Notes

  • No bStock market is listed in Core yet, so a full real liquidation and live RFQ settlement can only be exercised on mainnet once a market exists. The contract is collateral-agnostic and retargets to vTSLAB/vUSDT via env once listed.
  • Flash-loan authorization for the liquidator (whitelist + per-vToken setFlashLoanEnabled) is handled by a separate VIP (VPD-1433).

- bStock is RFQ-only (no AMM), so third-party liquidators may not
  show. Add a self-liquidation backstop that, in one tx, repays the
  borrow, seizes and redeems the bStock vToken, and sells it to USDT
  via a pre-signed Native RFQ quote with a hard minOut, so the
  protocol never holds the RFQ-only asset across a drift window.
- Two funding modes: own USDT inventory, or a Venus flash loan
  repaid (+ premium) in the same tx.
- Routes the repay through the pool-wide Venus Liquidator when that
  gate is set, else liquidates directly; the seized amount is read
  as a balance delta so the liquidator treasury cut is handled.
- Operator-gated and router-allowlisted: it custodies funds and
  forwards caller-supplied swap calldata to an external router.
- Extend the shared IComptroller with getAccountLiquidity and
  executeFlashLoan (additive); split storage-light interface into
  IBStockLiquidator.
- Add mocks, a unit suite (both modes, gate + treasury-cut, all
  guards and error paths), and a bscmainnet fork test on real Core.
- Native RFQ client (orderbook / firm-quote) plus a read-only quote
  smoke test for eyeballing price, depth and TTL
- atomic liquidation driver: precompute the seize, fetch the signed
  firm-quote with the contract as taker, then call liquidate or
  flashLiquidate; warn early if inventory cannot cover the repay
- mock-based test driving the script end to end: inventory and flash
  paths, plus the healthy-borrower and router-allowlist guards
- add scripts/ to the eslint tsconfig include (it was uncovered) and
  add an exchangeRateStored getter to the collateral mock that the
  driver reads
- Safe Transaction Builder JSON helper (encode calls, build batch)
- read-only script emitting a 4-tx batch (approve, liquidateBorrow,
  redeem, transfer raw bStock) for a multisig to settle a liquidation
  when the RFQ path is unavailable; seize amount snapshotted from
  chain, sends nothing
@Debugger022 Debugger022 changed the title [VPD-1430] feat(bstock): add atomic backstop liquidator [VPD-1430, 1431, 1432] feat(bstock): atomic backstop liquidator + off-chain scripts Jun 22, 2026
@Debugger022 Debugger022 self-assigned this Jun 22, 2026
@Debugger022
Debugger022 marked this pull request as ready for review June 25, 2026 04:09

@trumpgpt-bot trumpgpt-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.

Overall verdict

The BStockLiquidator contract itself is well-engineered: balance-delta accounting throughout both the seize and redeem legs, checks-effects-interactions preserved in the flash path, nonReentrant on every entry point, explicit router-approval reset to zero, and the inventory-mask guard in executeOperation (usdtOut < principal + premium rather than usdtOut < minOut) is an important correctness invariant that the tests exercise correctly. The atomic-liquidate.ts script and test suite are solid.

The blocker is in VPD-1432 (safe-fallback.ts): I verified on-chain that Comptroller.liquidatorContract() = 0x0870793286aada55d39ce7f82fb2766e8004cf43 on BSC mainnet. The generated batch calls vDebt.liquidateBorrow directly from the Safe, which will revert UNAUTHORIZED because the Safe is not the liquidatorContract. The script warns about this but still emits the batch. As a result, the entire VPD-1432 fallback is non-functional on mainnet — the safety valve for when the BStockLiquidator can't be used produces an unexecutable batch.

A secondary correctness gap is also in safe-fallback.ts: seizedRaw is computed without applying treasuryPercent (Comptroller.treasuryPercent() = 0 on mainnet today, so not currently broken, but atomic-liquidate.ts correctly adjusts for it and treasuryPercent is governance-settable).

No issues found on the contract security model, access control, reentrancy, or oracle/DeFi attack surface.

Comment thread scripts/bstock/safe-fallback.ts
Comment thread scripts/bstock/safe-fallback.ts Outdated
Comment thread scripts/bstock/lib/native.ts Outdated
Comment thread scripts/bstock/safe-fallback.ts
Comment thread contracts/BStock/BStockLiquidator.sol Outdated
Comment thread contracts/BStock/IBStockLiquidator.sol
Comment thread contracts/BStock/BStockLiquidator.sol Outdated
Comment thread contracts/BStock/BStockLiquidator.sol Outdated
Comment thread contracts/BStock/BStockLiquidator.sol
Comment thread contracts/BStock/BStockLiquidator.sol Outdated
Comment thread scripts/bstock/atomic-liquidate.ts Outdated
Comment thread contracts/BStock/BStockLiquidator.sol
Comment thread contracts/BStock/BStockLiquidator.sol Outdated
Comment thread contracts/BStock/BStockLiquidator.sol Outdated
Remove the shortfall guard from the liquidation pre-flight: it was
redundant with Core's liquidateBorrowAllowed and wrongly blocked forced
liquidations (which liquidate healthy, zero-shortfall accounts). Keep the
router allowlist and split it into _validateRouter(address). Rename the
struct param p -> params and drop the now-unused NotLiquidatable error
The Core liquidator gate is always configured on target networks, so
drop the direct-liquidateBorrow fallback and always route through the
Venus Liquidator, guarding against an unset gate. Removes the now-unused
LiquidateBorrowFailed error; tests set the gate by default.
Remove _flashActive: the FlashLoanFacet passes the executeFlashLoan
caller as initiator, and only flashLiquidate calls it, so initiator ==
this already proves the callback is ours. Saves two SSTOREs plus an
SLOAD and drops the NoFlashInFlight error. Add zero-address guards to
sweep for a clear revert.
The swap output is the generic debt asset (vDebt.underlying()), not
literally USDT, so rename usdtOut/usdtBefore to debtOut/debtBefore.
Naming only; the output-must-equal-debt invariant is unchanged.
The repay always routes through the pool-wide Venus Liquidator, which
keeps a treasury cut of the liquidation bonus, so the contract receives
fewer vTokens than seizeTokens. Deduct it before sizing the RFQ quote,
else the overstated amountIn makes the fixed-amount router pull revert.
Add mock getters so the fork test resolves the reads.
A direct vDebt.liquidateBorrow reverts UNAUTHORIZED while the pool gate
is set, so the emitted batch was unexecutable on mainnet. Route the
repay through the Venus Liquidator when set, redeem only the credited
amount (net of its bonus cut), and apply the redeem treasuryPercent so
the transfer never exceeds what the Safe holds. Add a driver test
Comment thread contracts/BStock/IBStockLiquidator.sol
Comment thread scripts/bstock/native-smoke.ts
Comment thread scripts/bstock/atomic-liquidate.ts Outdated
Comment thread contracts/BStock/BStockLiquidator.sol Outdated
Comment thread scripts/bstock/safe-fallback.ts
atomic-liquidate hardcoded the firm-quote output to USDT while the
contract measures proceeds and enforces minOut in vDebt.underlying().
A non-USDT debt market would then fail with a cryptic on-chain
InsufficientOut(0, minOut) revert.

Request the quote in the debt underlying and fail fast off-chain when
it isn't the Native output token (USDT on BSC, the only bStock quote
asset), pointing operators at the Safe fallback for non-USDT debt.
… map

The smoke test hardcoded a two-entry TSLAB/NVDAB address map, but Native
lists many more bStock tokens (SPCXB, CRCLB, MSTRB, ...) and keeps adding
them. Resolve the token address from the live orderbook instead, so the
script stays correct with no edits as the listing grows.
It is a read only diagnostic, not an operational script. Rename so it is
visibly distinct from the two runnable scripts (atomic-liquidate,
safe-fallback) and update its usage examples to the new path.
- Native RFQ only quotes bStock->USDT on BSC, so only USDT-debt
  markets could be liquidated atomically; non-USDT ERC20 debt
  (BTCB, ETH, CAKE) was pushed to the manual Safe fallback.
- Add an optional second hop (intermediate -> debt) through a
  second allowlisted router. A single final minOut in the debt
  asset covers the whole chain; when router2 is unset the path is
  the original single hop, unchanged.
- Keep the contract DEX-agnostic: hop 2 is an opaque router call
  guarded by the allowlist + minOut, so V2/V3/aggregator is a
  purely off-chain choice. The script sources hop 2 from a
  pluggable provider (KyberSwap default, OpenOcean, or a local
  PancakeSwap V2 encode for CI).
- The two-hop settle must be submitted via a private RPC: hop 2 is
  a public-mempool swap, so minOut bounds loss while the private
  RPC removes sandwich-induced reverts.
- LiquidationParams gains router2/swapCalldata2/intermediateToken,
  changing the liquidate/flashLiquidate selectors. Pre-launch, and
  the only consumer (the off-chain script) is updated in lockstep.

@trumpgpt-bot trumpgpt-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.

Overall verdict: COMMENT — no new blockers; previous blockers resolved; five smaller findings below.

What changed and what was verified on-chain

Every blocker from the previous review is addressed: safe-fallback.ts now correctly routes through the Venus Liquidator gate for step 2, applies both the Venus Liquidator treasury cut (treasuryPercentMantissa) and the Comptroller redeem fee (treasuryPercent) to seizedRaw, and the new two-hop swap extension (bStock → USDT → non-USDT debt) is cleanly integrated with delta accounting throughout.

On-chain verification:

  • Comptroller.liquidatorContract() = 0x0870793286aada55d39ce7f82fb2766e8004cf43 ✓ (gate set; routing path is always exercised)
  • Comptroller.treasuryPercent() = 0 (redeem fee zero today)
  • VenusLiquidator.treasuryPercentMantissa() = 0.5e18 = 50% — this is live and non-zero; the treasury-cut correction branch runs on every real liquidation. Both atomic-liquidate.ts and safe-fallback.ts handle it correctly via the !liqTreasuryPct.eq(0) conditional.
  • DEFAULT_SAFE (0xdc6E047...) is a deployed Safe proxy (5 signers, code verified).

Security model assessment

Reentrancy: liquidate and flashLiquidate are nonReentrant; executeOperation is callback-only, protected by msg.sender == comptroller && initiator == address(this) — these two guards together are sufficient. The removal of the _flashActive storage slot is correct.

Flash accounting: debtBefore is captured in _sellToDebt after liquidateBorrow has already pulled the repay, so debtOut measures swap proceeds only. The debtOut < amounts[0] + premiums[0] check in executeOperation ensures inventory cannot silently backfill an underwater swap — this invariant is sound.

Delta accounting: seize, redeem, and both swap hops all use balance-delta measurement. Pre-existing inventory (vBStock dust, intermediate USDT, debt) is correctly excluded at every step.

Access control: liquidate/flashLiquidate are onlyOperator; sweep/setRouter/setOperator are onlyOwner; the router allowlist bounds blast radius of a compromised operator key. ✓

No issues found in: reentrancy model, flash repayment correctness, intermediate validation in two-hop, approval hygiene in _swap, the WrongFlashAsset / BadInitiator guards, or the ensureNonzeroAddress(gate) guard.

Comment thread contracts/BStock/BStockLiquidator.sol
Comment thread contracts/BStock/BStockLiquidator.sol
Comment thread scripts/bstock/safe-fallback.ts
Comment thread scripts/bstock/atomic-liquidate.ts Outdated
Comment thread contracts/BStock/BStockLiquidator.sol Outdated
The event named the collateral market but not the repaid debt market,
so consumers couldn't filter liquidations by debt. Add vDebt as a third
indexed field, emitted in both inventory and flash paths.
Debugger022 and others added 26 commits July 15, 2026 18:36
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.
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.
… 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.
…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.
…and docs

- Add lm-smoke.ts: a read-only Liquid Mesh /quote probe, the counterpart to
  native-smoke.ts (never /swap; one best-effort symbol() label read, reusing
  ARCHIVE_NODE_bscmainnet). Both smokes now print the same shape (amountIn/out,
  px/token) with a mirrored firm-only / indicative block.
- Rename the hop-1 output var nativeOut -> usdtOut in atomic-liquidate.ts (it holds
  the USDT intermediate; "native" collided with native-BNB debt and the Native source)
  and de-stale "Native quote" -> "hop-1 quote" wording now that hop-1 is multi-source.
- Document the VAI path in the atomic header (PSM swapStableForVAI hop-2, inventory-only)
  and the test/fork env overrides (MOCK_NATIVE/MOCK_OUT/IMPERSONATE/USDT_ADDR/WBNB_ADDR).
- Refresh the flowchart + README: box 1 shows native-smoke and lm-smoke, the decision is
  "any RFQ source live?", and the Safe fallback trigger is the whole quote path (all RFQ
  sources down, or the VAI PSM paused/capped) rather than "Native unavailable".
- Add the bStock secrets to .env.example (NATIVE_API_KEY, LM_API_KEY, LM_PRIVATE_KEY_SEED,
  optional RPC_URL).

Unit: tests/hardhat/BStock*.ts - 106 passing.
[VPD-1589] Add Liquid Mesh as a hop-1 liquidation source
… matrix

Completes BStockLiquidatorFork.ts coverage across
{inventory, flash} × {native-style router-pulls, LM-style split-spender} ×
{USDT, CAKE (real PCS), BNB (WBNB unwrap), VAI (real PSM)}:

- adds the 8 previously untested positive cells — inv×LM×{CAKE,BNB,VAI},
  flash×native×{CAKE,BNB}, flash×LM×{USDT,CAKE,BNB} — plus an on-fork
  flash×VAI FlashNotSupportedForVai rejection check
- flash×two-hop and flash×split-spender existed nowhere: these are the cells
  where the flash principal+premium repay must come out of the swap chain
- hop-2 legs sized exactly like atomic-liquidate.ts (4-arg seize, gate cut off
  the effective incentive, redeem treasuryPercent), so flash settles at the
  real ~4% incentive margin instead of the main helper's blanket -10%
- pins WBNB as a ChainlinkOracle direct price: fork clock drift stales the
  live 3-oracle BNB config and reverts any vBNB borrow/liquidity check
  (the main suite's BNB cell hits this at its default block)

10 passing at FORK_BSTOCK_BLOCK=110490000.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ResilientOracle maps the native market to NATIVE_TOKEN_ADDR
(0xbBbB…BBbB), not WBNB (0xbb4CdB…095c), so the matrix suite's price pin
never reached vBNB. Every vBNB read stayed on the live feed and reverted
'invalid resilient oracle price' once a run drifted past its ~30min
staleness window, failing the BNB cells intermittently.
[VPD-1598] BStockLiquidator: Liquid Mesh hop-1 source, VAI debt support, and HashDit audit mitigations
test(bstock): fork-prove the full mode × hop-1 source × debt scenario matrix
[VPD-1636] BStockLiquidator deployment
@github-actions

Copy link
Copy Markdown

Code Coverage

Package Line Rate Branch Rate Health
contracts 100% 100%
contracts.Admin 88% 41%
contracts.BStock 100% 97%
contracts.Comptroller 100% 90%
contracts.Comptroller.Diamond 95% 64%
contracts.Comptroller.Diamond.facets 88% 74%
contracts.Comptroller.Diamond.interfaces 100% 100%
contracts.Comptroller.Types 100% 100%
contracts.Comptroller.legacy 100% 100%
contracts.Comptroller.legacy.Diamond 0% 0%
contracts.Comptroller.legacy.Diamond.facets 0% 0%
contracts.Comptroller.legacy.Diamond.interfaces 100% 100%
contracts.DelegateBorrowers 100% 89%
contracts.FlashLoan.interfaces 100% 100%
contracts.Governance 68% 45%
contracts.InterestRateModels 74% 59%
contracts.Lens 44% 47%
contracts.Liquidator 83% 60%
contracts.Oracle 100% 100%
contracts.PegStability 88% 84%
contracts.Swap 87% 58%
contracts.Swap.interfaces 100% 100%
contracts.Swap.lib 81% 55%
contracts.Tokens 100% 100%
contracts.Tokens.Prime 97% 81%
contracts.Tokens.Prime.Interfaces 100% 100%
contracts.Tokens.Prime.libs 90% 77%
contracts.Tokens.VAI 82% 52%
contracts.Tokens.VRT 20% 9%
contracts.Tokens.VTokens 71% 53%
contracts.Tokens.VTokens.legacy 0% 0%
contracts.Tokens.VTokens.legacy.Utils 0% 0%
contracts.Tokens.XVS 19% 8%
contracts.Tokens.test 100% 100%
contracts.Utils 52% 31%
contracts.VAIVault 50% 45%
contracts.VRTVault 49% 36%
contracts.XVSVault 63% 50%
contracts.external 100% 100%
contracts.lib 89% 71%
Summary 62% (4516 / 7315) 48% (1752 / 3646)

@fred-venus
fred-venus merged commit 083f75c into develop Jul 21, 2026
5 checks passed
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.

4 participants