Section 10 of 18
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:
cTokenBalancefromgetAccountSnapshot(). This is the raw number of cTokens the user holds.exchangeRatefrom the same snapshot. Converts cTokens to underlying:underlying = cTokens * exchangeRate.oraclePricefrom the Comptroller's oracle. Converts underlying to a common denominator (USD or ETH).collateralFactorfrom 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:
-
getAccountLiquidity(address account): public view, returns(uint256, uint256, uint256). Calls_getHypotheticalAccountLiquidityInternal(account, address(0), 0, 0). -
_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 = 0andsumBorrowPlusEffects = 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 * oraclePriceusing Exp multiplication. - Add
mul_ScalarTruncate(tokensToDenom, cTokenBalance)tosumCollateral. - Add
mul_(borrowBalance, oraclePrice)tosumBorrowPlusEffects. - If
asset == cTokenModify: addmul_ScalarTruncate(tokensToDenom, redeemTokens)andmul_(borrowAmount, oraclePrice)tosumBorrowPlusEffects.
- Call
- If
sumCollateral > sumBorrowPlusEffects: return (0, difference, 0). - Else: return (0, 0, difference).
- Initialize