CapyFi Audit

Table of Contents

Summary

Type
DeFi
Timeline
From 2025-06-25
To 2025-07-07
Languages
Solidity
Total Issues
11 (9 resolved)
Critical Severity Issues
0 (0 resolved)
High Severity Issues
0 (0 resolved)
Medium Severity Issues
0 (0 resolved)
Low Severity Issues
4 (4 resolved)
Notes & Additional Information
7 (5 resolved)

Scope

OpenZeppelin audited the LaChain/capyfi-sc repository at commit cf47234.

In scope were the following files:

 src
├── contracts
│   ├── BaseJumpRateModelV2.sol
│   ├── CDaiDelegate.sol
│   ├── CErc20.sol
│   ├── CErc20Delegator.sol
│   ├── CErc20Immutable.sol
│   ├── CEther.sol
│   ├── CToken.sol
│   ├── CTokenInterfaces.sol
│   ├── Comptroller.sol
│   ├── ComptrollerG7.sol
│   ├── Governance
│   │   ├── Comp.sol
│   │   ├── GovernorAlpha.sol
│   │   ├── GovernorBravoDelegate.sol
│   │   ├── GovernorBravoDelegateG1.sol
│   │   ├── GovernorBravoDelegator.sol
│   │   └── GovernorBravoInterfaces.sol
│   ├── Lens
│   │   └── CompoundLens.sol
│   └── PriceOracle
│       └── CapyfiAggregatorV3.sol
└── script
    ├── DeployCapyfiProtocolAll.s.sol
    └── capyfi
        ├── DeployInterestRateModels.s.sol
        ├── DeployWhitelist.s.sol
        └── UpgradeWhitelist.s.sol

System Overview

Capyfi is a lending protocol that has been designed to operate on both Ethereum and LaChain with a whitelist-based access control system that enables the enforcement of KYC/AML policies. The protocol employs a market-based lending architecture whereby users can supply assets to earn interest and borrow against their collateral. The system utilizes interest-bearing tokens (cTokens) to represent user positions and uses a custom oracle to determine prices.

The Whitelist contract, implemented as an upgradeable UUPS proxy, introduces role-based access control to markets. Two roles are defined: ADMIN_ROLE and WHITELISTED_ROLE. The admin holds elevated privileges including granting and revoking both roles and upgrading the proxy implementation. Whitelisted users can mint cTokens in designated markets where the whitelist contract is used, while non-whitelisted users can still transfer, redeem, and liquidate assets. The CapyfiAggregatorV3 contract allows CapyFi to support tokens that do not have active Chainlink price feeds. While inspired by Chainlink's model, the CapyFi price feed is updated by a permissioned owner instead of aggregating from different node operators.

The security review covered deployment scripts in addition to the smart contracts to ensure that contracts are deployed in the correct sequence and configuration.

Security Model and Privileged Roles

This section goes over the security model of the reviewed system and any privileged roles in it, along with the relevant trust assumptions.

Deployment

  • The system is designed to be deployed on both Ethereum mainnet and LaChain. The configuration for Ethereum correctly assumes a 12-second block time, but LaChain uses a 5-second block time. As such, any assumptions by the interest rate model or timing-dependent logic must account for this difference.

  • Deployment scripts lack protections against launching empty markets. Although the whitelist could provide a layer of access control for minting, the initializer does not set up the whitelist contract by default. This introduces a window where malicious actors could act as first minters unless additional protections are enforced externally. The audited scope does not cover the deployment of new markets to pre-existing deployments. However, it is recommended to some cTokens and burning them within the deployment transaction to ensure that the total supply never goes to zero. It is crucial that the market deployment and burning happen in the same transaction.

  • The storage layout is incompatible with legacy contracts. The introduction of the Whitelist functionality modifies the storage layout compared to the legacy CErc20Delegate contract. As a result, attempting to upgrade existing proxies to this new implementation would lead to storage collisions, potentially breaking core functionality or exposing vulnerabilities. However, as per the CapyFi team’s deployment plan, this implementation is only intended for new market deployments and not as an upgrade path for existing markets.

