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: 30/73
Findings: 2
Award: $149.29
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: 0xkazim
Also found by: Auditwolf, BRONZEDISC, Hama, MohammedRizwan, R-Nemes, dacian, kodyvim, markus_ether, nadin, niki, okolicodes
104.4113 USDC - $104.41
https://github.com/code-423n4/2023-07-moonwell/blob/fced18035107a345c31c9a9497d0da09105df4df/src/core/Oracles/ChainlinkCompositeOracle.sol#L189 https://github.com/code-423n4/2023-07-moonwell/blob/fced18035107a345c31c9a9497d0da09105df4df/src/core/Oracles/ChainlinkOracle.sol#L100-L101
Chainlink aggregators have a built in circuit breaker if the price of an asset goes outside of a predetermined price band. The result is that if an asset experiences a huge drop in value (i.e. LUNA crash) the price of the oracle will continue to return the minPrice instead of the actual price of the asset. This would allow user to continue borrowing with the asset but at the wrong price. This is exactly what happened to Venus on BSC when LUNA imploded
File: core/Oracles/ChainlinkCompositeOracle.sol function getPriceAndDecimals( address oracleAddress ) public view returns (int256, uint8) { ( uint80 roundId, int256 price, , , uint80 answeredInRound >> ) = AggregatorV3Interface(oracleAddress).latestRoundData(); bool valid = price > 0 && answeredInRound == roundId; // Some code }
Here, there is only a check for price to be non-negative, and not within an acceptable range.
File: core/Oracles/ChainlinkOracle.sol function getChainlinkPrice( AggregatorV3Interface feed ) internal view returns (uint256) { (, int256 answer, , uint256 updatedAt, ) = AggregatorV3Interface(feed) >> .latestRoundData(); require(answer > 0, "Chainlink price cannot be lower than 0"); require(updatedAt != 0, "Round is in incompleted state"); // Some code }
Here, there is only a check for answer to be non-negative, and not within an acceptable range.
Manual Review
Consider using the following checks.
For example:
(uint80, int256 answer, uint, uint, uint80) = oracle.latestRoundData(); // minPrice check require(answer > minPrice, "Min price exceeded"); // maxPrice check require(answer < maxPrice, "Max price exceeded");
Oracle
#0 - 0xSorryNotSorry
2023-08-02T17:54:52Z
The implementation does not set a min/max value by design. Also Chainlink does not return min/max price as per the AggregatorV3 docs HERE contrary to the reported.
Further proof required as per the context.
#1 - c4-pre-sort
2023-08-02T17:54:56Z
0xSorryNotSorry marked the issue as low quality report
#2 - c4-judge
2023-08-14T22:11:44Z
alcueca marked the issue as duplicate of #340
#3 - c4-judge
2023-08-14T22:11:49Z
alcueca marked the issue as satisfactory
🌟 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
Number | Issue | Instances | |
---|---|---|---|
[L‑01] | Incorrect timestampsPerYear constant in JUMPRATEMODEL documentation | 1 | |
[L‑02] | chainId should use uint64/uint256 instead of uint16 | 1 | |
[L‑03] | Calls inside loops that may address DoS | 1 | |
[L‑04] | Avoid using safeMath for solidity version 0.8.0 and above | 1 | |
[L‑05] | set limit for emissionCap in _setEmissionCap() | 1 | |
[L‑06] | delegatecall should check comptrollerImplementation address first | 1 | |
[L‑07] | block.timestamp can be directly used instead of making function | 13 | |
[L‑08] | A very Misleading comment and function parameter in MToken.sol | 3 | |
[L‑09] | require() check validations should be at top of functions, not at last | 2 | |
[L‑10] | Incomplete interaction summary and writeup for mint and borrow in documentation | 2 | |
[L‑11] | Use Inline assembly in contracts, if only neccessary | 7 | |
[L‑12] | Use of block.timestamp instead of block.number | All contracts | |
[L‑13] | Missing payment amount check in Comptroller.sol | 3 | |
[L‑14] | WETH9 interface function visibility does not match with L2 WETH9 interface | 1 | |
[L‑15] | Pre-EIP-155 is not supported by default | 1 | |
[L‑16] | Misleading comment in MIP00 documentation | 3 |
timestampsPerYear contant is incorrectly mentioned in documentation as compared to current implementation in JumpRateModel.sol contract. The documentation states,
timestampsPerYear: The number of timestamps per year (606024*365).
This is not correct and must be corrected per recommendation.
timestampsPerYear: The number of timestamps per year (60 * 60 * 24 * 365).
Per the ethereum paper, the chainID is defined in uint256 but later a proposal is submitted to make uint256 chainID to uint64. Reference:- https://github.com/ethereum/evmc/pull/420
But the current the implementation has used uint16 for chainID which will be insufficient with growing chainID as uint16 will not support large numbers. Some currently supported EVM chains with chainID can not support. For example, Aurora Mainnet ChainID- 1313161554 (0x4e454152)
Different EVM supported chainID can be seen from below link: https://chainlist.org/chain/1313161554
Use uint64 for chainId instead of uint16. If design allows, it is also recommended to use uint256 for chainId per Ethereum paper.
In TemporalGovernor.sol contract _executeProposal() function, Calls to external contracts inside a loop are dangerous because it could lead to DoS if one of the calls reverts or execution runs out of gas. Such issue also introduces chance of problems with the gas limits.
Per SWC-113: External calls can fail accidentally or deliberately, which can cause a DoS condition in the contract. To minimize the damage caused by such failures, it is better to isolate each external call into its own transaction that can be initiated by the recipient of the call.
Reference link- https://swcregistry.io/docs/SWC-113
Code Reference:
File: contracts/ArcadeTreasury.sol 344 function _executeProposal(bytes memory VAA, bool overrideDelay) private { // Some code 394 for (uint256 i = 0; i < targets.length; i++) { 395 address target = targets[i]; 396 uint256 value = values[i]; 397 bytes memory data = calldatas[i]; 398 399 // Go make our call, and if it is not successful revert with the error bubbling up 400 (bool success, bytes memory returnData) = target.call{value: value}( 401 data 402 ); // Some code 409 }
Contracts like WhitePaperInterestRateModel.sol and JumpRateModel.sol have used safeMath for calculations.Solidity ^0.8.0 handles overflow and underflow by default. There is no need of safeMath here, A direct arithmatic caculations can be performed.
There are 2 instances of this issue:
Remove safeMath from these contracts.
In MultiRewardDistributor.sol, _setEmissionCap() is used to set emission cap to any value and this can be accessed by comptroller admin howver emmision limit must be set otherwise 0 as well as any higher value can be passed accidentally or intentionally.
There is 1 instance of this issue:
File: src/core/MultiRewardDistributor/MultiRewardDistributor.sol function _setEmissionCap( uint256 _newEmissionCap ) external onlyComptrollersAdmin { uint256 oldEmissionCap = emissionCap; emissionCap = _newEmissionCap; emit NewEmissionCap(oldEmissionCap, _newEmissionCap); }
Add a limit check to validate the emissionCap.
Per solidity documentation, EXTCODE is not checked for delegatecall as it is low level call function. It is an exception by solidity compiler but this can result on functionality. If the comptrollerImplementation address is not existed, the delegatecall will return success. It means it always return success for non-existent address, Therefore comptrollerImplementation code existence check is very much required.
File: src/core/Unitroller.sol fallback() external { // delegate all other functions to current implementation (bool success, ) = comptrollerImplementation.delegatecall(msg.data); assembly { let free_mem_ptr := mload(0x40) returndatacopy(free_mem_ptr, 0, returndatasize()) switch success case 0 { revert(free_mem_ptr, returndatasize()) } default { return(free_mem_ptr, returndatasize()) } } }
File: src/core/Unitroller.sol fallback() external { + uint codeSize; + assembly { + codeSize := extcodesize(comptrollerImplementation) + } + require(codeSize > 0); // delegate all other functions to current implementation (bool success, ) = comptrollerImplementation.delegatecall(msg.data); assembly { let free_mem_ptr := mload(0x40) returndatacopy(free_mem_ptr, 0, returndatasize()) switch success case 0 { revert(free_mem_ptr, returndatasize()) } default { return(free_mem_ptr, returndatasize()) } } }
In MToken.sol, block.timestamp can be directly used instead of making function.
There is 13 instance of this issue:
File: src/core/MToken.sol function getBlockTimestamp() virtual internal view returns (uint) { return block.timestamp; }
Use block.timestamp instead of wrapping it under function.
In MToken.sol, in accrueInterest() function, there is misleading comment and function parameter which must be corrected. Here the calculations are performed in terms of blocktimestamp howevever comments and function parameters states it as in block terms which is incorrect.
There is 3 instances of this issue:
File: src/core/MToken.sol - /* Calculate the number of blocks elapsed since the last accrual */ + /* Calculate the total timestamp elapsed since the last accrual */ - (MathError mathErr, uint blockDelta) = subUInt(currentBlockTimestamp, accrualBlockTimestampPrior); + (MathError mathErr, uint timestampDelta) = subUInt(currentBlockTimestamp, accrualBlockTimestampPrior); require(mathErr == MathError.NO_ERROR, "could not calculate block delta"); /* * Calculate the interest accumulated into borrows and reserves and the new index: - * simpleInterestFactor = borrowRate * blockDelta + * simpleInterestFactor = borrowRate * timestampDelta * interestAccumulated = simpleInterestFactor * totalBorrows * totalBorrowsNew = interestAccumulated + totalBorrows * totalReservesNew = interestAccumulated * reserveFactor + totalReserves * borrowIndexNew = simpleInterestFactor * borrowIndex + borrowIndex */ Exp memory simpleInterestFactor; uint interestAccumulated; uint totalBorrowsNew; uint totalReservesNew; uint borrowIndexNew; - (mathErr, simpleInterestFactor) = mulScalar(Exp({mantissa: borrowRateMantissa}), blockDelta); + (mathErr, simpleInterestFactor) = mulScalar(Exp({mantissa: borrowRateMantissa}), timestampDelta);
In MErc20Delegate.sol, require() checks should always be at top checking the access control to function instead putting at lost. This avoid gas wastage till the code coming to require check however, the current implementation is not recommended considering checks, Effects, Interactions pattern too. However there is no external interactions here.
There are 2 instances of this issue:
File: src/core/MErc20Delegate.sol function _becomeImplementation(bytes memory data) virtual override public { + require(msg.sender == admin, "only the admin may call _becomeImplementation"); // Shh -- currently unused data; // Shh -- we don't ever want this hook to be marked pure if (false) { implementation = address(0); } - require(msg.sender == admin, "only the admin may call _becomeImplementation"); } /** * @notice Called by the delegator on a delegate to forfeit its responsibility */ function _resignImplementation() virtual override public { + require(msg.sender == admin, "only the admin may call _resignImplementation"); // Shh -- we don't ever want this hook to be marked pure if (false) { implementation = address(0); } - require(msg.sender == admin, "only the admin may call _resignImplementation"); }
In CROSSCONTRACTINTERACTION.md, cross chain interaction with mint and borrow is given however as the document says in depth look
which is missing. The interaction summary is incomplete given with direct path wothout mentioning the sub functions or internal functions for which new user will confuse and mislead them. Therefore the interaction summary for both mint and borrow needs to add more detail. I recommend the interaction summary for both, however the description should be modified for same by developers.
There are 2 instances of this issue: Document Link: click here
File: main/CROSSCONTRACTINTERACTION.md **Mint:** - Summarizing the interactions that occur when a user calls mint: MToken -> Comptroller -> MultiRewardDistributor -> MToken + Summarizing the interactions that occur when a user calls mint: MErc20 -> MToken -> mintInternal -> mintFresh -> Comptroller -> mintAllowed -> MultiRewardDistributor -> MToken -> MErc20 **Borrow:** - Summarizing the interactions that occur when a user calls borrow: MToken -> Comptroller -> MultiRewardDistributor -> MToken + Summarizing the interactions that occur when a user calls borrow: MErc20 -> MToken -> borrowInternal -> borrowFresh -> Comptroller -> -> borrowAllowed -> MultiRewardDistributor -> MToken -> MErc20
Note: Add more depth to description per above.
Inline assembly is a way to access the Virtual Machine at a low level. This discards several important safety features in Solidity. It is recommended only when inline assembly is very much needed in contracts.
There are 7 instances of this issue:
In MErc20.sol at L-193
In MErc20.sol at L-228
In MErc20Delegator.sol at L-457
In MErc20Delegator.sol at L-457
In MErc20Delegator.sol at L-484
In MErc20Delegator.sol at L-457
In MErc20Delegator.sol at L-140
The contracts should avoid using inline assembly because it interacts with the blockchain Virtual Machine at a low level. An attacker could bypass many essential safety features of Solidity.
The contracts JumpRateModel, WhitePaperInterestRateModel, Comptroller.sol and other contracts has used block.timestamp. The global variable block.timestamp does not necessarily hold the current time, and may not be accurate. Miners can influence the value of block.timestamp to perform Maximal Extractable Value (MEV) attacks. There is no guarantee that the value is correct, only that it is higher than the previous block’s timestamp. However, it is not as reliable as block.number, as it can be manipulated by miners. This makes it a less reliable choice for time-dependent computations.
Block.number is a more reliable source of truth than block.timestamp, as it is guaranteed by the protocol to increase by one compared to the previous block.
Instances are present in all contracts.
Use block.number instead of block.timestamp. However if it intended design with well accepted risk then it should be okay.
The protocol includes some financial functions Comptroller.sol such as borrowAllowed(), liquidateBorrowAllowed(), transferAllowed(), etc. Attackers could use these functions with large amounts in Flash-Loan or similar financial attacks. Other DeFi platforms like The DAO, Spartan, and others were hacked, and attackers stole a significant amount of funds.The issue here is there are no restrictions on large transfers. Prevention is better than cure. The already happended hacks suggest to have a check on large amounts and it should be considered in functions. Basically, users are not limited for transactions in these functions.
This could be a Medium severity finding but it could also be intended design so considered it as low severity with that assumption. However judge decision is respected.
There are 3 instances of this issue:
In Comptroller.sol, borrowAllowed() function
In Comptroller.sol, liquidateBorrowAllowed() function
In Comptroller.sol, transferAllowed() function
It is recommended to implement a mechanism which controls payment amount.
The WETH9 interface used in contract does not match the function visibility of L2 deployed WETH9 interface. Per the discussion with sponsor, The contracts will be deployed only on BASE. BASE is built on the Bedrock release of the OP Stack. Therefore on OP mainnet, WETH9 contract is installed on address 0x4200000000000000000000000000000000000006
. Check the contract address and see the functions visibility which differs in current implementation.
Code reference: Click here
WETH9 used interface: Click here
Use WETH9 interface per OP WETH9 contract address 0x4200000000000000000000000000000000000006
Per discussion with sponsor, the contracts will be deployed on BASE L2 blockchain. BASE is built on the Bedrock release of the OP Stack.
OP stack documentation states, "Pre-EIP-155 transactions do not have a chain ID, which means a transaction on one Ethereum blockchain can be replayed on others. This is a security risk, so pre-EIP-155 transactions are not supported on OP Stack by default." Reference link: Click here
An extra care should be taken for this EIP-155 default issue. Ensure it is documented in contracts as well as MoonWell documentation.
In MIP00.md documentation, It is referred as cToken instead of mToken which is not correct as the contracts has used MToken and no used compound Token.
Documentation reference link
There are 3 instances of this issue:
- Set Collateral Factor on CToken: // Some text + Set Collateral Factor on MToken: // Some text - Set Liquidation Incentive on CToken: // Some text + Set Liquidation Incentive on MToken: // Some text - Set Close Factor on CToken: // Some text + Set Close Factor on MToken: // Some text
#0 - c4-judge
2023-08-12T17:59:19Z
alcueca marked the issue as grade-a
#1 - c4-sponsor
2023-08-15T18:29:57Z
ElliotFriedman marked the issue as sponsor confirmed