From 8a95e6f7f97d599187126008eb7c367fa5f1c12f Mon Sep 17 00:00:00 2001 From: Pankaj Jagtap Date: Thu, 13 Nov 2025 16:12:09 -0500 Subject: [PATCH 1/2] chore: Update foundry.toml with linting and formatting configurations; add bytecode verification script --- foundry.toml | 106 +++++++++++++- script/verify-forge-fmt-bytecode.sh | 210 ++++++++++++++++++++++++++++ 2 files changed, 315 insertions(+), 1 deletion(-) create mode 100755 script/verify-forge-fmt-bytecode.sh diff --git a/foundry.toml b/foundry.toml index 56297c8d3..585d4d12a 100644 --- a/foundry.toml +++ b/foundry.toml @@ -18,7 +18,9 @@ max_shrink_iters = 100 mainnet = "${MAINNET_RPC_URL}" goerli = "${GOERLI_RPC_URL}" - +[lint] +ignore = ["test/**", "src/archive/**"] +exclude_lints = ["screaming-snake-case-immutable","screaming-snake-case-const","mixed-case-variable","mixed-case-function","camelcase-constants","pascal-case-struct","unaliased-plain-import","unaliased-import"] [etherscan] mainnet = { key = "${ETHERSCAN_API_KEY}" } @@ -26,3 +28,105 @@ goerli = { key = "${ETHERSCAN_API_KEY}" } # Used for verifying contracts on Tenderly Virtual Testnet unknown_chain = { key = "${TENDERLY_ACCESS_KEY}", chain =1, url = "${TENDERLY_VIRTUAL_TESTNET_RPC_URL}/verify/etherscan" } + +# ============================================================================== +# FORMATTER CONFIGURATION (forge fmt) +# ============================================================================== +# Comprehensive Solidity code formatting settings for consistent style across +# the entire codebase. These settings control how `forge fmt` formats your code. +[fmt] +# Maximum line length before wrapping (set very high to prevent line splitting) +# Setting to 999 essentially disables line wrapping, keeping code on single lines +# where possible. This prevents the formatter from splitting long statements. +line_length = 999 + +# Number of spaces per indentation level +# Controls the width of each indentation level. 4 spaces is the Solidity +# community standard and provides good readability while keeping code compact. +tab_width = 4 + +# Whether to add spaces inside array/function call brackets +# true: function( arg1, arg2 ) or array[ index ] +# false: function(arg1, arg2) or array[index] +# true improves readability, especially with complex expressions +bracket_spacing = false + +# String literal quote style: "double" or "single" +# "double" uses "string" (standard for most languages) +# "single" uses 'string' (some prefer this for Solidity) +# Double quotes are the Solidity convention and default compiler behavior +quote_style = "double" + +# Number formatting: where to place underscores in numeric literals +# "thousands": 1_000_000 (groups by thousands - most readable for large numbers) +# "none": 1000000 (no underscores - compact but harder to read) +# "decimal": 1000.000 (groups only around decimal point) +# Thousands separator improves readability for large numbers like wei amounts +number_underscore = "thousands" + +# Whether function bodies can be on a single line +# false: Always use braces and newlines even for single-statement functions +# true: Allows single-line bodies like "function f() external { doSomething(); }" +# false is safer and more consistent, enforcing explicit structure +multiline_func_body = false + +# Opening brace placement for code blocks +# false: Opening brace on same line (K&R style) +# if (condition) { +# code +# } +# true: Opening brace on new line (Allman style) +# if (condition) +# { +# code +# } +# Same-line braces are more compact and standard in Solidity community +multiline_block_brace = false + +# Whether single-line statement blocks are allowed +# "preserve": Keeps existing formatting (no change) +# "single": Allows single-line blocks without braces: if (condition) doSomething(); +# "multi": Always requires braces and multi-line formatting (safer, prevents bugs) +# if (condition) { +# doSomething(); +# } +# "multi" is safer as it prevents bugs from adding code to if statements +single_line_statement_blocks = "preserve" + +# Whether to add blank lines between contract/interface/library definitions +# false: No extra lines between definitions (compact) +# true: Adds blank lines between contracts for visual separation +# false keeps the file more compact, which helps with navigation +contract_new_lines = false + +# Whether to format inline Yul assembly code +# false: Leave Yul code as-is (assembly formatting is tricky) +# true: Attempt to format Yul code (may break some assembly) +# false is recommended as assembly often requires precise formatting +yul = false + +# Whether to sort imports alphabetically, preventing duplicate imports +sort_imports = true + +# true: import {Foo} -> import { Foo } (Solidity standard) +# false: import { Foo } -> import {Foo} (Solidity standard) +# false is recommended as it is the Solidity standard +import_spacing = false + +# Integer type preference: "long" (uint256) or "short" (uint) +# "long" uses explicit types like uint256, int256, bytes32 +# "short" uses aliases like uint, int, bytes32 (bytes32 stays same) +# Long is preferred for clarity and avoids confusion (uint is uint256, not uint8) +# int_types = "long" +int_types = "preserve" + +# Multiline function header formatting style +# "params_first": Parameters start on new line, type comes first +# function transfer( +# address to, +# uint256 amount +# ) external +# "all_one_line": Everything on one line if possible +# "types_first": Type comes before parameter name +# Since line_length is high, this mainly affects explicitly multiline functions +# multiline_func_header = "all_one_line" diff --git a/script/verify-forge-fmt-bytecode.sh b/script/verify-forge-fmt-bytecode.sh new file mode 100755 index 000000000..32c1f6291 --- /dev/null +++ b/script/verify-forge-fmt-bytecode.sh @@ -0,0 +1,210 @@ +#!/bin/bash + +# Command to Run: +# 1. chmod +x script/verify-forge-fmt-bytecode.sh +# 2. ./script/verify-forge-fmt-bytecode.sh + +set -e + +echo "🔍 Verifying that forge fmt doesn't change bytecode..." + +# Define cache directory +CACHE_DIR="cache/bytecodeComparison" + +# Create cache directory +mkdir -p "$CACHE_DIR" + +# Step 1: Compile and save bytecode before formatting +echo "📝 Compiling contracts before formatting..." +forge build --force + +echo "💾 Extracting bytecode for each contract (pre-format)..." + +# Create a temporary directory to store pre-format data +TEMP_BEFORE=$(mktemp -d) + +# Extract bytecode from each contract's JSON artifact (excluding build-info) +find out -name "*.json" -type f ! -path "*/build-info/*" | while read -r file; do + # Get relative path from out directory + rel_path="${file#out/}" + + # Get everything except the filename itself to preserve directory structure + dir_structure=$(dirname "$rel_path") + filename=$(basename "$rel_path" .json) + + # Full path preserves the directory structure + full_path="${dir_structure}/${filename}" + + # Extract contract name and bytecode + contract_name=$(basename "${file%.json}") + bytecode=$(jq -r '.deployedBytecode.object // .bytecode.object // ""' "$file" 2>/dev/null || echo "") + + # Save to temp file with proper directory structure + mkdir -p "$TEMP_BEFORE/$dir_structure" + echo "$bytecode" > "$TEMP_BEFORE/${full_path}.bytecode" + echo "$contract_name|$rel_path" > "$TEMP_BEFORE/${full_path}.meta" +done + +echo "✅ Pre-format bytecode extracted" + +# Step 2: Run forge fmt +echo "🎨 Running forge fmt..." +forge fmt + +# Step 3: Compile again after formatting +echo "📝 Recompiling contracts after formatting..." +forge build --force + +echo "💾 Extracting bytecode for each contract (post-format)..." + +# Extract post-format bytecode and create combined JSON files +find out -name "*.json" -type f ! -path "*/build-info/*" | while read -r file; do + rel_path="${file#out/}" + + # Preserve the directory structure + dir_structure=$(dirname "$rel_path") + filename=$(basename "$rel_path" .json) + full_path="${dir_structure}/${filename}" + + contract_name=$(basename "${file%.json}") + bytecode_after=$(jq -r '.deployedBytecode.object // .bytecode.object // ""' "$file" 2>/dev/null || echo "") + + # Read pre-format bytecode if exists + if [ -f "$TEMP_BEFORE/${full_path}.bytecode" ]; then + bytecode_before=$(cat "$TEMP_BEFORE/${full_path}.bytecode") + else + bytecode_before="" + fi + + # Create combined JSON file with proper directory structure + mkdir -p "$CACHE_DIR/$dir_structure" + output_file="$CACHE_DIR/${full_path}.json" + + # Normalize empty bytecode (treat "", "0x", and missing as equivalent) + normalized_before="$bytecode_before" + normalized_after="$bytecode_after" + + if [ -z "$normalized_before" ] || [ "$normalized_before" = "0x" ]; then + normalized_before="" + fi + + if [ -z "$normalized_after" ] || [ "$normalized_after" = "0x" ]; then + normalized_after="" + fi + + jq -n \ + --arg name "$contract_name" \ + --arg path "$rel_path" \ + --arg before "$bytecode_before" \ + --arg after "$bytecode_after" \ + --argjson changed "$([ "$normalized_before" != "$normalized_after" ] && echo true || echo false)" \ + '{ + contractName: $name, + artifactPath: $path, + preFormatBytecode: $before, + postFormatBytecode: $after, + bytecodeChanged: $changed + }' > "$output_file" +done + +echo "✅ Post-format bytecode extracted and comparison saved" + +# Cleanup temp directory +rm -rf "$TEMP_BEFORE" + +# Step 4: Compare bytecode for each contract +echo "" +echo "🔍 Analyzing bytecode changes..." +echo "" + +# Create temporary files to store results +TEMP_CHANGED=$(mktemp) +TEMP_STATS=$(mktemp) + +# Analyze all contracts recursively +while IFS= read -r json_file; do + # Extract contract info + artifact_path=$(jq -r '.artifactPath' "$json_file") + pre_bytecode=$(jq -r '.preFormatBytecode' "$json_file") + post_bytecode=$(jq -r '.postFormatBytecode' "$json_file") + + # Skip contracts with no bytecode (abstract contracts, interfaces, libraries without code) + # Normalize empty values: treat "", "0x", and missing as equivalent + normalized_pre="$pre_bytecode" + normalized_post="$post_bytecode" + + if [ -z "$normalized_pre" ] || [ "$normalized_pre" = "0x" ]; then + normalized_pre="" + fi + + if [ -z "$normalized_post" ] || [ "$normalized_post" = "0x" ]; then + normalized_post="" + fi + + if [ -z "$normalized_pre" ] && [ -z "$normalized_post" ]; then + echo "⊘ $artifact_path - SKIPPED (no bytecode)" + echo "skipped" >> "$TEMP_STATS" + continue + fi + + bytecode_changed=$(jq -r '.bytecodeChanged' "$json_file") + + if [ "$bytecode_changed" = "true" ]; then + echo "❌ $artifact_path - BYTECODE CHANGED" + echo "$artifact_path" >> "$TEMP_CHANGED" + echo "changed" >> "$TEMP_STATS" + else + echo "identical" >> "$TEMP_STATS" + fi +done < <(find "$CACHE_DIR" -name "*.json" -type f) + +# Count results +if [ -f "$TEMP_CHANGED" ]; then + CHANGED_COUNT=$(wc -l < "$TEMP_CHANGED") + # Read into array without mapfile + CHANGED_CONTRACTS=() + while IFS= read -r line; do + CHANGED_CONTRACTS+=("$line") + done < "$TEMP_CHANGED" +else + CHANGED_COUNT=0 +fi + +TOTAL_COUNT=$(wc -l < "$TEMP_STATS" 2>/dev/null || echo "0") +IDENTICAL_COUNT=$(grep -c "identical" "$TEMP_STATS" 2>/dev/null || echo "0") +SKIPPED_COUNT=$(grep -c "skipped" "$TEMP_STATS" 2>/dev/null || echo "0") + +echo "" +echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + +# Check if any contracts changed +if [ $CHANGED_COUNT -gt 0 ]; then + echo "❌ FAILURE: $CHANGED_COUNT contract(s) have bytecode changes!" + echo " ($IDENTICAL_COUNT verified, $SKIPPED_COUNT skipped, $TOTAL_COUNT total)" + echo "" + echo "Affected contracts:" + for contract in "${CHANGED_CONTRACTS[@]}"; do + echo " - $contract" + done + echo "" + echo "📁 Detailed comparison saved in: $CACHE_DIR/" + echo "" + echo "💡 To inspect a specific contract:" + echo " jq '.' $CACHE_DIR/src/AccessControl.sol/AccessControl.json" + echo "" + echo "💡 To see which contracts changed:" + echo " find $CACHE_DIR -name '*.json' -exec jq -r 'select(.bytecodeChanged == true) | .artifactPath' {} \\;" + + # Cleanup + rm -f "$TEMP_CHANGED" "$TEMP_STATS" + exit 1 +else + echo "✅ SUCCESS: All contracts have identical bytecode!" + echo " ($IDENTICAL_COUNT verified, $SKIPPED_COUNT skipped, $TOTAL_COUNT total)" + echo "" + echo "📁 Comparison data saved in: $CACHE_DIR/" + + # Cleanup + rm -f "$TEMP_CHANGED" "$TEMP_STATS" + exit 0 +fi \ No newline at end of file From af170cea12d21ff80106946353705c10e03e9ef6 Mon Sep 17 00:00:00 2001 From: pankajjagtapp Date: Tue, 3 Feb 2026 09:56:50 -0500 Subject: [PATCH 2/2] fix: clean and recreate cache directory in bytecode verification script --- script/verify-forge-fmt-bytecode.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/script/verify-forge-fmt-bytecode.sh b/script/verify-forge-fmt-bytecode.sh index 32c1f6291..ce93fd89b 100755 --- a/script/verify-forge-fmt-bytecode.sh +++ b/script/verify-forge-fmt-bytecode.sh @@ -11,7 +11,8 @@ echo "🔍 Verifying that forge fmt doesn't change bytecode..." # Define cache directory CACHE_DIR="cache/bytecodeComparison" -# Create cache directory +# Clean and create cache directory (removes stale data from previous runs) +rm -rf "$CACHE_DIR" mkdir -p "$CACHE_DIR" # Step 1: Compile and save bytecode before formatting