Prepared by:
HALBORN
Last Updated 03/18/2025
Date of Engagement: February 3rd, 2025 - February 5th, 2025
100% of all REPORTED Findings have been addressed
All findings
2
Critical
0
High
0
Medium
0
Low
1
Informational
1
Moonwell
engaged Halborn
to conduct a security assessment on their smart contracts beginning on February 3rd, 2025 and ending on February 5th. The security assessment was scoped to the smart contracts provided in the moonwell-fi/moonwell-contracts-v2 GitHub repository. Commit hash and further details can be found in the Scope section of this report.
Halborn
was provided three 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 purpose of the assessment is 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 acknowledged and accepted by the Moonwell team
. The main ones are the following:
Implement validation checks for the returned oracle data when using the AggregatorV3Interface
. Ensure that the latest returned timestamp is within the defined heartbeat interval for the requested asset.
Add an explicit check in the initiateSale()
function to ensure that _miniAuctionPeriod > 1
.
Create an access-controlled function, using SafeERC20
in order to allow the owner to withdraw unsold tokens after the sale.
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
).
EXPLOITABILITY METRIC () | METRIC VALUE | NUMERICAL 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 |
IMPACT METRIC () | METRIC VALUE | NUMERICAL 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 |
SEVERITY COEFFICIENT () | COEFFICIENT VALUE | NUMERICAL VALUE |
---|---|---|
Reversibility () | None (R:N) Partial (R:P) Full (R:F) | 1 0.5 0.25 |
Scope () | Changed (S:C) Unchanged (S:U) | 1.25 1 |
Severity | Score Value Range |
---|---|
Critical | 9 - 10 |
High | 7 - 8.9 |
Medium | 4.5 - 6.9 |
Low | 2 - 4.4 |
Informational | 0 - 1.9 |
Critical
0
High
0
Medium
0
Low
1
Informational
1
Security analysis | Risk level | Remediation Date |
---|---|---|
Missing oracle data staleness check | Low | Risk Accepted - 02/12/2025 |
Division by zero | Informational | Acknowledged - 02/12/2025 |
// Low
In the ReserveAutomation
contract, the getPriceAndDecimals()
function checks that the returned price is positive and that the round is valid, using answeredInRound >= roundId
and updatedAt != 0
, but it does not check that the price data is recent. In some applications, using stale oracle data is a risk.
/// @notice helper function to retrieve price from chainlink
/// @param oracleAddress The address of the chainlink oracle
/// returns the price and then the decimals of the given asset
/// reverts if price is 0 or if the oracle data is invalid
function getPriceAndDecimals(
address oracleAddress
) public view returns (int256, uint8) {
(
uint80 roundId,
int256 price,
,
uint256 updatedAt,
uint80 answeredInRound
) = AggregatorV3Interface(oracleAddress).latestRoundData();
bool valid = price > 0 && answeredInRound >= roundId && updatedAt != 0;
require(valid, "ReserveAutomationModule: Oracle data is invalid");
uint8 oracleDecimals = AggregatorV3Interface(oracleAddress).decimals();
return (price, oracleDecimals); /// price always gt 0 at this point
}
The getPriceAndDecimals()
function does not adequately handle cases where the oracle returns the latest timestamp (updatedAt
) outside the defined heartbeat interval for the requested asset.
Specifically, when using the AggregatorV3Interface
from Chainlink, it is essential to validate the updatedAt
timestamp returned by the latestRoundData
function to ensure it is within acceptable ranges.
Implement validation checks for the returned oracle data when using the AggregatorV3Interface
. Ensure that the latest returned timestamp is within the defined heartbeat interval for the requested asset.
RISK ACCEPTED: The Moonwell team has accepted the risk related to this finding.
// Informational
In the ReserveAutomation
contract, the discount (or premium) is computed in the function currentDiscount()
, as follows:
/// @notice Calculates the current discount or premium rate for reserve purchases
/// @return The current discount as a percentage scaled to 1e18, returns
/// 1e18 if no discount is applied
/// @dev Does not apply discount or premium if the sale is not active
function currentDiscount() public view returns (uint256) {
if (!isSaleActive()) {
return SCALAR;
}
uint256 decayDelta = startingPremium - maxDiscount;
uint256 periodStart = getCurrentPeriodStartTime();
uint256 periodEnd = getCurrentPeriodEndTime();
uint256 saleTimeRemaining = periodEnd - block.timestamp;
/// calculate the current premium or discount at the current time based
/// on the length you are into the current period
return
maxDiscount +
(decayDelta * saleTimeRemaining) /
(periodEnd - periodStart);
}
The value for periodStart
is obtained from the return of the getCurrentPeriodEndTime()
function, defined as follows:
/// @notice Returns the end time of the current mini auction period
/// @return The timestamp when the current mini auction period ends
/// @dev Returns 0 if no sale is active or if the sale hasn't started yet
/// @dev Each period is exactly miniAuctionPeriod in length
function getCurrentPeriodEndTime() public view returns (uint256) {
uint256 startTime = getCurrentPeriodStartTime();
if (startTime == 0) {
return 0;
}
return startTime + miniAuctionPeriod - 1;
}
In other words, the denominator becomes periodEnd - periodStart = miniAuctionPeriod - 1
. If the miniAuctionPeriod
is set to 1
- which is possible, because the condition in the require
statement of the initiateSale()
function is as follows: _auctionPeriod / _miniAuctionPeriod > 1
.
In extremely rare conditions, where _miniAuctionPeriod
is 1
and _auctionPeriod
is 2
, then the denominator is zero and the contract will revert with a division-by-zero error.
It is recommended to add an explicit check in the initiateSale()
function to ensure that _miniAuctionPeriod > 1
. For example:
require(_miniAuctionPeriod > 1, "ReserveAutomation: Mini auction period too short");
Alternatively, update the NatSpec documentation to provide proper information regarding the mini auction period.
ACKNOWLEDGED: The Moonwell team has acknowledged this issue.
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.
// Download the full report
Moonwell // SCA (Reserve + ERC20HoldingDeposit)
* Use Google Chrome for best results
** Check "Background Graphics" in the print settings if needed