Platform: Code4rena
Start Date: 24/07/2023
Pot Size: $100,000 USDC
Total HM: 18
Participants: 73
Period: 7 days
Judge: alcueca
Total Solo HM: 8
Id: 267
League: ETH
Rank: 55/73
Findings: 1
Award: $44.88
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: immeas
Also found by: 0x70C9, 0xAnah, 0xArcturus, 0xComfyCat, 0xWaitress, 0xackermann, 0xkazim, 2997ms, 33audits, Arz, Aymen0909, ChrisTina, JP_Courses, John_Femi, Jorgect, Kaysoft, LosPollosHermanos, MohammedRizwan, Nyx, Rolezn, Sathish9098, Stormreckson, T1MOH, Tendency, Topmark, Udsen, Vagner, albertwh1te, ast3ros, banpaleo5, berlin-101, catellatech, cats, codetilda, cryptonue, eeshenggoh, fatherOfBlocks, hals, jamshed, jaraxxus, josephdara, kankodu, kodyvim, kutugu, lanrebayode77, mert_eren, nadin, naman1778, niki, petrichor, ravikiranweb3, said, solsaver, souilos, twcctop, wahedtalash77
44.8793 USDC - $44.88
The mip00.sol
test contract imported the following files:
ITransparentUpgradeableProxy
in the statement import {TransparentUpgradeableProxy, ITransparentUpgradeableProxy} from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"
.TimelockController
in the statementimport {TimelockController} from "@openzeppelin/contracts/governance/TimelockController.sol"
.IVotes
in the statement import {IVotes} from "@openzeppelin/contracts/governance/extensions/GovernorVotes.sol"
.IERC20
in the statement import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"
.TimelockProposal
in the satement import {TimelockProposal} from "@test/proposals/proposalTypes/TimelockProposal.sol"
.IWormhole
in the statementimport {IWormhole} from "@protocol/core/Governance/IWormhole.sol";
FaucetTokenWithPermit
in the statement import {FaucetTokenWithPermit} from "@test/helper/FaucetToken.sol"
MockWeth
in the statement import {MockWeth} from "@test/mock/MockWeth.sol"
But these file were not used in the contract consider removing the import declarartion of these files as this is generally a bad coding practice and aslo these un-used imports pollute the mip00.sol
contract namespace.
When sensitive changes are made to the contracts, like changes in state variables events ought to be emitted, but in some functions changes were made to state and no event was emitted to corroborate the changes.
In the togglePause()
function the values of the state variables guardianPauseAllowed
and lastPauseTime
were changed. The code should be refactored to emit an event that represents the changes made to state. Adding an event will facilitate offchain monitoring when changing system parameters.
payable
keyword.The payable
keyword is used to create payable addresses for the transfer of the blockchain's native tokens. In transfer involving ERC-20 tokens the address does not have to be type converted to a payable address type. The function sendRewards()
in the MultiRewardDistributor.sol has an argument -user
of type address payable
but this -user
argument was only used in the transfer of ERC-20 token therefore using the payable
keyword in this instance is redundant also in the invocation of the sendRewards function on line 1194-1198 and line 1084-1088 the paremeters _borrower
and _supplier
were type converted to type payable address before been passed to the sendRewards()
function which is unnecessary.
payable
keyword in the _user
argument declaration can be safely removed then the type conversion of line 1195 and line 1085 can also be removed. The diff below shows how the code could be refactored.function sendReward( - address payable _user, //@audit user doesn't have to be a payable address to receive erc20 tokens + address _user, uint256 _amount, address _rewardToken ) internal nonReentrant returns (uint256) { . . . . } uint256 pendingRewards = sendReward( - payable(_borrower), + _borrower, emissionConfig.borrowerRewardsAccrued[_borrower], emissionConfig.config.emissionToken ); uint256 unsendableRewards = sendReward( - payable(_supplier), + _supplier, emissionConfig.supplierRewardsAccrued[_supplier], emissionConfig.config.emissionToken );
Using multiple require() statements rather than using boolean AND (&&) in multiple check combinations could help improve code readability and makes it easier to debug.
file: /src/core/Comptroller.sol 813: require(numMarkets != 0 && numMarkets == numBorrowCaps, "invalid input"); 850: require(numMarkets != 0 && numMarkets == numSupplyCaps, "invalid input");
The above line of code could be refactored as shown below to aid readability:
- require(numMarkets != 0 && numMarkets == numBorrowCaps, "invalid input"); + require(numMarkets != 0, "Invalid input"); + require(numMarkets == numBorrowCaps, "invalid input");
file: /src/core/Governance/TemporalGovernor.sol require( targets.length == values.length && targets.length == calldatas.length, "TemporalGovernor: Arity mismatch for payload" );
The above lines of code could be refactored as shown below to aid readability:
- require( - targets.length == values.length && - targets.length == calldatas.length, - "TemporalGovernor: Arity mismatch for payload" - ); + require(targets.length == values.length, "TemporalGovernor: Arity mismatch for payload"); + require(targets.length == calldatas.length, "TemporalGovernor: Arity mismatch for payload");
file: /src/core/MToken.sol 33: require(accrualBlockTimestamp == 0 && borrowIndex == 0, "market may only be initialized once");
The above line of code could be refactored as shown below to aid readability:
- require(accrualBlockTimestamp == 0 && borrowIndex == 0, "market may only be initialized once"); + require(accrualBlockTimestamp == 0, "market may only be initialized once" ); + require(borrowIndex == 0, "market may only be initialized once");
file: src/core/Oracles/ChainlinkCompositeOracle.sol 108: require( 109: expectedDecimals > uint8(0) && expectedDecimals <= uint8(18), 110: "CLCOracle: Invalid expected decimals" 111: ); 139: require( 140: expectedDecimals > uint8(0) && expectedDecimals <= uint8(18), 141: "CLCOracle: Invalid expected decimals" 142: );
The above lines of code could be refactored as shown below to aid readability:
- require( - expectedDecimals > uint8(0) && expectedDecimals <= uint8(18), - "CLCOracle: Invalid expected decimals" - ); + require(expectedDecimals > uint8(0), "CLCOracle: Invalid expected decimals"); + require(expectedDecimals <= uint8(18),"CLCOracle: Invalid expected decimals");
file: src/core/Oracles/ChainlinkOracle.sol 145: require( 146: feed != address(0) && feed != address(this), 147: "invalid feed address" 148: );
The above lines of code could be refactored as shown below to aid readability:
- require( - feed != address(0) && feed != address(this), - "invalid feed address" - ); + require(feed != address(0), "invalid feed address"); + require(feed != address(this), "invalid feed address");
When you're developing smart contracts in Solidity, it's of utmost importance to carefully validate the inputs of your functions. This validation process is particularly critical for bytes parameters, especially when they hold essential data like addresses, identifiers, or raw information that the contract relies on for its operations.
Neglecting to check for empty bytes can give rise to unexpected and undesirable outcomes within your contract. For instance, certain operations might encounter failures, produce inaccurate results, or unnecessarily consume gas when performed with empty bytes. Additionally, overlooking input validation may potentially expose your contract to malicious activities, including the exploitation of unhandled edge cases.
To safeguard against these issues, it is crucial to consistently validate that bytes parameters are not empty whenever your contract's logic necessitates their usage. By implementing such validations, you can significantly reduce the risks associated with erroneous behavior and enhance the overall security and reliability of your smart contract.
file: /src/core/MErc20Delegator.sol 471: function delegateToImplementation(bytes memory data) public returns (bytes memory) { 472: return delegateTo(implementation, data); 473: }
The delegateToImplementation()
function above has bytes memory parameter data
which takes in a value of type bytes memory and passes it as an argument to the delegateTo()
function which performs a delegate call. You should implement a check for the length of data
so as to avoid performing a delegate call with empty bytes values, a probable exploitation of unhandled edge cases and any other unexpected scenarios.
The code should be refactored as shown by the diff below:
function delegateToImplementation(bytes memory data) public returns (bytes memory) { + if (data.length == 0) + revert("Empty data"); return delegateTo(implementation, data); }
file: /src/core/MErc20Delegator.sol 482: function delegateToViewImplementation(bytes memory data) public view returns (bytes memory) { 483: (bool success, bytes memory returnData) = address(this).staticcall(abi.encodeWithSignature("delegateToImplementation(bytes)", data)); 484: assembly { 485: if eq(success, 0) { 486: revert(add(returnData, 0x20), returndatasize()) 487: } 488: } 489: return abi.decode(returnData, (bytes)); 490: }
The delegateToViewImplementation()
function performs a staticcall so checking the length of the bytes value is important as it helps prevent the possibility of reading a wrong value if an empty bytes of data is mistakenly given as argument to the function.
The code should be refactored as shown by the diff below:
function delegateToViewImplementation(bytes memory data) public view returns (bytes memory) { + if (data.length == 0) + revert("Empty data"); (bool success, bytes memory returnData) = address(this).staticcall(abi.encodeWithSignature("delegateToImplementation(bytes)", data)); assembly { if eq(success, 0) { revert(add(returnData, 0x20), returndatasize()) } } return abi.decode(returnData, (bytes)); }
The normal if / else statement can be written in a shorthand way using the ternary operator. It increases readability and reduces the number of lines of code.
file: src/core/MToken.sol 81: uint startingAllowance = 0; 82: if (spender == src) { 83: startingAllowance = type(uint).max; 84: } else { 85: startingAllowance = transferAllowances[src][spender]; 86: }
The if-else statement above could be converted to a ternary statement to enhance its simplicity and readability and also reduce the number of lines of code. The code could be refactored as shown in the diff below:
- uint startingAllowance = 0; - if (spender == src) { - startingAllowance = type(uint).max; - } else { - startingAllowance = transferAllowances[src][spender]; - } + uint startingAllowance = spender == src ? type(uint).max : transferAllowances[src][spender];
file: src/core/MToken.sol 633: if (redeemTokensIn == type(uint).max) { 634: vars.redeemTokens = accountTokens[redeemer]; 635: } else { 636: vars.redeemTokens = redeemTokensIn; 637: }
The if-else statement above could be converted to a ternary statement to enhance its simplicity and readability and also reduce the number of lines of code. The code could be refactored as shown in the diff below:
- if (redeemTokensIn == type(uint).max) { - vars.redeemTokens = accountTokens[redeemer]; - } else { - vars.redeemTokens = redeemTokensIn; - } + vars.redeemTokens = redeemTokensIn == type(uint).max ? accountTokens[redeemer] : redeemTokensIn;
file: src/core/MToken.sol 889: if (repayAmount == type(uint).max) { 890: vars.repayAmount = vars.accountBorrows; 891: } else { 892: vars.repayAmount = repayAmount; 893: }
The if-else statement above could be converted to a ternary statement to enhance its simplicity and readability and also reduce the number of lines of code. The code could be refactored as shown in the diff below:
- if (repayAmount == type(uint).max) { - vars.repayAmount = vars.accountBorrows; - } else { - vars.repayAmount = repayAmount; - } + vars.repayAmount = repayAmount == type(uint).max ? vars.accountBorrows : repayAmount;
One level of nesting can be removed by not having an else block when the if-block returns.
file: /src/core/MultiRewardDistributor/MultiRewardDistributor.sol 1235: if (_amount > 0 && _amount <= currentTokenHoldings) { 1236: // Ensure we use SafeERC20 to revert even if the reward token isn't ERC20 compliant 1237: token.safeTransfer(_user, _amount); 1238: return 0; 1239: } else { 1240: // If we've hit here it means we weren't able to emit the reward and we should emit an event 1241: // instead of failing. 1242: emit InsufficientTokensToEmit(_user, _rewardToken, _amount); 1243: 1244: // By default, return the same amount as what's left over to send, we accrue reward but don't send them out 1245: return _amount; 1246: }
In the if-else statement above we could remove the else block since the if block returns 0 when its conditional is true. The code should be refactored as shown in the diff below:
if (_amount > 0 && _amount <= currentTokenHoldings) { // Ensure we use SafeERC20 to revert even if the reward token isn't ERC20 compliant token.safeTransfer(_user, _amount); return 0; } - } else { - // If we've hit here it means we weren't able to emit the reward and we should emit an event - // instead of failing. - emit InsufficientTokensToEmit(_user, _rewardToken, _amount); - - // By default, return the same amount as what's left over to send, we accrue reward but don't send them out - return _amount; - } + // If we've hit here it means we weren't able to emit the reward and we should emit an event + // instead of failing. + emit InsufficientTokensToEmit(_user, _rewardToken, _amount); + + // By default, return the same amount as what's left over to send, we accrue reward but don't send them out + return _amount;
According to solidity style guide the naming convetion for argument of functions is mixedCase while capital letters should be reserved for constants. There are functions in the TemporalGovernor.sol
contract whose arguments do not follow this standard and this could lead to confusions or mistakes while audting or implementing changes to the contract. The contract should be refactored and these be corrected.
file: core/Governance/TemporalGovernor.sol 229: function queueProposal(bytes memory VAA) external whenNotPaused { 230: _queueProposal(VAA); 231: }
The diff below shows how the queueProposal()
function could be refactored:
- function queueProposal(bytes memory VAA) external whenNotPaused { - _queueProposal(VAA); - } + function queueProposal(bytes memory vaa) external whenNotPaused { + _queueProposal(vaa); + }
file: core/Governance/TemporalGovernor.sol 237: function executeProposal(bytes memory VAA) public whenNotPaused { 238: _executeProposal(VAA, false); 239: }
The diff below shows how the executeProposal()
function could be refactored:
- function executeProposal(bytes memory VAA) public whenNotPaused { - _executeProposal(VAA, false); - } + function executeProposal(bytes memory vaa) public whenNotPaused { + _executeProposal(vaa, false); + }
file: core/Governance/TemporalGovernor.sol 266: function fastTrackProposalExecution(bytes memory VAA) external onlyOwner { 267: _executeProposal(VAA, true); /// override timestamp checks and execute 268: }
The diff below shows how the fastTrackProposalExecution()
function could be refactored:
- function fastTrackProposalExecution(bytes memory VAA) external onlyOwner { - _executeProposal(VAA, true); /// override timestamp checks and execute - } + function fastTrackProposalExecution(bytes memory vaa) external onlyOwner { + _executeProposal(vaa, true); /// override timestamp checks and execute + }
file: core/Governance/TemporalGovernor.sol 295: function _queueProposal(bytes memory VAA) private { 296: /// Checks 297: 298: // This call accepts single VAAs and headless VAAs 299: ( 300: IWormhole.VM memory vm, 301: bool valid, 302: string memory reason 303: ) = wormholeBridge.parseAndVerifyVM(VAA); . . . 340: 341: emit QueuedTransaction(intendedRecipient, targets, values, calldatas); 342: }
The diff below shows how the _queueProposal()
function could be refactored:
- function _queueProposal(bytes memory VAA) private { + function _queueProposal(bytes memory vaa) private { /// Checks // This call accepts single VAAs and headless VAAs ( IWormhole.VM memory vm, bool valid, string memory reason - ) = wormholeBridge.parseAndVerifyVM(VAA); + ) = wormholeBridge.parseAndVerifyVM(vaa); . . . . emit QueuedTransaction(intendedRecipient, targets, values, calldatas); }
344: function _executeProposal(bytes memory VAA, bool overrideDelay) private { 345: // This call accepts single VAAs and headless VAAs 346: ( 347: IWormhole.VM memory vm, 348: bool valid, 349: string memory reason 350: ) = wormholeBridge.parseAndVerifyVM(VAA); . . . 408: }
The diff below shows how the _executeProposal()
function could be refactored:
- function _executeProposal(bytes memory VAA, bool overrideDelay) private { + function _executeProposal(bytes memory vaa, bool overrideDelay) private { // This call accepts single VAAs and headless VAAs ( IWormhole.VM memory vm, bool valid, string memory reason - ) = wormholeBridge.parseAndVerifyVM(VAA); + ) = wormholeBridge.parseAndVerifyVM(VAA); . . . }
block.timestamp
directly instead of using function callblock.timestamp
is a global variable therefore it can be used at anytime by any function of the contract so there is no reason you should defined a function to return block.timestamp
when it can be accessed directly. If it was required for test, the function should be removed and anywhere it was invoked should be refactored to call block.timestamp
directly.
In the contract below the function getBlockTimestamp()
should be removed.
In the contract below the function getBlockTimestamp()
should be removed.
In some instance the code could be refactored to not use some variable that were defined. It is good that when emitting an event that includes a new and an old value, you should emit both values for credibility and openness to users but in some instances you can avoid declaring a new variable to cache the old value you could Instead, emit the event, then save the new value in the variable.
file: src/core/MultiRewardDistributor/MultiRewardDistributor.sol function _setPauseGuardian( address _newPauseGuardian ) external onlyPauseGuardianOrAdmin { require( _newPauseGuardian != address(0), "Pause Guardian can't be the 0 address!" ); address currentPauseGuardian = pauseGuardian; pauseGuardian = _newPauseGuardian; emit NewPauseGuardian(currentPauseGuardian, _newPauseGuardian); }
In the _setPauseGuardian()
function above the is no need to declare the currentPauseGuardian
variable as we can emit the event with pauseGuardian
and _newPauseGuardian
then assign the _newPauseGuardian
value to pauseGuardian
. The diff below shows how this could be done:
function _setPauseGuardian( address _newPauseGuardian ) external onlyPauseGuardianOrAdmin { require( _newPauseGuardian != address(0), "Pause Guardian can't be the 0 address!" ); - address currentPauseGuardian = pauseGuardian; - pauseGuardian = _newPauseGuardian; - emit NewPauseGuardian(currentPauseGuardian, _newPauseGuardian); + emit NewPauseGuardian(pauseGuardian, _newPauseGuardian); + pauseGuardian = _newPauseGuardian; }
function _setEmissionCap( uint256 _newEmissionCap ) external onlyComptrollersAdmin { uint256 oldEmissionCap = emissionCap; emissionCap = _newEmissionCap; emit NewEmissionCap(oldEmissionCap, _newEmissionCap); }
In the _setEmissionCap()
function above the is no need to declare the oldEmissionCap
variable as we can emit the event with emissionCap
and _newEmissionCap
then assign the _newEmissionCap
value to emissionCap
. The diff below shows how this could be done:
function _setEmissionCap( uint256 _newEmissionCap ) external onlyComptrollersAdmin { - uint256 oldEmissionCap = emissionCap; - emissionCap = _newEmissionCap; - emit NewEmissionCap(oldEmissionCap, _newEmissionCap); + emit NewEmissionCap(emissionCap, _newEmissionCap); + emissionCap = _newEmissionCap; }
292: function redeemVerify(address mToken, address redeemer, uint redeemAmount, uint redeemTokens) override pure external { 293: // Shh - currently unused 294: mToken; //@audit what's the purpose of this line? 295: redeemer; //@audit what's the purpose of this line? 296: 297: // Require tokens is zero or amount is also zero 298: if (redeemTokens == 0 && redeemAmount > 0) { 299: revert("redeemTokens zero"); 300: } 301: }
Line 294 and line 295 of the redeemVerify()
function above are totally unnecessary as they have no effect on the execution of the function you should either remove them completly or put them in comments
366: function repayBorrowAllowed( 367: address mToken, 368: address payer, 369: address borrower, 370: uint repayAmount) override external returns (uint) { 371: // Shh - currently unused 372: payer; //@audit what is the purpose of this line 373: borrower; //@audit what is the purpose of this line 374: repayAmount; //@audit what is the purpose of this line 375: 376: if (!markets[mToken].isListed) { 377: return uint(Error.MARKET_NOT_LISTED); 378: } 379: 380: // Keep the flywheel moving 381: updateAndDistributeBorrowerRewardsForToken(mToken, borrower); 382: 383: return uint(Error.NO_ERROR); 384: }
Line 372, line 373 and line 374 of the repayBorrowAllowed()
function above are totally unnecessary as they have no effect on the execution of the function you should either remove them completly or put them in comments
#0 - code423n4
2023-07-31T19:58:18Z
Withdrawn by 0xAnah
#1 - liveactionllama
2023-08-24T17:21:12Z
It seems there was a submission form error that led to some confusion here, and the submission should not have been completely withdrawn. The warden's original content has now been restored from the past commit.
#2 - c4-judge
2023-08-24T20:55:57Z
alcueca marked the issue as grade-a