Keyboard shortcuts

Press ← or β†’ to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Part 2 β€” Module 4: Lending & Borrowing

Difficulty: Advanced

Estimated reading time: ~65 minutes | Exercises: ~3-4 hours


πŸ“š Table of Contents

The Lending Model from First Principles

Aave V3 Architecture β€” Supply and Borrow

Aave V3 β€” Risk Modes and Advanced Features

Compound V3 (Comet) β€” A Different Architecture

Liquidation Mechanics

Synthesis and Advanced Patterns


πŸ’‘ 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, Compound $3B+, Spark (MakerDAO) $2.5B+ (as of Q4 2024). Combined, lending protocols represent >$30B in user deposits.

Real impact β€” exploits: Lending protocols have been the target of some of DeFi’s largest hacks:

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:

  1. Suppliers deposit assets (e.g., USDC) into a pool. They earn interest from borrowers.
  2. 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.
  3. Interest accrues continuously. Borrowers pay it; suppliers receive it (minus a protocol cut called the reserve factor).
  4. 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):

UtilizationBorrow RateWhat’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 scaledBalance and 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:

OperationRound DirectionWhy
Deposit β†’ scaledBalanceRound downFewer shares = less claim on pool
Withdraw β†’ actual amountRound downUser gets slightly less
Borrow β†’ scaledDebtRound upMore debt recorded
Repay β†’ remaining debtRound upSlightly 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 because accrueInternal() is called frequently.

⚠️ Common Lending Accounting Mistakes

These patterns have caused real exploits and audit findings:

1. 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.

2. 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.

3. 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 up

Impact: Each borrow creates slightly less debt than it should. Over millions of borrows, the shortfall accumulates. Aave V3 uses rayDiv (round down) for deposits and rayDiv with round-up for debt.

4. Not handling type(uint256).max for full repayment

// ❌ WRONG β€” 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 this pattern, users can never fully repay their debt because interest accrues between calculation and execution. Tiny dust debts accumulate across thousands of users. Aave V3 handles this explicitly.

πŸ’Ό Job Market Context

What DeFi teams expect you to know about lending fundamentals:

  1. β€œExplain how a lending protocol’s interest rate model works.”

    Answer
    • 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
  2. β€œHow does interest accrue without updating every user’s balance?”

    Answer
    • 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
  3. β€œWhat happens if a user’s health factor drops below 1?”

    Answer
    • 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.


🎯 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:

  1. RAY multiplication (rayMul) β€” the bread-and-butter operation of all lending protocol math. A reference rayDiv implementation is provided for you to study.
  2. Utilization rate (getUtilization) β€” the x-axis of the kinked curve
  3. Kinked borrow rate (getBorrowRate) β€” the two-slope curve with the gentle slope below optimal and the steep slope above
  4. Supply rate (getSupplyRate) β€” derived from borrow rate, utilization, and reserve factor
  5. 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.

πŸ“‹ Key Takeaways: The Lending Model

After this section, you should be able to:

  • Explain the DeFi lending loop (overcollateralization β†’ interest accrual β†’ liquidation) and define each key parameter: LTV, Liquidation Threshold, Health Factor, Liquidation Bonus, Reserve Factor, Close Factor
  • Sketch the two-slope kinked interest rate curve and explain why slope2 is intentionally steep (mechanism design: high rates above optimal utilization force repayments, rebalancing supply/demand without governance)
  • Derive the supply rate from borrow rate, utilization, and reserve factor, and calculate a numeric example
  • Walk through index-based interest accrual: how a single global index (liquidityIndex, borrowIndex) avoids per-user updates, and perform a RAY multiplication by hand (27-decimal precision, rounding direction conventions)
