oDAO Membership Interest - peteris

Houston upgrade contract verification

Checking whether the source code published on GitHub matches what’s deployed on-chain.

This is similar to my verification of the Atlas upgrade above. Previous posts have more details how this works.

Payload

You can see the decoded payload of the proposal on Rocketscan:

Or you can verify it yourself:

new ethers.utils.Interface(['function proposalUpgrade(string action, string contractName, string contractABI, address contractAddress)']).decodeFunctionData('proposalUpgrade', '0xdfc970ef...')

You should see:

  • Contract name: rocketUpgradeOneDotThree
  • Contract address: 0x5dC69083B68CDb5c9ca492A0A5eC581e529fb73C
  • Contract ABI: eJzNmktT6zYUgP9KJ...

The contract ABI is compressed with zlib and base64-encoded. Here is a script to decode the ABI. You can also run it in your browser on replit.

Script to decode the payload and contract ABI
const ethers = require('ethers') // npm install ethers@5
const pako = require('pako') // npm install pako@2

// proposal payload
// https://etherscan.io/tx/0x2495669bf2479617ff09f1399a632893f8c866b4701152c73de80457d0f644d8
const payload = '0xdfc970ef000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000001000000000000000000000000005dc69083b68cdb5c9ca492a0a5ec581e529fb73c000000000000000000000000000000000000000000000000000000000000000b616464436f6e74726163740000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000018726f636b6574557067726164654f6e65446f745468726565000000000000000000000000000000000000000000000000000000000000000000000000000005ec654a7a4e6d6b7454367a59556750394b4a327357656c6d57324e477975517461426a7033777a4364492b6d4965676832786c59754d42332b657855496b4a64774343774f4779427972505070303175362b6d2f53744c4e35476962485634732f452f59745450392b6e4f486b654f4b374e765867303238586e622f46644a6d36486d3777782b4b684342346e52354d57376859502f744f76506e4153516f2f446b4a5054793374672b6348543964466b534a4477624a37414e644d6d50656255746d746e3841687569752f6679446b50715a2f372f4d4c3834644463744a446d2f57624b3039464b394e6476306541442b6e6c61764b3262702f66552f584b4f3839616e706d7333736d55506d69764e6261564763673172325734567165753636587535765766372f506e7538766e563450334f2b4a364c41746f304f6334466770767853673753654750583439304b614f6e6d366e7246356c33544e724d633046496b6271764d6a7a2b396f38666d34587a75627646782b4f597964796f366c4b6f716c666b4e706f76644e65384442664432304a61466a79767167534b3859594c586f6c687870706e676d38737447686d59636642526c6e5271716f2b5357635a464b646f57373138732f7a47463575373035433961666b46706939626833754766754f5a6a67747a424e65334e4c6f426c79726647487a305970316d78667237466e304d2f373776552b65654b5163694171577a4e685169664953416d775972676e654c6d4d776a353936776259457173763473616f324268764433735171476d52536c58617a50654e61327758474a4b4f5a44685a50366142794535576d714e6750774c514d515549526f6a2b5237643777366955387931726b6d3046456c666f5176716f44713342434b6d79506759515074506a54437652442f614f415636375968704c7869724436703162306a454e506c4b316b62702b68436d732b5753675a596c626f4c68746446664953496d7151374257426b4f3673442f7848546639626530484d6b614c57426758774169706f685a69596857486b54554261546c78334270745966444b6c796d495362486f7563412f4b434f2b774c766f512f55707476427155714c672f7773675967706b6842426f66616a524766593330377874466b453465617036382b676156736b4e70396a584f59664e69366f68454e4d5437537874685562373935655239417a614f454765317057514b756f5654552b793936674943616a43707858544932336c6556512b54744d6f66564972412f7a466c576555593933796873557847516f58546b6c3676454a39424c6a76472f4971596773324e7a374672654f647a4d514536455a7841726a48684235676b4a79453041784a5775687872633056776949535967475a47334c6878427243435448694e793352736578655079306934435942494f4d4f53664739355558434a634a626c39694979516867736a4c6544362b506c7768494361686a6c59734e766848455a627a38584e7947796d4951666d41376a4d4578435141696871344b626145766e516d516373456b3834623459743171596842545965794c415264334a7262346e68647378493939624a7363516863766834797a6b4e4d6b45636a4e4d54695a4c594964496c2b33692b794a755648686f7037575234495233474936524756744e7a79596f2b3878664d542b795932314b5a594e576855556861583455554d596a7071785373657974734a62787730577764494651446a2b4b68437444557746434338324c2f34543537665332774d4163597247634b6e4b596a4a6b4a3472363872543353304d6f6b4d363077364439635531654a6d446d4244764a546451336f4875567a64304c6c755944663932695a674d5755564157593155716b304759694b453549493556567a4c726b483837424b3578626a78576d756f523261356177444546466a4c596f7838593932783137566f2b504136394645426250305672686b322b64627655673862783164667677786347565a687247334a32532f73683948374e66503876396e6c364358686538383056417771564c6d565850385070754f3357673d3d0000000000000000000000000000000000000000'

