1 Executive Summary
This report presents the results of our engagement with Rocket Pool to review the protocol DAO and its voting and challenge mechanisms.
The review was conducted over two weeks, from 20.11.2023 to 01.12.2023, by Dominik Muhs and Valentin Quelquejay. A total of 2x10 person-days were spent.
Due to the time-boxed nature of this assessment and the overall complexity and size of the upgrade, the scope was reduced by the development team to prioritize high-risk components and objectives. It was agreed to conduct this review on a best-effort basis, prioritizing the focus areas. We strongly recommend a follow-up engagement with an enhanced time frame to account for areas of concern that this review could not cover.
2 Scope
Our review focused on the commit hash f26996f0afc1f276254da8104471b64643a8b671
. The list of files in scope can be found in the Appendix.
2.1 Objectives
Together with the Rocket Pool team, we identified the following priorities for our review:
- Correctness of the implementation, consistent with the intended functionality and without unintended edge cases.
- Identify known vulnerabilities particular to smart contract systems, as outlined in our Smart Contract Best Practices, and the Smart Contract Weakness Classification Registry.
- Adherence to the RPIP 33.
- Specifically the voting system’s security, snapshotting, and proposal challenges, referring to the protocol DAO specification.
3 System Overview
4 Findings
Each issue has an assigned severity:
- Minor issues are subjective in nature. They are typically suggestions around best practices or readability. Code maintainers should use their own judgment as to whether to address such issues.
- Medium issues are objective in nature but are not security vulnerabilities. These should be addressed unless there is a clear reason not to.
- Major issues are security vulnerabilities that may not be directly exploitable or may require certain conditions in order to be exploited. All major issues should be addressed.
- Critical issues are directly exploitable security vulnerabilities that need to be fixed.
4.1 Missing Events on Important State Changes Medium ✓ Fixed
Resolution
The client implemented a fix in commit 1be41a88a40125baf58d8904770cd9eb9e0732bb
and provided the following statement:
- RocketDAONodeTrusted is not a contract that is getting upgrade so this won’t be fixed
- RocketDAOProtocol has been updated to include events for each bootstrap function
- RocketNetworkVoting has been updated to emit an event
- RocketDAOSecurityProposals has been updated to emit events for all proposals
Description
Throughout the code base, various important settings-related state changes are not surfaced by events.
In RocketDAONodeTrusted
:
contracts/contract/dao/node/RocketDAONodeTrusted.sol:L149-L165
function bootstrapMember(string memory _id, string memory _url, address _nodeAddress) override external onlyGuardian onlyBootstrapMode onlyRegisteredNode(_nodeAddress) onlyLatestContract("rocketDAONodeTrusted", address(this)) {
// Ok good to go, lets add them
RocketDAONodeTrustedProposalsInterface(getContractAddress("rocketDAONodeTrustedProposals")).proposalInvite(_id, _url, _nodeAddress);
}
// Bootstrap mode - Uint Setting
function bootstrapSettingUint(string memory _settingContractName, string memory _settingPath, uint256 _value) override external onlyGuardian onlyBootstrapMode onlyLatestContract("rocketDAONodeTrusted", address(this)) {
// Ok good to go, lets update the settings
RocketDAONodeTrustedProposalsInterface(getContractAddress("rocketDAONodeTrustedProposals")).proposalSettingUint(_settingContractName, _settingPath, _value);
}
// Bootstrap mode - Bool Setting
function bootstrapSettingBool(string memory _settingContractName, string memory _settingPath, bool _value) override external onlyGuardian onlyBootstrapMode onlyLatestContract("rocketDAONodeTrusted", address(this)) {
// Ok good to go, lets update the settings
RocketDAONodeTrustedProposalsInterface(getContractAddress("rocketDAONodeTrustedProposals")).proposalSettingBool(_settingContractName, _settingPath, _value);
}
In RocketDAOProtocol
:
contracts/contract/dao/protocol/RocketDAOProtocol.sol:L42-L51
function bootstrapSettingMulti(string[] memory _settingContractNames, string[] memory _settingPaths, SettingType[] memory _types, bytes[] memory _values) override external onlyGuardian onlyBootstrapMode onlyLatestContract("rocketDAOProtocol", address(this)) {
// Ok good to go, lets update the settings
RocketDAOProtocolProposalsInterface(getContractAddress("rocketDAOProtocolProposals")).proposalSettingMulti(_settingContractNames, _settingPaths, _types, _values);
}
/// @notice Bootstrap mode - Uint Setting
function bootstrapSettingUint(string memory _settingContractName, string memory _settingPath, uint256 _value) override external onlyGuardian onlyBootstrapMode onlyLatestContract("rocketDAOProtocol", address(this)) {
// Ok good to go, lets update the settings
RocketDAOProtocolProposalsInterface(getContractAddress("rocketDAOProtocolProposals")).proposalSettingUint(_settingContractName, _settingPath, _value);
}
Treasury address setter:
contracts/contract/dao/protocol/RocketDAOProtocol.sol:L77-L79
function bootstrapTreasuryNewContract(string memory _contractName, address _recipientAddress, uint256 _amountPerPeriod, uint256 _periodLength, uint256 _startTime, uint256 _numPeriods) override external onlyGuardian onlyBootstrapMode onlyLatestContract("rocketDAOProtocol", address(this)) {
RocketDAOProtocolProposalsInterface(getContractAddress("rocketDAOProtocolProposals")).proposalTreasuryNewContract(_contractName, _recipientAddress, _amountPerPeriod, _periodLength, _startTime, _numPeriods);
}
Bootstrap mode management:
contracts/contract/dao/protocol/RocketDAOProtocol.sol:L97-L100
function bootstrapDisable(bool _confirmDisableBootstrapMode) override external onlyGuardian onlyBootstrapMode onlyLatestContract("rocketDAOProtocol", address(this)) {
require(_confirmDisableBootstrapMode == true, "You must confirm disabling bootstrap mode, it can only be done once!");
setBool(keccak256(abi.encodePacked(daoNameSpace, "bootstrapmode.disabled")), true);
}
One-time treasury spends:
contracts/contract/dao/protocol/RocketDAOProtocol.sol:L72-L74
function bootstrapSpendTreasury(string memory _invoiceID, address _recipientAddress, uint256 _amount) override external onlyGuardian onlyBootstrapMode onlyLatestContract("rocketDAOProtocol", address(this)) {
RocketDAOProtocolProposalsInterface(getContractAddress("rocketDAOProtocolProposals")).proposalTreasuryOneTimeSpend(_invoiceID, _recipientAddress, _amount);
}
In RocketNetworkVoting.sol
:
contracts/contract/network/RocketNetworkVoting.sol:L122
function setDelegate(address _newDelegate) external override onlyRegisteredNode(msg.sender) {
In RocketDAOSecurityProposals.sol
:
contracts/contract/dao/security/RocketDAOSecurityProposals.sol:L98-L99
function proposalSettingUint(string memory _settingNameSpace, string memory _settingPath, uint256 _value) override public onlyExecutingContracts() onlyValidSetting(_settingNameSpace, _settingPath) {
bytes32 namespace = keccak256(abi.encodePacked(protocolDaoSettingNamespace, _settingNameSpace));
contracts/contract/dao/security/RocketDAOSecurityProposals.sol:L107-L108
function proposalSettingBool(string memory _settingNameSpace, string memory _settingPath, bool _value) override public onlyExecutingContracts() onlyValidSetting(_settingNameSpace, _settingPath) {
bytes32 namespace = keccak256(abi.encodePacked(protocolDaoSettingNamespace, _settingNameSpace));
contracts/contract/dao/security/RocketDAOSecurityProposals.sol:L116-L117
function proposalSettingAddress(string memory _settingNameSpace, string memory _settingPath, address _value) override public onlyExecutingContracts() onlyValidSetting(_settingNameSpace, _settingPath) {
bytes32 namespace = keccak256(abi.encodePacked(protocolDaoSettingNamespace, _settingNameSpace));
contracts/contract/dao/security/RocketDAOSecurityProposals.sol:L126-L127
function proposalInvite(string calldata _id, address _memberAddress) override public onlyLatestContract("rocketDAOProtocolProposals", msg.sender) {
// Their proposal executed, record the block
Recommendation
We recommend emitting events on state changes, particularly when these are performed by an authorized party. The implementation of the recommendation should be analogous to the handling of events on state changes in the rest of the system, such as in the RocketMinipoolPenalty
contract:
contracts/contract/minipool/RocketMinipoolPenalty.sol:L28-L33
function setMaxPenaltyRate(uint256 _rate) external override onlyGuardian {
// Update rate
maxPenaltyRate = _rate;
// Emit event
emit MaxPenaltyRateUpdated(_rate, block.timestamp);
}
4.2 RocketDAOProtocolProposal._propose()
Should Revert if _blockNumber > block.number
Medium ✓ Fixed
Resolution
c60c1d292a81eb83c4c766425303f31c1d74901e
Description
Currently, the RocketDAOProtocolProposal._propose()
function does not account for scenarios where _blockNumber
is greater than block.number
. This is a critical oversight, as voting power cannot be determined for future block numbers.
contracts/contract/dao/protocol/RocketDAOProtocolProposal.sol:L351
function _propose(string memory _proposalMessage, uint256 _blockNumber, uint256 _totalVotingPower, bytes calldata _payload) internal returns (uint256) {
Recommendation
We recommend updating the function to revert on transactions where _blockNumber
exceeds block.number
. This will prevent the creation of proposals with undefined voting power and maintain the integrity of the voting process.
4.3 Unused Parameter and Improper Parameter Sanitization in RocketNetworkVoting.calculateVotingPower()
Minor ✓ Fixed
Resolution
The client fixed the issue in commit aff5be87c2bc6fd4966be743cf8370fb43fac917
and provided the following statement:
matchedETH
was left over from previous design, removed.- Added assertion for block number
- The upgrade script ensures there is at least 1 snapshot of the RPL price
Description
The matchedETH
parameter in RocketNetworkVoting.calculateVotingPower()
is unused.
contracts/contract/network/RocketNetworkVoting.sol:L110-L111
// Get contracts
RocketDAOProtocolSettingsNodeInterface rocketDAOProtocolSettingsNode = RocketDAOProtocolSettingsNodeInterface(getContractAddress("rocketDAOProtocolSettingsNode"));
Additionally, the _block
parameter is not sanitized. Thus, if calling the function with a block number _block
where _block >= block.number
, the call will revert because of a division-by-zero error. Indeed, rocketNetworkSnapshots.lookupRecent
will return a rplPrice
of zero since the checkpoint does not exist. Consequently, the function calculateVotingPower
will revert when computing the maximumStake
.
contracts/contract/network/RocketNetworkVoting.sol:L102-L105
key = keccak256(abi.encodePacked("rpl.staked.node.amount", _nodeAddress));
uint256 rplStake = uint256(rocketNetworkSnapshots.lookupRecent(key, uint32(_block), 5));
return calculateVotingPower(rplStake, ethMatched, ethProvided, rplPrice);
contracts/contract/network/RocketNetworkVoting.sol:L114-L114
uint256 maximumStake = providedETH * maximumStakePercent / rplPrice;
Recommendation
We recommend removing the unused parameter to enhance code clarity. The presence of unused parameters can lead to potential confusion for future developers. Additionally, we recommend ensuring that the snapshotted rplPrice
value exists before it is used to compute the maximumStake
value.
4.4 Wrong/Misleading NatSpec Documentation Minor ✓ Fixed
Resolution
Description
The NatSpec documentation in several parts of the code base contains inaccuracies or is misleading. This issue can lead to misunderstandings about how the code functions, especially for developers who rely on these comments for clarity and guidance.
Examples
In RocketDAOProtocolProposal
, the NatSpec comments are potentially misleading:
contracts/contract/dao/protocol/RocketDAOProtocolProposal.sol:L269-L270
/// @notice Get the votes against count of this proposal
/// @param _proposalID The ID of the proposal to query
contracts/contract/dao/protocol/RocketDAOProtocolProposal.sol:L282-L287
/// @notice Returns true if this proposal was supported by this node
/// @param _proposalID The ID of the proposal to query
/// @param _nodeAddress The node operator address to query
function getReceiptDirection(uint256 _proposalID, address _nodeAddress) override public view returns (VoteDirection) {
return VoteDirection(getUint(keccak256(abi.encodePacked(daoProposalNameSpace, "receipt.direction", _proposalID, _nodeAddress))));
}
In RocketDAOProtocolVerifier, the NatSpec documentation is incomplete, which might leave out critical information about the function’s purpose and behavior:
contracts/contract/dao/protocol/RocketDAOProtocolVerifier.sol:L133-L135
/// @notice Used by a verifier to challenge a specific index of a proposal's voting power tree
/// @param _proposalID The ID of the proposal being challenged
/// @param _index The global index of the node being challenged
Recommendation
The NatSpec documentation should be thoroughly reviewed and corrected where necessary. We recommend ensuring it accurately reflects the code’s functionality and provides complete information.
4.5 RocketDAOProtocolSettingsRewards.setSettingRewardClaimPeriods()
Cannot Be Invoked Minor ✓ Fixed
Resolution
setSettingUint()
function. Consequently, the redundant setter has been removed in commit 01897ca410ed2ef18f21818f68bb1d73af4fbe69
.
Description
The setSettingRewardClaimPeriods()
function in RocketDAOProtocolSettingsRewards.sol
currently serves no practical purpose as it cannot be invoked. This limitation arises because the only contract permitted to call this function is RocketDAOProtocolProposals
, which does not expose this specific functionality. While the setting can still be altered using the proposalSettingUint
setter in RocketDAOProtocolProposal
, it is assumed that the setSettingRewardClaimPeriods
function was intended for added clarity and ease of use.
contracts/contract/dao/protocol/settings/RocketDAOProtocolSettingsRewards.sol:L46
setUint(keccak256(abi.encodePacked(settingNameSpace, "rewards.claims", "periods")), _periods);
Recommendation
To make this function useful and align it with its intended purpose, we recommend integrating its functionality into RocketDAOProtocolProposals
. In addition, we recommend that this function emit an event upon successful change of settings, enhancing the transparency of the operation.
4.6 Redundant Comments ✓ Fixed
Resolution
Description
Throughout the code base, there are various redundant and duplicated comments. The following instances, and most likely a few more, can safely be removed to increase readability.
In RocketDaoProtocol
:
contracts/contract/dao/protocol/RocketDAOProtocol.sol:L43
// Ok good to go, lets update the settings
contracts/contract/dao/protocol/RocketDAOProtocol.sol:L49
// Ok good to go, lets update the settings
contracts/contract/dao/protocol/RocketDAOProtocol.sol:L55
// Ok good to go, lets update the settings
contracts/contract/dao/protocol/RocketDAOProtocol.sol:L61
// Ok good to go, lets update the settings
In RocketDAONodeTrusted*
:
contracts/contract/dao/node/RocketDAONodeTrusted.sol:L42
// Construct
contracts/contract/dao/node/RocketDAONodeTrusted.sol:L157
// Ok good to go, lets update the settings
contracts/contract/dao/node/RocketDAONodeTrusted.sol:L163
// Ok good to go, lets update the settings
contracts/contract/dao/node/RocketDAONodeTrusted.sol:L170
// Ok good to go, lets update the settings
contracts/contract/dao/node/RocketDAONodeTrusted.sol:L186
// Ok good to go, lets update the settings
contracts/contract/dao/node/RocketDAONodeTrustedActions.sol:L150-L151
// Log it
emit ActionLeave(msg.sender, rplBondRefundAmount, block.timestamp);
4.7 Use calldata
Storage Location Instead of memory
for Large Read-Only Data ✓ Fixed
Resolution
1cf8ff58c9e241663fd21a0e310009ce9898b92d
Description
The current implementation stores data, such as the pollard node array in RocketDAOProtocolVerifier
, in memory. Given this data structure’s size and read-only nature, using memory can be less efficient regarding gas usage.
contracts/contract/dao/protocol/RocketDAOProtocolVerifier.sol:L65-L66
function submitProposalRoot(uint256 _proposalID, address _proposer, uint32 _blockNumber, Types.Node[] memory _treeNodes) external onlyLatestContract("rocketDAOProtocolProposal", msg.sender) onlyLatestContract("rocketDAOProtocolVerifier", address(this)) {
// Retrieve the node count at _blockNumber
contracts/contract/dao/protocol/RocketDAOProtocolVerifier.sol:L513-L514
function computeRootFromNodes(Types.Node[] memory _nodes) internal pure returns (Types.Node memory) {
uint256 len = _nodes.length / 2;
Recommendation
Consider modifying the storage location of such large read-only data arrays from memory
to calldata
. This will save gas as calldata is a cheaper storage location for data that does not need modification. This optimization is particularly relevant for contracts that frequently interact with large arrays or data structures.
Appendix 1 - Fuzzing
While reviewing the code, we identified the “RocketNetworkSnapshot” contract as a good fit for fuzzing. As part of our pro-bono efforts, we created a fuzzing harness and wrote several Scribble properties for the “RocketNetworkSnapshot” contract to serve as a proof of concept. We then conducted a 24-hour fuzzing campaign using Diligence Fuzzing to enhance our confidence in the contract’s reliability.
It’s worth noting that the Scribble properties provided are not exhaustive. As an example, here are some of the properties we used to instrument the RocketNetworkSnapshot
contract:
function push(bytes32 _key, uint32 _block, uint224 _value)
:
/// #if_succeeds "push increases length by at most 1" length(_key) - old(length(_key)) <= 1;
/// #if_succeeds "latest retrieves latest pushed value" let exists,b,val := latest(_key) in exists && b == _block && _value == val;
/// #if_succeeds "latestBlock retrieves latest pushed block" latestBlock(_key) == _block;
/// #if_succeeds "latest value retrieves latest pushed value" latestValue(_key) == _value;
function latest(bytes32 _key) external view
:
/// #if_succeeds "latest and latestBlock match" let exists,b,v := latest(_key) in latestBlock(_key) == b && latestValue(_key) == v;
function length(bytes32 _key) public view
:
/// #if_succeeds "snapshot length cannot decrease" res - old(length(_key)) >= 0;
Appendix 2 - Files in Scope
This audit covered the following files:
File | SHA-1 hash |
---|---|
contracts/contract/dao/protocol/RocketDAOProtocol.sol |
e7dad65fa7ab9221955bc5e6d7bb1ddcbbb2ac6e |
contracts/contract/dao/protocol/RocketDAOProtocolActions.sol |
0efb8d201f346d8279ab65a4bd0d0ce6d6010fb5 |
contracts/contract/dao/protocol/RocketDAOProtocolProposal.sol |
a51ca9959b61b57b01c74728a56f2b0defa904b4 |
contracts/contract/dao/protocol/RocketDAOProtocolProposals.sol |
e845b8205e4c8897d9048ccabce4bbbe2016a7d0 |
contracts/contract/dao/protocol/RocketDAOProtocolVerifier.sol |
f9f7f3ac4d3143a2772c60580e412e27c44882fd |
contracts/contract/dao/protocol/settings/RocketDAOProtocolSettings.sol |
27ddd34a410ac0912bfb69ed69289b9ebd682f21 |
contracts/contract/dao/protocol/settings/RocketDAOProtocolSettingsAuction.sol |
070c65a08bb64cb1b7d7db9dc6fbd289edc39476 |
contracts/contract/dao/protocol/settings/RocketDAOProtocolSettingsDeposit.sol |
fbb55b500ef3ebcf0f08b36f5de9820de51844da |
contracts/contract/dao/protocol/settings/RocketDAOProtocolSettingsInflation.sol |
ed057678580f3a00a9eeda9e6dd1e2352938211d |
contracts/contract/dao/protocol/settings/RocketDAOProtocolSettingsMinipool.sol |
143915d11a952e4bce221f5bc0874630480056d1 |
contracts/contract/dao/protocol/settings/RocketDAOProtocolSettingsNetwork.sol |
3a53029fdd7d60c0795f3dc2f40364e57a210d92 |
contracts/contract/dao/protocol/settings/RocketDAOProtocolSettingsNode.sol |
63e6ead093f6801361bb30433cda500b4d533abf |
contracts/contract/dao/protocol/settings/RocketDAOProtocolSettingsProposals.sol |
f0626ba4f3294e8efe9b98648a709c9a508787fb |
contracts/contract/dao/protocol/settings/RocketDAOProtocolSettingsRewards.sol |
053cdc20457d8cec963c99bfd6f9842fb430802d |
contracts/contract/dao/protocol/settings/RocketDAOProtocolSettingsSecurity.sol |
f59214bb3ba2ec37e3822b2b82dd217a8fcafd5c |
contracts/contract/dao/RocketDAOProposal.sol |
8318682eab33e6087027380dc7cefd0131e1ca5f |
contracts/contract/dao/security/RocketDAOSecurity.sol |
1b41136a4812125573e3c3bca83eff33555951e9 |
contracts/contract/dao/security/RocketDAOSecurityActions.sol |
1ed12943b14f6d2f0770a2fdbcde3ee46548eab5 |
contracts/contract/dao/security/RocketDAOSecurityProposals.sol |
b1c5e1d45fce7b770a0737a6274b7b576cd71f3f |
contracts/contract/network/RocketNetworkPrices.sol |
5fd57c6bb18d8ec305b8334e5b4409685392d44f |
contracts/contract/network/RocketNetworkSnapshots.sol |
af0eb239041401678fe3ec502dfd1d9293dc5c93 |
contracts/contract/network/RocketNetworkVoting.sol |
03b3c56190ea18260bfd9149e7afd9cbf5370597 |
Appendix 3 - Disclosure
Consensys Diligence (“CD”) typically receives compensation from one or more clients (the “Clients”) for performing the analysis contained in these reports (the “Reports”). The Reports may be distributed through other means, including via Consensys publications and other distributions.
The Reports are not an endorsement or indictment of any particular project or team, and the Reports do not guarantee the security of any particular project. This Report does not consider, and should not be interpreted as considering or having any bearing on, the potential economics of a token, token sale or any other product, service or other asset. Cryptographic tokens are emergent technologies and carry with them high levels of technical risk and uncertainty. No Report provides any warranty or representation to any third party in any respect, including regarding the bug-free nature of code, the business model or proprietors of any such business model, and the legal compliance of any such business. No third party should rely on the Reports in any way, including for the purpose of making any decisions to buy or sell any token, product, service or other asset. Specifically, for the avoidance of doubt, this Report does not constitute investment advice, is not intended to be relied upon as investment advice, is not an endorsement of this project or team, and it is not a guarantee as to the absolute security of the project. CD owes no duty to any third party by virtue of publishing these Reports.
A.3.1 Purpose of Reports
The Reports and the analysis described therein are created solely for Clients and published with their consent. The scope of our review is limited to a review of code and only the code we note as being within the scope of our review within this report. Any Solidity code itself presents unique and unquantifiable risks as the Solidity language itself remains under development and is subject to unknown risks and flaws. The review does not extend to the compiler layer, or any other areas beyond specified code that could present security risks. Cryptographic tokens are emergent technologies and carry with them high levels of technical risk and uncertainty. In some instances, we may perform penetration testing or infrastructure assessments depending on the scope of the particular engagement.
CD makes the Reports available to parties other than the Clients (i.e., “third parties”) on its website. CD hopes that by making these analyses publicly available, it can help the blockchain ecosystem develop technical best practices in this rapidly evolving area of innovation.
A.3.2 Links to Other Web Sites from This Web Site
You may, through hypertext or other computer links, gain access to web sites operated by persons other than Consensys and CD. Such hyperlinks are provided for your reference and convenience only, and are the exclusive responsibility of such web sites’ owners. You agree that Consensys and CD are not responsible for the content or operation of such Web sites, and that Consensys and CD shall have no liability to you or any other person or entity for the use of third party Web sites. Except as described below, a hyperlink from this web Site to another web site does not imply or mean that Consensys and CD endorses the content on that Web site or the operator or operations of that site. You are solely responsible for determining the extent to which you may use any content at any other web sites to which you link from the Reports. Consensys and CD assumes no responsibility for the use of third-party software on the Web Site and shall have no liability whatsoever to any person or entity for the accuracy or completeness of any outcome generated by such software.
A.3.3 Timeliness of Content
The content contained in the Reports is current as of the date appearing on the Report and is subject to change without notice unless indicated otherwise, by Consensys and CD.