Platform: Code4rena
Start Date: 11/01/2023
Pot Size: $60,500 USDC
Total HM: 6
Participants: 69
Period: 6 days
Judge: Trust
Total Solo HM: 2
Id: 204
League: ETH
Rank: 32/69
Findings: 2
Award: $68.60
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: CodingNameKiki
Also found by: 0x1f8b, 0x52, 0x5rings, 0xAgro, 0xSmartContract, 0xcm, 0xkato, 2997ms, Aymen0909, BClabs, BPZ, BRONZEDISC, Bauer, Bnke0x0, Deekshith99, IllIllI, Josiah, Kaysoft, RaymondFam, Rolezn, SaeedAlipoor01988, Tajobin, Udsen, Viktor_Cortess, adriro, arialblack14, betweenETHlines, btk, chaduke, chrisdior4, cryptphi, csanuragjain, cygaar, defsec, descharre, erictee, gzeon, hansfriese, horsefacts, joestakey, koxuan, lukris02, luxartvinsec, nicobevi, oyc_109, pavankv, peanuts, rbserver, scokaf, shark, tnevler, tsvetanovv, zaskoh
36.2377 USDC - $36.24
All contracts
There is a difference between constant variables and immutable variables, and they should each be used in their appropriate contexts. While it doesn’t save any gas because the compiler knows that developers often make this mistake, it’s still best to use the right tool for the task at hand. Constants should be used for literal values written into the code, and immutable variables should be used for expressions, or values calculated in, or passed into the constructor.
contracts\cash\token\Cash.sol
22: bytes32 public constant TRANSFER_ROLE = keccak256("TRANSFER_ROLE");
contracts\cash\token\CashKYCSender.sol
26: bytes32 public constant KYC_CONFIGURER_ROLE = keccak256("KYC_CONFIGURER_ROLE");
contracts\cash\token\CashKYCSenderReceiver.sol
26: bytes32 public constant KYC_CONFIGURER_ROLE = keccak256("KYC_CONFIGURER_ROLE");
contracts\cash\factory\CashFactory.sol contracts\cash\factory\CashKYCSenderFactory.sol contracts\cash\factory\CashKYCSenderReceiverFactory.sol
44: bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");
45: bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE");
contracts\cash\kyc\KYCRegistry.sol
31: bytes32 public constant _APPROVAL_TYPEHASH = keccak256( "KYCApproval(uint256 kycRequirementGroup,address user,uint256 deadline)" ); 36: bytes32 public constant REGISTRY_ADMIN = keccak256("REGISTRY_ADMIN");
contracts\cash\CashManager.sol
122:bytes32 public constant MANAGER_ADMIN = keccak256("MANAGER_ADMIN"); 123: bytes32 public constant PAUSER_ADMIN = keccak256("PAUSER_ADMIN"); 124: bytes32 public constant SETTER_ADMIN = keccak256("SETTER_ADMIN");
contracts\cash\CashManager.sol
195: function requestMint( uint256 collateralAmountIn ) external override updateEpoch nonReentrant whenNotPaused checkKYC(msg.sender)
241: function claimMint( address user, uint256 epochToClaim ) external override updateEpoch nonReentrant whenNotPaused checkKYC(user)
662: function requestRedemption( uint256 amountCashToRedeem ) external override updateEpoch nonReentrant whenNotPaused checkKYC(msg.sender) {
949: function multiexcall( ExCallData[] calldata exCallData ) external payable override nonReentrant onlyRole(MANAGER_ADMIN) whenPaused returns (bytes[] memory results) {
contracts\lending\tokens\cToken\CTokenModified.sol
1090 : if (msg.sender != pendingAdmin || msg.sender == address(0)) { revert AcceptAdminPendingAdminCheck(); }
Parameter used in constructor is used to initialize the state variable, error in these can lead to redeployment of contract.
contracts\cash\factory\CashFactory.sol contracts\cash\factory\CashKYCSenderFactory.sol contracts\cash\factory\CashKYCSenderReceiverFactory.sol
53: constructor(address _guardian) { guardian = _guardian; }
contracts\lending\JumpRateModelV2.sol 66: owner = owner_;
Contracts use assert() instead of require() in multiple places. This causes a Panic error on failure and prevents the use of error strings.
contracts\cash\factory\CashFactory.sol
97: assert(cashProxyAdmin.owner() == guardian);
contracts\cash\factory\CashKYCSenderFactory.sol
107:assert(cashKYCSenderProxyAdmin.owner() == guardian);
contracts\cash\factory\CashKYCSenderReceiverFactory.sol
106: assert(cashKYCSenderReceiverProxyAdmin.owner() == guardian);
Avoid floating pragmas for non-library contracts. While floating pragmas make sense for libraries to allow them to be included with multiple different versions of applications, it may be a security risk for application implementations. A known vulnerable compiler version may accidentally be selected or security tools might fall-back to an older compiler version ending up checking a different EVM compilation that is ultimately deployed on the blockchain. It is recommended to pin to a concrete compiler version.
contracts\lending\tokens\cCash\CCashDelegate.sol => pragma solidity ^0.8.10; contracts\lending\tokens\cToken\CTokenDelegate.sol => pragma solidity ^0.8.10; contracts\lending\JumpRateModelV2.sol => pragma solidity ^0.5.16; contracts\lending\tokens\cCash\CCash.sol => pragma solidity ^0.8.10; contracts\lending\tokens\cToken\CErc20.sol => pragma solidity ^0.8.10; contracts\lending\tokens\cCash\CTokenInterfacesModifiedCash.sol => pragma solidity ^0.8.10; contracts\lending\tokens\cToken\CTokenInterfacesModified.sol => pragma solidity ^0.8.10; contracts\lending\tokens\cErc20ModifiedDelegator.sol pragma solidity ^0.5.12; contracts\lending\tokens\cCash\CTokenCash.sol pragma solidity ^0.8.10; contracts\lending\tokens\cToken\CTokenModified.sol pragma solidity ^0.8.10;
The current form of relative path import is not recommended for use because it can unpredictably pollute the namespace. Instead, the Solidity docs recommend specifying imported symbols explicitly.
Context: all contracts
There are checks of zero values for variables assetRecipient, feeRecipient and assetSender in the constructor, but they are not checked in functions that set them later.
Accidental input of address(0) can lead to loss of funds sending assets or fees to a 0 address.
contracts\cash\CashManager.sol
452: function setFeeRecipient( address _feeRecipient ) external override onlyRole(MANAGER_ADMIN) { address oldFeeRecipient = feeRecipient; feeRecipient = _feeRecipient; emit FeeRecipientSet(oldFeeRecipient, _feeRecipient); }
465: function setAssetRecipient( address _assetRecipient ) external override onlyRole(MANAGER_ADMIN) { address oldAssetRecipient = assetRecipient; assetRecipient = _assetRecipient; emit AssetRecipientSet(oldAssetRecipient, _assetRecipient); }
803: function setAssetSender( address newAssetSender ) external onlyRole(MANAGER_ADMIN) { address oldAssetSender = assetSender; assetSender = newAssetSender; emit AssetSenderSet(oldAssetSender, newAssetSender); }
There are 3 places in the code with the same require(msg.sender == admin, …)
It could be refactored to an error/modifier/function with message: “Only Admin”.
contracts\lending\tokens\cToken\CTokenModified.sol
44: require(msg.sender == admin, "only admin may initialize the market");
1360: require(msg.sender == admin, "Only admin can set KYC requirement group");
1382: require(msg.sender == admin, "Only admin can set KYC registry");
Also, modifiers make code more elegant but cost more than normal functions.
contracts\cash\CashManager.sol
modifier updateEpoch() { transitionEpoch(); _; }
modifier checkKYC(address account) { _checkKYC(account); _; }
modifier updateEpoch() { transitionEpoch(); _; }
#0 - c4-judge
2023-01-23T14:55:52Z
trust1995 marked the issue as grade-b
🌟 Selected for report: c3phas
Also found by: 0x1f8b, 0xSmartContract, Aymen0909, Bnke0x0, Diana, IllIllI, RaymondFam, Rolezn, Sathish9098, SleepingBugs, Viktor_Cortess, adriro, arialblack14, chaduke, cryptostellar5, cygaar, descharre, dharma09, eyexploit, halden, pavankv, saneryee, tsvetanovv
32.3616 USDC - $32.36
Instead of using the && operator in a single require statement to check multiple conditions, I suggest using multiple require statements with 1 condition per require statement (saving 3 gas per &):
contracts\lending\OndoPriceOracleV2.sol
292: require( (answeredInRound >= roundId) && // (updatedAt >= block.timestamp - maxChainlinkOracleTimeDelay), "Chainlink oracle price is stale" );
contracts\lending\tokens\cCash\CTokenCash.sol
45: require( accrualBlockNumber == 0 && borrowIndex == 0, "market may only be initialized once" );
contracts\lending\tokens\cToken\CTokenModified.sol
45 : require( accrualBlockNumber == 0 && borrowIndex == 0, "market may only be initialized once" );
contracts\lending\OndoPriceOracleV2.sol
94: external view override returns (uint256)
contracts\lending\tokens\cCash\CTokenCash.sol
771: internal returns (uint)
contracts\lending\tokens\cToken\CTokenModified.sol
771: internal returns (uint)
Checks that involve constants should come before checks that involve state variables, function calls, and calculations. By doing these checks first, the function is able to revert before wasting gas in a function that may ultimately revert in the unhappy case.
contracts\cash\token\CashKYCSenderReceiver.sol
56: function _beforeTokenTransfer( address from, address to, uint256 amount ) internal override { super._beforeTokenTransfer(from, to, amount); require( _getKYCStatus(_msgSender()), "CashKYCSenderReceiver: must be KYC'd to initiate transfer" );
if (from != address(0)) { // Only check KYC if not minting require( _getKYCStatus(from), "CashKYCSenderReceiver: `from` address must be KYC'd to send tokens" ); } if (to != address(0)) { // Only check KYC if not burning require( _getKYCStatus(to), "CashKYCSenderReceiver: `to` address must be KYC'd to receive tokens" ); }
}`
contracts\lending\tokens\cToken\CTokenModified.sol
borrowSnapshot.principal
in function borrowBalanceStoredInternal():
323: if (borrowSnapshot.principal == 0) {
330: ' uint principalTimesIndex = borrowSnapshot.principal * borrowIndex;`
contracts\lending\tokens\cToken\CTokenModified.sol
In this function we should check msg.sender first, then
if (reduceAmount > totalReserves) { revert ReduceReservesCashValidation(); }
1256 : function _reduceReservesFresh(uint reduceAmount) internal returns (uint) { uint totalReservesNew; if (msg.sender != admin) { revert ReduceReservesAdminCheck(); }
if (accrualBlockNumber != getBlockNumber()) { revert ReduceReservesFreshCheck(); }
if (getCashPrior() < reduceAmount) { revert ReduceReservesCashNotAvailable(); } if (reduceAmount > totalReserves) { revert ReduceReservesCashValidation(); }`
The same in function _setReserveFactorFresh()
After checking msg.sender we check newReserveFactorMantissa:
` if (newReserveFactorMantissa > reserveFactorMaxMantissa) { revert SetReserveFactorBoundsCheck(); }`
1154: ` function _setReserveFactorFresh( uint newReserveFactorMantissa ) internal returns (uint) { if (msg.sender != admin) { revert SetReserveFactorAdminCheck(); }
if (accrualBlockNumber != getBlockNumber()) { revert SetReserveFactorFreshCheck(); } if (newReserveFactorMantissa > reserveFactorMaxMantissa) { revert SetReserveFactorBoundsCheck(); }`
contracts\lending\tokens\cToken\CTokenModified.sol
When you use pendingAdmin = payable(address(0)); you are converting the address(0) to a payable address, which can be used to receive ether, but this is not necessary in this case because address(0) is already a valid address, so no conversion is needed.
On the other hand, when you use pendingAdmin = address(0); you are simply assigning the address(0) to the variable, and this is less expensive in terms of gas usage because it doesn't require any additional computation.
Therefore, using pendingAdmin = address(0); is more efficient in terms of gas usage, as it doesn't require the extra step of type casting the address to a payable address.
1101: // Clear the pending value pendingAdmin = payable(address(0));
#0 - c4-judge
2023-01-23T14:46:01Z
trust1995 marked the issue as grade-b