Skip to content

Merge pull request #117 from 0xIntuition/sean/audit/merge #310

Merge pull request #117 from 0xIntuition/sean/audit/merge

Merge pull request #117 from 0xIntuition/sean/audit/merge #310

Workflow file for this run

name: test
on:
push:
branches:
- main
pull_request:
types:
- opened
- synchronize
- reopened
- labeled
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true
env:
FOUNDRY_PROFILE: 'ci'
SEPOLIA_RPC_URL: 'https://sepolia.base.org' # TODO: Throw this in repo env once permissioned to do so
BASE_RPC_URL: 'https://mainnet.base.org' # TODO: Throw this in repo env once permissioned to do so
jobs:
lint:
name: Lint
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v3
with:
submodules: recursive
- name: Install Foundry
uses: foundry-rs/foundry-toolchain@v1
with:
version: nightly
- name: Install Node.js
uses: actions/setup-node@v3
with:
node-version: 'lts/*'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run fmt
run: npm run fmt
- name: Run fmt check
run: npm run fmt:check
test:
name: Foundry Tests
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@v3
with:
submodules: recursive
- name: Install Foundry
uses: foundry-rs/foundry-toolchain@v1
with:
version: nightly
- name: Cache Foundry dependencies
uses: actions/cache@v3
with:
path: |
~/.foundry/cache
~/.foundry/rpc-cache
cache/
out/
key: foundry-${{ runner.os }}-${{ hashFiles('foundry.toml') }}-${{ hashFiles('**/foundry.lock') }}
- name: Install Node.js
uses: actions/setup-node@v3
with:
node-version: 'lts/*'
cache: 'npm'
- name: Install dependencies
run: |
npm ci
forge install
- name: Run Forge tests
id: forge-test
run: |
mkdir -p artifacts/forge-test-results
forge test -vvv 2>&1 | tee artifacts/forge-test-results/forge-test-results.txt
exit ${PIPESTATUS[0]}
- name: Upload test results
if: always()
uses: actions/upload-artifact@v4
with:
name: test-results
path: artifacts/forge-test-results/forge-test-results.txt
security:
name: Security Analysis
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v3
with:
submodules: recursive
- name: Install Node.js
uses: actions/setup-node@v3
with:
node-version: 'lts/*'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Install Foundry
uses: foundry-rs/foundry-toolchain@v1
- name: Install Python
uses: actions/setup-python@v4
with:
python-version: '3.x'
cache: 'pip'
- name: Install Slither
run: |
python -m pip install --upgrade pip
pip install slither-analyzer
- name: Run Slither analysis
id: slither
continue-on-error: true
run: |
# Create a slither.config.json file with correct format
echo '{
"filter_paths": "lib/,node_modules/",
"exclude_informational": true,
"exclude_low": true,
"exclude_optimization": true
}' > slither.config.json
slither . --json slither-output.json
- name: Process Slither results
if: always()
run: |
if [ -f slither-output.json ]; then
echo "Analyzing Slither results..."
# Format and display high severity issues from src/ files
echo "=== HIGH SEVERITY ISSUES ===" > slither-report.txt
jq -r '.results.detectors[] |
select(.impact == "High") |
select(any(.elements[].source_mapping.filename_short; startswith("src/"))) |
"- [\(.check)]: \(.description | gsub("\n\t"; " ") | gsub("\n"; " "))"' slither-output.json | sort -u >> slither-report.txt
# Format and display medium severity issues from src/ files
echo -e "\n=== MEDIUM SEVERITY ISSUES ===" >> slither-report.txt
jq -r '.results.detectors[] |
select(.impact == "Medium") |
select(any(.elements[].source_mapping.filename_short; startswith("src/"))) |
"- [\(.check)]: \(.description | gsub("\n\t"; " ") | gsub("\n"; " "))"' slither-output.json | sort -u >> slither-report.txt
# Count issues (unique)
HIGH_SEVERITY=$(jq -r '[.results.detectors[] |
select(.impact == "High") |
select(any(.elements[].source_mapping.filename_short; startswith("src/")))] | length' slither-output.json)
MEDIUM_SEVERITY=$(jq -r '[.results.detectors[] |
select(.impact == "Medium") |
select(any(.elements[].source_mapping.filename_short; startswith("src/")))] | length' slither-output.json)
# Display summary
echo -e "\n=== SUMMARY ==="
echo "High Severity Issues: $HIGH_SEVERITY"
echo "Medium Severity Issues: $MEDIUM_SEVERITY"
# Output the report
cat slither-report.txt
# Fail on high severity issues
if [ "$HIGH_SEVERITY" -gt 0 ]; then
echo "❌ Found $HIGH_SEVERITY high severity issues"
echo "Review the detailed report above"
exit 1
else
echo "✅ No high severity issues found"
if [ "$MEDIUM_SEVERITY" -gt 0 ]; then
echo "⚠️ Found $MEDIUM_SEVERITY medium severity issues to review"
fi
fi
else
echo "❌ Slither analysis failed to produce output"
exit 1
fi
- name: Upload Slither results
if: always()
uses: actions/upload-artifact@v4
with:
name: slither-results
path: |
slither-output.json
slither-report.txt
gas:
name: Gas Analysis
runs-on: ubuntu-latest
needs: test
if: success() && needs.test.result == 'success'
timeout-minutes: 30
env:
FOUNDRY_FUZZ_RUNS: 256
FOUNDRY_FUZZ_MAX_TEST_REJECTS: 65536
steps:
- uses: actions/checkout@v3
with:
submodules: recursive
- name: Install Foundry
uses: foundry-rs/foundry-toolchain@v1
with:
version: nightly
- name: Cache Foundry dependencies
uses: actions/cache@v3
with:
path: |
~/.foundry/cache
~/.foundry/rpc-cache
cache/
out/
key: foundry-${{ runner.os }}-${{ hashFiles('foundry.toml') }}-${{ hashFiles('**/foundry.lock') }}
- name: Install Node.js
uses: actions/setup-node@v3
with:
node-version: 'lts/*'
cache: 'npm'
- name: Install dependencies
run: |
npm ci
forge install
- name: Generate gas report
id: gas-report
continue-on-error: true
run: |
mkdir -p snapshots
# Run tests and capture gas reports for successful tests
forge test --gas-report > snapshots/current-gas-report.txt
# Generate snapshot (Forge automatically includes only successful tests)
forge snapshot --snap snapshots/current-gas.snap
- name: Compare gas snapshots
if: always()
run: |
if [ ! -f "snapshots/current-gas.snap" ]; then
echo "⚠️ No gas snapshot generated - skipping comparison"
exit 0
fi
if [ -f ".gas-snapshot" ]; then
echo "📊 Gas Comparison:"
forge snapshot --diff .gas-snapshot snapshots/current-gas.snap || (
echo "🔍 Gas changes detected!"
exit 0
)
else
echo "Creating first snapshot for future comparisons"
cp snapshots/current-gas.snap .gas-snapshot
fi
- name: Upload gas reports
if: always()
uses: actions/upload-artifact@v4
with:
name: gas-reports
path: snapshots/
if-no-files-found: warn
summary:
name: Create Summary
if: always()
needs: [test, security, gas]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Install Foundry
uses: foundry-rs/foundry-toolchain@v1
with:
version: nightly
- name: Download all artifacts
uses: actions/download-artifact@v4
with:
path: artifacts
- name: Create Test Summary
if: always()
run: |
echo "=== Debug Information ==="
echo "Listing current directory:"
ls -la
echo
echo "Listing artifacts directory:"
ls -la artifacts || echo "artifacts directory does not exist"
echo
echo "Listing test-results directory (if it exists):"
ls -la artifacts/test-results || echo "test-results directory does not exist"
echo
echo "Checking for forge test results:"
ls -la artifacts/test-results/forge-test-results.txt || echo "forge-test-results.txt does not exist"
echo
if [ -f "artifacts/test-results/forge-test-results.txt" ]; then
# Get total test counts from the final summary line
TOTAL_LINE=$(grep "^Ran .* test suites" "artifacts/test-results/forge-test-results.txt" | tail -n 1)
if [[ $TOTAL_LINE =~ ([0-9]+)[[:space:]]tests[[:space:]]passed,[[:space:]]([0-9]+)[[:space:]]failed,[[:space:]]([0-9]+)[[:space:]]skipped[[:space:]]\(([0-9]+)[[:space:]]total[[:space:]]tests\) ]]; then
PASSED_TESTS="${BASH_REMATCH[1]}"
mapfile -t FAILING_TESTS < <(sed -n '/^Failing tests:/,/^Encountered a total of/p' "artifacts/test-results/forge-test-results.txt" | grep '^\[FAIL:')
FAILED_TESTS=${#FAILING_TESTS[@]}
SKIPPED_TESTS="${BASH_REMATCH[3]}"
TOTAL_TESTS=$((PASSED_TESTS + FAILED_TESTS + SKIPPED_TESTS))
# Write main status with emoji
if [ "$FAILED_TESTS" -gt 0 ]; then
echo "❌ $FAILED_TESTS tests failed, $PASSED_TESTS passed, $SKIPPED_TESTS skipped (Total: $TOTAL_TESTS)" >> test-summary.md
else
echo "✅ All $PASSED_TESTS tests passed! ($SKIPPED_TESTS skipped, Total: $TOTAL_TESTS)" >> test-summary.md
fi
# If there are failing tests, show them first
if [ "$FAILED_TESTS" -gt 0 ]; then
echo -e "\n### Failing Tests in this PR Branch\n" >> test-summary.md
# Process each failing test
while IFS= read -r line; do
if [[ $line =~ ^\[FAIL:[[:space:]]([^]]+)\][[:space:]]([^[:space:]]+) ]]; then
# Get the full test name including parameters
full_test=$(echo "$line" | sed -E 's/.*\][[:space:]](.*) \(gas:.*/\1/')
# Get the test name without parameters for the awk pattern
test_pattern=$(echo "$full_test" | sed -E 's/\(.*\)//')
# Write test name header
echo -e "\`$full_test\`\n" >> test-summary.md
echo -e "<details><summary>Stack Trace</summary>\n\n" >> test-summary.md
echo -e "\`\`\`\n" >> test-summary.md
# Extract stack trace for this test
awk -v test="$test_pattern" '
/^Traces:/ { p = 1 }
p == 1 { print }
/^$/ { p = 0 }
' "artifacts/test-results/forge-test-results.txt" >> test-summary.md
echo -e "\n\`\`\`\n\n</details>\n\n" >> test-summary.md
fi
done < <(sed -n '/^Failing tests:/,/^Encountered a total of/p' "artifacts/test-results/forge-test-results.txt" | grep '^\[FAIL:')
fi
# Add test suite details section
echo -e "\n### Test Results for Merge" >> test-summary.md
echo -e "\n| Test Suite | Status | Coverage | Time |" >> test-summary.md
echo "|------------|--------|----------|------|" >> test-summary.md
# Process each test suite
current_suite=""
while IFS= read -r line; do
if [[ $line =~ Ran[[:space:]][0-9]+[[:space:]]tests[[:space:]]for[[:space:]]([^:]+) ]]; then
current_suite="${BASH_REMATCH[1]}"
elif [[ $line =~ Suite[[:space:]]result:[[:space:]](ok|FAILED)\.[[:space:]]([0-9]+)[[:space:]]passed\;[[:space:]]([0-9]+)[[:space:]]failed\;[[:space:]]([0-9]+)[[:space:]]skipped\;[[:space:]]finished[[:space:]]in[[:space:]]([0-9.]+)(m?s) ]]; then
status="${BASH_REMATCH[1]}"
passed="${BASH_REMATCH[2]}"
failed="${BASH_REMATCH[3]}"
skipped="${BASH_REMATCH[4]}"
time="${BASH_REMATCH[5]}"
time_unit="${BASH_REMATCH[6]}"
if [ ! -z "$current_suite" ]; then
status_emoji="✅"
[ "$status" = "FAILED" ] && status_emoji="❌"
[ "$skipped" -gt 0 ] && [ "$failed" -eq 0 ] && status_emoji="⚠️"
total=$((passed + failed + skipped))
coverage=0
[ "$total" -gt 0 ] && coverage=$((passed * 100 / total))
# Convert time to seconds if in milliseconds
if [ "$time_unit" = "ms" ]; then
time=$(echo "scale=3; $time/1000" | bc)
fi
printf "| \`%s\` | %s | %d%% (%d/%d) | %.3fs |\n" \
"$current_suite" "$status_emoji" "$coverage" "$passed" "$total" "$time" >> test-summary.md
current_suite=""
fi
fi
done < "artifacts/test-results/forge-test-results.txt"
else
echo "⚠️ Could not parse test results" >> test-summary.md
fi
else
echo "⚠️ No test results found" >> test-summary.md
fi
- name: Create Slither Summary
id: slither-summary
run: |
echo "### 🔒 Security Analysis" > slither-summary.md
if [ -f "artifacts/slither-results/slither-output.json" ]; then
# Count vulnerabilities by severity
HIGH_COUNT=$(jq -r '[.results.detectors[] | select(.impact == "High")] | length' "artifacts/slither-results/slither-output.json")
MEDIUM_COUNT=$(jq -r '[.results.detectors[] | select(.impact == "Medium")] | length' "artifacts/slither-results/slither-output.json")
if [ "$HIGH_COUNT" -gt 0 ] || [ "$MEDIUM_COUNT" -gt 0 ]; then
echo "⚠️ Found **$HIGH_COUNT High** and **$MEDIUM_COUNT Medium** severity issues" >> slither-summary.md
# Process high severity issues
if [ "$HIGH_COUNT" -gt 0 ]; then
echo -e "\n#### High Severity Issues" >> slither-summary.md
# Group findings by check type
jq -r '
def clean_description:
gsub("\n\t"; " ") | gsub("\n"; " ");
def format_location:
. as $loc |
if contains("#") then split("#")[0]
else . end;
.results.detectors
| map(select(.impact == "High"))
| group_by(.check)[]
| {
check: .[0].check,
description: (.[0].description | clean_description),
files: ([.[].elements[].source_mapping.filename_short] | unique | sort),
findings: map({
file: .elements[0].source_mapping.filename_short,
function: (.elements[0].name | format_location),
line: .elements[0].source_mapping.lines[0]
})
}
| "##### \(.check)\n**Impact**: \(.description)\n\n**Affected Files**:\n\(.files | map("- `" + . + "`") | join("\n"))\n\n<details>\n<summary>View Detailed Findings</summary>\n\n\(.findings | map("- `" + .file + ":" + (.line|tostring) + "` in `" + .function + "`") | join("\n"))\n</details>\n"
' "artifacts/slither-results/slither-output.json" >> slither-summary.md
fi
# Process medium severity issues
if [ "$MEDIUM_COUNT" -gt 0 ]; then
echo -e "\n#### Medium Severity Issues" >> slither-summary.md
echo "<details><summary>View Medium Severity Issues</summary>" >> slither-summary.md
jq -r '
def clean_description:
gsub("\n\t"; " ") | gsub("\n"; " ");
def format_location:
. as $loc |
if contains("#") then split("#")[0]
else . end;
.results.detectors
| map(select(.impact == "Medium"))
| group_by(.check)[]
| {
check: .[0].check,
description: (.[0].description | clean_description),
files: ([.[].elements[].source_mapping.filename_short] | unique | sort),
findings: map({
file: .elements[0].source_mapping.filename_short,
function: (.elements[0].name | format_location),
line: .elements[0].source_mapping.lines[0]
})
}
| "##### \(.check)\n**Impact**: \(.description)\n\n**Affected Files**:\n\(.files | map("- `" + . + "`") | join("\n"))\n\n\(.findings | map("- `" + .file + ":" + (.line|tostring) + "` in `" + .function + "`") | join("\n"))\n"
' "artifacts/slither-results/slither-output.json" >> slither-summary.md
echo "</details>" >> slither-summary.md
fi
# Add recommendations section
echo -e "\n#### Recommended Actions" >> slither-summary.md
echo "1. Review and fix all high severity issues before deployment" >> slither-summary.md
echo "2. Implement thorough testing for affected components" >> slither-summary.md
echo "3. Consider additional security measures:" >> slither-summary.md
echo " - Access controls" >> slither-summary.md
echo " - Input validation" >> slither-summary.md
echo " - Invariant checks" >> slither-summary.md
else
echo "✅ No high or medium severity issues found!" >> slither-summary.md
fi
else
echo "⚠️ No security analysis results found" >> slither-summary.md
fi
- name: Create Gas Summary
id: gas-summary
run: |
echo "### ⛽ Gas Analysis" > gas-summary.md
if [ -f "artifacts/gas-reports/current-gas.snap" ] && [ -f ".gas-snapshot" ]; then
# Compare snapshots and capture the diff
DIFF_OUTPUT=$(forge snapshot --diff .gas-snapshot "artifacts/gas-reports/current-gas.snap" 2>&1 || true)
if echo "$DIFF_OUTPUT" | grep -q "Different"; then
echo "🔄 Gas changes detected" >> gas-summary.md
# Extract and format the changes
echo -e "\n**Changes:**" >> gas-summary.md
echo "\`\`\`" >> gas-summary.md
echo "$DIFF_OUTPUT" | grep -A 1 "Different" >> gas-summary.md || true
echo "\`\`\`" >> gas-summary.md
# Add full diff in collapsible section
echo -e "\n<details>" >> gas-summary.md
echo "<summary>📋 Full Gas Comparison</summary>" >> gas-summary.md
echo -e "\n\`\`\`" >> gas-summary.md
echo "$DIFF_OUTPUT" >> gas-summary.md
echo "\`\`\`" >> gas-summary.md
echo "</details>" >> gas-summary.md
else
echo "✅ No gas changes detected" >> gas-summary.md
fi
elif [ -f "artifacts/gas-reports/current-gas.snap" ]; then
echo "📊 First gas snapshot created" >> gas-summary.md
else
echo "⚠️ No gas snapshot generated" >> gas-summary.md
fi
- name: Combine Summaries
id: combine-summaries
run: |
{
echo "## Summary of Test Results if Merged To Main:"
echo
echo "- Full logs & artifacts are available in the [Actions tab](${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID})"
echo "- This comment will update automatically with new CI runs"
echo
cat test-summary.md
echo
cat slither-summary.md
echo
cat gas-summary.md
echo
} > combined-summary.md
- name: Find Comment
uses: peter-evans/find-comment@v2
id: fc
with:
issue-number: ${{ github.event.pull_request.number }}
comment-author: 'github-actions[bot]'
body-includes: CI Results Summary
- name: Create or Update Comment
uses: peter-evans/create-or-update-comment@v3
if: github.event_name == 'pull_request'
with:
comment-id: ${{ steps.fc.outputs.comment-id }}
issue-number: ${{ github.event.pull_request.number }}
body-file: combined-summary.md
edit-mode: replace
permissions:
pull-requests: write
contents: read