Bytecode verification
Previously I verified that the source code published on Etherscan is the same as the source available on GitHub. I had to trust that Etherscan performed bytecode verification correctly.
I have now also verified that 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 4c19daf
npm install
npm run compile
Install dependencies for the verification script:
npm install 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
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 = '0x9a0b5d3101d111EA0edD573d45ef2208CC97984a'
const upgradeContractABI = [
'function newRocketNodeDeposit() public view returns (address)',
'function newRocketMinipoolDelegate() public view returns (address)',
'function newRocketDAOProtocolSettingsMinipool() public view returns (address)',
'function newRocketMinipoolQueue() public view returns (address)',
'function newRocketDepositPool() public view returns (address)',
'function newRocketDAOProtocolSettingsDeposit() public view returns (address)',
'function newRocketMinipoolManager() public view returns (address)',
'function newRocketNodeStaking() public view returns (address)',
'function newRocketNodeDistributorDelegate() public view returns (address)',
'function newRocketMinipoolFactory() public view returns (address)',
'function newRocketNetworkFees() public view returns (address)',
'function newRocketNetworkPrices() public view returns (address)',
'function newRocketDAONodeTrustedSettingsMinipool() public view returns (address)',
'function newRocketNodeManager() public view returns (address)',
'function newRocketDAOProtocolSettingsNode() public view returns (address)',
'function newRocketNetworkBalances() public view returns (address)',
'function newRocketRewardsPool() public view returns (address)',
'function rocketMinipoolBase() public view returns (address)',
'function rocketMinipoolBondReducer() public view returns (address)'
]
const provider = new StaticJsonRpcProvider(providerUrl)
const upgradeContract = new Contract(upgradeContractAddress, upgradeContractABI, provider)
const contracts = [
['RocketUpgradeOneDotTwo', 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') || artifactName.includes('RocketMinipoolBase')
? await factory.deploy()
: await factory.deploy(storageAddress)
await deployment.deployTransaction.wait()
let deployedArtifactCode = await provider.getCode(deployment.address)
if (artifactName.includes('RocketMinipoolBase')) {
deployedArtifactCode = deployedArtifactCode.replace(
new RegExp(deployment.address.substring(2).toLowerCase(), 'g'),
address.substring(2).toLowerCase()
)
}
const match = blockchainCode === deployedArtifactCode
console.log(match ? '✅' : '❌', artifactName, address)
}
}
main()
Run the verification script:
node verify.js
✅ RocketUpgradeOneDotTwo 0x9a0b5d3101d111EA0edD573d45ef2208CC97984a
✅ RocketNodeDeposit 0x2FB42FfE2d7dF8381853e96304300c6a5E846905
✅ RocketMinipoolDelegate 0xA347C391bc8f740CAbA37672157c8aAcD08Ac567
✅ RocketDAOProtocolSettingsMinipool 0x42d4e4B59220dA435A0bd6b5892B90fF50e1D8D4
✅ RocketMinipoolQueue 0x9e966733e3E9BFA56aF95f762921859417cF6FaA
✅ RocketDepositPool 0xDD3f50F8A6CafbE9b31a427582963f465E745AF8
✅ RocketDAOProtocolSettingsDeposit 0xac2245BE4C2C1E9752499Bcd34861B761d62fC27
✅ RocketMinipoolManager 0x6d010C43d4e96D74C422f2e27370AF48711B49bF
✅ RocketNodeStaking 0x0d8D8f8541B12A0e1194B7CC4b6D954b90AB82ec
✅ RocketNodeDistributorDelegate 0x32778D6bf5b93B89177D328556EeeB35c09f472b
✅ RocketMinipoolFactory 0x7B8c48256CaF462670f84c7e849cab216922B8D3
✅ RocketNetworkFees 0xf824e2d69dc7e7c073162C2bdE87dA4746d27a0f
✅ RocketNetworkPrices 0x751826b107672360b764327631cC5764515fFC37
✅ RocketDAONodeTrustedSettingsMinipool 0xE535fA45e12d748393C117C6D8EEBe1a7D124d95
✅ RocketNodeManager 0x89F478E6Cc24f052103628f36598D4C14Da3D287
✅ RocketDAOProtocolSettingsNode 0x17Cf2c5d69E4F222bcaDD86d210FE9dc8BadA60B
✅ RocketNetworkBalances 0x07FCaBCbe4ff0d80c2b1eb42855C0131b6cba2F4
✅ RocketRewardsPool 0xA805d68b61956BC92d556F2bE6d18747adAeEe82
✅ RocketMinipoolBase 0x560656C8947564363497E9C78A8BDEff8d3EFF33
✅ RocketMinipoolBondReducer 0xf7aB34C74c02407ed653Ac9128731947187575C0
Explanation
If you compare the bytecode you get from getCode
to the bytecode from the artifacts generated by the Solidity compiler you’ll find that they sometimes don’t match.
const match = blockchainCode === artifact.deployedBytecode
❌ RocketUpgradeOneDotTwo 0x9a0b5d3101d111EA0edD573d45ef2208CC97984a
❌ RocketDepositPool 0xDD3f50F8A6CafbE9b31a427582963f465E745AF8
❌ RocketNodeDistributorDelegate 0x32778D6bf5b93B89177D328556EeeB35c09f472b
❌ RocketMinipoolBase 0x560656C8947564363497E9C78A8BDEff8d3EFF33
These contracts have immutable
fields whose values are not known until the contract is deployed.
One way to solve this would be to check the transactions in which the contracts were deployed but you need to somehow index them or manually look them up.
I decided to try deploying the compiled contracts to a local mainnet fork and then compare their getCode
results.
It almost worked.
❌ RocketUpgradeOneDotTwo 0x9a0b5d3101d111EA0edD573d45ef2208CC97984a
❌ RocketMinipoolBase 0x560656C8947564363497E9C78A8BDEff8d3EFF33
RocketUpgradeOneDotTwo
saves the deployer of the contract in an immutable variable:
contract RocketUpgradeOneDotTwo {
address immutable deployer;
constructor(RocketStorageInterface _rocketStorageAddress) {
deployer = msg.sender;
}
}
Luckily ganache-cli
lets you impersonate any address in your fork so I could unlock and use the Rocket Pool’s deployer like this:
ganache-cli --unlock 0x27e80db1f5a975f4c43c5ec163114e796cdb603d
const deployer = '0x27e80db1f5a975f4c43c5ec163114e796cdb603d'
const factory = ContractFactory.fromSolidity(artifact, provider.getSigner(deployer))
RocketMinipoolBase
was tricker because it stores its own address:
contract RocketMinipoolBase {
address immutable self;
constructor() {
self = address(this);
}
}
To do the verification exactly I’d need to fork mainnet earlier, deploy all contracts in the same order and then I should be able to get the exact same address during deployment.
However, I decided to simply try replacing the address of my newly deployed contract with the real contract address and it worked:
if (artifactName.includes('RocketMinipoolBase')) {
deployedArtifactCode = deployedArtifactCode.replace(
new RegExp(deployment.address.substring(2).toLowerCase(), 'g'),
address.substring(2).toLowerCase()
)
}
With these two workarounds all contracts match.