Rocket Storage contract deployment audit

MakerDAO has a proposal to add an rETH oracle. In the proposal, the author links to his personal blog where he has a post entitled Can you get rugged by RocketPool?

In the blog post, he says that it was theoretically possible for Rocket Pool to sneak in another address that could potentially modify the rETH exchange rate.

For example, in this transaction the key is a hash and it is not possible to reverse it to determine what the real value was.

Here is a script that does the following:

  • goes through all blocks since the start and end of the rocketStorage deployment
  • finds all transactions from the deployer/guardian that set values on the contract
  • looks up the unhashed key
  • prints a summary of all function calls, their hashed and unhashed keys
const ethers = require('ethers') // npm install ethers
const { keccak256, toUtf8Bytes, concat } = ethers.utils

;(async function () {
  const provider = new ethers.providers.StaticJsonRpcProvider('http://...') // archive node needed

  // https://etherscan.io/tx/0x5d30e1082de780e5fd231f507ed9809cf17847e4e46be319fe218eb2847cfafa
  const firstBlock = 13325233 // rocketStorage created
  // https://etherscan.io/tx/0x90c7f390f1315e98c1217c57b49399fc7013dd54b6f71635296ecb7fd6546ced
  const lastBlock = 13325559 // setDeployedStatus
  // https://etherscan.io/tx/0x5d30e1082de780e5fd231f507ed9809cf17847e4e46be319fe218eb2847cfafa
  const guardian = '0x0cCF14983364A7735d369879603930Afe10df21e'
  // https://etherscan.io/address/0x1d8f8f00cfa6758d7be78336684788fb0ee0fa46
  const storage = '0x1d8f8f00cfa6758d7bE78336684788Fb0ee0Fa46'

  // https://github.com/rocket-pool/rocketpool/blob/master/contracts/interface/RocketStorageInterface.sol
  // https://github.com/rocket-pool/rocketpool/blob/master/contracts/contract/RocketStorage.sol
  const contract = new ethers.Contract(
    storage,
    [
      'function getAddress(bytes32 key) external view returns (address)',
      'function setAddress(bytes32 key, address value)',
      'function setUint(bytes32 key, uint value)',
      'function setString(bytes32 key, string calldata value)',
      'function setBytes(bytes32 key, bytes calldata value)',
      'function setBool(bytes32 key, bool value)',
      'function setInt(bytes32 key, int value)',
      'function setBytes32(bytes32 key, bytes32 value)',
      'function setDeployedStatus()'
    ],
    provider
  )

  // https://github.com/rocket-pool/rocketpool/blob/master/migrations/2_deploy_contracts.js#L35
  const contracts = [
    'rocketVault',
    'rocketAuctionManager',
    'rocketDepositPool',
    'rocketMinipoolDelegate',
    'rocketMinipoolManager',
    'rocketMinipoolQueue',
    'rocketMinipoolStatus',
    'rocketMinipoolPenalty',
    'rocketNetworkBalances',
    'rocketNetworkFees',
    'rocketNetworkPrices',
    'rocketRewardsPool',
    'rocketClaimDAO',
    'rocketClaimNode',
    'rocketClaimTrustedNode',
    'rocketNodeDeposit',
    'rocketNodeManager',
    'rocketNodeStaking',
    'rocketDAOProposal',
    'rocketDAONodeTrusted',
    'rocketDAONodeTrustedProposals',
    'rocketDAONodeTrustedActions',
    'rocketDAONodeTrustedUpgrade',
    'rocketDAONodeTrustedSettingsMembers',
    'rocketDAONodeTrustedSettingsProposals',
    'rocketDAONodeTrustedSettingsMinipool',
    'rocketDAOProtocol',
    'rocketDAOProtocolProposals',
    'rocketDAOProtocolActions',
    'rocketDAOProtocolSettingsInflation',
    'rocketDAOProtocolSettingsRewards',
    'rocketDAOProtocolSettingsAuction',
    'rocketDAOProtocolSettingsNode',
    'rocketDAOProtocolSettingsNetwork',
    'rocketDAOProtocolSettingsDeposit',
    'rocketDAOProtocolSettingsMinipool',
    'rocketTokenRETH',
    'rocketTokenRPL',
    'addressQueueStorage',
    'addressSetStorage',
    'rocketStorage', // https://github.com/rocket-pool/rocketpool/blob/master/migrations/2_deploy_contracts.js#L32
    'casperDeposit', // https://github.com/rocket-pool/rocketpool/blob/master/migrations/2_deploy_contracts.js#L130
    'rocketMinipool' // https://github.com/rocket-pool/rocketpool/blob/master/migrations/2_deploy_contracts.js#L95
  ]

  const keys = {}
  keys[keccak256(toUtf8Bytes('deploy.block'))] = 'deploy.block' // https://github.com/rocket-pool/rocketpool/blob/master/migrations/2_deploy_contracts.js#L298
  for (const contractName of contracts) {
    // contracts may have been upgraded in the future and their address may have changed since the deployment
    // that's why getting the address at the time of the deployment
    const contractAddress = await contract.getAddress(keccak256(toUtf8Bytes('contract.address' + contractName)), { blockTag: lastBlock })

    // https://github.com/rocket-pool/rocketpool/blob/master/migrations/2_deploy_contracts.js#L193
    keys[keccak256(concat([toUtf8Bytes('contract.exists'), contractAddress]))] = 'contract.exists' + contractAddress
    keys[keccak256(concat([toUtf8Bytes('contract.name'), contractAddress]))] = 'contract.name' + contractAddress
    keys[keccak256(toUtf8Bytes('contract.address' + contractName))] = 'contract.address' + contractName
    keys[keccak256(toUtf8Bytes('contract.abi' + contractName))] = 'contract.abi' + contractName
  }

  const txs = []
  for (let i = firstBlock; i <= lastBlock; i++) {
    const block = await provider.getBlockWithTransactions(i)
    for (const tx of block.transactions) {
      if (tx.from == guardian && tx.to == storage) {
        txs.push({
          blockNumber: block.number,
          hash: tx.hash,
          from: tx.from,
          to: tx.to,
          data: tx.data
        })
      }
    }
  }

  console.log(['tx', 'function', 'key hash', 'key', 'value'].join('\t'))
  for (const tx of txs) {
    try {
      const { name } = contract.interface.getFunction(tx.data.substring(0, 10))
      const { key, value } = contract.interface.decodeFunctionData(name, tx.data)
      if (name == 'setDeployedStatus') {
        console.log([tx.hash, name].join('\t'))
      } else {
        console.log([tx.hash, name, key, keys[key] || '<unknown>', value].join('\t'))
      }
    } catch (e) {
      console.error(e)
    }
  }
})()

