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: 26/73
Findings: 3
Award: $299.96
🌟 Selected for report: 1
🚀 Solo Findings: 0
🌟 Selected for report: 0xkazim
Also found by: Auditwolf, BRONZEDISC, Hama, MohammedRizwan, R-Nemes, dacian, kodyvim, markus_ether, nadin, niki, okolicodes
135.7347 USDC - $135.73
the chainlinkOracle.sol
contract specially the getChainlinkPrice
function using the aggregator v2 and v3 to get/call the latestRoundData
. the function should check for the min and max amount return to prevent some case happen, something like this:
https://solodit.xyz/issues/missing-checks-for-chainlink-oracle-spearbit-connext-pdf https://solodit.xyz/issues/m-16-chainlinkadapteroracle-will-return-the-wrong-price-for-asset-if-underlying-aggregator-hits-minanswer-sherlock-blueberry-blueberry-git
if case like luna happen then the oracle will return the minimum price and not the crashed price.
the function getChainlinkPrice
:
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"); // Chainlink USD-denominated feeds store answers at 8 decimals uint256 decimalDelta = uint256(18).sub(feed.decimals()); // Ensure that we don't multiply the result by 0 if (decimalDelta > 0) { return uint256(answer).mul(10 ** decimalDelta); } else { return uint256(answer); } }
the function did not check for the min and max price.
manual review
some check like this can be added to avoid returning of the min price or the max price in case of the price crashes.
require(answer < _maxPrice, "Upper price bound breached"); require(answer > _minPrice, "Lower price bound breached");
Oracle
#0 - 0xSorryNotSorry
2023-08-01T11:04:12Z
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 below;
ChainlinkAggregators have minPrice and maxPrice circuit breakers built into them.
Further proof required as per the context.
#1 - c4-pre-sort
2023-08-01T11:04:16Z
0xSorryNotSorry marked the issue as low quality report
#2 - alcueca
2023-08-13T14:42:54Z
The warden is actually right. It is a bit difficult to find, but the minAnswer
and maxAnswer
can be retrieved from the Chainlink Aggregator, one step through the proxy. A circuit breaker should be implemented on the Moonwell oracle so that when the price edges close to minAnswer
or maxAnswer
it starts reverting, to avoid consuming stale prices when Chainlink freezes.
#3 - c4-judge
2023-08-13T14:43:13Z
alcueca marked the issue as satisfactory
#4 - c4-judge
2023-08-13T14:48:49Z
alcueca marked the issue as primary issue
#5 - c4-judge
2023-08-13T14:48:57Z
alcueca marked the issue as selected for report
🌟 Selected for report: immeas
Also found by: 0xkazim, ABAIKUNANBAEV, T1MOH, berlin-101, bin2chen, kutugu, markus_ether
119.3545 USDC - $119.35
https://github.com/code-423n4/2023-07-moonwell/blob/fced18035107a345c31c9a9497d0da09105df4df/src/core/Governance/TemporalGovernor.sol#L241-L259 https://github.com/code-423n4/2023-07-moonwell/blob/fced18035107a345c31c9a9497d0da09105df4df/src/core/Governance/TemporalGovernor.sol#L274-L290 https://github.com/code-423n4/2023-07-moonwell/blob/fced18035107a345c31c9a9497d0da09105df4df/src/core/Governance/TemporalGovernor.sol#L202-L221
the TemporalGovernor
implemented in a way that if the guardians pause the system then the system will be paused to period of time and the system will remove the guradians approve till the system reApprove them again and going to the unpause mood, the system really focus on to unpause the contract only if the specifc time is passed in the permissionlessUnpause
function. but any guardian can unpause the system even before the required time is passed. more details in POC
the guardians can pause the system by calling the togglePause
:
function togglePause() external onlyOwner { if (paused()) { _unpause(); } else { require( guardianPauseAllowed, "TemporalGovernor: guardian pause not allowed" ); guardianPauseAllowed = false; lastPauseTime = block.timestamp.toUint248(); _pause(); } /// statement for SMT solver assert(!guardianPauseAllowed); /// this should be an unreachable state }
and if the system is paused then the guardians should only be allowed to unpause the system only if period of time has passed in the permissionlessUnpause
:
function permissionlessUnpause() external whenPaused { /// lastPauseTime cannot be equal to 0 at this point because /// block.timstamp on a real chain will always be gt 0 and /// toggle pause will set lastPauseTime to block.timestamp /// which means if the contract is paused on a live network, /// its lastPauseTime cannot be 0 //@audit can be called if required time is passed require( lastPauseTime + permissionlessUnpauseTime <= block.timestamp, "TemporalGovernor: not past pause window" ); lastPauseTime = 0; _unpause(); assert(!guardianPauseAllowed); /// this should never revert, statement for SMT solving emit PermissionlessUnpaused(block.timestamp); }
but this won't be true because the guardians(the 19 guardians) can unpause the systme by calling one of these two functionstogglePause
and revokeGuardian
which allow to unpause the system without need of passing specific time passed.
the togglePause
will unpause the system if the system was paused already without need to wait for specific time to passed:
function togglePause() external onlyOwner { //@audit can be called when the contract is paused and set it to the unpause mood if (paused()) { _unpause(); } else { require( guardianPauseAllowed, "TemporalGovernor: guardian pause not allowed" ); guardianPauseAllowed = false; lastPauseTime = block.timestamp.toUint248(); _pause(); } /// statement for SMT solver assert(!guardianPauseAllowed); /// this should be an unreachable state }
same thing for the revokeGuardian
:
function revokeGuardian() external { //@audit bad guardian can unpause the systme even if the time not passed ! address oldGuardian = owner(); require( msg.sender == oldGuardian || msg.sender == address(this), "TemporalGovernor: cannot revoke guardian" ); _transferOwnership(address(0)); guardianPauseAllowed = false; lastPauseTime = 0; if (paused()) { _unpause(); } emit GuardianRevoked(oldGuardian); }
the protocol trust in the guardians but this not recommended and the protocol should think about this case and how to prevent it in case the guardians get hacked or act malicousliy. even it mention in the docs that If Moonbeam governance is compromised, the contract will be paused until governance control is restored. in this case the system should be care about prevent any mistakliy unpause the system:
https://github.com/code-423n4/2023-07-moonwell/blob/main/docs/TEMPORALGOVERNOR.md
the 19 guardians by wormhole: https://wormholecrypto.medium.com/wormhole-guardians-everstake-83afb735cddd
manual review
recommend to add the same check in the permissionlessUnpause
function to avoid unpause the system in bad time and bad case even by mistake or by an malicious guardians.
Governance
#0 - c4-pre-sort
2023-08-03T13:29:32Z
0xSorryNotSorry marked the issue as duplicate of #232
#1 - c4-judge
2023-08-12T20:51:35Z
alcueca marked the issue as satisfactory
#2 - c4-judge
2023-08-12T20:51:39Z
alcueca marked the issue as partial-50
#3 - c4-judge
2023-08-21T21:30:28Z
alcueca changed the severity to 2 (Med Risk)
🌟 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 function getPrice
may underflow if the mToken decimals was more than 18. this can happen because mToken can be any token with decimals more than 18 and in this case the function getPrice()
will underflow and revert.
the function getPrice
:
function getPrice(MToken mToken) internal view returns (uint256 price) { EIP20Interface token = EIP20Interface( MErc20(address(mToken)).underlying() ); if (prices[address(token)] != 0) { price = prices[address(token)]; } else { //add return here price = getChainlinkPrice(getFeed(token.symbol())); } /** @audit underflow may happen, some tokens have more than 18 tokens */ uint256 decimalDelta = uint256(18).sub(uint256(token.decimals())); // Ensure that we don't multiply the result by 0 if (decimalDelta > 0) { return price.mul(10 ** decimalDelta); } else { return price; } }
the function will get the decimal delta using the token decimals which can be more than 18 and cause underflow.
manual review
recommend to add check for token.decimals <= 18
to avoid underflow
Decimal
#0 - 0xSorryNotSorry
2023-08-03T08:02:36Z
The function can't underflow but will return the price
.
#1 - c4-pre-sort
2023-08-03T13:45:55Z
0xSorryNotSorry marked the issue as duplicate of #270
#2 - c4-judge
2023-08-12T22:09:06Z
alcueca changed the severity to QA (Quality Assurance)
#3 - c4-judge
2023-08-12T22:09:30Z
alcueca marked the issue as grade-a