Mamo PR - Moonwell


Prepared by:

Halborn Logo

HALBORN

Last Updated 06/26/2025

Date of Engagement: June 18th, 2025 - June 18th, 2025

Summary

100% of all REPORTED Findings have been addressed

All findings

9

Critical

0

High

0

Medium

1

Low

1

Informational

7


1. Summary

2. Introduction

Moonwell engaged Halborn to conduct a security assessment on their smart contracts beginning on June 18th, 2025 and ending on June 19th, 2025. The security assessment was scoped to the smart contracts provided to Halborn. Commit hash and further details can be found in the Scope section of this report.


The smart contracts under review consisted of StrategyFactory, a Factory contract for creating new strategy instances with configurable parameters, and StrategyMulticall, a contract to allow efficient batch updates and generic multicalls to strategies. Furthermore, the security review also added the upgrade of the SlippagePriceChecker contract to make sure no risks were introduced nor storage collisions.

3. Assessment Summary

Halborn was provided 2 days for the engagement, and assigned one full-time security engineer to review the security of the smart contracts in scope. The engineer is a blockchain and smart contract security expert with advanced penetration testing and smart contract hacking skills, and deep knowledge of multiple blockchain protocols.

The objectives of this assessment were to

    • Identify potential security issues within the smart contracts.

    • Ensure that smart contract functionality operates as intended.


In summary, Halborn identified some improvements to reduce the likelihood and impact of risks, which were completely addressed by the Moonwell team. The main ones were the following:

    • Consider refunding excess funds in genericMulticall() or add a withdraw functionallity.

    • Add a reentrancy protection to the genericMulticall() function in StrategyMulticall.

4. Test Approach and Methodology

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

    • 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.

    • Manual testing by custom scripts.

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

    • Testnet deployment (Foundry).


5. Storage collision Assessment

Storage collision study was performed as part of this security review. The original SlippagePriceChecker only used Slot 0 and Slot 1:

forge inspect src/SlippagePriceChecker.sol:SlippagePriceChecker storage 

╭-------------------+---------------------------------------------------------------------------+------+--------+-------+---------------------------------------------------╮
| Name              | Type                                                                      | Slot | Offset | Bytes | Contract                                          |
+===========================================================================================================================================================================+
| tokenOracleData   | mapping(address => struct ISlippagePriceChecker.TokenFeedConfiguration[]) | 0    | 0      | 32    | src/SlippagePriceChecker.sol:SlippagePriceChecker |
|-------------------+---------------------------------------------------------------------------+------+--------+-------+---------------------------------------------------|
| maxTimePriceValid | mapping(address => uint256)                                               | 1    | 0      | 32    | src/SlippagePriceChecker.sol:SlippagePriceChecker |
╰-------------------+---------------------------------------------------------------------------+------+--------+-------+---------------------------------------------------╯

The new SlippagePriceChecker uses Slot 0, Slot 1 and Slot 2:

forge inspect src/SlippagePriceChecker.sol:SlippagePriceChecker storage

╭---------------------+-----------------------------------------------------------------------------------------------+------+--------+-------+---------------------------------------------------╮
| Name                | Type                                                                                          | Slot | Offset | Bytes | Contract                                          |
+=================================================================================================================================================================================================+
| tokenOracleData     | mapping(address => struct ISlippagePriceChecker.TokenFeedConfiguration[])                     | 0    | 0      | 32    | src/SlippagePriceChecker.sol:SlippagePriceChecker |
|---------------------+-----------------------------------------------------------------------------------------------+------+--------+-------+---------------------------------------------------|
| maxTimePriceValid   | mapping(address => uint256)                                                                   | 1    | 0      | 32    | src/SlippagePriceChecker.sol:SlippagePriceChecker |
|---------------------+-----------------------------------------------------------------------------------------------+------+--------+-------+---------------------------------------------------|
| tokenPairOracleData | mapping(address => mapping(address => struct ISlippagePriceChecker.TokenFeedConfiguration[])) | 2    | 0      | 32    | src/SlippagePriceChecker.sol:SlippagePriceChecker |
╰---------------------+-----------------------------------------------------------------------------------------------+------+--------+-------+---------------------------------------------------╯

Therefore, no potential collisions were observed.

6. RISK METHODOLOGY

