Halborn Logo

Governance Dynamic Quorum - Moonwell


Prepared by:

Halborn Logo

HALBORN

Last Updated 06/11/2024

Date of Engagement by: September 18th, 2022 - September 26th, 2022

Summary

88% of all REPORTED Findings have been addressed

All findings

8

Critical

0

High

0

Medium

0

Low

0

Informational

8


1. INTRODUCTION

Moonwell Finance engaged Halborn to conduct a security audit on their Governance smart contracts beginning on September 18th, 2022 and ending on September 26th, 2022. The security assessment was scoped to the smart contracts provided to the Halborn Team.

2. AUDIT SUMMARY

The Team at Halborn was provided one week for the engagement and assigned a full-time security engineer to audit the security of the smart contract. The security engineer is a blockchain and smart-contract security expert with advanced penetration testing, smart-contract hacking, and deep knowledge of multiple blockchain protocols.

The purpose of this audit is to:

    • Ensure that smart contract functions operate as intended.

    • Identify potential security issues with the smart contracts.

In summary, Halborn identified some security risks that were addressed by the Moonwell team.

3. TEST APPROACH & METHODOLOGY

Halborn performed a combination of manual and automated security testing to balance efficiency, timeliness, practicality, and accuracy regarding the scope of the smart contract audit. While manual testing is recommended to uncover flaws in logic, process, and implementation; automated testing techniques help enhance coverage of smart contracts and can quickly identify items that do not follow security best practices. The following phases and associated tools were used throughout the term of the audit:

    • Research into architecture and purpose.

    • Smart Contract manual code review and walkthrough.

    • Graphing out functionality and contract logic/connectivity/functions(solgraph).

    • Manual Assessment of use and safety for the critical Solidity variables and functions in scope to identify any arithmetic related vulnerability classes.

    • Static Analysis of security for scoped contract, and imported functions.(Slither)

    • Dynamic Analysis (ganache-cli, brownie, hardhat).

4. SCOPE

