DeFi lending is one of the core primitives in decentralized finance: suppliers deposit assets to earn yield, borrowers lock collateral to borrow, interest accrues, and liquidations enforce solvency. This guide is a non-promotional, practical reference for developers and teams designing and defi lending platform development.
What this post covers
- System architecture and core components
- Smart-contract responsibilities and patterns
- Interest-rate, collateral, and liquidation mechanics (formulas)
- Security, testing, and auditing checklist
- Recommended tech stack and MVP roadmap
- Short Solidity sketch illustrating core flows
High-level architecture
A lending protocol coordinates deposits and borrows, enforces collateralization, and manages interest accrual and liquidations.
Core pieces:
- Smart contract layer: on-chain ledger, rate models, collateral & liquidation logic.
- Oracle layer: robust price feeds (on-chain/off-chain) and TWAPs for flash protection.
- Off-chain services: indexing, monitoring, health checks, relayers for liquidations.
- Frontend: wallet integration, clear risk UI (LTV, liquidation price, health factor).
- Governance (optional for v1): parameter updates via multisig or on-chain voting.
Core smart-contract components
- Lending Pool / Market Manager: central accounting for deposits and borrows per asset; deposit, withdraw, borrow, repay.
- Interest Rate Model: computes borrow and supply rates from utilization (on-chain formula).
- Collateral Manager: enables assets as collateral, sets LTV, liquidation thresholds and close factors.
- Liquidator Module: performs liquidations and distributes rewards/incentives.
- Reserve / Fee Collector: accrues protocol fees and reserves.
- Interest-bearing tokens: (optional) wrappers representing deposited balances (e.g., aTokens/cTokens/ERC-4626).
- Access control / upgradeability: minimal and time-locked admin privileges; prefer proxy patterns only when necessary.
Key mechanics & formulas
- Utilization (U):
U = totalBorrows / (totalSupply + totalBorrows - reserves)
- Borrow rate (example simple model):
borrowRate = base + slope * U
- Supply rate:
supplyRate = borrowRate * U * (1 - reserveFactor)
- Health factor (simple):
healthFactor = (collateralValue * liquidationThreshold) / borrowedValue
Liquidation triggers when healthFactor < 1. Close factor limits how much debt a liquidator can repay in one liquidation.
Interest accrual (per-block or per-second)
- Track lastAccrualTimestamp and update totalBorrows, interestIndex when users interact; minimize state changes by lazy accrual during user actions.
Design principles
- Simplicity first: start with a small set of markets (1–3 assets).
- Explicit invariants: ensure invariant checks in critical paths (e.g., borrow allowed only if post-borrow healthFactor >= threshold).
- Least privilege: keep admin roles minimal, audited, and time-locked.
- Composability-friendly: follow standard token interfaces (ERC-20, ERC-4626) and emit rich events.
- Observability: emit events used by indexers; provide health metrics and dashboards.
Security best practices
- Use audited standard libraries (OpenZeppelin) for ERC-20 handling and access control.
- Safe ERC-20 transfers: handle non-compliant tokens; use safeTransfer/transferFrom wrappers.
- Reentrancy protection: add reentrancy guards on state-changing external functions.
- Oracle safety: use multiple feeds, TWAPs, and staleness checks; restrict oracle manipulation windows.
- Liquidation protections: implement minimum liquidation size and slippage controls.
- Limit upgradeability: if using proxies, ensure governance delay/time locks and multisig controls.
- Economic safety: stress-test for extreme market moves and cascade liquidations; simulate oracle failures and black swan events.
Testing & audit checklist
- Unit tests: deposit/withdraw, borrow/repay, interest accrual edge cases.
- Property-based and fuzz tests: inputs over a wide range; assert invariants.
- Integration tests: oracle failures, liquidations, multi-market interactions.
- Simulation & fork tests: mainnet fork scenarios with real token behaviors.
- Gas profiling: optimize hot paths and user UX costs.
- Third-party audit: at least one reputable audit before mainnet launch; perform bug bounty programs.
Recommended tech stack
- Smart contracts: Solidity (latest stable), OpenZeppelin, ERC-4626 where useful.
- Oracle: Chainlink or decentralized aggregator + fallback TWAP contract.
- Indexing/graph: The Graph or custom indexing service for frontend queries.
- Frontend: React + web3/Ethers.js + WalletConnect/EIP-1193 compatible providers.
- Dev tools: Hardhat (tests, forking), Foundry (fast fuzzing), Tenderly (debugging), Slither/Maudit for static analysis.
- Monitoring: Prometheus/Grafana for off-chain metrics, alerts for unusual on-chain events.
MVP roadmap (practical sequence)
- Core single-asset market (stablecoin) with deposit/borrow/repay/withdraw.
- Interest rate model + accrual logic.
- Collateral manager + simple liquidation mechanism.
- Oracle integration with staleness checks.
- Frontend and simple analytics dashboard.
- Add one or two more markets and refine rate models.
- Security audit and staged testnet/mainnet rollout with bug bounty.
Gas & UX tips
- Bundle multiple state updates where possible to reduce user gas (but keep atomicity).
- Expose aggregated operations (e.g., deposit-and-mint) to simplify UX.
- Offer clear gas estimations and warnings for risky actions (near liquidation).
- Minimize on-chain storage churn; use mappings and checkpoints.
Simple Solidity sketch (very simplified)
Note: illustrative only — not production-ready. Omit forking, oracles, and safety checks in a real project.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
interface IERC20 {
function transferFrom(address, address, uint) external returns (bool);
function transfer(address, uint) external returns (bool);
function balanceOf(address) external view returns (uint);
function approve(address, uint) external returns (bool);
}
contract SimpleLending {
IERC20 public asset; // token users deposit / borrow
mapping(address => uint) public deposits;
mapping(address => uint) public borrows;
uint public totalDeposits;
uint public totalBorrows;
uint public reserveFactor = 10; // percent
constructor(IERC20 _asset) { asset = _asset; }
function deposit(uint amount) external {
require(amount > 0, "zero");
asset.transferFrom(msg.sender, address(this), amount);
deposits[msg.sender] += amount;
totalDeposits += amount;
}
function withdraw(uint amount) external {
require(deposits[msg.sender] >= amount, "insufficient");
// check protocol liquidity and invariants in production
deposits[msg.sender] -= amount;
totalDeposits -= amount;
asset.transfer(msg.sender, amount);
}
function borrow(uint amount) external {
// simplified collateral check; in production use multi-asset collateral valuation
uint collateral = deposits[msg.sender];
require(collateral * 50 / 100 >= borrows[msg.sender] + amount, "insufficient collateral (50% LTV)");
borrows[msg.sender] += amount;
totalBorrows += amount;
asset.transfer(msg.sender, amount);
}
function repay(uint amount) external {
require(amount > 0, "zero");
asset.transferFrom(msg.sender, address(this), amount);
uint toRepay = amount > borrows[msg.sender] ? borrows[msg.sender] : amount;
borrows[msg.sender] -= toRepay;
totalBorrows -= toRepay;
}
}
Operational checklist before launch
- Mainnet fork tests and flashloan attack simulations.
- Audits and resolved findings.
- Monitoring, alerting, and automated liquidation bots tested.
- Liquidity bootstrapping plan and gradual market opening (limit initial TVL).
- Bug bounty and emergency pause mechanisms.
Further reading & next steps
- Study open-source protocols (Aave, Compound) for architecture patterns and gas optimizations.
- Practice building a market on a testnet, integrate Chainlink oracles, and run forked scenario stress tests.
- Iterate rate models using simulation tooling to find sustainable yields and incentives.
Top comments (0)