Check your understanding
  • DeFi lending loop: Users deposit collateral worth more than they borrow (overcollateralization). Interest accrues continuously via per-second rate models. If collateral value drops relative to debt (health factor < 1), liquidators repay part of the debt and seize collateral at a discount. LTV caps max borrowing power, Liquidation Threshold triggers liquidation, Reserve Factor is the protocol’s cut of interest, and Close Factor limits how much can be liquidated per call.
  • Kinked interest rate curve: Below optimal utilization, the borrow rate rises gently (slope1) to encourage borrowing. Above optimal, slope2 is intentionally steep (often 50-300% APR) to make borrowing extremely expensive, forcing repayments and restoring the utilization ratio without requiring governance intervention.
  • Supply rate derivation: supplyRate = borrowRate * utilization * (1 - reserveFactor). For example: 5% borrow rate, 80% utilization, 10% reserve factor gives 0.05 * 0.8 * 0.9 = 3.6% supply APR. The reserve factor diverts a portion of interest to the protocol treasury.
  • Index-based accounting: Store a global liquidityIndex that starts at 1 RAY and grows with each interest accrual. A user’s actual balance = scaledBalance * currentIndex. When interest accrues, only the single global index updates β€” no iteration over users. RAY (10^27) provides sufficient precision for per-second compound interest calculations.

πŸ’‘ 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 was deprecated in Aave V3) 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:

  1. User calls Pool.supply(asset, amount, onBehalfOf, referralCode)
  2. Pool delegates to SupplyLogic.executeSupply()
  3. Logic validates the reserve is active and not paused
  4. Updates the reserve’s indexes (accrues interest up to this moment)
  5. Transfers the underlying asset from user to the aToken contract
  6. Mints aTokens to the onBehalfOf address (scaled by current index)
  7. Updates the user’s configuration bitmap (tracks which assets are supplied/borrowed)

πŸ“– Read: Borrow Flow

Source: BorrowLogic.sol

  1. User calls Pool.borrow(asset, amount, interestRateMode, referralCode, onBehalfOf)
  2. Pool delegates to BorrowLogic.executeBorrow()
  3. Logic validates: reserve active, borrowing enabled, amount ≀ borrow cap
  4. Validates the user’s health factor will remain > 1 after the borrow
  5. Mints debt tokens to the borrower (or onBehalfOf for credit delegation)
  6. Transfers the underlying asset from the aToken contract to the user
  7. 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:

  1. 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.

  2. Trace one complete flow end-to-end β€” Pick supply(). Follow it into SupplyLogic.sol. Read every line of executeSupply(). Note: index update β†’ transfer β†’ mint aTokens β†’ update user config bitmap. Draw this as a sequence diagram.

  3. Understand the data model β€” Read DataTypes.sol. The ReserveData struct is the central state. Map each field to what it controls (indexes for interest, configuration bitmap for risk params, address pointers for aToken/debtToken).

  4. Read the index math β€” Open ReserveLogic.sol and trace updateState() β†’ _updateIndexes(). This is the compound interest accumulation. Then read how balanceOf() in AToken.sol uses the index to compute the live balance.

  5. 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:

  1. supply(amount) β€” transfer tokens in, compute scaled deposit using the liquidity index
  2. withdraw(amount) β€” convert scaled balance back, validate sufficient funds
  3. depositCollateral(token, amount) β€” post collateral (no interest earned, like Compound V3)
  4. borrow(amount) β€” record scaled debt, enforce health factor >= 1.0
  5. repay(amount) β€” burn scaled debt, cap at actual debt to prevent overpayment
  6. accrueInterest() β€” update both indexes using linear interest (simplified from Aave’s compound)
  7. 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() and deal(). Compare the live behavior with your simplified implementation.


πŸ“‹ Key Takeaways: Aave V3 Supply and Borrow

After this section, you should be able to:

  • Map Aave V3’s architecture: Pool proxy β†’ logic libraries (Supply, Borrow, Liquidation, FlashLoan, EMode) and explain why this library-based pattern keeps the Pool under the contract size limit
  • Trace a supply flow end-to-end: validate β†’ update indexes β†’ transfer underlying β†’ mint aTokens β†’ update config bitmap, and explain how aToken’s auto-rebasing balanceOf() works via the liquidity index
  • Explain how debt tokens track borrower obligations through the borrow index and why they are non-transferable
  • Describe credit delegation (onBehalfOf + approveDelegation()) and identify where it enables undercollateralized lending for trusted counterparties
