Introduction
With the rapid development of the DeFi ecosystem, Compound Finance V2, as one of the pioneers in this field, has attracted a large number of users with its innovative lending model. However, any complex distributed application faces potential security threats, especially when it involves the flow of funds worth millions or even hundreds of millions of dollars. Therefore, conducting a comprehensive and detailed security audit of Compound Finance V2 and its fork projects is particularly important. This manual aims to provide developers, security researchers, and DeFi enthusiasts with a detailed security audit guide to help effectively identify and prevent potential risks.
1. Project Background Overview
Compound Finance V2 is an open lending platform built on the Ethereum blockchain that allows users to deposit various ERC-20 underlying tokens and earn interest from them while also allowing borrowing tokens from the market in exchange for paying interest. By introducing the concept of a 'rate market', it achieves decentralized fund pool management and automated interest rate adjustment mechanisms.
2. Project Architecture Analysis
The core architectural components of Compound Finance V2 include:
Comptroller: Controls the overall system logic, such as interest rate calculations, account status maintenance, etc.
cToken: Custom tokens that implement the ERC-20 standard, representing user rights within the system.
InterestRateModel: The model for calculating deposit and borrowing interest rates.
PriceOracle: Provides oracle for asset prices.
Governance: Responsible for community governance-related functions.
2.1 Comptroller
The Comptroller contract is the central nervous system of Compound Finance V2, responsible for coordinating the behavior of each cToken instance. Its main responsibilities include:
Manage the market list to determine which markets are active.
Perform various checks for cross-market operations, such as checking the health status of the user's positions.
Set and update global parameters, such as borrowing limits, collateral factors, liquidation thresholds, etc.
2.2 cToken
Each supported ERC-20 token has a corresponding cToken instance (i.e., CErc20 / CEther contract) to handle all interactions with that token in the project. In addition to implementing basic token transfer functionality, each cToken adds some Compound-specific functions, such as lending, accruing interest, and distributing rewards. Therefore, we can view cToken as a certificate for users depositing assets in Compound and an entry point for users conducting lending operations.
When users deposit the underlying asset tokens into the contract, they can mint the corresponding cToken. The exchange ratio between cToken and the underlying asset is calculated according to the following formula:
Note: borrows represent the borrowing amount, cash represents the balance of the fund pool, and reserves represent the reserves. The borrowing interest rate is determined by utilization, and the deposit interest rate is determined by the borrowing interest rate.
Users generally interact with different cToken contracts to conduct token lending operations in different markets:
2.3 InterestRateModel
The InterestRateModel contract defines the method for calculating interest rates. Different markets may use different types of interest rate models to accommodate their respective risk preferences and liquidity needs.
The interest rate models used in the Compound V2 market mainly include two types: linear and kink-type.
The calculation formula for the linear model's borrowing interest rate is as follows:
The calculation formula for the utilization rate is as follows:
The deposit interest rate changes linearly with the borrowing interest rate:
An increasing utilization rate means that the money in the fund pool is gradually decreasing, and when it reaches a certain peak, it may prevent users from depositing and borrowing normally. To minimize this situation, Compound introduced a second type of interest rate model - kink-type.
The calculation formula for the kink-type borrowing interest rate is as follows:
When the utilization rate reaches a certain peak, it suddenly significantly increases the borrowing and deposit interest rates, incentivizing users to deposit more and borrow less, thus controlling the utilization rate within an appropriate range. This peak is also called the kink (usually when utilization reaches 80%).
2.4 PriceOracle
The PriceOracle contract is responsible for obtaining external market price information and converting it into values used internally by the system, which is crucial for accurately calculating the user's position value.
2.5 Governance Mechanism and Incentive Model
Compound introduced a unique governance mechanism, allowing users holding governance tokens (COMP) to participate in votes on important decisions, such as changing certain parameters or adding new asset types. By issuing governance tokens (COMP), Compound incentivizes users to actively participate in platform activities and rewards contributors. For details, please refer to the official documentation and code repository of Compound. (https://docs.compound.finance/v2/; https://github.com/compound-finance/compound-protocol)
3. Interaction Process
Next, we will illustrate the general process of user interaction on Compound Finance V2 through a simple example:
3.1 Deposit and Redemption Process
If user Alice needs to deposit 1 WBTC into Compound, she will call the mint function of the cWBTC contract to proceed with the deposit. This contract inherits from the cToken contract and will first call the accrueInterest function through the mintInternal function to update the borrowing and deposit rates, and then call mintFresh to carry out the specific minting operation.
The mintFresh function will externally call the mintAllowed function of the Comptroller contract to check whether the current market allows deposits, then transfer 1 WBTC from the user into the contract through the doTransferIn function, and then mint the corresponding amount of cToken for the user based on the latest exchange rate (assuming the latest exchange rate is 0.1, then Alice will receive 10 cWBTC tokens).
If Alice decides to redeem her deposit in the future, she can call the redeem function to exchange cWBTC back to WBTC. The exchange rate may have changed (assumed to be 0.15), which means Alice can redeem 1.5 WBTC, of which 0.5 WBTC is interest income.
3.2 Borrowing and Repayment Process
Alice first needs to call the enterMarkets function of the Comptroller contract to set her cWBTC as collateral, and only then can she proceed to borrow.
Assuming Alice chooses to lend 70 USDC, because the collateral factor of WBTC is 0.75, Alice can borrow up to 75% of the value of WBTC assets, so this will not exceed her maximum borrowing limit.
Note: To avoid the risk of liquidation, Alice should keep a certain buffer space instead of exhausting her borrowing limit.
Alice calls the borrow function of the cUSDC contract, which first internally calls the accrueInterest function through the borrowInternal function to update the borrowing and deposit rates, and then calls borrowFresh to carry out the actual borrowing operation.
After checking the user's position value through the borrowAllowed function of the Comptroller contract, first record the borrowing data, and then transfer the tokens to the user through the doTransferOut function.
If Alice needs to repay, she can either call the repayBorrow function of the cUSDC contract to repay herself or let someone else call the repayBorrowBehalf function to repay on her behalf.
3.3 Liquidation Process
If the price of WBTC drops significantly, causing the value of Alice's collateral to fall below 75% of her borrowing limit, Alice's loan position will be in a liquidation state.
External liquidators (e.g., Bob) can call the liquidateBorrow function in the cUSDC contract to help Alice repay part of her debt. This will first update the interest rates of both cUSDC and the collateral cToken used for repayment through the liquidateBorrowInternal function, and then call liquidateBorrowFresh to perform the specific liquidation operation.
After checking whether liquidation is allowed through the liquidateBorrowAllowed function of the Comptroller contract, the repayBorrowFresh function will first transfer USDC into the contract for repayment and update the borrowing data of the liquidated person. Then, it will call the liquidateCalculateSeizeTokens function of the Comptroller contract to calculate how much collateral Bob can seize from Alice based on the value of the liquidation, and finally transfer cTokens to Bob and Alice using the seize function of the specified collateral market's cToken contract (e.g., cWBTC).
Open this link to view the high-definition version of the above image, and click 'Read the original text' to jump directly: https://www.figma.com/board/POkJlvKlWWc7jSccYMddet/Compound-V2?node-id=0-1&node-type=canvas.
Bob repays part of Alice's loan (e.g., 20 USDC) and thus receives collateral from Alice of equivalent value (such as WBTC), while Bob also receives an additional liquidation incentive (assumed to be 5%). The final result is that Bob receives WBTC worth 21 USDC (20 USDC of loan + 1 USDC of liquidation incentive).
4. Security Vulnerability Checklist
4.1 Rounding Vulnerability Caused by Empty Markets
In the case where the cToken is an empty market (i.e., no users are borrowing/lending in the market), since the value of the exchangeRate in the exchangeRateStoredInternal function depends on the number of underlying asset tokens corresponding to the contract, it can be manipulated by transferring a large number of underlying asset tokens into the cToken contract.

Therefore, a small amount of cToken can be used to borrow a large amount of other tokens, and then call the redeemUnderlying function of the cToken to extract the underlying asset tokens. In the redemption calculation, the number of cTokens to be deducted may result in a much smaller outcome than expected (almost only half) due to the downward rounding of division.
Assuming that the number of cTokens held at this time is 2 (which is also the total totalSupply), and the exchangeRate has been manipulated to 25,015,031,908,500,000,000,000,000, the number of underlying asset tokens to be redeemed should be 50,030,063,815. Therefore, the expected number of cTokens to be deducted should be:
The actual calculated number of cTokens is:
Therefore, in the end, only a small number of cTokens need to be liquidated to obtain a large amount of asset tokens borrowed from other markets.
You can refer to the transaction where the Compound fork project Hundred Finance was hacked due to this vulnerability: https://optimistic.etherscan.io/tx/0x6e9ebcdebbabda04fa9f2e3bc21ea8b2e4fb4bf4f4670cb8483e2f0b2604f451
Audit Highlights: During the audit, attention should be paid to whether the calculation method of the exchange rate is easy to manipulate and whether the rounding method is appropriate. It can also be suggested that the project team immediately mint a small amount of cTokens after the creation of a new market to prevent the market from being empty and thus manipulated.
4.2 Reentrancy Vulnerability Caused by ERC677 / ERC777 Tokens
ERC677 / ERC777 is an extension of the ERC20 contract, compatible with the protocol standard of ERC20 tokens. These tokens allow the execution of a callback function (such as transferAndCall or tokensReceived) if the receiving address is a contract during the transfer process.
In the old version of the Compound Finance V2 code, when a user borrows in the cToken market, the borrowed tokens are transferred out first, and then the borrowing data is recorded.
If the tokens borrowed by the user are ERC677 / ERC777 tokens with callback functionality, a malicious contract can be constructed to re-enter the borrow function through the callback function to borrow again. Since the borrowing data from the previous borrowing has not yet been recorded, the account's health factor check can pass, allowing the user to borrow tokens again.
You can refer to the transaction where the Compound fork project Hundred Finance was hacked due to this vulnerability: https://blockscout.com/xdai/mainnet/tx/0x534b84f657883ddc1b66a314e8b392feb35024afdec61dfe8e7c510cfac1a098
Audit Highlights: The latest version of the Compound V2 code has fixed the borrowing logic, changing it to first record the borrowing data before transferring the borrowed tokens. During the audit, attention needs to be paid to whether the code related to lending functions complies with the CEI (Checks-Effects-Interactions) standard, and also the impact of tokens with callback functionality.
4.3 Risks of Price Manipulation Caused by Inappropriate Oracle Mechanism
Since Compound Finance adopts an over-collateralized lending model, the number of tokens users can borrow depends on whether the value of the collateral is sufficient.
Therefore, if the oracle used by the project to calculate the value of collateral has a feeding mechanism that is easy to manipulate, it can easily lead to borrowing tokens beyond expectations.
For example, in the incident where the Compound fork project Lodestar Finance was hacked, the way the oracle obtained the price of the collateral plvGLP token was first to divide the number of plsGLP tokens in the plvGLP contract (totalAssets) by the total supply of plvGLP to calculate the exchange rate, and then multiply the exchange rate by the price of GLP tokens to calculate the price of plvGLP tokens.
The plvGLP also has a donation feature that allows users to donate sGLP to mint corresponding plsGLP tokens for the plvGLP token contract.
Thus, an attacker can first use a flash loan to create a large number of plvGLP collateral positions in the Lodestar Finance market, then use a flash loan to mint a large amount of sGLP on GMX, and then call the donate function to mint plsGLP tokens for the plvGLP contract to increase the value of totalAssets. With the increase in total assets, the exchange rate of plvGLP will increase, leading to a rapid surge in the price of plvGLP tokens, allowing the borrowing of other tokens beyond expectations in the market.
You can refer to the transaction where Lodestar Finance was hacked: https://arbiscan.io/tx/0xc523c6307b025ebd9aef155ba792d1ba18d5d83f97c7a846f267d3d9a3004e8c
It should also be noted that Compound Finance or its fork projects may use off-chain oracles such as ChainLink or CoinBase to obtain the prices of collateral. If there are drastic market fluctuations, it may lead to discrepancies between off-chain prices and on-chain prices, jeopardizing the project's fund security.
For example, the price of the LUNA token may plummet due to market reasons, while the fork protocols Venus Protocol and Blizz Finance of Compound Finance both use Chainlink oracles as the price source to calculate the value of collateral, where the minimum price (minAnswer) for the LUNA token is hardcoded to 0.10 USD.
When the price of the LUNA token falls below 0.1 USD (e.g., 0.001 USD), anyone can buy a large amount of LUNA at market price and use it as collateral (worth 0.10 USD) to borrow other assets from the platform.
Audit Highlights: During the audit, attention should be paid to whether the feeding mechanism used by the oracle to calculate the value of collateral is easy to manipulate. It may be suggested that the project team use multiple price sources for comprehensive assessment to avoid risks caused by a single price source.
4.4 Risks of Exchange Rate Manipulation Caused by Multi-entry Point Tokens
In the code of Compound, there is a function called sweepToken, which is designed to allow users who accidentally transferred tokens into the contract to retrieve these tokens. The old version of the code is as follows, and this function has an important security check: the token parameter passed in cannot be the underlying asset token of the contract.
However, if a certain cToken market has multiple entry point contracts for its underlying asset tokens (where multiple contract addresses can access the same underlying balance, and external interactions affect the balance of all entry points, which is an early proxy-like model), an attacker can call the sweepToken function with an entry point contract different from the underlying to transfer the underlying asset tokens out of the contract.
Taking TUSD as an example, it has two entry point contracts, and the auxiliary entry point contract 0x8dd5fbce will forward any calls (such as transfer or balanceOf) to the main contract, meaning that interactions with either of the contracts will affect the balance data in both contracts (i.e., the two different contracts share the same balance data).
At this point, assuming the underlying token address set in the market is the main contract address of TUSD, we can call the sweepToken function with the auxiliary entry point contract address 0x8dd5fbce as the token parameter, successfully passing the check address(token) != underlying, and then the contract will transfer all the underlying asset tokens TUSD to the administrator address.
The exchange rate of TUSD/cTUSD will be affected by the quantity of the underlying asset token TUSD in the cTUSD contract. When all TUSD is transferred to the administrator address, the exchange rate of TUSD/cTUSD will plummet. At this point, an attacker can liquidate other users at a very low exchange rate or repay fewer tokens than expected after borrowing to profit.
It is worth mentioning that the latest version of Compound V2's code has added permission verification to the sweepToken function, ensuring that only the administrator role can call this contract, and all markets with multi-entry point tokens have been removed.
Audit Highlights: During the audit, for the function of transferring tokens within the contract, it is essential to consider the impact of the existence of multi-entry point tokens on the project. It is recommended that the project team does not use multi-entry point tokens or verifies whether the number of underlying asset tokens in the contract changes before and after the token transfer, and ensures proper permission checks on relevant functions.
4.5 Compatibility Issues Between Old and New Versions of Contract Code
If in a Compound Finance V2 fork project, the code of a core contract is forked from the new version of Compound Finance V2 code, while another interacting contract uses the old code version, there may be compatibility issues.
For example, in the old version of the cToken's InterestRateModel contract, the function getBorrowRate returns two uint values, while in the new version of the InterestRateModel function, the getBorrowRate function returns only one uint value.
However, in the Compound Finance V2 fork project Percent Finance, the project team uses the old version of the cToken contract code, while the InterestRateModel contract uses the new version, which causes the accrueInterest function in cToken to fail when calling the getBorrowRate function. Since the accrueInterest function is used in both withdrawals and borrowing, this ultimately prevents both withdrawal and borrowing functions from functioning normally, locking the funds in the contract.
Audit Highlights: During the audit, attention needs to be paid to whether the changes in contract interfaces, state variables, function signatures, and events in the updated code will disrupt the normal operation of the existing system, ensuring the consistency of all contract code version updates or ensuring that the updated code can be compatible with the old version of the code.
4.6 Hardcoding Issues Caused by Multi-chain Deployment
In the code of Compound Finance V2, the constant blocksPerYear represents the estimated number of blocks produced per year, which is hardcoded to 2,102,400 in the interest rate model contract, because the average block time of Ethereum is 15 seconds.
However, the block times on different chains may not be the same, and similarly, the rough number of blocks produced throughout the year may not be the same. If a fork project of Compound is deployed on another chain but does not modify the hardcoded values according to the situation of different chains, it may result in the final calculated interest rate exceeding expectations. This is because the value of blocksPerYear affects the values of baseRatePerBlock and multiplierPerBlock, which ultimately affects the borrowing interest rate.
For example, if the block time of the BSC chain is 3 seconds, then the estimated number of blocks produced per year (blocksPerYear) should be 10,512,000. If the value of blocksPerYear is not modified before deployment, it will lead to the final calculated borrowing interest rate being five times higher than expected.
Audit Highlights: During the audit, attention should be paid to whether hardcoded constants or variables in the project contracts may cause unexpected results under the characteristics of different chains. It is recommended that the project team correctly modify these values according to the circumstances of different chains.
Others
In addition to the main concerns mentioned above, the fork projects of Compound V2 often modify some business logic based on the project team's design, such as adding code to interact with external third-party protocols. This needs to be evaluated during the audit based on specific business logic and design requirements to assess whether it will affect the core lending model of Compound Finance V2 and the project itself.
In Conclusion
I hope this security audit manual for Compound Finance V2 and its fork projects can help everyone better understand and assess the security of such complex systems during audits. As technology iterates and updates, this manual will also be updated and improved.
Reference:
[1] https://github.com/YAcademy-Residents/defi-fork-bugs
[2] https://medium.com/chainsecurity/trueusd-compound-vulnerability-bc5b696d29e2
[3] https://github.com/code-423n4/2023-05-venus-findings/issues/559
[4] https://learnblockchain.cn/article/2593
[5] https://github.com/compound-finance/compound-protocol
Author | Jiu Jiu
Editor | Liz