OpenZeppelin / openzeppelin-upgrades

Plugins for Hardhat and Foundry to deploy and manage upgradeable contracts on Ethereum.
MIT License
621 stars 266 forks source link

hardhat-upgrades: Use actual build info when verifiying contracts on explorers #1054

Open SKYBITDev3 opened 3 months ago

SKYBITDev3 commented 3 months ago

When verifying deployed contracts it's better practice to use the artifacts and source code that were actually used for the deployment instead of using old saved ones in hardhat-upgrades.

I don't use upgrades.deployProxy because I choose to deploy the proxy via the CREATE3 method in order to reliably get the same address on multiple blockchains. So after deploying the implementation and proxy, verification fails because of bytecode mismatch with the old saved bytecode of the proxy in hardhat-upgrades.

A solution that has worked for me is to use hre.artifacts.getBuildInfo which gives everything needed for verification. e.g. I replaced https://github.com/OpenZeppelin/openzeppelin-upgrades/blob/742415c28f8858fe93c0c21bc979414d90ed2ce6/packages/plugin-hardhat/src/verify-proxy.ts#L554 with

    const buildInfo = await hre.artifacts.getBuildInfo(`${contractInfo.artifact.sourceName}:${contractInfo.artifact.contractName}`);
    const bytecode = `0x${buildInfo.output.contracts[contractInfo.artifact.sourceName][contractInfo.artifact.contractName].evm.bytecode.object}`;

    const constructorArguments = inferConstructorArgs(tx.input, bytecode);

and https://github.com/OpenZeppelin/openzeppelin-upgrades/blob/742415c28f8858fe93c0c21bc979414d90ed2ce6/packages/plugin-hardhat/src/verify-proxy.ts#L590-L594 with

    const buildInfo = await hre.artifacts.getBuildInfo(`${artifact.sourceName}:${artifact.contractName}`);

    const buildInfoInput = Object.assign({}, buildInfo.input); // copies language, sources, settings
    buildInfoInput.sources = {}; // replace sources as otherwise it includes all solidity files under contracts directory
    [ // include only those needed by proxy
        "@openzeppelin/contracts/access/Ownable.sol",
        "@openzeppelin/contracts/interfaces/IERC1967.sol",
        "@openzeppelin/contracts/proxy/beacon/BeaconProxy.sol",
        "@openzeppelin/contracts/proxy/beacon/IBeacon.sol",
        "@openzeppelin/contracts/proxy/beacon/UpgradeableBeacon.sol",
        "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol",
        "@openzeppelin/contracts/proxy/ERC1967/ERC1967Utils.sol",
        "@openzeppelin/contracts/proxy/Proxy.sol",
        "@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol",
        "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol",
        "@openzeppelin/contracts/utils/Address.sol",
        "@openzeppelin/contracts/utils/Context.sol",
        "@openzeppelin/contracts/utils/StorageSlot.sol"
    ].forEach(path => buildInfoInput.sources[path] = buildInfo.input.sources[path]);

    const params = {
        contractAddress: address,
        sourceCode: JSON.stringify(buildInfoInput),
        contractName: `${artifact.sourceName}:${artifact.contractName}`,
        compilerVersion: `v${buildInfo.solcLongVersion}`,

Also, I've found that the bytecode can have extra data at the beginning (e.g. salt), so startsWith fails in such cases. So I replaced https://github.com/OpenZeppelin/openzeppelin-upgrades/blob/742415c28f8858fe93c0c21bc979414d90ed2ce6/packages/plugin-hardhat/src/verify-proxy.ts#L741-L742 with

    const creationCodeWithout0x = creationCode.slice(2)

    if (txInput.includes(creationCodeWithout0x)) {
        return txInput.substring(txInput.indexOf(creationCodeWithout0x) + creationCodeWithout0x.length);

Verification on explorers worked after these changes.

Please update the code in your repository so that verification on explorers will still work for upgradeable contracts even if we deploy a newer version of the proxy.

SKYBITDev3 commented 3 months ago

I've updated my code to replace sources with only those that are needed for a proxy contract (I just followed the list in @openzeppelin/upgrades-core/artifacts/build-info-v5.json), as otherwise all source files under contracts directory would be included, which may not be good for privacy. Verification still works after this change.