Section 13 of 18

Build
+15 Lynx

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:

  1. accrueInterest() to ensure the borrowIndex is current
  2. comptroller.repayBorrowAllowed() verifies the market is listed (minimal check, repaying is always safe)
  3. Calculate the borrower's current debt using borrowBalanceStoredInternal(borrower), which applies accrued interest via the BorrowSnapshot pattern
  4. Determine the actual repay amount. If the caller passed type(uint256).max, use the full borrow balance
  5. doTransferIn(payer, repayAmountFinal) pulls underlying tokens from the payer into the contract
  6. Update the BorrowSnapshot: reduce principal, reset interestIndex to current borrowIndex
  7. 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:

  1. repayBorrowInternal that accrues interest, then calls repayBorrowFresh with msg.sender as both payer and borrower
  2. repayBorrowBehalfInternal that accrues interest, then calls repayBorrowFresh with msg.sender as payer and a separate borrower
  3. repayBorrowFresh with the full repay flow: comptroller check, block number verification, borrow balance calculation, type(uint256).max handling, doTransferIn, BorrowSnapshot update, totalBorrows update, and event emission
  4. The doTransferIn virtual function declaration (no body, just the signature)

Your Code

Solution.sol
Solidity
Loading editor...

Requirements

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