Check your understanding
  • Aave V3 architecture: The Pool contract is a proxy that delegates to specialized logic libraries (SupplyLogic, BorrowLogic, LiquidationLogic, FlashLoanLogic, EModeLogic). This library-based pattern keeps the Pool contract under the 24KB EVM contract size limit while maintaining a single entry point for users.
  • Supply flow: Validate parameters, update reserve indexes (accrue pending interest), transfer underlying tokens from user to the aToken contract, mint aTokens to the user, update the reserve configuration bitmap. The aToken’s balanceOf() auto-rebases by multiplying the user’s scaled balance by the current liquidityIndex.
  • Debt tokens: Variable debt tokens track borrower obligations through the borrowIndex. A user’s actual debt = scaledDebt * currentBorrowIndex (the scaledDebt already incorporates division by the borrow index at time of borrowing). Debt tokens are intentionally non-transferable β€” if you could transfer debt, you could create undercollateralized positions by sending debt to an empty address.
  • Credit delegation: A collateral depositor calls approveDelegation() on a debt token, allowing a delegatee to borrow against their collateral via the onBehalfOf parameter. This enables undercollateralized lending between trusted parties (e.g., institutional desks), while the protocol’s risk remains fully collateralized from its perspective.

πŸ’‘ 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:

  1. setLtv/getLtv β€” bits 0-15 (simplest: no offset needed)
  2. setLiquidationThreshold/getLiquidationThreshold β€” bits 16-31
  3. setFlag/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).


πŸ“‹ Key Takeaways: Aave V3 Risk Modes

After this section, you should be able to:

  • Explain E-Mode, Isolation Mode, and Siloed Borrowing: what risk each addresses, what it restricts, and give a concrete example of when each is used
  • Describe how Supply and Borrow Caps prevent excessive concentration and how the Virtual Balance Layer defeats donation attacks
  • Decode Aave V3’s configuration bitmap: how all risk parameters are packed into a single uint256 using bit shifts, and why this is gas-efficient (1 SLOAD for all parameters)
Check your understanding
  • E-Mode, Isolation Mode, Siloed Borrowing: E-Mode allows higher LTV for correlated assets (e.g., stablecoin-to-stablecoin at 97% LTV). Isolation Mode restricts new/volatile assets to single-collateral positions with a debt ceiling, limiting protocol-wide exposure. Siloed Borrowing prevents an asset from being borrowed alongside others, containing risk from assets with manipulable oracle prices.
  • Supply/Borrow Caps and Virtual Balance: Caps limit concentration risk by setting maximum supply and borrow amounts per asset. The Virtual Balance Layer adds a protocol-controlled virtual amount to totalSupply and totalBorrow, making donation attacks (where an attacker sends tokens directly to inflate share prices) unprofitable by diluting the attacker’s impact.
  • Configuration bitmap: All risk parameters (LTV, liquidation threshold, decimals, flags for active/frozen/borrowing/paused, reserve factor, caps, e-mode category) are packed into a single uint256 using bit masks and shifts. Reading any combination of parameters costs one SLOAD (2100 gas) instead of multiple storage reads, which is critical for functions like health factor computation that read many parameters per asset.

πŸ’‘ 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 the repayAndSupplyAmount() 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 supplied
  • getSupplyRate() / getBorrowRate(): The kinked curve implementations
  • accrueInternal(): How indexes are updated using block.timestamp and per-second rates
  • isLiquidatable(): 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.

  1. 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.

  2. Read supplyInternal() and withdrawInternal() β€” 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.

  3. Trace the index update in accrueInternal() β€” This is simpler than Aave’s version. One function, linear compound, per-second rates. Map how baseSupplyIndex and baseBorrowIndex grow over time.

  4. 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.

  5. 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?


