Deploy Contract With Custom Metadata Hash

When you deploy a Solidity contract the compile has removed the comments from the bytecode, but the last 32 bytes of the bytecode is actually a hash of:

  • hashed bytecode
  • compiler settings
  • contract source including comments

When you verify a contract on EtherScan, or SonicScan, this hash is also checked to make sure the verified contract has the correct license, etc. even if the bytecode itself matches. So… can we deploy a contract with a custom hash that will let us verify contracts with customize comments?

This contract will take a custom metadata hash and update the bytecode of our contract before deploying a new instance of it.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import {YourContract} from './YourContract.sol';

contract CustomFactory {
    event ContractDeployed(address indexed contractAddress, bytes32 metadataHash);

    error BytecodeTooShort();

    /**
     * @notice Deploys a new contract with a modified metadata hash.
     * @param newMetadataHash The new metadata hash to append to the bytecode.
     */
    function deployWithCustomMetadata(bytes32 newMetadataHash) external returns (address) {
        // Get the creation code of the contract
        bytes memory bytecode = abi.encodePacked(type(YourContract).creationCode);
        require(bytecode.length > 32, BytecodeTooShort());

        // Replace the last 32 bytes of the bytecode with the new metadata hash
        for (uint256 i = 0; i < 32; i++) {
            bytecode[bytecode.length - 32 + i] = newMetadataHash[i];
        }

        // Deploy the contract with the modified bytecode
        address deployedContract;
        assembly {
            deployedContract := create(0, add(bytecode, 0x20), mload(bytecode))
            if iszero(deployedContract) {
                // if there wsa an error, revert and provide the bytecode
                revert(add(0x20, bytecode), mload(bytecode))
            }
        }

        emit ContractDeployed(deployedContract, newMetadataHash);

        return deployedContract;
    }
}

This is a sample contract with a comment /* YourContract */ we can use to replace with our custom comment.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

/* YourContract */

contract YourContract {
    /**
     * @notice Constructor for the contract.
     */
    constructor() {}

    /**
     * @notice Returns the stored message.
     */
    function getMessage() external view returns (string memory) {
        return 'hello world';
    }
}

This is a Hardhat test that loads necessary information from build info and artifacts provided by Hardhat, but you can also create this manually.

// Hardhat test for CustomFactory and YourContract
import * as fs from 'fs/promises';
import * as path from 'path';
import { expect } from 'chai';
import { ethers } from 'hardhat';
import { encode as cborEncode } from 'cbor';
import { loadFixture } from '@nomicfoundation/hardhat-network-helpers';
import { AddressLike, ContractTransactionResponse } from 'ethers';
import { YourContract } from 'sdk/types';

function calculateMetadataHash(sourceCode: string, comment: string, originalMetadata: any): string {
  const keccak256 = ethers.keccak256;
  const toUtf8Bytes = ethers.toUtf8Bytes;

  // Inject the comment
  const modifiedSourceCode = sourceCode.replace(/\/\* YourContract \*\//gs, comment);
  // console.log('modifiedSourceCode', modifiedSourceCode);

  // Calculate keccak256 of the modified source code
  const sourceCodeHash = keccak256(toUtf8Bytes(modifiedSourceCode));

  // Use original metadata as a base
  const metadata = {
    ...originalMetadata,
    sources: {
      'contracts/YourContract.sol': {
        ...originalMetadata.sources['contracts/YourContract.sol'],
        keccak256: sourceCodeHash,
      },
    },
  };

  // CBOR encode the metadata and calculate the hash
  const cborData = cborEncode(metadata);
  const metadataHash = keccak256(cborData);

  return metadataHash;
}

export const fixture = async () => {
  const Factory = await ethers.getContractFactory('CustomFactory');
  const factory = await Factory.deploy();
  await factory.waitForDeployment();

  const [owner] = await ethers.getSigners();

  return { factory, owner };
};

export async function getDeployedContractAddress(
  tx: Promise<ContractTransactionResponse> | ContractTransactionResponse,
): Promise<AddressLike> {
  const _tx = tx instanceof Promise ? await tx : tx;
  const _receipt = await _tx.wait();
  const _interface = new ethers.Interface([
    'event ContractDeployed(address indexed contractAddress, bytes32 metadataHash)',
  ]);
  const _data = _receipt?.logs[0].data;
  const _topics = _receipt?.logs[0].topics;
  const _event = _interface.decodeEventLog('ContractDeployed', _data || ethers.ZeroHash, _topics);
  return _event.contractAddress;
}

describe('CustomFactory and YourContract', function () {
  it('should deploy a contract with a custom metadata hash', async function () {
    const { factory } = await loadFixture(fixture);

    // Load source code of YourContract from file
    const sourceCodePath = path.join(__dirname, '../contracts/YourContract.sol');
    const sourceCode = await fs.readFile(sourceCodePath, 'utf8');

    const comment = ('/* My Contract */');

    // Load original metadata from build-info
    const buildInfoPath = path.join(__dirname, `../artifacts/build-info`);
    const buildInfoFiles = await fs.readdir(buildInfoPath);

    let buildInfo: any;

    for (const file of buildInfoFiles) {
      const fullPath = path.join(buildInfoPath, file);
      const currentBuildInfo = JSON.parse(await fs.readFile(fullPath, 'utf8'));
      if (currentBuildInfo.output.contracts['contracts/YourContract.sol']) {
        buildInfo = currentBuildInfo;
        break;
      }
    }

    if (!buildInfo) {
      throw new Error('Build info for YourContract not found.');
    }

    const originalMetadata = buildInfo.output.contracts['contracts/YourContract.sol'].YourContract.metadata;

    // Calculate the metadata hash
    const newMetadataHash = calculateMetadataHash(sourceCode, comment, JSON.parse(originalMetadata));

    // Deploy the contract via the Factory
    const tx = await factory.deployWithCustomMetadata(newMetadataHash);
    const deployedAddress = await getDeployedContractAddress(tx);
    // console.log('deployedAddress', deployedAddress);

    // Validate the deployment
    expect(deployedAddress).to.be.properAddress;

    // Interact with the deployed contract
    const YourContract = await ethers.getContractFactory('YourContract');
    const yourContract = YourContract.attach(deployedAddress) as YourContract;

    // Ensure it was initialized properly
    const message = await yourContract.message();
    expect(message).to.equal('hello world');

    // Get the bytecode of the deployed contract
    const deployedBytecode = await ethers.provider.getCode(deployedAddress);

    // Get the last 32 bytes of the deployed bytecode
    const deployedBytecodeHash = deployedBytecode.slice(-64);

    // Validate that the hash matches our new metadata hash
    expect('0x' + deployedBytecodeHash).to.equal(newMetadataHash);
  });
});

You may also like...

Leave a Reply

Your email address will not be published. Required fields are marked *