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
52 changes: 51 additions & 1 deletion .github/workflows/_ci.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Reusable CI workflow for Foundry projects
# Usage: jobs.<job>.uses: ./.github/workflows/_ci.yml
name: CI (Reusable)
name: Build & Test (Reusable)

on:
workflow_call:
Expand All @@ -13,6 +13,22 @@ on:
description: 'Test verbosity level (v, vv, vvv, vvvv)'
type: string
default: 'vvv'
foundry-profile:
description: 'Foundry profile to use for building/testing (leave empty for default)'
type: string
default: ''
run-slither:
description: 'Run Slither static analysis'
type: boolean
default: false
slither-fail-on:
description: 'Slither severity level to fail on (low, medium, high)'
type: string
default: 'medium'
slither-config:
description: 'Path to Slither configuration file'
type: string
default: 'slither.config.json'

jobs:
check:
Expand All @@ -36,6 +52,40 @@ jobs:

- name: Build contracts
run: forge build
env:
FOUNDRY_PROFILE: ${{ inputs.foundry-profile || 'default' }}

- name: Run tests
run: forge test -${{ inputs.test-verbosity }}
env:
FOUNDRY_PROFILE: ${{ inputs.foundry-profile || 'default' }}

slither:
name: Slither Analysis
runs-on: ubuntu-latest
if: inputs.run-slither
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
submodules: recursive

- name: Install Foundry
uses: foundry-rs/foundry-toolchain@v1

- name: Build with Forge
run: forge build

- name: Run Slither
uses: crytic/slither-action@v0.4.0
with:
fail-on: ${{ inputs.slither-fail-on }}
slither-config: ${{ inputs.slither-config }}
sarif: results.sarif

- name: Upload SARIF results
if: always()
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: results.sarif
continue-on-error: true
114 changes: 99 additions & 15 deletions .github/workflows/_deploy-mainnet.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Reusable mainnet deployment workflow with matrix support
# Usage: jobs.<job>.uses: ./.github/workflows/_deploy-mainnet.yml
name: Deploy Mainnet (Reusable)
name: Deploy & Verify Mainnet (Reusable)

on:
workflow_call:
Expand All @@ -21,6 +21,10 @@ on:
description: 'Seconds to wait for indexer before verification'
type: number
default: 60
verify-contracts:
description: 'Run Blockscout contract verification after deployment'
type: boolean
default: true
flatten-contracts:
description: 'Flatten and commit contract snapshots after deploy'
type: boolean
Expand All @@ -29,11 +33,18 @@ on:
description: 'Path to store flattened contract snapshots'
type: string
default: 'test/upgrades'
foundry-profile:
description: 'Foundry profile to use for deployment (leave empty for default)'
type: string
default: ''
secrets:
PRIVATE_KEY:
required: true
RPC_URL:
required: true
ADMIN_ADDRESS:
description: 'Optional admin address for upgradeable contract deploy scripts'
required: false
GH_TOKEN:
description: 'GitHub token for pushing commits (defaults to GITHUB_TOKEN)'
required: false
Expand Down Expand Up @@ -77,10 +88,27 @@ jobs:
- name: Install Foundry
uses: foundry-rs/foundry-toolchain@v1

- name: Validate Foundry profile
if: inputs.foundry-profile != ''
run: |
if ! grep -q "\[profile\.${{ inputs.foundry-profile }}\]" foundry.toml 2>/dev/null; then
echo "::error::Foundry profile '${{ inputs.foundry-profile }}' not found in foundry.toml"
exit 1
fi

- name: Validate foundry.toml configuration
run: |
# Detect etherscan config that references unavailable env vars
if grep -q 'ETHERSCAN_API_KEY' foundry.toml 2>/dev/null; then
echo "::warning::foundry.toml references ETHERSCAN_API_KEY which is not available in this workflow. Blockscout is used for verification."
fi

- name: Deploy contracts
env:
PRIVATE_KEY: ${{ secrets.PRIVATE_KEY }}
RPC_URL: ${{ secrets.RPC_URL }}
ADMIN_ADDRESS: ${{ secrets.ADMIN_ADDRESS }}
FOUNDRY_PROFILE: ${{ inputs.foundry-profile || 'default' }}
run: |
# Ensure PRIVATE_KEY has 0x prefix
if [[ "$PRIVATE_KEY" != 0x* ]]; then
Expand Down Expand Up @@ -110,37 +138,69 @@ jobs:
jq -r '.transactions[] | select(.transactionType == "CREATE") | "\(.contractName): \(.contractAddress)"' "$BROADCAST_FILE" | tee deployment-summary.txt

- name: Verify contracts on Blockscout
if: inputs.verify-contracts
env:
BLOCKSCOUT_URL: ${{ matrix.blockscout_url }}
RPC_URL: ${{ secrets.RPC_URL }}
run: |
BROADCAST_FILE="${{ steps.parse.outputs.broadcast_file }}"

# Extract and verify each deployed contract with retry logic
jq -c '.transactions[] | select(.transactionType == "CREATE")' "$BROADCAST_FILE" | while read -r tx; do
# Skip if no Blockscout URL configured
if [[ -z "$BLOCKSCOUT_URL" || "$BLOCKSCOUT_URL" == "null" ]]; then
echo "::warning::No Blockscout URL configured for ${{ matrix.name }}, skipping verification"
exit 0
fi

# Validate Blockscout URL format
if ! echo "$BLOCKSCOUT_URL" | grep -qE '^https?://[a-zA-Z0-9.-]+'; then
echo "::error::Invalid Blockscout URL format: $BLOCKSCOUT_URL"
exit 1
fi

