Houston hotfix upgrade contract verification
Checking whether the source code published on GitHub matches what’s deployed on-chain.
This is similar to my verification of previous upgrades. 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 Interface(['function proposalUpgrade(string action, string contractName, string contractABI, address contractAddress)']).decodeFunctionData('proposalUpgrade', '0xdfc970ef...')
You should see:
- Contract name:
rocketUpgradeOneDotThreeDotOne
- Contract address:
0xc2C81454427b1E53Fdf5d3B45561e3c18F90f9eD
- Contract ABI:
eJzNmMtu4jAU...
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')
const pako = require('pako')
// proposal payload
// https://etherscan.io/tx/0x5d5737e5c5fe3aee8ce232c6462c4b2753be6c4833567d024dbbe7b21bc1000c
const payload = '0xdfc970ef000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000c00000000000000000000000000000000000000000000000000000000000000100000000000000000000000000c2c81454427b1e53fdf5d3b45561e3c18f90f9ed000000000000000000000000000000000000000000000000000000000000000b616464436f6e7472616374000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001e726f636b6574557067726164654f6e65446f745468726565446f744f6e65000000000000000000000000000000000000000000000000000000000000000003d0654a7a4e6d4d7475346a4155686c396c6c44554c4f334553707a7330624762526d564537367161714b73632b526c597a4e6e4b63586a5471753439546f454249434c5249395135382f622f7a4838655832332b52306f76473164484662667654676457732b764f79674f6769346b593779376a37646d5834413768725a79796277342b326b5751636f6b6d6b32642b3234623364626a4156776b4a642b32713348496574436c37764a6c48746d49504c78724653566371392b46707439494b39734c4b435451382f632b3173772f324176724257633831635937733172354e4436746d376a4c564b62635142635a4f3945667a664f4d3232426842514f62627075717076735659742f4744666a6258416e544c614e7a534e57386b37476c3032657431376d7873396379516b467a6b6177573636716a64444e2f75432b627661656b66756544512f47387a68574f37483656484255322b4533724b42615264642b4a79416273517745524a7a7a48596a746f47485a2b434e677a50376c4747533453496c49374f4b772f45756a616e36504877725032755545737753796d6b787048634f3771702f655a2b534c73642b4454344977536d4b63523450427233794247663257644a4549466f4f5a6c63375a54677563356d6741754634534b32477036584c732b6d7633395973544d327173437757575a7a524241597437694f596c756f77684e394c6c4a37334d6178717a6f70414742434d4342794a344177335661426d674341693575496a4a4b475a6b73516b4b784e2b43736f314f4f6546314e4e6d6636662f636d2b794a4d754141663445554741574156436134504b6b64624d6d576d646459427457495341565a54363436783646464a684e5259706c7776684a69586344566b6b464e69783353696c497a464879455a4c4154496c4a586c425a35714d6f6c307172685439337a4b43434f65736369372f6345656d2f30426e6d34312f704c6b5a676470522b2b30383553592f6d7547546148376f4457783873497a496a36666870736b4d526d426d70774468465a447970666f4a374d7662687872696c756f4373344c496f736344794e4962416a4d4245466854612b2f38596842457741372f374b52655744515352784e382f78772f45577753426d534170532f4a692b454b376733447432454e776130477947464f4230536b45675a6d517979497559734750653979387664766f756c2b56776635376e472f5738794b336c4c3837524b6e714c6c2f6265525043476e59583375666655564b4b557044444a2b46487350586f4861743932715239486930727a72747245483836464b6e507372762f63764c3263673d3d00000000000000000000000000000000'
// https://github.com/rocket-pool/rocketpool/blob/master/contracts/interface/dao/node/RocketDAONodeTrustedProposalsInterface.sol
const iface = new ethers.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).toObject()
console.log(result)
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.Interface(abi).format('full'))
}
Source code
Checking whether the source code published on Etherscan matches what’s on GitHub.
I ran the team’s verification tool.
Instructions
$ git clone https://github.com/rocket-pool/verify-1.3.1.git
$ cd verify-1.3.1/
$ git log --oneline | head -n1
30d5829 Update readme
$ 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 '8d4d5c0f1b810f97ca42cd1ceece0e7c81eb81ee'
...
added 998 packages, and audited 999 packages in 19s
...
✔ Verified contract at 0xc2C81454427b1E53Fdf5d3B45561e3c18F90f9eD matches RocketUpgradeOneDotThreeDotOne
✔ Verified contract at 0x1e94e6131Ba5B4F193d2A1067517136C52ddF102 matches RocketDAOProposal
✔ Verified contract at 0x2D627A50Dc1C4EDa73E42858E8460b0eCF300b25 matches RocketDAOProtocolProposal
✔ Verified contract at 0xd1f7e573cdC64FC0B201ca37aB50bC7Dd880040A matches RocketDAOProtocolVerifier
✔ Verified contract at 0x59cd103DF1BE2EBd80D45c54a3cDE8d4F812C034 matches RocketDAOProtocolSettingsProposals
✔ Verified contract at 0x364F989A3C9a1F66cB51b9043680974eA08C0d18 matches RocketDAOProtocolAuction
✔ Verified contract at 0xF82991Bd8976c243eB3b7CDDc52AB0Fc8dc1246C matches RocketMinipoolManager
✔ Verified contract at 0xF18Dc176C10Ff6D8b5A17974126D43301F8EEB95 matches RocketNodeStaking
✔ Verified contract at 0x03d30466d199Ef540823fe2a22CAE2E3b9343bb0 matches RocketMinipoolDelegate
✔ Verified contract at 0x672335B91b4f2096D897cA1B12Ef4ec9346A5ff4 matches RocketNodeDeposit
✔ Verified contract at 0x77cF0f32BDd06242465eb3318a81196194a13daA matches RocketNetworkVoting
ETH matched corrections:
1: 0x9796dAd6a55c9501F83B0Dc41676bdC6d001dd32 = 16000000000000000000
2: 0x70D06394f33D56B6310778eC4E61033585038997 = 16000000000000000000
3: 0x4efc3E587A4c3Ae0899a0F6e20a78393FC9E39C8 = 16000000000000000000
✔ Verification successful
8d4d5c0
is the commit with the v1.3.1 tag.
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 8d4d5c0
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 = '0xc2C81454427b1E53Fdf5d3B45561e3c18F90f9eD'
const upgradeContractABI = [
'function newRocketDAOProposal() view returns (address)',
'function newRocketDAOProtocolProposal() view returns (address)',
'function newRocketDAOProtocolSettingsAuction() view returns (address)',
'function newRocketDAOProtocolSettingsProposals() view returns (address)',
'function newRocketDAOProtocolVerifier() view returns (address)',
'function newRocketMinipoolDelegate() view returns (address)',
'function newRocketMinipoolManager() view returns (address)',
'function newRocketNetworkVoting() view returns (address)',
'function newRocketNodeDeposit() view returns (address)',
'function newRocketNodeStaking() view returns (address)'
]
const provider = new StaticJsonRpcProvider(providerUrl)
const upgradeContract = new Contract(upgradeContractAddress, upgradeContractABI, provider)
const contracts = [
['rocketUpgradeOneDotThreeDotOne', 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 = artifactName.includes('Delegate') ? await factory.deploy() : await factory.deploy(storageAddress)
await deployment.deployTransaction.wait()
const 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 8d4d5c0f1b810f97ca42cd1ceece0e7c81eb81ee
:
✅ RocketUpgradeOneDotThreeDotOne 0xc2C81454427b1E53Fdf5d3B45561e3c18F90f9eD
✅ RocketDAOProposal 0x1e94e6131Ba5B4F193d2A1067517136C52ddF102
✅ RocketDAOProtocolProposal 0x2D627A50Dc1C4EDa73E42858E8460b0eCF300b25
✅ RocketDAOProtocolSettingsAuction 0x364F989A3C9a1F66cB51b9043680974eA08C0d18
✅ RocketDAOProtocolSettingsProposals 0x59cd103DF1BE2EBd80D45c54a3cDE8d4F812C034
✅ RocketDAOProtocolVerifier 0xd1f7e573cdC64FC0B201ca37aB50bC7Dd880040A
✅ RocketMinipoolDelegate 0x03d30466d199Ef540823fe2a22CAE2E3b9343bb0
✅ RocketMinipoolManager 0xF82991Bd8976c243eB3b7CDDc52AB0Fc8dc1246C
✅ RocketNetworkVoting 0x77cF0f32BDd06242465eb3318a81196194a13daA
✅ RocketNodeDeposit 0x672335B91b4f2096D897cA1B12Ef4ec9346A5ff4
✅ RocketNodeStaking 0xF18Dc176C10Ff6D8b5A17974126D43301F8EEB95
Everything matches.
Settings
The upgrade contract was deployed with 0x1d8f8f00cfa6758d7bE78336684788Fb0ee0Fa46
as the RocketStorage
address which is correct.
Upgraded contract addresses were set once in a separate transaction and I’ve checked them in this post.
There were 3 ETH matched corrections added but I have not checked if they are correct or not.
Finally the upgrade contract was locked which means contract addresses cannot be changed and no more corrections can be added.
The upgrade can be executed by the Protocol DAO guardian (an EOA controlled by the team) at their convenience which is scheduled for October 28 at 00:00 UTC.
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 hotfix upgrade.