Section 9 of 18

Build
+15 Lynx

Comptroller: Market Management

What You Are Building

You are building the Comptroller's market management layer. This is where the protocol tracks which markets exist, which users have opted into which markets, and the risk parameters for each market. Think of it as the risk engine's configuration layer.

Misconfigured collateral factors and missing access controls on market management are common vulnerabilities in Compound forks. Every parameter you set here directly determines how much risk the protocol takes on.

Concept: The Comptroller vs Uniswap Factory

Uniswap V2's Factory creates pairs and stores their addresses. That is the extent of its responsibility. Each pair operates independently. The Factory never checks whether a swap in one pair would affect another.

Compound's Comptroller is fundamentally different. It is a global risk engine. Every CToken calls the Comptroller before executing any operation. The Comptroller holds the cross-market view: it knows all markets, all user positions, and all risk parameters. When you try to borrow USDC using ETH as collateral, the CToken for USDC calls the Comptroller, which checks your ETH position in a completely different CToken.

This cross-market awareness is what makes lending possible. Without it, each market would be isolated, and collateral in one market could not back borrows in another.

Concept: The Market Struct

Each listed market has a Market struct:

struct Market {
    bool isListed;
    uint256 collateralFactorMantissa;
    mapping(address => bool) accountMembership;
}

isListed is a simple gate. If a market is not listed, no operations can proceed. This prevents users from minting cTokens for a rogue contract that was never approved.

collateralFactorMantissa determines how much borrowing power this asset provides. A factor of 0.75e18 means $100 of this collateral gives you $75 of borrowing power. Compound caps this at 0.9e18 (90%). Higher factors are more capital-efficient but riskier. If the collateral drops in value even slightly, it might not cover the borrow.

accountMembership tracks which users have "entered" this market. This is the explicit opt-in mechanism. You can supply cTokens to earn yield without entering the market. Your tokens will grow in value, but they will not count as collateral. You must explicitly call enterMarkets to enable collateral usage.

Concept: The Dual Storage Pattern

The Comptroller stores market membership in two places. The Market.accountMembership mapping answers "is user X in market Y?" in O(1). The accountAssets[user] array answers "which markets has user X entered?" and allows iteration. The liquidity calculation (section 10) needs to iterate over all of a user's markets, so the array is essential.

This dual storage means enterMarkets must update both, and exitMarket must update both. The array removal uses the swap-and-pop pattern for gas efficiency: swap the element to remove with the last element, then pop.

Concept: Why Collateral Must Be Explicitly Enabled

This is a user safety design. Imagine you hold a volatile meme token and stable USDC. You supply both to earn yield. Without explicit opt-in, both would automatically count as collateral if you borrow. If the meme token crashes, your entire position could be liquidated, even though you only wanted USDC as collateral.

The enterMarkets/exitMarket pattern gives users control over their risk exposure. Supply for yield without collateral risk, or opt in for borrowing power. exitMarket has safety checks: you cannot exit if you have an active borrow in that market, and you cannot exit if removing that collateral would make you undercollateralized.

Your Task

Build two contracts:

  1. ComptrollerStorage (inherits ExponentialNoError):

    • State variables: admin, oracle (PriceOracle), closeFactorMantissa, liquidationIncentiveMantissa.
    • The Market struct with isListed, collateralFactorMantissa, and accountMembership.
    • mapping(address => Market) public markets and mapping(address => address[]) public accountAssets.
    • Events: MarketListed, MarketEntered, MarketExited, NewOracle, NewCloseFactor, NewLiquidationIncentive, NewCollateralFactor.
  2. ComptrollerMarkets (inherits ComptrollerStorage):

    • Constructor: set admin = msg.sender.
    • _setOracle(PriceOracle newOracle): admin only, verify newOracle.isPriceOracle(), update and emit.
    • _setCloseFactor(uint256): admin only, require between 0.05e18 and 0.9e18, update and emit.
    • _setLiquidationIncentive(uint256): admin only, require between 1e18 and 1.5e18, update and emit.
    • _supportMarket(address cToken): admin only, require not already listed, set isListed = true and collateralFactorMantissa = 0, emit.
    • _setCollateralFactor(address cToken, uint256): admin only, require listed, require <= 0.9e18, require oracle price is not zero, update and emit.
    • enterMarkets(address[] calldata cTokens): loop through, call _addToMarketInternal for each, return results array.
    • _addToMarketInternal(address cToken, address borrower): require listed, if already member return 0, else set membership true and push to accountAssets, emit.
    • exitMarket(address cToken): get account snapshot, require no active borrow, check hypothetical liquidity (call the virtual _getHypotheticalAccountLiquidityInternal), require no shortfall, remove membership and swap-pop from array, emit.
    • Declare _getHypotheticalAccountLiquidityInternal as a virtual function that reverts (placeholder for section 10).

Your Code

Solution.sol
Solidity
Loading editor...

Requirements

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