You can see the result here: Rocket Storage Audit - Google Sheets

I was able to look up all unhashed keys used in setBool, setAddress, etc. and determine their unhashed values. This means that all these function calls were done by the deployment script and were expected.

There are no function calls with keys that I could not look up which means everything is accounted for and the team did not maliciously give write access to unexpected addresses.

5 Likes

I’ve written a mocha test (sorry) that does the following programmatically:

  1. Grab all contract creation transactions from the guardian from etherscan
  2. Grab the highest nonce for the guardian wallet and generate all possible contract addresses it could have created
  3. Grab all the transactions invoking setBool on the Storage contract
  4. Validate that every call to setBool matches a contract from step 1
  5. Validate that every call to setBool matches an address from step 2
  6. Validate that steps 4 and 5 have the same exact contract address list
  7. Validate setDeployedStatus() was called
  8. Download the source code (from etherscan) for all contracts deployed and compare them the contracts in tag v1.0.0 (in the github repository).

Everything except step 8 passes audit. Step 8 is failing due to 4 contracts.

Three of them do not have source code on etherscan:

0xdc9c66155667578179ed82bd17e725aba9dacd09
0xfc1a4a1eaf9e80fa5380ce45d9d12bdf7a81ca18
0x0444bc6fd57f157406d393570520d0472cafdfc4

One of them has a minor diff between the sourcecode on etherscan and the code in the repository:

