Section 5 of 18

Build
+15 Lynx

CToken: Exchange Rate and Interest Accrual

What You Are Building

This section adds the two most fundamental functions in the CToken: exchangeRateStoredInternal() and accrueInterest(). Every other operation in the protocol (mint, redeem, borrow, repay, liquidate) depends on these being correct. They are the heartbeat of the lending market.

You will also build borrowBalanceStoredInternal(), which uses the BorrowSnapshot pattern to compute a borrower's current debt, and getAccountSnapshot(), which the Comptroller uses for liquidity calculations.

The Exchange Rate Formula

The exchange rate answers the question: how much underlying does 1 cToken represent right now?

exchangeRate = (totalCash + totalBorrows - totalReserves) / totalSupply

This is analogous to how Uniswap V2 LP tokens represent a share of the pool. In Uniswap, LP value = total reserves / totalSupply. In Compound, the numerator is the total value controlled by the market: cash sitting in the contract plus outstanding borrows (the market is owed this money) minus reserves (the protocol's cut, not claimable by suppliers).

When totalSupply is 0 (no deposits yet), the formula would divide by zero. In that case, the function returns initialExchangeRateMantissa, typically 0.02e18. This means the first depositor of 100 DAI receives 5,000 cDAI (100 / 0.02). The high ratio gives cTokens more precision for interest tracking.

The exchange rate grows over time as borrowers pay interest. totalBorrows increases with each interest accrual, but totalSupply only changes when users mint or redeem. So the ratio increases, meaning each cToken is worth more underlying.

accrueInterest(): The Interest Engine

Every user-facing function starts by calling accrueInterest(). This function calculates how much interest has accumulated since the last accrual and updates the global state.

The algorithm uses simple interest per block (not compound interest). The compounding effect comes from calling the function frequently. Here is the step-by-step flow:

  1. Short-circuit check: If accrualBlockNumber == block.number, interest was already accrued this block. Return immediately. This makes it safe to call multiple times per block.

  2. Snapshot current state: Read cashPrior, borrowsPrior, reservesPrior, borrowIndexPrior.

  3. Get the borrow rate: Ask the InterestRateModel for the current borrow rate based on cash, borrows, and reserves.

  4. Calculate blocks elapsed: blockDelta = currentBlockNumber - accrualBlockNumberPrior. Could be 1 block or 1,000 blocks if nobody interacted with the market for a while.

  5. Calculate interest accumulated:

simpleInterestFactor = borrowRate * blockDelta
interestAccumulated = simpleInterestFactor * borrowsPrior / expScale
  1. Update global state:
totalBorrows += interestAccumulated
totalReserves += reserveFactor * interestAccumulated / expScale
borrowIndex += simpleInterestFactor * borrowIndexPrior / expScale
accrualBlockNumber = currentBlockNumber

The borrowIndex update is the critical line. It grows proportionally with the interest factor. Because every borrower's balance is computed as principal * currentBorrowIndex / snapshotIndex, growing the global index effectively applies interest to all borrowers simultaneously without iterating over them.

borrowBalanceStoredInternal: Lazy Per-User Interest

This function computes what a specific borrower currently owes:

currentBorrow = snapshot.principal * borrowIndex / snapshot.interestIndex

If the user has never borrowed (principal == 0), it returns 0. Otherwise, the ratio borrowIndex / snapshot.interestIndex captures all the interest that has accumulated since the user's last interaction. This is the payoff of the BorrowSnapshot pattern from section 4: one multiplication and one division replaces what would otherwise require iterating over every block since the last interaction.

getAccountSnapshot: The Comptroller's View

The Comptroller needs three pieces of information about each account to calculate liquidity: their cToken balance, their borrow balance, and the current exchange rate. getAccountSnapshot returns all three in a single call, avoiding multiple external calls and their associated gas costs.

function getAccountSnapshot(address account) external view returns (uint256, uint256, uint256, uint256) {
    return (0, accountTokens[account], borrowBalanceStoredInternal(account), exchangeRateStoredInternal());
}

The first return value (0) is a legacy error code from the original Compound implementation. Zero means no error.

Your Task

Build the CTokenInterest contract that inherits from CTokenStorage.

  1. Declare getCashPrior() as an internal virtual view function (returns uint256). This will be overridden by CErc20 later. It has no body in this contract.
  2. Implement exchangeRateStoredInternal(): if totalSupply == 0, return initialExchangeRateMantissa. Otherwise, compute (getCashPrior() + totalBorrows - totalReserves) * expScale / totalSupply.
  3. Implement accrueInterest(): short-circuit if already accrued this block, get the borrow rate from the interest rate model, compute blockDelta, compute simpleInterestFactor and interestAccumulated, update totalBorrows, totalReserves, borrowIndex, and accrualBlockNumber. Emit AccrueInterest. Return 0.
  4. Implement borrowBalanceStoredInternal(): if principal == 0 return 0, otherwise compute principal * borrowIndex / interestIndex.
  5. Implement getAccountSnapshot(): return the tuple (0, accountTokens[account], borrowBalanceStoredInternal(account), exchangeRateStoredInternal()).

Your Code

Solution.sol
Solidity
Loading editor...

Requirements

Write your implementation, then click Run Tests. Tests execute on the server.