Price Feed

  • The CapyfiAggregatorV3 contract relies on a centralized mechanism to update prices. As such, users and integrators must trust the designated price submitter to behave honestly and maintain accurate pricing.

  • The CapyFiAggregatorV3 contract adheres to the Chainlink interface but does not necessarily behave like a Chainlink oracle. Integrators should verify the specific behavior of the CapyFi oracle when using the oracle.

Privileged Roles

The access control mechanism implemented in the Whitelist contract grants significant authority to accounts holding the ADMIN_ROLE. A single malicious or compromised admin can revoke roles from all other users and unilaterally assume control of the system, including upgrading the contract. As such, proper operational security is assumed for ADMIN_ROLE holders. In addition, only whitelisted users can mint cTokens. Non-whitelisted users can still redeem, transfer, and liquidate cTokens.

 

Low Severity

Unlimited DEFAULT_ADMIN_ROLE Power Over ADMIN_ROLE and WHITELISTED_ROLE

The intended hierarchy in the Whitelist contract is that only users with ADMIN_ROLE can grant and revoke both ADMIN_ROLE and WHITELISTED_ROLE by using the addAdmin, removeAdmin, addWhitelisted, and removeWhitelisted functions. However, the AccessControlUpgradeable contract also exposes the grantRole and revokeRole public functions that allow the DEFAULT_ADMIN_ROLE to manage both roles freely. This is because the admin role for both is set by default to DEFAULT_ADMIN_ROLE. Hence, they can manage these 2 roles without any restriction even without having ADMIN_ROLE assigned.

Consider implementing one of the following solutions:

  1. Remove the addAdmin, removeAdmin, addWhitelisted, and removeWhitelisted functions to rely on the inherited grantRole and revokeRole functions. This solution would require properly setting up the role admin for both ADMIN_ROLE and WHITELISTED_ROLE such that only the role admin of the specific role can manage them.
  2. Utilize the custom functions to manage roles and disable the inherited grantRole and revokeRole functions by overriding them and making them inaccessible.

Update: Resolved in pull request #5 at commit d4abd3.

Unsafe Casting in getAnswer Function

The getAnswer function of the CapyFiAggreatorV3 contract accepts any uint256 value for the roundId. When the roundId is greater than uint80, the function will always revert but the returned error will incorrectly cast the value returned to uint80.

Consider either not casting roundId to uint80 in the RoundNotFound error or returning different data when the input parameter is greater than uint80.

Update: Resolved in pull request #6 at commit 5ac968.

There are some implementation differences between Chainlink's and Capyfi's aggregators:

  • In Chainlink price feeds, the roundId is composed of phaseId and originalId. The phaseId is a counter that gets incremented each time a new aggregator is referenced and the originalId is a counter to track each submitted price in the data feed. These two IDs are packed into the same uint80 shifted by uint80((phaseId << 64) + originalId). Both counters start at 1, hence, the first valid roundId should be 18446744073709551617. However, in Capyfi's implementation, it starts at 1 and gets incremented each time a new price is submitted. If an external integrator wants to fetch historical data from the very beginning and they try to fetch the first valid round from a Chainlink aggregator into Capyfi's implementation, it will revert.
  • When someone wants to fetch data from a round that has not yet been filled, Chainlink's implementation returns empty data. For getAnswer(uint256 roundId) and getTimestamp(uint256 roundId), it returns 0 and for getRoundData(uint80 roundId), it returns 0 for answer, startedAt, and updatedAt. However, in Capyfi's implementation, it reverts the execution. This can also break external integrations.
  • There are no minimum and maximum price bound checks in the CapyFi oracle.
  • For the CapyFi oracle, startedAt is always the same value as updatedAt. This is because when updateAnswer is called, that price is immediately the price of the oracle, whereas Chainlink aggregates multiple oracle sources which requires a time delay.

Consider documenting the above-listed differences in the codebase so that integrators can be aware of them.

Update: Resolved in pull request #7 at commit fb173f.

Missing State Change Validation

Throughout the codebase, multiple instances of functions that do not verify whether the new value actually differs from the existing one before updating were identified:

Consider adding validation checks that revert the transaction if the input value matches the existing value.

Update: Resolved in pull request #8 at commit 3c63ae.

Notes & Additional Information

Lack of Security Contact

