Section 12 of 18
CToken: Borrow
What You Are Building
You are adding the borrow mechanism to the CToken. This is what makes Compound a lending protocol, not just a savings account. Users who have supplied collateral and entered markets can borrow underlying tokens from any market. The borrowed amount accrues interest over time through the BorrowSnapshot pattern you saw in section 5.
Uniswap V2 has no equivalent. There is nothing in Uniswap that lets you take tokens out of a pool and owe them back later. Borrowing is an entirely new concept relative to what you built in the DEX module.
Concept: How Borrowing Works
The flow is straightforward: the user requests to borrow X underlying tokens, the CToken asks the Comptroller for permission (cross-market solvency check), and if approved, transfers the tokens out and records the debt.
What makes it interesting is how the debt is tracked. The protocol does not update every borrower's balance every block. Instead, it uses the BorrowSnapshot pattern from section 4:
struct BorrowSnapshot {
uint256 principal; // Borrow balance at last interaction
uint256 interestIndex; // borrowIndex at last interaction
}
A borrower's current debt is computed lazily:
currentBorrow = snapshot.principal * currentBorrowIndex / snapshot.interestIndex
The global borrowIndex grows every time accrueInterest() runs. By dividing the current index by the snapshot's index, you get the interest multiplier since the borrower's last interaction. This is the same mathematical trick as Uniswap V2's cumulative price oracle: store a running accumulator, compute differences on demand.
Concept: The borrowFresh Flow
borrowFresh follows the same pattern as mintFresh and redeemFresh, calling the Comptroller first:
uint256 allowed = comptroller.borrowAllowed(address(this), borrower, borrowAmount);
require(allowed == 0, "CToken: borrow rejected by comptroller");
The Comptroller's borrowAllowed (which you built in section 11) runs the hypothetical liquidity calculation: "What would this user's solvency look like if they borrowed this additional amount?" If it creates a shortfall, the borrow is rejected.
After the Comptroller check, borrowFresh verifies cash availability. The protocol can only lend out tokens it actually holds. If utilization is near 100%, there might not be enough cash. This is the same check as redeemFresh, and it is the reason high utilization drives interest rates up.
Concept: The BorrowSnapshot Update
The most important part of borrowFresh is the snapshot update:
accountBorrows[borrower].principal = accountBorrowsNew;
accountBorrows[borrower].interestIndex = borrowIndex;
This "resets" the borrower's interest accumulation point. Before the update, borrowBalanceStoredInternal() computed their balance using the OLD snapshot. After the update, it uses the NEW snapshot with the current borrowIndex. Any interest accrued before this moment is already baked into accountBorrowsNew (computed via borrowBalanceStoredInternal). Future interest will be calculated relative to the current borrowIndex.
The totalBorrows update is equally important. This global variable affects the exchange rate (which determines how much suppliers earn) and the interest rate model (which determines how much borrowers pay). Every new borrow increases totalBorrows, pushing utilization up and rates higher.
Your Task
Build CTokenBorrow inheriting from CTokenRedeem:
-
borrowInternal(uint256 borrowAmount): internal. CallaccrueInterest(), then callborrowFresh(msg.sender, borrowAmount). -
borrowFresh(address borrower, uint256 borrowAmount): internal.- Call
comptroller.borrowAllowed(address(this), borrower, borrowAmount)and require it returns 0. - Require
accrualBlockNumber == block.number. - Require
getCashPrior() >= borrowAmount. - Compute the borrower's previous borrow balance:
uint256 accountBorrowsPrev = borrowBalanceStoredInternal(borrower). - Compute
accountBorrowsNew = accountBorrowsPrev + borrowAmount. - Compute
totalBorrowsNew = totalBorrows + borrowAmount. - Update the BorrowSnapshot: set
accountBorrows[borrower].principal = accountBorrowsNewandaccountBorrows[borrower].interestIndex = borrowIndex. - Update
totalBorrows = totalBorrowsNew. - Call
doTransferOut(borrower, borrowAmount). - Emit
Borrow(borrower, borrowAmount, accountBorrowsNew, totalBorrowsNew).
- Call