Moonwell - markus_ether's results

An open lending and borrowing DeFi protocol.

General Information

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

Moonwell

Findings Distribution

Researcher Performance

Rank: 22/73

Findings: 3

Award: $494.68

🌟 Selected for report: 0

🚀 Solo Findings: 0

Findings Information

🌟 Selected for report: 0xkazim

Also found by: Auditwolf, BRONZEDISC, Hama, MohammedRizwan, R-Nemes, dacian, kodyvim, markus_ether, nadin, niki, okolicodes

Labels

bug
2 (Med Risk)
low quality report
satisfactory
edited-by-warden
duplicate-340

Awards

104.4113 USDC - $104.41

External Links

Lines of code

https://github.com/code-423n4/2023-07-moonwell/blob/4aaa7d6767da3bc42e31c18ea2e75736a4ea53d4/src/core/Oracles/ChainlinkOracle.sol#L100-L101 https://github.com/code-423n4/2023-07-moonwell/blob/4aaa7d6767da3bc42e31c18ea2e75736a4ea53d4/src/core/Oracles/ChainlinkCompositeOracle.sol#L183-L189

Vulnerability details

Impact

The ChainlinkCompositeOracle and ChainlinkOracle do not verify if the prices fall within the permissible minimum and maximum values established by Chainlink. Without these checks, prices outside of the extreme ends (i.e., minimum and maximum) will be not accurately reported and when lending for these assets is enabled the protocol can be drained.

Proof of Concept