0x6a032a901f17227b4db52937fb25f2523a529760

               // Initialize settings on deployment
               if(!getBool(keccak256(abi.encodePacked(settingNameSpace, "deployed")))) {
                   // Apply settings
                   setSettingBool("minipool.submit.withdrawable.enabled", false);
      -            setSettingUint("minipool.launch.timeout", 72 hours);
      +            setSettingUint("minipool.launch.timeout", 5760);
                   setSettingUint("minipool.maximum.count", 14);
                   // Settings initialised
                   setBool(keccak256(abi.encodePacked(settingNameSpace, "deployed")), true);
               }
           }
       
      -    // Update a setting, overrides inherited setting method with extra checks for this contract
      -    function setSettingUint(string memory _settingPath, uint256 _value) override public onlyDAOProtocolProposal {
      -        // Some safety guards for certain settings
      -        if(getBool(keccak256(abi.encodePacked(settingNameSpace, "deployed")))) {
      -            if(keccak256(abi.encodePacked(_settingPath)) == keccak256(abi.encodePacked("minipool.launch.timeout"))) {
      -                RocketDAONodeTrustedSettingsMinipoolInterface rocketDAONodeTrustedSettingsMinipool = RocketDAONodeTrustedSettingsMinipoolInterface(getContractAddress("rocketDAONodeTrustedSettingsMinipool"));
      -                require(_value >= (rocketDAONodeTrustedSettingsMinipool.getScrubPeriod().add(1 hours)), "Launch timeout must be greater than scrub period");
      -            }
      -        }
      -        // Update setting now
      -        setUint(keccak256(abi.encodePacked(settingNameSpace, _settingPath)), _value);
      -    }
      -
           // Balance required to launch minipool
           function getLaunchBalance() override public pure returns (uint256) {
               return 32 ether;
           }
--
           function getSubmitWithdrawableEnabled() override external view returns (bool) {
               return getSettingBool("minipool.submit.withdrawable.enabled");
           }
       
      -    // Timeout period in seconds for prelaunch minipools to launch
      +    // Timeout period in blocks for prelaunch minipools to launch
           function getLaunchTimeout() override external view returns (uint256) {
               return getSettingUint("minipool.launch.timeout");
           }

In order to “fully pass” this automated audit, I think the team should take 2 actions-

  1. Verify the contract source code on etherscan for the 3 missing contracts
  2. Update github to reflect the source code for the contract with the diff

I am given to understand 3 contracts were upgraded to fix an exploit, however, since the old contracts at one point had access to RocketStorage, they should be publicly verified on etherscan.

The code can be found here: GitHub - jshufro/rocketpool_deploy_auditor

5 Likes

@a35u pointed out that deleteBool is called, which led me down a rabbit hole and I discovered that my tx list is missing internal transactions. So I need to figure out a way to grab those, since they also contain calls to setBool, ideally without scanning the whole chain, since at least the block where RocketStorage was created.

Edited: I think it’s fair to say this audit will just validate that all the manual calls to setBool were to assign audited contracts write permissions, and any contract calls to setBool need to be manually audited in the source of those contracts.

Those 3 contracts are the original versions of: rocketMinipoolDelegate, rocketMinipoolManager and rocketNodeDeposit. They were upgraded to add the scrub period functionality just prior to public launch.

I have verified those three contracts on Etherscan per your request. The source for these 3 contracts can be verified against commit 67a64456397dc763b6831539221fdfb172d4335e.

EDIT: Thanks for doing this audit!

4 Likes

Thanks for updating the source and providing a commit reference. The code now successfully verifies the new and old versions of those 3 contracts.

In order to get the new versions, I’m scraping calls to bootstrapUpgrade for new contract addresses, which introduces the Old RPL contract. This contract doesn’t seem to exist in the github repo but I think we can also just write it off as out of scope.

However, 0x6a032a901f17227b4db52937fb25f2523a529760 still has a small diff in both versions (v1.0.0 and 67a644). Is there a commit I can reference for that contract’s code, as seen by etherscan?

All contracts (modulo old rpl) are now verified by the script. Thanks Kane.

5 Likes

Updated Summary of the script:

  1. Grab all contract creation transactions from the guardian from etherscan
  2. Grab the highest nonce for the guardian wallet and generate all possible contract addresses it could have created
  3. Grab all the transactions invoking setBool on the Storage contract
  4. Validate that every call to setBool matches a contract from step 1
  5. Validate that every call to setBool matches an address from step 2
  6. Validate that steps 4 and 5 have the same exact contract address list
  7. Validate setDeployedStatus() was called
  8. Grab all calls to the bootstrapUpgrade function in RocketDaoNodeTrusted contract and add the new contracts to the list to be verified, except the old RPL token contract (which doesn’t seem to be available in the git repo, but is trivially auditable by hand)
  9. Download the source code (from etherscan) for all contracts deployed and compare them the contracts in tag v1.0.0 and v1.0.0-pre (in the github repository).
4 Likes