Every vulnerability and issue observed by Halborn is ranked based on two sets of Metrics and a Severity Coefficient. This system is inspired by the industry standard Common Vulnerability Scoring System.
The two Metric sets are: Exploitability and Impact. Exploitability captures the ease and technical means by which vulnerabilities can be exploited and Impact describes the consequences of a successful exploit.
The Severity Coefficients is designed to further refine the accuracy of the ranking with two factors: Reversibility and Scope. These capture the impact of the vulnerability on the environment as well as the number of users and smart contracts affected.
The final score is a value between 0-10 rounded up to 1 decimal place and 10 corresponding to the highest security risk. This provides an objective and accurate rating of the severity of security vulnerabilities in smart contracts.
The system is designed to assist in identifying and prioritizing vulnerabilities based on their level of risk to address the most critical issues in a timely manner.

6.1 EXPLOITABILITY

Attack Origin (AO):
Captures whether the attack requires compromising a specific account.
Attack Cost (AC):
Captures the cost of exploiting the vulnerability incurred by the attacker relative to sending a single transaction on the relevant blockchain. Includes but is not limited to financial and computational cost.
Attack Complexity (AX):
Describes the conditions beyond the attacker’s control that must exist in order to exploit the vulnerability. Includes but is not limited to macro situation, available third-party liquidity and regulatory challenges.
Metrics:
EXPLOITABILITY METRIC (mem_e)METRIC VALUENUMERICAL VALUE
Attack Origin (AO)Arbitrary (AO:A)
Specific (AO:S)
1
0.2
Attack Cost (AC)Low (AC:L)
Medium (AC:M)
High (AC:H)
1
0.67
0.33
Attack Complexity (AX)Low (AX:L)
Medium (AX:M)
High (AX:H)
1
0.67
0.33
Exploitability EE is calculated using the following formula:

E=meE = \prod m_e

6.2 IMPACT

Confidentiality (C):
Measures the impact to the confidentiality of the information resources managed by the contract due to a successfully exploited vulnerability. Confidentiality refers to limiting access to authorized users only.
Integrity (I):
Measures the impact to integrity of a successfully exploited vulnerability. Integrity refers to the trustworthiness and veracity of data stored and/or processed on-chain. Integrity impact directly affecting Deposit or Yield records is excluded.
Availability (A):
Measures the impact to the availability of the impacted component resulting from a successfully exploited vulnerability. This metric refers to smart contract features and functionality, not state. Availability impact directly affecting Deposit or Yield is excluded.
Deposit (D):
Measures the impact to the deposits made to the contract by either users or owners.
Yield (Y):
Measures the impact to the yield generated by the contract for either users or owners.
Metrics:
IMPACT METRIC (mIm_I)METRIC VALUENUMERICAL VALUE
Confidentiality (C)None (I:N)
Low (I:L)
Medium (I:M)
High (I:H)
Critical (I:C)
0
0.25
0.5
0.75
1
Integrity (I)None (I:N)
Low (I:L)
Medium (I:M)
High (I:H)
Critical (I:C)
0
0.25
0.5
0.75
1
Availability (A)None (A:N)
Low (A:L)
Medium (A:M)
High (A:H)
Critical (A:C)
0
0.25
0.5
0.75
1
Deposit (D)None (D:N)
Low (D:L)
Medium (D:M)
High (D:H)
Critical (D:C)
0
0.25
0.5
0.75
1
Yield (Y)None (Y:N)
Low (Y:L)
Medium (Y:M)
High (Y:H)
Critical (Y:C)
0
0.25
0.5
0.75
1
Impact II is calculated using the following formula:

I=max(mI)+mImax(mI)4I = max(m_I) + \frac{\sum{m_I} - max(m_I)}{4}

6.3 SEVERITY COEFFICIENT

Reversibility (R):
Describes the share of the exploited vulnerability effects that can be reversed. For upgradeable contracts, assume the contract private key is available.
Scope (S):
Captures whether a vulnerability in one vulnerable contract impacts resources in other contracts.
Metrics:
SEVERITY COEFFICIENT (CC)COEFFICIENT VALUENUMERICAL VALUE
Reversibility (rr)None (R:N)
Partial (R:P)
Full (R:F)
1
0.5
0.25
Scope (ss)Changed (S:C)
Unchanged (S:U)
1.25
1
Severity Coefficient CC is obtained by the following product:

