Section 10 of 18

Build
+20 Lynx

Comptroller: Liquidity Calculation

What You Are Building

You are building the core risk calculation of Compound V2. This is the function that answers the most important question in the entire protocol: "Is this user solvent?" Every safety check in the system flows through this single calculation. It sums up a user's collateral and borrows across every market they have entered and returns either excess collateral (liquidity) or a deficit (shortfall).

This is the function that gets exploited most often in lending protocol audits. Rounding errors, stale prices, missing markets in the iteration, or incorrect collateral factor application here can make the entire protocol insolvent. When auditing Compound forks, this is where the majority of critical findings live.

Concept: The Cross-Market Solvency Check

Uniswap V2 has no concept of solvency. Each pool is self-contained. Compound's entire value proposition rests on cross-market accounting. A user deposits ETH in one market and borrows USDC from another. The protocol must compute the total value on each side:

Total Collateral = sum over all entered markets of:
    cTokenBalance * exchangeRate * oraclePrice * collateralFactor

Total Borrows = sum over all entered markets of:
    borrowBalance * oraclePrice

If Total Collateral exceeds Total Borrows, the user has "liquidity" (they can borrow more). If Total Borrows exceeds Total Collateral, the user has "shortfall" (they are undercollateralized and can be liquidated). Exactly one of liquidity or shortfall is non-zero. They are never both positive.

Concept: How Collateral Value Is Computed

The collateral calculation chains four values together for each market:

  1. cTokenBalance from getAccountSnapshot(). This is the raw number of cTokens the user holds.
  2. exchangeRate from the same snapshot. Converts cTokens to underlying: underlying = cTokens * exchangeRate.
  3. oraclePrice from the Comptroller's oracle. Converts underlying to a common denominator (USD or ETH).
  4. collateralFactor from the Market struct. Applies the risk haircut. A factor of 0.75 means only 75% of the value counts.

These are combined into a single conversion factor tokensToDenom:

Exp memory tokensToDenom = mul_(mul_(collateralFactor, exchangeRate), oraclePrice);
sumCollateral += mul_ScalarTruncate(tokensToDenom, cTokenBalance);

This three-way multiplication converts a cToken balance directly to its risk-adjusted dollar value in one step.

Concept: The Hypothetical Modification

The _getHypotheticalAccountLiquidityInternal function accepts two optional parameters: redeemTokens and borrowAmount. These let the Comptroller answer "what if" questions without actually executing the operation.

When a user tries to borrow 1,000 USDC, the CToken calls borrowAllowed(), which calls this liquidity function with borrowAmount = 1000e6. The function adds that hypothetical borrow to sumBorrowPlusEffects and checks if the user would still be solvent. If not, the borrow is rejected before any state changes.

For redemptions, the hypothetical redeem reduces collateral by adding to sumBorrowPlusEffects (increasing the "borrow side" by the value of redeemed tokens is mathematically equivalent to decreasing the collateral side). This avoids a subtraction that could underflow:

if (asset == cTokenModify) {
    // Redeem effect: add redeemed value to borrow side
    sumBorrowPlusEffects += mul_ScalarTruncate(tokensToDenom, redeemTokens);
    // Borrow effect: add borrowed value
    sumBorrowPlusEffects += mul_(borrowAmount, oraclePrice);
}

The public getAccountLiquidity() function is just a wrapper that calls the hypothetical version with no modifications (address(0), 0, 0).

Concept: Why This Function Iterates All Markets

The liquidity function loops over accountAssets[account], which is every market the user has entered. This is an O(N) operation where N is the number of entered markets. Compound limits the maximum number of markets a user can enter to bound gas costs. In practice, most users enter 2 to 5 markets.

This iteration is the reason the accountAssets array exists. The accountMembership mapping alone cannot be iterated. You need both data structures: the mapping for O(1) membership checks, the array for O(N) iteration.

Your Task

Build ComptrollerLiquidity inheriting from ComptrollerMarkets:

  1. getAccountLiquidity(address account): public view, returns (uint256, uint256, uint256). Calls _getHypotheticalAccountLiquidityInternal(account, address(0), 0, 0).

  2. _getHypotheticalAccountLiquidityInternal(address account, address cTokenModify, uint256 redeemTokens, uint256 borrowAmount): override the virtual function from section 9. Internal view, returns (uint256 error, uint256 liquidity, uint256 shortfall).

    • Initialize sumCollateral = 0 and sumBorrowPlusEffects = 0.
    • Get accountAssets[account] into a memory array.
    • For each asset in the array:
      • Call CTokenInterface(asset).getAccountSnapshot(account) to get (err, cTokenBalance, borrowBalance, exchangeRateMantissa). Require err == 0.
      • Read markets[asset].collateralFactorMantissa.
      • Read oracle.getUnderlyingPrice(asset). Require it is > 0.
      • Compute tokensToDenom = collateralFactor * exchangeRate * oraclePrice using Exp multiplication.
      • Add mul_ScalarTruncate(tokensToDenom, cTokenBalance) to sumCollateral.
      • Add mul_(borrowBalance, oraclePrice) to sumBorrowPlusEffects.
      • If asset == cTokenModify: add mul_ScalarTruncate(tokensToDenom, redeemTokens) and mul_(borrowAmount, oraclePrice) to sumBorrowPlusEffects.
    • If sumCollateral > sumBorrowPlusEffects: return (0, difference, 0).
    • Else: return (0, 0, difference).

Your Code

Solution.sol
Solidity
Loading editor...

Requirements

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