Section 11 of 18

Build
+15 Lynx

Comptroller: Policy Hooks

What You Are Building

You are building the policy hooks that CTokens call before executing operations. Every mint, redeem, borrow, repay, and liquidation goes through the Comptroller for permission first. These hooks are how the Comptroller enforces risk constraints across the entire protocol. You will also build liquidateCalculateSeizeTokens, the formula that determines how much collateral a liquidator receives.

The hook pattern is a security boundary. If a hook is missing, has incorrect logic, or can be bypassed, the entire protocol is at risk. The $80M COMP distribution bug in 2021 was caused by a broken hook during a Comptroller upgrade. Getting these right is non-negotiable.

Concept: The "Allowed" Pattern

Every CToken operation follows the same flow: call the Comptroller's xxxAllowed() hook, require it returns 0, then execute. This is a policy injection pattern. The CToken handles the token mechanics (balances, transfers). The Comptroller handles the risk checks (solvency, market listing, close factor).

This separation has a major advantage: you can upgrade risk logic without touching token contracts. Compound's governance has changed risk parameters many times without redeploying a single CToken. The CToken always delegates to whatever Comptroller it points to.

Compare this to Uniswap V2, where each pair is fully self-contained. The pair validates its own invariant (k), checks for minimum amounts, and handles fees. There is no external policy contract. This works because Uniswap pools are independent. Compound markets are interconnected, and an external policy engine is the natural consequence.

Concept: What Each Hook Checks

Each hook has a different risk profile:

mintAllowed: The simplest. Just checks that the market is listed. Minting (supplying) only adds collateral, so it never increases risk. Even if you have not entered the market, you can still supply. You just will not get borrowing power until you enter.

redeemAllowed: Checks listing plus solvency. If you have entered this market and have borrows elsewhere, the Comptroller runs the hypothetical liquidity check: "What if this user redeemed these tokens?" If that creates a shortfall, the redeem is blocked. If you have not entered the market, the redeem always succeeds because those tokens are not counted as collateral anyway.

borrowAllowed: The most complex. Three checks: market is listed, oracle has a price, and the hypothetical liquidity after the borrow shows no shortfall. Plus a UX convenience: if the borrower has not entered this market yet, the hook auto-enters them. When you borrow from a market, you implicitly need that market tracked for liquidation purposes.

repayBorrowAllowed: Just checks listing. Repaying is always good for protocol health. No risk check needed.

liquidateBorrowAllowed: Checks that both markets are listed, the borrower has a shortfall (is underwater), and the repay amount does not exceed closeFactor * borrowBalance. The close factor limit (typically 50%) prevents a liquidator from seizing all collateral at once. Partial liquidation gives the borrower a chance to recover.

Concept: Liquidation Seize Calculation

When a liquidator repays someone's debt, they receive the borrower's collateral at a discount. The formula for how many cTokens to seize is:

seizeTokens = (repayAmount * liquidationIncentive * priceBorrowed)
            / (priceCollateral * exchangeRate)

Breaking it down: repayAmount * priceBorrowed converts the repaid debt to dollar value. Multiply by liquidationIncentive (e.g., 1.08) to add the 8% bonus. Divide by priceCollateral * exchangeRate to convert that dollar value to cTokens of the collateral market.

Your Task

Build ComptrollerHooks inheriting from ComptrollerLiquidity:

  1. mintAllowed(address cToken, address, uint256): external view, returns uint256. Require market is listed. Return 0.

  2. redeemAllowed(address cToken, address redeemer, uint256 redeemTokens): external view, returns uint256. Require listed. If user has not entered the market, return 0. Otherwise, run hypothetical liquidity check with (redeemer, cToken, redeemTokens, 0), require no shortfall, return 0.

  3. borrowAllowed(address cToken, address borrower, uint256 borrowAmount): external (not view, since it modifies state), returns uint256. Require listed. If borrower has not entered, require msg.sender == cToken and call _addToMarketInternal. Require oracle price is not zero. Run hypothetical liquidity check with (borrower, cToken, 0, borrowAmount), require no shortfall, return 0.

  4. repayBorrowAllowed(address cToken, address, address, uint256): external view. Require listed. Return 0.

  5. liquidateBorrowAllowed(address cTokenBorrowed, address cTokenCollateral, address, address borrower, uint256 repayAmount): external view. Require both markets listed. Get borrower's liquidity and require shortfall > 0. Get borrower's borrow balance from CTokenInterface(cTokenBorrowed).getAccountSnapshot(borrower). Compute maxClose = mul_ScalarTruncate(Exp({mantissa: closeFactorMantissa}), borrowerBorrowBalance). Require repayAmount <= maxClose. Return 0.

  6. liquidateCalculateSeizeTokens(address cTokenBorrowed, address cTokenCollateral, uint256 actualRepayAmount): external view, returns (uint256, uint256). Get oracle prices for both. Get exchange rate from collateral's snapshot. Compute numerator = liquidationIncentive * priceBorrowed, denominator = priceCollateral * exchangeRate, ratio = numerator / denominator, seizeTokens = mul_ScalarTruncate(ratio, actualRepayAmount). Return (0, seizeTokens).

Your Code

Solution.sol
Solidity
Loading editor...

Requirements

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