C=rsC = rs

The Vulnerability Severity Score SS is obtained by:

S=min(10,EIC10)S = min(10, EIC * 10)

The score is rounded up to 1 decimal places.
SeverityScore Value Range
Critical9 - 10
High7 - 8.9
Medium4.5 - 6.9
Low2 - 4.4
Informational0 - 1.9

7. SCOPE

Files and Repository
(a) Repository: mamo-contracts
(b) Assessed Commit ID: c75d06a
(c) Items in scope:
  • src/SlippagePriceChecker.sol
  • src/StrategyFactory.sol
  • src/StrategyMulticall.sol
Out-of-Scope: Third party dependencies and economic attacks.
Remediation Commit ID:
Out-of-Scope: New features/implementations after the remediation commit IDs.

8. Assessment Summary & Findings Overview

Critical

0

High

0

Medium

1

Low

1

Informational

7

Security analysisRisk levelRemediation Date
Excess Value Becomes IrrecoverableMediumSolved - 06/20/2025
Missing Reentrancy Protection in StrategyMulticallLowSolved - 06/23/2025
Single-step Ownership Transfer ProcessInformationalAcknowledged - 06/25/2025
Owner Can Renounce OwnershipInformationalAcknowledged - 06/25/2025
Inaccurate NatSpec CommentsInformationalAcknowledged - 06/25/2025
Unused ImportsInformationalAcknowledged - 06/25/2025
Use of Revert Strings Instead of Custom ErrorsInformationalAcknowledged - 06/25/2025
Redundant Named ReturnInformationalAcknowledged - 06/25/2025
Style Guide OptimizationsInformationalAcknowledged - 06/25/2025

9. Findings & Tech Details

9.1 Excess Value Becomes Irrecoverable

//

Medium

Description

The genericMulticall() function does not verify that the combined call.value values across all batched calls are less than or equal to msg.value, nor does it provide any mechanism to automatically refund surplus native assets to the caller. Any native funds overpaid by the owner remain locked within the contract balance without a recovery mechanism. Over time, these unrecoverable balances can accumulate, leading to unnecessary capital inefficiency and potential confusion for the owner when reconciling contract holdings.

BVSS
Recommendation

Consider one or both of the following mitigations to ensure any excess native funds can be safely returned or recovered:

  • Automatic Refund of Overpayments: Before executing the batched calls, determine the total value required and enforce that it does not exceed msg.value. After the calls complete, any surplus should be immediately sent back to the caller in a single refund transaction.

  • Owner-Only Rescue Function: Implement a dedicated function protected by onlyOwner that allows the contract owner to withdraw any excess native funds held by the contract.


Remediation Comment

SOLVED: The Moonwell team fixed this finding in commit 37bc9da by verifying the amount sent to genericMulticall() as recommended.

Remediation Hash

9.2 Missing Reentrancy Protection in StrategyMulticall

//

Low

Description

The StrategyMulticall contract does not have any protection against reentrancy risks. For the genericMulticall() function, an attacker (or a mistaken owner transaction) can supply the StrategyMulticall contract's own address as a call target, causing it to reenter itself before the first invocation completes. Because genericMulticall() uses a low-level .call{value:…} without any nonReentrant guard, the nested invocation would execute the same loop logic recursively.


Although the function is restricted by onlyOwner and requires sufficient msg.value for all calls (including the nested ones), a malicious or misconfigured batch could still trigger unexpected behavior or resource exhaustion under edge conditions.

BVSS
Recommendation

Consider inheriting and applying OpenZeppelin’s ReentrancyGuard to StrategyMulticall and mark genericMulticall() with the nonReentrant modifier. This will block any attempt to call genericMulticall() (directly or indirectly) while it is already executing, fully eliminating the self-targeted reentrancy vector.

Remediation Comment

SOLVED: The Moonwell team fixed this finding in commit 24bb086 by adding a reentrancy protection as recommended.

Remediation Hash

9.3 Single-step Ownership Transfer Process

//

Informational

Description

It was identified that the StrategyMulticall and SlippagePriceChecker contracts in scope inherit from OpenZeppelin's OwnableUpgradeable library. Ownership of the contracts that are inherited from the OwnableUpgradeable module can be lost, as the ownership is transferred in a single-step process.