\begin{enumerate} \item Moonwell Finance Smart Contracts \begin{enumerate} \item PR 80: \href{https://github.com/moonwell-fi/moonwell-contracts-private/pull/80}{Moonwell Finance - Moonwell Core} \end{enumerate} \end{enumerate}

    • INSCOPE COMMIT ID :

d248cc9a4fc08849f0a5f5d34560f7998b182d4b

FIX COMMIT ID :

TAG :

5. RISK METHODOLOGY

Vulnerabilities or issues observed by Halborn are ranked based on the risk assessment methodology by measuring the LIKELIHOOD of a security incident and the IMPACT should an incident occur. This framework works for communicating the characteristics and impacts of technology vulnerabilities. The quantitative model ensures repeatable and accurate measurement while enabling users to see the underlying vulnerability characteristics that were used to generate the Risk scores. For every vulnerability, a risk level will be calculated on a scale of 5 to 1 with 5 being the highest likelihood or impact.
RISK SCALE - LIKELIHOOD
  • 5 - Almost certain an incident will occur.
  • 4 - High probability of an incident occurring.
  • 3 - Potential of a security incident in the long term.
  • 2 - Low probability of an incident occurring.
  • 1 - Very unlikely issue will cause an incident.
RISK SCALE - IMPACT
  • 5 - May cause devastating and unrecoverable impact or loss.
  • 4 - May cause a significant level of impact or loss.
  • 3 - May cause a partial impact or loss to many.
  • 2 - May cause temporary impact or loss.
  • 1 - May cause minimal or un-noticeable impact.
The risk level is then calculated using a sum of these two values, creating a value of 10 to 1 with 10 being the highest level of security risk.
Critical
High
Medium
Low
Informational
  • 10 - CRITICAL
  • 9 - 8 - HIGH
  • 7 - 6 - MEDIUM
  • 5 - 4 - LOW
  • 3 - 1 - VERY LOW AND INFORMATIONAL

6. SCOPE

Out-of-Scope: New features/implementations after the remediation commit IDs.

7. Assessment Summary & Findings Overview

Critical

0

High

0

Medium

0

Low

0

Informational

8

Impact x Likelihood

HAL-01

HAL-02

HAL-03

HAL-04

HAL-05

HAL-06

HAL-07

HAL-08

Security analysisRisk levelRemediation Date
MISSING QUORUM CAP COMPARISON CAN BREAK THE GOVERNANCEInformational-
ABIENCODERV2 IS ACTIVATED BY DEFAULT 0.8+InformationalSolved - 09/26/2022
BUMP SOLIDITY VERSIONInformationalSolved - 09/26/2022
NO NEED TO INITIALIZE QUORUMADJUSTED WITH FALSEInformationalSolved - 09/26/2022
CURRENT QUORUM CAN BE EMITTED DURING THE PROPOSAL CREATIONInformationalSolved - 09/26/2022
USE PREFIX INCREMENT WITH THE UNCHECK CAN SAVE GASInformationalSolved - 09/26/2022
SAFEMATH IS ACTIVATED BY DEFAULT AFTER 0.8.XInformationalSolved - 09/26/2022
MISSING NATSPEC DOCUMENTATION ON THE FUNCTIONSInformationalSolved - 09/26/2022

8. Findings & Tech Details

8.1 MISSING QUORUM CAP COMPARISON CAN BREAK THE GOVERNANCE

// Informational

Description
Finding description placeholder
Score
Impact:
Likelihood:

8.2 ABIENCODERV2 IS ACTIVATED BY DEFAULT 0.8+

// Informational

Description

ABIEncoderV2 is being stated in a solidity version 0.8+ which is not needed since ABIEncoderV2 is activated by default 0.8+.

Code Location

MoonwellApolloGovernor.sol#L2

MoonwellApolloGovernor.sol

pragma solidity 0.8.10;
pragma experimental ABIEncoderV2;

import "./IERC20.sol";
Score
Impact: 1
Likelihood: 1
Recommendation

SOLVED: The Moonwell team solved this issue by removing ABIEncoderV2.

Commit ID: c7da88a3fe3f0062d8a83ba808b648f1da369fec

8.3 BUMP SOLIDITY VERSION

// Informational

Description

During the review the newest version of solidity was released with the important bug fixes & Bug.

Code Location

MoonwellApolloGovernor.sol#L2

MoonwellApolloGovernor.sol

pragma solidity 0.8.10;
pragma experimental ABIEncoderV2;

import "./IERC20.sol";
Score
Impact: 1
Likelihood: 1
Recommendation

SOLVED: The Moonwell team solved this issue by updating pragma to 0.8.17.

Commit ID: c7da88a3fe3f0062d8a83ba808b648f1da369fec

8.4 NO NEED TO INITIALIZE QUORUMADJUSTED WITH FALSE

// Informational

Description

boolean variable are initialized to a default value of false per Solidity docs. Setting a variable to the default value is unnecessary.

Code Location

MoonwellApolloGovernor.sol#L278

MoonwellApolloGovernor.sol

      Proposal storage newProposal = proposals[proposalCount];
        newProposal.id = proposalCount;
        newProposal.proposer = msg.sender;
        newProposal.eta = 0;
        newProposal.targets = targets;
        newProposal.values = values;
        newProposal.signatures = signatures;
        newProposal.calldatas = calldatas;
        newProposal.startTimestamp = startTimestamp;
        newProposal.endTimestamp = endTimestamp;
        newProposal.startBlock = 0;
        newProposal.forVotes = 0;
        newProposal.againstVotes = 0;
        newProposal.abstainVotes = 0;
        newProposal.totalVotes = 0;
        newProposal.canceled = false;
        newProposal.executed = false;
        newProposal.quorum = currentQuorum;
        newProposal.quorumAdjusted = false;
Score
Impact: 1
Likelihood: 1
Recommendation

SOLVED: The Moonwell team solved this issue by removing explicit initialization.

Commit ID: c7da88a3fe3f0062d8a83ba808b648f1da369fec

8.5 CURRENT QUORUM CAN BE EMITTED DURING THE PROPOSAL CREATION

// Informational

Description

Events allow capturing the changed parameters so that off-chain tools/interfaces can register such changes with timelocks that allow users to evaluate them and consider if they would like to engage/exit based on how they perceive the changes as affecting the trustworthiness of the protocol or profitability of the implemented financial services. The alternative of directly querying on-chain contract state for such changes is not considered practical for most users/usages. In the implementation, current quorum is not emitted on the proposal generation.

Code Location

MoonwellApolloGovernor.sol#L282

MoonwellApolloGovernor.sol

        Proposal storage newProposal = proposals[proposalCount];
        newProposal.id = proposalCount;
        newProposal.proposer = msg.sender;
        newProposal.eta = 0;
        newProposal.targets = targets;
        newProposal.values = values;
        newProposal.signatures = signatures;
        newProposal.calldatas = calldatas;
        newProposal.startTimestamp = startTimestamp;
        newProposal.endTimestamp = endTimestamp;
        newProposal.startBlock = 0;
        newProposal.forVotes = 0;
        newProposal.againstVotes = 0;
        newProposal.abstainVotes = 0;
        newProposal.totalVotes = 0;
        newProposal.canceled = false;
        newProposal.executed = false;
        newProposal.quorum = currentQuorum;
        newProposal.quorumAdjusted = false;

        latestProposalIds[newProposal.proposer] = proposalCount;

        emit ProposalCreated(newProposal.id, msg.sender, targets, values, signatures, calldatas, startTimestamp, endTimestamp, description);
        return newProposal.id;
Score
Impact: 1
Likelihood: 1
Recommendation

SOLVED: The Moonwell team solved this issue by adding current quorum to the event.

Commit ID: c7da88a3fe3f0062d8a83ba808b648f1da369fec

8.6 USE PREFIX INCREMENT WITH THE UNCHECK CAN SAVE GAS

// Informational

Description

The code sections use i++ which costs more gas than ++i, especially in a loop. Finally, the initialization of i = 0 can be skipped, as 0 is the default value.

Code Location

MoonwellApolloGovernor.sol#L255-L676

MoonwellApolloGovernor.sol

    function getQuorum() public view returns (uint) {
        uint newQuorum = currentQuorum;

        // Start at the high water mark
        for (uint i = lastQuorumAdjustment + 1; i < proposalCount; i++) {
            // Pull state and ignore in flight proposals
            ProposalState proposalState = state(i);
            if (proposalState == ProposalState.Pending || proposalState == ProposalState.Active) {
                continue;
            }

            // Get the proposal
            Proposal storage proposal = proposals[i];

            // Only proceed if quorum for this proposal is not yet taken into account.
            if (!proposal.quorumAdjusted) {
                // If a proposal is canceled, ignore it in quorum calculations.
                if (proposalState == ProposalState.Canceled) {
                    continue;
                }

                // Adjust quorum in accordance with the proposal.
                newQuorum = _calculateNewQuorum(newQuorum, proposal.totalVotes);
            }
        }

        return newQuorum;
    }

Score
Impact: 1
Likelihood: 1
Recommendation

SOLVED: The Moonwell team solved this issue with using prefix increment.

Commit ID: c7da88a3fe3f0062d8a83ba808b648f1da369fec

8.7 SAFEMATH IS ACTIVATED BY DEFAULT AFTER 0.8.X

// Informational

Description

Solidity versions >= 0.8.x perform checked arithmetic by default, so the SafeMath library is unnecessary in most cases.

Code Location

MoonwellApolloGovernor.sol#L704-L713

MoonwellApolloGovernor.sol

    function add256(uint256 a, uint256 b) internal pure returns (uint) {
        uint c = a + b;
        require(c >= a, "addition overflow");
        return c;
    }

    function sub256(uint256 a, uint256 b) internal pure returns (uint) {
        require(b <= a, "subtraction underflow");
        return a - b;
    }
Score
Impact: 1
Likelihood: 1
Recommendation

SOLVED: The Moonwell team solved this issue with deleting SafeMath from the contract.

Commit ID: c7da88a3fe3f0062d8a83ba808b648f1da369fec

8.8 MISSING NATSPEC DOCUMENTATION ON THE FUNCTIONS

// Informational

Description

Some functions are missing @param for some of their parameters. Given that NatSpec is an important part of code documentation, this affects code comprehension, auditability, and usability.

Code Location

MoonwellApolloGovernor.sol#L523

MoonwellApolloGovernor.sol

    function setQuorumCaps(uint newLowerQuorumCap, uint newUpperQuorumCap) external {
        require(msg.sender == address(timelock), "only timelock");

        if (newLowerQuorumCap != lowerQuorumCap) {
            uint oldLowerQuorumCap = lowerQuorumCap;
            lowerQuorumCap = newLowerQuorumCap;
            emit LowerQuorumCapChanged(oldLowerQuorumCap, newLowerQuorumCap);
        }

        if (newUpperQuorumCap != upperQuorumCap) {
            uint oldUpperQuorumCap = upperQuorumCap;
            upperQuorumCap = newUpperQuorumCap;
            emit UpperQuorumCapChanged(oldUpperQuorumCap, newUpperQuorumCap);
        }
    }
Score
Impact: 1
Likelihood: 1
Recommendation

SOLVED: The Moonwell team solved this issue by adding natspecs on the functions.

Commit ID: c7da88a3fe3f0062d8a83ba808b648f1da369fec

Halborn strongly recommends conducting a follow-up assessment of the project either within six months or immediately following any material changes to the codebase, whichever comes first. This approach is crucial for maintaining the project’s integrity and addressing potential vulnerabilities introduced by code modifications.