|
| 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