Skip to content

Commit 6b39c24

Browse files
0xClandestineypatil12
authored andcommitted
feat: storage-diff.sh (#1054)
* feat: storage-diff.sh * refactor: storage-diff.sh * refactor: storage-diff.sh * refactor: storage-diff.sh * refactor: storage-diff.sh * refactor: storage-diff.sh
1 parent ebf555d commit 6b39c24

File tree

1 file changed

+328
-0
lines changed

1 file changed

+328
-0
lines changed

bin/storage-diff.sh

Lines changed: 328 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,328 @@
1+
#!/bin/bash
2+
3+
# Default values
4+
RPC_URL="https://eth.llamarpc.com"
5+
ETHERSCAN_API_KEY="1234567890123456789012345678901234567890"
6+
INPUT_FILE="contracts.json"
7+
QUIET=false
8+
TOTAL_ISSUES=0
9+
10+
# Help message
11+
usage() {
12+
cat << EOF
13+
Usage: bash $0 --rpc-url $RPC_URL --etherscan-key $ETHERSCAN_API_KEY --input $INPUT_FILE [--quiet] [--help]
14+
15+
Detects storage layout incompatibilities that could cause issues during upgrades.
16+
17+
Required:
18+
-r, --rpc-url <url> RPC endpoint URL for the target network (default: https://eth.llamarpc.com).
19+
-e, --etherscan-key <key> API key for Etherscan to fetch contract data.
20+
21+
Options:
22+
-i, --input <file> JSON file containing contract details, see format below (default: contracts.json).
23+
If not provided, reads from stdin.
24+
-q, --quiet Suppress informational output.
25+
-h, --help Show this help message.
26+
27+
28+
Input JSON format:
29+
{
30+
"contracts": [
31+
{
32+
"name": "AVSDirectory",
33+
"address": "0x135dda560e946695d6f155dacafc6f1f25c1f5af"
34+
},
35+
{
36+
"name": "DelegationManager",
37+
"address": "0x39053D51B77DC0d36036Fc1fCc8Cb819df8Ef37A"
38+
}
39+
]
40+
}
41+
EOF
42+
exit 1
43+
}
44+
45+
# Process command line arguments using a while loop and case statement.
46+
while [[ $# -gt 0 ]]; do
47+
case $1 in
48+
-r|--rpc-url)
49+
RPC_URL="$2"
50+
shift 2
51+
;;
52+
-e|--etherscan-key)
53+
ETHERSCAN_API_KEY="$2"
54+
shift 2
55+
;;
56+
-i|--input)
57+
INPUT_FILE="$2"
58+
shift 2
59+
;;
60+
-q|--quiet)
61+
QUIET=true
62+
shift
63+
;;
64+
-h|--help)
65+
usage
66+
;;
67+
*)
68+
echo "Unknown option: $1"
69+
usage
70+
;;
71+
esac
72+
done
73+
74+
# Validate required arguments
75+
if [ -z "$RPC_URL" ] || [ -z "$ETHERSCAN_API_KEY" ]; then
76+
echo "Error: RPC URL and Etherscan API key are required"
77+
usage
78+
fi
79+
80+
# Read JSON input
81+
if [ -n "$INPUT_FILE" ]; then
82+
if [ ! -f "$INPUT_FILE" ]; then
83+
echo "Error: Input file not found: $INPUT_FILE"
84+
exit 1
85+
fi
86+
json_input=$(cat "$INPUT_FILE")
87+
else
88+
json_input=$(cat)
89+
fi
90+
91+
# Parse JSON values using jq
92+
CONTRACTS=$(echo "$json_input" | jq -c '.contracts[]')
93+
94+
# Verify contracts are specified
95+
if [ -z "$CONTRACTS" ]; then
96+
echo "Error: No contracts specified in JSON input"
97+
exit 1
98+
fi
99+
100+
# Function to calculate number of slots a variable type occupies
101+
calculate_slots() {
102+
local var_type=$1
103+
104+
# Handle basic types
105+
case $var_type in
106+
*"uint256"*|*"int256"*|*"bytes32"*|*"address"*)
107+
echo 1
108+
;;
109+
*"mapping"*)
110+
echo 1 # Mappings use 1 slot for the starting position
111+
;;
112+
*"bytes"*|*"string"*)
113+
echo 1 # Dynamic types use 1 slot for length/pointer
114+
;;
115+
*"array"*)
116+
echo 1 # Arrays use 1 slot for length/pointer, need to figure out how to parse slots consumed.
117+
;;
118+
*)
119+
# Default to 1 slot if unknown
120+
echo 1
121+
;;
122+
esac
123+
}
124+
125+
# Function to analyze storage changes
126+
analyze_storage_changes() {
127+
local onchain_file=$1
128+
local local_file=$2
129+
local contract_name=$3
130+
local issues_found=0
131+
132+
# Get the storage layouts as arrays
133+
local onchain_slots=$(jq -r '.storage[] | "\(.slot)|\(.label)|\(.offset)|\(.type)"' "$onchain_file")
134+
local local_slots=$(jq -r '.storage[] | "\(.slot)|\(.label)|\(.offset)|\(.type)"' "$local_file")
135+
136+
echo "Storage Layout Analysis for $contract_name:"
137+
echo "----------------------------------------"
138+
139+
# Create temporary files for our data structures
140+
local onchain_map_file=$(mktemp)
141+
local local_map_file=$(mktemp)
142+
local processed_slots_file=$(mktemp)
143+
local renamed_vars_file=$(mktemp)
144+
145+
# Parse onchain slots
146+
echo "$onchain_slots" | while IFS='|' read -r slot label offset type; do
147+
if [[ -n "$slot" ]]; then
148+
echo "${slot}|${label}|${offset}|${type}" >> "$onchain_map_file"
149+
fi
150+
done
151+
152+
# Parse local slots
153+
echo "$local_slots" | while IFS='|' read -r slot label offset type; do
154+
if [[ -n "$slot" ]]; then
155+
echo "${slot}|${label}|${offset}|${type}" >> "$local_map_file"
156+
fi
157+
done
158+
159+
# First pass: Check for renames (same slot, same type, different name)
160+
while IFS='|' read -r slot local_label local_offset local_type; do
161+
if [[ -z "$slot" ]]; then continue; fi
162+
163+
# Look for matching slot in onchain
164+
onchain_line=$(grep "^${slot}|" "$onchain_map_file")
165+
if [[ -n "$onchain_line" ]]; then
166+
IFS='|' read -r _ onchain_label onchain_offset onchain_type <<< "$onchain_line"
167+
168+
if [[ "$local_label" != "$onchain_label" && "$local_type" == "$onchain_type" && "$local_offset" == "$onchain_offset" ]]; then
169+
echo "${slot}|${onchain_label}|${local_label}|${local_type}" >> "$renamed_vars_file"
170+
echo "$slot" >> "$processed_slots_file"
171+
issues_found=$((issues_found + 1))
172+
fi
173+
fi
174+
done < "$local_map_file"
175+
176+
# Print renames first
177+
while IFS='|' read -r slot old_name new_name type; do
178+
if [[ -n "$slot" ]]; then
179+
echo -e "\033[36m📝 Variable renamed at slot $slot:\033[0m"
180+
echo -e "\033[36m $old_name -> $new_name ($type)\033[0m"
181+
fi
182+
done < "$renamed_vars_file"
183+
184+
# Analyze other differences
185+
while IFS='|' read -r slot local_label local_offset local_type; do
186+
if [[ -z "$slot" ]]; then continue; fi
187+
188+
# Skip if this slot was processed as a rename
189+
if grep -q "^${slot}$" "$processed_slots_file"; then
190+
continue
191+
fi
192+
193+
# Look for matching slot in onchain
194+
onchain_line=$(grep "^${slot}|" "$onchain_map_file")
195+
if [[ -z "$onchain_line" ]]; then
196+
# New variable added
197+
slots_needed=$(calculate_slots "$local_type")
198+
echo -e "\033[32m✨ New variable added: $local_label ($local_type) at slot $slot\033[0m"
199+
issues_found=$((issues_found + 1))
200+
if [ "$slots_needed" -gt 1 ]; then
201+
echo -e "\033[33m 📦 This variable occupies $slots_needed slots\033[0m"
202+
fi
203+
else
204+
IFS='|' read -r _ onchain_label onchain_offset onchain_type <<< "$onchain_line"
205+
206+
if [[ "$local_label" != "$onchain_label" ]]; then
207+
echo -e "\033[31m🚨 Storage slot override detected at slot $slot:\033[0m"
208+
echo -e "\033[31m Previous: $onchain_label ($onchain_type)\033[0m"
209+
echo -e "\033[32m New: $local_label ($local_type)\033[0m"
210+
issues_found=$((issues_found + 1))
211+
212+
# Calculate potential impact
213+
old_slots=$(calculate_slots "$onchain_type")
214+
new_slots=$(calculate_slots "$local_type")
215+
slot_diff=$((new_slots - old_slots))
216+
217+
if [ "$slot_diff" -gt 0 ]; then
218+
echo -e "\033[33m ⚠️ This change will shift subsequent storage slots by +$slot_diff positions\033[0m"
219+
elif [ "$slot_diff" -lt 0 ]; then
220+
echo -e "\033[33m 💡 This change will reduce storage usage by $((slot_diff * -1)) slots\033[0m"
221+
fi
222+
elif [[ "$local_type" != "$onchain_type" ]]; then
223+
echo -e "\033[33m🔄 Type change detected for $local_label at slot $slot:\033[0m"
224+
echo -e "\033[31m Previous: $onchain_type\033[0m"
225+
echo -e "\033[32m New: $local_type\033[0m"
226+
issues_found=$((issues_found + 1))
227+
fi
228+
fi
229+
echo "$slot" >> "$processed_slots_file"
230+
done < "$local_map_file"
231+
232+
# Check for removed variables
233+
while IFS='|' read -r slot onchain_label onchain_offset onchain_type; do
234+
if [[ -z "$slot" ]]; then continue; fi
235+
236+
# Skip if this slot was processed as a rename or already handled
237+
if grep -q "^${slot}$" "$processed_slots_file"; then
238+
continue
239+
fi
240+
241+
# Look for matching slot in local
242+
if ! grep -q "^${slot}|" "$local_map_file"; then
243+
echo -e "\033[31m➖ Variable removed: $onchain_label ($onchain_type) from slot $slot\033[0m"
244+
issues_found=$((issues_found + 1))
245+
fi
246+
done < "$onchain_map_file"
247+
248+
# Cleanup temporary files
249+
rm -f "$onchain_map_file" "$local_map_file" "$processed_slots_file" "$renamed_vars_file"
250+
251+
echo "Issues found in $contract_name: $issues_found"
252+
return $issues_found
253+
}
254+
255+
# Function to process a single contract
256+
process_contract() {
257+
local contract_name=$1
258+
local contract_address=$2
259+
local issues_found=0
260+
261+
# Create directories for storing layouts and diffs
262+
mkdir -p "storage-report/layouts" "storage-report/diffs"
263+
local local_file="storage-report/layouts/${contract_name}_local.json"
264+
local onchain_file="storage-report/layouts/${contract_name}_onchain.json"
265+
local diff_file="storage-report/diffs/${contract_name}.diff"
266+
267+
# Generate storage layouts
268+
if ! forge inspect "$contract_name" storage --json > "$local_file" 2>/dev/null; then
269+
echo "Error: forge inspect failed for contract: $contract_name"
270+
return 1
271+
fi
272+
273+
if ! cast storage "$contract_address" --rpc-url "$RPC_URL" --etherscan-api-key "$ETHERSCAN_API_KEY" --json > "$onchain_file" 2>/dev/null; then
274+
echo "Error: cast storage failed for address: $contract_address"
275+
return 1
276+
fi
277+
278+
# Delete the first line of $onchain_file
279+
sed '1d' "$onchain_file" > "$onchain_file.tmp" && mv "$onchain_file.tmp" "$onchain_file"
280+
281+
# Filter out astId and contract fields from local and onchain files, and normalize type identifiers
282+
jq 'del(.types, .values, .storage[].astId, .storage[].contract) | .storage[].type |= gsub("\\([^)]+\\)[0-9]+"; "")' "$local_file" > "${local_file}.tmp" && mv "${local_file}.tmp" "$local_file"
283+
jq 'del(.types, .values, .storage[].astId, .storage[].contract) | .storage[].type |= gsub("\\([^)]+\\)[0-9]+"; "")' "$onchain_file" > "${onchain_file}.tmp" && mv "${onchain_file}.tmp" "$onchain_file"
284+
285+
if [ "$QUIET" = false ]; then
286+
echo "----------------------------------------"
287+
echo "Local contract: $contract_name"
288+
echo "Chain address: $contract_address"
289+
echo "Network RPC: $RPC_URL"
290+
echo "JSON files stored in: storage-report/layouts/"
291+
echo "Diffs stored in: storage-report/diffs/"
292+
echo "----------------------------------------"
293+
fi
294+
295+
# Generate and store diff
296+
diff -u "$onchain_file" "$local_file" > "$diff_file" 2>/dev/null || true
297+
298+
# Analyze storage changes
299+
analyze_storage_changes "$onchain_file" "$local_file" "$contract_name"
300+
issues_found=$?
301+
302+
return $issues_found
303+
}
304+
305+
# Process each contract from the JSON input
306+
while IFS= read -r contract; do
307+
contract_name=$(echo "$contract" | jq -r '.name')
308+
contract_address=$(echo "$contract" | jq -r '.address')
309+
310+
if [ -z "$contract_name" ] || [ -z "$contract_address" ]; then
311+
echo "Error: Each contract must specify both name and address"
312+
continue
313+
fi
314+
315+
if [ "$QUIET" = false ]; then
316+
echo "Processing contract: $contract_name at $contract_address"
317+
fi
318+
process_contract "$contract_name" "$contract_address"
319+
TOTAL_ISSUES=$((TOTAL_ISSUES + $?))
320+
done <<< "$CONTRACTS"
321+
322+
if [ "$TOTAL_ISSUES" -gt 0 ]; then
323+
echo -e "\n\033[31m🚨 Total storage layout issues found: $TOTAL_ISSUES\033[0m"
324+
exit 1
325+
else
326+
echo -e "\n\033[32m✅ No storage layout issues found\033[0m"
327+
exit 0
328+
fi

0 commit comments

Comments
 (0)