Section 7 of 18
CToken: Withdraw (redeem)
What You Are Building
You are adding the withdrawal mechanism to the CToken. Users burn cTokens to get their underlying tokens back. This is the reverse of mintFresh() from the previous section, but with a critical difference: the Comptroller can block the withdrawal if those cTokens are backing a borrow.
Concept: The Dual-Redeem Pattern
Compound V2 offers two ways to withdraw. First, "I want to burn exactly X cTokens" (redeemInternal). Second, "I want to receive exactly Y underlying tokens" (redeemUnderlyingInternal). Both funnel into a single redeemFresh() function that accepts two parameters, one of which must be zero.
This dual-parameter pattern exists because users think in different terms depending on context. A user closing their entire position wants to burn all their cTokens. A user who needs exactly 1,000 DAI for a payment wants to specify the underlying amount. Forcing everyone through one interface creates UX friction and rounding confusion.
The conversion between the two uses the exchange rate you built in section 5:
// Burning exact cTokens: calculate underlying received
redeemAmount = mul_ScalarTruncate(exchangeRate, redeemTokensIn);
// Receiving exact underlying: calculate cTokens to burn
redeemTokens = div_(redeemAmountIn, exchangeRate);
Concept: Comptroller Solvency Check
In Uniswap V2, you can always burn your LP tokens. Nobody can stop you from withdrawing liquidity. In Compound, withdrawal can be blocked.
When a user has entered a market as collateral and has outstanding borrows elsewhere, their cTokens are "working" as collateral. The Comptroller checks whether removing those cTokens would make the user's position undercollateralized. If it would, the redeem is rejected.
This is the line that enforces it:
uint256 allowed = comptroller.redeemAllowed(address(this), redeemer, redeemTokens);
require(allowed == 0, "CToken: redeem rejected by comptroller");
The Comptroller runs a hypothetical liquidity calculation: "What would this user's collateral ratio look like if they redeemed these tokens?" If the answer is "underwater," the transaction reverts. You will build redeemAllowed in section 11.
Concept: Cash Sufficiency
Even if the Comptroller approves the withdrawal, the protocol must have enough underlying tokens sitting in the contract. If most of the pool is currently borrowed out, there might not be enough cash. This is the utilization problem: high utilization means suppliers cannot withdraw freely.
require(getCashPrior() >= redeemAmount, "CToken: insufficient cash");
This is why Compound's interest rate model spikes rates at high utilization. It creates economic pressure for borrowers to repay, freeing up cash for suppliers who want to withdraw.
Your Task
Implement three functions in the CTokenRedeem contract that inherits from CTokenMint:
-
redeemInternal(uint256 redeemTokens): CallaccrueInterest(), then callredeemFresh(msg.sender, redeemTokens, 0). -
redeemUnderlyingInternal(uint256 redeemAmount): CallaccrueInterest(), then callredeemFresh(msg.sender, 0, redeemAmount). -
redeemFresh(address redeemer, uint256 redeemTokensIn, uint256 redeemAmountIn):- Require that exactly one of the two inputs is zero.
- Get the exchange rate via
exchangeRateStoredInternal(). - If
redeemTokensIn > 0: setredeemTokens = redeemTokensInand computeredeemAmount = mul_ScalarTruncate(exchangeRate, redeemTokensIn). - If
redeemAmountIn > 0: setredeemTokens = div_(redeemAmountIn, exchangeRate)andredeemAmount = redeemAmountIn. - Call
comptroller.redeemAllowed(address(this), redeemer, redeemTokens)and require it returns 0. - Require
accrualBlockNumber == block.number. - Require
getCashPrior() >= redeemAmount. - Subtract
redeemTokensfromtotalSupplyandaccountTokens[redeemer]. - Call
doTransferOut(redeemer, redeemAmount). - Emit
Redeem(redeemer, redeemAmount, redeemTokens)andTransfer(redeemer, address(0), redeemTokens).