Section 13 of 18
CToken: Repay
What You Are Building
You are building the repayment mechanism that reverses a borrow. When a borrower wants to reduce their debt, they send underlying tokens back to the protocol. The protocol reduces their BorrowSnapshot and decreases totalBorrows. This is the mirror image of borrowFresh from the previous section.
Repayment introduces two patterns that do not exist in Uniswap V2 because Uniswap has no debt. First, the "repay on behalf" pattern: anyone can repay another user's debt. No approval is needed because repaying someone's debt can only help them. This is essential for liquidation, where the liquidator repays the borrower's debt as part of the seizure process. Second, the type(uint256).max convenience pattern: instead of forcing the user to calculate their exact debt (which changes every block due to interest accrual), they can pass max uint to mean "repay everything."
How Repayment Works
The flow mirrors borrowFresh in reverse:
accrueInterest()to ensure the borrowIndex is currentcomptroller.repayBorrowAllowed()verifies the market is listed (minimal check, repaying is always safe)- Calculate the borrower's current debt using
borrowBalanceStoredInternal(borrower), which applies accrued interest via the BorrowSnapshot pattern - Determine the actual repay amount. If the caller passed
type(uint256).max, use the full borrow balance doTransferIn(payer, repayAmountFinal)pulls underlying tokens from the payer into the contract- Update the BorrowSnapshot: reduce principal, reset interestIndex to current borrowIndex
- Decrease totalBorrows
The BorrowSnapshot update follows exactly the same logic as in borrowFresh. After repayment, the borrower's snapshot records their new (lower) principal and the current borrowIndex. Future interest calculations start from this point.
The type(uint256).max Pattern
Borrowers accrue interest every block. If a borrower owes 1000 USDC and tries to repay exactly 1000 USDC, by the time the transaction executes, they might owe 1000.0001 USDC. The transaction would leave a tiny dust balance. The type(uint256).max pattern solves this:
uint256 repayAmountFinal;
if (repayAmount == type(uint256).max) {
repayAmountFinal = accountBorrowsPrev;
} else {
repayAmountFinal = repayAmount;
}
The contract reads the exact current debt and uses that as the repay amount. The user approves a generous token allowance, and the contract pulls exactly what is owed.
repayBorrowBehalf: Anyone Can Repay
The two entry points differ only in who the borrower is:
// Repay your own debt
function repayBorrowInternal(uint256 repayAmount) internal {
accrueInterest();
repayBorrowFresh(msg.sender, msg.sender, repayAmount);
}
// Repay someone else's debt
function repayBorrowBehalfInternal(address borrower, uint256 repayAmount) internal {
accrueInterest();
repayBorrowFresh(msg.sender, borrower, repayAmount);
}
In repayBorrowFresh, the payer and borrower are separate parameters. The payer's tokens are transferred in, but the borrower's BorrowSnapshot is updated. This separation is what makes liquidation possible: the liquidator is the payer, the underwater user is the borrower.
The doTransferIn Virtual Function
doTransferIn is declared as a virtual function here. It returns the actual amount received, not the requested amount. This matters for fee-on-transfer tokens: if a token charges a 1% transfer fee, requesting 100 tokens only deposits 99. The protocol uses the actual received amount for all accounting. The concrete implementation lives in CErc20 (section 17), where it uses ERC-20 transferFrom with a before/after balance check.
Your Task
Build the CTokenRepay contract inheriting from CTokenBorrow. Implement:
repayBorrowInternalthat accrues interest, then calls repayBorrowFresh with msg.sender as both payer and borrowerrepayBorrowBehalfInternalthat accrues interest, then calls repayBorrowFresh with msg.sender as payer and a separate borrowerrepayBorrowFreshwith the full repay flow: comptroller check, block number verification, borrow balance calculation, type(uint256).max handling, doTransferIn, BorrowSnapshot update, totalBorrows update, and event emission- The
doTransferInvirtual function declaration (no body, just the signature)