@@ -2,99 +2,324 @@ import { task } from "hardhat/config";
22import type { HardhatRuntimeEnvironment } from "hardhat/types" ;
33import "hardhat-deploy" ;
44import "@nomiclabs/hardhat-ethers" ;
5+ import fs from "fs" ;
6+ import path from "path" ;
7+ import { execSync } from "child_process" ;
8+
9+ type VerifyBytecodeArgs = {
10+ contract ?: string ;
11+ txHash ?: string ;
12+ libraries ?: string ;
13+ broadcast ?: string ;
14+ } ;
15+
16+ /**
17+ * Best-effort parser for `foundry.toml` that extracts the default profile's `out` directory.
18+ * Falls back to `<repo>/out` if anything goes wrong.
19+ */
20+ function getFoundryOutDir ( ) : string {
21+ const root = process . cwd ( ) ;
22+ const configPath = path . join ( root , "foundry.toml" ) ;
23+
24+ if ( ! fs . existsSync ( configPath ) ) {
25+ return path . join ( root , "out" ) ;
26+ }
27+
28+ const contents = fs . readFileSync ( configPath , "utf8" ) ;
29+ const lines = contents . split ( / \r ? \n / ) ;
30+
31+ let inDefaultProfile = false ;
32+ for ( const rawLine of lines ) {
33+ const line = rawLine . trim ( ) ;
34+
35+ if ( line . startsWith ( "[" ) && line . endsWith ( "]" ) ) {
36+ // Enter or exit `[profile.default]` section.
37+ inDefaultProfile = line === "[profile.default]" ;
38+ continue ;
39+ }
40+
41+ if ( ! inDefaultProfile ) continue ;
42+
43+ const match = line . match ( / ^ o u t \s * = \s * " ( .* ) " \s * $ / ) ;
44+ if ( match ) {
45+ const configuredOut = match [ 1 ] . trim ( ) ;
46+ if ( configuredOut . length > 0 ) {
47+ return path . isAbsolute ( configuredOut ) ? configuredOut : path . join ( root , configuredOut ) ;
48+ }
49+ }
50+ }
51+
52+ // Default Foundry output directory.
53+ return path . join ( root , "out" ) ;
54+ }
55+
56+ function normalizeFoundryBytecode ( raw : any , key : "bytecode" | "deployedBytecode" ) : [ string , any ] {
57+ const value = raw [ key ] ;
58+ if ( ! value ) {
59+ return [ "0x" , { } ] ;
60+ }
61+
62+ if ( typeof value === "string" ) {
63+ const linksKey = key === "bytecode" ? "linkReferences" : "deployedLinkReferences" ;
64+ const links = raw [ linksKey ] ?? { } ;
65+ return [ value , links ] ;
66+ }
67+
68+ if ( typeof value === "object" ) {
69+ return [ value . object ?? "0x" , value . linkReferences ?? { } ] ;
70+ }
71+
72+ return [ "0x" , { } ] ;
73+ }
574
675/**
7- * Verify that the deployment init code (creation bytecode + encoded constructor args)
8- * matches the locally reconstructed init code from artifacts and recorded args.
9- *
10- * Compares keccak256(initCodeOnChain) vs keccak256(initCodeLocal).
11- *
12- * Sample usage:
13- * yarn hardhat verify-bytecode --contract Arbitrum_Adapter --network mainnet
14- * yarn hardhat verify-bytecode --contract Arbitrum_Adapter --tx-hash 0x... --network mainnet
15- * yarn hardhat verify-bytecode --contract X --tx-hash 0x... --libraries "MyLib=0x...,OtherLib=0x..." --network mainnet
76+ * Load a Foundry artifact (`out/...json`) and adapt it into a Hardhat-style artifact
77+ * that can be consumed by `ethers.getContractFactoryFromArtifact`.
78+ */
79+ function loadFoundryArtifact ( contractName : string ) : any {
80+ const outDir = getFoundryOutDir ( ) ;
81+ const candidates = [
82+ path . join ( outDir , `${ contractName } .sol` , `${ contractName } .json` ) ,
83+ path . join ( outDir , `${ contractName } .json` ) ,
84+ ] ;
85+
86+ const artifactPath = candidates . find ( ( p ) => fs . existsSync ( p ) ) ;
87+ if ( ! artifactPath ) {
88+ throw new Error (
89+ `Could not find Foundry artifact for contract "${ contractName } ". Tried:\n` +
90+ candidates . map ( ( p ) => ` - ${ p } ` ) . join ( "\n" )
91+ ) ;
92+ }
93+
94+ const rawJson = fs . readFileSync ( artifactPath , "utf8" ) ;
95+ const raw : any = JSON . parse ( rawJson ) ;
96+
97+ const abi = raw . abi ?? [ ] ;
98+ const [ bytecode , linkReferences ] = normalizeFoundryBytecode ( raw , "bytecode" ) ;
99+ const [ deployedBytecode , deployedLinkReferences ] = normalizeFoundryBytecode ( raw , "deployedBytecode" ) ;
100+
101+ return {
102+ _format : "hh-foundry-compat-0" ,
103+ contractName,
104+ sourceName : raw . sourceName ?? raw . source_name ?? path . basename ( artifactPath ) ,
105+ abi,
106+ bytecode,
107+ deployedBytecode,
108+ linkReferences,
109+ deployedLinkReferences,
110+ } ;
111+ }
112+
113+ function ensureForgeBuildArtifacts ( ) {
114+ try {
115+ // This keeps Foundry's `out/` artifacts up to date when verifying Foundry deployments.
116+ console . log ( "Running `forge build` to refresh Foundry artifacts..." ) ;
117+ execSync ( "forge build" , { stdio : "inherit" } ) ;
118+ } catch ( error : any ) {
119+ throw new Error ( `forge build failed: ${ error ?. message ?? String ( error ) } ` ) ;
120+ }
121+ }
122+
123+ /**
124+ Verify that the deployment init code (creation bytecode + encoded constructor args)
125+ matches the locally reconstructed init code from artifacts and recorded args.
126+
127+ Compares keccak256(initCodeOnChain) vs keccak256(initCodeLocal).
128+
129+ Sample usage:
130+ yarn hardhat verify-bytecode --contract Arbitrum_Adapter --network mainnet
131+ yarn hardhat verify-bytecode --contract Arbitrum_Adapter --tx-hash 0x... --network mainnet
132+ yarn hardhat verify-bytecode --contract X --tx-hash 0x... --libraries "MyLib=0x...,OtherLib=0x..." --network mainnet
133+
134+ For Foundry deployments that used `forge script --broadcast`, you can instead
135+ point this task at the Foundry broadcast JSON:
136+
137+ yarn hardhat verify-bytecode \
138+ --contract DstOFTHandler \
139+ --broadcast broadcast/DeployDstHandler.s.sol/999/run-latest.json \
140+ --network hyperevm
16141 */
17142task ( "verify-bytecode" , "Verify deploy transaction input against local artifacts" )
18143 . addOptionalParam ( "contract" , "Contract name; falls back to env CONTRACT" )
19144 // @dev For proxies, we don't save transactionHash in deployments/. You have to provide it manually via --tx-hash 0x... by checking e.g. block explorer first
20145 . addOptionalParam ( "txHash" , "Deployment transaction hash (defaults to deployments JSON)" )
21146 . addOptionalParam ( "libraries" , "Libraries to link. JSON string or 'Name=0x..,Other=0x..'" )
22- . setAction (
23- async ( args : { contract ?: string ; txHash ?: string ; libraries ?: string } , hre : HardhatRuntimeEnvironment ) => {
24- const { deployments, ethers, artifacts, network } = hre ;
147+ . addOptionalParam (
148+ "broadcast" ,
149+ "Path to Foundry broadcast JSON (e.g. broadcast/DeployFoo.s.sol/1/run-latest.json). " +
150+ "If set, constructor args and default txHash are taken from this file instead of hardhat-deploy deployments."
151+ )
152+ . setAction ( async ( args : VerifyBytecodeArgs , hre : HardhatRuntimeEnvironment ) => {
153+ const { deployments, ethers, artifacts, network } = hre ;
25154
26- // make sure we're using latest local contract artifacts for verification
155+ const useFoundryArtifacts = Boolean ( args . broadcast ) ;
156+
157+ // For Hardhat deployments, make sure we're using latest local Hardhat artifacts.
158+ if ( ! useFoundryArtifacts ) {
27159 await hre . run ( "compile" ) ;
160+ } else {
161+ // For Foundry deployments, refresh Foundry's `out/` artifacts instead.
162+ ensureForgeBuildArtifacts ( ) ;
163+ }
28164
29- const contractName = args . contract || process . env . CONTRACT ;
30- if ( ! contractName ) throw new Error ( "Please provide --contract or set CONTRACT env var" ) ;
165+ const contractName = args . contract || process . env . CONTRACT ;
166+ if ( ! contractName ) throw new Error ( "Please provide --contract or set CONTRACT env var" ) ;
31167
32- const deployment = await deployments . get ( contractName ) ;
33- const deployedAddress : string = deployment . address ;
34- const constructorArgs : any [ ] = deployment . args || [ ] ;
35-
36- const parseLibraries = ( s ?: string ) : Record < string , string > => {
37- if ( ! s ) return { } ;
38- const out : Record < string , string > = { } ;
39- const trimmed = s . trim ( ) ;
40- if ( trimmed . startsWith ( "{" ) && trimmed . endsWith ( "}" ) ) {
41- const parsed = JSON . parse ( trimmed ) ;
42- for ( const [ k , v ] of Object . entries ( parsed ) ) out [ k ] = String ( v ) ;
43- return out ;
44- }
45- for ( const part of trimmed . split ( / [ \, \n ] / ) ) {
46- const [ k , v ] = part . split ( "=" ) . map ( ( x ) => x . trim ( ) ) ;
47- if ( k && v ) out [ k ] = v ;
48- }
49- return out ;
168+ /**
169+ * Resolve constructor args, deployed address and default tx hash either from:
170+ * - hardhat-deploy deployments (default), or
171+ * - Foundry broadcast JSON (when --broadcast is provided).
172+ */
173+ let deployedAddress : string | undefined ;
174+ let constructorArgs : any [ ] = [ ] ;
175+ let defaultTxHash : string | undefined ;
176+
177+ if ( args . broadcast ) {
178+ const resolvedPath = path . isAbsolute ( args . broadcast ) ? args . broadcast : path . join ( process . cwd ( ) , args . broadcast ) ;
179+
180+ if ( ! fs . existsSync ( resolvedPath ) ) {
181+ throw new Error ( `Broadcast file not found at path ${ resolvedPath } ` ) ;
182+ }
183+
184+ // Narrow JSON structure to only what we need.
185+ type BroadcastTx = {
186+ hash ?: string ;
187+ transactionType ?: string ;
188+ contractName ?: string ;
189+ contractAddress ?: string ;
190+ arguments ?: any [ ] ;
191+ transaction ?: {
192+ input ?: string ;
193+ } ;
50194 } ;
195+ type BroadcastJson = {
196+ transactions ?: BroadcastTx [ ] ;
197+ } ;
198+
199+ const raw = fs . readFileSync ( resolvedPath , "utf8" ) ;
200+ const parsed : BroadcastJson = JSON . parse ( raw ) ;
201+ const txs = parsed . transactions || [ ] ;
51202
52- // Read local compilation artifact
53- const artifact = await artifacts . readArtifact ( contractName ) ;
54- console . log ( "Reading compilation artifact for" , artifact . sourceName ) ;
55-
56- /**
57- * TODO
58- * the `libraries` bit is untested. Could be wrong. Could remove this part if we don't have contracts with dynamic libraries
59- * artifact.linkReferences might help solve this better. Also, deployments.libraries. Implement only if required later.
60- */
61- const libraries : Record < string , string > = parseLibraries ( args . libraries ) ;
62- const factory = await ethers . getContractFactoryFromArtifact (
63- artifact ,
64- Object . keys ( libraries ) . length ? { libraries } : { }
203+ const createTxsForContract = txs . filter (
204+ ( tx ) => tx . transactionType === "CREATE" && tx . contractName === contractName
65205 ) ;
66206
67- // Note: `factory.getDeployTransaction` populates the transaction with whatever data we WOULD put in it if we were deploying it right now
68- const populatedDeployTransaction = factory . getDeployTransaction ( ...constructorArgs ) ;
69- const expectedInit : string = ethers . utils . hexlify ( populatedDeployTransaction . data ! ) . toLowerCase ( ) ;
70- if ( ! expectedInit || expectedInit === "0x" ) {
71- throw new Error ( "Failed to reconstruct deployment init code from local artifacts" ) ;
207+ if ( ! createTxsForContract . length ) {
208+ throw new Error ( `No CREATE transaction for contract "${ contractName } " found in broadcast file ${ resolvedPath } ` ) ;
209+ }
210+
211+ let selected : BroadcastTx ;
212+ if ( args . txHash ) {
213+ const match = createTxsForContract . find (
214+ ( tx ) => tx . hash && tx . hash . toLowerCase ( ) === args . txHash ! . toLowerCase ( )
215+ ) ;
216+ if ( ! match ) {
217+ throw new Error (
218+ `No CREATE transaction with hash ${ args . txHash } for contract "${ contractName } " in ${ resolvedPath } `
219+ ) ;
220+ }
221+ selected = match ;
222+ } else if ( createTxsForContract . length === 1 ) {
223+ selected = createTxsForContract [ 0 ] ;
224+ } else {
225+ const hashes = createTxsForContract
226+ . map ( ( tx ) => tx . hash )
227+ . filter ( Boolean )
228+ . join ( ", " ) ;
229+ throw new Error (
230+ `Multiple CREATE transactions for contract "${ contractName } " found in ${ resolvedPath } . ` +
231+ `Please re-run with --tx-hash set to one of: ${ hashes } `
232+ ) ;
233+ }
234+
235+ if ( ! selected . hash ) {
236+ throw new Error ( `Selected broadcast transaction for "${ contractName } " is missing a tx hash` ) ;
72237 }
73238
74- // Get on-chain creation input
75- const txHash = args . txHash ?? deployment . transactionHash ;
76- if ( ! txHash ) {
77- throw new Error ( "Could not find deployment tx hash. Pass --tx-hash when running script." ) ;
239+ deployedAddress = selected . contractAddress ;
240+ constructorArgs = selected . arguments || [ ] ;
241+ defaultTxHash = selected . hash ;
242+ } else {
243+ const deployment = await deployments . get ( contractName ) ;
244+ deployedAddress = deployment . address ;
245+ constructorArgs = deployment . args || [ ] ;
246+ defaultTxHash = deployment . transactionHash ;
247+ }
248+
249+ const parseLibraries = ( s ?: string ) : Record < string , string > => {
250+ if ( ! s ) return { } ;
251+ const out : Record < string , string > = { } ;
252+ const trimmed = s . trim ( ) ;
253+ if ( trimmed . startsWith ( "{" ) && trimmed . endsWith ( "}" ) ) {
254+ const parsed = JSON . parse ( trimmed ) ;
255+ for ( const [ k , v ] of Object . entries ( parsed ) ) out [ k ] = String ( v ) ;
256+ return out ;
78257 }
79- const tx = await ethers . provider . getTransaction ( txHash ) ;
80- if ( ! tx ) throw new Error ( `Transaction not found for hash ${ txHash } ` ) ;
81- if ( tx . to && tx . to != "" ) {
82- throw new Error ( `Transaction ${ txHash } is not a direct contract creation (tx.to=${ tx . to } )` ) ;
258+ for ( const part of trimmed . split ( / [ \, \n ] / ) ) {
259+ const [ k , v ] = part . split ( "=" ) . map ( ( x ) => x . trim ( ) ) ;
260+ if ( k && v ) out [ k ] = v ;
83261 }
262+ return out ;
263+ } ;
84264
85- const expectedHash = ethers . utils . keccak256 ( expectedInit ) ;
86- const onchainHash = ethers . utils . keccak256 ( tx . data . toLowerCase ( ) ) ;
265+ // Read local compilation artifact (Hardhat or Foundry) for reconstructing init code.
266+ const artifact = useFoundryArtifacts
267+ ? loadFoundryArtifact ( contractName )
268+ : await artifacts . readArtifact ( contractName ) ;
269+ console . log (
270+ "Reading compilation artifact for" ,
271+ ( artifact as any ) . sourceName ?? ( useFoundryArtifacts ? "<foundry>" : "<hardhat>" )
272+ ) ;
87273
88- console . log ( "\n=============== Deploy Tx Verification ===============" ) ;
89- console . log ( `Contract : ${ contractName } ` ) ;
90- console . log ( `Network : ${ network . name } ` ) ;
274+ /**
275+ * TODO
276+ * the `libraries` bit is untested. Could be wrong. Could remove this part if we don't have contracts with dynamic libraries
277+ * artifact.linkReferences might help solve this better. Also, deployments.libraries. Implement only if required later.
278+ */
279+ const libraries : Record < string , string > = parseLibraries ( args . libraries ) ;
280+ const factory = await ethers . getContractFactoryFromArtifact (
281+ artifact ,
282+ Object . keys ( libraries ) . length ? { libraries } : { }
283+ ) ;
284+
285+ // Note: `factory.getDeployTransaction` populates the transaction with whatever data we WOULD put in it if we were deploying it right now
286+ const populatedDeployTransaction = factory . getDeployTransaction ( ...constructorArgs ) ;
287+ const expectedInit : string = ethers . utils . hexlify ( populatedDeployTransaction . data ! ) . toLowerCase ( ) ;
288+ if ( ! expectedInit || expectedInit === "0x" ) {
289+ throw new Error ( "Failed to reconstruct deployment init code from local artifacts" ) ;
290+ }
291+
292+ // Get on-chain creation input
293+ const txHash = args . txHash ?? defaultTxHash ;
294+ if ( ! txHash ) {
295+ throw new Error (
296+ "Could not find deployment tx hash. Pass --tx-hash when running script, " +
297+ "or ensure deployments / broadcast metadata includes it."
298+ ) ;
299+ }
300+ const tx = await ethers . provider . getTransaction ( txHash ) ;
301+ if ( ! tx ) throw new Error ( `Transaction not found for hash ${ txHash } ` ) ;
302+ if ( tx . to && tx . to != "" ) {
303+ throw new Error ( `Transaction ${ txHash } is not a direct contract creation (tx.to=${ tx . to } )` ) ;
304+ }
305+
306+ const expectedHash = ethers . utils . keccak256 ( expectedInit ) ;
307+ const onchainHash = ethers . utils . keccak256 ( tx . data . toLowerCase ( ) ) ;
308+
309+ console . log ( "\n=============== Deploy Tx Verification ===============" ) ;
310+ console . log ( `Contract : ${ contractName } ` ) ;
311+ console . log ( `Network : ${ network . name } ` ) ;
312+ if ( deployedAddress ) {
91313 console . log ( `Deployed address : ${ deployedAddress } ` ) ;
92- if ( txHash ) console . log ( `Tx hash : ${ txHash } ` ) ;
93- console . log ( "-------------------------------------------------------" ) ;
94- console . log ( `On-chain init hash : ${ onchainHash } ` ) ;
95- console . log ( `Local init hash : ${ expectedHash } ` ) ;
96- console . log ( "-------------------------------------------------------" ) ;
97- console . log ( onchainHash === expectedHash ? "✅ MATCH" : "❌ MISMATCH – init code differs" ) ;
98- console . log ( "=======================================================\n" ) ;
99314 }
100- ) ;
315+ if ( args . broadcast ) {
316+ console . log ( `Broadcast file : ${ args . broadcast } ` ) ;
317+ }
318+ if ( txHash ) console . log ( `Tx hash : ${ txHash } ` ) ;
319+ console . log ( "-------------------------------------------------------" ) ;
320+ console . log ( `On-chain init hash : ${ onchainHash } ` ) ;
321+ console . log ( `Local init hash : ${ expectedHash } ` ) ;
322+ console . log ( "-------------------------------------------------------" ) ;
323+ console . log ( onchainHash === expectedHash ? "✅ MATCH" : "❌ MISMATCH – init code differs" ) ;
324+ console . log ( "=======================================================\n" ) ;
325+ } ) ;
0 commit comments