Part 2 β Module 4: Lending & Borrowing
Difficulty: Advanced
Estimated reading time: ~60 minutes | Exercises: ~4-5 hours
π Table of Contents
The Lending Model from First Principles
- How DeFi Lending Works
- Key Parameters
- Interest Rate Models: The Kinked Curve
- Interest Accrual: Indexes and Scaling
- Deep Dive: RAY Arithmetic
- Deep Dive: Compound Interest Approximation
- Exercise: Build the Math
Aave V3 Architecture β Supply and Borrow
- Contract Architecture Overview
- aTokens: Interest-Bearing Receipts
- Debt Tokens: Tracking Whatβs Owed
- Read: Supply Flow
- Read: Borrow Flow
- Exercise: Fork and Interact
Aave V3 β Risk Modes and Advanced Features
- Efficiency Mode (E-Mode)
- Isolation Mode
- Supply and Borrow Caps
- Read: Configuration Bitmap
- Deep Dive: Bitmap Encoding/Decoding
Compound V3 (Comet) β A Different Architecture
- The Single-Asset Model
- Comet Contract Architecture
- Principal and Index Accounting
- Read: Comet.sol Core Functions
Liquidation Mechanics
- Why Liquidation Exists
- The Liquidation Flow
- Aave V3 Liquidation
- Compound V3 Liquidation (βAbsorbβ)
- Liquidation Bot Economics
Build Exercise: Simplified Lending Protocol
Synthesis and Advanced Patterns
- Architectural Comparison: Aave V3 vs Compound V3
- Bad Debt and Protocol Solvency
- The Liquidation Cascade Problem
- Emerging Patterns (Morpho Blue, Euler V2)
- Aave V3.1 / V3.2 / V3.3 Updates
π‘ The Lending Model from First Principles
Why this matters: Lending is where everything youβve learned converges. Token mechanics (Module 1) govern how assets move in and out. Oracle integration (Module 3) determines collateral valuation and liquidation triggers. And the interest rate math shares DNA with the constant product formula from AMMs (Module 2) β both are mechanism design problems where smart contracts use mathematical curves to balance supply and demand without human intervention.
Real impact: Lending protocols are the highest-TVL category in DeFi. Aave holds $18B+ TVL (2024), Compound $3B+, Spark (MakerDAO) $2.5B+. Combined, lending protocols represent >$30B in user deposits.
Real impact β exploits: Lending protocols have been the target of some of DeFiβs largest hacks:
- Euler Finance ($197M, March 2023) β donation attack bypassing health checks
- Radiant Capital ($4.5M, January 2024) β flash loan rounding exploit on newly activated empty market
- Rari Capital/Fuse ($80M, May 2022) β reentrancy in pool withdrawals
- Cream Finance ($130M, October 2021) β oracle manipulation
- Hundred Finance ($7M, March 2022) β ERC-777 reentrancy
- Venus Protocol ($11M, May 2023) β stale oracle pricing
If youβre building DeFi products, youβll either build a lending protocol, integrate with one, or compete with one. Understanding the internals is non-negotiable.
π‘ Concept: How DeFi Lending Works
Why this matters: Traditional lending requires a bank to assess creditworthiness, set terms, and enforce repayment. DeFi lending replaces all of this with overcollateralization and algorithmic liquidation.
The core loop:
- Suppliers deposit assets (e.g., USDC) into a pool. They earn interest from borrowers.
- Borrowers deposit collateral (e.g., ETH), then borrow from the pool (e.g., USDC) up to a limit determined by their collateralβs value and the protocolβs risk parameters.
- Interest accrues continuously. Borrowers pay it; suppliers receive it (minus a protocol cut called the reserve factor).
- If collateral value drops (or debt grows) past a threshold, the position becomes eligible for liquidation β a third party repays part of the debt and receives the collateral at a discount.
No credit checks. No loan officers. No repayment schedule. Borrowers can hold positions indefinitely as long as they remain overcollateralized.
Used by: Aave V3 (May 2022 deployment), Compound V3 (Comet) (August 2022), Spark (fork of Aave V3, May 2023)
π‘ Concept: Key Parameters
Loan-to-Value (LTV): The maximum ratio of borrowed value to collateral value at the time of borrowing. If ETH has an LTV of 80%, depositing $10,000 of ETH lets you borrow up to $8,000.
Liquidation Threshold (LT): The ratio at which a position becomes liquidatable. Always higher than LTV (e.g., 82.5% for ETH). The gap between LTV and LT is the borrowerβs safety buffer.
Health Factor: The single number that determines whether a position is safe:
Health Factor = (Collateral Value Γ Liquidation Threshold) / Debt Value
HF > 1 = safe. HF < 1 = eligible for liquidation. HF = 1.5 means the collateral could lose 33% of its value before liquidation.
π Deep Dive: Health Factor Calculation Step-by-Step
Scenario: Alice deposits 5 ETH and 10,000 USDC as collateral, then borrows 8,000 DAI.
Step 1: Get collateral values in USD (from oracle)
ETH price = $2,000 β 5 ETH Γ $2,000 = $10,000
USDC price = $1.00 β 10,000 USDC Γ $1 = $10,000
Total collateral = $20,000
Step 2: Apply each assetβs Liquidation Threshold
ETH LT = 82.5% β $10,000 Γ 0.825 = $8,250
USDC LT = 85.0% β $10,000 Γ 0.850 = $8,500
Weighted collateral = $16,750
Step 3: Get total debt value in USD
DAI price = $1.00 β 8,000 DAI Γ $1 = $8,000
Step 4: Compute Health Factor
HF = Weighted Collateral / Total Debt
HF = $16,750 / $8,000
HF = 2.09
What does 2.09 mean? Aliceβs collateral (after risk-weighting) is 2.09Γ her debt. Her position can absorb a ~52% collateral value drop before liquidation.
When does Alice get liquidated? When HF drops below 1.0:
If ETH drops to $1,200 (-40%):
ETH value = 5 Γ $1,200 = $6,000 β weighted = $6,000 Γ 0.825 = $4,950
USDC value = $10,000 β weighted = $10,000 Γ 0.850 = $8,500
HF = ($4,950 + $8,500) / $8,000 = 1.68 β still safe
If ETH drops to $400 (-80%):
ETH value = 5 Γ $400 = $2,000 β weighted = $2,000 Γ 0.825 = $1,650
USDC value = $10,000 β weighted = $10,000 Γ 0.850 = $8,500
HF = ($1,650 + $8,500) / $8,000 = 1.27 β still safe (USDC cushion!)
If ETH drops to $0 (100% crash):
HF = $8,500 / $8,000 = 1.06 β still safe! USDC collateral alone covers the debt
Key takeaway: Multi-collateral positions are more resilient. The stablecoin collateral acts as a floor.
Example: On Aave V3 Ethereum mainnet, ETH has LTV = 80.5%, LT = 82.5%. If you deposit $10,000 ETH:
- Maximum initial borrow: $8,050 (80.5%)
- Liquidation triggered when debt/collateral exceeds 82.5%
- Safety buffer: 2% price movement room before liquidation risk
Liquidation Bonus (Penalty): The discount a liquidator receives on seized collateral (e.g., 5%). This incentivizes liquidators to monitor and act quickly, keeping the protocol solvent.
Why this matters: Without sufficient liquidation bonus, liquidators have no incentive to act during high gas prices or volatile markets. Too high, and liquidations become excessively punitive for borrowers.
Reserve Factor: The percentage of interest that goes to the protocol treasury rather than suppliers (typically 10β25%). This builds a reserve fund for bad debt coverage.
Used by: Aave V3 reserves range from 10-20% depending on asset, Compound V3 uses protocol-specific reserve factors
Close Factor: How much of the debt a liquidator can repay in a single liquidation. Aave V3 uses 50% normally, but allows 100% when HF < 0.95 to clear dangerous positions faster.
Common pitfall: Setting close factor too high (100% always) can lead to liquidation cascades where all collateral is dumped at once, crashing prices further. Gradual liquidation (50%) reduces market impact.
π» Quick Try:
Before diving into the math, read live Aave V3 data on a mainnet fork. In your Foundry test:
// Paste into a test file and run with --fork-url
interface IPool {
struct ReserveData {
uint256 configuration;
uint128 liquidityIndex; // RAY (27 decimals)
uint128 currentLiquidityRate; // RAY β APY for suppliers
uint128 variableBorrowIndex;
uint128 currentVariableBorrowRate; // RAY β APY for borrowers
uint128 currentStableBorrowRate;
uint40 lastUpdateTimestamp;
uint16 id;
address aTokenAddress;
address stableDebtTokenAddress;
address variableDebtTokenAddress;
address interestRateStrategyAddress;
uint128 accruedToTreasury;
uint128 unbacked;
uint128 isolationModeTotalDebt;
}
function getReserveData(address asset) external view returns (ReserveData memory);
}
function testReadAaveReserveData() public {
IPool pool = IPool(0x87870Bca3F3fD6335C3F4ce8392D69350B4fA4E2);
address usdc = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48;
IPool.ReserveData memory data = pool.getReserveData(usdc);
// Convert RAY rates to human-readable APY
uint256 supplyAPY = data.currentLiquidityRate / 1e23; // basis points
uint256 borrowAPY = data.currentVariableBorrowRate / 1e23;
emit log_named_uint("USDC Supply APY (bps)", supplyAPY);
emit log_named_uint("USDC Borrow APY (bps)", borrowAPY);
emit log_named_uint("Liquidity Index (RAY)", data.liquidityIndex);
emit log_named_uint("Borrow Index (RAY)", data.variableBorrowIndex);
}
Run with forge test --match-test testReadAaveReserveData --fork-url $ETH_RPC_URL -vv. See the live rates and indexes β these are the numbers the kinked curve produces.
π‘ Concept: Interest Rate Models: The Kinked Curve
Why this matters: The interest rate model is the mechanism that balances supply and demand for each asset pool. Every major lending protocol uses some variant of a piecewise linear βkinkedβ curve.
Utilization rate:
U = Total Borrowed / Total Supplied
When U is low, thereβs plenty of liquidity β rates should be low to encourage borrowing. When U is high, liquidity is scarce β rates should spike to attract suppliers and discourage borrowing. If U hits 100%, suppliers canβt withdraw. Thatβs a crisis.
Real impact: During the March 2020 crash, Aaveβs USDC borrow rate spiked past 50% APR when utilization hit 98%. This was working as designed β extreme rates forced borrowers to repay, restoring liquidity.
The two-slope model:
Below the optimal utilization (the βkink,β typically 80β90%):
BorrowRate = BaseRate + (U / U_optimal) Γ Slope1
Above the optimal utilization:
BorrowRate = BaseRate + Slope1 + ((U - U_optimal) / (1 - U_optimal)) Γ Slope2
Slope2 is dramatically steeper than Slope1. This creates a sharp increase in rates past the kink, which acts as a self-correcting mechanism β expensive borrowing pushes utilization back down.
π Deep Dive: Visualizing the Kinked Curve
Borrow Rate
(APR)
β
110%β β± β Slope2 (100%)
β β± Steep! Forces borrowers
β β± to repay, restoring
β β± utilization below kink
β β±
β β±
β β±
β β±
β β±
β β±
β β β β β β β β β β ββ±β β β β β β β β β
β β±Β·β
8%β β±Β·Β·Β·β
β β±Β·Β·Β·Β·Β·β
β β±Β·Β·Β·Β·Β·Β·Β·β β Slope1 (8%)
β β±Β·Β·Β·Β·Β·Β·Β·Β·Β·β Gentle: borrowing is cheap
β β±Β·Β·Β·Β·Β·Β·Β·Β·Β·Β·Β·β when liquidity is ample
β β±Β·Β·Β·Β·Β·Β·Β·Β·Β·Β·Β·Β·Β·β
β β±Β·Β·Β·Β·Β·Β·Β·Β·Β·Β·Β·Β·Β·Β·Β·β
β β±Β·Β·Β·Β·Β·Β·Β·Β·Β·Β·Β·Β·Β·Β·Β·Β·Β·β
β β±Β·Β·Β·Β·Β·Β·Β·Β·Β·Β·Β·Β·Β·Β·Β·Β·Β·Β·Β·β
2%ββ±Β·Β·Β·Β·Β·Β·Β·Β·Β·Β·Β·Β·Β·Β·Β·Β·Β·Β·Β·Β·β β Base rate
ββββββββββββββββββββββββΌββββββββββββββββββββ Utilization
0% 80% 100%
"The Kink"
(Optimal Utilization)
Note: Production values vary by asset. Stablecoins often use Slope2 = 60-80%,
volatile assets 200-300%+. The table below uses moderate values for clarity.
Reading the curve with numbers (USDC-like parameters):
| Utilization | Borrow Rate | Whatβs happening |
|---|---|---|
| 0% | 2% (base) | No borrows β minimum rate |
| 40% | 2% + 4% = 6% | Normal borrowing β gentle slope |
| 80% (kink) | 2% + 8% = 10% | At optimal β slope about to steepen |
| 85% | 10% + ~25% = 35% | Past kink β rates spiking rapidly |
| 90% | 10% + ~50% = 60% | Severe β borrowers forced to repay |
| 95% | 10% + ~75% = 85% | Emergency β liquidity nearly gone |
| 100% | 10% + 100% = 110% | Crisis β suppliers canβt withdraw |
Why this works as mechanism design:
- Below the kink: rates are predictable and affordable β borrowers stay, utilization is healthy
- At the kink: rates start climbing β signal to borrowers that liquidity is tightening
- Past the kink: rates explode β economic force that pushes borrowers to repay
- The kink acts as a βthermostatβ β the system self-corrects without governance intervention
Deep dive: Aave interest rate strategy contracts, Compound V3 rate model, RareSkills guide to interest rate models
Supply rate derivation:
SupplyRate = BorrowRate Γ U Γ (1 - ReserveFactor)
Suppliers earn a fraction of what borrowers pay, reduced by utilization (not all capital is lent out) and the reserve factor (the protocolβs cut).
Numeric example (USDC-like parameters):
Pool state: $100M supplied, $80M borrowed β U = 80%
Borrow rate at 80% utilization (from kinked curve) = 10% APR
Reserve factor = 15%
SupplyRate = 10% Γ 0.80 Γ (1 - 0.15)
= 10% Γ 0.80 Γ 0.85
= 6.8% APR
Where the interest goes:
Borrowers pay: $80M Γ 10% = $8M/year
Suppliers receive: $100M Γ 6.8% = $6.8M/year
Protocol treasury: $8M - $6.8M = $1.2M/year (= reserve factor's cut)
Why suppliers earn less than borrowers pay: Two factors compound β not all supplied capital is borrowed (utilization < 100%), and the protocol takes a cut (reserve factor). This βspreadβ funds the protocol treasury and bad debt reserves.
Common pitfall: Expecting supply rate to equal borrow rate. Suppliers always earn less due to utilization < 100% and reserve factor. If U = 80% and reserve factor = 15%, suppliers earn only
BorrowRate Γ 0.8 Γ 0.85 = 68%of the gross borrow rate.
π‘ Concept: Interest Accrual: Indexes and Scaling
Why this matters: Interest doesnβt accrue by updating every userβs balance every second. That would be impossibly expensive. Instead, protocols use a global index that compounds over time:
currentIndex = previousIndex Γ (1 + ratePerSecond Γ timeElapsed)
A userβs actual balance is:
actualBalance = storedPrincipal Γ currentIndex / indexAtDeposit
When a user deposits, the protocol stores their principal and the current index. When they withdraw, the protocol computes their balance using the latest index. This means the protocol only needs to update one global variable, not millions of individual balances.
Used by: Aave V3 supply index, Compound V3 supply/borrow indexes, every modern lending protocol
π Deep Dive: Index Accrual β A Numeric Walkthrough
Setup: A pool with 5% APR borrow rate, two users deposit at different times.
Step 0 β Pool creation:
supplyIndex = 1.000000000000000000000000000 (1e27 in RAY)
Time: Tβ
Step 1 β Alice deposits 1,000 USDC at Tβ:
Alice's scaledBalance = 1,000 / supplyIndex = 1,000 / 1.0 = 1,000
Alice's balanceOf() = 1,000 Γ 1.0 = 1,000 USDC β
Step 2 β 6 months pass (5% APR β ~2.5% for 6 months):
ratePerSecond = 5% / 31,536,000 = 0.00000000158549 per second
timeElapsed = 15,768,000 seconds (β 6 months)
supplyIndex = 1.0 Γ (1 + 0.00000000158549 Γ 15,768,000)
= 1.0 Γ 1.025
= 1.025000000000000000000000000
Alice's balanceOf() = 1,000 Γ 1.025 / 1.0 = 1,025 USDC (+$25 interest)
Step 3 β Bob deposits 2,000 USDC at Tβ + 6 months:
Current supplyIndex = 1.025
Bob's scaledBalance = 2,000 / 1.025 = 1,951.22
Bob's balanceOf() = 1,951.22 Γ 1.025 = 2,000 USDC β (no interest yet)
Step 4 β Another 6 months pass (full year from Tβ):
supplyIndex = 1.025 Γ (1 + 0.00000000158549 Γ 15,768,000)
= 1.025 Γ 1.025
= 1.050625000000000000000000000
Alice's balanceOf() = 1,000.00 Γ 1.050625 / 1.0 = 1,050.63 USDC (+$50.63 total β 1 year)
Bob's balanceOf() = 1,951.22 Γ 1.050625 / 1.025 = 2,050.00 USDC (+$50.00 β 6 months)
Why this is elegant:
- Only ONE storage write per pool interaction (update the global index)
- Alice and Bobβs balances are computed on-the-fly from their
scaledBalanceand the current index - No iteration over users, no batch updates, no cron jobs
- Works for millions of users with the same O(1) gas cost
The pattern: actualBalance = scaledBalance Γ currentIndex / indexAtDeposit
This is the same math behind ERC-4626 vault shares (Module 7) and staking reward distribution.
Compound interest approximation: Aave V3 uses a binomial expansion to approximate (1 + r)^n on-chain, which is cheaper than computing exponents. For small r (per-second rates are tiny), the approximation is extremely accurate.
Deep dive: Aave V3 MathUtils.sol β compound interest calculation
π Deep Dive: RAY Arithmetic β Why 27 Decimals?
The problem: Solidity has no floating point. Lending protocols need to represent per-second interest rates like 0.000000001585489599 (5% APR / 31,536,000 seconds). With 18-decimal WAD precision, this would be 1585489599 β losing 9 digits of precision. Over a year of compounding, those lost digits accumulate into significant errors.
The solution: RAY uses 27 decimals (1e27 = 1 RAY), giving 9 extra digits of precision compared to WAD:
WAD (18 decimals): 1.000000000000000000
RAY (27 decimals): 1.000000000000000000000000000
5% APR per-second rate:
As WAD: 0.000000001585489599 β 1585489599 (10 significant digits)
As RAY: 0.000000001585489599000000000 β 1585489599000000000 (19 significant digits)
How rayMul and rayDiv work:
// From Aave V3 WadRayMath.sol
uint256 constant RAY = 1e27;
uint256 constant HALF_RAY = 0.5e27;
// rayMul: multiply two RAY values, round to nearest
function rayMul(uint256 a, uint256 b) internal pure returns (uint256) {
return (a * b + HALF_RAY) / RAY;
}
// rayDiv: divide two RAY values, round to nearest
function rayDiv(uint256 a, uint256 b) internal pure returns (uint256) {
return (a * RAY + b / 2) / b;
}
Step-by-step example β computing Aliceβs aToken balance:
supplyIndex = 1.025 RAY = 1_025_000_000_000_000_000_000_000_000
Alice's scaledBalance = 1000e6 (1,000 USDC, 6 decimals)
// balanceOf() calls: scaledBalance.rayMul(currentIndex)
// rayMul(1000e6, 1.025e27)
a = 1_000_000_000 // 1000 USDC in 6-decimal
b = 1_025_000_000_000_000_000_000_000_000 // 1.025 RAY
a * b = 1_025_000_000_000_000_000_000_000_000_000_000_000
+ HALF_RAY = ... + 500_000_000_000_000_000_000_000_000
/ RAY = 1_025_000_000 // 1,025 USDC β
Rounding direction matters for protocol solvency:
| Operation | Round Direction | Why |
|---|---|---|
| Deposit β scaledBalance | Round down | Fewer shares = less claim on pool |
| Withdraw β actual amount | Round down | User gets slightly less |
| Borrow β scaledDebt | Round up | More debt recorded |
| Repay β remaining debt | Round up | Slightly more left to repay |
The rule: Always round against the user, in favor of the protocol. This prevents rounding-based drain attacks where millions of tiny operations each round in the userβs favor, slowly bleeding the pool.
Used by: WadRayMath.sol β Aaveβs core math library. Compound V3 uses a simpler approach with
BASE_INDEX_SCALE = 1e15.
π Deep Dive: Compound Interest Approximation
The problem: True compound interest requires computing (1 + r)^n where r is the per-second rate and n is seconds elapsed. Exponentiation is expensive on-chain β and n can be millions (months of elapsed time).
Aaveβs solution: Use a Taylor series expansion truncated at 3 terms:
(1 + r)^n β 1 + nΒ·r + nΒ·(n-1)Β·rΒ²/2 + nΒ·(n-1)Β·(n-2)Β·rΒ³/6
β β β
linear quadratic cubic
term correction correction
Why this works: Per-second rates are tiny (on the order of 1e-9 to 1e-8). When r is small:
rΒ²is vanishingly small (~1e-18)rΒ³is essentially zero (~1e-27)- Three terms give accuracy to 27+ decimal places β well within RAY precision
Numeric example β 10% APR over 30 days:
r = 10% / 31,536,000 = 3.170979198e-9 per second
n = 30 Γ 86,400 = 2,592,000 seconds
3-term approx: 1 + nΒ·r + nΒ·(n-1)Β·rΒ²/2 + nΒ·(n-1)Β·(n-2)Β·rΒ³/6
Term 1 (linear): n Γ r = 0.008219178...
Term 2 (quadratic): nΓ(n-1) Γ rΒ² / 2 = 0.000033778...
Term 3 (cubic): nΓ(n-1)Γ(n-2) Γ rΒ³ / 6 = 0.000000092...
3-term approximation: 1.008253048...
True compound value: (1 + r)^n = 1.008253048... β essentially identical!
Simple interest: 1 + nΓr = 1.008219178... β 0.003% lower (missing quadratic+cubic)
The 3-term approximation matches the true compound value to ~10 decimal places. The 4th term (n(n-1)(n-2)(n-3)Β·rβ΄/24) is on the order of 1e-10 β negligible at RAY precision. This is why Aave stops at 3 terms.
Why not just use n Γ r (simple interest)? Over long periods, the quadratic term matters:
10% APR over 1 year:
Simple interest (1 term): 1.10000 (+10.0%)
Aave approximation (3 terms): 1.10517 (+10.517%)
True compound: 1.10517 (+10.517%)
Error: <0.001% β the 3-term approximation matches!
Simple interest error: 0.517% β real money at $18B TVL
In code (MathUtils.sol):
function calculateCompoundedInterest(uint256 rate, uint40 lastUpdateTimestamp, uint256 currentTimestamp)
internal pure returns (uint256)
{
uint256 exp = currentTimestamp - lastUpdateTimestamp;
if (exp == 0) return RAY;
uint256 expMinusOne = exp - 1;
uint256 expMinusTwo = exp > 2 ? exp - 2 : 0;
uint256 basePowerTwo = rate.rayMul(rate); // rΒ²
uint256 basePowerThree = basePowerTwo.rayMul(rate); // rΒ³
uint256 secondTerm = exp * expMinusOne * basePowerTwo / 2;
uint256 thirdTerm = exp * expMinusOne * expMinusTwo * basePowerThree / 6;
return RAY + (rate * exp) + secondTerm + thirdTerm;
}
Key insight: This function runs on every supply(), borrow(), repay(), and withdraw() call. Using a 3-term approximation instead of iterative exponentiation saves thousands of gas per interaction β across millions of transactions, this is a significant optimization.
Compound V3βs approach: Comet uses simple interest per-period (
index Γ (1 + rate Γ elapsed)), which is slightly less accurate for long gaps but even cheaper. The difference is negligible becauseaccrueInternal()is called frequently.
π― Build Exercise: Interest Rate Model
Workspace: workspace/src/part2/module4/exercise1-interest-rate/ β starter file: InterestRateModel.sol, tests: InterestRateModel.t.sol
Implement a complete interest rate model contract that covers all the math from this section:
- RAY multiplication (
rayMul) β the bread-and-butter operation of all lending protocol math. A referencerayDivimplementation is provided for you to study. - Utilization rate (
getUtilization) β the x-axis of the kinked curve - Kinked borrow rate (
getBorrowRate) β the two-slope curve with the gentle slope below optimal and the steep slope above - Supply rate (
getSupplyRate) β derived from borrow rate, utilization, and reserve factor - Compound interest approximation (
calculateCompoundInterest) β the 3-term Taylor expansion used by Aave V3βs MathUtils
All rates use RAY precision (27 decimals), matching Aave V3βs internal math. The exercise scaffold has detailed hints and worked examples in the TODO comments.
Common pitfall: Integer overflow when multiplying rates. Always ensure intermediate calculations donβt overflow. Use smaller precision (e.g., per-second rates in RAY = 27 decimals) rather than storing APY directly.
π― Goal: Internalize RAY arithmetic and the kinked curve math before moving to the full lending pool. This is pure math β no tokens, no protocol state.
π Summary: The Lending Model
Covered:
- How DeFi lending works: overcollateralization β interest accrual β liquidation loop
- Key parameters: LTV, Liquidation Threshold, Health Factor, Liquidation Bonus, Reserve Factor, Close Factor
- Interest rate models: the two-slope kinked curve and why slope2 is steep (self-correcting mechanism)
- Supply rate derivation from borrow rate, utilization, and reserve factor (with numeric example)
- Index-based interest accrual: global index pattern that scales to millions of users
- RAY arithmetic: why 27 decimals, rayMul/rayDiv mechanics, rounding direction conventions
- Compound interest approximation: 3-term Taylor expansion, accuracy vs gas trade-off, Aaveβs MathUtils implementation
Key insight: The kinked curve is mechanism design β it uses price signals (rates) to automatically rebalance supply and demand without human intervention.
Next: Aave V3 architecture β how these concepts are implemented in production code.
πΌ Job Market Context
What DeFi teams expect you to know about lending fundamentals:
-
βExplain how a lending protocolβs interest rate model works.β
- Good answer: Describes the kinked curve, utilization-based rates, slope1/slope2 distinction
- Great answer: Explains why the kink exists (self-correcting mechanism), how supply rate derives from borrow rate Γ utilization Γ (1 - reserve factor), and mentions that Compound V3 uses independent curves vs Aaveβs derived approach
-
βHow does interest accrue without updating every userβs balance?β
- Good answer: Global index pattern β store principal and index at deposit, compute live balance as
principal Γ currentIndex / depositIndex - Great answer: Explains the gas motivation (O(1) vs O(n) updates), mentions the compound interest approximation in Aaveβs MathUtils.sol, and notes this same pattern appears in ERC-4626 vaults and staking contracts
- Good answer: Global index pattern β store principal and index at deposit, compute live balance as
-
βWhat happens if a userβs health factor drops below 1?β
- Good answer: Position becomes liquidatable, a third party repays part of the debt and receives collateral at a discount
- Great answer: Explains close factor mechanics (50% vs 100% at HF < 0.95 in Aave V3), liquidation bonus calibration trade-offs, minimum position rules to prevent dust, and Compound V3βs absorb/auction alternative
Interview red flags:
- Saying lending protocols βchargeβ interest (they donβt β interest is algorithmic, not invoiced)
- Not understanding why collateral doesnβt earn interest in Compound V3
- Confusing LTV (max borrow ratio) with Liquidation Threshold (liquidation trigger ratio)
Pro tip: The single most impressive thing you can do in a lending protocol interview is articulate the trade-offs between Aave and Compound architectures. This signals senior-level thinking.
π‘ Aave V3 Architecture β Supply and Borrow
π‘ Concept: Contract Architecture Overview
Why this matters: Aave V3 (deployed May 2022) uses a proxy pattern with logic delegated to libraries. Understanding this architecture is essential for reading production lending code.
The entry point is the Pool contract (behind a proxy), which delegates to specialized logic libraries:
User β Pool (proxy)
ββ SupplyLogic
ββ BorrowLogic
ββ LiquidationLogic
ββ FlashLoanLogic
ββ BridgeLogic
ββ EModeLogic
Supporting contracts:
- PoolAddressesProvider: Registry for all protocol contracts. Single source of truth for addresses.
- AaveOracle: Wraps Chainlink feeds. Each asset has a registered price source.
- PoolConfigurator: Governance-controlled contract that sets risk parameters (LTV, LT, reserve factor, caps).
- PriceOracleSentinel: L2-specific β checks sequencer uptime before allowing liquidations.
Deep dive: Aave V3 technical paper, MixBytes architecture analysis
π‘ Concept: aTokens: Interest-Bearing Receipts
Why this matters: When you supply USDC to Aave, you receive aUSDC. This is an ERC-20 token whose balance automatically increases over time as interest accrues. You donβt need to claim anything β your balanceOf() result grows continuously.
How it works internally:
aTokens store a βscaled balanceβ (principal divided by the current liquidity index). The balanceOf() function multiplies the scaled balance by the current index:
function balanceOf(address user) public view returns (uint256) {
return scaledBalanceOf(user).rayMul(pool.getReserveNormalizedIncome(asset));
}
getReserveNormalizedIncome() returns the current supply index, which grows every second based on the supply rate. This design means:
- Transferring aTokens transfers the proportional claim on the pool (including future interest)
- aTokens are composable β they can be used in other DeFi protocols as yield-bearing collateral
- No explicit βharvestβ or βclaimβ step for interest
Used by: Yearn V3 vaults accept aTokens as deposits, Convex wraps aTokens for boosted rewards, many protocols use aTokens as collateral in other lending markets
Common pitfall: Assuming aToken balance is static. If you cache
balanceOf()at t0 and check again at t1, the balance will have increased. Always read the current value.
π‘ Concept: Debt Tokens: Tracking Whatβs Owed
When you borrow, the protocol mints variableDebtTokens (or stable debt tokens, though stable rate borrowing is being deprecated) to your address. These are non-transferable ERC-20 tokens whose balance increases over time as interest accrues on your debt.
The mechanics mirror aTokens but use the borrow index instead of the supply index:
function balanceOf(address user) public view returns (uint256) {
return scaledBalanceOf(user).rayMul(pool.getReserveNormalizedVariableDebt(asset));
}
Debt tokens being non-transferable is a deliberate security choice β you canβt transfer your debt to someone else without their consent (credit delegation notwithstanding).
Common pitfall: Trying to
transfer()debt tokens. This reverts. Debt can only be transferred via credit delegation (approveDelegation()).
π Read: Supply Flow
Source: aave-v3-core/contracts/protocol/libraries/logic/SupplyLogic.sol
Trace the supply path through Aave V3:
- User calls
Pool.supply(asset, amount, onBehalfOf, referralCode) - Pool delegates to
SupplyLogic.executeSupply() - Logic validates the reserve is active and not paused
- Updates the reserveβs indexes (accrues interest up to this moment)
- Transfers the underlying asset from user to the aToken contract
- Mints aTokens to the
onBehalfOfaddress (scaled by current index) - Updates the userβs configuration bitmap (tracks which assets are supplied/borrowed)
π Read: Borrow Flow
Source: BorrowLogic.sol
- User calls
Pool.borrow(asset, amount, interestRateMode, referralCode, onBehalfOf) - Pool delegates to
BorrowLogic.executeBorrow() - Logic validates: reserve active, borrowing enabled, amount β€ borrow cap
- Validates the userβs health factor will remain > 1 after the borrow
- Mints debt tokens to the borrower (or
onBehalfOffor credit delegation) - Transfers the underlying asset from the aToken contract to the user
- Updates the interest rate for the reserve (utilization changed)
Key insight: The health factor check happens before the tokens are transferred. If the borrow would make the position undercollateralized, it reverts.
Common pitfall: Not accounting for accrued interest when calculating max borrow. Debt grows continuously, so the maximum borrowable amount decreases over time even if collateral price stays constant.
π How to Study Aave V3 Architecture
The Aave V3 codebase is ~15,000+ lines across many libraries. Hereβs how to approach it without getting lost:
-
Start with the Pool proxy entry points β Open Pool.sol and read just the function signatures. Each one (
supply,borrow,repay,withdraw,liquidationCall) delegates to a Logic library. Map the routing: which function calls which library. -
Trace one complete flow end-to-end β Pick
supply(). Follow it into SupplyLogic.sol. Read every line ofexecuteSupply(). Note: index update β transfer β mint aTokens β update user config bitmap. Draw this as a sequence diagram. -
Understand the data model β Read DataTypes.sol. The
ReserveDatastruct is the central state. Map each field to what it controls (indexes for interest, configuration bitmap for risk params, address pointers for aToken/debtToken). -
Read the index math β Open ReserveLogic.sol and trace
updateState()β_updateIndexes(). This is the compound interest accumulation. Then read howbalanceOf()in AToken.sol uses the index to compute the live balance. -
Then read ValidationLogic.sol β This is where all the safety checks live: health factor validation, borrow cap checks, E-Mode constraints. Read
validateBorrow()to understand every condition that must pass before a borrow succeeds.
Donβt get stuck on: The configuration bitmap encoding initially. Itβs clever bit manipulation (Part 1 Module 1 territory) but you can treat getters as black boxes on first pass. Focus on the flow: entry point β logic library β state update β token operations.
π‘ Concept: Credit Delegation
The onBehalfOf parameter enables credit delegation: Alice can allow Bob to borrow using her collateral. Aliceβs health factor is affected, but Bob receives the borrowed assets. This is done through approveDelegation() on the debt token contract.
Used by: InstaDapp uses credit delegation for automated strategies, institutional custody solutions use it for sub-account management
π― Build Exercise: Simplified Lending Pool
Workspace: workspace/src/part2/module4/exercise2-lending-pool/ β starter file: LendingPool.sol, tests: LendingPool.t.sol
Build a minimal but correct lending pool that puts the Aave V3 concepts into practice. The scaffold provides the state layout (Reserve struct, user positions, collateral configs) and RAY math helpers. You implement the core protocol logic:
supply(amount)β transfer tokens in, compute scaled deposit using the liquidity indexwithdraw(amount)β convert scaled balance back, validate sufficient fundsdepositCollateral(token, amount)β post collateral (no interest earned, like Compound V3)borrow(amount)β record scaled debt, enforce health factor >= 1.0repay(amount)β burn scaled debt, cap at actual debt to prevent overpaymentaccrueInterest()β update both indexes using linear interest (simplified from Aaveβs compound)getHealthFactor(user)β iterate collateral tokens, fetch oracle prices, compute weighted collateral vs debt
The exercise tests cover: happy path supply/withdraw/borrow/repay, interest accrual over time, health factor computation, over-borrow reverts, and multi-supplier independence.
π― Goal: Understand how index-based accounting works end-to-end in a lending pool, from accrual to health factor enforcement.
Bonus (no workspace): Fork Ethereum mainnet and run a full supply β borrow β repay β withdraw cycle on Aave V3 directly, using
vm.prank()anddeal(). Compare the live behavior with your simplified implementation.
π Summary: Aave V3 Supply and Borrow
Covered:
- Aave V3 architecture: Pool proxy β logic libraries (Supply, Borrow, Liquidation, FlashLoan, Bridge, EMode)
- aTokens: interest-bearing ERC-20 receipts with auto-growing
balanceOf()via liquidity index - Debt tokens: non-transferable ERC-20s tracking borrow obligations via borrow index
- Supply flow: validate β update indexes β transfer underlying β mint aTokens β update config bitmap
- Borrow flow: validate β health factor check β mint debt tokens β transfer underlying β update rates
- Credit delegation:
onBehalfOfpattern andapproveDelegation() - Code reading strategy for the 15,000+ line Aave V3 codebase
Key insight: aTokensβ auto-rebasing balance enables composability β they can be used as yield-bearing collateral across DeFi without explicit claim steps.
Next: Aave V3βs risk isolation features β E-Mode, Isolation Mode, and the configuration bitmap.
π‘ Aave V3 β Risk Modes and Advanced Features
π‘ Concept: Efficiency Mode (E-Mode)
Why this matters: E-Mode allows higher capital efficiency when collateral and borrowed assets are correlated. For example, borrowing USDC against DAI β both are USD stablecoins, so the risk of the collateral losing value relative to the debt is minimal.
When a user activates an E-Mode category (e.g., βUSD stablecoinsβ), the protocol overrides the standard LTV and liquidation threshold with higher values specific to that category. A stablecoin category might allow 97% LTV vs the normal 75%.
E-Mode categories can also specify a custom oracle. For stablecoin-to-stablecoin, a fixed 1:1 oracle might be used instead of market price feeds, eliminating unnecessary liquidations from minor depeg events.
Real impact: During the March 2023 USDC depeg (Silicon Valley Bank crisis), E-Mode users with DAI collateral borrowing USDC were not liquidated due to the correlated asset treatment, while non-E-Mode users faced liquidation risk from the price deviation.
Used by: Aave V3 E-Mode categories β stablecoins, ETH derivatives (ETH/wstETH/rETH), BTC derivatives
π‘ Concept: Isolation Mode
Why this matters: New or volatile assets can be listed in Isolation Mode. When a user supplies an isolated asset as collateral:
- They cannot use any other assets as collateral simultaneously
- They can only borrow assets approved for isolation mode (typically stablecoins)
- Thereβs a hard debt ceiling for the isolated asset across all users
This prevents a volatile long-tail asset from threatening the entire protocol. If SHIB were listed in isolation mode with a $1M debt ceiling, even a complete collapse of SHIBβs price could only create $1M of potential bad debt.
Common pitfall: Not understanding the trade-off. Isolation Mode severely limits composability β users canβt mix isolated collateral with other assets. This is intentional for risk management.
π‘ Concept: Siloed Borrowing
Assets with manipulatable oracles (e.g., tokens with thin liquidity that could be subject to the oracle attacks from Module 3) can be listed as βsiloed.β Users borrowing siloed assets can only borrow that single asset β no mixing with other borrows.
Deep dive: Aave V3 siloed borrowing
π‘ Concept: Supply and Borrow Caps
V3 introduces governance-set caps per asset:
- Supply cap: Maximum total deposits. Prevents excessive concentration of a single collateral asset.
- Borrow cap: Maximum total borrows. Limits the protocolβs exposure to any single borrowed asset.
These are simple but critical risk controls that didnβt exist in V2.
Real impact: After the CRV liquidity crisis (November 2023), Aave governance tightened CRV supply caps to limit exposure. This prevented further accumulation of risky CRV positions.
π‘οΈ Virtual Balance Layer
Aave V3 tracks balances internally rather than relying on actual balanceOf() calls to the token contract. This protects against donation attacks (someone sending tokens directly to the aToken contract to manipulate share ratios) and makes accounting predictable regardless of external token transfers like airdrops.
Real impact: Euler Finance hack ($197M, March 2023) exploited donation attack vectors in ERC-4626-like vaults. Aaveβs virtual balance approach prevents this entire class of attacks.
π Read: Configuration Bitmap
Aave V3 packs all risk parameters for a reserve into a single uint256 bitmap in ReserveConfigurationMap. This is extreme gas optimization:
Bit 0-15: LTV
Bit 16-31: Liquidation threshold
Bit 32-47: Liquidation bonus
Bit 48-55: Decimals
Bit 56: Active flag
Bit 57: Frozen flag
Bit 58: Borrowing enabled
Bit 59: Stable rate borrowing enabled (deprecated)
Bit 60: Paused
Bit 61: Borrowable in isolation
Bit 62: Siloed borrowing
Bit 63: Flashloaning enabled
...
Deep dive: ReserveConfiguration.sol β read the getter/setter library functions to understand bitwise manipulation patterns used throughout production DeFi.
π Deep Dive: Encoding and Decoding the Configuration Bitmap
The problem: Each reserve in Aave V3 has ~20 configuration parameters (LTV, liquidation threshold, bonus, decimals, flags, caps, e-mode category, etc.). Storing each in a separate uint256 storage slot would cost 20 Γ 2,100 gas for a cold read. Packing them into a single uint256 costs just one 2,100 gas SLOAD.
The bitmap layout (first 64 bits):
Bit position: 63 56 55 48 47 32 31 16 15 0
ββββββββββ¬βββββββββ¬ββββββββββ¬ββββββββββ¬ββββββββββ
β flags βdecimalsβ bonus β LT β LTV β
β 8 bits β 8 bits β 16 bits β 16 bits β 16 bits β
ββββββββββ΄βββββββββ΄ββββββββββ΄ββββββββββ΄ββββββββββ
Example for USDC on Aave V3 Ethereum:
LTV = 77% β stored as 7700 (bits 0-15)
LT = 80% β stored as 8000 (bits 16-31)
Bonus = 104.5% (4.5%) β stored as 10450 (bits 32-47)
Decimals = 6 β stored as 6 (bits 48-55)
Reading LTV (bits 0-15) β mask the lower 16 bits:
uint256 constant LTV_MASK = 0xFFFF; // = 65535 = 16 bits of 1s
function getLtv(uint256 config) internal pure returns (uint256) {
return config & LTV_MASK;
}
// Example:
// config = ...0001_1110_0001_0100_0010_1000_1110_0010_0001_0001_0100 (binary)
// βββββββββββββββ
// LTV = 7700 (77%)
// config & 0xFFFF = 7700 β
Reading Liquidation Threshold (bits 16-31) β shift right, then mask:
uint256 constant LIQUIDATION_THRESHOLD_START_BIT_POSITION = 16;
function getLiquidationThreshold(uint256 config) internal pure returns (uint256) {
return (config >> 16) & 0xFFFF;
}
// Step by step:
// 1. config >> 16 β shifts right 16 bits, LTV bits fall off
// Now LT occupies bits 0-15
// 2. & 0xFFFF β masks to get just those 16 bits
// Result: 8000 (80%)
Writing LTV β clear the old bits, then set new ones:
uint256 constant LTV_MASK = 0xFFFF;
function setLtv(uint256 config, uint256 ltv) internal pure returns (uint256) {
// Step 1: Clear bits 0-15 (set them to 0)
// ~LTV_MASK = 0xFFFF...FFFF0000 (all 1s except bits 0-15)
// config & ~LTV_MASK zeroes out the LTV field
// Step 2: OR in the new value
return (config & ~LTV_MASK) | (ltv & LTV_MASK);
}
// Example β changing LTV from 7700 to 8050:
// Before: ...0001_1110_0001_0100 (7700 in bits 0-15)
// After: ...0001_1111_0111_0010 (8050 in bits 0-15)
// All other bits unchanged β
Reading a single-bit flag (e.g., βActiveβ at bit 56):
uint256 constant ACTIVE_MASK = 1 << 56;
function getActive(uint256 config) internal pure returns (bool) {
return (config & ACTIVE_MASK) != 0;
}
function setActive(uint256 config, bool active) internal pure returns (uint256) {
if (active) return config | ACTIVE_MASK; // set bit 56 to 1
else return config & ~ACTIVE_MASK; // set bit 56 to 0
}
Why this matters for DeFi development: This bitmap pattern appears everywhere β Uniswap V3/V4 tick bitmaps, Compound V3βs assetsIn field, governance proposal states. Once you understand the mask-shift-or pattern, you can read any packed configuration in production code.
Connection: Part 1 Module 1 covers bit manipulation fundamentals. This is the production application of those patterns.
π― Build Exercise: Configuration Bitmap
Workspace: workspace/src/part2/module4/exercise3-config-bitmap/ β starter file: ConfigBitmap.sol, tests: ConfigBitmap.t.sol
Implement an Aave-style bitmap library that packs multiple risk parameters into a single uint256. The exercise provides reference implementations for setLiquidationBonus/getLiquidationBonus and setDecimals/getDecimals β study the pattern, then apply it to:
setLtv/getLtvβ bits 0-15 (simplest: no offset needed)setLiquidationThreshold/getLiquidationThresholdβ bits 16-31setFlag/getFlagβ generalized single-bit setter/getter for boolean flags (active, frozen, borrow enabled, etc.)
The tests include field independence checks (setting LTV must not corrupt the threshold) and a full roundtrip test with all fields set simultaneously β matching real Aave V3 USDC and WETH configurations.
π― Goal: Master the mask-shift-or pattern used throughout production DeFi (Aave bitmaps, Uniswap tick bitmaps, Compound V3 assetsIn).
π Summary: Aave V3 Risk Modes
Covered:
- E-Mode: higher LTV/LT for correlated asset pairs (stablecoins, ETH derivatives)
- Isolation Mode: risk-containing new/volatile assets with debt ceilings and single-collateral restriction
- Siloed Borrowing: restricting assets with manipulatable oracles to single-borrow-asset positions
- Supply and Borrow Caps: governance-set limits preventing excessive concentration
- Virtual Balance Layer: internal balance tracking that prevents donation attacks
- Configuration bitmap: all risk parameters packed into a single
uint256for gas efficiency
Key insight: Aave V3βs risk features (E-Mode, Isolation, Siloed, Caps) are defense in depth β each addresses a different attack vector or risk scenario, and they compose together.
Next: Compound V3 (Comet) β a fundamentally different architectural approach to the same problem.
π‘ Compound V3 (Comet) β A Different Architecture
π» Quick Try:
Before reading Cometβs architecture, see how differently it stores state compared to Aave. On a mainnet fork:
interface IComet {
function getUtilization() external view returns (uint256);
function getSupplyRate(uint256 utilization) external view returns (uint64);
function getBorrowRate(uint256 utilization) external view returns (uint64);
function totalSupply() external view returns (uint256);
function totalBorrow() external view returns (uint256);
function baseTrackingSupplySpeed() external view returns (uint256);
}
function testReadCometState() public {
IComet comet = IComet(0xc3d688B66703497DAA19211EEdff47f25384cdc3); // USDC market
uint256 util = comet.getUtilization();
uint64 supplyRate = comet.getSupplyRate(util);
uint64 borrowRate = comet.getBorrowRate(util);
// Rates are per-second, scaled by 1e18. Convert to APR:
emit log_named_uint("Utilization (1e18 = 100%)", util);
emit log_named_uint("Supply APR (bps)", uint256(supplyRate) * 365 days / 1e14);
emit log_named_uint("Borrow APR (bps)", uint256(borrowRate) * 365 days / 1e14);
emit log_named_uint("Total Supply (USDC)", comet.totalSupply() / 1e6);
emit log_named_uint("Total Borrow (USDC)", comet.totalBorrow() / 1e6);
}
Run with forge test --match-test testReadCometState --fork-url $ETH_RPC_URL -vv. Compare the rates and utilization with Aaveβs USDC market β youβll see theyβre in the same ballpark but computed independently.
π‘ Concept: Why Study Both Aave and Compound
Why this matters: Aave V3 and Compound V3 represent two fundamentally different architectural approaches to the same problem. Understanding both gives you the design vocabulary to make informed choices when building your own protocol.
Deep dive: RareSkills Compound V3 Book, RareSkills architecture walkthrough
π‘ Concept: The Single-Asset Model
Why this matters: Compound V3βs (deployed August 2022) key architectural decision: each market only lends one asset (the βbase asset,β typically USDC). This is a radical departure from V2 and from Aave, where every asset in the pool can be both collateral and borrowable.
Implications:
- Simpler risk model: Thereβs no cross-asset risk contagion. If one collateral asset collapses, it can only affect the single base asset pool.
- Collateral doesnβt earn interest. Your ETH or wBTC sitting as collateral in Compound V3 earns nothing. This is the trade-off for the simpler, safer architecture.
- Separate markets for each base asset. Thereβs a USDC market and an ETH market β completely independent contracts with separate parameters.
Common pitfall: Expecting collateral to earn yield in Compound V3 like it does in Aave. It doesnβt. Users must choose: deposit as base asset (earns interest), or deposit as collateral (enables borrowing, no interest).
π‘ Concept: Comet Contract Architecture
Source: compound-finance/comet/contracts/Comet.sol
Everything lives in one contract (behind a proxy), called Comet:
User β Comet Proxy
ββ Comet Implementation
ββ Supply/withdraw logic
ββ Borrow/repay logic
ββ Liquidation logic (absorb)
ββ Interest rate model
ββ CometExt (fallback for auxiliary functions)
Supporting contracts:
- CometExt: Handles overflow functions that donβt fit in the main contract (24KB limit workaround via the fallback extension pattern)
- Configurator: Sets parameters, deploys new Comet implementations when governance changes settings
- CometFactory: Deploys new Comet instances
- Rewards: Distributes COMP token incentives (separate from the lending logic)
π‘ Concept: Immutable Variables: A Unique Design Choice
Why this matters: Compound V3 stores all parameters (interest rate model coefficients, collateral factors, liquidation factors) as immutable variables, not storage. To change any parameter, governance must deploy an entirely new Comet implementation and update the proxy.
Why? Immutable variables are significantly cheaper to read than storage (3 gas vs 2100 gas for cold SLOAD). Since rate calculations happen on every interaction, this saves substantial gas across millions of transactions. The trade-off is governance friction β changing a parameter requires a full redeployment, not just a storage write.
Common pitfall: Trying to update parameters via governance without redeploying. Compound V3 parameters are immutable β you must deploy a new implementation.
π‘ Concept: Principal and Index Accounting
Compound V3 tracks balances using a principal/index system similar to Aave but with a twist: the principal is a signed integer. Positive means the user is a supplier; negative means theyβre a borrower. Thereβs no separate debt token.
struct UserBasic {
int104 principal; // signed: positive = supply, negative = borrow
uint64 baseTrackingIndex;
uint64 baseTrackingAccrued;
uint16 assetsIn; // bitmap of which collateral assets are deposited
}
The actual balance is computed:
If principal > 0: balance = principal Γ supplyIndex / indexScale
If principal < 0: balance = |principal| Γ borrowIndex / indexScale
π‘ Concept: Separate Supply and Borrow Rate Curves
Unlike Aave (where supply rate is derived from borrow rate), Compound V3 defines independent kinked curves for both supply and borrow rates. Both are functions of utilization with their own base rates, kink points, and slopes. This gives governance more flexibility but means the spread isnβt automatically guaranteed.
Deep dive: Comet.sol getSupplyRate() / getBorrowRate()
π Read: Comet.sol Core Functions
Source: compound-finance/comet/contracts/Comet.sol
Key functions to read:
supplyInternal(): How supply is processed, including therepayAndSupplyAmount()split (if user has debt, supply first repays debt, then adds to balance)withdrawInternal(): How withdrawal works, including automatic borrow creation if withdrawing more than suppliedgetSupplyRate()/getBorrowRate(): The kinked curve implementationsaccrueInternal(): How indexes are updated usingblock.timestampand per-second ratesisLiquidatable(): Health check using collateral factors and oracle prices
Note: Compound V3 is ~4,300 lines of Solidity (excluding comments). This is compact for a lending protocol and very readable.
π How to Study Compound V3 (Comet)
Comet is dramatically simpler than Aave β one contract, ~4,300 lines. This makes it the better starting point if youβre new to lending protocol code.
-
Start with the state variables β Open Comet.sol and read the immutable declarations (lines ~65-109). These ARE the protocol configuration β base token, interest rate params, collateral factors, oracle feeds. Notice: all immutable, not storage. Understanding why this matters (gas) and the trade-off (redeployment for changes) is key.
-
Read
supplyInternal()andwithdrawInternal()β These are the core flows. Notice the signed principal pattern: supplying when you have debt first repays debt. Withdrawing when you have no supply creates a borrow. This dual behavior is elegant but different from Aaveβs separate supply/borrow paths. -
Trace the index update in
accrueInternal()β This is simpler than Aaveβs version. One function, linear compound, per-second rates. Map howbaseSupplyIndexandbaseBorrowIndexgrow over time. -
Read
isLiquidatable()β Follow the health check: for each collateral asset, fetch oracle price, multiply by collateral factor, sum up. Compare to borrow balance. This is the health factor equivalent, computed inline rather than as a separate ratio. -
Compare with Aave β After reading both, you should be able to articulate: why did Compound choose a single-asset model? (Risk isolation.) Why immutables? (Gas.) Why signed principal? (Simplicity β no separate debt tokens.) These are the architectural trade-offs interviewers ask about.
Donβt get stuck on: The CometExt fallback pattern. Itβs a workaround for the 24KB contract size limit β auxiliary functions are deployed separately and called via the fallback function. Understand that it exists, but focus on the core Comet logic.
π― Build Exercise: Compound V3 Code Analysis
Exercise 1: Read the Compound V3 getUtilization(), getBorrowRate(), and getSupplyRate() functions. For each, trace the math and verify it matches the kinked curve formula from the Lending Model section.
Exercise 2: Compare Aave V3 and Compound V3 storage layout for user positions. Aave uses separate aToken and debtToken balances; Compound uses a single signed principal. Write a comparison document: what are the trade-offs of each approach for gas, composability, and complexity?
π Summary: Compound V3 (Comet)
Covered:
- Compound V3βs single-asset model: one borrowable asset per market, simpler risk isolation
- Comet contract architecture: everything in one contract (vs Aaveβs library pattern)
- Immutable variables for parameters: 3 gas reads vs 2100 gas SLOAD, but requires full redeployment
- Signed principal pattern: positive = supplier, negative = borrower (no separate debt tokens)
- Independent supply and borrow rate curves (vs Aaveβs derived supply rate)
- Code reading strategy for the ~4,300 line Comet codebase
Key insight: Compound V3 trades composability (no yield on collateral) for simplicity and risk isolation. Neither architecture is strictly better β the choice depends on what youβre building.
Next: The protocolβs immune system β liquidation mechanics in both Aave and Compound.
π‘ Liquidation Mechanics
π» Quick Try:
Before diving into liquidation theory, find a real position close to liquidation on Aave V3. On a mainnet fork:
interface IPool {
function getUserAccountData(address user) external view returns (
uint256 totalCollateralBase, // in USD (8 decimals, base currency units)
uint256 totalDebtBase,
uint256 availableBorrowsBase,
uint256 currentLiquidationThreshold, // percentage (4 decimals: 8250 = 82.50%)
uint256 ltv,
uint256 healthFactor // 18 decimals: 1e18 = HF of 1.0
);
}
function testReadHealthFactor() public {
IPool pool = IPool(0x87870Bca3F3fD6335C3F4ce8392D69350B4fA4E2);
// Pick any active Aave borrower from Etherscan or Dune
// Or use your own address if you have an Aave position
address borrower = 0x...; // replace with a real borrower
(
uint256 collateral, uint256 debt, uint256 available,
uint256 lt, uint256 ltvVal, uint256 hf
) = pool.getUserAccountData(borrower);
emit log_named_uint("Collateral (USD, 8 dec)", collateral);
emit log_named_uint("Debt (USD, 8 dec)", debt);
emit log_named_uint("Health Factor (18 dec)", hf);
emit log_named_uint("Liquidation Threshold (bps)", lt);
// Manually verify: HF = (collateral Γ LT / 10000) / debt
uint256 manualHF = (collateral * lt / 10000) * 1e18 / debt;
emit log_named_uint("Manual HF calc", manualHF);
// These should match (within rounding)
}
Run with forge test --match-test testReadHealthFactor --fork-url $ETH_RPC_URL -vv. Seeing real health factors brings the abstraction to life β a number printed on screen is someoneβs real money at risk.
π‘ Concept: Why Liquidation Exists
Why this matters: Lending without credit checks requires overcollateralization. But crypto prices are volatile β collateral can lose value. Without liquidation, a $10,000 ETH collateral backing an $8,000 USDC loan could become worth $7,000, leaving the protocol with unrecoverable bad debt.
Liquidation is the protocolβs immune system. It removes unhealthy positions before they can create bad debt, keeping the system solvent for all suppliers.
Real impact: During the May 2021 crypto crash, Aave processed $521M in liquidations across 2,800+ positions in a single day. The system remained solvent β no bad debt accrued despite 40%+ price drops.
π‘ Concept: The Liquidation Flow
Step 1: Detection. A positionβs health factor drops below 1 (meaning debt value exceeds collateral value Γ liquidation threshold). This happens when collateral price drops or debt value increases (from accrued interest or borrowed asset price increase).
Step 2: A liquidator calls the liquidation function. Liquidation is permissionless β anyone can do it. In practice, itβs done by specialized bots that monitor all positions and submit transactions the moment a position becomes liquidatable.
Step 3: Debt repayment. The liquidator repays some or all of the borrowerβs debt (up to the close factor).
Step 4: Collateral seizure. The liquidator receives an equivalent value of the borrowerβs collateral, plus the liquidation bonus (discount). For example, repaying $5,000 of USDC debt might yield $5,250 worth of ETH (at 5% bonus).
Step 5: Health factor restoration. After liquidation, the borrowerβs health factor should be above 1 (smaller debt, proportionally less collateral).
π Aave V3 Liquidation
Source: LiquidationLogic.sol β executeLiquidationCall()
Key details:
- Caller specifies
collateralAsset,debtAsset,user, anddebtToCover - Protocol validates HF < 1 using oracle prices
- Close factor: 50% normally. If HF < 0.95, the full 100% can be liquidated (V3 improvement over V2βs fixed 50%)
- Minimum position: Partial liquidations must leave at least $1,000 of both collateral and debt remaining β otherwise the position must be fully cleared (prevents dust accumulation)
- Liquidator can choose to receive aTokens (collateral stays in the protocol) or the underlying asset
- Oracle prices are fetched fresh during the liquidation call
Common pitfall: Forgetting to approve the liquidator contract to spend the debt asset. The liquidation call transfers debt tokens from the liquidator to the protocol β this requires prior approval.
Aave V3 Liquidation Flow:
βββββββββββββββββββββββββββββββββββββββββββ
β Liquidator calls liquidationCall() β
β (collateralAsset, debtAsset, user, amt) β
ββββββββββββββββββββ¬βββββββββββββββββββββββ
β
ββββββββββββββββββββΌβββββββββββββββββββββββ
β 1. Validate: is user's HF < 1.0? β
β β Fetch oracle prices (AaveOracle) β
β β Compute HF using all collateral β
β β If HF β₯ 1.0, REVERT β
ββββββββββββββββββββ¬βββββββββββββββββββββββ
β
ββββββββββββββββββββΌβββββββββββββββββββββββ
β 2. Determine close factor β
β β HF < 0.95: can liquidate 100% β
β β HF β₯ 0.95: can liquidate max 50% β
β β Cap debtToCover at close factor β
ββββββββββββββββββββ¬βββββββββββββββββββββββ
β
ββββββββββββββββββββΌβββββββββββββββββββββββ
β 3. Calculate collateral to seize β
β β
β collateral = debtToCover Γ debtPrice β
β Γ (1 + liquidationBonus) β
β / collateralPrice β
β β
β e.g., $5,000 USDC Γ 1.05 / $2,000 ETH β
β = 2.625 ETH seized β
ββββββββββββββββββββ¬βββββββββββββββββββββββ
β
ββββββββββββββββββββΌβββββββββββββββββββββββ
β 4. Execute transfers β
β β Liquidator sends debtAsset to pool β
β β Pool burns user's debt tokens β
β β Pool transfers collateral to β
β liquidator (aTokens or underlying) β
ββββββββββββββββββββ¬βββββββββββββββββββββββ
β
ββββββββββββββββββββΌβββββββββββββββββββββββ
β 5. Post-liquidation state β
β β User's debt decreased β
β β User's collateral decreased β
β β User's HF should now be > 1.0 β
β β Liquidator profit = bonus portion β
βββββββββββββββββββββββββββββββββββββββββββ
π Compound V3 Liquidation (βAbsorbβ)
Why this matters: Compound V3 takes a different approach: the protocol itself absorbs underwater positions, rather than individual liquidators repaying debt.
The absorb() function:
- Anyone can call
absorb(absorber, [accounts])for one or more underwater accounts - The protocol seizes the underwater accountβs collateral and stores it internally
- The underwater accountβs debt is written off (socialized across suppliers via a βdeficitβ in the protocol)
- The caller (absorber) receives no direct compensation from the absorb itself
The buyCollateral() function:
After absorption, the protocol holds seized collateral. Anyone can buy this collateral at a discount through buyCollateral(), paying in the base asset. The protocol uses the proceeds to cover the deficit. The discount follows a Dutch auction pattern β it starts small and increases over time until someone buys.
This two-step process (absorb β buyCollateral) separates the urgency of removing bad positions from the market dynamics of selling collateral. It prevents sandwich attacks on liquidations and gives the market time to find the right price.
Deep dive: Compound V3 absorb documentation, buyCollateral Dutch auction
π‘ Concept: Liquidation Bot Economics
Why this matters: Running a liquidation bot is a competitive business:
Revenue: Liquidation bonus (typically 4β10% of seized collateral)
Costs:
- Gas for monitoring + execution
- Capital for repaying debt (or flash loan fees)
- Smart contract risk
- Oracle latency risk
Competition: Multiple bots compete for the same liquidation. In practice, the winner is often the one with the lowest latency to the mempool or the best MEV strategy (priority gas auctions, Flashbots bundles)
Flash loan liquidations: Liquidators can use flash loans to avoid needing capital β borrow the repayment asset, execute the liquidation, sell the seized collateral, repay the flash loan, keep the profit. All in one transaction.
Real impact: During the May 2021 crash, liquidation bots earned an estimated $50M+ in bonuses across all protocols. The largest single liquidation on Aave was ~$30M collateral seized.
Deep dive: Flashbots docs β MEV infrastructure and searcher strategies, Eigenphi liquidation tracking
π― Build Exercise: Flash Loan Liquidation Bot
Workspace: workspace/src/part2/module4/exercise4-flash-liquidator/ β starter file: FlashLiquidator.sol, tests: FlashLiquidator.t.sol
Build a zero-capital liquidation bot using ERC-3156 flash loans. The scaffold provides all the mock infrastructure (flash lender, lending pool, DEX) and the contract skeleton with interfaces. You wire together the composability flow:
liquidate(borrower, debtToken, debtAmount, collateralToken)β entry point that encodes parameters and requests a flash loanonFlashLoan(...)β ERC-3156 callback that performs the liquidation with borrowed funds. Two critical security checks are required (caller validation and initiator validation)._sellCollateral(...)β approve and swap seized collateral on the DEX_verifyProfit(...)β ensure the liquidation was profitable after accounting for flash loan fees
The tests cover: profitable liquidation end-to-end, exact profit calculation (5% bonus minus 0.09% flash fee), close factor mechanics (50% vs 100%), unprofitable liquidation revert, callback security (wrong caller/initiator), and profit withdrawal.
π― Goal: Understand how MEV bots and liquidation bots compose flash loans, lending pools, and DEXes in a single atomic transaction.
π Summary: Liquidation Mechanics
Covered:
- Why liquidation exists: the immune system that prevents bad debt from price volatility
- The 5-step liquidation flow: detection β call β debt repayment β collateral seizure β HF restoration
- Aave V3 liquidation: direct liquidator model, close factor (50% normal, 100% when HF < 0.95), minimum position rules
- Compound V3 liquidation: two-step
absorb()+buyCollateral()Dutch auction (separates urgency from market dynamics) - Liquidation bot economics: revenue (bonus) vs costs (gas, capital, latency, competition)
- Flash loan liquidations: zero-capital liquidation using atomic borrow β liquidate β swap β repay
Key insight: Compound V3βs absorb/auction split is architecturally elegant β it prevents sandwich attacks on liquidations and decouples βremove the riskβ from βfind the best price for collateral.β
πΌ Job Market Context β Liquidation Mechanics
What DeFi teams expect you to know about liquidation:
-
βDesign a liquidation bot. Whatβs your architecture?β
- Good answer: Monitor health factors, submit liquidation tx when HF < 1, use flash loans for capital efficiency
- Great answer: Discusses mempool monitoring vs on-chain event listening, Flashbots bundles to avoid front-running, priority gas auction dynamics, the economics of when liquidation is profitable (bonus vs gas + flash loan fee + swap slippage), and multi-protocol monitoring (Aave + Compound + Euler simultaneously)
-
βA user reports they were liquidated unfairly. How do you investigate?β
- Good answer: Check oracle prices at the liquidation block, verify HF was actually < 1
- Great answer: Trace the full sequence β was the oracle price stale? Was the sequencer down (L2)? Was there a price manipulation in the same block? Did the liquidator front-run an oracle update? Check if the liquidation bonus was correctly applied and the close factor respected. This is a real scenario teams face in post-mortems.
-
βCompare Aaveβs direct liquidation with Compound V3βs absorb/auction model.β
- Great answer: Aaveβs model is simpler β one atomic transaction, liquidator bears price risk. Compoundβs two-step model (absorb β buyCollateral) separates urgency from price discovery β absorption happens immediately (protocol takes bad debt), then Dutch auction finds optimal price for seized collateral. Trade-off: Compoundβs model socializes losses temporarily but gets better execution prices; Aaveβs model relies on liquidator speed and can suffer from sandwich attacks.
Interview red flags:
- Not knowing that liquidation is permissionless (anyone can call it)
- Thinking flash loan liquidations are βcheatingβ (theyβre essential for market health)
- Not understanding why close factor exists (prevent cascade selling)
Pro tip: If asked about liquidation in an interview, mention the Euler V1 exploit β the attacker used donateToReserves() to manipulate health factors, bypassing the standard liquidation check. This shows you understand how liquidation edge cases create attack surfaces.
Next: Build a simplified lending protocol (SimpleLendingPool) that integrates everything from the previous sections.
π― Build Exercise: Simplified Lending Protocol
SimpleLendingPool.sol
Build a minimal but correct lending protocol that incorporates everything from this module:
State:
struct Reserve {
uint256 totalSupplied;
uint256 totalBorrowed;
uint256 supplyIndex; // RAY (27 decimals)
uint256 borrowIndex; // RAY
uint256 lastUpdateTimestamp;
uint256 reserveFactor; // WAD (18 decimals)
}
struct UserPosition {
uint256 scaledSupply; // supply principal / supplyIndex at deposit
uint256 scaledDebt; // borrow principal / borrowIndex at borrow
mapping(address => uint256) collateral; // collateral token => amount
}
mapping(address => Reserve) public reserves; // asset => reserve data
mapping(address => mapping(address => UserPosition)) public positions; // user => asset => position
Core functions:
supply(asset, amount)β Transfer tokens in, update supply index, store scaled balancewithdraw(asset, amount)β Check health factor remains > 1 after withdrawal, transfer tokens outdepositCollateral(asset, amount)β Transfer collateral tokens in (no interest earned)borrow(asset, amount)β Check health factor after borrow, mint scaled debt, transfer tokens outrepay(asset, amount)β Burn scaled debt, transfer tokens in. Handletype(uint256).maxfor full repayment (see Aaveβs pattern for handling dust from continuous interest accrual)liquidate(user, collateralAsset, debtAsset, debtAmount)β Validate HF < 1, repay debt, seize collateral with bonus
Supporting functions:
accrueInterest(asset)β Update supply and borrow indexes using kinked rate modelgetHealthFactor(user)β Sum collateral values Γ LT, sum debt values, compute ratio. Use Chainlink mock for prices.getAccountLiquidity(user)β Return available borrow capacity
Interest rate model: Implement the kinked curve from the Lending Model section as a separate contract referenced by the pool.
Oracle integration: Use the safe Chainlink consumer pattern from Module 3. Mock the oracle in tests.
Test Suite
Write comprehensive Foundry tests:
- Happy path: supply β borrow β accrue interest β repay β withdraw (verify balances at each step)
- Interest accuracy: supply, warp 365 days, verify balance matches expected APY within tolerance
- Health factor boundary: borrow right at the limit, verify HF β LT/LTV ratio
- Liquidation trigger: manipulate oracle price to push HF below 1, execute liquidation, verify correct collateral seizure and debt reduction
- Liquidation bonus math: verify liquidator receives exactly (debtRepaid Γ (1 + bonus) / collateralPrice) collateral
- Over-borrow revert: attempt to borrow more than health factor allows, verify revert
- Withdrawal blocked: attempt to withdraw collateral that would make HF < 1, verify revert
- Multiple collateral types: deposit ETH + WBTC as collateral, borrow USDC, verify combined collateral valuation
- Interest rate jumps: push utilization past the kink, verify rate jumps to the steep slope
- Reserve factor accumulation: verify protocolβs share of interest accumulates correctly
Common pitfall: Not accounting for rounding errors in index calculations. Use a tolerance (e.g., Β±1 wei) when comparing expected vs actual balances after interest accrual.
π Summary: SimpleLendingPool
Covered:
- Building SimpleLendingPool.sol: state design (Reserve struct, UserPosition struct, index-based accounting)
- Core functions: supply, withdraw, depositCollateral, borrow, repay, liquidate
- Supporting functions: accrueInterest (kinked rate model), getHealthFactor (Chainlink integration), getAccountLiquidity
- Full test suite design: happy path, interest accuracy, HF boundaries, liquidation correctness, over-borrow reverts, multi-collateral
Key insight: Building a lending pool from scratch β even a simplified one β forces you to understand every interaction between interest math, oracle pricing, and health factor enforcement. The tests are where the real learning happens.
Next: Synthesis β architectural comparison, bad debt, liquidation cascades, and emerging patterns.
π‘ Synthesis and Advanced Patterns
π Architectural Comparison: Aave V3 vs Compound V3
| Dimension | Aave V3 | Compound V3 |
|---|---|---|
| Borrowable assets | Multiple per pool | Single base asset per market |
| Collateral interest | Yes (aTokens accrue) | No |
| Debt representation | Non-transferable debt tokens | Signed principal in UserBasic |
| Parameter storage | Storage variables | Immutable variables (cheaper reads, costlier updates) |
| Interest rate model | Borrow rate from curve, supply derived | Independent supply and borrow curves |
| Liquidation model | Direct liquidator repays, receives collateral | Protocol absorbs, then Dutch auction for collateral |
| Risk isolation | E-Mode, Isolation Mode, Siloed Borrowing | Inherent via single-asset markets |
| Code size | ~15,000+ lines across libraries | ~4,300 lines in Comet |
| Upgrade path | Update logic libraries, keep proxy | Deploy new Comet, update proxy |
π‘ Concept: Bad Debt and Protocol Solvency
Why this matters: What happens when collateral value drops so fast that liquidation canβt happen in time? The position becomes underwater β debt exceeds collateral. This creates bad debt that the protocol must absorb.
Aaveβs approach: The Safety Module (staked AAVE) serves as a backstop. If bad debt accumulates, governance can trigger a βshortfall eventβ that slashes staked AAVE to cover losses. This is insurance funded by AAVE stakers who earn protocol revenue in return.
Compoundβs approach: The absorb function socializes the loss across all suppliers (the protocolβs reserves decrease). The subsequent buyCollateral() Dutch auction recovers what it can.
Real impact: During the CRV liquidity crisis (November 2023), several Aave markets accumulated bad debt from a large borrower whose CRV collateral couldnβt be liquidated fast enough due to thin liquidity. This led to governance discussions about tightening risk parameters for illiquid assets β and informed the design of Isolation Mode and supply/borrow caps in V3.
β οΈ The Liquidation Cascade Problem
Why this matters: When crypto prices drop sharply, many positions become liquidatable simultaneously. Liquidators selling seized collateral on DEXes pushes prices down further, triggering more liquidations. This positive feedback loop is a liquidation cascade.
Defenses:
- Gradual liquidation (close factor < 100%): Prevents dumping all collateral at once
- Liquidation bonus calibration: Too high = excessive selling pressure; too low = no incentive to liquidate
- Oracle smoothing / PriceOracleSentinel: Delays liquidations briefly after sequencer recovery on L2 to let prices stabilize
- Supply/borrow caps: Limit total exposure so cascades canβt grow unbounded
Real impact: The March 2020 βBlack Thursdayβ crash saw over $8M in bad debt on Maker due to liquidation cascades and network congestion preventing timely liquidations. This informed V2/V3 risk parameter designs.
π‘ Concept: Emerging Patterns
Morpho Blue β The Minimalist Lending Core:
Morpho Blue (deployed January 2024) represents a radical departure from both Aave and Compound. The core contract is ~650 lines of Solidity β smaller than most ERC-20 tokens with governance.
Key architectural insight: Instead of one big pool with many assets (Aave) or one contract per base asset (Compound), Morpho Blue creates isolated markets defined by 5 immutable parameters: loan token, collateral token, oracle, interest rate model (IRM), and LTV. Anyone can create a market β no governance vote needed.
Traditional (Aave/Compound): Morpho Blue:
βββββββββββββββββββββββββββ βββββββββββββββ βββββββββββββββ
β One Pool / One Market β β Market A β β Market B β
β ETH, USDC, DAI, WBTC β β USDC/ETH β β DAI/wstETH β
β all cross-collateral β β 86% LTV β β 94.5% LTV β
β shared risk params β β Oracle X β β Oracle Y β
βββββββββββββββββββββββββββ βββββββββββββββ βββββββββββββββ
Each market is fully isolated
Parameters immutable at creation
Why ~650 lines? Morpho Blue pushes complexity to the edges:
- No governance, no upgradeability, no admin functions β parameters are immutable
- No interest rate model built in β itβs an external contract passed at market creation
- No oracle built in β itβs an external contract passed at market creation
- No token wrappers (no aTokens) β balances are tracked as simple mappings
- The result: a minimal, auditable core thatβs extremely hard to exploit
The MetaMorpho layer: On top of Morpho Blue, MetaMorpho vaults (ERC-4626 vaults managed by curators) allocate capital across multiple Morpho Blue markets. This separates lending logic (Morpho Blue, immutable) from risk management (MetaMorpho, managed).
Real impact: Morpho Blue crossed $3B+ TVL within its first year. Its market creation is permissionless β over 1,000 unique markets created by Q4 2024.
π How to study: Read Morpho.sol β itβs short enough to read entirely in one sitting. Focus on
supply(),borrow(), andliquidate(). Compare the simplicity with Aaveβs 15,000 lines.
Euler V2: Modular architecture where each vault has its own risk parameters. Vaults can connect to each other via a βconnectorβ system, creating a graph of lending relationships rather than a single pool. Represents the same βmodular lendingβ trend as Morpho Blue but with different trade-offs (more flexibility, more complexity).
Variable liquidation incentives: Some protocols adjust the liquidation bonus dynamically based on how far underwater a position is, how much collateral is being liquidated, and current market conditions. This optimizes between βenough incentive to liquidate quicklyβ and βnot so much that borrowers are unfairly punished.β
π‘ Aave V3.1 / V3.2 / V3.3 β Recent Updates (Awareness)
Aave continues evolving within the V3 framework. These updates are important to know about even if you study the V3 base code:
Aave V3.1 (April 2024):
- Liquid eMode: Each asset can belong to multiple E-Mode categories simultaneously (previously limited to one). A user can activate the category that best matches their position. This increases capital efficiency for LST/LRT positions.
- Stateful interest rate model: The DefaultReserveInterestRateStrategyV2 can adjust the base rate based on recent utilization history, making the curve adaptive rather than static.
Aave V3.2 (July 2024):
- Umbrella (Safety Module replacement): Replaces the staked-AAVE backstop with a more flexible insurance system. Individual βaToken umbrellasβ protect specific reserves, allowing targeted risk coverage rather than one-size-fits-all protection.
- Virtual accounting enforced: The virtual balance layer (internal balance tracking vs
balanceOf()) is now the default, not optional. This hardens all reserves against donation attacks.
Aave V3.3 (February 2025):
- Deficit handling mechanism: Automated bad debt handling where governance can write off accumulated deficits across reserves, replacing manual proposals with a standardized process.
- Deprecation of stable rate borrowing: The stable rate mode is fully removed from new deployments, simplifying the codebase.
GHO β Aaveβs Native Stablecoin: GHO is minted directly through Aave V3 borrowing (a βfacilitatorβ pattern). Users borrow GHO instead of withdrawing existing assets from the pool. This means Aave acts as both a lending protocol and a stablecoin issuer β connecting Module 4 directly to Module 6 (Stablecoins).
Why this matters for interviews: Knowing about V3.1+ updates signals that you follow the space actively. Mentioning Liquid eMode or Umbrella shows youβre beyond textbook knowledge.
π― Build Exercise: Liquidation Scenarios
Workspace: workspace/test/part2/module4/exercise4b-liquidation-scenarios/ β test-only exercise: LiquidationScenarios.t.sol (implements BadDebtPool.handleBadDebt() inline, then runs cascade and bad debt tests)
Exercise 1: Liquidation cascade simulation. Using your SimpleLendingPool from the Build exercise, set up 5 users with progressively tighter health factors. Drop the oracle price in steps. After each drop, execute available liquidations. Track how each liquidation changes the βmarketβ (the oracle price reflects the collateral being sold). Does the cascade stabilize or spiral?
Exercise 2: Bad debt scenario. Configure your pool with a very volatile collateral. Use vm.warp and vm.mockCall to simulate a 50% price crash in a single block (too fast for liquidation). Show the resulting bad debt. Implement a handleBadDebt() function that socializes the loss across suppliers.
Exercise 3: Read Morpho Blueβs minimal core. Read Morpho.sol (~650 lines). Focus on: how are markets created (the 5 immutable parameters)? How does supply() / borrow() / liquidate() work without aTokens or debt tokens? How does the architecture achieve risk isolation without Aaveβs E-Mode/Isolation Mode complexity? Compare the simplicity with Aaveβs 15,000 lines. No build β just analysis.
π Summary: Synthesis and Advanced Patterns
Covered:
- Architectural comparison: Aave V3 (multi-asset, composable, complex) vs Compound V3 (single-asset, isolated, simple)
- Bad debt mechanics: Aaveβs Safety Module (staked AAVE backstop) vs Compoundβs absorb/auction socialization
- Liquidation cascades: the positive feedback loop and defenses (close factor, bonus calibration, oracle smoothing, caps)
- Emerging protocols: Morpho Blue (~650-line minimal core, permissionless isolated markets), Euler V2 (modular vaults), variable liquidation incentives
- Aave V3.1/V3.2/V3.3 updates: Liquid eMode, Umbrella, virtual accounting enforcement, deficit handling, stable rate deprecation
- GHO stablecoin: Aave as both lending protocol and stablecoin issuer via facilitator pattern
Key insight: The Aave vs Compound architectural trade-off is a core interview topic. Being able to articulate why each design was chosen (not just what it does) separates senior DeFi engineers from juniors.
Internalized patterns: Interest rates are mechanism design (kinked curves as calibrated incentive systems). Indexes are the universal scaling pattern (global indexes amortize per-user computation). Liquidation is the protocolβs immune system. Oracle integration is load-bearing (health factor, liquidation trigger, collateral valuation). RAY precision and rounding direction are protocol-critical (27-decimal, round against the user). Modular lending is the emerging trend (Morpho Blue ~650 lines, Euler V2 vault graphs). The type(uint256).max pattern solves the dust repayment problem.
Next: Module 5 β Flash Loans (atomic uncollateralized borrowing, composing multi-step arbitrage and liquidation flows).
πΌ Job Market Context
What DeFi teams expect you to know about lending architecture:
-
βCompare Aave V3 and Compound V3 architectures. When would you choose one over the other?β
- Good answer: Lists the differences (multi-asset vs single-asset, aTokens vs signed principal, libraries vs monolith)
- Great answer: Frames it as a trade-off space β Aave optimizes for composability and capital efficiency (yield-bearing collateral, E-Mode), Compound optimizes for risk isolation and simplicity (no cross-asset contagion, smaller attack surface). Choice depends on whether youβre building a general lending market (Aave) or a focused, risk-minimized product (Compound)
-
βHow would you prevent bad debt in a lending protocol?β
- Good answer: Overcollateralization, timely liquidations, conservative risk parameters
- Great answer: Discusses defense in depth β E-Mode/Isolation/Siloed borrowing for risk segmentation, supply/borrow caps for exposure limits, virtual balance layer against donation attacks, PriceOracleSentinel for L2 sequencer recovery, Safety Module as backstop, and the fundamental tension between capital efficiency and safety margin
-
βWalk me through a liquidation cascade. How would you design defenses?β
- Great answer: Explains the positive feedback loop (liquidation β collateral sold β price drops β more liquidations), then discusses close factor < 100%, bonus calibration, oracle smoothing, and references Black Thursday 2020 as the canonical example that shaped current designs
Hot topics in 2025-2026:
- Cross-chain lending (L2 β L1 collateral, shared liquidity across chains)
- Modular lending (Euler V2 vault graph, Morpho Blueβs minimal core + modules)
- Real-World Assets (RWA) as collateral in lending markets (Maker/Sky, Centrifuge)
- Point-of-sale lending with on-chain credit scoring (undercollateralized lending frontier)
β οΈ Common Mistakes
Mistakes that have caused real exploits and audit findings in lending protocols:
-
Not accruing interest before state changes
// WRONG β reads stale index function borrow(uint256 amount) external { uint256 debt = getDebt(msg.sender); // uses old borrowIndex require(isHealthy(msg.sender), "undercollateralized"); // ... } // CORRECT β accrue first, then compute function borrow(uint256 amount) external { accrueInterest(); // updates indexes to current timestamp uint256 debt = getDebt(msg.sender); // uses fresh borrowIndex require(isHealthy(msg.sender), "undercollateralized"); // ... }Impact: Stale indexes undercount debt β users borrow more than they should β protocol becomes undercollateralized.
-
Using
balanceOf()instead of internal accounting for pool balances// WRONG β vulnerable to donation attacks function totalDeposits() public view returns (uint256) { return token.balanceOf(address(this)); } // CORRECT β track internally function totalDeposits() public view returns (uint256) { return _internalTotalDeposits; }Impact: Attacker sends tokens directly to the contract β inflates share ratio β drains funds. This is how Euler was exploited for $197M.
-
Rounding in the wrong direction
// WRONG β rounds in user's favor for debt scaledDebt = debtAmount * RAY / borrowIndex; // rounds down = less debt // CORRECT β round UP for debt, DOWN for deposits scaledDebt = (debtAmount * RAY + borrowIndex - 1) / borrowIndex; // rounds upImpact: Each borrow creates slightly less debt than it should. Over millions of borrows, the shortfall accumulates. Aave V3 uses
rayDiv(round down) for deposits andrayDivwith round-up for debt. -
Not checking oracle freshness before liquidation
// WRONG β uses potentially stale price uint256 price = oracle.latestAnswer(); // CORRECT β validate freshness (, int256 answer,, uint256 updatedAt,) = oracle.latestRoundData(); require(block.timestamp - updatedAt < STALENESS_THRESHOLD, "stale price"); require(answer > 0, "invalid price");Impact: Stale oracle β incorrect HF calculation β either wrongful liquidation (user loss) or missed liquidation (protocol loss). See Module 3 for complete oracle safety patterns.
-
Liquidation that doesnβt restore health
// WRONG β doesn't check post-liquidation state function liquidate(address user, uint256 amount) external { _repayDebt(user, amount); _seizeCollateral(user, amount * bonus); // done β but what if HF is still < 1? } // CORRECT β verify the liquidation actually helped // Aave V3 enforces minimum position sizes and validates post-liquidation stateImpact: Partial liquidation that leaves a dust position still underwater β no one can liquidate the remainder profitably β bad debt.
-
Not handling
type(uint256).maxfor full repayment// WRONG β user passes type(uint256).max to mean "repay all" // but interest accrues between tx submission and execution function repay(uint256 amount) external { token.transferFrom(msg.sender, address(this), amount); userDebt[msg.sender] -= amount; // If amount > actual debt β underflow revert // If amount < actual debt β dust remains } // CORRECT β handle the "repay everything" case explicitly function repay(uint256 amount) external { accrueInterest(); uint256 currentDebt = getDebt(msg.sender); uint256 repayAmount = amount == type(uint256).max ? currentDebt : amount; require(repayAmount <= currentDebt, "repay exceeds debt"); token.transferFrom(msg.sender, address(this), repayAmount); userDebt[msg.sender] -= repayAmount; }Impact: Without the
type(uint256).maxpattern, users can never fully repay their debt because interest accrues between the time they calculate the amount and when the transaction executes. This leaves tiny dust debts that accumulate across thousands of users. Aave V3 handles this explicitly.
π Cross-Module Concept Links
The lending module is the curriculumβs crossroads β nearly every other module either feeds into it (oracles, tokens) or builds on it (flash loans, stablecoins, vaults).
β Backward References (Part 1 + Modules 1β3)
| Source | Concept | How It Connects |
|---|---|---|
| Part 1 Module 1 | Bit manipulation / UDVTs | Aaveβs ReserveConfigurationMap packs all risk params into a single uint256 bitmap β production example of Module 1 patterns |
| Part 1 Module 1 | mulDiv / fixed-point math | RAY (27-decimal) arithmetic for index calculations; rayMul/rayDiv used in every balance computation |
| Part 1 Module 1 | Custom errors | Aave V3 uses custom errors for revert reasons; Compound V3 uses custom errors throughout Comet |
| Part 1 Module 2 | Transient storage | Reentrancy guards in lending pools; V4-era lending integrations can use TSTORE for flash accounting |
| Part 1 Module 3 | Permit / Permit2 | Gasless approvals for supply/repay operations; Compound V3 supports EIP-2612 permit natively |
| Part 1 Module 5 | Fork testing / vm.mockCall | Essential for testing against live Aave/Compound state and simulating oracle price movements |
| Part 1 Module 5 | Invariant / fuzz testing | Property-based testing for lending invariants: total debt β€ total supply, HF checks, index monotonicity |
| Part 1 Module 6 | Proxy patterns | Both Aave V3 (Pool proxy + logic libraries) and Compound V3 (Comet proxy + CometExt fallback) use proxy architecture |
| Module 1 | SafeERC20 / token decimals | Safe transfers for supply/withdraw/liquidate; decimal normalization when computing collateral values across different tokens |
| Module 2 | Constant product / mechanism design | AMMs use x Γ y = k to set prices; lending uses kinked curves to set rates β both replace human market-makers with math |
| Module 2 | DEX liquidity for liquidation | Liquidators sell seized collateral on AMMs; pool depth determines liquidation feasibility for illiquid assets |
| Module 3 | Chainlink consumer / staleness | Lending protocols are the #1 consumer of oracles β every M3 pattern (staleness, deviation, L2 sequencer) is load-bearing here |
| Module 3 | Dual oracle / fallback | Liquityβs 5-state oracle machine directly protects lending liquidation triggers |
β Forward References (Modules 5β9 + Part 3)
| Target | Concept | How Lending Knowledge Applies |
|---|---|---|
| Module 5 (Flash Loans) | Flash loan liquidation | Flash loans enable zero-capital liquidation β borrow β liquidate β swap β repay atomically |
| Module 6 (Stablecoins) | CDP liquidation | CDPs are a specialized lending model where the βborrowedβ asset is minted (DAI); same HF math, same liquidation triggers |
| Module 7 (Yield/Vaults) | Index-based accounting | ERC-4626 share pricing uses the same scaledBalance Γ index pattern; vaults use totalAssets / totalShares instead of accumulating index |
| Module 7 (Yield/Vaults) | aToken composability | aTokens as yield-bearing inputs to vault strategies; auto-compounding aToken deposits |
| Module 8 (Security) | Economic attack modeling | Reserve factor determines treasury growth; economic exploits target the gap between reserves and potential bad debt |
| Module 8 (Security) | Invariant testing targets | Lending pool invariants (solvency, HF consistency, index monotonicity) are prime targets for formal verification |
| Module 9 (Integration) | Full-stack lending integration | Capstone combines lending + AMMs + oracles + flash loans in a production-grade protocol |
| Part 3 Module 8 (Governance) | Governance attack surface | Credit delegation and risk parameter changes create governance attack vectors; lending param manipulation |
| Part 3 Module 6 (Cross-chain) | Cross-chain lending | L2 β L1 collateral, shared liquidity across chains β extending lending architecture cross-chain |
π Production Study Order
Study these codebases in order β each builds on the previous oneβs patterns:
| # | Repository | Why Study This | Key Files |
|---|---|---|---|
| 1 | Compound V3 Comet | Simplest production lending codebase (~4,300 lines) β single-asset model, signed principal, immutable params | contracts/Comet.sol, contracts/CometExt.sol |
| 2 | Aave V3 Core | The dominant lending architecture β library pattern, aTokens, debt tokens, index accrual | contracts/protocol/pool/Pool.sol, contracts/protocol/libraries/logic/SupplyLogic.sol, contracts/protocol/libraries/logic/BorrowLogic.sol |
| 3 | Aave V3 LiquidationLogic | Production liquidation: close factor, collateral seizure, minimum position rules | contracts/protocol/libraries/logic/LiquidationLogic.sol |
| 4 | Aave V3 Interest Rate Strategy | The kinked curve in production β parameter encoding, compound interest approximation in MathUtils | contracts/protocol/pool/DefaultReserveInterestRateStrategy.sol, contracts/protocol/libraries/math/MathUtils.sol |
| 5 | Morpho Blue | Minimal lending core (~650 lines) β permissionless isolated markets, no governance, no upgradeability | src/Morpho.sol, src/libraries/ |
| 6 | Liquity V1 | CDP-style lending with zero governance β redemption mechanism, stability pool, recovery mode | contracts/BorrowerOperations.sol, contracts/TroveManager.sol, contracts/StabilityPool.sol |
Reading strategy: Start with Compound V3 (smallest codebase, single file). Then Aave V3 β trace one flow end-to-end (supply β index update β aToken mint). Study liquidation separately. Read the interest rate strategy to see the kinked curve in production. Morpho Blue shows the minimalist alternative. Liquity shows CDP-style lending with no governance dependency.
π Resources
Aave V3:
- Protocol documentation
- Source code (deployed May 2022)
- Risk parameters dashboard
- Technical paper
- MixBytes architecture analysis
- Cyfrin Aave V3 course
Compound V3:
- Documentation
- Source code (deployed August 2022)
- RareSkills Compound V3 Book
- RareSkills architecture walkthrough
Interest rate models:
Advanced / Emerging:
- Morpho Blue β minimal lending core (~650 lines), permissionless market creation
- MetaMorpho β ERC-4626 vault layer on top of Morpho Blue
- Euler V2 β modular vault architecture with connector system
- GHO stablecoin β Aaveβs native stablecoin via facilitator pattern
- Berkeley DeFi MOOC β Lending protocols
Exploits and postmortems:
- Euler Finance postmortem β $197M donation attack
- Radiant Capital postmortem β $4.5M flash loan rounding exploit
- Rari Capital/Fuse postmortem β $80M reentrancy
- Cream Finance postmortem β $130M oracle manipulation
- Hundred Finance postmortem β $7M ERC-777 reentrancy
- Venus Protocol postmortem β $11M stale oracle
- CRV liquidity crisis analysis β bad debt accumulation
- MakerDAO Black Thursday report β liquidation cascades
Navigation: β Module 3: Oracles | Module 5: Flash Loans β