Loan Lifecycle
A Lista Credit loan runs from credit-limit provisioning through borrowing, repayment, the grace and penalty window, and reinstatement. Every step is on-chain in CreditToken and CreditBroker; only the Merkle root is committed in advance. A user's score becomes public on-chain state once synced — CreditToken.creditScores is a public mapping, the sync emits ScoreSynced, and the credit-token balance is itself 1 token per USD of limit. What stays off-chain is the scoring inputs and model, not the resulting score.
The grace period, penalty rate, no-interest window and LISTA discount are all manager-adjustable on-chain. Read
graceConfig()andlistaDiscountRateonCreditBroker, andwaitingPeriodonCreditToken, at the time you need them — none of them is a fixed product guarantee.
Roles in the flow
Borrower
Supplies CreditToken collateral, borrows the loan token, repays principal + interest (+ penalty if overdue).
Off-chain assessment
Computes eligibility/limits off-chain and publishes only a Merkle root to CreditToken. Scoring inputs and logic are not on-chain.
BOT role
Rotates the CreditToken Merkle root (two-step, time-locked) and triggers liquidation of penalized positions. Operational keeper.
CreditBroker
Orchestrates collateral supply/withdraw and fixed-term borrow/repay against the underlying Moolah market.
CreditBrokerInterestRelayer
Receives interest and penalty from the broker and supplies it into the Moolah Credit Vault as revenue.
Lifecycle
1. Credit-limit publication
Off-chain assessment → CreditToken.setPendingMerkleRoot then acceptMerkleRoot (BOT)
A new Merkle root is staged, then accepted after a waiting period (waitingPeriod, floored at 6h). Accepting increments versionId. Only the root is published — not the underlying scores.
2. Score sync (mint/burn)
syncCreditScore(user, score, proof) — also run implicitly by broker actions
The user's (score, proof) is verified against the current root only when the submitted score or the stored versionId differs from what is already recorded — an unchanged score accepts an empty proof and versionId. CreditToken mints up to the new score or burns down to it (1 token = 1 USD of credit capacity).
3. Supply collateral
supplyCollateral(amount, score, proof)
Broker syncs the score, pulls CreditToken from the user, and supplies it as collateral into the broker's Moolah market.
4. Borrow (open fixed position)
supplyAndBorrow(collateralAmount, borrowAmount, termId, score, proof) or borrow(amount, termId, score, proof)
Guarded by noDebt and userNotPenalized. The borrower receives the full borrowAmount; on an upfront-interest term the whole term's interest is owed from the start rather than deducted at disbursement. A FixedLoanPosition is created with the term's apr, start, end, and termType; the broker borrows from Moolah and transfers the loan token to the user. Emits FixedLoanPositionCreated.
5. Interest accrual
Time
Interest grows per the position's FixedTermType (see below). Interest is tracked at the broker. For a broker market _isHealthy ignores the per-user price entirely and compares collateral at the plain market price against the broker's total debt; the per-user price only shapes the seize math once a liquidation runs. So Moolah perceives the rising risk.
6. Repayment
repay(amount, posId, onBehalf) / repayAndWithdraw(...) / repayInterestWithLista(...) (gated — see below)
Interest is repaid first, then principal. Interest (and any penalty) is supplied to the Credit Vault via the relayer. Emits RepaidFixedLoanPosition.
7. Grace period
end → end + graceConfig.period
The loan is overdue but no penalty applies yet. The borrower can still repay normally.
8. Penalty window
After end + graceConfig.period (dueTime)
The position is "penalized." A penalty of penaltyRate × (remaining principal + accrued interest) / RATE_SCALE is required, and the position must be repaid in full in a single repayment. graceConfig.penaltyRate is RATE_SCALE-scaled (1e27), so 3 * 1e25 reads as 3%; setGraceConfig caps it at RATE_SCALE. Read it from graceConfig(); the initialize() default in the source is not what is deployed.
9. Liquidation / bad debt
CreditBroker.liquidate(borrower, posId) (BOT only)
A penalized, not-already-bad-debt position is written off via Moolah.liquidateBrokerPosition; the position is flagged isBadDebt. See Bad Debt Handling.
10. Reinstatement
Full repayment of the penalized/bad-debt position
The position is paid off and removed; PaidOffPenalizedPosition is emitted when a penalty was settled. Once outstanding debt (CreditToken.debtOf) is cleared, the user may borrow again.
Fixed-term interest modes (FixedTermType)
Each fixed-term product carries one of two interest modes. CreditBroker's FixedTermAndRate is not the same struct as LendingBroker's: it adds a fourth field, termType, so the two cannot share an ABI decoder. The mode is fixed at borrow time and stored on the position; it determines how interest is computed.
Accrue per-second
ACCRUE_INTEREST (0)
Interest accrues linearly on the remaining principal: (principal − principalRepaid) × aprPerSecond × elapsed / RATE_SCALE, where aprPerSecond = (apr − RATE_SCALE) / 365 days. elapsed runs from lastRepaidTime, not from the position start, and a partial principal repayment resets lastRepaidTime to the current block — so interest is not a single accrual over the whole term. Both ends are capped at the position end. There is no upfront charge.
Upfront
UPFRONT_INTEREST (1)
Full term interest is owed once the no-interest window passes: principal × (apr − RATE_SCALE) × term / (365 days × RATE_SCALE). Within noInterestUntil (set to start + graceConfig.noInterestPeriod) the interest is 0.
apr is scaled by RATE_SCALE = 1e27 and encoded as RATE_SCALE + rate — a 10% APR is 1.10e27. Subtract RATE_SCALE, not 1, before using it in either formula above. apr − RATE_SCALE is itself still RATE_SCALE-scaled (10% APR → 1e26, not 0.1) — both formulas above, and the penalty formula below, each carry an explicit / RATE_SCALE alongside the subtraction to remove that scale; drop either the subtraction or the division and the result is 1e27× too large.
Repayment rules
Interest first, then principal. A repayment pays down outstanding interest before principal. Interest collected is supplied to the Credit Vault through the relayer.
Penalized positions must be repaid in full. Once past
dueTime, a repayment must cover remaining principal + remaining interest + penalty in one transaction, or it reverts.Repay interest with LISTA (discounted).
repayInterestWithLista(loanTokenAmount, listaAmount, posId, onBehalf)lets a borrower settle outstanding interest using LISTA at a discount. ReadlistaDiscountRatefor the rate andallowTransferLoanfor whether the path is open — it revertsrelayer/transfer-loan-not-allowedwhen it is not. The LISTA is sent to the relayer; the equivalent loan-token interest is credited from the relayer to the broker.Minimum loan check. A borrow validates only the position it just opened against the Moolah market
minLoan; a repay validates only the position it just touched, and only if it isn't fully cleared. Neither sweeps the account's other positions. Moolah separately enforces the same floor on the account's aggregate borrow.
Borrow guards
A new fixed position can only be opened when both broker modifiers pass:
noDebt
CreditToken.debtOf(borrower) == 0 — the user must have no outstanding credit-token debt. This is an accounted amount, not a wallet balance: it counts credit tokens held plus those posted as collateral, so an address with a zero wallet balance can still be blocked here.
CreditBroker._borrow
userNotPenalized
The user has no fixed position past its dueTime.
CreditBroker._borrow
Before borrowing, the broker calls _tryWithdrawAndBurnDebt, which withdraws and burns any CreditToken debt so the noDebt guard can be satisfied.
Credit-limit gating (Merkle root)
CreditTokenis non-transferable except by whitelistedTRANSFERERs (the brokers and Moolah).1 CreditToken = 1 USDof borrow capacity.A user's balance is reconciled to their published score on every
syncCreditScore: the broker passes(score, proof)and the token verifies the leafkeccak256(abi.encode(chainid, creditToken, user, score, versionId))against the current root before minting up to, or burning down to, the score.The root is rotated in two steps by the
BOTrole:setPendingMerkleRoot→ waitwaitingPeriod(floored at 6h) →acceptMerkleRoot(incrementsversionId). A pending root can be cancelled by theMANAGERviarevokePendingMerkleRoot.
Do not infer scoring inputs from the on-chain flow — the assessment that produces a limit is off-chain, and only its output reaches the chain.
Key events
AcceptMerkleRoot
A new credit-limit Merkle root is accepted (CreditToken).
ScoreSynced
A user's score/balance is reconciled to the current root (CreditToken).
FixedLoanPositionCreated
A new fixed-term position is opened.
RepaidFixedLoanPosition
A repayment is applied (interest/principal/penalty breakdown in the event).
RepayInterestWithLista
Interest is settled using discounted LISTA.
PaidOffPenalizedPosition
A penalized position is fully paid off.
PositionLiquidate
A penalized position is liquidated and flagged as bad debt.
Related
Smart Contract — contract roster and roles.
Canonical addresses: BSC Credit (Lista Lending).
Last updated
Was this helpful?