Platform: Code4rena
Start Date: 03/03/2023
Pot Size: $90,500 USDC
Total HM: 4
Participants: 42
Period: 7 days
Judge: 0xean
Total Solo HM: 2
Id: 219
League: ETH
Rank: 30/42
Findings: 1
Award: $72.43
๐ Selected for report: 0
๐ Solo Findings: 0
๐ Selected for report: rbserver
Also found by: 0x6980, 0xAgro, 0xSmartContract, 0xmichalis, 0xnev, BRONZEDISC, DevABDee, IceBear, RaymondFam, Rolezn, SaeedAlipoor01988, Sathish9098, arialblack14, brgltd, chrisdior4, codeislight, descharre, imare, lukris02, luxartvinsec, matrix_0wl, tnevler, yongskiws
72.4344 USDC - $72.43
The TokenFactory
contract creates governance tokens and may be vulnerable to re-entrancy attacks through the createToken()
function, which can allow an attacker to re-enter the function and repeatedly execute malicious code. The vulnerability exists because the MerkleMinter.mintFor()
function, which is called within the createToken()
function, can call back into the TokenFactory
contract and execute code within the context of the initial function call.
An attacker could exploit this vulnerability to cause the function to become stuck in an infinite loop, drain tokens or Ether from the contract, or cause other unintended consequences. Additionally, if the createToken()
function is called by another contract, the attacker could potentially exploit the vulnerability to take control of the calling contract.
To mitigate this vulnerability, the createToken()
function should be modified to prevent re-entrancy attacks. One solution would be to use the OpenZeppelin ReentrancyGuard
contract, which provides protection against re-entrancy attacks by using a mutex lock to prevent a function from being called again before the previous call has completed. Alternatively, the MerkleMinter.mintFor()
function could be modified to prevent any re-entry into the TokenFactory
contract, such as by using the OpenZeppelin nonReentrant
modifier.
InterfaceBasedRegistry
The InterfaceBasedRegistry
contract has a vulnerability that can allow an attacker to register a contract that does not fully support the required ERC165 interface, by exploiting a missing check on the contract's ERC165 support. This can lead to issues with the reliability and security of the registry, as well as the contracts that depend on it.
An attacker can register a contract with the InterfaceBasedRegistry
contract without it having full support for the required ERC165 interface, as the code does not explicitly check for it. This can cause issues for contracts that depend on this registry, as they will not receive the expected functionality from the registered contract. This vulnerability can also result in false positives, where the registry indicates that a contract is supported when it actually is not.
To mitigate this vulnerability, a check for full support of the required ERC165 interface should be added in the _register
function. This can be done by adding a check for the ERC165 support of the contract before checking for the target interface support. If the contract does not support ERC165, it should revert with the ContractERC165SupportInvalid
error. This will ensure that the registry only registers contracts that fully support the required ERC165 and target interface.
Here is an example of the modified _register
function that includes the recommended check:
function _register(address _registrant) internal { if (entries[_registrant]) { revert ContractAlreadyRegistered({registrant: _registrant}); } // Check for ERC165 support first if (!_registrant.supportsInterface(type(IERC165).interfaceId)) { revert ContractERC165SupportInvalid(_registrant); } // Check for target interface support if (!_registrant.supportsInterface(targetInterfaceId)) { revert ContractInterfaceInvalid(_registrant); } entries[_registrant] = true; }
isSubdomainValid
function due to missing visibility and return typeThe isSubdomainValid
function in the provided code is missing a visibility modifier and a return type. The lack of a visibility modifier means that the function can be accessed by any contract, and the missing return type could result in unexpected behavior or errors. Additionally, the lack of input validation could lead to vulnerabilities.
An attacker could call the isSubdomainValid
function from a contract that has access to sensitive information or functions, potentially resulting in a loss of funds or unauthorized access. The lack of a return type could lead to errors in contract execution, while the lack of input validation could allow attackers to pass invalid or malicious inputs to the function.
To address this issue, the function should have a visibility modifier and a return type specified. Additionally, input validation should be added to ensure that only valid inputs are accepted. For example, the function could be updated as follows:
// SPDX-License-Identifier: AGPL-3.0-or-later pragma solidity 0.8.17; /// @notice Validates that a subdomain name is composed only from characters in the allowed character set: /// - the lowercase letters `a-z` /// - the digits `0-9` /// - the hyphen `-` /// @dev This function allows empty (zero-length) subdomains. If this should not be allowed, make sure to add a respective check when using this function in your code. /// @param subDomain The name of the DAO. /// @return `true` if the name is valid or `false` if at least one char is invalid. function isSubdomainValid(string calldata subDomain) public pure returns (bool) { require(bytes(subDomain).length > 0, "Subdomain name cannot be empty"); bytes calldata nameBytes = bytes(subDomain); uint256 nameLength = nameBytes.length; for (uint256 i; i < nameLength; i++) { uint8 char = uint8(nameBytes[i]); // if char is between a-z if (char > 96 && char < 123) { continue; } // if char is between 0-9 if (char > 47 && char < 58) { continue; } // if char is - if (char == 45) { continue; } // invalid if one char doesn't work with the rules above return false; } return true; }
In this updated version, the function is marked as public
to ensure that it can only be called by authorized contracts, and a require
statement is added to validate that the input is not empty. The function also specifies a return type of bool
to ensure consistent behavior, and the comments have been updated to reflect these changes.
MajorityVotingBase
contract allows a proposal to be executed before the start dateMajorityVotingBase
contract allows the execution of proposals before the start date. The vulnerability can allow attackers to manipulate the voting outcome and execute proposals before the scheduled time, thus harming the system's integrity.
An attacker can manipulate the voting outcome and execute proposals before the scheduled time, thus harming the system's integrity.
The contract should include a check that prohibits proposals from being executed before the start date. The execute
function should check whether the current block timestamp is greater than or equal to the proposal's start date.
function execute(uint256 _proposalId) public virtual { if (block.timestamp < proposals[_proposalId].parameters.startDate) { revert DateOutOfBounds({ limit: proposals[_proposalId].parameters.startDate, actual: block.timestamp }); } if (!_canExecute(_proposalId)) { revert ProposalExecutionForbidden(_proposalId); } _execute(_proposalId); }
isSupportThresholdReached
functionThe function isSupportThresholdReached
in MajorityVotingBase
contract may suffer from integer underflow when computing the expression (RATIO_BASE - proposal_.parameters.supportThreshold)
.
The integer underflow can cause unexpected behavior when checking the support threshold of a proposal, which may lead to proposals being executed even when they do not meet the required support threshold. An attacker could potentially exploit this vulnerability to execute a malicious proposal.
It is recommended to add a check for the input parameter proposal_.parameters.supportThreshold
to ensure it is not greater than RATIO_BASE
, which is 10^18
. This can be done by adding the following require statement at the beginning of the isSupportThresholdReached
function:
function isSupportThresholdReached(uint256 _proposalId) public view virtual returns (bool) { Proposal storage proposal_ = proposals[_proposalId]; require(proposal_.parameters.supportThreshold <= RATIO_BASE, "Invalid support threshold"); return (RATIO_BASE - proposal_.parameters.supportThreshold) * proposal_.tally.yes > proposal_.parameters.supportThreshold * proposal_.tally.no; }
The AddresslistVoting
contract is potentially vulnerable to reentrancy attacks in the createProposal
function, as it is not implementing proper checks to prevent reentrancy attacks.
A malicious attacker could exploit this vulnerability to call the createProposal
function multiple times in a single transaction, executing malicious code that could lead to unexpected behavior such as stealing funds from the contract or locking it up.
To prevent reentrancy attacks, I recommend using the nonReentrant
modifier provided by OpenZeppelin's ReentrancyGuard.sol
library in the createProposal
function. The nonReentrant
modifier ensures that the function can only be called once at a time, preventing reentrancy attacks.
import "@openzeppelin/contracts/security/ReentrancyGuard.sol"; contract AddresslistVoting is IMembership, Addresslist, MajorityVotingBase, ReentrancyGuard { ... function createProposal( bytes calldata _metadata, IDAO.Action[] calldata _actions, uint256 _allowFailureMap, uint64 _startDate, uint64 _endDate, VoteOption _voteOption, bool _tryEarlyExecution ) external override nonReentrant returns (uint256 proposalId) { ... } }
Replace this code with:
contract TokenVoting is MajorityVotingBase, IMembership, IMajorityVoting { ...ยจ }
The DAOFactory contract is used to create new DAOs and install plugins on them. There is a possible re-entrancy vulnerability in the createDao
function of the contract. An attacker may call a malicious contract that could execute code during the execution of the createDao
function, potentially causing the contract to behave unexpectedly.
An attacker exploiting this vulnerability could perform a variety of attacks depending on the contract they call during the re-entrancy attack. They may be able to modify the DAO's internal state, steal funds or execute other malicious code. The impact of the attack is dependent on the nature of the contract the attacker calls.
To prevent re-entrancy attacks, the DAOFactory contract should follow the checks-effects-interactions pattern, i.e., ensure that all state changes are made before calling external contracts. Additionally, all functions that can be called externally should be marked as nonReentrant
to prevent re-entrancy attacks. One way to do this is to use the ReentrancyGuard
library from the OpenZeppelin contracts library.
import "@openzeppelin/contracts/security/ReentrancyGuard.sol"; contract DAOFactory is ReentrancyGuard { ... function createDao( DAOSettings calldata _daoSettings, PluginSettings[] calldata _pluginSettings ) external nonReentrant returns (DAO createdDao) { ... } }
The CounterV1
and CounterV2
contract has a missing access control vulnerability where anyone can call the execute()
function without requiring any permission or authentication. This can lead to unauthorized execution of DAO functions, allowing attackers to potentially disrupt the entire system.
An attacker can call the execute()
function without proper authentication, allowing them to execute arbitrary code on the DAO. Depending on the permissions granted to the execute()
function, this could result in the attacker gaining full control of the DAO, including the ability to transfer funds, change contract settings, and execute other critical functions. This can cause significant financial losses and reputational damage to the DAO and its users.
The DAO should implement proper access control mechanisms to restrict access to critical functions like execute()
. This can be achieved by implementing a permission system that requires specific roles or permissions to be granted to an address before allowing them to call the function.
The MerkleDistributor
contract has an incorrect ordering of functions, which could make the contract less readable and harder to maintain. Specifically, the isClaimed
and _setClaimed
functions are defined after the claim function, even though they are related to the validity of the claim and the state of the contract.
The incorrect ordering of functions may not affect the functionality of the MerkleDistributor
contract, but it could lead to confusion and difficulty in understanding the code. This could make it harder to maintain and debug the contract in the future.
To improve the readability and maintainability of the MerkleDistributor contract, it is recommended that the isClaimed
and _setClaimed
functions be defined before the claim
function. While this is not strictly necessary, it is a good practice to organize the code in a logical and readable way. Additionally, it is important to ensure that all functions are defined before they are used, regardless of their order in the code.
#0 - c4-judge
2023-03-12T16:33:40Z
0xean marked the issue as grade-b
#1 - novaknole20
2023-03-22T10:04:35Z
1: I cannot reproduce this. Please give a POC.
2: The InterfaceBasedRegistry is used by higher level contracts and they only allow registration from trusted source where we know that it implements the interface.
3: Free functions cannot have a visibility
4:
Idk but I see an if in your description that checks if block.timestamp
is smaller than the start date and reverts.
5: We use solidity > 0.8.0 and thus this reverts if it over- or underflows.
6: Where?
7: Some more infos?
8: Where?
9: ???
10: ???
#2 - c4-sponsor
2023-03-22T10:04:41Z
novaknole20 marked the issue as sponsor disputed