Liquidator Integration
To run a third-party liquidator against Lista Lending (Moolah) you need four things: which contract to call, how eligibility is gated, how to size the liquidation, and how the collateral comes back to you.
Liquidation on Moolah is permissionless at the contract level but gated per market. Rather than calling Moolah.liquidate directly, external liquidators go through PublicLiquidator, which is the contract that Lista's own Liquidation Zone front end uses. That page is a working reference implementation of everything described here.
For the product-level explanation of liquidation see Liquidation; for the data feeds that tell you what to liquidate see Positions, Liquidation & Emission API. For the low-level core function see Contract & Interface Reference § Liquidation.
The three liquidator contracts
Moolah deploys three liquidator contracts. PublicLiquidator is the third-party integration surface.
PublicLiquidator
Anyone — no role gate
Your own capital; you approve the loan token and receive the seized collateral
Third-party liquidators. This is the contract to integrate against.
Liquidator
BOT role only
Protocol-held funds
Lista's internal keeper. Not callable by external parties.
BrokerLiquidator
BOT role only
Protocol-held funds
Fixed-term / broker markets, where health accounting sits at the broker. Not callable by external parties.
Deployed addresses: BSC Core for Liquidator and PublicLiquidator, BSC Lending Brokers for BrokerLiquidator, and Ethereum for all three.
Eligibility: which positions you can actually liquidate
Every PublicLiquidator liquidation path runs the same internal eligibility check. On the flash paths it runs after the pair / smart-provider whitelist checks, and all of them revert with the same NotWhitelisted() selector, so the selector alone will not tell you which one failed:
function isLiquidatable(bytes32 id, address borrower) internal view returns (bool) {
return
IMoolah(MOOLAH).isLiquidationWhitelist(id, address(0)) || // 1. cheap probe — NOT proof the market is open; see below
marketWhitelist[id] || // 2. market opened on PublicLiquidator
marketUserWhitelist[id][borrower]; // 3. this position opened on PublicLiquidator
}A position is reachable if any of three conditions holds:
The market is open at the Moolah level.
Moolah._checkLiquidationWhiteListreturnsliquidationWhitelist[id].length() == 0 || contains(account), so an empty per-market whitelist means the market is open to every caller.isLiquidationWhitelist(id, address(0))is the cheap probePublicLiquidatoritself uses, but it does not prove the market is open: it also returnstruefor a gated market that happens to haveaddress(0)in its whitelist, andbatchToggleLiquidationWhitelisthas no zero-address guard to rule that out. In that casePublicLiquidatorpasses its own gate and Moolah then rejects the call, because the caller Moolah checks isPublicLiquidator, notaddress(0).
Answer these two separately — one does not imply the other:
Is the market open to anyone?
getLiquidationWhitelist(id).length == 0. That is the only definitive test.Can
PublicLiquidatorreach this market?isLiquidationWhitelist(id, <PublicLiquidator address>).
Query both rather than assuming — an open market is the common case, not a universal one.
The market has been opened on
PublicLiquidatorby theBOTrole (marketWhitelist[id]). This is how a market that is gated at the Moolah level can still be routed through the public path.This specific borrower has been opened by the
BOTrole (marketUserWhitelist[id][borrower]).
On a market that is gated at the Moolah level, an under-water position must first be surfaced through condition 2 or 3. The gating is not a health check: a position failing
isLiquidatablereverts withNotWhitelisted()even when it is deeply under water. Select the feed that matches the gate./api/liquidation/zone/listreturns the borrower whitelist, so it finds candidates on gated markets and may be empty for open markets. For open markets use/api/moolah/redPositions, which takes a single marketid— enumerate markets from/api/moolah/allMarketsand fan out.
Both setMarketWhitelist and setMarketUserWhitelist refuse to run on a market that is already open at the Moolah level, and setMarketUserWhitelist additionally refuses when the market has already been opened on PublicLiquidator itself. These are preconditions checked at set time only — Moolah-level state can change afterwards (a MANAGER can empty liquidationWhitelist[id] later), so do not assume the two mechanisms stay mutually exclusive.
After a successful liquidation, postLiquidate removes the borrower from marketUserWhitelist if the position has become healthy again. A partial liquidation that leaves the position under water keeps it open — but see the sizing constraint below: not every partial size is permitted.
Entry points
Five liquidation entry points. Pick by (a) whether you bring your own loan-token capital or flash-swap the collateral, and (b) whether the collateral is a plain ERC-20 or a smart collateral LP token.
Self-funded
The main path. Pulls the required loan token from you, calls Moolah.liquidate, and transfers the seized collateral to you. Thin wrapper over liquidateWithCollTransferOpt(..., doTransferColl: true).
Same, but doTransferColl: false leaves the seized collateral inside PublicLiquidator, credited to you in lpCollaterals[msg.sender][collateralToken], to be unwound later with redeemSmartCollateral. Only pass false for smart-collateral LP tokens — for a plain ERC-20 collateral there is no redeem path and the tokens are stranded in the contract.
Returns (actualSeizedAssets, repaidAssets), both straight from Moolah.liquidate. Self-funded liquidation of a smart-collateral market that redeems the LP in the same transaction. smartProvider must be registered in smartProviders and its TOKEN() must equal the market's collateral token. payload is abi.encode(minToken0Amt, minToken1Amt) — your slippage bound on the LP redeem. On flashLiquidateSmartCollateral these values do double duty: for a leg whose token is native BNB, the contract forwards the min amount as the swap call's exact msg.value. Setting a conservative floor there underfunds the swap rather than protecting you, so for a native-BNB leg the value must be the amount you intend to send. Both underlying tokens are sent to you directly (native BNB is transferred as value, not wrapped).
Flash-swap (no loan-token capital required)
Uses Moolah's onMoolahLiquidate callback to swap the seized collateral into the loan token before the repayment is pulled, so you need no loan-token balance — only enough gas. swapCollateralData is raw calldata low-level-called against pair; obtain it from an aggregator (1inch, etc.) with slippage already applied. Your profit is the loan-token surplus, transferred to you at the end.
The swap must pay out to
PublicLiquidator, not to you. The callback measures the contract's own loan-token balance before and after the swap and approves Moolah out of that balance. Aggregator calldata built with your own address as the recipient will leave the contract unable to repay and the transaction reverts. Set the recipient to thePublicLiquidatoraddress on every leg — both token legs, for the smart-collateral variant.
The smart-collateral variant: redeems the LP inside the callback, then swaps each leg to the loan token. Any leftover of either underlying is returned to you. Returns (actualSeizedAssets, repayAmount) — note the second value here is the locally pre-computed loanTokenAmountNeed, not Moolah's returned repaid amount as in liquidateSmartCollateral.
Every pair must be whitelisted. Both flash paths require
pairWhitelist[pair], which only theMANAGERrole can set. OnflashLiquidateSmartCollateralbothtoken0Pairandtoken1Pairmust be whitelisted unconditionally — including a leg that is never actually swapped, e.g. when that token already is the loan token — an arbitrary DEX router will revert withNotWhitelisted(). ReadpairWhitelist(address)before building the route. Self-funded paths have no such constraint, so if the venue you want is not whitelisted, useliquidateand do the swap yourself afterwards.
Unwinding deferred collateral
Returns (token0Amount, token1Amount).
Redeems LP collateral you previously accumulated via doTransferColl: false. Reverts with "insufficient lp collateral" if lpAmount exceeds your tracked balance.
Sizing a liquidation
Exactly one of seizedAssets / repaidShares must be non-zero. The other is derived:
seizedAssets > 0— "buy this much collateral." The required repayment is computed from the oracle price and the liquidation incentive factor.repaidShares > 0— "close this much debt." Pass the borrower's fullborrowSharesto close the position entirely. The Liquidation Zone front end uses this branch for a full close-out and theseizedAssetsbranch for a partial buy. This only succeeds when the resulting seizure does not exceed the borrower's remaining collateral —Moolah.liquidatedoesposition.collateral -= seizedAssets, which underflows and reverts on a deeply under-water position. Size byseizedAssetsin that case.
The incentive factor is the same formula as in Moolah core:
For standard (non-broker) markets, PublicLiquidator exposes a public view that reproduces Moolah's liquidation sizing:
Approve at least this amount of the loan token to PublicLiquidator before calling a self-funded path. Any unused loan token is refunded in the same transaction. The quoted amount therefore covers the expected repayment; a small buffer may be needed where the quote can change before execution.
Do not use this quote on a broker market.
loanTokenAmountNeedprices the collateral withMoolah.getPrice(marketParams), which is the plain market price (user = address(0)).Moolah.liquidateinstead prices with_getPrice(marketParams, borrower), and on a market with a broker that routes to the broker and can deviate from the market price according to the borrower's position. The broker's collateral price is the market price minus a deduction, so it is never higher — the quote therefore over-states the repayment.PublicLiquidatorpulls the larger amount and refunds the surplus in the same transaction, so the call succeeds, but you must hold the inflated figure to make it. On broker markets, derive the repayment from the broker's own pricing —peek(token, user)andgetUserTotalDebt(user), see Broker Reference.
Sizing a partial liquidation carries one more constraint:
A residual below minLoan must be healthy. After the repayment, Moolah requires _isHealthyAfterLiquidate: if the borrower still has both debt and collateral and the remaining borrow assets fall below minLoan(marketParams), the position must be healthy or the whole transaction reverts Moolah's "position is unhealthy" (see the Reverts table below). So you cannot leave a small unhealthy dust position behind: either size the liquidation to restore health, keep the residual at or above minLoan, or clear the debt or collateral entirely.
Call it after interest has been accrued for the market, or accept that the quote drifts: every entry point calls Moolah.accrueInterest(params) before computing the amount, and accrueInterest is permissionless, so a fresh quote is best obtained by simulating against current state rather than reading a value cached from an earlier block.
Reverts
NotWhitelisted()
isLiquidatable failed (position not open to the public path), or pair / smartProvider not whitelisted. This one selector covers both eligibility and whitelist failures — check isLiquidationWhitelist / marketWhitelist / marketUserWhitelist and pairWhitelist separately to tell them apart.
NoProfit()
Self-funded paths: the contract's collateral balance did not strictly increase. Flash paths: the swap output did not exceed the repayment — exact break-even reverts too, since a strictly positive loan-token surplus is required. Widen slippage, reduce seizedAssets, or pick a deeper venue.
SwapFailed()
The low-level call to pair reverted. Stale aggregator calldata is the usual cause.
"Invalid smart provider"
ISmartProvider(smartProvider).TOKEN() != marketParams.collateralToken.
Moolah "position is healthy"
The position was healthy by the time your transaction landed — for example, because someone repaid or the price moved. Re-check immediately before submitting and expect execution races.
Moolah "inconsistent input"
exactlyOneZero(seizedAssets, repaidShares) failed. Note the flash paths hardcode repaidShares = 0, so calling one with seizedAssets = 0 reverts here.
Moolah "position is unhealthy"
The residual position was left with debt and collateral, borrow assets below minLoan, and still unhealthy. Size the liquidation differently — see above.
Moolah "not liquidation whitelist"
PublicLiquidator itself is not on the market's Moolah-level whitelist. Can surface when a market was gated after marketWhitelist[id] was set.
Four entry points carry nonReentrant directly; liquidate inherits it by delegating to liquidateWithCollTransferOpt, which is public nonReentrant. redeemSmartCollateral is nonReentrant too.
Event
PublicLiquidator emits Liquidated(id, borrower, seizedAssets, repaidAssets, repaidShares, liquidator) — id and borrower indexed — in addition to Moolah's own Liquidate event (see Events & Callbacks). Index on this one to attribute liquidations to the public path specifically — but read the settled amounts from Moolah's Liquidate: the fields here echo the caller's inputs, so repaidShares is 0 on both flash paths and on any seizedAssets > 0 call even though Moolah burned a non-zero share amount, and seizedAssets is likewise the requested figure rather than Moolah's returned value. repaidAssets is PublicLiquidator's own loanTokenAmountNeed quote, priced at the plain market price, so on a broker market it diverges from what Moolah actually settled. Settled liquidations are also served by /api/liquidation/zone/history.
Reference flow
Poll candidates.
GET /api/liquidation/zone/listfor whitelisted / flagged positions, orGET /api/liquidation/zone/closeToLiquidatefor positions approaching the threshold (safety factor< 1.5). The Moolah liquidatable endpoint returnsborrowShares,totalBorrowAssets, andtotalBorrowSharesso you can recompute exact current debt from shares.Re-verify on chain. API data is indexed and can lag. Confirm reachability the way the eligibility section describes —
getLiquidationWhitelist(id).length == 0for an open market, orisLiquidationWhitelist(id, <PublicLiquidator>)plusmarketWhitelist(id)/marketUserWhitelist(id, borrower)for a gated one — and that the position is still unhealthy at the live oracle price — Moolah re-checks health at execution time and will revert otherwise.Size it. Choose
seizedAssetsorrepaidShares, then callloanTokenAmountNeedfor the repayment.Approve that amount of the loan token to
PublicLiquidator(self-funded paths only).Execute.
liquidatefor plain collateral;liquidateSmartCollateralfor smart collateral; theflash*variants if you would rather not hold the loan token and the venue is whitelisted.Settle. Seized collateral arrives in the same transaction unless you passed
doTransferColl: false, in which case unwind later withredeemSmartCollateral.
Last updated
Was this helpful?