11#! /bin/bash
2+ set -euo pipefail
23
34# This script verifies the bytecode of a contract onchain matches the bytecode in the artifact
45# It takes the following arguments:
89
910# Note that this script doesn't take into account any link libraries that are used in the contract
1011
11- args=(" $@ " )
12+ # Example commands:
13+ # ./scripts/verifyBytecode.sh 0x2015905f5cbdb4afeba30deb6dc0f0f779ba4af5bc43edceabf0bf4343cb290b "$NODE_URL_1" SponsoredCCTPSrcPeriphery script/mintburn/cctp/DeploySponsoredCCTPSrcPeriphery.s.sol
14+ # ./scripts/verifyBytecode.sh --broadcast "$NODE_URL_1" SponsoredCCTPSrcPeriphery script/mintburn/cctp/DeploySponsoredCCTPSrcPeriphery.s.sol
1215
13- if [ ${# args[@]} -ne 3 ]; then
14- echo " Usage: $0 <tx_hash> <rpc_url> <contract_name>"
16+ strip_cbor_metadata () {
17+ local hex=" $1 "
18+ local n=${# hex}
19+ if (( n < 4 )) ; then
20+ echo " $hex "
21+ return
22+ fi
23+
24+ local len_hex=" ${hex: $((n - 4)): 4} "
25+ [[ " $len_hex " =~ ^[0-9a-fA-F]{4}$ ]] || { echo " $hex " ; return ; }
26+
27+ local metadata_bytes=$(( 16 #$len_hex ))
28+ local remove_nibbles=$(( metadata_bytes * 2 + 4 ))
29+ if (( remove_nibbles >= n )) ; then
30+ echo " $hex "
31+ return
32+ fi
33+
34+ echo " ${hex: 0: $((n - remove_nibbles))} "
35+ }
36+
37+ if [[ " $1 " != " --broadcast" && $# -ne 4 ]] || [[ " $1 " == " --broadcast" && $# -ne 4 ]]; then
38+ echo " Usage:"
39+ echo " $0 <tx_hash> <rpc_url> <contract_name> <script_path|run_json_path>"
40+ echo " $0 --broadcast <rpc_url> <contract_name> <script_path|run_json_path>"
1541 exit 1
1642fi
1743
18- TX=${args[0]}
19- RPC=${args[1]}
20- CONTRACT_NAME=${args[2]}
44+ if [[ " $1 " == " --broadcast" ]]; then
45+ RPC=" $2 "
46+ CONTRACT_NAME=" $3 "
47+ BROADCAST_REF=" $4 "
48+ if [[ " $BROADCAST_REF " == * .json ]]; then
49+ RUN_JSON=" $BROADCAST_REF "
50+ else
51+ CHAIN_ID=$( cast chain-id --rpc-url " $RPC " )
52+ RUN_JSON=" broadcast/$( basename " $BROADCAST_REF " ) /$CHAIN_ID /run-latest.json"
53+ [[ -f " $RUN_JSON " ]] || RUN_JSON=" broadcast/$( basename " $BROADCAST_REF " ) /$CHAIN_ID /dry-run/run-latest.json"
54+ fi
55+ [[ -f " $RUN_JSON " ]] || { echo " run json not found: $RUN_JSON " ; exit 1; }
56+
57+ TX_ENTRY=$( jq -c --arg name " $CONTRACT_NAME " ' [.transactions[] | select(.transactionType=="CREATE" and .contractName==$name)][-1]' " $RUN_JSON " )
58+ [[ " $TX_ENTRY " != " null" ]] || { echo " no CREATE tx found for $CONTRACT_NAME in $RUN_JSON " ; exit 1; }
59+ TX=$( jq -r ' .hash' <<< " $TX_ENTRY" )
60+ else
61+ TX=" $1 "
62+ RPC=" $2 "
63+ CONTRACT_NAME=" $3 "
64+ BROADCAST_REF=" $4 "
65+ if [[ " $BROADCAST_REF " == * .json ]]; then
66+ RUN_JSON=" $BROADCAST_REF "
67+ else
68+ CHAIN_ID=$( cast chain-id --rpc-url " $RPC " )
69+ RUN_JSON=" broadcast/$( basename " $BROADCAST_REF " ) /$CHAIN_ID /run-latest.json"
70+ [[ -f " $RUN_JSON " ]] || RUN_JSON=" broadcast/$( basename " $BROADCAST_REF " ) /$CHAIN_ID /dry-run/run-latest.json"
71+ fi
72+ [[ -f " $RUN_JSON " ]] || { echo " run json not found: $RUN_JSON " ; exit 1; }
73+
74+ TX_ENTRY=$( jq -c --arg tx " $TX " --arg name " $CONTRACT_NAME " ' [.transactions[]? | select((((.hash // "") | tostring | ascii_downcase) == ($tx | ascii_downcase)) and .transactionType=="CREATE" and .contractName==$name)][0]' " $RUN_JSON " )
75+ [[ " $TX_ENTRY " != " null" ]] || {
76+ echo " tx $TX for CREATE $CONTRACT_NAME not found in $RUN_JSON "
77+ echo " Hint: run-latest.json may have an incorrect/stale CREATE tx hash. You can try to FIX MANUALLY."
78+ echo " Hint: pass the exact broadcast run JSON that contains this tx hash."
79+ exit 1
80+ }
81+ fi
2182
22- ONCHAIN=$( cast tx $TX --rpc-url $RPC --json | jq -r ' .input' | sed ' s/^0x//' )
83+ ONCHAIN=$( cast tx " $TX " --rpc-url " $RPC " --json | jq -r ' .input' | sed ' s/^0x//' )
2384
2485ART=out/$CONTRACT_NAME .sol/$CONTRACT_NAME .json
86+ [[ -f " $ART " ]] || { echo " artifact not found: $ART " ; exit 1; }
2587
2688CREATION=$( jq -r ' .bytecode.object' " $ART " | sed ' s/^0x//' )
2789
28- CODE_ONCHAIN= ${ONCHAIN : 0 : ${ # CREATION} }
90+ LOCAL_INIT= " $ CREATION"
2991
30- cast keccak $CODE_ONCHAIN
31- cast keccak $CREATION
92+ if [[ -n " ${TX_ENTRY:- } " && " $TX_ENTRY " != " null" ]]; then
93+ CONSTRUCTOR_TYPES=$( jq -r ' ([.abi[]? | select(.type=="constructor")][0].inputs // []) | map(.type) | join(",")' " $ART " )
94+ CONSTRUCTOR_SIG=" constructor($CONSTRUCTOR_TYPES )"
95+ CONSTRUCTOR_ARGS=()
96+ while IFS= read -r arg; do
97+ CONSTRUCTOR_ARGS+=(" $arg " )
98+ done < <( jq -cr ' .arguments[]?' <<< " $TX_ENTRY" )
99+ ENCODED_ARGS=$( cast abi-encode " $CONSTRUCTOR_SIG " " ${CONSTRUCTOR_ARGS[@]} " | sed ' s/^0x//' )
100+ LOCAL_INIT=" ${CREATION}${ENCODED_ARGS} "
101+ [[ -n " ${RUN_JSON:- } " ]] && echo " Using constructor args from: $RUN_JSON "
102+ fi
103+
104+ echo -n " 0x$ONCHAIN " | cast keccak
105+ echo -n " 0x$LOCAL_INIT " | cast keccak
32106
33- if [[ $CODE_ONCHAIN == $CREATION ]]; then
107+ if [[ " $ONCHAIN " == " $LOCAL_INIT " ]]; then
34108 echo " ✅ Code match"
109+ elif [[ -n " ${ENCODED_ARGS:- } " ]]; then
110+ ONCHAIN_CREATION=" ${ONCHAIN: 0: ${# CREATION} } "
111+ ONCHAIN_ARGS=" ${ONCHAIN: ${# CREATION} : ${# ENCODED_ARGS} } "
112+ STRIPPED_ONCHAIN_CREATION=$( strip_cbor_metadata " $ONCHAIN_CREATION " )
113+ STRIPPED_LOCAL_CREATION=$( strip_cbor_metadata " $CREATION " )
114+ if [[ " $ONCHAIN_ARGS " == " $ENCODED_ARGS " && " $STRIPPED_ONCHAIN_CREATION " == " $STRIPPED_LOCAL_CREATION " ]]; then
115+ echo " ✅ Code match (metadata hash differs)"
116+ exit 0
117+ fi
118+ echo " ❌ Code mismatch"
119+ echo " Onchain bytes : ${# ONCHAIN} "
120+ echo " Local bytes : ${# LOCAL_INIT} "
121+ echo " Hint: compare verified compiler settings (solc/optimizer runs/via-ir/evmVersion) against your local build."
122+ echo " Hint: metadata-only drift is common; if constructor args match, check explorer verification details."
123+
124+ # Focused diagnostics: avoid printing giant payloads.
125+ echo -n " 0x$ONCHAIN_CREATION " | cast keccak
126+ echo -n " 0x$CREATION " | cast keccak
127+
128+ echo -n " 0x$ONCHAIN_ARGS " | cast keccak
129+ echo -n " 0x$ENCODED_ARGS " | cast keccak
130+ if [[ " $ONCHAIN_ARGS " == " $ENCODED_ARGS " ]]; then
131+ echo " Constructor args: ✅ match"
132+ else
133+ echo " Constructor args: ❌ mismatch"
134+ fi
135+
136+ FIRST_DIFF=-1
137+ for (( i= 0 ; i< ${# ONCHAIN} ; i++ )) ; do
138+ if [[ " ${ONCHAIN: $i : 1} " != " ${LOCAL_INIT: $i : 1} " ]]; then
139+ FIRST_DIFF=$i
140+ break
141+ fi
142+ done
143+ if [[ $FIRST_DIFF -ge 0 ]]; then
144+ BYTE_INDEX=$(( FIRST_DIFF / 2 ))
145+ START=$(( FIRST_DIFF - 20 ))
146+ (( START < 0 )) && START=0
147+ echo " First differing nibble index: $FIRST_DIFF (byte ~$BYTE_INDEX )"
148+ echo " Onchain snippet: ${ONCHAIN: $START : 80} "
149+ echo " Local snippet : ${LOCAL_INIT: $START : 80} "
150+ fi
151+ exit 1
35152else
36153 echo " ❌ Code mismatch"
154+ echo " Onchain bytes : ${# ONCHAIN} "
155+ echo " Local bytes : ${# LOCAL_INIT} "
156+ echo " Hint: compare verified compiler settings (solc/optimizer runs/via-ir/evmVersion) against your local build."
157+ echo " Hint: metadata-only drift is common; if constructor args match, check explorer verification details."
158+ FIRST_DIFF=-1
159+ for (( i= 0 ; i< ${# ONCHAIN} ; i++ )) ; do
160+ if [[ " ${ONCHAIN: $i : 1} " != " ${LOCAL_INIT: $i : 1} " ]]; then
161+ FIRST_DIFF=$i
162+ break
163+ fi
164+ done
165+ if [[ $FIRST_DIFF -ge 0 ]]; then
166+ BYTE_INDEX=$(( FIRST_DIFF / 2 ))
167+ START=$(( FIRST_DIFF - 20 ))
168+ (( START < 0 )) && START=0
169+ echo " First differing nibble index: $FIRST_DIFF (byte ~$BYTE_INDEX )"
170+ echo " Onchain snippet: ${ONCHAIN: $START : 80} "
171+ echo " Local snippet : ${LOCAL_INIT: $START : 80} "
172+ fi
173+ exit 1
37174fi
0 commit comments