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: 34/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
ID | Finding | Instances |
---|---|---|
1 | Other contract addresses can only be set once | 1 |
2 | Everyone can call initialize() function | 4 |
3 | type(uint).max is not equal to -1 | 1 |
ID | Finding | Instances |
---|---|---|
1 | Don't use keccack256 in constants | 8 |
2 | Require() or revert() statement that check input arguments should be at the top of the function | 5 |
3 | Similar contracts | 4 |
4 | Use modifier instead of duplicate require | 5 |
5 | Multiple address mappings can be combined into a single mapping of an address to a struct | 1 |
6 | Miscellaneous | 1 |
Other contract addresses are immutable so they can only be set once in the constructor. There is no other setter method for this.
This makes it so there is no room for error and can lead to contract reverts and redeployment.
A solution for this is to make a setter method which only the owner can call. So if the contract address is wrong it can always be reset.
L163: collateral = IERC20(_collateral); L164: cash = Cash(_cash);
When the contract is not initialized, the initialize() function can be called by anybody. This can be an issue because roles will be set in the __ERC20PresetMinterPauser_init function.
Documentation says fail if the repayAmount is -1. However type(uint).max is not equal to -1. Better use just -1 here.
/* Fail if repayAmount = -1 */ if (repayAmount == type(uint).max) { revert LiquidateCloseAmountIsUintMax(); }
Use the literal bytes32 value instead of the keccak256 or other computations
L122: bytes32 public constant MANAGER_ADMIN = keccak256("MANAGER_ADMIN"); L123: bytes32 public constant PAUSER_ADMIN = keccak256("PAUSER_ADMIN"); L124: bytes32 public constant SETTER_ADMIN = keccak256("SETTER_ADMIN");
Also found in the following contracts:
This way no gas is wasted when the function is destined to fail.
CashKYCSender and CashKYCSenderReceiver are almost Identical.
It's better to work with an abstract contract here where both of the contracts inherit from. This will save a lot of duplicate code.
The same applies for
CashKYCSenderFactory and CashKYCSenderReceiverFactory
For the two erc20 contracts CTokenModified.sol and CTokenCash.sol there can also be one main contract where both of them inherit from, there are only 4 functions that are different so it will remove a lot of duplicate code.
Same applies for CCash.sol and CErc20.sol
Same thing can be done for the statement if (accrualBlockNumber != getBlockNumber()). This is called 9 times
/// @notice fToken to Oracle Type association mapping(address => OracleType) public fTokenToOracleType; /// @notice Contract storage for fToken's underlying asset prices mapping(address => uint256) public fTokenToUnderlyingPrice; /// @notice fToken to cToken associations for piggy backing off /// of Compound's Oracle mapping(address => address) public fTokenToCToken; struct ChainlinkOracleInfo { AggregatorV3Interface oracle; uint256 scaleFactor; } /// @notice fToken to Chainlink oracle association mapping(address => ChainlinkOracleInfo) public fTokenToChainlinkOracle; /// @notice Price cap for the underlying asset of an fToken. Optional. mapping(address => uint256) public fTokenToUnderlyingPriceCap;
Can be changed to
struct tokenInfo{ OracleType oracleType; uint256 underlyingPrice; address cToken; ChainlinkOracleInfo oracleInfo; uint256 underlyingPriceCap; } /// @notice fToken to Oracle Type association mapping(address => tokenInfo) public fTokenToInfo; struct ChainlinkOracleInfo { AggregatorV3Interface oracle; uint256 scaleFactor; }
The evm will revert whenever over or underflow occurs because the contract is using a higher version of solidity than 0.8.0. So I don't see the point of doing this seperately. CTokenCash.sol#L121-L130
My solution with also a bit of gas saved:
/* Do the calculations, checking for {under,over}flow */ uint allowanceNew = startingAllowance - tokens; - uint srcTokensNew = accountTokens[src] - tokens; - uint dstTokensNew = accountTokens[dst] + tokens; + accountTokens[src] = accountTokens[src] - tokens; + accountTokens[dst] = accountTokens[dst] + tokens; ///////////////////////// // EFFECTS & INTERACTIONS // (No safe failures beyond this point) - accountTokens[src] = srcTokensNew; - accountTokens[dst] = dstTokensNew;
#0 - c4-judge
2023-01-23T10:41:43Z
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
ID | Finding | Gas saved | Instances |
---|---|---|---|
1 | Make for loops unchecked | 414 | 1 |
2 | Save storage variable to memory when it's used more than once in a function | 296 | 3 |
3 | Don't write old value to memory | 500 | 25 |
4 | Function getBlockNumber is useless | 30 | 2 |
5 | Don't emit 2 similar events in 1 function | 1289 | 2 |
7 | Miscellaneous | 4000 | 2 |
The risk of for loops getting overflowed is extremely low. Because it always increments by 1. Even if the arrays are extremely long, it will take a massive amount of time and gas to let the for loop overflow.
addKycAdresses function avg gas saved: 414
The more addresses you want to add, the more gas you will save with this gas optimization
L163 + uint256 i; - for (uint256 i = 0; i < length; i++) { + for (; i < lenght; ){ kycState[kycRequirementGroup][addresses[i]] = true; + unchecked { + i++; + } }
It can also be done on every other for loop in the project. The larger the array the more gas you will save.
When a storage variable is read for the second time from storage it costs 100 gas. When it's read from memory it only costs 3 gas. CashManager.sol#L241-L269
claimMint function avg gas saved: 130
function claimMint( address user, uint256 epochToClaim ) external override updateEpoch nonReentrant whenNotPaused checkKYC(user) { + uint256 epochToExchangeRateMemory = epochToExchangeRate[epochToClaim]; uint256 collateralDeposited = mintRequestsPerEpoch[epochToClaim][user]; if (collateralDeposited == 0) { revert NoCashToClaim(); } - if (epochToExchangeRate[epochToClaim] == 0) { + if (epochToExchangeRateMemory == 0) { revert ExchangeRateNotSet(); } // Get the amount of CASH due at a given rate per epoch uint256 cashOwed = _getMintAmountForEpoch( collateralDeposited, epochToClaim ); mintRequestsPerEpoch[epochToClaim][user] = 0; cash.mint(user, cashOwed); emit MintCompleted( user, cashOwed, collateralDeposited, - epochToExchangeRate[epochToClaim], + epochToExchangeRateMemory epochToClaim ); }
transitionEpoch function avg gas saved: 89
function transitionEpoch() public { + uint256 epochDurationMemory = epochDuration; uint256 epochDifference = (block.timestamp - currentEpochStartTimestamp) / - epochDuration; + epochDurationMemory if (epochDifference > 0) { currentRedeemAmount = 0; currentMintAmount = 0; currentEpoch += epochDifference; currentEpochStartTimestamp = block.timestamp - - (block.timestamp % epochDuration); + (block.timestamp % epochDurationMemory); } }
getUnderlyingPrice() avg gas saved: 77
function getUnderlyingPrice( address fToken ) external view override returns (uint256) { + uint256 price = fTokenToUnderlyingPrice[fToken]; - if (fTokenToUnderlyingPrice[fToken] != 0) { - return fTokenToUnderlyingPrice[fToken]; + if (price != 0) { + return price; } else { // Price is not manually set, attempt to retrieve price from Compound's // oracle address cTokenAddress = fTokenToCToken[fToken]; return cTokenOracle.getUnderlyingPrice(cTokenAddress); } }
Other places where this can be done:
Instead of writing the old value to memory and putting it in the event at the end. You can put the event first so you don't have to write the old value to memory.
Around 20 gas saved per function: CashManager.sol#L596-L600
function setMintLimit(uint256 _mintLimit) external onlyRole(MANAGER_ADMIN) { + emit MintLimitSet(mintlimit, _mintLimit); - uint256 oldMintLimit = mintLimit; mintLimit = _mintLimit; - emit MintLimitSet(oldMintLimit, _mintLimit); }
Can also be done at
The function only returns the blocknumber and does nothing else.
function getBlockNumber() internal view virtual returns (uint) { return block.number; }
It's cheaper and even shorter to just call block.number directly. Calling block.number directly only costs 2 gas.
Around 30 gas can be saved per function where you apply this.
L62: - accrualBlockNumber = getBlockNumber(); + accrualBlockNumber = block.number;
Documentation says the function is mainly used for inheriting test contracts to stub the result. If that's the case the function can still exist but don't use the function anywhere else in the contract.
This also applies for CTokenCash.sol
Having a transfer event and a mint event is useless instead use address(0) in the transfer event to make clear it's a mint.
- emit Mint(minter, actualMintAmount, mintTokens); - emit Transfer(address(this), minter, mintTokens); + emit Transfer(address(0), minter, mintTokens);
Try to minimize emitting events in other functions as well. It costs a lot of gas to emit an event.
IOndoPriceOracleV2.sol#L71-L76
According to the tests, in the function setFTokenToOracleType is the least set to UNINITIALIZED. I don't know if this will occur in real life also. If it does it's better to place the value that gets set the most at the first place in the enum. Writing the first value of an enum to a variable requires way less gas. In the tests this was an average saving of 4000 gas in the setFTokenToOracleType() function with the following. setup.
enum OracleType { + MANUAL, UNINITIALIZED, - MANUAL, COMPOUND, CHAINLINK }
Owner is only set in the constructor so it's best to make it immutable to save some gas on deployment.
#0 - c4-judge
2023-01-23T10:40:11Z
trust1995 marked the issue as grade-b