Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
106 changes: 105 additions & 1 deletion foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,115 @@ 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}" }
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"
211 changes: 211 additions & 0 deletions script/verify-forge-fmt-bytecode.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
#!/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"

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