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 legacyCErc20Delegate
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:
- Remove the
addAdmin
,removeAdmin
,addWhitelisted
, andremoveWhitelisted
functions to rely on the inheritedgrantRole
andrevokeRole
functions. This solution would require properly setting up the role admin for bothADMIN_ROLE
andWHITELISTED_ROLE
such that only the role admin of the specific role can manage them. - Utilize the custom functions to manage roles and disable the inherited
grantRole
andrevokeRole
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.
Differences Between CapyFi And Chainlink Oracles
There are some implementation differences between Chainlink's and Capyfi's aggregators:
- In Chainlink price feeds, the
roundId
is composed ofphaseId
andoriginalId
. ThephaseId
is a counter that gets incremented each time a new aggregator is referenced and theoriginalId
is a counter to track each submitted price in the data feed. These two IDs are packed into the sameuint80
shifted byuint80((phaseId << 64) + originalId)
. Both counters start at 1, hence, the first validroundId
should be18446744073709551617
. 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)
andgetTimestamp(uint256 roundId)
, it returns 0 and forgetRoundData(uint80 roundId)
, it returns 0 foranswer
,startedAt
, andupdatedAt
. 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 asupdatedAt
. This is because whenupdateAnswer
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:
- The
activate
function inWhitelist.sol
- The
deactivate
function inWhitelist.sol
- The
addAuthorizedAddress
function inCapyfiAggregatorV3.sol
- The
removeAuthorizedAddress
function inCapyfiAggregatorV3.sol
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:
- In
Whitelist.sol
, theADMIN_ROLE
state variable,WHITELISTED_ROLE
state variable,WhitelistActivated
event,WhitelistDeactivated
event, and theWhitelistUpgraded
event. - All functions and events in
AggregatorV3Interface.sol
- In
CapyfiAggregatorV3.sol
, theauthorizedAddresses
state variable,AuthorizedAddressAdded
event, and theAuthorizedAddressRemoved
event
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.