Skip to content

feat: add SmartProvider support to BrokerLiquidator#159

Open
ricklista wants to merge 13 commits into
masterfrom
feature/brokerLiquidatorSupportSmartProvider
Open

feat: add SmartProvider support to BrokerLiquidator#159
ricklista wants to merge 13 commits into
masterfrom
feature/brokerLiquidatorSupportSmartProvider

Conversation

@ricklista

@ricklista ricklista commented Apr 1, 2026

Copy link
Copy Markdown
Contributor

Summary

Add SmartProvider (LP token) liquidation support to BrokerLiquidator, move LendingBroker RELAYER/ORACLE from immutables to storage for shared implementation across proxies, fix interest tracking in partial liquidation, and improve convertDynamicToFixed to prioritize interest deduction before principal.

Change type

  • New contract
  • Upgrade (existing proxy)
  • Bug fix
  • Gas optimization
  • Configuration change
  • Migration / deploy script
  • Test
  • Dependency update

Contracts changed

Contract File Type
LendingBroker src/broker/LendingBroker.sol modified
IBroker src/broker/interfaces/IBroker.sol modified
BrokerLiquidator src/liquidator/BrokerLiquidator.sol modified
IBrokerLiquidator src/liquidator/IBrokerLiquidator.sol modified
BrokerMathDeductFixed.t test/broker/BrokerMathDeductFixed.t.sol new
MockSmartProvider test/liquidator/mocks/MockSmartProvider.sol new

Interface changes

LendingBroker:

  • Constructor: (moolah, relayer, oracle, wbnb)(moolah, wbnb) — relayer/oracle removed from immutables
  • initialize(): appended _relayer, _oracle parameters
  • New: setRelayer(address) — one-time migration setter (DEFAULT_ADMIN_ROLE)
  • New: setOracle(address) — one-time migration setter (DEFAULT_ADMIN_ROLE)
  • Modified: convertDynamicToFixed(amount, termId) — amount now prioritizes clearing interest first, then principal; new fixed position principal <= amount
  • New events: RelayerSet, OracleSet

BrokerLiquidator:

  • New: receive() — accept native ETH
  • New: withdrawETH(address, uint256) — MANAGER-only
  • New: liquidateSmartCollateral(id, borrower, smartProvider, seizedAssets, repaidShares, payload) — BOT-only
  • New: flashLiquidateSmartCollateral(id, borrower, smartProvider, seizedAssets, token0Pair, token1Pair, swapToken0Data, swapToken1Data, payload) — BOT-only
  • New: redeemSmartCollateral(smartProvider, lpAmount, minToken0Amt, minToken1Amt) — BOT-only
  • New: batchSetSmartProviders(providers, status) — MANAGER-only
  • New events: SmartProvidersChanged, SmartLiquidation

Storage layout

LendingBroker — safe. RELAYER (slot 18) and ORACLE (slot 19) appended after existing storage (liquidationWhitelist at slots 16–17). No collisions, no reordering.

BrokerLiquidator — new smartProviders mapping and _lastRepaidAssets uint256 appended. No collision risk.

Access control

LendingBroker:

  • setRelayer(), setOracle(): DEFAULT_ADMIN_ROLE only, one-time migration (reverts if already non-zero)

BrokerLiquidator:

  • withdrawETH(), batchSetSmartProviders(): MANAGER role
  • liquidateSmartCollateral(), flashLiquidateSmartCollateral(), redeemSmartCollateral(): BOT role

Risk assessment

Area Risk Note
Storage collision 🟢 None RELAYER/ORACLE appended at end of storage (slots 18-19), verified via forge inspect
Fund safety 🟡 Low New flash liquidation path swaps LP tokens via whitelisted pairs; withdrawETH restricted to MANAGER
Access control 🟢 None All new functions gated by existing roles (DEFAULT_ADMIN, MANAGER, BOT)
External call safety 🟡 Low Flash liquidation calls external swap pairs — restricted to whitelisted addresses only; onMoolahLiquidate callback uses reentrancy guard

Deployment

  • Chain: BSC mainnet
  • Scripts: script/broker/deploy_broker.s.sol, script/broker/deploy_brokerImpl.s.sol, script/broker/deploy_broker_20260408.s.sol
  • Env vars: MOOLAH, WBNB (optional, defaults to address(0))
  • Migration: After upgrading LendingBroker impl, call setRelayer() and setOracle() once per proxy to migrate from immutable values

Test plan

  • forge test --mc LendingBrokerTest passes (includes convertDynamicToFixed partial/full/excess tests)
  • forge test --mc BrokerMathDeductFixedTest passes (deductFixedPositionDebt interest tracking)
  • forge test --mc BrokerLiquidatorTest passes (smart collateral liquidation + flash liquidation)
  • forge test --mc MarketFactoryTest passes (constructor arg update)
  • forge test --mc PositionManagerTest passes (constructor arg update)