πŸ“‹ Key Takeaways: Compound V3 (Comet)

After this section, you should be able to:

  • Contrast Compound V3’s single-asset model with Aave V3’s multi-asset pool: what each gains and sacrifices (V3 trades composability for simpler risk isolation)
  • Explain the signed principal pattern (positive = supplier, negative = borrower) and why Compound V3 doesn’t need separate debt tokens
  • Compare immutable variables (3 gas) vs SLOAD (2100 gas) for parameter storage and explain the redeployment trade-off
  • Articulate why neither architecture is strictly better and when you’d choose each for a new protocol
Check your understanding
  • Single-asset vs multi-asset: Compound V3 deploys a separate market per base asset (e.g., USDC market, ETH market), giving inherent risk isolation β€” a bad collateral in one market cannot affect another. Aave V3’s shared pool allows cross-collateralization and more capital efficiency but requires complex risk modes (E-Mode, Isolation) to manage contagion.
  • Signed principal pattern: Compound V3 stores a single int104 principal per user β€” positive means supplier, negative means borrower. Combined with a global baseSupplyIndex or baseBorrowIndex, this eliminates the need for separate aToken and debtToken contracts, reducing code complexity and deployment cost.
  • Immutable vs storage parameters: Compound V3 stores rate model parameters as immutable variables (3 gas to read vs 2100 for SLOAD). The trade-off is that changing any parameter requires redeploying the entire Comet contract and migrating the proxy, making governance updates more operationally complex.
  • Choosing an architecture: Choose Aave-style for protocols needing cross-collateralization, broad asset support, and composable receipt tokens (aTokens). Choose Compound V3-style for simpler risk models, lower gas costs, and when each market should be independently isolated. Choose Morpho Blue-style for maximum modularity with minimal core complexity.

πŸ’‘ 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, and debtToCover
  • 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:

  1. Anyone can call absorb(absorber, [accounts]) for one or more underwater accounts
  2. The protocol seizes the underwater account’s collateral and stores it internally
  3. The underwater account’s debt is written off (socialized across suppliers via a β€œdeficit” in the protocol)
  4. 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

⚠️ Common Liquidation Mistakes

1. 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.

2. 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 state

Impact: Partial liquidation that leaves a dust position still underwater β†’ no one can liquidate the remainder profitably β†’ bad debt.

πŸ’Ό Job Market Context β€” Liquidation Mechanics

What DeFi teams expect you to know about liquidation:

  1. β€œDesign a liquidation bot. What’s your architecture?”

    Answer
    • 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)
  2. β€œA user reports they were liquidated unfairly. How do you investigate?”

    Answer
    • 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.
  3. β€œCompare Aave’s direct liquidation with Compound V3’s absorb/auction model.”

    Answer
    • 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.


🎯 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:

  1. liquidate(borrower, debtToken, debtAmount, collateralToken) β€” entry point that encodes parameters and requests a flash loan
  2. onFlashLoan(...) β€” ERC-3156 callback that performs the liquidation with borrowed funds. Two critical security checks are required (caller validation and initiator validation).
  3. _sellCollateral(...) β€” approve and swap seized collateral on the DEX
  4. _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.

πŸ“‹ Key Takeaways: Liquidation Mechanics

After this section, you should be able to:

  • Trace the 5-step liquidation flow (detection β†’ call β†’ debt repayment β†’ collateral seizure β†’ HF restoration) in both Aave V3 and Compound V3
  • Compare Aave’s direct liquidator model (close factor, liquidation bonus) with Compound V3’s two-step absorb() + buyCollateral() Dutch auction and explain why the latter prevents sandwich attacks
  • Describe flash loan liquidation: how atomic borrow β†’ liquidate β†’ swap β†’ repay enables zero-capital liquidation, and calculate whether a given liquidation is profitable (bonus vs gas + slippage)
