Section 16 of 18
Liquidation: CToken Side
What You Are Building
You are building the CToken execution side of liquidation. The previous section built the policy (when liquidation is allowed, how many tokens to seize). This section builds the mechanism: accrue interest on both markets, repay the borrower's debt, calculate the seize amount, and transfer collateral from the borrower to the liquidator with a protocol cut.
This is the most complex CToken operation because it involves TWO markets simultaneously. The liquidator repays debt in the borrowed market (say cUSDC) and receives collateral from a different market (say cETH). The Comptroller's cross-market view is what makes this possible.
The Full Liquidation Flow
When a liquidator calls liquidateBorrow(borrower, repayAmount, cTokenCollateral):
-
Accrue interest on BOTH markets. The borrowed market needs current borrow balances (to verify the repay amount against the close factor). The collateral market needs a current exchange rate (to convert seized underlying value to cToken quantity). Stale interest on either side produces wrong numbers.
-
Policy check.
comptroller.liquidateBorrowAllowed()verifies the borrower is underwater and the repay amount respects the close factor. -
Repay the debt. This reuses
repayBorrowFreshfrom section 13, with the liquidator as payer and the borrower as the debtor. The liquidator's tokens flow into the borrowed market, and the borrower's BorrowSnapshot is reduced. -
Calculate seize amount.
comptroller.liquidateCalculateSeizeTokens()returns the number of collateral cTokens to seize based on the repaid amount, oracle prices, the liquidation incentive, and the collateral exchange rate. -
Seize collateral.
seizeInternaltransfers cTokens from the borrower to the liquidator, with 2.8% going to the protocol.
The Protocol Seize Share: 2.8%
When collateral is seized, 2.8% goes to the protocol as reserves. The remaining 97.2% goes to the liquidator:
uint256 protocolSeizeTokens = mul_(seizeTokens, Exp({ mantissa: protocolSeizeShareMantissa }));
uint256 liquidatorSeizeTokens = seizeTokens - protocolSeizeTokens;
The protocol's portion is not kept as cTokens. Instead, those cTokens are effectively burned (totalSupply decreases) and their underlying value is added to totalReserves. This increases the exchange rate for all remaining cToken holders slightly, as reserves back the same totalSupply minus the burned tokens.
Without this cut, all liquidation revenue would go to liquidators. The protocol would bear the risk of bad debt with no income from the liquidation mechanism that prevents it. The 2.8% creates a revenue stream that builds the protocol's reserve buffer.
Why Two Markets Are Involved
Consider a user who deposited ETH and borrowed USDC. When they get liquidated:
- The borrowed market is cUSDC: the liquidator sends USDC here to repay debt
- The collateral market is cETH: the borrower's cETH tokens are seized
The liquidation function on cUSDC needs to call seize() on cETH. If both happen to be the same market (a user borrowed and collateralized in the same asset), seizeInternal is called directly. Otherwise, the external seize() function is called on the collateral cToken.
MEV and Liquidation Bots
In practice, liquidation is a competitive market. Bots monitor every borrower's health factor and submit liquidation transactions the moment shortfall appears. They compete on gas price (or via Flashbots bundles) for the right to capture the 8% incentive minus the 2.8% protocol cut. This competition is what makes the system work: the protocol does not need to run its own liquidation infrastructure. Economic incentives ensure third parties do it.
Your Task
Build the CTokenLiquidate contract inheriting from CTokenRepay. Implement:
liquidateBorrowInternal: Accrue interest on both the borrowed market (this contract) and the collateral market, then call liquidateBorrowFreshliquidateBorrowFresh: Validate liquidator is not the borrower, check with comptroller, call repayBorrowFresh to repay debt, call comptroller.liquidateCalculateSeizeTokens, verify borrower has enough collateral, call seize (internal or external)seizeInternal: Check with comptroller.seizeAllowed, calculate protocol and liquidator shares using protocolSeizeShareMantissa, convert protocol's cTokens to underlying for reserves, update all balances (borrower, liquidator, totalSupply, totalReserves)seize: External function that delegates to seizeInternal (called by other cTokens during cross-market liquidation)