Skip to main content
Web311 min readUpdated

DeFi Lending Protocol Architecture: Interest Rate Models, Collateral Factors, and Liquidation Engines

A working engineer's breakdown of how overcollateralized lending protocols like Aave and Compound are built: interest rate curves, health factors, liquidation incentives, oracle dependencies, and the failure modes that drain them.

Summary

An overcollateralized lending protocol is four subsystems that must agree on one number: the value of a borrower's position. You need an accounting layer that tracks debt with interest-bearing share tokens, a rate model that prices utilization, an oracle layer that reports collateral value, and a liquidation engine that pays third parties to close unhealthy positions before the protocol takes a loss. Almost every lending exploit is a disagreement between those four subsystems, not a broken loop in the Solidity.

SolidityFoundryChainlinkViemNext.js

The four subsystems

It is tempting to think of a lending market as a pool with deposits and withdrawals. That model breaks the moment interest accrues, because every depositor's claim is growing continuously while the pool's token balance sits still. The architecture that actually survives contact with production splits into four parts, each with a single responsibility.

  • Accounting: converts underlying assets into non-rebasing share tokens so that interest accrual is a change in exchange rate, not a change in balance.
  • Rate model: a pure function from pool utilization to a borrow APR, with a kink that makes the last 10% of liquidity expensive.
  • Valuation: oracle reads plus per-asset collateral factors, producing a health factor for every account.
  • Liquidation: a permissionless entry point that lets anyone repay part of an unhealthy debt in exchange for discounted collateral.

Keep those boundaries hard. When teams let the liquidation engine read raw oracle prices instead of going through the valuation layer, they end up with two different definitions of solvency in the same codebase — and the gap between them is exactly where value leaks out.

Accounting: why you never store a raw balance

Storing a per-user underlying balance forces you to loop over every account to apply interest, which is impossible on-chain. Instead, store shares. A depositor's claim on the pool is shares multiplied by an exchange rate that only ever increases as interest is repaid. Debt works the same way in reverse: borrowers hold debt shares whose value grows with an accumulated borrow index.

// Interest is applied once, globally, on every state-changing entry point.
function accrue() public {
    uint256 delta = block.timestamp - lastAccrual;
    if (delta == 0) return;

    uint256 rate = rateModel.borrowRatePerSecond(totalCash(), totalBorrows);
    uint256 interest = (totalBorrows * rate * delta) / 1e18;

    totalBorrows += interest;
    totalReserves += (interest * reserveFactor) / 1e18;

    // Depositors capture the remainder implicitly: the same totalBorrows
    // increase raises the value of every supply share at once.
    borrowIndex += (borrowIndex * rate * delta) / 1e18;
    lastAccrual = block.timestamp;
}

Two rules make this safe. Call accrue() as the first statement of every function that reads or writes balances, so no user ever transacts against a stale index. And round every conversion in the protocol's favour: shares round down when minting, up when burning. A single wei of favourable rounding for the user, repeated across millions of transactions, is a slow drain — and in an empty pool it becomes the classic share-inflation attack, where the first depositor donates assets directly to the contract to make one share worth more than a later depositor's entire deposit. Mint a small number of dead shares at deployment to close it.

The rate model is a policy decision, not a formula

Utilization is borrows divided by borrows plus cash. The rate model maps that ratio to an APR, and its shape decides whether depositors can ever withdraw. Below a kink — typically 80% utilization — the curve is gentle, because you want borrowing to be cheap when liquidity is plentiful. Above it, the slope goes near-vertical, so that the cost of holding the last slice of liquidity becomes punitive and borrowers repay before the pool is fully drained.

UtilizationDesign intentTypical borrow APR
0–40%Attract borrowers, accept thin yield1–4%
40–80%Healthy equilibrium, linear growth4–10%
80–95%Above kink, force repayment10–60%
95–100%Emergency, withdrawals at risk60–200%+

The rate model should be a separate, swappable contract behind a governance-gated setter. Rate parameters are the knob you will actually want to turn during a market event, and you do not want turning it to require a full protocol upgrade.

Health factor and the liquidation engine

Health factor is the single number the entire protocol pivots on: the risk-adjusted value of collateral divided by the value of debt. Above one, the position is safe. At or below one, anyone may liquidate it.

healthFactor = sum(collateral_i * price_i * liquidationThreshold_i)
             / sum(debt_j * price_j)