FAILED=0

# Use process substitution to propagate failures
while read -r tx; do
CONTRACT_NAME=$(echo "$tx" | jq -r '.contractName')
CONTRACT_ADDR=$(echo "$tx" | jq -r '.contractAddress')

echo "Verifying $CONTRACT_NAME at $CONTRACT_ADDR..."

# Retry up to 3 times with exponential backoff
VERIFIED=0
for attempt in 1 2 3; do
if forge verify-contract "$CONTRACT_ADDR" "$CONTRACT_NAME" \
# Capture output and use timeout to prevent hanging
VERIFY_OUTPUT=$(timeout 120 forge verify-contract "$CONTRACT_ADDR" "$CONTRACT_NAME" \
--verifier blockscout \
--verifier-url "${BLOCKSCOUT_URL}/api" \
--guess-constructor-args \
--watch; then
--watch 2>&1) || true
echo "$VERIFY_OUTPUT"

# Check for actual verification success (not just exit code)
if echo "$VERIFY_OUTPUT" | grep -qiE "Contract successfully verified|Pass - Verified"; then
echo "✓ Verified $CONTRACT_NAME"
VERIFIED=1
break
elif echo "$VERIFY_OUTPUT" | grep -qi "Already Verified"; then
echo "✓ $CONTRACT_NAME already verified"
VERIFIED=1
break
else
if [[ $attempt -lt 3 ]]; then
echo "Attempt $attempt failed, retrying in $((attempt * 30))s..."
sleep $((attempt * 30))
else
echo "⚠ Verification pending for $CONTRACT_NAME after 3 attempts"
echo "::warning::Verification failed for $CONTRACT_NAME after 3 attempts"
FAILED=1
fi
fi
done
done
done < <(jq -c '.transactions[] | select(.transactionType == "CREATE")' "$BROADCAST_FILE")

if [[ $FAILED -eq 1 ]]; then
echo "::error::One or more contracts failed verification"
exit 1
fi

- name: Save deployment artifacts
run: |
Expand All @@ -161,16 +221,37 @@ jobs:
env:
BLOCKSCOUT_URL: ${{ matrix.blockscout_url }}
run: |
BROADCAST_FILE="${{ steps.parse.outputs.broadcast_file }}"

echo "## ${{ matrix.name }} Deployment Summary" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| Contract | Address | Explorer |" >> $GITHUB_STEP_SUMMARY
echo "|----------|---------|----------|" >> $GITHUB_STEP_SUMMARY
echo "| Contract | Address | Constructor Args | Explorer |" >> $GITHUB_STEP_SUMMARY
echo "|----------|---------|------------------|----------|" >> $GITHUB_STEP_SUMMARY

jq -r '.transactions[] | select(.transactionType == "CREATE") | "\(.contractName)|\(.contractAddress)|\(.arguments // [] | join(", "))"' "$BROADCAST_FILE" | while IFS='|' read -r CONTRACT ADDRESS ARGS; do
if [[ -n "$BLOCKSCOUT_URL" && "$BLOCKSCOUT_URL" != "null" ]]; then
EXPLORER="[View](${BLOCKSCOUT_URL}/address/$ADDRESS)"
else
EXPLORER="-"
fi
echo "| $CONTRACT | \`$ADDRESS\` | ${ARGS:--} | $EXPLORER |" >> $GITHUB_STEP_SUMMARY
done

while read -r line; do
CONTRACT=$(echo "$line" | cut -d: -f1)
ADDRESS=$(echo "$line" | cut -d: -f2 | tr -d ' ')
echo "| $CONTRACT | \`$ADDRESS\` | [View](${BLOCKSCOUT_URL}/address/$ADDRESS) |" >> $GITHUB_STEP_SUMMARY
done < deployment-summary.txt
# Method calls section
CALLS=$(jq -r '.transactions[] | select(.transactionType == "CALL") | "\(.contractName)|\(.function // "unknown")|\(.arguments // [] | join(", "))"' "$BROADCAST_FILE" 2>/dev/null)
if [[ -n "$CALLS" ]]; then
echo "" >> $GITHUB_STEP_SUMMARY
echo "<details>" >> $GITHUB_STEP_SUMMARY
echo "<summary>Method Calls</summary>" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| Contract | Method | Arguments |" >> $GITHUB_STEP_SUMMARY
echo "|----------|--------|-----------|" >> $GITHUB_STEP_SUMMARY
echo "$CALLS" | while IFS='|' read -r CONTRACT METHOD ARGS; do
echo "| $CONTRACT | \`$METHOD\` | ${ARGS:--} |" >> $GITHUB_STEP_SUMMARY
done
echo "" >> $GITHUB_STEP_SUMMARY
echo "</details>" >> $GITHUB_STEP_SUMMARY
fi

flatten-snapshots:
name: Flatten & Commit Snapshots
Expand All @@ -194,6 +275,8 @@ jobs:

- name: Build contracts
run: forge build
env:
FOUNDRY_PROFILE: ${{ inputs.foundry-profile || 'default' }}

- name: Extract contract list from broadcast artifacts
id: contracts
Expand All @@ -216,6 +299,7 @@ jobs:
- name: Flatten contracts to current snapshot
env:
UPGRADES_PATH: ${{ inputs.upgrades-path }}
FOUNDRY_PROFILE: ${{ inputs.foundry-profile || 'default' }}
run: |
mkdir -p "${UPGRADES_PATH}/current"

Expand Down
Loading