@ricklista ricklista force-pushed the feature/brokerLiquidatorSupportSmartProvider branch from c5205c0 to f4a2e4a Compare April 16, 2026 04:03
ricklista and others added 13 commits June 16, 2026 16:49
Add `redeemSmartCollateral` to allow BOT role to redeem smart collateral
LP tokens via whitelisted SmartProviders. Add `batchSetSmartProviders` for
MANAGER role to manage the provider whitelist.
…r shared impl

Move RELAYER and ORACLE from constructor immutables to storage variables
(appended at end of layout to preserve upgrade compatibility). This allows
all LendingBroker proxies to share a single implementation deployment.

- Constructor now only takes moolah address
- initialize() accepts relayer and oracle as additional params
- Added setRelayer() and setOracle() manager-only setters
- Updated deploy scripts and all tests

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…al liquidation

When partial liquidation deducts both interest and principal from a fixed
position, the old code unconditionally reset interestRepaid=0 and
lastRepaidTime=now. This erased any unpaid interest from position tracking,
causing getUserTotalDebt() to under-report debt.

The fix introduces three branches:
1. All interest covered -> safe to reset (unchanged behavior)
2. Partial interest + formula can represent outstanding -> adjust interestRepaid
   precisely so outstanding = accruedInterest - paidInterest (exact)
3. Partial interest + formula too small (most principal repaid) -> set
   interestRepaid=0 without resetting lastRepaidTime to maximize preserved
   outstanding

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add RelayerSet/OracleSet event emissions in LendingBroker migration setters (#7)
- Rename withdrawERC20 to withdraw with native BNB support in BrokerLiquidator (#1)
- Add NatSpec documenting custody requirement for redeemSmartCollateral (#2)
… functions

- liquidateSmartCollateral: return actual seized (collAmount) and
  repaid (loanToken balance diff) instead of placeholder values
- flashLiquidateSmartCollateral: capture repaidAssets from
  onMoolahLiquidate callback via _lastRepaidAssets storage
…ation

Remove partial interest tracking logic and always reset interestRepaid
and lastRepaidTime after liquidation deduction.
Amount now clears interest first, then principal. New fixed position
principal <= amount. Also fix constructor args after rebase and add
exact-full and excess-capped conversion tests.
- [L03] Block smart collateral markets from being liquidated via normal
  liquidate() by adding _isSmartCollateral() check that detects
  StableSwapLPCollateral via minter() -> TOKEN() chain
- [I01] Add sellBNB() function for selling native BNB received from
  smart collateral redemptions, mirroring Liquidator.sol
- [I05] Add zero address check in batchSetSmartProviders()
- [I07] Fix incorrect comment on BOT role constant
LendingBroker
- Add repayAll(onBehalf) for full position clearing, gated by whenNotPaused.
- Replace global checkPositionsMeetsMinLoan with per-position validators
  (_validateDynamicPosition, _validateFixedPosition); remove the post-cascade
  global check from onMoolahLiquidate so partial liquidations no longer revert
  when a single position lands in the (0, minLoan) band.
- Reject non-divisible repaidShares in liquidate() to preserve the broker
  market's totalBorrowAssets:totalBorrowShares = 1:VIRTUAL_SHARES invariant.
- Re-check amount == 0 after interest/principal clamping in
  convertDynamicToFixed to prevent zero-principal fixed positions when the
  user has no dynamic debt.
- Add docstring notes that fixed-repay amount and liquidate caller-whitelist
  / cascade ordering match the actual behavior.

Size optimization
- Move repay / repayAll bodies into LendingBrokerOperatorLib (DELEGATECALL'd).
- Move liquidation cascade orchestration to BrokerMath.executeLiquidationCascade.
- Move repayAll / convertDynamicToFixed pre-compute math to BrokerMath helpers.
- Consolidate duplicate errors and dead helpers.
LendingBroker shrinks from 27,358 -> 22,462 bytes (back under EIP-170).

Addresses audit findings: H1 (loan-token price drift stranding positions),
M (positions stranded by minLoanValue change), M (liquidation cascade dead
zone revert), M (non-divisible share liquidation drift), Info (zero-principal
fixed via convert), Low (_validatePositions stricter than Moolah post-liq
check), Info (fixed-repay natspec), Info (liquidate natspec outdated).

Generated with Claude Code
- BrokerLiquidator: drop duplicate receive() — master's d309fdc and the
  branch each declared an identical receive(); Solidity allows only one.
  The documented receive() at the end of the contract is kept, so the
  liquidator can still receive native BNB.
- BatchManagementUtils.t.sol: update _deployBroker() to the new LendingBroker
  2-arg constructor and 8-arg initialize (RELAYER/ORACLE moved from
  immutables to storage in 22bb8cf).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@ricklista ricklista force-pushed the feature/brokerLiquidatorSupportSmartProvider branch from 2e7613d to 3d3d63c Compare June 16, 2026 09:29
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