Prepared by:
HALBORN
Last Updated 06/11/2024
Date of Engagement by: April 5th, 2022 - June 17th, 2022
83% of all REPORTED Findings have been addressed
All findings
18
Critical
0
High
0
Medium
3
Low
1
Informational
14
Moonwell Finance engaged Halborn to conduct a security audit on their smart contracts beginning on 2022-04-05 and ending on 2022-06-17. The security assessment was scoped to the smart contracts provided to the Halborn team.
The team at Halborn was provided a 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 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.
Dynamic Analysis (ganache-cli
, brownie
, hardhat
).
\begin{enumerate} \item Moonwell Finance Token Sale Contracts \begin{enumerate} \item Repository: \href{https://github.com/moonwell-fi/moonwell-contracts-private/tree/726dcbaef18670d344fa5621c23c4db0e403583a/contracts/tokensale}{Token Sale} \item Commit ID: \href{https://github.com/moonwell-fi/moonwell-contracts-private/tree/726dcbaef18670d344fa5621c23c4db0e403583a/contracts/tokensale}{726dcbaef18670d344fa5621c23c4db0e403583a} \item New PR : \href{ https://github.com/moonwell-fi/moonwell-contracts-private/pull/43 }{PR} \item New Commit : \href{ https://github.com/moonwell-fi/moonwell-contracts-private/tree/9c51e4860c3f768190036ddcc7dbc4ef3d497c1f/contracts/tokensale }{New Commit} \end{enumerate} \item Out-of-Scope \begin{enumerate} \item test/*.sol \end{enumerate} \end{enumerate}
Out-of-scope:
External contract, libraries and financial related attacks.
FIX Commit ID
: 762cdc4cd9a8d09f29765f9e143b25af0ebe9720
TAG :
artemis-v1
Critical
0
High
0
Medium
3
Low
1
Informational
14
Impact x Likelihood
HAL-02
HAL-06
HAL-01
HAL-03
HAL-04
HAL-05
HAL-07
HAL-08
HAL-09
HAL-10
HAL-11
HAL-13
HAL-14
HAL-15
HAL-16
HAL-17
HAL-18
HAL-12
Security analysis | Risk level | Remediation Date |
---|---|---|
OLD TOKENS ARE NOT RECOVERABLE WHEN THE NEW TOKEN IS SET | Medium | Solved - 04/14/2022 |
EXPIRED TOKENS ARE NOT CONSIDERED IN THE VOTING POWER | Medium | Solved - 06/17/2022 |
OWNER CAN RESET ALLOCATIONS - DELEGATIONS | Medium | Risk Accepted |
UNNEEDED INITIALIZATION OF UINT256 VARIABLES TO 0 | Low | Solved - 04/14/2022 |
MISSING ZERO ADDRESS CHECKS | Informational | - |
MISSING EVENTS FOR ADMIN ONLY FUNCTIONS THAT CHANGE CRITICAL PARAMETERS | Informational | - |
USING ++I CONSUMES LESS GAS THAN I+=1 IN LOOPS | Informational | Solved - 04/14/2022 |
CACHING THE LENGTH IN THE FOR LOOPS | Informational | Solved - 04/14/2022 |
REVERT STRING SIZE OPTIMIZATION | Informational | Acknowledged |
MISSING CHECKS FOR NON-ZERO TRANSFER VALUE CALLS | Informational | Solved - 04/14/2022 |
BLOCK WITH GAS LIMIT | Informational | Acknowledged |
EXPERIMENTAL KEYWORD USAGE | Informational | Solved - 04/14/2022 |
UPGRADE AT LEAST PRAGMA 0.8.10 | Informational | Solved - 04/14/2022 |
OPEN TODOS | Informational | Solved - 06/17/2022 |
CHANGING FUNCTION VISIBILITY FROM PUBLIC TO EXTERNAL CAN SAVE GAS | Informational | Solved - 06/17/2022 |
DIRECT USAGE OF ECRECOVER ALLOWS SIGNATURE MALLEABILITY | Informational | Solved - 06/17/2022 |
MISSING EVENTS | Informational | Solved - 06/17/2022 |
OPTIMIZE UNSIGNED INTEGER COMPARISON | Informational | - |
// Medium
The privileged address can set the token. However, when the token is set to another address, the old tokens cannot be retrieved using the contract.
function setTokenAddress(address newTokenAddress) external adminOnly {
require(tokenAddress == address(0), "Address already set");
tokenAddress = newTokenAddress;
}
SOLVED: The Moonwell Team
solved this issue by implementing the withdraw function in the TokenSaleDistributor.sol
contract.
Commit ID:
Commit ID
// Medium
During the code review, It has been observed that expired tokens are not included in the voting power. Although the expired tokens are not used in the contract depends on the protocol behavior that can directly affect voting. The voting power should be carefully designed with expired tokens.
function totalVotingPower(address user) public view returns (uint) {
uint totalAllocatedToUser = totalAllocated(user);
uint totalClaimedByUser = totalClaimed(user);
return totalAllocatedToUser - totalClaimedByUser;
}
SOLVED: The Moonwell Team
solved this issue by deleting expired tokens from the code base.
Commit ID:
Commit ID
// Medium
During the code review, It has been noticed that the owner can delete all delegations from any account.
function resetAllocationsByUser(address[] memory recipients) external adminOnly {
uint length = recipients.length;
for (uint i; i < length; ++i) {
uint votingPower = totalVotingPower(recipients[i]);
_moveDelegates(delegates[recipients[i]], address(0), votingPower);
delete allocations[recipients[i]];
}
}
RISK ACCEPTED: The Moonwell Team
states that they will do this if they found a critical vulnerability after deployment, and they needed to claw back all tokens to prevent them from being stolen. In that case, they will deploy a new contract and re-add the claims as well. On the other hand, The Moonwell Team
commented the feature on the code with the following PR.
Commit ID:
Commit ID
// Low
Since i
is an uint256
, it is already initialized to 0. uint256 i = 0
reassigns the 0 to i
which wastes gas.
TokenSaleDistributor.sol
for (uint i = 0; i < recipients.length; i += 1) {
SOLVED: The Moonwell Team
solved this issue by removing initialization.
Commit ID:
Commit ID
// Informational
// Informational
// Informational
In the loop below, the variable i
is incremented using i+=
. It is known that, in loops, using ++i
costs less gas per iteration than i+=1
.
TokenSaleDistributor.sol::31 => for (uint i; i < allocations[msg.sender].length; i += 1) {
TokenSaleDistributor.sol::59 => for (uint i; i < allocations[recipient].length; i += 1) {
TokenSaleDistributor.sol::71 => for (uint i; i < allocations[recipient].length; i += 1) {
TokenSaleDistributor.sol::83 => for (uint i; i < allocations[recipient].length; i += 1) {
TokenSaleDistributor.sol::95 => for (uint i; i < allocations[recipient].length; i += 1) {
TokenSaleDistributor.sol::107 => for (uint i; i < allocations[recipient].length; i += 1) {
TokenSaleDistributor.sol::157 => for (uint i = 0; i < recipients.length; i += 1) {
TokenSaleDistributor.sol::180 => for (uint i; i < recipients.length; i += 1) {
For example, based on the following test contract:
//SPDX-License-Identifier: MIT
pragma solidity 0.8.9;
contract test {
function postiincrement(uint256 iterations) public {
for (uint256 i = 0; i < iterations; i+=1) {
}
}
function preiincrement(uint256 iterations) public {
for (uint256 i = 0; i < iterations; ++i) {
}
}
}
We can see the difference in gas costs:
SOLVED: The Moonwell Team
solved this issue by following the above recommendation.
Commit ID:
Commit ID
// Informational
The solidity compiler will always read the length of the array during each iteration.
TokenSaleDistributor.sol::31 => for (uint i; i < allocations[msg.sender].length; i += 1) {
TokenSaleDistributor.sol::59 => for (uint i; i < allocations[recipient].length; i += 1) {
TokenSaleDistributor.sol::71 => for (uint i; i < allocations[recipient].length; i += 1) {
TokenSaleDistributor.sol::83 => for (uint i; i < allocations[recipient].length; i += 1) {
TokenSaleDistributor.sol::95 => for (uint i; i < allocations[recipient].length; i += 1) {
TokenSaleDistributor.sol::107 => for (uint i; i < allocations[recipient].length; i += 1) {
TokenSaleDistributor.sol::157 => for (uint i = 0; i < recipients.length; i += 1) {
TokenSaleDistributor.sol::180 => for (uint i; i < recipients.length; i += 1) {
SOLVED: The Moonwell Team
solved this issue by caching arrays.
Commit ID:
Commit ID
// Informational
Shortening the revert strings to fit within 32 bytes will decrease deployment time gas and decrease runtime gas when the revert condition is met.
Revert strings that are longer than 32 bytes require at least one additional mstore
, along with additional overhead to calculate memory offset, etc.
function becomeImplementation(TokenSaleDistributorProxy proxy) external {
require(msg.sender == proxy.admin(), "Only proxy admin can change the implementation");
proxy.acceptPendingImplementation();
}
ACKNOWLEDGED: The Moonwell Team
acknowledged this issue. This is a long-tail gas estimation issue, and the Moonwell Team
would rather have clean error messages than fix gas on a revert the user could have detected.
// Informational
Checking for non-zero transfer values can prevent an external call to save gas.
TokenSaleDistributor.sol
function withdraw(uint amount) external adminOnly {
SOLVED: The Moonwell Team
solved this issue by implementing amount check.
Commit ID:
Commit ID
// Informational
There are several loops in the contract that can eventually grow large enough to make future operations of the contract cost too much gas to fit in one block.
TokenSaleDistributor.sol
TokenSaleDistributor.sol::31 => for (uint i; i < allocations[msg.sender].length; i += 1) {
TokenSaleDistributor.sol::59 => for (uint i; i < allocations[recipient].length; i += 1) {
TokenSaleDistributor.sol::71 => for (uint i; i < allocations[recipient].length; i += 1) {
TokenSaleDistributor.sol::83 => for (uint i; i < allocations[recipient].length; i += 1) {
TokenSaleDistributor.sol::95 => for (uint i; i < allocations[recipient].length; i += 1) {
TokenSaleDistributor.sol::107 => for (uint i; i < allocations[recipient].length; i += 1) {
TokenSaleDistributor.sol::157 => for (uint i = 0; i < recipients.length; i += 1) {
TokenSaleDistributor.sol::180 => for (uint i; i < recipients.length; i += 1) {
ACKNOWLEDGED: The Moonwell Team
acknowledged this issue.
// Informational
ABIEncoderV2 is enabled and using experimental features could be dangerous in live deployments. The experimental ABI encoder does not correctly handle non-integer values less than 32 bytes. This applies to bytesNN types, bool, enum and other types when they are part of an array or a struct and encoded directly from storage. This means these storage references have to be used directly inside abi.encode(...)
as arguments in external function calls or in event data without prior assignment to a local variable. The types bytesNN and bool will result in corrupted data, while enum could cause an invalid revert.
TokenSaleDistributor.sol - Line#4
SOLVED: The Moonwell Team
solved this issue by deleting experimental check.
Commit ID:
Commit ID
// Informational
Additional gas optimizations and security checks are available for free when using newer versions of the compiler and optimizer.
pragma solidity 0.6.12;
SOLVED: The Moonwell Team
solved this issue by updating pragma to 0.8.10.
Commit ID:
Commit ID
// Informational
Open TODOs can point to architecture or programming issues that still need to be resolved.
/// @notice EIP-20 token name for this token
// TODO(lunar): validate this name.
string public constant name = "vWELL";
SOLVED: The Moonwell Team
solved this issue by deleting TODOs check.
Commit ID:
Commit ID
// Informational
There is a function declared as public that is never called internally within the contract. It is best practice to mark such functions as external instead, as this saves gas (especially in the case where the function takes arguments, as external functions can read arguments directly from calldata instead of having to allocate memory).
function delegate(address delegatee) public {
return _delegate(msg.sender, delegatee);
}
SOLVED: The Moonwell Team
solved this issue by marking function as an external.
Commit ID:
Commit ID
// Informational
delegateBySig calls the Solidity ecrecover function directly to verify the given signatures. However, the ecrecover EVM opcode allows malleable (non-unique) signatures and thus is susceptible to replay attacks.
Although a replay attack seems not possible here since the nonce is increased each time, ensuring the signatures are not malleable is considered a best practice.
function delegateBySig(address delegatee, uint nonce, uint expiry, uint8 v, bytes32 r, bytes32 s) public {
bytes32 domainSeparator = keccak256(abi.encode(DOMAIN_TYPEHASH, keccak256(bytes(name)), getChainId(), address(this)));
bytes32 structHash = keccak256(abi.encode(DELEGATION_TYPEHASH, delegatee, nonce, expiry));
bytes32 digest = keccak256(abi.encodePacked("\x19\x01", domainSeparator, structHash));
address signatory = ecrecover(digest, v, r, s);
require(signatory != address(0), "VestingWell::delegateBySig: invalid signature");
require(nonce == nonces[signatory]++, "VestingWell::delegateBySig: invalid nonce");
require(block.timestamp <= expiry, "VestingWell::delegateBySig: signature expired");
return _delegate(signatory, delegatee);
}
SOLVED: The Moonwell Team
solved this issue by changing implementation with ECDSA library.
Commit ID:
Commit ID
// Informational
Owner/governor only functions that change critical parameters should emit events. 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.
function setVotingEnabled(bool enabled) external adminOnly {
votingEnabled = enabled;
}
SOLVED: The Moonwell Team
solved this issue by adding event on the related function.
Commit ID:
Commit ID
// Informational
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 carried out with the Slither tool, some results were obtained and these results were reviewed by Halborn
. Based on the results reviewed, some vulnerabilities were determined to be false positives and these results were not included in the report. The actual vulnerabilities found by Slither are already included in the report findings.
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