Omniscia Rari Capital Audit
CToken Manual Review Findings
CToken Manual Review Findings
CTN-01M: Bypassable Limit Evaluation
Type | Severity | Location |
---|---|---|
Logical Fault | Minor | CToken.sol:L817, L965 |
Description:
The borrowWithinLimits
is meant to guarantee a lower-bound for the amount borrowed, however, a user is able to borrow at the designated amount and then immediately repay a part of the debt to essentially acquire a debt lower than the bound without the transactions reverting.
Example:
958(vars.mathErr, vars.accountBorrowsNew) = subUInt(vars.accountBorrows, vars.actualRepayAmount);959require(vars.mathErr == MathError.NO_ERROR, "REPAY_BORROW_NEW_ACCOUNT_BORROW_BALANCE_CALCULATION_FAILED");960961(vars.mathErr, vars.totalBorrowsNew) = subUInt(totalBorrows, vars.actualRepayAmount);962require(vars.mathErr == MathError.NO_ERROR, "REPAY_BORROW_NEW_TOTAL_BALANCE_CALCULATION_FAILED");963964/* We write the previously calculated values into storage */965accountBorrows[borrower].principal = vars.accountBorrowsNew;966accountBorrows[borrower].interestIndex = borrowIndex;967totalBorrows = vars.totalBorrowsNew;
Recommendation:
We advise that proper sanitization is added in the borrow repay function as well that ensures that the repay either totally wipes the debt off or repays up to the lower bound limit.
Alleviation:
The Rari team has stated that they are currently undecided as to whether they intend this to be expected functionality of the system or whether it should be prevented and as a result the codebase has remained unaltered in the latest iteration.
CTN-02M: Inconsistent Access Control
Type | Severity | Location |
---|---|---|
Logical Fault | Minor | CToken.sol:L34, L1164, L1214, L1253, L1341, L1451, L1631 |
Description:
The linked require
check and if
clauses seem to be unadjusted code from the original Compound codebase and do not conform to the new access-control paradigm of utilizing the hasAdminRights()
utility function that contains additional checks.
Example:
26function initialize(ComptrollerInterface comptroller_,27 InterestRateModel interestRateModel_,28 uint initialExchangeRateMantissa_,29 string memory name_,30 string memory symbol_,31 uint8 decimals_,32 uint256 reserveFactorMantissa_,33 uint256 adminFeeMantissa_) public {34 require(msg.sender == admin, "only admin may initialize the market");35 require(accrualBlockNumber == 0 && borrowIndex == 0, "market may only be initialized once");3637 // Set initial exchange rate38 initialExchangeRateMantissa = initialExchangeRateMantissa_;39 require(initialExchangeRateMantissa > 0, "initial exchange rate must be greater than zero.");4041 // Set the comptroller42 uint err = _setComptroller(comptroller_);43 require(err == uint(Error.NO_ERROR), "setting comptroller failed");4445 // Initialize block number and borrow index (block number mocks depend on comptroller being set)46 accrualBlockNumber = getBlockNumber();47 borrowIndex = mantissaOne;4849 // Set the interest rate model (depends on block number / borrow index)50 err = _setInterestRateModelFresh(interestRateModel_);51 require(err == uint(Error.NO_ERROR), "setting interest rate model failed");5253 name = name_;54 symbol = symbol_;55 decimals = decimals_;5657 // Set reserve factor58 err = _setReserveFactorFresh(reserveFactorMantissa_);59 require(err == uint(Error.NO_ERROR), "setting reserve factor failed");6061 // Set admin fee62 err = _setAdminFeeFresh(adminFeeMantissa_);63 require(err == uint(Error.NO_ERROR), "setting admin fee failed");6465 // Set Fuse fee66 err = _setFuseFeeFresh(getPendingFuseFeeFromAdmin());67 require(err == uint(Error.NO_ERROR), "setting Fuse fee failed");6869 // The counter starts true to prevent changing it from zero to non-zero (i.e. smaller cost/refund)70 _notEntered = true;71}
Recommendation:
We advise them to be replaced by a proper invocation of the hasAdminRights()
inherited function.
Alleviation:
The Rari team responded by stating that they slimmed down the codebase prior to the audit and reverted certain changes they had made to the admin-specific access control imposed on certain functions. In the latest commit, they have restored all references to hasAdminRights()
ensuring proper enforcement of the new access control system.
CTN-03M: Incorrect Limit Evaluation
Type | Severity | Location |
---|---|---|
Logical Fault | Minor | CToken.sol:L555, L572 |
Description:
The mintWithinLimits
function is invoked by passing in the input mintAmount
, however, the actual mint amount may differ as denoted by the comments surrounding the vars.actualMintAmount
assignment.
Example:
554// Check max supply555allowed = comptroller.mintWithinLimits(address(this), vars.exchangeRateMantissa, accountTokens[minter], mintAmount);556if (allowed != 0) {557 return (failOpaque(Error.COMPTROLLER_REJECTION, FailureInfo.MINT_COMPTROLLER_REJECTION, allowed), 0);558}559560/////////////////////////561// EFFECTS & INTERACTIONS562// (No safe failures beyond this point)563564/*565 * We call `doTransferIn` for the minter and the mintAmount.566 * Note: The cToken must handle variations between ERC-20 and ETH underlying.567 * `doTransferIn` reverts if anything goes wrong, since we can't be sure if568 * side-effects occurred. The function returns the amount actually transferred,569 * in case of a fee. On success, the cToken holds an additional `actualMintAmount`570 * of cash.571 */572vars.actualMintAmount = doTransferIn(minter, mintAmount);
Recommendation:
We advise the limit to be evaluated with the actual mint amount rather than the estimated mint amount, perhaps via a require
evaluation to ensure tokens are returned to the sender.
Alleviation:
The Rari team has stated that these minor inconsistences due to f.e. token fees should have negligible impact to the overall system and thus can be ignored.
CTN-04M: Inexistent Access Control
Type | Severity | Location |
---|---|---|
Logical Fault | Minor | CToken.sol:L1494, L1601 |
Description:
The _withdrawFuseFees
and _withdrawAdminFees
functions do not contain any access control with regards to the caller of the function permitting them to be arbitrarily called even against the will of the recipients of the fees.
Example:
1551function _withdrawAdminFees(uint withdrawAmount) external nonReentrant returns (uint) {1552 uint error = accrueInterest();1553 if (error != uint(Error.NO_ERROR)) {1554 // accrueInterest emits logs on errors, but on top of that we want to log the fact that an attempted admin fee withdrawal failed.1555 return fail(Error(error), FailureInfo.WITHDRAW_ADMIN_FEES_ACCRUE_INTEREST_FAILED);1556 }1557 // _withdrawAdminFeesFresh emits reserve-reduction-specific logs on errors, so we don't need to.1558 return _withdrawAdminFeesFresh(withdrawAmount);1559}
Recommendation:
We advise that proper access control is introduced here as, for example, the admin may have lost his private key, the fuse address is waiting a proxy upgrade and more that can lead to fees accumulated being permanently lost.
Alleviation:
The Rari team has responded by stating that they wish these functions to be easily invoke-able in contrast to admin related functions to ensure that a powerful key is not pulled from cold storage every time they desire to withdraw fees.
CTN-05M: Literal enum
Comparison
Type | Severity | Location |
---|---|---|
Indeterminate Code | Minor | CToken.sol:L92, L538, L556, L698, L783, L818, L913, L1015, L1108 |
Description:
The linked segments evaluate the result of a comptroller
call that returns the Error enum
casted to a uint
and return an error if the returned casted enum
is not equal to 0
.
Example:
536/* Fail if mint not allowed */537uint allowed = comptroller.mintAllowed(address(this), minter, mintAmount);538if (allowed != 0) {539 return (failOpaque(Error.COMPTROLLER_REJECTION, FailureInfo.MINT_COMPTROLLER_REJECTION, allowed), 0);540}541542/* Verify market's block number equals current block number */543if (accrualBlockNumber != getBlockNumber()) {544 return (fail(Error.MARKET_NOT_FRESH, FailureInfo.MINT_FRESHNESS_CHECK), 0);545}546547MintLocalVars memory vars;548549(vars.mathErr, vars.exchangeRateMantissa) = exchangeRateStoredInternal();550if (vars.mathErr != MathError.NO_ERROR) {551 return (failOpaque(Error.MATH_ERROR, FailureInfo.MINT_EXCHANGE_RATE_READ_FAILED, uint(vars.mathErr)), 0);552}553554// Check max supply555allowed = comptroller.mintWithinLimits(address(this), vars.exchangeRateMantissa, accountTokens[minter], mintAmount);556if (allowed != 0) {557 return (failOpaque(Error.COMPTROLLER_REJECTION, FailureInfo.MINT_COMPTROLLER_REJECTION, allowed), 0);558}
Recommendation:
This comparison can be dangerous as it relies on the order of declaration within the enum
to remain the same which is not a good programming paradigm. As the Error enum
is accessible here, we advise these changes to be altered to instead conduct an actual cast of Error.NO_ERROR
to a uint
on the 0
part of the comparison to ensure the code blocks will function as expected regardless of how the code is altered.
Alleviation:
The Rari team has stated that they expect Error.NO_ERROR
to remain unchanged even in future iterations of the codebase and as such, the codebase remained as is in the latest iteration.