Check your understanding
  • 5-step liquidation flow: Monitor positions for health factor < 1 (detection). Call liquidationCall() specifying the borrower, debt token, amount, and collateral token to receive. The protocol transfers debt tokens from the liquidator, seizes collateral (plus bonus) from the borrower, and the borrower’s health factor is restored above 1.
  • Aave vs Compound V3 liquidation: Aave uses direct liquidation β€” the liquidator repays debt and receives collateral plus a fixed bonus (e.g., 5%) in one call, but this is sandwichable (MEV bots can front-run). Compound V3 uses two steps: absorb() lets the protocol seize the position, then buyCollateral() sells it via a Dutch auction that starts above market price and decays, making sandwich attacks unprofitable.
  • Flash loan liquidation: Borrow the debt token via flash loan (zero upfront capital), use it to call liquidationCall(), receive the seized collateral (worth more than the debt due to liquidation bonus), swap the collateral back to the debt token on a DEX, repay the flash loan plus fee, keep the profit. Profitable when: collateral * (1 + bonus) * DEX_rate > debt + flash_fee + gas.

🎯 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:

  1. supply(asset, amount) β€” Transfer tokens in, update supply index, store scaled balance
  2. withdraw(asset, amount) β€” Check health factor remains > 1 after withdrawal, transfer tokens out
  3. depositCollateral(asset, amount) β€” Transfer collateral tokens in (no interest earned)
  4. borrow(asset, amount) β€” Check health factor after borrow, mint scaled debt, transfer tokens out
  5. repay(asset, amount) β€” Burn scaled debt, transfer tokens in. Handle type(uint256).max for full repayment (see Aave’s pattern for handling dust from continuous interest accrual)
  6. liquidate(user, collateralAsset, debtAsset, debtAmount) β€” Validate HF < 1, repay debt, seize collateral with bonus

Supporting functions:

  1. accrueInterest(asset) β€” Update supply and borrow indexes using kinked rate model
  2. getHealthFactor(user) β€” Sum collateral values Γ— LT, sum debt values, compute ratio. Use Chainlink mock for prices.
  3. 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.


πŸ“‹ Key Takeaways: SimpleLendingPool

After this section, you should be able to:

  • Design a lending pool’s state layout: Reserve struct (index, rate, totalSupply), UserPosition struct (scaled balances), and explain why index-based accounting avoids per-user iteration
  • Implement the core flow: supply β†’ withdraw β†’ depositCollateral β†’ borrow β†’ repay β†’ liquidate, with correct interest accrual before every state change
  • Integrate Chainlink oracles for health factor computation and explain the full formula: HF = Ξ£(collateral Γ— price Γ— LT) / Ξ£(debt Γ— price)
  • Write tests that verify interest accuracy across time jumps, HF boundary behavior, liquidation correctness, and over-borrow reverts
Check your understanding
  • Lending pool state layout: The Reserve struct holds global state per asset (supply/borrow indexes, totals, timestamps, reserve factor). UserPosition holds scaled balances (actual = scaled * currentIndex). Index-based accounting means interest accrual only updates the global index, not individual user records β€” O(1) regardless of user count.
  • Core flow with interest accrual: Before any state-changing operation (supply, withdraw, borrow, repay, liquidate), first call accrueInterest() to update the reserve’s indexes based on elapsed time and current utilization. This ensures all subsequent calculations use up-to-date values. Forgetting to accrue before a state change is a common bug.
  • Health factor with oracles: HF = sum(collateral_amount * oracle_price * liquidation_threshold) / sum(debt_amount * oracle_price). Each collateral and debt asset gets its price from a Chainlink feed, normalized to a common decimal base. HF >= 1 means safe; HF < 1 means liquidatable.
  • Testing lending protocols: Use vm.warp() to advance time and verify interest accrual matches expected values (within 1 wei tolerance for rounding). Test HF boundary: borrow up to exactly HF=1, then verify one more wei reverts. Test liquidation: drop oracle price via vm.mockCall, verify liquidation succeeds and HF improves.