Chainlink aggregators have a built in circuit breaker when 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.
In its current form, the getChainlinkPrice() / getPriceAndDecimals() function retrieve the latest round data from Chainlink. If the asset's market price plummets below minAnswer or skyrockets above maxAnswer, the returned price will still be minAnswer or maxAnswer, respectively, rather than the actual market price. This could potentially lead to an exploitation scenario where the protocol accounts the asset using incorrect price information.

    function getChainlinkPrice(
        AggregatorV3Interface feed
    ) internal view returns (uint256) {
        (, int256 answer, , uint256 updatedAt, ) = AggregatorV3Interface(feed)
            .latestRoundData();

Illustration:

Present price of TokenA is $10 TokenA has a minimum price set at $1 on chainlink The actual price of TokenA dips to $0.10 The aggregator continues to report $1 as the price. Consequently, users can interact with protocol using TokenA as though it were still valued at $1, which is a tenfold overestimate of its real market value.

Tools Used

Manual review

Compare the current price and compare it to the minPrice/maxPrice and revert when the price reached the bounds.

(, int256 answer, , uint256 updatedAt, ) = AggregatorV3Interface(feed) .latestRoundData(); + IChainlinkAggregator aggregator = IChainlinkAggregator(IChainlinkPriceFeed(source).aggregator()); + require(price > int256(aggregator.minAnswer()) && price < int256(aggregator.maxAnswer());

Alternatively a fallback oracle can be used.

Assessed type

Oracle

#0 - 0xSorryNotSorry

2023-08-02T08:22:34Z

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-02T08:22:39Z

0xSorryNotSorry marked the issue as low quality report

#2 - alcueca

2023-08-13T20:48:11Z

The mitigation is wrong, but the finding is valid.

#3 - c4-judge

2023-08-13T20:48:15Z

alcueca marked the issue as satisfactory

#4 - c4-judge

2023-08-13T21:06:57Z

alcueca marked the issue as duplicate of #340

Findings Information

🌟 Selected for report: immeas

Also found by: 0xkazim, ABAIKUNANBAEV, T1MOH, berlin-101, bin2chen, kutugu, markus_ether

Labels

bug
2 (Med Risk)
high quality report
satisfactory
sponsor confirmed
edited-by-warden
duplicate-314

Awards

238.709 USDC - $238.71

External Links

Lines of code

https://github.com/code-423n4/2023-07-moonwell/blob/main/src/core/Governance/TemporalGovernor.sol#L288-L289 https://github.com/code-423n4/2023-07-moonwell/blob/main/src/core/Governance/TemporalGovernor.sol#L256

Vulnerability details

Impact

Upon calling the pause function, the guardian's right to pause the protocol is revoked until the governance or the guardian themselves reinstate the right.

If the right to paused is restored when the contract is still paused, the contract becomes permanently unpausable. The only way to unpause the contract is to remove the guardian from the protocol.

Proof of Concept

As soon as the guardian pauses the contract, the boolean guardianPauseAllowed is reset to false. The governance or the guardian themselves can reinstate the guardian's right to pause by invoking grantGuardiansPause via _executeProposal , flipping guardianPauseAllowed back to true. However, this action stops the the unpausing process, as unpausing requires that guardianPauseAllowed is false.

assert(!guardianPauseAllowed); /// this should never revert, statement for SMT solving

The only viable resolution would be for the guardian to call revokeGuardian, which also unpauses the contract. This action removes the guardian from the protocol by transferring ownership to the null address and can lead to a loss of funds when no guard can stop the protocol during a hack.

A proof of concept that illustrates the issue is shown below:

function testBreakPause() public {
    // start: contract is paused
    assertFalse(governor.paused());
    
    // call togglePause(), now the guardianPauseAllowed = false && lastPauseTime = 0
    governor.togglePause();
    assertTrue(governor.paused());
    assertFalse(governor.guardianPauseAllowed());

    // call fastTrackProposalExecution() to set guardianPauseAllowed = true 
    vm.prank(address(governor)); // done as prank to simplify
    governor.grantGuardiansPause();
    assertTrue(governor.guardianPauseAllowed()); 

    // now the contract is can not be unpaused again
    vm.expectRevert();
    governor.togglePause();

    vm.warp(block.timestamp + governor.permissionlessUnpauseTime());
    vm.expectRevert();
    governor.permissionlessUnpause();

    // unpause contract by revoking guardian
    governor.revokeGuardian();
    assertTrue(governor.paused());
    assertEq(governor.owner(), address(0));
}

Tools Used

Manual review.

As long as is desired behavior that the guardian should not receive the right to pause the contract (before the contract is unpaused), we should encorce that bevaior in the function grantGuardiansPause by enforcing that it can not be called when the contract is not paused.

-    function grantGuardiansPause() external {
+    function grantGuardiansPause() external whenPaused {
    
    }

As an alternative solution, permissionlessUnpause and togglePause could be adjusted to allow to invoke them, even when guardianPauseAllowed is true.

- assert(!guardianPauseAllowed); 

Assessed type

Governance

#0 - 0xSorryNotSorry

2023-08-02T09:44:04Z

The issue is well demonstrated, properly formatted, contains a coded POC. Marking as HQ.

#1 - c4-pre-sort

2023-08-02T09:44:07Z

0xSorryNotSorry marked the issue as high quality report

#2 - c4-pre-sort

2023-08-03T13:28:25Z

0xSorryNotSorry marked the issue as primary issue

#3 - c4-sponsor

2023-08-03T22:17:02Z

ElliotFriedman marked the issue as sponsor confirmed

#4 - ElliotFriedman

2023-08-03T22:17:06Z

great finding!

#5 - c4-judge

2023-08-12T20:53:10Z

alcueca marked issue #314 as primary and marked this issue as a duplicate of 314

#6 - c4-judge

2023-08-12T20:53:18Z

alcueca marked the issue as satisfactory

Findings Information

🌟 Selected for report: immeas

Also found by: 0xComfyCat, Vagner, ast3ros, kutugu, markus_ether, volodya

Labels

bug
2 (Med Risk)
partial-50
edited-by-warden
duplicate-308

Awards

151.5613 USDC - $151.56

External Links

Lines of code

https://github.com/code-423n4/2023-07-moonwell/blob/fced18035107a345c31c9a9497d0da09105df4df/src/core/Governance/TemporalGovernor.sol#L385-L388

Vulnerability details

Impact

The _executeProposal fails to validate that the message received from Wormhole is intended for this contract. A malicous guardian can repurpose a message transmitted via the Wormhole Bridge and execute a illegitimate proposal

Proof of Concept

The Wormhole book suggest various checks when receiving a message. One such check isto ensure that the incoming message is inteded explicitly for the recipient contract. The recipient contract is expected to perform the following verification

require(parseIntendedRecipient(vm.payload) == address(this));

While this check is included in the _queueProposal function, is is absent from _executeProposal. Regular proposals are typically processed through the queuedTransactions workflow, which includes this validation. However, the the overrideDelay option allows a guardian to execute proposals instantly, thereby bypassing this check.

Tools Used

Manual review

Ensure that the received message was crafted for this contract by including the verification step in the code:

+ address intendedRecipient;
- (, targets, values, calldatas) = abi.decode(
+ (intendedRecipient, targets, values, calldatas) = abi.decode(
     vm.payload,
    (address, address[], uint256[], bytes[])
);

+ // check that VAA is for this contract
+ require(intendedRecipient == address(this), "TemporalGovernor: Incorrect destination");

Assessed type

Invalid Validation

#0 - c4-pre-sort

2023-08-03T13:24:19Z

0xSorryNotSorry marked the issue as duplicate of #308

#1 - c4-judge

2023-08-12T20:27:58Z

alcueca marked the issue as satisfactory

#2 - c4-judge

2023-08-12T20:28:08Z

alcueca marked the issue as partial-50

AuditHub

A portfolio for auditors, a security profile for protocols, a hub for web3 security.

Built bymalatrax © 2024

Auditors

Browse

Contests

Browse

Get in touch

ContactTwitter