diff --git a/.github/actions/composite-action-pr-status-comment/action.yml b/.github/actions/composite-action-pr-status-comment/action.yml new file mode 100644 index 00000000..ddfb6eae --- /dev/null +++ b/.github/actions/composite-action-pr-status-comment/action.yml @@ -0,0 +1,82 @@ +name: Update CI Status Comment +description: Posts or updates a status comment on a pull request. +inputs: + workflow-conclusion: + description: 'The conclusion of the parent workflow (success, failure, or running).' + required: true + workflow-url: + description: 'The URL to the parent workflow run.' + required: true + workflow-name: + description: 'The name of the CI workflow that triggered this update.' + required: true + github-token: + description: 'The GitHub token to use for making API requests.' + required: true +runs: + using: "composite" + steps: + - name: Echo Workflow Details + shell: bash + run: | + echo "Workflow conclusion: ${{ inputs.workflow-conclusion }}" + echo "Workflow name: ${{ inputs.workflow-name }}" + echo "Workflow URL: ::add-mask::${{ inputs.workflow-url }}" + - name: Comment on Pull Requests + uses: actions/github-script@v6 + with: + github-token: ${{ inputs.github-token }} + script: | + const workflowConclusion = "${{ inputs.workflow-conclusion }}"; + const workflowRunUrl = "${{ inputs.workflow-url }}"; + const workflowName = "${{ inputs.workflow-name }}"; + const pullRequests = context.payload.workflow_run.pull_requests; + + if (pullRequests.length === 0) { + console.log('No pull requests associated with this workflow run.'); + } else { + for (const pr of pullRequests) { + try { + const existingComments = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: pr.number, + }); + + const existingComment = existingComments.data.find(comment => + comment.body.includes(`The ${workflowName} workflow`) + ); + + let commentBody; + + if (workflowConclusion === 'running') { + commentBody = `🔄 The ${workflowName} workflow is currently running. Check the [workflow run](${workflowRunUrl}) for progress.`; + } else if (workflowConclusion === 'failure') { + commentBody = `⚠️ The ${workflowName} workflow has failed! Check the [workflow run](${workflowRunUrl}) for details.`; + } else if (workflowConclusion === 'success') { + commentBody = `✅ The ${workflowName} workflow has completed successfully. Check the [workflow run](${workflowRunUrl}) for details.`; + } else if (workflowConclusion === 'skipped') { + commentBody = `⏭️ The ${workflowName} workflow was skipped. Check the [workflow run](${workflowRunUrl}) for details.`; + } else { + commentBody = `❓ The ${workflowName} workflow has completed with an unknown status. Check the [workflow run](${workflowRunUrl}) for details.`; + } + if (existingComment) { + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: existingComment.id, + body: commentBody, + }); + } else { + await github.rest.issues.createComment({ + issue_number: pr.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: commentBody, + }); + } + } catch (error) { + console.error(`Failed to comment on PR #${pr.number}: ${error.message}`); + } + } + } diff --git a/.github/workflows/compare-layouts.yml b/.github/workflows/compare-layouts.yml new file mode 100644 index 00000000..a22fa760 --- /dev/null +++ b/.github/workflows/compare-layouts.yml @@ -0,0 +1,171 @@ +name: Compare Storage Layouts + +on: + workflow_run: + workflows: ["Forge CI"] + types: + - completed + +jobs: + # The actual job to compare the storage layouts. + compare_storage_layouts: + runs-on: ubuntu-latest + + permissions: + contents: read + # comment.yml inherits these permissions, so we need `write` here. + pull-requests: write + # to mark the commit with the status + statuses: write + + env: + ALCHEMY_API_KEY: ${{ secrets.ALCHEMY_API_KEY }} + ETHERSCAN_API_KEY: ${{ secrets.ETHERSCAN_API_KEY }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + 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 }}" + # For the composite action to be available, we must checkout the repository. + # Later, the repository is used to run the compareLayouts.js script. + - name: Checkout repository + uses: actions/checkout@v3 + # The CI status needs to be posted explicitly because this + # workflow is not triggered by a pull request, and hence, + # the status is not visible under the "Checks" tab. + - name: Post CI Status + if: ${{ github.event.workflow_run.event == 'pull_request' }} + uses: ./.github/actions/composite-action-pr-status-comment + with: + workflow-conclusion: ${{ github.event.workflow_run.conclusion == 'failure' && 'skipped' || 'running' }} + workflow-url: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + workflow-name: "Storage layout comparison" + github-token: ${{ secrets.GITHUB_TOKEN }} + - name: Restore the cached output file + if: ${{ github.event.workflow_run.conclusion == 'success' }} + uses: actions/cache/restore@v3 + with: + path: res.txt + key: cache-${{ github.sha }} + fail-on-cache-miss: true + - name: Load the outputs from the cached file + if: ${{ github.event.workflow_run.conclusion == 'success' }} + id: load-outputs + run: | + echo "cache-key=$(cat res.txt | head -1)" >> "$GITHUB_OUTPUT" + echo "installation-dir=$(cat res.txt | tail -1)" >> "$GITHUB_OUTPUT" + # The toolchain is needed to run `cast storage` + - name: Restore cached Foundry toolchain + if: ${{ github.event.workflow_run.conclusion == 'success' }} + uses: actions/cache/restore@v3 + with: + key: ${{ steps.load-outputs.outputs.cache-key }} + path: ${{ steps.load-outputs.outputs.installation-dir }} + - name: Add Foundry to PATH + if: ${{ github.event.workflow_run.conclusion == 'success' }} + run: echo "${{ steps.load-outputs.outputs.installation-dir }}" >> $GITHUB_PATH + - name: Fetch the deployed layouts + if: ${{ github.event.workflow_run.conclusion == 'success' }} + 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') + pwd=$(pwd) + cd /tmp + # Create an array of contract names and addresses + declare -A contracts=( + ["Bootstrap"]="$bootstrap" + ["ClientChainGateway"]="$clientGateway" + ["Vault"]="$vault" + ["RewardVault"]="$rewardVault" + ["ExoCapsule"]="$capsule" + ) + # Iterate over the array and run `cast storage` for each contract + for contract in "${!contracts[@]}"; do + address=${contracts[$contract]} + if [[ -n $address ]]; then + echo "Processing $contract at address $address" + cast storage --json "$address" --rpc-url "https://eth-sepolia.g.alchemy.com/v2/$ALCHEMY_API_KEY" \ + --etherscan-api-key "$ETHERSCAN_API_KEY" > "$contract.deployed.json" + mv "$contract.deployed.json" "$pwd" + else + echo "Skipping $contract as no address is provided" + fi + done + cd "$pwd" + # Restore the layouts from the cache + - name: Restore the layout files from the previous job + if: ${{ github.event.workflow_run.conclusion == 'success' }} + uses: actions/cache/restore@v3 + with: + path: storage-layouts.zip + key: storage-layouts-${{ github.sha }} + - name: Extract the restored layouts + if: ${{ github.event.workflow_run.conclusion == 'success' }} + run: unzip storage-layouts.zip + - name: Set up Node.js + if: ${{ github.event.workflow_run.conclusion == 'success' }} + uses: actions/setup-node@v2 + 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: Comment or update CI Status + # Only update the status if + # 1. The workflow run was triggered by a pull request + # 2. The triggering workflow run was successful + # We skip the case wherein the triggering workflow failed because + # it was already handled by the "Post CI Status" step. + if: ${{ github.event.workflow_run.event == 'pull_request' && github.event.workflow_run.conclusion == 'success' }} + uses: ./.github/actions/composite-action-pr-status-comment + with: + workflow-conclusion: ${{ steps.compare_layouts.outcome == 'success' && 'success' || 'failure' }} + workflow-url: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + workflow-name: "Storage layout comparison" + github-token: ${{ secrets.GITHUB_TOKEN }} + - name: Update parent commit status + if: always() + # if the outcome is not set, it will post failure + run: | + outcome=$([[ "${{ steps.compare_layouts.outcome }}" == "success" ]] && echo "success" || echo "failure") + curl -X POST \ + -H "Authorization: token $GITHUB_TOKEN" \ + -H "Accept: application/vnd.github+json" \ + -d '{ + "state": "'${outcome}'", + "context": "Compare Storage Layouts", + "description": "Storage layout comparison results.", + "target_url": "https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" + }' \ + https://api.github.com/repos/${{ github.repository }}/statuses/${{ github.event.workflow_run.head_commit.id }} + - name: Exit with the correct code + if: always() + # if the outcome is not set, it will exit 1 + 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..df491cd2 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,21 +12,27 @@ on: jobs: setup: - uses: ./.github/workflows/foundry-setup.yml + # A full job can be used as a reusable workflow. + # However, individual steps cannot call a reusable workflow. + # Instead, they can call a composite action. + 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: 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: @@ -34,20 +40,12 @@ jobs: key: ${{ needs.setup.outputs.cache-key }} - name: Add Foundry to 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,14 +54,12 @@ jobs: ./out ./cache ./broadcast - key: ${{ runner.os }}-build-${{ github.sha }} + key: build-${{ github.sha }} test: 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: @@ -71,6 +67,10 @@ jobs: key: ${{ needs.build.outputs.cache-key }} - name: Add Foundry to PATH run: echo "${{ needs.build.outputs.installation-dir }}" >> $GITHUB_PATH + - name: Checkout repository + uses: actions/checkout@v4 + with: + submodules: recursive - name: Restore build artifacts uses: actions/cache/restore@v3 with: @@ -79,30 +79,16 @@ jobs: ./out ./cache ./broadcast - key: ${{ runner.os }}-build-${{ github.sha }} + key: build-${{ 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: 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: @@ -110,6 +96,10 @@ jobs: key: ${{ needs.build.outputs.cache-key }} - name: Add Foundry to PATH run: echo "${{ needs.build.outputs.installation-dir }}" >> $GITHUB_PATH + - name: Checkout repository + uses: actions/checkout@v4 + with: + submodules: recursive - name: Restore build artifacts uses: actions/cache/restore@v3 with: @@ -118,31 +108,14 @@ jobs: ./out ./cache ./broadcast - key: ${{ runner.os }}-build-${{ github.sha }} + key: build-${{ 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-storage-layout: 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 - uses: actions/checkout@v4 - name: Restore cached Foundry toolchain uses: actions/cache/restore@v3 with: @@ -150,6 +123,29 @@ jobs: 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 }} + 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: Cache base branch layout file + uses: actions/cache/save@v3 + with: + path: ExocoreGateway.base.json + key: ExocoreGateway-base-layout-${{ github.sha }} + # Now we can generate the layout for the PR level. + - name: Checkout back to PR + uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.head.sha }} + submodules: recursive + # Restoring these will help make `forge inspect` faster. - name: Restore build artifacts uses: actions/cache/restore@v3 with: @@ -158,48 +154,33 @@ jobs: ./out ./cache ./broadcast - key: ${{ runner.os }}-build-${{ github.sha }} - - name: Checkout target branch - uses: actions/checkout@v4 + key: build-${{ github.sha }} + # This step restores the base branch layout file, while the next step generates the PR + # branch layout files. + - name: Restore base branch layout file + uses: actions/cache/restore@v3 with: - ref: ${{ github.event.pull_request.base.ref }} - submodules: recursive - - name: Generate target branch layout files + path: ExocoreGateway.base.json + key: ExocoreGateway-base-layout-${{ github.sha }} + - name: Generate storage layout files for the PR run: | - forge inspect src/core/ExocoreGateway.sol:ExocoreGateway storage-layout --pretty > ExocoreGateway_target.txt - - name: Cache target branch layout file + for file in Bootstrap ClientChainGateway RewardVault Vault ExocoreGateway ExoCapsule; do + forge inspect src/core/${file}.sol:${file} storage-layout > ${file}.compiled.json; + done + # At this point, we will make our cache to send to another workflow, which will have access to the secrets. + - name: Zip storage layout files + run: zip storage-layouts.zip *.compiled.json ExocoreGateway.base.json + - name: Cache storage layout files 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 + path: storage-layouts.zip + key: storage-layouts-${{ github.sha }} + - name: Store the cache output file + run: | + echo ${{ needs.build.outputs.cache-key }} > res.txt + echo ${{ needs.build.outputs.installation-dir }} >> res.txt + - name: Cache the output file + uses: actions/cache/save@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 - 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: res.txt + key: cache-${{ github.sha }} diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index e1addf49..c8006ccc 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -11,11 +11,10 @@ on: - "*" jobs: - check: + lint: strategy: fail-fast: true - name: Foundry project runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -23,11 +22,7 @@ jobs: - name: Set up Node.js uses: actions/setup-node@v2 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: '22' - name: Clear npm cache run: npm cache clean --force @@ -38,16 +33,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 89% rename from .github/workflows/foundry-setup.yml rename to .github/workflows/reusable-foundry-setup.yml index d4367318..2118d229 100644 --- a/.github/workflows/foundry-setup.yml +++ b/.github/workflows/reusable-foundry-setup.yml @@ -40,8 +40,8 @@ jobs: 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 }} - 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" diff --git a/.github/workflows/status-comment.yml b/.github/workflows/status-comment.yml new file mode 100644 index 00000000..b6a82b84 --- /dev/null +++ b/.github/workflows/status-comment.yml @@ -0,0 +1,38 @@ +name: Comment the status of the CI run on the pull request + +on: + workflow_run: + workflows: ["Forge CI", "Slither Analysis", "Run `solhint` linter"] + types: + - completed + +permissions: + pull-requests: write + issues: read + +jobs: + comment_status: + runs-on: ubuntu-latest + 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 }}" + # Checkout the repository to make the composite action available. + - name: Checkout repository + uses: actions/checkout@v3 + if: ${{ github.event.workflow_run.event == 'pull_request' }} + # Finally, run the composite action to post/update the status comment. + - name: Update CI Status + uses: ./.github/actions/composite-action-pr-status-comment + if: ${{ github.event.workflow_run.event == 'pull_request' }} + with: + workflow-conclusion: ${{ github.event.workflow_run.conclusion }} + workflow-url: ${{ github.event.workflow_run.html_url }} + workflow-name: ${{ github.event.workflow_run.name }} + github-token: ${{ secrets.GITHUB_TOKEN }} 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..9e0fab34 --- /dev/null +++ b/script/compareLayouts.js @@ -0,0 +1,47 @@ +const fs = require('fs'); +const { getStorageUpgradeReport } = require('@openzeppelin/upgrades-core/dist/storage'); + +// Mapping of deployed and compiled file names +const fileMappings = [ + { before: 'Bootstrap.deployed.json', after: 'Bootstrap.compiled.json' }, + { before: 'ClientChainGateway.deployed.json', after: 'ClientChainGateway.compiled.json' }, + { before: 'Vault.deployed.json', after: 'Vault.compiled.json' }, + { before: 'RewardVault.deployed.json', after: 'RewardVault.compiled.json' }, + { before: 'ExoCapsule.deployed.json', after: 'ExoCapsule.compiled.json' }, + { before: 'ExocoreGateway.base.json', after: 'ExocoreGateway.compiled.json' }, + { before: 'Bootstrap.compiled.json', after: 'ClientChainGateway.compiled.json' }, +]; + +// Loop through each mapping, load JSON files, and run the comparison +fileMappings.forEach(({ before, after }) => { + console.log(`🔍 Comparing ${before} and ${after}...`); + try { + // Check if both files exist and exit silently if not + if (!fs.existsSync(before)) { + console.log(`⚠️ Skipping: ${before} does not exist.`); + return; + } + if (!fs.existsSync(after)) { + console.log(`⚠️ Skipping: ${after} does not exist.`); + 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} or ${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 ------------------------------ */