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