Prepared by:
HALBORN
Last Updated 04/26/2024
Date of Engagement by: February 7th, 2023 - February 24th, 2023
100% of all REPORTED Findings have been addressed
All findings
6
Critical
0
High
0
Medium
2
Low
2
Informational
2
Aragon engaged Halborn to conduct a security audit on their smart contracts beginning on 2023-02-07 and ending on 2023-02-24. The security assessment was scoped to the smart contracts provided to the Halborn team.
The team at Halborn was provided four weeks 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 mostly addressed by the Aragon team
.
Halborn performed a combination of manual and automated security testing to balance efficiency, timeliness, practicality, and accuracy in regard to the scope of this audit. While manual testing is recommended to uncover flaws in logic, process, and implementation; automated testing techniques help enhance coverage of the bridge code 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 walk-through
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
)
Local deployment (Hardhat
, Remix IDE
, Brownie
)
The security assessment was scoped to the following smart contracts:
core/dao/DAO.sol
core/dao/IDAO.sol
core/dao/IEIP4824.sol
core/permission/IPermissionCondition.sol
core/permission/PermissionLib.sol
core/permission/PermissionManager.sol
core/plugin/IPlugin.sol
core/plugin/Plugin.sol
core/plugin/PluginCloneable.sol
core/plugin/PluginUUPSUpgradeable.sol
core/plugin/dao-authorizable/DaoAuthorizable.sol
core/plugin/dao-authorizable/DaoAuthorizableUpgradeable.sol
core/plugin/membership/IMembership.sol
core/plugin/membership/IMembershipContract.sol
core/plugin/proposal/Proposal.sol
core/plugin/proposal/ProposalBase.sol
core/plugin/proposal/ProposalUpgradeable.sol
core/utils/BitMap.sol
core/utils/CallbackHandler.sol
core/utils/auth.sol
framework/dao/DAOFactory.sol
framework/dao/DAORegistry.sol
framework/plugin/repo/IPluginRepo.sol
framework/plugin/repo/PluginRepo.sol
framework/plugin/repo/PluginRepoFactory.sol
framework/plugin/repo/PluginRepoRegistry.sol
framework/plugin/setup/IPluginSetup.sol
framework/plugin/setup/PluginSetup.sol
framework/plugin/setup/PluginSetupProcessor.sol
framework/plugin/setup/PluginSetupProcessorHelpers.sol
framework/utils/InterfaceBasedRegistry.sol
framework/utils/RegistryUtils.sol
framework/utils/TokenFactory.sol
framework/utils/ens/ENSMigration.sol
framework/utils/ens/ENSSubdomainRegistrar.sol
plugins/counter-example/MultiplyHelper.sol
plugins/counter-example/v1/CounterV1.sol
plugins/counter-example/v1/CounterV1PluginSetup.sol
plugins/counter-example/v2/CounterV2.sol
plugins/counter-example/v2/CounterV2PluginSetup.sol
plugins/governance/admin/Admin.sol
plugins/governance/admin/AdminSetup.sol
plugins/governance/majority-voting/IMajorityVoting.sol
plugins/governance/majority-voting/MajorityVotingBase.sol
plugins/governance/majority-voting/addresslist/AddresslistVoting.sol
plugins/governance/majority-voting/addresslist/AddresslistVotingSetup.sol
plugins/governance/majority-voting/token/TokenVoting.sol
plugins/governance/majority-voting/token/TokenVotingSetup.sol
plugins/governance/multisig/Multisig.sol
plugins/governance/multisig/MultisigSetup.sol
plugins/token/IMerkleDistributor.sol
plugins/token/IMerkleMinter.sol
plugins/token/MerkleDistributor.sol
plugins/token/MerkleMinter.sol
plugins/utils/Addresslist.sol
plugins/utils/Ratio.sol
token/ERC20/IERC20MintableUpgradeable.sol
token/ERC20/governance/GovernanceERC20.sol
token/ERC20/governance/GovernanceWrappedERC20.sol
token/ERC20/governance/IGovernanceWrappedERC20.sol
utils/Proxy.sol
utils/UncheckedMath.sol
Branch: 85f8b31e1b6dd012542cf3430b6985030ab3b44b Pull request: 257 Pull request: 266
Remediation Plan:
Branch: cb0621dc5185a73240a6ca33fccc7698f059fdf5
OUT-OF-SCOPE: Other smart contracts in the repository, external libraries and economical attacks.
Critical
0
High
0
Medium
2
Low
2
Informational
2
Impact x Likelihood
HAL-01
HAL-02
HAL-04
HAL-05
HAL-06
HAL-03
Security analysis | Risk level | Remediation Date |
---|---|---|
SELF PERMISSIONS DO CASCADE TO EXTERNAL CONTRACTS | Medium | Solved - 02/27/2023 |
UNTRUSTED PLUGIN USAGE CAN CAUSE DOS | Medium | Solved - 02/27/2023 |
MERKLE ROOT RE-USAGE | Low | Not Applicable |
EMPTY SUBDOMAIN ALLOWED | Low | Solved - 02/27/2023 |
MISSING CALLBACK SELECTOR ZERO CASE | Informational | Acknowledged |
UNUSED CODE | Informational | Solved - 02/27/2023 |
// Medium
The _auth
function on the PermissionManager
is being used to verify if a given permission (permission id) is set to a contract (where) from a specific origin (from). The check does verify in two different scenarios:
where
contract does have the permission from the current sender.address(this)
) does hold the permission from the current sender.This means that if a given permission (PERMx) is set to the self
contract, any call to any contract (anywhere) that may use the permission (PERMX) will succeed due to the second check. It has been seen that the entire code base is always using a where of address(this)
which means that the where
check is redundant and unused and adds a possible critical scenario described below.
function _auth(address _where, bytes32 _permissionId) private view {
if (
!isGranted(address(this), msg.sender, _permissionId, msg.data) &&
!isGranted(_where, msg.sender, _permissionId, msg.data)
) {
revert Unauthorized({
here: address(this),
where: _where,
who: msg.sender,
permissionId: _permissionId
});
}
}
SOLVED: The Aragon team
did change all auth(where, permission)
modifiers to use auth(permission)
which explicitly uses the DAO authorization system and does not rely on the Permissionmanager
function.
// Medium
Installing or upgrading a plugin without prior validation can lead to the contract and DAO being nonfunctional. This is possible if a plugin does self-destruct during the installation or upgrade. Furthermore, not only caution should be taken during the installation/upgradability but also on the deployed implementation contract as it would lead all proxied contracts to be unusable.
SOLVED: The Aragon team
added _disableInitializers
and stated that no action could happen due to a bad implementation initialization or take-over.
// Low
The merkleMint
function on the MerkleMinter
contract does allow specifying a previous used _merkleRoot
. The merkle proofs do not consider the token being used as part of the proof. This allows the same proof, with the same merkle root, to be used for any token. This means that the created MerkleDistributor
will successfully mint the tokens independently of the internal token used for a valid proof. This scenario is unlikely to happen, but bots could be constantly scanning the blockchain for such scenario and profit from it.
NOT APPLICABLE: The Aragon team
stated that this is considered a feature rather than a potential issue, as this would allow multiple tokens to be generated for the same address list without having to modify the root.
// Low
The registerPluginRepo
function on the PluginRepoRegistry
contract does allow specifying an empty string as a subdomain, which will result in the c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470
hash node. This is allowed due to the registering function and the isSubdomainValid
internal function considering an empty string as a valid subdomain.
SOLVED: The code now reverts if the subdomain string is empty before the isSubdomainValid
check.
// Informational
The _registerCallback
function on the CallbackHandler
contract does allow registering the _callbackSelector
of 0x00000000
which refers to the Solidity EVM fallback or receive functions. The callBackSelector
is expected to be equal to _magicNumber
in most of the cases. However, it is possible that _magicNumber
is different from callBackSelector
. For the former, when they are equal, registering the _magicNumber/callBackSelector
of 0x00000000
will cause any call not handled by the EVM, such as data=0x
to revert even after register. However, the _handleCallback
, does treat the 0x0
(_magicNubmer
) as UNREGISTERED_CALLBACK
which will cause the transaction to revert.
ACKNOWLEDGED: The Aragon team
and Halborn agreed on the low severity of this issue and stated that reverting unhandled selectors to default is a good option, as it would force the developers to explicitly add the selector if it is required.
// Informational
The _applyRatioFloored
is never being used in the code. Furthermore, as the function name states, the result will be floored and the decimal precision lost. It is recommended to either remove the code or take extra precaution when using the function, as unexpected results may occur.
SOLVED: The Aragon team
did remove the unused code.
ROOT
permissions or protected permission to ANY_ADDR
is restricted correctly.ANY_ADDR
. However, it should be stated that revoking a strictly specified where/from permission with ANY_ADDR
will not revoke the permission. ANY_ADDR
on the revoke function cannot be treated as a "clean all" permissions.isPermissionRestrictedForAnyAddr
.DAORegistry
.preparedSetupIdToBlockNumber
internally to store the current state. All the states are differentiated against them by the action, such as install, upgrade and uninstall. This is important; otherwise, a prepare statement will be applicable in any action independently of the prepare action. Meaning that a prepareUninstall
could be used on the applyUpdate
state, causing bad behaviours and unexpected results.prepareUpdate
does not verify that the repo is registered, as it is assumed to be if installed by checking currentAppliedSetupId
.keccak256
of the subdomain as specified in the ENS documentation.Addresslist
implementation, which uses the CheckpointsUpgradeable
contract from OZ.ERC20Voting
implementation, which uses snapshot mechanisms to store the balance for a given address back in time.Halborn used automated testing techniques to enhance the coverage of certain areas of the smart contract in scope. Among the tools used was Slither, a Solidity static analysis framework. After Halborn verified the smart contract in the repository and was able to compile it correctly into its ABI and binary format, Slither was run against the contract. 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.
Several tools were executed, including Mythx
and Slither. All the reported issues were verified and, if applicable, reported in previous sections. Slither reported several issues regarding uninitialized variables, which were false positives as inheritance was in place. Furthermore, an issue regarding the possibility to self-destruct the DAO contract due to not blocking the initializer was also a false positive as upgradeTo
can only be called through a delegatecall.
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