// https://github.com/rocket-pool/rocketpool/blob/master/contracts/interface/dao/node/RocketDAONodeTrustedProposalsInterface.sol
const iface = new ethers.utils.Interface([
  'function proposalInvite(string id, string url, address nodeAddress)',
  'function proposalLeave(address nodeAddress)',
  'function proposalKick(address nodeAddress, uint256 rplFine)',
  'function proposalSettingUint(string settingContractName, string settingPath, uint256 value)',
  'function proposalSettingBool(string settingContractName, string settingPath, bool value)',
  'function proposalUpgrade(string type, string name, string contractABI, address contractAddress)'
])

const result = iface.decodeFunctionData(iface.getFunction(payload.substring(0, 10)), payload)

console.log(Object.fromEntries(Object.entries(result).filter(([key]) => isNaN(key)))) // filter out duplicate unnamed args

if (result.type == 'addContract' || result.type == 'upgradeContract') {
  const abi = JSON.parse(pako.inflate(Buffer.from(result.contractABI, 'base64'), { to: 'string' }))
  //console.log(abi) // json format
  console.log(new ethers.utils.Interface(abi).format(ethers.utils.FormatTypes.Full))
}

Source code

Checking whether the source code published on Etherscan matches what’s on GitHub.

Manual

I will not do it manually this time since I am already comparing the bytecode in the next section.

Team script

But I ran the team’s verification tool.

Instructions
$ git clone https://github.com/rocket-pool/verify-1.3.git
$ cd verify-1.3/

$ git log --oneline | head -n1
74c8023 Fix submodule tag

$ cp .env.example .env
$ cat .env
ETH_RPC=http://xxx
NETWORK=mainnet
ETHERSCAN_API_KEY=xxx

# change .gitmodules to clone over HTTPS not SSH
$ cat .gitmodules
[submodule "rocketpool"]
	path = rocketpool
	url = https://github.com/rocket-pool/rocketpool.git
$ git submodule sync

Here is the output:

