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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -83,3 +83,6 @@ jobs:

- name: Test
run: pnpm test

- name: Storage gaps
run: pnpm test:storage
27 changes: 8 additions & 19 deletions .github/workflows/health.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ jobs:
name: Health Checks

runs-on: ubuntu-latest
timeout-minutes: 30

steps:
- name: Check out the repository
Expand All @@ -19,7 +20,7 @@ jobs:

- name: Set up pnpm
uses: pnpm/action-setup@v4
with:
with:
version: 9

- name: Set up Node.js
Expand All @@ -28,32 +29,20 @@ jobs:
node-version: '20'
cache: 'pnpm'

- name: Tune GitHub-hosted runner network
uses: smorimoto/tune-github-hosted-runner-network@v1

- name: Install Foundry
uses: foundry-rs/foundry-toolchain@v1
with:
version: v1.3.5 # stable version 09.09.2025

# remappings resolve @openzeppelin, @bancor and hardhat-deploy from node_modules
- name: Install dependencies
run: pnpm install

- name: Build
run: pnpm build

- name: Setup tenderly
run: |
mkdir ~/.tenderly
touch ~/.tenderly/config.yaml
echo "access_key: $TENDERLY_ACCESS_KEY" >> ~/.tenderly/config.yaml
env:
TENDERLY_ACCESS_KEY: ${{ secrets.TENDERLY_ACCESS_KEY }}
- name: Storage gaps
run: pnpm test:storage

- name: Test deployment and network health
# forks mainnet from utils/rpcUrls.json; set the MAINNET_RPC_URL secret to use a dedicated endpoint instead
- name: Mainnet health on a fork
run: pnpm test:health
env:
TENDERLY_PROJECT: carbon-temp-forks
TENDERLY_TEST_PROJECT: carbon-temp-forks
TENDERLY_USERNAME: bancor
TENDERLY_ACCESS_KEY: ${{ secrets.TENDERLY_ACCESS_KEY }}
MAINNET_RPC_URL: ${{ secrets.MAINNET_RPC_URL }}
80 changes: 80 additions & 0 deletions deployments/check-storage-gaps.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
#!/bin/bash
# Verifies that every upgradeable contract reserves exactly MAX_GAP_SIZE contract-level storage slots.
#
# Same rule as the hardhat `shouldHaveGap` helper it replaces:
# - the last storage variable of the contract must be `__gap`
# - if the contract declares its own state (FIRST_VAR given), the span from that first variable to the end
# of the gap must be exactly MAX_GAP_SIZE slots
# - otherwise the gap alone must be MAX_GAP_SIZE slots
#
# Layouts come from `forge inspect <Contract> storage`, so this runs on the Foundry build and needs no hardhat.
set -euo pipefail

MAX_GAP_SIZE=50

# Contract:FirstStateVariable (empty when the contract declares no state of its own)
CONTRACTS=(
"CarbonController:"
"Pairs:_lastPairId"
"Strategies:_strategyCounter"
"Voucher:_useGlobalURI"
"CarbonVortex:_totalCollected"
"CarbonPOL:_marketPriceMultiply"
)

cd "$(dirname "$0")/.."

failures=0
for spec in "${CONTRACTS[@]}"; do
contract=${spec%%:*}
first_var=${spec#*:}

layout=$(forge inspect "$contract" storage --json)

last_label=$(jq -r '.storage | last | .label' <<<"$layout")
last_slot=$(jq -r '.storage | last | .slot | tonumber' <<<"$layout")
last_type=$(jq -r '.storage | last | .type' <<<"$layout")

if [[ "$last_label" != "__gap" ]]; then
echo "FAIL $contract: last storage variable is '$last_label', expected '__gap'"
failures=$((failures + 1))
continue
fi

# e.g. t_array(t_uint256)47_storage -> 47
if [[ "$last_type" =~ t_array\(t_uint256\)([0-9]+)_storage ]]; then
gap_size=${BASH_REMATCH[1]}
else
echo "FAIL $contract: cannot read gap size from type '$last_type'"
failures=$((failures + 1))
continue
fi

if [[ -n "$first_var" ]]; then
# last declaration wins when a label is shadowed; "null" when the variable is not in the layout at all
first_slot=$(jq -r --arg v "$first_var" '[.storage[] | select(.label == $v)] | last | if . == null then "null" else (.slot | tonumber) end' <<<"$layout")
if [[ "$first_slot" == "null" ]]; then
echo "FAIL $contract: state variable '$first_var' not found in storage layout"
failures=$((failures + 1))
continue
fi
used=$((last_slot - first_slot + gap_size))
detail="slots $first_slot..$last_slot + gap $gap_size = $used"
else
used=$gap_size
detail="gap $gap_size"
fi

if [[ "$used" -ne "$MAX_GAP_SIZE" ]]; then
echo "FAIL $contract: $detail, expected exactly $MAX_GAP_SIZE"
failures=$((failures + 1))
else
echo "ok $contract: $detail"
fi
done

if [[ "$failures" -ne 0 ]]; then
echo
echo "$failures contract(s) violate the $MAX_GAP_SIZE-slot storage gap rule"
exit 1
fi
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,10 @@
"test:coverage:nightly": "NIGHTLY=1 pnpm test:coverage",
"test:coverage:report": "forge coverage --report lcov && genhtml lcov.info --branch-coverage --output-dir coverage",
"test:nightly": "NIGHTLY=1 CI=1 forge test",
"test:storage": "./deployments/check-storage-gaps.sh",
"test:deploy": "TEST_FORK=1 ./deployments/run-testnet.sh HARDHAT_NETWORK=tenderly mocha --require hardhat/register --extension ts --recursive --exit --timeout 600000 --bail --no-exit 'deploy/tests/network/**/*.ts'",
"test:deploy:mainnet": "TEST_FORK=1 ./deployments/run-testnet.sh HARDHAT_NETWORK=tenderly mocha --require hardhat/register --extension ts --recursive --exit --timeout 600000 --bail --no-exit 'deploy/tests/mainnet/**/*.ts'",
"test:health": "pnpm test:deploy:mainnet",
"test:health": "FORK_TESTS=1 forge test --match-path 'test/forge/fork/*' -vv",
"export:storage": "pnpm cleanbuild && hardhat run deployments/storage-layout.ts",
"deploy:prepare": "rm -rf ./node_modules && rm pnpm-lock.yaml && pnpm install && pnpm cleanbuild",
"deploy:network": "./deployments/run-network.sh hardhat deploy",
Expand Down
Loading