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); }); });