$ docker run --rm -it -v $(pwd):/app -w /app node:20 sh -c 'npm install && ./verify.sh'
...
Cloning into '/app/rocketpool'...
Submodule path 'rocketpool': checked out 'a08da9639b8a1619c06f6ec314e36b4765b9452c'
...
added 998 packages, and audited 999 packages in 21s
...
✔️Verified contract at 0x5dC69083B68CDb5c9ca492A0A5eC581e529fb73C matches RocketUpgradeOneDotThree
✔️Verified contract at 0x7603352f1C4752Ac07AAC94e48632b65FDF1D35c matches RocketNetworkSnapshots
✔️Verified contract at 0xA9d27E1952f742d659143a544d3e535fFf3Eebe1 matches RocketNetworkVoting
✔️Verified contract at 0x5f24E4a1A1f134a5a6952A9965721E6344898497 matches RocketDAOProtocolSettingsProposals
✔️Verified contract at 0x25F41Cd11d95DBEC0919A0440343698cf1472a33 matches RocketDAOProtocolVerifier
✔️Verified contract at 0x84aE6D61Df5c6ba7196b5C76Bcb112B8a689aD37 matches RocketDAOSecurity
✔️Verified contract at 0xeaa442dF4Bb5394c66C8024eFb4979bEc89Eb59a matches RocketDAOSecurityActions
✔️Verified contract at 0x6004Fa90a27dB9971aDD200d1A3BB34444db9Fb7 matches RocketDAOSecurityProposals
✔️Verified contract at 0x1ec364CDD9697F56B8CB17a745B98C2b862CBE29 matches RocketDAOProtocolSettingsSecurity
✔️Verified contract at 0x7cee91F49001B08f8D562d58510C76bcEcD61FA0 matches RocketDAOProtocolProposal
✔️Verified contract at 0x1b714ed0ce30A8BeDC5b4253DaAa08c84CA5BFcb matches RocketDAOProtocol
✔️Verified contract at 0x6D736da1dC2562DBeA9998385A0A27d8c2B2793e matches RocketDAOProtocolProposals
✔️Verified contract at 0x25E54Bf48369b8FB25bB79d3a3Ff7F3BA448E382 matches RocketNetworkPrices
✔️Verified contract at 0x9304B4ebFbE68932Cf9Af8De4d21D7e7621f701a matches RocketNodeDeposit
✔️Verified contract at 0x2b52479F6ea009907e46fc43e91064D1b92Fdc86 matches RocketNodeManager
✔️Verified contract at 0x0e29BA1155cE103A07118c8912dA44B0507A982D matches RocketNodeStaking
✔️Verified contract at 0xFe6Db0ce3F61a4aE04c0A3E62F775a6f511C9aaC matches RocketClaimDAO
✔️Verified contract at 0x8857610Ba0A7caFD4dBE1120bfF03E9c74fc4124 matches RocketDAOProtocolSettingsRewards
✔️Verified contract at 0x09fbCE43e4021a3F69C4599FF00362b83edA501E matches RocketMinipoolManager
✔️Verified contract at 0xEE4d2A71cF479e0D3d0c3c2C923dbfEB57E73111 matches RocketRewardsPool
✔️Verified contract at 0x6Cc65bF618F55ce2433f9D8d827Fc44117D81399 matches RocketNetworkBalances
✔️Verified contract at 0x89682e5F9bf69C909FC5E21a06495ac35E3671Ab matches RocketDAOProtocolSettingsNetwork
✔️Verified contract at 0xEF75e83633E686D3085b3a988b937D021e2fA628 matches RocketDAOProtocolSettingsAuction
✔️Verified contract at 0xD846AA34caEf083DC4797d75096F60b6E08B7418 matches RocketDAOProtocolSettingsDeposit
✔️Verified contract at 0x1d4AAEaE7C8b75a8e5ab589a84516853DBDdd735 matches RocketDAOProtocolSettingsInflation
✔️Verified contract at 0xA416A7a07925d60F794E20532bc730749611A220 matches RocketDAOProtocolSettingsMinipool
✔️Verified contract at 0x448DA008c7EB2501165c9Aa62DfFEeC4405bC660 matches RocketDAOProtocolSettingsNode
✔️Verified contract at 0x5cE71E603B138F7e65029Cc1918C0566ed0dBD4B matches RocketMerkleDistributorMainnet
✔ Verification successful

a08da96 is the commit with the v1.3.0 tag:

image

Bytecode

This verifies whether the bytecode of the contracts on-chain matches the bytecode produced by compiling the source code from GitHub.

Script

Get the source code and compile the contracts:

git clone https://github.com/rocket-pool/rocketpool.git
cd rocketpool
git checkout a08da96
npm install
npm run compile

Install dependencies for the verification script:

npm install --no-save ethers@5 glob@10 ganache-cli

Fork mainnet and impersonate Rocket Pool’s deployer address:

npx ganache-cli --fork https://cloudflare-eth.com --unlock 0x27e80db1f5a975f4c43c5ec163114e796cdb603d --gasPrice 1