Note that the collateral factor used for borrowing and the liquidation threshold used for solvency are two different numbers. If a user can borrow up to 75% of collateral value but is only liquidated at 80%, that 5% band is the buffer that stops ordinary volatility from liquidating everyone at once. Collapsing them into one parameter is a common early mistake and it makes the protocol brutally unstable.

Liquidation must be permissionless and profitable. A liquidator repays a fraction of the debt — capped by a close factor, usually 50% — and receives collateral worth the repaid amount plus a bonus of 5–10%. That bonus is the entire economic security model: it must exceed gas plus DEX slippage on the collateral, or nobody runs the bot and bad debt accumulates. For long-tail collateral in thin markets, a 5% bonus is not enough, and you either raise it or refuse to list the asset.

  1. Liquidator calls liquidate(borrower, repayAsset, collateralAsset, amount).
  2. Contract accrues interest, then recomputes the health factor from scratch — never from a cached value.
  3. Reverts unless healthFactor < 1e18 and amount <= debt * closeFactor.
  4. Pulls repayAsset from the liquidator, burns the borrower's debt shares.
  5. Seizes collateral shares worth (repaid value * (1 + bonus)) and transfers them.
  6. Emits an event carrying pre- and post-health factors for off-chain monitoring.

The oracle is your real attack surface

Most large lending exploits were not Solidity bugs. They were price manipulations: an attacker moved a thin spot market, borrowed against the inflated mark, and left the protocol holding worthless collateral. If you take one thing from this page, take this — never read a spot price from an AMM pair as collateral valuation.

  • Use aggregated push oracles (Chainlink and similar) for any asset with real market depth, and treat the feed's heartbeat and deviation threshold as hard inputs to your risk parameters.
  • Check staleness on every read: reject an answer whose updatedAt is older than the feed's heartbeat plus a grace period, and reject non-positive answers.
  • For assets with no professional feed, a long-window TWAP raises manipulation cost but does not eliminate it — size the borrow cap to the cost of moving that market for the TWAP window.
  • Cap total exposure per asset. A borrow cap is the only parameter that bounds the loss when an oracle is wrong, and it is the cheapest insurance in the system.
  • Add a circuit breaker: if a price moves more than a governance-set percentage within one block, pause borrows against that asset rather than trying to be clever.

What I ship alongside the contracts

Contracts are maybe half the work. A lending protocol that nobody can monitor is a protocol that fails silently. The delivery I consider complete includes an invariant test suite in Foundry — total supply shares always redeemable, no path where health factor improves for free, accrual monotonic — plus a fork-test harness replaying real historical volatility against the deployed parameters.

  • Foundry invariant and fuzz suites, with fork tests against mainnet state.
  • A liquidation bot reference implementation, because if you do not ship one, launch day has no liquidators.
  • A Next.js dashboard reading positions via Viem multicall: utilization, per-asset caps, and the distribution of health factors across all borrowers.
  • Alerting on the tail of that distribution — the number that tells you a bad debt event is coming hours before it arrives.

Frequently asked questions

How long does it take to build a DeFi lending protocol?
A single-market fork with modified parameters is roughly two to three weeks to testnet. A multi-asset protocol with its own rate model, isolated markets, liquidation bot, and monitoring dashboard is a two to four month build before audit, and audit plus remediation typically adds another four to eight weeks.
Should I fork Aave or Compound instead of building from scratch?
Fork if your differentiation is in parameters, chain, or assets — you inherit battle-tested accounting and years of audit coverage. Build from scratch only when your core mechanism genuinely differs, for example undercollateralized lending against off-chain credit, or intent-based matching rather than pooled liquidity. Forking a codebase you do not fully understand is the worst of both options.
What is the most common cause of DeFi lending protocol exploits?
Price oracle manipulation against thinly traded collateral, followed by share-inflation and rounding errors in the accounting layer. Reentrancy is well understood and largely designed out by modern patterns; valuation disagreements are not.
Can you build this on chains other than Ethereum?
Yes. The same architecture deploys to any EVM chain — Arbitrum, Base, Optimism, Polygon, BNB Chain — with the caveats that oracle feed availability, sequencer uptime checks on L2s, and DEX depth for liquidations all differ per chain and must be re-parameterised, not copied.

Building something like this?

I'm Harsh Mittal — I build production systems across Web3, AI, and financial infrastructure: smart contracts and DeFi protocols, RAG pipelines and LLM agents, market data infrastructure, and the interfaces on top of them. If this is the kind of problem you're working on, I can help you ship it.