The address that the ownership is changed to should be verified to be active or willing to act as the owner. Ownable2StepUpgradeable is safer than OwnableUpgradeable for smart contracts because the owner cannot accidentally transfer smart contract ownership to a mistyped address. Rather than directly transferring to the new owner, the transfer only completes when the new owner accepts ownership.

BVSS
Recommendation

To mitigate the risks associated with single-step ownership transitions and enhance contract security, it is recommended to adopt a two-step ownership transition mechanism, such as OpenZeppelin's Ownable2StepUpgradeable. This approach introduces an additional step in the ownership transfer process, requiring the new owner to accept ownership before the transition is finalized. The process typically involves the current owner calling a function to nominate a new owner, and the nominee then calling another function to accept ownership.


Implementing Ownable2StepUpgradeable provides several benefits:


1. Reduces Risk of Accidental Loss of Ownership: By requiring explicit acceptance of ownership, the risk of accidentally transferring ownership to an incorrect or zero address is significantly reduced.

2. Enhanced Security: It adds another layer of security by ensuring that the new owner is prepared and willing to take over the responsibilities associated with contract ownership.

3. Flexibility in Ownership Transitions: Allows for a smoother transition of ownership, as the nominee has the opportunity to prepare for the acceptance of their new role.


By adopting Ownable2StepUpgradeable, contract administrators can ensure a more secure and controlled process for transferring ownership, safeguarding against the risks associated with accidental or unauthorized ownership changes.

Remediation Comment

ACKNOWLEDGED: The Moonwell team acknowledged this finding.

9.4 Owner Can Renounce Ownership

//

Informational

Description

It was identified that the StrategyMulticall and SlippagePriceChecker contracts in scope inherit from OpenZeppelin's OwnableUpgradeable library. In the OwnableUpgradeable contracts, the renounceOwnership() function is used to renounce the Owner permission. Renouncing ownership before transferring would result in the contract having no owner, eliminating the ability to call privileged functions.

/**
 * @dev Leaves the contract without owner. It will not be possible to call
 * `onlyOwner` functions. Can only be called by the current owner.
 *
 * NOTE: Renouncing ownership will leave the contract without an owner,
 * thereby disabling any functionality that is only available to the owner.
 */
function renounceOwnership() public virtual onlyOwner {
    _transferOwnership(address(0));
}

BVSS
Recommendation

It is recommended that the Owner cannot call renounceOwnership() without first transferring ownership to another address. In addition, if a multi-signature wallet is used, the call to the renounceOwnership() function should be confirmed for two or more users.

Remediation Comment

ACKNOWLEDGED: The Moonwell team acknowledged this finding.

9.5 Inaccurate NatSpec Comments

//

Informational

Description

In the StrategyFactory contract, the @dev comment says Only callable by accounts with the BACKEND_ROLE in the MamoStrategyRegistry, but it is actually allowing the user to call createStrategyForUser() for itself:

/**
  * @notice Creates a new strategy for a specified user
  * @dev Only callable by accounts with the BACKEND_ROLE in the MamoStrategyRegistry
  * @param user The address of the user to create the strategy for
  * @return strategy The address of the newly created strategy
  */