Create a file called verify.js:

const fs = require('node:fs/promises')
const { glob } = require('glob') // npm install glob@10
const { Contract, ContractFactory } = require('ethers') // npm install ethers@5
const { StaticJsonRpcProvider } = require('@ethersproject/providers')

async function main() {
  const providerUrl = 'http://127.0.0.1:8545'

  const deployer = '0x27e80db1f5a975f4c43c5ec163114e796cdb603d'
  const storageAddress = '0x1d8f8f00cfa6758d7bE78336684788Fb0ee0Fa46'
  const upgradeContractAddress = '0x5dC69083B68CDb5c9ca492A0A5eC581e529fb73C'

  const upgradeContractABI = [
    'function newRocketClaimDAO() view returns (address)',
    'function newRocketDAOProtocol() view returns (address)',
    'function newRocketDAOProtocolProposals() view returns (address)',
    'function newRocketDAOProtocolSettingsAuction() view returns (address)',
    'function newRocketDAOProtocolSettingsDeposit() view returns (address)',
    'function newRocketDAOProtocolSettingsInflation() view returns (address)',
    'function newRocketDAOProtocolSettingsMinipool() view returns (address)',
    'function newRocketDAOProtocolSettingsNetwork() view returns (address)',
    'function newRocketDAOProtocolSettingsNode() view returns (address)',
    'function newRocketDAOProtocolSettingsRewards() view returns (address)',
    'function newRocketMerkleDistributorMainnet() view returns (address)',
    'function newRocketMinipoolManager() view returns (address)',
    'function newRocketNetworkBalances() view returns (address)',
    'function newRocketNetworkPrices() view returns (address)',
    'function newRocketNodeDeposit() view returns (address)',
    'function newRocketNodeManager() view returns (address)',
    'function newRocketNodeStaking() view returns (address)',
    'function newRocketRewardsPool() view returns (address)',
    'function rocketDAOProtocolProposal() view returns (address)',
    'function rocketDAOProtocolSettingsProposals() view returns (address)',
    'function rocketDAOProtocolSettingsSecurity() view returns (address)',
    'function rocketDAOProtocolVerifier() view returns (address)',
    'function rocketDAOSecurity() view returns (address)',
    'function rocketDAOSecurityActions() view returns (address)',
    'function rocketDAOSecurityProposals() view returns (address)',
    'function rocketNetworkSnapshots() view returns (address)',
    'function rocketNetworkVoting() view returns (address)'
  ]

  const provider = new StaticJsonRpcProvider(providerUrl)

  const upgradeContract = new Contract(upgradeContractAddress, upgradeContractABI, provider)

  const contracts = [
    ['RocketUpgradeOneDotThree', upgradeContractAddress],
    ...(await Promise.all(
      Object.values(upgradeContract.interface.functions)
            .map(async f => [ f.name.replace('new', ''), await upgradeContract[f.name]() ])
    ))
  ]

  for (const [contractName, address] of contracts) {
    const blockchainCode = await provider.getCode(address)

    const artifactName = contractName[0].toUpperCase() + contractName.substring(1) // rocketContract => RocketContract
    const artifactFilename = (await glob(`artifacts/**/${artifactName}.sol/${artifactName}.json`))[0]
    const artifact = JSON.parse(await fs.readFile(artifactFilename, 'utf-8'))

    const factory = ContractFactory.fromSolidity(artifact, provider.getSigner(deployer))
    const deployment = await factory.deploy(storageAddress)
    await deployment.deployTransaction.wait()
    let deployedArtifactCode = await provider.getCode(deployment.address)

    const match = blockchainCode === deployedArtifactCode
    console.log(match ? '✅' : '❌', artifactName, address)
  }
}

main()

Run the verification script:

node verify.js

Script output for a08da9639b8a1619c06f6ec314e36b4765b9452c:

✅ RocketUpgradeOneDotThree 0x5dC69083B68CDb5c9ca492A0A5eC581e529fb73C
✅ RocketClaimDAO 0xFe6Db0ce3F61a4aE04c0A3E62F775a6f511C9aaC
✅ RocketDAOProtocol 0x1b714ed0ce30A8BeDC5b4253DaAa08c84CA5BFcb
✅ RocketDAOProtocolProposals 0x6D736da1dC2562DBeA9998385A0A27d8c2B2793e
✅ RocketDAOProtocolSettingsAuction 0xEF75e83633E686D3085b3a988b937D021e2fA628
✅ RocketDAOProtocolSettingsDeposit 0xD846AA34caEf083DC4797d75096F60b6E08B7418
✅ RocketDAOProtocolSettingsInflation 0x1d4AAEaE7C8b75a8e5ab589a84516853DBDdd735
✅ RocketDAOProtocolSettingsMinipool 0xA416A7a07925d60F794E20532bc730749611A220
✅ RocketDAOProtocolSettingsNetwork 0x89682e5F9bf69C909FC5E21a06495ac35E3671Ab
✅ RocketDAOProtocolSettingsNode 0x448DA008c7EB2501165c9Aa62DfFEeC4405bC660
✅ RocketDAOProtocolSettingsRewards 0x8857610Ba0A7caFD4dBE1120bfF03E9c74fc4124
❌ RocketMerkleDistributorMainnet 0x5cE71E603B138F7e65029Cc1918C0566ed0dBD4B
✅ RocketMinipoolManager 0x09fbCE43e4021a3F69C4599FF00362b83edA501E
✅ RocketNetworkBalances 0x6Cc65bF618F55ce2433f9D8d827Fc44117D81399
✅ RocketNetworkPrices 0x25E54Bf48369b8FB25bB79d3a3Ff7F3BA448E382
✅ RocketNodeDeposit 0x9304B4ebFbE68932Cf9Af8De4d21D7e7621f701a
✅ RocketNodeManager 0x2b52479F6ea009907e46fc43e91064D1b92Fdc86
✅ RocketNodeStaking 0x0e29BA1155cE103A07118c8912dA44B0507A982D
✅ RocketRewardsPool 0xEE4d2A71cF479e0D3d0c3c2C923dbfEB57E73111
✅ RocketDAOProtocolProposal 0x7cee91F49001B08f8D562d58510C76bcEcD61FA0
✅ RocketDAOProtocolSettingsProposals 0x5f24E4a1A1f134a5a6952A9965721E6344898497
✅ RocketDAOProtocolSettingsSecurity 0x1ec364CDD9697F56B8CB17a745B98C2b862CBE29
✅ RocketDAOProtocolVerifier 0x25F41Cd11d95DBEC0919A0440343698cf1472a33
✅ RocketDAOSecurity 0x84aE6D61Df5c6ba7196b5C76Bcb112B8a689aD37
✅ RocketDAOSecurityActions 0xeaa442dF4Bb5394c66C8024eFb4979bEc89Eb59a
✅ RocketDAOSecurityProposals 0x6004Fa90a27dB9971aDD200d1A3BB34444db9Fb7
✅ RocketNetworkSnapshots 0x7603352f1C4752Ac07AAC94e48632b65FDF1D35c
✅ RocketNetworkVoting 0xA9d27E1952f742d659143a544d3e535fFf3Eebe1

Bytecode for RocketMerkleDistributorMainnet is almost identical except for some bytes at the end:

  • On-chain: ...221220f75764e6da0af61a491821a4735054ae09dab1877bf776e7e5c0c30978e1f3b264736f6c63430008120033
  • GitHub: ...221220cc43dfad62e22893c6ab9b5517ac4fb734f7ae4363841ce701724e858c3da49164736f6c63430008120033

After checking out 8a86af4e37299dbdd8e0a07fd80a51887278557c (previous commit):

