Prepared by:
HALBORN
Last Updated 06/11/2024
Date of Engagement by: January 24th, 2022 - February 1st, 2022
88% of all REPORTED Findings have been addressed
All findings
8
Critical
0
High
0
Medium
3
Low
3
Informational
2
Moonwell Finance
engaged Halborn to conduct a security audit on their smart contracts beginning on January 24th, 2022 and ending on February 1st, 2022. The security assessment was scoped to the smart contracts provided to the Halborn team.
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 solved and addressed by the Moonwell Finance
team.
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
)
\begin{enumerate} \item Moonwell Finance Smart Contracts \begin{enumerate} \item Repository: \href{https://github.com/moonwell-fi/contracts-open-source/tree/master/contracts/core}{Moonwell Finance - Moonwell Core} \item Commit ID: \href{https://github.com/moonwell-fi/contracts-open-source/tree/master/contracts/core}{70fb9c4a899ba0cd787855582ea3bd803317f51a} \end{enumerate} \end{enumerate}
FIX Commit ID
: e23657c5fbeb12c7393fa49da6f350dc0bd5114e
TAG
: apollo-v1
Critical
0
High
0
Medium
3
Low
3
Informational
2
Impact x Likelihood
HAL-02
HAL-03
HAL-01
HAL-06
HAL-07
HAL-08
HAL-04
HAL-05
Security analysis | Risk level | Remediation Date |
---|---|---|
ALLOWING ERC777-KIND TOKENS ON PROTOCOL LEADS RE-ENTRANCY | Medium | Solved - 02/04/2022 |
USE OF DEPRECATED CHAINLINK API | Medium | Solved - 02/04/2022 |
ASSETS MAY LOCKED DOWN ON GOVERNORALPHA CONTRACT | Medium | Solved - 02/04/2022 |
SHORT CIRCUIT IS NECESSARY FOR GAS OPTIMIZATION | Low | Not Applicable |
GOVERNORALPHA DOES NOT CONTROL QUEUED PROPOSALS ON CANCEL METHOD | Low | Not Applicable |
MISSING ZERO ADDRESS CHECKS | Low | Risk Accepted |
MULTIPLE PRAGMA DEFINITION | Informational | - |
UNUSED FUNCTION PARAMETERS | Informational | Solved - 02/04/2022 |
// Medium
The ERC777 standard allows the token contract to notify senders and recipients when ERC777 tokens are sent or received from their accounts with function hooks. These hooks are called as callbacks. If the recipient of the token is a smart contract, the smart contract may cause to re-entrancy by calling another transfer function.
During the tests, it was seen that the protocol could be affected by this vulnerability if ERC777-kind tokens are planned to be used. This may cause loss of funds.
/////////////////////////
// EFFECTS & INTERACTIONS
// (No safe failures beyond this point)
/* We write previously calculated values into storage */
totalSupply = vars.totalSupplyNew;
accountTokens[redeemer] = vars.accountTokensNew;
/* We emit a Transfer event, and a Redeem event */
emit Transfer(redeemer, address(this), vars.redeemTokens);
emit Redeem(redeemer, vars.redeemAmount, vars.redeemTokens);
/* We call the defense hook */
comptroller.redeemVerify(address(this), redeemer, vars.redeemAmount, vars.redeemTokens);
/*
* We invoke doTransferOut for the redeemer and the redeemAmount.
* Note: The mToken must handle variations between ERC-20 and GLMR underlying.
* On success, the mToken has redeemAmount less of cash.
* doTransferOut reverts if anything goes wrong, since we can't be sure if side effects occurred.
*/
doTransferOut(redeemer, vars.redeemAmount);
return uint(Error.NO_ERROR);
SOLVED: Moonwell Team
solved this issue by implementing Reentrancy Guard and better check-effect-interaction design to Comptroller.sol
contract.
Commit ID:
e23657c5fbeb12c7393fa49da6f350dc0bd5114e && 762cdc4cd9a8d09f29765f9e143b25af0ebe9720
// Medium
The ChainlinkOracle
contract uses Chainlink’s deprecated API latestAnswer()
. Such functions might suddenly stop working if Chainlink stopped supporting deprecated APIs. This method will return the last value, but it is possible to check if the data is fresh.
function getChainlinkPrice(AggregatorV2V3Interface feed) internal view returns (uint) {
// Chainlink USD-denominated feeds store answers at 8 decimals
uint decimalDelta = uint(18).sub(feed.decimals());
// Ensure that we don't multiply the result by 0
if (decimalDelta > 0) {
return uint(feed.latestAnswer()).mul(10**decimalDelta);
} else {
return uint(feed.latestAnswer());
}
}
SOLVED: This issue was solved by implementing better ChainLink Oracle API call (latestRoundData()
).
Commit ID:
e23657c5fbeb12c7393fa49da6f350dc0bd5114e && 762cdc4cd9a8d09f29765f9e143b25af0ebe9720.
// Medium
Eth sent to Timelock will be locked in current implementation.
function execute(uint proposalId) external {
require(state(proposalId) == ProposalState.Queued, "GovernorAlpha::execute: proposal can only be executed if it is queued");
Proposal storage proposal = proposals[proposalId];
proposal.executed = true;
for (uint i = 0; i < proposal.targets.length; i++) {
timelock.executeTransaction(proposal.targets[i], proposal.values[i], proposal.signatures[i], proposal.calldatas[i], proposal.eta);
}
emit ProposalExecuted(proposalId);
}
await proxy.propose(
[signers[3].address],
[ethers.utils.parseEther("0.1")],
[""],
[ethers.BigNumber.from(0)],
"Send funds to 3rd signer"
);
await proxy.execute(2); // this fails
await proxy.execute(2, {value: ethers.utils.parseEther("0.1")}) // this would work
SOLVED: This issue was solved by removing payable
keyword and call.value()
method from the execute()
function on GovernorAlpha.sol
contract.
Commit ID:
e23657c5fbeb12c7393fa49da6f350dc0bd5114e && 762cdc4cd9a8d09f29765f9e143b25af0ebe9720.
// Low
If votes
variable is equal to zero on GovernorAlpha.sol:_castVote()
method contract should short circuit itself to consume less gas. The following lines will be executed even if votes
amount is zero.
if (support) {
proposal.forVotes = add256(proposal.forVotes, votes);
} else {
proposal.againstVotes = add256(proposal.againstVotes, votes);
}
receipt.hasVoted = true;
receipt.support = support;
receipt.votes = votes;
emit VoteCast(voter, proposalId, support, votes);
}
Basically, the contract will call more functions such as add256()
even it is not necessary.
The same issue also exists on Well.sol:transferFrom()
function. If rawAmount
parameter is equal to zero, the contract should short-circuit itself to prevent gas consume. The following lines will be executed even if rawAmount
is equal to zero.
if (spender != src && spenderAllowance != uint96(-1)) {
uint96 newAllowance = sub96(spenderAllowance, amount, "Well::transferFrom: transfer amount exceeds spender allowance");
allowances[src][spender] = newAllowance;
emit Approval(src, spender, newAllowance);
}
_transferTokens(src, dst, amount);
return true;
GovernorAlpha.sol:_castVote(address voter, uint proposalId, bool support)
Well.sol:transferFrom(address src, address dst, uint rawAmount)
NOT APPLICABLE: This issue was marked as NOT APPLICABLE
since the recommendation does not fit to intended behavior of Compound Protocol. Furthermore, Moonwell Team
stay as close to the original contracts as possible, even if they are not completely optimal concerning gas efficiency, so that improvements to the original contracts may be adopted without significant refactoring, and the community can have better certainty that they function similarly to other contracts with the same code.
// Low
The cancel(uint proposalId)
The cancel function is used to cancel the proposals. There is a check on the contract to do not cancel executed proposals. If the state of a proposal is not QUEUED
yet, the contract will revert to cancel function. However, it will consume gas to achieve this. There is a missing control on the contract to only cancel QUEUED
proposals.
function cancel(uint proposalId) public {
ProposalState state = state(proposalId);
require(state != ProposalState.Executed, "GovernorAlpha::cancel: cannot cancel executed proposal");
Proposal storage proposal = proposals[proposalId];
require(msg.sender == guardian || well.getPriorVotes(proposal.proposer, sub256(block.number, 1)) < proposalThreshold(), "GovernorAlpha::cancel: proposer above threshold");
proposal.canceled = true;
for (uint i = 0; i < proposal.targets.length; i++) {
timelock.cancelTransaction(proposal.targets[i], proposal.values[i], proposal.signatures[i], proposal.calldatas[i], proposal.eta);
}
emit ProposalCanceled(proposalId);
}
NOT APPLICABLE: This issue was marked as NOT APPLICABLE
since the recommendation does not fit to intended behavior of Compound Protocol.
“A proposal is eligible to be cancelled at any time before its execution, including while queued in the Timelock, using this function.”
// Low
Moonwell-Core contracts have multiple input fields on their both public and private functions. Some of these inputs are required as address
variable.
During the test, it has seen all of these inputs are not protected against using the address(0)
as the target address. It is not recommended to use zero address as target addresses on the contracts.
ChainlinkOracle.setAdmin(address).newAdmin (contracts/Chainlink/ChainlinkOracle.sol#88)
Comptroller._setBorrowCapGuardian(address).newBorrowCapGuardian (contracts/Comptroller.sol#968)
Comptroller._setPauseGuardian(address).newPauseGuardian (contracts/Comptroller.sol#986)
Comptroller.setWellAddress(address).newWellAddress (contracts/Comptroller.sol#1351)
MErc20.initialize(address,ComptrollerInterface,InterestRateModel,uint256,string,string,uint8).underlying_ (contracts/MErc20.sol#21)
MToken._setPendingAdmin(address).newPendingAdmin (contracts/MToken.sol#1144)
MErc20Delegator.constructor(address,ComptrollerInterface,InterestRateModel,uint256,string,string,uint8,address,address,bytes).admin_ (contracts/MErc20Delegator.sol#31)
MErc20Delegator._setImplementation(address,bool,bytes).implementation_ (contracts/MErc20Delegator.sol#60)
MErc20Immutable.constructor(address,ComptrollerInterface,InterestRateModel,uint256,string,string,uint8,address).admin_ (contracts/MErc20Immutable.sol#29)
MGlimmer.constructor(ComptrollerInterface,InterestRateModel,uint256,string,string,uint8,address).admin_ (contracts/MGlimmer.sol#27)
Reservoir.constructor(uint256,EIP20Interface,address).target_ (contracts/Reservoir.sol#32)
Timelock.constructor(address,uint256).admin_ (contracts/Timelock.sol#26)
Timelock.executeTransaction(address,uint256,string,bytes,uint256).target (contracts/Timelock.sol#81)
Unitroller._setPendingImplementation(address).newPendingImplementation (contracts/Unitroller.sol#38)
Unitroller._setPendingAdmin(address).newPendingAdmin (contracts/Unitroller.sol#85)
RISK ACCEPTED: The Moonwell Team
accepts the risk of this finding. Except the ChainlinkOracle contract, all the contracts mentioned require the new admin key to execute the _acceptPendingAdmin
function, which protects against accidental attempts to set the admin or guardian to the zero address. It was decided not to make any changes.
// Informational
// Informational
During the test, it was determined that a variable on the contract was not used for any purpose, although it was defined on the contract. This situation does not pose any risk in terms of security. But it is important for the readability and applicability of the code.
The baseRatePerYear
parameter of updateJumpRateModel
function on DAIInterestRateModelV3.sol
contract is unused on that function.
function updateJumpRateModel(uint baseRatePerYear, uint gapPerYear, uint jumpMultiplierPerYear, uint kink_) external {
require(msg.sender == owner, "only the owner may call this function.");
gapPerTimestamp = gapPerYear / timestampsPerYear;
updateJumpRateModelInternal(0, 0, jumpMultiplierPerYear, kink_);
poke();
}
SOLVED: This issue has was solved by the Moonwell Team
. The DAIInterestRateModel.sol
contract is not used by the Moonwell
community, so this contract has been removed from the repository.
Halborn used automated testing techniques to enhance coverage of certain areas of the scoped contract. Among the tools used was Slither, a Solidity static analysis framework. After Halborn verified all the contracts in the repository and was able to compile them correctly into their ABI and binary formats. 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.
As a result of the tests completed with the Slither tool, some results were obtained and these results were reviewed by Halborn
. In line with the reviewed results, it was decided that some vulnerabilities were false-positive and these results were not included in the report. The actual vulnerabilities found by Slither are already included in the findings on the 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
* Use Google Chrome for best results
** Check "Background Graphics" in the print settings if needed