|
| 1 | +import { DeployFunction } from "hardhat-deploy/types"; |
| 2 | +import { HardhatRuntimeEnvironment } from "hardhat/types"; |
| 3 | +import { ethers } from "hardhat"; |
| 4 | +import fs from "fs"; |
| 5 | +import path from "path"; |
| 6 | + |
| 7 | +/** |
| 8 | + * Hardhat-deploy script that verifies that the bytecode of a live deployment matches the byte-code |
| 9 | + * produced by the current compilation artefacts in the repo. |
| 10 | + * |
| 11 | + * Usage example (Arbitrum_Adapter on mainnet): |
| 12 | + * CONTRACT=Arbitrum_Adapter yarn hardhat deploy --network mainnet --tags verifyBytecode |
| 13 | + * |
| 14 | + * The script will: |
| 15 | + * 1. Read the compiled artefact for `CONTRACT` (artifacts/<source>/<Contract>.json) in the build |
| 16 | + * folder and grab the creation byte-code and constructor ABI. |
| 17 | + * 2. Read the hardhat-deploy deployment file for the same network (`deployments/<network>/<CONTRACT>.json`) |
| 18 | + * to obtain the constructor arguments that were used for the actual deployment together with the |
| 19 | + * deployed address. |
| 20 | + * 3. Encode the constructor arguments with the ABI and append them to the creation byte-code – this |
| 21 | + * is identical to the calldata used when the contract was created on-chain. |
| 22 | + * 4. Simulate a CREATE call (eth_call) with this creation code to obtain the runtime byte-code that |
| 23 | + * would be stored on-chain. |
| 24 | + * 5. Fetch the real runtime byte-code from the chain via `eth_getCode`. |
| 25 | + * 6. Keccak-hash both byte-codes and print a comparison so reviewers can quickly see whether they |
| 26 | + * match. |
| 27 | + */ |
| 28 | +const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { |
| 29 | + const contractName = process.env.CONTRACT; |
| 30 | + if (contractName === undefined) { |
| 31 | + throw new Error("Please provide CONTRACT env var, e.g. Arbitrum_Adapter"); |
| 32 | + } |
| 33 | + |
| 34 | + // --------------------------------------------------------------------------- |
| 35 | + // 1. Read compiled artefact (creation bytecode & constructor ABI) |
| 36 | + // --------------------------------------------------------------------------- |
| 37 | + const artifact = await hre.artifacts.readArtifact(contractName); |
| 38 | + const creationBytecode: string = artifact.bytecode; // hex-string starting with 0x |
| 39 | + const constructorFragment = artifact.abi.find((e: any) => e.type === "constructor"); |
| 40 | + const constructorInputs = constructorFragment?.inputs || []; |
| 41 | + |
| 42 | + // --------------------------------------------------------------------------- |
| 43 | + // 2. Read deployment JSON for the current network (constructor args & address) |
| 44 | + // --------------------------------------------------------------------------- |
| 45 | + const networkName = hre.network.name; |
| 46 | + const deploymentInfo = await hre.deployments.get(contractName); |
| 47 | + const deployedAddress: string = deploymentInfo.address; |
| 48 | + const constructorArgs: any[] = deploymentInfo.args || []; |
| 49 | + |
| 50 | + // --------------------------------------------------------------------------- |
| 51 | + // 3. Encode constructor args and build full creation bytecode |
| 52 | + // --------------------------------------------------------------------------- |
| 53 | + const argTypes = constructorInputs.map((c: any) => c.type); |
| 54 | + const encodedArgs = |
| 55 | + argTypes.length > 0 ? ethers.utils.defaultAbiCoder.encode(argTypes, constructorArgs).slice(2) : ""; |
| 56 | + const creationCodeWithArgs = creationBytecode + encodedArgs; // strip 0x from encodedArgs already |
| 57 | + |
| 58 | + // --------------------------------------------------------------------------- |
| 59 | + // 4. Simulate CREATE to obtain runtime bytecode (set generous gas limit) |
| 60 | + // --------------------------------------------------------------------------- |
| 61 | + const gasLimitEnv = process.env.GAS_LIMIT || "5000000"; // default 5M |
| 62 | + const gasLimit = ethers.BigNumber.from(gasLimitEnv); |
| 63 | + const runtimeBytecodeSim = await hre.ethers.provider.call({ data: creationCodeWithArgs, gasLimit }); |
| 64 | + const runtimeBytecodeSimHash = ethers.utils.keccak256(runtimeBytecodeSim); |
| 65 | + |
| 66 | + // --------------------------------------------------------------------------- |
| 67 | + // 5. Fetch on–chain runtime bytecode for deployed address |
| 68 | + // --------------------------------------------------------------------------- |
| 69 | + const onchainRuntimeBytecode = await hre.ethers.provider.getCode(deployedAddress); |
| 70 | + const onchainRuntimeBytecodeHash = ethers.utils.keccak256(onchainRuntimeBytecode); |
| 71 | + |
| 72 | + // --------------------------------------------------------------------------- |
| 73 | + // 6. Print comparison for reviewers |
| 74 | + // --------------------------------------------------------------------------- |
| 75 | + console.log("\n================ Bytecode Verification ================"); |
| 76 | + console.log(`Contract : ${contractName}`); |
| 77 | + console.log(`Network : ${networkName}`); |
| 78 | + console.log(`Deployed address : ${deployedAddress}`); |
| 79 | + console.log("-------------------------------------------------------"); |
| 80 | + console.log(`On-chain code hash : ${onchainRuntimeBytecodeHash}`); |
| 81 | + console.log(`Simulated code hash : ${runtimeBytecodeSimHash}`); |
| 82 | + console.log("-------------------------------------------------------"); |
| 83 | + console.log( |
| 84 | + onchainRuntimeBytecodeHash === runtimeBytecodeSimHash |
| 85 | + ? "✅ MATCH" |
| 86 | + : "❌ MISMATCH – deployment does not match current artefacts" |
| 87 | + ); |
| 88 | + console.log("=======================================================\n"); |
| 89 | +}; |
| 90 | + |
| 91 | +module.exports = func; |
| 92 | +func.tags = ["verifyBytecode"]; |
0 commit comments