✅ RocketUpgradeOneDotThree 0x5dC69083B68CDb5c9ca492A0A5eC581e529fb73C
✅ RocketClaimDAO 0xFe6Db0ce3F61a4aE04c0A3E62F775a6f511C9aaC
✅ RocketDAOProtocol 0x1b714ed0ce30A8BeDC5b4253DaAa08c84CA5BFcb
✅ RocketDAOProtocolProposals 0x6D736da1dC2562DBeA9998385A0A27d8c2B2793e
✅ RocketDAOProtocolSettingsAuction 0xEF75e83633E686D3085b3a988b937D021e2fA628
✅ RocketDAOProtocolSettingsDeposit 0xD846AA34caEf083DC4797d75096F60b6E08B7418
✅ RocketDAOProtocolSettingsInflation 0x1d4AAEaE7C8b75a8e5ab589a84516853DBDdd735
✅ RocketDAOProtocolSettingsMinipool 0xA416A7a07925d60F794E20532bc730749611A220
✅ RocketDAOProtocolSettingsNetwork 0x89682e5F9bf69C909FC5E21a06495ac35E3671Ab
✅ RocketDAOProtocolSettingsNode 0x448DA008c7EB2501165c9Aa62DfFEeC4405bC660
✅ RocketDAOProtocolSettingsRewards 0x8857610Ba0A7caFD4dBE1120bfF03E9c74fc4124
✅ RocketMerkleDistributorMainnet 0x5cE71E603B138F7e65029Cc1918C0566ed0dBD4B
✅ RocketMinipoolManager 0x09fbCE43e4021a3F69C4599FF00362b83edA501E
✅ RocketNetworkBalances 0x6Cc65bF618F55ce2433f9D8d827Fc44117D81399
✅ RocketNetworkPrices 0x25E54Bf48369b8FB25bB79d3a3Ff7F3BA448E382
✅ RocketNodeDeposit 0x9304B4ebFbE68932Cf9Af8De4d21D7e7621f701a
✅ RocketNodeManager 0x2b52479F6ea009907e46fc43e91064D1b92Fdc86
✅ RocketNodeStaking 0x0e29BA1155cE103A07118c8912dA44B0507A982D
✅ RocketRewardsPool 0xEE4d2A71cF479e0D3d0c3c2C923dbfEB57E73111
✅ RocketDAOProtocolProposal 0x7cee91F49001B08f8D562d58510C76bcEcD61FA0
✅ RocketDAOProtocolSettingsProposals 0x5f24E4a1A1f134a5a6952A9965721E6344898497
✅ RocketDAOProtocolSettingsSecurity 0x1ec364CDD9697F56B8CB17a745B98C2b862CBE29
✅ RocketDAOProtocolVerifier 0x25F41Cd11d95DBEC0919A0440343698cf1472a33
✅ RocketDAOSecurity 0x84aE6D61Df5c6ba7196b5C76Bcb112B8a689aD37
✅ RocketDAOSecurityActions 0xeaa442dF4Bb5394c66C8024eFb4979bEc89Eb59a
✅ RocketDAOSecurityProposals 0x6004Fa90a27dB9971aDD200d1A3BB34444db9Fb7
✅ RocketNetworkSnapshots 0x7603352f1C4752Ac07AAC94e48632b65FDF1D35c
✅ RocketNetworkVoting 0xA9d27E1952f742d659143a544d3e535fFf3Eebe1

The last commit only changes a comment however it produces different bytecode.

It turns out that

The Solidity compiler appends the metadata file hash at the end of the bytecode
Metadata file is a json file that contains hashes of source files so changing comments changes the metadata file hash.
Source

So why did the team’s tool pass and Etherscan didn’t complain?

Etherscan only compares the bytecode of the submitted contract to the bytecode of the deployed contract (excluding the metadata hash). So you can change any comments and the source code will still be verified by Etherscan.
Source

While it’s a minor thing it should be noted that the deployed contracts are from commit 8a86af4 not a08da96 (tagged as v1.3.0). It’s also not yet merged into master.

Conclusion

I have verified myself that the contracts whose source code is published on GitHub are the same ones that are deployed on-chain and will be used for the Houston upgrade.

Except for one fixed comment after deployment that changed the bytecode of one contract.

7 Likes