function createStrategyForUser(address user) external returns (address strategy) {
  require(user != address(0), "Invalid user address");
  require(msg.sender == mamoBackend || msg.sender == user, "Only backend or user can create strategy");


Also, in the SlippagePriceChecker.sol file, the @title says PriceChecker, but the actual name of the contract is SlippagePriceChecker:

/**
 * @title PriceChecker
 * @notice Checks swap prices using Chainlink price feeds and applies slippage tolerance
 * @dev Implements the ISlippagePriceChecker interface with UUPS upgradeability
 */
contract SlippagePriceChecker is ISlippagePriceChecker, Initializable, UUPSUpgradeable, OwnableUpgradeable {

BVSS
Recommendation

It is recommended to update the NatSpec comments to accurately reflect the actual implementation.

Remediation Comment

ACKNOWLEDGED: The Moonwell team acknowledged this finding.

9.6 Unused Imports

//

Informational

Description

The following imports are not used and could be removed:

  • In StrategyMulticall.sol:

import {IStrategy} from "./interfaces/IStrategy.sol";

  • In SlippagePriceChecker.sol:

import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";

BVSS
Recommendation

Consider removing any redundant import to simplify the codebase.

Remediation Comment

ACKNOWLEDGED: The Moonwell team acknowledged this finding.

9.7 Use of Revert Strings Instead of Custom Errors

//

Informational

Description

In Solidity smart contract development, replacing hard-coded revert message strings with the Error() syntax is an optimization strategy that can significantly reduce gas costs. Hard-coded strings, stored on the blockchain, increase the size and cost of deploying and executing contracts.


The Error() syntax allows for the definition of reusable, parameterized custom errors, leading to a more efficient use of storage and reduced gas consumption. This approach not only optimizes gas usage during deployment and interaction with the contract but also enhances code maintainability and readability by providing clearer, context-specific error information.

BVSS
Recommendation

It is recommended to replace hard-coded revert strings in require statements for custom errors, which can be done following the logic below:


1. Standard require statement (to be replaced):

require(condition, "Condition not met");

2. Declare the error definition to state:

error ConditionNotMet();

3. As currently is not possible to use custom errors in combination with require statements, the standard syntax is:

if (!condition) revert ConditionNotMet();

More information about this topic in the official Solidity documentation.

Remediation Comment

ACKNOWLEDGED: The Moonwell team acknowledged this finding.

9.8 Redundant Named Return

//

Informational

Description

In the function createStrategyForUser(), a named return variable strategy is declared in the function signature and then it is explicitly (and redundantly) returned:

function createStrategyForUser(address user) external returns (address strategy) {
  ...
  return strategy;
}

BVSS
Recommendation

Consider simplifying the code so it does not explicitly return named returns to avoid redundancies.

Remediation Comment

ACKNOWLEDGED: The Moonwell team acknowledged this finding.

9.9 Style Guide Optimizations

//

Informational

Description

The project codebase contains some stylistic inconsistencies and deviations from Solidity best practices, which, while not directly impacting functionality, reduce code readability, maintainability, and adherence to standard conventions. Addressing these inconsistencies can enhance the clarity and professionalism of the code.

  • Use Underscores or Scientific Notation for Number Literals: Instead of using 10_000 or 1_000, or 1e4 and 1e3, the StrategyFactory contract is using 10000 and 1000 as shown below.

uint256 public constant SPLIT_TOTAL = 10000; // 100% in basis points

uint256 public constant MAX_SLIPPAGE_IN_BPS = 1000; // 10% in basis points

uint256 public constant MAX_COMPOUND_FEE = 1000; // 10% in basis points

require(_splitMToken + _splitVault == SPLIT_TOTAL, "Split parameters must add up to 10000");

  • Internal variables, constants and functions should start with an underscore (_): Notice the internal constant MAX_BPS does not start with an underscore (_) contrary to the style guide.

uint256 internal constant MAX_BPS = 10_000;

  • Interface declaration inside a contract file: The SlippagePriceChecker.sol file is not only declaring the SlippagePriceChecker contract as the style guide recommends, but also an interface named IERC20MetaData. One file per declaration is preferred.

interface IERC20MetaData {
    function decimals() external view returns (uint8);
}

/**
 * @title PriceChecker
 * @notice Checks swap prices using Chainlink price feeds and applies slippage tolerance
 * @dev Implements the ISlippagePriceChecker interface with UUPS upgradeability
 */
contract SlippagePriceChecker is ISlippagePriceChecker, Initializable, UUPSUpgradeable, OwnableUpgradeable {
...

BVSS
Recommendation

Consider implementing the style improvements highlighted above to enhance the readability and consistency of the project.

Remediation Comment

ACKNOWLEDGED: The Moonwell team acknowledged this finding.

10. Automated Testing

Halborn used automated testing techniques to enhance the coverage of certain areas of the smart contracts in scope. Among the tools used was Slither, a Solidity static analysis framework. After Halborn verified the smart contracts in the repository and was able to compile them correctly into their ABIs and binary format, Slither was run against the contracts. This tool can statically verify mathematical relationships between Solidity variables to detect invalid or inconsistent usage of the contracts' APIs across the entire code-base.





All issues identified by Slither were proved to be false positives or have been added to the issue list in this report.

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.

© Halborn 2025. All rights reserved.