Smart Lending & StableSwap
Smart Lending lets a user deposit a Lista StableSwap LP position as collateral into Moolah, borrow against it, and keep earning trading fees on the underlying pool at the same time. The LP tokens never leave the system idle: they are held by a SmartProvider, wrapped 1:1 into a collateral token, and supplied to a Moolah market, while the pool they represent continues to accrue swap fees.
The collateral token is not a freely transferable ERC-20.
mintandburnare restricted to the minter, andtransfer/transferFromare restricted to Moolah and addresses holding theTRANSFERERrole. Do not build a flow that moves it between arbitrary accounts — route position changes through the provider.
Route swaps through Lista StableSwap, or use an LP position as Moolah collateral:
discover Lista StableSwap pools and route/quote swaps against them, and
move StableSwap LP into a Moolah Smart Lending market via
SmartProvider.
For deployed addresses, see BSC Smart Lending (do not hard-code addresses from this page — the address tables are the source of truth). For how providers fit into Moolah, see Integration Patterns.
Lista StableSwap is a two-coin Curve-style stable pool: every pool has exactly two coins (N_COINS = 2), indexed 0 and 1 in sorted-address order.
Factory: pool discovery
StableSwapFactory is the registry of all deployed pools. Use it to enumerate pools and resolve a token pair to its pool and LP token.
Pool count
pairLength() → uint256
Number of registered pools.
Pool by index
swapPairContract(uint256 index) → address
The pool (swap) contract at index, for index in [0, pairLength).
Pools for a pair
getPairInfos(address tokenA, address tokenB) → StableSwapPairInfo[]
All pools registered for the pair (order-independent — inputs are sorted internally).
Sort helper
sortTokens(address tokenA, address tokenB) → (address token0, address token1)
Canonical (token0, token1) ordering; reverts IDENTICAL_ADDRESSES if equal.
StableSwapPairInfo is:
struct StableSwapPairInfo {
address swapContract; // the StableSwap pool
address token0; // sorted coin 0
address token1; // sorted coin 1
address LPContract; // the StableSwapLP (ERC-20) minted by the pool
}Coins are always stored in sorted order (token0 < token1). A single token pair can have more than one pool (for example a standard and a narrow-spread pool), which is why getPairInfos returns an array.
To keep an up-to-date pool list, index the factory's NewStableSwapPair event, or walk swapPairContract(i) over [0, pairLength).
Pool reads
Read pool state directly from the StableSwapPool (the swapContract address).
Coin address
coins(uint256 i) → address
i ∈ {0, 1}. For a native-BNB pool the BNB side returns the sentinel 0xEeee…eEeE (see Native BNB).
Pool balance
balances(uint256 i) → uint256
Pool-tracked balance of coin i, in that coin's own decimals.
LP token
token() → address
The StableSwapLP ERC-20 for this pool.
Amplification
A() → uint256
Current amplification coefficient (already de-scaled).
Swap fee
fee() → uint256
Fee numerator over FEE_DENOMINATOR = 1e10.
Admin fee
admin_fee() → uint256
Share of fee taken as protocol fee, over 1e10.
Native support
support_BNB() → bool
true if one coin is native BNB.
Virtual price
get_virtual_price() → uint256
Invariant value per LP token, 1e18-scaled. Reverts on reentrancy.
Precision multipliers
PRECISION_MUL(uint256 i) → uint256
10 ** (18 - decimals_i); scales coin i to 18-decimal internal units.
StableSwapPoolInfo is a stateless helper that reads a pool for you and adds convenience views:
Balances
balances(address pool) → uint256[2]
Both pool balances.
LP → coins
calc_coins_amount(address pool, uint256 lpAmount) → uint256[2]
Coin amounts a proportional withdrawal of lpAmount LP would yield.
Holder → coins
get_coins_amount_of(address pool, address account) → uint256[2]
Same, for an account's full LP balance.
Mint preview
get_add_liquidity_mint_amount(address pool, uint256[2] amounts) → uint256
LP that add_liquidity(amounts) would mint (net of deposit fee).
Reverse quote
get_dx(address pool, uint256 i, uint256 j, uint256 dy, uint256 max_dx) → uint256
Input of coin i required to receive dy of coin j, grossed up to cover the fee. Reverts Excess balance / Exchange resulted in fewer coins than expected.
Quoting a swap: get_dy
get_dy returns the output amount net of the fee — what you actually receive — in the output coin's own decimals:
i— input coin index,j— output coin index ({0,1},i != j).dx— input amount, in coini's decimals.Returns the amount of coin
jout after the pool's swap fee.
Convention: token0 → token1 is i = 0, j = 1; token1 → token0 is i = 1, j = 0.
get_dy_without_fee(i, j, dx) returns the pre-fee output if you need to isolate the fee component. The executed swap is:
min_dy is your slippage floor; the swap reverts Exchange resulted in fewer coins than expected if the realized output is below it. Quote with get_dy immediately before execution and derive min_dy from it with your slippage tolerance.
Oracle price-difference guard
Each pool holds a resilient-oracle reference and can enforce a price-difference guard on state-changing operations — but whether it does is per-pool and manager-controlled. Read skipPriceDiff() on the specific pool rather than assuming the guard protects you — true means the guard is off.
Where it is enabled, exchange, add_liquidity, and every remove_liquidity* variant call checkPriceDiff(), which reverts when the pool's implied price for either coin diverges from the oracle price beyond a per-coin threshold:
Price difference for token0 exceeds thresholdPrice difference for token1 exceeds threshold
Relevant reads:
Oracle prices
fetchOraclePrice() → uint256[2]
Oracle price of each coin, 1e18-scaled.
Guard check
checkPriceDiff()
view; reverts if either coin's price diff exceeds its threshold. Safe to call as a pre-flight probe.
Skip flag
skipPriceDiff() → bool
When true, the guard is not enforced. One older registered pool predates this function and reverts when you call it — treat a revert as "guard behaviour unknown", not as false.
Thresholds
price0DiffThreshold(), price1DiffThreshold() → uint256
Per-coin thresholds, 1e18-scaled. Values differ per pool — read them rather than assuming the deployment default.
These thresholds and the skip flag are manager-adjustable on-chain values; read them at call time rather than assuming a fixed number. For a large or price-sensitive route, call checkPriceDiff() as a staticcall before submitting so you can surface a clear error instead of a failed transaction.
Because the guard runs on liquidity operations too, an LP withdrawal (including via
SmartProvider) can revert when the pool price has drifted from the oracle. Treat "price difference exceeds threshold" as a transient, retry-when-repegged condition, not a permanent failure.
Native BNB handling
A pool "supports BNB" (support_BNB() == true) when one of its coins is native BNB, represented by the sentinel address:
Handling rules when interacting with a BNB pool:
coins(i)returns the sentinel for the BNB side; it is not an ERC-20 — do not callapprove/transferFromon it.For the BNB coin, send the amount as
msg.value. The pool requiresmsg.valueto exactly equal the BNB input amount (revertsInconsistent quantityotherwise).For the ERC-20 coin, approve the pool and it will
transferFromyou as usual.On a non-BNB pool, any non-zero
msg.valueis rejected (Inconsistent quantity) to prevent accidentally sending BNB.BNB payouts use a bounded gas stipend, so a receiving contract's
receive()/fallback must stay within that budget or the transfer revertsBNB transfer failed.
Detect the pool type from support_BNB() and by comparing each coins(i) against the sentinel before deciding whether to use msg.value or ERC-20 transfers.
Supplying LP into Moolah via SmartProvider
SmartProvider is the provider that bridges StableSwap LP into a Moolah market. It holds the LP, mints a 1:1 collateral token (StableSwapLPCollateral), and supplies that to Moolah on the user's behalf — the LP keeps earning pool fees while it backs the loan. Each SmartProvider is bound at deploy time to one pool and one collateral token; resolve the right instance for your pool from BSC Smart Lending.
There are two entry points, depending on whether the user already holds LP.
supplyDexLp — supply existing LP
Use when the user already holds StableSwap LP tokens.
Flow: the provider pulls lpAmount LP from msg.sender (approve the provider on the LP token first), mints the collateral token 1:1, and calls MOOLAH.supplyCollateral for onBehalf. marketParams.collateralToken must equal the provider's collateral token (reverts invalid collateral token); lpAmount must be non-zero (zero lp amount).
supplyCollateral — zap tokens into LP, then supply
Use when the user holds the underlying tokens and wants the provider to add liquidity for them in one call.
Flow: the provider pulls amount0/amount1 (approve each ERC-20 coin on the provider first), calls the pool's add_liquidity([amount0, amount1], minLpAmount), mints the resulting LP 1:1 as collateral, and supplies it for onBehalf.
At least one of
amount0/amount1must be > 0 (invalid amounts).minLpAmountis the slippage floor for the mint; add-liquidity revertsSlippage screwed youif fewer LP would be minted.For a BNB pool, pass the BNB coin's amount as
msg.value(it must equal that coin's amount;amount0 should equal msg.value/amount1 should equal msg.value). On a non-BNB poolmsg.valuemust be 0 (msg.value must be 0).
Both paths emit the provider's SupplyCollateral event.
Once collateral is in the market, borrowing and repayment happen against the Moolah market as usual. Withdrawal has four paths, not a one-to-one mirror of the two supply entry points: the provider exposes withdrawDexLp (return LP), withdrawCollateral (proportional token0/token1), withdrawCollateralImbalance (exact token amounts), and withdrawCollateralOneCoin (single coin) — one for each way collateral can be split on exit. All four burn the collateral token, but only the latter three call remove_liquidity* on the pool and are therefore subject to the price-difference guard. withdrawDexLp returns the raw LP token without touching the pool, so it never hits the guard — it is the exit that stays available when the guard would otherwise block a withdrawal.
For the TypeScript builders that assemble these calls as ready-to-send steps (buildSmartSupplyDexLpParams, buildSmartSupplyCollateralParams, and the matching withdraw/repay builders), see the Moolah Lending SDK.
Revert checklist
Common reverts when integrating, with the contract that raises them:
IDENTICAL_ADDRESSES
Factory
sortTokens/lookup called with tokenA == tokenB.
Exchange resulted in fewer coins than expected
Pool exchange
Realized output below min_dy. Re-quote with get_dy and widen slippage.
Slippage screwed you
Pool add_liquidity / remove_liquidity_imbalance
Mint below min_mint_amount / burn above max_burn_amount. Adjust the slippage bound.
Withdrawal resulted in fewer coins than expected
Pool remove_liquidity
A min_amounts[i] floor not met.
Not enough coins removed
Pool remove_liquidity_one_coin
Output below min_amount.
Price difference for token0 exceeds threshold / …token1…
Pool checkPriceDiff
Pool price drifted from oracle beyond threshold. Retry once repegged.
Inconsistent quantity
Pool
BNB amount ≠ msg.value, or msg.value sent to a non-BNB pool.
BNB transfer failed
Pool
BNB recipient reverted or exceeded the gas stipend.
Reentrant call
Pool get_virtual_price
Called during an in-progress pool operation.
invalid collateral token
SmartProvider
marketParams.collateralToken ≠ the provider's collateral token.
zero lp amount / invalid amounts
SmartProvider
supplyDexLp with 0 LP / supplyCollateral with both amounts 0.
amount0 should equal msg.value / amount1 should equal msg.value / msg.value must be 0
SmartProvider
Native-BNB msg.value mismatch on supplyCollateral.
no lp minted
SmartProvider
add_liquidity produced no LP (e.g. dust amounts).
unauthorized sender
SmartProvider
Withdrawing for an onBehalf you are not authorized on.
Last updated
Was this helpful?