πŸ’‘ Synthesis and Advanced Patterns

πŸ“‹ Architectural Comparison: Aave V3 vs Compound V3

DimensionAave V3Compound V3
Borrowable assetsMultiple per poolSingle base asset per market
Collateral interestYes (aTokens accrue)No
Debt representationNon-transferable debt tokensSigned principal in UserBasic
Parameter storageStorage variablesImmutable variables (cheaper reads, costlier updates)
Interest rate modelBorrow rate from curve, supply derivedIndependent supply and borrow curves
Liquidation modelDirect liquidator repays, receives collateralProtocol absorbs, then Dutch auction for collateral
Risk isolationE-Mode, Isolation Mode, Siloed BorrowingInherent via single-asset markets
Code size~15,000+ lines across libraries~4,300 lines in Comet
Upgrade pathUpdate logic libraries, keep proxyDeploy 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 (as of Q4 2024). 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(), and liquidate(). 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.

πŸ’Ό Job Market Context

What DeFi teams expect you to know about lending architecture:

  1. β€œCompare Aave V3 and Compound V3 architectures. When would you choose one over the other?”

    Answer
    • 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)
  2. β€œHow would you prevent bad debt in a lending protocol?”

    Answer
    • 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
  3. β€œWalk me through a liquidation cascade. How would you design defenses?”

    Answer
    • 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)

🎯 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.

πŸ“‹ Key Takeaways: Synthesis and Advanced Patterns

After this section, you should be able to:

  • Articulate the Aave V3 vs Compound V3 architectural trade-off in an interview: why each design was chosen, what each gains, and when you’d pick one over the other
  • Explain bad debt handling: Aave’s Safety Module (staked AAVE backstop) vs Compound’s absorb/auction socialization, and describe the liquidation cascade feedback loop and its defenses
  • Describe the modular lending trend: Morpho Blue’s ~650-line minimal core with permissionless isolated markets, Euler V2’s vault graph architecture, and how they differ from Aave/Compound monoliths
  • Explain GHO’s facilitator pattern: how Aave serves as both lending protocol and stablecoin issuer
Check your understanding
  • Aave V3 vs Compound V3 trade-off: Aave offers richer features (cross-collateralization, composable aTokens, E-Mode) at the cost of complexity (~15,000 lines). Compound V3 offers simplicity and lower gas (~4,300 lines, immutable params) but sacrifices multi-asset borrowing and collateral interest. In an interview, frame it as a modularity-vs-monolith trade-off and reference which real protocols chose which approach.
  • Bad debt handling: Aave’s Safety Module holds staked AAVE that can be slashed to cover bad debt β€” an explicit insurance mechanism funded by stakers who earn protocol revenue. Compound V3’s absorb() socializes losses across all suppliers by reducing the protocol’s reserves. Liquidation cascades occur when seized collateral is sold into thin markets, further depressing prices and triggering more liquidations.
  • Modular lending trend: Morpho Blue’s ~650-line core allows permissionless market creation with 5 immutable parameters per market, achieving risk isolation without E-Mode/Isolation complexity. Euler V2 uses a vault graph where vaults can accept other vault shares as collateral, creating composable risk tiers. Both represent a shift from monolithic pools to modular, permissionless primitives.
  • GHO facilitator pattern: GHO is a stablecoin minted directly through Aave V3 β€” when users borrow GHO, new tokens are minted (no liquidity pool needed). Aave acts as a β€œfacilitator” with a minting cap set by governance. Other facilitators can be added (e.g., FlashMinter for flash-mintable GHO), making the stablecoin supply modular and governance-controlled.

πŸ“š Resources

Aave V3:

Compound V3:

Interest rate models:

Advanced / Emerging:

Exploits and postmortems:


Navigation: ← Module 3: Oracles | Module 5: Flash Loans β†’