Rocket Pool (Houston)

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:

  1. Correctness of the implementation, consistent with the intended functionality and without unintended edge cases.
  2. Identify known vulnerabilities particular to smart contract systems, as outlined in our Smart Contract Best Practices, and the Smart Contract Weakness Classification Registry.
  3. Adherence to the RPIP 33.
  4. 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

The client fixed this issue in commit 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

The client acknowledged this issue, fixed the highlighted discrepancies, and notified us that they will continue reviewing the rest of codebase for inaccuracies

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

The client acknowledged this issue and let us know that this setting was meant to be adjustable via the 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

The client removed redundant comments from contracts that are being upgraded in this release.

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

The client applied the recommended optimisation in commit 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.

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.