diff --git a/.github/workflows/compare-layouts.yml b/.github/workflows/compare-layouts.yml new file mode 100644 index 00000000..da4408b9 --- /dev/null +++ b/.github/workflows/compare-layouts.yml @@ -0,0 +1,310 @@ +name: Compare Storage Layouts + +on: + workflow_run: + workflows: ["Forge CI"] + types: + - completed + +permissions: + contents: read + statuses: write + pull-requests: write + +jobs: + # The cache storage in the reusable foundry setup takes far too long. + # Do this job first to update the commit status and comment ASAP. + set-commit-status: + # Typically takes no more than 30s + timeout-minutes: 5 + runs-on: ubuntu-latest + outputs: + number: ${{ steps.pr-context.outputs.number }} + steps: + # Log the workflow trigger details for debugging. + - name: Echo workflow trigger details + run: | + echo "Workflow run event: ${{ github.event.workflow_run.event }}" + echo "Workflow run conclusion: ${{ github.event.workflow_run.conclusion }}" + echo "Workflow run name: ${{ github.event.workflow_run.name }}" + echo "Workflow run URL: ${{ github.event.workflow_run.html_url }}" + echo "Commit SHA: ${{ github.event.workflow_run.head_commit.id }}" + echo "Workflow Run ID: ${{ github.event.workflow_run.id }}" + - name: Set commit status + # trigger is no matter what, because the status should be updated + if: always() + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # this step would have been better located in forge-ci.yml, but since it needs the secret + # it is placed here. first, it is updated to pending here and failure/success is updated later. + run: | + gh api \ + --method POST \ + /repos/${{ github.repository }}/statuses/${{ github.event.workflow_run.head_commit.id }} \ + -f state=pending \ + -f context="${{ github.workflow }}" \ + -f description="In progress..." \ + -f target_url="https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" + - name: Get PR number + id: pr-context + if: ${{ github.event.workflow_run.event == 'pull_request' }} + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PR_TARGET_REPO: ${{ github.repository }} + PR_BRANCH: |- + ${{ + (github.event.workflow_run.head_repository.owner.login != github.event.workflow_run.repository.owner.login) + && format('{0}:{1}', github.event.workflow_run.head_repository.owner.login, github.event.workflow_run.head_branch) + || github.event.workflow_run.head_branch + }} + run: | + pr_number=$(gh pr view --repo "${PR_TARGET_REPO}" "${PR_BRANCH}" \ + --json 'number' --jq '.number') + if [ -z "$pr_number" ]; then + echo "Error: PR number not found for branch '${PR_BRANCH}' in repository '${PR_TARGET_REPO}'" >&2 + exit 1 + fi + echo "number=$pr_number" >> "${GITHUB_OUTPUT}" + - name: Set message + id: set-message + env: + WORKFLOW_URL: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }} + WORKFLOW_NAME: ${{ github.workflow }} + SHA: ${{ github.event.workflow_run.head_commit.id }} + run: | + message="🚀 The $WORKFLOW_NAME workflow has started." + echo "message=$message Check the [workflow run]($WORKFLOW_URL) for progress. ($SHA)" >> "${GITHUB_OUTPUT}" + - name: Comment CI Status + uses: marocchino/sticky-pull-request-comment@v2 + if: ${{ github.event.workflow_run.event == 'pull_request' }} + with: + header: ${{ github.workflow }} + hide_details: true + number: ${{ steps.pr-context.outputs.number }} + message: ${{ steps.set-message.outputs.message }} + + setup: + # The caching of the binaries is necessary because we run the job to fetch + # the deployed layouts via a matrix strategy. This job is the parent of that job. + uses: ./.github/workflows/reusable-foundry-setup.yml + with: + # The below line does not accept environment variables, + # so it becomes the single source of truth for the version, within this workflow. + # Any `pinning` of the version should be done here and in forge-ci.yml. + foundry-version: nightly + # Skip the setup job if the parent job failed. + skip-install: ${{ github.event.workflow_run.conclusion != 'success' }} + + create-deployed-layouts-matrix: + # Takes about 2 seconds + timeout-minutes: 5 + # Generating the matrix is very quick. It should be done regardless of the parent + # workflow status, because an empty matrix will result in no `fetch-deployed-layouts` + # jobs, which will cascade to no `compare-storage-layouts` job. + runs-on: ubuntu-latest + outputs: + matrix: ${{ steps.generate-matrix.outputs.matrix }} + steps: + - name: Checkout code + uses: actions/checkout@v4 + - name: Generate matrix from deployedContracts.json + id: generate-matrix + run: | + set -e + data=$(cat script/deployedContracts.json) + + bootstrap=$(echo "$data" | jq -r '.clientChain.bootstrapLogic // empty') + clientGateway=$(echo "$data" | jq -r '.clientChain.clientGatewayLogic // empty') + vault=$(echo "$data" | jq -r '.clientChain.vaultImplementation // empty') + rewardVault=$(echo "$data" | jq -r '.clientChain.rewardVaultImplementation // empty') + capsule=$(echo "$data" | jq -r '.clientChain.capsuleImplementation // empty') + + # Create the matrix as a JSON array + matrix=$(jq -n \ + --arg bootstrap "$bootstrap" \ + --arg clientGateway "$clientGateway" \ + --arg vault "$vault" \ + --arg rewardVault "$rewardVault" \ + --arg capsule "$capsule" \ + '[{name: "Bootstrap", address: $bootstrap}, + {name: "ClientChainGateway", address: $clientGateway}, + {name: "Vault", address: $vault}, + {name: "RewardVault", address: $rewardVault}, + {name: "ExoCapsule", address: $capsule}] | map(select(.address != ""))') + + echo "Matrix: $matrix" + echo "matrix=$(echo "$matrix" | jq -c .)" >> "${GITHUB_OUTPUT}" + + fetch-deployed-layouts: + # Takes about 15 seconds + timeout-minutes: 5 + strategy: + matrix: + # if the parent workflow failed, the matrix will be empty. hence, no jobs will run. + contract: ${{ fromJSON(needs.create-deployed-layouts-matrix.outputs.matrix) }} + needs: + - setup + - create-deployed-layouts-matrix + runs-on: ubuntu-latest + steps: + - name: Echo a message to prevent "no steps" warning. + run: echo "Fetching the deployed layouts." + - name: Restore cached Foundry toolchain + if: ${{ github.event.workflow_run.conclusion == 'success' }} + uses: actions/cache/restore@v3 + with: + path: ${{ needs.setup.outputs.installation-dir }} + key: ${{ needs.setup.outputs.cache-key }} + - name: Add Foundry to PATH + if: ${{ github.event.workflow_run.conclusion == 'success' }} + run: echo "${{ needs.setup.outputs.installation-dir }}" >> "$GITHUB_PATH" + - name: Fetch the deployed layout + if: ${{ github.event.workflow_run.conclusion == 'success' }} + env: + ALCHEMY_API_KEY: ${{ secrets.ALCHEMY_API_KEY }} + ETHERSCAN_API_KEY: ${{ secrets.ETHERSCAN_API_KEY }} + run: | + echo "Processing ${{ matrix.contract.name }} at address ${{ matrix.contract.address }}" + RPC_URL="https://eth-sepolia.g.alchemy.com/v2/$ALCHEMY_API_KEY" + cast storage --json "${{ matrix.contract.address }}" \ + --rpc-url "$RPC_URL" \ + --etherscan-api-key "$ETHERSCAN_API_KEY" > "${{ matrix.contract.name }}.deployed.json" + - name: Upload the deployed layout file as an artifact + if: ${{ github.event.workflow_run.conclusion == 'success' }} + uses: actions/upload-artifact@v4 + with: + path: ${{ matrix.contract.name }}.deployed.json + name: deployed-layout-${{ matrix.contract.name }}-${{ github.event.workflow_run.head_commit.id }} + + combine-deployed-layouts: + # Takes about 4 seconds + timeout-minutes: 5 + needs: fetch-deployed-layouts + runs-on: ubuntu-latest + steps: + - name: Echo a message to prevent "no steps" warning. + run: echo "Combining the deployed layouts." + - name: Download artifacts + if: ${{ github.event.workflow_run.conclusion == 'success' }} + uses: actions/download-artifact@v4 + with: + path: combined + - name: Zip up the deployed layouts + if: ${{ github.event.workflow_run.conclusion == 'success' }} + run: zip -j deployed-layouts.zip combined/*/*.json + - name: Upload the deployed layout files as an artifact + if: ${{ github.event.workflow_run.conclusion == 'success' }} + uses: actions/upload-artifact@v4 + with: + path: deployed-layouts.zip + name: deployed-layouts-${{ github.event.workflow_run.head_commit.id }} + + # The actual job to compare the storage layouts. + compare-storage-layouts: + # Takes no more than a minute + timeout-minutes: 5 + needs: + - setup + - set-commit-status + - combine-deployed-layouts + runs-on: ubuntu-latest + + steps: + # The repository needs to be available for script/deployedContracts.json + # and script/compareLayouts.js. + - name: Checkout the repository + if: ${{ github.event.workflow_run.conclusion == 'success' }} + uses: actions/checkout@v4 + - name: Restore the compiled layout files from the artifact + if: ${{ github.event.workflow_run.conclusion == 'success' }} + uses: dawidd6/action-download-artifact@v6 + with: + name: compiled-layouts-${{ github.event.workflow_run.head_commit.id }} + run_id: ${{ github.event.workflow_run.id }} + - name: Restore the deployed layout files from the artifact + if: ${{ github.event.workflow_run.conclusion == 'success' }} + uses: actions/download-artifact@v4 + with: + name: deployed-layouts-${{ github.event.workflow_run.head_commit.id }} + path: ./ + - name: Extract the restored compiled layouts + if: ${{ github.event.workflow_run.conclusion == 'success' }} + run: unzip compiled-layouts.zip + - name: Extract the restored deployed layouts + if: ${{ github.event.workflow_run.conclusion == 'success' }} + run: unzip deployed-layouts.zip + - name: Set up Node.js + if: ${{ github.event.workflow_run.conclusion == 'success' }} + uses: actions/setup-node@v4 + with: + node-version: '18' + - name: Clear npm cache + if: ${{ github.event.workflow_run.conclusion == 'success' }} + run: npm cache clean --force + - name: Install the required dependency + if: ${{ github.event.workflow_run.conclusion == 'success' }} + run: npm install @openzeppelin/upgrades-core + - name: Compare the layouts + if: ${{ github.event.workflow_run.conclusion == 'success' }} + id: compare-layouts + run: | + node script/compareLayouts.js + # Even if this fails, the CI status should be updated. + continue-on-error: true + - name: Update parent commit status + if: always() + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # if the outcome is not set, it will post failure + run: | + if [[ "${{ steps.compare-layouts.outcome }}" == "success" ]]; then + outcome="success" + description="Storage layouts match" + elif [[ "${{ steps.compare-layouts.outcome }}" == "failure" ]]; then + outcome="failure" + description="Storage layouts do not match" + else + outcome="failure" + description="Job skipped since ${{ github.event.workflow_run.name }} failed." + fi + gh api \ + --method POST \ + /repos/${{ github.repository }}/statuses/${{ github.event.workflow_run.head_commit.id }} \ + -f state="$outcome" \ + -f context="${{ github.workflow }}" \ + -f description="$description" \ + -f target_url="https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" + - name: Set message again + # Even though the job is different, specify a unique ID. + id: set-message-again + env: + WORKFLOW_URL: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }} + WORKFLOW_NAME: ${{ github.workflow }} + SHA: ${{ github.event.workflow_run.head_commit.id }} + run: | + if [ ${{ steps.compare-layouts.outcome }} == "success" ]; then + message="✅ The $WORKFLOW_NAME workflow has completed successfully." + elif [ ${{ steps.compare-layouts.outcome }} == "failure" ]; then + message="❌ The $WORKFLOW_NAME workflow has failed!" + else + message="⏭ The $WORKFLOW_NAME workflow was skipped." + fi + echo "message=$message Check the [workflow run]($WORKFLOW_URL) for details. ($SHA)" >> "${GITHUB_OUTPUT}" + - name: Comment CI Status + uses: marocchino/sticky-pull-request-comment@v2 + if: ${{ github.event.workflow_run.event == 'pull_request' }} + with: + header: ${{ github.workflow }} + hide_details: true + number: ${{ needs.set-commit-status.outputs.number }} + message: ${{ steps.set-message-again.outputs.message }} + - name: Exit with the correct code + if: always() + # if the outcome is not set, it will exit 1. so, a failure in the parent job will + # result in a failure here. + run: | + if [[ "${{ steps.compare-layouts.outcome }}" == "success" ]]; then + exit 0 + else + exit 1 + fi diff --git a/.github/workflows/compare_deployed_storage_layout.py b/.github/workflows/compare_deployed_storage_layout.py deleted file mode 100644 index 3c3737aa..00000000 --- a/.github/workflows/compare_deployed_storage_layout.py +++ /dev/null @@ -1,87 +0,0 @@ -#!/usr/bin/env python - -import json -import subprocess -import pandas as pd -import os -from compare_storage_layout import parse_output, compare_layouts, get_current_layout - -def get_deployed_addresses(): - with open('script/deployedContracts.json', 'r') as f: - data = json.load(f) - return { - 'Bootstrap': data['clientChain'].get('bootstrapLogic'), - 'ClientChainGateway': data['clientChain'].get('clientGatewayLogic'), - 'Vault': data['clientChain'].get('vaultImplementation'), - 'RewardVault': data['clientChain'].get('rewardVaultImplementation'), - 'ExoCapsule': data['clientChain'].get('capsuleImplementation') - } - -def get_storage_layout(contract_name, address, rpc_url, etherscan_api_key): - if not address: - print(f"Skipping {contract_name} as it's not deployed.") - return pd.DataFrame() - - result = subprocess.run(['cast', 'storage', address, '--rpc-url', rpc_url, '--etherscan-api-key', etherscan_api_key], capture_output=True, text=True) - print(f"finish executing: cast storage {address} --rpc-url ...") - - if result.returncode != 0: - raise Exception(f"Error getting current layout for {contract_name}: {result.stderr}") - - return parse_output(contract_name, result.stdout.split('\n')) - -def load_and_parse_layout(contract_name, path): - with open(path, 'r') as f: - lines = f.readlines() - return parse_output(contract_name, lines) - -if __name__ == "__main__": - try: - api_key = os.getenv('ALCHEMY_API_KEY') - if not api_key: - raise ValueError("ALCHEMY_API_KEY environment variable is not set") - etherscan_api_key = os.getenv('ETHERSCAN_API_KEY') - if not etherscan_api_key: - raise ValueError("ETHERSCAN_API_KEY environment variable is not set") - - # Construct the RPC URL for Sepolia - rpc_url = f"https://eth-sepolia.g.alchemy.com/v2/{api_key}" - - addresses = get_deployed_addresses() - all_mismatches = {} - - for contract_name, address in addresses.items(): - print(f"Checking {contract_name}...") - deployed_layout = get_storage_layout(contract_name, address, rpc_url, etherscan_api_key) - if deployed_layout.empty: - print(f"No deployed layout found for {contract_name}.") - continue - - current_layout = get_current_layout(contract_name) - if current_layout.empty: - raise ValueError(f"Error: No valid entries of current layout found for {contract_name}.") - - mismatches = compare_layouts(deployed_layout, current_layout) - if mismatches: - all_mismatches[contract_name] = mismatches - - # then we load the layout file of ExocoreGateway on target branch and compare it with the current layout - print("Checking ExocoreGateway...") - target_branch_layout = load_and_parse_layout('ExocoreGateway', 'ExocoreGateway_target.txt') - current_layout = get_current_layout('ExocoreGateway') - mismatches = compare_layouts(target_branch_layout, current_layout) - if mismatches: - all_mismatches['ExocoreGateway'] = mismatches - - if all_mismatches: - print("Mismatches found for current contracts:") - for contract, mismatches in all_mismatches.items(): - print(f"{contract}:") - for mismatch in mismatches: - print(f" {mismatch}") - exit(1) - else: - print("Storage layout is compatible with all deployed contracts.") - except Exception as e: - print(f"Error: {e}") - exit(1) diff --git a/.github/workflows/compare_storage_layout.py b/.github/workflows/compare_storage_layout.py deleted file mode 100755 index 7ec6aee7..00000000 --- a/.github/workflows/compare_storage_layout.py +++ /dev/null @@ -1,81 +0,0 @@ -#!/usr/bin/env python - -import pandas as pd -import subprocess - -def parse_output(contract_name, lines): - # Clean up the output and create a dataframe - data = [] - separator_line = len(lines) - for i, line in enumerate(lines): # start from the line next to the separator - if i > separator_line and line.startswith('|'): - parts = [part.strip() for part in line.split('|')[1:-1]] # Remove empty first and last elements - data.append(parts[:6]) # Keep Name, Type, Slot, Offset, Bytes, Contract - elif line.startswith('|') and 'Name' in line: - separator_line = i + 1 - - if not data: - raise Exception(f"No valid storage layout data found for {contract_name}") - - df = pd.DataFrame(data, columns=['Name', 'Type', 'Slot', 'Offset', 'Bytes', 'Contract']) - - # Convert numeric columns - for col in ['Slot', 'Offset', 'Bytes']: - df[col] = pd.to_numeric(df[col]) - - return df - -def get_current_layout(contract_name): - result = subprocess.run(['forge', 'inspect', f'src/core/{contract_name}.sol:{contract_name}', 'storage-layout', '--pretty'], capture_output=True, text=True) - print(f"finished executing forge inspect for {contract_name}") - - if result.returncode != 0: - raise Exception(f"Error getting current layout for {contract_name}: {result.stderr}") - - return parse_output(contract_name, result.stdout.split('\n')) - -def compare_layouts(old_layout, new_layout): - mismatches = [] - - # Ensure both dataframes have the same columns - columns = ['Name', 'Type', 'Slot', 'Offset', 'Bytes'] - old_layout = old_layout[columns].copy() - new_layout = new_layout[columns].copy() - - # Compare non-gap variables - for index, row in old_layout.iterrows(): - if row['Name'] != '__gap': - current_row = new_layout.loc[new_layout['Name'] == row['Name']] - if current_row.empty: - mismatches.append(f"Variable {row['Name']} is missing in the current layout") - elif not current_row.iloc[0].equals(row): - mismatches.append(f"Variable {row['Name']} has changed") - - if not mismatches: - print("No mismatches found") - - return mismatches - -if __name__ == "__main__": - try: - clientChainGateway_layout = get_current_layout("ClientChainGateway") - bootstrap_layout = get_current_layout("Bootstrap") - - if clientChainGateway_layout.empty: - raise ValueError("Error: No valid entries found for ClientChainGateway.") - - if bootstrap_layout.empty: - raise ValueError("Error: No valid entries found for Bootstrap.") - - mismatches = compare_layouts(bootstrap_layout, clientChainGateway_layout) - - if mismatches: - print(f"Mismatches found: {len(mismatches)}") - for mismatch in mismatches: - print(mismatch) - exit(1) - else: - print("All entries in Bootstrap match ClientChainGateway at the correct positions.") - except Exception as e: - print(f"Error: {e}") - exit(1) diff --git a/.github/workflows/forge-ci.yml b/.github/workflows/forge-ci.yml index a5e0e735..926f3544 100644 --- a/.github/workflows/forge-ci.yml +++ b/.github/workflows/forge-ci.yml @@ -1,4 +1,4 @@ -name: Forge CI to build, test, format and compare storage layout +name: Forge CI on: merge_group: @@ -12,42 +12,40 @@ on: jobs: setup: - uses: ./.github/workflows/foundry-setup.yml + # A full job can be used as a reusable workflow but not a step. + uses: ./.github/workflows/reusable-foundry-setup.yml with: + # The below line does not accept environment variables, + # so it becomes the single source of truth for the version. foundry-version: nightly build: + # Caching is slow; takes about 3 minutes. + timeout-minutes: 15 runs-on: ubuntu-latest needs: setup outputs: - installation-dir: ${{ needs.setup.outputs.installation-dir }} + # The cache-key only contains the version name. It is only used so that the name does not + # need to be repeated everywhere; instead setting the `foundry-version` above suffices. cache-key: ${{ needs.setup.outputs.cache-key }} + # Github's cache actions are a bit weird to deal with. It wouldn't let me restore the + # binaries to /usr/bin, so I restored them to the original location and added it to PATH. + # This output will let us carry it to other jobs. + installation-dir: ${{ needs.setup.outputs.installation-dir }} steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - submodules: recursive - name: Restore cached Foundry toolchain uses: actions/cache/restore@v3 with: path: ${{ needs.setup.outputs.installation-dir }} key: ${{ needs.setup.outputs.cache-key }} - name: Add Foundry to PATH - run: echo "${{ needs.setup.outputs.installation-dir }}" >> $GITHUB_PATH + run: echo "${{ needs.setup.outputs.installation-dir }}" >> "$GITHUB_PATH" + - name: Checkout repository + uses: actions/checkout@v4 + with: + submodules: recursive - name: Build run: forge build - - name: Add comment for build failure - if: failure() - uses: actions/github-script@v6 - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - script: | - github.rest.issues.createComment({ - issue_number: context.issue.number, - owner: context.repo.owner, - repo: context.repo.repo, - body: 'The build has failed. Please check the logs.' - }) - name: Cache build artifacts uses: actions/cache/save@v3 with: @@ -56,21 +54,23 @@ jobs: ./out ./cache ./broadcast - key: ${{ runner.os }}-build-${{ github.sha }} + key: build-${{ github.event.pull_request.head.sha || github.event.after || github.sha }} test: + # Takes less than 30s + timeout-minutes: 5 runs-on: ubuntu-latest needs: build steps: - - name: Checkout repository - uses: actions/checkout@v4 - name: Restore cached Foundry toolchain uses: actions/cache/restore@v3 with: path: ${{ needs.build.outputs.installation-dir }} key: ${{ needs.build.outputs.cache-key }} - name: Add Foundry to PATH - run: echo "${{ needs.build.outputs.installation-dir }}" >> $GITHUB_PATH + run: echo "${{ needs.build.outputs.installation-dir }}" >> "$GITHUB_PATH" + - name: Checkout repository + uses: actions/checkout@v4 - name: Restore build artifacts uses: actions/cache/restore@v3 with: @@ -79,37 +79,27 @@ jobs: ./out ./cache ./broadcast - key: ${{ runner.os }}-build-${{ github.sha }} + key: build-${{ github.event.pull_request.head.sha || github.event.after || github.sha }} - name: Test run: forge test -vvv - name: Set test snapshot as summary run: NO_COLOR=1 forge snapshot >> $GITHUB_STEP_SUMMARY - - name: Add comment for test failure - if: failure() - uses: actions/github-script@v6 - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - script: | - github.rest.issues.createComment({ - issue_number: context.issue.number, - owner: context.repo.owner, - repo: context.repo.repo, - body: 'The tests have failed. Please check the logs.' - }) format: + # Takes less than 30s + timeout-minutes: 5 runs-on: ubuntu-latest needs: build steps: - - name: Checkout repository - uses: actions/checkout@v4 - name: Restore cached Foundry toolchain uses: actions/cache/restore@v3 with: path: ${{ needs.build.outputs.installation-dir }} key: ${{ needs.build.outputs.cache-key }} - name: Add Foundry to PATH - run: echo "${{ needs.build.outputs.installation-dir }}" >> $GITHUB_PATH + run: echo "${{ needs.build.outputs.installation-dir }}" >> "$GITHUB_PATH" + - name: Checkout repository + uses: actions/checkout@v4 - name: Restore build artifacts uses: actions/cache/restore@v3 with: @@ -118,38 +108,61 @@ jobs: ./out ./cache ./broadcast - key: ${{ runner.os }}-build-${{ github.sha }} + key: build-${{ github.event.pull_request.head.sha || github.event.after || github.sha }} - name: Check formatting run: forge fmt --check - - name: Add comment for format check failure - if: failure() - uses: actions/github-script@v6 - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - script: | - github.rest.issues.createComment({ - issue_number: context.issue.number, - owner: context.repo.owner, - repo: context.repo.repo, - body: 'The code is not formatted correctly. Please run `forge fmt` and push the changes.' - }) - compare-storage-layout: + extract-base-storage-layout-exocore-gateway: + # Takes less than 30 seconds, but add some margin for git clone + timeout-minutes: 10 runs-on: ubuntu-latest needs: build - env: - ALCHEMY_API_KEY: ${{ secrets.ALCHEMY_API_KEY }} - ETHERSCAN_API_KEY: ${{ secrets.ETHERSCAN_API_KEY }} steps: - - name: Checkout repository + - name: Restore cached Foundry toolchain + uses: actions/cache/restore@v3 + with: + path: ${{ needs.build.outputs.installation-dir }} + key: ${{ needs.build.outputs.cache-key }} + - name: Add Foundry to PATH + run: echo "${{ needs.build.outputs.installation-dir }}" >> "$GITHUB_PATH" + - name: Checkout base branch or previous commit uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.base.ref || github.event.before }} + # We don't have a `lib` folder to restore for this step, so we + # recursively checkout the submodules. In other steps, we use the + # `lib` folder from the `build` job. + submodules: recursive + - name: Generate base branch layout file + # Note that this `run` will do a `forge build` so we don't need to do it ourselves. + # The build artifacts of this step are not relevant to us either, so we don't need to + # cache them. + run: | + forge inspect src/core/ExocoreGateway.sol:ExocoreGateway storage-layout > ExocoreGateway.base.json + - name: Upload storage layout file as an artifact + uses: actions/upload-artifact@v4 + with: + path: ExocoreGateway.base.json + name: compiled-layout-ExocoreGateway-base-${{ github.event.pull_request.base.sha || github.event.after || github.sha }} + + extract-storage-layout: + # Takes less than 30 seconds + timeout-minutes: 5 + runs-on: ubuntu-latest + needs: build + strategy: + matrix: + contract: [Bootstrap, ClientChainGateway, RewardVault, Vault, ExocoreGateway, ExoCapsule] + steps: - name: Restore cached Foundry toolchain uses: actions/cache/restore@v3 with: path: ${{ needs.build.outputs.installation-dir }} key: ${{ needs.build.outputs.cache-key }} - name: Add Foundry to PATH - run: echo "${{ needs.build.outputs.installation-dir }}" >> $GITHUB_PATH + run: echo "${{ needs.build.outputs.installation-dir }}" >> "$GITHUB_PATH" + - name: Checkout repository + uses: actions/checkout@v4 - name: Restore build artifacts uses: actions/cache/restore@v3 with: @@ -158,48 +171,34 @@ jobs: ./out ./cache ./broadcast - key: ${{ runner.os }}-build-${{ github.sha }} - - name: Checkout target branch - uses: actions/checkout@v4 - with: - ref: ${{ github.event.pull_request.base.ref }} - submodules: recursive - - name: Generate target branch layout files + key: build-${{ github.event.pull_request.head.sha || github.event.after || github.sha }} + - name: Generate storage layout file for ${{ matrix.contract }} run: | - forge inspect src/core/ExocoreGateway.sol:ExocoreGateway storage-layout --pretty > ExocoreGateway_target.txt - - name: Cache target branch layout file - uses: actions/cache/save@v3 - with: - path: ExocoreGateway_target.txt - key: ${{ runner.os }}-exocore-target-${{ github.sha }} - - name: Checkout back to PR - uses: actions/checkout@v4 - with: - ref: ${{ github.event.pull_request.head.sha }} - - name: Restore target branch layout file - uses: actions/cache/restore@v3 - with: - path: ExocoreGateway_target.txt - key: ${{ runner.os }}-exocore-target-${{ github.sha }} - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: '3.12.4' - - name: Install pandas - run: pip install --root-user-action=ignore pandas==2.2.2 - - name: Run the comparison script for Bootstrap and ClientChainGateway - run: python .github/workflows/compare_storage_layout.py - - name: Run the comparison script for deployed contracts - run: python .github/workflows/compare_deployed_storage_layout.py - - name: Add comment for storage layout mismatch failure - if: failure() - uses: actions/github-script@v6 + forge inspect src/core/${{ matrix.contract }}.sol:${{ matrix.contract }} storage-layout > ${{ matrix.contract }}.compiled.json; + - name: Upload storage layout file as an artifact + uses: actions/upload-artifact@v4 with: - github-token: ${{ secrets.GITHUB_TOKEN }} - script: | - github.rest.issues.createComment({ - issue_number: context.issue.number, - owner: context.repo.owner, - repo: context.repo.repo, - body: 'Storage layout compatibility check failed. This could be due to a mismatch between Bootstrap and ClientChainGateway, or incompatibility with deployed contracts on Sepolia. Please check the logs for details.' - }) + path: ${{ matrix.contract }}.compiled.json + name: compiled-layout-${{ matrix.contract}}-${{ github.event.pull_request.head.sha || github.event.after || github.sha }} + + combine-storage-layouts: + # Takes less than 10 seconds + timeout-minutes: 5 + runs-on: ubuntu-latest + needs: + - extract-base-storage-layout-exocore-gateway + - extract-storage-layout + steps: + - name: Download artifacts + uses: actions/download-artifact@v4 + # No name means all artifacts are downloaded within their respective subfolders + # inside the provided path. + with: + path: combined + - name: Zip up the compiled layouts + run: zip -j compiled-layouts.zip combined/*/*.json + - name: Upload the compiled layouts file as an artifact + uses: actions/upload-artifact@v4 + with: + path: compiled-layouts.zip + name: compiled-layouts-${{ github.event.pull_request.head.sha || github.event.after || github.sha }} diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index e1addf49..b4e82c73 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -1,4 +1,4 @@ -name: Run `solhint` linter +name: Solhint on: merge_group: @@ -11,23 +11,20 @@ on: - "*" jobs: - check: + lint: + # Usually done in 30 seconds + timeout-minutes: 5 strategy: fail-fast: true - name: Foundry project runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Set up Node.js - uses: actions/setup-node@v2 + uses: actions/setup-node@v4 with: - # Node 22.5 has a bug - # https://github.com/nodejs/node/pull/53904 - # TODO: Once a version with the bug fix (above) is released, - # the version pin can be moved back to 22 - node-version: '22.4' + node-version: '20' # LTS till Oct-25 - name: Clear npm cache run: npm cache clean --force @@ -38,16 +35,3 @@ jobs: - name: Run Solhint run: | npx solhint 'src/**/*.sol' -c ./src/.solhint.json - - - name: Add comment on failure - if: failure() - uses: actions/github-script@v6 - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - script: | - github.rest.issues.createComment({ - issue_number: context.issue.number, - owner: context.repo.owner, - repo: context.repo.repo, - body: 'Linting failed. Please check the logs.' - }) diff --git a/.github/workflows/foundry-setup.yml b/.github/workflows/reusable-foundry-setup.yml similarity index 57% rename from .github/workflows/foundry-setup.yml rename to .github/workflows/reusable-foundry-setup.yml index d4367318..a0284c8f 100644 --- a/.github/workflows/foundry-setup.yml +++ b/.github/workflows/reusable-foundry-setup.yml @@ -6,28 +6,41 @@ on: inputs: foundry-version: required: true + description: "The version of Foundry to install" type: string + skip-install: + required: false + description: "Skip the installation. Useful to avoid installation and the extremely time consuming caching but still run this job to avoid notifications." + type: boolean + default: false outputs: installation-dir: description: "The installation directory of Foundry toolchain" - value: ${{ jobs.setup.outputs.installation-dir }} + value: ${{ jobs.install.outputs.installation-dir }} cache-key: description: "The cache key for Foundry toolchain" - value: ${{ jobs.setup.outputs.cache-key }} + value: ${{ jobs.install.outputs.cache-key }} jobs: - setup: + install: + # Caching is slow, takes about 3 minutes total + timeout-minutes: 15 runs-on: ubuntu-latest outputs: cache-key: ${{ steps.set-cache-key.outputs.cache-key }} installation-dir: ${{ steps.find-path.outputs.installation-dir }} steps: + - name: Echo skipping status + if: ${{ inputs.skip-install }} + run: echo "Skipping Foundry installation" - name: Install Foundry uses: foundry-rs/foundry-toolchain@v1 with: version: ${{ inputs.foundry-version }} + if: ${{ !inputs.skip-install }} - name: Print forge version run: forge --version + if: ${{ !inputs.skip-install }} # Unfortunately, the `foundry-toolchain` action installs it in a # randomly generated location, so we must determine it ourselves - name: Determine Foundry installation path @@ -36,12 +49,15 @@ jobs: installation_path=$(which forge) installation_dir=$(dirname $installation_path) echo "installation-dir=$installation_dir" >> "$GITHUB_OUTPUT" + if: ${{ !inputs.skip-install }} - name: Cache the Foundry toolchain uses: actions/cache/save@v3 with: path: ${{ steps.find-path.outputs.installation-dir }} - key: ${{ runner.os }}-foundry-${{ inputs.foundry-version }} + key: foundry-${{ inputs.foundry-version }} + if: ${{ !inputs.skip-install }} - name: Set cache key id: set-cache-key run: | - echo "cache-key=${{ runner.os }}-foundry-${{ inputs.foundry-version }}" >> "$GITHUB_OUTPUT" + echo "cache-key=foundry-${{ inputs.foundry-version }}" >> "$GITHUB_OUTPUT" + if: ${{ !inputs.skip-install }} diff --git a/.github/workflows/slither.yml b/.github/workflows/slither.yml index 15a096a3..2ecc8b37 100644 --- a/.github/workflows/slither.yml +++ b/.github/workflows/slither.yml @@ -12,6 +12,8 @@ on: jobs: analyze: + # Takes about 2 minutes + timeout-minutes: 10 runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/status-comment.yml b/.github/workflows/status-comment.yml new file mode 100644 index 00000000..dd41b927 --- /dev/null +++ b/.github/workflows/status-comment.yml @@ -0,0 +1,86 @@ +name: Comment CI status on PR + +on: + workflow_run: + workflows: + - "Forge CI" + - "Slither Analysis" + - "Solhint" + # Nested workflow_run is not supported, so this doesn't work. Instead + # that workflow should make comments by itself. + # - "Compare Storage Layouts" + types: + - completed + - requested + +permissions: + pull-requests: write + issues: read + +jobs: + comment_status: + runs-on: ubuntu-latest + # Typically takes no more than 30s + timeout-minutes: 5 + steps: + # Log the workflow trigger details for debugging. + - name: Echo workflow trigger details + run: | + echo "Event action: ${{ github.event.action }}" + echo "Workflow run event: ${{ github.event.workflow_run.event }}" + echo "Workflow run conclusion: ${{ github.event.workflow_run.conclusion }}" + echo "Workflow run name: ${{ github.event.workflow_run.name }}" + echo "Workflow run URL: ${{ github.event.workflow_run.html_url }}" + echo "Commit SHA: ${{ github.event.workflow_run.head_commit.id }}" + echo "Workflow Run ID: ${{ github.event.workflow_run.id }}" + - name: Get PR number + id: pr-context + if: ${{ github.event.workflow_run.event == 'pull_request' }} + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PR_TARGET_REPO: ${{ github.repository }} + PR_BRANCH: |- + ${{ + (github.event.workflow_run.head_repository.owner.login != github.event.workflow_run.repository.owner.login) + && format('{0}:{1}', github.event.workflow_run.head_repository.owner.login, github.event.workflow_run.head_branch) + || github.event.workflow_run.head_branch + }} + run: | + pr_number=$(gh pr view --repo "${PR_TARGET_REPO}" "${PR_BRANCH}" \ + --json 'number' --jq '.number') + if [ -z "$pr_number" ]; then + echo "Error: PR number not found for branch '${PR_BRANCH}' in repository '${PR_TARGET_REPO}'" >&2 + exit 1 + fi + echo "number=$pr_number" >> "${GITHUB_OUTPUT}" + # Construct the message + - name: Set message + id: set-message + if: ${{ github.event.workflow_run.event == 'pull_request' }} + env: + WORKFLOW_NAME: ${{ github.event.workflow_run.name }} + WORKFLOW_URL: ${{ github.event.workflow_run.html_url }} + WORKFLOW_CONCLUSION: ${{ github.event.workflow_run.conclusion }} + SHA: ${{ github.event.workflow_run.head_commit.id }} + run: | + if [ "${{ github.event.action }}" == "requested" ]; then + message="🚀 The $WORKFLOW_NAME workflow has started." + elif [ "${{ github.event.workflow_run.conclusion }}" == "success" ]; then + message="✅ The $WORKFLOW_NAME workflow has completed successfully." + elif [ "${{ github.event.workflow_run.conclusion }}" == "failure" ]; then + message="❌ The $WORKFLOW_NAME workflow has failed!" + elif [ "${{ github.event.workflow_run.conclusion }}" == "cancelled" ]; then + message="⏚ī¸ The $WORKFLOW_NAME workflow was cancelled." + else + message="❓ The $WORKFLOW_NAME workflow has completed with an unknown status." + fi + echo "message=$message Check the [workflow run]($WORKFLOW_URL) for details. ($SHA)" >> "${GITHUB_OUTPUT}" + # Finally, post the status comment on the PR + - name: Comment parent CI Status + uses: marocchino/sticky-pull-request-comment@v2 + if: ${{ github.event.workflow_run.event == 'pull_request' }} + with: + header: ${{ github.event.workflow_run.name }} + hide_details: true + number: ${{ steps.pr-context.outputs.number }} + message: ${{ steps.set-message.outputs.message }} diff --git a/package-lock.json b/package-lock.json index e66d06cc..732b39c6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "1.0.0", "license": "ISC", "dependencies": { + "@openzeppelin/upgrades-core": "^1.40.0", "abbrev": "^1.0.9", "abstract-level": "^1.0.3", "acorn": "^8.11.2", @@ -1740,6 +1741,97 @@ "hardhat": "^2.0.4" } }, + "node_modules/@nomicfoundation/slang": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@nomicfoundation/slang/-/slang-0.17.0.tgz", + "integrity": "sha512-1GlkGRcGpVnjFw9Z1vvDKOKo2mzparFt7qrl2pDxWp+jrVtlvej98yCMX52pVyrYE7ZeOSZFnx/DtsSgoukStQ==", + "dependencies": { + "@nomicfoundation/slang-darwin-arm64": "0.17.0", + "@nomicfoundation/slang-darwin-x64": "0.17.0", + "@nomicfoundation/slang-linux-arm64-gnu": "0.17.0", + "@nomicfoundation/slang-linux-arm64-musl": "0.17.0", + "@nomicfoundation/slang-linux-x64-gnu": "0.17.0", + "@nomicfoundation/slang-linux-x64-musl": "0.17.0", + "@nomicfoundation/slang-win32-arm64-msvc": "0.17.0", + "@nomicfoundation/slang-win32-ia32-msvc": "0.17.0", + "@nomicfoundation/slang-win32-x64-msvc": "0.17.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@nomicfoundation/slang-darwin-arm64": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@nomicfoundation/slang-darwin-arm64/-/slang-darwin-arm64-0.17.0.tgz", + "integrity": "sha512-O0q94EUtoWy9A5kOTOa9/khtxXDYnLqmuda9pQELurSiwbQEVCPQL8kb34VbOW+ifdre66JM/05Xw9JWhIZ9sA==", + "engines": { + "node": ">= 10" + } + }, + "node_modules/@nomicfoundation/slang-darwin-x64": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@nomicfoundation/slang-darwin-x64/-/slang-darwin-x64-0.17.0.tgz", + "integrity": "sha512-IaDbHzvT08sBK2HyGzonWhq1uu8IxdjmTqAWHr25Oh/PYnamdi8u4qchZXXYKz/DHLoYN3vIpBXoqLQIomhD/g==", + "engines": { + "node": ">= 10" + } + }, + "node_modules/@nomicfoundation/slang-linux-arm64-gnu": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@nomicfoundation/slang-linux-arm64-gnu/-/slang-linux-arm64-gnu-0.17.0.tgz", + "integrity": "sha512-Lj4anvOsQZxs1SycG8VyT2Rl2oqIhyLSUCgGepTt3CiJ/bM+8r8bLJIgh8vKkki4BWz49YsYIgaJB2IPv8FFTw==", + "engines": { + "node": ">= 10" + } + }, + "node_modules/@nomicfoundation/slang-linux-arm64-musl": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@nomicfoundation/slang-linux-arm64-musl/-/slang-linux-arm64-musl-0.17.0.tgz", + "integrity": "sha512-/xkTCa9d5SIWUBQE3BmLqDFfJRr4yUBwbl4ynPiGUpRXrD69cs6pWKkwjwz/FdBpXqVo36I+zY95qzoTj/YhOA==", + "engines": { + "node": ">= 10" + } + }, + "node_modules/@nomicfoundation/slang-linux-x64-gnu": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@nomicfoundation/slang-linux-x64-gnu/-/slang-linux-x64-gnu-0.17.0.tgz", + "integrity": "sha512-oe5IO5vntOqYvTd67deCHPIWuSuWm6aYtT2/0Kqz2/VLtGz4ClEulBSRwfnNzBVtw2nksWipE1w8BzhImI7Syg==", + "engines": { + "node": ">= 10" + } + }, + "node_modules/@nomicfoundation/slang-linux-x64-musl": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@nomicfoundation/slang-linux-x64-musl/-/slang-linux-x64-musl-0.17.0.tgz", + "integrity": "sha512-PpYCI5K/kgLAMXaPY0V4VST5gCDprEOh7z/47tbI8kJQumI5odjsj/Cs8MpTo7/uRH6flKYbVNgUzcocWVYrAQ==", + "engines": { + "node": ">= 10" + } + }, + "node_modules/@nomicfoundation/slang-win32-arm64-msvc": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@nomicfoundation/slang-win32-arm64-msvc/-/slang-win32-arm64-msvc-0.17.0.tgz", + "integrity": "sha512-u/Mkf7OjokdBilP7QOJj6QYJU4/mjkbKnTX21wLyCIzeVWS7yafRPYpBycKIBj2pRRZ6ceAY5EqRpb0aiCq+0Q==", + "engines": { + "node": ">= 10" + } + }, + "node_modules/@nomicfoundation/slang-win32-ia32-msvc": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@nomicfoundation/slang-win32-ia32-msvc/-/slang-win32-ia32-msvc-0.17.0.tgz", + "integrity": "sha512-XJBVQfNnZQUv0tP2JSJ573S+pmgrLWgqSZOGaMllnB/TL1gRci4Z7dYRJUF2s82GlRJE+FHSI2Ro6JISKmlXCg==", + "engines": { + "node": ">= 10" + } + }, + "node_modules/@nomicfoundation/slang-win32-x64-msvc": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@nomicfoundation/slang-win32-x64-msvc/-/slang-win32-x64-msvc-0.17.0.tgz", + "integrity": "sha512-zPGsAeiTfqfPNYHD8BfrahQmYzA78ZraoHKTGraq/1xwJwzBK4bu/NtvVA4pJjBV+B4L6DCxVhSbpn40q26JQA==", + "engines": { + "node": ">= 10" + } + }, "node_modules/@nomicfoundation/solidity-analyzer": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/@nomicfoundation/solidity-analyzer/-/solidity-analyzer-0.1.1.tgz", @@ -1775,6 +1867,123 @@ "node": ">= 10" } }, + "node_modules/@openzeppelin/upgrades-core": { + "version": "1.40.0", + "resolved": "https://registry.npmjs.org/@openzeppelin/upgrades-core/-/upgrades-core-1.40.0.tgz", + "integrity": "sha512-4bPSXdEqHsNRL5T1ybPLneWGYjzGl6XWGWkv7aUoFFgz8mOdarstRBX1Wi4XJFw6IeHPUI7mMSQr2jdz8Y2ypQ==", + "dependencies": { + "@nomicfoundation/slang": "^0.17.0", + "cbor": "^9.0.0", + "chalk": "^4.1.0", + "compare-versions": "^6.0.0", + "debug": "^4.1.1", + "ethereumjs-util": "^7.0.3", + "minimatch": "^9.0.5", + "minimist": "^1.2.7", + "proper-lockfile": "^4.1.1", + "solidity-ast": "^0.4.51" + }, + "bin": { + "openzeppelin-upgrades-core": "dist/cli/cli.js" + } + }, + "node_modules/@openzeppelin/upgrades-core/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@openzeppelin/upgrades-core/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@openzeppelin/upgrades-core/node_modules/cbor": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/cbor/-/cbor-9.0.2.tgz", + "integrity": "sha512-JPypkxsB10s9QOWwa6zwPzqE1Md3vqpPc+cai4sAecuCsRyAtAl/pMyhPlMbT/xtPnm2dznJZYRLui57qiRhaQ==", + "dependencies": { + "nofilter": "^3.1.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/@openzeppelin/upgrades-core/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@openzeppelin/upgrades-core/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/@openzeppelin/upgrades-core/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/@openzeppelin/upgrades-core/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/@openzeppelin/upgrades-core/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@openzeppelin/upgrades-core/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/@pnpm/config.env-replace": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@pnpm/config.env-replace/-/config.env-replace-1.1.0.tgz", @@ -3038,6 +3247,11 @@ "resolved": "https://registry.npmjs.org/commander/-/commander-3.0.2.tgz", "integrity": "sha512-Gar0ASD4BDyKC4hl4DwHqDrmvjoxWKZigVnAbn5H1owvm4CxCPdb0HQDehwNYMJpla5+M2tPmPARzhtYuwpHow==" }, + "node_modules/compare-versions": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-6.1.1.tgz", + "integrity": "sha512-4hm4VPpIecmlg59CHXnRDnqGplJFrbLG4aFEl5vl6cK1u76ws3LLvX7ikFnTDl5vo39sjWD6AaDPYodJp/NNHg==" + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -5939,6 +6153,16 @@ "asap": "~2.0.6" } }, + "node_modules/proper-lockfile": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz", + "integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==", + "dependencies": { + "graceful-fs": "^4.2.4", + "retry": "^0.12.0", + "signal-exit": "^3.0.2" + } + }, "node_modules/proto-list": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", @@ -6208,6 +6432,14 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "engines": { + "node": ">= 4" + } + }, "node_modules/reusify": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", @@ -6530,6 +6762,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" + }, "node_modules/slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", @@ -6834,6 +7071,11 @@ "node": ">=8" } }, + "node_modules/solidity-ast": { + "version": "0.4.59", + "resolved": "https://registry.npmjs.org/solidity-ast/-/solidity-ast-0.4.59.tgz", + "integrity": "sha512-I+CX0wrYUN9jDfYtcgWSe+OAowaXy8/1YQy7NS4ni5IBDmIYBq7ZzaP/7QqouLjzZapmQtvGLqCaYgoUWqBo5g==" + }, "node_modules/solidity-coverage": { "version": "0.8.5", "resolved": "https://registry.npmjs.org/solidity-coverage/-/solidity-coverage-0.8.5.tgz", diff --git a/package.json b/package.json index 87cd8749..d7c72afa 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "test": "test" }, "dependencies": { + "@openzeppelin/upgrades-core": "^1.40.0", "abbrev": "^1.0.9", "abstract-level": "^1.0.3", "acorn": "^8.11.2", diff --git a/script/compareLayouts.js b/script/compareLayouts.js new file mode 100644 index 00000000..11c57891 --- /dev/null +++ b/script/compareLayouts.js @@ -0,0 +1,52 @@ +const fs = require('fs'); +const { getStorageUpgradeReport } = require('@openzeppelin/upgrades-core'); + +// Mapping of deployed and compiled file names +const fileMappings = [ + { before: 'Bootstrap.deployed.json', after: 'Bootstrap.compiled.json', mustExist: true }, + { before: 'ClientChainGateway.deployed.json', after: 'ClientChainGateway.compiled.json', mustExist: true }, + { before: 'Vault.deployed.json', after: 'Vault.compiled.json', mustExist: true }, + // TODO: once RewardVault is deployed, change mustExist to true + { before: 'RewardVault.deployed.json', after: 'RewardVault.compiled.json', mustExist: false }, + { before: 'ExoCapsule.deployed.json', after: 'ExoCapsule.compiled.json', mustExist: true }, + { before: 'ExocoreGateway.base.json', after: 'ExocoreGateway.compiled.json', mustExist: true }, + { before: 'Bootstrap.compiled.json', after: 'ClientChainGateway.compiled.json', mustExist: true }, +]; + +// Loop through each mapping, load JSON files, and run the comparison +fileMappings.forEach(({ before, after, mustExist }) => { + console.log(`🔍 Comparing ${before} and ${after}...`); + + try { + // Ensure files exist + const beforeExists = fs.existsSync(before); + const afterExists = fs.existsSync(after); + + if (!beforeExists || !afterExists) { + if (mustExist) { + throw new Error(`❌ Required file(s) missing: ${beforeExists ? '' : before} ${afterExists ? '' : after}`); + } + console.log(`⚠ī¸ Skipping: Missing file(s): ${beforeExists ? '' : before} ${afterExists ? '' : after}`); + return; + } + + // Load the JSON files + const deployedData = JSON.parse(fs.readFileSync(before, 'utf8')); + const compiledData = JSON.parse(fs.readFileSync(after, 'utf8')); + + // Run the storage upgrade comparison + const report = getStorageUpgradeReport(deployedData, compiledData, { unsafeAllowCustomTypes: true }); + + // Print the report if issues are found + if (!report.ok) { + console.log(`⚠ī¸ Issues found in ${before} and ${after}:`); + console.log(report.explain()); + process.exitCode = 1; + } else { + console.log(`✅ No issues detected between ${before} and ${after}.`); + } + } catch (error) { + console.error(`❌ Error processing ${before} and ${after}: ${error.message}`); + process.exitCode = 1; + } +}); diff --git a/src/storage/ClientChainGatewayStorage.sol b/src/storage/ClientChainGatewayStorage.sol index dc161ee2..47f0a235 100644 --- a/src/storage/ClientChainGatewayStorage.sol +++ b/src/storage/ClientChainGatewayStorage.sol @@ -41,7 +41,7 @@ contract ClientChainGatewayStorage is BootstrapStorage { IRewardVault public rewardVault; /// @dev Storage gap to allow for future upgrades. - uint256[40] private __gap; + uint256[39] private __gap; /* ----------------------------- restaking events ------------------------------ */