Providing a specific security contact (such as an email or ENS name) within a smart contract significantly simplifies the process for individuals to communicate if they identify a vulnerability in the code. This practice is quite beneficial as it permits the code owners to dictate the communication channel for vulnerability disclosure, eliminating the risk of miscommunication or failure to report due to a lack of knowledge on how to do so. In addition, if the contract incorporates third-party libraries and a bug surfaces in those, it becomes easier for their maintainers to contact the appropriate person about the problem and provide mitigation instructions.

The contracts in the audit scope do not have a security contact.

Consider adding a NatSpec comment containing a security contact above each contract definition. Using the @custom:security-contact convention is recommended as it has been adopted by the OpenZeppelin Wizard and the ethereum-lists.

Update: Acknowledged not resolved.

Missing Named Parameters in Mappings

Since Solidity 0.8.18, mappings can include named parameters to provide more clarity about their purpose. Named parameters allow mappings to be declared in the form mapping(KeyType KeyName? => ValueType ValueName?). This feature enhances code readability and maintainability.

Within CapyfiAggregatorV3.sol, multiple instances of mappings without named parameters were identified:

Consider adding named parameters to mappings in order to improve the readability and maintainability of the codebase.

Update: Resolved in pull request #9 at commit d2c855.

Lack of Indexed Event Parameters

Within Whitelist.sol, multiple instances of events missing indexed parameters were identified:

To improve the ability of off-chain services to search and filter for specific events, consider indexing event parameters.

Update: Resolved in pull request #10 at commit fb7ad5.

Lack of Oracle Staleness Check

The protocol relies on Chainlink price feeds for asset valuation. When using Chainlink's latestRoundData, it is crucial to thoroughly validate all the returned data to prevent the use of stale or incorrect prices.

The priceFeed.latestRoundData call within ChainlinkPriceOracle.sol does not check whether the price is stale.

Consider fully validating the result of the latestRoundData() output to ensure that the data feed has returned a recent and correct price. Failure to do so can introduce material risks such as undercollateralized loans due to tokens being borrowed against assets with outdated prices.

Update: Acknowledged, not resolved.

Missing Docstrings

Throughout the codebase, multiple instances of missing docstrings were identified:

Consider thoroughly documenting all functions (and their parameters) that are part of any contract's public API. Functions implementing sensitive functionality, even if not public, should be clearly documented as well. When writing docstrings, consider following the Ethereum Natural Specification Format (NatSpec).

Update: Resolved in pull request #11 at commit 4ca9de.

Variables Could Be immutable

If a variable is only ever assigned a value from within the constructor of a contract, it could be declared immutable.

Within CapyfiAggregatorV3.sol, multiple instances of variables that could be made immutable were identified:

To better convey the intended use of variables and to potentially save gas, consider adding the immutable keyword to variables that are only set in the constructor.

Update: Resolved in pull request #12 at commit 0dfc1e.

Silent Failure During Protocol Configuration

When interacting with the underlying Compound V2 contracts, most functions return values to indicate errors instead of reverting. This behavior must be carefully considered during protocol configuration, especially during deployment. If a specific operation such as _supportMarket or _setCollateralFactor fails and returns an error code, the deployment script will still succeed, but the intended configuration will not be applied.

To avoid silent failures, consider storing the returned value from these function calls and explicitly checking that it equals zero (NO_ERROR). This ensures that the deployment script fails immediately if any of these internal calls encounters an error.

Update: Resolved in pull request #14 at commit 0acfdf and commit 244cd4.

Conclusion

The audited scope encompasses the deployment of the CapyFi lending protocol, with particular emphasis on the addition of the whitelist mechanism, the implementation of the CapyFi oracle, and the safety of deployment scripts. This codebase implements changes to a protocol that has undergone multiple audits, and minimizing the modifications made to the original codebase allows it to benefit from the security of the original architecture. The deployment script covers the launch of the lending protocol, oracle, and whitelist. While the deployment of new markets to an already deployed contract was not part of the scope of this audit, the importance of adding initial assets to empty markets upon launch to prevent inflation attacks must be emphasized.

No critical-, high-, or medium-severity issues were identified, which is a testament to the robustness of the codebase. Nonetheless, some low-severity issues were reported, and various code improvements were suggested. The CapyFi team is appreciated for their exceptional collaboration throughout this engagement. The team clearly explained the contracts and provided relevant documentation outlining the protocol's functionality and their specific areas of concern.

Request Audit