The post Deploy Contract With Custom Metadata Hash appeared first on Justin Silver.
]]>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:
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); }); });
The post Deploy Contract With Custom Metadata Hash appeared first on Justin Silver.
]]>The post Slither & Echidna + Remappings appeared first on Justin Silver.
]]>While testing a project using hardhat and Echidna I was able to run all tests in the project with echidna-test .
but was not able to run tests in a specific contract that imported contracts using NPM and the node_modules directory, such as @openzeppelin. When running echidna-test
the following error would be returned
> echidna-test path/to/my/Contract.sol --contract Contract echidna-test: Couldn't compile given file stdout: stderr: ERROR:CryticCompile:Invalid solc compilation Error: Source "@openzeppelin/contracts/utils/Address.sol" not found: File not found. Searched the following locations: "". --> path/to/my/Contract.sol:4:1: | 4 | import {Address} from '@openzeppelin/contracts/utils/Address.sol'; | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
To fix this, I added Solc Remappings for Slither and Echidna.
Make sure that you have Slither and Echidna installed. Follow the install instructions on their site, or on OSX with Homebrew run brew install echidna
Create a Slither JSON config file – named slither.config.json
– to use filter_paths
to exclude some directories and provide remappings for node_modules
to solc
.
{ "filter_paths": "(mocks/|test/|@openzeppelin/)", "solc_remaps": "@=node_modules/@" }
Slither will pick up the config file automatically.
slither path/to/my/Contract.sol
For multiple remappings using an array of strings for solc_remaps
.
{ "filter_paths": "(mocks/|test/|@openzeppelin/)", "solc_remaps": [ "@openzeppelin/contracts/=node_modules/@openzeppelin/contracts/", "@openzeppelin/contracts-upgradeable/=node_modules/@openzeppelin/contracts-upgradeable/" ] }
Note that if you are using Hardhat or similar for your projects, slither will use it for the compile if a configuration can be found.
slither .
To force a particular compiler, specify it with the command.
slither --compile-force-framework solc ./contracts
For Echidna we can create a YAML config file and pass the solc
remappings to crytic-compile
via cryticArgs
.
# provide solc remappings to crytic-compile cryticArgs: ['--solc-remaps', '@=node_modules/@']
When running echidna-test
we can use the --config
option to specify the YAML config file and pick up our remappings (and other settings).
echidna-test --config echidna.yaml path/to/my/Contract.sol --contract Contract
{ "remappings": ["@openzeppelin/=node_modules/@openzeppelin/"] }
myth analyze --solc-json mythril.solc.json path/to/my/Contract.sol
The post Slither & Echidna + Remappings appeared first on Justin Silver.
]]>