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: 16/73
Findings: 3
Award: $695.21
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: immeas
Also found by: 0xkazim, ABAIKUNANBAEV, T1MOH, berlin-101, bin2chen, kutugu, markus_ether
119.3545 USDC - $119.35
Revoking the guardian of TemporalGovernor.sol contract does unpause the contract as an unexpected side effect which may put funds at risk.
When revoking the guardian of TemporalGovernor.sol via the "revokeGuardian" of function (https://github.com/code-423n4/2023-07-moonwell/blob/fced18035107a345c31c9a9497d0da09105df4df/src/core/Governance/TemporalGovernor.sol#L205) the contract is also automatically unpaused (https://github.com/code-423n4/2023-07-moonwell/blob/fced18035107a345c31c9a9497d0da09105df4df/src/core/Governance/TemporalGovernor.sol#L217).
According to docs in code "Revoking the guardian can only be done via governance proposal or by the guardian itself." And it is further documented in code that the function "unpauses the contract if paused": (https://github.com/code-423n4/2023-07-moonwell/blob/fced18035107a345c31c9a9497d0da09105df4df/src/core/Governance/TemporalGovernor.sol#L217)
Nevertheless, it cannot be assumed that a guardian or someone creating a proposal to revoke the guardian (most likely both done via UI) is familiar with the code and the function comments. Therefore he may be unaware that his "revoke the guardian" action instantaneously unpauses the contract. There are two facts that further support this assumption:
Due to the combination of missing transparency for users in regards to the "unpause" side-effect of the "revokeGuardian" function and the importance of a controlled pause/unpause of the TemporalGovernor.sol contract, which if done wrong, may put funds at risk when contract assumptions (https://github.com/code-423n4/2023-07-moonwell/blob/main/src/core/Governance/TemporalGovernor.sol#L13-L26) are broken, this is considered more than just a QA issue.
Manual review
Governance
#0 - c4-pre-sort
2023-08-03T13:29:28Z
0xSorryNotSorry marked the issue as duplicate of #232
#1 - c4-judge
2023-08-12T20:51:17Z
alcueca marked the issue as satisfactory
#2 - c4-judge
2023-08-12T20:51:20Z
alcueca marked the issue as partial-50
🌟 Selected for report: immeas
Also found by: 0xkazim, ABAIKUNANBAEV, T1MOH, berlin-101, bin2chen, kutugu, markus_ether
119.3545 USDC - $119.35
Calling grantGuardiansPause() on TemporalGovernor.sol can brick governance that can only be escaped by revoking the guardian, which removes contract pauseability.
Governor calls togglePause() when contract is not paused: https://github.com/code-423n4/2023-07-moonwell/blob/main/src/core/Governance/TemporalGovernor.sol#L274 In that function call "guardianPauseAllowed" is set to false: https://github.com/code-423n4/2023-07-moonwell/blob/main/src/core/Governance/TemporalGovernor.sol#L283.
The governor could call togglePause() again to unpause which would happen here: https://github.com/code-423n4/2023-07-moonwell/blob/main/src/core/Governance/TemporalGovernor.sol#L276 and would succeed as "guardianPauseAllowed" is still false: https://github.com/code-423n4/2023-07-moonwell/blob/main/src/core/Governance/TemporalGovernor.sol#L289.
grantGuardianPause() gets called: https://github.com/code-423n4/2023-07-moonwell/blob/main/src/core/Governance/TemporalGovernor.sol#L188 In that function call "guardianPauseAllowed" is set to true: https://github.com/code-423n4/2023-07-moonwell/blob/main/src/core/Governance/TemporalGovernor.sol#L194.
When the guardian now calls togglePause() to unpause the contract, it fails since the "guardianPauseAllowed" will make the call fail here: https://github.com/code-423n4/2023-07-moonwell/blob/main/src/core/Governance/TemporalGovernor.sol#L289.
Also, this will fail any call to "permissionlessUnpause" to unpause the contract here: https://github.com/code-423n4/2023-07-moonwell/blob/main/src/core/Governance/TemporalGovernor.sol#L256.
In consequence, while the contract is still in a paused state, this would leave calling revokeGuardian() as the only option to unpause the contract: https://github.com/code-423n4/2023-07-moonwell/blob/main/src/core/Governance/TemporalGovernor.sol#L217, because it does not evaluate the "guardianPauseAllowed" state variable in its logic.
Once revokeGuardian() was called, no pausing can ever be done to the contract since pausing is only allowed for the guardian (the contract owner, https://github.com/code-423n4/2023-07-moonwell/blob/fced18035107a345c31c9a9497d0da09105df4df/src/core/Governance/TemporalGovernor.sol#L285), which was revoked.
This order of calls can certainly happen since it is not prevented by the implementation, which would force revoking the guardian to get the contract unpaused, which also removes the contract pauseability. The option to pause the contract if assumptions are broken (see: https://github.com/code-423n4/2023-07-moonwell/blob/fced18035107a345c31c9a9497d0da09105df4df/src/core/Governance/TemporalGovernor.sol#L13-L26) is critical to protect funds. Losing this essential security mechanism will put funds at risk.
Manual review
Check within "guardianPauseAllowed" whether the contract is still paused, and don't allow the action if it is still paused. Alternatively, within the "permissionlessUnpause" function, reset the "guardianPauseAllowed" to true and don't have a "guardianPauseAllowed" function. Then the conflict that causes the issue cannot be created. Rethink whether it is meaningful to allow resetting the guardian's allowance for pausing while the contract is still in paused state.
Governance
#0 - c4-pre-sort
2023-08-03T13:29:24Z
0xSorryNotSorry marked the issue as duplicate of #232
#1 - c4-judge
2023-08-12T20:50:59Z
alcueca marked the issue as satisfactory
#2 - c4-judge
2023-08-12T20:51:04Z
alcueca marked the issue as partial-50
🌟 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
A message can never be sent by the zero address and both functions. The first check of both statements that include the check for the zero address would already fail in case the zero address was set as pending. https://github.com/code-423n4/2023-07-moonwell/blob/fced18035107a345c31c9a9497d0da09105df4df/src/core/Unitroller.sol#L61, https://github.com/code-423n4/2023-07-moonwell/blob/fced18035107a345c31c9a9497d0da09105df4df/src/core/Unitroller.sol#L111. The check for the zero address can be removed.
Bot findings highlight that "Non-external/public variable and function names should begin with an underscore". But in the codebase, there is also underscores used as a prefix for external/public functions. Examples: https://github.com/code-423n4/2023-07-moonwell/blob/fced18035107a345c31c9a9497d0da09105df4df/src/core/MErc20.sol#L160, https://github.com/code-423n4/2023-07-moonwell/blob/fced18035107a345c31c9a9497d0da09105df4df/src/core/MErc20Delegator.sol#L412. This should be cleaned up.
https://github.com/code-423n4/2023-07-moonwell/blob/fced18035107a345c31c9a9497d0da09105df4df/src/core/MErc20.sol#L194-L202 and https://github.com/code-423n4/2023-07-moonwell/blob/fced18035107a345c31c9a9497d0da09105df4df/src/core/MErc20.sol#L194-L202 that are identical but have different indentations for comments. This should be cleaned up.
It should be "setter" instead of "settor": https://github.com/code-423n4/2023-07-moonwell/blob/fced18035107a345c31c9a9497d0da09105df4df/src/core/MErc20Delegator.sol#L48.
Here is one example: https://github.com/code-423n4/2023-07-moonwell/blob/fced18035107a345c31c9a9497d0da09105df4df/src/core/Governance/TemporalGovernor.sol#L71-L72. This should be documented in accompanying documents but not be left in code.
Setting and unsetting trusted senders via the respective functions of TemporalGovernor.sol emits TrustedSenderUpdated events (https://github.com/code-423n4/2023-07-moonwell/blob/fced18035107a345c31c9a9497d0da09105df4df/src/core/Governance/TemporalGovernor.sol#L151-L154, https://github.com/code-423n4/2023-07-moonwell/blob/fced18035107a345c31c9a9497d0da09105df4df/src/core/Governance/TemporalGovernor.sol#L178-L181). The constructor does not (https://github.com/code-423n4/2023-07-moonwell/blob/fced18035107a345c31c9a9497d0da09105df4df/src/core/Governance/TemporalGovernor.sol#L74-L78), but could to have complete transparency from the start, who the trusted senders are (e.g. for off-chain risk monitoring).
There is already a check that returns 0 if _amount is equal to 0 (https://github.com/code-423n4/2023-07-moonwell/blob/fced18035107a345c31c9a9497d0da09105df4df/src/core/MultiRewardDistributor/MultiRewardDistributor.sol#L1220). The datatype (uint256) does not allow negative values so the additional check for _amount > 0 (https://github.com/code-423n4/2023-07-moonwell/blob/fced18035107a345c31c9a9497d0da09105df4df/src/core/MultiRewardDistributor/MultiRewardDistributor.sol#L1235) is unnecessary and can be removed.
Here a cast is done to address: https://github.com/code-423n4/2023-07-moonwell/blob/fced18035107a345c31c9a9497d0da09105df4df/src/core/MultiRewardDistributor/MultiRewardDistributor.sol#L126. The same check is done a few lines lower without casting to address: https://github.com/code-423n4/2023-07-moonwell/blob/fced18035107a345c31c9a9497d0da09105df4df/src/core/MultiRewardDistributor/MultiRewardDistributor.sol#L136. The cast to address can be removed for keep consistency.
The following comments should be removed as it is only view functions: https://github.com/code-423n4/2023-07-moonwell/blob/fced18035107a345c31c9a9497d0da09105df4df/src/core/MultiRewardDistributor/MultiRewardDistributor.sol#L348, https://github.com/code-423n4/2023-07-moonwell/blob/fced18035107a345c31c9a9497d0da09105df4df/src/core/MultiRewardDistributor/MultiRewardDistributor.sol#L363.
JumpRateModel.sol represents the state variable as an integer multiplication: https://github.com/code-423n4/2023-07-moonwell/blob/fced18035107a345c31c9a9497d0da09105df4df/src/core/IRModels/JumpRateModel.sol#L20. WhitePaperInterestRateModel.sol represents the constant as a single integer (multiplication result): https://github.com/code-423n4/2023-07-moonwell/blob/fced18035107a345c31c9a9497d0da09105df4df/src/core/IRModels/WhitePaperInterestRateModel.sol#L20. The same representation should be used across all contracts for consistency. The integer multiplication should be preferred.
It should be "timestamp" instead of "timestmp": https://github.com/code-423n4/2023-07-moonwell/blob/fced18035107a345c31c9a9497d0da09105df4df/src/core/IRModels/WhitePaperInterestRateModel.sol#L61, https://github.com/code-423n4/2023-07-moonwell/blob/fced18035107a345c31c9a9497d0da09105df4df/src/core/IRModels/WhitePaperInterestRateModel.sol#L65.
The add() and remove functions of EnumerableSet return true if actually a change to the set was made (https://docs.openzeppelin.com/contracts/4.x/api/utils#EnumerableSet-add-struct-EnumerableSet-Bytes32Set-bytes32-, https://docs.openzeppelin.com/contracts/4.x/api/utils#EnumerableSet-remove-struct-EnumerableSet-Bytes32Set-bytes32-. But the logic of TemporalGovernor.sol does not evaluate these return values and always emits the events (https://github.com/code-423n4/2023-07-moonwell/blob/fced18035107a345c31c9a9497d0da09105df4df/src/core/Governance/TemporalGovernor.sol#L151-L155, https://github.com/code-423n4/2023-07-moonwell/blob/fced18035107a345c31c9a9497d0da09105df4df/src/core/Governance/TemporalGovernor.sol#L178-L182). This should be corrected, and only when the add() or remove() functions returns true, the respective event should be emitted. With the current implementation, off-chain monitoring cannot differentiate whether a trusted sender was actually added or removed from the set.
calculateSupplyRewardsForUser() contains:
userSupplyIndex = initialIndexConstant; //_globalSupplyIndex;
calculateBorrowRewardsForUser() contains:
userBorrowIndex = initialIndexConstant; //userBorrowIndex = _globalBorrowIndex;
The commented-out code should be removed. It could get changed again and make it into production accidentally, which would change the accounting logic.
Change the error messages to the following to be more differentiated according to the "<" used for the check (>= should fail) and harmonize them regarding their wording (e.g., Cannot vs. Can't):
The comment should be "Called by the admin to update the implementation of the delegate" instead of "Called by the admin to update the implementation of the delegator" (https://github.com/code-423n4/2023-07-moonwell/blob/fced18035107a345c31c9a9497d0da09105df4df/src/core/MErc20Delegator.sol#L56).
#0 - c4-judge
2023-08-12T18:12:09Z
alcueca marked the issue as grade-a
#1 - c4-sponsor
2023-08-15T18:26:55Z
ElliotFriedman marked the issue as sponsor confirmed
🌟 Selected for report: Sathish9098
Also found by: 0xSmartContract, K42, Udsen, berlin-101, catellatech, cryptonue, hals, jaraxxus, kodyvim, kutugu, solsaver
The approach with the MErc20Delegator.sol and MErc20Delegate.sol felt a bit odd. I have done a few audits already, and this kind of setup, where I contract basically repeats all the functions of the other and delegates them, I have not seen before. There potentially is a more elegant approach to this. To better judge this, I would have needed to dive deeper into the proxy/upgradeable setup of the protocol, which I did not focus on.
The Chainlink Oracle implementation does not follow Compound's expected way of error handling (returning code 0 instead of reverting in some cases). Since the project is heavily based on Compound logic, I would suggest conforming to Compound standards. I checked all the places where the Oracle is used, and in every case, the 0 return code was handled correctly with an Error.PRICE_ERROR.
The approach to map the symbol of an underlying token for a market (mToken) I would suggest reviewing it. It is not guaranteed by EIP-20 standards that a token returns a symbol, and it is also possible that a token's symbol will arbitrarily change, which would break the mapping/lose the oracle until it is fixed. I encourage you to rethink this design decision and replace it with an address-to-address mapping (token address -> Chainlink price feed address).
I would suggest documenting what an "index" is in the context of the accounting math and why you changed from the Compound way of accounting to using "emissions per second". Please also extend the very critical MultiRewardDistributor.Sol contract with a lot more inline comments. This will allow an auditor to quicker grasp the accounting logic and spend more time on breaking things than on contract comprehension. In MToken.sol, this is way better. Generally, it would be very beneficial if the accounting logic was laid out in writing (e.g., in a whitepaper).
Having an admin controlled overwrite for retrieving a price via the Chainlink Oracle and making the admin price the default case was discovered the first time within Moonbeam. It was not seen in previous audits, and nearly every protocol used a Chainlink oracle. Please rethink if you cannot invert this to use the Chainlink oracle first and fall back to the price set by admin.
32 hours
#0 - c4-judge
2023-08-11T20:56:00Z
alcueca marked the issue as grade-a