OpenZeppelin, in its role as Security Partner to the Compound DAO, audited an ERC-4626-compliant wrapper contract for the Comet token commissioned by the community through the Compound Grants Program.
Summary
Timeline: From 2023-10-30 – To 2023-11-10
Total Issues: 16 (0 resolved)
Critical Severity Issues: 1 (0 resolved)
Medium Severity Issues: 2 (0 resolved)
Low Severity Issues: 6 (0 resolved)
Notes & Additional Information: 7 (0 resolved)
Scope
We audited the compound-finance/comet-wrapper repository at the 1c08f3f
commit.
In scope were the following contracts:
src
├── CometHelpers.sol
├── CometWrapper.sol
└── vendor
├── CometExtInterface.sol
├── CometInterface.sol
├── CometMainIntefrace.sol
├── CometMath.sol
├── ICometConfigurator.sol
├── ICometRewards.sol
└── IERC7246.sol
System Overview
A Comet market is a rebasing, ERC-20-compliant contract with user balances adjusted by the corresponding indexes tracking the overall state of supplying and borrowing. The rebasing nature of the Comet token may cause issues when integrating into the wider Defi ecosystem. The community commissioned, through their grants program, an ERC-4626-compliant wrapper contract which allows users to have a non-rebased balance of shares that correspond to the principal balance of the corresponding Comet market.
The CometWrapper conforms to the proposed EIP-7246 Encumber Interface. Backwards-compatible with ERC-20, EIP-7246 allows a user to encumber
tokens to other addresses which cannot be revoked. This is motivated by the often necessary need to know that an amount of tokens is available to take with the ability to claim them at a future point when specific conditions arise. The CometWrapper thus conforms to ERC-20, ERC-4626, and EIP-7246.
This may lead to unanticipated behavior for users. An example is the convention of using balanceOf
or allowance
in ERC-20 contracts. Unscrupulous users who call these functions may be misled into using those values when the new ‘availableBalanceOf’ and encumbrances
should be used instead. Users and developers should review the external/public functions carefully to ensure they are using the contract correctly. Of course, should EIP-7246 become more broadly used, its conventions and norms will be better understood and practiced.
Like a Comet market, the proposed CometWrapper allows users to accrue rewards.
Security Model and Trust Assumptions
Unlike Comet, there are no privileged functions. The usual governor or pause guardian has no special functions they can invoke. Since it is a wrapper of a Comet market contract, users are taking on the assumption that the Compound DAO will manage the market and the wrapper. Wrappers, like markets, are meant to be upgradeable, so future functionality will be determined by the usual DAO proposal process.
Critical Severity
Encumbered Amount Can Be Withdrawn by Self
The CometWrapper implements the IERC7246
Encumber Interface, thus the shares in the CometWrapper can be encumbered to other accounts. According to the specification, the encumbered amount cannot be accessed by the token holder until it is released to guarantee token availability to the encumbered.
However, an account can withdraw its entire balance without any consideration of encumbered amount via the withdraw and redeem functions if the caller is also the owner.
This has some consequences to other functions, particularly, any call to availableBalanceOf can underflow if the outstanding encumbered amount is more than the balance available. Any calls to availableBalanceOf
will revert, for instance,
encumberInternal, transfer, and spendEncumbranceThenAllowanceInternal. Thus, the core mechanism for encumbering will not work for accounts with higher encumbered amount than the actual balance.
This also means that by EIP-4626, the maxWithdraw
and maxRedeem
functions should return the availableBalanceOf
instead of the actual balanceOf
.
Consider only allowing the withdrawal of availableBalanceOf
by the owner and also taking into account any outstanding encumbered amount in the max limit for maxWithdraw
and maxRedeem
.
Medium Severity
Underflows in previewWithdraw
and previewRedeem
In previewWithdraw
, if the argument assets
is greater than totalAssets()
, this line will revert due to underflow. This can happen to any positive assets
value when totalAssets()
is zero.
According to the ERC-4626 specification, previewWithdraw should return a value in such a case without reverting due to insufficient shares/assets, as quoted from the specification, previewWithdraw
MUST NOT account for withdrawal limits like those returned from maxWithdraw and should always act as though the withdrawal would be accepted, regardless if the user has enough shares, etc.
An underflow can also happen in previewRedeem
, where the totalSupply()
is used.
Consider removing the lines with potential underflows and allowing successful previews without reverting.
Inconsistent Interpretation of type(uint256).max
When depositing with assets = type(uint256).max
in CometWrapper
, this will be interpreted in Comet
as a request to transfer the entire balance (i.e., Comet.balanceOf(msg.sender)
) instead of the literal max uint256
value. Thus, the transfer will be successful with the user’s entire balance.
However this interpretation is not considered in the wrapper, instead, the parameter asset
is used for computing shares. This will make the calculation within the call to previewDeposit
always revert due to an overflow.
Consider either using a transferFrom
mechanism consistent with Comet
or documenting this discrepancy explicitly for all relevant functions. Also consider adding explicit errors for potential under/overflows for better user/developer experience.
Low Severity
Max Deposit Limit Does Not Conform to EIP-4626 Specification
The functions maxDeposit
and maxMint
return type(uint256).max
; however, a user cannot deposit a max value without reverting.
There is an implicit upper limit on principal of type(uint104).max
from Comet
enforced in the safe casting from the
previewDeposit
call. Asset values above it will revert. Thus the returned amount from both maxDeposit
and maxMint
will always cause reverts due to overflow in casting.
The EIP-4626 specification requires that maxDeposit
, like maxMint
,
MUST return the maximum amount of assets
deposit
would allow to be deposited forreceiver
and not cause a revert, which MUST NOT be higher than the actual maximum that would be accepted (it should underestimate if necessary).
Consider modifying the limits to reflect the maximum values correctly.
Comet Transfer Flag Is Not Considered In Preview Functions
Both the Governor and PauseGuardian roles can pause transfer actions within a Comet market. The corresponding CometWrapper
must respect whether transfer actions are paused. According to the EIP-4626 specification, the previewDeposit
, previewMint
, previewWithdraw
and previewRedeem
functions should reflect the on-chain conditions at the current block, for instance, previewDeposit
:
Allows an on-chain or off-chain user to simulate the effects of their deposit at the current block, given current on-chain conditions.
Consider including a check on the transfer pause flag in the preview functions to reflect the on-chain condition of the underlying Comet market.
Inconsistent Imports From Inconsistent Interfaces
The interfaces CometMainInterface
and CometExtInterface
defined in this codebase are different from those in Comet. For instance CometExtInterface
contains the TotalsBasic
struct while the cometWrapper
CometExtInterface
does not.
This is made more confusing with non-explicit imports. Within CometInterface.sol
, global imports are being used. For instance:
- The import “./CometMainInterface.sol”; import includes the
AssetInfo
struct together. - The import “./CometExtInterface.sol”; import includes the
TotalsBasic
struct together.
But in the main CometWrapper.sol
, explicit imports are used.
This may create confusion for developers and readers who are familiar with the comet interfaces. Consider using consistent named import syntax (import {A, B, C} from "X"
) to explicitly declare which contracts are being imported.
Unnecessary Boolean Comparison
Within the isValidSignature
method is a check to see if a boolean variable is equal to false
. Such comparisons can obfuscate the code’s intention, and use of boolean literals is a common code smell. Consider removing the boolean literal so the check is just on the variable itself (e.g., if(!success) ...
).
Solidity Version May Be Incompatible With L2 Chains
The pragma for these contracts is set to Solidity 0.8.21. Version 0.8.20 introduced the new PUSH0
opcode that is not supported by all L2 chains. It is supported by Polygon but not by Arbitrum or Base. When compiling the contracts, be sure to select the correct EVM version for the target network (shanghai being the EVM that supports PUSH0
) or consider downgrading all contracts to version 0.8.19.
Potential Locked Tokens
Comet has an
approveThis
function which allows it to approve users to transfer tokens away from the contract. Thus any manually transferred tokens to the comet market can be accessed. However CometWrapper
has no such functionality and as such, errantly transferred tokens can be locked in the contract.
Consider adding such a function if the protocol would like to have control over locked assets.
Notes & Additional Information
Typographical Error
accruedInterestedIndices
on line 373 of CometWrapper
should read accruedInterestIndices
.
Multiple Instances of Missing Named Parameters in Mappings
Since Solidity 0.8.18, developers can utilize named parameters in mappings. This means mappings can take the form of mapping(KeyType KeyName? => ValueType ValueName?)
. This updated syntax provides a more transparent representation of the mapping’s purpose.
Within CometWrapper.sol
, there are multiple mappings without named parameters:
- The
rewardsClaimed
state variable - The
encumberedBalanceOf
state variable - The
encumbrances
state variable - The
nonces
state variable
Consider adding named parameters to the mappings to improve the readability and maintainability of the code.
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 proves 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. Additionally, if the contract incorporates third-party libraries and a bug surfaces in these, it becomes easier for the maintainers of those libraries to make contact with the appropriate person about the problem and provide mitigation instructions.
Consider adding a NatSpec comment to the CometWrapper contract containing a security contact on top of the contract’s definition. Using the @custom:security-contact
convention is recommended as it has been adopted by the OpenZeppelin Wizard and the ethereum-lists.
Missing Docstrings
Throughout the codebase, there are several parts that do not have docstrings. See CometExtInterface.sol
, CometInterface.sol
, CometMainInterface.sol
, ICometConfigurator.sol
, and ICometRewards.sol
in the src/vendor
directory. Considering these are different from the usual Comet interfaces, it is helpful to spell out the differences for developers and readers.
Potential Unsafe ABI Encoding
It is common practice to use abi.encodeWithSignature
or abi.encodeWithSelector
to generate calldata for a low-level call. However, the first option is not typo-safe and the second option is not type-safe. The result is that both of these methods are error-prone and should be considered unsafe.
On line 717 of CometWrapper.sol
an unsafe ABI encoding is used.
Consider replacing the occurrences of unsafe ABI encodings with abi.encodeCall
, which checks whether the supplied values actually match the types expected by the called function and also avoids errors caused by typos.
Floating Pragma
Pragma directives should be fixed to clearly identify the Solidity version with which the contracts will be compiled.
The file IERC7246.sol
has the solidity ^0.8.21
floating pragma directive.
Consider adopting a fixed pragma version.
Inconsistent Use Of Named Returns
To improve the readability of the contract, use the same return style in all of its functions.
CometWrapper
’s getSupplyIndices
function is the only function to use named return variables.
Consider using a consistent style for function returns.
Conclusion
This audit examined a new feature of Compound Comet markets offering users a non-rebased balance of shares for convenience and compatibility with the wider Defi ecosystem. At the start of this audit, we were very pleased, again, to see a robust testing suite alongside the scope we were given. We expect to see iterations of the contract as we have received it and look forward to the utility it will provide to the community and DeFi generally.