Omniscia Rari Capital Audit

Comptroller Manual Review Findings

Comptroller Manual Review Findings

COM-01M: Inconsistent Access Control

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:

contracts/Comptroller.sol
883function _setPriceOracle(PriceOracle newOracle) public returns (uint) {
884 // Check caller is admin
885 if (msg.sender != admin) {
886 return fail(Error.UNAUTHORIZED, FailureInfo.SET_PRICE_ORACLE_OWNER_CHECK);
887 }
888
889 // Track the old oracle for the comptroller
890 PriceOracle oldOracle = oracle;
891
892 // Set comptroller's oracle to newOracle
893 oracle = newOracle;
894
895 // Emit NewPriceOracle(oldOracle, newOracle)
896 emit NewPriceOracle(oldOracle, newOracle);
897
898 return uint(Error.NO_ERROR);
899}

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.

COM-02M: Inconsistent Limit Evaluation

Description:

The linked segment conducts a mulScalarTruncateAddUint execution by passing in the existing accountTokens and then simply adding the mintAmount instead of conducting the actual calculation of CToken whereby the mintAmount is divided instead.

Example:

contracts/Comptroller.sol
414function mintWithinLimits(address cToken, uint exchangeRateMantissa, uint accountTokens, uint mintAmount) external returns (uint) {
415 // Check max supply
416 uint maxSupplyEth = fuseAdmin.maxSupplyEth();
417
418 if (maxSupplyEth < uint(-1)) {
419 (MathError mathErr, uint newUnderlyingBalance) = mulScalarTruncateAddUInt(Exp({mantissa: exchangeRateMantissa}), accountTokens, mintAmount);
420 if (mathErr != MathError.NO_ERROR) return uint(Error.MATH_ERROR);
421 uint newEthBalance;
422 (mathErr, newEthBalance) = mulScalarTruncate(Exp({mantissa: oracle.getUnderlyingPrice(CToken(cToken))}), newUnderlyingBalance);
423 if (mathErr != MathError.NO_ERROR) return uint(Error.MATH_ERROR);
424 if (newEthBalance > maxSupplyEth) return uint(Error.SUPPLY_ABOVE_MAX);
425 }
426}

Recommendation:

We advise that the same calculations are carried out in both code segments as truncation can lead to inconsistencies across the two checks (i.e. the check passing yet the final result could have lead to the check failing).

Alleviation:

After discussing with Rari, we have concluded that the inconsistencies caused by these truncations are negligible and will not create significant security risks.

COM-03M: Inexistent Error Handling

TypeSeverityLocation
Logical FaultMinorComptroller.sol:L401, L422

Description:

The codebase of Compound contains an error-handling path of getUnderlyingPrice calls whereby the Error.PRICE_ERROR is returned if the reported price is zero.

Example:

contracts/Comptroller.sol
400function borrowWithinLimits(address cToken, uint accountBorrowsNew) external returns (uint) {
401 (MathError mathErr, uint borrowBalanceEth) = mulScalarTruncate(Exp({mantissa: oracle.getUnderlyingPrice(CToken(cToken))}), accountBorrowsNew);
402 if (mathErr != MathError.NO_ERROR) return uint(Error.MATH_ERROR);
403 if (borrowBalanceEth < fuseAdmin.minBorrowEth()) return uint(Error.BORROW_BELOW_MIN);
404 return uint(Error.NO_ERROR);
405}

Recommendation:

We advise a similar check to be imposed in these newly introduced segments of code to ensure the code behaves as expected even in the case of misbehaviour by the oracles.

Alleviation:

Error handling code was introduced to ensure that failures of the oracles are accounted for in the newly introduced code segments.

COM-04M: Misconstrued Data Erasure

Description:

The linked segment of the exitMarket function is meant to remove a borrower from the allBorrowers address list, however, in doing so uses a redundant storedList2 variable and does not zero out the borrowersIndexes of the borrower (msg.sender) themselves.

Example:

contracts/Comptroller.sol
230// If the user has exited all markets, remove them from the `allBorrowers` array
231if (storedList.length == 0) {
232 // copy last item in list to location of item to be removed, reduce length by 1
233 address[] storage storedList2 = allBorrowers;
234 storedList2[borrowerIndexes[msg.sender]] = storedList2[storedList2.length - 1];
235 storedList2.length--;
236 borrowerIndexes[storedList2[borrowerIndexes[msg.sender]]] = borrowerIndexes[msg.sender];
237 borrowers[msg.sender] = false;
238}

Recommendation:

We advise that the local storedList2 is rendered redundant and that the borrowerIndexes of msg.sender is also zeroed out during this code block.

Alleviation:

We advise this finding to be evaluated prior to launch.

COM-05M: Unutilized Function Return

TypeSeverityLocation
Logical FaultMinorComptroller.sol:L1046

Description:

The isCToken function returns a bool that remains unused in the linked code segment.

Example:

contracts/Comptroller.sol
1034function _supportMarket(CToken cToken) public returns (uint) {
1035 // Check caller is admin
1036 if (msg.sender != admin) {
1037 return fail(Error.UNAUTHORIZED, FailureInfo.SUPPORT_MARKET_OWNER_CHECK);
1038 }
1039
1040 // Is market already listed?
1041 if (markets[address(cToken)].isListed) {
1042 return fail(Error.MARKET_ALREADY_LISTED, FailureInfo.SUPPORT_MARKET_EXISTS);
1043 }
1044
1045 // Sanity check to make sure its really a CToken
1046 cToken.isCToken();
1047
1048 // Check cToken.comptroller == this
1049 require(address(cToken.comptroller()) == address(this), "Cannot support a market with a different Comptroller.");
1050
1051 // Make sure market is not already listed
1052 address underlying = cToken.isCEther() ? address(0) : CErc20(address(cToken)).underlying();
1053
1054 if (address(cTokensByUnderlying[underlying]) != address(0)) {
1055 return fail(Error.MARKET_ALREADY_LISTED, FailureInfo.SUPPORT_MARKET_EXISTS);
1056 }
1057
1058 // List market and emit event
1059 markets[address(cToken)] = Market({isListed: true, collateralFactorMantissa: 0});
1060 allMarkets.push(cToken);
1061 cTokensByUnderlying[underlying] = cToken;
1062 emit MarketListed(cToken);
1063
1064 return uint(Error.NO_ERROR);
1065}

Recommendation:

We advise that it is correctly utilized so by adding a require check.

Alleviation:

The Rari team stated that a require check would incur a redundant gas overhead as the isCToken function is not meant to be utilized for what it returns, it is meant to be utilized as a utility function the contract should implement.